From ef681e4eb308357e359777e8a67dd725aacd129a Mon Sep 17 00:00:00 2001 From: Zy <1448159279@qq.com> Date: Fri, 7 Aug 2026 11:46:15 +0800 Subject: [PATCH] =?UTF-8?q?8/7=20=20=E5=BA=94=E6=94=B6=E6=AC=A0=E8=B4=B9?= =?UTF-8?q?=E6=B1=87=E6=80=BB=E8=A1=A8=EF=BC=8C=E5=BA=94=E6=94=B6=E6=AC=A0?= =?UTF-8?q?=E8=B4=B9=E6=98=8E=E7=BB=86=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +- src/api/system/report/aging/index.ts | 12 + src/api/system/report/aging/type.ts | 74 ++++ src/api/system/report/arrears/arrears.ts | 18 + src/api/system/report/arrears/type.ts | 113 ++++++ src/api/system/residentReviewProcess/type.ts | 140 ------- src/components/Pagination/index.vue | 2 +- src/utils/request.ts | 3 +- src/utils/xlsx.ts | 312 ++++++++++++++++ src/views/system/report/aging.vue | 241 ++++++++++++ src/views/system/report/agingDetailed.vue | 223 +++++++++++ src/views/system/report/arrears.vue | 372 +++++++++++++++++++ src/views/system/report/detailed.vue | 356 ++++++++++++++++++ 13 files changed, 1727 insertions(+), 143 deletions(-) create mode 100644 src/api/system/report/aging/index.ts create mode 100644 src/api/system/report/aging/type.ts create mode 100644 src/api/system/report/arrears/arrears.ts create mode 100644 src/api/system/report/arrears/type.ts create mode 100644 src/utils/xlsx.ts create mode 100644 src/views/system/report/aging.vue create mode 100644 src/views/system/report/agingDetailed.vue create mode 100644 src/views/system/report/arrears.vue create mode 100644 src/views/system/report/detailed.vue diff --git a/package.json b/package.json index e432aa2..7617c1e 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,8 @@ "vue-types": "6.0.0", "vue3-print-nb": "^0.1.4", "vuedraggable": "^4.1.0", - "vxe-table": "4.18.1" + "vxe-table": "4.18.1", + "xlsx-js-style": "^1.2.0" }, "devDependencies": { "@iconify/json": "^2.2.448", @@ -64,6 +65,7 @@ "@types/js-cookie": "3.0.6", "@types/node": "^25.4.0", "@types/nprogress": "0.2.3", + "@types/xlsx": "^0.0.35", "@unocss/preset-attributify": "66.6.6", "@unocss/preset-icons": "66.6.6", "@unocss/preset-uno": "66.6.6", diff --git a/src/api/system/report/aging/index.ts b/src/api/system/report/aging/index.ts new file mode 100644 index 0000000..0515c2b --- /dev/null +++ b/src/api/system/report/aging/index.ts @@ -0,0 +1,12 @@ +import request from '@/utils/request'; +import { AgingListResponse, OwnerArrearsDetailVo } from '@/api/system/report/aging/type'; +import { AxiosPromise } from 'axios'; + +export const getAgingList = (query): Promise => { + return request({ + url: '/agingAnalysis/summary', + method: 'get', + params: query + }); +}; + diff --git a/src/api/system/report/aging/type.ts b/src/api/system/report/aging/type.ts new file mode 100644 index 0000000..518d185 --- /dev/null +++ b/src/api/system/report/aging/type.ts @@ -0,0 +1,74 @@ +export interface AgingListResponse { + total: number; + rows: AgingSummary[]; + summary: AgingSummary; + code: number; + msg: string; +} + +export interface AgingSummary { + amountLe6: string; + amountM6To12: string; + amountOver5: string; + amountY1To2: string; + amountY2To3: string; + amountY3To4: string; + amountY4To5: string; + cutoffPeriod: string; + houseTypeName: string; + totalAmount: string; + villageName: string; +} +// 明细 --------------------------------- +/** + * 业主欠费明细视图对象 + * + * OwnerArrearsDetailVo + */ +export type OwnerArrearsDetailVo = { + /** + * 区域(小区名称) + */ + area?: string; + /** + * 欠费月数 + */ + arrearsMonths?: number; + /** + * 应账账期(如:20170101-20171231) + */ + billPeriod?: string; + /** + * 客户(业主姓名) + */ + customerName?: string; + /** + * 垃圾清运费 + */ + garbageFee?: number; + /** + * 房号 + */ + houseNo?: string; + /** + * 其他费用合计 + */ + otherFee?: number; + /** + * 联系电话 + */ + phone?: string; + /** + * 物业费 + */ + propertyFee?: number; + /** + * 序号 + */ + serialNumber?: number; + /** + * 金额合计 + */ + totalAmount?: number; + [property: string]: any; +}; diff --git a/src/api/system/report/arrears/arrears.ts b/src/api/system/report/arrears/arrears.ts new file mode 100644 index 0000000..da77054 --- /dev/null +++ b/src/api/system/report/arrears/arrears.ts @@ -0,0 +1,18 @@ +import request from '@/utils/request'; +import { ArrearsDetailQuery, ArrearsListResponse, ArrearsQuery, OwnerArrearsDetailVo } from '@/api/system/report/arrears/type'; +import { AxiosPromise } from 'axios'; + +export const getArrearsList = (query?: ArrearsQuery): Promise => { + return request({ + url: '/outstandingFeeSummary/list', + method: 'get', + params: query + }); +}; +export const getArrearsDetails = (query?: ArrearsDetailQuery): AxiosPromise => { + return request({ + url: '/outstandingFeeSummary/ownerArrearsDetail', + method: 'get', + params: query + }); +}; diff --git a/src/api/system/report/arrears/type.ts b/src/api/system/report/arrears/type.ts new file mode 100644 index 0000000..8d1f1aa --- /dev/null +++ b/src/api/system/report/arrears/type.ts @@ -0,0 +1,113 @@ + +export interface ArrearsQuery extends PageQuery { + /** + * 视图类型: + * 1 - 维度一(收费类型为行) + * 2 - 维度二(房屋类型为行) + * */ + viewType: number; + chargeItemId?: string; + houseUseId?: string; + villageId?: string; +} + +export interface ArrearsListResponse { + code: number; + msg: string; + data: ArrearsVO[]; +} + +export interface ArrearsVO extends ArrearsType1, ArrearsType2 { + 'villageName': string; + 'totalAmount': string; +} + + +export interface ArrearsType1 { + 'chargeItemName'?: string; + 'houseTypeAmounts'?: { + [key: string]: string; + }; +} + +export interface ArrearsType2 { + 'houseTypeName'?: string; + 'chargeItemAmounts'?: { + [key: string]: string; + }; +} + +export interface ArrearsDetailQuery extends PageQuery { + /** + * 客户姓名(模糊查询) + */ + customerName?: string; + /** + * 结束时间 + */ + endTime?: string; + /** + * 房号(模糊查询) + */ + houseNo?: string; + /** + * 联系电话(模糊查询) + */ + phone?: string; + /** + * 开始时间 + */ + startTime?: string; +} +/** + * 业主欠费明细视图对象 + * + * OwnerArrearsDetailVo + */ +export type OwnerArrearsDetailVo = { + /** + * 区域(小区名称) + */ + area?: string; + /** + * 欠费月数 + */ + arrearsMonths?: number; + /** + * 应账账期(如:20170101-20171231) + */ + billPeriod?: string; + /** + * 各收费类型金额(key: 收费类型名称, value: 金额) + */ + chargeItemAmounts?: MapBigDecimal; + /** + * 客户(业主姓名) + */ + customerName?: string; + /** + * 房号 + */ + houseNo?: string; + /** + * 联系电话 + */ + phone?: string; + /** + * 序号 + */ + serialNumber?: number | string; + /** + * 金额合计 + */ + totalAmount?: number; +}; + +/** + * 各收费类型金额(key: 收费类型名称, value: 金额) + * + * MapBigDecimal + */ +export type MapBigDecimal = { + [property: string]: number; +}; diff --git a/src/api/system/residentReviewProcess/type.ts b/src/api/system/residentReviewProcess/type.ts index 10ce8e4..e69de29 100644 --- a/src/api/system/residentReviewProcess/type.ts +++ b/src/api/system/residentReviewProcess/type.ts @@ -1,140 +0,0 @@ -export interface ResidentReviewProcessVO { - /** - * 住户审核表 - */ - id: string | number; - - /** - * 住户表id - */ - residentId: string | number; - - /** - * 提交时间 - */ - submitTime: string; - - /** - * 提交状态 0未提交 1已提交 - */ - submitStatus: number; - - /** - * 物业审核时间 - */ - propertyTime: string; - - /** - * 物业审核状态 0 待审核 1审核通过 2审核未通过 - */ - propertyStatus: number; - - /** - * 物业审核意见 - */ - propertyReviewFeedback: string; - - /** - * 认证时间 - */ - certificationTime: string; - - /** - * 认证状态 0未认证 1已认证 - */ - certificationStatus: number; -} - -export interface ResidentReviewProcessForm extends BaseEntity { - /** - * 住户审核表 - */ - id?: string | number; - - /** - * 住户表id - */ - residentId?: string | number; - - /** - * 提交时间 - */ - submitTime?: string; - - /** - * 提交状态 0未提交 1已提交 - */ - submitStatus?: number; - - /** - * 物业审核时间 - */ - propertyTime?: string; - - /** - * 物业审核状态 0 待审核 1审核通过 2审核未通过 - */ - propertyStatus?: number; - - /** - * 物业审核意见 - */ - propertyReviewFeedback?: string; - - /** - * 认证时间 - */ - certificationTime?: string; - - /** - * 认证状态 0未认证 1已认证 - */ - certificationStatus?: number; -} - -export interface ResidentReviewProcessQuery extends PageQuery { - /** - * 住户表id - */ - residentId?: string | number; - - /** - * 提交时间 - */ - submitTime?: string; - - /** - * 提交状态 0未提交 1已提交 - */ - submitStatus?: number; - - /** - * 物业审核时间 - */ - propertyTime?: string; - - /** - * 物业审核状态 0 待审核 1审核通过 2审核未通过 - */ - propertyStatus?: number; - - /** - * 物业审核意见 - */ - propertyReviewFeedback?: string; - - /** - * 认证时间 - */ - certificationTime?: string; - - /** - * 认证状态 0未认证 1已认证 - */ - certificationStatus?: number; - - /** - * 日期范围参数 - */ - params?: any; -} diff --git a/src/components/Pagination/index.vue b/src/components/Pagination/index.vue index 06a72a8..51e808e 100644 --- a/src/components/Pagination/index.vue +++ b/src/components/Pagination/index.vue @@ -25,7 +25,7 @@ const props = defineProps({ pageSizes: { type: Array, default: () => [10, 20, 30, 50, 100] }, // 移动端页码按钮的数量端默认值5 pagerCount: propTypes.number.def(document.body.clientWidth < 992 ? 5 : 7), - layout: propTypes.string.def('total, sizes, prev, pager, next, jumper'), + layout: propTypes.string.def('sizes, prev, pager, next, jumper,total'), background: propTypes.bool.def(true), autoScroll: propTypes.bool.def(true), hidden: propTypes.bool.def(false), diff --git a/src/utils/request.ts b/src/utils/request.ts index ed291a9..c1aac2d 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -46,7 +46,8 @@ const whiteVillageId = [ '/cash/billList', '/houseRental/changeStatus', // 房屋租赁 - 上架/下架 '/points/pointsProducts/list', // 积分商品列表 - '/points/*' + '/points/*', + '/outstandingFeeSummary/*' // '/building/list' ]; diff --git a/src/utils/xlsx.ts b/src/utils/xlsx.ts new file mode 100644 index 0000000..8f9107a --- /dev/null +++ b/src/utils/xlsx.ts @@ -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(); + + 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; +} diff --git a/src/views/system/report/aging.vue b/src/views/system/report/aging.vue new file mode 100644 index 0000000..bf465e5 --- /dev/null +++ b/src/views/system/report/aging.vue @@ -0,0 +1,241 @@ + + + + + diff --git a/src/views/system/report/agingDetailed.vue b/src/views/system/report/agingDetailed.vue new file mode 100644 index 0000000..0cb0c4c --- /dev/null +++ b/src/views/system/report/agingDetailed.vue @@ -0,0 +1,223 @@ + + + + + diff --git a/src/views/system/report/arrears.vue b/src/views/system/report/arrears.vue new file mode 100644 index 0000000..c5c5f31 --- /dev/null +++ b/src/views/system/report/arrears.vue @@ -0,0 +1,372 @@ + + + + + diff --git a/src/views/system/report/detailed.vue b/src/views/system/report/detailed.vue new file mode 100644 index 0000000..7082399 --- /dev/null +++ b/src/views/system/report/detailed.vue @@ -0,0 +1,356 @@ + +账龄 + + +