Stepper 小数输入精度丢失 Bug 修复

📋 问题背景

Issue : #4319

现象t-stepper 组件输入小数时,如果小数点后输入 0(如 1.0),会被直接格式化成没有小数点(变成 1)。

涉及文件

  • packages/components/stepper/stepper.ts(小程序原生版)
  • packages/uniapp-components/stepper/stepper.vue(uniapp 版)

涉及方法handleInputhandleBlurformataddsetValue


🔍 根因链路(5 层问题,逐层暴露)

核心教训:数字 ↔ 字符串转换是精度丢失的根源,需要在整个数据流中追踪值的类型变化。修复一个点可能引入新问题,需要全场景验证。

数据流总览

lua 复制代码
用户输入 → handleInput → filterIllegalChar → format → setValue → updateCurrentValue → 显示
按加减号 → add → setValue → format → updateCurrentValue → 显示
失焦     → handleBlur → filterIllegalChar → format → setValue → updateCurrentValue → 显示

问题 ❶:handleInput 正则过度触发

场景 :用户输入 "1.0",被立即格式化成 "1"

根因

js 复制代码
// 原代码
if (this.integer || /\.\d+/.test(formatted)) {
  this.setValue(formatted);
}

/\.\d+/ 匹配 "1.0" 成功 → 触发 setValue("1.0")formatNumber("1.0") = 1getLen(1) = 0toFixed(0) = "1" → 小数点消失

修复 :正则改为 /\.\d*[1-9]/,要求小数部分至少包含一个非零数字才触发 setValue

js 复制代码
if (this.integer || /\.\d*[1-9]/.test(formatted)) {
  this.setValue(formatted);
}
输入值 旧正则 /\.\d+/ 新正则 /\.\d*[1-9]/ 行为
1. ❌ 不匹配 ❌ 不匹配 ✅ 保留,等待继续输入
1.0 ✅ 匹配 → 被格式化为 1 ❌ 不匹配 ✅ 保留,等待继续输入
1.00 ✅ 匹配 → 被格式化为 1 ❌ 不匹配 ✅ 保留,等待继续输入
1.05 ✅ 匹配 ✅ 匹配 ✅ 正常格式化
1.5 ✅ 匹配 ✅ 匹配 ✅ 正常格式化
5 ❌ 不匹配 ❌ 不匹配 ✅ 不触发 setValue,blur 时统一处理

说明integer=false 且输入整数(如 "5")时,正则不匹配,setValue 不会在 input 阶段调用。这是可接受的行为,因为 handleBlur 一定会调用 setValue,最终值和 change 事件不会丢失。无需额外加 !formatted.includes('.') 条件。


问题 ❷:Vue 值回填失效

场景integer = true 时,用户粘贴 "3.5" → 过滤后应显示 "3" 但 input 仍显示 "3.5"

根因 :Vue 响应式系统中,currentValue3 设回 "3" 时,Vue 认为值未变化,跳过 DOM 更新

修复 :先清空再通过 nextTick 回填,强制触发视图更新

js 复制代码
const displayValue = this.integer ? newValue : formatted;
if (String(this.currentValue) === String(displayValue)) {
  this.updateCurrentValue('');
  nextTick().then(() => {
    this.updateCurrentValue(displayValue);
  });
} else {
  this.updateCurrentValue(displayValue);
}

注意 :小程序原生版不需要此修复,因为 setData 即使值相同也会强制更新视图。


问题 ❸:format 中 getLen 的隐式类型转换

场景 :blur 时 "1.0" 变成 "1"

根因format(value)this.getLen(value),当 value 在 JS 运算中被隐式转为数字时,Number("1.0") = 1(1).toString() = "1"getLen = 0

js 复制代码
// 修复前
const len = Math.max(this.getLen(step), this.getLen(value));

// 修复后 ------ 用 String(value) 确保字符串形式
const len = Math.max(this.getLen(step), this.getLen(String(value)));

同时 handleBlur 中需先 filterIllegalChar 再传给 format

js 复制代码
// 修复前
handleBlur(e) {
  const { value: rawValue } = e.detail;
  const value = this.format(rawValue);
  ...
}

