style(export): 四小时导出采用简洁矩阵样式,匹配参考表格
Deploy to Server / deploy (push) Successful in 2m4s

- 新增 cleanMatrix 样式:仅上下横线边框、无填充底色、全列居中+自动换行
- 部员列 16pt、义工号/合计列 14pt 加粗、活动列 12pt 宋体
- 新增标题行支持:首列左对齐大标题,末列居中日戳
- 四小时导出移除班级/工作组列,列结构对齐参考表
- 表头行高与数据行高均按内容行数自适应
- 自动筛选范围修正为从表头行开始
This commit is contained in:
Trae Assistant
2026-10-11 16:22:32 +08:00
parent 28ba850bfa
commit 635a21d811
2 changed files with 112 additions and 37 deletions
+6 -5
View File
@@ -203,9 +203,7 @@ export default function FourHourExport() {
const data = users.map((u: any) => { const data = users.map((u: any) => {
const row: any = { const row: any = {
'部员': u.name, '部员': u.name,
'班级': u.class || '-',
'义工号': u.volunteer_no || '-', '义工号': u.volunteer_no || '-',
'工作组': u.group_name || '-',
}; };
for (const a of activities) { for (const a of activities) {
const h = Number(u[String(a.id)] || 0); const h = Number(u[String(a.id)] || 0);
@@ -224,10 +222,13 @@ export default function FourHourExport() {
sheets.push({ sheets.push({
name: '活动矩阵总表(四小时核销)', name: '活动矩阵总表(四小时核销)',
data, data,
hint: firstColTitle ? { hint: {
matrixColStart: firstColTitle, style: 'cleanMatrix',
title: '宣传部义工时长记录系统 · 四小时导出核销',
subtitle: utc8DateStamp(),
matrixColStart: firstColTitle || undefined,
matrixColEnd: '本次核销合计(h)', matrixColEnd: '本次核销合计(h)',
} : undefined, },
}); });
exportToExcel(sheets, `义工时长-四小时导出-活动矩阵总表-${utc8DateStamp()}.xlsx`); exportToExcel(sheets, `义工时长-四小时导出-活动矩阵总表-${utc8DateStamp()}.xlsx`);
}; };
+80 -6
View File
@@ -96,19 +96,39 @@ export function exportToExcel(
matrixColEnd?: string; matrixColEnd?: string;
/** row index (0-based in data array) of header */ /** row index (0-based in data array) of header */
headerRow?: number; headerRow?: number;
/** cleanMatrix: 仅上下横线、无填充、全居中换行 */
style?: 'default' | 'cleanMatrix';
/** 标题行文本(首列),提供时会自动在数据前插入标题行 */
title?: string;
/** 标题行右侧副文本(末列),如日期 */
subtitle?: string;
}; };
}>, }>,
filename: string filename: string
) { ) {
const wb = XLSX.utils.book_new(); const wb = XLSX.utils.book_new();
for (const s of sheets) { for (const s of sheets) {
const ws = XLSX.utils.json_to_sheet(s.data || []); const isClean = s.hint?.style === 'cleanMatrix';
const hasTitle = !!s.hint?.title;
let sheetData = s.data || [];
if (hasTitle && sheetData.length > 0) {
const keys = Object.keys(sheetData[0]);
const titleRow: any = {};
keys.forEach((k, i) => {
if (i === 0) titleRow[k] = s.hint!.title || '';
else if (i === keys.length - 1) titleRow[k] = s.hint!.subtitle || '';
else titleRow[k] = '';
});
sheetData = [titleRow, ...sheetData];
}
const ws = XLSX.utils.json_to_sheet(sheetData);
const rangeRef = ws['!ref'] || 'A1'; const rangeRef = ws['!ref'] || 'A1';
const range = XLSX.utils.decode_range(rangeRef); const range = XLSX.utils.decode_range(rangeRef);
const headerR = s.hint?.headerRow ?? (hasTitle ? range.s.r + 1 : range.s.r);
const cols: string[] = []; const cols: string[] = [];
const headers: string[] = []; const headers: string[] = [];
for (let c = range.s.c; c <= range.e.c; c++) { for (let c = range.s.c; c <= range.e.c; c++) {
const addr = XLSX.utils.encode_cell({ r: range.s.r, c }); const addr = XLSX.utils.encode_cell({ r: headerR, c });
const cell = ws[addr]; const cell = ws[addr];
const headerName = cell ? String(cell.v ?? '') : ''; const headerName = cell ? String(cell.v ?? '') : '';
headers.push(headerName); headers.push(headerName);
@@ -117,7 +137,7 @@ export function exportToExcel(
ws['!cols'] = headers.map((name, idx) => { ws['!cols'] = headers.map((name, idx) => {
const colLetter = cols[idx]; const colLetter = cols[idx];
let max = Math.max(8, String(name ?? '').length + 2); let max = Math.max(8, String(name ?? '').length + 2);
const startRow = s.hint?.numericStartRow ?? (range.s.r + 1); const startRow = s.hint?.numericStartRow ?? (headerR + 1);
for (let r = startRow; r <= Math.min(range.e.r, startRow + 1999); r++) { for (let r = startRow; r <= Math.min(range.e.r, startRow + 1999); r++) {
const a = XLSX.utils.encode_cell({ r, c: range.s.c + idx }); const a = XLSX.utils.encode_cell({ r, c: range.s.c + idx });
const v = ws[a]; const v = ws[a];
@@ -141,10 +161,19 @@ export function exportToExcel(
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
void colLetter; void colLetter;
}); });
const headerR = s.hint?.headerRow ?? range.s.r;
// 1. Header 样式 // 1. Header 样式
for (let c = range.s.c; c <= range.e.c; c++) { for (let c = range.s.c; c <= range.e.c; c++) {
const addr = XLSX.utils.encode_cell({ r: headerR, c }); const addr = XLSX.utils.encode_cell({ r: headerR, c });
if (isClean) {
applyCellStyle(ws, addr, {
bold: true,
align: 'center',
fontSize: 12,
wrap: true,
top: true,
bottom: true,
});
} else {
applyCellStyle(ws, addr, { applyCellStyle(ws, addr, {
bold: true, bold: true,
align: 'center', align: 'center',
@@ -155,9 +184,37 @@ export function exportToExcel(
borderAll: true, borderAll: true,
}); });
} }
}
// 标题行样式(首列左对齐大字号加粗,末列居中)
if (hasTitle) {
for (let c = range.s.c; c <= range.e.c; c++) {
const addr = XLSX.utils.encode_cell({ r: range.s.r, c });
if (!ws[addr]) continue;
if (c === range.s.c) {
applyCellStyle(ws, addr, {
bold: true,
align: 'left',
fontSize: 16,
wrap: false,
});
} else if (c === range.e.c) {
applyCellStyle(ws, addr, {
align: 'center',
fontSize: 12,
wrap: false,
});
}
}
if (!ws['!rows']) ws['!rows'] = [];
ws['!rows'][range.s.r] = { hpt: 28 };
}
// 首行冻结(表头行下一行) // 首行冻结(表头行下一行)
ws['!freeze'] = { xSplit: 0, ySplit: Math.min(headerR + 1, range.e.r + 1) }; ws['!freeze'] = { xSplit: 0, ySplit: Math.min(headerR + 1, range.e.r + 1) };
ws['!autofilter'] = { ref: XLSX.utils.encode_range(range) }; const filterRange = XLSX.utils.encode_range({
s: { r: headerR, c: range.s.c },
e: { r: range.e.r, c: range.e.c },
});
ws['!autofilter'] = { ref: filterRange };
// 2. body 样式 + 数字列 + 居中等 + 交替底色 // 2. body 样式 + 数字列 + 居中等 + 交替底色
const numericColIdx = new Set<number>(); const numericColIdx = new Set<number>();
for (let c = range.s.c; c <= range.e.c; c++) { for (let c = range.s.c; c <= range.e.c; c++) {
@@ -200,8 +257,11 @@ export function exportToExcel(
} }
} }
const dataStartR = headerR + 1; const dataStartR = headerR + 1;
const firstCol = range.s.c;
const lastCol = range.e.c;
const volNoColIdx = headers.indexOf('义工号');
for (let r = dataStartR; r <= range.e.r; r++) { for (let r = dataStartR; r <= range.e.r; r++) {
const altFill = ((r - dataStartR) % 2 === 1) ? ALT_ROW : undefined; const altFill = isClean ? undefined : (((r - dataStartR) % 2 === 1) ? ALT_ROW : undefined);
for (let c = range.s.c; c <= range.e.c; c++) { for (let c = range.s.c; c <= range.e.c; c++) {
const addr = XLSX.utils.encode_cell({ r, c }); const addr = XLSX.utils.encode_cell({ r, c });
if (!ws[addr]) { if (!ws[addr]) {
@@ -212,6 +272,19 @@ export function exportToExcel(
const isCenter = centerColIdx.has(c); const isCenter = centerColIdx.has(c);
const inMatrix = (matrixCS >= 0 && matrixCE >= 0) && (c >= matrixCS && c <= matrixCE); const inMatrix = (matrixCS >= 0 && matrixCE >= 0) && (c >= matrixCS && c <= matrixCE);
const isTotalCol = (headers[c - range.s.c] === '总时长'); const isTotalCol = (headers[c - range.s.c] === '总时长');
if (isClean) {
const isVolNo = c - range.s.c === volNoColIdx;
const isFirst = c === firstCol;
const isLast = c === lastCol;
applyCellStyle(ws, addr, {
align: 'center',
wrap: true,
top: true,
bottom: true,
bold: isVolNo || isLast,
fontSize: isFirst ? 16 : (isVolNo || isLast ? 14 : 12),
});
} else {
applyCellStyle(ws, addr, { applyCellStyle(ws, addr, {
align: (inMatrix || isCenter || isNum) ? 'center' : 'left', align: (inMatrix || isCenter || isNum) ? 'center' : 'left',
numFmt: (isNum || inMatrix) ? '0.0' : undefined, numFmt: (isNum || inMatrix) ? '0.0' : undefined,
@@ -231,6 +304,7 @@ export function exportToExcel(
}; };
} }
} }
}
// 行高:根据每个单元格换行估算(备注行或矩阵有备注行高放大) // 行高:根据每个单元格换行估算(备注行或矩阵有备注行高放大)
let lines = 1; let lines = 1;
for (let c = range.s.c; c <= range.e.c; c++) { for (let c = range.s.c; c <= range.e.c; c++) {