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

View File

@ -1,7 +1,7 @@
"use client";
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 dayjs from 'dayjs';
import { api } from '@nice/client';
@ -11,239 +11,367 @@ const { Option } = Select;
// 日志接口定义
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;
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'>) => {
try {
// 使用tRPC发送日志
const result = await api.systemLog.create.mutate({
...logData,
level: logData.level || 'info',
status: logData.status || 'success',
});
try {
// 检查 api.systemLog 是否存在
if (!api.systemLog) {
console.error('systemLog API 不可用');
return false;
}
console.log('日志已写入数据库:', result);
return true;
} catch (error) {
console.error('写入日志失败:', error);
// 可以考虑添加本地缓存逻辑
return false;
}
// 使用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
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
});
return api.systemLog.logStaffAction.mutate({
action,
targetId,
targetName,
beforeData,
afterData,
status,
errorMessage
});
};
const SystemLogPage = () => {
const [form] = Form.useForm();
const [queryParams, setQueryParams] = useState({
page: 1,
pageSize: 10,
});
// 使用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,
const [form] = Form.useForm();
const [queryParams, setQueryParams] = useState({
page: 1,
pageSize: 10,
where: {}
});
};
// 处理表单查询
const handleSearch = (values: any) => {
const { timeRange, ...rest } = values;
// 使用 getLogs API 替代 searchLogs
const { data, isLoading, refetch } = api.systemLog.getLogs.useQuery(queryParams, {
// 启用数据保留,避免加载时页面闪烁
keepPreviousData: true
});
const params: any = {
...rest,
page: 1, // 重置到第一页
pageSize: queryParams.pageSize,
// 处理表格分页变化
const handleTableChange = (pagination: any) => {
setQueryParams({
...queryParams,
page: pagination.current,
pageSize: pagination.pageSize,
});
};
// 处理时间范围
if (timeRange && timeRange.length === 2) {
params.startTime = timeRange[0].startOf('day').toISOString();
params.endTime = timeRange[1].endOf('day').toISOString();
}
// 处理表单查询
const handleSearch = (values: any) => {
const { timeRange, keyword, level, module, status, ...rest } = values;
setQueryParams(params);
};
// 构建 where 条件
const where: any = {};
// 格式化时间显示
const formatTime = (timeStr: string) => {
return dayjs(timeStr).format('YYYY-MM-DD HH:mm:ss');
};
if (level) where.level = level;
if (module) where.module = { contains: module };
if (status) where.status = status;
// 表格列定义
const columns = [
{
title: '操作时间',
dataIndex: 'timestamp',
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>
)
},
];
// 处理时间范围
if (timeRange && timeRange.length === 2) {
where.timestamp = {
gte: timeRange[0].startOf('day').toISOString(),
lte: timeRange[1].endOf('day').toISOString()
};
}
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>
// 处理关键词搜索
if (keyword) {
where.OR = [
{ module: { contains: keyword } },
{ action: { contains: keyword } },
{ targetName: { contains: keyword } }
];
}
<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>
// 处理其他条件
if (rest.operatorId) where.operatorId = rest.operatorId;
if (rest.targetId) where.targetId = rest.targetId;
<Form.Item name="status" label="状态">
<Select placeholder="请选择状态" allowClear>
<Option value="success"></Option>
<Option value="failure"></Option>
</Select>
</Form.Item>
console.log('查询参数:', { where });
setQueryParams({
...queryParams,
page: 1, // 重置到第一页
where
});
};
<Form.Item name="timeRange" label="时间范围">
<RangePicker className="w-full" />
</Form.Item>
</div>
// 格式化时间显示
const formatTime = (timeStr: string) => {
return dayjs(timeStr).format('YYYY-MM-DD HH:mm:ss');
};
<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>
// 表格列定义
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>
)
}
];
<Card>
<Table
columns={columns}
dataSource={data?.items || []}
rowKey="id"
pagination={{
current: data?.pagination?.current || 1,
pageSize: data?.pagination?.pageSize || 10,
total: data?.pagination?.total || 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条日志`
}}
loading={isLoading}
onChange={handleTableChange}
/>
</Card>
</div>
);
// 显示日志详情的函数
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 (
<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;

View File

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