diff --git a/docs/superpowers/plans/2026-10-10-payment-info-detail-list-mode.md b/docs/superpowers/plans/2026-10-10-payment-info-detail-list-mode.md new file mode 100644 index 00000000..d4d4c46a --- /dev/null +++ b/docs/superpowers/plans/2026-10-10-payment-info-detail-list-mode.md @@ -0,0 +1,376 @@ +# 缴费信息「缴费明细」列表模式转账 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:** 新增独立弹窗组件 `SamplePaymentDetailDialog`(摘要 + 各期只读头 + 可编辑转账表),由 `PaymentExcelList` 挂载。转账写操作复用现有 payment-transfers API;补充软删接口供已落库草稿删除。本页移除对 `PaymentReceiptConfirmDialog` 的主路径依赖。 + +**Tech Stack:** Vue 3 + Element Plus(web-admin)、FastAPI + SQLAlchemy(backend)、现有 `protocol-sample-payment` API。 + +**Spec:** `docs/superpowers/specs/2026-10-10-payment-info-detail-list-mode-design.md` +**Demo:** `docs/demos/2026-10-10-payment-confirm-sample-detail-demo.html` + +--- + +## File map + +| 文件 | 职责 | +|------|------| +| `backend/app/crud/protocol_sample_payment.py` | 新增 `soft_delete_payment_transfer`(仅 `dfj`) | +| `backend/app/api/protocol_sample_payment.py` | `DELETE /payment-transfers/{id}` | +| `backend/tests/test_payment_transfer_delete.py`(新建) | 软删用例 | +| `web-admin/src/api/protocol-sample-payment.ts` | `fetchDeletePaymentTransfer` | +| `web-admin/src/components/business/sample-payment-detail-dialog/index.vue`(新建) | 详情弹窗 + 列表模式转账 | +| `web-admin/src/views/cell-storage/payment-info/PaymentExcelList.vue` | 按钮改名、挂载弹窗、去掉旧确认路径与单期弹窗确认按钮 | +| `docs/sql/` | 本迭代无表结构变更 | + +--- + +### Task 1: Backend — soft-delete payment transfer (dfj only) + +**Files:** +- Modify: `backend/app/crud/protocol_sample_payment.py` +- Modify: `backend/app/api/protocol_sample_payment.py` +- Create: `backend/tests/test_payment_transfer_delete.py` + +- [ ] **Step 1: Write failing tests** + +```python +# backend/tests/test_payment_transfer_delete.py +"""软删转账笔:仅 dfj 可删。""" +from unittest.mock import MagicMock +import pytest + +from app.crud import protocol_sample_payment as crud + + +def test_soft_delete_dfj_ok(): + tr = MagicMock() + tr.pay_status = "dfj" + tr.del_flag = 0 + db = MagicMock() + # 需要 crud 暴露 soft_delete_payment_transfer;先写期望调用形态 + # 实现后改为真实函数 + mock get + assert hasattr(crud, "soft_delete_payment_transfer") + + +def test_soft_delete_rejects_jfqrz(): + assert hasattr(crud, "soft_delete_payment_transfer") +``` + +完善为:mock `get_transfer_or_raise` 返回 `jfqrz` 时抛 `ValueError`;`dfj` 时 `del_flag=1`。 + +- [ ] **Step 2: Run tests — expect fail** + +```bash +cd backend && source venv/bin/activate && pytest tests/test_payment_transfer_delete.py -v +``` + +Expected: FAIL(函数不存在) + +- [ ] **Step 3: Implement CRUD** + +在 `protocol_sample_payment.py` 靠近 `get_transfer` / update 处新增: + +```python +def soft_delete_payment_transfer( + db: Session, + *, + transfer_id: str, + tenant_no: str, + user_id: Optional[str], +) -> None: + tr = get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no) + if tr.pay_status != "dfj": + raise ValueError("仅待缴费(已缴费草稿)转账笔可删除") + tr.del_flag = 1 + tr.update_by = user_id + tr.update_time = _now() + db.add(tr) + db.commit() +``` + +(函数名以文件内现有 `get_*_or_raise` 为准;若不存在则复用现有 get + 校验模式。) + +- [ ] **Step 4: Wire DELETE route** + +在 `backend/app/api/protocol_sample_payment.py` 的 withdraw 路由附近: + +```python +@router.delete("/payment-transfers/{transfer_id}") +async def delete_payment_transfer( + transfer_id: str, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + # tenant_no / user_id 取法与同文件其它 transfer 路由一致 + try: + crud.soft_delete_payment_transfer( + db, + transfer_id=transfer_id, + tenant_no=tenant_no, + user_id=user_id, + ) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + return {"code": 0, "message": "ok"} # 或项目统一响应封装 +``` + +对齐同文件 `create_payment_transfer` 的响应与鉴权写法,勿自创响应形状。 + +- [ ] **Step 5: Run tests — expect pass** + +```bash +cd backend && pytest tests/test_payment_transfer_delete.py -v +``` + +Expected: PASS + +- [ ] **Step 6: Commit** + +```bash +git add backend/app/crud/protocol_sample_payment.py backend/app/api/protocol_sample_payment.py backend/tests/test_payment_transfer_delete.py +git commit -m "$(cat <<'EOF' +feat(backend): allow soft-delete of dfj payment transfers + +Supports 缴费明细 list-mode row delete for draft transfers. +EOF +)" +``` + +--- + +### Task 2: Frontend API — fetchDeletePaymentTransfer + +**Files:** +- Modify: `web-admin/src/api/protocol-sample-payment.ts` + +- [ ] **Step 1: Add delete helper after `fetchWithdrawPaymentTransfer`** + +```typescript +export function fetchDeletePaymentTransfer(transferId: string) { + return request.del({ + url: `/web/admin/payment-transfers/${transferId}`, + showSuccessMessage: true + }) +} +``` + +若项目 HTTP 封装是 `request.delete` 而非 `request.del`,与同文件其它 delete(如 `fetchDeleteProtocolRefundRecord`)保持一致。 + +- [ ] **Step 2: Commit** + +```bash +git add web-admin/src/api/protocol-sample-payment.ts +git commit -m "feat(web-admin): add fetchDeletePaymentTransfer API" +``` + +--- + +### Task 3: SamplePaymentDetailDialog component + +**Files:** +- Create: `web-admin/src/components/business/sample-payment-detail-dialog/index.vue` + +- [ ] **Step 1: Scaffold dialog shell** + +Props: + +```typescript +const props = defineProps<{ + modelValue: boolean + samplePaymentId: string +}>() +const emit = defineEmits<{ + (e: 'update:modelValue', v: boolean): void + (e: 'changed'): void // 转账保存/提交/撤回/删除后通知列表刷新 +}>() +``` + +结构: + +- `el-dialog`:`class="dialog-form-layout"`、`width="1080px"`、`align-center`、`append-to-body`、`modal-class="protocol-dialog-mask"`、`destroy-on-close`、`:close-on-click-modal="false"` +- 标题:`样本缴费详情` +- body:`v-loading`;摘要 `form-grid-2` / summary-grid;`应缴批次` 区块列表 +- footer:关闭按钮(Close 图标) + +打开时 `watch(modelValue + samplePaymentId)` → `fetchSamplePaymentDetail`。 + +摘要字段:`protocolNo`、`motherName`/`motherPhone`、`sampleCode`、`payMode` Tag、`contractAmount`、`paidAmount`/`dueRemainAmount`、进度 Tag、`clearedScheduleCount`/`activeScheduleCount`。 + +期头只读文案:第 N 期 · 应缴 · 应缴日 · 已缴 · 欠缴 · `scheduleStatus` Tag。 +`arrearsAmount > 0` 时显示「添加转账」按钮(Plus)。 + +- [ ] **Step 2: Embed transfer table (copy interaction from SamplePaymentPanel)** + +从 `SamplePaymentPanel.vue` 复制并裁剪转账表格相关逻辑(勿整文件引入 Panel): + +- 列:转账方式、金额、时间、凭证上传、备注、财务备注、状态、操作 +- `isTransferLocked`:`yjf` 全锁;`jfqrz` 字段只读但可撤回 +- `addTransfer(schedule)`:本地 push `{ payStatus:'dfj', transferType:'', ... }` +- `saveTransferRow`:`fetchCreatePaymentTransfer` / `fetchUpdatePaymentTransfer` +- `submitTransfer`:`fetchSubmitPaymentTransferFinance` +- `withdrawTransfer`:`fetchWithdrawPaymentTransfer` + `ElMessageBox.confirm` +- `removeTransfer`:无 `id` 本地 splice;有 `id` 且 `dfj` → `fetchDeletePaymentTransfer` 后刷新详情 +- 附件:`fetchUploadAttachment` folder `protocol-transfer`;卡片布局对齐 UI 规范 +- 字典:`loadTransferTypeDict` / `getTransferTypeOptions` +- Tag:`payStatusLabel` / `payStatusTagType` 等从 `@/utils/status-tag-registry` 或 `protocol-payment-labels`(与 Panel 一致) + +每次写成功后:重新 `loadDetail()`,并 `emit('changed')`。 + +样式:可参考 Panel 的 `.schedule-block` / `.transfer-table`,scoped 到本组件;金额用 `AmountInput` + `formatMoneyDisplay`。 + +- [ ] **Step 3: Manual smoke(dev)** + +```bash +cd web-admin && pnpm dev +``` + +用临时挂载或 Task 4 完成后验证:打开弹窗、添加行、保存、提交、撤回、删除草稿。 + +- [ ] **Step 4: Commit** + +```bash +git add web-admin/src/components/business/sample-payment-detail-dialog/index.vue +git commit -m "$(cat <<'EOF' +feat(web-admin): add SamplePaymentDetailDialog with list-mode transfers + +Readonly schedules; transfer CRUD aligned with SamplePaymentPanel. +EOF +)" +``` + +--- + +### Task 4: Wire PaymentExcelList + +**Files:** +- Modify: `web-admin/src/views/cell-storage/payment-info/PaymentExcelList.vue` + +- [ ] **Step 1: Replace action button** + +操作列: + +```vue + + 缴费明细 + +``` + +- 去掉 `v-if="row.canConfirm"`(始终可点;无 `row.id` 时禁用)。 +- 图标从 `@element-plus/icons-vue` 引入 `List`(或 `Tickets`);可移除仅用于旧确认的 `Check`(若单期弹窗也不再需要)。 + +```typescript +async function handleOpenDetail(row: Record) { + const id = String(row.id || '') + if (!id) { + ElMessage.warning('缺少缴费方案,无法查看明细') + return + } + detailSamplePaymentId.value = id + detailVisible.value = true +} +``` + +- [ ] **Step 2: Mount dialog; remove old confirm path** + +模板增加: + +```vue + +``` + +删除或停用: + +- `PaymentReceiptConfirmDialog` 及相关 `receiptDialogVisible` / `receiptTargetId` / `receiptPreviewRow` / `confirming` / `handleConfirm` / `openConfirmFromPeriod` +- `fetchCreatePaymentTransfer` 若仅被确认路径使用则从 import 移除 + +单期弹窗 `openPeriodDialog` **保留**只读转账列表;footer 中: + +```vue + +``` + +可删除 `periodCanConfirm` / `periodSchedule` 中仅服务于确认的状态(若仍用于它处则保留最小集)。 + +- [ ] **Step 3: Smoke on page** + +打开 `/#/market/cell-storage/payment-info`: + +1. 「缴费明细」打开全量详情 +2. 添加转账 → 保存 → 提交财务确认 +3. 列表进度/金额刷新 +4. 点某期实缴金额仍只读,无确认按钮 + +- [ ] **Step 4: Commit** + +```bash +git add web-admin/src/views/cell-storage/payment-info/PaymentExcelList.vue +git commit -m "$(cat <<'EOF' +feat(web-admin): open 缴费明细 dialog from payment-info list + +Rename action button; drop direct PaymentReceiptConfirmDialog path. +EOF +)" +``` + +--- + +### Task 5: Graph update + verification + +**Files:** none required beyond graph script + +- [ ] **Step 1: Update frontend knowledge graph** + +```bash +./scripts/update-graph.sh frontend +``` + +若 Task 1 改了 backend: + +```bash +./scripts/update-graph.sh backend +``` + +- [ ] **Step 2: Run backend tests + frontend typecheck (scoped)** + +```bash +cd backend && pytest tests/test_payment_transfer_delete.py -v +cd web-admin && pnpm exec vue-tsc --noEmit 2>&1 | head -80 +``` + +修复本改动引入的类型错误。 + +- [ ] **Step 3: Final commit if graph/files dirty** + +```bash +git add web-admin/graphify-out backend/graphify-out 2>/dev/null || true +git status +# 若有图谱变更: +git commit -m "chore: update graphify after payment-info detail dialog" +``` + +--- + +## Spec coverage checklist + +| Spec 要求 | Task | +|-----------|------| +| 按钮改名「缴费明细」且始终可开 | Task 4 | +| 样本全量明细、应缴只读 | Task 3 | +| 列表模式转账 + 保存/提交/撤回/删除 | Task 3 + Task 1/2 | +| 去掉操作列旧确认路径 | Task 4 | +| 单期实缴弹窗去确认按钮 | Task 4 | +| 变更后刷新列表 | Task 3 emit + Task 4 `@changed` | +| 不改缴费方案 / 不改造财务确认页 | 无任务(非目标) | + +## Out of scope (do not implement) + +- 改造 `SamplePaymentPanel` 抽公共组件复用(可后续) +- 改造 `PaymentBySampleTab`(旧 Tab,非当前入口) +- 财务确认待办页