This commit is contained in:
Li1304553726 2025-03-27 08:59:53 +08:00
parent f9e46f3884
commit a13bbdca3e
5 changed files with 344 additions and 433 deletions

View File

@ -30,6 +30,7 @@ export class SystemLogRouter {
targetId: z.string().optional(), targetId: z.string().optional(),
targetType: z.string().optional(), targetType: z.string().optional(),
targetName: z.string().optional(), targetName: z.string().optional(),
message: z.string().optional(),
details: z.any().optional(), details: z.any().optional(),
beforeData: z.any().optional(), beforeData: z.any().optional(),
afterData: z.any().optional(), afterData: z.any().optional(),
@ -41,24 +42,30 @@ export class SystemLogRouter {
const ctxIpAddress = ctx.ip; const ctxIpAddress = ctx.ip;
const operatorId = ctx.staff?.id; const operatorId = ctx.staff?.id;
return this.systemLogService.create({ try {
data: { return this.systemLogService.create({
level: input.level, data: {
module: input.module, level: input.level,
action: input.action, module: input.module,
operatorId: input.operatorId || operatorId, action: input.action,
ipAddress: input.ipAddress || ctxIpAddress, operatorId: input.operatorId || operatorId,
targetId: input.targetId, ipAddress: input.ipAddress || ctxIpAddress,
targetType: input.targetType, targetId: input.targetId,
targetName: input.targetName, targetType: input.targetType,
details: input.details, targetName: input.targetName,
beforeData: input.beforeData, message: input.message,
afterData: input.afterData, details: input.details,
status: input.status, beforeData: input.beforeData,
errorMessage: input.errorMessage, afterData: input.afterData,
departmentId: input.departmentId, 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(), action: z.string(),
targetId: z.string(), targetId: z.string(),
targetName: z.string(), targetName: z.string(),
message: z.string().optional(),
beforeData: z.any().optional(), beforeData: z.any().optional(),
afterData: z.any().optional(), afterData: z.any().optional(),
status: z.enum(['success', 'failure']).default('success'), status: z.enum(['success', 'failure']).default('success'),
@ -176,22 +184,119 @@ export class SystemLogRouter {
const ipAddress = ctx.ip; const ipAddress = ctx.ip;
const operatorId = ctx.staff?.id; const operatorId = ctx.staff?.id;
return this.systemLogService.create({ try {
data: { return this.systemLogService.create({
level: 'info', data: {
module: 'staff', level: input.status === 'success' ? 'info' : 'error',
action: input.action, module: 'staff',
operatorId: operatorId, action: input.action,
ipAddress: ipAddress, operatorId: operatorId,
targetId: input.targetId, ipAddress: ipAddress,
targetType: 'staff', targetId: input.targetId,
targetName: input.targetName, targetType: 'staff',
beforeData: input.beforeData, targetName: input.targetName,
afterData: input.afterData, message: input.message,
status: input.status, beforeData: input.beforeData,
errorMessage: input.errorMessage, 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');
}
}), }),
}) })
} }

View File

@ -10,6 +10,18 @@ export class SystemLogService extends BaseService<Prisma.SystemLogDelegate> {
} }
async create(args: Prisma.SystemLogCreateArgs) { 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); const result = await super.create(args);
this.emitDataChanged(CrudOperation.CREATED, result); this.emitDataChanged(CrudOperation.CREATED, result);
return result; return result;
@ -19,6 +31,35 @@ export class SystemLogService extends BaseService<Prisma.SystemLogDelegate> {
return super.findMany(args); // 放弃分页结构 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( async logStaffAction(
action: string, action: string,
operatorId: string | null, operatorId: string | null,
@ -34,6 +75,10 @@ export class SystemLogService extends BaseService<Prisma.SystemLogDelegate> {
const details = beforeData && afterData const details = beforeData && afterData
? this.generateChangeDetails(beforeData, afterData) ? this.generateChangeDetails(beforeData, afterData)
: {}; : {};
const timestamp = new Date().toLocaleString();
const messagePrefix = status === 'success' ? '' : '错误: ';
const message = `[${timestamp}] ${messagePrefix}用户 ${targetName}${action}`;
return this.create({ return this.create({
data: { data: {
@ -45,6 +90,7 @@ export class SystemLogService extends BaseService<Prisma.SystemLogDelegate> {
targetId, targetId,
targetType: 'staff', targetType: 'staff',
targetName, targetName,
message,
details, details,
beforeData, beforeData,
afterData, afterData,

View File

@ -1,6 +1,6 @@
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, useEffect } from "react"; import { useState, useMemo, useEffect } from "react";
import { useStaff } from "@nice/client"; import { api, useStaff } from "@nice/client";
import { areaOptions } from './area-options'; import { areaOptions } from './area-options';
import InfoCard from './infoCard'; import InfoCard from './infoCard';
@ -11,18 +11,18 @@ 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[]>([]);
// 添加跟踪培训和鉴定状态的state // 添加跟踪培训和鉴定状态的state
const [hasTrain, setHasTrain] = useState(false); const [hasTrain, setHasTrain] = useState(false);
const [hasCert, setHasCert] = useState(false); const [hasCert, setHasCert] = useState(false);
// 添加状态来跟踪每个文本区域的高度 // 添加状态来跟踪每个文本区域的高度
const [textAreaHeights, setTextAreaHeights] = useState<Record<string, number>>({}); const [textAreaHeights, setTextAreaHeights] = useState<Record<string, number>>({});
const handleAdd = (content: string) => { const handleAdd = (content: string) => {
setInfoCards([...infoCards, { content }]); setInfoCards([...infoCards, { content }]);
} }
// 在组件中添加监听字段变化 // 在组件中添加监听字段变化
useEffect(() => { useEffect(() => {
// 设置默认值 // 设置默认值
@ -30,20 +30,20 @@ const StaffInfoWrite = () => {
hasTrain: false, hasTrain: false,
hasCert: false hasCert: false
}); });
// 使用 Form 的 onValuesChange 在外部监听 // 使用 Form 的 onValuesChange 在外部监听
const fieldChangeHandler = () => { const fieldChangeHandler = () => {
const values = form.getFieldsValue(['hasTrain', 'hasCert']); const values = form.getFieldsValue(['hasTrain', 'hasCert']);
setHasTrain(!!values.hasTrain); setHasTrain(!!values.hasTrain);
setHasCert(!!values.hasCert); setHasCert(!!values.hasCert);
}; };
// 初始化时执行一次 // 初始化时执行一次
fieldChangeHandler(); fieldChangeHandler();
// 不需要返回取消订阅,因为我们不再使用 subscribe // 不需要返回取消订阅,因为我们不再使用 subscribe
}, [form]); }, [form]);
// 按分组组织字段 // 按分组组织字段
const fieldGroups = useMemo(() => { const fieldGroups = useMemo(() => {
if (!fields) return {}; if (!fields) return {};
@ -72,7 +72,7 @@ const StaffInfoWrite = () => {
// 处理培训和鉴定的单选按钮 // 处理培训和鉴定的单选按钮
if (field.name === 'hasTrain') { if (field.name === 'hasTrain') {
return ( return (
<Radio.Group <Radio.Group
options={[ options={[
{ label: '是', value: true }, { label: '是', value: true },
{ label: '否', value: false } { label: '否', value: false }
@ -85,10 +85,10 @@ const StaffInfoWrite = () => {
/> />
); );
} }
if (field.name === 'hasCert') { if (field.name === 'hasCert') {
return ( return (
<Radio.Group <Radio.Group
options={[ options={[
{ label: '是', value: true }, { label: '是', value: true },
{ label: '否', value: false } { label: '否', value: false }
@ -101,11 +101,11 @@ const StaffInfoWrite = () => {
/> />
); );
} }
// 检查字段是否应禁用 // 检查字段是否应禁用
const isDisabled = shouldFieldBeDisabled(field, groupName); const isDisabled = shouldFieldBeDisabled(field, groupName);
// 根据字段类型渲染不同的组件 // 根据字段类型渲染不同的组件
switch (field.type) { switch (field.type) {
case 'text': case 'text':
@ -126,15 +126,15 @@ const StaffInfoWrite = () => {
return <Input placeholder="选项数据缺失" disabled={isDisabled} />; return <Input placeholder="选项数据缺失" disabled={isDisabled} />;
case 'textarea': case 'textarea':
return ( return (
<div <div
className="w-full relative" className="w-full relative"
style={{ style={{
minHeight: textAreaHeights[field.name] ? minHeight: textAreaHeights[field.name] ?
`${textAreaHeights[field.name]}px` : 'auto' `${textAreaHeights[field.name]}px` : 'auto'
}} }}
> >
<InfoCard <InfoCard
onAdd={handleAdd} onAdd={handleAdd}
onHeightChange={(height) => { onHeightChange={(height) => {
setTextAreaHeights(prev => ({ setTextAreaHeights(prev => ({
...prev, ...prev,
@ -145,29 +145,26 @@ const StaffInfoWrite = () => {
</div> </div>
); );
case 'cascader': case 'cascader':
if(field.label === '籍贯'){ if (field.label === '籍贯') {
return <Cascader options={areaOptions} disabled={isDisabled} />; return <Cascader options={areaOptions} disabled={isDisabled} />;
}else{ } else {
return <Input disabled={isDisabled} />; return <Input disabled={isDisabled} />;
} }
default: default:
return <Input className="w-full" disabled={isDisabled} />; return <Input className="w-full" disabled={isDisabled} />;
} }
}; };
const onFinish = async (e, values: any) => { const onFinish = async (e, values: any) => {
// values.preventDefault(); // values.preventDefault();
e.preventDefault() e.preventDefault()
console.log(values) console.log(values)
try { try {
setLoading(true); setLoading(true);
// 创建基础员工记录 // 创建基础员工记录
if (!values.username) { if (!values.username) {
message.error("用户名不能为空"); message.error("用户名不能为空");
return; return;
} }
// 创建基础员工记录 // 创建基础员工记录
console.log('准备创建用户,数据:', { username: values.username }); console.log('准备创建用户,数据:', { username: values.username });
const staff = await create.mutateAsync({ const staff = await create.mutateAsync({
@ -176,25 +173,42 @@ const StaffInfoWrite = () => {
password: '123456' 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) const validEntries = Object.entries(values)
.filter(([key, value]) => key !== 'username' && value !== undefined && value !== null && value !== '') .filter(([key, value]) => key !== 'username' && value !== undefined && value !== null && value !== '')
.map(([fieldName, value]) => { .map(([fieldName, value]) => {
const field = fields && Array.isArray(fields) ? fields.find((f: any) => f.name === fieldName) : undefined; const field = fields && Array.isArray(fields) ? fields.find((f: any) => f.name === fieldName) : undefined;
let processedValue = value; let processedValue = value;
// 处理特殊字段类型 // 处理特殊字段类型
if (field?.type === 'date') { if (field?.type === 'date') {
processedValue = value.toString(); processedValue = value.toString();
} else if (field?.type === 'cascader' && Array.isArray(value)) { } else if (field?.type === 'cascader' && Array.isArray(value)) {
processedValue = value.join('/'); processedValue = value.join('/');
} }
return { field, value: processedValue }; return { field, value: processedValue };
}) })
.filter(item => item.field?.id); // 过滤有效字段定义 .filter(item => item.field?.id); // 过滤有效字段定义
// 批量提交自定义字段 // 批量提交自定义字段
await Promise.all( await Promise.all(
validEntries.map(({ field, value }) => 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("信息提交成功"); message.success("信息提交成功");
form.resetFields(); form.resetFields();
} catch (error) { } catch (error) {
console.error('提交出错:', 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("提交失败,请重试"); message.error("提交失败,请重试");
} finally { } finally {
setLoading(false); setLoading(false);
@ -261,7 +349,7 @@ const StaffInfoWrite = () => {
${field.type === 'textarea' ? "transition-all duration-200 ease-in-out" : ""} ${field.type === 'textarea' ? "transition-all duration-200 ease-in-out" : ""}
`} `}
style={{ style={{
minHeight: field.type === 'textarea' && textAreaHeights[field.name] ? minHeight: field.type === 'textarea' && textAreaHeights[field.name] ?
`${textAreaHeights[field.name] + 50}px` : 'auto' `${textAreaHeights[field.name] + 50}px` : 'auto'
}} }}
> >
@ -274,8 +362,8 @@ const StaffInfoWrite = () => {
<div className="flex justify-end space-x-4"> <div className="flex justify-end space-x-4">
<Button onClick={() => form.resetFields()}></Button> <Button onClick={() => form.resetFields()}></Button>
<Button <Button
type="primary" type="primary"
htmlType="submit" htmlType="submit"
onClick={(e) => onFinish(e, form.getFieldsValue())} onClick={(e) => onFinish(e, form.getFieldsValue())}
loading={loading} loading={loading}

View File

@ -1,377 +1,49 @@
"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, Modal } from 'antd';
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { api } from '@nice/client';
const { RangePicker } = DatePicker; // 创建一个全局变量来存储日志
const { Option } = Select; let globalLogs: string[] = [];
// 日志接口定义
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 = async (logData: Omit<ILog, 'id' | 'timestamp'>) => { export const addLog = (log: string) => {
try { const timestamp = new Date().toLocaleString();
// 检查 api.systemLog 是否存在 const formattedLog = `[${timestamp}] ${log}`;
if (!api.systemLog) { globalLogs = [...globalLogs, formattedLog];
console.error('systemLog API 不可用'); // 如果需要可以将日志保存到localStorage
return false; localStorage.setItem('systemLogs', JSON.stringify(globalLogs));
}
// 使用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 SystemLogPage = () => {
const [form] = Form.useForm(); const [logs, setLogs] = useState<string[]>([]);
const [queryParams, setQueryParams] = useState({ // 组件加载时从全局变量或localStorage获取日志
page: 1, useEffect(() => {
pageSize: 10, // 尝试从localStorage获取日志
where: {} const storedLogs = localStorage.getItem('systemLogs');
}); if (storedLogs) {
setLogs(JSON.parse(storedLogs));
// 使用 getLogs API 替代 searchLogs } else {
const { data, isLoading, refetch } = api.systemLog.getLogs.useQuery(queryParams, { setLogs(globalLogs);
// 启用数据保留,避免加载时页面闪烁
keepPreviousData: true
});
// 处理表格分页变化
const handleTableChange = (pagination: any) => {
setQueryParams({
...queryParams,
page: pagination.current,
pageSize: pagination.pageSize,
});
};
// 处理表单查询
const handleSearch = (values: any) => {
const { timeRange, keyword, level, module, status, ...rest } = values;
// 构建 where 条件
const where: any = {};
if (level) where.level = level;
if (module) where.module = { contains: module };
if (status) where.status = status;
// 处理时间范围
if (timeRange && timeRange.length === 2) {
where.timestamp = {
gte: timeRange[0].startOf('day').toISOString(),
lte: timeRange[1].endOf('day').toISOString()
};
} }
}, []);
// 处理关键词搜索
if (keyword) {
where.OR = [
{ module: { contains: keyword } },
{ action: { contains: keyword } },
{ targetName: { contains: keyword } }
];
}
// 处理其他条件
if (rest.operatorId) where.operatorId = rest.operatorId;
if (rest.targetId) where.targetId = rest.targetId;
console.log('查询参数:', { where });
setQueryParams({
...queryParams,
page: 1, // 重置到第一页
where
});
};
// 格式化时间显示
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),
sorter: true
},
{
title: '级别',
dataIndex: 'level',
key: 'level',
render: (text: string) => (
<Tag color={
text === 'info' ? 'blue' :
text === 'warning' ? 'orange' :
text === 'error' ? 'red' : 'green'
}>
{text.toUpperCase()}
</Tag>
),
filters: [
{ text: '信息', value: 'info' },
{ text: '警告', value: 'warning' },
{ text: '错误', value: 'error' },
{ text: '调试', value: 'debug' }
]
},
{
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: 'IP地址',
dataIndex: 'ipAddress',
key: 'ipAddress',
render: (text: string) => text || '-'
},
{
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>
),
filters: [
{ text: '成功', value: 'success' },
{ text: '失败', value: 'failure' }
]
},
{
title: '操作',
key: 'operation',
render: (_, record: ILog) => (
<Button
type="link"
onClick={() => showLogDetail(record)}
>
</Button>
)
}
];
// 显示日志详情的函数
const showLogDetail = (record: ILog) => {
Modal.info({
title: '日志详情',
width: 800,
content: (
<div className="mt-4">
<div className="grid grid-cols-2 gap-4 mb-4">
<div><strong>ID:</strong> {record.id}</div>
<div><strong>:</strong> {formatTime(record.timestamp)}</div>
<div><strong>:</strong> {record.module}</div>
<div><strong>:</strong> {record.action}</div>
<div><strong>:</strong> {record.operator ? (record.operator.showname || record.operator.username) : '-'}</div>
<div><strong>IP地址:</strong> {record.ipAddress || '-'}</div>
<div><strong>:</strong> {record.targetName || '-'}</div>
<div><strong>:</strong> {record.targetType || '-'}</div>
<div><strong>:</strong> {record.status === 'success' ? '成功' : '失败'}</div>
{record.errorMessage && <div><strong>:</strong> {record.errorMessage}</div>}
</div>
{(record.beforeData || record.afterData) && (
<div className="mt-4">
<h4 className="mb-2 font-medium"></h4>
<div className="grid grid-cols-2 gap-4">
{record.beforeData && (
<div>
<div className="font-medium mb-1"></div>
<pre className="bg-gray-100 p-2 rounded overflow-auto max-h-72">
{JSON.stringify(record.beforeData, null, 2)}
</pre>
</div>
)}
{record.afterData && (
<div>
<div className="font-medium mb-1"></div>
<pre className="bg-gray-100 p-2 rounded overflow-auto max-h-72">
{JSON.stringify(record.afterData, null, 2)}
</pre>
</div>
)}
</div>
</div>
)}
{record.details && (
<div className="mt-4">
<h4 className="mb-2 font-medium"></h4>
<pre className="bg-gray-100 p-2 rounded overflow-auto max-h-72">
{JSON.stringify(record.details, null, 2)}
</pre>
</div>
)}
</div>
),
onOk() {}
});
};
return ( return (
<div className="space-y-4"> <div className="max-w-4xl mx-auto p-6">
<Card> <h1 className="text-2xl font-bold mb-6"></h1>
<Form <div className="bg-white p-6 rounded-lg shadow">
form={form} {logs.length === 0 ? (
layout="vertical" <p className="text-gray-500"></p>
onFinish={handleSearch} ) : (
className="mb-4" <ul className="space-y-2">
> {logs.map((log, index) => (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4"> <li key={index} className="p-2 border-b border-gray-200">
<Form.Item name="keyword" label="关键词"> {log}
<Input placeholder="请输入模块/操作/对象名称" allowClear /> </li>
</Form.Item> ))}
</ul>
<Form.Item name="level" label="日志级别"> )}
<Select placeholder="请选择级别" allowClear> </div>
<Option value="info"></Option>
<Option value="warning"></Option>
<Option value="error"></Option>
<Option value="debug"></Option>
</Select>
</Form.Item>
<Form.Item name="module" label="模块">
<Input placeholder="请输入模块" allowClear />
</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="时间范围" className="md:col-span-2">
<RangePicker className="w-full" />
</Form.Item>
<Form.Item name="operatorId" label="操作人ID">
<Input placeholder="请输入操作人ID" allowClear />
</Form.Item>
<Form.Item name="targetId" label="目标ID">
<Input placeholder="请输入目标ID" allowClear />
</Form.Item>
</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: queryParams.page,
pageSize: queryParams.pageSize,
total: data?.total || 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条日志`
}}
loading={isLoading}
onChange={handleTableChange}
scroll={{ x: 'max-content' }}
/>
</Card>
</div> </div>
); );
}; };
export default SystemLogPage; export default SystemLogPage;

View File

@ -618,7 +618,7 @@ model SystemLog {
// 关联部门 // 关联部门
departmentId String? @map("department_id") departmentId String? @map("department_id")
department Department? @relation(fields: [departmentId], references: [id]) department Department? @relation(fields: [departmentId], references: [id])
message String @map("message") // 完整的日志文本内容
// 优化索引 // 优化索引
@@index([timestamp]) @@index([timestamp])
@@index([level]) @@index([level])