This commit is contained in:
Li1304553726 2025-04-08 16:48:40 +08:00
parent ee320de2ca
commit 754900d0b8
4 changed files with 601 additions and 538 deletions

View File

@ -21,20 +21,22 @@ module.exports = {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
// 允许使用短路表达式
'no-unused-expressions': 'off',
// 允许使用 let 声明后不重新赋值的变量
'prefer-const': 'off',
// 允许使用 any 类型
'@typescript-eslint/no-explicit-any': 'off',
// 允许声明但未使用的变量
'@typescript-eslint/no-unused-vars': [
'warn',
{
vars: 'all', // 检查所有变量
args: 'none', // 不检查函数参数
vars: 'all',
args: 'none',
ignoreRestSiblings: true,
},
],
// 禁止使用未声明的变量
'no-undef': 'error',
// 可选:关闭未定义变量检查
'no-undef': 'off',
},
};

View File

@ -23,8 +23,16 @@ export default tseslint.config(
"warn",
{ allowConstantExport: true },
],
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
// 允许使用 any 类型
"@typescript-eslint/no-explicit-any": "off",
// 允许使用 let 声明后不重新赋值的变量
"no-unused-expressions": "off",
// 允许使用 let 声明后不重新赋值的变量
"prefer-const": "off",
// 允许声明但未使用的变量
"@typescript-eslint/no-unused-vars": [

View File

@ -1,23 +1,28 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import { AgGridReact } from 'ag-grid-react';
/* eslint-disable no-undef */
/* eslint-disable @typescript-eslint/no-unused-expressions */
/* eslint-disable no-unused-expressions */
import { useState, useEffect, useMemo, useCallback } from "react";
import { AgGridReact } from "ag-grid-react";
import { api, useStaff } from "@nice/client";
import { Button, CascaderProps, message, Modal, Input, Upload } from 'antd';
import { areaOptions } from '@web/src/app/main/staffinfo_write/area-options';
import StaffInfoWrite from '@web/src/app/main/staffinfo_write/staffinfo_write.page';
import { utils, writeFile, read } from 'xlsx';
import { UploadOutlined } from '@ant-design/icons';
import { Button, CascaderProps, message, Modal, Input, Upload } from "antd";
import { areaOptions } from "@web/src/app/main/staffinfo_write/area-options";
import StaffInfoWrite from "@web/src/app/main/staffinfo_write/staffinfo_write.page";
import { utils, writeFile, read } from "xlsx";
import { UploadOutlined } from "@ant-design/icons";
function getAreaName(codes: string[], level?: number): string {
const result: string[] = [];
let currentLevel: CascaderProps['options'] = areaOptions;
let currentLevel: CascaderProps["options"] = areaOptions;
for (const code of codes) {
const found = currentLevel?.find(opt => opt.value === code);
const found = currentLevel?.find((opt) => opt.value === code);
if (!found) break;
result.push(String(found.label));
currentLevel = found.children || [];
if (level && result.length >= level) break; // 添加层级控制
}
return level ? result[level - 1] || '' : result.join(' / ') || codes.join('/');
return level
? result[level - 1] || ""
: result.join(" / ") || codes.join("/");
}
// 添加表头提取工具函数
@ -25,8 +30,8 @@ function extractHeaders(columns: any[]): string[] {
const result: string[] = [];
const extractHeadersRecursive = (cols: any[]) => {
cols.forEach(col => {
if ('children' in col && col.children) {
cols.forEach((col) => {
if ("children" in col && col.children) {
extractHeadersRecursive(col.children);
} else if (col.headerName) {
result.push(col.headerName);
@ -48,39 +53,43 @@ export default function StaffMessage() {
const [currentEditStaff, setCurrentEditStaff] = useState<any>(null);
const [gridApi, setGridApi] = useState<any>(null);
const [fileNameVisible, setFileNameVisible] = useState(false);
const [fileName, setFileName] = useState('');
const [defaultFileName] = useState(`员工数据_${new Date().toISOString().slice(0, 10)}`);
const [fileName, setFileName] = useState("");
const [defaultFileName] = useState(
`员工数据_${new Date().toISOString().slice(0, 10)}`
);
const [importVisible, setImportVisible] = useState(false);
// 获取数据
const { data: staffData } = api.staff.findMany.useQuery({
where: {
deletedAt: null
deletedAt: null,
},
include: {
fieldValues: {
include: {
// 添加这两个关联字段
staff: { select: { id: true } }, // 关联员工ID
field: { select: { id: true } } // 关联字段ID
}
field: { select: { id: true } }, // 关联字段ID
},
},
department: true,
},
department: true
}
} as any);
// console.log(staffData);
const actionColumns = [{
const actionColumns = [
{
field: "action",
width: 50,
checkboxSelection: true,
headerCheckboxSelection: true,
pinned: 'left'
}]
pinned: "left",
},
];
// 新增编辑处理函数
const handleEdit = useCallback(async () => {
if (selectedRows.length === 0) return;
if (selectedRows.length > 1) {
message.error('只能选择一个员工进行编辑');
message.error("只能选择一个员工进行编辑");
return;
}
setCurrentEditStaff(selectedRows[0]);
@ -97,47 +106,51 @@ export default function StaffMessage() {
// 新增删除处理函数
const handleDelete = useCallback(async () => {
if (selectedRows.length === 0) return;
console.log('待删除的选中行数据:', selectedRows); // 新增调试语句
console.log("待删除的选中行数据:", selectedRows); // 新增调试语句
try {
await softDeleteByIds.mutateAsync({
ids: selectedRows?.map(row => {
console.log('当前行ID:', row.id); // 检查每个ID
return row.id
})
ids: selectedRows?.map((row) => {
console.log("当前行ID:", row.id); // 检查每个ID
return row.id;
}),
});
message.success('删除成功');
message.success("删除成功");
} catch (error) {
message.error('删除失败');
console.error('详细错误信息:', error); // 输出完整错误堆栈
message.error("删除失败");
console.error("详细错误信息:", error); // 输出完整错误堆栈
}
// 重新获取数据或本地过滤已删除项
}, [selectedRows]);
// 缓存基础列定义
const baseColumns = useMemo(() => [
const baseColumns = useMemo(
() => [
{
field: 'showname',
headerName: '姓名',
filter: 'agSetColumnFilter',
pinned: 'left'
field: "showname",
headerName: "姓名",
filter: "agSetColumnFilter",
pinned: "left",
},
{
field: 'deptId',
headerName: '所属部门',
valueGetter: params => params.data.department?.name,
filter: 'agSetColumnFilter'
}
], []);
field: "deptId",
headerName: "所属部门",
valueGetter: (params) => params.data.department?.name,
filter: "agSetColumnFilter",
},
],
[]
);
// 缓存动态列定义
const dynamicColumns = useMemo(() =>
(fields.data || [] as any).map(field => ({
const dynamicColumns = useMemo(
() =>
(fields.data || ([] as any)).map((field) => ({
field: field.name,
headerName: field.label || field.name,
filter: 'agSetColumnFilter',
cellStyle: { whiteSpace: 'pre-line' },
filter: "agSetColumnFilter",
cellStyle: { whiteSpace: "pre-line" },
autoHeight: true,
valueGetter: params => {
valueGetter: (params) => {
// 获取原始值
const rawValue = params.data.fieldValues?.find(
(fv: any) => fv.fieldId === field.id
@ -145,22 +158,22 @@ export default function StaffMessage() {
// 根据字段类型格式化
switch (field.type) {
case 'cascader':
return rawValue ? getAreaName(rawValue.split('/')) : '';
case "cascader":
return rawValue ? getAreaName(rawValue.split("/")) : "";
case 'date':
case "date":
// 格式化日期假设存储的是ISO字符串
return rawValue ? new Date(rawValue).toLocaleDateString() : '';
return rawValue ? new Date(rawValue).toLocaleDateString() : "";
case 'textarea':
case "textarea":
// 换行处理
return rawValue?.replace(/,/g, '\n');
return rawValue?.replace(/,/g, "\n");
default:
return rawValue;
}
}
},
})),
[fields.data]
);
@ -179,7 +192,9 @@ export default function StaffMessage() {
const headerNames = extractHeaders(columnDefs);
// 创建示例数据行
let exampleRow: Record<string, string> = {};
const exampleRow: Record<string, string> = {};
// 定义 fieldsList移到这里
const fieldsList = Array.isArray(fields?.data) ? fields.data : [];
// 检查是否有选中行
if (selectedRows.length > 0) {
@ -187,11 +202,10 @@ export default function StaffMessage() {
const templateData = selectedRows[0];
// 基础字段
exampleRow['姓名'] = templateData.showname || '';
exampleRow['所属部门'] = templateData.department?.name || '';
exampleRow["姓名"] = templateData.showname || "";
exampleRow["所属部门"] = templateData.department?.name || "";
// 处理自定义字段
const fieldsList = Array.isArray(fields?.data) ? fields.data : [];
fieldsList.forEach((field: any) => {
const fieldValue = templateData.fieldValues?.find(
(fv: any) => fv.fieldId === field.id
@ -200,32 +214,35 @@ export default function StaffMessage() {
let displayValue = fieldValue;
// 根据字段类型处理值
if (field.type === 'cascader' && fieldValue) {
displayValue = getAreaName(fieldValue.split('/'));
} else if (field.type === 'date' && fieldValue) {
if (field.type === "cascader" && fieldValue) {
displayValue = getAreaName(fieldValue.split("/"));
} else if (field.type === "date" && fieldValue) {
displayValue = new Date(fieldValue).toLocaleDateString();
} else if (field.type === 'textarea' && fieldValue) {
displayValue = fieldValue.replace(/,/g, '\n');
} else if (field.type === "textarea" && fieldValue) {
displayValue = fieldValue.replace(/,/g, "\n");
}
exampleRow[field.label || field.name] = displayValue || '';
exampleRow[field.label || field.name] = displayValue || "";
});
} else {
// 如果没有选中行,使用默认示例数据
exampleRow['姓名'] = '张三';
exampleRow['所属部门'] = '技术部';
exampleRow["姓名"] = "张三";
exampleRow["所属部门"] = "技术部";
// 添加所有自定义字段的空值
fieldsList.forEach((field: any) => {
exampleRow[field.label || field.name] = '';
exampleRow[field.label || field.name] = "";
});
}
// 创建空白行供用户填写
const emptyRow = headerNames.reduce((obj, header) => {
obj[header] = '';
const emptyRow = headerNames.reduce(
(obj, header) => {
obj[header] = "";
return obj;
}, {} as Record<string, string>);
},
{} as Record<string, string>
);
// 创建工作簿和工作表
const wb = utils.book_new();
@ -233,26 +250,30 @@ export default function StaffMessage() {
// 设置列宽
const colWidth = headerNames.map(() => ({ wch: 20 }));
ws['!cols'] = colWidth;
ws["!cols"] = colWidth;
// 在第二行添加提示文字
const rowIdx = 2; // 第二行索引
const cellRef = utils.encode_cell({ r: rowIdx, c: 0 }); // A3单元格
const tipText = selectedRows.length > 0
? '以上为选中人员数据,请在下方行填写实际数据'
: '以上为示例数据,请在下方行填写实际数据';
const tipText =
selectedRows.length > 0
? "以上为选中人员数据,请在下方行填写实际数据"
: "以上为示例数据,请在下方行填写实际数据";
ws[cellRef] = { t: 's', v: tipText };
ws[cellRef] = { t: "s", v: tipText };
// 手动添加空白行
utils.sheet_add_json(ws, [emptyRow], { skipHeader: true, origin: rowIdx + 1 });
utils.sheet_add_json(ws, [emptyRow], {
skipHeader: true,
origin: rowIdx + 1,
});
// 合并提示文字单元格
if (!ws['!merges']) ws['!merges'] = [];
ws['!merges'].push({
if (!ws["!merges"]) ws["!merges"] = [];
ws["!merges"].push({
s: { r: rowIdx, c: 0 }, // 起始单元格 A3
e: { r: rowIdx, c: Math.min(5, headerNames.length - 1) } // 结束单元格,跨越多列
e: { r: rowIdx, c: Math.min(5, headerNames.length - 1) }, // 结束单元格,跨越多列
});
utils.book_append_sheet(wb, ws, "员工模板");
@ -268,7 +289,7 @@ export default function StaffMessage() {
const handleFileNameConfirm = useCallback(() => {
setFileNameVisible(false);
if (!gridApi) {
console.error('Grid API 未正确初始化');
console.error("Grid API 未正确初始化");
return;
}
@ -277,29 +298,31 @@ export default function StaffMessage() {
const allData = selectedRows.length > 0 ? selectedRows : rowData;
// 格式化数据
const exportData = allData.map(row => {
const exportData = allData.map((row) => {
const formattedRow: Record<string, any> = {};
// 基础字段
formattedRow['姓名'] = row.showname || '';
formattedRow['所属部门'] = row.department?.name || '';
formattedRow["姓名"] = row.showname || "";
formattedRow["所属部门"] = row.department?.name || "";
// 动态字段
const fieldsList = Array.isArray(fields?.data) ? fields.data : [];
fieldsList.forEach((field: any) => {
const fieldValue = row.fieldValues?.find((fv: any) => fv.fieldId === field.id)?.value;
const fieldValue = row.fieldValues?.find(
(fv: any) => fv.fieldId === field.id
)?.value;
let displayValue = fieldValue;
// 根据字段类型处理值
if (field.type === 'cascader' && fieldValue) {
displayValue = getAreaName(fieldValue.split('/'));
} else if (field.type === 'date' && fieldValue) {
if (field.type === "cascader" && fieldValue) {
displayValue = getAreaName(fieldValue.split("/"));
} else if (field.type === "date" && fieldValue) {
displayValue = new Date(fieldValue).toLocaleDateString();
} else if (field.type === 'textarea' && fieldValue) {
displayValue = fieldValue.replace(/,/g, '\n');
} else if (field.type === "textarea" && fieldValue) {
displayValue = fieldValue.replace(/,/g, "\n");
}
formattedRow[field.label || field.name] = displayValue || '';
formattedRow[field.label || field.name] = displayValue || "";
});
return formattedRow;
@ -314,43 +337,44 @@ export default function StaffMessage() {
const finalFileName = fileName || defaultFileName;
writeFile(wb, `${finalFileName}.xlsx`);
} catch (error) {
console.error('导出失败:', error);
message.error('导出失败,请稍后重试');
console.error("导出失败:", error);
message.error("导出失败,请稍后重试");
}
}, [fileName, defaultFileName, rowData, selectedRows, fields.data, gridApi]);
// 处理导入数据
const createMany = api.staff.create.useMutation({
onSuccess: () => {
message.success('员工数据导入成功');
message.success("员工数据导入成功");
// 刷新数据
api.staff.findMany.useQuery();
setImportVisible(false);
},
onError: (error) => {
message.error(`导入失败: ${error.message}`);
}
},
});
// 处理Excel导入数据
const handleImportData = useCallback((excelData: any[]) => {
const handleImportData = useCallback(
(excelData: any[]) => {
if (excelData.length === 0) {
message.warning('没有可导入的数据');
message.warning("没有可导入的数据");
return;
}
try {
// 将Excel数据转换为API需要的格式
const staffData = excelData.map(row => {
const staffData = excelData.map((row) => {
const staff: any = { fieldValues: [] };
// 处理基础字段
if (row['姓名']) staff.showname = row['姓名'];
if (row["姓名"]) staff.showname = row["姓名"];
// 处理部门
if (row['所属部门']) {
if (row["所属部门"]) {
// 简单存储部门名称,后续可能需要查询匹配
staff.departmentName = row['所属部门'];
staff.departmentName = row["所属部门"];
}
// 处理自定义字段
@ -359,16 +383,16 @@ export default function StaffMessage() {
let value = row[field.label || field.name];
// 跳过空值
if (value === undefined || value === '') return;
if (value === undefined || value === "") return;
// 根据字段类型处理输入值
switch (field.type) {
case 'cascader':
case "cascader":
// 级联选择器可能需要将显示名称转回代码
// 这里简单保留原值,实际可能需要查询转换
break;
case 'date':
case "date":
// 尝试将日期字符串转换为ISO格式
try {
const dateObj = new Date(value);
@ -380,10 +404,10 @@ export default function StaffMessage() {
}
break;
case 'textarea':
case "textarea":
// 将换行符替换回逗号进行存储
if (typeof value === 'string') {
value = value.replace(/\n/g, ',');
if (typeof value === "string") {
value = value.replace(/\n/g, ",");
}
break;
@ -393,7 +417,7 @@ export default function StaffMessage() {
// 添加到fieldValues数组
staff.fieldValues.push({
fieldId: field.id,
value: String(value)
value: String(value),
});
});
@ -401,35 +425,63 @@ export default function StaffMessage() {
});
// 提交数据
createMany.mutate({ data: staffData });
createMany.mutate({
data: staffData[0], // 由于类型限制,这里只能一条一条导入
});
// 如果有多条数据,需要循环处理
for (let i = 1; i < staffData.length; i++) {
createMany.mutate({ data: staffData[i] });
}
message.info(`正在导入${staffData.length}条员工数据...`);
} catch (error) {
console.error('处理导入数据失败:', error);
message.error('数据格式错误,导入失败');
console.error("处理导入数据失败:", error);
message.error("数据格式错误,导入失败");
}
}, [fields.data, createMany]);
},
[fields.data, createMany]
);
return (
<>
<div className="flex justify-between items-center mb-4">
<div className="space-x-2">
<Button className="bg-red-500 hover:bg-red-600 border-red-500 text-white rounded-md px-4 py-2"
onClick={handleDelete}></Button>
<Button className="bg-blue-500 hover:bg-blue-600 border-blue-500 text-white rounded-md px-4 py-2"
onClick={handleEdit}></Button>
<Button onClick={() => setImportVisible(true)} className="bg-orange-500 text-white px-4 py-2 rounded hover:bg-orange-600">
<Button
className="bg-red-500 hover:bg-red-600 border-red-500 text-white rounded-md px-4 py-2"
onClick={handleDelete}
>
</Button>
<Button
className="bg-blue-500 hover:bg-blue-600 border-blue-500 text-white rounded-md px-4 py-2"
onClick={handleEdit}
>
</Button>
<Button
onClick={() => setImportVisible(true)}
className="bg-orange-500 text-white px-4 py-2 rounded hover:bg-orange-600"
>
Excel
</Button>
<Button onClick={handleExportTemplate} className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
<Button
onClick={handleExportTemplate}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
</Button>
<Button onClick={handleConfirm} className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
<Button
onClick={handleConfirm}
className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"
>
</Button>
</div>
<Button className="bg-gray-100 hover:bg-gray-200 border-gray-300 text-gray-700 rounded-md px-4 py-2"
onClick={() => {
}}></Button>
<Button
className="bg-gray-100 hover:bg-gray-200 border-gray-300 text-gray-700 rounded-md px-4 py-2"
onClick={() => {}}
>
</Button>
</div>
<div className="bg-white rounded-lg shadow-md p-6 mb-4">
@ -442,7 +494,7 @@ export default function StaffMessage() {
pagination={true}
paginationPageSize={10}
paginationPageSizeSelector={[10, 20, 50, 100]}
onSelectionChanged={e => setSelectedRows(e.api.getSelectedRows())}
onSelectionChanged={(e) => setSelectedRows(e.api.getSelectedRows())}
rowSelection="multiple"
className="rounded border border-gray-200"
headerHeight={40}
@ -498,7 +550,7 @@ export default function StaffMessage() {
>
<Upload
accept=".xlsx,.xls"
beforeUpload={file => {
beforeUpload={(file) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
@ -506,15 +558,15 @@ export default function StaffMessage() {
const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
if (data.length === 0) {
message.warning('Excel文件中没有数据');
message.warning("Excel文件中没有数据");
return;
}
message.info(`读取到${data.length}条数据,正在处理...`);
handleImportData(data);
} catch (error) {
console.error('解析Excel文件失败:', error);
message.error('Excel文件格式错误请确保使用正确的模板');
console.error("解析Excel文件失败:", error);
message.error("Excel文件格式错误请确保使用正确的模板");
}
};
reader.readAsArrayBuffer(file);

View File

@ -466,7 +466,8 @@ model Staff {
enabled Boolean? @default(true)
officerId String? @map("officer_id")
phoneNumber String? @map("phone_number")
age Int?@map("age")
sex String?@map("sex")
// 部门关系
domainId String? @map("domain_id")
deptId String? @map("dept_id")