| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- import * as XLSX from "xlsx";
- /**
- * 员工档案「导入/导出模板」唯一来源。
- *
- * 表头文案、顺序、工作表名、样例行 100% 对齐用户拍板的《员工档案导入模板.xlsx》(28 列,含「微信号」),
- * 「导出当前结果 / 导出已选」与「下载导入模板」都从这里生成,两处口径不允许再各写一份。
- * 两处重复表头(第 3 列与第 14 列都叫「*工号」、第 11 列与第 16 列都叫「岗位」)是模板
- * 自身的写法,这里原样保留,不做「优化」,否则回导对不上列。
- *
- * 取值来源说明(均为 GET /main/user/getUserPage 列表行实测字段,无需逐人查详情):
- * - 所属机构名称 ← 组织列表 groupId 对应节点名,回落 groupName
- * - 所属工厂名称 ← 人员 factoryId 经「工厂区域」字典翻译
- * - 所属班组 ← 人员 teamId(逗号串)经「班组」字典翻译
- * - 负责部门编码 ← 人员 deptIds 经组织列表节点 groupCode 翻译
- * - 岗位序列/层级/性质 ← 人员 positionList 主岗位(positionSequenceName / positionLevelName / positionTypeName)
- * - 员工状态 ← status 数字码中文(1全职 2兼职 3实习 4正式 5试用 6离职)
- */
- /** 文本归一:null/undefined → 空串,其余 trim */
- function textOf(value) {
- if (value == null) return "";
- return String(value).trim();
- }
- /** 逗号串 / 数组 → 去空白后的字符串数组 */
- function toIdList(value) {
- if (Array.isArray(value)) return value.map(textOf).filter(Boolean);
- return textOf(value)
- .split(/[,,]/)
- .map(textOf)
- .filter(Boolean);
- }
- /** 主岗位绑定项:primaryFlag=1 优先,其次第一条 */
- function primaryBinding(row) {
- const list = (row && row.positionBindings) || [];
- return list.find((item) => Number(item.primaryFlag) === 1) || list[0] || null;
- }
- /** id 列表 → 名称列表;字典里查不到时保留原 id,保证信息不丢(不静默丢列) */
- function mapNames(ids, dict) {
- return ids.map((id) => textOf(dict && dict[id]) || id);
- }
- /**
- * 「YYYY-MM-DD」→ Excel 日期单元格。
- * 模板里 入职时间 / 生日 是真日期(序列号 40987),不是文本,这里同样写日期单元格,
- * 保证导出的文件后端能按日期解析。无值写空串,格式异常原样输出。
- */
- function toExcelDate(value) {
- const text = textOf(value);
- if (!text) return "";
- // 后端个别接口把日期回成纯数字毫秒时间戳(如 558115200000),先按时间戳还原
- if (/^\d{13}$/.test(text)) {
- const stamp = new Date(Number(text));
- return Number.isNaN(stamp.getTime()) ? text : stamp;
- }
- const matched = /^(\d{4})-(\d{1,2})-(\d{1,2})/.exec(text);
- if (!matched) return text;
- const date = new Date(Number(matched[1]), Number(matched[2]) - 1, Number(matched[3]));
- return Number.isNaN(date.getTime()) ? text : date;
- }
- /** 列定义:header 为模板表头,value(row, index, ctx) 为该列取值 */
- export const EMPLOYEE_TEMPLATE_COLUMNS = [
- { header: "序号", value: (row, index) => index + 1 },
- {
- header: "*所属机构名称",
- value: (row, index, ctx) =>
- textOf(ctx.orgById?.[String(row.groupId)]?.name) || textOf(row.company),
- },
- { header: "*工号", value: (row) => textOf(row.employeeNo) },
- { header: "*姓名", value: (row) => textOf(row.name) },
- { header: "*性别", value: (row) => textOf(row.gender) },
- { header: "*手机号", value: (row) => textOf(row.phone) },
- { header: "*员工状态", value: (row) => textOf(row.formStatus) },
- {
- header: "所属工厂名称",
- value: (row, index, ctx) => textOf(ctx.factoryById?.[String(row.factoryId)]) || "",
- },
- {
- header: "所属班组",
- value: (row, index, ctx) =>
- mapNames(toIdList(row.teamIds), ctx.teamById).join(","),
- },
- { header: "*隶属部门", value: (row) => textOf(row.deptName) },
- { header: "岗位", value: (row) => textOf(row.position) },
- {
- header: "负责部门编码(多个逗号分隔)",
- value: (row, index, ctx) =>
- toIdList(row.deptIds)
- .map((id) => textOf(ctx.orgById?.[id]?.groupCode) || id)
- .join(","),
- },
- { header: "*身份证号", value: (row) => textOf(row.idCard) },
- /* 与第 3 列同名的模板列,原样保留 */
- { header: "*工号", value: (row) => textOf(row.employeeNo) },
- { header: "*岗位序列", value: (row) => textOf(primaryBinding(row)?.positionSequenceName) },
- { header: "*岗位", value: (row) => textOf(row.position) },
- {
- header: "*岗位性质",
- value: (row) => textOf(row.employmentTypeName) || textOf(row.employmentType),
- },
- { header: "*岗位层级", value: (row) => textOf(row.level) },
- { header: "邮箱", value: (row) => textOf(row.email) },
- {
- header: "年龄",
- value: (row) => (row.age == null || row.age === "" ? "" : Number(row.age)),
- },
- { header: "*入职时间", value: (row) => toExcelDate(row.hireDate) },
- { header: "籍贯", value: (row) => textOf(row.nativePlace) },
- {
- header: "工种",
- value: (row) =>
- ((row.workTypes || []).map((item) => textOf(item.name) || textOf(item.code)).filter(Boolean)).join(","),
- },
- { header: "专业", value: (row) => textOf(row.major) },
- { header: "毕业院校", value: (row) => textOf(row.school) },
- { header: "生日", value: (row) => toExcelDate(row.birthDate) },
- { header: "微信号", value: (row) => textOf(row.wechat) },
- { header: "备注:标*号为必填项", value: () => "" },
- ];
- /** 模板表头(顺序即列顺序,供比对/复用) */
- export const EMPLOYEE_TEMPLATE_HEADERS = EMPLOYEE_TEMPLATE_COLUMNS.map(
- (column) => column.header,
- );
- /** 模板工作表名:与《员工档案导入模板.xlsx》一致(Sheet1),保证导出文件与模板结构完全相同 */
- export const EMPLOYEE_TEMPLATE_SHEET_NAME = "Sheet1";
- /**
- * 模板自带的样例行(原文件的第 2 行,逐格原样复刻,含数字/日期单元格类型)。
- * 只用于「下载导入模板」,导出真实数据时不带这一行。
- * 空列写 null 而不是空串:SheetJS 会跳过 null,生成的空单元格与原模板一致。
- */
- export function buildEmployeeTemplateSampleRow() {
- return [
- 1,
- "公司",
- 1212333,
- "曹孟德",
- "男",
- 18895956063,
- "全职",
- "万隆本部",
- "质检班组",
- null,
- "总经理",
- "G0018,G0019",
- "430904198506061566",
- null,
- null,
- null,
- null,
- null,
- "aa@qq.com",
- 37,
- /* 入职时间:原文件为日期序列号 40987 = 2012-03-19 */
- new Date(2012, 2, 19),
- "湖南",
- null,
- 111,
- "湖南测试学院",
- /* 生日:原文件为日期序列号 25569 = 1970-01-01 */
- new Date(1970, 0, 1),
- "test001",
- null,
- ];
- }
- /**
- * 员工档案行 → 模板二维数组(首行为表头)
- * @param {Array} rows adaptEmployee 产出的员工行
- * @param {object} ctx { orgById, factoryById, teamById } 字典(缺失时对应列退化为 id / 空)
- */
- export function buildEmployeeTemplateSheet(rows, ctx = {}) {
- const header = EMPLOYEE_TEMPLATE_HEADERS.slice();
- const body = (rows || []).map((row, index) =>
- EMPLOYEE_TEMPLATE_COLUMNS.map((column) => column.value(row, index, ctx)),
- );
- return { header, body };
- }
- /**
- * 员工档案 → 模板格式 .xlsx
- * @param {Array} rows 员工行(模板下载传 [])
- * @param {object} ctx 字典
- * @param {string} fileName 目标文件名
- * @param {object} [options]
- * @param {boolean} [options.withSample=false] 是否带模板自带的样例行(第 2 行)
- * @returns {number} 写入的数据行数(不含表头/样例行)
- */
- export function writeEmployeeTemplateXlsx(rows, ctx, fileName, options = {}) {
- const { header, body } = buildEmployeeTemplateSheet(rows, ctx);
- const aoa = [header];
- if (options.withSample) aoa.push(buildEmployeeTemplateSampleRow());
- aoa.push(...body);
- const sheet = XLSX.utils.aoa_to_sheet(aoa, { cellDates: true });
- sheet["!cols"] = EMPLOYEE_TEMPLATE_COLUMNS.map((column) => ({
- // 中文按 2 字宽估算,保证表头不被截断
- wch: Math.max(10, column.header.length * 2),
- }));
- const book = XLSX.utils.book_new();
- XLSX.utils.book_append_sheet(book, sheet, EMPLOYEE_TEMPLATE_SHEET_NAME);
- XLSX.writeFile(book, fileName);
- return body.length;
- }
- /**
- * 「下载导入模板」:本地生成与《员工档案导入模板.xlsx》同款空白模板(表头 + 样例行)。
- * 不再调后端 `POST /main/user/downLoadTemplate`(后端下发的是旧 22 列,与页面回导口径不一致)。
- * @returns {string} 生成的文件名
- */
- export function downloadEmployeeTemplate(fileName = "员工档案导入模板.xlsx") {
- writeEmployeeTemplateXlsx([], {}, fileName, { withSample: true });
- return fileName;
- }
|