# Counter Topup Detail 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:** 修复柜台收费预存款记录点击详情后全部显示 `--` 的问题,为预存款提供独立且语义正确的详情展示。 **Architecture:** 前端根据展示行的 `displayChargeType` 分流详情链路。普通账单继续查询 `/business/charge/get`;预存款直接使用支付记录快照与当前客户资料打开专用详情弹窗,不把支付记录 ID 当作账单 ID。 **Tech Stack:** Vue 3、TypeScript、Element Plus、现有 Axios API 封装、Node.js `node:test` --- ## File Structure - Modify: `water-frontend/src/views/operatingCharges/counterCharging/index.vue` - 扩充预存展示行字段、映射支付余额快照、详情类型分流、增加预存款专用弹窗。 - Modify: `water-frontend/tests/operatingCharges/counterChargingPersistentTopup.test.mjs` - 增加预存款详情分流、字段展示、余额映射和普通账单链路不回归的契约测试。 - Create: `water-docs/docs/evidence/bugfix/2026-07-14-counter-topup-detail.md` - 记录根因、RED/GREEN 结果、构建结果与未运行 `vue-tsc` 的约束。 ### Task 1: Add failing topup-detail contract tests **Files:** - Modify: `water-frontend/tests/operatingCharges/counterChargingPersistentTopup.test.mjs` - [ ] **Step 1: Add a failing test for detail routing** 在现有测试文件中增加源码契约断言,要求 `openChargeDetail` 对 `displayChargeType === 'topup'` 分流,并且仅普通账单调用 `getChargeById`: ```js test('counter charging routes topup rows to a dedicated payment detail', () => { assert.match( source, /if \(row\.displayChargeType === 'topup'\) \{[\s\S]*openTopupDetail\(row\)[\s\S]*return[\s\S]*getChargeById\(row\.id\)/ ) }) ``` - [ ] **Step 2: Add a failing test for dedicated fields and balance snapshots** ```js test('topup detail shows payment semantics and keeps balance snapshots', () => { for (const label of ['预存款详情', '业务类型', '支付记录ID', '预存金额', '期初余额', '期末余额', '收费时间', '收费方式', '收费状态']) { assert.match(source, new RegExp(label)) } assert.match(source, /lastDeposit:\s*record\.lastDeposit/) assert.match(source, /deposit:\s*record\.deposit/) assert.match(source, /displayTopupMoney\(topupDetailData\?\.lastDeposit\)/) assert.match(source, /displayTopupMoney\(topupDetailData\?\.deposit\)/) }) ``` - [ ] **Step 3: Add a failing test for preserving normal bill details and zero values** ```js test('normal bills keep their original detail request and zero topup values remain visible', () => { assert.match(source, /chargeDetailData\.value = await getChargeById\(row\.id\)/) assert.match(source, /value === null \|\| value === undefined \|\| value === ''/) assert.match(source, /Number\(value\)\.toFixed\(2\)/) }) ``` - [ ] **Step 4: Run the focused test and verify RED** Run: ```bash node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs ``` Expected: FAIL on the new detail-routing/detail-fields assertions because no topup-specific detail path exists yet. - [ ] **Step 5: Commit the RED test** ```bash git add tests/operatingCharges/counterChargingPersistentTopup.test.mjs git commit -m "test: reproduce empty counter topup detail" ``` ### Task 2: Preserve complete topup display data **Files:** - Modify: `water-frontend/src/views/operatingCharges/counterCharging/index.vue` - [ ] **Step 1: Extend the display-row type with payment snapshots** ```ts type ChargeDisplayRowVO = CounterChargingChargeVO & { displayChargeState?: 'settled' | 'history' displayChargeType?: 'bill' | 'topup' payTime?: string chargeWay?: number lastDeposit?: number deposit?: number } ``` - [ ] **Step 2: Extend the topup-row builder input and output** ```ts const setSettledTopupDisplayRow = (record: { id: number amount: number payTime: string chargeWay: number lastDeposit?: number deposit?: number }, displayChargeState: 'settled' | 'history' = 'settled') => { const current = currentCustomer.value settledChargeDisplayRows.value = [ { id: record.id, meterId: 0, recordId: 0, billMonth: '预存款', custId: current?.id || 0, custCode: current?.custCode || current?.code, custName: current?.custName || current?.name, custAddress: current?.custAddress || current?.address, billAmount: record.amount, lateFee: 0, extendedAmount: record.amount, payState: 1, payStateName: '收讫', meterCode: '预存', recordNo: '预存', payTime: record.payTime, chargeWay: record.chargeWay, lastDeposit: record.lastDeposit, deposit: record.deposit, displayChargeState, displayChargeType: 'topup' } as ChargeDisplayRowVO ] } ``` 客户地址使用 `current?.custAddress || current?.address`;金额继续放在 `billAmount` 和 `extendedAmount`,余额快照分别放入 `lastDeposit`、`deposit`。 - [ ] **Step 3: Map persisted payment snapshots** 在 `loadLatestCounterTopupDisplay` 中传入: ```ts setSettledTopupDisplayRow({ id: record.id, amount: Number(record.actualMoney ?? 0), payTime: record.payDate || record.creationTime || '', chargeWay: Number(record.chargeWay ?? 1), lastDeposit: record.lastDeposit, deposit: record.deposit }, 'history') ``` - [ ] **Step 4: Avoid overwriting the persisted row after an immediate topup** 收费成功刷新后,只有在 `loadCustomerChargeData` 未恢复任何预存行时才用成功响应建立降级行: ```ts if (!settledChargeDisplayRows.value.length && counterTopupResult?.paymentRecordId) { setSettledTopupDisplayRow({ id: counterTopupResult.paymentRecordId, amount: Number(counterTopupResult.amount ?? enteredAmount.value), payTime: counterTopupResult.payTime || payDate, chargeWay: chargeWayKeyToValueMap[payType.value as keyof typeof chargeWayKeyToValueMap] || 1, deposit: counterTopupResult.balanceAfter }) } ``` - [ ] **Step 5: Run the focused test** Run: ```bash node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs ``` Expected: detail-routing test remains FAIL; balance-mapping assertions PASS. - [ ] **Step 6: Commit the data mapping** ```bash git add src/views/operatingCharges/counterCharging/index.vue tests/operatingCharges/counterChargingPersistentTopup.test.mjs git commit -m "fix: preserve counter topup detail snapshots" ``` ### Task 3: Add the dedicated topup-detail dialog and routing **Files:** - Modify: `water-frontend/src/views/operatingCharges/counterCharging/index.vue` - [ ] **Step 1: Add topup-detail state and zero-safe formatting** ```ts const showTopupDetailDialog = ref(false) const topupDetailData = ref(null) const displayTopupMoney = (value: unknown) => { if (value === null || value === undefined || value === '') return '--' const amount = Number(value) return Number.isFinite(amount) ? amount.toFixed(2) : '--' } const getChargeWayLabel = (value: unknown) => { const chargeWay = Number(value) return chargeWayLabelMap.value[chargeWay] || '--' } ``` - [ ] **Step 2: Add the dedicated dialog** 在账单详情弹窗后增加以下完整弹窗: ```vue {{ topupDetailData?.custCode || '--' }} {{ topupDetailData?.custName || '--' }} {{ topupDetailData?.custAddress || '--' }} 预存款 {{ topupDetailData?.id || '--' }} {{ displayTopupMoney(topupDetailData?.billAmount) }} {{ displayTopupMoney(topupDetailData?.lastDeposit) }} {{ displayTopupMoney(topupDetailData?.deposit) }} {{ formatChargeDateTime(topupDetailData?.payTime) }} {{ getChargeWayLabel(topupDetailData?.chargeWay) }} 收讫 ``` - [ ] **Step 3: Route topup rows before the bill request** ```ts const openTopupDetail = (row: ChargeDisplayRowVO) => { if (!row.id) { ElMessage.warning('预存记录信息不完整') return } topupDetailData.value = row showTopupDetailDialog.value = true } const openChargeDetail = async (row: ChargeDisplayRowVO) => { if (row.displayChargeType === 'topup') { openTopupDetail(row) return } if (!row?.id) return chargeDetailLoading.value = true showChargeDetailDialog.value = true try { chargeDetailData.value = await getChargeById(row.id) } catch (error: any) { ElMessage.error(error?.message || '查看账单详情失败') showChargeDetailDialog.value = false } finally { chargeDetailLoading.value = false } } ``` - [ ] **Step 4: Clear topup-detail state with the page state** 在 `resetCustomerChargeState` 与 `onPageActivated` 的无恢复参数分支中加入: ```ts showTopupDetailDialog.value = false topupDetailData.value = null ``` - [ ] **Step 5: Run the focused test and verify GREEN** Run: ```bash node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs ``` Expected: PASS, including all pre-existing topup persistence tests and new detail tests. - [ ] **Step 6: Run the existing counter-charging regression test** Run: ```bash pnpm test:counter-charging ``` Expected: PASS. - [ ] **Step 7: Commit the UI fix** ```bash git add src/views/operatingCharges/counterCharging/index.vue tests/operatingCharges/counterChargingPersistentTopup.test.mjs git commit -m "fix: show counter topup payment details" ``` ### Task 4: Verify and record evidence **Files:** - Create: `water-docs/docs/evidence/bugfix/2026-07-14-counter-topup-detail.md` - [ ] **Step 1: Run focused tests together** Run: ```bash node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs tests/operatingCharges/counterChargingInventory.test.mjs ``` Expected: all focused tests PASS. - [ ] **Step 2: Run the frontend build with sufficient heap** Run: ```bash NODE_OPTIONS=--max-old-space-size=8192 pnpm build:dev ``` Expected: `Build successful. Please see dist directory`. Do not run `vue-tsc`, `pnpm ts:check`, or any command that invokes `vue-tsc`. - [ ] **Step 3: Check the diff and worktree** Run: ```bash git diff --check git status --short ``` Expected: no whitespace errors; only intentional files are present before the final commit. - [ ] **Step 4: Write verification evidence** 全部验证达到本计划预期后,创建以下证据;若实际结果不同,停止提交证据并先处理差异: ```markdown # 柜台收费预存款详情修复验证 ## 根因 预存款展示行使用支付记录 ID,但详情按钮统一调用账单详情接口 `/business/charge/get`,导致支付记录 ID 被误作账单 ID,详情字段全部显示为 `--`。 ## 修复 - 按 `displayChargeType` 分流普通账单与预存款详情。 - 预存款使用专用弹窗展示支付及余额快照。 - 普通账单继续使用原账单详情接口。 ## TDD 证据 - RED:`node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs` - 结果:7 个测试中原有 4 个通过,新增 3 个按预期失败,失败原因分别为缺少详情分流、专用字段和零值格式化。 - GREEN:`node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs` - 结果:7/7 通过。 ## 回归验证 - `pnpm test:counter-charging`:退出码 0,4/4 通过。 - `node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs tests/operatingCharges/counterChargingInventory.test.mjs`:退出码 0,11/11 通过。 - `NODE_OPTIONS=--max-old-space-size=8192 pnpm build:dev`:退出码 0,输出 `Build successful. Please see dist directory`。 ## 约束 按用户要求,未运行 `vue-tsc`、`pnpm ts:check` 或任何会调用 `vue-tsc` 的命令。 ``` - [ ] **Step 5: Commit evidence** ```bash git add docs/evidence/bugfix/2026-07-14-counter-topup-detail.md git commit -m "docs: record counter topup detail verification" ```