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 事件值应该用数字(方便业务使用)

相关推荐
蜡台9 分钟前
Vue 打包优化
前端·javascript·vue.js·vite·vue-cli
木斯佳9 分钟前
前端八股文面经大全:快手前端一面 (2026-03-29)·面经深度解析
前端·宏任务·原型链·闭包
皙然24 分钟前
Redis配置文件(redis.conf)超详细详解
前端·redis·bootstrap
卷帘依旧1 小时前
JavaScript中this绑定问题详解
前端·javascript
dweizhao1 小时前
突发!Claude Code源码泄露了
前端
sunny_2 小时前
💥 Claude Code 源码泄露?我把这个最强 AI Coding Agent 的架构扒干净了
前端·agent·claude
西洼工作室2 小时前
React轮播图优化:通过延迟 + 动画的组合,彻底消除视觉上的闪烁感
前端·react.js·前端框架
yaaakaaang2 小时前
(八)前端,如此简单!---五组结构
前端·javascript
我是若尘2 小时前
我的需求代码被主干 revert 了,接下来我该怎么操作?
前端·后端·代码规范
魁首2 小时前
Claude Code 源码泄露的背后,到底与Codex,Gemini 有啥不一样?
前端·openai·claude