From a13bbdca3e995d92820458791e544d50b7c10742 Mon Sep 17 00:00:00 2001 From: Li1304553726 <1304553726@qq.com> Date: Thu, 27 Mar 2025 08:59:53 +0800 Subject: [PATCH] add --- .../src/models/sys-logs/systemLog.router.ts | 173 ++++++-- .../src/models/sys-logs/systemLog.service.ts | 46 ++ .../staffinfo_write/staffinfo_write.page.tsx | 160 +++++-- .../src/app/main/systemlog/SystemLogPage.tsx | 396 ++---------------- packages/common/prisma/schema.prisma | 2 +- 5 files changed, 344 insertions(+), 433 deletions(-) diff --git a/apps/server/src/models/sys-logs/systemLog.router.ts b/apps/server/src/models/sys-logs/systemLog.router.ts index 3eda41d..2d7d4ca 100644 --- a/apps/server/src/models/sys-logs/systemLog.router.ts +++ b/apps/server/src/models/sys-logs/systemLog.router.ts @@ -30,6 +30,7 @@ export class SystemLogRouter { targetId: z.string().optional(), targetType: z.string().optional(), targetName: z.string().optional(), + message: z.string().optional(), details: z.any().optional(), beforeData: z.any().optional(), afterData: z.any().optional(), @@ -41,24 +42,30 @@ export class SystemLogRouter { 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, - } - }); + try { + 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, + message: input.message, + details: input.details, + beforeData: input.beforeData, + afterData: input.afterData, + status: input.status, + errorMessage: input.errorMessage, + departmentId: input.departmentId, + } + }); + } catch (error) { + console.error('Error creating system log:', error); + throw new Error('Failed to create system log'); + } }), // 查询日志列表 @@ -167,6 +174,7 @@ export class SystemLogRouter { action: z.string(), targetId: z.string(), targetName: z.string(), + message: z.string().optional(), beforeData: z.any().optional(), afterData: z.any().optional(), status: z.enum(['success', 'failure']).default('success'), @@ -176,22 +184,119 @@ export class SystemLogRouter { 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, - } - }); + try { + return this.systemLogService.create({ + data: { + level: input.status === 'success' ? 'info' : 'error', + module: 'staff', + action: input.action, + operatorId: operatorId, + ipAddress: ipAddress, + targetId: input.targetId, + targetType: 'staff', + targetName: input.targetName, + message: input.message, + beforeData: input.beforeData, + afterData: input.afterData, + status: input.status, + errorMessage: input.errorMessage, + } + }); + } catch (error) { + console.error('Error logging staff action:', error); + throw new Error('Failed to log staff action'); + } + }), + + // 高级搜索日志 + 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 }) => { + console.log('Received input for searchLogs:', 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 } }, + ]; + } + + try { + const result = await 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, + } + } + } + }); + console.log('Search logs result:', result); + return result; + } catch (error) { + console.error('Error in searchLogs:', error); + throw new Error('Failed to search logs'); + } }), }) } \ 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 index 6cbad5c..76a7f77 100644 --- a/apps/server/src/models/sys-logs/systemLog.service.ts +++ b/apps/server/src/models/sys-logs/systemLog.service.ts @@ -10,6 +10,18 @@ export class SystemLogService extends BaseService { } async create(args: Prisma.SystemLogCreateArgs) { + // 确保消息字段有值 + if (args.data && typeof args.data === 'object') { + const { level, module, action, targetName } = args.data as any; + const timestamp = new Date().toLocaleString(); + const messagePrefix = level === 'error' ? '错误' : ''; + + // 添加默认消息格式 - 确保 message 字段存在 + if (!args.data.message) { + args.data.message = `[${timestamp}] ${messagePrefix}${module || ''} ${action || ''}: ${targetName || ''}`; + } + } + const result = await super.create(args); this.emitDataChanged(CrudOperation.CREATED, result); return result; @@ -19,6 +31,35 @@ export class SystemLogService extends BaseService { return super.findMany(args); // 放弃分页结构 } + // 添加分页查询方法 + async findManyWithPagination({ page = 1, pageSize = 20, where = {}, ...rest }: any) { + const skip = (page - 1) * pageSize; + + try { + const [items, total] = await Promise.all([ + this.delegate.findMany({ + where, + skip, + take: pageSize, + orderBy: { timestamp: 'desc' }, + ...rest + }), + this.delegate.count({ where }) + ]); + + return { + items, + total, + page, + pageSize, + totalPages: Math.ceil(total / pageSize) + }; + } catch (error) { + console.error('Error in findManyWithPagination:', error); + throw error; + } + } + async logStaffAction( action: string, operatorId: string | null, @@ -34,6 +75,10 @@ export class SystemLogService extends BaseService { const details = beforeData && afterData ? this.generateChangeDetails(beforeData, afterData) : {}; + + const timestamp = new Date().toLocaleString(); + const messagePrefix = status === 'success' ? '' : '错误: '; + const message = `[${timestamp}] ${messagePrefix}用户 ${targetName} 的${action}`; return this.create({ data: { @@ -45,6 +90,7 @@ export class SystemLogService extends BaseService { targetId, targetType: 'staff', targetName, + message, details, beforeData, afterData, 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 9bf7dbd..061faf9 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,6 +1,6 @@ import { Button, Form, Input, Select, DatePicker, Radio, message, Modal, Cascader, InputNumber } from "antd"; import { useState, useMemo, useEffect } from "react"; -import { useStaff } from "@nice/client"; +import { api, useStaff } from "@nice/client"; import { areaOptions } from './area-options'; import InfoCard from './infoCard'; @@ -11,18 +11,18 @@ const StaffInfoWrite = () => { const { create, setCustomFieldValue, useCustomFields } = useStaff(); const { data: fields, isLoading: fieldsLoading } = useCustomFields(); const [infoCards, setInfoCards] = useState([]); - + // 添加跟踪培训和鉴定状态的state const [hasTrain, setHasTrain] = useState(false); const [hasCert, setHasCert] = useState(false); - + // 添加状态来跟踪每个文本区域的高度 const [textAreaHeights, setTextAreaHeights] = useState>({}); - + const handleAdd = (content: string) => { setInfoCards([...infoCards, { content }]); } - + // 在组件中添加监听字段变化 useEffect(() => { // 设置默认值 @@ -30,20 +30,20 @@ const StaffInfoWrite = () => { 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 {}; @@ -72,7 +72,7 @@ const StaffInfoWrite = () => { // 处理培训和鉴定的单选按钮 if (field.name === 'hasTrain') { return ( - { /> ); } - + if (field.name === 'hasCert') { return ( - { /> ); } - + // 检查字段是否应禁用 const isDisabled = shouldFieldBeDisabled(field, groupName); - - + + // 根据字段类型渲染不同的组件 switch (field.type) { case 'text': @@ -126,15 +126,15 @@ const StaffInfoWrite = () => { return ; case 'textarea': return ( -
- { setTextAreaHeights(prev => ({ ...prev, @@ -145,29 +145,26 @@ const StaffInfoWrite = () => {
); case 'cascader': - if(field.label === '籍贯'){ + if (field.label === '籍贯') { return ; - }else{ + } else { return ; } default: return ; } }; - const onFinish = async (e, values: any) => { // values.preventDefault(); e.preventDefault() console.log(values) try { setLoading(true); - // 创建基础员工记录 if (!values.username) { message.error("用户名不能为空"); return; } - // 创建基础员工记录 console.log('准备创建用户,数据:', { username: values.username }); const staff = await create.mutateAsync({ @@ -176,25 +173,42 @@ const StaffInfoWrite = () => { password: '123456' } }); - console.log('创建员工记录:', staff); + console.log('创建员工记录:', staff); + // 创建系统日志记录 + await api.systemLog.create.mutateAsync({ + level: "info", + module: "人员管理", + action: "创建用户", + targetId: staff.id, + targetName: staff.username, + message: `[${new Date().toLocaleString()}] 用户 ${staff.username} 的人员信息已成功添加`, + details: { + fields: validEntries.map(({ field, value }) => ({ + name: field.label, + value + })) + }, + status: "success", + departmentId: staff.deptId // 用户所属部门 + }); // 过滤有效字段并转换值 const validEntries = Object.entries(values) .filter(([key, value]) => key !== 'username' && value !== undefined && value !== null && value !== '') .map(([fieldName, value]) => { const field = fields && Array.isArray(fields) ? fields.find((f: any) => f.name === fieldName) : undefined; let processedValue = value; - + // 处理特殊字段类型 if (field?.type === 'date') { processedValue = value.toString(); } else if (field?.type === 'cascader' && Array.isArray(value)) { processedValue = value.join('/'); } - + return { field, value: processedValue }; }) .filter(item => item.field?.id); // 过滤有效字段定义 - + // 批量提交自定义字段 await Promise.all( validEntries.map(({ field, value }) => @@ -205,12 +219,86 @@ const StaffInfoWrite = () => { }) ) ); - console.log('自定义字段提交成功',staff.username); - + console.log('自定义字段提交成功', staff.username); + // 记录系统日志 - 用户创建成功 + const timestamp = new Date().toLocaleString(); + const logs = []; + // 记录用户创建 + logs.push(`${timestamp} - 用户创建成功:${staff.username}`); + // 记录人员信息添加 + logs.push(`[${timestamp}] 用户 ${staff.username} 的人员信息已成功添加`); + // 记录每个字段的详细信息 + validEntries.forEach(({ field, value }) => { + if (field && field.label && value) { + logs.push(`[${timestamp}] 提交的数据: ${field.label}=${value}`); + } + }); + // 根据字段分组记录 + const fieldsByGroup = validEntries.reduce((groups, { field, value }) => { + if (field && field.group && value) { + if (!groups[field.group]) { + groups[field.group] = []; + } + groups[field.group].push({ field, value }); + } + return groups; + }, {}); + + // 为每个分组记录信息 + Object.entries(fieldsByGroup).forEach(([groupName, fields]) => { + const groupValues = (fields as any[]).map(f => `${f.field.label}=${f.value}`).join(', '); + logs.push(`[${timestamp}] ${staff.username} 的${groupName}:${groupValues || '无'}`); + }); + + // 获取现有日志 + let currentLogs = []; + try { + const storedLogs = localStorage.getItem('systemLogs'); + currentLogs = storedLogs ? JSON.parse(storedLogs) : []; + } catch (error) { + console.error('读取系统日志失败', error); + } + + // 添加新日志(倒序添加,最新的在最前面) + const updatedLogs = [...logs.reverse(), ...currentLogs]; + + // 保存到 localStorage + localStorage.setItem('systemLogs', JSON.stringify(updatedLogs)); + + // 如果有全局变量,也更新它 + if (typeof window !== 'undefined') { + (window as any).globalLogs = updatedLogs; + } + message.success("信息提交成功"); form.resetFields(); } catch (error) { console.error('提交出错:', error); + + // 记录错误日志 + const timestamp = new Date().toLocaleString(); + const logMessage = `${timestamp} - 创建用户失败:${values.username || '未知用户'}, 错误: ${error.message || '未知错误'}`; + + // 获取现有日志 + let currentLogs = []; + try { + const storedLogs = localStorage.getItem('systemLogs'); + currentLogs = storedLogs ? JSON.parse(storedLogs) : []; + } catch (err) { + console.error('读取系统日志失败', err); + } + + // 添加新日志 + const updatedLogs = [logMessage, ...currentLogs]; + + // 保存到 localStorage + localStorage.setItem('systemLogs', JSON.stringify(updatedLogs)); + + // 如果有全局变量,也更新它 + if (typeof window !== 'undefined') { + (window as any).globalLogs = updatedLogs; + } + message.error("提交失败,请重试"); } finally { setLoading(false); @@ -261,7 +349,7 @@ const StaffInfoWrite = () => { ${field.type === 'textarea' ? "transition-all duration-200 ease-in-out" : ""} `} style={{ - minHeight: field.type === 'textarea' && textAreaHeights[field.name] ? + minHeight: field.type === 'textarea' && textAreaHeights[field.name] ? `${textAreaHeights[field.name] + 50}px` : 'auto' }} > @@ -274,8 +362,8 @@ const StaffInfoWrite = () => {
- - ) - } - ]; - - // 显示日志详情的函数 - const showLogDetail = (record: ILog) => { - Modal.info({ - title: '日志详情', - width: 800, - content: ( -
-
-
ID: {record.id}
-
时间: {formatTime(record.timestamp)}
-
模块: {record.module}
-
操作: {record.action}
-
操作人: {record.operator ? (record.operator.showname || record.operator.username) : '-'}
-
IP地址: {record.ipAddress || '-'}
-
操作对象: {record.targetName || '-'}
-
对象类型: {record.targetType || '-'}
-
状态: {record.status === 'success' ? '成功' : '失败'}
- {record.errorMessage &&
错误信息: {record.errorMessage}
} -
- - {(record.beforeData || record.afterData) && ( -
-

数据变更

-
- {record.beforeData && ( -
-
变更前
-
-                                            {JSON.stringify(record.beforeData, null, 2)}
-                                        
-
- )} - {record.afterData && ( -
-
变更后
-
-                                            {JSON.stringify(record.afterData, null, 2)}
-                                        
-
- )} -
-
- )} - - {record.details && ( -
-

详细信息

-
-                                {JSON.stringify(record.details, null, 2)}
-                            
-
- )} -
- ), - onOk() {} - }); - }; - + }, []); return ( -
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - -
-
-
- - - `共 ${total} 条日志` - }} - loading={isLoading} - onChange={handleTableChange} - scroll={{ x: 'max-content' }} - /> - +
+

系统日志

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

暂无系统日志

+ ) : ( +
    + {logs.map((log, index) => ( +
  • + {log} +
  • + ))} +
+ )} +
); }; -export default SystemLogPage; \ No newline at end of file +export default SystemLogPage; diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index b7aa322..5e2aece 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -618,7 +618,7 @@ model SystemLog { // 关联部门 departmentId String? @map("department_id") department Department? @relation(fields: [departmentId], references: [id]) - + message String @map("message") // 完整的日志文本内容 // 优化索引 @@index([timestamp]) @@index([level])