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 row: any = {
'部员': u.name,
'班级': u.class || '-',
'义工号': u.volunteer_no || '-',
'工作组': u.group_name || '-',
};
for (const a of activities) {
const h = Number(u[String(a.id)] || 0);
@@ -224,10 +222,13 @@ export default function FourHourExport() {
sheets.push({
name: '活动矩阵总表(四小时核销)',
data,
hint: firstColTitle ? {
matrixColStart: firstColTitle,
hint: {
style: 'cleanMatrix',
title: '宣传部义工时长记录系统 · 四小时导出核销',
subtitle: utc8DateStamp(),
matrixColStart: firstColTitle || undefined,
matrixColEnd: '本次核销合计(h)',
} : undefined,
},
});
exportToExcel(sheets, `义工时长-四小时导出-活动矩阵总表-${utc8DateStamp()}.xlsx`);
};
+106 -32
View File
@@ -96,19 +96,39 @@ export function exportToExcel(
matrixColEnd?: string;
/** row index (0-based in data array) of header */
headerRow?: number;
/** cleanMatrix: 仅上下横线、无填充、全居中换行 */
style?: 'default' | 'cleanMatrix';
/** 标题行文本(首列),提供时会自动在数据前插入标题行 */
title?: string;
/** 标题行右侧副文本(末列),如日期 */
subtitle?: string;
};
}>,
filename: string
) {
const wb = XLSX.utils.book_new();
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 range = XLSX.utils.decode_range(rangeRef);
const headerR = s.hint?.headerRow ?? (hasTitle ? range.s.r + 1 : range.s.r);
const cols: string[] = [];
const headers: string[] = [];
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 headerName = cell ? String(cell.v ?? '') : '';
headers.push(headerName);
@@ -117,7 +137,7 @@ export function exportToExcel(
ws['!cols'] = headers.map((name, idx) => {
const colLetter = cols[idx];
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++) {
const a = XLSX.utils.encode_cell({ r, c: range.s.c + idx });
const v = ws[a];
@@ -141,23 +161,60 @@ export function exportToExcel(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
void colLetter;
});
const headerR = s.hint?.headerRow ?? range.s.r;
// 1. Header 样式
for (let c = range.s.c; c <= range.e.c; c++) {
const addr = XLSX.utils.encode_cell({ r: headerR, c });
applyCellStyle(ws, addr, {
bold: true,
align: 'center',
fill: THEME_BG_FG,
fg: THEME_FG_FG,
fontSize: 12,
wrap: true,
borderAll: true,
});
if (isClean) {
applyCellStyle(ws, addr, {
bold: true,
align: 'center',
fontSize: 12,
wrap: true,
top: true,
bottom: true,
});
} else {
applyCellStyle(ws, addr, {
bold: true,
align: 'center',
fill: THEME_BG_FG,
fg: THEME_FG_FG,
fontSize: 12,
wrap: 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['!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 样式 + 数字列 + 居中等 + 交替底色
const numericColIdx = new Set<number>();
for (let c = range.s.c; c <= range.e.c; c++) {
@@ -200,8 +257,11 @@ export function exportToExcel(
}
}
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++) {
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++) {
const addr = XLSX.utils.encode_cell({ r, c });
if (!ws[addr]) {
@@ -212,23 +272,37 @@ export function exportToExcel(
const isCenter = centerColIdx.has(c);
const inMatrix = (matrixCS >= 0 && matrixCE >= 0) && (c >= matrixCS && c <= matrixCE);
const isTotalCol = (headers[c - range.s.c] === '总时长');
applyCellStyle(ws, addr, {
align: (inMatrix || isCenter || isNum) ? 'center' : 'left',
numFmt: (isNum || inMatrix) ? '0.0' : undefined,
wrap: inMatrix || /备注|内容|活动名称|活动标题|说明/.test(headers[c - range.s.c] || ''),
fill: altFill,
borderAll: true,
bold: isTotalCol,
fg: isTotalCol ? THEME_BG_FG : undefined,
});
// 总时长列额外加底色
if (isTotalCol) {
const existing = (ws[addr].s as any) || {};
ws[addr].s = {
...existing,
fill: { patternType: 'solid', fgColor: { rgb: 'FFEDF7F7' }, bgColor: { rgb: 'FFEDF7F7' } },
alignment: existing.alignment || { horizontal: 'center', vertical: 'center' },
};
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, {
align: (inMatrix || isCenter || isNum) ? 'center' : 'left',
numFmt: (isNum || inMatrix) ? '0.0' : undefined,
wrap: inMatrix || /备注|内容|活动名称|活动标题|说明/.test(headers[c - range.s.c] || ''),
fill: altFill,
borderAll: true,
bold: isTotalCol,
fg: isTotalCol ? THEME_BG_FG : undefined,
});
// 总时长列额外加底色
if (isTotalCol) {
const existing = (ws[addr].s as any) || {};
ws[addr].s = {
...existing,
fill: { patternType: 'solid', fgColor: { rgb: 'FFEDF7F7' }, bgColor: { rgb: 'FFEDF7F7' } },
alignment: existing.alignment || { horizontal: 'center', vertical: 'center' },
};
}
}
}
// 行高:根据每个单元格换行估算(备注行或矩阵有备注行高放大)