This commit is contained in:
Li1304553726 2025-03-26 13:00:55 +08:00
parent 14b977b28a
commit 128296b675
14 changed files with 980 additions and 193 deletions

View File

@ -18,7 +18,7 @@ import { ExceptionsFilter } from './filters/exceptions.filter';
import { TransformModule } from './models/transform/transform.module';
import { RealTimeModule } from './socket/realtime/realtime.module';
import { UploadModule } from './upload/upload.module';
import { SystemLogModule } from '@server/models/sys-logs/systemLog.module';
@Module({
imports: [
ConfigModule.forRoot({
@ -42,7 +42,8 @@ import { UploadModule } from './upload/upload.module';
MinioModule,
CollaborationModule,
RealTimeModule,
UploadModule
UploadModule,
SystemLogModule
],
providers: [{
provide: APP_FILTER,

View File

@ -0,0 +1,10 @@
import { Controller, UseGuards } from "@nestjs/common";
import { AuthGuard } from '@server/auth/auth.guard';
import { SystemLogService } from "./systemLog.service";
@Controller('system-logs')
export class SystemLogController {
constructor(private readonly systemLogService: SystemLogService) {}
// @UseGuards(AuthGuard)
// 控制器使用trpc路由不需要在这里定义API端点
}

View File

@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { StaffModule } from '../staff/staff.module';
import { TrpcService } from '@server/trpc/trpc.service';
import { SystemLogController } from './systemLog.controller';
import { SystemLogService } from './systemLog.service';
import { SystemLogRouter } from './systemLog.router';
@Module({
imports: [StaffModule],
controllers: [SystemLogController],
providers: [SystemLogService, SystemLogRouter, TrpcService],
exports: [SystemLogService, SystemLogRouter],
})
export class SystemLogModule {}

View File

@ -0,0 +1,283 @@
import { Injectable } from "@nestjs/common";
import { TrpcService } from "@server/trpc/trpc.service";
import { SystemLogService } from "./systemLog.service";
import { z, ZodType } from "zod";
import { Prisma } from "@nice/common";
// 定义Zod类型Schema
const SystemLogCreateArgsSchema: ZodType<Prisma.SystemLogCreateArgs> = z.any();
const SystemLogFindManyArgsSchema: ZodType<Prisma.SystemLogFindManyArgs> = z.any();
const SystemLogFindUniqueArgsSchema: ZodType<Prisma.SystemLogFindUniqueArgs> = z.any();
const SystemLogWhereInputSchema: ZodType<Prisma.SystemLogWhereInput> = z.any();
const SystemLogSelectSchema: ZodType<Prisma.SystemLogSelect> = z.any();
@Injectable()
export class SystemLogRouter {
constructor(
private readonly trpc: TrpcService,
private readonly systemLogService: SystemLogService,
) { }
router = this.trpc.router({
// 创建日志
create: this.trpc.procedure
.input(z.object({
level: z.enum(['info', 'warning', 'error', 'debug']).default('info'),
module: z.string(),
action: z.string(),
operatorId: z.string().optional(),
ipAddress: z.string().optional(),
targetId: z.string().optional(),
targetType: z.string().optional(),
targetName: z.string().optional(),
details: z.any().optional(),
beforeData: z.any().optional(),
afterData: z.any().optional(),
status: z.enum(['success', 'failure']).default('success'),
errorMessage: z.string().optional(),
departmentId: z.string().optional(),
}))
.mutation(async ({ input, ctx }) => {
const ctxIpAddress = ctx.ip;
const operatorId = ctx.staff?.id;
return this.systemLogService.create({
data: {
level: input.level,
module: input.module,
action: input.action,
operatorId: input.operatorId || operatorId,
ipAddress: input.ipAddress || ctxIpAddress,
targetId: input.targetId,
targetType: input.targetType,
targetName: input.targetName,
details: input.details,
beforeData: input.beforeData,
afterData: input.afterData,
status: input.status,
errorMessage: input.errorMessage,
departmentId: input.departmentId,
}
});
}),
// 查询日志列表
findMany: this.trpc.procedure
.input(SystemLogFindManyArgsSchema)
.query(async ({ input }) => {
return this.systemLogService.findMany(input);
}),
// 查询日志列表(带分页) - 保留原名
getLogs: this.trpc.procedure
.input(z.object({
page: z.number().default(1),
pageSize: z.number().default(20),
where: SystemLogWhereInputSchema.optional(),
select: SystemLogSelectSchema.optional(),
}))
.query(async ({ input }) => {
try {
const { page, pageSize, where = {}, select } = input;
return await this.systemLogService.findManyWithPagination({
page,
pageSize,
where,
...(select ? { select } : {})
});
} catch (error) {
console.error('Error in getLogs:', error);
// 返回空结果,避免崩溃
return {
items: [],
total: 0,
page: input.page,
pageSize: input.pageSize,
totalPages: 0
};
}
}),
// 查询日志列表(带分页) - 新名称
findManyWithPagination: this.trpc.procedure
.input(z.object({
page: z.number().default(1),
pageSize: z.number().default(20),
where: SystemLogWhereInputSchema.optional(),
select: SystemLogSelectSchema.optional(),
}))
.query(async ({ input }) => {
try {
const { page, pageSize, where = {}, select } = input;
return await this.systemLogService.findManyWithPagination({
page,
pageSize,
where,
...(select ? { select } : {})
});
} catch (error) {
console.error('Error in findManyWithPagination:', error);
// 返回空结果,避免崩溃
return {
items: [],
total: 0,
page: input.page,
pageSize: input.pageSize,
totalPages: 0
};
}
}),
// 获取单个日志详情
findUnique: this.trpc.procedure
.input(SystemLogFindUniqueArgsSchema)
.query(async ({ input }) => {
return this.systemLogService.findUnique(input);
}),
// 通过ID获取日志详情(简化版)
findById: this.trpc.procedure
.input(z.string())
.query(async ({ input }) => {
return this.systemLogService.findUnique({
where: { id: input },
include: {
operator: {
select: {
id: true,
username: true,
showname: true,
}
},
department: {
select: {
id: true,
name: true,
}
}
}
});
}),
// 记录人员操作日志的便捷方法
logStaffAction: this.trpc.protectProcedure
.input(z.object({
action: z.string(),
targetId: z.string(),
targetName: z.string(),
beforeData: z.any().optional(),
afterData: z.any().optional(),
status: z.enum(['success', 'failure']).default('success'),
errorMessage: z.string().optional(),
}))
.mutation(async ({ input, ctx }) => {
const ipAddress = ctx.ip;
const operatorId = ctx.staff?.id;
return this.systemLogService.create({
data: {
level: 'info',
module: 'staff',
action: input.action,
operatorId: operatorId,
ipAddress: ipAddress,
targetId: input.targetId,
targetType: 'staff',
targetName: input.targetName,
beforeData: input.beforeData,
afterData: input.afterData,
status: input.status,
errorMessage: input.errorMessage,
}
});
}),
// 高级搜索日志
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

@ -0,0 +1,87 @@
import { Injectable } from "@nestjs/common";
import { BaseService } from "../base/base.service";
import { db, ObjectType, Prisma } from "@nice/common";
import EventBus, { CrudOperation } from "@server/utils/event-bus";
@Injectable()
export class SystemLogService extends BaseService<Prisma.SystemLogDelegate> {
constructor() {
super(db, ObjectType.SYSTEM_LOG, false); // 不自动处理更新时间和删除时间
}
async create(args: Prisma.SystemLogCreateArgs) {
const result = await super.create(args);
this.emitDataChanged(CrudOperation.CREATED, result);
return result;
}
async findMany(args: Prisma.SystemLogFindManyArgs): Promise<Prisma.SystemLogGetPayload<{}>[]> {
return super.findMany(args); // 放弃分页结构
}
async logStaffAction(
action: string,
operatorId: string | null,
ipAddress: string | null,
targetId: string,
targetName: string,
beforeData: any = null,
afterData: any = null,
status: 'success' | 'failure' = 'success',
errorMessage?: string
) {
// 生成变更详情
const details = beforeData && afterData
? this.generateChangeDetails(beforeData, afterData)
: {};
return this.create({
data: {
level: status === 'success' ? 'info' : 'error',
module: '人员管理',
action,
operatorId,
ipAddress,
targetId,
targetType: 'staff',
targetName,
details,
beforeData,
afterData,
status,
errorMessage,
}
});
}
/**
*
*/
private generateChangeDetails(before: any, after: any) {
if (!before || !after) return {};
const changes: Record<string, { oldValue: any; newValue: any }> = {};
Object.keys(after).forEach(key => {
// 忽略一些不需要记录的字段
if (['password', 'createdAt', 'updatedAt', 'deletedAt'].includes(key)) return;
if (JSON.stringify(before[key]) !== JSON.stringify(after[key])) {
changes[key] = {
oldValue: before[key],
newValue: after[key]
};
}
});
return { changes };
}
private emitDataChanged(operation: CrudOperation, data: any) {
EventBus.emit('dataChanged', {
type: ObjectType.SYSTEM_LOG,
operation,
data,
});
}
}

View File

@ -18,6 +18,7 @@ import { TrainContentModule } from '@server/models/train-content/trainContent.mo
import { ResourceModule } from '@server/models/resource/resource.module';
import { TrainSituationModule } from '@server/models/train-situation/trainSituation.module';
import { DailyTrainModule } from '@server/models/daily-train/dailyTrain.module';
import { SystemLogModule } from '@server/models/sys-logs/systemLog.module';
@Module({
imports: [
AuthModule,
@ -37,6 +38,7 @@ import { DailyTrainModule } from '@server/models/daily-train/dailyTrain.module';
TrainContentModule,
TrainSituationModule,
DailyTrainModule,
SystemLogModule,
],
controllers: [],
providers: [TrpcService, TrpcRouter, Logger],

View File

@ -17,6 +17,7 @@ import { ResourceRouter } from '../models/resource/resource.router';
import { TrainContentRouter } from '@server/models/train-content/trainContent.router';
import { TrainSituationRouter } from '@server/models/train-situation/trainSituation.router';
import { DailyTrainRouter } from '@server/models/daily-train/dailyTrain.router';
import { SystemLogRouter } from '@server/models/sys-logs/systemLog.router';
@Injectable()
export class TrpcRouter {
@ -38,6 +39,7 @@ export class TrpcRouter {
private readonly trainContent: TrainContentRouter,
private readonly trainSituation:TrainSituationRouter,
private readonly dailyTrain:DailyTrainRouter,
private readonly systemLogRouter: SystemLogRouter,
) {}
getRouter() {
return;
@ -57,7 +59,8 @@ export class TrpcRouter {
resource: this.resource.router,
trainContent:this.trainContent.router,
trainSituation:this.trainSituation.router,
dailyTrain:this.dailyTrain.router
dailyTrain:this.dailyTrain.router,
systemLog: this.systemLogRouter.router,
});
wss: WebSocketServer = undefined;

View File

@ -255,7 +255,11 @@ export default function StaffTable() {
{ field: 'trainMajor', headerName: '培训专业', },
{
field: 'hasTrain', headerName: '是否参加培训',
cellRenderer: (params: any) => params.value ? '是' : '否'
cellRenderer: (params: any) =>(
<div>
<Input value="false" disabled />
</div>
)
}
]
},
@ -266,7 +270,11 @@ export default function StaffTable() {
{ field: 'certWork', headerName: '鉴定工种', },
{
field: 'hasCert', headerName: '是否参加鉴定',
cellRenderer: (params: any) => params.value ? '是' : '否'
cellRenderer: (params: any) =>(
<div>
<Input value="false" disabled />
</div>
)
}
]
},

View File

@ -1,12 +1,37 @@
import { Input, Button, Space } from 'antd';
import React, { useState } from 'react';
import { Input, Button } from 'antd';
import React, { useState, useRef, useEffect } from 'react';
type InfoCardProps = {
onAdd: (content: string) => void;
onHeightChange?: (height: number) => void; // 添加高度变化回调
}
const InfoCard: React.FC<InfoCardProps> = ({ onAdd }) => {
const InfoCard: React.FC<InfoCardProps> = ({ onAdd, onHeightChange }) => {
const [content, setContent] = useState('');
const [addedContents, setAddedContents] = useState<string[]>([]);
const contentContainerRef = useRef<HTMLDivElement>(null);
// 监控内容区域高度变化并通知父组件
useEffect(() => {
if (!contentContainerRef.current) return;
// 使用ResizeObserver监控元素大小变化
const resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
const height = entry.contentRect.height;
// 通知父组件高度变化
onHeightChange && onHeightChange(height + 20); // 添加一些缓冲空间
}
});
resizeObserver.observe(contentContainerRef.current);
return () => {
if (contentContainerRef.current) {
resizeObserver.unobserve(contentContainerRef.current);
}
};
}, [addedContents, onHeightChange]);
const handleAdd = () => {
if (content) {
@ -15,23 +40,39 @@ const InfoCard: React.FC<InfoCardProps> = ({ onAdd }) => {
setContent('');
}
}
return (
// 增大内边距,避免内容被覆盖
<div style={{ border: '1px solid #d9d9d9', padding: '24px' }}>
<Space>
<div className="w-full">
<div className="flex items-center mb-3 w-full">
<Input
placeholder='请输入内容'
value={content}
onChange={(e) => setContent(e.target.value)}
className="flex-1 mr-2"
onPressEnter={handleAdd}
/>
<Button type='primary' onClick={handleAdd}></Button>
</Space>
<div style={{ marginTop: '24px' }}>
<Button
type='primary'
onClick={handleAdd}
className="shrink-0"
>
</Button>
</div>
{/* 内容容器 */}
<div ref={contentContainerRef} className="w-full bg-white">
{addedContents.map((item, index) => (
<div key={index}>{item}</div>
<div
key={index}
className="p-2 border border-gray-200 rounded bg-gray-50 mb-2 last:mb-0"
>
{item}
</div>
))}
</div>
</div>
);
}
export default InfoCard;

View File

@ -1,7 +1,7 @@
"use client";
import { Button, Form, Input, Select, DatePicker, Radio, message, Modal, Cascader, InputNumber } from "antd";
import { useState, useMemo } from "react";
import { useState, useMemo, useEffect } from "react";
import { useStaff } from "@nice/client";
import { areaOptions } from './area-options';
import InfoCard from './infoCard';
@ -12,9 +12,39 @@ const StaffInfoWrite = () => {
const { create, useCustomFields } = useStaff();
const { data: fields, isLoading: fieldsLoading } = useCustomFields();
const [infoCards, setInfoCards] = useState<any[]>([]);
// 添加跟踪培训和鉴定状态的state
const [hasTrain, setHasTrain] = useState(false);
const [hasCert, setHasCert] = useState(false);
// 添加状态来跟踪每个文本区域的高度
const [textAreaHeights, setTextAreaHeights] = useState<Record<string, number>>({});
const handleAdd = (content: string) => {
setInfoCards([...infoCards, { content }]);
}
// 在组件中添加监听字段变化
useEffect(() => {
// 设置默认值
form.setFieldsValue({
hasTrain: false,
hasCert: false
});
// 使用 Form 的 onValuesChange 在外部监听
const fieldChangeHandler = () => {
const values = form.getFieldsValue(['hasTrain', 'hasCert']);
setHasTrain(!!values.hasTrain);
setHasCert(!!values.hasCert);
};
// 初始化时执行一次
fieldChangeHandler();
// 不需要返回取消订阅,因为我们不再使用 subscribe
}, [form]);
// 按分组组织字段
const fieldGroups = useMemo(() => {
if (!fields) return {};
@ -28,34 +58,103 @@ const StaffInfoWrite = () => {
}, {});
}, [fields]);
const renderField = (field: any) => {
// 检查字段是否应该被禁用的辅助函数
const shouldFieldBeDisabled = (field: any, groupName: string) => {
if (groupName === '培训信息' && !hasTrain) {
return ['trainType', 'trainInstitute', 'trainMajor'].includes(field.name);
}
if (groupName === '鉴定信息' && !hasCert) {
return ['certRank', 'certWork'].includes(field.name);
}
return false;
};
const renderField = (field: any, groupName: string) => {
// 处理培训和鉴定的单选按钮
if (field.name === 'hasTrain') {
return (
<Radio.Group
options={[
{ label: '是', value: true },
{ label: '否', value: false }
]}
onChange={(e) => {
setHasTrain(e.target.value);
form.setFieldsValue({ hasTrain: e.target.value });
}}
value={hasTrain}
/>
);
}
if (field.name === 'hasCert') {
return (
<Radio.Group
options={[
{ label: '是', value: true },
{ label: '否', value: false }
]}
onChange={(e) => {
setHasCert(e.target.value);
form.setFieldsValue({ hasCert: e.target.value });
}}
value={hasCert}
/>
);
}
// 检查字段是否应禁用
const isDisabled = shouldFieldBeDisabled(field, groupName);
// 如果是工作信息相关的字段,添加额外的类名
const isWorkInfo = groupName === "工作信息";
// 根据字段类型渲染不同的组件
switch (field.type) {
case 'text':
return <Input />;
return <Input disabled={isDisabled} />;
case 'number':
return <InputNumber />;
return <InputNumber disabled={isDisabled} />;
case 'date':
return <DatePicker />;
return <DatePicker disabled={isDisabled} />;
case 'select':
if (field.options && field.options.length > 0) {
return <Select options={field.options} />;
return <Select options={field.options} disabled={isDisabled} />;
}
return <Input placeholder="选项数据缺失" />;
return <Input placeholder="选项数据缺失" disabled={isDisabled} />;
case 'radio':
if (field.options && field.options.length > 0) {
return <Radio.Group options={field.options} />;
return <Radio.Group options={field.options} disabled={isDisabled} />;
}
return <Input placeholder="选项数据缺失" />;
return <Input placeholder="选项数据缺失" disabled={isDisabled} />;
case 'textarea':
return <InfoCard onAdd={handleAdd} />;
return (
<div
className="w-full relative"
style={{
minHeight: textAreaHeights[field.name] ?
`${textAreaHeights[field.name]}px` : 'auto'
}}
>
<InfoCard
onAdd={handleAdd}
onHeightChange={(height) => {
setTextAreaHeights(prev => ({
...prev,
[field.name]: height
}));
}}
/>
</div>
);
case 'cascader':
if(field.label === '籍贯'){
return <Cascader options={areaOptions} />;
return <Cascader options={areaOptions} disabled={isDisabled} />;
}else{
return <Input />;
return <Input disabled={isDisabled} />;
}
default:
return <Input />;
return <Input className="w-full" disabled={isDisabled} />;
}
};
@ -92,16 +191,27 @@ const StaffInfoWrite = () => {
form={form}
layout="vertical"
onFinish={onFinish}
onValuesChange={(changedValues) => {
if ('hasTrain' in changedValues) {
setHasTrain(!!changedValues.hasTrain);
}
if ('hasCert' in changedValues) {
setHasCert(!!changedValues.hasCert);
}
}}
className="space-y-6 mt-6"
>
{Object.entries(fieldGroups).map(([groupName, groupFields]) => (
<div key={groupName} className="bg-white p-6 rounded-lg shadow">
<div key={groupName} className="bg-white p-6 rounded-lg shadow relative">
<h2 className="text-lg font-semibold mb-4">{groupName}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className={`
grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-12
${groupName === "工作信息" ? "md:[&>*:nth-child(odd)]:col-start-1 md:[&>*:nth-child(even)]:col-start-2" : ""}
`}>
{groupFields.map((field: any) => (
<Form.Item
key={field.id}
label={field.label}
label={<span className="block mb-2">{field.label}</span>}
name={field.name}
rules={[
{
@ -109,8 +219,16 @@ const StaffInfoWrite = () => {
message: `请输入${field.label}`,
},
]}
className={`
${field.type === 'textarea' && groupName !== "工作信息" ? "md:col-span-2" : ""}
${field.type === 'textarea' ? "transition-all duration-200 ease-in-out" : ""}
`}
style={{
minHeight: field.type === 'textarea' && textAreaHeights[field.name] ?
`${textAreaHeights[field.name] + 50}px` : 'auto'
}}
>
{renderField(field)}
{renderField(field, groupName)}
</Form.Item>
))}
</div>

View File

@ -5,13 +5,10 @@ import { useState } from "react";
import dayjs from "dayjs";
import { useStaff } from "@nice/client";
import DepartmentChildrenSelect from "@web/src/components/models/department/department-children-select";
import { areaOptions } from './area-options';
import { areaOptions } from '../staffinfo_write/area-options';
import DepartmentSelect from "@web/src/components/models/department/department-select";
import { addLog } from "@web/src/app/main/systemlog/SystemLogPage";
const { TextArea } = Input;
const StaffInformation = () => {
const [modalForm] = Form.useForm();
const [form] = Form.useForm();
@ -61,8 +58,10 @@ const StaffInformation = () => {
message.warning('请输入内容');
}
};
const onFinish = async (values: any) => {
console.log('开始提交表单');
try {
setLoading(true);
const formattedValues = {
@ -75,7 +74,7 @@ const StaffInformation = () => {
hireDate: values.hireDate?.toISOString(),
seniority: values.seniority?.toISOString(),
currentPositionDate: values.currentPositionDate?.toISOString(),
rankDate: values.rankDate?.toISOString(),
rankDate: values.rankDate?.toISOString(), // 修改这里
};
await create.mutateAsync(
@ -88,31 +87,9 @@ const StaffInformation = () => {
console.log('奖励列表:', rewardsList);
console.log('处分列表:', punishmentsList);
// 添加日志记录
addLog(`用户 ${values.username || '未知'} 的人员信息已成功添加`);
addLog(`提交的数据: 姓名=${values.username}, 身份证号=${values.idNumber}, 警号=${values.officerId}, 部门ID=${values.deptId}`);
if (rewardsList.length > 0) {
addLog(`${values.username} 的奖励信息: ${rewardsList.join(' | ')}`);
}
if (punishmentsList.length > 0) {
addLog(`${values.username} 的处分信息: ${punishmentsList.join(' | ')}`);
}
if (equipmentList.length > 0) {
addLog(`${values.username} 的装备信息: ${equipmentList.join(' | ')}`);
}
if (projectsList.length > 0) {
addLog(`${values.username} 的任务信息: ${projectsList.join(' | ')}`);
}
message.success("信息提交成功");
} catch (error) {
console.error('提交出错:', error);
// 添加错误日志
addLog(`提交人员信息失败: ${error instanceof Error ? error.message : '未知错误'}`);
message.error("提交失败,请重试");
} finally {
setLoading(false);

View File

@ -1,47 +1,247 @@
"use client";
import React, { useState, useEffect } from 'react';
import { Card, Table, Space, Tag, Form, Select, DatePicker, Input, Button } from 'antd';
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { api } from '@nice/client';
// 创建一个全局变量来存储日志
let globalLogs: string[] = [];
const { RangePicker } = DatePicker;
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;
}
// 添加日志的函数
export const addLog = (log: string) => {
const timestamp = new Date().toLocaleString();
const formattedLog = `[${timestamp}] ${log}`;
globalLogs = [...globalLogs, formattedLog];
// 如果需要可以将日志保存到localStorage
localStorage.setItem('systemLogs', JSON.stringify(globalLogs));
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',
});
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 [logs, setLogs] = useState<string[]>([]);
// 组件加载时从全局变量或localStorage获取日志
useEffect(() => {
// 尝试从localStorage获取日志
const storedLogs = localStorage.getItem('systemLogs');
if (storedLogs) {
setLogs(JSON.parse(storedLogs));
} else {
setLogs(globalLogs);
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 handleSearch = (values: any) => {
const { timeRange, ...rest } = values;
const params: any = {
...rest,
page: 1, // 重置到第一页
pageSize: queryParams.pageSize,
};
// 处理时间范围
if (timeRange && timeRange.length === 2) {
params.startTime = timeRange[0].startOf('day').toISOString();
params.endTime = timeRange[1].endOf('day').toISOString();
}
}, []);
setQueryParams(params);
};
// 格式化时间显示
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)
},
{
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="max-w-4xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-6"></h1>
<div className="bg-white p-6 rounded-lg shadow">
{logs.length === 0 ? (
<p className="text-gray-500"></p>
) : (
<ul className="space-y-2">
{logs.map((log, index) => (
<li key={index} className="p-2 border-b border-gray-200">
{log}
</li>
))}
</ul>
)}
<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="status" label="状态">
<Select placeholder="请选择状态" allowClear>
<Option value="success"></Option>
<Option value="failure"></Option>
</Select>
</Form.Item>
<Form.Item name="timeRange" label="时间范围">
<RangePicker className="w-full" />
</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: 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>
);
};

View File

@ -412,6 +412,7 @@ model Department {
// watchedPost Post[] @relation("post_watch_dept")
hasChildren Boolean? @default(false) @map("has_children")
logs SystemLog[]
@@index([parentId])
@@index([isDomain])
@ -432,11 +433,11 @@ model StaffField {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
StaffFieldValue StaffFieldValue[]
@@index([group])
@@index([order])
@@map("staff_field")
StaffFieldValue StaffFieldValue[]
}
model StaffFieldValue {
@ -464,7 +465,6 @@ model Staff {
avatar String? @map("avatar")
enabled Boolean? @default(true)
// 个人基本信息
idNumber String? @unique @map("id_number") // 身份证号
type String? @map("type") // 人员类型
@ -486,7 +486,6 @@ model Staff {
proxyPosition String? @map("proxy_position") // 代理职务
post String? @map("post") // 岗位
// 入职相关信息
hireDate DateTime? @map("hire_date") // 入职时间
seniority DateTime? @map("seniority_date") // 工龄认定时间
@ -544,7 +543,8 @@ model Staff {
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
absent Boolean? @default(false) @map("absent")
// 系统日志
logs SystemLog[] @relation("log_operator")
// 添加自定义字段值关联
fieldValues StaffFieldValue[]
@ -583,7 +583,49 @@ model ShareCode {
expiresAt DateTime? @map("expires_at")
isUsed Boolean? @default(false)
fileName String? @map("file_name")
@@index([code])
@@index([fileId])
@@index([expiresAt])
}
model SystemLog {
id String @id @default(cuid())
timestamp DateTime @default(now()) @map("timestamp")
level String? @map("level") // info, warning, error, debug
module String? @map("module") // 操作模块,如"人员管理"
action String? @map("action") // 具体操作,如"新增人员"、"修改人员"
// 操作人信息
operatorId String? @map("operator_id")
operator Staff? @relation("log_operator", fields: [operatorId], references: [id])
ipAddress String? @map("ip_address")
// 操作对象信息
targetId String? @map("target_id") // 操作对象ID
targetType String? @map("target_type") // 操作对象类型,如"staff"、"department"
targetName String? @map("target_name") // 操作对象名称
// 详细信息
details Json? @map("details") // 详细变更信息存储为JSON
beforeData Json? @map("before_data") // 操作前数据
afterData Json? @map("after_data") // 操作后数据
// 操作结果
status String? @map("status") // success, failure
errorMessage String? @map("error_message") // 如果操作失败,记录错误信息
// 关联部门
departmentId String? @map("department_id")
department Department? @relation(fields: [departmentId], references: [id])
// 优化索引
@@index([timestamp])
@@index([level])
@@index([module, action])
@@index([operatorId])
@@index([targetId, targetType])
@@index([status])
@@index([departmentId])
@@map("system_log")
}

View File

@ -61,7 +61,8 @@ export enum ObjectType {
RESOURCE = "resource",
TRAIN_CONTENT = "trainContent",
TRAIN_SITUATION = "trainSituation",
DAILY_TRAIN = "dailyTrainTime"
DAILY_TRAIN = "dailyTrainTime",
SYSTEM_LOG = 'system_log'
}
export enum RolePerms {
// Create Permissions 创建权限