This commit is contained in:
Rao 2025-04-10 11:10:33 +08:00
parent 34dde48aaf
commit 9f1bc38acd
20 changed files with 841 additions and 175 deletions

View File

@ -4,6 +4,7 @@ import { TrpcService } from "@server/trpc/trpc.service";
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { Prisma } from "@nice/common"; import { Prisma } from "@nice/common";
const ShareCodeWhereInputSchema: ZodType<Prisma.ShareCodeWhereInput> = z.any(); const ShareCodeWhereInputSchema: ZodType<Prisma.ShareCodeWhereInput> = z.any();
const ShareCodeFindManyArgsSchema: ZodType<Prisma.ShareCodeFindManyArgs> = z.any();
@Injectable() @Injectable()
export class ShareCodeRouter { export class ShareCodeRouter {
constructor( constructor(
@ -12,31 +13,6 @@ export class ShareCodeRouter {
) { } ) { }
router = this.trpc.router({ router = this.trpc.router({
generateShareCode: this.trpc.procedure
.input(z.object({ fileId: z.string(), expiresAt: z.date(), canUseTimes: z.number() }))
.mutation(async ({ input }) => {
return this.shareCodeService.generateShareCode(input.fileId, input.expiresAt, input.canUseTimes);
}),
validateAndUseCode: this.trpc.procedure
.input(z.object({ code: z.string() }))
.mutation(async ({ input }) => {
return this.shareCodeService.validateAndUseCode(input.code);
}),
getShareCodeInfo: this.trpc.procedure
.input(z.object({ code: z.string() }))
.query(async ({ input }) => {
return this.shareCodeService.getShareCodeInfo(input.code);
}),
hasActiveShareCode: this.trpc.procedure
.input(z.object({ fileId: z.string() }))
.query(async ({ input }) => {
return this.shareCodeService.hasActiveShareCode(input.fileId);
}),
getFileShareHistory: this.trpc.procedure
.input(z.object({ fileId: z.string() }))
.query(async ({ input }) => {
return this.shareCodeService.getFileShareHistory(input.fileId);
}),
getFileByShareCode: this.trpc.procedure getFileByShareCode: this.trpc.procedure
.input(z.object({ code: z.string() })) .input(z.object({ code: z.string() }))
.query(async ({ input }) => { .query(async ({ input }) => {
@ -44,8 +20,8 @@ export class ShareCodeRouter {
}), }),
generateShareCodeByFileId: this.trpc.procedure generateShareCodeByFileId: this.trpc.procedure
.input(z.object({ fileId: z.string(), expiresAt: z.date(), canUseTimes: z.number() })) .input(z.object({ fileId: z.string(), expiresAt: z.date(), canUseTimes: z.number() }))
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
return this.shareCodeService.generateShareCodeByFileId(input.fileId, input.expiresAt, input.canUseTimes); return this.shareCodeService.generateShareCodeByFileId(input.fileId, input.expiresAt, input.canUseTimes, ctx.ip);
}), }),
getShareCodesWithResources: this.trpc.procedure getShareCodesWithResources: this.trpc.procedure
.input(z.object({ page: z.number(), pageSize: z.number(), where: ShareCodeWhereInputSchema.optional() })) .input(z.object({ page: z.number(), pageSize: z.number(), where: ShareCodeWhereInputSchema.optional() }))
@ -68,5 +44,23 @@ export class ShareCodeRouter {
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
return this.shareCodeService.updateShareCode(input.id, input.data); return this.shareCodeService.updateShareCode(input.id, input.data);
}), }),
getShareCodeResourcesTotalSize: this.trpc.procedure
.query(async () => {
return this.shareCodeService.getShareCodeResourcesTotalSize();
}),
getShareCodeResourcesSizeByDateRange: this.trpc.procedure
.input(z.object({ dateType: z.enum(['today', 'yesterday']) }))
.query(async ({ input }) => {
return this.shareCodeService.getShareCodeResourcesSizeByDateRange(input.dateType);
}),
countDistinctUploadIPs: this.trpc.procedure
.query(async () => {
return this.shareCodeService.countDistinctUploadIPs();
}),
findShareCodes: this.trpc.procedure
.input(ShareCodeFindManyArgsSchema)
.query(async ({ input }) => {
return this.shareCodeService.findShareCodes(input);
}),
}); });
} }

View File

