- 新增 cleanMatrix 样式:仅上下横线边框、无填充底色、全列居中+自动换行 - 部员列 16pt、义工号/合计列 14pt 加粗、活动列 12pt 宋体 - 新增标题行支持:首列左对齐大标题,末列居中日戳 - 四小时导出移除班级/工作组列,列结构对齐参考表 - 表头行高与数据行高均按内容行数自适应 - 自动筛选范围修正为从表头行开始
This commit is contained in:
@@ -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
@@ -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' },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
// 行高:根据每个单元格换行估算(备注行或矩阵有备注行高放大)
|
||||
|
||||
Reference in New Issue
Block a user