import type ExcelJS from 'exceljs' import { SALARY_ROLES } from './performance-salary-roles' import { getSalaryTemplate, getPositionSalaryTemplate, isSalaryMonth, SALARY_TEMPLATE_BLANK_ROWS } from './performance-salary-templates' import type { SalaryPosition } from './salary-position-contract' const XLSX_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' export function salaryExportFileName(month: string) { if (!isSalaryMonth(month)) throw new Error('请选择有效月份后重试。') return `绩效薪资模板_${month}.xlsx` } export async function buildSalaryTemplateWorkbook(month: string, positions?: SalaryPosition[]) { salaryExportFileName(month) const { default: Excel } = await import('exceljs') const workbook = new Excel.Workbook() workbook.creator = '品牌中心' const border: Partial = { top: { style: 'thin', color: { argb: 'FFE2E8F0' } }, bottom: { style: 'thin', color: { argb: 'FFE2E8F0' } }, left: { style: 'thin', color: { argb: 'FFE2E8F0' } }, right: { style: 'thin', color: { argb: 'FFE2E8F0' } }, } const templates = SALARY_ROLES.flatMap(role => { const configured = positions?.filter(position => position.salaryRole === role.key) ?? [] return configured.length ? configured.map(position => getPositionSalaryTemplate(position, month)) : [getSalaryTemplate(role.key, month)] }) const used = new Set() for (const template of templates) { const baseName = template.title.replace(/[\\/*?:\[\]]/g, ' ').replace(/^'+|'+$/g, '').trim().slice(0, 26) || '岗位' let sheetName = baseName, counter = 1 while (used.has(sheetName.toLocaleLowerCase())) sheetName = `${baseName}_${++counter}` used.add(sheetName.toLocaleLowerCase()) const sheet = workbook.addWorksheet(sheetName) const size = template.columns.length const headerRow = template.grouped ? 3 : 2 sheet.mergeCells(1, 1, 1, size) const title = sheet.getCell(1, 1) title.value = `${month} ${template.title}薪资明细` title.font = { name: '微软雅黑', size: 14, bold: true, color: { argb: 'FF101828' } } title.alignment = { vertical: 'middle', horizontal: 'left' } sheet.getRow(1).height = 34 if (template.grouped) { let start = 1 for (const group of template.groups) { const end = start + group.columns.length - 1 if (end > start) sheet.mergeCells(2, start, 2, end) sheet.getCell(2, start).value = group.label start = end + 1 } sheet.getRow(2).height = 28 } template.columns.forEach((column, index) => { sheet.getColumn(index + 1).width = Math.round(column.width / 7) sheet.getCell(headerRow, index + 1).value = column.label }) sheet.getRow(headerRow).height = 64 for (let row = 2; row <= headerRow + SALARY_TEMPLATE_BLANK_ROWS; row += 1) { if (row > headerRow) sheet.getRow(row).height = 30 for (let col = 1; col <= size; col += 1) { const cell = sheet.getCell(row, col) cell.border = border cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true } cell.font = { name: '微软雅黑', size: 11, color: { argb: 'FF344054' }, bold: row <= headerRow } if (row <= headerRow) cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: row < headerRow ? 'FFE6EDF5' : 'FFF8FAFC' } } else { const kind = template.columns[col - 1].kind cell.numFmt = kind === 'money' ? '#,##0.00' : template.columns[col - 1].key.endsWith('Score') ? '0.00' : kind === 'percent' ? '0.00%' : kind === 'number' ? '0.##' : '@' } } } sheet.views = [{ state: 'frozen', xSplit: 1, ySplit: headerRow, topLeftCell: `B${headerRow + 1}` }] sheet.pageSetup = { orientation: 'landscape', printTitlesRow: `1:${headerRow}` } sheet.pageSetup.printArea = `A1:${sheet.getColumn(size).letter}${headerRow + SALARY_TEMPLATE_BLANK_ROWS}` } return workbook } export async function saveSalaryTemplateWorkbook(month: string, positions?: SalaryPosition[]) { const workbook = await buildSalaryTemplateWorkbook(month, positions) const buffer = await workbook.xlsx.writeBuffer() const blob = new Blob([buffer], { type: XLSX_MIME }) if (typeof URL.createObjectURL !== 'function') throw new Error('当前浏览器无法下载文件,请更换浏览器后重试。') const url = URL.createObjectURL(blob) const anchor = document.createElement('a') anchor.href = url anchor.download = salaryExportFileName(month) anchor.style.display = 'none' document.body.appendChild(anchor) try { anchor.click() } finally { anchor.remove() window.setTimeout(() => URL.revokeObjectURL(url), 1000) } }