// 修复后
handleBlur(e) {
  const { value: rawValue } = e.detail;
  const formatted = this.filterIllegalChar(rawValue);
  const value = this.format(formatted);
  ...
}

问题 ❹:add 方法返回数字丢失精度

场景currentValue = "3.0",按 + 号(step=1),结果显示 4 而非 4.0

根因add("3.0", 1) 返回数字 4String(4) = "4" 无小数位信息

js 复制代码
// 修复前
add(a, b) {
  const maxLen = Math.max(this.getLen(a), this.getLen(b));
  const base = 10 ** maxLen;
  return Math.round(a * base + b * base) / base; // 返回数字,丢失精度
}

// 修复后 ------ 保留运算涉及的最大小数位数
add(a, b) {
  const maxLen = Math.max(this.getLen(a), this.getLen(b));
  const base = 10 ** maxLen;
  const result = Math.round(a * base + b * base) / base;
  return maxLen > 0 ? result.toFixed(maxLen) : result; // 返回字符串保留精度
}

问题 ❺:setValue 中 Number() 转换丢失末尾0

场景format 返回 "4.0",但显示 4

根因setValueNumber("4.0") = 4,然后用数字 4 更新显示值

js 复制代码
// 修复前
setValue(value) {
  const newValue = Number(this.format(value));
  this.updateCurrentValue(newValue); // Number("4.0") = 4 → 显示 4
}

// 修复后 ------ 用字符串更新显示,数字仅用于 change 事件
setValue(value) {
  const formattedStr = this.format(value);      // "4.0"
  const newValue = Number(formattedStr);         // 4(用于 change 事件)
  this.updateCurrentValue(formattedStr);         // "4.0"(用于显示)
  if (this.preValue === newValue) return;
  this.preValue = newValue;
  this._trigger('change', { value: newValue });  // 对外传数字
}

📊 完整修复效果

步骤 修复前 修复后
add("3.0", 1) 返回 4(数字) 返回 "4.0"(字符串)
format("4.0")getLen getLen(4) = 0 getLen(String("4.0")) = 1
format 返回 "4" "4.0"
setValue → 显示更新 Number("4.0") = 4 直接用 "4.0"
输入框显示 4 4.0

💡 通用经验总结

  1. 数字↔字符串转换是精度丢失的核心原因Number("1.0")=1(1).toString()="1"String(4)="4" 这些隐式转换会在链路的每一环吃掉末尾的 0

  2. 修一个点可能引入新 bug :正则从 /\.\d+//\.\d*[1-9]/ 修了末尾0的问题,却让整数输入不触发 setValue,必须全场景验证

  3. 需要全链路追踪 :从 handleInputfilterIllegalCharformatsetValueaddupdateCurrentValue,每一步都可能是精度丢失的入口

  4. 平台差异要注意

    • 小程序原生 setData 强制更新视图 vs Vue 响应式值相同时跳过更新
    • 小程序原生版和 uniapp 版的 API 差异(如 input type 绑定方式)
  5. 显示值与数据值分离 :input 框的显示值应该用字符串 (保留格式),对外 emit 的 change 事件值应该用数字(方便业务使用)

相关推荐
陈随易2 小时前
刚上市就断货?如此火爆的编程显示器到底有什么魔力
前端·后端·程序员
兆子龙2 小时前
前端哨兵模式(Sentinel Pattern):优雅实现无限滚动加载
前端·javascript·算法
豆苗学前端2 小时前
彻底讲透浏览器渲染原理,吊打面试官
前端·javascript·面试
踩着两条虫2 小时前
AI 驱动的 Vue3 应用开发平台 入门指南(五):创建 H5 移动应用
前端·vue.js·ai编程
ZengLiangYi2 小时前
用 AudioContext.suspend()/resume() 作为流式音视频的同步门控
前端·音视频开发
踩着两条虫2 小时前
AI 驱动的 Vue3 应用开发平台 入门指南(二):快速入门
前端·vue.js·ai编程
程序员阿耶2 小时前
CSS滚动条样式从入门到实战:打造跨浏览器的自定义滚动条
前端
范小饭3 小时前
哼,要变天了:副业赚18块的自救实录
前端
天蓝色的鱼鱼4 小时前
从“死了么”到“我在”:用uniCloud开发一款温暖人心的App
前端·uni-app