@ -9,7 +9,6 @@ import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc'; import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone'; import timezone from 'dayjs/plugin/timezone';
import { BaseService } from '../base/base.service'; import { BaseService } from '../base/base.service';
import { url } from 'inspector';
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);
export interface ShareCode { export interface ShareCode {
@ -23,6 +22,7 @@ export interface ShareCode {
canUseTimes: number | null; canUseTimes: number | null;
} }
export interface GenerateShareCodeResponse { export interface GenerateShareCodeResponse {
id?: string;
code: string; code: string;
expiresAt: Date; expiresAt: Date;
canUseTimes: number; canUseTimes: number;
@ -33,6 +33,8 @@ export interface GenerateShareCodeResponse {
url: string; url: string;
meta: ResourceMeta meta: ResourceMeta
} }
uploadIp?: string;
createdAt?: Date;
} }
interface ResourceMeta { interface ResourceMeta {
@ -41,6 +43,25 @@ interface ResourceMeta {
size: string; size: string;
} }
const ShareCodeSelect = {
id: true,
code: true,
fileId: true,
expiresAt: true,
fileName: true,
canUseTimes: true,
resource: {
select: {
id: true,
type: true,
url: true,
meta: true,
}
},
uploadIp: true,
createdAt: true,
}
@Injectable() @Injectable()
export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> { export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
private readonly logger = new Logger(ShareCodeService.name); private readonly logger = new Logger(ShareCodeService.name);
@ -53,12 +74,26 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
constructor(private readonly resourceService: ResourceService) { constructor(private readonly resourceService: ResourceService) {
super(db, ObjectType.SHARE_CODE, false); super(db, ObjectType.SHARE_CODE, false);
} }
// 根据文件ID生成分享码
async generateShareCodeByFileId(fileId: string, expiresAt: Date, canUseTimes: number, ip: string) {
try {
this.logger.log('收到生成分享码请求fileId:', fileId);
this.logger.log('客户端IP:', ip);
const result = await this.generateShareCode(fileId, expiresAt, canUseTimes, undefined, ip);
this.logger.log('生成分享码结果:', result);
return result;
} catch (error) {
this.logger.error('生成分享码错误:', error);
return error
}
}
async generateShareCode( async generateShareCode(
fileId: string, fileId: string,
expiresAt: Date, expiresAt: Date,
canUseTimes: number, canUseTimes: number,
fileName?: string, fileName?: string,
uploadIp?: string,
): Promise<GenerateShareCodeResponse> { ): Promise<GenerateShareCodeResponse> {
try { try {
// 检查文件是否存在 // 检查文件是否存在
@ -87,6 +122,7 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
canUseTimes, canUseTimes,
isUsed: false, isUsed: false,
fileName: filename || "downloaded_file", fileName: filename || "downloaded_file",
uploadIp,
}, },
}); });
} else { } else {
@ -99,6 +135,7 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
canUseTimes, canUseTimes,
isUsed: false, isUsed: false,
fileName: filename || "downloaded_file", fileName: filename || "downloaded_file",
uploadIp,
}, },
}); });
} }
@ -130,7 +167,7 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
this.logger.log(`尝试验证分享码: ${code}`); this.logger.log(`尝试验证分享码: ${code}`);
// 查找有效的分享码 // 查找有效的分享码
const shareCode = await db.shareCode.findFirst({ const shareCode = await super.findFirst({
where: { where: {
code, code,
isUsed: false, isUsed: false,
@ -142,7 +179,7 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
return null; return null;
} }
//更新已使用次数 //更新已使用次数
await db.shareCode.update({ await super.update({
where: { id: shareCode.id }, where: { id: shareCode.id },
data: { canUseTimes: shareCode.canUseTimes - 1 }, data: { canUseTimes: shareCode.canUseTimes - 1 },
}); });
@ -167,7 +204,7 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
async cleanupExpiredShareCodes() { async cleanupExpiredShareCodes() {
try { try {
const shareCodes = await db.shareCode.findMany({ const shareCodes = await super.findMany({
where: { where: {
OR: [ OR: [
{ expiresAt: { lt: dayjs().tz('Asia/Shanghai').toDate() } }, { expiresAt: { lt: dayjs().tz('Asia/Shanghai').toDate() } },
@ -180,7 +217,7 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
shareCodes.forEach(code => { shareCodes.forEach(code => {
this.cleanupUploadFolder(code.fileId); this.cleanupUploadFolder(code.fileId);
}) })
const result = await db.shareCode.deleteMany({ const result = await super.deleteMany({
where: { where: {
fileId: { fileId: {
in: shareCodes.map(code => code.fileId) in: shareCodes.map(code => code.fileId)
@ -235,48 +272,6 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
this.logger.log(`Deleted folder: ${dirPath}`); this.logger.log(`Deleted folder: ${dirPath}`);
} }
} }
// 获取分享码信息
async getShareCodeInfo(code: string): Promise<ShareCode | null> {
try {
return await db.shareCode.findFirst({
where: { code },
});
} catch (error) {
this.logger.error('Failed to get share code info', error);
return null;
}
}
// 检查文件是否已经生成过分享码
async hasActiveShareCode(fileId: string): Promise<boolean> {
try {
const activeCode = await db.shareCode.findFirst({
where: {
fileId,
isUsed: false,
expiresAt: { gt: dayjs().tz('Asia/Shanghai').toDate() },
},
});
return !!activeCode;
} catch (error) {
this.logger.error('Failed to check active share code', error);
return false;
}
}
// 获取文件的所有分享记录
async getFileShareHistory(fileId: string) {
try {
return await db.shareCode.findMany({
where: { fileId },
orderBy: { createdAt: 'desc' },
});
} catch (error) {
this.logger.error('Failed to get file share history', error);
return [];
}
}
// 根据分享码获取文件 // 根据分享码获取文件
async getFileByShareCode(code: string) { async getFileByShareCode(code: string) {
@ -293,7 +288,7 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
where: { fileId: shareCode.fileId }, where: { fileId: shareCode.fileId },
}); });
this.logger.log('获取到的资源信息:', resource); this.logger.log('获取到的资源信息:', resource);
const { filename,filetype,size } = resource.meta as any as ResourceMeta const { filename, filetype, size } = resource.meta as any as ResourceMeta
const fileUrl = resource?.url const fileUrl = resource?.url
if (!resource) { if (!resource) {
throw new NotFoundException('文件不存在'); throw new NotFoundException('文件不存在');
@ -301,17 +296,17 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
// 直接返回正确的数据结构 // 直接返回正确的数据结构
const response = { const response = {
id:shareCode.id, id: shareCode.id,
code: shareCode.code, code: shareCode.code,
fileName: filename || 'downloaded_file', fileName: filename || 'downloaded_file',
expiresAt: shareCode.expiresAt, expiresAt: shareCode.expiresAt,
canUseTimes: shareCode.canUseTimes - 1, canUseTimes: shareCode.canUseTimes - 1,
resource:{ resource: {
id:resource.id, id: resource.id,
type:resource.type, type: resource.type,
url:resource.url, url: resource.url,
meta:{ meta: {
filename,filetype,size filename, filetype, size
} }
} }
}; };
@ -320,18 +315,6 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
return response; return response;
} }
// 根据文件ID生成分享码
async generateShareCodeByFileId(fileId: string, expiresAt: Date, canUseTimes: number) {
try {
this.logger.log('收到生成分享码请求fileId:', fileId);
const result = await this.generateShareCode(fileId, expiresAt, canUseTimes);
this.logger.log('生成分享码结果:', result);
return result;
} catch (error) {
this.logger.error('生成分享码错误:', error);
return error
}
}
async getShareCodesWithResources(args: { async getShareCodesWithResources(args: {
page?: number; page?: number;
@ -345,24 +328,8 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
// 使用include直接关联查询Resource // 使用include直接关联查询Resource
const { items, totalPages } = await super.findManyWithPagination({ const { items, totalPages } = await super.findManyWithPagination({
...args, ...args,
select: { select: ShareCodeSelect
id: true,
code: true,
fileId: true,
expiresAt: true,
fileName: true,
canUseTimes: true,
resource: {
select: {
id: true,
type: true,
url: true,
meta: true,
}
}
}
}); });
this.logger.log('search result:', items);
return { return {
items, items,
totalPages totalPages
@ -394,4 +361,231 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
throw error; throw error;
} }
} }
/**
*
* @returns
*/
async getShareCodeResourcesTotalSize(): Promise<{ totalSize: number; resourceCount: number }> {
try {
this.logger.log('获取所有分享码关联资源的总大小');
// 查询所有有效的分享码及其关联的资源
const shareCodes = await super.findMany({
where: {
deletedAt: null
},
select: { ...ShareCodeSelect }
});
// 计算总大小和资源数量
let totalSize = 0;
let resourceCount = 0;
shareCodes.forEach(shareCode => {
if ((shareCode as any as GenerateShareCodeResponse).resource && (shareCode as any as GenerateShareCodeResponse).resource.meta) {
const meta = (shareCode as any as GenerateShareCodeResponse).resource.meta as any;
if (meta.size) {
// 如果size是字符串格式(如 "1024"或"1 MB"),需要转换
let sizeValue: number;
if (typeof meta.size === 'string') {
// 尝试直接解析数字
sizeValue = parseInt(meta.size, 10);
// 如果解析失败,可能需要更复杂的处理
if (isNaN(sizeValue)) {
// 简单处理,实际应用中可能需要更复杂的单位转换
this.logger.warn(`无法解析资源大小: ${meta.size}`);
sizeValue = 0;
}
} else if (typeof meta.size === 'number') {
sizeValue = meta.size;
} else {
sizeValue = 0;
}
totalSize += sizeValue;
resourceCount++;
}
}
});
this.logger.log(`资源总大小: ${totalSize}, 资源数量: ${resourceCount}`);
return { totalSize, resourceCount };
} catch (error) {
this.logger.error('获取分享码资源总大小失败', error);
throw error;
}
}
/**
*
* @param dateType : 'today' 'yesterday'
* @returns
*/
async getShareCodeResourcesSizeByDateRange(dateType: 'today' | 'yesterday'): Promise<{ totalSize: number; resourceCount: number }> {
try {
let startDate: Date;
let endDate: Date;
const now = dayjs().tz('Asia/Shanghai');
if (dateType === 'today') {
startDate = now.startOf('day').toDate();
endDate = now.endOf('day').toDate();
this.logger.log(`获取今天创建的分享码资源大小, 日期范围: ${startDate}${endDate}`);
} else {
startDate = now.subtract(1, 'day').startOf('day').toDate();
endDate = now.subtract(1, 'day').endOf('day').toDate();
this.logger.log(`获取昨天创建的分享码资源大小, 日期范围: ${startDate}${endDate}`);
}
// 查询特定日期范围内创建的分享码及其关联的资源
const shareCodes = await super.findMany({
where: {
createdAt: {
gte: startDate,
lte: endDate
},
deletedAt: null
},
select: {
...ShareCodeSelect
}
});
// 计算总大小和资源数量
let totalSize = 0;
let resourceCount = 0;
shareCodes.forEach(shareCode => {
if ((shareCode as any as GenerateShareCodeResponse).resource && (shareCode as any as GenerateShareCodeResponse).resource.meta) {
const meta = (shareCode as any as GenerateShareCodeResponse).resource.meta as any;
if (meta.size) {
// 如果size是字符串格式(如 "1024"或"1 MB"),需要转换
let sizeValue: number;
if (typeof meta.size === 'string') {
// 尝试直接解析数字
sizeValue = parseInt(meta.size, 10);
// 如果解析失败,可能需要更复杂的处理
if (isNaN(sizeValue)) {
// 简单处理,实际应用中可能需要更复杂的单位转换
this.logger.warn(`无法解析资源大小: ${meta.size}`);
sizeValue = 0;
}
} else if (typeof meta.size === 'number') {
sizeValue = meta.size;
} else {
sizeValue = 0;
}
totalSize += sizeValue;
resourceCount++;
}
}
});
this.logger.log(`${dateType}资源总大小: ${totalSize}, 资源数量: ${resourceCount}`);
return { totalSize, resourceCount };
} catch (error) {
this.logger.error(`获取${dateType === 'today' ? '今天' : '昨天'}的分享码资源大小失败`, error);
throw error;
}
}
/**
* uploadIp的数量
* @returns uploadIp数量
*/
async countDistinctUploadIPs(): Promise<{ thisWeek: number; lastWeek: number; all: number }> {
try {
const now = dayjs().tz('Asia/Shanghai');
// 本周的开始和结束
const thisWeekStart = now.startOf('week').toDate();
const thisWeekEnd = now.endOf('week').toDate();
// 上周的开始和结束
const lastWeekStart = now.subtract(1, 'week').startOf('week').toDate();
const lastWeekEnd = now.subtract(1, 'week').endOf('week').toDate();
this.logger.log(`统计本周IP数量, 日期范围: ${thisWeekStart}${thisWeekEnd}`);
this.logger.log(`统计上周IP数量, 日期范围: ${lastWeekStart}${lastWeekEnd}`);
// 查询所有不同IP
const allIPs = await super.findMany({
where: {
deletedAt: null,
uploadIp: {
not: null
}
},
select: {
uploadIp: true
},
distinct: ['uploadIp']
});
// 查询本周的不同IP
const thisWeekIPs = await super.findMany({
where: {
createdAt: {
gte: thisWeekStart,
lte: thisWeekEnd
},
uploadIp: {
not: null
},
deletedAt: null
},
select: {
uploadIp: true
},
distinct: ['uploadIp']
});
// 查询上周的不同IP
const lastWeekIPs = await super.findMany({
where: {
createdAt: {
gte: lastWeekStart,
lte: lastWeekEnd
},
uploadIp: {
not: null
},
deletedAt: null
},
select: {
uploadIp: true
},
distinct: ['uploadIp']
});
const thisWeekCount = thisWeekIPs.length;
const lastWeekCount = lastWeekIPs.length;
const allCount = allIPs.length;
this.logger.log(`本周不同IP数量: ${thisWeekCount}, 上周不同IP数量: ${lastWeekCount}, 所有不同IP数量: ${allCount}`);
return { thisWeek: thisWeekCount, lastWeek: lastWeekCount, all: allCount };
} catch (error) {
this.logger.error('统计不同uploadIp数量失败', error);
throw error;
}
}
/**
* 使ShareCodeSelect并按创建时间倒序排序
* @param args
* @returns
*/
async findShareCodes(args?: Omit<Prisma.ShareCodeFindManyArgs, 'select' | 'orderBy'>): Promise<GenerateShareCodeResponse[]> {
try {
const result = await super.findMany({
...args,
select: ShareCodeSelect,
});
this.logger.log(`获取分享码列表成功, 数量: ${result.length}`);
return result as unknown as GenerateShareCodeResponse[];
} catch (error) {
this.logger.error('获取分享码列表失败', error);
throw error;
}
}
} }

View File

@ -19,7 +19,8 @@ interface CodeManageContextType {
searchRefetch: () => void, searchRefetch: () => void,
setSearchKeyword: (keyword: string) => void, setSearchKeyword: (keyword: string) => void,
currentCode: string | null, currentCode: string | null,
setCurrentCode: (code: string) => void setCurrentCode: (code: string) => void,
searchKeyword: string
} }
interface ShareCodeWithResource { interface ShareCodeWithResource {
@ -97,7 +98,8 @@ export const CodeManageProvider = ({ children }: { children: React.ReactNode })
searchRefetch, searchRefetch,
setSearchKeyword, setSearchKeyword,
currentCode, currentCode,
setCurrentCode setCurrentCode,
searchKeyword
}}> }}>
{children} {children}
</CodeManageContext.Provider> </CodeManageContext.Provider>

View File

@ -1,9 +1,25 @@
import { useSearchParams } from "react-router-dom";
import CodeManageSearchBase from "./components/CodeManageSearchBase"; import CodeManageSearchBase from "./components/CodeManageSearchBase";
import CodeManageDisplay from "./components/CodeMangeDisplay"; import CodeManageDisplay from "./components/CodeMangeDisplay";
import { useCodeManageContext } from "./CodeManageContext";
import { useEffect, useState } from "react";
export default function CodeManageLayout() { export default function CodeManageLayout() {
const [searchParams] = useSearchParams();
const { setSearchKeyword, searchRefetch, setCurrentPage } = useCodeManageContext();
const [localKeyword, setLocalKeyword] = useState("");
useEffect(() => {
const keyword = searchParams.get('keyword');
if (keyword) {
setSearchKeyword(keyword);
setLocalKeyword(keyword);
setCurrentPage(1);
searchRefetch();
}
}, [searchParams, setSearchKeyword, setCurrentPage, searchRefetch]);
return ( return (
<div className="max-w-[1100px] mx-auto h-[100vh]"> <div className="max-w-[1100px] mx-auto h-[100vh]">
<CodeManageSearchBase></CodeManageSearchBase> <CodeManageSearchBase keyword={localKeyword}></CodeManageSearchBase>
<CodeManageDisplay></CodeManageDisplay> <CodeManageDisplay></CodeManageDisplay>
</div> </div>
) )

View File

@ -1,16 +1,26 @@
import { Button, Form, Input } from 'antd'; import { Button, Form, Input } from 'antd';
import { useCodeManageContext } from '../CodeManageContext'; import { useCodeManageContext } from '../CodeManageContext';
import { ChangeEvent, useRef } from 'react'; import { ChangeEvent, useEffect, useRef } from 'react';
export default function CodeManageSearchBase() { export default function CodeManageSearchBase({ keyword }: { keyword?: string }) {
const { setCurrentPage, searchRefetch, setSearchKeyword } = useCodeManageContext(); const { setCurrentPage, searchRefetch, setSearchKeyword, searchKeyword } = useCodeManageContext();
const debounceTimer = useRef<NodeJS.Timeout | null>(null); const debounceTimer = useRef<NodeJS.Timeout | null>(null);
const formRef = Form.useForm()[0]; // 获取表单实例
// 当 keyword 属性变化时更新表单值
useEffect(() => {
if (keyword) {
formRef.setFieldsValue({ search: keyword });
}
}, [keyword, formRef]);
const onSearch = (value: string) => { const onSearch = (value: string) => {
console.log(value); console.log(value);
setSearchKeyword(value); setSearchKeyword(value);
setCurrentPage(1) setCurrentPage(1)
searchRefetch() searchRefetch()
}; };
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => { const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// 设置表单值 // 设置表单值
setSearchKeyword(e.target.value); setSearchKeyword(e.target.value);
@ -25,13 +35,15 @@ export default function CodeManageSearchBase() {
searchRefetch(); searchRefetch();
}, 300); // 300毫秒的防抖延迟 }, 300); // 300毫秒的防抖延迟
}; };
return <> return <>
<div className="py-4 mt-10 w-2/3 mx-auto"> <div className="py-4 mt-2 w-2/3 mx-auto">
<Form> <Form form={formRef}>
<Form.Item name="search" label="关键字搜索"> <Form.Item name="search" label="关键字搜索">
<Input.Search <Input.Search
placeholder="输入分享码或文件名" placeholder="输入分享码或文件名"
enterButton enterButton
value={searchKeyword}
onSearch={onSearch} onSearch={onSearch}
onChange={onChange} onChange={onChange}
/> />

View File

@ -51,7 +51,7 @@ export default function CodeMangeDisplay() {
console.log('currentShareCodes:', currentShareCodes); console.log('currentShareCodes:', currentShareCodes);
}, [currentShareCodes]); }, [currentShareCodes]);
return <> return <>
<div className="w-full min-h-[550px] mx-auto"> <div className="w-full min-h-[720px] mx-auto">
<ShareCodeList <ShareCodeList
data={currentShareCodes?.items} data={currentShareCodes?.items}
loading={isLoading} loading={isLoading}

View File

@ -17,7 +17,7 @@ export default function ShareCodeListCard({ item, onEdit, onDelete, styles, onDo
}, [item]); }, [item]);
return <div className={`${styles}`}> return <div className={`${styles}`}>
<Card <Card
className="shadow-md hover:shadow-lg transition-shadow duration-300 space-x-4" className="shadow-md hover:shadow-lg h-[344px] transition-shadow duration-300 space-x-4"
title={ title={
<Typography.Text <Typography.Text
strong strong
@ -53,18 +53,26 @@ export default function ShareCodeListCard({ item, onEdit, onDelete, styles, onDo
].filter(Boolean)} ].filter(Boolean)}
> >
<div className="space-y-2"> <div className="space-y-2">
<p className="text-gray-700"> <p className="text-gray-700 whitespace-nowrap overflow-hidden text-overflow-ellipsis hover:overflow-auto">
<span className="font-medium">:</span> {item.fileName} <span className="font-medium">:</span> {item.fileName}
</p> </p>
<p className="text-gray-700"> <p className="text-gray-700 whitespace-nowrap overflow-hidden text-overflow-ellipsis hover:overflow-auto">
<span className="font-medium">:</span> {Math.max(0.01, (Number(item?.resource?.meta?.size) / 1024 / 1024)).toFixed(2)} MB <span className="font-medium">:</span> {Math.max(0.01, (Number(item?.resource?.meta?.size) / 1024 / 1024)).toFixed(2)} MB
</p> </p>
<p className="text-gray-700 ">
<span className="font-medium">:</span> {dayjs(item.expiresAt).format('YYYY-MM-DD HH:mm:ss')} <p className="text-gray-700 whitespace-nowrap overflow-hidden text-overflow-ellipsis hover:overflow-auto">
</p>
<p className="text-gray-700">
<span className="font-medium">使:</span> {item.canUseTimes} <span className="font-medium">使:</span> {item.canUseTimes}
</p> </p>
<p className="text-gray-700 whitespace-nowrap overflow-hidden text-overflow-ellipsis hover:overflow-auto">
<span className="font-medium">IP:</span> {item.uploadIp}
</p>
<p className="text-gray-700 whitespace-nowrap overflow-hidden text-overflow-ellipsis hover:overflow-auto">
<span className="font-medium">:</span> {dayjs(item.expiresAt).format('YYYY-MM-DD HH:mm:ss')}
</p>
<p className="text-gray-700 whitespace-nowrap overflow-hidden text-overflow-ellipsis hover:overflow-auto">
<span className="font-medium">:</span> {dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss')}
</p>
</div> </div>
</Card> </Card>
</div> </div>

View File

@ -0,0 +1,72 @@
import { api } from "@nice/client";
import { createContext, useContext, useState } from "react";
import { ShareCodeResponse } from "../quick-file/quickFileContext";
interface DashboardContextType {
shareCodeAll: ShareCodeResourcesSizeByDateRange;
shareCodeToday: ShareCodeResourcesSizeByDateRange;
shareCodeYesterday: ShareCodeResourcesSizeByDateRange;
isShareCodeAllLoading: boolean;
isShareCodeTodayLoading: boolean;
isShareCodeYesterdayLoading: boolean;
distinctUploadIPs: { thisWeek: number; lastWeek: number; all: number };
isDistinctUploadIPsLoading: boolean;
shareCodeList: ShareCodeResponse[];
isShareCodeListLoading: boolean;
}
interface ShareCodeResourcesSizeByDateRange {
totalSize: number;
resourceCount: number;
}
export const DashboardContext = createContext<DashboardContextType | null>(null);
export const DashboardProvider = ({ children }: { children: React.ReactNode }) => {
const { data: shareCodeAll, isLoading: isShareCodeAllLoading }:
{ data: ShareCodeResourcesSizeByDateRange, isLoading: boolean }
= api.shareCode.getShareCodeResourcesTotalSize.useQuery()
const { data: shareCodeToday, isLoading: isShareCodeTodayLoading }:
{ data: ShareCodeResourcesSizeByDateRange, isLoading: boolean }
= api.shareCode.getShareCodeResourcesSizeByDateRange.useQuery(
{ dateType: "today" })
const { data: shareCodeYesterday, isLoading: isShareCodeYesterdayLoading }:
{ data: ShareCodeResourcesSizeByDateRange, isLoading: boolean }
= api.shareCode.getShareCodeResourcesSizeByDateRange.useQuery(
{ dateType: "yesterday" })
const { data: distinctUploadIPs, isLoading: isDistinctUploadIPsLoading }:
{ data: { thisWeek: number; lastWeek: number; all: number }, isLoading: boolean }
= api.shareCode.countDistinctUploadIPs.useQuery()
const { data: shareCodeList, isLoading: isShareCodeListLoading }:
{ data: ShareCodeResponse[], isLoading: boolean }
= api.shareCode.findShareCodes.useQuery({
where: {
deletedAt: null
},
take: 8,
orderBy: {
createdAt: 'desc',
},
})
return <>
<DashboardContext.Provider value={{
shareCodeAll,
shareCodeToday,
shareCodeYesterday,
isShareCodeAllLoading,
isShareCodeTodayLoading,
isShareCodeYesterdayLoading,
distinctUploadIPs,
isDistinctUploadIPsLoading,
shareCodeList,
isShareCodeListLoading
}}>
{children}
</DashboardContext.Provider>
</>
};
export const useDashboardContext = () => {
const context = useContext(DashboardContext);
if (!context) {
throw new Error("useDashboardContext must be used within a DashboardProvider");
}
return context;
};

View File

@ -0,0 +1,13 @@
import Board from './components/Board';
import Activate from './components/Activate';
export default function DashboardLayout() {
return (
<div className="bg-gray-100 p-5">
<Board />
{/* 最近活动 */}
<Activate />
</div>
);
}

View File

@ -0,0 +1,37 @@
import { Typography, Space, Skeleton } from 'antd';
import { FileOutlined } from '@ant-design/icons';
import DashboardCard from '@web/src/components/presentation/dashboard-card';
import ActivityItem from './ActivityItem';
import { useDashboardContext } from '../DashboardContext';
import { ShareCodeResponse } from '../../quick-file/quickFileContext';
import { useNavigate } from 'react-router-dom';
const { Title, Text } = Typography;
export default function Activate() {
const { shareCodeList, isShareCodeListLoading } = useDashboardContext();
const navigate = useNavigate()
const handleClick = (item: ShareCodeResponse) => {
navigate(`/manage/share-code?keyword=${encodeURIComponent(item.code)}`)
}
return <>
<div className="mt-6">
<Title level={4} className="mb-4"></Title>
<DashboardCard className="w-full">
<Space direction="vertical" className="w-full" size="middle">
{isShareCodeListLoading ?
<Skeleton active />
:
shareCodeList.map((item) => (
<div className='my-1 cursor-pointer' onClick={() => handleClick(item)}>
<ActivityItem
key={item.id}
icon={<FileOutlined className="text-blue-500" />}
title={`来自${item.uploadIp}的用户上传了文件:"${item.fileName}",文件大小:${Math.max(Number(item.resource.meta.size) / 1024 / 1024, 0.01).toFixed(2)}MB分享码${item.code}`}
time={item.createdAt?.toLocaleString()}
/>
</div>
))}
</Space>
</DashboardCard>
</div>
</>
}

View File

@ -0,0 +1,26 @@
import { motion } from 'framer-motion';
// 活动项组件
interface ActivityItemProps {
icon: React.ReactNode;
title: string;
time: string;
}
export default function ActivityItem({ icon, title, time }: ActivityItemProps) {
return (
<motion.div
className="flex items-center py-2"
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3 }}
>
<div className="flex-shrink-0 w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center mr-3">
{icon}
</div>
<div className="flex-grow">
<div className="text-gray-800">{title}</div>
<div className="text-gray-400 text-xs">{time}</div>
</div>
</motion.div>
);
}

View File

@ -0,0 +1,138 @@
import { Row, Col, Typography, Space, Tooltip, Statistic, Spin } from 'antd';
import { FileOutlined, HddOutlined, UserOutlined, CheckCircleOutlined, HistoryOutlined } from '@ant-design/icons';
import DashboardCard from '@web/src/components/presentation/dashboard-card';
import { useDashboardContext } from '../DashboardContext';
import { useEffect, useState } from 'react';
const { Title, Text } = Typography;
export default function Board() {
const { shareCodeAll, shareCodeToday, shareCodeYesterday,
isShareCodeAllLoading, isShareCodeTodayLoading, isShareCodeYesterdayLoading,
distinctUploadIPs, isDistinctUploadIPsLoading } = useDashboardContext();
const [serverUptime, setServerUptime] = useState('');
useEffect(() => {
const calculateTimeDifference = () => {
const now = new Date();
const targetDate = new Date('2025-04-09T15:00:00');
const diffMs = now.getTime()- targetDate.getTime();
// 如果是负数,表示目标日期已过
if (diffMs < 0) {
setServerUptime('0天0小时0分钟');
return;
}
// 计算天数、小时数和分钟数
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
setServerUptime(`${days}${hours}小时${minutes}分钟`);
};
calculateTimeDifference();
// 每分钟更新一次
const timer = setInterval(calculateTimeDifference, 60000);
return () => clearInterval(timer);
}, []);
return <>
<Title level={3} className="mb-4"></Title>
<Row gutter={[16, 16]}>
{/* 总文件数卡片 */}
<Col xs={24} sm={12} md={6}>
<DashboardCard
title={
<div className="flex items-center">
<FileOutlined style={{ marginRight: 8, fontSize: 16 }} />
</div>
}
className="h-full"
>
<div className="flex flex-col h-full justify-between py-2">
<Statistic value={isShareCodeAllLoading ? 0 : shareCodeAll?.resourceCount} />
{
isShareCodeTodayLoading || isShareCodeYesterdayLoading
? <Spin />
: (<div className="text-gray-500 text-sm mt-2">
: {shareCodeYesterday?.resourceCount} : {shareCodeToday?.resourceCount}
</div>)
}
</div>
</DashboardCard>
</Col>
{/* 存储空间卡片 */}
<Col xs={24} sm={12} md={6}>
<DashboardCard
title={
<div className="flex items-center">
<HddOutlined style={{ marginRight: 8, fontSize: 16 }} />
使
</div>
}
className="h-full"
>
<div className="flex flex-col h-full justify-between py-2">
<Statistic value={isShareCodeAllLoading ? 0 : `${(shareCodeAll?.totalSize / 1024 / 1024).toFixed(2)}MB`} />
{
isShareCodeTodayLoading || isShareCodeYesterdayLoading
? <Spin />
: (<div className="text-gray-500 text-sm mt-2">
: {(shareCodeYesterday?.totalSize / 1024 / 1024).toFixed(2)}MB : {(shareCodeToday?.totalSize / 1024 / 1024).toFixed(2)}MB
</div>)
}
</div>
</DashboardCard>
</Col>
{/* 活跃用户卡片 */}
<Col xs={24} sm={12} md={6}>
<DashboardCard
title={
<div className="flex items-center">
<UserOutlined style={{ marginRight: 8, fontSize: 16 }} />
</div>
}
className="h-full"
>
<div className="flex flex-col h-full justify-between py-2">
<Statistic value={isDistinctUploadIPsLoading ? 0 : distinctUploadIPs?.all} />
<div className="text-gray-500 text-sm mt-2">
使: {distinctUploadIPs?.lastWeek} 使: {distinctUploadIPs?.thisWeek}
{
distinctUploadIPs?.lastWeek ? (
distinctUploadIPs?.lastWeek > distinctUploadIPs?.thisWeek ?
<span className="text-red-500">{((distinctUploadIPs?.lastWeek - distinctUploadIPs?.thisWeek) / distinctUploadIPs?.lastWeek * 100).toFixed(2)}%</span>
: <span className="text-green-500"> {((distinctUploadIPs?.thisWeek - distinctUploadIPs?.lastWeek) / distinctUploadIPs?.lastWeek * 100).toFixed(2)}%</span>
) : null
}
</div>
</div>
</DashboardCard>
</Col>
{/* 系统状态卡片 */}
<Col xs={24} sm={12} md={6}>
<DashboardCard
title={
<div className="flex items-center">
<CheckCircleOutlined style={{ marginRight: 8, fontSize: 16, color: "#52c41a" }} />
</div>
}
className="h-full"
>
<div className="flex flex-col h-full justify-between py-2">
<Statistic value="正常" valueStyle={{ color: '#52c41a' }} />
<div className="text-gray-500 text-sm mt-2">
: {serverUptime}
</div>
</div>
</DashboardCard>
</Col>
</Row>
</>
}

View File

@ -0,0 +1,41 @@
import { useAuth } from "@web/src/providers/auth-provider";
import { Button } from "antd";
import { RolePerms } from "@nice/common";
import { useNavigate } from "react-router-dom";
interface HeaderProps {
showLoginButton?: boolean;
}
export const Header: React.FC<HeaderProps> = ({ showLoginButton = true }) => {
const navigate = useNavigate();
const {isAuthenticated , hasEveryPermissions} = useAuth();
const isAdmin = hasEveryPermissions(RolePerms.MANAGE_ANY_POST) && isAuthenticated;
return (
<div className="w-[1000px] mx-auto flex justify-between items-center mt-2">
<div className="flex items-center">
<img src="/logo.svg" className="h-16 w-16" />
<span className="text-4xl py-4 mx-4 font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-500 to-blue-700 drop-shadow-sm"></span>
</div>
{showLoginButton && (
<Button
type="primary"
className="bg-gradient-to-r from-blue-500 to-blue-700"
onClick={() => {
if(isAdmin){
navigate('/manage');
}else{
navigate('/login');
}
}}
>
{isAdmin ? '管理后台' : '登录'}
</Button>
)}
</div>
);
};
export default Header;

View File

@ -0,0 +1,66 @@
import { useState, useEffect } from "react";
import { Layout, Menu } from "antd";
import { FileOutlined, DashboardOutlined } from "@ant-design/icons";
import { NavLink, Outlet, useNavigate, useLocation } from "react-router-dom";
const { Sider, Content } = Layout;
export default function QuickFileManage() {
const [collapsed, setCollapsed] = useState(false);
const navigate = useNavigate();
const location = useLocation();
const [currentKey, setCurrentKey] = useState("1");
useEffect(() => {
// 根据当前URL路径设置currentKey
if (location.pathname.includes("/manage/dashboard")) {
setCurrentKey("1");
} else if (location.pathname.includes("/manage/share-code")) {
setCurrentKey("2");
}
}, [location.pathname]);
return (
<Layout style={{ minHeight: "100vh" }}>
<Sider
collapsible
collapsed={collapsed}
onCollapse={(value) => setCollapsed(value)}
theme="light"
style={{
borderRight: "1px solid #f0f0f0"
}}
>
<div
className="flex items-center justify-center cursor-pointer w-full"
onClick={() => {
navigate('/');
}}
>
<img src="/logo.svg" className="h-10 w-10" />
{!collapsed && <span className="text-2xl py-4 mx-4 font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-500 to-blue-700 drop-shadow-sm"></span>}
</div>
<Menu
theme="light"
mode="inline"
selectedKeys={[currentKey]}
items={[
{
key: "1",
icon: <DashboardOutlined />,
label: <NavLink to="/manage/dashboard"></NavLink>,
},
{
key: "2",
icon: <FileOutlined />,
label: <NavLink to="/manage/share-code"></NavLink>,
},
]}
/>
</Sider>
<Content style={{ padding: "24px" }}>
<Outlet></Outlet>
</Content>
</Layout>
);
}

View File

@ -1,26 +1,28 @@
import { ShareCodeGenerator } from "../sharecode/sharecodegenerator"; import { ShareCodeGenerator } from "../sharecode/sharecodegenerator";
import { ShareCodeValidator } from "../sharecode/sharecodevalidator"; import { ShareCodeValidator } from "../sharecode/sharecodevalidator";
import { message, Tabs, Form, Spin, Alert } from "antd"; import { message, Tabs, Form } from "antd";
import { env } from '../../../env'
import { TusUploader } from "@web/src/components/common/uploader/TusUploader"; import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
import { useState } from "react";
import CodeRecord from "../sharecode/components/CodeRecord"; import CodeRecord from "../sharecode/components/CodeRecord";
import Header from "./components/header";
const { TabPane } = Tabs; const { TabPane } = Tabs;
export default function QuickUploadPage() { export default function QuickUploadPage() {
const [form] = Form.useForm(); const [form] = Form.useForm();
const uploadFileId = Form.useWatch(["file"], form)?.[0] const uploadFileId = Form.useWatch(["file"], form)?.[0]
const handleShareSuccess = (code: string) => {
message.success('分享码生成成功:' + code);
}
return ( return (
<div style={{ maxWidth: '1000px', margin: '0 auto', padding: '20px' }}> <div className="w-full ">
<div className="flex items-center"> <Header />
<img src="/logo.svg" className="h-16 w-16 rounded-xl" /> <div className="max-w-[1000px] mx-auto">
<span className="text-4xl py-4 mx-4"></span> <div className="my-4 p-5 border-3 border-gray-200 rounded-lg shadow-lg">
<div className="flex justify-between items-center mb-4">
<span className="text-lg block text-zinc-700 py-4">使</span>
<CodeRecord title="我使用的分享码" btnContent="使用记录" recordName="shareCodeDownloadRecords" isDownload={true} />
</div> </div>
<Tabs defaultActiveKey="upload"> <div>
<TabPane tab="上传分享" key="upload"> <ShareCodeValidator />
</div>
</div>
<div className="my-4 p-5 border-3 border-gray-200 rounded-lg shadow-lg">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<span className="text-lg text-zinc-700"></span> <span className="text-lg text-zinc-700"></span>
<CodeRecord title="我生成的分享码" btnContent="上传记录" recordName="shareCodeGeneratorRecords" /> <CodeRecord title="我生成的分享码" btnContent="上传记录" recordName="shareCodeGeneratorRecords" />
@ -40,17 +42,17 @@ export default function QuickUploadPage() {
fileId={uploadFileId} fileId={uploadFileId}
/> />
</div> </div>
</div>
{/* <Tabs defaultActiveKey="upload">
<TabPane tab="上传分享" key="upload">
</TabPane> </TabPane>
<TabPane tab="下载文件" key="download"> <TabPane tab="下载文件" key="download">
<div className="flex justify-between items-center mb-4">
<span className="text-lg block text-zinc-700 py-4">使</span>
<CodeRecord title="我使用的分享码" btnContent="使用记录" recordName="shareCodeDownloadRecords" isDownload={true}/>
</div>
<div>
<ShareCodeValidator />
</div>
</TabPane> </TabPane>
</Tabs> </Tabs> */}
</div>
</div> </div>
) )

View File

@ -25,6 +25,8 @@ export interface ShareCodeResponse {
url: string; url: string;
meta: ResourceMeta meta: ResourceMeta
} }
uploadIp?: string;
createdAt?: Date;
} }
interface ResourceMeta { interface ResourceMeta {
filename: string; filename: string;
@ -57,7 +59,10 @@ export const QuickFileProvider = ({ children }: { children: React.ReactNode }) =
}; };
// 获取已有记录并添加新记录 // 获取已有记录并添加新记录
const existingGeneratorRecords = localStorage.getItem(recordName); const existingGeneratorRecords = localStorage.getItem(recordName);
const generatorRecords = existingGeneratorRecords ? JSON.parse(existingGeneratorRecords) : []; let generatorRecords = existingGeneratorRecords ? JSON.parse(existingGeneratorRecords) : [];
if (data.code) {
generatorRecords = generatorRecords.filter((item: ShareCodeResponse) => item.code !== data.code)
}
generatorRecords.unshift(newRecord); // 添加到最前面 generatorRecords.unshift(newRecord); // 添加到最前面
localStorage.setItem(recordName, JSON.stringify(generatorRecords)); localStorage.setItem(recordName, JSON.stringify(generatorRecords));
} }

View File

@ -72,7 +72,7 @@ export default function CodeRecord({ title, btnContent, recordName ,styles,isDow
<div className="space-y-4"> <div className="space-y-4">
{records.map(item => ( {records.map(item => (
<ShareCodeListCard <ShareCodeListCard
key={item.code} key={item.id}
item={item} item={item}
onDelete={handleDelete} onDelete={handleDelete}
onDownload={isDownload ? handleDownload : undefined} onDownload={isDownload ? handleDownload : undefined}

View File

@ -39,7 +39,7 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
const [expiresAt, setExpiresAt] = useState<string | null>(null); const [expiresAt, setExpiresAt] = useState<string | null>(null);
const [canUseTimes, setCanUseTimes] = useState<number>(null); const [canUseTimes, setCanUseTimes] = useState<number>(null);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const queryKey = getQueryKey(api.term); const queryKey = getQueryKey(api.shareCode);
const [isGenerate, setIsGenerate] = useState(false); const [isGenerate, setIsGenerate] = useState(false);
const [currentFileId, setCurrentFileId] = useState<string>(''); const [currentFileId, setCurrentFileId] = useState<string>('');
const [form] = Form.useForm(); const [form] = Form.useForm();

View File

@ -2,6 +2,7 @@ import {
createBrowserRouter, createBrowserRouter,
IndexRouteObject, IndexRouteObject,
Link, Link,
Navigate,
NonIndexRouteObject, NonIndexRouteObject,
useParams, useParams,
} from "react-router-dom"; } from "react-router-dom";
@ -12,6 +13,9 @@ import { CodeManageProvider } from "../app/admin/code-manage/CodeManageContext";
import CodeManageLayout from "../app/admin/code-manage/CodeManageLayout"; import CodeManageLayout from "../app/admin/code-manage/CodeManageLayout";
import QuickUploadPage from "../app/admin/quick-file/page"; import QuickUploadPage from "../app/admin/quick-file/page";
import { QuickFileProvider } from "../app/admin/quick-file/quickFileContext"; import { QuickFileProvider } from "../app/admin/quick-file/quickFileContext";
import QuickFileManage from "../app/admin/quick-file/manage";
import { DashboardProvider } from "../app/admin/dashboard/DashboardContext";
import DashboardLayout from "../app/admin/dashboard/DashboardLayout";
interface CustomIndexRouteObject extends IndexRouteObject { interface CustomIndexRouteObject extends IndexRouteObject {
name?: string; name?: string;
breadcrumb?: string; breadcrumb?: string;
@ -57,6 +61,36 @@ export const routes: CustomRouteObject[] = [
</WithAuth> </WithAuth>
</> </>
}, },
{
path: "/manage",
element: <>
<WithAuth>
<QuickFileManage></QuickFileManage>
</WithAuth>
</>,
children: [
{
index: true,
element: <Navigate to="/manage/dashboard" replace />
},
{
path: "/manage/share-code",
element: <>
<CodeManageProvider>
<CodeManageLayout></CodeManageLayout>
</CodeManageProvider>
</>
},
{
path: "/manage/dashboard",
element: <>
<DashboardProvider>
<DashboardLayout></DashboardLayout>
</DashboardProvider>
</>
}
]
}
]; ];
export const router = createBrowserRouter(routes); export const router = createBrowserRouter(routes);

View File

@ -426,14 +426,20 @@ model Person {
model ShareCode { model ShareCode {
id String @id @default(cuid()) id String @id @default(cuid())
code String? @unique code String? @unique
fileId String? @unique fileId String? @unique
fileName String? @map("file_name")
resource Resource? @relation(fields: [fileId], references: [fileId])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
expiresAt DateTime? @map("expires_at") expiresAt DateTime? @map("expires_at")
isUsed Boolean? @default(false)
fileName String? @map("file_name")
canUseTimes Int?
resource Resource? @relation(fields: [fileId], references: [fileId])
deletedAt DateTime? @map("deleted_at") deletedAt DateTime? @map("deleted_at")
isUsed Boolean? @default(false)
canUseTimes Int?
uploadIp String? @map("upload_ip")
@@index([code]) @@index([code])
@@index([fileId]) @@index([fileId])
@@index([expiresAt]) @@index([expiresAt])