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