Merge branch 'main' of http://113.45.157.195:3003/linfeng/staff_data
This commit is contained in:
commit
d8d2e9f8e4
|
@ -18,7 +18,7 @@ import { ExceptionsFilter } from './filters/exceptions.filter';
|
||||||
import { TransformModule } from './models/transform/transform.module';
|
import { TransformModule } from './models/transform/transform.module';
|
||||||
import { RealTimeModule } from './socket/realtime/realtime.module';
|
import { RealTimeModule } from './socket/realtime/realtime.module';
|
||||||
import { UploadModule } from './upload/upload.module';
|
import { UploadModule } from './upload/upload.module';
|
||||||
|
import { SystemLogModule } from '@server/models/sys-logs/systemLog.module';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
|
@ -42,7 +42,8 @@ import { UploadModule } from './upload/upload.module';
|
||||||
MinioModule,
|
MinioModule,
|
||||||
CollaborationModule,
|
CollaborationModule,
|
||||||
RealTimeModule,
|
RealTimeModule,
|
||||||
UploadModule
|
UploadModule,
|
||||||
|
SystemLogModule
|
||||||
],
|
],
|
||||||
providers: [{
|
providers: [{
|
||||||
provide: APP_FILTER,
|
provide: APP_FILTER,
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Controller, UseGuards } from "@nestjs/common";
|
||||||
|
import { AuthGuard } from '@server/auth/auth.guard';
|
||||||
|
import { SystemLogService } from "./systemLog.service";
|
||||||
|
|
||||||
|
@Controller('system-logs')
|
||||||
|
export class SystemLogController {
|
||||||
|
constructor(private readonly systemLogService: SystemLogService) {}
|
||||||
|
// @UseGuards(AuthGuard)
|
||||||
|
// 控制器使用trpc路由,不需要在这里定义API端点
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { StaffModule } from '../staff/staff.module';
|
||||||
|
import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
|
import { SystemLogController } from './systemLog.controller';
|
||||||
|
import { SystemLogService } from './systemLog.service';
|
||||||
|
import { SystemLogRouter } from './systemLog.router';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [StaffModule],
|
||||||
|
controllers: [SystemLogController],
|
||||||
|
providers: [SystemLogService, SystemLogRouter, TrpcService],
|
||||||
|
exports: [SystemLogService, SystemLogRouter],
|
||||||
|
})
|
||||||
|
export class SystemLogModule {}
|
|
@ -0,0 +1,283 @@
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { TrpcService } from "@server/trpc/trpc.service";
|
||||||
|
import { SystemLogService } from "./systemLog.service";
|
||||||
|
import { z, ZodType } from "zod";
|
||||||
|
import { Prisma } from "@nice/common";
|
||||||
|
|
||||||
|
// 定义Zod类型Schema
|
||||||
|
const SystemLogCreateArgsSchema: ZodType<Prisma.SystemLogCreateArgs> = z.any();
|
||||||
|
const SystemLogFindManyArgsSchema: ZodType<Prisma.SystemLogFindManyArgs> = z.any();
|
||||||
|
const SystemLogFindUniqueArgsSchema: ZodType<Prisma.SystemLogFindUniqueArgs> = z.any();
|
||||||
|
const SystemLogWhereInputSchema: ZodType<Prisma.SystemLogWhereInput> = z.any();
|
||||||
|
const SystemLogSelectSchema: ZodType<Prisma.SystemLogSelect> = z.any();
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SystemLogRouter {
|
||||||
|
constructor(
|
||||||
|
private readonly trpc: TrpcService,
|
||||||
|
private readonly systemLogService: SystemLogService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
router = this.trpc.router({
|
||||||
|
// 创建日志
|
||||||
|
create: this.trpc.procedure
|
||||||
|
.input(z.object({
|
||||||
|
level: z.enum(['info', 'warning', 'error', 'debug']).default('info'),
|
||||||
|
module: z.string(),
|
||||||
|
action: z.string(),
|
||||||
|
operatorId: z.string().optional(),
|
||||||
|
ipAddress: z.string().optional(),
|
||||||
|
targetId: z.string().optional(),
|
||||||
|
targetType: z.string().optional(),
|
||||||
|
targetName: z.string().optional(),
|
||||||
|
details: z.any().optional(),
|
||||||
|
beforeData: z.any().optional(),
|
||||||
|
afterData: z.any().optional(),
|
||||||
|
status: z.enum(['success', 'failure']).default('success'),
|
||||||
|
errorMessage: z.string().optional(),
|
||||||
|
departmentId: z.string().optional(),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const ctxIpAddress = ctx.ip;
|
||||||
|
const operatorId = ctx.staff?.id;
|
||||||
|
|
||||||
|
return this.systemLogService.create({
|
||||||
|
data: {
|
||||||
|
level: input.level,
|
||||||
|
module: input.module,
|
||||||
|
action: input.action,
|
||||||
|
operatorId: input.operatorId || operatorId,
|
||||||
|
ipAddress: input.ipAddress || ctxIpAddress,
|
||||||
|
targetId: input.targetId,
|
||||||
|
targetType: input.targetType,
|
||||||
|
targetName: input.targetName,
|
||||||
|
details: input.details,
|
||||||
|
beforeData: input.beforeData,
|
||||||
|
afterData: input.afterData,
|
||||||
|
status: input.status,
|
||||||
|
errorMessage: input.errorMessage,
|
||||||
|
departmentId: input.departmentId,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 查询日志列表
|
||||||
|
findMany: this.trpc.procedure
|
||||||
|
.input(SystemLogFindManyArgsSchema)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return this.systemLogService.findMany(input);
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 查询日志列表(带分页) - 保留原名
|
||||||
|
getLogs: this.trpc.procedure
|
||||||
|
.input(z.object({
|
||||||
|
page: z.number().default(1),
|
||||||
|
pageSize: z.number().default(20),
|
||||||
|
where: SystemLogWhereInputSchema.optional(),
|
||||||
|
select: SystemLogSelectSchema.optional(),
|
||||||
|
}))
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
const { page, pageSize, where = {}, select } = input;
|
||||||
|
|
||||||
|
return await this.systemLogService.findManyWithPagination({
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
where,
|
||||||
|
...(select ? { select } : {})
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in getLogs:', error);
|
||||||
|
// 返回空结果,避免崩溃
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
total: 0,
|
||||||
|
page: input.page,
|
||||||
|
pageSize: input.pageSize,
|
||||||
|
totalPages: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 查询日志列表(带分页) - 新名称
|
||||||
|
findManyWithPagination: this.trpc.procedure
|
||||||
|
.input(z.object({
|
||||||
|
page: z.number().default(1),
|
||||||
|
pageSize: z.number().default(20),
|
||||||
|
where: SystemLogWhereInputSchema.optional(),
|
||||||
|
select: SystemLogSelectSchema.optional(),
|
||||||
|
}))
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
const { page, pageSize, where = {}, select } = input;
|
||||||
|
|
||||||
|
return await this.systemLogService.findManyWithPagination({
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
where,
|
||||||
|
...(select ? { select } : {})
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in findManyWithPagination:', error);
|
||||||
|
// 返回空结果,避免崩溃
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
total: 0,
|
||||||
|
page: input.page,
|
||||||
|
pageSize: input.pageSize,
|
||||||
|
totalPages: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 获取单个日志详情
|
||||||
|
findUnique: this.trpc.procedure
|
||||||
|
.input(SystemLogFindUniqueArgsSchema)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return this.systemLogService.findUnique(input);
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 通过ID获取日志详情(简化版)
|
||||||
|
findById: this.trpc.procedure
|
||||||
|
.input(z.string())
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return this.systemLogService.findUnique({
|
||||||
|
where: { id: input },
|
||||||
|
include: {
|
||||||
|
operator: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
showname: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
department: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 记录人员操作日志的便捷方法
|
||||||
|
logStaffAction: this.trpc.protectProcedure
|
||||||
|
.input(z.object({
|
||||||
|
action: z.string(),
|
||||||
|
targetId: z.string(),
|
||||||
|
targetName: z.string(),
|
||||||
|
beforeData: z.any().optional(),
|
||||||
|
afterData: z.any().optional(),
|
||||||
|
status: z.enum(['success', 'failure']).default('success'),
|
||||||
|
errorMessage: z.string().optional(),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const ipAddress = ctx.ip;
|
||||||
|
const operatorId = ctx.staff?.id;
|
||||||
|
|
||||||
|
return this.systemLogService.create({
|
||||||
|
data: {
|
||||||
|
level: 'info',
|
||||||
|
module: 'staff',
|
||||||
|
action: input.action,
|
||||||
|
operatorId: operatorId,
|
||||||
|
ipAddress: ipAddress,
|
||||||
|
targetId: input.targetId,
|
||||||
|
targetType: 'staff',
|
||||||
|
targetName: input.targetName,
|
||||||
|
beforeData: input.beforeData,
|
||||||
|
afterData: input.afterData,
|
||||||
|
status: input.status,
|
||||||
|
errorMessage: input.errorMessage,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 高级搜索日志
|
||||||
|
searchLogs: this.trpc.procedure
|
||||||
|
.input(z.object({
|
||||||
|
page: z.number().default(1),
|
||||||
|
pageSize: z.number().default(20),
|
||||||
|
level: z.enum(['info', 'warning', 'error', 'debug']).optional(),
|
||||||
|
module: z.string().optional(),
|
||||||
|
action: z.string().optional(),
|
||||||
|
operatorId: z.string().optional(),
|
||||||
|
targetId: z.string().optional(),
|
||||||
|
targetType: z.string().optional(),
|
||||||
|
status: z.enum(['success', 'failure']).optional(),
|
||||||
|
startTime: z.string().optional(),
|
||||||
|
endTime: z.string().optional(),
|
||||||
|
keyword: z.string().optional(),
|
||||||
|
departmentId: z.string().optional(),
|
||||||
|
}))
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
// 构建查询条件
|
||||||
|
const where: Prisma.SystemLogWhereInput = {};
|
||||||
|
|
||||||
|
if (input.level) where.level = input.level;
|
||||||
|
if (input.module) where.module = input.module;
|
||||||
|
if (input.action) where.action = input.action;
|
||||||
|
if (input.operatorId) where.operatorId = input.operatorId;
|
||||||
|
if (input.targetId) where.targetId = input.targetId;
|
||||||
|
if (input.targetType) where.targetType = input.targetType;
|
||||||
|
if (input.status) where.status = input.status;
|
||||||
|
if (input.departmentId) where.departmentId = input.departmentId;
|
||||||
|
|
||||||
|
// 时间范围查询
|
||||||
|
if (input.startTime || input.endTime) {
|
||||||
|
where.timestamp = {};
|
||||||
|
if (input.startTime) where.timestamp.gte = new Date(input.startTime);
|
||||||
|
if (input.endTime) where.timestamp.lte = new Date(input.endTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关键词搜索
|
||||||
|
if (input.keyword) {
|
||||||
|
where.OR = [
|
||||||
|
{ targetName: { contains: input.keyword } },
|
||||||
|
{ action: { contains: input.keyword } },
|
||||||
|
{ module: { contains: input.keyword } },
|
||||||
|
{ errorMessage: { contains: input.keyword } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用select代替include
|
||||||
|
return this.systemLogService.findManyWithPagination({
|
||||||
|
page: input.page,
|
||||||
|
pageSize: input.pageSize,
|
||||||
|
where,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
level: true,
|
||||||
|
module: true,
|
||||||
|
action: true,
|
||||||
|
timestamp: true,
|
||||||
|
operatorId: true,
|
||||||
|
ipAddress: true,
|
||||||
|
targetId: true,
|
||||||
|
targetType: true,
|
||||||
|
targetName: true,
|
||||||
|
details: true,
|
||||||
|
beforeData: true,
|
||||||
|
afterData: true,
|
||||||
|
status: true,
|
||||||
|
errorMessage: true,
|
||||||
|
departmentId: true,
|
||||||
|
operator: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
showname: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
department: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { BaseService } from "../base/base.service";
|
||||||
|
import { db, ObjectType, Prisma } from "@nice/common";
|
||||||
|
import EventBus, { CrudOperation } from "@server/utils/event-bus";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SystemLogService extends BaseService<Prisma.SystemLogDelegate> {
|
||||||
|
constructor() {
|
||||||
|
super(db, ObjectType.SYSTEM_LOG, false); // 不自动处理更新时间和删除时间
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(args: Prisma.SystemLogCreateArgs) {
|
||||||
|
const result = await super.create(args);
|
||||||
|
this.emitDataChanged(CrudOperation.CREATED, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findMany(args: Prisma.SystemLogFindManyArgs): Promise<Prisma.SystemLogGetPayload<{}>[]> {
|
||||||
|
return super.findMany(args); // 放弃分页结构
|
||||||
|
}
|
||||||
|
|
||||||
|
async logStaffAction(
|
||||||
|
action: string,
|
||||||
|
operatorId: string | null,
|
||||||
|
ipAddress: string | null,
|
||||||
|
targetId: string,
|
||||||
|
targetName: string,
|
||||||
|
beforeData: any = null,
|
||||||
|
afterData: any = null,
|
||||||
|
status: 'success' | 'failure' = 'success',
|
||||||
|
errorMessage?: string
|
||||||
|
) {
|
||||||
|
// 生成变更详情
|
||||||
|
const details = beforeData && afterData
|
||||||
|
? this.generateChangeDetails(beforeData, afterData)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return this.create({
|
||||||
|
data: {
|
||||||
|
level: status === 'success' ? 'info' : 'error',
|
||||||
|
module: '人员管理',
|
||||||
|
action,
|
||||||
|
operatorId,
|
||||||
|
ipAddress,
|
||||||
|
targetId,
|
||||||
|
targetType: 'staff',
|
||||||
|
targetName,
|
||||||
|
details,
|
||||||
|
beforeData,
|
||||||
|
afterData,
|
||||||
|
status,
|
||||||
|
errorMessage,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成变更详情
|
||||||
|
*/
|
||||||
|
private generateChangeDetails(before: any, after: any) {
|
||||||
|
if (!before || !after) return {};
|
||||||
|
|
||||||
|
const changes: Record<string, { oldValue: any; newValue: any }> = {};
|
||||||
|
|
||||||
|
Object.keys(after).forEach(key => {
|
||||||
|
// 忽略一些不需要记录的字段
|
||||||
|
if (['password', 'createdAt', 'updatedAt', 'deletedAt'].includes(key)) return;
|
||||||
|
|
||||||
|
if (JSON.stringify(before[key]) !== JSON.stringify(after[key])) {
|
||||||
|
changes[key] = {
|
||||||
|
oldValue: before[key],
|
||||||
|
newValue: after[key]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { changes };
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitDataChanged(operation: CrudOperation, data: any) {
|
||||||
|
EventBus.emit('dataChanged', {
|
||||||
|
type: ObjectType.SYSTEM_LOG,
|
||||||
|
operation,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ import { TrainContentModule } from '@server/models/train-content/trainContent.mo
|
||||||
import { ResourceModule } from '@server/models/resource/resource.module';
|
import { ResourceModule } from '@server/models/resource/resource.module';
|
||||||
import { TrainSituationModule } from '@server/models/train-situation/trainSituation.module';
|
import { TrainSituationModule } from '@server/models/train-situation/trainSituation.module';
|
||||||
import { DailyTrainModule } from '@server/models/daily-train/dailyTrain.module';
|
import { DailyTrainModule } from '@server/models/daily-train/dailyTrain.module';
|
||||||
|
import { SystemLogModule } from '@server/models/sys-logs/systemLog.module';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
AuthModule,
|
AuthModule,
|
||||||
|
@ -37,6 +38,7 @@ import { DailyTrainModule } from '@server/models/daily-train/dailyTrain.module';
|
||||||
TrainContentModule,
|
TrainContentModule,
|
||||||
TrainSituationModule,
|
TrainSituationModule,
|
||||||
DailyTrainModule,
|
DailyTrainModule,
|
||||||
|
SystemLogModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [TrpcService, TrpcRouter, Logger],
|
providers: [TrpcService, TrpcRouter, Logger],
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { ResourceRouter } from '../models/resource/resource.router';
|
||||||
import { TrainContentRouter } from '@server/models/train-content/trainContent.router';
|
import { TrainContentRouter } from '@server/models/train-content/trainContent.router';
|
||||||
import { TrainSituationRouter } from '@server/models/train-situation/trainSituation.router';
|
import { TrainSituationRouter } from '@server/models/train-situation/trainSituation.router';
|
||||||
import { DailyTrainRouter } from '@server/models/daily-train/dailyTrain.router';
|
import { DailyTrainRouter } from '@server/models/daily-train/dailyTrain.router';
|
||||||
|
import { SystemLogRouter } from '@server/models/sys-logs/systemLog.router';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TrpcRouter {
|
export class TrpcRouter {
|
||||||
|
@ -38,6 +39,7 @@ export class TrpcRouter {
|
||||||
private readonly trainContent: TrainContentRouter,
|
private readonly trainContent: TrainContentRouter,
|
||||||
private readonly trainSituation:TrainSituationRouter,
|
private readonly trainSituation:TrainSituationRouter,
|
||||||
private readonly dailyTrain:DailyTrainRouter,
|
private readonly dailyTrain:DailyTrainRouter,
|
||||||
|
private readonly systemLogRouter: SystemLogRouter,
|
||||||
) {}
|
) {}
|
||||||
getRouter() {
|
getRouter() {
|
||||||
return;
|
return;
|
||||||
|
@ -57,7 +59,8 @@ export class TrpcRouter {
|
||||||
resource: this.resource.router,
|
resource: this.resource.router,
|
||||||
trainContent:this.trainContent.router,
|
trainContent:this.trainContent.router,
|
||||||
trainSituation:this.trainSituation.router,
|
trainSituation:this.trainSituation.router,
|
||||||
dailyTrain:this.dailyTrain.router
|
dailyTrain:this.dailyTrain.router,
|
||||||
|
systemLog: this.systemLogRouter.router,
|
||||||
});
|
});
|
||||||
wss: WebSocketServer = undefined;
|
wss: WebSocketServer = undefined;
|
||||||
|
|
||||||
|
|
|
@ -255,7 +255,11 @@ export default function StaffTable() {
|
||||||
{ field: 'trainMajor', headerName: '培训专业', },
|
{ field: 'trainMajor', headerName: '培训专业', },
|
||||||
{
|
{
|
||||||
field: 'hasTrain', headerName: '是否参加培训',
|
field: 'hasTrain', headerName: '是否参加培训',
|
||||||
cellRenderer: (params: any) => params.value ? '是' : '否'
|
cellRenderer: (params: any) =>(
|
||||||
|
<div>
|
||||||
|
<Input value="false" disabled />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -266,7 +270,11 @@ export default function StaffTable() {
|
||||||
{ field: 'certWork', headerName: '鉴定工种', },
|
{ field: 'certWork', headerName: '鉴定工种', },
|
||||||
{
|
{
|
||||||
field: 'hasCert', headerName: '是否参加鉴定',
|
field: 'hasCert', headerName: '是否参加鉴定',
|
||||||
cellRenderer: (params: any) => params.value ? '是' : '否'
|
cellRenderer: (params: any) =>(
|
||||||
|
<div>
|
||||||
|
<Input value="false" disabled />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,12 +1,37 @@
|
||||||
import { Input, Button, Space } from 'antd';
|
import { Input, Button } from 'antd';
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
|
||||||
type InfoCardProps = {
|
type InfoCardProps = {
|
||||||
onAdd: (content: string) => void;
|
onAdd: (content: string) => void;
|
||||||
|
onHeightChange?: (height: number) => void; // 添加高度变化回调
|
||||||
}
|
}
|
||||||
const InfoCard: React.FC<InfoCardProps> = ({ onAdd }) => {
|
|
||||||
|
const InfoCard: React.FC<InfoCardProps> = ({ onAdd, onHeightChange }) => {
|
||||||
const [content, setContent] = useState('');
|
const [content, setContent] = useState('');
|
||||||
const [addedContents, setAddedContents] = useState<string[]>([]);
|
const [addedContents, setAddedContents] = useState<string[]>([]);
|
||||||
|
const contentContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// 监控内容区域高度变化并通知父组件
|
||||||
|
useEffect(() => {
|
||||||
|
if (!contentContainerRef.current) return;
|
||||||
|
|
||||||
|
// 使用ResizeObserver监控元素大小变化
|
||||||
|
const resizeObserver = new ResizeObserver(entries => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
const height = entry.contentRect.height;
|
||||||
|
// 通知父组件高度变化
|
||||||
|
onHeightChange && onHeightChange(height + 20); // 添加一些缓冲空间
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.observe(contentContainerRef.current);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (contentContainerRef.current) {
|
||||||
|
resizeObserver.unobserve(contentContainerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [addedContents, onHeightChange]);
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
if (content) {
|
if (content) {
|
||||||
|
@ -15,23 +40,39 @@ const InfoCard: React.FC<InfoCardProps> = ({ onAdd }) => {
|
||||||
setContent('');
|
setContent('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// 增大内边距,避免内容被覆盖
|
<div className="w-full">
|
||||||
<div style={{ border: '1px solid #d9d9d9', padding: '24px' }}>
|
<div className="flex items-center mb-3 w-full">
|
||||||
<Space>
|
|
||||||
<Input
|
<Input
|
||||||
placeholder='请输入内容'
|
placeholder='请输入内容'
|
||||||
value={content}
|
value={content}
|
||||||
onChange={(e) => setContent(e.target.value)}
|
onChange={(e) => setContent(e.target.value)}
|
||||||
|
className="flex-1 mr-2"
|
||||||
|
onPressEnter={handleAdd}
|
||||||
/>
|
/>
|
||||||
<Button type='primary' onClick={handleAdd}>添加</Button>
|
<Button
|
||||||
</Space>
|
type='primary'
|
||||||
<div style={{ marginTop: '24px' }}>
|
onClick={handleAdd}
|
||||||
|
className="shrink-0"
|
||||||
|
>
|
||||||
|
添加
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内容容器 */}
|
||||||
|
<div ref={contentContainerRef} className="w-full bg-white">
|
||||||
{addedContents.map((item, index) => (
|
{addedContents.map((item, index) => (
|
||||||
<div key={index}>{item}</div>
|
<div
|
||||||
|
key={index}
|
||||||
|
className="p-2 border border-gray-200 rounded bg-gray-50 mb-2 last:mb-0"
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default InfoCard;
|
export default InfoCard;
|
|
@ -1,5 +1,5 @@
|
||||||
import { Button, Form, Input, Select, DatePicker, Radio, message, Modal, Cascader, InputNumber } from "antd";
|
import { Button, Form, Input, Select, DatePicker, Radio, message, Modal, Cascader, InputNumber } from "antd";
|
||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo, useEffect } from "react";
|
||||||
import { useStaff } from "@nice/client";
|
import { useStaff } from "@nice/client";
|
||||||
import { areaOptions } from './area-options';
|
import { areaOptions } from './area-options';
|
||||||
import InfoCard from './infoCard';
|
import InfoCard from './infoCard';
|
||||||
|
@ -11,9 +11,39 @@ const StaffInfoWrite = () => {
|
||||||
const { create, setCustomFieldValue, useCustomFields } = useStaff();
|
const { create, setCustomFieldValue, useCustomFields } = useStaff();
|
||||||
const { data: fields, isLoading: fieldsLoading } = useCustomFields();
|
const { data: fields, isLoading: fieldsLoading } = useCustomFields();
|
||||||
const [infoCards, setInfoCards] = useState<any[]>([]);
|
const [infoCards, setInfoCards] = useState<any[]>([]);
|
||||||
const handleAdd = ( content: string) => {
|
|
||||||
|
// 添加跟踪培训和鉴定状态的state
|
||||||
|
const [hasTrain, setHasTrain] = useState(false);
|
||||||
|
const [hasCert, setHasCert] = useState(false);
|
||||||
|
|
||||||
|
// 添加状态来跟踪每个文本区域的高度
|
||||||
|
const [textAreaHeights, setTextAreaHeights] = useState<Record<string, number>>({});
|
||||||
|
|
||||||
|
const handleAdd = (content: string) => {
|
||||||
setInfoCards([...infoCards, { content }]);
|
setInfoCards([...infoCards, { content }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 在组件中添加监听字段变化
|
||||||
|
useEffect(() => {
|
||||||
|
// 设置默认值
|
||||||
|
form.setFieldsValue({
|
||||||
|
hasTrain: false,
|
||||||
|
hasCert: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 使用 Form 的 onValuesChange 在外部监听
|
||||||
|
const fieldChangeHandler = () => {
|
||||||
|
const values = form.getFieldsValue(['hasTrain', 'hasCert']);
|
||||||
|
setHasTrain(!!values.hasTrain);
|
||||||
|
setHasCert(!!values.hasCert);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化时执行一次
|
||||||
|
fieldChangeHandler();
|
||||||
|
|
||||||
|
// 不需要返回取消订阅,因为我们不再使用 subscribe
|
||||||
|
}, [form]);
|
||||||
|
|
||||||
// 按分组组织字段
|
// 按分组组织字段
|
||||||
const fieldGroups = useMemo(() => {
|
const fieldGroups = useMemo(() => {
|
||||||
if (!fields) return {};
|
if (!fields) return {};
|
||||||
|
@ -27,34 +57,103 @@ const StaffInfoWrite = () => {
|
||||||
}, {});
|
}, {});
|
||||||
}, [fields]);
|
}, [fields]);
|
||||||
|
|
||||||
const renderField = (field: any) => {
|
// 检查字段是否应该被禁用的辅助函数
|
||||||
|
const shouldFieldBeDisabled = (field: any, groupName: string) => {
|
||||||
|
if (groupName === '培训信息' && !hasTrain) {
|
||||||
|
return ['trainType', 'trainInstitute', 'trainMajor'].includes(field.name);
|
||||||
|
}
|
||||||
|
if (groupName === '鉴定信息' && !hasCert) {
|
||||||
|
return ['certRank', 'certWork'].includes(field.name);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderField = (field: any, groupName: string) => {
|
||||||
|
// 处理培训和鉴定的单选按钮
|
||||||
|
if (field.name === 'hasTrain') {
|
||||||
|
return (
|
||||||
|
<Radio.Group
|
||||||
|
options={[
|
||||||
|
{ label: '是', value: true },
|
||||||
|
{ label: '否', value: false }
|
||||||
|
]}
|
||||||
|
onChange={(e) => {
|
||||||
|
setHasTrain(e.target.value);
|
||||||
|
form.setFieldsValue({ hasTrain: e.target.value });
|
||||||
|
}}
|
||||||
|
value={hasTrain}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.name === 'hasCert') {
|
||||||
|
return (
|
||||||
|
<Radio.Group
|
||||||
|
options={[
|
||||||
|
{ label: '是', value: true },
|
||||||
|
{ label: '否', value: false }
|
||||||
|
]}
|
||||||
|
onChange={(e) => {
|
||||||
|
setHasCert(e.target.value);
|
||||||
|
form.setFieldsValue({ hasCert: e.target.value });
|
||||||
|
}}
|
||||||
|
value={hasCert}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查字段是否应禁用
|
||||||
|
const isDisabled = shouldFieldBeDisabled(field, groupName);
|
||||||
|
|
||||||
|
// 如果是工作信息相关的字段,添加额外的类名
|
||||||
|
const isWorkInfo = groupName === "工作信息";
|
||||||
|
|
||||||
|
// 根据字段类型渲染不同的组件
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
case 'text':
|
case 'text':
|
||||||
return <Input />;
|
return <Input disabled={isDisabled} />;
|
||||||
case 'number':
|
case 'number':
|
||||||
return <InputNumber />;
|
return <InputNumber disabled={isDisabled} />;
|
||||||
case 'date':
|
case 'date':
|
||||||
return <DatePicker />;
|
return <DatePicker disabled={isDisabled} />;
|
||||||
case 'select':
|
case 'select':
|
||||||
if (field.options && field.options.length > 0) {
|
if (field.options && field.options.length > 0) {
|
||||||
return <Select options={field.options} />;
|
return <Select options={field.options} disabled={isDisabled} />;
|
||||||
}
|
}
|
||||||
return <Input placeholder="选项数据缺失" />;
|
return <Input placeholder="选项数据缺失" disabled={isDisabled} />;
|
||||||
case 'radio':
|
case 'radio':
|
||||||
if (field.options && field.options.length > 0) {
|
if (field.options && field.options.length > 0) {
|
||||||
return <Radio.Group options={field.options} />;
|
return <Radio.Group options={field.options} disabled={isDisabled} />;
|
||||||
}
|
}
|
||||||
return <Input placeholder="选项数据缺失" />;
|
return <Input placeholder="选项数据缺失" disabled={isDisabled} />;
|
||||||
case 'textarea':
|
case 'textarea':
|
||||||
return <InfoCard onAdd={handleAdd} />;
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full relative"
|
||||||
|
style={{
|
||||||
|
minHeight: textAreaHeights[field.name] ?
|
||||||
|
`${textAreaHeights[field.name]}px` : 'auto'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InfoCard
|
||||||
|
onAdd={handleAdd}
|
||||||
|
onHeightChange={(height) => {
|
||||||
|
setTextAreaHeights(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field.name]: height
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
case 'cascader':
|
case 'cascader':
|
||||||
if(field.label === '籍贯'){
|
if(field.label === '籍贯'){
|
||||||
return <Cascader options={areaOptions} />;
|
return <Cascader options={areaOptions} disabled={isDisabled} />;
|
||||||
}else{
|
}else{
|
||||||
return <Input />;
|
return <Input disabled={isDisabled} />;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return <Input />;
|
return <Input className="w-full" disabled={isDisabled} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -120,16 +219,27 @@ const StaffInfoWrite = () => {
|
||||||
form={form}
|
form={form}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
onFinish={onFinish}
|
onFinish={onFinish}
|
||||||
|
onValuesChange={(changedValues) => {
|
||||||
|
if ('hasTrain' in changedValues) {
|
||||||
|
setHasTrain(!!changedValues.hasTrain);
|
||||||
|
}
|
||||||
|
if ('hasCert' in changedValues) {
|
||||||
|
setHasCert(!!changedValues.hasCert);
|
||||||
|
}
|
||||||
|
}}
|
||||||
className="space-y-6 mt-6"
|
className="space-y-6 mt-6"
|
||||||
>
|
>
|
||||||
{Object.entries(fieldGroups).map(([groupName, groupFields]) => (
|
{Object.entries(fieldGroups).map(([groupName, groupFields]) => (
|
||||||
<div key={groupName} className="bg-white p-6 rounded-lg shadow">
|
<div key={groupName} className="bg-white p-6 rounded-lg shadow relative">
|
||||||
<h2 className="text-lg font-semibold mb-4">{groupName}</h2>
|
<h2 className="text-lg font-semibold mb-4">{groupName}</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className={`
|
||||||
|
grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-12
|
||||||
|
${groupName === "工作信息" ? "md:[&>*:nth-child(odd)]:col-start-1 md:[&>*:nth-child(even)]:col-start-2" : ""}
|
||||||
|
`}>
|
||||||
{groupFields.map((field: any) => (
|
{groupFields.map((field: any) => (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
key={field.id}
|
key={field.id}
|
||||||
label={field.label}
|
label={<span className="block mb-2">{field.label}</span>}
|
||||||
name={field.name}
|
name={field.name}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
|
@ -137,8 +247,16 @@ const StaffInfoWrite = () => {
|
||||||
message: `请输入${field.label}`,
|
message: `请输入${field.label}`,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
className={`
|
||||||
|
${field.type === 'textarea' && groupName !== "工作信息" ? "md:col-span-2" : ""}
|
||||||
|
${field.type === 'textarea' ? "transition-all duration-200 ease-in-out" : ""}
|
||||||
|
`}
|
||||||
|
style={{
|
||||||
|
minHeight: field.type === 'textarea' && textAreaHeights[field.name] ?
|
||||||
|
`${textAreaHeights[field.name] + 50}px` : 'auto'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{renderField(field)}
|
{renderField(field, groupName)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,13 +5,10 @@ import { useState } from "react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useStaff } from "@nice/client";
|
import { useStaff } from "@nice/client";
|
||||||
import DepartmentChildrenSelect from "@web/src/components/models/department/department-children-select";
|
import DepartmentChildrenSelect from "@web/src/components/models/department/department-children-select";
|
||||||
import { areaOptions } from './area-options';
|
import { areaOptions } from '../staffinfo_write/area-options';
|
||||||
import DepartmentSelect from "@web/src/components/models/department/department-select";
|
import DepartmentSelect from "@web/src/components/models/department/department-select";
|
||||||
import { addLog } from "@web/src/app/main/systemlog/SystemLogPage";
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const StaffInformation = () => {
|
const StaffInformation = () => {
|
||||||
const [modalForm] = Form.useForm();
|
const [modalForm] = Form.useForm();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
@ -61,8 +58,10 @@ const StaffInformation = () => {
|
||||||
message.warning('请输入内容');
|
message.warning('请输入内容');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFinish = async (values: any) => {
|
const onFinish = async (values: any) => {
|
||||||
console.log('开始提交表单');
|
console.log('开始提交表单');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const formattedValues = {
|
const formattedValues = {
|
||||||
|
@ -75,7 +74,7 @@ const StaffInformation = () => {
|
||||||
hireDate: values.hireDate?.toISOString(),
|
hireDate: values.hireDate?.toISOString(),
|
||||||
seniority: values.seniority?.toISOString(),
|
seniority: values.seniority?.toISOString(),
|
||||||
currentPositionDate: values.currentPositionDate?.toISOString(),
|
currentPositionDate: values.currentPositionDate?.toISOString(),
|
||||||
rankDate: values.rankDate?.toISOString(),
|
rankDate: values.rankDate?.toISOString(), // 修改这里
|
||||||
};
|
};
|
||||||
|
|
||||||
await create.mutateAsync(
|
await create.mutateAsync(
|
||||||
|
@ -88,31 +87,9 @@ const StaffInformation = () => {
|
||||||
console.log('奖励列表:', rewardsList);
|
console.log('奖励列表:', rewardsList);
|
||||||
console.log('处分列表:', punishmentsList);
|
console.log('处分列表:', punishmentsList);
|
||||||
|
|
||||||
// 添加日志记录
|
|
||||||
addLog(`用户 ${values.username || '未知'} 的人员信息已成功添加`);
|
|
||||||
addLog(`提交的数据: 姓名=${values.username}, 身份证号=${values.idNumber}, 警号=${values.officerId}, 部门ID=${values.deptId}`);
|
|
||||||
|
|
||||||
if (rewardsList.length > 0) {
|
|
||||||
addLog(`${values.username} 的奖励信息: ${rewardsList.join(' | ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (punishmentsList.length > 0) {
|
|
||||||
addLog(`${values.username} 的处分信息: ${punishmentsList.join(' | ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (equipmentList.length > 0) {
|
|
||||||
addLog(`${values.username} 的装备信息: ${equipmentList.join(' | ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (projectsList.length > 0) {
|
|
||||||
addLog(`${values.username} 的任务信息: ${projectsList.join(' | ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
message.success("信息提交成功");
|
message.success("信息提交成功");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('提交出错:', error);
|
console.error('提交出错:', error);
|
||||||
// 添加错误日志
|
|
||||||
addLog(`提交人员信息失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
|
||||||
message.error("提交失败,请重试");
|
message.error("提交失败,请重试");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
|
@ -1,47 +1,247 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Card, Table, Space, Tag, Form, Select, DatePicker, Input, Button } from 'antd';
|
||||||
|
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { api } from '@nice/client';
|
||||||
|
|
||||||
// 创建一个全局变量来存储日志
|
const { RangePicker } = DatePicker;
|
||||||
let globalLogs: string[] = [];
|
const { Option } = Select;
|
||||||
|
|
||||||
|
// 日志接口定义
|
||||||
|
interface ILog {
|
||||||
|
id: string;
|
||||||
|
timestamp: string;
|
||||||
|
level: string;
|
||||||
|
module: string;
|
||||||
|
action: string;
|
||||||
|
operatorId?: string;
|
||||||
|
operator?: {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
showname?: string;
|
||||||
|
};
|
||||||
|
ipAddress?: string;
|
||||||
|
targetId?: string;
|
||||||
|
targetType?: string;
|
||||||
|
targetName?: string;
|
||||||
|
details?: any;
|
||||||
|
beforeData?: any;
|
||||||
|
afterData?: any;
|
||||||
|
status: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// 添加日志的函数
|
// 添加日志的函数
|
||||||
export const addLog = (log: string) => {
|
export const addLog = async (logData: Omit<ILog, 'id' | 'timestamp'>) => {
|
||||||
const timestamp = new Date().toLocaleString();
|
try {
|
||||||
const formattedLog = `[${timestamp}] ${log}`;
|
// 使用tRPC发送日志
|
||||||
globalLogs = [...globalLogs, formattedLog];
|
const result = await api.systemLog.create.mutate({
|
||||||
// 如果需要,可以将日志保存到localStorage
|
...logData,
|
||||||
localStorage.setItem('systemLogs', JSON.stringify(globalLogs));
|
level: logData.level || 'info',
|
||||||
|
status: logData.status || 'success',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('日志已写入数据库:', result);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('写入日志失败:', error);
|
||||||
|
// 可以考虑添加本地缓存逻辑
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 用于添加人员操作日志的便捷方法
|
||||||
|
export const addStaffLog = async (
|
||||||
|
action: string,
|
||||||
|
targetId: string,
|
||||||
|
targetName: string,
|
||||||
|
beforeData: any = null,
|
||||||
|
afterData: any = null,
|
||||||
|
status: 'success' | 'failure' = 'success',
|
||||||
|
errorMessage?: string
|
||||||
|
) => {
|
||||||
|
return api.systemLog.logStaffAction.mutate({
|
||||||
|
action,
|
||||||
|
targetId,
|
||||||
|
targetName,
|
||||||
|
beforeData,
|
||||||
|
afterData,
|
||||||
|
status,
|
||||||
|
errorMessage
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const SystemLogPage = () => {
|
const SystemLogPage = () => {
|
||||||
const [logs, setLogs] = useState<string[]>([]);
|
const [form] = Form.useForm();
|
||||||
// 组件加载时从全局变量或localStorage获取日志
|
const [queryParams, setQueryParams] = useState({
|
||||||
useEffect(() => {
|
page: 1,
|
||||||
// 尝试从localStorage获取日志
|
pageSize: 10,
|
||||||
const storedLogs = localStorage.getItem('systemLogs');
|
});
|
||||||
if (storedLogs) {
|
|
||||||
setLogs(JSON.parse(storedLogs));
|
// 使用tRPC查询日志
|
||||||
} else {
|
const { data, isLoading, refetch } = api.systemLog.getLogs.useQuery({
|
||||||
setLogs(globalLogs);
|
page: queryParams.page,
|
||||||
|
pageSize: queryParams.pageSize
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理表格分页变化
|
||||||
|
const handleTableChange = (pagination: any) => {
|
||||||
|
setQueryParams({
|
||||||
|
...queryParams,
|
||||||
|
page: pagination.current,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理表单查询
|
||||||
|
const handleSearch = (values: any) => {
|
||||||
|
const { timeRange, ...rest } = values;
|
||||||
|
|
||||||
|
const params: any = {
|
||||||
|
...rest,
|
||||||
|
page: 1, // 重置到第一页
|
||||||
|
pageSize: queryParams.pageSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理时间范围
|
||||||
|
if (timeRange && timeRange.length === 2) {
|
||||||
|
params.startTime = timeRange[0].startOf('day').toISOString();
|
||||||
|
params.endTime = timeRange[1].endOf('day').toISOString();
|
||||||
}
|
}
|
||||||
}, []);
|
|
||||||
|
setQueryParams(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化时间显示
|
||||||
|
const formatTime = (timeStr: string) => {
|
||||||
|
return dayjs(timeStr).format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '操作时间',
|
||||||
|
dataIndex: 'timestamp',
|
||||||
|
key: 'timestamp',
|
||||||
|
render: (text: string) => formatTime(text)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '级别',
|
||||||
|
dataIndex: 'level',
|
||||||
|
key: 'level',
|
||||||
|
render: (text: string) => (
|
||||||
|
<Tag color={
|
||||||
|
text === 'info' ? 'blue' :
|
||||||
|
text === 'warning' ? 'orange' :
|
||||||
|
text === 'error' ? 'red' : 'green'
|
||||||
|
}>
|
||||||
|
{text.toUpperCase()}
|
||||||
|
</Tag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '模块',
|
||||||
|
dataIndex: 'module',
|
||||||
|
key: 'module',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
dataIndex: 'action',
|
||||||
|
key: 'action',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作人',
|
||||||
|
key: 'operator',
|
||||||
|
render: (_, record: ILog) => (
|
||||||
|
record.operator ? record.operator.showname || record.operator.username : '-'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作对象',
|
||||||
|
dataIndex: 'targetName',
|
||||||
|
key: 'targetName',
|
||||||
|
render: (text: string) => text || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
render: (text: string) => (
|
||||||
|
<Tag color={text === 'success' ? 'green' : 'red'}>
|
||||||
|
{text === 'success' ? '成功' : '失败'}
|
||||||
|
</Tag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto p-6">
|
<div className="space-y-4">
|
||||||
<h1 className="text-2xl font-bold mb-6">系统日志</h1>
|
<Card>
|
||||||
<div className="bg-white p-6 rounded-lg shadow">
|
<Form
|
||||||
{logs.length === 0 ? (
|
form={form}
|
||||||
<p className="text-gray-500">暂无系统日志</p>
|
layout="vertical"
|
||||||
) : (
|
onFinish={handleSearch}
|
||||||
<ul className="space-y-2">
|
className="mb-4"
|
||||||
{logs.map((log, index) => (
|
>
|
||||||
<li key={index} className="p-2 border-b border-gray-200">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
{log}
|
<Form.Item name="keyword" label="关键词">
|
||||||
</li>
|
<Input placeholder="请输入关键词" allowClear />
|
||||||
))}
|
</Form.Item>
|
||||||
</ul>
|
|
||||||
)}
|
<Form.Item name="level" label="日志级别">
|
||||||
|
<Select placeholder="请选择级别" allowClear>
|
||||||
|
<Option value="info">信息</Option>
|
||||||
|
<Option value="warning">警告</Option>
|
||||||
|
<Option value="error">错误</Option>
|
||||||
|
<Option value="debug">调试</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="status" label="状态">
|
||||||
|
<Select placeholder="请选择状态" allowClear>
|
||||||
|
<Option value="success">成功</Option>
|
||||||
|
<Option value="failure">失败</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="timeRange" label="时间范围">
|
||||||
|
<RangePicker className="w-full" />
|
||||||
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Space>
|
||||||
|
<Button onClick={() => form.resetFields()}>重置</Button>
|
||||||
|
<Button type="primary" htmlType="submit" icon={<SearchOutlined />}>
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={() => refetch()}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={data?.items || []}
|
||||||
|
rowKey="id"
|
||||||
|
pagination={{
|
||||||
|
current: data?.pagination?.current || 1,
|
||||||
|
pageSize: data?.pagination?.pageSize || 10,
|
||||||
|
total: data?.pagination?.total || 0,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total) => `共 ${total} 条日志`
|
||||||
|
}}
|
||||||
|
loading={isLoading}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -412,6 +412,7 @@ model Department {
|
||||||
|
|
||||||
// watchedPost Post[] @relation("post_watch_dept")
|
// watchedPost Post[] @relation("post_watch_dept")
|
||||||
hasChildren Boolean? @default(false) @map("has_children")
|
hasChildren Boolean? @default(false) @map("has_children")
|
||||||
|
logs SystemLog[]
|
||||||
|
|
||||||
@@index([parentId])
|
@@index([parentId])
|
||||||
@@index([isDomain])
|
@@index([isDomain])
|
||||||
|
@ -432,11 +433,11 @@ model StaffField {
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
deletedAt DateTime? @map("deleted_at")
|
deletedAt DateTime? @map("deleted_at")
|
||||||
|
StaffFieldValue StaffFieldValue[]
|
||||||
|
|
||||||
@@index([group])
|
@@index([group])
|
||||||
@@index([order])
|
@@index([order])
|
||||||
@@map("staff_field")
|
@@map("staff_field")
|
||||||
StaffFieldValue StaffFieldValue[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model StaffFieldValue {
|
model StaffFieldValue {
|
||||||
|
@ -464,7 +465,6 @@ model Staff {
|
||||||
avatar String? @map("avatar")
|
avatar String? @map("avatar")
|
||||||
enabled Boolean? @default(true)
|
enabled Boolean? @default(true)
|
||||||
|
|
||||||
|
|
||||||
// 个人基本信息
|
// 个人基本信息
|
||||||
idNumber String? @unique @map("id_number") // 身份证号
|
idNumber String? @unique @map("id_number") // 身份证号
|
||||||
type String? @map("type") // 人员类型
|
type String? @map("type") // 人员类型
|
||||||
|
@ -486,7 +486,6 @@ model Staff {
|
||||||
proxyPosition String? @map("proxy_position") // 代理职务
|
proxyPosition String? @map("proxy_position") // 代理职务
|
||||||
post String? @map("post") // 岗位
|
post String? @map("post") // 岗位
|
||||||
|
|
||||||
|
|
||||||
// 入职相关信息
|
// 入职相关信息
|
||||||
hireDate DateTime? @map("hire_date") // 入职时间
|
hireDate DateTime? @map("hire_date") // 入职时间
|
||||||
seniority DateTime? @map("seniority_date") // 工龄认定时间
|
seniority DateTime? @map("seniority_date") // 工龄认定时间
|
||||||
|
@ -544,7 +543,8 @@ model Staff {
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
deletedAt DateTime? @map("deleted_at")
|
deletedAt DateTime? @map("deleted_at")
|
||||||
absent Boolean? @default(false) @map("absent")
|
absent Boolean? @default(false) @map("absent")
|
||||||
|
// 系统日志
|
||||||
|
logs SystemLog[] @relation("log_operator")
|
||||||
// 添加自定义字段值关联
|
// 添加自定义字段值关联
|
||||||
fieldValues StaffFieldValue[]
|
fieldValues StaffFieldValue[]
|
||||||
|
|
||||||
|
@ -583,7 +583,49 @@ model ShareCode {
|
||||||
expiresAt DateTime? @map("expires_at")
|
expiresAt DateTime? @map("expires_at")
|
||||||
isUsed Boolean? @default(false)
|
isUsed Boolean? @default(false)
|
||||||
fileName String? @map("file_name")
|
fileName String? @map("file_name")
|
||||||
|
|
||||||
@@index([code])
|
@@index([code])
|
||||||
@@index([fileId])
|
@@index([fileId])
|
||||||
@@index([expiresAt])
|
@@index([expiresAt])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model SystemLog {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
timestamp DateTime @default(now()) @map("timestamp")
|
||||||
|
level String? @map("level") // info, warning, error, debug
|
||||||
|
module String? @map("module") // 操作模块,如"人员管理"
|
||||||
|
action String? @map("action") // 具体操作,如"新增人员"、"修改人员"
|
||||||
|
|
||||||
|
// 操作人信息
|
||||||
|
operatorId String? @map("operator_id")
|
||||||
|
operator Staff? @relation("log_operator", fields: [operatorId], references: [id])
|
||||||
|
ipAddress String? @map("ip_address")
|
||||||
|
|
||||||
|
// 操作对象信息
|
||||||
|
targetId String? @map("target_id") // 操作对象ID
|
||||||
|
targetType String? @map("target_type") // 操作对象类型,如"staff"、"department"
|
||||||
|
targetName String? @map("target_name") // 操作对象名称
|
||||||
|
|
||||||
|
// 详细信息
|
||||||
|
details Json? @map("details") // 详细变更信息,存储为JSON
|
||||||
|
beforeData Json? @map("before_data") // 操作前数据
|
||||||
|
afterData Json? @map("after_data") // 操作后数据
|
||||||
|
|
||||||
|
// 操作结果
|
||||||
|
status String? @map("status") // success, failure
|
||||||
|
errorMessage String? @map("error_message") // 如果操作失败,记录错误信息
|
||||||
|
|
||||||
|
// 关联部门
|
||||||
|
departmentId String? @map("department_id")
|
||||||
|
department Department? @relation(fields: [departmentId], references: [id])
|
||||||
|
|
||||||
|
// 优化索引
|
||||||
|
@@index([timestamp])
|
||||||
|
@@index([level])
|
||||||
|
@@index([module, action])
|
||||||
|
@@index([operatorId])
|
||||||
|
@@index([targetId, targetType])
|
||||||
|
@@index([status])
|
||||||
|
@@index([departmentId])
|
||||||
|
@@map("system_log")
|
||||||
|
}
|
||||||
|
|
|
@ -61,7 +61,8 @@ export enum ObjectType {
|
||||||
RESOURCE = "resource",
|
RESOURCE = "resource",
|
||||||
TRAIN_CONTENT = "trainContent",
|
TRAIN_CONTENT = "trainContent",
|
||||||
TRAIN_SITUATION = "trainSituation",
|
TRAIN_SITUATION = "trainSituation",
|
||||||
DAILY_TRAIN = "dailyTrainTime"
|
DAILY_TRAIN = "dailyTrainTime",
|
||||||
|
SYSTEM_LOG = 'system_log'
|
||||||
}
|
}
|
||||||
export enum RolePerms {
|
export enum RolePerms {
|
||||||
// Create Permissions 创建权限
|
// Create Permissions 创建权限
|
||||||
|
|
Loading…
Reference in New Issue