fujian_water_biz_doc/docs/superpowers/plans/2026-06-08-arrearage-reminder-frontend.md
tangweijie 3eccab2cf9 docs: 文档治理统一 — AGENTS.md 生命周期规则 + 模块归档 + DDL 修正
1. AGENTS.md 更新
   - water-docs: 新增 specs/ 与 docs/design/ 生命周期规则章节
   - water-backend: 更新协作引用(建设期/建成后、evidence 模块化)

2. specs/ 重复合并
   - 006-reminder-event-design 合并入 003-rev006-reminder-event-design
   - 001-rev004-accounting 删除冗余 data-model.md + contracts/
   - 002-rev005-invoice-flow 删除冗余 data-model.md + contracts/

3. evidence 按模块归档
   - 35 个 REV-004 文件归入 evidence/rev004-accounting/
   - 7 个通用 bugfix 文件归入 evidence/bugfix/ 和 bugfix/frontend/
   - 新建 rev005-invoice/、rev006-reminder/、rev007-statistics/ 目录

4. guides/ 清理
   - 14 个 REV004_*.md 移入 evidence/rev004-accounting/

5. 遗留文件处理
   - docs/research/ 归档到 Archive/06_Migration_Plans/
   - backend-check detached worktrees 清理

6. 交叉引用修复
   - 006-reminder-event-design → 003-rev006-reminder-event-design
   - docs/guides/REV004_ → docs/evidence/rev004-accounting/REV004_

7. DB 设计文档修正(01_Database_Design.md)
   - biz_invoice 明确为开票配置表,非发票记录表
   - 新增 biz_invoice_record 为发票申请/结果主表
   - 新增 biz_charge_invoice_rel 账单-发票关联说明
   - REV-005 承接口径表名全部修正

8. 发票审计证据
   - 新增 evidence/rev005-invoice/2026-06-16-invoice-document-audit.md
2026-06-16 11:47:16 +08:00

