Compare commits
10 Commits
66fc63c682
...
a9eedd743e
| Author | SHA1 | Date |
|---|---|---|
|
|
a9eedd743e | |
|
|
f991fbe979 | |
|
|
e746d84fa9 | |
|
|
1de0c878ad | |
|
|
bed7a4e690 | |
|
|
48c22c9348 | |
|
|
305ecf78ed | |
|
|
e290a9d51c | |
|
|
e186c77f61 | |
|
|
57e438efd1 |
|
|
@ -1,5 +1,6 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable ,Logger} from '@nestjs/common';
|
||||||
import { BaseService } from '../base/base.service';
|
import { BaseService } from '../base/base.service';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
UserProfile,
|
UserProfile,
|
||||||
db,
|
db,
|
||||||
|
|
@ -11,6 +12,7 @@ import {
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ResourceService extends BaseService<Prisma.ResourceDelegate> {
|
export class ResourceService extends BaseService<Prisma.ResourceDelegate> {
|
||||||
|
private readonly logger = new Logger(ResourceService.name);
|
||||||
constructor() {
|
constructor() {
|
||||||
super(db, ObjectType.RESOURCE);
|
super(db, ObjectType.RESOURCE);
|
||||||
}
|
}
|
||||||
|
|
@ -33,4 +35,53 @@ export class ResourceService extends BaseService<Prisma.ResourceDelegate> {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// 添加保存文件名的方法
|
||||||
|
async saveFileName(fileId: string, fileName: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.logger.log(`尝试保存文件名 "${fileName}" 到文件 ${fileId}`);
|
||||||
|
|
||||||
|
// 首先检查是否已存在 ShareCode 记录
|
||||||
|
const existingShareCode = await db.shareCode.findUnique({
|
||||||
|
where: { fileId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingShareCode) {
|
||||||
|
// 如果记录存在,更新文件名
|
||||||
|
await db.shareCode.update({
|
||||||
|
where: { fileId },
|
||||||
|
data: { fileName },
|
||||||
|
});
|
||||||
|
this.logger.log(`更新了现有记录的文件名 "${fileName}" 到文件 ${fileId}`);
|
||||||
|
} else {
|
||||||
|
// 如果记录不存在,创建新记录
|
||||||
|
await db.shareCode.create({
|
||||||
|
data: {
|
||||||
|
fileId,
|
||||||
|
fileName,
|
||||||
|
code: null, // 这里可以设置为 null 或生成一个临时码
|
||||||
|
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24小时后过期
|
||||||
|
isUsed: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.logger.log(`创建了新记录并保存文件名 "${fileName}" 到文件 ${fileId}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`保存文件名失败,文件ID: ${fileId}`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加获取文件名的方法
|
||||||
|
async getFileName(fileId: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const shareCode = await db.shareCode.findUnique({
|
||||||
|
where: { fileId },
|
||||||
|
select: { fileName: true },
|
||||||
|
});
|
||||||
|
return shareCode?.fileName || null;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to get filename for ${fileId}`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ import { DailyTrainModule } from '@server/models/daily-train/dailyTrain.module';
|
||||||
ResourceModule,
|
ResourceModule,
|
||||||
TrainContentModule,
|
TrainContentModule,
|
||||||
TrainSituationModule,
|
TrainSituationModule,
|
||||||
DailyTrainModule
|
DailyTrainModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [TrpcService, TrpcRouter, Logger],
|
providers: [TrpcService, TrpcRouter, Logger],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||||
|
import { customAlphabet } from 'nanoid-cjs';
|
||||||
|
import { db } from '@nice/common';
|
||||||
|
import { ShareCode, GenerateShareCodeResponse } from './types';
|
||||||
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
|
import { ResourceService } from '@server/models/resource/resource.service';
|
||||||
|
|
||||||
|
@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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -131,4 +131,5 @@ export class TusService implements OnModuleInit {
|
||||||
// console.log(req)
|
// console.log(req)
|
||||||
return this.tusServer.handle(req, res);
|
return this.tusServer.handle(req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,22 @@ export interface UploadLock {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// 添加重试机制,处理临时网络问题
|
// 添加重试机制,处理临时网络问题
|
||||||
// 实现定期清理过期的临时文件
|
// 实现定期清理过期的临时文件
|
||||||
// 添加文件完整性校验
|
// 添加文件完整性校验
|
||||||
|
|
@ -27,3 +43,10 @@ export interface UploadLock {
|
||||||
// 添加并发限制,防止系统资源耗尽
|
// 添加并发限制,防止系统资源耗尽
|
||||||
// 实现文件去重功能,避免重复上传
|
// 实现文件去重功能,避免重复上传
|
||||||
// 添加日志记录和监控机制
|
// 添加日志记录和监控机制
|
||||||
|
// 添加文件类型限制
|
||||||
|
// 添加文件大小限制
|
||||||
|
// 添加文件上传时间限制
|
||||||
|
// 添加文件上传速度限制
|
||||||
|
// 添加文件上传队列管理
|
||||||
|
// 添加文件上传断点续传
|
||||||
|
// 添加文件上传进度条
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,22 @@ import {
|
||||||
Delete,
|
Delete,
|
||||||
Head,
|
Head,
|
||||||
Options,
|
Options,
|
||||||
|
NotFoundException,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Body,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { TusService } from './tus.service';
|
import { TusService } from './tus.service';
|
||||||
|
import { ShareCodeService } from './share-code.service';
|
||||||
|
import { ResourceService } from '@server/models/resource/resource.service';
|
||||||
@Controller('upload')
|
@Controller('upload')
|
||||||
export class UploadController {
|
export class UploadController {
|
||||||
constructor(private readonly tusService: TusService) {}
|
constructor(
|
||||||
|
private readonly tusService: TusService,
|
||||||
|
private readonly shareCodeService: ShareCodeService,
|
||||||
|
private readonly resourceService: ResourceService,
|
||||||
|
) {}
|
||||||
// @Post()
|
// @Post()
|
||||||
// async handlePost(@Req() req: Request, @Res() res: Response) {
|
// async handlePost(@Req() req: Request, @Res() res: Response) {
|
||||||
// return this.tusService.handleTus(req, res);
|
// return this.tusService.handleTus(req, res);
|
||||||
|
|
@ -36,6 +45,52 @@ export class UploadController {
|
||||||
async handlePost(@Req() req: Request, @Res() res: Response) {
|
async handlePost(@Req() req: Request, @Res() res: Response) {
|
||||||
return this.tusService.handleTus(req, res);
|
return this.tusService.handleTus(req, res);
|
||||||
}
|
}
|
||||||
|
@Get('share/:code')
|
||||||
|
async validateShareCode(@Param('code') code: string) {
|
||||||
|
console.log('收到验证分享码请求,code:', code);
|
||||||
|
|
||||||
|
const shareCode = await this.shareCodeService.validateAndUseCode(code);
|
||||||
|
console.log('验证分享码结果:', shareCode);
|
||||||
|
|
||||||
|
if (!shareCode) {
|
||||||
|
throw new NotFoundException('分享码无效或已过期');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件信息
|
||||||
|
const resource = await this.resourceService.findUnique({
|
||||||
|
where: { fileId: shareCode.fileId },
|
||||||
|
});
|
||||||
|
console.log('获取到的资源信息:', resource);
|
||||||
|
|
||||||
|
if (!resource) {
|
||||||
|
throw new NotFoundException('文件不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接返回正确的数据结构
|
||||||
|
const response = {
|
||||||
|
fileId: shareCode.fileId,
|
||||||
|
fileName:shareCode.fileName || 'downloaded_file',
|
||||||
|
code: shareCode.code,
|
||||||
|
expiresAt: shareCode.expiresAt
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('返回给前端的数据:', response); // 添加日志
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('share/info/:code')
|
||||||
|
async getShareCodeInfo(@Param('code') code: string) {
|
||||||
|
const info = await this.shareCodeService.getShareCodeInfo(code);
|
||||||
|
if (!info) {
|
||||||
|
throw new NotFoundException('分享码不存在');
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('share/history/:fileId')
|
||||||
|
async getFileShareHistory(@Param('fileId') fileId: string) {
|
||||||
|
return this.shareCodeService.getFileShareHistory(fileId);
|
||||||
|
}
|
||||||
@Get('/*')
|
@Get('/*')
|
||||||
async handleGet(@Req() req: Request, @Res() res: Response) {
|
async handleGet(@Req() req: Request, @Res() res: Response) {
|
||||||
return this.tusService.handleTus(req, res);
|
return this.tusService.handleTus(req, res);
|
||||||
|
|
@ -51,4 +106,80 @@ export class UploadController {
|
||||||
async handleUpload(@Req() req: Request, @Res() res: Response) {
|
async handleUpload(@Req() req: Request, @Res() res: Response) {
|
||||||
return this.tusService.handleTus(req, res);
|
return this.tusService.handleTus(req, res);
|
||||||
}
|
}
|
||||||
|
@Post('share/:fileId(*)')
|
||||||
|
async generateShareCode(@Param('fileId') fileId: string) {
|
||||||
|
try {
|
||||||
|
console.log('收到生成分享码请求,fileId:', fileId);
|
||||||
|
const result = await this.shareCodeService.generateShareCode(fileId);
|
||||||
|
console.log('生成分享码结果:', result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成分享码错误:', error);
|
||||||
|
throw new HttpException(
|
||||||
|
{
|
||||||
|
message: (error as Error).message || '生成分享码失败',
|
||||||
|
error: 'SHARE_CODE_GENERATION_FAILED'
|
||||||
|
},
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('filename')
|
||||||
|
async saveFileName(@Body() data: { fileId: string; fileName: string }) {
|
||||||
|
try {
|
||||||
|
console.log('收到保存文件名请求:', data);
|
||||||
|
|
||||||
|
// 检查参数
|
||||||
|
if (!data.fileId || !data.fileName) {
|
||||||
|
throw new HttpException(
|
||||||
|
{ message: '缺少必要参数' },
|
||||||
|
HttpStatus.BAD_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 保存文件名
|
||||||
|
await this.resourceService.saveFileName(data.fileId, data.fileName);
|
||||||
|
console.log('文件名保存成功:', data.fileName, '对应文件ID:', data.fileId);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存文件名失败:', error);
|
||||||
|
throw new HttpException(
|
||||||
|
{
|
||||||
|
message: '保存文件名失败',
|
||||||
|
error: (error instanceof Error) ? error.message : String(error)
|
||||||
|
},
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('download/:fileId')
|
||||||
|
async downloadFile(@Param('fileId') fileId: string, @Res() res: Response) {
|
||||||
|
try {
|
||||||
|
// 获取文件信息
|
||||||
|
const resource = await this.resourceService.findUnique({
|
||||||
|
where: { fileId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resource) {
|
||||||
|
throw new NotFoundException('文件不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取原始文件名
|
||||||
|
const fileName = await this.resourceService.getFileName(fileId) || 'downloaded-file';
|
||||||
|
|
||||||
|
// 设置响应头,包含原始文件名
|
||||||
|
res.setHeader(
|
||||||
|
'Content-Disposition',
|
||||||
|
`attachment; filename="${encodeURIComponent(fileName)}"`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 其他下载逻辑...
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// 错误处理...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { UploadController } from './upload.controller';
|
||||||
import { BullModule } from '@nestjs/bullmq';
|
import { BullModule } from '@nestjs/bullmq';
|
||||||
import { TusService } from './tus.service';
|
import { TusService } from './tus.service';
|
||||||
import { ResourceModule } from '@server/models/resource/resource.module';
|
import { ResourceModule } from '@server/models/resource/resource.module';
|
||||||
|
import { ShareCodeService } from './share-code.service';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
BullModule.registerQueue({
|
BullModule.registerQueue({
|
||||||
|
|
@ -12,6 +12,6 @@ import { ResourceModule } from '@server/models/resource/resource.module';
|
||||||
ResourceModule,
|
ResourceModule,
|
||||||
],
|
],
|
||||||
controllers: [UploadController],
|
controllers: [UploadController],
|
||||||
providers: [TusService],
|
providers: [TusService, ShareCodeService],
|
||||||
})
|
})
|
||||||
export class UploadModule {}
|
export class UploadModule {}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
"@ag-grid-community/react": "~32.3.2",
|
"@ag-grid-community/react": "~32.3.2",
|
||||||
"@ag-grid-enterprise/clipboard": "~32.3.2",
|
"@ag-grid-enterprise/clipboard": "~32.3.2",
|
||||||
"@ag-grid-enterprise/column-tool-panel": "~32.3.2",
|
"@ag-grid-enterprise/column-tool-panel": "~32.3.2",
|
||||||
"@ag-grid-enterprise/core": "~32.3.2",
|
"@ag-grid-enterprise/core": "~32.3.3",
|
||||||
"@ag-grid-enterprise/filter-tool-panel": "~32.3.2",
|
"@ag-grid-enterprise/filter-tool-panel": "~32.3.2",
|
||||||
"@ag-grid-enterprise/master-detail": "~32.3.2",
|
"@ag-grid-enterprise/master-detail": "~32.3.2",
|
||||||
"@ag-grid-enterprise/menu": "~32.3.2",
|
"@ag-grid-enterprise/menu": "~32.3.2",
|
||||||
|
|
@ -41,13 +41,16 @@
|
||||||
"@trpc/client": "11.0.0-rc.456",
|
"@trpc/client": "11.0.0-rc.456",
|
||||||
"@trpc/react-query": "11.0.0-rc.456",
|
"@trpc/react-query": "11.0.0-rc.456",
|
||||||
"@trpc/server": "11.0.0-rc.456",
|
"@trpc/server": "11.0.0-rc.456",
|
||||||
|
"@types/xlsx": "^0.0.36",
|
||||||
"@xyflow/react": "^12.3.6",
|
"@xyflow/react": "^12.3.6",
|
||||||
"ag-grid-community": "~32.3.2",
|
"ag-grid-community": "~32.0.0",
|
||||||
"ag-grid-enterprise": "~32.3.2",
|
"ag-grid-enterprise": "~32.3.2",
|
||||||
"ag-grid-react": "~32.3.2",
|
"ag-grid-react": "~32.3.2",
|
||||||
"antd": "^5.19.3",
|
"ag-grid.i18n.zh-CN.json": "link:ag-grid-community/dist/locale/ag-grid.i18n.zh-CN.json",
|
||||||
|
"antd": "^5.23.0",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"browser-image-compression": "^2.0.2",
|
"browser-image-compression": "^2.0.2",
|
||||||
|
"china-area-data": "^5.0.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"d3-dag": "^1.1.0",
|
"d3-dag": "^1.1.0",
|
||||||
|
|
@ -57,7 +60,6 @@
|
||||||
"framer-motion": "^11.15.0",
|
"framer-motion": "^11.15.0",
|
||||||
"hls.js": "^1.5.18",
|
"hls.js": "^1.5.18",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"mind-elixir": "workspace:^",
|
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"quill": "2.0.3",
|
"quill": "2.0.3",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
|
|
@ -70,6 +72,7 @@
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"use-debounce": "^10.0.4",
|
"use-debounce": "^10.0.4",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
|
"xlsx": "^0.18.5",
|
||||||
"yjs": "^13.6.20",
|
"yjs": "^13.6.20",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,304 @@
|
||||||
|
import { useTusUpload } from "@web/src/hooks/useTusUpload";
|
||||||
|
import { ShareCodeGenerator } from "../sharecode/sharecodegenerator";
|
||||||
|
import { ShareCodeValidator } from "../sharecode/sharecodevalidator";
|
||||||
|
import { useState, useRef, useCallback } from "react";
|
||||||
|
import { message, Progress, Button, Tabs, DatePicker } from "antd";
|
||||||
|
import { UploadOutlined, DeleteOutlined, InboxOutlined } from "@ant-design/icons";
|
||||||
|
import {env} from '../../../../env'
|
||||||
|
const { TabPane } = Tabs;
|
||||||
|
|
||||||
|
export default function DeptSettingPage() {
|
||||||
|
const [uploadedFileId, setUploadedFileId] = useState<string>('');
|
||||||
|
const [uploadedFileName, setUploadedFileName] = useState<string>('');
|
||||||
|
const [fileNameMap, setFileNameMap] = useState<Record<string, string>>({});
|
||||||
|
const [uploadedFiles, setUploadedFiles] = useState<{id: string, name: string}[]>([]);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [expireTime, setExpireTime] = useState<Date | null>(null);
|
||||||
|
const dropRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// 使用您的 useTusUpload hook
|
||||||
|
const { uploadProgress, isUploading, uploadError, handleFileUpload } = useTusUpload({
|
||||||
|
onSuccess: (result) => {
|
||||||
|
setUploadedFileId(result.fileId);
|
||||||
|
setUploadedFileName(result.fileName);
|
||||||
|
message.success('文件上传成功');
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
message.error('上传失败:' + error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理文件上传
|
||||||
|
const handleFileSelect = async (file: File) => {
|
||||||
|
const fileKey = `file-${Date.now()}`; // 生成唯一的文件标识
|
||||||
|
|
||||||
|
handleFileUpload(
|
||||||
|
file,
|
||||||
|
async (result) => {
|
||||||
|
setUploadedFileId(result.fileId);
|
||||||
|
setUploadedFileName(result.fileName);
|
||||||
|
|
||||||
|
|
||||||
|
// 添加到已上传文件列表
|
||||||
|
setUploadedFiles(prev => [...prev, {id: result.fileId, name: file.name}]);
|
||||||
|
|
||||||
|
// 在前端保存文件名映射(用于当前会话)
|
||||||
|
setFileNameMap(prev => ({
|
||||||
|
...prev,
|
||||||
|
[result.fileId]: file.name
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 上传成功后保存原始文件名到数据库
|
||||||
|
try {
|
||||||
|
console.log('正在保存文件名到数据库:', result.fileName, '对应文件ID:', result.fileId);
|
||||||
|
|
||||||
|
const response = await fetch(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/filename`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
fileId: result.fileId,
|
||||||
|
fileName: file.name
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const responseText = await response.text();
|
||||||
|
console.log('保存文件名响应:', response.status, responseText);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('保存文件名失败:', responseText);
|
||||||
|
message.warning('文件名保存失败,下载时可能无法显示原始文件名');
|
||||||
|
} else {
|
||||||
|
console.log('文件名保存成功:', file.name);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存文件名请求失败:', error);
|
||||||
|
message.warning('文件名保存失败,下载时可能无法显示原始文件名');
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success('文件上传成功');
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
message.error('上传失败:' + error.message);
|
||||||
|
},
|
||||||
|
fileKey
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理多个文件上传
|
||||||
|
// const handleFilesUpload = (file: File) => {
|
||||||
|
// handleFileSelect(file);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// 拖拽相关处理函数
|
||||||
|
const handleDragEnter = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDragging(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDragLeave = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDragging(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDragging(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDrop = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDragging(false);
|
||||||
|
|
||||||
|
handleFileSelect(e.dataTransfer.files[0]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
// 处理分享码生成成功
|
||||||
|
const handleShareSuccess = (code: string) => {
|
||||||
|
message.success('分享码生成成功:' + code);
|
||||||
|
// 可以在这里添加其他逻辑,比如保存到历史记录
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理分享码验证成功
|
||||||
|
const handleValidSuccess = async (fileId: string, fileName: string) => {
|
||||||
|
try {
|
||||||
|
// 构建下载URL(包含文件名参数)
|
||||||
|
const downloadUrl = `/upload/download/${fileId}?fileName=${encodeURIComponent(fileName)}`;
|
||||||
|
const response = await fetch(downloadUrl);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('文件下载失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建下载链接
|
||||||
|
const blob = await response.blob();
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
|
||||||
|
// 直接使用传入的 fileName
|
||||||
|
link.download = fileName;
|
||||||
|
|
||||||
|
// 触发下载
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
message.success('文件下载开始');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载失败:', error);
|
||||||
|
message.error('文件下载失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
|
||||||
|
<h2>文件分享中心</h2>
|
||||||
|
|
||||||
|
<Tabs defaultActiveKey="upload">
|
||||||
|
<TabPane tab="上传分享" key="upload">
|
||||||
|
{/* 文件上传区域 */}
|
||||||
|
<div style={{ marginBottom: '40px' }}>
|
||||||
|
<h3>第一步:上传文件</h3>
|
||||||
|
<div
|
||||||
|
ref={dropRef}
|
||||||
|
onDragEnter={handleDragEnter}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
style={{
|
||||||
|
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>
|
||||||
|
|
||||||
|
{/* 已上传文件列表 */}
|
||||||
|
{uploadedFiles.length > 0 && (
|
||||||
|
<div style={{
|
||||||
|
border: '1px solid #f0f0f0',
|
||||||
|
borderRadius: '4px',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}>
|
||||||
|
{uploadedFiles.map((file, index) => (
|
||||||
|
<div key={file.id} style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '10px 15px',
|
||||||
|
borderBottom: index < uploadedFiles.length - 1 ? '1px solid #f0f0f0' : 'none',
|
||||||
|
backgroundColor: index % 2 === 0 ? '#fafafa' : 'white'
|
||||||
|
}}>
|
||||||
|
<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' }} />}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
{/* 生成分享码区域 */}
|
||||||
|
{uploadedFileId && (
|
||||||
|
<div style={{ marginBottom: '40px' }}>
|
||||||
|
<h3>第二步:生成分享码</h3>
|
||||||
|
|
||||||
|
<ShareCodeGenerator
|
||||||
|
fileId={uploadedFileId}
|
||||||
|
onSuccess={handleShareSuccess}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabPane>
|
||||||
|
|
||||||
|
{/* 使用分享码区域 */}
|
||||||
|
<TabPane tab="下载文件" key="download">
|
||||||
|
<div>
|
||||||
|
<h3>使用分享码下载文件</h3>
|
||||||
|
<ShareCodeValidator
|
||||||
|
onValidSuccess={handleValidSuccess}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
font-size: 16px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Button, message } from 'antd';
|
||||||
|
import { CopyOutlined } from '@ant-design/icons';
|
||||||
|
import {env} from '../../../../env'
|
||||||
|
interface ShareCodeGeneratorProps {
|
||||||
|
fileId: string;
|
||||||
|
onSuccess?: (code: string) => void;
|
||||||
|
fileName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShareCodeGenerator: React.FC<ShareCodeGeneratorProps> = ({
|
||||||
|
fileId,
|
||||||
|
onSuccess,
|
||||||
|
fileName,
|
||||||
|
}) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [shareCode, setShareCode] = useState<string>('');
|
||||||
|
const [expiresAt, setExpiresAt] = useState<Date | null>(null);
|
||||||
|
|
||||||
|
const generateCode = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
console.log('开始生成分享码,fileId:', fileId, 'fileName:', fileName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/share/${fileId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
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);
|
||||||
|
setExpiresAt(data.expiresAt ? new Date(data.expiresAt) : null);
|
||||||
|
onSuccess?.(data.code);
|
||||||
|
message.success('分享码生成成功');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成分享码错误:', error);
|
||||||
|
message.error('生成分享码失败: ' + (error instanceof Error ? error.message : '未知错误'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px', backgroundColor: '#f8f9fa', borderRadius: '8px' }}>
|
||||||
|
<div style={{ marginBottom: '10px' }}>
|
||||||
|
{/* 添加调试信息 */}
|
||||||
|
<small style={{ color: '#666' }}>文件ID: {fileId}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!shareCode ? (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={generateCode}
|
||||||
|
loading={loading}
|
||||||
|
block
|
||||||
|
>
|
||||||
|
生成分享码
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<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={() => {
|
||||||
|
navigator.clipboard.writeText(shareCode);
|
||||||
|
message.success('分享码已复制');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{expiresAt ? (
|
||||||
|
<div style={{ color: '#666' }}>
|
||||||
|
有效期至: {expiresAt.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ color: 'red' }}>
|
||||||
|
未获取到有效期信息
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Input, Button, message } from 'antd';
|
||||||
|
import styles from './ShareCodeValidator.module.css';
|
||||||
|
import {env} from '../../../../env'
|
||||||
|
interface ShareCodeValidatorProps {
|
||||||
|
onValidSuccess: (fileId: string, fileName: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShareCodeValidator: React.FC<ShareCodeValidatorProps> = ({
|
||||||
|
onValidSuccess,
|
||||||
|
}) => {
|
||||||
|
const [code, setCode] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const validateCode = async () => {
|
||||||
|
if (!code.trim()) {
|
||||||
|
message.warning('请输入分享码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload/share/${code.trim()}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
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) {
|
||||||
|
console.error('验证分享码失败:', error);
|
||||||
|
message.error('分享码无效或已过期');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<Input
|
||||||
|
className={styles.input}
|
||||||
|
value={code}
|
||||||
|
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
||||||
|
placeholder="请输入分享码"
|
||||||
|
maxLength={8}
|
||||||
|
onPressEnter={validateCode}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={validateCode}
|
||||||
|
loading={loading}
|
||||||
|
disabled={!code.trim()}
|
||||||
|
>
|
||||||
|
验证并下载
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -44,6 +44,15 @@ const items = [
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
),
|
),
|
||||||
|
getItem(
|
||||||
|
"系统设置",
|
||||||
|
"/admin",
|
||||||
|
<i className="iconfont icon-icon-user" />,
|
||||||
|
[
|
||||||
|
getItem("部门设置", "/admin/department", null, null, null),
|
||||||
|
],
|
||||||
|
null,
|
||||||
|
),
|
||||||
getItem(
|
getItem(
|
||||||
"训练计划",
|
"训练计划",
|
||||||
"/plan",
|
"/plan",
|
||||||
|
|
@ -83,7 +92,8 @@ const NavigationMenu: React.FC = () => {
|
||||||
// 添加考核成绩子路径的匹配规则
|
// 添加考核成绩子路径的匹配规则
|
||||||
"^/assessment/positionassessment": ["/assessment"],
|
"^/assessment/positionassessment": ["/assessment"],
|
||||||
"^/assessment/commonassessment": ["/assessment"],
|
"^/assessment/commonassessment": ["/assessment"],
|
||||||
"^/assessment/sportsassessment": ["/assessment"]
|
"^/assessment/sportsassessment": ["/assessment"],
|
||||||
|
"^/admin/department": ["/admin"],
|
||||||
};
|
};
|
||||||
|
|
||||||
// 选中的菜单
|
// 选中的菜单
|
||||||
|
|
@ -109,7 +119,10 @@ const NavigationMenu: React.FC = () => {
|
||||||
setOpenKeys(["/staff"]);
|
setOpenKeys(["/staff"]);
|
||||||
} else if (path.startsWith("/assessment/") || path.startsWith("/plan/")) {
|
} else if (path.startsWith("/assessment/") || path.startsWith("/plan/")) {
|
||||||
setOpenKeys([path.split('/').slice(0, 2).join('/')]);
|
setOpenKeys([path.split('/').slice(0, 2).join('/')]);
|
||||||
} else {
|
} else if(path.startsWith("/admin/")){
|
||||||
|
setOpenKeys(["/admin"]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
setOpenKeys(openKeyMerge(path));
|
setOpenKeys(openKeyMerge(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button, Form, Input, Select, DatePicker, Radio, message, Modal, Cascader } from "antd";
|
import { Button, Form, Input, Select, DatePicker, Radio, message, Modal, Cascader, InputNumber } from "antd";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useStaff } from "@nice/client";
|
import { useStaff } from "@nice/client";
|
||||||
|
|
@ -144,7 +144,7 @@ const StaffInformation = () => {
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label='年龄' name='age'>
|
<Form.Item label='年龄' name='age'>
|
||||||
<Input />
|
<InputNumber />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="血型" name="bloodType">
|
<Form.Item label="血型" name="bloodType">
|
||||||
<Select>
|
<Select>
|
||||||
|
|
@ -218,7 +218,9 @@ const StaffInformation = () => {
|
||||||
<Form.Item label="代理职务" name="proxyPosition">
|
<Form.Item label="代理职务" name="proxyPosition">
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label="岗位" name="post">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { useState, useEffect, useImperativeHandle, forwardRef } from 'react';
|
import { useState } from 'react';
|
||||||
import { AgGridReact } from 'ag-grid-react';
|
import { AgGridReact } from '@ag-grid-community/react';
|
||||||
import { ColDef, ColGroupDef } from 'ag-grid-community';
|
import { ColDef, ColGroupDef } from '@ag-grid-community/core';
|
||||||
import { api } from '@nice/client';
|
import { SetFilterModule } from '@ag-grid-enterprise/set-filter';
|
||||||
import 'ag-grid-community/styles/ag-grid.css';
|
import 'ag-grid-community/styles/ag-grid.css';
|
||||||
import 'ag-grid-community/styles/ag-theme-alpine.css';
|
import 'ag-grid-community/styles/ag-theme-alpine.css';
|
||||||
import { areaOptions } from '@web/src/app/main/staffinformation/area-options';
|
import { areaOptions } from '@web/src/app/main/staffinformation/area-options';
|
||||||
import type { CascaderProps } from 'antd/es/cascader';
|
import type { CascaderProps } from 'antd/es/cascader';
|
||||||
import { SetFilterModule } from '@ag-grid-enterprise/set-filter';
|
import { utils, writeFile } from 'xlsx';
|
||||||
import { zhCN } from 'ag-grid-community/dist/ag-grid-community';
|
import { Modal, Input, Button, Switch } from 'antd';
|
||||||
|
import { api } from '@nice/client';
|
||||||
|
import DepartmentSelect from '@web/src/components/models/department/department-select';
|
||||||
|
import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model';
|
||||||
|
|
||||||
// 修改函数类型定义
|
// 修改函数类型定义
|
||||||
|
|
||||||
|
|
@ -28,81 +31,110 @@ function getAreaName(codes: string[], level?: number): string {
|
||||||
|
|
||||||
return level ? result[level - 1] || '' : result.join(' / ') || codes.join('/');
|
return level ? result[level - 1] || '' : result.join(' / ') || codes.join('/');
|
||||||
}
|
}
|
||||||
// 自定义下拉过滤器组件
|
|
||||||
// interface ICustomFilterProps {
|
|
||||||
// column: {
|
|
||||||
// getColId: () => string;
|
|
||||||
// };
|
|
||||||
// api: {
|
|
||||||
// forEachNode: (callback: (node: any) => void) => void;
|
|
||||||
// };
|
|
||||||
// filterChangedCallback?: () => void; // 设置为可选属性
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const CustomDropdownFilter = forwardRef((props: ICustomFilterProps, ref) => {
|
|
||||||
// const [selectedValue, setSelectedValue] = useState<string>('');
|
|
||||||
// const [uniqueValues, setUniqueValues] = useState<string[]>([]);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// const colId = props.column.getColId();
|
|
||||||
// const values = new Set<string>();
|
|
||||||
|
|
||||||
// props.api.forEachNode(node => {
|
|
||||||
// const value = node.data[colId];
|
|
||||||
// if (value != null) {
|
|
||||||
// values.add(String(value));
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// setUniqueValues(Array.from(values).sort());
|
|
||||||
// }, [props.api, props.column]);
|
|
||||||
|
|
||||||
// useImperativeHandle(ref, () => ({
|
|
||||||
// isFilterActive: () => !!selectedValue,
|
|
||||||
// doesFilterPass: (params: any) => {
|
|
||||||
// const value = String(params.data[props.column.getColId()] || '');
|
|
||||||
// // 精确匹配选中的值
|
|
||||||
// return value === selectedValue;
|
|
||||||
// },
|
|
||||||
// getModel: () => {
|
|
||||||
// return selectedValue ? { value: selectedValue } : null;
|
|
||||||
// },
|
|
||||||
// setModel: (model: any) => {
|
|
||||||
// setSelectedValue(model?.value || '');
|
|
||||||
// }
|
|
||||||
// }));
|
|
||||||
|
|
||||||
// const handleChange = (value: string) => {
|
|
||||||
// setSelectedValue(value);
|
|
||||||
// // 立即触发过滤器更新
|
|
||||||
// if (props.filterChangedCallback) {
|
|
||||||
// props.filterChangedCallback();
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <div style={{ padding: '4px', width: '100%' }}>
|
|
||||||
// <Select
|
|
||||||
// value={selectedValue}
|
|
||||||
// onChange={handleChange}
|
|
||||||
// style={{ width: '100%' }}
|
|
||||||
// size="small"
|
|
||||||
// placeholder="选择筛选..."
|
|
||||||
// allowClear
|
|
||||||
// options={uniqueValues.map(value => ({
|
|
||||||
// value: value,
|
|
||||||
// label: value
|
|
||||||
// }))}
|
|
||||||
// onClear={() => handleChange('')}
|
|
||||||
// />
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
|
|
||||||
export default function StaffTable() {
|
export default function StaffTable() {
|
||||||
const { data: staffs, isLoading } = api.staff.findMany.useQuery({
|
const { data: staffs, isLoading } = api.staff.findMany.useQuery({
|
||||||
where: { deletedAt: null },
|
where: { deletedAt: null },
|
||||||
|
include: { // 添加关联查询
|
||||||
|
department: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
const [gridApi, setGridApi] = useState<any>(null); // 添加gridApi状态
|
||||||
|
const [fileNameVisible, setFileNameVisible] = useState(false);
|
||||||
|
const [fileName, setFileName] = useState('');
|
||||||
|
const [defaultFileName] = useState(`员工数据_${new Date().toISOString().slice(0, 10)}`);
|
||||||
|
const [paginationEnabled, setPaginationEnabled] = useState(true);
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
setFileNameVisible(true);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加导出处理函数
|
||||||
|
const handleFileNameConfirm = () => {
|
||||||
|
setFileNameVisible(false);
|
||||||
|
if (!gridApi || typeof gridApi.getRenderedNodes !== 'function') {
|
||||||
|
console.error('Grid API 未正确初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改获取节点方式(使用更可靠的 getRenderedNodes)
|
||||||
|
const rowNodes = gridApi.getRenderedNodes();
|
||||||
|
|
||||||
|
// 获取所有列定义
|
||||||
|
const flattenColumns = (cols: any[]): any[] => cols.flatMap(col => col.children ? flattenColumns(col.children) : col);
|
||||||
|
const allColDefs = flattenColumns(gridApi.getColumnDefs());
|
||||||
|
|
||||||
|
// 获取数据(兼容分页状态)
|
||||||
|
|
||||||
|
// 处理数据格式
|
||||||
|
const processRowData = (node: any) => {
|
||||||
|
const row: Record<string, any> = {};
|
||||||
|
allColDefs.forEach((colDef: any) => {
|
||||||
|
if (!colDef.field || !colDef.headerName) return;
|
||||||
|
|
||||||
|
// 修改字段访问方式,支持嵌套对象
|
||||||
|
const value = colDef.field.includes('.')
|
||||||
|
? colDef.field.split('.').reduce((obj: any, key: string) => (obj || {})[key], node.data)
|
||||||
|
: node.data[colDef.field];
|
||||||
|
|
||||||
|
let renderedValue = value;
|
||||||
|
|
||||||
|
// 应用列格式化
|
||||||
|
if (colDef.valueFormatter) {
|
||||||
|
renderedValue = colDef.valueFormatter({ value });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理特殊数据类型
|
||||||
|
if (colDef.cellRenderer) {
|
||||||
|
const renderResult = colDef.cellRenderer({ value });
|
||||||
|
if (typeof renderResult === 'string') {
|
||||||
|
renderedValue = renderResult;
|
||||||
|
} else if (renderResult?.props?.dangerouslySetInnerHTML?.__html) {
|
||||||
|
renderedValue = renderResult.props.dangerouslySetInnerHTML.__html.replace(/<br\s*\/?>/gi, '\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一布尔值显示
|
||||||
|
if (typeof renderedValue === 'boolean') {
|
||||||
|
renderedValue = renderedValue ? '是' : '否';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日期字段处理
|
||||||
|
if (['hireDate', 'rankDate', 'seniority'].includes(colDef.field) && value) {
|
||||||
|
renderedValue = new Date(value).toLocaleDateString('zh-CN');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 特别处理部门名称显示
|
||||||
|
if (colDef.field === 'department.name' && renderedValue === undefined) {
|
||||||
|
renderedValue = node.data.department?.name || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
row[colDef.headerName] = renderedValue;
|
||||||
|
});
|
||||||
|
return row;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 生成工作表
|
||||||
|
const rowData = rowNodes.map(processRowData);
|
||||||
|
const ws = utils.json_to_sheet(rowData);
|
||||||
|
const wb = utils.book_new();
|
||||||
|
utils.book_append_sheet(wb, ws, "员工数据");
|
||||||
|
|
||||||
|
// 生成文件名
|
||||||
|
const finalFileName = fileName || `${defaultFileName}_${paginationEnabled ? '当前页' : '全部'}`;
|
||||||
|
writeFile(wb, `${finalFileName}.xlsx`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导出失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleResetFilters = () => {
|
||||||
|
if (gridApi) {
|
||||||
|
gridApi.setFilterModel(null);
|
||||||
|
gridApi.onFilterChanged(); // 触发筛选更新
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const columnDefs: (ColDef | ColGroupDef)[] = [
|
const columnDefs: (ColDef | ColGroupDef)[] = [
|
||||||
{
|
{
|
||||||
|
|
@ -128,8 +160,20 @@ export default function StaffTable() {
|
||||||
{ field: 'officerId', headerName: '警号', minWidth: 120 },
|
{ field: 'officerId', headerName: '警号', minWidth: 120 },
|
||||||
{ field: 'phoneNumber', headerName: '手机号', minWidth: 130 },
|
{ field: 'phoneNumber', headerName: '手机号', minWidth: 130 },
|
||||||
{ field: 'age', headerName: '年龄', minWidth: 80 },
|
{ field: 'age', headerName: '年龄', minWidth: 80 },
|
||||||
{ field: 'sex', headerName: '性别', minWidth: 80,
|
{
|
||||||
cellRenderer: (params: any) => params.value ? '男' : '女' },
|
field: 'sex', headerName: '性别', minWidth: 80,
|
||||||
|
cellRenderer: (params: any) => {
|
||||||
|
switch (params.value) {
|
||||||
|
case true:
|
||||||
|
return '男';
|
||||||
|
case false:
|
||||||
|
return '女';
|
||||||
|
default:
|
||||||
|
return '未知';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
{ field: 'bloodType', headerName: '血型', minWidth: 80 },
|
{ field: 'bloodType', headerName: '血型', minWidth: 80 },
|
||||||
{
|
{
|
||||||
field: 'birthplace',
|
field: 'birthplace',
|
||||||
|
|
@ -150,27 +194,40 @@ export default function StaffTable() {
|
||||||
{
|
{
|
||||||
headerName: '职务信息',
|
headerName: '职务信息',
|
||||||
children: [
|
children: [
|
||||||
{field: 'deptId', headerName: '所属部门', minWidth: 200 },
|
{ field: 'department.name', headerName: '所属部门', minWidth: 200 },
|
||||||
{ field: 'rank', headerName: '衔职级别', minWidth: 120 },
|
{ field: 'rank', headerName: '衔职级别', minWidth: 120 },
|
||||||
{ field: 'rankDate', headerName: '衔职时间', minWidth: 120,
|
{
|
||||||
valueFormatter: (params: any) => params.value ? new Date(params.value).toLocaleDateString() : '' },
|
field: 'rankDate', headerName: '衔职时间', minWidth: 120,
|
||||||
{ field: 'proxyPosition', headerName: '代理职务', minWidth: 120 }
|
valueFormatter: (params: any) => params.value ? new Date(params.value).toLocaleDateString() : ''
|
||||||
|
},
|
||||||
|
{ field: 'proxyPosition', headerName: '代理职务', minWidth: 120 },
|
||||||
|
{field: 'post', headerName: '岗位', minWidth: 120}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headerName: '入职信息',
|
headerName: '入职信息',
|
||||||
children: [
|
children: [
|
||||||
{ field: 'hireDate', headerName: '入职时间', minWidth: 120,
|
{
|
||||||
valueFormatter: (params: any) => params.value ? new Date(params.value).toLocaleDateString() : '' },
|
field: 'hireDate', headerName: '入职时间', minWidth: 120,
|
||||||
{ field: 'seniority', headerName: '工龄认定时间', minWidth: 140,
|
valueFormatter: (params: any) => params.value ? new Date(params.value).toLocaleDateString() : ''
|
||||||
valueFormatter: (params: any) => params.value ? new Date(params.value).toLocaleDateString() : '' },
|
},
|
||||||
|
{
|
||||||
|
field: 'seniority', headerName: '工龄认定时间', minWidth: 140,
|
||||||
|
valueFormatter: (params: any) => params.value ? new Date(params.value).toLocaleDateString() : ''
|
||||||
|
},
|
||||||
{ field: 'sourceType', headerName: '来源类型', minWidth: 120 },
|
{ field: 'sourceType', headerName: '来源类型', minWidth: 120 },
|
||||||
{ field: 'isReentry', headerName: '是否二次入职', minWidth: 120,
|
{
|
||||||
cellRenderer: (params: any) => params.value ? '是' : '否' },
|
field: 'isReentry', headerName: '是否二次入职', minWidth: 120,
|
||||||
{ field: 'isExtended', headerName: '是否延期服役', minWidth: 120,
|
cellRenderer: (params: any) => params.value ? '是' : '否'
|
||||||
cellRenderer: (params: any) => params.value ? '是' : '否' },
|
},
|
||||||
{ field: 'currentPositionDate', headerName: '现岗位开始时间', minWidth: 140,
|
{
|
||||||
valueFormatter: (params: any) => params.value ? new Date(params.value).toLocaleDateString() : '' }
|
field: 'isExtended', headerName: '是否延期服役', minWidth: 120,
|
||||||
|
cellRenderer: (params: any) => params.value ? '是' : '否'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'currentPositionDate', headerName: '现岗位开始时间', minWidth: 140,
|
||||||
|
valueFormatter: (params: any) => params.value ? new Date(params.value).toLocaleDateString() : ''
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -178,8 +235,10 @@ export default function StaffTable() {
|
||||||
children: [
|
children: [
|
||||||
{ field: 'education', headerName: '学历', minWidth: 100 },
|
{ field: 'education', headerName: '学历', minWidth: 100 },
|
||||||
{ field: 'educationType', headerName: '学历形式', minWidth: 120 },
|
{ field: 'educationType', headerName: '学历形式', minWidth: 120 },
|
||||||
{ field: 'isGraduated', headerName: '是否毕业', minWidth: 100,
|
{
|
||||||
cellRenderer: (params: any) => params.value ? '是' : '否' },
|
field: 'isGraduated', headerName: '是否毕业', minWidth: 100,
|
||||||
|
cellRenderer: (params: any) => params.value ? '是' : '否'
|
||||||
|
},
|
||||||
{ field: 'major', headerName: '专业', minWidth: 150 },
|
{ field: 'major', headerName: '专业', minWidth: 150 },
|
||||||
{ field: 'foreignLang', headerName: '外语能力', minWidth: 120 }
|
{ field: 'foreignLang', headerName: '外语能力', minWidth: 120 }
|
||||||
]
|
]
|
||||||
|
|
@ -190,8 +249,10 @@ export default function StaffTable() {
|
||||||
{ field: 'trainType', headerName: '培训类型', minWidth: 120 },
|
{ field: 'trainType', headerName: '培训类型', minWidth: 120 },
|
||||||
{ field: 'trainInstitute', headerName: '培训机构', minWidth: 150 },
|
{ field: 'trainInstitute', headerName: '培训机构', minWidth: 150 },
|
||||||
{ field: 'trainMajor', headerName: '培训专业', minWidth: 150 },
|
{ field: 'trainMajor', headerName: '培训专业', minWidth: 150 },
|
||||||
{ field: 'hasTrain', headerName: '是否参加培训', minWidth: 120,
|
{
|
||||||
cellRenderer: (params: any) => params.value ? '是' : '否' }
|
field: 'hasTrain', headerName: '是否参加培训', minWidth: 120,
|
||||||
|
cellRenderer: (params: any) => params.value ? '是' : '否'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -199,8 +260,10 @@ export default function StaffTable() {
|
||||||
children: [
|
children: [
|
||||||
{ field: 'certRank', headerName: '鉴定等级', minWidth: 120 },
|
{ field: 'certRank', headerName: '鉴定等级', minWidth: 120 },
|
||||||
{ field: 'certWork', headerName: '鉴定工种', minWidth: 120 },
|
{ field: 'certWork', headerName: '鉴定工种', minWidth: 120 },
|
||||||
{ field: 'hasCert', headerName: '是否参加鉴定', minWidth: 120,
|
{
|
||||||
cellRenderer: (params: any) => params.value ? '是' : '否' }
|
field: 'hasCert', headerName: '是否参加鉴定', minWidth: 120,
|
||||||
|
cellRenderer: (params: any) => params.value ? '是' : '否'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -263,7 +326,6 @@ export default function StaffTable() {
|
||||||
const defaultColDef: ColDef = {
|
const defaultColDef: ColDef = {
|
||||||
sortable: true,
|
sortable: true,
|
||||||
filter: 'agSetColumnFilter',
|
filter: 'agSetColumnFilter',
|
||||||
// floatingFilter: true,
|
|
||||||
resizable: false,
|
resizable: false,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
|
|
@ -273,7 +335,6 @@ export default function StaffTable() {
|
||||||
whiteSpace: 'normal',
|
whiteSpace: 'normal',
|
||||||
overflowWrap: 'break-word'
|
overflowWrap: 'break-word'
|
||||||
},
|
},
|
||||||
tooltipValueGetter: (params: any) => params.value,
|
|
||||||
wrapText: true,
|
wrapText: true,
|
||||||
autoHeight: true
|
autoHeight: true
|
||||||
};
|
};
|
||||||
|
|
@ -283,17 +344,57 @@ export default function StaffTable() {
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
overflow: 'hidden',
|
|
||||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{!isLoading && (
|
||||||
|
<div className="flex items-center gap-4 mb-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleConfirm}
|
||||||
|
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
导出Excel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleResetFilters}
|
||||||
|
className="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600"
|
||||||
|
>
|
||||||
|
重置筛选
|
||||||
|
</Button>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-gray-600">启用分页:</span>
|
||||||
|
<Switch
|
||||||
|
checked={paginationEnabled}
|
||||||
|
onChange={(checked) => setPaginationEnabled(checked)}
|
||||||
|
checkedChildren="开"
|
||||||
|
unCheckedChildren="关"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="输入文件名"
|
||||||
|
open={fileNameVisible}
|
||||||
|
onOk={handleFileNameConfirm}
|
||||||
|
onCancel={() => setFileNameVisible(false)}
|
||||||
|
okText="导出"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder={`默认名称: ${defaultFileName}`}
|
||||||
|
value={fileName}
|
||||||
|
onChange={(e) => setFileName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="h-full flex items-center justify-center">
|
<div className="h-full flex items-center justify-center">
|
||||||
<div className="text-gray-600 text-xl">加载中...</div>
|
<div className="text-gray-600 text-xl">加载中...</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<AgGridReact
|
<AgGridReact
|
||||||
modules={[SetFilterModule]}
|
modules={[SetFilterModule, ClientSideRowModelModule]}
|
||||||
|
onGridReady={(params) => setGridApi(params.api)} // 添加gridApi回调
|
||||||
rowData={staffs}
|
rowData={staffs}
|
||||||
columnDefs={columnDefs}
|
columnDefs={columnDefs}
|
||||||
defaultColDef={{
|
defaultColDef={{
|
||||||
|
|
@ -302,9 +403,8 @@ export default function StaffTable() {
|
||||||
textCustomComparator: (_, value) => value !== '',
|
textCustomComparator: (_, value) => value !== '',
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
localeText={zhCN} // 注入中文语言包
|
|
||||||
enableCellTextSelection={true}
|
enableCellTextSelection={true}
|
||||||
pagination={true}
|
pagination={paginationEnabled}
|
||||||
paginationAutoPageSize={true}
|
paginationAutoPageSize={true}
|
||||||
cacheQuickFilter={true}
|
cacheQuickFilter={true}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,10 @@ interface UploadResult {
|
||||||
compressedUrl: string;
|
compressedUrl: string;
|
||||||
url: string;
|
url: string;
|
||||||
fileId: string;
|
fileId: string;
|
||||||
|
fileName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTusUpload() {
|
export function useTusUpload(p0: { onSuccess: (result: UploadResult) => void; onError: (error: Error) => void; }) {
|
||||||
const [uploadProgress, setUploadProgress] = useState<
|
const [uploadProgress, setUploadProgress] = useState<
|
||||||
Record<string, number>
|
Record<string, number>
|
||||||
>({});
|
>({});
|
||||||
|
|
@ -87,6 +88,7 @@ export function useTusUpload() {
|
||||||
compressedUrl: getCompressedImageUrl(url),
|
compressedUrl: getCompressedImageUrl(url),
|
||||||
url,
|
url,
|
||||||
fileId,
|
fileId,
|
||||||
|
fileName: uploadFile.name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import DailyPage from "../app/main/daily/page";
|
||||||
import Dashboard from "../app/main/home/page";
|
import Dashboard from "../app/main/home/page";
|
||||||
import WeekPlanPage from "../app/main/plan/weekplan/page";
|
import WeekPlanPage from "../app/main/plan/weekplan/page";
|
||||||
import StaffInformation from "../app/main/staffinformation/page";
|
import StaffInformation from "../app/main/staffinformation/page";
|
||||||
|
import DeptSettingPage from "../app/main/admin/deptsettingpage/page";
|
||||||
interface CustomIndexRouteObject extends IndexRouteObject {
|
interface CustomIndexRouteObject extends IndexRouteObject {
|
||||||
name?: string;
|
name?: string;
|
||||||
breadcrumb?: string;
|
breadcrumb?: string;
|
||||||
|
|
@ -59,6 +60,15 @@ export const routes: CustomRouteObject[] = [
|
||||||
path: "/staff",
|
path: "/staff",
|
||||||
element: <StaffMessage></StaffMessage>,
|
element: <StaffMessage></StaffMessage>,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/admin",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "department",
|
||||||
|
element: <DeptSettingPage></DeptSettingPage>,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path:"/plan",
|
path:"/plan",
|
||||||
children:[
|
children:[
|
||||||
|
|
|
||||||
|
|
@ -449,8 +449,8 @@ model Staff {
|
||||||
rank String? @map("rank") // 衔职级别
|
rank String? @map("rank") // 衔职级别
|
||||||
rankDate DateTime? @map("rank_date") // 衔职时间
|
rankDate DateTime? @map("rank_date") // 衔职时间
|
||||||
proxyPosition String? @map("proxy_position") // 代理职务
|
proxyPosition String? @map("proxy_position") // 代理职务
|
||||||
position Position? @relation("StaffPosition", fields: [positionId], references: [id]) // 岗位
|
post String? @map("post") // 岗位
|
||||||
positionId String? @map("position_id")
|
|
||||||
|
|
||||||
// 入职相关信息
|
// 入职相关信息
|
||||||
hireDate DateTime? @map("hire_date") // 入职时间
|
hireDate DateTime? @map("hire_date") // 入职时间
|
||||||
|
|
@ -501,7 +501,8 @@ model Staff {
|
||||||
enrollments Enrollment[]
|
enrollments Enrollment[]
|
||||||
teachedPosts PostInstructor[]
|
teachedPosts PostInstructor[]
|
||||||
ownedResources Resource[]
|
ownedResources Resource[]
|
||||||
|
position Position? @relation("StaffPosition", fields: [positionId], references: [id])
|
||||||
|
positionId String? @map("position_id")
|
||||||
// 系统信息
|
// 系统信息
|
||||||
registerToken String?
|
registerToken String?
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
@ -535,3 +536,16 @@ model TrainPlan {
|
||||||
|
|
||||||
@@map("train_plan")
|
@@map("train_plan")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model ShareCode {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
code String? @unique
|
||||||
|
fileId String? @unique
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
expiresAt DateTime? @map("expires_at")
|
||||||
|
isUsed Boolean? @default(false)
|
||||||
|
fileName String? @map("file_name")
|
||||||
|
@@index([code])
|
||||||
|
@@index([fileId])
|
||||||
|
@@index([expiresAt])
|
||||||
|
}
|
||||||
|
|
|
||||||
1662
pnpm-lock.yaml
1662
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue