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