This commit is contained in:
Rao 2025-04-21 22:52:10 +08:00
parent b9426cd0ea
commit 6c26d26c2a
33 changed files with 4 additions and 2733 deletions

View File

@ -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 { }

View File

@ -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();
})
});
}

View File

@ -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 }
}
}

View File

@ -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 { }

View File

@ -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);
}),
});
}

View File

@ -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;
}
}

View File

@ -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],

View File

@ -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;

View File

@ -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;
};

View File

@ -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>
)
}

View File

@ -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>
);
}

View File

@ -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>
</>
}

View File

@ -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>
</>
}

View File

@ -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;

View File

@ -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>
}

View File

@ -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;
};

View File

@ -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>
);
}

View File

@ -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>
</>
}

View File

@ -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>
);
}

View File

@ -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>
</>
}

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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>
)
}

View File

@ -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;
};

View File

@ -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;
}

View File

@ -1,12 +0,0 @@
.container {
display: flex;
gap: 12px;
padding: 20px;
border-radius: 8px;
background-color: #f8f9fa;
}
.input {
font-size: 16px;
text-transform: uppercase;
}

View File

@ -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>
</>
);
}

View File

@ -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>
);
};

View File

@ -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>
)
}
</>
)
}
</>
);
};

View File

@ -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);

View File

@ -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;

View File

@ -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"

View File

@ -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,
};
}