627 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 催缴登记 (Arrearage Reminder) 前端对接 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将两个前端页面(欠费催缴池 + 催缴记录)从 mock 数据切换到真实后端 API包括 API 层补齐、催缴表单对接、汇总统计、导出功能。
**Architecture:** 后端 9 个接口均已就绪 (`POST /create`, `GET /page`, `GET /pending-page`, `GET /get`, `GET /detail-list`, `POST /batch-create`, `GET /pending-summary`, `GET /pending-export`, `GET /pending-detail-export`)。前端按 "API 层 → 催缴表单 → 欠费池页面 → 催缴记录页面" 的顺序逐层对接,每层完成后可独立验证。
**Tech Stack:** Vue 3, TypeScript, Element Plus, Axios (`@/config/axios`), pnpm
---
## File Structure
### API layer
- Modify: `../water-frontend/src/api/collectionManage/arrears/index.ts`
- 新增所有请求/响应 VO 类型 + 7 个缺失的 API 方法
### Components
- Modify: `../water-frontend/src/views/collectionManage/arrears/components/RemindForm.vue`
- 对接 `batchCreate` API接收父组件传入的选中行数据
### Views
- Modify: `../water-frontend/src/views/collectionManage/arrears/index.vue`
- 汇总统计条对接 `getPendingSummary`、导出对接 `getPendingExportList`、催缴按钮传递选中数据给 RemindForm
- Modify: `../water-frontend/src/views/collectionManage/collectionRecord/index.vue`
- 列表对接 `getPage`、详情展开对接 `getDetailList`、导出对接后端(如有对应接口)
- 查询表单字段对齐 `ArrearageReminderPageReqVO`
---
### Task 1: 补齐 API 层 — 类型定义与全部接口方法
**Files:**
- Modify: `../water-frontend/src/api/collectionManage/arrears/index.ts`
- [ ] **Step 1: 添加所有 VO 类型定义和 API 方法**
```typescript
import request from '@/config/axios'
import download from '@/utils/download'
// ========== 请求 VO ==========
export interface ArrearagePendingPageReqVO {
pageNo: number
pageSize: number
deptId?: number
code?: string
name?: string
address?: string
mobile?: string
custStates?: number[]
custType?: number
billMonthStart?: number
billMonthEnd?: number
meterReaderId?: number
bookCode?: string
arrearsCountMin?: number
arrearsCountMax?: number
arrearsAmountMin?: number
arrearsAmountMax?: number
remindFilter?: string
}
export interface ArrearageReminderPageReqVO {
pageNo: number
pageSize: number
custId?: number
reminderUser?: string
reminderType?: number
reminderReason?: number
reminderResult?: number
}
export interface ArrearageReminderCreateReqVO {
custId: number
chargeIds: number[]
reminderType: number
reminderReason: number
reminderResult: number
reminderUser: string
reminderTemplate?: number
remark?: string
}
export interface ArrearageReminderBatchCreateReqVO {
customers: {
custId: number
chargeIds: number[]
}[]
reminderType: number
reminderReason: number
reminderResult: number
reminderUser: string
reminderTemplate?: number
remark?: string
}
// ========== 响应 VO ==========
export interface ArrearagePendingPageRespVO {
custId: number
custCode: string
custName: string
custAddress: string
meterCaliber: string
waterNature: string
arrearsCount: number
waterVolume: number
totalAmount: number
penaltyAmount: number
receivableAmount: number
billAmount: number
accountMonthRange: string
custStatus: string
prestoreAmount: number
mobile: string
isRemindedThisMonth: boolean
agreementNo: string
contractNo: string
details: ArrearagePendingChargeDetailRespVO[]
}
export interface ArrearagePendingChargeDetailRespVO {
chargeId: number
billMonth: string
lastReading: number
currentReading: number
waterVolume: number
billAmount: number
penaltyAmount: number
readingDate: string
}
export interface ArrearagePendingSummaryRespVO {
custCount: number
arrearsCount: number
waterVolume: number
billAmount: number
penaltyAmount: number
totalAmount: number
receivableAmount: number
}
export interface ArrearageReminderPageRespVO {
id: number
custId: number
custCode: string
custName: string
reminderType: number
reminderReason: number
reminderUser: string
remark: string
reminderTemplate: number
completeTime: string
pushState: number
pushResults: string
reminderResult: number
batchStamp: string
totalBillWater: number
totalExtendedAmount: number
totalLateFee: number
deposit: number
mobile: string
createTime: string
updateTime: string
}
export interface ArrearageReminderRespVO extends ArrearageReminderPageRespVO {}
export interface ArrearageReminderDetailRespVO {
id: number
arrearageReminderId: number
chargeId: number
lateFee: number
createTime: string
updateTime: string
}
export interface ArrearageReminderCreateRespVO {
id: number
detailCount: number
}
export interface ArrearageReminderBatchCreateRespVO {
successCount: number
failCount: number
successItems: { custId: number; reminderId: number; detailCount: number }[]
failItems: { custId: number; reason: string }[]
}
// ========== API 方法 ==========
export const ArrearsApi = {
// 待催欠费池
getPendingPage: async (params: ArrearagePendingPageReqVO) => {
return await request.get<{ list: ArrearagePendingPageRespVO[]; total: number }>({
url: '/business/arrearage-reminder/pending-page',
params
})
},
getPendingSummary: async (params: ArrearagePendingPageReqVO) => {
return await request.get<ArrearagePendingSummaryRespVO>({
url: '/business/arrearage-reminder/pending-summary',
params
})
},
// 催缴登记 CRUD
create: async (data: ArrearageReminderCreateReqVO) => {
return await request.post<ArrearageReminderCreateRespVO>({
url: '/business/arrearage-reminder/create',
data
})
},
batchCreate: async (data: ArrearageReminderBatchCreateReqVO) => {
return await request.post<ArrearageReminderBatchCreateRespVO>({
url: '/business/arrearage-reminder/batch-create',
data
})
},
getPage: async (params: ArrearageReminderPageReqVO) => {
return await request.get<{ list: ArrearageReminderPageRespVO[]; total: number }>({
url: '/business/arrearage-reminder/page',
params
})
},
get: async (id: number) => {
return await request.get<ArrearageReminderRespVO>({
url: '/business/arrearage-reminder/get',
params: { id }
})
},
getDetailList: async (reminderId: number) => {
return await request.get<ArrearageReminderDetailRespVO[]>({
url: '/business/arrearage-reminder/detail-list',
params: { reminderId }
})
},
// 导出
exportPending: async (params: ArrearagePendingPageReqVO) => {
const res = await request.download({
url: '/business/arrearage-reminder/pending-export',
params
})
download.response(res, '待催欠费客户.xlsx')
},
exportPendingDetail: async (params: ArrearagePendingPageReqVO) => {
const res = await request.download({
url: '/business/arrearage-reminder/pending-detail-export',
params
})
download.response(res, '待催欠费明细.xlsx')
}
}
```
- [ ] **Step 2: 验证 API 层编译**
Run: `cd ../water-frontend && npx vue-tsc --noEmit src/api/collectionManage/arrears/index.ts 2>&1 | head -20`
Expected: No type errors
---
### Task 2: RemindForm 对接 batchCreate API
**Files:**
- Modify: `../water-frontend/src/views/collectionManage/arrears/components/RemindForm.vue`
- [ ] **Step 1: 重写 RemindForm 脚本,接收选中行 props 并调 batchCreate**
`<script setup lang="ts">` 段替换为:
```typescript
import { ArrearsApi } from '@/api/collectionManage/arrears'
const message = useMessage()
const dialogVisible = ref(false)
const formRef = ref()
const props = defineProps<{
selectedRows: any[]
}>()
// reminderType 映射: '催缴单'->1, '短信'->2, '电话'->3, '微信'->4, '其它'->5, '热线'->6
const methodMap: Record<string, number> = {
'催缴单': 1, '短信': 2, '电话': 3, '微信': 4, '其它': 5, '热线': 6
}
const formData = ref({
method: '催缴单',
operator: '',
reason: '',
remark: ''
})
const rules = {
method: [{ required: true, message: '请选择催缴方式', trigger: 'change' }],
operator: [{ required: true, message: '请选择催缴人员', trigger: 'change' }],
reason: [{ required: true, message: '请选择催缴原因', trigger: 'change' }]
}
const open = () => {
dialogVisible.value = true
formData.value = {
method: '催缴单',
operator: '',
reason: '',
remark: ''
}
}
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return
try {
const customers = props.selectedRows.map((row: any) => ({
custId: row.custId,
chargeIds: row.details?.map((d: any) => d.chargeId) || []
}))
const res = await ArrearsApi.batchCreate({
customers,
reminderType: methodMap[formData.value.method] || 1,
reminderReason: Number(formData.value.reason),
reminderResult: 0,
reminderUser: formData.value.operator,
remark: formData.value.remark
})
if (res.failCount > 0) {
const failMsgs = res.failItems.map((f: any) => `客户${f.custId}: ${f.reason}`).join('; ')
message.warning(`成功 ${res.successCount} 条,失败 ${res.failCount} 条。${failMsgs}`)
} else {
message.success(`成功创建 ${res.successCount} 条催缴记录`)
}
dialogVisible.value = false
emit('success')
} catch (e: any) {
message.error(e?.message || '催缴失败')
}
})
}
const emit = defineEmits(['success'])
defineExpose({ open })
```
- [ ] **Step 2: 修改父组件传参 — 更新 arrears/index.vue 中 RemindForm 的使用**
`../water-frontend/src/views/collectionManage/arrears/index.vue` 底部模板中:
```
<!-- 修改前 -->
<RemindForm ref="remindFormRef" @success="handleSuccess" />
<!-- 修改后 -->
<RemindForm ref="remindFormRef" :selected-rows="selectedRows" @success="handleSuccess" />
```
- [ ] **Step 3: 验证编译**
Run: `cd ../water-frontend && npx vue-tsc --noEmit 2>&1 | tail -20`
Expected: No type errors (may have pre-existing errors in other files, check only arrearage-related)
---
### Task 3: 欠费催缴池页面 — 汇总统计 + 导出 + 全选催缴
**Files:**
- Modify: `../water-frontend/src/views/collectionManage/arrears/index.vue`
- [ ] **Step 1: 添加汇总统计数据获取**
`getList` 调用后追加 summary 请求。找到 `script setup` 中的 `getList` 函数(约 line 629在其后添加
```typescript
const summary = ref<{
custCount: number
arrearsCount: number
waterVolume: number
billAmount: number
penaltyAmount: number
totalAmount: number
receivableAmount: number
}>({
custCount: 0, arrearsCount: 0, waterVolume: 0,
billAmount: 0, penaltyAmount: 0, totalAmount: 0, receivableAmount: 0
})
const fetchSummary = async () => {
try {
summary.value = await ArrearsApi.getPendingSummary(buildPageParams())
} catch {
// summary stays at zero
}
}
```
修改 `handleQuery``onMounted` 中的 `getList()` 调用为并行:
```typescript
const handleQuery = () => {
queryParams.pageNo = 1
Promise.all([getList(), fetchSummary()])
}
```
并在 `onMounted` 中:
```typescript
onMounted(async () => {
await Promise.resolve(getSiteTree())
await Promise.all([getList(), fetchSummary()])
})
```
- [ ] **Step 2: 替换模板中的硬编码统计值为真实数据**
找到 `<el-descriptions>` 区域(约 line 211-226替换其中各 `el-descriptions-item` 的值为 `summary` 响应式数据:
```html
<el-descriptions :column="5" class="mt-15px mb-8px" border label-width="120">
<el-descriptions-item label="客户数">
<span class="text-orange-500 font-bold">{{ summary.custCount }}</span>
</el-descriptions-item>
<el-descriptions-item label="欠费笔数">
<span class="text-orange-500 font-bold">{{ summary.arrearsCount }}</span>
</el-descriptions-item>
<el-descriptions-item label="用水量">
<span class="text-orange-500 font-bold">{{ summary.waterVolume }}</span>
</el-descriptions-item>
<el-descriptions-item label="合计金额">
<span class="text-orange-500 font-bold">¥{{ summary.totalAmount }}</span>
</el-descriptions-item>
<el-descriptions-item label="账单金额">
<span class="text-orange-500 font-bold">¥{{ summary.billAmount }}</span>
</el-descriptions-item>
<el-descriptions-item label="违约金">
<span class="text-orange-500 font-bold">¥{{ summary.penaltyAmount }}</span>
</el-descriptions-item>
<el-descriptions-item label="应缴金额">
<span class="text-orange-500 font-bold">¥{{ summary.receivableAmount }}</span>
</el-descriptions-item>
</el-descriptions>
```
- [ ] **Step 3: 导出对接真实 API**
找到 `handleExport` 函数(约 line 663替换为
```typescript
const handleExport = async () => {
try {
message.loading('正在导出...')
await ArrearsApi.exportPending(buildPageParams())
message.success('导出成功')
} catch {
message.error('导出失败')
}
}
```
- [ ] **Step 4: 全部催缴传参**
找到 `handleRemindAll`(约 line 657当前直接 `remindFormRef.value.open()`,需要传入当前列表中所有行的数据。修改为:
```typescript
const handleRemindAll = () => {
selectedRows.value = list.value
remindFormRef.value.open()
}
```
- [ ] **Step 5: 从 API 导入汇总相关方法**
`arrears/index.vue` 的 import 中,`ArrearsApi` 的导入行保持不变(当前已导入),新增 `summary` ref 定义的位置放到 `selectedRows` 附近。
- [ ] **Step 6: 验证编译**
Run: `cd ../water-frontend && npx vue-tsc --noEmit 2>&1 | tail -20`
Expected: No new type errors from arrears files
---
### Task 4: 催缴记录页面 — 列表与详情对接
**Files:**
- Modify: `../water-frontend/src/views/collectionManage/collectionRecord/index.vue`
- [ ] **Step 1: 添加 API 导入**
在 script setup 顶部 import 中加入:
```typescript
import { ArrearsApi } from '@/api/collectionManage/arrears'
```
- [ ] **Step 2: 添加详情数据 ref 和获取方法**
`list` ref 附近(约 line 294追加
```typescript
const detailMap = ref<Record<number, any[]>>({})
const fetchDetailList = async (reminderId: number) => {
if (detailMap.value[reminderId]) return
try {
const details = await ArrearsApi.getDetailList(reminderId)
detailMap.value[reminderId] = details
} catch {
detailMap.value[reminderId] = []
}
}
```
- [ ] **Step 3: 替换 getList 为真实 API 调用**
找到现有的 getList 函数(当前为 mock 数据),替换为:
```typescript
const getList = async () => {
loading.value = true
try {
const data = await ArrearsApi.getPage({
pageNo: queryParams.pageNo,
pageSize: queryParams.pageSize,
custId: undefined, // 后续可扩展
reminderUser: queryParams.remindUser || undefined,
reminderType: undefined,
reminderReason: queryParams.remindReason ? Number(queryParams.remindReason) : undefined,
reminderResult: queryParams.remindResult ? Number(queryParams.remindResult) : undefined
})
list.value = (data?.list || []).map((item: any) => ({
...item,
id: item.id,
deptName: item.deptName || '-',
custCode: item.custCode,
custName: item.custName,
custAddress: item.custAddress || '-',
remindUser: item.reminderUser,
remindMethod: item.reminderType,
remindReason: item.reminderReason,
mobile: item.mobile,
arrearsCount: 0, // reminder records don't carry count — fetched via details
arrearsWaterVolume: item.totalBillWater,
arrearsAmount: item.totalExtendedAmount,
penaltyAmount: item.totalLateFee,
prestoreBalance: item.deposit,
completeDate: item.completeTime,
registerDate: item.createTime,
remark: item.remark,
expanded: false,
pushStatus: item.pushState,
details: []
}))
total.value = data?.total || 0
} finally {
loading.value = false
}
}
```
- [ ] **Step 4: 展开行对接详情**
找到模板中的 expand slot当前显示 mock 数据 `detailsMock1`),替换为绑定 `detailMap`
```html
<template #expand="scope">
<div class="p-20px bg-gray-50">
<el-table
:data="detailMap[scope.row.id] || []"
border
style="width: 100%"
v-loading="!detailMap[scope.row.id]"
>
<el-table-column prop="chargeId" label="账单ID" width="120" />
<el-table-column prop="lateFee" label="违约金" width="120" />
<el-table-column prop="createTime" label="创建时间" width="180" />
</el-table>
</div>
</template>
```
- [ ] **Step 5: 展开事件触发详情加载**
`handleExpandChange` 中调用 `fetchDetailList`
```typescript
const handleExpandChange = (row: any, expandedRows: any[]) => {
row.expanded = expandedRows.includes(row)
if (row.expanded) {
fetchDetailList(row.id)
}
}
```
- [ ] **Step 6: 删除 mock 数据**
删除文件中的 `detailsMock1` 等 mock 数据声明。
- [ ] **Step 7: 验证编译**
Run: `cd ../water-frontend && npx vue-tsc --noEmit 2>&1 | tail -20`
Expected: No new type errors
---
## Verification Checklist
完成所有 4 个 Task 后,执行端到端 smoke
- [ ] `npx vue-tsc --noEmit` 编译通过
- [ ] 欠费催缴池页面:汇总统计数字不再是硬编码
- [ ] 欠费催缴池页面:点击"导出"触发 `GET /pending-export` 下载 xlsx
- [ ] 欠费催缴池页面:勾选客户 → 点击"选中催缴" → 弹窗填写 → 提交 → 调 `POST /batch-create`
- [ ] 欠费催缴池页面:点击"全部催缴"将当前列表全部行传入 RemindForm
- [ ] 催缴记录页面:列表从 `GET /page` 加载,查询条件生效
- [ ] 催缴记录页面:展开行从 `GET /detail-list` 加载账单明细