rht
This commit is contained in:
parent
34dde48aaf
commit
9f1bc38acd
|
@ -4,6 +4,7 @@ import { TrpcService } from "@server/trpc/trpc.service";
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma } from "@nice/common";
|
||||
const ShareCodeWhereInputSchema: ZodType<Prisma.ShareCodeWhereInput> = z.any();
|
||||
const ShareCodeFindManyArgsSchema: ZodType<Prisma.ShareCodeFindManyArgs> = z.any();
|
||||
@Injectable()
|
||||
export class ShareCodeRouter {
|
||||
constructor(
|
||||
|
@ -12,31 +13,6 @@ export class ShareCodeRouter {
|
|||
) { }
|
||||
|
||||
router = this.trpc.router({
|
||||
generateShareCode: this.trpc.procedure
|
||||
.input(z.object({ fileId: z.string(), expiresAt: z.date(), canUseTimes: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
return this.shareCodeService.generateShareCode(input.fileId, input.expiresAt, input.canUseTimes);
|
||||
}),
|
||||
validateAndUseCode: this.trpc.procedure
|
||||
.input(z.object({ code: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
return this.shareCodeService.validateAndUseCode(input.code);
|
||||
}),
|
||||
getShareCodeInfo: this.trpc.procedure
|
||||
.input(z.object({ code: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
return this.shareCodeService.getShareCodeInfo(input.code);
|
||||
}),
|
||||
hasActiveShareCode: this.trpc.procedure
|
||||
.input(z.object({ fileId: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
return this.shareCodeService.hasActiveShareCode(input.fileId);
|
||||
}),
|
||||
getFileShareHistory: this.trpc.procedure
|
||||
.input(z.object({ fileId: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
return this.shareCodeService.getFileShareHistory(input.fileId);
|
||||
}),
|
||||
getFileByShareCode: this.trpc.procedure
|
||||
.input(z.object({ code: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
|
@ -44,8 +20,8 @@ export class ShareCodeRouter {
|
|||
}),
|
||||
generateShareCodeByFileId: this.trpc.procedure
|
||||
.input(z.object({ fileId: z.string(), expiresAt: z.date(), canUseTimes: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
return this.shareCodeService.generateShareCodeByFileId(input.fileId, input.expiresAt, input.canUseTimes);
|
||||
.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() }))
|
||||
|
@ -68,5 +44,23 @@ export class ShareCodeRouter {
|
|||
.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);
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -9,7 +9,6 @@ import dayjs from 'dayjs';
|
|||
import utc from 'dayjs/plugin/utc';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import { BaseService } from '../base/base.service';
|
||||
import { url } from 'inspector';
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
export interface ShareCode {
|
||||
|
@ -23,6 +22,7 @@ export interface ShareCode {
|
|||
canUseTimes: number | null;
|
||||
}
|
||||
export interface GenerateShareCodeResponse {
|
||||
id?: string;
|
||||
code: string;
|
||||
expiresAt: Date;
|
||||
canUseTimes: number;
|
||||
|
@ -33,6 +33,8 @@ export interface GenerateShareCodeResponse {
|
|||
url: string;
|
||||
meta: ResourceMeta
|
||||
}
|
||||
uploadIp?: string;
|
||||
createdAt?: Date;
|
||||
}
|
||||
|
||||
interface ResourceMeta {
|
||||
|
@ -41,6 +43,25 @@ interface ResourceMeta {
|
|||
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);
|
||||
|
@ -53,12 +74,26 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
|
|||
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 {
|
||||
// 检查文件是否存在
|
||||
|
@ -87,6 +122,7 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
|
|||
canUseTimes,
|
||||
isUsed: false,
|
||||
fileName: filename || "downloaded_file",
|
||||
uploadIp,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
|
@ -99,6 +135,7 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
|
|||
canUseTimes,
|
||||
isUsed: false,
|
||||
fileName: filename || "downloaded_file",
|
||||
uploadIp,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -130,7 +167,7 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
|
|||
this.logger.log(`尝试验证分享码: ${code}`);
|
||||
|
||||
// 查找有效的分享码
|
||||
const shareCode = await db.shareCode.findFirst({
|
||||
const shareCode = await super.findFirst({
|
||||
where: {
|
||||
code,
|
||||
isUsed: false,
|
||||
|
@ -142,7 +179,7 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
|
|||
return null;
|
||||
}
|
||||
//更新已使用次数
|
||||
await db.shareCode.update({
|
||||
await super.update({
|
||||
where: { id: shareCode.id },
|
||||
data: { canUseTimes: shareCode.canUseTimes - 1 },
|
||||
});
|
||||
|
@ -167,7 +204,7 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
|
|||
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
|
||||
async cleanupExpiredShareCodes() {
|
||||
try {
|
||||
const shareCodes = await db.shareCode.findMany({
|
||||
const shareCodes = await super.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ expiresAt: { lt: dayjs().tz('Asia/Shanghai').toDate() } },
|
||||
|
@ -180,7 +217,7 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
|
|||
shareCodes.forEach(code => {
|
||||
this.cleanupUploadFolder(code.fileId);
|
||||
})
|
||||
const result = await db.shareCode.deleteMany({
|
||||
const result = await super.deleteMany({
|
||||
where: {
|
||||
fileId: {
|
||||
in: shareCodes.map(code => code.fileId)
|
||||
|
@ -235,48 +272,6 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
|
|||
this.logger.log(`Deleted folder: ${dirPath}`);
|
||||
}
|
||||
}
|
||||
// 获取分享码信息
|
||||
async getShareCodeInfo(code: string): Promise<ShareCode | null> {
|
||||
try {
|
||||
return await db.shareCode.findFirst({
|
||||
where: { code },
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to get share code info', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查文件是否已经生成过分享码
|
||||
async hasActiveShareCode(fileId: string): Promise<boolean> {
|
||||
try {
|
||||
const activeCode = await db.shareCode.findFirst({
|
||||
where: {
|
||||
fileId,
|
||||
isUsed: false,
|
||||
expiresAt: { gt: dayjs().tz('Asia/Shanghai').toDate() },
|
||||
},
|
||||
});
|
||||
|
||||
return !!activeCode;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to check active share code', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取文件的所有分享记录
|
||||
async getFileShareHistory(fileId: string) {
|
||||
try {
|
||||
return await db.shareCode.findMany({
|
||||
where: { fileId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to get file share history', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 根据分享码获取文件
|
||||
async getFileByShareCode(code: string) {
|
||||
|
@ -320,18 +315,6 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
|
|||
return response;
|
||||
}
|
||||
|
||||
// 根据文件ID生成分享码
|
||||
async generateShareCodeByFileId(fileId: string, expiresAt: Date, canUseTimes: number) {
|
||||
try {
|
||||
this.logger.log('收到生成分享码请求,fileId:', fileId);
|
||||
const result = await this.generateShareCode(fileId, expiresAt, canUseTimes);
|
||||
this.logger.log('生成分享码结果:', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error('生成分享码错误:', error);
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
async getShareCodesWithResources(args: {
|
||||
page?: number;
|
||||
|
@ -345,24 +328,8 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
|
|||
// 使用include直接关联查询Resource
|
||||
const { items, totalPages } = await super.findManyWithPagination({
|
||||
...args,
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
fileId: true,
|
||||
expiresAt: true,
|
||||
fileName: true,
|
||||
canUseTimes: true,
|
||||
resource: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
url: true,
|
||||
meta: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
select: ShareCodeSelect
|
||||
});
|
||||
this.logger.log('search result:', items);
|
||||
return {
|
||||
items,
|
||||
totalPages
|
||||
|
@ -394,4 +361,231 @@ export class ShareCodeService extends BaseService<Prisma.ShareCodeDelegate> {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有分享码关联资源的总大小
|
||||
* @returns 返回所有资源的总大小和资源数量
|
||||
*/
|
||||
async getShareCodeResourcesTotalSize(): Promise<{ totalSize: number; resourceCount: number }> {
|
||||
try {
|
||||
this.logger.log('获取所有分享码关联资源的总大小');
|
||||
|
||||
// 查询所有有效的分享码及其关联的资源
|
||||
const shareCodes = await super.findMany({
|
||||
where: {
|
||||
deletedAt: null
|
||||
},
|
||||
select: { ...ShareCodeSelect }
|
||||
});
|
||||
|
||||
// 计算总大小和资源数量
|
||||
let totalSize = 0;
|
||||
let resourceCount = 0;
|
||||
|
||||
shareCodes.forEach(shareCode => {
|
||||
|
||||
if ((shareCode as any as GenerateShareCodeResponse).resource && (shareCode as any as GenerateShareCodeResponse).resource.meta) {
|
||||
const meta = (shareCode as any as GenerateShareCodeResponse).resource.meta as any;
|
||||
if (meta.size) {
|
||||
// 如果size是字符串格式(如 "1024"或"1 MB"),需要转换
|
||||
let sizeValue: number;
|
||||
if (typeof meta.size === 'string') {
|
||||
// 尝试直接解析数字
|
||||
sizeValue = parseInt(meta.size, 10);
|
||||
// 如果解析失败,可能需要更复杂的处理
|
||||
if (isNaN(sizeValue)) {
|
||||
// 简单处理,实际应用中可能需要更复杂的单位转换
|
||||
this.logger.warn(`无法解析资源大小: ${meta.size}`);
|
||||
sizeValue = 0;
|
||||
}
|
||||
} else if (typeof meta.size === 'number') {
|
||||
sizeValue = meta.size;
|
||||
} else {
|
||||
sizeValue = 0;
|
||||
}
|
||||
|
||||
totalSize += sizeValue;
|
||||
resourceCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.logger.log(`资源总大小: ${totalSize}, 资源数量: ${resourceCount}`);
|
||||
return { totalSize, resourceCount };
|
||||
} catch (error) {
|
||||
this.logger.error('获取分享码资源总大小失败', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 根据日期范围获取分享码关联资源的大小
|
||||
* @param dateType 日期类型: 'today' 或 'yesterday'
|
||||
* @returns 返回特定日期范围内的资源总大小和资源数量
|
||||
*/
|
||||
async getShareCodeResourcesSizeByDateRange(dateType: 'today' | 'yesterday'): Promise<{ totalSize: number; resourceCount: number }> {
|
||||
try {
|
||||
let startDate: Date;
|
||||
let endDate: Date;
|
||||
const now = dayjs().tz('Asia/Shanghai');
|
||||
|
||||
if (dateType === 'today') {
|
||||
startDate = now.startOf('day').toDate();
|
||||
endDate = now.endOf('day').toDate();
|
||||
this.logger.log(`获取今天创建的分享码资源大小, 日期范围: ${startDate} 到 ${endDate}`);
|
||||
} else {
|
||||
startDate = now.subtract(1, 'day').startOf('day').toDate();
|
||||
endDate = now.subtract(1, 'day').endOf('day').toDate();
|
||||
this.logger.log(`获取昨天创建的分享码资源大小, 日期范围: ${startDate} 到 ${endDate}`);
|
||||
}
|
||||
|
||||
// 查询特定日期范围内创建的分享码及其关联的资源
|
||||
const shareCodes = await super.findMany({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: startDate,
|
||||
lte: endDate
|
||||
},
|
||||
deletedAt: null
|
||||
},
|
||||
select: {
|
||||
...ShareCodeSelect
|
||||
}
|
||||
});
|
||||
|
||||
// 计算总大小和资源数量
|
||||
let totalSize = 0;
|
||||
let resourceCount = 0;
|
||||
|
||||
shareCodes.forEach(shareCode => {
|
||||
if ((shareCode as any as GenerateShareCodeResponse).resource && (shareCode as any as GenerateShareCodeResponse).resource.meta) {
|
||||
const meta = (shareCode as any as GenerateShareCodeResponse).resource.meta as any;
|
||||
if (meta.size) {
|
||||
// 如果size是字符串格式(如 "1024"或"1 MB"),需要转换
|
||||
let sizeValue: number;
|
||||
if (typeof meta.size === 'string') {
|
||||
// 尝试直接解析数字
|
||||
sizeValue = parseInt(meta.size, 10);
|
||||
// 如果解析失败,可能需要更复杂的处理
|
||||
if (isNaN(sizeValue)) {
|
||||
// 简单处理,实际应用中可能需要更复杂的单位转换
|
||||
this.logger.warn(`无法解析资源大小: ${meta.size}`);
|
||||
sizeValue = 0;
|
||||
}
|
||||
} else if (typeof meta.size === 'number') {
|
||||
sizeValue = meta.size;
|
||||
} else {
|
||||
sizeValue = 0;
|
||||
}
|
||||
|
||||
totalSize += sizeValue;
|
||||
resourceCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.logger.log(`${dateType}资源总大小: ${totalSize}, 资源数量: ${resourceCount}`);
|
||||
return { totalSize, resourceCount };
|
||||
} catch (error) {
|
||||
this.logger.error(`获取${dateType === 'today' ? '今天' : '昨天'}的分享码资源大小失败`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 统计不同时间段内独立uploadIp的数量
|
||||
* @returns 返回本周和上周的不同uploadIp数量
|
||||
*/
|
||||
async countDistinctUploadIPs(): Promise<{ thisWeek: number; lastWeek: number; all: number }> {
|
||||
try {
|
||||
const now = dayjs().tz('Asia/Shanghai');
|
||||
|
||||
// 本周的开始和结束
|
||||
const thisWeekStart = now.startOf('week').toDate();
|
||||
const thisWeekEnd = now.endOf('week').toDate();
|
||||
|
||||
// 上周的开始和结束
|
||||
const lastWeekStart = now.subtract(1, 'week').startOf('week').toDate();
|
||||
const lastWeekEnd = now.subtract(1, 'week').endOf('week').toDate();
|
||||
|
||||
this.logger.log(`统计本周IP数量, 日期范围: ${thisWeekStart} 到 ${thisWeekEnd}`);
|
||||
this.logger.log(`统计上周IP数量, 日期范围: ${lastWeekStart} 到 ${lastWeekEnd}`);
|
||||
// 查询所有不同IP
|
||||
const allIPs = await super.findMany({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
uploadIp: {
|
||||
not: null
|
||||
}
|
||||
},
|
||||
select: {
|
||||
uploadIp: true
|
||||
},
|
||||
distinct: ['uploadIp']
|
||||
});
|
||||
// 查询本周的不同IP
|
||||
const thisWeekIPs = await super.findMany({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: thisWeekStart,
|
||||
lte: thisWeekEnd
|
||||
},
|
||||
uploadIp: {
|
||||
not: null
|
||||
},
|
||||
deletedAt: null
|
||||
},
|
||||
select: {
|
||||
uploadIp: true
|
||||
},
|
||||
distinct: ['uploadIp']
|
||||
});
|
||||
|
||||
// 查询上周的不同IP
|
||||
const lastWeekIPs = await super.findMany({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: lastWeekStart,
|
||||
lte: lastWeekEnd
|
||||
},
|
||||
uploadIp: {
|
||||
not: null
|
||||
},
|
||||
deletedAt: null
|
||||
},
|
||||
select: {
|
||||
uploadIp: true
|
||||
},
|
||||
distinct: ['uploadIp']
|
||||
});
|
||||
|
||||
const thisWeekCount = thisWeekIPs.length;
|
||||
const lastWeekCount = lastWeekIPs.length;
|
||||
const allCount = allIPs.length;
|
||||
|
||||
this.logger.log(`本周不同IP数量: ${thisWeekCount}, 上周不同IP数量: ${lastWeekCount}, 所有不同IP数量: ${allCount}`);
|
||||
|
||||
return { thisWeek: thisWeekCount, lastWeek: lastWeekCount, all: allCount };
|
||||
} catch (error) {
|
||||
this.logger.error('统计不同uploadIp数量失败', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分享码列表,使用ShareCodeSelect并按创建时间倒序排序
|
||||
* @param args 查询参数
|
||||
* @returns 返回分享码列表
|
||||
*/
|
||||
async findShareCodes(args?: Omit<Prisma.ShareCodeFindManyArgs, 'select' | 'orderBy'>): Promise<GenerateShareCodeResponse[]> {
|
||||
try {
|
||||
const result = await super.findMany({
|
||||
...args,
|
||||
select: ShareCodeSelect,
|
||||
});
|
||||
this.logger.log(`获取分享码列表成功, 数量: ${result.length}`);
|
||||
return result as unknown as GenerateShareCodeResponse[];
|
||||
} catch (error) {
|
||||
this.logger.error('获取分享码列表失败', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,8 @@ interface CodeManageContextType {
|
|||
searchRefetch: () => void,
|
||||
setSearchKeyword: (keyword: string) => void,
|
||||
currentCode: string | null,
|
||||
setCurrentCode: (code: string) => void
|
||||
setCurrentCode: (code: string) => void,
|
||||
searchKeyword: string
|
||||
}
|
||||
|
||||
interface ShareCodeWithResource {
|
||||
|
@ -97,7 +98,8 @@ export const CodeManageProvider = ({ children }: { children: React.ReactNode })
|
|||
searchRefetch,
|
||||
setSearchKeyword,
|
||||
currentCode,
|
||||
setCurrentCode
|
||||
setCurrentCode,
|
||||
searchKeyword
|
||||
}}>
|
||||
{children}
|
||||
</CodeManageContext.Provider>
|
||||
|
|
|
@ -1,9 +1,25 @@
|
|||
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></CodeManageSearchBase>
|
||||
<CodeManageSearchBase keyword={localKeyword}></CodeManageSearchBase>
|
||||
<CodeManageDisplay></CodeManageDisplay>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,16 +1,26 @@
|
|||
import { Button, Form, Input } from 'antd';
|
||||
import { useCodeManageContext } from '../CodeManageContext';
|
||||
import { ChangeEvent, useRef } from 'react';
|
||||
import { ChangeEvent, useEffect, useRef } from 'react';
|
||||
|
||||
export default function CodeManageSearchBase() {
|
||||
const { setCurrentPage, searchRefetch, setSearchKeyword } = useCodeManageContext();
|
||||
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]; // 获取表单实例
|
||||
|
||||
// 当 keyword 属性变化时更新表单值
|
||||
useEffect(() => {
|
||||
if (keyword) {
|
||||
formRef.setFieldsValue({ search: keyword });
|
||||
}
|
||||
}, [keyword, formRef]);
|
||||
|
||||
const onSearch = (value: string) => {
|
||||
console.log(value);
|
||||
setSearchKeyword(value);
|
||||
setCurrentPage(1)
|
||||
searchRefetch()
|
||||
};
|
||||
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// 设置表单值
|
||||
setSearchKeyword(e.target.value);
|
||||
|
@ -25,13 +35,15 @@ export default function CodeManageSearchBase() {
|
|||
searchRefetch();
|
||||
}, 300); // 300毫秒的防抖延迟
|
||||
};
|
||||
|
||||
return <>
|
||||
<div className="py-4 mt-10 w-2/3 mx-auto">
|
||||
<Form>
|
||||
<div className="py-4 mt-2 w-2/3 mx-auto">
|
||||
<Form form={formRef}>
|
||||
<Form.Item name="search" label="关键字搜索">
|
||||
<Input.Search
|
||||
placeholder="输入分享码或文件名"
|
||||
enterButton
|
||||
value={searchKeyword}
|
||||
onSearch={onSearch}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
|
|
@ -51,7 +51,7 @@ export default function CodeMangeDisplay() {
|
|||
console.log('currentShareCodes:', currentShareCodes);
|
||||
}, [currentShareCodes]);
|
||||
return <>
|
||||
<div className="w-full min-h-[550px] mx-auto">
|
||||
<div className="w-full min-h-[720px] mx-auto">
|
||||
<ShareCodeList
|
||||
data={currentShareCodes?.items}
|
||||
loading={isLoading}
|
||||
|
|
|
@ -17,7 +17,7 @@ export default function ShareCodeListCard({ item, onEdit, onDelete, styles, onDo
|
|||
}, [item]);
|
||||
return <div className={`${styles}`}>
|
||||
<Card
|
||||
className="shadow-md hover:shadow-lg transition-shadow duration-300 space-x-4"
|
||||
className="shadow-md hover:shadow-lg h-[344px] transition-shadow duration-300 space-x-4"
|
||||
title={
|
||||
<Typography.Text
|
||||
strong
|
||||
|
@ -53,18 +53,26 @@ export default function ShareCodeListCard({ item, onEdit, onDelete, styles, onDo
|
|||
].filter(Boolean)}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<p className="text-gray-700">
|
||||
<p className="text-gray-700 whitespace-nowrap overflow-hidden text-overflow-ellipsis hover:overflow-auto">
|
||||
<span className="font-medium">文件名:</span> {item.fileName}
|
||||
</p>
|
||||
<p className="text-gray-700">
|
||||
<p className="text-gray-700 whitespace-nowrap overflow-hidden text-overflow-ellipsis hover:overflow-auto">
|
||||
<span className="font-medium">文件大小:</span> {Math.max(0.01, (Number(item?.resource?.meta?.size) / 1024 / 1024)).toFixed(2)} MB
|
||||
</p>
|
||||
<p className="text-gray-700 ">
|
||||
<span className="font-medium">过期时间:</span> {dayjs(item.expiresAt).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</p>
|
||||
<p className="text-gray-700">
|
||||
|
||||
<p className="text-gray-700 whitespace-nowrap overflow-hidden text-overflow-ellipsis hover:overflow-auto">
|
||||
<span className="font-medium">剩余使用次数:</span> {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>
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
import { api } from "@nice/client";
|
||||
import { createContext, useContext, useState } from "react";
|
||||
import { ShareCodeResponse } from "../quick-file/quickFileContext";
|
||||
interface DashboardContextType {
|
||||
shareCodeAll: ShareCodeResourcesSizeByDateRange;
|
||||
shareCodeToday: ShareCodeResourcesSizeByDateRange;
|
||||
shareCodeYesterday: ShareCodeResourcesSizeByDateRange;
|
||||
isShareCodeAllLoading: boolean;
|
||||
isShareCodeTodayLoading: boolean;
|
||||
isShareCodeYesterdayLoading: boolean;
|
||||
distinctUploadIPs: { thisWeek: number; lastWeek: number; all: number };
|
||||
isDistinctUploadIPsLoading: boolean;
|
||||
shareCodeList: ShareCodeResponse[];
|
||||
isShareCodeListLoading: boolean;
|
||||
}
|
||||
interface ShareCodeResourcesSizeByDateRange {
|
||||
totalSize: number;
|
||||
resourceCount: number;
|
||||
}
|
||||
export const DashboardContext = createContext<DashboardContextType | null>(null);
|
||||
|
||||
export const DashboardProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const { data: shareCodeAll, isLoading: isShareCodeAllLoading }:
|
||||
{ data: ShareCodeResourcesSizeByDateRange, isLoading: boolean }
|
||||
= api.shareCode.getShareCodeResourcesTotalSize.useQuery()
|
||||
const { data: shareCodeToday, isLoading: isShareCodeTodayLoading }:
|
||||
{ data: ShareCodeResourcesSizeByDateRange, isLoading: boolean }
|
||||
= api.shareCode.getShareCodeResourcesSizeByDateRange.useQuery(
|
||||
{ dateType: "today" })
|
||||
const { data: shareCodeYesterday, isLoading: isShareCodeYesterdayLoading }:
|
||||
{ data: ShareCodeResourcesSizeByDateRange, isLoading: boolean }
|
||||
= api.shareCode.getShareCodeResourcesSizeByDateRange.useQuery(
|
||||
{ dateType: "yesterday" })
|
||||
const { data: distinctUploadIPs, isLoading: isDistinctUploadIPsLoading }:
|
||||
{ data: { thisWeek: number; lastWeek: number; all: number }, isLoading: boolean }
|
||||
= api.shareCode.countDistinctUploadIPs.useQuery()
|
||||
const { data: shareCodeList, isLoading: isShareCodeListLoading }:
|
||||
{ data: ShareCodeResponse[], isLoading: boolean }
|
||||
= api.shareCode.findShareCodes.useQuery({
|
||||
where: {
|
||||
deletedAt: null
|
||||
},
|
||||
take: 8,
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
})
|
||||
return <>
|
||||
<DashboardContext.Provider value={{
|
||||
shareCodeAll,
|
||||
shareCodeToday,
|
||||
shareCodeYesterday,
|
||||
isShareCodeAllLoading,
|
||||
isShareCodeTodayLoading,
|
||||
isShareCodeYesterdayLoading,
|
||||
distinctUploadIPs,
|
||||
isDistinctUploadIPsLoading,
|
||||
shareCodeList,
|
||||
isShareCodeListLoading
|
||||
}}>
|
||||
{children}
|
||||
</DashboardContext.Provider>
|
||||
</>
|
||||
};
|
||||
|
||||
export const useDashboardContext = () => {
|
||||
const context = useContext(DashboardContext);
|
||||
if (!context) {
|
||||
throw new Error("useDashboardContext must be used within a DashboardProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
import Board from './components/Board';
|
||||
import Activate from './components/Activate';
|
||||
|
||||
export default function DashboardLayout() {
|
||||
return (
|
||||
<div className="bg-gray-100 p-5">
|
||||
<Board />
|
||||
{/* 最近活动 */}
|
||||
<Activate />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { Typography, Space, Skeleton } from 'antd';
|
||||
import { FileOutlined } from '@ant-design/icons';
|
||||
import DashboardCard from '@web/src/components/presentation/dashboard-card';
|
||||
import ActivityItem from './ActivityItem';
|
||||
import { useDashboardContext } from '../DashboardContext';
|
||||
import { ShareCodeResponse } from '../../quick-file/quickFileContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
const { Title, Text } = Typography;
|
||||
export default function Activate() {
|
||||
const { shareCodeList, isShareCodeListLoading } = useDashboardContext();
|
||||
const navigate = useNavigate()
|
||||
const handleClick = (item: ShareCodeResponse) => {
|
||||
navigate(`/manage/share-code?keyword=${encodeURIComponent(item.code)}`)
|
||||
}
|
||||
return <>
|
||||
<div className="mt-6">
|
||||
<Title level={4} className="mb-4">最新上传活动</Title>
|
||||
<DashboardCard className="w-full">
|
||||
<Space direction="vertical" className="w-full" size="middle">
|
||||
{isShareCodeListLoading ?
|
||||
<Skeleton active />
|
||||
:
|
||||
shareCodeList.map((item) => (
|
||||
<div className='my-1 cursor-pointer' onClick={() => handleClick(item)}>
|
||||
<ActivityItem
|
||||
key={item.id}
|
||||
icon={<FileOutlined className="text-blue-500" />}
|
||||
title={`来自${item.uploadIp}的用户上传了文件:"${item.fileName}",文件大小:${Math.max(Number(item.resource.meta.size) / 1024 / 1024, 0.01).toFixed(2)}MB,分享码:${item.code}`}
|
||||
time={item.createdAt?.toLocaleString()}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
</DashboardCard>
|
||||
</div>
|
||||
</>
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { motion } from 'framer-motion';
|
||||
// 活动项组件
|
||||
interface ActivityItemProps {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
export default function ActivityItem({ icon, title, time }: ActivityItemProps) {
|
||||
return (
|
||||
<motion.div
|
||||
className="flex items-center py-2"
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="flex-shrink-0 w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center mr-3">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<div className="text-gray-800">{title}</div>
|
||||
<div className="text-gray-400 text-xs">{time}</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
import { Row, Col, Typography, Space, Tooltip, Statistic, Spin } from 'antd';
|
||||
import { FileOutlined, HddOutlined, UserOutlined, CheckCircleOutlined, HistoryOutlined } from '@ant-design/icons';
|
||||
import DashboardCard from '@web/src/components/presentation/dashboard-card';
|
||||
import { useDashboardContext } from '../DashboardContext';
|
||||
import { useEffect, useState } from 'react';
|
||||
const { Title, Text } = Typography;
|
||||
export default function Board() {
|
||||
const { shareCodeAll, shareCodeToday, shareCodeYesterday,
|
||||
isShareCodeAllLoading, isShareCodeTodayLoading, isShareCodeYesterdayLoading,
|
||||
distinctUploadIPs, isDistinctUploadIPsLoading } = useDashboardContext();
|
||||
const [serverUptime, setServerUptime] = useState('');
|
||||
useEffect(() => {
|
||||
const calculateTimeDifference = () => {
|
||||
const now = new Date();
|
||||
const targetDate = new Date('2025-04-09T15:00:00');
|
||||
const diffMs = now.getTime()- targetDate.getTime();
|
||||
|
||||
// 如果是负数,表示目标日期已过
|
||||
if (diffMs < 0) {
|
||||
setServerUptime('0天0小时0分钟');
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算天数、小时数和分钟数
|
||||
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||
|
||||
setServerUptime(`${days}天${hours}小时${minutes}分钟`);
|
||||
};
|
||||
|
||||
calculateTimeDifference();
|
||||
// 每分钟更新一次
|
||||
const timer = setInterval(calculateTimeDifference, 60000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
return <>
|
||||
<Title level={3} className="mb-4">仪表盘</Title>
|
||||
<Row gutter={[16, 16]}>
|
||||
{/* 总文件数卡片 */}
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<DashboardCard
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<FileOutlined style={{ marginRight: 8, fontSize: 16 }} />
|
||||
总文件数
|
||||
</div>
|
||||
}
|
||||
className="h-full"
|
||||
>
|
||||
<div className="flex flex-col h-full justify-between py-2">
|
||||
<Statistic value={isShareCodeAllLoading ? 0 : shareCodeAll?.resourceCount} />
|
||||
{
|
||||
isShareCodeTodayLoading || isShareCodeYesterdayLoading
|
||||
? <Spin />
|
||||
: (<div className="text-gray-500 text-sm mt-2">
|
||||
昨天: {shareCodeYesterday?.resourceCount} 今天: {shareCodeToday?.resourceCount}
|
||||
</div>)
|
||||
}
|
||||
</div>
|
||||
</DashboardCard>
|
||||
</Col>
|
||||
|
||||
{/* 存储空间卡片 */}
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<DashboardCard
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<HddOutlined style={{ marginRight: 8, fontSize: 16 }} />
|
||||
已使用的存储空间
|
||||
</div>
|
||||
}
|
||||
className="h-full"
|
||||
>
|
||||
<div className="flex flex-col h-full justify-between py-2">
|
||||
<Statistic value={isShareCodeAllLoading ? 0 : `${(shareCodeAll?.totalSize / 1024 / 1024).toFixed(2)}MB`} />
|
||||
{
|
||||
isShareCodeTodayLoading || isShareCodeYesterdayLoading
|
||||
? <Spin />
|
||||
: (<div className="text-gray-500 text-sm mt-2">
|
||||
昨天: {(shareCodeYesterday?.totalSize / 1024 / 1024).toFixed(2)}MB 今天: {(shareCodeToday?.totalSize / 1024 / 1024).toFixed(2)}MB
|
||||
</div>)
|
||||
}
|
||||
</div>
|
||||
</DashboardCard>
|
||||
</Col>
|
||||
|
||||
{/* 活跃用户卡片 */}
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<DashboardCard
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<UserOutlined style={{ marginRight: 8, fontSize: 16 }} />
|
||||
全部用户
|
||||
</div>
|
||||
}
|
||||
className="h-full"
|
||||
>
|
||||
<div className="flex flex-col h-full justify-between py-2">
|
||||
<Statistic value={isDistinctUploadIPsLoading ? 0 : distinctUploadIPs?.all} />
|
||||
|
||||
<div className="text-gray-500 text-sm mt-2">
|
||||
上周使用用户: {distinctUploadIPs?.lastWeek} 本周使用用户: {distinctUploadIPs?.thisWeek}
|
||||
{
|
||||
distinctUploadIPs?.lastWeek ? (
|
||||
distinctUploadIPs?.lastWeek > distinctUploadIPs?.thisWeek ?
|
||||
<span className="text-red-500">↓{((distinctUploadIPs?.lastWeek - distinctUploadIPs?.thisWeek) / distinctUploadIPs?.lastWeek * 100).toFixed(2)}%</span>
|
||||
: <span className="text-green-500">↑ {((distinctUploadIPs?.thisWeek - distinctUploadIPs?.lastWeek) / distinctUploadIPs?.lastWeek * 100).toFixed(2)}%</span>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</DashboardCard>
|
||||
</Col>
|
||||
|
||||
{/* 系统状态卡片 */}
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<DashboardCard
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<CheckCircleOutlined style={{ marginRight: 8, fontSize: 16, color: "#52c41a" }} />
|
||||
系统状态
|
||||
</div>
|
||||
}
|
||||
className="h-full"
|
||||
>
|
||||
<div className="flex flex-col h-full justify-between py-2">
|
||||
<Statistic value="正常" valueStyle={{ color: '#52c41a' }} />
|
||||
<div className="text-gray-500 text-sm mt-2">
|
||||
服务器运行时间: {serverUptime}
|
||||
</div>
|
||||
</div>
|
||||
</DashboardCard>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import { Button } from "antd";
|
||||
import { RolePerms } from "@nice/common";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
interface HeaderProps {
|
||||
showLoginButton?: boolean;
|
||||
}
|
||||
|
||||
export const Header: React.FC<HeaderProps> = ({ showLoginButton = true }) => {
|
||||
const navigate = useNavigate();
|
||||
const {isAuthenticated , hasEveryPermissions} = useAuth();
|
||||
const isAdmin = hasEveryPermissions(RolePerms.MANAGE_ANY_POST) && isAuthenticated;
|
||||
|
||||
return (
|
||||
<div className="w-[1000px] mx-auto flex justify-between items-center mt-2">
|
||||
<div className="flex items-center">
|
||||
<img src="/logo.svg" className="h-16 w-16" />
|
||||
<span className="text-4xl py-4 mx-4 font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-500 to-blue-700 drop-shadow-sm">烽火快传</span>
|
||||
</div>
|
||||
|
||||
{showLoginButton && (
|
||||
<Button
|
||||
type="primary"
|
||||
className="bg-gradient-to-r from-blue-500 to-blue-700"
|
||||
onClick={() => {
|
||||
if(isAdmin){
|
||||
navigate('/manage');
|
||||
}else{
|
||||
navigate('/login');
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isAdmin ? '管理后台' : '登录'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
|
@ -0,0 +1,66 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { Layout, Menu } from "antd";
|
||||
import { FileOutlined, DashboardOutlined } from "@ant-design/icons";
|
||||
import { NavLink, Outlet, useNavigate, useLocation } from "react-router-dom";
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
|
||||
export default function QuickFileManage() {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [currentKey, setCurrentKey] = useState("1");
|
||||
|
||||
useEffect(() => {
|
||||
// 根据当前URL路径设置currentKey
|
||||
if (location.pathname.includes("/manage/dashboard")) {
|
||||
setCurrentKey("1");
|
||||
} else if (location.pathname.includes("/manage/share-code")) {
|
||||
setCurrentKey("2");
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: "100vh" }}>
|
||||
<Sider
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
onCollapse={(value) => setCollapsed(value)}
|
||||
theme="light"
|
||||
style={{
|
||||
borderRight: "1px solid #f0f0f0"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex items-center justify-center cursor-pointer w-full"
|
||||
onClick={() => {
|
||||
navigate('/');
|
||||
}}
|
||||
>
|
||||
<img src="/logo.svg" className="h-10 w-10" />
|
||||
{!collapsed && <span className="text-2xl py-4 mx-4 font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-500 to-blue-700 drop-shadow-sm">烽火快传</span>}
|
||||
</div>
|
||||
<Menu
|
||||
theme="light"
|
||||
mode="inline"
|
||||
selectedKeys={[currentKey]}
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
icon: <DashboardOutlined />,
|
||||
label: <NavLink to="/manage/dashboard">数据看板</NavLink>,
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
icon: <FileOutlined />,
|
||||
label: <NavLink to="/manage/share-code">所有文件</NavLink>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Sider>
|
||||
<Content style={{ padding: "24px" }}>
|
||||
<Outlet></Outlet>
|
||||
</Content>
|
||||
</Layout>
|
||||
);
|
||||
}
|
|
@ -1,26 +1,28 @@
|
|||
|
||||
import { ShareCodeGenerator } from "../sharecode/sharecodegenerator";
|
||||
import { ShareCodeValidator } from "../sharecode/sharecodevalidator";
|
||||
import { message, Tabs, Form, Spin, Alert } from "antd";
|
||||
import { env } from '../../../env'
|
||||
import { message, Tabs, Form } from "antd";
|
||||
import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
|
||||
import { useState } from "react";
|
||||
import CodeRecord from "../sharecode/components/CodeRecord";
|
||||
import Header from "./components/header";
|
||||
const { TabPane } = Tabs;
|
||||
export default function QuickUploadPage() {
|
||||
const [form] = Form.useForm();
|
||||
const uploadFileId = Form.useWatch(["file"], form)?.[0]
|
||||
const handleShareSuccess = (code: string) => {
|
||||
message.success('分享码生成成功:' + code);
|
||||
}
|
||||
return (
|
||||
<div style={{ maxWidth: '1000px', margin: '0 auto', padding: '20px' }}>
|
||||
<div className="flex items-center">
|
||||
<img src="/logo.svg" className="h-16 w-16 rounded-xl" />
|
||||
<span className="text-4xl py-4 mx-4">烽火快传</span>
|
||||
<div className="w-full ">
|
||||
<Header />
|
||||
<div className="max-w-[1000px] mx-auto">
|
||||
<div className="my-4 p-5 border-3 border-gray-200 rounded-lg shadow-lg">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<span className="text-lg block text-zinc-700 py-4">使用分享码下载文件</span>
|
||||
<CodeRecord title="我使用的分享码" btnContent="使用记录" recordName="shareCodeDownloadRecords" isDownload={true} />
|
||||
</div>
|
||||
<Tabs defaultActiveKey="upload">
|
||||
<TabPane tab="上传分享" key="upload">
|
||||
<div>
|
||||
<ShareCodeValidator />
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-4 p-5 border-3 border-gray-200 rounded-lg shadow-lg">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<span className="text-lg text-zinc-700">上传文件并生成分享码</span>
|
||||
<CodeRecord title="我生成的分享码" btnContent="上传记录" recordName="shareCodeGeneratorRecords" />
|
||||
|
@ -40,17 +42,17 @@ export default function QuickUploadPage() {
|
|||
fileId={uploadFileId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <Tabs defaultActiveKey="upload">
|
||||
<TabPane tab="上传分享" key="upload">
|
||||
|
||||
</TabPane>
|
||||
<TabPane tab="下载文件" key="download">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<span className="text-lg block text-zinc-700 py-4">使用分享码下载文件</span>
|
||||
<CodeRecord title="我使用的分享码" btnContent="使用记录" recordName="shareCodeDownloadRecords" isDownload={true}/>
|
||||
</div>
|
||||
<div>
|
||||
<ShareCodeValidator />
|
||||
</div>
|
||||
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Tabs> */}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
|
|
@ -25,6 +25,8 @@ export interface ShareCodeResponse {
|
|||
url: string;
|
||||
meta: ResourceMeta
|
||||
}
|
||||
uploadIp?: string;
|
||||
createdAt?: Date;
|
||||
}
|
||||
interface ResourceMeta {
|
||||
filename: string;
|
||||
|
@ -57,7 +59,10 @@ export const QuickFileProvider = ({ children }: { children: React.ReactNode }) =
|
|||
};
|
||||
// 获取已有记录并添加新记录
|
||||
const existingGeneratorRecords = localStorage.getItem(recordName);
|
||||
const generatorRecords = existingGeneratorRecords ? JSON.parse(existingGeneratorRecords) : [];
|
||||
let generatorRecords = existingGeneratorRecords ? JSON.parse(existingGeneratorRecords) : [];
|
||||
if (data.code) {
|
||||
generatorRecords = generatorRecords.filter((item: ShareCodeResponse) => item.code !== data.code)
|
||||
}
|
||||
generatorRecords.unshift(newRecord); // 添加到最前面
|
||||
localStorage.setItem(recordName, JSON.stringify(generatorRecords));
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ export default function CodeRecord({ title, btnContent, recordName ,styles,isDow
|
|||
<div className="space-y-4">
|
||||
{records.map(item => (
|
||||
<ShareCodeListCard
|
||||
key={item.code}
|
||||
key={item.id}
|
||||
item={item}
|
||||
onDelete={handleDelete}
|
||||
onDownload={isDownload ? handleDownload : undefined}
|
||||
|
|
|
@ -39,7 +39,7 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
|
|||
const [expiresAt, setExpiresAt] = useState<string | null>(null);
|
||||
const [canUseTimes, setCanUseTimes] = useState<number>(null);
|
||||
const queryClient = useQueryClient();
|
||||
const queryKey = getQueryKey(api.term);
|
||||
const queryKey = getQueryKey(api.shareCode);
|
||||
const [isGenerate, setIsGenerate] = useState(false);
|
||||
const [currentFileId, setCurrentFileId] = useState<string>('');
|
||||
const [form] = Form.useForm();
|
||||
|
|
|
@ -2,6 +2,7 @@ import {
|
|||
createBrowserRouter,
|
||||
IndexRouteObject,
|
||||
Link,
|
||||
Navigate,
|
||||
NonIndexRouteObject,
|
||||
useParams,
|
||||
} from "react-router-dom";
|
||||
|
@ -12,6 +13,9 @@ import { CodeManageProvider } from "../app/admin/code-manage/CodeManageContext";
|
|||
import CodeManageLayout from "../app/admin/code-manage/CodeManageLayout";
|
||||
import 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 {
|
||||
name?: string;
|
||||
breadcrumb?: string;
|
||||
|
@ -57,6 +61,36 @@ export const routes: CustomRouteObject[] = [
|
|||
</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);
|
||||
|
|
|
@ -426,14 +426,20 @@ model Person {
|
|||
model ShareCode {
|
||||
id String @id @default(cuid())
|
||||
code String? @unique
|
||||
|
||||
fileId String? @unique
|
||||
fileName String? @map("file_name")
|
||||
resource Resource? @relation(fields: [fileId], references: [fileId])
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
expiresAt DateTime? @map("expires_at")
|
||||
isUsed Boolean? @default(false)
|
||||
fileName String? @map("file_name")
|
||||
canUseTimes Int?
|
||||
resource Resource? @relation(fields: [fileId], references: [fileId])
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
isUsed Boolean? @default(false)
|
||||
canUseTimes Int?
|
||||
|
||||
uploadIp String? @map("upload_ip")
|
||||
|
||||
@@index([code])
|
||||
@@index([fileId])
|
||||
@@index([expiresAt])
|
||||
|
|
Loading…
Reference in New Issue