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

📋 问题背景

Issue : #4319

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

涉及文件:

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

涉及方法 :handleInput、handleBlur、format、add、setValue


🔍 根因链路(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") → format 中 Number("1.0") = 1,getLen(1) = 0,toFixed(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 响应式系统中,currentValue 从 3 设回 "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) 返回数字 4,String(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

根因 :setValue 中 Number("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. 需要全链路追踪 :从 handleInput → filterIllegalChar → format → setValue → add → updateCurrentValue,每一步都可能是精度丢失的入口

  4. 平台差异要注意:

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

相关推荐
java1234_小锋4 小时前
【技术专题】HTML5 网页技术 - HTML5 Canvas 基础以及动画入门
前端·html·html5
知了学历孙老师7 小时前
小自考的校考和统考,出题方和难度差在哪
java·大数据·前端
ShineWinsu9 小时前
对于JavaScript:操作元素的解析
开发语言·前端·javascript·面试·web·dom·元素
福兮说10 小时前
JSON 格式正确,数据就可靠吗?我用一份自编配置检查类型与空值
java·前端·json
程序员老赵12 小时前
Docker 部署青龙面板:轻松搭建定时任务管理平台
前端·docker·开源
Swot12 小时前
Asciidoctor 公式数字下标错位问题:原因分析与解决方案
前端
Swot12 小时前
leo-editor 中将 Markdown 内容转成 asciidoc 格式
前端
狒狒AI12 小时前
Remotion / GSAP+Three / HyperFrames / Skia 四栈实测:代码渲染知识视频的选栈方法(附开源技能)
前端·人工智能·aigc
修炼的dance12 小时前
浏览器里导出H.265,8.5秒和55.5秒差在硬件
前端
补码缩写12 小时前
WebCodecs选编码器,探测之后还要验解码和播放
前端