2026-05-25-money-input-thousand-separator-design.md 8.8 KB

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

// 展示:整数千分位 + 最多 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