This commit is contained in:
zlwu committed 2026-05-25 17:43:49 +08:00
1 parent 771f010a39
commit ee4abd361e
102 files changed
+8021 -4094

No files matched your search

@@ -0,0 +1,15 @@
-- 渠道服务订单操作流水:财务确认人、作废人、作废时间
ALTER TABLE `biz_channel_service_order_txn`
ADD COLUMN `finance_confirm_by` varchar(64) DEFAULT NULL COMMENT '财务确认人ID' AFTER `finance_confirm_time`,
ADD COLUMN `void_by` varchar(64) DEFAULT NULL COMMENT '作废人ID' AFTER `finance_remark`,
ADD COLUMN `void_time` datetime DEFAULT NULL COMMENT '作废时间' AFTER `void_by`;
-- 历史已确认流水:用 update_by 回填确认人
UPDATE `biz_channel_service_order_txn`
SET `finance_confirm_by` = `update_by`
WHERE `txn_status` = 'yqr' AND `finance_confirm_time` IS NOT NULL AND `finance_confirm_by` IS NULL;
-- 历史已作废流水:用 update_by / update_time 回填(仅曾提交财务的流水视为财务作废)
UPDATE `biz_channel_service_order_txn`
SET `void_by` = `update_by`, `void_time` = `update_time`
WHERE `txn_status` = 'yzf' AND `finance_submit_time` IS NOT NULL AND `void_by` IS NULL;
+3
View File
@@ -1035,8 +1035,11 @@ CREATE TABLE `biz_channel_service_order_txn` (
`finance_submit_remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '发起确认备注',
`finance_submit_attachments` text COLLATE utf8mb4_unicode_ci COMMENT '发起确认附件',
`finance_confirm_time` datetime DEFAULT NULL COMMENT '财务确认时间',
`finance_confirm_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '财务确认人ID',
`finance_confirm_attachments` text COLLATE utf8mb4_unicode_ci COMMENT '财务确认附件',
`finance_remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '财务确认备注',
`void_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '作废人ID',
`void_time` datetime DEFAULT NULL COMMENT '作废时间',
`balance_before` decimal(18,2) DEFAULT NULL COMMENT '操作前余额(yfk类型)',
`balance_after` decimal(18,2) DEFAULT NULL COMMENT '操作后余额(yfk类型)',
`tenant_no` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
@@ -0,0 +1,176 @@
# 金额/单价千分位输入与展示 — 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:** 在 web-admin 统一金额/单价的「聚焦纯数字、失焦千分位、提交数值」交互,并统一只读列表/详情的千分位展示。
**Architecture:** 抽取 `money-format.ts` 纯函数 + `AmountInput` 封装 `el-input`;业务页替换金额类 `el-input-number` 与局部重复逻辑;只读处使用 `formatMoneyDisplay`。
**Tech Stack:** Vue 3、Element Plus、TypeScript、现有 Prettier/ESLint 规范。
**Design spec:** `docs/superpowers/specs/2026-05-25-money-input-thousand-separator-design.md`
---
## File map
| 操作 | 路径 |
|------|------|
| Create | `web-admin/src/utils/money-format.ts` |
| Create | `web-admin/src/components/common/amount-input/index.vue` |
| Modify | `web-admin/src/views/pilot-service/channel-service-order/index.vue` |
| Modify | `web-admin/src/views/pilot-service/channel-service-order-finance/index.vue` |
| Modify | `openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md` |
| Later P1+ | `channel/index.vue`, `cell-product-price/index.vue`, `project-price-system/index.vue`, `ProtocolFormDialog.vue`, `payment-info/index.vue`, … |
---
### Task 1: 金额格式化工具
**Files:**
- Create: `web-admin/src/utils/money-format.ts`
- [ ] **Step 1: 实现三个导出函数**
```typescript
export function addThousandSeparators(intStr: string): string { /* 字符串分段,无前瞻正则 */ }
export function sanitizeMoneyInput(value: string, precision = 2): string { /* 去逗号、单小数点、截断小数 */ }
export function formatMoneyDisplay(value: string | number | null | undefined, precision = 2): string
export function parseMoneyToNumber(value: string, precision = 2): number | null
```
- [ ] **Step 2: 本地快速验证**
在 Node 或浏览器控制台执行:
```javascript
formatMoneyDisplay(1234567.89) // '1,234,567.89'
parseMoneyToNumber('1,234.56') // 1234.56
```
---
### Task 2: AmountInput 组件
**Files:**
- Create: `web-admin/src/components/common/amount-input/index.vue`
- [ ] **Step 1: 实现组件**
- `defineModel<number | null>()` 或 `modelValue` + `emit('update:modelValue')`
- 内部 `displayText`,`watch(modelValue)` 在非聚焦时同步为 `formatMoneyDisplay`
- `@focus` → sanitize 展示
- `@input` → `sanitizeMoneyInput` 写回 `displayText`
- `@blur` → `parseMoneyToNumber` → emit;`displayText = formatMoneyDisplay`
- Props: `min`, `max`, `precision`, `placeholder`, `disabled`, `clearable`
- [ ] **Step 2: 在渠道服务订单页面临时引用验证**
仅 import 一个弹窗字段 smoke test(Task 3 会全量替换)。
---
### Task 3: 渠道服务订单页(P0)
**Files:**
- Modify: `web-admin/src/views/pilot-service/channel-service-order/index.vue`
- [ ] **Step 1: 删除页面内重复函数**
移除 `addThousandSeparators`、`formatRechargeAmountDisplay`、`sanitizeRechargeAmountInput`、`parseRechargeAmountInput`、`parseAmountInputToNumber`(若仅用于充值)、`rechargeAmountInput` 及相关 handler。
- [ ] **Step 2: 替换可编辑金额**
| 位置 | 替换为 |
|------|--------|
| 发起充值 | `<AmountInput v-model="rechargeForm.amount" :min="0.01" />` |
| 申请退款 | `<AmountInput v-model="refundTxnForm.amount" :min="0.01" :max="currentBalance" />` |
| 新增/编辑 产品明细单价 | `<AmountInput v-model="row.unitPrice" :min="0" @change="calcItemAmount(row)" />` |
- [ ] **Step 3: 只读展示**
将列表/查看/合计中的 `formatAmount`(金额类)改为 `formatMoneyDisplay`,或:
```typescript
const formatAmount = (v: any) => formatMoneyDisplay(v, 2)
```
仅当该文件内 `formatAmount` 全部用于金额时方可整文件替换。
- [ ] **Step 4: 手工验收**
按 spec 第 6 节 5 条检查;提交充值/退款 payload 为 number。
---
### Task 4: 财务确认页只读列(P1 片段)
**Files:**
- Modify: `web-admin/src/views/pilot-service/channel-service-order-finance/index.vue`
- [ ] **Step 1: `formatAmount` → `formatMoneyDisplay`**
列表金额、详情单价/金额、确认弹窗中的金额文案。
---
### Task 5: UI 规范 OpenSpec
**Files:**
- Modify: `openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md`
- [ ] **Step 1: 新增 Requirement「金额与单价千分位」**
SHALL 行为表 + `AmountInput` + `formatMoneyDisplay` 路径说明。
---
### Task 6: P1 中试服务其余页(可并行子任务)
**Files:**
- Modify: `web-admin/src/views/pilot-service/channel/index.vue`(协议价行内)
- Modify: `web-admin/src/views/pilot-service/cell-product-price/index.vue`
- Modify: `web-admin/src/views/pilot-service/project-price-system/index.vue`
- Modify: `web-admin/src/views/pilot-service/checkup-institution/index.vue`、`CheckupPackagePanel.vue`
每文件重复 Task 3 的替换模式:金额类 `el-input-number` → `AmountInput`;只读 → `formatMoneyDisplay`。
---
### Task 7: P2 细胞存储(后续迭代)
**Files:**
- Modify: `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue`
- Modify: `web-admin/src/views/cell-storage/payment-info/index.vue`
- 只读:`ProtocolViewDialog.vue`、payment/refund confirm 列表
---
### Task 8: 图谱更新(Agent 规则)
- [ ] **Step 1:** `./scripts/update-graph.sh frontend`(完成 P0 后)
---
## Self-review(plan ↔ spec)
| Spec 要求 | Task |
|-----------|------|
| 聚焦/失焦/提交行为 | Task 1–2 |
| 只读千分位 | Task 3–4, 6–7 |
| 禁止页面内复制正则 | Task 3 删除重复 |
| 数量字段排除 | 各 Task 仅改单价/金额 |
| OpenSpec | Task 5 |
| 分期 P0–P3 | Task 3–7 |
## 执行选项
Plan 已保存至 `docs/superpowers/plans/2026-05-25-money-input-thousand-separator.md`。
1. **Subagent-Driven** — 按 Task 派发子代理,逐 Task 评审
2. **Inline Execution** — 当前会话按 Task 1→3 连续实现(建议先 P0)
请告知:是否批准设计 spec,以及希望先执行 **P0(工具 + 组件 + 渠道服务订单)** 还是全量分期推进。
@@ -0,0 +1,189 @@
# Web 管理端金额/单价千分位输入与展示 — 设计说明
## 背景
业务人员在录入或核对大额「单价」「金额」时,需要千分位逗号辅助阅读,但输入过程中插入逗号会干扰光标与连续键入。渠道服务订单「发起充值」已在 `channel-service-order/index.vue` 用页面内局部函数实现「聚焦纯数字、失焦千分位」,尚未复用到其他页面。
本设计将交互固化为**统一工具 + 统一组件**,并明确**可编辑输入**与**只读展示**两类场景,避免再次在 `.vue` 中复制正则或误用前瞻表达式导致编译错误。
## 目标行为(SHALL)
| 时机 | 可编辑输入框 | 列表/查看等只读展示 |
|------|----------------|---------------------|
| 聚焦输入中 | 纯数字,如 `1234567.89`(无逗号) | — |
| 失焦后 | 千分位,如 `1,234,567.89` | 始终千分位(同上格式) |
| 再次聚焦 | 去掉逗号,恢复纯数字 | — |
| 提交/API | `number` 或约定精度的小数,**不受逗号影响** | — |
**不在范围:** 数量、份数、排序号、百分比系数等非金额字段(仍用 `el-input-number` 整数或原控件)。
## 范围
### 纳入(单价 / 金额类)
**中试服务(pilot-service)**
| 页面 | 路由 | 可编辑 | 只读展示 |
|------|------|--------|----------|
| 渠道服务购买订单 | `/#/pilot-service/channel-service-order` | 充值金额、退款金额、产品明细单价;发起充值已改 | 列表余额/购买总额、查看流水与持仓、合计 |
| 渠道服务订单财务确认 | `/#/pilot-service/channel-service-order-finance` | — | 列表/详情金额、单价 |
| 渠道信息管理 | `/#/pilot-service/channel` | 协议价(元)行内编辑 | 价格选择器单价列 |
| 细胞产品价格体系 | `/#/pilot-service/cell-product-price` | 表单单价 | 列表单价 |
| 细胞产品项目价格 | `/#/pilot-service/project-price-system` | 表格单价 | 查看单价 |
| 体检机构及项目 | `/#/pilot-service/checkup-institution` | 项目费用等 | 套餐合计费用 |
**细胞存储(cell-storage)**
| 页面 | 路由 | 可编辑 | 只读展示 |
|------|------|--------|----------|
| 采集协议单录入 | `/#/cell-storage/protocol` | 合同金额、应缴费、转账金额、采集金额等 | 查看弹窗同字段 |
| 缴费信息 | `/#/cell-storage/payment-info` | 退费金额 | 应缴/实缴列 |
| 缴费/退费确认(财务) | `/#/cell-storage/payment-confirm`、`refund-confirm` | — | 金额列 |
**其他(第二批)**
| 页面 | 说明 |
|------|------|
| 订单 Demo `system/order/index.vue` | 供货价/售价及金额(若仍为可编辑控件则纳入) |
| 研究捐赠 `DonationFormDialog` | 采集金额等 |
| 第三方检测公司等 | `feeAmount` 等 `:precision="2"` 的金额 |
### 明确排除
- 小程序端 `miniprogram/`
- 后端 API(仍接收 JSON number)
- 非金额 `el-input-number`(数量、排序、`priceCoefficient` 百分比等)
## 方案对比
### 方案 A:每页复制 focus/blur 函数(现状扩展)
- 优点:无新依赖,单页改动快
- 缺点:已出现正则笔误导致整页无法加载;难维护;`formatAmount` 与输入逻辑不一致
### 方案 B:工具函数 + `AmountInput` 组件(推荐)
- `@/utils/money-format.ts`:`sanitizeMoneyInput`、`formatMoneyDisplay`、`parseMoneyToNumber`
- `@/components/common/amount-input/index.vue`:`v-model` 绑定 `number | null`,封装 focus/blur/input
- 只读:`formatMoneyDisplay(n)` 或增强现有列表 `formatAmount` 的展示路径
- 优点:单测友好、符合 Fresh Mint 组件化、表格/弹窗同一套
- 缺点:需逐页替换 `el-input-number`(金额类)
### 方案 C:全局指令 `v-money-input`
- 优点:模板改动小
- 缺点:难支持 `min`/`max`/`precision`、表格行绑定不清晰、与 Element Plus 集成弱
**结论:采用方案 B。**
## 技术设计
### 1. 工具模块 `web-admin/src/utils/money-format.ts`
```typescript
// 展示:整数千分位 + 最多 precision 位小数(默认 2)
export function formatMoneyDisplay(value: string | number | null | undefined, precision?: number): string
// 输入清洗:去逗号、仅保留数字与一个点
export function sanitizeMoneyInput(value: string, precision?: number): string
// 提交:number | null;空串 / 仅 '.' → null
export function parseMoneyToNumber(value: string, precision?: number): number | null
```
实现要点:
- 千分位用**字符串分段拼接**(`addThousandSeparators`),**禁止**在 `.vue` 内联复杂前瞻正则。
- 小数位截断与 `el-input-number` 的 `:precision="2"` 行为对齐。
### 2. 组件 `AmountInput`
路径:`web-admin/src/components/common/amount-input/index.vue`
| Prop | 类型 | 默认 | 说明 |
|------|------|------|------|
| `modelValue` | `number \| null` | — | 双向绑定数值 |
| `min` | `number` | — | 可选下限 |
| `max` | `number` | — | 可选上限 |
| `precision` | `number` | `2` | 小数位数;单价/金额均为 2 |
| `placeholder` | `string` | — | |
| `disabled` | `boolean` | `false` | |
| `clearable` | `boolean` | `true` | |
事件:`update:modelValue`、`change`(失焦且数值变化时,便于触发行内 `calcAmount`)
内部状态:
- `displayText: ref<string>` — 绑定 `el-input`
- `@focus` → `displayText = sanitize(modelValue)`
- `@input` → 清洗 + 临时更新 `displayText`;可选同步 `modelValue` 为中间值
- `@blur` → `emit(update:modelValue, parseMoneyToNumber(...))`;`displayText = formatMoneyDisplay(...)`
校验:`min`/`max` 在 blur 或 submit 时由父级 `el-form` rules 负责;组件可在 blur 时 `ElMessage.warning` 可选(默认不弹,避免表格内刷屏)。
样式:宽度 `100%`;与现有 `el-input` 高度 36px 规范一致。
### 3. 只读展示
- 列表列、查看弹窗、合计文案:统一 `formatMoneyDisplay(v)`,前缀 `¥` 仍在模板侧拼接(与现网 `¥{{ formatAmount(...) }}` 一致)。
- **迁移策略:** 在 `money-format.ts` 导出 `formatMoneyDisplay`;各页逐步将只读 `formatAmount` 替换为 `formatMoneyDisplay`(或全局别名 `formatAmount = formatMoneyDisplay` 仅用于金额列——需逐文件审查,避免误改非金额 toFixed)。
建议新增 `formatMoneyAmount(v)` 专用于金额展示,保留原 `formatAmount` 为两位小数无逗号直至迁移完成,**P0 页面直接使用 `formatMoneyDisplay`**。
### 4. 与 OpenSpec / UI 规范
在 `openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md`(或归档后 `openspec/specs/ui-conventions/spec.md`)增补一节:
- 金额/单价**可编辑**字段 SHALL 使用 `AmountInput`(或经评审的等价封装),SHALL 满足聚焦/失焦/提交行为表。
- 金额/单价**只读**展示 SHALL 使用 `formatMoneyDisplay`,整数部分千分位、小数位默认 2 位。
- SHALL NOT 在业务 `.vue` 中复制千分位正则实现。
### 5. 渠道服务订单页迁移(含已改充值)
- 删除页面内 `addThousandSeparators`、`formatRechargeAmountDisplay` 等重复函数,改为 import 工具 + `AmountInput`。
- 「发起充值」「申请退款」弹窗金额 → `AmountInput`。
- 新增/编辑订单产品明细「单价」列 → `AmountInput`(`@change` 仍调 `calcItemAmount`)。
### 6. 测试与验收
**单元(建议)** `web-admin/src/utils/money-format.spec.ts` 或 vitest:
- `formatMoneyDisplay(1234567.89)` → `1,234,567.89`
- `sanitizeMoneyInput('1,234.5')` → `1234.5`
- `parseMoneyToNumber('1,234.56')` → `1234.56`
**手工验收(每批至少 1 页)**
1. 聚焦输入 `1000000.5`,框内无逗号
2. 失焦显示 `1,000,000.5`
3. 再聚焦变回 `1000000.5`
4. 保存后网络请求 payload 为数字 `1000000.5`
5. 列表只读列为千分位
## 分期交付
| 阶段 | 内容 | 产出 |
|------|------|------|
| P0 | 工具 + 组件 + 规范 + 渠道服务订单全页 | 可复用基座 |
| P1 | 渠道信息、细胞价格、项目价格、财务确认页只读列 | 中试服务闭环 |
| P2 | 细胞存储协议/缴费/退费 | 存储线金额 |
| P3 | Demo 订单、捐赠、第三方费用等 | 余量 |
## 风险与约束
- **表格行内多个 `AmountInput`**:须 `row-key` 稳定,避免 Vue 复用 DOM 导致显示串行。
- **`el-form` 校验**:rules 仍针对 `number` 字段,blur 后须已 `emit` 最新值。
- **min/max**:退款金额等保留 `max=currentBalance`,在 `AmountInput` 上透传并在 blur 校验。
- **不要**对 `quantity` 等整数字段使用本组件。
## 非目标(YAGNI)
- 货币符号内置于组件(仍由模板加 `¥`)
- 多币种、汇率
- 输入法全角数字自动转换(可后续加)
---
**状态:** 待评审
**关联实现:** `docs/superpowers/plans/2026-05-25-money-input-thousand-separator.md`