diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index 3bc892f..9906ea5 100755 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -18,7 +18,7 @@ import { ExceptionsFilter } from './filters/exceptions.filter'; import { TransformModule } from './models/transform/transform.module'; import { RealTimeModule } from './socket/realtime/realtime.module'; import { UploadModule } from './upload/upload.module'; - +import { SystemLogModule } from '@server/models/sys-logs/systemLog.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -42,7 +42,8 @@ import { UploadModule } from './upload/upload.module'; MinioModule, CollaborationModule, RealTimeModule, - UploadModule + UploadModule, + SystemLogModule ], providers: [{ provide: APP_FILTER, diff --git a/apps/server/src/models/sys-logs/systemLog.controller.ts b/apps/server/src/models/sys-logs/systemLog.controller.ts new file mode 100644 index 0000000..0c3931b --- /dev/null +++ b/apps/server/src/models/sys-logs/systemLog.controller.ts @@ -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端点 +} \ No newline at end of file diff --git a/apps/server/src/models/sys-logs/systemLog.module.ts b/apps/server/src/models/sys-logs/systemLog.module.ts new file mode 100644 index 0000000..3529c01 --- /dev/null +++ b/apps/server/src/models/sys-logs/systemLog.module.ts @@ -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 {} \ No newline at end of file diff --git a/apps/server/src/models/sys-logs/systemLog.router.ts b/apps/server/src/models/sys-logs/systemLog.router.ts new file mode 100644 index 0000000..218d5ec --- /dev/null +++ b/apps/server/src/models/sys-logs/systemLog.router.ts @@ -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 = z.any(); +const SystemLogFindManyArgsSchema: ZodType = z.any(); +const SystemLogFindUniqueArgsSchema: ZodType = z.any(); +const SystemLogWhereInputSchema: ZodType = z.any(); +const SystemLogSelectSchema: ZodType = 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, + } + } + } + }); + }), + }) +} \ No newline at end of file diff --git a/apps/server/src/models/sys-logs/systemLog.service.ts b/apps/server/src/models/sys-logs/systemLog.service.ts new file mode 100644 index 0000000..6cbad5c --- /dev/null +++ b/apps/server/src/models/sys-logs/systemLog.service.ts @@ -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 { + 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[]> { + 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 = {}; + + 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, + }); + } +} \ No newline at end of file diff --git a/apps/server/src/trpc/trpc.module.ts b/apps/server/src/trpc/trpc.module.ts index 2a209eb..13bf946 100755 --- a/apps/server/src/trpc/trpc.module.ts +++ b/apps/server/src/trpc/trpc.module.ts @@ -18,6 +18,7 @@ import { TrainContentModule } from '@server/models/train-content/trainContent.mo import { ResourceModule } from '@server/models/resource/resource.module'; import { TrainSituationModule } from '@server/models/train-situation/trainSituation.module'; import { DailyTrainModule } from '@server/models/daily-train/dailyTrain.module'; +import { SystemLogModule } from '@server/models/sys-logs/systemLog.module'; @Module({ imports: [ AuthModule, @@ -37,6 +38,7 @@ import { DailyTrainModule } from '@server/models/daily-train/dailyTrain.module'; TrainContentModule, TrainSituationModule, DailyTrainModule, + SystemLogModule, ], controllers: [], providers: [TrpcService, TrpcRouter, Logger], diff --git a/apps/server/src/trpc/trpc.router.ts b/apps/server/src/trpc/trpc.router.ts index 959c61c..258652d 100755 --- a/apps/server/src/trpc/trpc.router.ts +++ b/apps/server/src/trpc/trpc.router.ts @@ -17,6 +17,7 @@ import { ResourceRouter } from '../models/resource/resource.router'; import { TrainContentRouter } from '@server/models/train-content/trainContent.router'; import { TrainSituationRouter } from '@server/models/train-situation/trainSituation.router'; import { DailyTrainRouter } from '@server/models/daily-train/dailyTrain.router'; +import { SystemLogRouter } from '@server/models/sys-logs/systemLog.router'; @Injectable() export class TrpcRouter { @@ -38,6 +39,7 @@ export class TrpcRouter { private readonly trainContent: TrainContentRouter, private readonly trainSituation:TrainSituationRouter, private readonly dailyTrain:DailyTrainRouter, + private readonly systemLogRouter: SystemLogRouter, ) {} getRouter() { return; @@ -57,7 +59,8 @@ export class TrpcRouter { resource: this.resource.router, trainContent:this.trainContent.router, trainSituation:this.trainSituation.router, - dailyTrain:this.dailyTrain.router + dailyTrain:this.dailyTrain.router, + systemLog: this.systemLogRouter.router, }); wss: WebSocketServer = undefined; diff --git a/apps/web/src/app/main/staffinfo_show/stafftable/page.tsx b/apps/web/src/app/main/staffinfo_show/stafftable/page.tsx index 0e14359..3463aec 100644 --- a/apps/web/src/app/main/staffinfo_show/stafftable/page.tsx +++ b/apps/web/src/app/main/staffinfo_show/stafftable/page.tsx @@ -255,7 +255,11 @@ export default function StaffTable() { { field: 'trainMajor', headerName: '培训专业', }, { field: 'hasTrain', headerName: '是否参加培训', - cellRenderer: (params: any) => params.value ? '是' : '否' + cellRenderer: (params: any) =>( +
+ +
+ ) } ] }, @@ -266,7 +270,11 @@ export default function StaffTable() { { field: 'certWork', headerName: '鉴定工种', }, { field: 'hasCert', headerName: '是否参加鉴定', - cellRenderer: (params: any) => params.value ? '是' : '否' + cellRenderer: (params: any) =>( +
+ +
+ ) } ] }, diff --git a/apps/web/src/app/main/staffinfo_write/infoCard.tsx b/apps/web/src/app/main/staffinfo_write/infoCard.tsx index 33373f2..6e72cf5 100644 --- a/apps/web/src/app/main/staffinfo_write/infoCard.tsx +++ b/apps/web/src/app/main/staffinfo_write/infoCard.tsx @@ -1,13 +1,38 @@ -import { Input, Button, Space } from 'antd'; -import React, { useState } from 'react'; +import { Input, Button } from 'antd'; +import React, { useState, useRef, useEffect } from 'react'; type InfoCardProps = { onAdd: (content: string) => void; + onHeightChange?: (height: number) => void; // 添加高度变化回调 } -const InfoCard: React.FC = ({ onAdd }) => { + +const InfoCard: React.FC = ({ onAdd, onHeightChange }) => { const [content, setContent] = useState(''); const [addedContents, setAddedContents] = useState([]); - + const contentContainerRef = useRef(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 = () => { if (content) { onAdd(content); @@ -15,23 +40,39 @@ const InfoCard: React.FC = ({ onAdd }) => { setContent(''); } } + return ( - // 增大内边距,避免内容被覆盖 -
- +
+
setContent(e.target.value)} + className="flex-1 mr-2" + onPressEnter={handleAdd} /> - - -
+ +
+ + {/* 内容容器 */} +
{addedContents.map((item, index) => ( -
{item}
+
+ {item} +
))}
); } + export default InfoCard; \ No newline at end of file diff --git a/apps/web/src/app/main/staffinfo_write/staffinfo_write.page.tsx b/apps/web/src/app/main/staffinfo_write/staffinfo_write.page.tsx index 0f03bb2..083c82a 100644 --- a/apps/web/src/app/main/staffinfo_write/staffinfo_write.page.tsx +++ b/apps/web/src/app/main/staffinfo_write/staffinfo_write.page.tsx @@ -1,7 +1,7 @@ "use client"; 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 { areaOptions } from './area-options'; import InfoCard from './infoCard'; @@ -12,9 +12,39 @@ const StaffInfoWrite = () => { const { create, useCustomFields } = useStaff(); const { data: fields, isLoading: fieldsLoading } = useCustomFields(); const [infoCards, setInfoCards] = useState([]); - const handleAdd = ( content: string) => { + + // 添加跟踪培训和鉴定状态的state + const [hasTrain, setHasTrain] = useState(false); + const [hasCert, setHasCert] = useState(false); + + // 添加状态来跟踪每个文本区域的高度 + const [textAreaHeights, setTextAreaHeights] = useState>({}); + + const handleAdd = (content: string) => { 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(() => { if (!fields) return {}; @@ -28,34 +58,103 @@ const StaffInfoWrite = () => { }, {}); }, [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 ( + { + setHasTrain(e.target.value); + form.setFieldsValue({ hasTrain: e.target.value }); + }} + value={hasTrain} + /> + ); + } + + if (field.name === 'hasCert') { + return ( + { + setHasCert(e.target.value); + form.setFieldsValue({ hasCert: e.target.value }); + }} + value={hasCert} + /> + ); + } + + // 检查字段是否应禁用 + const isDisabled = shouldFieldBeDisabled(field, groupName); + + // 如果是工作信息相关的字段,添加额外的类名 + const isWorkInfo = groupName === "工作信息"; + + // 根据字段类型渲染不同的组件 switch (field.type) { case 'text': - return ; + return ; case 'number': - return ; + return ; case 'date': - return ; + return ; case 'select': if (field.options && field.options.length > 0) { - return ; } - return ; + return ; case 'radio': if (field.options && field.options.length > 0) { - return ; + return ; } - return ; + return ; case 'textarea': - return ; + return ( +
+ { + setTextAreaHeights(prev => ({ + ...prev, + [field.name]: height + })); + }} + /> +
+ ); case 'cascader': if(field.label === '籍贯'){ - return ; + return ; }else{ - return ; + return ; } default: - return ; + return ; } }; @@ -92,16 +191,27 @@ const StaffInfoWrite = () => { form={form} layout="vertical" onFinish={onFinish} + onValuesChange={(changedValues) => { + if ('hasTrain' in changedValues) { + setHasTrain(!!changedValues.hasTrain); + } + if ('hasCert' in changedValues) { + setHasCert(!!changedValues.hasCert); + } + }} className="space-y-6 mt-6" > {Object.entries(fieldGroups).map(([groupName, groupFields]) => ( -
+

{groupName}

-
+
*:nth-child(odd)]:col-start-1 md:[&>*:nth-child(even)]:col-start-2" : ""} + `}> {groupFields.map((field: any) => ( {field.label}} name={field.name} rules={[ { @@ -109,8 +219,16 @@ const StaffInfoWrite = () => { 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)} ))}
diff --git a/apps/web/src/app/main/staffinformation/page.tsx b/apps/web/src/app/main/staffinformation/page.tsx index dc558a6..5c7c50c 100644 --- a/apps/web/src/app/main/staffinformation/page.tsx +++ b/apps/web/src/app/main/staffinformation/page.tsx @@ -5,13 +5,10 @@ import { useState } from "react"; import dayjs from "dayjs"; import { useStaff } from "@nice/client"; 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 { addLog } from "@web/src/app/main/systemlog/SystemLogPage"; const { TextArea } = Input; - - const StaffInformation = () => { const [modalForm] = Form.useForm(); const [form] = Form.useForm(); @@ -23,7 +20,7 @@ const StaffInformation = () => { const [equipmentList, setEquipmentList] = useState([]); // 新增装备列表 const [projectsList, setProjectsList] = useState([]); // 新增任务列表 const {create, update} = useStaff(); - + const showModal = (type: 'awards' | 'punishments' | 'equipment' | 'projects') => { setModalType(type); setIsModalVisible(true); @@ -61,8 +58,10 @@ const StaffInformation = () => { message.warning('请输入内容'); } }; + const onFinish = async (values: any) => { console.log('开始提交表单'); + try { setLoading(true); const formattedValues = { @@ -75,7 +74,7 @@ const StaffInformation = () => { hireDate: values.hireDate?.toISOString(), seniority: values.seniority?.toISOString(), currentPositionDate: values.currentPositionDate?.toISOString(), - rankDate: values.rankDate?.toISOString(), + rankDate: values.rankDate?.toISOString(), // 修改这里 }; await create.mutateAsync( @@ -88,31 +87,9 @@ const StaffInformation = () => { console.log('奖励列表:', rewardsList); 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("信息提交成功"); } catch (error) { console.error('提交出错:', error); - // 添加错误日志 - addLog(`提交人员信息失败: ${error instanceof Error ? error.message : '未知错误'}`); message.error("提交失败,请重试"); } finally { setLoading(false); diff --git a/apps/web/src/app/main/systemlog/SystemLogPage.tsx b/apps/web/src/app/main/systemlog/SystemLogPage.tsx index a0bbe4a..a8a2b75 100644 --- a/apps/web/src/app/main/systemlog/SystemLogPage.tsx +++ b/apps/web/src/app/main/systemlog/SystemLogPage.tsx @@ -1,49 +1,249 @@ "use client"; 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'; -// 创建一个全局变量来存储日志 -let globalLogs: string[] = []; +const { RangePicker } = DatePicker; +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) => { - const timestamp = new Date().toLocaleString(); - const formattedLog = `[${timestamp}] ${log}`; - globalLogs = [...globalLogs, formattedLog]; - // 如果需要,可以将日志保存到localStorage - localStorage.setItem('systemLogs', JSON.stringify(globalLogs)); +export const addLog = async (logData: Omit) => { + try { + // 使用tRPC发送日志 + const result = await api.systemLog.create.mutate({ + ...logData, + 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 [logs, setLogs] = useState([]); - // 组件加载时从全局变量或localStorage获取日志 - useEffect(() => { - // 尝试从localStorage获取日志 - const storedLogs = localStorage.getItem('systemLogs'); - if (storedLogs) { - setLogs(JSON.parse(storedLogs)); - } else { - setLogs(globalLogs); - } - }, []); - return ( -
-

系统日志

-
- {logs.length === 0 ? ( -

暂无系统日志

- ) : ( -
    - {logs.map((log, index) => ( -
  • - {log} -
  • - ))} -
- )} -
-
- ); + const [form] = Form.useForm(); + const [queryParams, setQueryParams] = useState({ + page: 1, + pageSize: 10, + }); + + // 使用tRPC查询日志 + const { data, isLoading, refetch } = api.systemLog.getLogs.useQuery({ + 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) => ( + + {text.toUpperCase()} + + ) + }, + { + 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) => ( + + {text === 'success' ? '成功' : '失败'} + + ) + }, + ]; + + return ( +
+ +
+
+ + + + + + + + + + + + + + + +
+ +
+ + + + + +
+
+
+ + + `共 ${total} 条日志` + }} + loading={isLoading} + onChange={handleTableChange} + /> + + + ); }; -export default SystemLogPage; +export default SystemLogPage; \ No newline at end of file diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index 2437871..b7aa322 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -411,7 +411,8 @@ model Department { trainPlans TrainPlan[] @relation("TrainPlanDept") // 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([isDomain]) @@ -421,33 +422,33 @@ model Department { } model StaffField { - id String @id @default(cuid()) - name String @unique // 字段名称 - label String? // 字段显示名称 - type String // 字段类型 (text, number, date, select 等) - required Boolean? @default(false) - order Float? // 显示顺序 - options Json? // 对于选择类型字段的可选值 - group String? // 字段分组 (基本信息、工作信息等) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") + id String @id @default(cuid()) + name String @unique // 字段名称 + label String? // 字段显示名称 + type String // 字段类型 (text, number, date, select 等) + required Boolean? @default(false) + order Float? // 显示顺序 + options Json? // 对于选择类型字段的可选值 + group String? // 字段分组 (基本信息、工作信息等) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + StaffFieldValue StaffFieldValue[] @@index([group]) @@index([order]) @@map("staff_field") - StaffFieldValue StaffFieldValue[] } model StaffFieldValue { - id String @id @default(cuid()) - staffId String @map("staff_id") - fieldId String @map("field_id") - value String? // 字段值 - staff Staff @relation(fields: [staffId], references: [id]) - field StaffField @relation(fields: [fieldId], references: [id]) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + id String @id @default(cuid()) + staffId String @map("staff_id") + fieldId String @map("field_id") + value String? // 字段值 + staff Staff @relation(fields: [staffId], references: [id]) + field StaffField @relation(fields: [fieldId], references: [id]) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") @@unique([staffId, fieldId]) @@index([staffId]) @@ -457,96 +458,95 @@ model StaffFieldValue { model Staff { // 基础信息 - id String @id @default(cuid()) - username String @unique @map("username") - password String? @map("password") - showname String? @map("showname") - avatar String? @map("avatar") - enabled Boolean? @default(true) - + id String @id @default(cuid()) + username String @unique @map("username") + password String? @map("password") + showname String? @map("showname") + avatar String? @map("avatar") + enabled Boolean? @default(true) // 个人基本信息 - idNumber String? @unique @map("id_number") // 身份证号 - type String? @map("type") // 人员类型 - officerId String? @unique @map("officer_id") // 警号 - phoneNumber String? @unique @map("phone_number") // 手机号 - age Int? @map("age") // 年龄 - sex Boolean? @map("sex") // 性别 - bloodType String? @map("blood_type") // 血型 - birthplace String? @map("birthplace") // 籍贯 - source String? @map("source") // 来源 + idNumber String? @unique @map("id_number") // 身份证号 + type String? @map("type") // 人员类型 + officerId String? @unique @map("officer_id") // 警号 + phoneNumber String? @unique @map("phone_number") // 手机号 + age Int? @map("age") // 年龄 + sex Boolean? @map("sex") // 性别 + bloodType String? @map("blood_type") // 血型 + birthplace String? @map("birthplace") // 籍贯 + source String? @map("source") // 来源 // 政治信息 - politicalStatus String? @map("political_status") // 政治面貌 - partyPosition String? @map("party_position") // 党内职务 + politicalStatus String? @map("political_status") // 政治面貌 + partyPosition String? @map("party_position") // 党内职务 // 职务信息 - rank String? @map("rank") // 衔职级别 - rankDate DateTime? @map("rank_date") // 衔职时间 - proxyPosition String? @map("proxy_position") // 代理职务 - post String? @map("post") // 岗位 - + rank String? @map("rank") // 衔职级别 + rankDate DateTime? @map("rank_date") // 衔职时间 + proxyPosition String? @map("proxy_position") // 代理职务 + post String? @map("post") // 岗位 // 入职相关信息 - hireDate DateTime? @map("hire_date") // 入职时间 - seniority DateTime? @map("seniority_date") // 工龄认定时间 - sourceType String? @map("source_type") // 来源类型 - isReentry Boolean? @default(false) @map("is_reentry") // 是否二次入职 - isExtended Boolean? @default(false) @map("is_extended") // 是否延期服役 + hireDate DateTime? @map("hire_date") // 入职时间 + seniority DateTime? @map("seniority_date") // 工龄认定时间 + sourceType String? @map("source_type") // 来源类型 + isReentry Boolean? @default(false) @map("is_reentry") // 是否二次入职 + isExtended Boolean? @default(false) @map("is_extended") // 是否延期服役 currentPositionDate DateTime? @map("current_position_date") // 现岗位开始时间 // 教育背景 - education String? @map("education") // 学历 - educationType String? @map("education_type") // 学历形式 - isGraduated Boolean? @default(true) @map("is_graduated") // 是否毕业 - major String? @map("major") // 专业 - foreignLang String? @map("foreign_lang") // 外语能力 + education String? @map("education") // 学历 + educationType String? @map("education_type") // 学历形式 + isGraduated Boolean? @default(true) @map("is_graduated") // 是否毕业 + major String? @map("major") // 专业 + foreignLang String? @map("foreign_lang") // 外语能力 // 培训 - trainType String? @map("train_type") // 培训类型 - trainInstitute String? @map("train_institute") // 培训机构 - trainMajor String? @map("train_major") // 培训专业 - hasTrain Boolean? @default(false) @map("has_train") // 是否参加培训 + trainType String? @map("train_type") // 培训类型 + trainInstitute String? @map("train_institute") // 培训机构 + trainMajor String? @map("train_major") // 培训专业 + hasTrain Boolean? @default(false) @map("has_train") // 是否参加培训 //鉴定 - certRank String? @map("cert_rank") // 鉴定等级 - certWork String? @map("cert_work") // 鉴定工种 - hasCert Boolean? @default(false) @map("has_cert") // 是否参加鉴定 + certRank String? @map("cert_rank") // 鉴定等级 + certWork String? @map("cert_work") // 鉴定工种 + hasCert Boolean? @default(false) @map("has_cert") // 是否参加鉴定 // 工作信息 - equipment String? @map("equipment") // 操作维护装备 - projects String? @map("projects") // 演训任务经历 - awards String? @map("awards") // 奖励信息 - punishments String? @map("staff_punishments") // 处分信息 + equipment String? @map("equipment") // 操作维护装备 + projects String? @map("projects") // 演训任务经历 + awards String? @map("awards") // 奖励信息 + punishments String? @map("staff_punishments") // 处分信息 // 部门关系 - domainId String? @map("domain_id") - deptId String? @map("dept_id") - domain Department? @relation("DomainStaff", fields: [domainId], references: [id]) - department Department? @relation("DeptStaff", fields: [deptId], references: [id]) - order Float? + domainId String? @map("domain_id") + deptId String? @map("dept_id") + domain Department? @relation("DomainStaff", fields: [domainId], references: [id]) + department Department? @relation("DeptStaff", fields: [deptId], references: [id]) + order Float? // 关联关系 trainSituations TrainSituation[] visits Visit[] posts Post[] - learningPosts Post[] @relation("post_student") - sentMsgs Message[] @relation("message_sender") - receivedMsgs Message[] @relation("message_receiver") + learningPosts Post[] @relation("post_student") + sentMsgs Message[] @relation("message_sender") + receivedMsgs Message[] @relation("message_receiver") enrollments Enrollment[] teachedPosts PostInstructor[] ownedResources Resource[] - position Position? @relation("StaffPosition", fields: [positionId], references: [id]) - positionId String? @map("position_id") + position Position? @relation("StaffPosition", fields: [positionId], references: [id]) + positionId String? @map("position_id") // 系统信息 - registerToken String? - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - absent Boolean? @default(false) @map("absent") - + registerToken String? + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + absent Boolean? @default(false) @map("absent") + // 系统日志 + logs SystemLog[] @relation("log_operator") // 添加自定义字段值关联 - fieldValues StaffFieldValue[] + fieldValues StaffFieldValue[] @@index([officerId]) @@index([deptId]) @@ -576,14 +576,56 @@ model TrainPlan { } 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]) + 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]) +} + +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") } diff --git a/packages/common/src/enum.ts b/packages/common/src/enum.ts index 4fc900a..897a3bb 100755 --- a/packages/common/src/enum.ts +++ b/packages/common/src/enum.ts @@ -61,7 +61,8 @@ export enum ObjectType { RESOURCE = "resource", TRAIN_CONTENT = "trainContent", TRAIN_SITUATION = "trainSituation", - DAILY_TRAIN = "dailyTrainTime" + DAILY_TRAIN = "dailyTrainTime", + SYSTEM_LOG = 'system_log' } export enum RolePerms { // Create Permissions 创建权限