rht
This commit is contained in:
parent
0008f405fe
commit
6985ee863f
|
@ -0,0 +1,14 @@
|
||||||
|
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 { }
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
import { ShareCodeService } from "./share-code.service";
|
||||||
|
import { TrpcService } from "@server/trpc/trpc.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
@Injectable()
|
||||||
|
export class ShareCodeRouter {
|
||||||
|
constructor(
|
||||||
|
private readonly shareCodeService: ShareCodeService,
|
||||||
|
private readonly trpc: TrpcService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
router = this.trpc.router({
|
||||||
|
generateShareCode: this.trpc.procedure
|
||||||
|
.input(z.object({ fileId: z.string() }))
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
return this.shareCodeService.generateShareCode(input.fileId);
|
||||||
|
}),
|
||||||
|
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 }) => {
|
||||||
|
return this.shareCodeService.getFileByShareCode(input.code);
|
||||||
|
}),
|
||||||
|
generateShareCodeByFileId: this.trpc.procedure
|
||||||
|
.input(z.object({ fileId: z.string() }))
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
return this.shareCodeService.generateShareCodeByFileId(input.fileId);
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,239 @@
|
||||||
|
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||||
|
import { customAlphabet } from 'nanoid-cjs';
|
||||||
|
import { db } from '@nice/common';
|
||||||
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
|
import { ResourceService } from '@server/models/resource/resource.service';
|
||||||
|
|
||||||
|
export interface ShareCode {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
fileId: string;
|
||||||
|
createdAt: Date;
|
||||||
|
expiresAt: Date;
|
||||||
|
isUsed: boolean;
|
||||||
|
fileName?: string | null;
|
||||||
|
}
|
||||||
|
export interface GenerateShareCodeResponse {
|
||||||
|
code: string;
|
||||||
|
expiresAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResourceMeta {
|
||||||
|
filename: string;
|
||||||
|
filetype: string;
|
||||||
|
filesize: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ShareCodeService {
|
||||||
|
private readonly logger = new Logger(ShareCodeService.name);
|
||||||
|
// 生成8位分享码,使用易读的字符
|
||||||
|
private readonly generateCode = customAlphabet(
|
||||||
|
'23456789ABCDEFGHJKLMNPQRSTUVWXYZ',
|
||||||
|
8,
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(private readonly resourceService: ResourceService) { }
|
||||||
|
|
||||||
|
async generateShareCode(
|
||||||
|
fileId: string,
|
||||||
|
fileName?: string,
|
||||||
|
): Promise<GenerateShareCodeResponse> {
|
||||||
|
try {
|
||||||
|
// 检查文件是否存在
|
||||||
|
const resource = await this.resourceService.findUnique({
|
||||||
|
where: { fileId },
|
||||||
|
});
|
||||||
|
console.log('完整 fileId:', fileId); // 确保与前端一致
|
||||||
|
|
||||||
|
if (!resource) {
|
||||||
|
throw new NotFoundException('文件不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成分享码
|
||||||
|
const code = this.generateCode();
|
||||||
|
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24小时后过期
|
||||||
|
|
||||||
|
// 查找是否已有分享码记录
|
||||||
|
const existingShareCode = await db.shareCode.findUnique({
|
||||||
|
where: { fileId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingShareCode) {
|
||||||
|
// 更新现有记录,但保留原有文件名
|
||||||
|
await db.shareCode.update({
|
||||||
|
where: { fileId },
|
||||||
|
data: {
|
||||||
|
code,
|
||||||
|
expiresAt,
|
||||||
|
isUsed: false,
|
||||||
|
// 只在没有现有文件名且提供了新文件名时才更新文件名
|
||||||
|
...(fileName && !existingShareCode.fileName ? { fileName } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 创建新记录
|
||||||
|
await db.shareCode.create({
|
||||||
|
data: {
|
||||||
|
code,
|
||||||
|
fileId,
|
||||||
|
expiresAt,
|
||||||
|
isUsed: false,
|
||||||
|
fileName: fileName || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.logger.log(`Generated share code ${code} for file ${fileId}`);
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
expiresAt,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Failed to generate share code', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateAndUseCode(code: string): Promise<ShareCode | null> {
|
||||||
|
try {
|
||||||
|
console.log(`尝试验证分享码: ${code}`);
|
||||||
|
|
||||||
|
// 查找有效的分享码
|
||||||
|
const shareCode = await db.shareCode.findFirst({
|
||||||
|
where: {
|
||||||
|
code,
|
||||||
|
isUsed: false,
|
||||||
|
expiresAt: { gt: new Date() },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('查询结果:', shareCode);
|
||||||
|
|
||||||
|
if (!shareCode) {
|
||||||
|
console.log('分享码无效或已过期');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记分享码为已使用
|
||||||
|
// await db.shareCode.update({
|
||||||
|
// where: { id: shareCode.id },
|
||||||
|
// data: { isUsed: true },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// 记录使用日志
|
||||||
|
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(CronExpression.EVERY_DAY_AT_MIDNIGHT)
|
||||||
|
async cleanupExpiredShareCodes() {
|
||||||
|
try {
|
||||||
|
const result = await db.shareCode.deleteMany({
|
||||||
|
where: {
|
||||||
|
OR: [{ expiresAt: { lt: new Date() } }, { isUsed: true }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`Cleaned up ${result.count} expired share codes`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Failed to cleanup expired share codes', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取分享码信息
|
||||||
|
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: new Date() },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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) {
|
||||||
|
console.log('收到验证分享码请求,code:', code);
|
||||||
|
|
||||||
|
const shareCode = await this.validateAndUseCode(code);
|
||||||
|
console.log('验证分享码结果:', shareCode);
|
||||||
|
|
||||||
|
if (!shareCode) {
|
||||||
|
throw new NotFoundException('分享码无效或已过期');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件信息
|
||||||
|
const resource = await this.resourceService.findUnique({
|
||||||
|
where: { fileId: shareCode.fileId },
|
||||||
|
});
|
||||||
|
console.log('获取到的资源信息:', resource);
|
||||||
|
const { filename } = resource.meta as any as ResourceMeta
|
||||||
|
if (!resource) {
|
||||||
|
throw new NotFoundException('文件不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接返回正确的数据结构
|
||||||
|
const response = {
|
||||||
|
fileId: shareCode.fileId,
|
||||||
|
fileName: filename || 'downloaded_file',
|
||||||
|
code: shareCode.code,
|
||||||
|
expiresAt: shareCode.expiresAt
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('返回给前端的数据:', response); // 添加日志
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据文件ID生成分享码
|
||||||
|
async generateShareCodeByFileId(fileId: string) {
|
||||||
|
try {
|
||||||
|
console.log('收到生成分享码请求,fileId:', fileId);
|
||||||
|
const result = await this.generateShareCode(fileId);
|
||||||
|
console.log('生成分享码结果:', result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成分享码错误:', error);
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ 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({
|
||||||
|
@ -33,6 +33,7 @@ import { ResourceModule } from '@server/models/resource/resource.module';
|
||||||
VisitModule,
|
VisitModule,
|
||||||
WebSocketModule,
|
WebSocketModule,
|
||||||
ResourceModule,
|
ResourceModule,
|
||||||
|
ShareCodeModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [TrpcService, TrpcRouter, Logger],
|
providers: [TrpcService, TrpcRouter, Logger],
|
||||||
|
|
|
@ -14,7 +14,7 @@ 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);
|
||||||
|
@ -32,6 +32,7 @@ export class TrpcRouter {
|
||||||
private readonly message: MessageRouter,
|
private readonly message: MessageRouter,
|
||||||
private readonly visitor: VisitRouter,
|
private readonly visitor: VisitRouter,
|
||||||
private readonly resource: ResourceRouter,
|
private readonly resource: ResourceRouter,
|
||||||
|
private readonly shareCode: ShareCodeRouter,
|
||||||
) {}
|
) {}
|
||||||
getRouter() {
|
getRouter() {
|
||||||
return;
|
return;
|
||||||
|
@ -49,6 +50,7 @@ export class TrpcRouter {
|
||||||
app_config: this.app_config.router,
|
app_config: this.app_config.router,
|
||||||
visitor: this.visitor.router,
|
visitor: this.visitor.router,
|
||||||
resource: this.resource.router,
|
resource: this.resource.router,
|
||||||
|
shareCode: this.shareCode.router,
|
||||||
});
|
});
|
||||||
wss: WebSocketServer = undefined;
|
wss: WebSocketServer = undefined;
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,13 @@ import { Request, Response } from 'express';
|
||||||
import { TusService } from './tus.service';
|
import { TusService } from './tus.service';
|
||||||
import { ShareCodeService } from './share-code.service';
|
import { ShareCodeService } from './share-code.service';
|
||||||
import { ResourceService } from '@server/models/resource/resource.service';
|
import { ResourceService } from '@server/models/resource/resource.service';
|
||||||
|
|
||||||
|
interface ResourceMeta {
|
||||||
|
filename: string;
|
||||||
|
filetype: string;
|
||||||
|
filesize: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Controller('upload')
|
@Controller('upload')
|
||||||
export class UploadController {
|
export class UploadController {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -61,7 +68,7 @@ export class UploadController {
|
||||||
where: { fileId: shareCode.fileId },
|
where: { fileId: shareCode.fileId },
|
||||||
});
|
});
|
||||||
console.log('获取到的资源信息:', resource);
|
console.log('获取到的资源信息:', resource);
|
||||||
|
const {filename} = resource.meta as any as ResourceMeta
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
throw new NotFoundException('文件不存在');
|
throw new NotFoundException('文件不存在');
|
||||||
}
|
}
|
||||||
|
@ -69,7 +76,7 @@ export class UploadController {
|
||||||
// 直接返回正确的数据结构
|
// 直接返回正确的数据结构
|
||||||
const response = {
|
const response = {
|
||||||
fileId: shareCode.fileId,
|
fileId: shareCode.fileId,
|
||||||
fileName:shareCode.fileName || 'downloaded_file',
|
fileName:filename || 'downloaded_file',
|
||||||
code: shareCode.code,
|
code: shareCode.code,
|
||||||
expiresAt: shareCode.expiresAt
|
expiresAt: shareCode.expiresAt
|
||||||
};
|
};
|
||||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 828 B |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 828 B |
|
@ -1,10 +1,11 @@
|
||||||
import { useTusUpload } from "@web/src/hooks/useTusUpload";
|
import { useTusUpload } from "@web/src/hooks/useTusUpload";
|
||||||
import { ShareCodeGenerator } from "../sharecode/sharecodegenerator";
|
import { ShareCodeGenerator } from "../sharecode/sharecodegenerator";
|
||||||
import { ShareCodeValidator } from "../sharecode/sharecodevalidator";
|
import { ShareCodeValidator } from "../sharecode/sharecodevalidator";
|
||||||
import { useState, useRef, useCallback } from "react";
|
import { useState, useRef, useCallback, useEffect } from "react";
|
||||||
import { message, Progress, Button, Tabs, DatePicker } from "antd";
|
import { message, Progress, Button, Tabs, DatePicker, Form } from "antd";
|
||||||
import { UploadOutlined, DeleteOutlined, InboxOutlined } from "@ant-design/icons";
|
import { UploadOutlined, DeleteOutlined, InboxOutlined } from "@ant-design/icons";
|
||||||
import { env } from '../../../env'
|
import { env } from '../../../env'
|
||||||
|
import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
|
||||||
const { TabPane } = Tabs;
|
const { TabPane } = Tabs;
|
||||||
|
|
||||||
export default function DeptSettingPage() {
|
export default function DeptSettingPage() {
|
||||||
|
@ -15,18 +16,11 @@ export default function DeptSettingPage() {
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [expireTime, setExpireTime] = useState<Date | null>(null);
|
const [expireTime, setExpireTime] = useState<Date | null>(null);
|
||||||
const dropRef = useRef<HTMLDivElement>(null);
|
const dropRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [currentFile, setCurrentFile] = useState<string[]>([])
|
||||||
|
const uploadFileId = Form.useWatch(["file"], form)?.[0]
|
||||||
// 使用您的 useTusUpload hook
|
// 使用您的 useTusUpload hook
|
||||||
const { uploadProgress, isUploading, uploadError, handleFileUpload } = useTusUpload({
|
const { uploadProgress, isUploading, uploadError, handleFileUpload } = useTusUpload();
|
||||||
onSuccess: (result) => {
|
|
||||||
setUploadedFileId(result.fileId);
|
|
||||||
setUploadedFileName(result.fileName);
|
|
||||||
message.success('文件上传成功');
|
|
||||||
},
|
|
||||||
onError: (error: Error) => {
|
|
||||||
message.error('上传失败:' + error.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 清除已上传文件
|
// 清除已上传文件
|
||||||
const handleClearFile = () => {
|
const handleClearFile = () => {
|
||||||
|
@ -43,7 +37,7 @@ export default function DeptSettingPage() {
|
||||||
message.warning('只能上传一个文件,请先删除已上传的文件');
|
message.warning('只能上传一个文件,请先删除已上传的文件');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileKey = `file-${Date.now()}`; // 生成唯一的文件标识
|
const fileKey = `file-${Date.now()}`; // 生成唯一的文件标识
|
||||||
|
|
||||||
handleFileUpload(
|
handleFileUpload(
|
||||||
|
@ -110,17 +104,17 @@ export default function DeptSettingPage() {
|
||||||
// const response = await fetch(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/delete/${fileId}`, {
|
// const response = await fetch(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/delete/${fileId}`, {
|
||||||
// method: 'DELETE'
|
// method: 'DELETE'
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// if (!response.ok) {
|
// if (!response.ok) {
|
||||||
// throw new Error('删除文件失败');
|
// throw new Error('删除文件失败');
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// 无论服务器删除是否成功,前端都需要更新状态
|
// 无论服务器删除是否成功,前端都需要更新状态
|
||||||
setUploadedFiles([]);
|
setUploadedFiles([]);
|
||||||
setUploadedFileId('');
|
setUploadedFileId('');
|
||||||
setUploadedFileName('');
|
setUploadedFileName('');
|
||||||
setFileNameMap({});
|
setFileNameMap({});
|
||||||
|
|
||||||
message.success('文件已删除');
|
message.success('文件已删除');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('删除文件错误:', error);
|
console.error('删除文件错误:', error);
|
||||||
|
@ -202,150 +196,32 @@ export default function DeptSettingPage() {
|
||||||
<TabPane tab="上传分享" key="upload">
|
<TabPane tab="上传分享" key="upload">
|
||||||
{/* 文件上传区域 */}
|
{/* 文件上传区域 */}
|
||||||
<div style={{ marginBottom: '40px' }}>
|
<div style={{ marginBottom: '40px' }}>
|
||||||
<h3>第一步:上传文件</h3>
|
<span className="text-lg block text-zinc-700 py-2">第一步:上传文件</span>
|
||||||
|
|
||||||
{/* 如果没有已上传文件,显示上传区域 */}
|
{/* 如果没有已上传文件,显示上传区域 */}
|
||||||
{uploadedFiles.length === 0 ? (
|
<Form form={form}>
|
||||||
<div
|
<Form.Item name="file">
|
||||||
ref={dropRef}
|
<TusUploader
|
||||||
onDragEnter={handleDragEnter}
|
multiple={false}
|
||||||
onDragOver={handleDragOver}
|
style={"w-full py-4"}
|
||||||
onDragLeave={handleDragLeave}
|
></TusUploader>
|
||||||
onDrop={handleDrop}
|
</Form.Item>
|
||||||
style={{
|
</Form>
|
||||||
padding: '20px',
|
|
||||||
border: `2px dashed ${isDragging ? '#1890ff' : '#d9d9d9'}`,
|
|
||||||
borderRadius: '8px',
|
|
||||||
textAlign: 'center',
|
|
||||||
backgroundColor: isDragging ? 'rgba(24, 144, 255, 0.05)' : 'transparent',
|
|
||||||
transition: 'all 0.3s',
|
|
||||||
marginBottom: '20px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<InboxOutlined style={{ fontSize: '48px', color: isDragging ? '#1890ff' : '#d9d9d9' }} />
|
|
||||||
<p>点击或拖拽文件到此区域进行上传</p>
|
|
||||||
<p style={{ fontSize: '12px', color: '#888' }}>只能上传单个文件</p>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
id="file-input"
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
onChange={(e) => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (file) {
|
|
||||||
handleFileSelect(file);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={isUploading}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="file-input"
|
|
||||||
style={{
|
|
||||||
display: 'inline-block',
|
|
||||||
padding: '8px 16px',
|
|
||||||
backgroundColor: '#1890ff',
|
|
||||||
color: 'white',
|
|
||||||
borderRadius: '4px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
marginTop: '10px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<UploadOutlined /> 选择文件
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div style={{ marginBottom: '20px' }}>
|
|
||||||
<div style={{
|
|
||||||
padding: '10px',
|
|
||||||
backgroundColor: '#f6ffed',
|
|
||||||
border: '1px solid #b7eb8f',
|
|
||||||
borderRadius: '4px',
|
|
||||||
marginBottom: '10px'
|
|
||||||
}}>
|
|
||||||
<p style={{ color: '#52c41a', margin: 0 }}>
|
|
||||||
您已上传文件,请继续下一步生成分享码
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 已上传文件列表 */}
|
|
||||||
{uploadedFiles.length > 0 && (
|
|
||||||
<div style={{
|
|
||||||
border: '1px solid #f0f0f0',
|
|
||||||
borderRadius: '4px',
|
|
||||||
overflow: 'hidden'
|
|
||||||
}}>
|
|
||||||
{uploadedFiles.map((file) => (
|
|
||||||
<div key={file.id} style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: '10px 15px',
|
|
||||||
backgroundColor: '#fafafa'
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
flex: 1
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
width: '20px',
|
|
||||||
height: '20px',
|
|
||||||
borderRadius: '50%',
|
|
||||||
backgroundColor: '#52c41a',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
marginRight: '10px'
|
|
||||||
}}>
|
|
||||||
<span style={{ color: 'white', fontSize: '12px' }}>✓</span>
|
|
||||||
</div>
|
|
||||||
<span>{file.name}</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<DeleteOutlined style={{ color: '#ff4d4f' }} />}
|
|
||||||
onClick={() => handleDeleteFile(file.id)}
|
|
||||||
title="删除此文件"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isUploading && (
|
|
||||||
<div style={{ marginTop: '20px' }}>
|
|
||||||
<Progress
|
|
||||||
percent={Math.round(Object.values(uploadProgress)[0] || 0)}
|
|
||||||
status="active"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{uploadError && (
|
|
||||||
<div style={{ color: '#ff4d4f', marginTop: '10px' }}>
|
|
||||||
{uploadError}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 生成分享码区域 */}
|
{/* 生成分享码区域 */}
|
||||||
{uploadedFileId && (
|
<div style={{ marginBottom: '40px' }}>
|
||||||
<div style={{ marginBottom: '40px' }}>
|
<span className="text-lg block text-zinc-700 py-4">第二步:生成分享码</span>
|
||||||
<h3>第二步:生成分享码</h3>
|
<ShareCodeGenerator
|
||||||
|
fileId={uploadFileId}
|
||||||
<ShareCodeGenerator
|
onSuccess={handleShareSuccess}
|
||||||
fileId={uploadedFileId}
|
/>
|
||||||
onSuccess={handleShareSuccess}
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TabPane>
|
</TabPane>
|
||||||
|
|
||||||
{/* 使用分享码区域 */}
|
{/* 使用分享码区域 */}
|
||||||
<TabPane tab="下载文件" key="download">
|
<TabPane tab="下载文件" key="download">
|
||||||
<div>
|
<div>
|
||||||
<h3>使用分享码下载文件</h3>
|
<span className="text-lg block text-zinc-700 py-4">使用分享码下载文件</span>
|
||||||
<ShareCodeValidator
|
<ShareCodeValidator
|
||||||
onValidSuccess={handleValidSuccess}
|
onValidSuccess={handleValidSuccess}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, message } from 'antd';
|
import { Button, message } from 'antd';
|
||||||
import { CopyOutlined } from '@ant-design/icons';
|
import { CopyOutlined } from '@ant-design/icons';
|
||||||
import {env} from '../../../env'
|
import { env } from '../../../env'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { getQueryKey } from '@trpc/react-query';
|
||||||
|
import { api } from '@nice/client';
|
||||||
interface ShareCodeGeneratorProps {
|
interface ShareCodeGeneratorProps {
|
||||||
fileId: string;
|
fileId: string;
|
||||||
onSuccess?: (code: string) => void;
|
onSuccess?: (code: string) => void;
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
}
|
}
|
||||||
|
interface ShareCodeResponse {
|
||||||
|
code?: string;
|
||||||
|
expiresAt?: Date;
|
||||||
|
}
|
||||||
export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
|
export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
|
||||||
fileId,
|
fileId,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
|
@ -17,53 +23,29 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [shareCode, setShareCode] = useState<string>('');
|
const [shareCode, setShareCode] = useState<string>('');
|
||||||
const [expiresAt, setExpiresAt] = useState<Date | null>(null);
|
const [expiresAt, setExpiresAt] = useState<Date | null>(null);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const queryKey = getQueryKey(api.term);
|
||||||
|
const [isGenerate, setIsGenerate] = useState(false);
|
||||||
|
const [currentFileId, setCurrentFileId] = useState<string>('');
|
||||||
|
const generateShareCode = api.shareCode.generateShareCodeByFileId.useMutation({
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
if (fileId !== currentFileId || !fileId) {
|
||||||
|
setIsGenerate(false);
|
||||||
|
}
|
||||||
|
setCurrentFileId(fileId);
|
||||||
|
}, [fileId])
|
||||||
const generateCode = async () => {
|
const generateCode = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
console.log('开始生成分享码,fileId:', fileId, 'fileName:', fileName);
|
console.log('开始生成分享码,fileId:', fileId, 'fileName:', fileName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/share/${fileId}`, {
|
const data: ShareCodeResponse = await generateShareCode.mutateAsync({ fileId });
|
||||||
method: 'POST',
|
console.log('生成分享码结果:', data);
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
fileId
|
|
||||||
})
|
|
||||||
});
|
|
||||||
console.log('Current fileId:', fileId); // 确保 fileId 有效
|
|
||||||
console.log('请求URL:', `http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/share/${fileId}`);
|
|
||||||
console.log('API响应状态:', response.status);
|
|
||||||
|
|
||||||
const responseText = await response.text();
|
|
||||||
console.log('API原始响应:', responseText);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`请求失败: ${response.status} ${responseText || '无错误信息'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保响应不为空
|
|
||||||
if (!responseText) {
|
|
||||||
throw new Error('服务器返回空响应');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试解析JSON
|
|
||||||
let data;
|
|
||||||
try {
|
|
||||||
data = JSON.parse(responseText);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('解析响应JSON失败:', e);
|
|
||||||
throw new Error('服务器响应格式错误');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('解析后的响应数据:', data); // 调试日志
|
|
||||||
|
|
||||||
if (!data.code) {
|
|
||||||
throw new Error('响应中没有分享码');
|
|
||||||
}
|
|
||||||
|
|
||||||
setShareCode(data.code);
|
setShareCode(data.code);
|
||||||
|
setIsGenerate(true);
|
||||||
setExpiresAt(data.expiresAt ? new Date(data.expiresAt) : null);
|
setExpiresAt(data.expiresAt ? new Date(data.expiresAt) : null);
|
||||||
onSuccess?.(data.code);
|
onSuccess?.(data.code);
|
||||||
message.success('分享码生成成功');
|
message.success('分享码生成成功');
|
||||||
|
@ -79,10 +61,10 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
|
||||||
<div style={{ padding: '20px', backgroundColor: '#f8f9fa', borderRadius: '8px' }}>
|
<div style={{ padding: '20px', backgroundColor: '#f8f9fa', borderRadius: '8px' }}>
|
||||||
<div style={{ marginBottom: '10px' }}>
|
<div style={{ marginBottom: '10px' }}>
|
||||||
{/* 添加调试信息 */}
|
{/* 添加调试信息 */}
|
||||||
<small style={{ color: '#666' }}>文件ID: {fileId}</small>
|
<small style={{ color: '#666' }}>文件ID: {fileId ? fileId : '未选择文件'}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!shareCode ? (
|
{!isGenerate ? (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={generateCode}
|
onClick={generateCode}
|
||||||
|
@ -93,7 +75,7 @@ export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { Input, Button, message } from 'antd';
|
import { Input, Button, message } from 'antd';
|
||||||
import styles from './ShareCodeValidator.module.css';
|
import styles from './ShareCodeValidator.module.css';
|
||||||
import {env} from '../../../env'
|
import {env} from '../../../env'
|
||||||
|
import { api } from '@nice/client';
|
||||||
interface ShareCodeValidatorProps {
|
interface ShareCodeValidatorProps {
|
||||||
onValidSuccess: (fileId: string, fileName: string) => void;
|
onValidSuccess: (fileId: string, fileName: string) => void;
|
||||||
}
|
}
|
||||||
|
@ -11,8 +12,16 @@ export const ShareCodeValidator: React.FC<ShareCodeValidatorProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [code, setCode] = useState('');
|
const [code, setCode] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { data: result, isLoading } = api.shareCode.getFileByShareCode.useQuery(
|
||||||
|
{ code: code.trim() },
|
||||||
|
{
|
||||||
|
enabled: !!code.trim()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const validateCode = async () => {
|
|
||||||
|
|
||||||
|
const validateCode = useCallback(() => {
|
||||||
if (!code.trim()) {
|
if (!code.trim()) {
|
||||||
message.warning('请输入分享码');
|
message.warning('请输入分享码');
|
||||||
return;
|
return;
|
||||||
|
@ -20,31 +29,16 @@ export const ShareCodeValidator: React.FC<ShareCodeValidatorProps> = ({
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/share/${code.trim()}`);
|
console.log('验证分享码返回数据:', result);
|
||||||
|
onValidSuccess(result.fileId, result.fileName);
|
||||||
if (!response.ok) {
|
message.success(`验证成功,文件名:${result.fileName}`);
|
||||||
const errorData = await response.json().catch(() => ({}));
|
|
||||||
throw new Error(errorData.message || '分享码无效或已过期');
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
console.log('验证分享码返回数据:', data);
|
|
||||||
|
|
||||||
if (!data.fileId) {
|
|
||||||
throw new Error('未找到文件ID');
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileName = data.fileName || 'downloaded_file';
|
|
||||||
|
|
||||||
onValidSuccess(data.fileId, fileName);
|
|
||||||
message.success(`验证成功,文件名:${fileName}`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('验证分享码失败:', error);
|
console.error('验证分享码失败:', error);
|
||||||
message.error('分享码无效或已过期');
|
message.error('分享码无效或已过期');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
},[result])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
|
|
|
@ -86,6 +86,7 @@ export const TusUploader = ({
|
||||||
handleFileUpload(
|
handleFileUpload(
|
||||||
file,
|
file,
|
||||||
(result) => {
|
(result) => {
|
||||||
|
console.log(result)
|
||||||
setCompletedFiles((prev) => [
|
setCompletedFiles((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
|
|
|
@ -10,7 +10,7 @@ interface UploadResult {
|
||||||
fileName: string;
|
fileName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTusUpload(p0?: { onSuccess: (result: UploadResult) => void; onError: (error: Error) => void; }) {
|
export function useTusUpload() {
|
||||||
const [uploadProgress, setUploadProgress] = useState<
|
const [uploadProgress, setUploadProgress] = useState<
|
||||||
Record<string, number>
|
Record<string, number>
|
||||||
>({});
|
>({});
|
||||||
|
@ -77,6 +77,7 @@ export function useTusUpload(p0?: { onSuccess: (result: UploadResult) => void; o
|
||||||
onSuccess: async (payload) => {
|
onSuccess: async (payload) => {
|
||||||
if (upload.url) {
|
if (upload.url) {
|
||||||
const fileId = getFileId(upload.url);
|
const fileId = getFileId(upload.url);
|
||||||
|
//console.log(fileId)
|
||||||
const url = getResourceUrl(upload.url);
|
const url = getResourceUrl(upload.url);
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
setUploadProgress((prev) => ({
|
setUploadProgress((prev) => ({
|
||||||
|
|
|
@ -29,10 +29,10 @@ export type CustomRouteObject =
|
||||||
| CustomNonIndexRouteObject;
|
| CustomNonIndexRouteObject;
|
||||||
export const routes: CustomRouteObject[] = [
|
export const routes: CustomRouteObject[] = [
|
||||||
{
|
{
|
||||||
path:'/',
|
path: "/",
|
||||||
element:<DeptSettingPage></DeptSettingPage>,
|
element: <DeptSettingPage></DeptSettingPage>,
|
||||||
errorElement: <ErrorPage />,
|
errorElement: <ErrorPage />,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const router = createBrowserRouter(routes);
|
export const router = createBrowserRouter(routes);
|
||||||
|
|
Loading…
Reference in New Issue