import * as ExcelJS from 'exceljs'; import { EliteFormData } from '@fenghuo/common'; /** * 导出精英表单为 Excel 文件 * @param data 表格数据 * @param columns 列头 * @param filename 文件名(可选) */ export async function exportEliteFormToExcel( data: EliteFormData[], columns: string[], filename?: string ): Promise { try { // 创建工作簿 const workbook = new ExcelJS.Workbook(); const worksheet = workbook.addWorksheet('骨干名册'); // 设置列定义 worksheet.columns = [ { header: '序号', key: 'sequence', width: 8 }, { header: '专业', key: 'profession', width: 15 }, { header: '台站', key: 'station', width: 20 }, { header: '技师(值班代号)', key: 'technician', width: 25 }, { header: '领班员/台站长(值班代号)', key: 'supervisor', width: 28 }, { header: '值机员/值班员(值班代号)', key: 'operator', width: 25 } ]; // 设置表头样式 const headerRow = worksheet.getRow(1); headerRow.height = 25; headerRow.eachCell((cell) => { // 移除背景色 cell.font = { size: 10, name: '黑体' }; cell.alignment = { vertical: 'middle', horizontal: 'center' }; cell.border = { top: { style: 'thin' }, left: { style: 'thin' }, bottom: { style: 'thin' }, right: { style: 'thin' } }; }); // 按专业分组数据 const groupedData = new Map(); data.forEach(row => { const profession = row.profession; if (!groupedData.has(profession)) { groupedData.set(profession, []); } const professionRows = groupedData.get(profession); if (professionRows) { professionRows.push(row); } }); // 添加数据行并处理合并单元格 let currentRow = 2; // 从第2行开始(第1行是表头) let sequenceNumber = 1; // 辅助函数:根据职务等级生成星星 const getStars = (dutyLevel: number): string => { return '★'.repeat(dutyLevel); }; // 辅助函数:格式化人员信息 const formatPersonInfo = (person: { name: string; dutyCode: string; dutyLevel: number }): string => { const stars = person.dutyLevel > 0 ? getStars(person.dutyLevel) : ''; return `${person.name}(${person.dutyCode})${stars}`; }; for (const [profession, rows] of groupedData) { const startRow = currentRow; // 添加该专业的所有行 for (let i = 0; i < rows.length; i++) { const row = rows[i]; if (!row) continue; // 跳过undefined的行 const excelRow = worksheet.addRow({ sequence: i === 0 ? sequenceNumber : '', // 只在第一行显示序号 profession: i === 0 ? profession : '', // 只在第一行显示专业名称 station: row.parentOrganization ? `${row.parentOrganization}${row.station}` : row.station, technician: row.technicians.map(formatPersonInfo).join('、') || '', supervisor: row.supervisors.map(formatPersonInfo).join('、') || '', operator: row.operators.map(formatPersonInfo).join('、') || '' }); // 设置行高(根据内容自动调整) const maxLines = Math.max( row.technicians.length || 1, row.supervisors.length || 1, row.operators.length || 1 ); excelRow.height = Math.max(40, maxLines * 20); // 设置单元格样式 excelRow.eachCell((cell, colNumber) => { cell.alignment = { vertical: 'middle', horizontal: colNumber <= 3 ? 'center' : 'left', // 序号、专业、台站居中,其他左对齐 wrapText: true }; cell.border = { top: { style: 'thin' }, left: { style: 'thin' }, bottom: { style: 'thin' }, right: { style: 'thin' } }; cell.font = { name: '仿宋GB2312', size: 10 }; }); currentRow++; } // 如果该专业有多行,合并序号和专业单元格 if (rows.length > 1) { const endRow = currentRow - 1; // 合并序号列 worksheet.mergeCells(startRow, 1, endRow, 1); // 合并专业列 worksheet.mergeCells(startRow, 2, endRow, 2); // 设置合并后单元格的对齐方式和字体 worksheet.getCell(startRow, 1).alignment = { vertical: 'middle', horizontal: 'center' }; worksheet.getCell(startRow, 1).font = { name: '仿宋GB2312', size: 11 }; worksheet.getCell(startRow, 2).alignment = { vertical: 'middle', horizontal: 'center' }; worksheet.getCell(startRow, 2).font = { name: '仿宋GB2312', size: 11 }; } sequenceNumber++; } // 生成文件并下载 const buffer = await workbook.xlsx.writeBuffer(); const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); const defaultFilename = `骨干名册_${new Date().toISOString()}.xlsx`; // 创建下载链接 const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename || defaultFilename; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); } catch (error) { console.error('导出Excel失败:', error); throw new Error('导出Excel失败,请稍后重试'); } }