lin
This commit is contained in:
parent
feae80d0f8
commit
076c92ab7e
|
@ -1,15 +1,16 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { StaffService } from './staff.service';
|
import { StaffService } from './staff.service';
|
||||||
import { StaffRouter } from './staff.router';
|
|
||||||
import { TrpcService } from '@server/trpc/trpc.service';
|
|
||||||
import { DepartmentModule } from '../department/department.module';
|
|
||||||
import { StaffController } from './staff.controller';
|
import { StaffController } from './staff.controller';
|
||||||
import { StaffRowService } from './staff.row.service';
|
|
||||||
|
|
||||||
|
import { DepartmentModule } from '../department/department.module';
|
||||||
|
import { TrpcModule } from '../../trpc/trpc.module';
|
||||||
|
import { StaffRouter } from './staff.router';
|
||||||
|
import { StaffRowService } from './staff.row.service';
|
||||||
|
import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DepartmentModule],
|
imports: [DepartmentModule],
|
||||||
providers: [StaffService, StaffRouter, TrpcService, StaffRowService],
|
providers: [StaffService, StaffRouter, StaffRowService, TrpcService],
|
||||||
exports: [StaffService, StaffRouter, StaffRowService],
|
exports: [StaffService, StaffRouter],
|
||||||
controllers: [StaffController],
|
controllers: [StaffController, ],
|
||||||
})
|
})
|
||||||
export class StaffModule { }
|
export class StaffModule {}
|
||||||
|
|
|
@ -95,5 +95,50 @@ export class StaffRouter {
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await this.staffService.findUnique(input);
|
return await this.staffService.findUnique(input);
|
||||||
}),
|
}),
|
||||||
|
addCustomField: this.trpc.procedure
|
||||||
|
.input(z.object({
|
||||||
|
name: z.string(),
|
||||||
|
label: z.string().optional(),
|
||||||
|
type: z.string(), // text, number, date, select 等
|
||||||
|
required: z.boolean().optional(),
|
||||||
|
order: z.number().optional(),
|
||||||
|
options: z.any().optional(), // 对于选择类型字段的可选值
|
||||||
|
group: z.string().optional(), // 字段分组
|
||||||
|
}))
|
||||||
|
.mutation(({ input }) => {
|
||||||
|
return this.staffService.addCustomField(input as any);
|
||||||
|
}),
|
||||||
|
updateCustomField: this.trpc.procedure
|
||||||
|
.input(z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string().optional(),
|
||||||
|
label: z.string().optional(),
|
||||||
|
type: z.string().optional(),
|
||||||
|
required: z.boolean().optional(),
|
||||||
|
order: z.number().optional(),
|
||||||
|
options: z.any().optional(),
|
||||||
|
group: z.string().optional(),
|
||||||
|
}))
|
||||||
|
.mutation(({ input }) => {
|
||||||
|
return this.staffService.updateCustomField(input as any);
|
||||||
|
}),
|
||||||
|
deleteCustomField: this.trpc.procedure
|
||||||
|
.input(z.string())
|
||||||
|
.mutation(({ input }) => {
|
||||||
|
return this.staffService.deleteCustomField(input as any);
|
||||||
|
}),
|
||||||
|
getCustomFields: this.trpc.procedure
|
||||||
|
.query(() => {
|
||||||
|
return this.staffService.getCustomFields();
|
||||||
|
}),
|
||||||
|
setCustomFieldValue: this.trpc.procedure
|
||||||
|
.input(z.object({
|
||||||
|
staffId: z.string(),
|
||||||
|
fieldId: z.string(),
|
||||||
|
value: z.string().optional(),
|
||||||
|
}))
|
||||||
|
.mutation(({ input }) => {
|
||||||
|
return this.staffService.setCustomFieldValue(input as any);
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
|
||||||
constructor(private readonly departmentService: DepartmentService) {
|
constructor(private readonly departmentService: DepartmentService) {
|
||||||
super(db, ObjectType.STAFF, true);
|
super(db, ObjectType.STAFF, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取某一单位下所有staff的记录
|
* 获取某一单位下所有staff的记录
|
||||||
* @param deptId 单位的id
|
* @param deptId 单位的id
|
||||||
|
@ -36,30 +37,78 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(args: Prisma.StaffCreateArgs) {
|
async create(args: Prisma.StaffCreateArgs) {
|
||||||
const { data } = args;
|
const { data, select } = args;
|
||||||
await this.validateUniqueFields(data);
|
const { fieldValues, ...staffData } = data as any;
|
||||||
const createData = {
|
|
||||||
...data,
|
// 创建员工基本信息
|
||||||
password: await argon2.hash((data.password || '123456') as string),
|
const staff = await super.create({
|
||||||
};
|
...args,
|
||||||
const result = await super.create({ ...args, data: createData });
|
data: {
|
||||||
this.emitDataChangedEvent(result, CrudOperation.CREATED);
|
...staffData,
|
||||||
return result;
|
password: await argon2.hash((staffData.password || '123456') as string),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果有自定义字段值,创建它们
|
||||||
|
if (fieldValues) {
|
||||||
|
await db.staffFieldValue.createMany({
|
||||||
|
data: Object.entries(fieldValues).map(([fieldId, value]) => ({
|
||||||
|
staffId: staff.id,
|
||||||
|
fieldId,
|
||||||
|
value: String(value),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emitDataChangedEvent(staff, CrudOperation.CREATED);
|
||||||
|
return staff;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(args: Prisma.StaffUpdateArgs) {
|
async update(args: Prisma.StaffUpdateArgs) {
|
||||||
const { data, where } = args;
|
const { data, where } = args;
|
||||||
await this.validateUniqueFields(data, where.id);
|
const { fieldValues, ...staffData } = data as any;
|
||||||
const updateData = {
|
|
||||||
...data,
|
// 更新员工基本信息
|
||||||
...(data.password && {
|
const staff = await super.update({
|
||||||
password: await argon2.hash(data.password as string),
|
...args,
|
||||||
}),
|
data: {
|
||||||
};
|
...staffData,
|
||||||
const result = await super.update({ ...args, data: updateData });
|
...(staffData.password && {
|
||||||
this.emitDataChangedEvent(result, CrudOperation.UPDATED);
|
password: await argon2.hash(staffData.password as string),
|
||||||
return result;
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果有自定义字段值,更新它们
|
||||||
|
if (fieldValues) {
|
||||||
|
await Promise.all(
|
||||||
|
Object.entries(fieldValues).map(([fieldId, value]) =>
|
||||||
|
db.staffFieldValue.upsert({
|
||||||
|
where: {
|
||||||
|
staffId_fieldId: {
|
||||||
|
staffId: staff.id,
|
||||||
|
fieldId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
staffId: staff.id,
|
||||||
|
fieldId,
|
||||||
|
value: String(value),
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
value: String(value),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emitDataChangedEvent(staff, CrudOperation.UPDATED);
|
||||||
|
return staff;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async validateUniqueFields(data: any, excludeId?: string) {
|
private async validateUniqueFields(data: any, excludeId?: string) {
|
||||||
const uniqueFields = [
|
const uniqueFields = [
|
||||||
{
|
{
|
||||||
|
@ -119,72 +168,91 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
async findUnique(args: Prisma.StaffFindUniqueArgs) {
|
||||||
// * 根据关键词或ID集合查找员工
|
const staff = await super.findUnique(args);
|
||||||
// * @param data 包含关键词、域ID和ID集合的对象
|
if (!staff) return null;
|
||||||
// * @returns 匹配的员工记录列表
|
|
||||||
// */
|
|
||||||
// async findMany(data: z.infer<typeof StaffMethodSchema.findMany>) {
|
|
||||||
// const { keyword, domainId, ids, deptId, limit = 30 } = data;
|
|
||||||
// const idResults = ids
|
|
||||||
// ? await db.staff.findMany({
|
|
||||||
// where: {
|
|
||||||
// id: { in: ids },
|
|
||||||
// deletedAt: null,
|
|
||||||
// domainId,
|
|
||||||
// deptId,
|
|
||||||
// },
|
|
||||||
// select: {
|
|
||||||
// id: true,
|
|
||||||
// showname: true,
|
|
||||||
// username: true,
|
|
||||||
// deptId: true,
|
|
||||||
// domainId: true,
|
|
||||||
// department: true,
|
|
||||||
// domain: true,
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
// : [];
|
|
||||||
|
|
||||||
// const mainResults = await db.staff.findMany({
|
// 获取自定义字段值
|
||||||
// where: {
|
const fieldValues = await db.staffFieldValue.findMany({
|
||||||
// deletedAt: null,
|
where: { staffId: staff.id },
|
||||||
// domainId,
|
include: { field: true },
|
||||||
// deptId,
|
});
|
||||||
// OR: (keyword || ids) && [
|
|
||||||
// { showname: { contains: keyword } },
|
|
||||||
// {
|
|
||||||
// username: {
|
|
||||||
// contains: keyword,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// { phoneNumber: { contains: keyword } },
|
|
||||||
// // {
|
|
||||||
// // id: { in: ids },
|
|
||||||
// // },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// select: {
|
|
||||||
// id: true,
|
|
||||||
// showname: true,
|
|
||||||
// username: true,
|
|
||||||
// deptId: true,
|
|
||||||
// domainId: true,
|
|
||||||
// department: true,
|
|
||||||
// domain: true,
|
|
||||||
// },
|
|
||||||
// orderBy: { order: 'asc' },
|
|
||||||
// take: limit !== -1 ? limit : undefined,
|
|
||||||
// });
|
|
||||||
// // Combine results, ensuring no duplicates
|
|
||||||
// const combinedResults = [
|
|
||||||
// ...mainResults,
|
|
||||||
// ...idResults.filter(
|
|
||||||
// (idResult) =>
|
|
||||||
// !mainResults.some((mainResult) => mainResult.id === idResult.id),
|
|
||||||
// ),
|
|
||||||
// ];
|
|
||||||
|
|
||||||
// return combinedResults;
|
return {
|
||||||
// }
|
...staff,
|
||||||
|
fieldValues: fieldValues.reduce((acc, { field, value }) => ({
|
||||||
|
...acc,
|
||||||
|
[field.name]: value,
|
||||||
|
}), {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async addCustomField(data: {
|
||||||
|
name: string;
|
||||||
|
label?: string;
|
||||||
|
type: string;
|
||||||
|
required?: boolean;
|
||||||
|
order?: number;
|
||||||
|
options?: any;
|
||||||
|
group?: string;
|
||||||
|
}) {
|
||||||
|
return this.prisma.staffField.create({
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCustomField(data: {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
label?: string;
|
||||||
|
type?: string;
|
||||||
|
required?: boolean;
|
||||||
|
order?: number;
|
||||||
|
options?: any;
|
||||||
|
group?: string;
|
||||||
|
}) {
|
||||||
|
const { id, ...updateData } = data;
|
||||||
|
return this.prisma.staffField.update({
|
||||||
|
where: { id },
|
||||||
|
data: updateData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCustomField(id: string) {
|
||||||
|
return this.prisma.staffField.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCustomFields() {
|
||||||
|
return this.prisma.staffField.findMany({
|
||||||
|
orderBy: { order: 'asc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setCustomFieldValue(data: {
|
||||||
|
staffId: string;
|
||||||
|
fieldId: string;
|
||||||
|
value?: string;
|
||||||
|
}) {
|
||||||
|
const { staffId, fieldId, value } = data;
|
||||||
|
return this.prisma.staffFieldValue.upsert({
|
||||||
|
where: {
|
||||||
|
staffId_fieldId: {
|
||||||
|
staffId,
|
||||||
|
fieldId,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
staffId,
|
||||||
|
fieldId,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ export class TrpcRouter {
|
||||||
private readonly resource: ResourceRouter,
|
private readonly resource: ResourceRouter,
|
||||||
private readonly trainContent: TrainContentRouter,
|
private readonly trainContent: TrainContentRouter,
|
||||||
private readonly trainSituation:TrainSituationRouter,
|
private readonly trainSituation:TrainSituationRouter,
|
||||||
private readonly dailyTrain:DailyTrainRouter
|
private readonly dailyTrain:DailyTrainRouter,
|
||||||
) {}
|
) {}
|
||||||
getRouter() {
|
getRouter() {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { message, Modal, Button } from 'antd';
|
||||||
|
import { useStaff } from '@nice/client';
|
||||||
|
import { defaultFields } from './defaultFields';
|
||||||
|
|
||||||
|
type DefaultFieldInitializerProps = {
|
||||||
|
fields: any[] | undefined;
|
||||||
|
isProcessing: boolean;
|
||||||
|
setIsProcessing: (isProcessing: boolean) => void;
|
||||||
|
isInitConfirmVisible: boolean;
|
||||||
|
setIsInitConfirmVisible: (isVisible: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkFieldExists = (fields: any[] | undefined, name: string) => {
|
||||||
|
return fields?.some(field => field.name === name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DefaultFieldInitializer: React.FC<DefaultFieldInitializerProps> = ({
|
||||||
|
fields,
|
||||||
|
isProcessing,
|
||||||
|
setIsProcessing,
|
||||||
|
isInitConfirmVisible,
|
||||||
|
setIsInitConfirmVisible
|
||||||
|
}) => {
|
||||||
|
const { addCustomField } = useStaff();
|
||||||
|
|
||||||
|
const initializeDefaultFields = async () => {
|
||||||
|
try {
|
||||||
|
setIsProcessing(true);
|
||||||
|
|
||||||
|
// 检查是否已有数据
|
||||||
|
if (fields && fields.length > 0) {
|
||||||
|
message.warning('数据库中已存在字段数据,请先清空数据后再初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 使用 Promise.all 批量处理
|
||||||
|
await Promise.all(
|
||||||
|
defaultFields.map(async (field) => {
|
||||||
|
try {
|
||||||
|
if (!checkFieldExists(fields, field.name)) {
|
||||||
|
await addCustomField.mutateAsync(field);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`添加字段 ${field.name} 失败:`, error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
message.success('默认字段初始化成功');
|
||||||
|
setIsInitConfirmVisible(false);
|
||||||
|
} catch (error) {
|
||||||
|
message.error('初始化失败:' + (error instanceof Error ? error.message : '未知错误'));
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsInitConfirmVisible(true)}
|
||||||
|
type="default"
|
||||||
|
loading={isProcessing}
|
||||||
|
>
|
||||||
|
初始化默认字段
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
title="确认初始化"
|
||||||
|
open={isInitConfirmVisible}
|
||||||
|
onOk={initializeDefaultFields}
|
||||||
|
onCancel={() => setIsInitConfirmVisible(false)}
|
||||||
|
confirmLoading={isProcessing}
|
||||||
|
>
|
||||||
|
<p>初始化将导入所有预设字段,确定要继续吗?</p>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DefaultFieldInitializer;
|
|
@ -0,0 +1,174 @@
|
||||||
|
export const defaultFields = [
|
||||||
|
// 基本信息组
|
||||||
|
{ name: 'username', label: '用户名', type: 'text',required: true, group: '基本信息', order: 1 },
|
||||||
|
{ name: 'showname', label: '显示名称', type: 'text', group: '基本信息', order: 2 },
|
||||||
|
{ name: 'idNumber', label: '身份证号', type: 'text', group: '基本信息', order: 3 },
|
||||||
|
{ name: 'officerId', label: '警号', type: 'text', group: '基本信息', order: 4 },
|
||||||
|
{ name: 'phoneNumber', label: '手机号', type: 'text', group: '基本信息', order: 5 },
|
||||||
|
{ name: 'age', label: '年龄', type: 'number', group: '基本信息', order: 6 },
|
||||||
|
{ name: 'sex',
|
||||||
|
label: '性别',
|
||||||
|
type: 'radio',
|
||||||
|
options: [
|
||||||
|
{ label: '男', value: 'male' },
|
||||||
|
{ label: '女', value: 'female' },
|
||||||
|
],
|
||||||
|
group: '基本信息',
|
||||||
|
order: 7 },
|
||||||
|
{ name: 'bloodType',
|
||||||
|
label: '血型',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'A', value: 'A' },
|
||||||
|
{ label: 'B', value: 'B' },
|
||||||
|
{ label: 'AB', value: 'AB' },
|
||||||
|
{ label: 'O', value: 'O' },
|
||||||
|
],
|
||||||
|
group: '基本信息',
|
||||||
|
order: 8 },
|
||||||
|
{ name: 'birthplace', label: '籍贯', type: 'text', group: '基本信息', order: 9 },
|
||||||
|
{ name: 'source', label: '来源', type: 'text', group: '基本信息', order: 10 },
|
||||||
|
|
||||||
|
// 政治信息组
|
||||||
|
{ name: 'politicalStatus',
|
||||||
|
label: '政治面貌',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: '中共党员', value: '中共党员' },
|
||||||
|
{ label: '中共预备党员', value: '中共预备党员' },
|
||||||
|
{ label: '共青团员', value: '共青团员' },
|
||||||
|
{ label: '群众', value: '群众' },
|
||||||
|
],
|
||||||
|
group: '政治信息',
|
||||||
|
order: 11 },
|
||||||
|
{ name: 'partyPosition', label: '党内职务', type: 'text', group: '政治信息', order: 12 },
|
||||||
|
|
||||||
|
// 职务信息组
|
||||||
|
{ name: 'rank',
|
||||||
|
label: '衔职级别',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: '', value: '' },
|
||||||
|
{ label: '', value: '' },
|
||||||
|
],
|
||||||
|
group: '职务信息',
|
||||||
|
order: 13 },
|
||||||
|
{ name: 'rankDate', label: '衔职时间', type: 'date', group: '职务信息', order: 14 },
|
||||||
|
{ name: 'proxyPosition', label: '代理职务', type: 'text', group: '职务信息', order: 15 },
|
||||||
|
{ name: 'post', label: '岗位', type: 'text', group: '职务信息', order: 16 },
|
||||||
|
|
||||||
|
// 入职信息组
|
||||||
|
{ name: 'hireDate', label: '入职时间', type: 'date', group: '入职信息', order: 17 },
|
||||||
|
{ name: 'seniority', label: '工龄认定时间', type: 'date', group: '入职信息', order: 18 },
|
||||||
|
{ name: 'sourceType', label: '来源类型', type: 'text', group: '入职信息', order: 19 },
|
||||||
|
{ name: 'isReentry',
|
||||||
|
label: '是否二次入职',
|
||||||
|
type: 'radio',
|
||||||
|
options: [
|
||||||
|
{ label: '是', value: '是' },
|
||||||
|
{ label: '否', value: '否' },
|
||||||
|
],
|
||||||
|
group: '入职信息',
|
||||||
|
order: 20 },
|
||||||
|
{ name: 'isExtended',
|
||||||
|
label: '是否延期服役',
|
||||||
|
type: 'radio',
|
||||||
|
options: [
|
||||||
|
{ label: '是', value: '是' },
|
||||||
|
{ label: '否', value: '否' },
|
||||||
|
],
|
||||||
|
group: '入职信息',
|
||||||
|
order: 21 },
|
||||||
|
{ name: 'currentPositionDate', label: '现岗位开始时间', type: 'date', group: '入职信息', order: 22 },
|
||||||
|
|
||||||
|
// 教育信息组
|
||||||
|
{ name: 'education',
|
||||||
|
label: '学历',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: '博士', value: '博士' },
|
||||||
|
{ label: '硕士', value: '硕士' },
|
||||||
|
{ label: '本科', value: '本科' },
|
||||||
|
{ label: '专科', value: '专科' },
|
||||||
|
{ label: '中专', value: '中专' },
|
||||||
|
{ label: '高中', value: '高中' },
|
||||||
|
{ label: '初中', value: '初中' },
|
||||||
|
{ label: '小学', value: '小学' },
|
||||||
|
],
|
||||||
|
group: '教育信息',
|
||||||
|
order: 23 },
|
||||||
|
{ name: 'educationType',
|
||||||
|
label: '学历形式',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: '全日制', value: '全日制' },
|
||||||
|
{ label: '非全日制', value: '非全日制' },
|
||||||
|
],
|
||||||
|
group: '教育信息',
|
||||||
|
order: 24 },
|
||||||
|
{ name: 'isGraduated',
|
||||||
|
label: '是否毕业',
|
||||||
|
type: 'radio',
|
||||||
|
options: [
|
||||||
|
{ label: '是', value: '是' },
|
||||||
|
{ label: '否', value: '否' },
|
||||||
|
],
|
||||||
|
group: '教育信息',
|
||||||
|
order: 25 },
|
||||||
|
{ name: 'major', label: '专业', type: 'text', group: '教育信息', order: 26 },
|
||||||
|
{ name: 'foreignLang', label: '外语能力', type: 'text', group: '教育信息', order: 27 },
|
||||||
|
|
||||||
|
// 培训信息组
|
||||||
|
{ name: 'trainType', label: '培训类型', type: 'text', group: '培训信息', order: 28 },
|
||||||
|
{ name: 'trainInstitute', label: '培训机构', type: 'text', group: '培训信息', order: 29 },
|
||||||
|
{ name: 'trainMajor', label: '培训专业', type: 'text', group: '培训信息', order: 30 },
|
||||||
|
{ name: 'hasTrain',
|
||||||
|
label: '是否参加培训',
|
||||||
|
type: 'radio',
|
||||||
|
options: [
|
||||||
|
{ label: '是', value: '是' },
|
||||||
|
{ label: '否', value: '否' },
|
||||||
|
],
|
||||||
|
group: '培训信息',
|
||||||
|
order: 31 },
|
||||||
|
|
||||||
|
// 鉴定信息组
|
||||||
|
{ name: 'certRank', label: '鉴定等级', type: 'text', group: '鉴定信息', order: 32 },
|
||||||
|
{ name: 'certWork', label: '鉴定工种', type: 'text', group: '鉴定信息', order: 33 },
|
||||||
|
{ name: 'hasCert',
|
||||||
|
label: '是否参加鉴定',
|
||||||
|
type: 'radio',
|
||||||
|
options: [
|
||||||
|
{ label: '是', value: '是' },
|
||||||
|
{ label: '否', value: '否' },
|
||||||
|
],
|
||||||
|
group: '鉴定信息',
|
||||||
|
order: 34 },
|
||||||
|
|
||||||
|
// 工作信息组
|
||||||
|
{ name: 'equipment', label: '操作维护装备', type: 'textarea', group: '工作信息', order: 35 },
|
||||||
|
{ name: 'projects', label: '演训任务经历', type: 'textarea', group: '工作信息', order: 36 },
|
||||||
|
{ name: 'awards', label: '奖励信息', type: 'textarea', group: '工作信息', order: 37 },
|
||||||
|
{ name: 'punishments', label: '处分信息', type: 'textarea', group: '工作信息', order: 38 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FieldTypeOptions = [
|
||||||
|
{ label: '文本', value: 'text' },
|
||||||
|
{ label: '数字', value: 'number' },
|
||||||
|
{ label: '日期', value: 'date' },
|
||||||
|
{ label: '选择', value: 'select' },
|
||||||
|
{ label: '单选', value: 'radio' },
|
||||||
|
{ label: '多行文本', value: 'textarea' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const GroupOptions = [
|
||||||
|
{ label: '个人基本信息', value: '个人基本信息' },
|
||||||
|
{ label: '政治信息', value: '政治信息' },
|
||||||
|
{ label: '教育背景', value: '教育背景' },
|
||||||
|
{ label: '职务信息', value: '职务信息' },
|
||||||
|
{ label: '入职信息', value: '入职信息' },
|
||||||
|
{ label: '培训信息', value: '培训信息' },
|
||||||
|
{ label: '鉴定信息', value: '鉴定信息' },
|
||||||
|
{ label: '工作信息', value: '工作信息' },
|
||||||
|
{ label: '其他信息', value: '其他信息' },
|
||||||
|
];
|
|
@ -0,0 +1,77 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Button, Input, Space, Tag } from 'antd';
|
||||||
|
import { DeleteOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
type OptionCreatorProps = {
|
||||||
|
options: { [key: string]: string } | { label: string; value: string }[] | undefined;
|
||||||
|
onChange: (newOptions: { label: string; value: string }[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OptionCreator: React.FC<OptionCreatorProps> = ({ options, onChange }) => {
|
||||||
|
const [localOptions, setLocalOptions] = useState<{ label: string; value: string }[]>([]);
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
|
||||||
|
// 监听传入的 options 变化,更新本地状态
|
||||||
|
useEffect(() => {
|
||||||
|
let validOptions: { label: string; value: string }[] = [];
|
||||||
|
if (Array.isArray(options)) {
|
||||||
|
validOptions = options;
|
||||||
|
} else if (typeof options === 'object' && options !== null) {
|
||||||
|
validOptions = Object.entries(options).map(([value, label]) => ({ label, value }));
|
||||||
|
}
|
||||||
|
setLocalOptions(validOptions);
|
||||||
|
}, [options]);
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setInputValue(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddOption = () => {
|
||||||
|
if (inputValue.trim()) {
|
||||||
|
const newOption = { label: inputValue, value: inputValue };
|
||||||
|
// 检查新选项是否已存在,避免重复添加
|
||||||
|
const isDuplicate = localOptions.some(option => option.value === newOption.value);
|
||||||
|
if (!isDuplicate) {
|
||||||
|
const newOptions = [...localOptions, newOption];
|
||||||
|
setLocalOptions(newOptions);
|
||||||
|
// 即时调用 onChange 通知父组件更新表单值
|
||||||
|
onChange(newOptions);
|
||||||
|
}
|
||||||
|
setInputValue('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteOption = (value: string) => {
|
||||||
|
const newOptions = localOptions.filter(option => option.value !== value);
|
||||||
|
setLocalOptions(newOptions);
|
||||||
|
// 即时调用 onChange 通知父组件更新表单值
|
||||||
|
onChange(newOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Space>
|
||||||
|
<Input
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="输入新选项"
|
||||||
|
/>
|
||||||
|
<Button onClick={handleAddOption}>添加选项</Button>
|
||||||
|
</Space>
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
{localOptions.map(option => (
|
||||||
|
<Tag
|
||||||
|
key={option.value}
|
||||||
|
closable
|
||||||
|
onClose={() => handleDeleteOption(option.value)}
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OptionCreator;
|
|
@ -0,0 +1,162 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { Button, Space, message } from 'antd';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useStaff } from '@nice/client';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import DefaultFieldInitializer from './defaultFieldInitializer';
|
||||||
|
import StaffFieldTable from './staffFieldTable';
|
||||||
|
import StaffFieldModal from './staffFieldModal';
|
||||||
|
import { useWatch } from 'antd/es/form/Form';
|
||||||
|
import { Form } from 'antd';
|
||||||
|
const StaffFieldManage = () => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
|
const [editingField, setEditingField] = useState<any>(null);
|
||||||
|
const {
|
||||||
|
addCustomField,
|
||||||
|
updateCustomField,
|
||||||
|
deleteCustomField,
|
||||||
|
useCustomFields
|
||||||
|
} = useStaff();
|
||||||
|
const { data: fields, isLoading } = useCustomFields();
|
||||||
|
const optionFieldTypes = ['select', 'radio'];
|
||||||
|
const [showOptions, setShowOptions] = useState(false);
|
||||||
|
const typeValue = useWatch('type', form);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShowOptions(optionFieldTypes.includes(typeValue));
|
||||||
|
}, [typeValue, optionFieldTypes]);
|
||||||
|
|
||||||
|
const [isInitConfirmVisible, setIsInitConfirmVisible] = useState(false);
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const checkFieldExists = (name: string, excludeId?: string) => {
|
||||||
|
return fields?.some(field =>
|
||||||
|
field.name === name && field.id !== excludeId
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
setEditingField(null);
|
||||||
|
form.resetFields();
|
||||||
|
form.setFieldsValue({ options: [] });
|
||||||
|
setIsModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (record: any) => {
|
||||||
|
setEditingField(record);
|
||||||
|
let optionsToSet = record.options;
|
||||||
|
if (typeof record.options === 'object' && record.options !== null && !Array.isArray(record.options)) {
|
||||||
|
optionsToSet = Object.entries(record.options).map(([value, label]) => ({ label, value }));
|
||||||
|
}
|
||||||
|
form.setFieldsValue({ ...record, options: optionsToSet });
|
||||||
|
setIsModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await deleteCustomField.mutateAsync(id);
|
||||||
|
message.success('字段删除成功');
|
||||||
|
} catch (error) {
|
||||||
|
message.error('字段删除失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields();
|
||||||
|
|
||||||
|
if (checkFieldExists(values.name, editingField?.id)) {
|
||||||
|
message.error('字段标识已存在,请使用其他标识');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsProcessing(true);
|
||||||
|
|
||||||
|
if (editingField) {
|
||||||
|
await updateCustomField.mutateAsync({
|
||||||
|
id: editingField.id,
|
||||||
|
...values,
|
||||||
|
});
|
||||||
|
message.success('字段更新成功');
|
||||||
|
} else {
|
||||||
|
await addCustomField.mutateAsync(values);
|
||||||
|
message.success('字段添加成功');
|
||||||
|
}
|
||||||
|
setIsModalVisible(false);
|
||||||
|
} catch (error) {
|
||||||
|
message.error('操作失败:' + (error instanceof Error ? error.message : '未知错误'));
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBatchDelete = async () => {
|
||||||
|
if (selectedRowKeys.length === 0) {
|
||||||
|
message.warning('请选择要删除的字段');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
selectedRowKeys.map(async (id: string) => {
|
||||||
|
await deleteCustomField.mutateAsync(id);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
message.success('批量删除成功');
|
||||||
|
setSelectedRowKeys([]);
|
||||||
|
} catch (error) {
|
||||||
|
message.error('批量删除失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h2 className="text-lg font-semibold">字段管理</h2>
|
||||||
|
<Space>
|
||||||
|
<DefaultFieldInitializer
|
||||||
|
fields={fields}
|
||||||
|
isProcessing={isProcessing}
|
||||||
|
setIsProcessing={setIsProcessing}
|
||||||
|
isInitConfirmVisible={isInitConfirmVisible}
|
||||||
|
setIsInitConfirmVisible={setIsInitConfirmVisible}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={handleAdd}
|
||||||
|
loading={isProcessing}
|
||||||
|
>
|
||||||
|
添加字段
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StaffFieldTable
|
||||||
|
fields={fields}
|
||||||
|
isLoading={isLoading}
|
||||||
|
isProcessing={isProcessing}
|
||||||
|
selectedRowKeys={selectedRowKeys}
|
||||||
|
setSelectedRowKeys={setSelectedRowKeys}
|
||||||
|
handleEdit={handleEdit}
|
||||||
|
handleDelete={handleDelete}
|
||||||
|
handleBatchDelete={handleBatchDelete}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StaffFieldModal
|
||||||
|
form={form}
|
||||||
|
isModalVisible={isModalVisible}
|
||||||
|
setIsModalVisible={setIsModalVisible}
|
||||||
|
editingField={editingField}
|
||||||
|
isProcessing={isProcessing}
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
optionFieldTypes={optionFieldTypes}
|
||||||
|
showOptions={showOptions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StaffFieldManage;
|
|
@ -0,0 +1,107 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Form, Input, Select, InputNumber, Modal, message, FormInstance } from 'antd';
|
||||||
|
import OptionCreator from './optionCreator';
|
||||||
|
import { FieldTypeOptions, GroupOptions } from './defaultFields';
|
||||||
|
|
||||||
|
type StaffFieldModalProps = {
|
||||||
|
form: FormInstance;
|
||||||
|
isModalVisible: boolean;
|
||||||
|
setIsModalVisible: (visible: boolean) => void;
|
||||||
|
editingField: any;
|
||||||
|
isProcessing: boolean;
|
||||||
|
handleSubmit: () => Promise<void>;
|
||||||
|
optionFieldTypes: string[];
|
||||||
|
showOptions: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StaffFieldModal: React.FC<StaffFieldModalProps> = ({
|
||||||
|
form,
|
||||||
|
isModalVisible,
|
||||||
|
setIsModalVisible,
|
||||||
|
editingField,
|
||||||
|
isProcessing,
|
||||||
|
handleSubmit,
|
||||||
|
optionFieldTypes,
|
||||||
|
showOptions
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={editingField ? '编辑字段' : '添加字段'}
|
||||||
|
open={isModalVisible}
|
||||||
|
onOk={handleSubmit}
|
||||||
|
onCancel={() => setIsModalVisible(false)}
|
||||||
|
confirmLoading={isProcessing}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="name"
|
||||||
|
label="字段标识"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入字段标识' },
|
||||||
|
{ pattern: /^[a-zA-Z][a-zA-Z0-9]*$/, message: '字段标识必须以字母开头,只能包含字母和数字' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入字段标识,如:phoneNumber" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="label"
|
||||||
|
label="字段名称"
|
||||||
|
rules={[{ required: true, message: '请输入字段名称' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入字段名称,如:手机号码" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="type"
|
||||||
|
label="字段类型"
|
||||||
|
rules={[{ required: true, message: '请选择字段类型' }]}
|
||||||
|
>
|
||||||
|
<Select options={FieldTypeOptions} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="options"
|
||||||
|
label="字段选项"
|
||||||
|
hidden={!showOptions}
|
||||||
|
>
|
||||||
|
<OptionCreator
|
||||||
|
options={form.getFieldValue('options') || []}
|
||||||
|
onChange={(newOptions) => form.setFieldsValue({ options: newOptions })}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="required"
|
||||||
|
label="是否必填"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ label: '是', value: true },
|
||||||
|
{ label: '否', value: false },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="group"
|
||||||
|
label="字段分组"
|
||||||
|
>
|
||||||
|
<Select options={GroupOptions} allowClear />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="order"
|
||||||
|
label="显示顺序"
|
||||||
|
>
|
||||||
|
<InputNumber min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StaffFieldModal;
|
|
@ -0,0 +1,123 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Table, Space, Button, Popconfirm, message } from 'antd';
|
||||||
|
import { EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
|
import { FieldTypeOptions } from './defaultFields';
|
||||||
|
|
||||||
|
type StaffFieldTableProps = {
|
||||||
|
fields: any[] | undefined;
|
||||||
|
isLoading: boolean;
|
||||||
|
isProcessing: boolean;
|
||||||
|
selectedRowKeys: string[];
|
||||||
|
setSelectedRowKeys: (keys: string[]) => void;
|
||||||
|
handleEdit: (record: any) => void;
|
||||||
|
handleDelete: (id: string) => Promise<void>;
|
||||||
|
handleBatchDelete: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkFieldExists = (name: string, excludeId?: string, fields?: any[]) => {
|
||||||
|
return fields?.some(field =>
|
||||||
|
field.name === name && field.id !== excludeId
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const StaffFieldTable: React.FC<StaffFieldTableProps> = ({
|
||||||
|
fields,
|
||||||
|
isLoading,
|
||||||
|
isProcessing,
|
||||||
|
selectedRowKeys,
|
||||||
|
setSelectedRowKeys,
|
||||||
|
handleEdit,
|
||||||
|
handleDelete,
|
||||||
|
handleBatchDelete
|
||||||
|
}) => {
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '字段名称',
|
||||||
|
dataIndex: 'label',
|
||||||
|
key: 'label',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '字段标识',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '字段类型',
|
||||||
|
dataIndex: 'type',
|
||||||
|
key: 'type',
|
||||||
|
render: (type: string) => FieldTypeOptions.find(t => t.value === type)?.label,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '字段选项',
|
||||||
|
dataIndex: 'options',
|
||||||
|
key: 'options',
|
||||||
|
render: (options) => {
|
||||||
|
const validOptions = Array.isArray(options) ? options : [];
|
||||||
|
return validOptions.map(option => option.label).join(', ');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '必填',
|
||||||
|
dataIndex: 'required',
|
||||||
|
key: 'required',
|
||||||
|
render: (required: boolean) => required ? '是' : '否',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '分组',
|
||||||
|
dataIndex: 'group',
|
||||||
|
key: 'group',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
render: (_: any, record: any) => (
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => handleEdit(record)}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Popconfirm
|
||||||
|
title="确定要删除这个字段吗?"
|
||||||
|
onConfirm={() => handleDelete(record.id)}
|
||||||
|
>
|
||||||
|
<Button type="link" danger icon={<DeleteOutlined />}>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const rowSelection = {
|
||||||
|
selectedRowKeys,
|
||||||
|
onChange: (keys: string[]) => {
|
||||||
|
setSelectedRowKeys(keys);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={fields}
|
||||||
|
loading={isLoading || isProcessing}
|
||||||
|
rowKey="id"
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={handleBatchDelete}
|
||||||
|
loading={isProcessing}
|
||||||
|
>
|
||||||
|
批量删除
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StaffFieldTable;
|
|
@ -58,6 +58,7 @@ const items = [
|
||||||
[
|
[
|
||||||
getItem("部门设置", "/admin/department", null, null, null),
|
getItem("部门设置", "/admin/department", null, null, null),
|
||||||
getItem("角色设置", "/admin/role", null, null, null),
|
getItem("角色设置", "/admin/role", null, null, null),
|
||||||
|
getItem("人员信息管理", "/admin/staffinfo", null, null, null),
|
||||||
],
|
],
|
||||||
null,
|
null,
|
||||||
),
|
),
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { useCallback, useEffect} from "react";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useMainContext } from "../layout/MainProvider";
|
import { useMainContext } from "../layout/MainProvider";
|
||||||
import StaffTable from "./stafftable/page";
|
import StaffTable from "./stafftable/page";
|
||||||
import StaffModal from "./staffmodal/page";
|
|
||||||
|
|
||||||
export default function StaffMessage() {
|
export default function StaffMessage() {
|
||||||
const { form, formValue, setFormValue, setVisible, setSearchValue, editingRecord } = useMainContext();
|
const { form, formValue, setFormValue, setVisible, setSearchValue, editingRecord } = useMainContext();
|
||||||
|
@ -34,7 +33,6 @@ export default function StaffMessage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StaffTable />
|
<StaffTable />
|
||||||
<StaffModal />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -5,7 +5,7 @@ import { ColDef, ColGroupDef } from '@ag-grid-community/core';
|
||||||
import { SetFilterModule } from '@ag-grid-enterprise/set-filter';
|
import { SetFilterModule } from '@ag-grid-enterprise/set-filter';
|
||||||
import 'ag-grid-community/styles/ag-grid.css';
|
import 'ag-grid-community/styles/ag-grid.css';
|
||||||
import 'ag-grid-community/styles/ag-theme-alpine.css';
|
import 'ag-grid-community/styles/ag-theme-alpine.css';
|
||||||
import { areaOptions } from '@web/src/app/main/staffinformation/area-options';
|
import { areaOptions } from '@web/src/app/main/staffinfo_write/area-options';
|
||||||
import type { CascaderProps } from 'antd/es/cascader';
|
import type { CascaderProps } from 'antd/es/cascader';
|
||||||
import { utils, writeFile, read } from 'xlsx';
|
import { utils, writeFile, read } from 'xlsx';
|
||||||
import { Modal, Input, Button, Switch, Upload, message } from 'antd';
|
import { Modal, Input, Button, Switch, Upload, message } from 'antd';
|
||||||
|
@ -345,275 +345,7 @@ export default function StaffTable() {
|
||||||
wrapText: true,
|
wrapText: true,
|
||||||
autoHeight: true
|
autoHeight: true
|
||||||
};
|
};
|
||||||
// 修改导出模板处理函数
|
|
||||||
const handleExportTemplate = () => {
|
|
||||||
const headerNames = extractHeaders(columnDefs);
|
|
||||||
|
|
||||||
// 创建一个空白行对象,键为列名,值为空字符串
|
|
||||||
const emptyRow = headerNames.reduce((obj, header) => {
|
|
||||||
obj[header] = '';
|
|
||||||
return obj;
|
|
||||||
}, {} as Record<string, string>);
|
|
||||||
|
|
||||||
// 创建示例数据行
|
|
||||||
const exampleRow = headerNames.reduce((obj, header) => {
|
|
||||||
// 根据不同的表头设置不同的示例值
|
|
||||||
switch(header) {
|
|
||||||
// 基本信息示例
|
|
||||||
case '姓名': obj[header] = '张三'; break;
|
|
||||||
case '身份证号': obj[header] = '110101199001011234'; break;
|
|
||||||
case '人员类型': obj[header] = '在职'; break;
|
|
||||||
case '警号': obj[header] = '012345'; break;
|
|
||||||
case '手机号': obj[header] = '13800138000'; break;
|
|
||||||
case '年龄': obj[header] = '30'; break;
|
|
||||||
case '性别': obj[header] = '男'; break;
|
|
||||||
case '血型': obj[header] = 'A型'; break;
|
|
||||||
case '籍贯': obj[header] = '北京市海淀区'; break;
|
|
||||||
case '来源': obj[header] = '社会招聘'; break;
|
|
||||||
|
|
||||||
// 政治信息示例
|
|
||||||
case '政治面貌': obj[header] = '党员'; break;
|
|
||||||
case '党内职务': obj[header] = '支部书记'; break;
|
|
||||||
|
|
||||||
// 职务信息示例
|
|
||||||
case '所属部门': obj[header] = '技术部'; break;
|
|
||||||
case '衔职级别': obj[header] = '三级警司'; break;
|
|
||||||
case '衔职时间': obj[header] = '2020-01-01'; break;
|
|
||||||
case '代理职务': obj[header] = '技术组长'; break;
|
|
||||||
case '岗位': obj[header] = '技术开发'; break;
|
|
||||||
|
|
||||||
// 入职信息示例
|
|
||||||
case '入职时间': obj[header] = '2015-07-01'; break;
|
|
||||||
case '工龄认定时间': obj[header] = '2015-07-01'; break;
|
|
||||||
case '来源类型': obj[header] = '招聘'; break;
|
|
||||||
case '是否二次入职': obj[header] = '否'; break;
|
|
||||||
case '是否延期服役': obj[header] = '否'; break;
|
|
||||||
case '现岗位开始时间': obj[header] = '2018-05-01'; break;
|
|
||||||
|
|
||||||
// 教育背景示例
|
|
||||||
case '学历': obj[header] = '本科'; break;
|
|
||||||
case '学历形式': obj[header] = '全日制'; break;
|
|
||||||
case '是否毕业': obj[header] = '是'; break;
|
|
||||||
case '专业': obj[header] = '计算机科学与技术'; break;
|
|
||||||
case '外语能力': obj[header] = '英语四级'; break;
|
|
||||||
|
|
||||||
// 培训信息示例
|
|
||||||
case '培训类型': obj[header] = '专业技能'; break;
|
|
||||||
case '培训机构': obj[header] = '公安大学'; break;
|
|
||||||
case '培训专业': obj[header] = '网络安全'; break;
|
|
||||||
case '是否参加培训': obj[header] = '是'; break;
|
|
||||||
|
|
||||||
// 鉴定信息示例
|
|
||||||
case '鉴定等级': obj[header] = '高级'; break;
|
|
||||||
case '鉴定工种': obj[header] = '信息安全'; break;
|
|
||||||
case '是否参加鉴定': obj[header] = '是'; break;
|
|
||||||
|
|
||||||
// 工作信息示例
|
|
||||||
case '操作维护装备': obj[header] = '服务器,网络设备,安全设备'; break;
|
|
||||||
case '演训任务经历': obj[header] = '2019年网络安全演习,2020年数据恢复演练'; break;
|
|
||||||
case '奖励信息': obj[header] = '2018年度优秀员工,2020年技术创新奖'; break;
|
|
||||||
case '处分信息': obj[header] = ''; break;
|
|
||||||
|
|
||||||
default: obj[header] = `示例${header}`; break;
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}, {} as Record<string, string>);
|
|
||||||
|
|
||||||
// 创建工作簿和工作表,包含示例行和空白行
|
|
||||||
const wb = utils.book_new();
|
|
||||||
const ws = utils.json_to_sheet([exampleRow, emptyRow], { header: headerNames });
|
|
||||||
|
|
||||||
// 设置列宽
|
|
||||||
const colWidth = headerNames.map(() => ({ wch: 20 }));
|
|
||||||
ws['!cols'] = colWidth;
|
|
||||||
|
|
||||||
// 添加一些样式表示示例数据行
|
|
||||||
// XLSX.js 不直接支持样式,但我们可以添加注释
|
|
||||||
const note = { t: 's', v: '以上为示例数据,请在下方行填写实际数据' };
|
|
||||||
ws['A3'] = note;
|
|
||||||
|
|
||||||
utils.book_append_sheet(wb, ws, "员工模板");
|
|
||||||
writeFile(wb, `员工数据模板_${new Date().toISOString().slice(0, 10)}.xlsx`);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加导入API钩子
|
|
||||||
const createManyMutation = api.staff.create.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
message.success('员工数据导入成功');
|
|
||||||
refetch(); // 刷新表格数据
|
|
||||||
setImportVisible(false);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
message.error(`导入失败: ${error.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 处理Excel导入数据
|
|
||||||
const handleImportData = (excelData: any[]) => {
|
|
||||||
// 转换Excel数据为后端接受的格式
|
|
||||||
const staffData = excelData.map(row => {
|
|
||||||
// 创建一个标准的员工对象
|
|
||||||
const staff: any = {};
|
|
||||||
|
|
||||||
// 遍历列定义,匹配Excel中的数据
|
|
||||||
columnDefs.forEach(colDef => {
|
|
||||||
if ('children' in colDef && colDef.children) {
|
|
||||||
colDef.children.forEach(childCol => {
|
|
||||||
if ('field' in childCol && childCol.headerName) {
|
|
||||||
// 使用表头名称查找Excel数据
|
|
||||||
const value = row[childCol.headerName];
|
|
||||||
if (value !== undefined) {
|
|
||||||
// 处理嵌套属性 (如 department.name)
|
|
||||||
if (childCol.field.includes('.')) {
|
|
||||||
const [parent, child] = childCol.field.split('.');
|
|
||||||
// 对于department.name特殊处理
|
|
||||||
if (parent === 'department' && child === 'name') {
|
|
||||||
// 仅存储部门名称,后续可处理
|
|
||||||
staff.departmentName = value;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 根据字段类型进行处理
|
|
||||||
processFieldValue(staff, childCol.field, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if ('field' in colDef && colDef.headerName) {
|
|
||||||
const value = row[colDef.headerName];
|
|
||||||
if (value !== undefined) {
|
|
||||||
processFieldValue(staff, colDef.field, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return staff;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 调用后端API保存数据
|
|
||||||
if (staffData.length > 0) {
|
|
||||||
// 逐个创建员工记录
|
|
||||||
staffData.forEach(staff => {
|
|
||||||
createManyMutation.mutate({
|
|
||||||
data: staff
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
message.success(`已提交${staffData.length}条员工数据导入请求`);
|
|
||||||
} else {
|
|
||||||
message.warning('没有可导入的有效数据');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 字段值处理函数
|
|
||||||
const processFieldValue = (staff: any, field: string, value: any) => {
|
|
||||||
// 跳过空值
|
|
||||||
if (value === null || value === undefined || value === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据字段类型分别处理
|
|
||||||
switch (field) {
|
|
||||||
// 字符串字段 - 确保转为字符串
|
|
||||||
case 'idNumber':
|
|
||||||
case 'officerId':
|
|
||||||
case 'phoneNumber':
|
|
||||||
case 'username':
|
|
||||||
case 'password':
|
|
||||||
case 'showname':
|
|
||||||
case 'avatar':
|
|
||||||
case 'type':
|
|
||||||
case 'bloodType':
|
|
||||||
case 'birthplace':
|
|
||||||
case 'source':
|
|
||||||
case 'politicalStatus':
|
|
||||||
case 'partyPosition':
|
|
||||||
case 'rank':
|
|
||||||
case 'proxyPosition':
|
|
||||||
case 'post':
|
|
||||||
case 'sourceType':
|
|
||||||
case 'education':
|
|
||||||
case 'educationType':
|
|
||||||
case 'major':
|
|
||||||
case 'foreignLang':
|
|
||||||
case 'trainType':
|
|
||||||
case 'trainInstitute':
|
|
||||||
case 'trainMajor':
|
|
||||||
case 'certRank':
|
|
||||||
case 'certWork':
|
|
||||||
case 'equipment':
|
|
||||||
case 'projects':
|
|
||||||
case 'awards':
|
|
||||||
case 'punishments':
|
|
||||||
case 'domainId':
|
|
||||||
case 'deptId':
|
|
||||||
staff[field] = String(value);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// 布尔字段 - 转为布尔值
|
|
||||||
case 'sex':
|
|
||||||
staff[field] = value === '男' ? true : value === '女' ? false : null;
|
|
||||||
break;
|
|
||||||
case 'enabled':
|
|
||||||
case 'isReentry':
|
|
||||||
case 'isExtended':
|
|
||||||
case 'isGraduated':
|
|
||||||
case 'hasTrain':
|
|
||||||
case 'hasCert':
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
staff[field] = value === '是' || value === '√' || value === 'true' || value === '1';
|
|
||||||
} else {
|
|
||||||
staff[field] = Boolean(value);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// 数值字段 - 转为数字
|
|
||||||
case 'age':
|
|
||||||
staff[field] = parseInt(value, 10);
|
|
||||||
break;
|
|
||||||
case 'order':
|
|
||||||
staff[field] = parseFloat(value);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// 日期字段 - 转为日期格式
|
|
||||||
case 'rankDate':
|
|
||||||
case 'hireDate':
|
|
||||||
case 'seniority':
|
|
||||||
case 'currentPositionDate':
|
|
||||||
// 尝试将日期字符串转换为日期对象
|
|
||||||
try {
|
|
||||||
// Excel日期可能以不同格式导出
|
|
||||||
let dateValue = value;
|
|
||||||
|
|
||||||
// 如果是Excel序列号格式的日期
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
// Excel日期是从1900年1月1日开始的天数
|
|
||||||
// 需要转换为JavaScript日期
|
|
||||||
const excelEpoch = new Date(1899, 11, 30);
|
|
||||||
dateValue = new Date(excelEpoch.getTime() + value * 86400000);
|
|
||||||
}
|
|
||||||
// 如果是字符串格式的日期
|
|
||||||
else if (typeof value === 'string') {
|
|
||||||
// 尝试解析常见日期格式
|
|
||||||
dateValue = new Date(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查日期是否有效
|
|
||||||
if (dateValue instanceof Date && !isNaN(dateValue.getTime())) {
|
|
||||||
staff[field] = dateValue.toISOString();
|
|
||||||
} else {
|
|
||||||
console.warn(`无效的日期格式: ${field} = ${value}`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`日期转换错误 (${field}): ${e}`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// 默认情况下保持原值
|
|
||||||
default:
|
|
||||||
staff[field] = value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ag-theme-alpine w-full h-[calc(100vh-100px)]"
|
<div className="ag-theme-alpine w-full h-[calc(100vh-100px)]"
|
||||||
style={{
|
style={{
|
||||||
|
@ -624,18 +356,10 @@ export default function StaffTable() {
|
||||||
>
|
>
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<div className="flex items-center gap-4 mb-2">
|
<div className="flex items-center gap-4 mb-2">
|
||||||
<Button
|
|
||||||
onClick={handleConfirm}
|
|
||||||
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
导出Excel
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => setImportVisible(true)} className="bg-orange-500 text-white px-4 py-2 rounded hover:bg-orange-600">
|
<Button onClick={() => setImportVisible(true)} className="bg-orange-500 text-white px-4 py-2 rounded hover:bg-orange-600">
|
||||||
导入Excel
|
导入Excel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleExportTemplate} className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
|
|
||||||
导出模板
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleResetFilters}
|
onClick={handleResetFilters}
|
||||||
className="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600"
|
className="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600"
|
||||||
|
@ -668,43 +392,7 @@ export default function StaffTable() {
|
||||||
onChange={(e) => setFileName(e.target.value)}
|
onChange={(e) => setFileName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal
|
|
||||||
title="导入员工数据"
|
|
||||||
open={importVisible}
|
|
||||||
onCancel={() => setImportVisible(false)}
|
|
||||||
footer={null}
|
|
||||||
>
|
|
||||||
<Upload
|
|
||||||
accept=".xlsx,.xls"
|
|
||||||
beforeUpload={file => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
try {
|
|
||||||
const wb = read(e.target?.result);
|
|
||||||
const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
|
|
||||||
|
|
||||||
if (data.length === 0) {
|
|
||||||
message.warning('Excel文件中没有数据');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.info(`读取到${data.length}条数据,正在处理...`);
|
|
||||||
handleImportData(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('解析Excel文件失败:', error);
|
|
||||||
message.error('Excel文件格式错误,请确保使用正确的模板');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsArrayBuffer(file);
|
|
||||||
return false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button icon={<UploadOutlined />}>选择Excel文件</Button>
|
|
||||||
</Upload>
|
|
||||||
<div className="mt-4 text-gray-500 text-sm">
|
|
||||||
提示:请使用导出模板功能获取标准模板,按格式填写数据后导入
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="h-full flex items-center justify-center">
|
<div className="h-full flex items-center justify-center">
|
||||||
<div className="text-gray-600 text-xl">加载中...</div>
|
<div className="text-gray-600 text-xl">加载中...</div>
|
|
@ -0,0 +1,129 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button, Form, Input, Select, DatePicker, Radio, message, Modal, Cascader, InputNumber } from "antd";
|
||||||
|
import { useState, useMemo } from "react";
|
||||||
|
import { useStaff } from "@nice/client";
|
||||||
|
import { areaOptions } from './area-options';
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
const StaffInfoWrite = () => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { create, useCustomFields } = useStaff();
|
||||||
|
const { data: fields, isLoading: fieldsLoading } = useCustomFields();
|
||||||
|
|
||||||
|
// 按分组组织字段
|
||||||
|
const fieldGroups = useMemo(() => {
|
||||||
|
if (!fields) return {};
|
||||||
|
return fields.reduce((groups: any, field: any) => {
|
||||||
|
const group = field.group || '其他信息';
|
||||||
|
if (!groups[group]) {
|
||||||
|
groups[group] = [];
|
||||||
|
}
|
||||||
|
groups[group].push(field);
|
||||||
|
return groups;
|
||||||
|
}, {});
|
||||||
|
}, [fields]);
|
||||||
|
|
||||||
|
const renderField = (field: any) => {
|
||||||
|
switch (field.type) {
|
||||||
|
case 'text':
|
||||||
|
return <Input />;
|
||||||
|
case 'number':
|
||||||
|
return <InputNumber />;
|
||||||
|
case 'date':
|
||||||
|
return <DatePicker />;
|
||||||
|
case 'select':
|
||||||
|
// 检查 field.options 是否存在且有数据
|
||||||
|
if (field.options && field.options.length > 0) {
|
||||||
|
return <Select options={field.options} />;
|
||||||
|
}
|
||||||
|
return <Input placeholder="选项数据缺失" />;
|
||||||
|
case 'radio':
|
||||||
|
// 检查 field.options 是否存在且有数据
|
||||||
|
if (field.options && field.options.length > 0) {
|
||||||
|
return <Radio.Group options={field.options} />;
|
||||||
|
}
|
||||||
|
return <Input placeholder="选项数据缺失" />;
|
||||||
|
case 'textarea':
|
||||||
|
return <Input.TextArea />;
|
||||||
|
default:
|
||||||
|
return <Input />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFinish = async (values: any) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const formattedValues = {
|
||||||
|
...values,
|
||||||
|
birthplace: values.birthplace?.join('/'),
|
||||||
|
};
|
||||||
|
|
||||||
|
await create.mutateAsync({
|
||||||
|
data: formattedValues
|
||||||
|
});
|
||||||
|
|
||||||
|
message.success("信息提交成功");
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交出错:', error);
|
||||||
|
message.error("提交失败,请重试");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fieldsLoading) {
|
||||||
|
return <div>加载中...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto p-6">
|
||||||
|
<h1 className="text-2xl font-bold mb-6">人员信息管理</h1>
|
||||||
|
|
||||||
|
{/* 信息填报表单 */}
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={onFinish}
|
||||||
|
className="space-y-6 mt-6"
|
||||||
|
>
|
||||||
|
{Object.entries(fieldGroups).map(([groupName, groupFields]) => (
|
||||||
|
<div key={groupName} className="bg-white p-6 rounded-lg shadow">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">{groupName}</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{groupFields.map((field: any) => (
|
||||||
|
<Form.Item
|
||||||
|
key={field.id}
|
||||||
|
label={field.label}
|
||||||
|
name={field.name}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: field.required,
|
||||||
|
message: `请输入${field.label}`,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{renderField(field)}
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-4">
|
||||||
|
<Button onClick={() => form.resetFields()}>重置</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
提交
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StaffInfoWrite;
|
|
@ -1,519 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { Button, Form, Input, Select, DatePicker, Radio, message, Modal, Cascader, InputNumber } from "antd";
|
|
||||||
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 DepartmentSelect from "@web/src/components/models/department/department-select";
|
|
||||||
const { TextArea } = Input;
|
|
||||||
|
|
||||||
const StaffInformation = () => {
|
|
||||||
const [modalForm] = Form.useForm();
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
|
||||||
const [modalType, setModalType] = useState<'awards' | 'punishments' | 'equipment' | 'projects'>('awards');
|
|
||||||
const [rewardsList, setRewardsList] = useState<string[]>([]);
|
|
||||||
const [punishmentsList, setPunishmentsList] = useState<string[]>([]);
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleModalOk = async () => {
|
|
||||||
try {
|
|
||||||
const values = await modalForm.validateFields();
|
|
||||||
const value = values[modalType];
|
|
||||||
|
|
||||||
if (value) {
|
|
||||||
switch(modalType) {
|
|
||||||
case 'awards':
|
|
||||||
setRewardsList([...rewardsList, value]);
|
|
||||||
break;
|
|
||||||
case 'punishments':
|
|
||||||
setPunishmentsList([...punishmentsList, value]);
|
|
||||||
break;
|
|
||||||
case 'equipment':
|
|
||||||
setEquipmentList([...equipmentList, value]);
|
|
||||||
break;
|
|
||||||
case 'projects':
|
|
||||||
setProjectsList([...projectsList, value]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
modalForm.resetFields();
|
|
||||||
setIsModalVisible(false);
|
|
||||||
message.success(
|
|
||||||
modalType === 'awards' ? '奖励信息添加成功' :
|
|
||||||
modalType === 'punishments' ? '处分信息添加成功' :
|
|
||||||
modalType === 'equipment' ? '装备信息添加成功' : '任务信息添加成功'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
message.warning('请输入内容');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFinish = async (values: any) => {
|
|
||||||
console.log('开始提交表单');
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const formattedValues = {
|
|
||||||
...values,
|
|
||||||
birthplace: values.birthplace?.join('/'), // 将数组转换为以'/'分隔的字符串
|
|
||||||
awards: rewardsList.join(','),
|
|
||||||
punishments: punishmentsList.join(','),
|
|
||||||
equipment: equipmentList.join(','),
|
|
||||||
projects: projectsList.join(','),
|
|
||||||
hireDate: values.hireDate?.toISOString(),
|
|
||||||
seniority: values.seniority?.toISOString(),
|
|
||||||
currentPositionDate: values.currentPositionDate?.toISOString(),
|
|
||||||
rankDate: values.rankDate?.toISOString(), // 修改这里
|
|
||||||
};
|
|
||||||
|
|
||||||
await create.mutateAsync(
|
|
||||||
{
|
|
||||||
data: formattedValues
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('提交的表单数据:', formattedValues);
|
|
||||||
console.log('奖励列表:', rewardsList);
|
|
||||||
console.log('处分列表:', punishmentsList);
|
|
||||||
|
|
||||||
message.success("信息提交成功");
|
|
||||||
} catch (error) {
|
|
||||||
console.error('提交出错:', error);
|
|
||||||
message.error("提交失败,请重试");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.MouseEvent) => {
|
|
||||||
e.preventDefault(); // 阻止默认行为
|
|
||||||
console.log('提交按钮被点击');
|
|
||||||
form.submit();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="max-w-4xl mx-auto p-6">
|
|
||||||
<h1 className="text-2xl font-bold mb-6">人员信息填报</h1>
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
layout="vertical"
|
|
||||||
onFinish={onFinish}
|
|
||||||
onFinishFailed={(errorInfo) => {
|
|
||||||
console.log('表单验证失败:', errorInfo);
|
|
||||||
}}
|
|
||||||
className="space-y-6"
|
|
||||||
>
|
|
||||||
{/* 个人基本信息 */}
|
|
||||||
<div className="bg-white p-6 rounded-lg shadow">
|
|
||||||
<h2 className="text-lg font-semibold mb-4">个人基本信息</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<Form.Item label="姓名" name="username" rules={[{ required: true }]}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="身份证号" name="idNumber" rules={[{required: true}]}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="警号" name="officerId">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="手机号" name="phoneNumber">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="人员类型" name="type">
|
|
||||||
<Select>
|
|
||||||
<Select.Option value="警官">警官</Select.Option>
|
|
||||||
<Select.Option value="文职">文职</Select.Option>
|
|
||||||
<Select.Option value="警士">警士</Select.Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="性别" name="sex">
|
|
||||||
<Radio.Group>
|
|
||||||
<Radio value={true}>男</Radio>
|
|
||||||
<Radio value={false}>女</Radio>
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label='年龄' name='age'>
|
|
||||||
<InputNumber />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="血型" name="bloodType">
|
|
||||||
<Select>
|
|
||||||
<Select.Option value="A">A型</Select.Option>
|
|
||||||
<Select.Option value="B">B型</Select.Option>
|
|
||||||
<Select.Option value="O">O型</Select.Option>
|
|
||||||
<Select.Option value="AB">AB型</Select.Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label="籍贯"
|
|
||||||
name="birthplace"
|
|
||||||
rules={[{ required: true, message: '请选择籍贯' }]}
|
|
||||||
>
|
|
||||||
<Cascader
|
|
||||||
options={areaOptions}
|
|
||||||
placeholder="请选择省/市/区"
|
|
||||||
className="w-full"
|
|
||||||
expandTrigger="hover"
|
|
||||||
showSearch={{
|
|
||||||
filter: (inputValue, path) => {
|
|
||||||
return path.some(option =>
|
|
||||||
typeof option.label === 'string' &&
|
|
||||||
option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
changeOnSelect
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 政治信息 */}
|
|
||||||
<div className="bg-white p-6 rounded-lg shadow">
|
|
||||||
<h2 className="text-lg font-semibold mb-4">政治信息</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<Form.Item label="政治面貌" name="politicalStatus">
|
|
||||||
<Select>
|
|
||||||
<Select.Option value="中共党员">中共党员</Select.Option>
|
|
||||||
<Select.Option value="中共预备党员">中共预备党员</Select.Option>
|
|
||||||
<Select.Option value="共青团员">共青团员</Select.Option>
|
|
||||||
<Select.Option value="群众">群众</Select.Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="党内职务" name="partyPosition">
|
|
||||||
<Select>
|
|
||||||
<Select.Option value="党委书记">党委书记</Select.Option>
|
|
||||||
<Select.Option value="委员">委员</Select.Option>
|
|
||||||
<Select.Option value="党支部书记">党支部书记</Select.Option>
|
|
||||||
<Select.Option value="党支部委员">党支部委员</Select.Option>
|
|
||||||
<Select.Option value="无">无</Select.Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 职务信息 */}
|
|
||||||
<div className="bg-white p-6 rounded-lg shadow">
|
|
||||||
<h2 className="text-lg font-semibold mb-4">职务信息</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<Form.Item label = "部门" name="deptId" >
|
|
||||||
<DepartmentSelect/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="衔职级别" name="rank">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="衔职时间" name="rankDate">
|
|
||||||
<DatePicker className="w-full" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="代理职务" name="proxyPosition">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="岗位" name="post">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 入职信息 */}
|
|
||||||
<div className="bg-white p-6 rounded-lg shadow">
|
|
||||||
<h2 className="text-lg font-semibold mb-4">入职信息</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<Form.Item label="入职时间" name="hireDate">
|
|
||||||
<DatePicker className="w-full" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="工龄认定时间" name="seniority">
|
|
||||||
<DatePicker className="w-full" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="是否二次入职" name="isReentry" initialValue={false}>
|
|
||||||
<Radio.Group>
|
|
||||||
<Radio value={true}>是</Radio>
|
|
||||||
<Radio value={false}>否</Radio>
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="来源类别" name="source">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="现岗位开始时间" name="currentPositionDate">
|
|
||||||
<DatePicker className="w-full" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="是否延期服役" name="isExtended" initialValue={false}>
|
|
||||||
<Radio.Group>
|
|
||||||
<Radio value={true}>是</Radio>
|
|
||||||
<Radio value={false}>否</Radio>
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 教育背景 */}
|
|
||||||
<div className="bg-white p-6 rounded-lg shadow">
|
|
||||||
<h2 className="text-lg font-semibold mb-4">教育背景</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<Form.Item label="学历" name="education">
|
|
||||||
<Select>
|
|
||||||
<Select.Option value="高中">高中</Select.Option>
|
|
||||||
<Select.Option value="专科">专科</Select.Option>
|
|
||||||
<Select.Option value="本科">本科</Select.Option>
|
|
||||||
<Select.Option value="硕士">硕士</Select.Option>
|
|
||||||
<Select.Option value="博士">博士</Select.Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="学历形式" name="educationType">
|
|
||||||
<Select>
|
|
||||||
<Select.Option value="全日制">全日制</Select.Option>
|
|
||||||
<Select.Option value="非全日制">非全日制</Select.Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="是否毕业" name="isGraduated">
|
|
||||||
<Radio.Group>
|
|
||||||
<Radio value={true}>是</Radio>
|
|
||||||
<Radio value={false}>否</Radio>
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="专业" name="major">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="外语能力" name="foreignLang">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 培训信息 */}
|
|
||||||
<div className="bg-white p-6 rounded-lg shadow">
|
|
||||||
<h2 className="text-lg font-semibold mb-4">培训信息</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<Form.Item label="是否参加培训" name="hasTrain" initialValue={false}>
|
|
||||||
<Radio.Group>
|
|
||||||
<Radio value={true}>是</Radio>
|
|
||||||
<Radio value={false}>否</Radio>
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
noStyle
|
|
||||||
shouldUpdate={(prevValues, currentValues) => prevValues.hasTrain !== currentValues.hasTrain}
|
|
||||||
>
|
|
||||||
{({ getFieldValue }) => (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-1 gap-4">
|
|
||||||
<Form.Item label="培训类型" name="trainType">
|
|
||||||
<Input disabled={!getFieldValue('hasTrain')} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="培训机构" name="trainInstitute">
|
|
||||||
<Input disabled={!getFieldValue('hasTrain')} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="培训专业" name="trainMajor">
|
|
||||||
<Input disabled={!getFieldValue('hasTrain')} />
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 鉴定信息 */}
|
|
||||||
<div className="bg-white p-6 rounded-lg shadow">
|
|
||||||
<h2 className="text-lg font-semibold mb-4">鉴定信息</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<Form.Item label="是否参加鉴定" name="hasCert" initialValue={false}>
|
|
||||||
<Radio.Group>
|
|
||||||
<Radio value={true}>是</Radio>
|
|
||||||
<Radio value={false}>否</Radio>
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
noStyle
|
|
||||||
shouldUpdate={(prevValues, currentValues) => prevValues.hasCert !== currentValues.hasCert}
|
|
||||||
>
|
|
||||||
{({ getFieldValue }) => (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-1 gap-4">
|
|
||||||
<Form.Item label="鉴定等级" name="certRank">
|
|
||||||
<Input disabled={!getFieldValue('hasCert')} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="鉴定工种" name="certWork">
|
|
||||||
<Input disabled={!getFieldValue('hasCert')} />
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 工作信息 */}
|
|
||||||
<div className="bg-white p-6 rounded-lg shadow">
|
|
||||||
<h2 className="text-lg font-semibold mb-4">工作信息</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<label>操作维护装备</label>
|
|
||||||
<Button type="primary" onClick={() => showModal('equipment')}>
|
|
||||||
添加装备
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="border p-2 min-h-[100px] rounded">
|
|
||||||
{equipmentList.map((equipment, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="mb-2 p-2 bg-gray-50 rounded-md border border-gray-200 flex justify-between items-center"
|
|
||||||
>
|
|
||||||
<span>{equipment}</span>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
danger
|
|
||||||
onClick={() => {
|
|
||||||
setEquipmentList(equipmentList.filter((_, i) => i !== index));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<label>任务经历</label>
|
|
||||||
<Button type="primary" onClick={() => showModal('projects')}>
|
|
||||||
添加任务
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="border p-2 min-h-[100px] rounded">
|
|
||||||
{projectsList.map((mission, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="mb-2 p-2 bg-gray-50 rounded-md border border-gray-200 flex justify-between items-center"
|
|
||||||
>
|
|
||||||
<span>{mission}</span>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
danger
|
|
||||||
onClick={() => {
|
|
||||||
setProjectsList(projectsList.filter((_, i) => i !== index));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<label>奖励信息</label>
|
|
||||||
<Button type="primary" onClick={() => showModal('awards')}>
|
|
||||||
添加奖励
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="border p-2 min-h-[100px] rounded">
|
|
||||||
{rewardsList.map((reward, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="mb-2 p-2 bg-gray-50 rounded-md border border-gray-200 flex justify-between items-center"
|
|
||||||
>
|
|
||||||
<span>{reward}</span>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
danger
|
|
||||||
onClick={() => {
|
|
||||||
setRewardsList(rewardsList.filter((_, i) => i !== index));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<label>处分信息</label>
|
|
||||||
<Button type="primary" onClick={() => showModal('punishments')}>
|
|
||||||
添加处分
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="border p-2 min-h-[100px] rounded">
|
|
||||||
{punishmentsList.map((punishment, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="mb-2 p-2 bg-gray-50 rounded-md border border-gray-200 flex justify-between items-center"
|
|
||||||
>
|
|
||||||
<span>{punishment}</span>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
danger
|
|
||||||
onClick={() => {
|
|
||||||
setPunishmentsList(punishmentsList.filter((_, i) => i !== index));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-lg shadow">
|
|
||||||
<div className="flex justify-end space-x-4">
|
|
||||||
<Button onClick={() => form.resetFields()}>重置</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={handleSubmit}
|
|
||||||
loading={loading}
|
|
||||||
>
|
|
||||||
提交
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
title={
|
|
||||||
modalType === 'awards' ? '添加奖励信息' :
|
|
||||||
modalType === 'punishments' ? '添加处分信息' :
|
|
||||||
modalType === 'equipment' ? '添加装备信息' : '添加任务信息'
|
|
||||||
}
|
|
||||||
open={isModalVisible}
|
|
||||||
onOk={handleModalOk}
|
|
||||||
onCancel={() => {
|
|
||||||
setIsModalVisible(false);
|
|
||||||
modalForm.resetFields();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Form form={modalForm}>
|
|
||||||
<Form.Item
|
|
||||||
label={
|
|
||||||
modalType === 'awards' ? '奖励内容' :
|
|
||||||
modalType === 'punishments' ? '处分内容' :
|
|
||||||
modalType === 'equipment' ? '装备信息' : '任务信息'
|
|
||||||
}
|
|
||||||
name={modalType}
|
|
||||||
rules={[{ required: true, message: '请输入内容' }]}
|
|
||||||
>
|
|
||||||
<TextArea
|
|
||||||
rows={4}
|
|
||||||
placeholder={
|
|
||||||
modalType === 'awards' ? '请输入获得的表彰奖励信息' :
|
|
||||||
modalType === 'punishments' ? '请输入处分信息' :
|
|
||||||
modalType === 'equipment' ? '请输入装备信息' : '请输入任务经历详情'
|
|
||||||
}
|
|
||||||
maxLength={500}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default StaffInformation;
|
|
|
@ -1,129 +0,0 @@
|
||||||
import { api, useStaff } from "@nice/client";
|
|
||||||
import { useMainContext } from "../../layout/MainProvider";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { Button, Form, Input, Modal, Select } from "antd";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import TrainContentTreeSelect from "@web/src/components/models/trainContent/train-content-tree-select";
|
|
||||||
import DepartmentChildrenSelect from "@web/src/components/models/department/department-children-select";
|
|
||||||
|
|
||||||
export default function StaffModal() {
|
|
||||||
const { data: traincontents} = api.trainSituation.findMany.useQuery({
|
|
||||||
select:{
|
|
||||||
id: true,
|
|
||||||
trainContent:{
|
|
||||||
select:{
|
|
||||||
id: true,
|
|
||||||
title: true,
|
|
||||||
type: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
// useEffect(() => {
|
|
||||||
// traincontents?.forEach((situation)=>{
|
|
||||||
// console.log(situation.id);
|
|
||||||
// });
|
|
||||||
// }, [traincontents]);
|
|
||||||
|
|
||||||
const { form, formValue, setVisible, visible, editingRecord, setEditingRecord } = useMainContext()
|
|
||||||
const { create, update } = useStaff();
|
|
||||||
const handleOk = async () => {
|
|
||||||
const values = await form.getFieldsValue();
|
|
||||||
// console.log(values);
|
|
||||||
// console.log(values.position);
|
|
||||||
try {
|
|
||||||
if (editingRecord && editingRecord.id) {
|
|
||||||
await update.mutateAsync({
|
|
||||||
where: { id: editingRecord.id },
|
|
||||||
data: {
|
|
||||||
username: values.username,
|
|
||||||
department: {
|
|
||||||
connect: { id: values.deptId }
|
|
||||||
},
|
|
||||||
rank: values.rank,
|
|
||||||
type: values.type,
|
|
||||||
showname: values.username,
|
|
||||||
updatedAt: new Date()
|
|
||||||
} ,
|
|
||||||
});
|
|
||||||
// console.log(result);
|
|
||||||
} else {
|
|
||||||
const result = await create.mutateAsync(
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
username: values.username,
|
|
||||||
department: {
|
|
||||||
connect: { id: values.deptId }
|
|
||||||
},
|
|
||||||
createdAt: new Date(),
|
|
||||||
showname: values.username,
|
|
||||||
rank: values.rank,
|
|
||||||
type: values.type,
|
|
||||||
} as any,
|
|
||||||
}
|
|
||||||
) ;
|
|
||||||
}
|
|
||||||
toast.success("保存成功");
|
|
||||||
setVisible(false);
|
|
||||||
setEditingRecord(null);
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("保存失败");
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setVisible(false);
|
|
||||||
setEditingRecord(null);
|
|
||||||
form.resetFields();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (visible&&editingRecord) {
|
|
||||||
form.setFieldsValue(editingRecord);
|
|
||||||
}
|
|
||||||
}, [visible,editingRecord]);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal
|
|
||||||
title="编辑员工信息"
|
|
||||||
visible={visible}
|
|
||||||
onOk={handleOk}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
>
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
initialValues={formValue}
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
name={"username"}
|
|
||||||
label="姓名"
|
|
||||||
>
|
|
||||||
<Input className="rounded-lg" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={"deptId"}
|
|
||||||
label="部门"
|
|
||||||
>
|
|
||||||
<DepartmentChildrenSelect></DepartmentChildrenSelect>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={["type"]}
|
|
||||||
label="人员类别"
|
|
||||||
>
|
|
||||||
<Input className="rounded-lg" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name={["rank"]}
|
|
||||||
label="衔职"
|
|
||||||
>
|
|
||||||
<Input className="rounded-lg" />
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
{/* <Button onClick={() =>{console.log(traincontents);}}>TEST</Button> */}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -15,6 +15,7 @@ import WithAuth from "../components/utils/with-auth";
|
||||||
import { CustomRouteObject } from "./types";
|
import { CustomRouteObject } from "./types";
|
||||||
import StaffPage from "../app/admin/staff/page";
|
import StaffPage from "../app/admin/staff/page";
|
||||||
import AdminLayout from "../components/layout/admin/AdminLayout";
|
import AdminLayout from "../components/layout/admin/AdminLayout";
|
||||||
|
import StaffFieldManage from "../app/admin/staffinfo-manage/staffFieldManage";
|
||||||
|
|
||||||
export const adminRoute: CustomRouteObject = {
|
export const adminRoute: CustomRouteObject = {
|
||||||
path: "admin",
|
path: "admin",
|
||||||
|
@ -120,5 +121,11 @@ export const adminRoute: CustomRouteObject = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "staffinfo",
|
||||||
|
name: "人员信息管理",
|
||||||
|
icon: <UserOutlined />,
|
||||||
|
element: <StaffFieldManage></StaffFieldManage>,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,12 +8,12 @@ import {
|
||||||
import ErrorPage from "../app/error";
|
import ErrorPage from "../app/error";
|
||||||
import LoginPage from "../app/login";
|
import LoginPage from "../app/login";
|
||||||
import HomePage from "../app/main/home/page";
|
import HomePage from "../app/main/home/page";
|
||||||
import StaffMessage from "../app/main/staffpage/page";
|
import StaffMessage from "../app/main/staffinfo_show/page";
|
||||||
import MainLayout from "../app/main/layout/MainLayout";
|
import MainLayout from "../app/main/layout/MainLayout";
|
||||||
import DailyPage from "../app/main/daily/page";
|
import DailyPage from "../app/main/daily/page";
|
||||||
import Dashboard from "../app/main/home/page";
|
import Dashboard from "../app/main/home/page";
|
||||||
import WeekPlanPage from "../app/main/plan/weekplan/page";
|
import WeekPlanPage from "../app/main/plan/weekplan/page";
|
||||||
import StaffInformation from "../app/main/staffinformation/page";
|
import StaffInformation from "../app/main/staffinfo_write/page";
|
||||||
import DeptSettingPage from "../app/main/admin/deptsettingpage/page";
|
import DeptSettingPage from "../app/main/admin/deptsettingpage/page";
|
||||||
import { adminRoute } from "./admin-route";
|
import { adminRoute } from "./admin-route";
|
||||||
import AdminLayout from "../components/layout/admin/AdminLayout";
|
import AdminLayout from "../components/layout/admin/AdminLayout";
|
||||||
|
|
|
@ -4,7 +4,37 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { ObjectType, Staff } from "@nice/common";
|
import { ObjectType, Staff } from "@nice/common";
|
||||||
import { findQueryData } from "../utils";
|
import { findQueryData } from "../utils";
|
||||||
import { CrudOperation, emitDataChange } from "../../event";
|
import { CrudOperation, emitDataChange } from "../../event";
|
||||||
export function useStaff() {
|
|
||||||
|
export interface CustomField {
|
||||||
|
name: string;
|
||||||
|
label?: string;
|
||||||
|
type: string;
|
||||||
|
required?: boolean;
|
||||||
|
order?: number;
|
||||||
|
options?: any;
|
||||||
|
group?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomFieldValue {
|
||||||
|
staffId: string;
|
||||||
|
fieldId: string;
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StaffHookReturn {
|
||||||
|
create: ReturnType<typeof api.staff.create.useMutation>;
|
||||||
|
update: ReturnType<typeof api.staff.update.useMutation>;
|
||||||
|
softDeleteByIds: ReturnType<typeof api.staff.softDeleteByIds.useMutation>;
|
||||||
|
getStaff: (key: string) => Staff | undefined;
|
||||||
|
updateUserDomain: ReturnType<typeof api.staff.updateUserDomain.useMutation>;
|
||||||
|
addCustomField: ReturnType<typeof api.staff.addCustomField.useMutation>;
|
||||||
|
updateCustomField: ReturnType<typeof api.staff.updateCustomField.useMutation>;
|
||||||
|
deleteCustomField: ReturnType<typeof api.staff.deleteCustomField.useMutation>;
|
||||||
|
useCustomFields: () => ReturnType<typeof api.staff.getCustomFields.useQuery>;
|
||||||
|
setCustomFieldValue: ReturnType<typeof api.staff.setCustomFieldValue.useMutation>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStaff(): StaffHookReturn {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const queryKey = getQueryKey(api.staff);
|
const queryKey = getQueryKey(api.staff);
|
||||||
|
|
||||||
|
@ -19,8 +49,9 @@ export function useStaff() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const updateUserDomain = api.staff.updateUserDomain.useMutation({
|
const updateUserDomain = api.staff.updateUserDomain.useMutation({
|
||||||
onSuccess: async (result) => {
|
onSuccess: (result) => {
|
||||||
queryClient.invalidateQueries({ queryKey });
|
queryClient.invalidateQueries({ queryKey });
|
||||||
|
emitDataChange(ObjectType.STAFF, result as any, CrudOperation.UPDATED);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const update = api.staff.update.useMutation({
|
const update = api.staff.update.useMutation({
|
||||||
|
@ -39,14 +70,45 @@ export function useStaff() {
|
||||||
queryClient.invalidateQueries({ queryKey });
|
queryClient.invalidateQueries({ queryKey });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const addCustomField = api.staff.addCustomField.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: getQueryKey(api.staff.getCustomFields) });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const updateCustomField = api.staff.updateCustomField.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: getQueryKey(api.staff.getCustomFields) });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const deleteCustomField = api.staff.deleteCustomField.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: getQueryKey(api.staff.getCustomFields) });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const useCustomFields = () => {
|
||||||
|
return api.staff.getCustomFields.useQuery(undefined, {
|
||||||
|
staleTime: 1000 * 60 * 5, // 5分钟缓存
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const setCustomFieldValue = api.staff.setCustomFieldValue.useMutation({
|
||||||
|
onSuccess: (result) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey });
|
||||||
|
},
|
||||||
|
});
|
||||||
const getStaff = (key: string) => {
|
const getStaff = (key: string) => {
|
||||||
return findQueryData<Staff>(queryClient, api.staff, key);
|
return findQueryData<Staff>(queryClient, api.staff, key);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
create,
|
create,
|
||||||
update,
|
update,
|
||||||
softDeleteByIds,
|
softDeleteByIds,
|
||||||
getStaff,
|
getStaff,
|
||||||
updateUserDomain,
|
updateUserDomain,
|
||||||
|
addCustomField,
|
||||||
|
updateCustomField,
|
||||||
|
deleteCustomField,
|
||||||
|
useCustomFields,
|
||||||
|
setCustomFieldValue,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -420,6 +420,41 @@ model Department {
|
||||||
@@map("department")
|
@@map("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")
|
||||||
|
|
||||||
|
@@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")
|
||||||
|
|
||||||
|
@@unique([staffId, fieldId])
|
||||||
|
@@index([staffId])
|
||||||
|
@@index([fieldId])
|
||||||
|
@@map("staff_field_value")
|
||||||
|
}
|
||||||
|
|
||||||
model Staff {
|
model Staff {
|
||||||
// 基础信息
|
// 基础信息
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
|
@ -510,6 +545,9 @@ model Staff {
|
||||||
deletedAt DateTime? @map("deleted_at")
|
deletedAt DateTime? @map("deleted_at")
|
||||||
absent Boolean? @default(false) @map("absent")
|
absent Boolean? @default(false) @map("absent")
|
||||||
|
|
||||||
|
// 添加自定义字段值关联
|
||||||
|
fieldValues StaffFieldValue[]
|
||||||
|
|
||||||
@@index([officerId])
|
@@index([officerId])
|
||||||
@@index([deptId])
|
@@index([deptId])
|
||||||
@@index([domainId])
|
@@index([domainId])
|
||||||
|
|
Loading…
Reference in New Issue