8/7 应收欠费汇总表,应收欠费明细表
This commit is contained in:
@@ -46,7 +46,8 @@ const whiteVillageId = [
|
||||
'/cash/billList',
|
||||
'/houseRental/changeStatus', // 房屋租赁 - 上架/下架
|
||||
'/points/pointsProducts/list', // 积分商品列表
|
||||
'/points/*'
|
||||
'/points/*',
|
||||
'/outstandingFeeSummary/*'
|
||||
// '/building/list'
|
||||
];
|
||||
|
||||
|
||||
312
src/utils/xlsx.ts
Normal file
312
src/utils/xlsx.ts
Normal file
@@ -0,0 +1,312 @@
|
||||
import * as XLSX from 'xlsx-js-style';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
/**
|
||||
* 列配置类型(支持多级表头)
|
||||
*/
|
||||
export type ColumnConfig = {
|
||||
label: string; // 列名
|
||||
prop?: string; // 字段名(叶子节点必须有)
|
||||
children?: ColumnConfig[]; // 子列(用于多级表头)
|
||||
align?: 'left' | 'center' | 'right'; // 对齐方式
|
||||
width?: number; // 列宽
|
||||
};
|
||||
|
||||
/**
|
||||
* 合并单元格回调函数类型(类似 Element Plus 的 span-method)
|
||||
*/
|
||||
export type SpanMethodCallback = (params: { row: any; rowIndex: number; columnIndex: number }) => { rowspan: number; colspan: number };
|
||||
|
||||
/**
|
||||
* 前端导出Excel(支持多级表头)
|
||||
*/
|
||||
export function exportToExcel(data: any[], columns: ColumnConfig[], filename: string, spanMethod?: SpanMethodCallback) {
|
||||
if (!data || data.length === 0) {
|
||||
ElMessage.warning('没有可导出的数据');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取叶子列(实际数据列)
|
||||
const leafColumns = getLeafColumns(columns);
|
||||
|
||||
// 获取表头深度
|
||||
const headerRows = getMaxHeaderDepth(columns);
|
||||
|
||||
// 手动创建工作表(不使用 json_to_sheet)
|
||||
const worksheet: XLSX.WorkSheet = {};
|
||||
|
||||
// 1. 填充表头
|
||||
fillHeaderCells(worksheet, columns, 0, 0, headerRows);
|
||||
|
||||
// 2. 填充数据(从 headerRows 行开始)
|
||||
data.forEach((row, rowIndex) => {
|
||||
leafColumns.forEach((col, colIndex) => {
|
||||
if (col.prop) {
|
||||
const cellAddress = XLSX.utils.encode_cell({ r: rowIndex + headerRows, c: colIndex });
|
||||
const value = getCellValue(row, col.prop);
|
||||
worksheet[cellAddress] = { t: 's', v: value !== undefined && value !== null ? String(value) : '' };
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 3. 设置表头合并
|
||||
const merges = calculateHeaderMerges(columns, leafColumns);
|
||||
|
||||
// 4. 处理数据行合并
|
||||
if (spanMethod) {
|
||||
const dataMerges = calculateMergesBySpanMethod(data, leafColumns, spanMethod, headerRows);
|
||||
merges.push(...dataMerges);
|
||||
}
|
||||
|
||||
worksheet['!merges'] = merges;
|
||||
|
||||
// 5. 设置列宽
|
||||
worksheet['!cols'] = leafColumns.map((col) => ({ wch: col.width || 20 }));
|
||||
|
||||
// 6. 设置范围
|
||||
const lastRow = data.length + headerRows - 1;
|
||||
const lastCol = leafColumns.length - 1;
|
||||
worksheet['!ref'] = XLSX.utils.encode_range({ s: { r: 0, c: 0 }, e: { r: lastRow, c: lastCol } });
|
||||
|
||||
// 7. 设置所有单元格样式(包含黑色边框)
|
||||
const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1');
|
||||
for (let R = range.s.r; R <= range.e.r; ++R) {
|
||||
for (let C = range.s.c; C <= range.e.c; ++C) {
|
||||
const cellAddress = XLSX.utils.encode_cell({ r: R, c: C });
|
||||
if (worksheet[cellAddress]) {
|
||||
const isHeader = R < headerRows;
|
||||
worksheet[cellAddress].s = {
|
||||
fill: isHeader
|
||||
? {
|
||||
fgColor: { rgb: 'bec4cb' },
|
||||
patternType: 'solid'
|
||||
}
|
||||
: undefined,
|
||||
font: isHeader
|
||||
? {
|
||||
color: { rgb: 'FFFFFF' },
|
||||
bold: true,
|
||||
sz: 11
|
||||
}
|
||||
: undefined,
|
||||
border: {
|
||||
top: { style: 'thin', color: { rgb: '000000' } },
|
||||
bottom: { style: 'thin', color: { rgb: '000000' } },
|
||||
left: { style: 'thin', color: { rgb: '000000' } },
|
||||
right: { style: 'thin', color: { rgb: '000000' } }
|
||||
},
|
||||
alignment: {
|
||||
horizontal: 'center',
|
||||
vertical: 'center'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 设置合并单元格居中
|
||||
if (worksheet['!merges']) {
|
||||
worksheet['!merges'].forEach((merge) => {
|
||||
for (let R = merge.s.r; R <= merge.e.r; ++R) {
|
||||
for (let C = merge.s.c; C <= merge.e.c; ++C) {
|
||||
const cellAddress = XLSX.utils.encode_cell({ r: R, c: C });
|
||||
if (worksheet[cellAddress]) {
|
||||
worksheet[cellAddress].s = {
|
||||
...worksheet[cellAddress].s,
|
||||
alignment: {
|
||||
horizontal: 'center',
|
||||
vertical: 'center'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
|
||||
|
||||
// 导出文件
|
||||
const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
|
||||
const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||
saveAs(blob, `${filename}.xlsx`);
|
||||
|
||||
ElMessage.success('导出成功');
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error);
|
||||
ElMessage.error('导出失败,请重试');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 spanMethod 回调计算需要合并的单元格范围
|
||||
* @param headerRows 表头行数(数据行需要偏移)
|
||||
*/
|
||||
function calculateMergesBySpanMethod(data: any[], columns: ColumnConfig[], spanMethod: SpanMethodCallback, headerRows: number = 1): XLSX.Range[] {
|
||||
const merges: XLSX.Range[] = [];
|
||||
const coveredCells = new Set<string>();
|
||||
|
||||
for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
|
||||
const row = data[rowIndex];
|
||||
for (let colIndex = 0; colIndex < columns.length; colIndex++) {
|
||||
const cellKey = `${rowIndex}-${colIndex}`;
|
||||
if (coveredCells.has(cellKey)) continue;
|
||||
|
||||
const result = spanMethod({ row, rowIndex, columnIndex: colIndex });
|
||||
if (result && (result.rowspan > 1 || result.colspan > 1)) {
|
||||
// 数据行需要加上表头行数的偏移
|
||||
const startRow = rowIndex + headerRows;
|
||||
const startCol = colIndex;
|
||||
const endRow = startRow + result.rowspan - 1;
|
||||
const endCol = startCol + result.colspan - 1;
|
||||
|
||||
merges.push({
|
||||
s: { r: startRow, c: startCol },
|
||||
e: { r: endRow, c: endCol }
|
||||
});
|
||||
|
||||
for (let r = startRow; r <= endRow; r++) {
|
||||
for (let c = startCol; c <= endCol; c++) {
|
||||
if (r !== startRow || c !== startCol) {
|
||||
coveredCells.add(`${r - headerRows}-${c}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return merges;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有叶子列(实际数据列)
|
||||
*/
|
||||
function getLeafColumns(columns: ColumnConfig[]): ColumnConfig[] {
|
||||
const leaves: ColumnConfig[] = [];
|
||||
function traverse(cols: ColumnConfig[]) {
|
||||
cols.forEach((col) => {
|
||||
if (col.children && col.children.length > 0) {
|
||||
traverse(col.children);
|
||||
} else {
|
||||
leaves.push(col);
|
||||
}
|
||||
});
|
||||
}
|
||||
traverse(columns);
|
||||
return leaves;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最大表头深度
|
||||
*/
|
||||
function getMaxHeaderDepth(columns: ColumnConfig[]): number {
|
||||
function getDepth(cols: ColumnConfig[]): number {
|
||||
let maxDepth = 0;
|
||||
cols.forEach((col) => {
|
||||
if (col.children && col.children.length > 0) {
|
||||
maxDepth = Math.max(maxDepth, 1 + getDepth(col.children));
|
||||
} else {
|
||||
maxDepth = Math.max(maxDepth, 1);
|
||||
}
|
||||
});
|
||||
return maxDepth;
|
||||
}
|
||||
return getDepth(columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算表头合并范围
|
||||
*/
|
||||
function calculateHeaderMerges(columns: ColumnConfig[], leafColumns: ColumnConfig[]): XLSX.Range[] {
|
||||
const merges: XLSX.Range[] = [];
|
||||
const maxDepth = getMaxHeaderDepth(columns);
|
||||
|
||||
function traverse(cols: ColumnConfig[], depth: number, startLeafIndex: number): number {
|
||||
let currentLeafIndex = startLeafIndex;
|
||||
|
||||
cols.forEach((col) => {
|
||||
if (col.children && col.children.length > 0) {
|
||||
const endLeafIndex = traverse(col.children, depth + 1, currentLeafIndex);
|
||||
|
||||
if (endLeafIndex - currentLeafIndex > 0) {
|
||||
merges.push({
|
||||
s: { r: depth, c: currentLeafIndex },
|
||||
e: { r: depth, c: endLeafIndex }
|
||||
});
|
||||
}
|
||||
|
||||
if (endLeafIndex > currentLeafIndex) {
|
||||
merges.push({
|
||||
s: { r: depth, c: currentLeafIndex },
|
||||
e: { r: depth, c: endLeafIndex } // 只水平合并
|
||||
});
|
||||
}
|
||||
|
||||
currentLeafIndex = endLeafIndex + 1;
|
||||
} else {
|
||||
if (depth < maxDepth - 1) {
|
||||
merges.push({
|
||||
s: { r: depth, c: currentLeafIndex },
|
||||
e: { r: maxDepth - 1, c: currentLeafIndex }
|
||||
});
|
||||
}
|
||||
currentLeafIndex++;
|
||||
}
|
||||
});
|
||||
|
||||
return currentLeafIndex - 1;
|
||||
}
|
||||
|
||||
traverse(columns, 0, 0);
|
||||
return merges;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单元格值(支持嵌套属性)
|
||||
*/
|
||||
function getCellValue(row: any, prop: string): any {
|
||||
if (!prop.includes('.')) {
|
||||
return row[prop];
|
||||
}
|
||||
|
||||
const keys = prop.split('.');
|
||||
let value = row;
|
||||
for (const key of keys) {
|
||||
if (value === null || value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
value = value[key];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充表头单元格
|
||||
*/
|
||||
function fillHeaderCells(worksheet: XLSX.WorkSheet, columns: ColumnConfig[], startCol: number, depth: number, maxDepth: number): number {
|
||||
let currentCol = startCol;
|
||||
|
||||
columns.forEach((col) => {
|
||||
if (col.children && col.children.length > 0) {
|
||||
// 有子列,递归处理
|
||||
const endCol = fillHeaderCells(worksheet, col.children, currentCol, depth + 1, maxDepth);
|
||||
|
||||
// 父列表头:在 depth 行,从 currentCol 到 endCol
|
||||
const cellAddress = XLSX.utils.encode_cell({ r: depth, c: currentCol });
|
||||
worksheet[cellAddress] = { t: 's', v: col.label };
|
||||
|
||||
currentCol = endCol + 1;
|
||||
} else {
|
||||
// 叶子节点
|
||||
const cellAddress = XLSX.utils.encode_cell({ r: depth, c: currentCol });
|
||||
worksheet[cellAddress] = { t: 's', v: col.label };
|
||||
currentCol++;
|
||||
}
|
||||
});
|
||||
|
||||
return currentCol - 1;
|
||||
}
|
||||
Reference in New Issue
Block a user