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

18 KiB
Raw Blame History

催缴登记 (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 方法

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"> 段替换为:

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在其后添加

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
  }
}

修改 handleQueryonMounted 中的 getList() 调用为并行:

const handleQuery = () => {
  queryParams.pageNo = 1
  Promise.all([getList(), fetchSummary()])
}

并在 onMounted 中:

onMounted(async () => {
  await Promise.resolve(getSiteTree())
  await Promise.all([getList(), fetchSummary()])
})
  • Step 2: 替换模板中的硬编码统计值为真实数据

找到 <el-descriptions> 区域(约 line 211-226替换其中各 el-descriptions-item 的值为 summary 响应式数据:

<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替换为

const handleExport = async () => {
  try {
    message.loading('正在导出...')
    await ArrearsApi.exportPending(buildPageParams())
    message.success('导出成功')
  } catch {
    message.error('导出失败')
  }
}
  • Step 4: 全部催缴传参

找到 handleRemindAll(约 line 657当前直接 remindFormRef.value.open(),需要传入当前列表中所有行的数据。修改为:

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 中加入:

import { ArrearsApi } from '@/api/collectionManage/arrears'
  • Step 2: 添加详情数据 ref 和获取方法

list ref 附近(约 line 294追加

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 数据),替换为:

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

<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

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 加载账单明细