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';
@ -36,6 +36,12 @@ interface ILog {
// 添加日志的函数 // 添加日志的函数
export const addLog = async (logData: Omit<ILog, 'id' | 'timestamp'>) => { export const addLog = async (logData: Omit<ILog, 'id' | 'timestamp'>) => {
try { try {
// 检查 api.systemLog 是否存在
if (!api.systemLog) {
console.error('systemLog API 不可用');
return false;
}
// 使用tRPC发送日志 // 使用tRPC发送日志
const result = await api.systemLog.create.mutate({ const result = await api.systemLog.create.mutate({
...logData, ...logData,
@ -78,12 +84,13 @@ const SystemLogPage = () => {
const [queryParams, setQueryParams] = useState({ const [queryParams, setQueryParams] = useState({
page: 1, page: 1,
pageSize: 10, pageSize: 10,
where: {}
}); });
// 使用tRPC查询日志 // 使用 getLogs API 替代 searchLogs
const { data, isLoading, refetch } = api.systemLog.getLogs.useQuery({ const { data, isLoading, refetch } = api.systemLog.getLogs.useQuery(queryParams, {
page: queryParams.page, // 启用数据保留,避免加载时页面闪烁
pageSize: queryParams.pageSize keepPreviousData: true
}); });
// 处理表格分页变化 // 处理表格分页变化
@ -97,21 +104,42 @@ const SystemLogPage = () => {
// 处理表单查询 // 处理表单查询
const handleSearch = (values: any) => { const handleSearch = (values: any) => {
const { timeRange, ...rest } = values; const { timeRange, keyword, level, module, status, ...rest } = values;
const params: any = { // 构建 where 条件
...rest, const where: any = {};
page: 1, // 重置到第一页
pageSize: queryParams.pageSize, if (level) where.level = level;
}; if (module) where.module = { contains: module };
if (status) where.status = status;
// 处理时间范围 // 处理时间范围
if (timeRange && timeRange.length === 2) { if (timeRange && timeRange.length === 2) {
params.startTime = timeRange[0].startOf('day').toISOString(); where.timestamp = {
params.endTime = timeRange[1].endOf('day').toISOString(); gte: timeRange[0].startOf('day').toISOString(),
lte: timeRange[1].endOf('day').toISOString()
};
} }
setQueryParams(params); // 处理关键词搜索
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
});
}; };
// 格式化时间显示 // 格式化时间显示
@ -125,7 +153,8 @@ const SystemLogPage = () => {
title: '操作时间', title: '操作时间',
dataIndex: 'timestamp', dataIndex: 'timestamp',
key: 'timestamp', key: 'timestamp',
render: (text: string) => formatTime(text) render: (text: string) => formatTime(text),
sorter: true
}, },
{ {
title: '级别', title: '级别',
@ -139,7 +168,13 @@ const SystemLogPage = () => {
}> }>
{text.toUpperCase()} {text.toUpperCase()}
</Tag> </Tag>
) ),
filters: [
{ text: '信息', value: 'info' },
{ text: '警告', value: 'warning' },
{ text: '错误', value: 'error' },
{ text: '调试', value: 'debug' }
]
}, },
{ {
title: '模块', title: '模块',
@ -158,6 +193,12 @@ const SystemLogPage = () => {
record.operator ? record.operator.showname || record.operator.username : '-' record.operator ? record.operator.showname || record.operator.username : '-'
) )
}, },
{
title: 'IP地址',
dataIndex: 'ipAddress',
key: 'ipAddress',
render: (text: string) => text || '-'
},
{ {
title: '操作对象', title: '操作对象',
dataIndex: 'targetName', dataIndex: 'targetName',
@ -172,10 +213,84 @@ const SystemLogPage = () => {
<Tag color={text === 'success' ? 'green' : 'red'}> <Tag color={text === 'success' ? 'green' : 'red'}>
{text === 'success' ? '成功' : '失败'} {text === 'success' ? '成功' : '失败'}
</Tag> </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="space-y-4">
<Card> <Card>
@ -187,7 +302,7 @@ const SystemLogPage = () => {
> >
<div className="grid grid-cols-1 md:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Form.Item name="keyword" label="关键词"> <Form.Item name="keyword" label="关键词">
<Input placeholder="请输入关键词" allowClear /> <Input placeholder="请输入模块/操作/对象名称" allowClear />
</Form.Item> </Form.Item>
<Form.Item name="level" label="日志级别"> <Form.Item name="level" label="日志级别">
@ -199,6 +314,10 @@ const SystemLogPage = () => {
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item name="module" label="模块">
<Input placeholder="请输入模块" allowClear />
</Form.Item>
<Form.Item name="status" label="状态"> <Form.Item name="status" label="状态">
<Select placeholder="请选择状态" allowClear> <Select placeholder="请选择状态" allowClear>
<Option value="success"></Option> <Option value="success"></Option>
@ -206,9 +325,17 @@ const SystemLogPage = () => {
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item name="timeRange" label="时间范围"> <Form.Item name="timeRange" label="时间范围" className="md:col-span-2">
<RangePicker className="w-full" /> <RangePicker className="w-full" />
</Form.Item> </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"> <div className="flex justify-end">
@ -231,15 +358,16 @@ const SystemLogPage = () => {
dataSource={data?.items || []} dataSource={data?.items || []}
rowKey="id" rowKey="id"
pagination={{ pagination={{
current: data?.pagination?.current || 1, current: queryParams.page,
pageSize: data?.pagination?.pageSize || 10, pageSize: queryParams.pageSize,
total: data?.pagination?.total || 0, total: data?.total || 0,
showSizeChanger: true, showSizeChanger: true,
showQuickJumper: true, showQuickJumper: true,
showTotal: (total) => `${total} 条日志` showTotal: (total) => `${total} 条日志`
}} }}
loading={isLoading} loading={isLoading}
onChange={handleTableChange} onChange={handleTableChange}
scroll={{ x: 'max-content' }}
/> />
</Card> </Card>
</div> </div>

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;