Align mobile invoice selection with web-admin at attachment granularity; include interactive HTML demo for the 3-column picker UX. Co-authored-by: Cursor <cursoragent@cursor.com>
5.0 KiB
5.0 KiB
bx-mobile 报销申请:发票选择对齐 Web(附件粒度)
日期:2026-08-09
状态:已确认待实现
范围:bx-mobile 报销申请页;不动后端契约、不动 web-admin
视觉 Demo:mobile-ui/shanxia-mp-bx-reimburse-invoice-picker-demo.html
1. 背景与问题
报销申请「从发票夹选择」在两端粒度不一致:
| 端 | 选择粒度 | 提交 |
|---|---|---|
| web-admin | 附件(invoiceId + attachmentId) |
invoiceLinks 带附件 |
| bx-mobile(现状) | 发票档案(仅 invoiceId) |
invoiceLinks 仅发票 |
领域模型:一次上传创建 1 条发票(金额入账)+ N 个附件文件。Web 已按附件勾选;移动端需对齐。
后端 ReimburseService.normalize_invoice_links / sync_reimburse_invoices 已支持可选 attachment_id;详情 get_linked_invoices_brief 在缺 attachment_id 时回退该发票第一个附件。
2. 目标
- 移动端选择器展示附件(缩略图网格),多附件可分别勾选。
- 保存/提交携带
attachmentId。 - 编辑回显与 Web 一致:有关联附件 ID 则用该 ID;否则用详情接口已回退的首附件。
- 余额扣减规则不变(按报销合计 vs 发票钱包余额)。
3. 非目标
- 不改后端 API / 表结构。
- 不改 web-admin。
- 不抽公共组件(当前仅申请页需要)。
- 不改审批页、列表页;详情页若已有关联展示则保持,本变更不强制改造。
4. 方案选择
采用:页面内改造 ReimburseApplyPage.vue(方案 1)
- 对齐 Demo A:底部 sheet + 按月分组 + 3 列附件缩略图。
- 状态模型从
selectedInvoiceIds: string[]改为selectedInvoicePicks(附件级)。 - 预览加载复用发票夹页已有模式(
attachmentApi.preview)。
不采用:抽公共组件(过重);仅改 payload 保留列表行(与 UI 决策 A 冲突)。
5. 数据流
refreshWallet:
invoiceApi.list → walletInvoices
flatten attachments → attachItems[]
key = `${invoiceId}:${attachmentId}`
amount = 发票金额(整票,非附件拆分)
选择器:
draftKeys ⊆ attachItems.keys → confirm → selectedPicks
提交:
invoiceLinks = selectedPicks.map({ invoiceId, attachmentId })
invoiceIds = unique(invoiceLinks.invoiceId)
编辑加载:
data.invoices[] → 每条取 attachmentId(或 attachment_id)
若无附件信息:用发票列表中该发票 attachments[0]
恢复 selectedPicks(key / 文件名 / 预览)
换申请人:清空 selectedPicks,并刷新钱包与发票列表(与现逻辑一致)。
6. UI 规格(对齐 Demo)
参考:mobile-ui/shanxia-mp-bx-reimburse-invoice-picker-demo.html。
6.1 表单区「关联发票」
- 保留余额提示文案。
- 按钮:从发票夹选择。
- 已选:横向滚动附件卡片(缩略图 / PDF 占位、文件名、整票金额、移除)。
- 提示补充:选择对象为附件文件(可沿用 Demo 文案或等价简短说明)。
6.2 选择器 Sheet
- 标题:选择发票附件。
- 按
createTime月份分组,月内新→旧。 - 3 列网格卡片:序号、选中勾、预览、文件名、整票金额。
- 无附件的发票不出现。
- 底部:确定(已选 N)。
6.3 Token
使用 bx-mobile Fresh Mint(--primary: #00c98d 等),不混用小程序三墙色。
7. 实现要点(文件)
| 文件 | 变更 |
|---|---|
bx-mobile/src/views/reimburse/ReimburseApplyPage.vue |
选择状态、flatten、picker UI、payload、编辑回显、样式 |
mobile-ui/shanxia-mp-bx-reimburse-invoice-picker-demo.html |
已落地的视觉/交互参考(实现对照用) |
可选复用(优先内联,避免过度抽象):
InvoiceWalletMinePage.vue中normalizeInvoiceFiles/ preview kick 的写法可抄到申请页私有函数。
8. 边界与错误
| 场景 | 行为 |
|---|---|
| 发票无任何附件 | 不进入选择器列表 |
| 旧关联仅有 invoiceId | 详情 brief 已带首附件 attachmentId;前端按附件级恢复;若仍缺则本地用该票 attachments[0] |
| 预览失败 | 显示文档/占位,不阻断勾选 |
| 余额不足 | 现有提交拦截不变 |
| 同一发票勾多附件 | 允许;invoiceIds 去重,invoiceLinks 多条 |
9. 验收
- 打开
/reimburse/apply,选择器为 3 列附件网格且按月分组。 - 同一发票多附件可只勾部分;保存后详情/再编辑仍为对应附件。
- 网络请求 body 中
invoiceLinks含attachmentId。 - 仅有历史 invoiceId 关联的单据再编辑时,回显为首附件(与 Web 一致)。
- 余额不足仍无法提交;换申请人清空已选。
10. 测试建议
- 手工:单附件发票、多附件发票、取消勾选、编辑回显、代申请换人。
- 无需新增后端测试(契约已存在)。
- 可选:对 flatten / payload 纯函数若抽出则可单测;本方案内联则手工即可。