docs: add implementation plan for payment-info 缴费明细 dialog

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-10-10 14:17:28 +08:00
1 parent 34c8a1a62d
commit 5025aa09c2
1 file changed
+376
@@ -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
<el-button class="btn-op-item primary" @click="handleOpenDetail(row)">
<el-icon><List /></el-icon>缴费明细
</el-button>
```
- 去掉 `v-if="row.canConfirm"`(始终可点;无 `row.id` 时禁用)。
- 图标从 `@element-plus/icons-vue` 引入 `List`(或 `Tickets`);可移除仅用于旧确认的 `Check`(若单期弹窗也不再需要)。
```typescript
async function handleOpenDetail(row: Record<string, any>) {
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
<SamplePaymentDetailDialog
v-model="detailVisible"
:sample-payment-id="detailSamplePaymentId"
@changed="loadData"
/>
```
删除或停用:
- `PaymentReceiptConfirmDialog` 及相关 `receiptDialogVisible` / `receiptTargetId` / `receiptPreviewRow` / `confirming` / `handleConfirm` / `openConfirmFromPeriod`
- `fetchCreatePaymentTransfer` 若仅被确认路径使用则从 import 移除
单期弹窗 `openPeriodDialog` **保留**只读转账列表;footer 中:
```vue
<!-- 删除 v-if="periodCanConfirm" 的「缴费确认」按钮,仅保留关闭 -->
```
可删除 `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,非当前入口)
- 财务确认待办页