This commit is contained in:
linfeng 2025-03-26 13:03:19 +08:00
commit d8d2e9f8e4
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 { TransformModule } from './models/transform/transform.module';
import { RealTimeModule } from './socket/realtime/realtime.module'; import { RealTimeModule } from './socket/realtime/realtime.module';
import { UploadModule } from './upload/upload.module'; import { UploadModule } from './upload/upload.module';
import { SystemLogModule } from '@server/models/sys-logs/systemLog.module';
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
@ -42,7 +42,8 @@ import { UploadModule } from './upload/upload.module';
MinioModule, MinioModule,
CollaborationModule, CollaborationModule,
RealTimeModule, RealTimeModule,
UploadModule UploadModule,
SystemLogModule
], ],
providers: [{ providers: [{
provide: APP_FILTER, 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 { ResourceModule } from '@server/models/resource/resource.module';
import { TrainSituationModule } from '@server/models/train-situation/trainSituation.module'; import { TrainSituationModule } from '@server/models/train-situation/trainSituation.module';
import { DailyTrainModule } from '@server/models/daily-train/dailyTrain.module'; import { DailyTrainModule } from '@server/models/daily-train/dailyTrain.module';
import { SystemLogModule } from '@server/models/sys-logs/systemLog.module';
@Module({ @Module({
imports: [ imports: [
AuthModule, AuthModule,
@ -37,6 +38,7 @@ import { DailyTrainModule } from '@server/models/daily-train/dailyTrain.module';
TrainContentModule, TrainContentModule,
TrainSituationModule, TrainSituationModule,
DailyTrainModule, DailyTrainModule,
SystemLogModule,
], ],
controllers: [], controllers: [],
providers: [TrpcService, TrpcRouter, Logger], 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 { TrainContentRouter } from '@server/models/train-content/trainContent.router';
import { TrainSituationRouter } from '@server/models/train-situation/trainSituation.router'; import { TrainSituationRouter } from '@server/models/train-situation/trainSituation.router';
import { DailyTrainRouter } from '@server/models/daily-train/dailyTrain.router'; import { DailyTrainRouter } from '@server/models/daily-train/dailyTrain.router';
import { SystemLogRouter } from '@server/models/sys-logs/systemLog.router';
@Injectable() @Injectable()
export class TrpcRouter { export class TrpcRouter {
@ -38,6 +39,7 @@ export class TrpcRouter {
private readonly trainContent: TrainContentRouter, private readonly trainContent: TrainContentRouter,
private readonly trainSituation:TrainSituationRouter, private readonly trainSituation:TrainSituationRouter,
private readonly dailyTrain:DailyTrainRouter, private readonly dailyTrain:DailyTrainRouter,
private readonly systemLogRouter: SystemLogRouter,
) {} ) {}
getRouter() { getRouter() {
return; return;
@ -57,7 +59,8 @@ export class TrpcRouter {
resource: this.resource.router, resource: this.resource.router,
trainContent:this.trainContent.router, trainContent:this.trainContent.router,
trainSituation:this.trainSituation.router, trainSituation:this.trainSituation.router,
dailyTrain:this.dailyTrain.router dailyTrain:this.dailyTrain.router,
systemLog: this.systemLogRouter.router,
}); });
wss: WebSocketServer = undefined; wss: WebSocketServer = undefined;

View File

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

View File

@ -1,13 +1,38 @@
import { Input, Button, Space } from 'antd'; import { Input, Button } from 'antd';
import React, { useState } from 'react'; import React, { useState, useRef, useEffect } from 'react';
type InfoCardProps = { type InfoCardProps = {
onAdd: (content: string) => void; 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 [content, setContent] = useState('');
const [addedContents, setAddedContents] = useState<string[]>([]); 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 = () => { const handleAdd = () => {
if (content) { if (content) {
onAdd(content); onAdd(content);
@ -15,23 +40,39 @@ const InfoCard: React.FC<InfoCardProps> = ({ onAdd }) => {
setContent(''); setContent('');
} }
} }
return ( return (
// 增大内边距,避免内容被覆盖 <div className="w-full">
<div style={{ border: '1px solid #d9d9d9', padding: '24px' }}> <div className="flex items-center mb-3 w-full">
<Space>
<Input <Input
placeholder='请输入内容' placeholder='请输入内容'
value={content} value={content}
onChange={(e) => setContent(e.target.value)} onChange={(e) => setContent(e.target.value)}
className="flex-1 mr-2"
onPressEnter={handleAdd}
/> />
<Button type='primary' onClick={handleAdd}></Button> <Button
</Space> type='primary'
<div style={{ marginTop: '24px' }}> onClick={handleAdd}
className="shrink-0"
>
</Button>
</div>
{/* 内容容器 */}
<div ref={contentContainerRef} className="w-full bg-white">
{addedContents.map((item, index) => ( {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>
</div> </div>
); );
} }
export default InfoCard; export default InfoCard;

View File

@ -1,5 +1,5 @@
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 } from "react"; import { useState, useMemo, useEffect } from "react";
import { useStaff } from "@nice/client"; import { useStaff } from "@nice/client";
import { areaOptions } from './area-options'; import { areaOptions } from './area-options';
import InfoCard from './infoCard'; import InfoCard from './infoCard';
@ -11,9 +11,39 @@ const StaffInfoWrite = () => {
const { create, setCustomFieldValue, useCustomFields } = useStaff(); const { create, setCustomFieldValue, useCustomFields } = useStaff();
const { data: fields, isLoading: fieldsLoading } = useCustomFields(); const { data: fields, isLoading: fieldsLoading } = useCustomFields();
const [infoCards, setInfoCards] = useState<any[]>([]); const [infoCards, setInfoCards] = useState<any[]>([]);
const handleAdd = ( content: string) => {
// 添加跟踪培训和鉴定状态的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 }]); 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(() => { const fieldGroups = useMemo(() => {
if (!fields) return {}; if (!fields) return {};
@ -27,34 +57,103 @@ const StaffInfoWrite = () => {
}, {}); }, {});
}, [fields]); }, [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) { switch (field.type) {
case 'text': case 'text':
return <Input />; return <Input disabled={isDisabled} />;
case 'number': case 'number':
return <InputNumber />; return <InputNumber disabled={isDisabled} />;
case 'date': case 'date':
return <DatePicker />; return <DatePicker disabled={isDisabled} />;
case 'select': case 'select':
if (field.options && field.options.length > 0) { 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': case 'radio':
if (field.options && field.options.length > 0) { 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': 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': case 'cascader':
if(field.label === '籍贯'){ if(field.label === '籍贯'){
return <Cascader options={areaOptions} />; return <Cascader options={areaOptions} disabled={isDisabled} />;
}else{ }else{
return <Input />; return <Input disabled={isDisabled} />;
} }
default: default:
return <Input />; return <Input className="w-full" disabled={isDisabled} />;
} }
}; };
@ -120,16 +219,27 @@ const StaffInfoWrite = () => {
form={form} form={form}
layout="vertical" layout="vertical"
onFinish={onFinish} onFinish={onFinish}
onValuesChange={(changedValues) => {
if ('hasTrain' in changedValues) {
setHasTrain(!!changedValues.hasTrain);
}
if ('hasCert' in changedValues) {
setHasCert(!!changedValues.hasCert);
}
}}
className="space-y-6 mt-6" className="space-y-6 mt-6"
> >
{Object.entries(fieldGroups).map(([groupName, groupFields]) => ( {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> <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) => ( {groupFields.map((field: any) => (
<Form.Item <Form.Item
key={field.id} key={field.id}
label={field.label} label={<span className="block mb-2">{field.label}</span>}
name={field.name} name={field.name}
rules={[ rules={[
{ {
@ -137,8 +247,16 @@ const StaffInfoWrite = () => {
message: `请输入${field.label}`, 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> </Form.Item>
))} ))}
</div> </div>

View File

@ -5,13 +5,10 @@ import { useState } from "react";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useStaff } from "@nice/client"; import { useStaff } from "@nice/client";
import DepartmentChildrenSelect from "@web/src/components/models/department/department-children-select"; 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 DepartmentSelect from "@web/src/components/models/department/department-select";
import { addLog } from "@web/src/app/main/systemlog/SystemLogPage";
const { TextArea } = Input; const { TextArea } = Input;
const StaffInformation = () => { const StaffInformation = () => {
const [modalForm] = Form.useForm(); const [modalForm] = Form.useForm();
const [form] = Form.useForm(); const [form] = Form.useForm();
@ -23,7 +20,7 @@ const StaffInformation = () => {
const [equipmentList, setEquipmentList] = useState<string[]>([]); // 新增装备列表 const [equipmentList, setEquipmentList] = useState<string[]>([]); // 新增装备列表
const [projectsList, setProjectsList] = useState<string[]>([]); // 新增任务列表 const [projectsList, setProjectsList] = useState<string[]>([]); // 新增任务列表
const {create, update} = useStaff(); const {create, update} = useStaff();
const showModal = (type: 'awards' | 'punishments' | 'equipment' | 'projects') => { const showModal = (type: 'awards' | 'punishments' | 'equipment' | 'projects') => {
setModalType(type); setModalType(type);
setIsModalVisible(true); setIsModalVisible(true);
@ -61,8 +58,10 @@ const StaffInformation = () => {
message.warning('请输入内容'); message.warning('请输入内容');
} }
}; };
const onFinish = async (values: any) => { const onFinish = async (values: any) => {
console.log('开始提交表单'); console.log('开始提交表单');
try { try {
setLoading(true); setLoading(true);
const formattedValues = { const formattedValues = {
@ -75,7 +74,7 @@ const StaffInformation = () => {
hireDate: values.hireDate?.toISOString(), hireDate: values.hireDate?.toISOString(),
seniority: values.seniority?.toISOString(), seniority: values.seniority?.toISOString(),
currentPositionDate: values.currentPositionDate?.toISOString(), currentPositionDate: values.currentPositionDate?.toISOString(),
rankDate: values.rankDate?.toISOString(), rankDate: values.rankDate?.toISOString(), // 修改这里
}; };
await create.mutateAsync( await create.mutateAsync(
@ -88,31 +87,9 @@ const StaffInformation = () => {
console.log('奖励列表:', rewardsList); console.log('奖励列表:', rewardsList);
console.log('处分列表:', punishmentsList); 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("信息提交成功"); message.success("信息提交成功");
} catch (error) { } catch (error) {
console.error('提交出错:', error); console.error('提交出错:', error);
// 添加错误日志
addLog(`提交人员信息失败: ${error instanceof Error ? error.message : '未知错误'}`);
message.error("提交失败,请重试"); message.error("提交失败,请重试");
} finally { } finally {
setLoading(false); setLoading(false);

View File

@ -1,49 +1,249 @@
"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 { SearchOutlined, ReloadOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { api } from '@nice/client';
// 创建一个全局变量来存储日志 const { RangePicker } = DatePicker;
let globalLogs: string[] = []; 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) => { export const addLog = async (logData: Omit<ILog, 'id' | 'timestamp'>) => {
const timestamp = new Date().toLocaleString(); try {
const formattedLog = `[${timestamp}] ${log}`; // 使用tRPC发送日志
globalLogs = [...globalLogs, formattedLog]; const result = await api.systemLog.create.mutate({
// 如果需要可以将日志保存到localStorage ...logData,
localStorage.setItem('systemLogs', JSON.stringify(globalLogs)); 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 [logs, setLogs] = useState<string[]>([]); const [form] = Form.useForm();
// 组件加载时从全局变量或localStorage获取日志 const [queryParams, setQueryParams] = useState({
useEffect(() => { page: 1,
// 尝试从localStorage获取日志 pageSize: 10,
const storedLogs = localStorage.getItem('systemLogs'); });
if (storedLogs) {
setLogs(JSON.parse(storedLogs)); // 使用tRPC查询日志
} else { const { data, isLoading, refetch } = api.systemLog.getLogs.useQuery({
setLogs(globalLogs); page: queryParams.page,
} pageSize: queryParams.pageSize
}, []); });
return (
<div className="max-w-4xl mx-auto p-6"> // 处理表格分页变化
<h1 className="text-2xl font-bold mb-6"></h1> const handleTableChange = (pagination: any) => {
<div className="bg-white p-6 rounded-lg shadow"> setQueryParams({
{logs.length === 0 ? ( ...queryParams,
<p className="text-gray-500"></p> page: pagination.current,
) : ( pageSize: pagination.pageSize,
<ul className="space-y-2"> });
{logs.map((log, index) => ( };
<li key={index} className="p-2 border-b border-gray-200">
{log} // 处理表单查询
</li> const handleSearch = (values: any) => {
))} const { timeRange, ...rest } = values;
</ul>
)} const params: any = {
</div> ...rest,
</div> 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="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>
);
}; };
export default SystemLogPage; export default SystemLogPage;

View File

@ -411,7 +411,8 @@ model Department {
trainPlans TrainPlan[] @relation("TrainPlanDept") trainPlans TrainPlan[] @relation("TrainPlanDept")
// watchedPost Post[] @relation("post_watch_dept") // watchedPost Post[] @relation("post_watch_dept")
hasChildren Boolean? @default(false) @map("has_children") hasChildren Boolean? @default(false) @map("has_children")
logs SystemLog[]
@@index([parentId]) @@index([parentId])
@@index([isDomain]) @@index([isDomain])
@ -421,33 +422,33 @@ model Department {
} }
model StaffField { model StaffField {
id String @id @default(cuid()) id String @id @default(cuid())
name String @unique // 字段名称 name String @unique // 字段名称
label String? // 字段显示名称 label String? // 字段显示名称
type String // 字段类型 (text, number, date, select 等) type String // 字段类型 (text, number, date, select 等)
required Boolean? @default(false) required Boolean? @default(false)
order Float? // 显示顺序 order Float? // 显示顺序
options Json? // 对于选择类型字段的可选值 options Json? // 对于选择类型字段的可选值
group String? // 字段分组 (基本信息、工作信息等) group String? // 字段分组 (基本信息、工作信息等)
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at") deletedAt DateTime? @map("deleted_at")
StaffFieldValue StaffFieldValue[]
@@index([group]) @@index([group])
@@index([order]) @@index([order])
@@map("staff_field") @@map("staff_field")
StaffFieldValue StaffFieldValue[]
} }
model StaffFieldValue { model StaffFieldValue {
id String @id @default(cuid()) id String @id @default(cuid())
staffId String @map("staff_id") staffId String @map("staff_id")
fieldId String @map("field_id") fieldId String @map("field_id")
value String? // 字段值 value String? // 字段值
staff Staff @relation(fields: [staffId], references: [id]) staff Staff @relation(fields: [staffId], references: [id])
field StaffField @relation(fields: [fieldId], references: [id]) field StaffField @relation(fields: [fieldId], references: [id])
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
@@unique([staffId, fieldId]) @@unique([staffId, fieldId])
@@index([staffId]) @@index([staffId])
@ -457,96 +458,95 @@ model StaffFieldValue {
model Staff { model Staff {
// 基础信息 // 基础信息
id String @id @default(cuid()) id String @id @default(cuid())
username String @unique @map("username") username String @unique @map("username")
password String? @map("password") password String? @map("password")
showname String? @map("showname") showname String? @map("showname")
avatar String? @map("avatar") avatar String? @map("avatar")
enabled Boolean? @default(true) enabled Boolean? @default(true)
// 个人基本信息 // 个人基本信息
idNumber String? @unique @map("id_number") // 身份证号 idNumber String? @unique @map("id_number") // 身份证号
type String? @map("type") // 人员类型 type String? @map("type") // 人员类型
officerId String? @unique @map("officer_id") // 警号 officerId String? @unique @map("officer_id") // 警号
phoneNumber String? @unique @map("phone_number") // 手机号 phoneNumber String? @unique @map("phone_number") // 手机号
age Int? @map("age") // 年龄 age Int? @map("age") // 年龄
sex Boolean? @map("sex") // 性别 sex Boolean? @map("sex") // 性别
bloodType String? @map("blood_type") // 血型 bloodType String? @map("blood_type") // 血型
birthplace String? @map("birthplace") // 籍贯 birthplace String? @map("birthplace") // 籍贯
source String? @map("source") // 来源 source String? @map("source") // 来源
// 政治信息 // 政治信息
politicalStatus String? @map("political_status") // 政治面貌 politicalStatus String? @map("political_status") // 政治面貌
partyPosition String? @map("party_position") // 党内职务 partyPosition String? @map("party_position") // 党内职务
// 职务信息 // 职务信息
rank String? @map("rank") // 衔职级别 rank String? @map("rank") // 衔职级别
rankDate DateTime? @map("rank_date") // 衔职时间 rankDate DateTime? @map("rank_date") // 衔职时间
proxyPosition String? @map("proxy_position") // 代理职务 proxyPosition String? @map("proxy_position") // 代理职务
post String? @map("post") // 岗位 post String? @map("post") // 岗位
// 入职相关信息 // 入职相关信息
hireDate DateTime? @map("hire_date") // 入职时间 hireDate DateTime? @map("hire_date") // 入职时间
seniority DateTime? @map("seniority_date") // 工龄认定时间 seniority DateTime? @map("seniority_date") // 工龄认定时间
sourceType String? @map("source_type") // 来源类型 sourceType String? @map("source_type") // 来源类型
isReentry Boolean? @default(false) @map("is_reentry") // 是否二次入职 isReentry Boolean? @default(false) @map("is_reentry") // 是否二次入职
isExtended Boolean? @default(false) @map("is_extended") // 是否延期服役 isExtended Boolean? @default(false) @map("is_extended") // 是否延期服役
currentPositionDate DateTime? @map("current_position_date") // 现岗位开始时间 currentPositionDate DateTime? @map("current_position_date") // 现岗位开始时间
// 教育背景 // 教育背景
education String? @map("education") // 学历 education String? @map("education") // 学历
educationType String? @map("education_type") // 学历形式 educationType String? @map("education_type") // 学历形式
isGraduated Boolean? @default(true) @map("is_graduated") // 是否毕业 isGraduated Boolean? @default(true) @map("is_graduated") // 是否毕业
major String? @map("major") // 专业 major String? @map("major") // 专业
foreignLang String? @map("foreign_lang") // 外语能力 foreignLang String? @map("foreign_lang") // 外语能力
// 培训 // 培训
trainType String? @map("train_type") // 培训类型 trainType String? @map("train_type") // 培训类型
trainInstitute String? @map("train_institute") // 培训机构 trainInstitute String? @map("train_institute") // 培训机构
trainMajor String? @map("train_major") // 培训专业 trainMajor String? @map("train_major") // 培训专业
hasTrain Boolean? @default(false) @map("has_train") // 是否参加培训 hasTrain Boolean? @default(false) @map("has_train") // 是否参加培训
//鉴定 //鉴定
certRank String? @map("cert_rank") // 鉴定等级 certRank String? @map("cert_rank") // 鉴定等级
certWork String? @map("cert_work") // 鉴定工种 certWork String? @map("cert_work") // 鉴定工种
hasCert Boolean? @default(false) @map("has_cert") // 是否参加鉴定 hasCert Boolean? @default(false) @map("has_cert") // 是否参加鉴定
// 工作信息 // 工作信息
equipment String? @map("equipment") // 操作维护装备 equipment String? @map("equipment") // 操作维护装备
projects String? @map("projects") // 演训任务经历 projects String? @map("projects") // 演训任务经历
awards String? @map("awards") // 奖励信息 awards String? @map("awards") // 奖励信息
punishments String? @map("staff_punishments") // 处分信息 punishments String? @map("staff_punishments") // 处分信息
// 部门关系 // 部门关系
domainId String? @map("domain_id") domainId String? @map("domain_id")
deptId String? @map("dept_id") deptId String? @map("dept_id")
domain Department? @relation("DomainStaff", fields: [domainId], references: [id]) domain Department? @relation("DomainStaff", fields: [domainId], references: [id])
department Department? @relation("DeptStaff", fields: [deptId], references: [id]) department Department? @relation("DeptStaff", fields: [deptId], references: [id])
order Float? order Float?
// 关联关系 // 关联关系
trainSituations TrainSituation[] trainSituations TrainSituation[]
visits Visit[] visits Visit[]
posts Post[] posts Post[]
learningPosts Post[] @relation("post_student") learningPosts Post[] @relation("post_student")
sentMsgs Message[] @relation("message_sender") sentMsgs Message[] @relation("message_sender")
receivedMsgs Message[] @relation("message_receiver") receivedMsgs Message[] @relation("message_receiver")
enrollments Enrollment[] enrollments Enrollment[]
teachedPosts PostInstructor[] teachedPosts PostInstructor[]
ownedResources Resource[] ownedResources Resource[]
position Position? @relation("StaffPosition", fields: [positionId], references: [id]) position Position? @relation("StaffPosition", fields: [positionId], references: [id])
positionId String? @map("position_id") positionId String? @map("position_id")
// 系统信息 // 系统信息
registerToken String? registerToken String?
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at") deletedAt DateTime? @map("deleted_at")
absent Boolean? @default(false) @map("absent") absent Boolean? @default(false) @map("absent")
// 系统日志
logs SystemLog[] @relation("log_operator")
// 添加自定义字段值关联 // 添加自定义字段值关联
fieldValues StaffFieldValue[] fieldValues StaffFieldValue[]
@@index([officerId]) @@index([officerId])
@@index([deptId]) @@index([deptId])
@ -576,14 +576,56 @@ model TrainPlan {
} }
model ShareCode { model ShareCode {
id String @id @default(cuid()) id String @id @default(cuid())
code String? @unique code String? @unique
fileId String? @unique fileId String? @unique
createdAt DateTime @default(now()) createdAt DateTime @default(now())
expiresAt DateTime? @map("expires_at") expiresAt DateTime? @map("expires_at")
isUsed Boolean? @default(false) isUsed Boolean? @default(false)
fileName String? @map("file_name") fileName String? @map("file_name")
@@index([code])
@@index([fileId]) @@index([code])
@@index([expiresAt]) @@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", RESOURCE = "resource",
TRAIN_CONTENT = "trainContent", TRAIN_CONTENT = "trainContent",
TRAIN_SITUATION = "trainSituation", TRAIN_SITUATION = "trainSituation",
DAILY_TRAIN = "dailyTrainTime" DAILY_TRAIN = "dailyTrainTime",
SYSTEM_LOG = 'system_log'
} }
export enum RolePerms { export enum RolePerms {
// Create Permissions 创建权限 // Create Permissions 创建权限