This commit is contained in:
Li1304553726 2025-03-26 16:21:41 +08:00
parent 9685d94b96
commit 538f2f5b38
4 changed files with 369 additions and 313 deletions

View File

@ -193,91 +193,5 @@ export class SystemLogRouter {
} }
}); });
}), }),
// 高级搜索日志
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,
}
}
}
});
}),
}) })
} }

View File

@ -47,7 +47,7 @@ const StaffInfoWrite = () => {
// 按分组组织字段 // 按分组组织字段
const fieldGroups = useMemo(() => { const fieldGroups = useMemo(() => {
if (!fields) return {}; if (!fields) return {};
return fields.reduce((groups: any, field: any) => { return (fields as any[]).reduce((groups: any, field: any) => {
const group = field.group || '其他信息'; const group = field.group || '其他信息';
if (!groups[group]) { if (!groups[group]) {
groups[group] = []; groups[group] = [];
@ -157,28 +157,38 @@ const StaffInfoWrite = () => {
} }
}; };
const onFinish = async (values: any) => { const onFinish = async (e, values: any) => {
// values.preventDefault();
e.preventDefault()
console.log(values)
try { try {
setLoading(true); setLoading(true);
// 创建基础员工记录 // 创建基础员工记录
if (!values.username) {
message.error("用户名不能为空");
return;
}
// 创建基础员工记录
console.log('准备创建用户,数据:', { username: values.username });
const staff = await create.mutateAsync({ const staff = await create.mutateAsync({
data: { data: {
username: values.username, username: values.username,
password: '123456' password: '123456'
} }
}); });
console.log('创建员工记录:', staff);
// 过滤有效字段并转换值 // 过滤有效字段并转换值
const validEntries = Object.entries(values) const validEntries = Object.entries(values)
.filter(([_, value]) => value !== undefined && value !== null && value !== '') .filter(([key, value]) => key !== 'username' && value !== undefined && value !== null && value !== '')
.map(([fieldName, value]) => { .map(([fieldName, value]) => {
const field = fields?.find((f: any) => f.name === fieldName); const field = fields && Array.isArray(fields) ? fields.find((f: any) => f.name === fieldName) : undefined;
let processedValue = value; let processedValue = value;
// 处理特殊字段类型 // 处理特殊字段类型
if (field?.type === 'date' && value instanceof moment) { if (field?.type === 'date') {
processedValue = value.format('YYYY-MM-DD'); 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('/');
} }
@ -197,8 +207,10 @@ const StaffInfoWrite = () => {
}) })
) )
); );
console.log('自定义字段提交成功',staff.username);
message.success("信息提交成功"); message.success("信息提交成功");
form.resetFields();
} catch (error) { } catch (error) {
console.error('提交出错:', error); console.error('提交出错:', error);
message.error("提交失败,请重试"); message.error("提交失败,请重试");
@ -218,7 +230,6 @@ const StaffInfoWrite = () => {
<Form <Form
form={form} form={form}
layout="vertical" layout="vertical"
onFinish={onFinish}
onValuesChange={(changedValues) => { onValuesChange={(changedValues) => {
if ('hasTrain' in changedValues) { if ('hasTrain' in changedValues) {
setHasTrain(!!changedValues.hasTrain); setHasTrain(!!changedValues.hasTrain);
@ -268,6 +279,7 @@ const StaffInfoWrite = () => {
<Button <Button
type="primary" type="primary"
htmlType="submit" htmlType="submit"
onClick={(e) => onFinish(e, form.getFieldsValue())}
loading={loading} loading={loading}
> >

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Card, Table, Space, Tag, Form, Select, DatePicker, Input, Button } from 'antd'; import { Card, Table, Space, Tag, Form, Select, DatePicker, Input, Button, Modal } from 'antd';
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons'; import { SearchOutlined, ReloadOutlined } from '@ant-design/icons';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { api } from '@nice/client'; import { api } from '@nice/client';
@ -11,239 +11,367 @@ const { Option } = Select;
// 日志接口定义 // 日志接口定义
interface ILog { interface ILog {
id: string;
timestamp: string;
level: string;
module: string;
action: string;
operatorId?: string;
operator?: {
id: string; id: string;
username: string; timestamp: string;
showname?: string; level: string;
}; module: string;
ipAddress?: string; action: string;
targetId?: string; operatorId?: string;
targetType?: string; operator?: {
targetName?: string; id: string;
details?: any; username: string;
beforeData?: any; showname?: string;
afterData?: any; };
status: string; ipAddress?: string;
errorMessage?: 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 = async (logData: Omit<ILog, 'id' | 'timestamp'>) => {
try { try {
// 使用tRPC发送日志 // 检查 api.systemLog 是否存在
const result = await api.systemLog.create.mutate({ if (!api.systemLog) {
...logData, console.error('systemLog API 不可用');
level: logData.level || 'info', return false;
status: logData.status || 'success', }
});
console.log('日志已写入数据库:', result); // 使用tRPC发送日志
return true; const result = await api.systemLog.create.mutate({
} catch (error) { ...logData,
console.error('写入日志失败:', error); level: logData.level || 'info',
// 可以考虑添加本地缓存逻辑 status: logData.status || 'success',
return false; });
}
console.log('日志已写入数据库:', result);
return true;
} catch (error) {
console.error('写入日志失败:', error);
// 可以考虑添加本地缓存逻辑
return false;
}
}; };
// 用于添加人员操作日志的便捷方法 // 用于添加人员操作日志的便捷方法
export const addStaffLog = async ( export const addStaffLog = async (
action: string, action: string,
targetId: string, targetId: string,
targetName: string, targetName: string,
beforeData: any = null, beforeData: any = null,
afterData: any = null, afterData: any = null,
status: 'success' | 'failure' = 'success', status: 'success' | 'failure' = 'success',
errorMessage?: string errorMessage?: string
) => { ) => {
return api.systemLog.logStaffAction.mutate({ return api.systemLog.logStaffAction.mutate({
action, action,
targetId, targetId,
targetName, targetName,
beforeData, beforeData,
afterData, afterData,
status, status,
errorMessage errorMessage
}); });
}; };
const SystemLogPage = () => { const SystemLogPage = () => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [queryParams, setQueryParams] = useState({ const [queryParams, setQueryParams] = useState({
page: 1, page: 1,
pageSize: 10, pageSize: 10,
}); where: {}
// 使用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,
}); });
};
// 处理表单查询 // 使用 getLogs API 替代 searchLogs
const handleSearch = (values: any) => { const { data, isLoading, refetch } = api.systemLog.getLogs.useQuery(queryParams, {
const { timeRange, ...rest } = values; // 启用数据保留,避免加载时页面闪烁
keepPreviousData: true
});
const params: any = { // 处理表格分页变化
...rest, const handleTableChange = (pagination: any) => {
page: 1, // 重置到第一页 setQueryParams({
pageSize: queryParams.pageSize, ...queryParams,
page: pagination.current,
pageSize: pagination.pageSize,
});
}; };
// 处理时间范围 // 处理表单查询
if (timeRange && timeRange.length === 2) { const handleSearch = (values: any) => {
params.startTime = timeRange[0].startOf('day').toISOString(); const { timeRange, keyword, level, module, status, ...rest } = values;
params.endTime = timeRange[1].endOf('day').toISOString();
}
setQueryParams(params); // 构建 where 条件
}; const where: any = {};
// 格式化时间显示 if (level) where.level = level;
const formatTime = (timeStr: string) => { if (module) where.module = { contains: module };
return dayjs(timeStr).format('YYYY-MM-DD HH:mm:ss'); if (status) where.status = status;
};
// 表格列定义 // 处理时间范围
const columns = [ if (timeRange && timeRange.length === 2) {
{ where.timestamp = {
title: '操作时间', gte: timeRange[0].startOf('day').toISOString(),
dataIndex: 'timestamp', lte: timeRange[1].endOf('day').toISOString()
key: 'timestamp', };
render: (text: string) => formatTime(text) }
},
{
title: '级别',
dataIndex: 'level',
key: 'level',
render: (text: string) => (
<Tag color={
text === 'info' ? 'blue' :
text === 'warning' ? 'orange' :
text === 'error' ? 'red' : 'green'
}>
{text.toUpperCase()}
</Tag>
)
},
{
title: '模块',
dataIndex: 'module',
key: 'module',
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
},
{
title: '操作人',
key: 'operator',
render: (_, record: ILog) => (
record.operator ? record.operator.showname || record.operator.username : '-'
)
},
{
title: '操作对象',
dataIndex: 'targetName',
key: 'targetName',
render: (text: string) => text || '-'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (text: string) => (
<Tag color={text === 'success' ? 'green' : 'red'}>
{text === 'success' ? '成功' : '失败'}
</Tag>
)
},
];
return ( // 处理关键词搜索
<div className="space-y-4"> if (keyword) {
<Card> where.OR = [
<Form { module: { contains: keyword } },
form={form} { action: { contains: keyword } },
layout="vertical" { targetName: { contains: keyword } }
onFinish={handleSearch} ];
className="mb-4" }
>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Form.Item name="keyword" label="关键词">
<Input placeholder="请输入关键词" allowClear />
</Form.Item>
<Form.Item name="level" label="日志级别"> // 处理其他条件
<Select placeholder="请选择级别" allowClear> if (rest.operatorId) where.operatorId = rest.operatorId;
<Option value="info"></Option> if (rest.targetId) where.targetId = rest.targetId;
<Option value="warning"></Option>
<Option value="error"></Option>
<Option value="debug"></Option>
</Select>
</Form.Item>
<Form.Item name="status" label="状态"> console.log('查询参数:', { where });
<Select placeholder="请选择状态" allowClear> setQueryParams({
<Option value="success"></Option> ...queryParams,
<Option value="failure"></Option> page: 1, // 重置到第一页
</Select> where
</Form.Item> });
};
<Form.Item name="timeRange" label="时间范围"> // 格式化时间显示
<RangePicker className="w-full" /> const formatTime = (timeStr: string) => {
</Form.Item> return dayjs(timeStr).format('YYYY-MM-DD HH:mm:ss');
</div> };
<div className="flex justify-end"> // 表格列定义
<Space> const columns = [
<Button onClick={() => form.resetFields()}></Button> {
<Button type="primary" htmlType="submit" icon={<SearchOutlined />}> title: '操作时间',
dataIndex: 'timestamp',
</Button> key: 'timestamp',
<Button icon={<ReloadOutlined />} onClick={() => refetch()}> render: (text: string) => formatTime(text),
sorter: true
</Button> },
</Space> {
</div> title: '级别',
</Form> dataIndex: 'level',
</Card> 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>
)
}
];
<Card> // 显示日志详情的函数
<Table const showLogDetail = (record: ILog) => {
columns={columns} Modal.info({
dataSource={data?.items || []} title: '日志详情',
rowKey="id" width: 800,
pagination={{ content: (
current: data?.pagination?.current || 1, <div className="mt-4">
pageSize: data?.pagination?.pageSize || 10, <div className="grid grid-cols-2 gap-4 mb-4">
total: data?.pagination?.total || 0, <div><strong>ID:</strong> {record.id}</div>
showSizeChanger: true, <div><strong>:</strong> {formatTime(record.timestamp)}</div>
showQuickJumper: true, <div><strong>:</strong> {record.module}</div>
showTotal: (total) => `${total} 条日志` <div><strong>:</strong> {record.action}</div>
}} <div><strong>:</strong> {record.operator ? (record.operator.showname || record.operator.username) : '-'}</div>
loading={isLoading} <div><strong>IP地址:</strong> {record.ipAddress || '-'}</div>
onChange={handleTableChange} <div><strong>:</strong> {record.targetName || '-'}</div>
/> <div><strong>:</strong> {record.targetType || '-'}</div>
</Card> <div><strong>:</strong> {record.status === 'success' ? '成功' : '失败'}</div>
</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 (
<div className="space-y-4">
<Card>
<Form
form={form}
layout="vertical"
onFinish={handleSearch}
className="mb-4"
>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Form.Item name="keyword" label="关键词">
<Input placeholder="请输入模块/操作/对象名称" allowClear />
</Form.Item>
<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 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>
);
}; };
export default SystemLogPage; export default SystemLogPage;

View File

@ -1,3 +1,4 @@
//@ts-nocheck
import { getQueryKey } from "@trpc/react-query"; import { getQueryKey } from "@trpc/react-query";
import { api } from "../trpc"; // Adjust path as necessary import { api } from "../trpc"; // Adjust path as necessary
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
@ -5,6 +6,7 @@ import { ObjectType, Staff } from "@nice/common";
import { findQueryData } from "../utils"; import { findQueryData } from "../utils";
import { CrudOperation, emitDataChange } from "../../event"; import { CrudOperation, emitDataChange } from "../../event";
export interface CustomField { export interface CustomField {
name: string; name: string;
label?: string; label?: string;