employee-template.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import * as XLSX from "xlsx";
  2. /**
  3. * 员工档案「导入/导出模板」唯一来源。
  4. *
  5. * 表头文案、顺序、工作表名、样例行 100% 对齐用户拍板的《员工档案导入模板.xlsx》(28 列,含「微信号」),
  6. * 「导出当前结果 / 导出已选」与「下载导入模板」都从这里生成,两处口径不允许再各写一份。
  7. * 两处重复表头(第 3 列与第 14 列都叫「*工号」、第 11 列与第 16 列都叫「岗位」)是模板
  8. * 自身的写法,这里原样保留,不做「优化」,否则回导对不上列。
  9. *
  10. * 取值来源说明(均为 GET /main/user/getUserPage 列表行实测字段,无需逐人查详情):
  11. * - 所属机构名称 ← 组织列表 groupId 对应节点名,回落 groupName
  12. * - 所属工厂名称 ← 人员 factoryId 经「工厂区域」字典翻译
  13. * - 所属班组 ← 人员 teamId(逗号串)经「班组」字典翻译
  14. * - 负责部门编码 ← 人员 deptIds 经组织列表节点 groupCode 翻译
  15. * - 岗位序列/层级/性质 ← 人员 positionList 主岗位(positionSequenceName / positionLevelName / positionTypeName)
  16. * - 员工状态 ← status 数字码中文(1全职 2兼职 3实习 4正式 5试用 6离职)
  17. */
  18. /** 文本归一:null/undefined → 空串,其余 trim */
  19. function textOf(value) {
  20. if (value == null) return "";
  21. return String(value).trim();
  22. }
  23. /** 逗号串 / 数组 → 去空白后的字符串数组 */
  24. function toIdList(value) {
  25. if (Array.isArray(value)) return value.map(textOf).filter(Boolean);
  26. return textOf(value)
  27. .split(/[,,]/)
  28. .map(textOf)
  29. .filter(Boolean);
  30. }
  31. /** 主岗位绑定项:primaryFlag=1 优先,其次第一条 */
  32. function primaryBinding(row) {
  33. const list = (row && row.positionBindings) || [];
  34. return list.find((item) => Number(item.primaryFlag) === 1) || list[0] || null;
  35. }
  36. /** id 列表 → 名称列表;字典里查不到时保留原 id,保证信息不丢(不静默丢列) */
  37. function mapNames(ids, dict) {
  38. return ids.map((id) => textOf(dict && dict[id]) || id);
  39. }
  40. /**
  41. * 「YYYY-MM-DD」→ Excel 日期单元格。
  42. * 模板里 入职时间 / 生日 是真日期(序列号 40987),不是文本,这里同样写日期单元格,
  43. * 保证导出的文件后端能按日期解析。无值写空串,格式异常原样输出。
  44. */
  45. function toExcelDate(value) {
  46. const text = textOf(value);
  47. if (!text) return "";
  48. // 后端个别接口把日期回成纯数字毫秒时间戳(如 558115200000),先按时间戳还原
  49. if (/^\d{13}$/.test(text)) {
  50. const stamp = new Date(Number(text));
  51. return Number.isNaN(stamp.getTime()) ? text : stamp;
  52. }
  53. const matched = /^(\d{4})-(\d{1,2})-(\d{1,2})/.exec(text);
  54. if (!matched) return text;
  55. const date = new Date(Number(matched[1]), Number(matched[2]) - 1, Number(matched[3]));
  56. return Number.isNaN(date.getTime()) ? text : date;
  57. }
  58. /** 列定义:header 为模板表头,value(row, index, ctx) 为该列取值 */
  59. export const EMPLOYEE_TEMPLATE_COLUMNS = [
  60. { header: "序号", value: (row, index) => index + 1 },
  61. {
  62. header: "*所属机构名称",
  63. value: (row, index, ctx) =>
  64. textOf(ctx.orgById?.[String(row.groupId)]?.name) || textOf(row.company),
  65. },
  66. { header: "*工号", value: (row) => textOf(row.employeeNo) },
  67. { header: "*姓名", value: (row) => textOf(row.name) },
  68. { header: "*性别", value: (row) => textOf(row.gender) },
  69. { header: "*手机号", value: (row) => textOf(row.phone) },
  70. { header: "*员工状态", value: (row) => textOf(row.formStatus) },
  71. {
  72. header: "所属工厂名称",
  73. value: (row, index, ctx) => textOf(ctx.factoryById?.[String(row.factoryId)]) || "",
  74. },
  75. {
  76. header: "所属班组",
  77. value: (row, index, ctx) =>
  78. mapNames(toIdList(row.teamIds), ctx.teamById).join(","),
  79. },
  80. { header: "*隶属部门", value: (row) => textOf(row.deptName) },
  81. { header: "岗位", value: (row) => textOf(row.position) },
  82. {
  83. header: "负责部门编码(多个逗号分隔)",
  84. value: (row, index, ctx) =>
  85. toIdList(row.deptIds)
  86. .map((id) => textOf(ctx.orgById?.[id]?.groupCode) || id)
  87. .join(","),
  88. },
  89. { header: "*身份证号", value: (row) => textOf(row.idCard) },
  90. /* 与第 3 列同名的模板列,原样保留 */
  91. { header: "*工号", value: (row) => textOf(row.employeeNo) },
  92. { header: "*岗位序列", value: (row) => textOf(primaryBinding(row)?.positionSequenceName) },
  93. { header: "*岗位", value: (row) => textOf(row.position) },
  94. {
  95. header: "*岗位性质",
  96. value: (row) => textOf(row.employmentTypeName) || textOf(row.employmentType),
  97. },
  98. { header: "*岗位层级", value: (row) => textOf(row.level) },
  99. { header: "邮箱", value: (row) => textOf(row.email) },
  100. {
  101. header: "年龄",
  102. value: (row) => (row.age == null || row.age === "" ? "" : Number(row.age)),
  103. },
  104. { header: "*入职时间", value: (row) => toExcelDate(row.hireDate) },
  105. { header: "籍贯", value: (row) => textOf(row.nativePlace) },
  106. {
  107. header: "工种",
  108. value: (row) =>
  109. ((row.workTypes || []).map((item) => textOf(item.name) || textOf(item.code)).filter(Boolean)).join(","),
  110. },
  111. { header: "专业", value: (row) => textOf(row.major) },
  112. { header: "毕业院校", value: (row) => textOf(row.school) },
  113. { header: "生日", value: (row) => toExcelDate(row.birthDate) },
  114. { header: "微信号", value: (row) => textOf(row.wechat) },
  115. { header: "备注:标*号为必填项", value: () => "" },
  116. ];
  117. /** 模板表头(顺序即列顺序,供比对/复用) */
  118. export const EMPLOYEE_TEMPLATE_HEADERS = EMPLOYEE_TEMPLATE_COLUMNS.map(
  119. (column) => column.header,
  120. );
  121. /** 模板工作表名:与《员工档案导入模板.xlsx》一致(Sheet1),保证导出文件与模板结构完全相同 */
  122. export const EMPLOYEE_TEMPLATE_SHEET_NAME = "Sheet1";
  123. /**
  124. * 模板自带的样例行(原文件的第 2 行,逐格原样复刻,含数字/日期单元格类型)。
  125. * 只用于「下载导入模板」,导出真实数据时不带这一行。
  126. * 空列写 null 而不是空串:SheetJS 会跳过 null,生成的空单元格与原模板一致。
  127. */
  128. export function buildEmployeeTemplateSampleRow() {
  129. return [
  130. 1,
  131. "公司",
  132. 1212333,
  133. "曹孟德",
  134. "男",
  135. 18895956063,
  136. "全职",
  137. "万隆本部",
  138. "质检班组",
  139. null,
  140. "总经理",
  141. "G0018,G0019",
  142. "430904198506061566",
  143. null,
  144. null,
  145. null,
  146. null,
  147. null,
  148. "aa@qq.com",
  149. 37,
  150. /* 入职时间:原文件为日期序列号 40987 = 2012-03-19 */
  151. new Date(2012, 2, 19),
  152. "湖南",
  153. null,
  154. 111,
  155. "湖南测试学院",
  156. /* 生日:原文件为日期序列号 25569 = 1970-01-01 */
  157. new Date(1970, 0, 1),
  158. "test001",
  159. null,
  160. ];
  161. }
  162. /**
  163. * 员工档案行 → 模板二维数组(首行为表头)
  164. * @param {Array} rows adaptEmployee 产出的员工行
  165. * @param {object} ctx { orgById, factoryById, teamById } 字典(缺失时对应列退化为 id / 空)
  166. */
  167. export function buildEmployeeTemplateSheet(rows, ctx = {}) {
  168. const header = EMPLOYEE_TEMPLATE_HEADERS.slice();
  169. const body = (rows || []).map((row, index) =>
  170. EMPLOYEE_TEMPLATE_COLUMNS.map((column) => column.value(row, index, ctx)),
  171. );
  172. return { header, body };
  173. }
  174. /**
  175. * 员工档案 → 模板格式 .xlsx
  176. * @param {Array} rows 员工行(模板下载传 [])
  177. * @param {object} ctx 字典
  178. * @param {string} fileName 目标文件名
  179. * @param {object} [options]
  180. * @param {boolean} [options.withSample=false] 是否带模板自带的样例行(第 2 行)
  181. * @returns {number} 写入的数据行数(不含表头/样例行)
  182. */
  183. export function writeEmployeeTemplateXlsx(rows, ctx, fileName, options = {}) {
  184. const { header, body } = buildEmployeeTemplateSheet(rows, ctx);
  185. const aoa = [header];
  186. if (options.withSample) aoa.push(buildEmployeeTemplateSampleRow());
  187. aoa.push(...body);
  188. const sheet = XLSX.utils.aoa_to_sheet(aoa, { cellDates: true });
  189. sheet["!cols"] = EMPLOYEE_TEMPLATE_COLUMNS.map((column) => ({
  190. // 中文按 2 字宽估算,保证表头不被截断
  191. wch: Math.max(10, column.header.length * 2),
  192. }));
  193. const book = XLSX.utils.book_new();
  194. XLSX.utils.book_append_sheet(book, sheet, EMPLOYEE_TEMPLATE_SHEET_NAME);
  195. XLSX.writeFile(book, fileName);
  196. return body.length;
  197. }
  198. /**
  199. * 「下载导入模板」:本地生成与《员工档案导入模板.xlsx》同款空白模板(表头 + 样例行)。
  200. * 不再调后端 `POST /main/user/downLoadTemplate`(后端下发的是旧 22 列,与页面回导口径不一致)。
  201. * @returns {string} 生成的文件名
  202. */
  203. export function downloadEmployeeTemplate(fileName = "员工档案导入模板.xlsx") {
  204. writeEmployeeTemplateXlsx([], {}, fileName, { withSample: true });
  205. return fileName;
  206. }