rht
This commit is contained in:
parent
34dde48aaf
commit
9f1bc38acd
|
@ -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);
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
}
|
|
@ -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;
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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])
|
||||||
|
|
Loading…
Reference in New Issue