rht
This commit is contained in:
parent
b9426cd0ea
commit
6c26d26c2a
|
@ -1,14 +0,0 @@
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { TrpcService } from '@server/trpc/trpc.service';
|
|
||||||
import { ResourceModule } from '../resource/resource.module';
|
|
||||||
import { ShareCodeService } from './share-code.service';
|
|
||||||
import { ShareCodeRouter } from './share-code.router';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [ResourceModule],
|
|
||||||
providers: [TrpcService, ShareCodeService, ShareCodeRouter],
|
|
||||||
exports: [ShareCodeService, ShareCodeRouter],
|
|
||||||
controllers: [],
|
|
||||||
})
|
|
||||||
export class ShareCodeModule { }
|
|
|
@ -1,70 +0,0 @@
|
||||||
import { z, ZodType } from "zod";
|
|
||||||
import { ShareCodeService } from "./share-code.service";
|
|
||||||
import { TrpcService } from "@server/trpc/trpc.service";
|
|
||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
import { Prisma } from "@nice/common";
|
|
||||||
const ShareCodeWhereInputSchema: ZodType<Prisma.ShareCodeWhereInput> = z.any();
|
|
||||||
const ShareCodeFindManyArgsSchema: ZodType<Prisma.ShareCodeFindManyArgs> = z.any();
|
|
||||||
@Injectable()
|
|
||||||
export class ShareCodeRouter {
|
|
||||||
constructor(
|
|
||||||
private readonly shareCodeService: ShareCodeService,
|
|
||||||
private readonly trpc: TrpcService
|
|
||||||
) { }
|
|
||||||
|
|
||||||
router = this.trpc.router({
|
|
||||||
getFileByShareCode: this.trpc.procedure
|
|
||||||
.input(z.object({ code: z.string() }))
|
|
||||||
.query(async ({ input }) => {
|
|
||||||
return this.shareCodeService.getFileByShareCode(input.code);
|
|
||||||
}),
|
|
||||||
generateShareCodeByFileId: this.trpc.procedure
|
|
||||||
.input(z.object({ fileId: z.string(), expiresAt: z.date(), canUseTimes: z.number() }))
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
return this.shareCodeService.generateShareCodeByFileId(input.fileId, input.expiresAt, input.canUseTimes, ctx.ip);
|
|
||||||
}),
|
|
||||||
getShareCodesWithResources: this.trpc.procedure
|
|
||||||
.input(z.object({ page: z.number(), pageSize: z.number(), where: ShareCodeWhereInputSchema.optional() }))
|
|
||||||
.query(async ({ input }) => {
|
|
||||||
return this.shareCodeService.getShareCodesWithResources(input);
|
|
||||||
}),
|
|
||||||
softDeleteShareCodes: this.trpc.procedure
|
|
||||||
.input(z.object({ ids: z.array(z.string()) }))
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
return this.shareCodeService.softDeleteShareCodes(input.ids);
|
|
||||||
}),
|
|
||||||
updateShareCode: this.trpc.procedure
|
|
||||||
.input(z.object({
|
|
||||||
id: z.string(),
|
|
||||||
data: z.object({
|
|
||||||
expiresAt: z.date().optional(),
|
|
||||||
canUseTimes: z.number().optional(),
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
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);
|
|
||||||
}),
|
|
||||||
getAllreadlyDeletedShareCodes:this.trpc.procedure
|
|
||||||
.query(async () => {
|
|
||||||
return this.shareCodeService.getAllreadlyDeletedShareCodes();
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,610 +0,0 @@
|
||||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
||||||
import { customAlphabet } from 'nanoid-cjs';
|
|
||||||
import { db, ObjectType, Prisma, Resource } from '@nice/common';
|
|
||||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
|
||||||
import { ResourceService } from '@server/models/resource/resource.service';
|
|
||||||
import * as fs from 'fs'
|
|
||||||
import * as path from 'path'
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import utc from 'dayjs/plugin/utc';
|
|
||||||
import timezone from 'dayjs/plugin/timezone';
|
|
||||||
import { BaseService } from '../base/base.service';
|
|
||||||
dayjs.extend(utc);
|
|
||||||
dayjs.extend(timezone);
|
|
||||||
export interface ShareCode {
|
|
||||||
id: string;
|
|
||||||
code: string;
|
|
||||||
fileId: string;
|
|
||||||
createdAt: Date;
|
|
||||||
expiresAt: Date;
|
|
||||||
isUsed: boolean;
|
|
||||||
fileName?: string | null;
|
|
||||||
canUseTimes: number | null;
|
|
||||||
uploadIp?: string;
|
|
||||||
}
|
|
||||||
export interface GenerateShareCodeResponse {
|
|
||||||
id?: string;
|
|
||||||
code: string;
|
|
||||||
expiresAt: Date;
|
|
||||||
canUseTimes: number;
|
|
||||||
fileName?: string;
|
|
||||||
resource: {
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
url: string;
|
|
||||||
meta: ResourceMeta
|
|
||||||
}
|
|
||||||
uploadIp?: string;
|
|
||||||
createdAt?: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ResourceMeta {
|
|
||||||
filename: string;
|
|
||||||
filetype: 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()
|
|
||||||
export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
|
|
||||||
private readonly logger = new Logger(ShareCodeService.name);
|
|
||||||
// 生成8位分享码,使用易读的字符
|
|
||||||
private readonly generateCode = customAlphabet(
|
|
||||||
'0123456789ABCDEFGHJKLMNPQRSTUVWXYZ',
|
|
||||||
5,
|
|
||||||
);
|
|
||||||
|
|
||||||
constructor(private readonly resourceService: ResourceService) {
|
|
||||||
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(
|
|
||||||
fileId: string,
|
|
||||||
expiresAt: Date,
|
|
||||||
canUseTimes: number,
|
|
||||||
fileName?: string,
|
|
||||||
uploadIp?: string,
|
|
||||||
): Promise<GenerateShareCodeResponse> {
|
|
||||||
try {
|
|
||||||
// 检查文件是否存在
|
|
||||||
const resource = await this.resourceService.findUnique({
|
|
||||||
where: { fileId },
|
|
||||||
});
|
|
||||||
this.logger.log('完整 resource:', resource);
|
|
||||||
if (!resource) {
|
|
||||||
throw new NotFoundException('文件不存在');
|
|
||||||
}
|
|
||||||
const { filename, filetype, size } = resource.meta as any as ResourceMeta
|
|
||||||
// 生成分享码(修改的逻辑保证分享码的唯一性)
|
|
||||||
let code = this.generateCode();
|
|
||||||
let existingShareCode;
|
|
||||||
do {
|
|
||||||
// 查找是否已有相同 shareCode 或者相同 FileID 的分享码记录
|
|
||||||
existingShareCode = await super.findFirst({
|
|
||||||
where: {
|
|
||||||
OR: [
|
|
||||||
{ code },
|
|
||||||
{ fileId }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// 如果找到的是已经被删除的码,则可以使用并更新其他信息,否则重新生成
|
|
||||||
if(!existingShareCode){
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (existingShareCode.deleteAt !== null) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (existingShareCode && existingShareCode.code === code) {
|
|
||||||
code = this.generateCode();
|
|
||||||
}
|
|
||||||
} while (existingShareCode && existingShareCode.code === code);
|
|
||||||
|
|
||||||
if (existingShareCode) {
|
|
||||||
// 更新现有记录,但保留原有文件名
|
|
||||||
await super.update({
|
|
||||||
where: { id: existingShareCode.id },
|
|
||||||
data: {
|
|
||||||
code,
|
|
||||||
expiresAt,
|
|
||||||
canUseTimes,
|
|
||||||
isUsed: false,
|
|
||||||
fileId,
|
|
||||||
fileName: filename || "downloaded_file",
|
|
||||||
uploadIp,
|
|
||||||
createdAt: new Date(),
|
|
||||||
deletedAt: null
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 创建新记录
|
|
||||||
await super.create({
|
|
||||||
data: {
|
|
||||||
code,
|
|
||||||
fileId,
|
|
||||||
expiresAt,
|
|
||||||
canUseTimes,
|
|
||||||
isUsed: false,
|
|
||||||
fileName: filename || "downloaded_file",
|
|
||||||
uploadIp,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.logger.log(`Generated share code ${code} for file ${fileId} canUseTimes: ${canUseTimes}`);
|
|
||||||
return {
|
|
||||||
code,
|
|
||||||
expiresAt,
|
|
||||||
canUseTimes,
|
|
||||||
fileName: filename || "downloaded_file",
|
|
||||||
resource: {
|
|
||||||
id: resource.id,
|
|
||||||
type: resource.type,
|
|
||||||
url: resource.url,
|
|
||||||
meta: {
|
|
||||||
filename,
|
|
||||||
filetype,
|
|
||||||
size,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
uploadIp,
|
|
||||||
createdAt: new Date()
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('Failed to generate share code', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async validateAndUseCode(code: string): Promise<ShareCode | null> {
|
|
||||||
try {
|
|
||||||
this.logger.log(`尝试验证分享码: ${code}`);
|
|
||||||
|
|
||||||
// 查找有效的分享码
|
|
||||||
const shareCode = await super.findFirst({
|
|
||||||
where: {
|
|
||||||
code,
|
|
||||||
isUsed: false,
|
|
||||||
expiresAt: { gt: dayjs().tz('Asia/Shanghai').toDate() },
|
|
||||||
deletedAt: null
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (shareCode.canUseTimes <= 0) {
|
|
||||||
this.logger.log('分享码已使用次数超过限制');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
//更新已使用次数
|
|
||||||
await super.update({
|
|
||||||
where: { id: shareCode.id },
|
|
||||||
data: { canUseTimes: shareCode.canUseTimes - 1 },
|
|
||||||
});
|
|
||||||
this.logger.log('查询结果:', shareCode);
|
|
||||||
|
|
||||||
if (!shareCode) {
|
|
||||||
this.logger.log('分享码无效或已过期');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// 记录使用日志
|
|
||||||
this.logger.log(`Share code ${code} used for file ${shareCode.fileId}`);
|
|
||||||
// 返回完整的分享码信息,包括文件名
|
|
||||||
return shareCode;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('Failed to validate share code', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 每天清理过期的分享码
|
|
||||||
//@Cron('*/30 * * * * *')
|
|
||||||
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
|
|
||||||
async cleanupExpiredShareCodes() {
|
|
||||||
try {
|
|
||||||
const shareCodes = await super.findMany({
|
|
||||||
where: {
|
|
||||||
OR: [
|
|
||||||
{ expiresAt: { lt: dayjs().tz('Asia/Shanghai').toDate() } },
|
|
||||||
{ isUsed: true },
|
|
||||||
{ canUseTimes: { lte: 0 } }
|
|
||||||
],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.logger.log('需要清理的分享码:', shareCodes);
|
|
||||||
//文件资源硬删除
|
|
||||||
shareCodes.forEach(code => {
|
|
||||||
this.cleanupUploadFolder(code.fileId);
|
|
||||||
})
|
|
||||||
//数据库资源软删除
|
|
||||||
const result = await super.softDeleteByIds(
|
|
||||||
[...shareCodes.map(code => code.id)]
|
|
||||||
);
|
|
||||||
const deleteResource = await this.resourceService.updateMany({
|
|
||||||
where: {
|
|
||||||
fileId: {
|
|
||||||
in: shareCodes.map(code => code.fileId)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
deletedAt: new Date()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.logger.log(`Cleaned up ${result} ${deleteResource.count} expired share codes`);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('Failed to cleanup expired share codes', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async cleanupUploadFolder(file?: string) {
|
|
||||||
//const uploadDir = path.join(__dirname, '../../../uploads');
|
|
||||||
const uploadDir = path.join('/data/uploads/', file || '');
|
|
||||||
this.logger.log('uploadDir:', uploadDir);
|
|
||||||
try {
|
|
||||||
if (!fs.existsSync(uploadDir)) {
|
|
||||||
this.logger.warn(`Upload directory does not exist: ${uploadDir}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 递归删除文件夹及其内容
|
|
||||||
this.deleteFolderRecursive(uploadDir);
|
|
||||||
this.logger.log(`Cleaned up upload folder: ${uploadDir}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('读取上传目录失败:', error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private deleteFolderRecursive(dirPath: string) {
|
|
||||||
if (fs.existsSync(dirPath)) {
|
|
||||||
fs.readdirSync(dirPath).forEach((file) => {
|
|
||||||
const filePath = path.join(dirPath, file);
|
|
||||||
if (fs.statSync(filePath).isDirectory()) {
|
|
||||||
// 递归删除子目录
|
|
||||||
this.deleteFolderRecursive(filePath);
|
|
||||||
} else {
|
|
||||||
// 删除文件
|
|
||||||
fs.unlinkSync(filePath);
|
|
||||||
this.logger.log(`Deleted file: ${filePath}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// 删除空文件夹
|
|
||||||
fs.rmdirSync(dirPath);
|
|
||||||
this.logger.log(`Deleted folder: ${dirPath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据分享码获取文件
|
|
||||||
async getFileByShareCode(code: string) {
|
|
||||||
this.logger.log('收到验证分享码请求,code:', code);
|
|
||||||
const shareCode = await this.validateAndUseCode(code);
|
|
||||||
this.logger.log('验证分享码结果:', shareCode);
|
|
||||||
|
|
||||||
if (!shareCode) {
|
|
||||||
this.logger.log('分享码无效或已过期');
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取文件信息
|
|
||||||
const resource = await this.resourceService.findUnique({
|
|
||||||
where: { fileId: shareCode.fileId },
|
|
||||||
});
|
|
||||||
this.logger.log('获取到的资源信息:', resource);
|
|
||||||
const { filename, filetype, size } = resource.meta as any as ResourceMeta
|
|
||||||
const fileUrl = resource?.url
|
|
||||||
if (!resource) {
|
|
||||||
throw new NotFoundException('文件不存在');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 直接返回正确的数据结构
|
|
||||||
const response = {
|
|
||||||
id: shareCode.id,
|
|
||||||
code: shareCode.code,
|
|
||||||
fileName: filename || 'downloaded_file',
|
|
||||||
expiresAt: shareCode.expiresAt,
|
|
||||||
canUseTimes: shareCode.canUseTimes - 1,
|
|
||||||
resource: {
|
|
||||||
id: resource.id,
|
|
||||||
type: resource.type,
|
|
||||||
url: resource.url,
|
|
||||||
meta: {
|
|
||||||
filename, filetype, size
|
|
||||||
}
|
|
||||||
},
|
|
||||||
uploadIp: shareCode.uploadIp,
|
|
||||||
createdAt: shareCode.createdAt
|
|
||||||
};
|
|
||||||
|
|
||||||
this.logger.log('返回给前端的数据:', response); // 添加日志
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async getShareCodesWithResources(args: {
|
|
||||||
page?: number;
|
|
||||||
pageSize?: number;
|
|
||||||
where?: Prisma.ShareCodeWhereInput;
|
|
||||||
}): Promise<{
|
|
||||||
items: Array<ShareCode & { resource?: Resource }>;
|
|
||||||
totalPages: number;
|
|
||||||
}> {
|
|
||||||
try {
|
|
||||||
// 使用include直接关联查询Resource
|
|
||||||
const { items, totalPages } = await super.findManyWithPagination({
|
|
||||||
...args,
|
|
||||||
select: ShareCodeSelect
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
items,
|
|
||||||
totalPages
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('Failed to get share codes with resources', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async softDeleteShareCodes(ids: string[]): Promise<any> {
|
|
||||||
try {
|
|
||||||
this.logger.log(`尝试软删除分享码,IDs: ${ids.join(', ')}`);
|
|
||||||
const result = await super.softDeleteByIds(ids);
|
|
||||||
this.logger.log(`软删除分享码成功,数量: ${result.length}`);
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('软删除分享码失败', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async updateShareCode(id: string, data: Partial<ShareCode>): Promise<any> {
|
|
||||||
try {
|
|
||||||
this.logger.log(`尝试更新分享码,ID: ${id},数据:`, data);
|
|
||||||
const result = await super.updateById(id, data);
|
|
||||||
this.logger.log(`更新分享码成功:`, result);
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('更新分享码失败', 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 }
|
|
||||||
});
|
|
||||||
const {totalSize, resourceCount} = this.calculateTotalSize(shareCodes as any as GenerateShareCodeResponse[]);
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const {totalSize, resourceCount} = this.calculateTotalSize(shareCodes as any as GenerateShareCodeResponse[]);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async getAllreadlyDeletedShareCodes(args?: Omit<Prisma.ShareCodeFindManyArgs, 'select' | 'orderBy'>):Promise<{ totalSize: number; resourceCount: number; }> {
|
|
||||||
try {
|
|
||||||
const result = await super.findMany({
|
|
||||||
...args,
|
|
||||||
where: {
|
|
||||||
deletedAt: {
|
|
||||||
not: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
select: ShareCodeSelect,
|
|
||||||
});
|
|
||||||
// 计算总大小和资源数量
|
|
||||||
const { totalSize, resourceCount } = this.calculateTotalSize(result as unknown as GenerateShareCodeResponse[]);
|
|
||||||
this.logger.log(`获取已删除分享码列表成功, 数量: ${resourceCount}, 总大小: ${totalSize}`);
|
|
||||||
return {totalSize,resourceCount}
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error('获取已删除分享码列表失败', err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
calculateTotalSize(shareCodes: GenerateShareCodeResponse[]): { totalSize: number; resourceCount: number } {
|
|
||||||
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++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return { totalSize, resourceCount }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { VisitService } from './visit.service';
|
|
||||||
import { VisitRouter } from './visit.router';
|
|
||||||
import { TrpcService } from '@server/trpc/trpc.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
providers: [VisitService, VisitRouter, TrpcService],
|
|
||||||
exports: [VisitRouter]
|
|
||||||
})
|
|
||||||
export class VisitModule { }
|
|
|
@ -1,37 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { TrpcService } from '@server/trpc/trpc.service';
|
|
||||||
import { Prisma } from '@nice/common';
|
|
||||||
|
|
||||||
import { VisitService } from './visit.service';
|
|
||||||
import { z, ZodType } from 'zod';
|
|
||||||
const VisitCreateArgsSchema: ZodType<Prisma.VisitCreateArgs> = z.any();
|
|
||||||
const VisitCreateManyInputSchema: ZodType<Prisma.VisitCreateManyInput> =
|
|
||||||
z.any();
|
|
||||||
const VisitDeleteManyArgsSchema: ZodType<Prisma.VisitDeleteManyArgs> = z.any();
|
|
||||||
@Injectable()
|
|
||||||
export class VisitRouter {
|
|
||||||
constructor(
|
|
||||||
private readonly trpc: TrpcService,
|
|
||||||
private readonly visitService: VisitService,
|
|
||||||
) {}
|
|
||||||
router = this.trpc.router({
|
|
||||||
create: this.trpc.procedure
|
|
||||||
.input(VisitCreateArgsSchema)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const { staff } = ctx;
|
|
||||||
return await this.visitService.create(input, staff);
|
|
||||||
}),
|
|
||||||
createMany: this.trpc.procedure
|
|
||||||
.input(z.array(VisitCreateManyInputSchema))
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const { staff } = ctx;
|
|
||||||
|
|
||||||
return await this.visitService.createMany({ data: input }, staff);
|
|
||||||
}),
|
|
||||||
deleteMany: this.trpc.procedure
|
|
||||||
.input(VisitDeleteManyArgsSchema)
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
return await this.visitService.deleteMany(input);
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,152 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { BaseService } from '../base/base.service';
|
|
||||||
import { UserProfile, db, ObjectType, Prisma, VisitType } from '@nice/common';
|
|
||||||
import EventBus from '@server/utils/event-bus';
|
|
||||||
@Injectable()
|
|
||||||
export class VisitService extends BaseService<Prisma.VisitDelegate> {
|
|
||||||
constructor() {
|
|
||||||
super(db, ObjectType.VISIT);
|
|
||||||
}
|
|
||||||
async create(args: Prisma.VisitCreateArgs, staff?: UserProfile) {
|
|
||||||
const { postId, lectureId, messageId } = args.data;
|
|
||||||
const visitorId = args.data?.visitorId || staff?.id;
|
|
||||||
let result;
|
|
||||||
const existingVisit = await db.visit.findFirst({
|
|
||||||
where: {
|
|
||||||
type: args.data.type,
|
|
||||||
// visitorId: visitorId ? visitorId : null,
|
|
||||||
OR: [{ postId }, { messageId }],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!existingVisit) {
|
|
||||||
result = await super.create(args);
|
|
||||||
} else if (args.data.type === VisitType.READED) {
|
|
||||||
result = await super.update({
|
|
||||||
where: { id: existingVisit.id },
|
|
||||||
data: {
|
|
||||||
...args.data,
|
|
||||||
views: existingVisit.views + 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
[VisitType.READED, VisitType.LIKE, VisitType.HATE].includes(
|
|
||||||
args.data.type as VisitType,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
EventBus.emit('updateVisitCount', {
|
|
||||||
objectType: ObjectType.POST,
|
|
||||||
id: postId,
|
|
||||||
visitType: args.data.type, // 直接复用传入的类型
|
|
||||||
});
|
|
||||||
EventBus.emit('updateTotalCourseViewCount', {
|
|
||||||
visitType: args.data.type, // 直接复用传入的类型
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
async createMany(args: Prisma.VisitCreateManyArgs, staff?: UserProfile) {
|
|
||||||
const data = Array.isArray(args.data) ? args.data : [args.data];
|
|
||||||
const updatePromises: any[] = [];
|
|
||||||
const createData: Prisma.VisitCreateManyInput[] = [];
|
|
||||||
await Promise.all(
|
|
||||||
data.map(async (item) => {
|
|
||||||
if (staff && !item.visitorId) item.visitorId = staff.id;
|
|
||||||
const { postId, lectureId, messageId, visitorId } = item;
|
|
||||||
const existingVisit = await db.visit.findFirst({
|
|
||||||
where: {
|
|
||||||
visitorId,
|
|
||||||
OR: [{ postId }, { lectureId }, { messageId }],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingVisit) {
|
|
||||||
updatePromises.push(
|
|
||||||
super.update({
|
|
||||||
where: { id: existingVisit.id },
|
|
||||||
data: {
|
|
||||||
...item,
|
|
||||||
views: existingVisit.views + 1,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
createData.push(item);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
// Execute all updates in parallel
|
|
||||||
await Promise.all(updatePromises);
|
|
||||||
// Create new visits for those not existing
|
|
||||||
if (createData.length > 0) {
|
|
||||||
return super.createMany({
|
|
||||||
...args,
|
|
||||||
data: createData,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { count: updatePromises.length }; // Return the number of updates if no new creates
|
|
||||||
}
|
|
||||||
async deleteMany(args: Prisma.VisitDeleteManyArgs, staff?: UserProfile) {
|
|
||||||
// const where = Array.isArray(args.where) ? args.where : [args.where];
|
|
||||||
// const updatePromises: any[] = [];
|
|
||||||
// const createData: Prisma.VisitCreateManyInput[] = [];
|
|
||||||
// super
|
|
||||||
// await Promise.all(
|
|
||||||
// data.map(async (item) => {
|
|
||||||
// if (staff && !item.visitorId) item.visitorId = staff.id;
|
|
||||||
// const { postId, messageId, visitorId } = item;
|
|
||||||
// const existingVisit = await db.visit.findFirst({
|
|
||||||
// where: {
|
|
||||||
// visitorId,
|
|
||||||
// OR: [{ postId }, { messageId }],
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (existingVisit) {
|
|
||||||
// updatePromises.push(
|
|
||||||
// super.update({
|
|
||||||
// where: { id: existingVisit.id },
|
|
||||||
// data: {
|
|
||||||
// ...item,
|
|
||||||
// views: existingVisit.views + 1,
|
|
||||||
// },
|
|
||||||
// }),
|
|
||||||
// );
|
|
||||||
// } else {
|
|
||||||
// createData.push(item);
|
|
||||||
// }
|
|
||||||
// }),
|
|
||||||
// );
|
|
||||||
// // Execute all updates in parallel
|
|
||||||
// await Promise.all(updatePromises);
|
|
||||||
// // Create new visits for those not existing
|
|
||||||
// if (createData.length > 0) {
|
|
||||||
// return super.createMany({
|
|
||||||
// ...args,
|
|
||||||
// data: createData,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// return { count: updatePromises.length }; // Return the number of updates if no new creates
|
|
||||||
const superDetele = super.deleteMany(args, staff);
|
|
||||||
if (args?.where?.postId) {
|
|
||||||
if (
|
|
||||||
[VisitType.READED, VisitType.LIKE, VisitType.HATE].includes(
|
|
||||||
args.where.type as any,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
EventBus.emit('updateVisitCount', {
|
|
||||||
objectType: ObjectType.POST,
|
|
||||||
id: args?.where?.postId as string,
|
|
||||||
visitType: args.where.type as any, // 直接复用传入的类型
|
|
||||||
});
|
|
||||||
EventBus.emit('updateTotalCourseViewCount', {
|
|
||||||
visitType: args.where.type as any, // 直接复用传入的类型
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return superDetele;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -10,11 +10,9 @@ import { AuthModule } from '@server/auth/auth.module';
|
||||||
import { AppConfigModule } from '@server/models/app-config/app-config.module';
|
import { AppConfigModule } from '@server/models/app-config/app-config.module';
|
||||||
import { MessageModule } from '@server/models/message/message.module';
|
import { MessageModule } from '@server/models/message/message.module';
|
||||||
import { PostModule } from '@server/models/post/post.module';
|
import { PostModule } from '@server/models/post/post.module';
|
||||||
import { VisitModule } from '@server/models/visit/visit.module';
|
|
||||||
import { WebSocketModule } from '@server/socket/websocket.module';
|
import { WebSocketModule } from '@server/socket/websocket.module';
|
||||||
import { RoleMapModule } from '@server/models/rbac/rbac.module';
|
import { RoleMapModule } from '@server/models/rbac/rbac.module';
|
||||||
import { TransformModule } from '@server/models/transform/transform.module';
|
import { TransformModule } from '@server/models/transform/transform.module';
|
||||||
import { ShareCodeModule } from '@server/models/share-code/share-code.module';
|
|
||||||
import { ResourceModule } from '@server/models/resource/resource.module';
|
import { ResourceModule } from '@server/models/resource/resource.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
@ -30,10 +28,8 @@ import { ResourceModule } from '@server/models/resource/resource.module';
|
||||||
MessageModule,
|
MessageModule,
|
||||||
AppConfigModule,
|
AppConfigModule,
|
||||||
PostModule,
|
PostModule,
|
||||||
VisitModule,
|
|
||||||
WebSocketModule,
|
WebSocketModule,
|
||||||
ResourceModule,
|
ResourceModule,
|
||||||
ShareCodeModule,
|
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [TrpcService, TrpcRouter, Logger],
|
providers: [TrpcService, TrpcRouter, Logger],
|
||||||
|
|
|
@ -9,12 +9,10 @@ import ws, { WebSocketServer } from 'ws';
|
||||||
import { AppConfigRouter } from '@server/models/app-config/app-config.router';
|
import { AppConfigRouter } from '@server/models/app-config/app-config.router';
|
||||||
import { MessageRouter } from '@server/models/message/message.router';
|
import { MessageRouter } from '@server/models/message/message.router';
|
||||||
import { PostRouter } from '@server/models/post/post.router';
|
import { PostRouter } from '@server/models/post/post.router';
|
||||||
import { VisitRouter } from '@server/models/visit/visit.router';
|
|
||||||
import { RoleMapRouter } from '@server/models/rbac/rolemap.router';
|
import { RoleMapRouter } from '@server/models/rbac/rolemap.router';
|
||||||
import { TransformRouter } from '@server/models/transform/transform.router';
|
import { TransformRouter } from '@server/models/transform/transform.router';
|
||||||
import { RoleRouter } from '@server/models/rbac/role.router';
|
import { RoleRouter } from '@server/models/rbac/role.router';
|
||||||
import { ResourceRouter } from '../models/resource/resource.router';
|
import { ResourceRouter } from '../models/resource/resource.router';
|
||||||
import { ShareCodeRouter } from '@server/models/share-code/share-code.router';
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TrpcRouter {
|
export class TrpcRouter {
|
||||||
logger = new Logger(TrpcRouter.name);
|
logger = new Logger(TrpcRouter.name);
|
||||||
|
@ -30,9 +28,7 @@ export class TrpcRouter {
|
||||||
private readonly transform: TransformRouter,
|
private readonly transform: TransformRouter,
|
||||||
private readonly app_config: AppConfigRouter,
|
private readonly app_config: AppConfigRouter,
|
||||||
private readonly message: MessageRouter,
|
private readonly message: MessageRouter,
|
||||||
private readonly visitor: VisitRouter,
|
|
||||||
private readonly resource: ResourceRouter,
|
private readonly resource: ResourceRouter,
|
||||||
private readonly shareCode: ShareCodeRouter,
|
|
||||||
) {}
|
) {}
|
||||||
getRouter() {
|
getRouter() {
|
||||||
return;
|
return;
|
||||||
|
@ -48,9 +44,7 @@ export class TrpcRouter {
|
||||||
rolemap: this.rolemap.router,
|
rolemap: this.rolemap.router,
|
||||||
message: this.message.router,
|
message: this.message.router,
|
||||||
app_config: this.app_config.router,
|
app_config: this.app_config.router,
|
||||||
visitor: this.visitor.router,
|
|
||||||
resource: this.resource.router,
|
resource: this.resource.router,
|
||||||
shareCode: this.shareCode.router,
|
|
||||||
});
|
});
|
||||||
wss: WebSocketServer = undefined;
|
wss: WebSocketServer = undefined;
|
||||||
|
|
||||||
|
|
|
@ -1,116 +0,0 @@
|
||||||
import { Form, FormInstance, message } from "antd";
|
|
||||||
import { api } from "@nice/client";
|
|
||||||
import { createContext, useContext, useState } from "react";
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { getQueryKey } from "@trpc/react-query";
|
|
||||||
import { ShareCodeResponse } from "../quick-file/quickFileContext";
|
|
||||||
|
|
||||||
interface CodeManageContextType {
|
|
||||||
editForm: FormInstance<any>;
|
|
||||||
isLoading: boolean;
|
|
||||||
currentShareCodes: ShareCodeWithResource;
|
|
||||||
currentPage: number;
|
|
||||||
setCurrentPage: (page: number) => void;
|
|
||||||
pageSize: number;
|
|
||||||
deletShareCode: (id: string) => void;
|
|
||||||
updateCode: (expiresAt: Date, canUseTimes: number) => void
|
|
||||||
setCurrentCodeId: (id: string) => void,
|
|
||||||
currentCodeId: string | null,
|
|
||||||
searchRefetch: () => void,
|
|
||||||
setSearchKeyword: (keyword: string) => void,
|
|
||||||
currentCode: string | null,
|
|
||||||
setCurrentCode: (code: string) => void,
|
|
||||||
searchKeyword: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ShareCodeWithResource {
|
|
||||||
items: ShareCodeResponse[],
|
|
||||||
totalPages: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CodeManageContext = createContext<CodeManageContextType | null>(null);
|
|
||||||
|
|
||||||
export const CodeManageProvider = ({ children }: { children: React.ReactNode }) => {
|
|
||||||
const [editForm] = Form.useForm();
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
const [currentCodeId, setCurrentCodeId] = useState<string | null>()
|
|
||||||
const [currentCode, setCurrentCode] = useState<string | null>()
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const pageSize = 8;
|
|
||||||
// 在组件顶部添加
|
|
||||||
const [searchKeyword, setSearchKeyword] = useState('');
|
|
||||||
// 构建查询条件
|
|
||||||
const whereCondition = {
|
|
||||||
deletedAt: null,
|
|
||||||
...(searchKeyword ? {
|
|
||||||
OR: [
|
|
||||||
{ fileName: { contains: searchKeyword } },
|
|
||||||
{ code: { contains: searchKeyword } },
|
|
||||||
{ uploadIp: { contains: searchKeyword } }
|
|
||||||
]
|
|
||||||
} : {})
|
|
||||||
};
|
|
||||||
const { data: currentShareCodes, refetch: searchRefetch }: { data: ShareCodeWithResource, refetch: () => void } = api.shareCode.getShareCodesWithResources.useQuery(
|
|
||||||
{
|
|
||||||
page: currentPage,
|
|
||||||
pageSize: pageSize,
|
|
||||||
where: whereCondition,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: true,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const { mutate: softDeleteShareCode } = api.shareCode.softDeleteShareCodes.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: getQueryKey(api.shareCode) });
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
message.error('删除失败')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const { mutate: updateShareCode } = api.shareCode.updateShareCode.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: getQueryKey(api.shareCode) });
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
message.error('更新失败')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const deletShareCode = (id: string) => {
|
|
||||||
softDeleteShareCode({ ids: [id] })
|
|
||||||
}
|
|
||||||
const updateCode = (expiresAt: Date, canUseTimes: number) => {
|
|
||||||
if (currentCodeId) updateShareCode({ id: currentCodeId, data: { expiresAt, canUseTimes } })
|
|
||||||
}
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
return <>
|
|
||||||
<CodeManageContext.Provider value={{
|
|
||||||
editForm,
|
|
||||||
isLoading,
|
|
||||||
currentShareCodes,
|
|
||||||
currentPage,
|
|
||||||
setCurrentPage,
|
|
||||||
pageSize,
|
|
||||||
deletShareCode,
|
|
||||||
updateCode,
|
|
||||||
currentCodeId,
|
|
||||||
setCurrentCodeId,
|
|
||||||
searchRefetch,
|
|
||||||
setSearchKeyword,
|
|
||||||
currentCode,
|
|
||||||
setCurrentCode,
|
|
||||||
searchKeyword
|
|
||||||
}}>
|
|
||||||
{children}
|
|
||||||
</CodeManageContext.Provider>
|
|
||||||
</>
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useCodeManageContext = () => {
|
|
||||||
const context = useContext(CodeManageContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useCodeManageContext must be used within a CodeManageProvider");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
|
@ -1,26 +0,0 @@
|
||||||
import { useSearchParams } from "react-router-dom";
|
|
||||||
import CodeManageSearchBase from "./components/CodeManageSearchBase";
|
|
||||||
import CodeManageDisplay from "./components/CodeMangeDisplay";
|
|
||||||
import { useCodeManageContext } from "./CodeManageContext";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
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 (
|
|
||||||
<div className="max-w-[1100px] mx-auto h-[100vh]">
|
|
||||||
<CodeManageSearchBase keyword={localKeyword}></CodeManageSearchBase>
|
|
||||||
<CodeManageDisplay></CodeManageDisplay>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
import { useCodeManageContext } from "../CodeManageContext";
|
|
||||||
import { Form, DatePicker, Input, Button } from "antd";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
export default function CodeManageEdit() {
|
|
||||||
const { editForm } = useCodeManageContext();
|
|
||||||
// 验证数字输入只能是大于等于0的整数
|
|
||||||
const validatePositiveInteger = (_: any, value: string) => {
|
|
||||||
const num = parseInt(value, 10);
|
|
||||||
if (isNaN(num) || num < 0 || num !== parseFloat(value)) {
|
|
||||||
return Promise.reject("请输入大于等于0的整数");
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full max-w-md mx-auto bg-white p-6 rounded-lg">
|
|
||||||
<Form
|
|
||||||
form={editForm}
|
|
||||||
layout="vertical"
|
|
||||||
className="space-y-4"
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
label={<span className="text-gray-700 font-medium">分享码有效期</span>}
|
|
||||||
name="expiresAt"
|
|
||||||
rules={[{ required: true, message: "请选择有效期" }]}
|
|
||||||
className="mb-5"
|
|
||||||
>
|
|
||||||
<DatePicker
|
|
||||||
className="w-full"
|
|
||||||
showTime
|
|
||||||
placeholder="选择日期和时间"
|
|
||||||
disabledDate={(current) => current && current < dayjs().startOf('day')}
|
|
||||||
disabledTime={(current) => {
|
|
||||||
if (current && current.isSame(dayjs(), 'day')) {
|
|
||||||
return {
|
|
||||||
disabledHours: () => [...Array(dayjs().hour()).keys()],
|
|
||||||
disabledMinutes: (selectedHour) => {
|
|
||||||
if (selectedHour === dayjs().hour()) {
|
|
||||||
return [...Array(dayjs().minute()).keys()];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={<span className="text-gray-700 font-medium">使用次数</span>}
|
|
||||||
name="canUseTimes"
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: "请输入使用次数" },
|
|
||||||
{ validator: validatePositiveInteger }
|
|
||||||
]}
|
|
||||||
className="mb-5"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
step={1}
|
|
||||||
placeholder="请输入使用次数"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
import { Button, Form, Input } from 'antd';
|
|
||||||
import { useCodeManageContext } from '../CodeManageContext';
|
|
||||||
import { ChangeEvent, useEffect, useRef } from 'react';
|
|
||||||
import { useSearchParams } from "react-router-dom";
|
|
||||||
|
|
||||||
export default function CodeManageSearchBase({ keyword }: { keyword?: string }) {
|
|
||||||
const { setCurrentPage, searchRefetch, setSearchKeyword, searchKeyword } = useCodeManageContext();
|
|
||||||
const debounceTimer = useRef<NodeJS.Timeout | null>(null);
|
|
||||||
const formRef = Form.useForm()[0]; // 获取表单实例
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
|
||||||
|
|
||||||
// 当 keyword 属性变化时更新表单值
|
|
||||||
useEffect(() => {
|
|
||||||
if (keyword) {
|
|
||||||
formRef.setFieldsValue({ search: keyword });
|
|
||||||
}
|
|
||||||
}, [keyword, formRef]);
|
|
||||||
|
|
||||||
// 监听 searchKeyword 变化,如果 URL 参数中有 keyword 且 searchKeyword 发生变化,则清空 URL 参数
|
|
||||||
useEffect(() => {
|
|
||||||
const urlKeyword = searchParams.get('keyword');
|
|
||||||
if (urlKeyword && searchKeyword !== urlKeyword) {
|
|
||||||
// 创建一个新的 URLSearchParams 对象,不包含 keyword 参数
|
|
||||||
const newParams = new URLSearchParams(searchParams);
|
|
||||||
newParams.delete('keyword');
|
|
||||||
setSearchParams(newParams);
|
|
||||||
}
|
|
||||||
}, [searchKeyword, searchParams, setSearchParams]);
|
|
||||||
|
|
||||||
const onSearch = (value: string) => {
|
|
||||||
console.log(value);
|
|
||||||
setSearchKeyword(value);
|
|
||||||
setCurrentPage(1)
|
|
||||||
searchRefetch()
|
|
||||||
};
|
|
||||||
|
|
||||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
// 设置表单值
|
|
||||||
setSearchKeyword(e.target.value);
|
|
||||||
// 设置页码为1,确保从第一页开始显示搜索结果
|
|
||||||
setCurrentPage(1);
|
|
||||||
// 使用防抖处理,避免频繁发送请求
|
|
||||||
if (debounceTimer.current) {
|
|
||||||
clearTimeout(debounceTimer.current);
|
|
||||||
}
|
|
||||||
debounceTimer.current = setTimeout(() => {
|
|
||||||
// 触发查询
|
|
||||||
searchRefetch();
|
|
||||||
}, 300); // 300毫秒的防抖延迟
|
|
||||||
};
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<div className="py-4 mt-2 w-2/3 mx-auto">
|
|
||||||
<Form form={formRef}>
|
|
||||||
<Form.Item name="search" label="关键字搜索">
|
|
||||||
<Input.Search
|
|
||||||
placeholder="输入分享码、文件名或用户IP"
|
|
||||||
enterButton
|
|
||||||
value={searchKeyword}
|
|
||||||
allowClear
|
|
||||||
onSearch={onSearch}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
import { message, Modal, Pagination } from "antd";
|
|
||||||
import ShareCodeList from "./ShareCodeList";
|
|
||||||
import { useCodeManageContext } from "../CodeManageContext";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { ExclamationCircleFilled } from "@ant-design/icons";
|
|
||||||
import CodeManageEdit from "./CodeManageEdit";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
export default function CodeMangeDisplay() {
|
|
||||||
const { isLoading, currentShareCodes, pageSize, currentPage,
|
|
||||||
setCurrentPage, deletShareCode, editForm,
|
|
||||||
updateCode, setCurrentCodeId, setCurrentCode, currentCode
|
|
||||||
} = useCodeManageContext();
|
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
|
||||||
const [formLoading, setFormLoading] = useState(false);
|
|
||||||
const { confirm } = Modal;
|
|
||||||
const handleEdit = (id: string, expiresAt: Date, canUseTimes: number, code: string) => {
|
|
||||||
console.log('编辑分享码:', id);
|
|
||||||
setCurrentCodeId(id)
|
|
||||||
setModalOpen(true);
|
|
||||||
setCurrentCode(code)
|
|
||||||
editForm.setFieldsValue({
|
|
||||||
expiresAt: dayjs(expiresAt),
|
|
||||||
canUseTimes: canUseTimes
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const handleEditOk = () => {
|
|
||||||
const expiresAt = editForm.getFieldsValue().expiresAt.tz('Asia/Shanghai').toDate()
|
|
||||||
const canUseTimes = Number(editForm.getFieldsValue().canUseTimes)
|
|
||||||
updateCode(expiresAt, canUseTimes)
|
|
||||||
message.success('分享码已更新')
|
|
||||||
setModalOpen(false)
|
|
||||||
}
|
|
||||||
const handleDelete = (id: string) => {
|
|
||||||
console.log('删除分享码:', id);
|
|
||||||
confirm({
|
|
||||||
title: '确定删除该分享码吗',
|
|
||||||
icon: <ExclamationCircleFilled />,
|
|
||||||
content: '',
|
|
||||||
okText: '删除',
|
|
||||||
okType: 'danger',
|
|
||||||
cancelText: '取消',
|
|
||||||
async onOk() {
|
|
||||||
deletShareCode(id)
|
|
||||||
message.success('分享码已删除')
|
|
||||||
},
|
|
||||||
onCancel() {
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('currentShareCodes:', currentShareCodes);
|
|
||||||
}, [currentShareCodes]);
|
|
||||||
return <>
|
|
||||||
<div className="w-full min-h-[720px] mx-auto">
|
|
||||||
<ShareCodeList
|
|
||||||
data={currentShareCodes?.items}
|
|
||||||
loading={isLoading}
|
|
||||||
onEdit={handleEdit}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="py-4 mt-10 w-2/3 mx-auto flex justify-center">
|
|
||||||
<Pagination
|
|
||||||
defaultCurrent={currentPage}
|
|
||||||
total={currentShareCodes?.totalPages * pageSize}
|
|
||||||
pageSize={pageSize}
|
|
||||||
onChange={(page, pageSize) => {
|
|
||||||
setCurrentPage(page);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Modal
|
|
||||||
width={550}
|
|
||||||
onOk={() => {
|
|
||||||
handleEditOk()
|
|
||||||
}}
|
|
||||||
centered
|
|
||||||
open={modalOpen}
|
|
||||||
confirmLoading={formLoading}
|
|
||||||
onCancel={() => {
|
|
||||||
setModalOpen(false);
|
|
||||||
}}
|
|
||||||
title={`编辑分享码:${currentCode}`}>
|
|
||||||
<CodeManageEdit></CodeManageEdit>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { List,} from 'antd';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import utc from 'dayjs/plugin/utc';
|
|
||||||
import timezone from 'dayjs/plugin/timezone';
|
|
||||||
import ShareCodeListCard from './ShareCodeListCard';
|
|
||||||
import { ShareCodeResponse } from '../../quick-file/quickFileContext';
|
|
||||||
|
|
||||||
dayjs.extend(utc);
|
|
||||||
dayjs.extend(timezone);
|
|
||||||
|
|
||||||
|
|
||||||
interface ShareCodeListProps {
|
|
||||||
data: ShareCodeResponse[];
|
|
||||||
loading?: boolean;
|
|
||||||
onEdit?: (id: string,expiresAt:Date,canUseTimes:number,code:string) => void;
|
|
||||||
onDelete?: (id: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ShareCodeList: React.FC<ShareCodeListProps> = ({
|
|
||||||
data,
|
|
||||||
loading,
|
|
||||||
onEdit,
|
|
||||||
onDelete
|
|
||||||
}) => {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<List
|
|
||||||
grid={{ gutter: 16, xs: 1, sm: 2, md: 4, lg: 4 }}
|
|
||||||
dataSource={data}
|
|
||||||
loading={loading}
|
|
||||||
renderItem={(item) => (
|
|
||||||
<List.Item>
|
|
||||||
<ShareCodeListCard item={item} onEdit={onEdit} onDelete={onDelete} styles='w-[262px]' />
|
|
||||||
</List.Item>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ShareCodeList;
|
|
|
@ -1,79 +0,0 @@
|
||||||
import { DeleteOutlined, DownloadOutlined, EditOutlined } from "@ant-design/icons";
|
|
||||||
import { Button, Card, Typography } from "antd";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { ShareCodeResponse } from "../../quick-file/quickFileContext";
|
|
||||||
|
|
||||||
export default function ShareCodeListCard({ item, onEdit, onDelete, styles, onDownload }:
|
|
||||||
{
|
|
||||||
item: ShareCodeResponse,
|
|
||||||
styles?: string,
|
|
||||||
onDelete: (id: string) => void,
|
|
||||||
onEdit?: (id: string, expiresAt: Date, canUseTimes: number, code: string) => void,
|
|
||||||
onDownload?: (id: string) => void
|
|
||||||
}) {
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('item:', item);
|
|
||||||
}, [item]);
|
|
||||||
return <div className={`${styles}`}>
|
|
||||||
<Card
|
|
||||||
className="shadow-md hover:shadow-lg h-[344px] transition-shadow duration-300 space-x-4"
|
|
||||||
title={
|
|
||||||
<Typography.Text
|
|
||||||
strong
|
|
||||||
className="text-lg font-semibold text-blue-600"
|
|
||||||
>
|
|
||||||
{item.code}
|
|
||||||
</Typography.Text>
|
|
||||||
}
|
|
||||||
hoverable
|
|
||||||
actions={[
|
|
||||||
onEdit && (
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
onClick={() => onEdit?.(item.id, dayjs(item.expiresAt).toDate(), item.canUseTimes, item.code)}
|
|
||||||
className="text-blue-500 hover:text-blue-700"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
onDownload && (
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<DownloadOutlined />}
|
|
||||||
onClick={() => onDownload?.(item.code)}
|
|
||||||
className="text-blue-500 hover:text-blue-700"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
onClick={() => onDelete?.(item.id)}
|
|
||||||
className="text-red-500 hover:text-red-700"
|
|
||||||
/>
|
|
||||||
].filter(Boolean)}
|
|
||||||
>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-gray-700 whitespace-nowrap overflow-hidden text-overflow-ellipsis hover:overflow-auto">
|
|
||||||
<span className="font-medium">文件名:</span> {item.fileName}
|
|
||||||
</p>
|
|
||||||
<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
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="text-gray-700 whitespace-nowrap overflow-hidden text-overflow-ellipsis hover:overflow-auto">
|
|
||||||
<span className="font-medium">剩余使用次数:</span> {item.canUseTimes}
|
|
||||||
</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>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
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;
|
|
||||||
deletedData: { totalSize?: number; resourceCount?: number; };
|
|
||||||
isDeletedLoading: 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',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const {data:deletedData , isLoading:isDeletedLoading} = api.shareCode.getAllreadlyDeletedShareCodes.useQuery()
|
|
||||||
return <>
|
|
||||||
<DashboardContext.Provider value={{
|
|
||||||
shareCodeAll,
|
|
||||||
shareCodeToday,
|
|
||||||
shareCodeYesterday,
|
|
||||||
isShareCodeAllLoading,
|
|
||||||
isShareCodeTodayLoading,
|
|
||||||
isShareCodeYesterdayLoading,
|
|
||||||
distinctUploadIPs,
|
|
||||||
isDistinctUploadIPsLoading,
|
|
||||||
shareCodeList,
|
|
||||||
isShareCodeListLoading,
|
|
||||||
deletedData,
|
|
||||||
isDeletedLoading
|
|
||||||
}}>
|
|
||||||
{children}
|
|
||||||
</DashboardContext.Provider>
|
|
||||||
</>
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useDashboardContext = () => {
|
|
||||||
const context = useContext(DashboardContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useDashboardContext must be used within a DashboardProvider");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
|
@ -1,13 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
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" />}
|
|
||||||
time={item.createdAt?.toLocaleString()}
|
|
||||||
ip={item.uploadIp}
|
|
||||||
filename={item.fileName}
|
|
||||||
filesize={Math.max(Number(item.resource.meta.size) / 1024 / 1024, 0.01).toFixed(2)}
|
|
||||||
code={item.code}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Space>
|
|
||||||
</DashboardCard>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
// 活动项组件
|
|
||||||
interface ActivityItemProps {
|
|
||||||
icon: React.ReactNode;
|
|
||||||
time: string;
|
|
||||||
ip: string;
|
|
||||||
filename: string;
|
|
||||||
filesize: string;
|
|
||||||
code: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ActivityItem({ icon, time, ip ,filename, filesize, code }: 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-500'>
|
|
||||||
来自<span className='text-red-500'>{ip}</span>的用户
|
|
||||||
上传了文件:<span className='text-blue-500'>"{filename}"</span>,
|
|
||||||
文件大小:<span className='text-gray-500'>{filesize}MB</span>,
|
|
||||||
分享码:<span className='text-primary-900'>{code}</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-gray-400 text-xs">{time}</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,157 +0,0 @@
|
||||||
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,deletedData,isDeletedLoading } = 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 / 1024).toFixed(2)}GB`} />
|
|
||||||
{
|
|
||||||
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>
|
|
||||||
{/* 删除状态卡片 */}
|
|
||||||
{/* <Col xs={24} sm={12} md={6}>
|
|
||||||
<DashboardCard
|
|
||||||
title={
|
|
||||||
<div className="flex items-center">
|
|
||||||
<CheckCircleOutlined style={{ marginRight: 8, fontSize: 16 }} />
|
|
||||||
已清理文件数
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
className="h-full"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col h-full justify-between py-2">
|
|
||||||
<Statistic value={isDeletedLoading ? 0 : `${deletedData.resourceCount}`} />
|
|
||||||
<div className="text-gray-500 text-sm mt-2">
|
|
||||||
已经清理文件大小: {isDeletedLoading? 0 : `${(deletedData.totalSize / 1024 / 1024 / 1024).toFixed(2)}GB`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DashboardCard>
|
|
||||||
</Col> */}
|
|
||||||
</Row>
|
|
||||||
</>
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
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 pt-3">
|
|
||||||
<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;
|
|
|
@ -1,86 +0,0 @@
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { Layout, Menu, Button, Space } from "antd";
|
|
||||||
import { FileOutlined, DashboardOutlined, HomeOutlined, LogoutOutlined } from "@ant-design/icons";
|
|
||||||
import { NavLink, Outlet, useNavigate, useLocation } from "react-router-dom";
|
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
|
||||||
|
|
||||||
const { Sider, Content } = Layout;
|
|
||||||
|
|
||||||
export default function QuickFileManage() {
|
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
const [currentKey, setCurrentKey] = useState("1");
|
|
||||||
const { logout } = useAuth();
|
|
||||||
|
|
||||||
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",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center cursor-pointer w-full">
|
|
||||||
<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>,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<div className="mt-auto p-2 flex justify-between" style={{marginTop: "auto"}}>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
icon={<HomeOutlined />}
|
|
||||||
onClick={() => navigate('/')}
|
|
||||||
style={{flex: 1}}
|
|
||||||
>
|
|
||||||
{!collapsed && "首页"}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
icon={<LogoutOutlined />}
|
|
||||||
onClick={async () => {
|
|
||||||
navigate('/');
|
|
||||||
await logout();
|
|
||||||
}}
|
|
||||||
style={{flex: 1}}
|
|
||||||
>
|
|
||||||
{!collapsed && "登出"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Sider>
|
|
||||||
<Content style={{ padding: "24px" }}>
|
|
||||||
<Outlet></Outlet>
|
|
||||||
</Content>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
|
|
||||||
import { ShareCodeGenerator } from "../sharecode/sharecodegenerator";
|
|
||||||
import { ShareCodeValidator } from "../sharecode/sharecodevalidator";
|
|
||||||
import { Form } from "antd";
|
|
||||||
import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
|
|
||||||
import CodeRecord from "../sharecode/components/CodeRecord";
|
|
||||||
import Header from "./components/header";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { MainFooter } from "../../main/layout/MainFooter";
|
|
||||||
export default function QuickUploadPage() {
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const uploadFileId = Form.useWatch(["file"], form)?.[0]
|
|
||||||
const [fileMsg, setFileMsg] = useState<File|null>(null)
|
|
||||||
return (
|
|
||||||
<div className="w-full min-h-screen bg-gray-50">
|
|
||||||
<Header />
|
|
||||||
<div className="max-w-[1000px] mx-auto px-4 py-6">
|
|
||||||
<div className="my-1 p-6 bg-white border border-gray-100 rounded-xl shadow-md hover:shadow-lg transition-shadow duration-300">
|
|
||||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 mb-5 pb-4 border-b border-gray-100">
|
|
||||||
<span className="text-xl font-medium text-gray-800">使用分享码下载文件</span>
|
|
||||||
<CodeRecord title="我使用的分享码" btnContent="使用记录" recordName="shareCodeDownloadRecords" />
|
|
||||||
</div>
|
|
||||||
<div className="bg-gray-50 bg-opacity-70 rounded-lg p-4">
|
|
||||||
<ShareCodeValidator />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="my-6 p-6 bg-white border border-gray-100 rounded-xl shadow-md hover:shadow-lg transition-shadow duration-300">
|
|
||||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 mb-5 pb-4 border-b border-gray-100">
|
|
||||||
<span className="text-xl font-medium text-gray-800">上传文件并生成分享码</span>
|
|
||||||
<CodeRecord title="我生成的分享码" btnContent="上传记录" recordName="shareCodeGeneratorRecords" />
|
|
||||||
</div>
|
|
||||||
<div className="mb-8">
|
|
||||||
<ShareCodeGenerator fileId={uploadFileId} fileMsg={fileMsg} />
|
|
||||||
</div>
|
|
||||||
<div className="mt-4">
|
|
||||||
<Form form={form}>
|
|
||||||
<Form.Item name="file">
|
|
||||||
<TusUploader
|
|
||||||
multiple={false}
|
|
||||||
style="w-full py-8 px-4 mb-0 h-72 border-2 border-dashed border-gray-200 hover:border-blue-400 bg-gray-50 bg-opacity-50 rounded-lg transition-colors"
|
|
||||||
getFileMsg={(file) => {
|
|
||||||
setFileMsg(file)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<MainFooter />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,127 +0,0 @@
|
||||||
import { message } from "antd";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { createContext, useContext, useState } from "react";
|
|
||||||
import { env } from "@web/src/env";
|
|
||||||
import { api } from "@nice/client";
|
|
||||||
interface QuickFileContextType {
|
|
||||||
saveCodeRecord: (data: ShareCodeResponse, recordName: string) => void;
|
|
||||||
handleValidSuccess: (fileUrl: string, fileName: string) => void;
|
|
||||||
isGetingFileId: boolean;
|
|
||||||
downloadCode: string | null;
|
|
||||||
setDownloadCode: (code: string | null) => void;
|
|
||||||
refetchShareCodeWithResource: () => Promise<{ data: any }>;
|
|
||||||
isLoading: boolean;
|
|
||||||
downloadResult: ShareCodeResponse | null;
|
|
||||||
}
|
|
||||||
export interface ShareCodeResponse {
|
|
||||||
id?: string;
|
|
||||||
code?: string;
|
|
||||||
fileName?: string;
|
|
||||||
expiresAt?: Date;
|
|
||||||
canUseTimes?: number;
|
|
||||||
resource?: {
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
url: string;
|
|
||||||
meta: ResourceMeta
|
|
||||||
}
|
|
||||||
uploadIp?: string;
|
|
||||||
createdAt?: Date;
|
|
||||||
}
|
|
||||||
interface ResourceMeta {
|
|
||||||
filename: string;
|
|
||||||
filetype: string;
|
|
||||||
size: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const QuickFileContext = createContext<QuickFileContextType | null>(null);
|
|
||||||
|
|
||||||
export const QuickFileProvider = ({ children }: { children: React.ReactNode }) => {
|
|
||||||
const [isGetingFileId, setIsGetingFileId] = useState(false);
|
|
||||||
const [downloadCode, setDownloadCode] = useState<string | null>(null);
|
|
||||||
const saveCodeRecord = (data: ShareCodeResponse, recordName: string) => {
|
|
||||||
if (data.canUseTimes == 0) {
|
|
||||||
const existingGeneratorRecords = localStorage.getItem(recordName);
|
|
||||||
if (existingGeneratorRecords && data.code) {
|
|
||||||
const generatorRecords = JSON.parse(existingGeneratorRecords);
|
|
||||||
const filteredRecords = generatorRecords.filter((item: ShareCodeResponse) => item.code !== data.code);
|
|
||||||
localStorage.setItem(recordName, JSON.stringify(filteredRecords));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newRecord = {
|
|
||||||
id: `${Date.now()}`, // 生成唯一ID
|
|
||||||
code: data.code,
|
|
||||||
expiresAt: dayjs(data.expiresAt).format('YYYY-MM-DD HH:mm:ss'),
|
|
||||||
fileName: data.fileName || `文件_${data.code}`,
|
|
||||||
canUseTimes: data.canUseTimes,
|
|
||||||
resource: {
|
|
||||||
id: data.resource.id,
|
|
||||||
type: data.resource.type,
|
|
||||||
url: data.resource.url,
|
|
||||||
meta: {
|
|
||||||
size: data.resource.meta.size,
|
|
||||||
filename: data.resource.meta.filename,
|
|
||||||
filetype: data.resource.meta.filetype
|
|
||||||
}
|
|
||||||
},
|
|
||||||
uploadIp: data.uploadIp,
|
|
||||||
createdAt: data.createdAt
|
|
||||||
};
|
|
||||||
// 获取已有记录并添加新记录
|
|
||||||
const existingGeneratorRecords = localStorage.getItem(recordName);
|
|
||||||
let generatorRecords = existingGeneratorRecords ? JSON.parse(existingGeneratorRecords) : [];
|
|
||||||
if (data.code) {
|
|
||||||
generatorRecords = generatorRecords.filter((item: ShareCodeResponse) => item.code !== data.code)
|
|
||||||
}
|
|
||||||
generatorRecords.unshift(newRecord); // 添加到最前面
|
|
||||||
localStorage.setItem(recordName, JSON.stringify(generatorRecords));
|
|
||||||
}
|
|
||||||
const handleValidSuccess = async (fileUrl: string, fileName: string) => {
|
|
||||||
setIsGetingFileId(true);
|
|
||||||
try {
|
|
||||||
const downloadUrl = `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${fileUrl}`;
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = downloadUrl;
|
|
||||||
link.download = fileName;
|
|
||||||
link.target = '_blank';
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
message.success('文件下载开始');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('下载失败:', error);
|
|
||||||
message.error('文件下载失败');
|
|
||||||
} finally {
|
|
||||||
setIsGetingFileId(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const { data: downloadResult, isLoading, refetch: refetchShareCodeWithResource } = api.shareCode.getFileByShareCode.useQuery(
|
|
||||||
{ code: downloadCode?.trim() },
|
|
||||||
{
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return <>
|
|
||||||
<QuickFileContext.Provider value={{
|
|
||||||
saveCodeRecord,
|
|
||||||
handleValidSuccess,
|
|
||||||
isGetingFileId,
|
|
||||||
downloadCode,
|
|
||||||
setDownloadCode,
|
|
||||||
refetchShareCodeWithResource,
|
|
||||||
isLoading,
|
|
||||||
downloadResult: downloadResult as any as ShareCodeResponse
|
|
||||||
}}>
|
|
||||||
{children}
|
|
||||||
</QuickFileContext.Provider>
|
|
||||||
</>
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useQuickFileContext = () => {
|
|
||||||
const context = useContext(QuickFileContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useQuickFileContext must be used within a QuickFileProvider");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
|
@ -1,68 +0,0 @@
|
||||||
.container {
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generateButton {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
background-color: #1890ff;
|
|
||||||
color: white;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generateButton:hover {
|
|
||||||
background-color: #40a9ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generateButton:disabled {
|
|
||||||
background-color: #d9d9d9;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.codeDisplay {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.codeWrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 12px;
|
|
||||||
margin: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: bold;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
color: #1890ff;
|
|
||||||
padding: 8px 16px;
|
|
||||||
background-color: #e6f7ff;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copyButton {
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #1890ff;
|
|
||||||
font-size: 18px;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expireInfo {
|
|
||||||
color: #666;
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hint {
|
|
||||||
color: #ff4d4f;
|
|
||||||
margin: 8px 0;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
|
||||||
font-size: 16px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { Button, Drawer, Empty } from "antd";
|
|
||||||
import { HistoryOutlined } from "@ant-design/icons";
|
|
||||||
import ShareCodeListCard from "../../code-manage/components/ShareCodeListCard";
|
|
||||||
import { ShareCodeResponse, useQuickFileContext } from "../../quick-file/quickFileContext";
|
|
||||||
|
|
||||||
export default function CodeRecord({ title, btnContent, recordName ,styles,isDownload}:
|
|
||||||
{ title: string, btnContent: string , recordName: string, styles?:string,isDownload?:boolean}) {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [records, setRecords] = useState<ShareCodeResponse[]>([]);
|
|
||||||
const {handleValidSuccess,saveCodeRecord,refetchShareCodeWithResource,setDownloadCode} = useQuickFileContext();
|
|
||||||
|
|
||||||
const loadRecordsFromStorage = () => {
|
|
||||||
const storedRecords = localStorage.getItem(recordName);
|
|
||||||
if (storedRecords) {
|
|
||||||
setRecords(JSON.parse(storedRecords));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadRecordsFromStorage();
|
|
||||||
}, [recordName]);
|
|
||||||
|
|
||||||
const saveAndUpdateRecord = (data: ShareCodeResponse, name: string) => {
|
|
||||||
saveCodeRecord(data, name);
|
|
||||||
if (name === recordName) {
|
|
||||||
loadRecordsFromStorage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOpen = () => {
|
|
||||||
loadRecordsFromStorage();
|
|
||||||
setOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
|
||||||
const updatedRecords = records.filter(record => record.id !== id);
|
|
||||||
setRecords(updatedRecords);
|
|
||||||
localStorage.setItem(recordName, JSON.stringify(updatedRecords));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDownload = async (code:string,recordName:string) => {
|
|
||||||
await setDownloadCode(code)
|
|
||||||
const {data:result} = await refetchShareCodeWithResource()
|
|
||||||
console.log('下载', result);
|
|
||||||
handleValidSuccess(result.resource.url, result.fileName);
|
|
||||||
saveAndUpdateRecord(result as any as ShareCodeResponse, 'shareCodeUploadRecords');
|
|
||||||
saveAndUpdateRecord(result as any as ShareCodeResponse, 'shareCodeDownloadRecords');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<HistoryOutlined />}
|
|
||||||
onClick={handleOpen}
|
|
||||||
style={{ marginBottom: '16px' }}
|
|
||||||
>
|
|
||||||
{btnContent}
|
|
||||||
</Button>
|
|
||||||
<Drawer
|
|
||||||
title={title}
|
|
||||||
placement="right"
|
|
||||||
width={420}
|
|
||||||
onClose={handleClose}
|
|
||||||
open={open}
|
|
||||||
>
|
|
||||||
{records.length > 0 ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{records.map(item => (
|
|
||||||
<ShareCodeListCard
|
|
||||||
key={item.id}
|
|
||||||
item={item}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
onDownload={() => handleDownload(item.code, recordName)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Empty description="暂无分享码记录" />
|
|
||||||
)}
|
|
||||||
</Drawer>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,203 +0,0 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { Button, DatePicker, Form, InputNumber, message } from 'antd';
|
|
||||||
import { CopyOutlined } from '@ant-design/icons';
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { getQueryKey } from '@trpc/react-query';
|
|
||||||
import { api } from '@nice/client';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import utc from 'dayjs/plugin/utc';
|
|
||||||
import timezone from 'dayjs/plugin/timezone';
|
|
||||||
import { ShareCodeResponse, useQuickFileContext } from '../quick-file/quickFileContext';
|
|
||||||
dayjs.extend(utc);
|
|
||||||
dayjs.extend(timezone);
|
|
||||||
interface ShareCodeGeneratorProps {
|
|
||||||
fileId: string;
|
|
||||||
onSuccess?: (code: string) => void;
|
|
||||||
fileName?: string;
|
|
||||||
fileMsg?: File;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function copyToClipboard(text) {
|
|
||||||
if (navigator.clipboard) {
|
|
||||||
return navigator.clipboard.writeText(text);
|
|
||||||
} else {
|
|
||||||
const textarea = document.createElement('textarea');
|
|
||||||
textarea.value = text;
|
|
||||||
document.body.appendChild(textarea);
|
|
||||||
textarea.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
document.body.removeChild(textarea);
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
|
|
||||||
fileId,
|
|
||||||
fileName,
|
|
||||||
fileMsg
|
|
||||||
}) => {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [shareCode, setShareCode] = useState<string>('');
|
|
||||||
const [expiresAt, setExpiresAt] = useState<string | null>(null);
|
|
||||||
const [canUseTimes, setCanUseTimes] = useState<number>(null);
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const queryKey = getQueryKey(api.shareCode);
|
|
||||||
const [isGenerate, setIsGenerate] = useState(false);
|
|
||||||
const [currentFileId, setCurrentFileId] = useState<string>('');
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const { saveCodeRecord } = useQuickFileContext();
|
|
||||||
const generateShareCode = api.shareCode.generateShareCodeByFileId.useMutation({
|
|
||||||
onSuccess: (data) => {
|
|
||||||
queryClient.invalidateQueries({ queryKey });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
useEffect(() => {
|
|
||||||
if (fileId !== currentFileId || !fileId) {
|
|
||||||
setIsGenerate(false);
|
|
||||||
}
|
|
||||||
setCurrentFileId(fileId);
|
|
||||||
}, [fileId])
|
|
||||||
const generateCode = async () => {
|
|
||||||
if (!fileId) {
|
|
||||||
message.error('请先上传文件');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLoading(true);
|
|
||||||
console.log('开始生成分享码,fileId:', fileId, 'fileName:', fileName);
|
|
||||||
try {
|
|
||||||
let expiresAt = form.getFieldsValue()?.expiresAt ? form.getFieldsValue().expiresAt.tz('Asia/Shanghai').toDate() : dayjs().add(1, 'day').tz('Asia/Shanghai').toDate();
|
|
||||||
if (fileMsg && fileMsg.size > 1024 * 1024 * 1024 * 5) {
|
|
||||||
message.info('文件大小超过5GB,系统将设置过期时间为1天', 6);
|
|
||||||
expiresAt = dayjs().add(1, 'day').tz('Asia/Shanghai').toDate();
|
|
||||||
}
|
|
||||||
const data: ShareCodeResponse = await generateShareCode.mutateAsync({
|
|
||||||
fileId,
|
|
||||||
expiresAt: expiresAt,
|
|
||||||
canUseTimes: form.getFieldsValue()?.canUseTimes ? form.getFieldsValue().canUseTimes : 10,
|
|
||||||
});
|
|
||||||
console.log('data', data)
|
|
||||||
setShareCode(data.code);
|
|
||||||
setIsGenerate(true);
|
|
||||||
setExpiresAt(dayjs(data.expiresAt).format('YYYY-MM-DD HH:mm:ss'));
|
|
||||||
setCanUseTimes(data.canUseTimes);
|
|
||||||
saveCodeRecord(data, 'shareCodeGeneratorRecords');
|
|
||||||
message.success('分享码生成成功' + data.code);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('生成分享码错误:', error);
|
|
||||||
message.error('生成分享码失败: ' + (error instanceof Error ? error.message : '未知错误'));
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopy = (code) => {
|
|
||||||
copyToClipboard(code)
|
|
||||||
.then(() => console.log('复制成功'))
|
|
||||||
.catch(() => console.error('复制失败'));
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
const date = dayjs().add(1, 'day').tz('Asia/Shanghai');
|
|
||||||
form.setFieldsValue({
|
|
||||||
expiresAt: date,
|
|
||||||
canUseTimes: 5
|
|
||||||
});
|
|
||||||
}, [form]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (fileId) {
|
|
||||||
generateCode()
|
|
||||||
}
|
|
||||||
}, [fileId])
|
|
||||||
return (
|
|
||||||
<div style={{ padding: '20px', backgroundColor: '#f8f9fa', borderRadius: '8px' }}>
|
|
||||||
<div style={{ marginBottom: '3px' }}>
|
|
||||||
<small style={{ color: '#666' }}>文件ID: {fileId ? fileId : '暂未上传文件'}</small>
|
|
||||||
</div>
|
|
||||||
{!isGenerate ? (
|
|
||||||
<>
|
|
||||||
<Form form={form}>
|
|
||||||
<div className='w-4/5 h-16 flex flex-row justify-between items-center'>
|
|
||||||
<small style={{ color: '#666' }}>
|
|
||||||
{"分享码的有效期"}
|
|
||||||
</small>
|
|
||||||
<Form.Item name="expiresAt" className='mt-5'>
|
|
||||||
<DatePicker
|
|
||||||
showTime
|
|
||||||
disabledDate={(current) =>
|
|
||||||
(current && current < dayjs().startOf('day')) ||
|
|
||||||
(current && current > dayjs().add(7, 'day').endOf('day'))
|
|
||||||
}
|
|
||||||
disabledTime={(current) => {
|
|
||||||
if (current && current.isSame(dayjs(), 'day')) {
|
|
||||||
return {
|
|
||||||
disabledHours: () => [...Array(dayjs().hour() + 1).keys()],
|
|
||||||
disabledMinutes: (selectedHour) => {
|
|
||||||
if (selectedHour === dayjs().hour()) {
|
|
||||||
return [...Array(dayjs().minute() + 1).keys()];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<small style={{ color: '#666' }}>
|
|
||||||
{"分享码的使用次数"}
|
|
||||||
</small>
|
|
||||||
<Form.Item name="canUseTimes" className='mt-5'>
|
|
||||||
<InputNumber
|
|
||||||
style={{ width: 120 }}
|
|
||||||
min={1}
|
|
||||||
max={10}
|
|
||||||
defaultValue={5}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</>
|
|
||||||
|
|
||||||
) : (
|
|
||||||
<div style={{ textAlign: 'center' }}>
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: '12px',
|
|
||||||
margin: '16px 0'
|
|
||||||
}}>
|
|
||||||
<span style={{
|
|
||||||
fontSize: '24px',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
letterSpacing: '2px',
|
|
||||||
color: '#1890ff',
|
|
||||||
padding: '8px 16px',
|
|
||||||
backgroundColor: '#e6f7ff',
|
|
||||||
borderRadius: '4px'
|
|
||||||
}}>
|
|
||||||
{shareCode}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
icon={<CopyOutlined />}
|
|
||||||
onClick={() => {
|
|
||||||
handleCopy(shareCode)
|
|
||||||
//navigator.clipboard.writeText(shareCode);
|
|
||||||
message.success('分享码已复制');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{isGenerate && expiresAt ? (
|
|
||||||
<div style={{ color: '#666' }}>
|
|
||||||
有效期至: {expiresAt} 可使用次数: {canUseTimes}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div style={{ color: 'red' }}>
|
|
||||||
未获取到有效期信息
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,102 +0,0 @@
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
|
||||||
import { Input, Button, message, Spin } from 'antd';
|
|
||||||
import styles from './ShareCodeValidator.module.css';
|
|
||||||
import { api } from '@nice/client';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import utc from 'dayjs/plugin/utc';
|
|
||||||
import timezone from 'dayjs/plugin/timezone';
|
|
||||||
import { env } from '@web/src/env';
|
|
||||||
import { copyToClipboard } from './sharecodegenerator';
|
|
||||||
import { ShareCodeResponse, useQuickFileContext } from '../quick-file/quickFileContext';
|
|
||||||
dayjs.extend(utc);
|
|
||||||
dayjs.extend(timezone);
|
|
||||||
export const ShareCodeValidator: React.FC = ({ }) => {
|
|
||||||
const { saveCodeRecord,handleValidSuccess,isGetingFileId,downloadCode,setDownloadCode,refetchShareCodeWithResource,isLoading,downloadResult } = useQuickFileContext();
|
|
||||||
|
|
||||||
const validateCode = useCallback(async () => {
|
|
||||||
|
|
||||||
if (!downloadCode?.trim()) {
|
|
||||||
message.warning('请输入正确的分享码');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { data: latestResult } = await refetchShareCodeWithResource();
|
|
||||||
console.log('验证分享码返回数据:', latestResult);
|
|
||||||
if (latestResult) {
|
|
||||||
saveCodeRecord(latestResult as any as ShareCodeResponse,'shareCodeDownloadRecords')
|
|
||||||
handleValidSuccess(latestResult.resource.url, latestResult.fileName);
|
|
||||||
} else {
|
|
||||||
message.error('分享码无效或已过期');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('验证分享码失败:', error);
|
|
||||||
message.error('分享码无效或已过期');
|
|
||||||
}
|
|
||||||
}, [refetchShareCodeWithResource, downloadCode, handleValidSuccess]);
|
|
||||||
const getDownloadUrl = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const { data: latestResult } = await refetchShareCodeWithResource();
|
|
||||||
saveCodeRecord(latestResult as any as ShareCodeResponse,'shareCodeDownloadRecords')
|
|
||||||
console.log('验证分享码返回数据:', latestResult);
|
|
||||||
const downloadUrl = `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${latestResult.resource.url}`;
|
|
||||||
copyToClipboard(downloadUrl)
|
|
||||||
.then(() => {
|
|
||||||
message.success(`${latestResult.fileName}的下载链接已复制`, 6)
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
message.error('复制失败')
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('验证分享码失败:', error);
|
|
||||||
message.error('分享码无效或已过期');
|
|
||||||
}
|
|
||||||
}, [refetchShareCodeWithResource, downloadCode, handleValidSuccess]);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{
|
|
||||||
isGetingFileId ?
|
|
||||||
(<Spin spinning={isGetingFileId} fullscreen />) :
|
|
||||||
(
|
|
||||||
<>
|
|
||||||
<div className={styles.container}>
|
|
||||||
<Input
|
|
||||||
className={styles.input}
|
|
||||||
value={downloadCode}
|
|
||||||
onChange={(e) => setDownloadCode(e.target.value.toUpperCase())}
|
|
||||||
placeholder="请输入分享码"
|
|
||||||
maxLength={8}
|
|
||||||
onPressEnter={validateCode}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={getDownloadUrl}
|
|
||||||
loading={isLoading}
|
|
||||||
disabled={!downloadCode?.trim()}
|
|
||||||
>
|
|
||||||
获取下载链接
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={validateCode}
|
|
||||||
loading={isLoading}
|
|
||||||
disabled={!downloadCode?.trim()}
|
|
||||||
>
|
|
||||||
下载
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{
|
|
||||||
!isLoading && downloadResult && (
|
|
||||||
<div className='w-full flex justify-between my-2 p-1 antialiased text-secondary-600'>
|
|
||||||
<span >{`分享码:${downloadResult?.code ? downloadResult.code : ''}`}</span>
|
|
||||||
<span >{`文件名:${downloadResult?.fileName ? downloadResult.fileName : ''}`}</span>
|
|
||||||
<span >{`过期时间:${downloadResult?.expiresAt ? dayjs(downloadResult.expiresAt).format('YYYY-MM-DD HH:mm:ss') : ''}`}</span>
|
|
||||||
<span >{`剩余使用次数:${downloadResult?.canUseTimes ? downloadResult.canUseTimes : 0}`}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,21 +1,10 @@
|
||||||
import {
|
import {
|
||||||
createBrowserRouter,
|
createBrowserRouter,
|
||||||
IndexRouteObject,
|
IndexRouteObject,
|
||||||
Link,
|
|
||||||
Navigate,
|
|
||||||
NonIndexRouteObject,
|
NonIndexRouteObject,
|
||||||
useParams,
|
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import ErrorPage from "../app/error";
|
import ErrorPage from "../app/error";
|
||||||
import LoginPage from "../app/login";
|
import LoginPage from "../app/login";
|
||||||
import WithAuth from "../components/utils/with-auth";
|
|
||||||
import { CodeManageProvider } from "../app/admin/code-manage/CodeManageContext";
|
|
||||||
import CodeManageLayout from "../app/admin/code-manage/CodeManageLayout";
|
|
||||||
import QuickUploadPage from "../app/admin/quick-file/page";
|
|
||||||
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;
|
||||||
|
@ -39,11 +28,7 @@ export type CustomRouteObject =
|
||||||
export const routes: CustomRouteObject[] = [
|
export const routes: CustomRouteObject[] = [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
element: <>
|
element: <></>,
|
||||||
<QuickFileProvider>
|
|
||||||
<QuickUploadPage></QuickUploadPage>
|
|
||||||
</QuickFileProvider>
|
|
||||||
</>,
|
|
||||||
errorElement: <ErrorPage />,
|
errorElement: <ErrorPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -51,46 +36,7 @@ export const routes: CustomRouteObject[] = [
|
||||||
breadcrumb: "登录",
|
breadcrumb: "登录",
|
||||||
element: <LoginPage></LoginPage>,
|
element: <LoginPage></LoginPage>,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/code-manage",
|
|
||||||
element: <>
|
|
||||||
<WithAuth>
|
|
||||||
<CodeManageProvider>
|
|
||||||
<CodeManageLayout></CodeManageLayout>
|
|
||||||
</CodeManageProvider>
|
|
||||||
</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);
|
||||||
|
|
|
@ -2,7 +2,7 @@ server {
|
||||||
# 监听80端口
|
# 监听80端口
|
||||||
listen 80;
|
listen 80;
|
||||||
# 服务器域名/IP地址,使用环境变量
|
# 服务器域名/IP地址,使用环境变量
|
||||||
server_name 192.168.43.206;
|
server_name 192.168.252.77;
|
||||||
|
|
||||||
# 基础性能优化配置
|
# 基础性能优化配置
|
||||||
# 启用tcp_nopush以优化数据发送
|
# 启用tcp_nopush以优化数据发送
|
||||||
|
@ -100,7 +100,7 @@ server {
|
||||||
# 仅供内部使用
|
# 仅供内部使用
|
||||||
internal;
|
internal;
|
||||||
# 代理到认证服务
|
# 代理到认证服务
|
||||||
proxy_pass http://192.168.43.206:3006/auth/file;
|
proxy_pass http://192.168.252.77:3000/auth/file;
|
||||||
|
|
||||||
# 请求优化:不传递请求体
|
# 请求优化:不传递请求体
|
||||||
proxy_pass_request_body off;
|
proxy_pass_request_body off;
|
||||||
|
|
|
@ -6,7 +6,6 @@ export * from "./useRole"
|
||||||
export * from "./useRoleMap"
|
export * from "./useRoleMap"
|
||||||
export * from "./useTransform"
|
export * from "./useTransform"
|
||||||
export * from "./useTaxonomy"
|
export * from "./useTaxonomy"
|
||||||
export * from "./useVisitor"
|
|
||||||
export * from "./useMessage"
|
export * from "./useMessage"
|
||||||
export * from "./usePost"
|
export * from "./usePost"
|
||||||
export * from "./useEntity"
|
export * from "./useEntity"
|
||||||
|
|
|
@ -1,178 +0,0 @@
|
||||||
import { api } from "../trpc";
|
|
||||||
import { PostParams } from "../../singleton/DataHolder";
|
|
||||||
|
|
||||||
export function useVisitor() {
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const postParams = PostParams.getInstance();
|
|
||||||
|
|
||||||
const create = api.visitor.create.useMutation({
|
|
||||||
onSuccess() {
|
|
||||||
utils.visitor.invalidate();
|
|
||||||
|
|
||||||
// utils.post.invalidate();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
/**
|
|
||||||
* 通用的乐观更新mutation工厂函数
|
|
||||||
* @param updateFn 更新数据的具体逻辑函数
|
|
||||||
* @returns 封装后的mutation配置对象
|
|
||||||
*/
|
|
||||||
const createOptimisticMutation = (
|
|
||||||
updateFn: (item: any, variables: any) => any
|
|
||||||
) => ({
|
|
||||||
//在请求发送前执行本地数据预更新
|
|
||||||
onMutate: async (variables: any) => {
|
|
||||||
const previousDataList: any[] = [];
|
|
||||||
const previousDetailDataList: any[] = [];
|
|
||||||
|
|
||||||
// 处理列表数据
|
|
||||||
const paramsList = postParams.getItems();
|
|
||||||
for (const params of paramsList) {
|
|
||||||
await utils.post.findManyWithCursor.cancel();
|
|
||||||
const previousData =
|
|
||||||
utils.post.findManyWithCursor.getInfiniteData({
|
|
||||||
...params,
|
|
||||||
});
|
|
||||||
previousDataList.push(previousData);
|
|
||||||
utils.post.findManyWithCursor.setInfiniteData(
|
|
||||||
{
|
|
||||||
...params,
|
|
||||||
},
|
|
||||||
(oldData) => {
|
|
||||||
if (!oldData) return oldData;
|
|
||||||
return {
|
|
||||||
...oldData,
|
|
||||||
pages: oldData.pages.map((page) => ({
|
|
||||||
...page,
|
|
||||||
items: (page.items as any).map((item) =>
|
|
||||||
item.id === variables?.postId
|
|
||||||
? updateFn(item, variables)
|
|
||||||
: item
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理详情数据
|
|
||||||
const detailParamsList = postParams.getDetailItems();
|
|
||||||
for (const params of detailParamsList) {
|
|
||||||
await utils.post.findFirst.cancel();
|
|
||||||
const previousDetailData = utils.post.findFirst.getData(params);
|
|
||||||
previousDetailDataList.push(previousDetailData);
|
|
||||||
utils.post.findFirst.setData(params, (oldData) => {
|
|
||||||
if (!oldData) return oldData;
|
|
||||||
return oldData.id === variables?.postId
|
|
||||||
? updateFn(oldData, variables)
|
|
||||||
: oldData;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { previousDataList, previousDetailDataList };
|
|
||||||
},
|
|
||||||
// 错误处理:数据回滚
|
|
||||||
onError: (_err: any, _variables: any, context: any) => {
|
|
||||||
const paramsList = postParams.getItems();
|
|
||||||
paramsList.forEach((params, index) => {
|
|
||||||
if (context?.previousDataList?.[index]) {
|
|
||||||
utils.post.findManyWithCursor.setInfiniteData(
|
|
||||||
{ ...params },
|
|
||||||
context.previousDataList[index]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// 成功后的缓存失效
|
|
||||||
onSuccess: async (_: any, variables: any) => {
|
|
||||||
await Promise.all([
|
|
||||||
utils.visitor.invalidate(),
|
|
||||||
utils.post.findFirst.invalidate({
|
|
||||||
where: {
|
|
||||||
id: (variables as any)?.postId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
utils.post.findManyWithCursor.invalidate(),
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// 定义具体的mutation
|
|
||||||
const read = api.visitor.create.useMutation(
|
|
||||||
createOptimisticMutation((item) => ({
|
|
||||||
...item,
|
|
||||||
views: (item.views || 0) + 1,
|
|
||||||
readed: true,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
const like = api.visitor.create.useMutation(
|
|
||||||
createOptimisticMutation((item) => ({
|
|
||||||
...item,
|
|
||||||
likes: (item.likes || 0) + 1,
|
|
||||||
liked: true,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
const unLike = api.visitor.deleteMany.useMutation(
|
|
||||||
createOptimisticMutation((item) => ({
|
|
||||||
...item,
|
|
||||||
likes: item.likes - 1 || 0,
|
|
||||||
liked: false,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
const hate = api.visitor.create.useMutation(
|
|
||||||
createOptimisticMutation((item) => ({
|
|
||||||
...item,
|
|
||||||
hates: (item.hates || 0) + 1,
|
|
||||||
hated: true,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
const unHate = api.visitor.deleteMany.useMutation(
|
|
||||||
createOptimisticMutation((item) => ({
|
|
||||||
...item,
|
|
||||||
hates: item.hates - 1 || 0,
|
|
||||||
hated: false,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
const addStar = api.visitor.create.useMutation(
|
|
||||||
createOptimisticMutation((item) => ({
|
|
||||||
...item,
|
|
||||||
star: true,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
const deleteStar = api.visitor.deleteMany.useMutation(
|
|
||||||
createOptimisticMutation((item) => ({
|
|
||||||
...item,
|
|
||||||
star: false,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
const deleteMany = api.visitor.deleteMany.useMutation({
|
|
||||||
onSuccess() {
|
|
||||||
utils.visitor.invalidate();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const createMany = api.visitor.createMany.useMutation({
|
|
||||||
onSuccess() {
|
|
||||||
utils.visitor.invalidate();
|
|
||||||
utils.message.invalidate();
|
|
||||||
utils.post.invalidate();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
postParams,
|
|
||||||
create,
|
|
||||||
createMany,
|
|
||||||
deleteMany,
|
|
||||||
read,
|
|
||||||
addStar,
|
|
||||||
deleteStar,
|
|
||||||
like,
|
|
||||||
unLike,
|
|
||||||
hate,
|
|
||||||
unHate,
|
|
||||||
};
|
|
||||||
}
|
|
Loading…
Reference in New Issue