8/7 应收欠费汇总表,应收欠费明细表

This commit is contained in:
Zy
2026-08-07 11:46:15 +08:00
parent 40196737d3
commit ef681e4eb3
13 changed files with 1727 additions and 143 deletions

View File

@@ -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
View 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;
}