Merge branch 'main' of http://113.45.157.195:3003/linfeng/staff_data
This commit is contained in:
commit
12dbc91830
|
@ -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,6 +42,7 @@ export class SystemLogRouter {
|
||||||
const ctxIpAddress = ctx.ip;
|
const ctxIpAddress = ctx.ip;
|
||||||
const operatorId = ctx.staff?.id;
|
const operatorId = ctx.staff?.id;
|
||||||
|
|
||||||
|
try {
|
||||||
return this.systemLogService.create({
|
return this.systemLogService.create({
|
||||||
data: {
|
data: {
|
||||||
level: input.level,
|
level: input.level,
|
||||||
|
@ -51,6 +53,7 @@ export class SystemLogRouter {
|
||||||
targetId: input.targetId,
|
targetId: input.targetId,
|
||||||
targetType: input.targetType,
|
targetType: input.targetType,
|
||||||
targetName: input.targetName,
|
targetName: input.targetName,
|
||||||
|
message: input.message,
|
||||||
details: input.details,
|
details: input.details,
|
||||||
beforeData: input.beforeData,
|
beforeData: input.beforeData,
|
||||||
afterData: input.afterData,
|
afterData: input.afterData,
|
||||||
|
@ -59,6 +62,10 @@ export class SystemLogRouter {
|
||||||
departmentId: input.departmentId,
|
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,9 +184,10 @@ export class SystemLogRouter {
|
||||||
const ipAddress = ctx.ip;
|
const ipAddress = ctx.ip;
|
||||||
const operatorId = ctx.staff?.id;
|
const operatorId = ctx.staff?.id;
|
||||||
|
|
||||||
|
try {
|
||||||
return this.systemLogService.create({
|
return this.systemLogService.create({
|
||||||
data: {
|
data: {
|
||||||
level: 'info',
|
level: input.status === 'success' ? 'info' : 'error',
|
||||||
module: 'staff',
|
module: 'staff',
|
||||||
action: input.action,
|
action: input.action,
|
||||||
operatorId: operatorId,
|
operatorId: operatorId,
|
||||||
|
@ -186,12 +195,108 @@ export class SystemLogRouter {
|
||||||
targetId: input.targetId,
|
targetId: input.targetId,
|
||||||
targetType: 'staff',
|
targetType: 'staff',
|
||||||
targetName: input.targetName,
|
targetName: input.targetName,
|
||||||
|
message: input.message,
|
||||||
beforeData: input.beforeData,
|
beforeData: input.beforeData,
|
||||||
afterData: input.afterData,
|
afterData: input.afterData,
|
||||||
status: input.status,
|
status: input.status,
|
||||||
errorMessage: input.errorMessage,
|
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');
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -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,
|
||||||
|
@ -35,6 +76,10 @@ export class SystemLogService extends BaseService<Prisma.SystemLogDelegate> {
|
||||||
? 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: {
|
||||||
level: status === 'success' ? 'info' : 'error',
|
level: status === 'success' ? 'info' : 'error',
|
||||||
|
@ -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,
|
||||||
|
|
|
@ -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';
|
||||||
import DepartmentSelect from "@web/src/components/models/department/department-select";
|
import DepartmentSelect from "@web/src/components/models/department/department-select";
|
||||||
|
@ -168,20 +168,17 @@ const StaffInfoWrite = () => {
|
||||||
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.showname) {
|
if (!values.showname) {
|
||||||
message.error("姓名不能为空");
|
message.error("姓名不能为空");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建基础员工记录
|
// 创建基础员工记录
|
||||||
console.log('准备创建用户,数据:', { showname: values.showname });
|
console.log('准备创建用户,数据:', { showname: values.showname });
|
||||||
const staff = await create.mutateAsync({
|
const staff = await create.mutateAsync({
|
||||||
|
@ -191,6 +188,23 @@ const StaffInfoWrite = () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
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]) =>
|
.filter(([key, value]) =>
|
||||||
|
@ -230,11 +244,85 @@ 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);
|
||||||
|
|
|
@ -1,375 +1,47 @@
|
||||||
"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>
|
|
||||||
<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>
|
||||||
|
|
||||||
<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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -567,7 +567,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])
|
||||||
|
|
Loading…
Reference in New Issue