普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月13日首页

从一个“不能输负号”的数字输入框说起:Web Component 数字输入组件重构实录

作者 莫石
2025年12月13日 17:56

背景

拿到需求时,因为工期还比较宽松,官网开发,我又只做其中一个组件,框架又没有定。

我决定使用原生开发,并封装为Web Component以适配任何框架(如果不能适配,说明框架有问题)。其中就有一个数字输入框带拉杆的,数字输入框和拉杆这两个东西,原生组件都有。

于是在我的要求下,ai很快给我封装了一个还可以的东西,不过后面ui又去掉拉杆了。

临近发布,要合代码了,同事才发现这个输入框有点儿问题!


起点:一段“差不多能用”的代码

这里就不赘述Web Component的开发了,因为确实很简单,看代码就行了。

这是我最初写的 NumInput 组件(为简洁省略部分 CSS):

export class NumInput extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    // ... 模板里用了 <input type="number" steop="0.1">
  }
  _onNumberInput(event) {
    const newValue = this.clamp(event.target.value, this.min, this.max);
    this.setAttribute('value', newValue);
    this._dispatchEvent();
  }
}

功能上:

  • 支持 labelunitminmaxstep
  • 聚焦有高亮效果
  • 值变化会派发 value-changed 事件
  • 还有后缀单位

看起来没问题?直到女同事问我:“为什么我输 - 没反应?” 我压根儿没想过手输,因为这个组件最开始的时候,还是带拉杆的,纯鼠标操作一点儿问题没有。

当然,现在UI变了。


问题一:type="number" 不让你输 “-” 和 “.”

首先,这不是一个bug,嗯。 我仔细研究了一下原生的数字框的机制。

1 实时校验,你每输入一个字符都会校验。 2 只能输入数字相关的,0-9 . -

那么问题来了,为什么她无法输入“-” 和 “.”呢?

实际上,并不是无法输入,只是时机和位置不对。 如果输入框中已经有一个数字1了,这个时候,你就可以在这个数字前面输入一个 “-”,在它后面输入一个“.”,这两种情况(-1和1.)都是合法的。

其余情况都是不合法的,所以无法输入。

结论:type="number"校验过于严苛,鼠标操作足矣,不适合手动输入。


重构第一步:放弃 type="number",拥抱 type="text"

使用text输入框,意味着之前数字框有的功能,我现在也都要也有,这是这个手动输入的校验规则要自定义。

我改成了:

<input type="text" inputmode="decimal" />

inputmode="decimal" 能让移动端弹出带小数点的数字键盘,体验不降反升。

但光改类型不够,得自己控制输入内容。

宽松过滤,只拦非法字符

input 事件中,我只做一件事:

_onTextInput(e) {
  let val = e.target.value;
  val = val.replace(/[^0-9.\-+]/g, ''); // 只留数字、点、正负号
  // 再处理符号位置、小数点数量...
  e.target.value = val;
}

关键原则

输入过程中,只过滤,不校验
允许用户输 -.5-12.,这些“中间状态”必须保留。


重构第二步:什么时候才该“认真”校验?

要保留用户输入的字符,又要在结束后校验,一般可能会想到节流,我觉得太麻烦了,不是指实现节流麻烦,而是节流这个逻辑本身,会一直后延,让js很麻烦。

所以,怎么判断:输入结束了

我定义了两个“结束信号”:

  1. 失焦(blur
  2. 按下回车(Enter) 刚开始没想到这个,直到我输了数字没有反应,习惯性地回车了一下。

在这两个时机,调用同一个函数 _finalizeInput()

_finalizeInput() {
  const raw = this.numberEl.value.trim();
  // 如果是中间状态(如 '-'),不处理
  if (raw === '' || raw === '-' || raw === '.') return;

  let num = parseFloat(raw);
  if (isNaN(num)) {
    // 无效?回退到上次合法值
    this.numberEl.value = this.getAttribute('value') || '';
    return;
  }

  // clamp 到 [min, max]
  num = Math.min(Math.max(num, this.min), this.max);

  // 修正浮点精度(关键!)
  num = this._roundToStepPrecision(num, this.step);

  this.numberEl.value = String(num);
  this.setAttribute('value', num);
  this._dispatchEvent(); // 派发的是数字,不是字符串!
}

问题二:0.1 + 0.2 ≠ 0.3?

  • 0.1 + 0.2 → 显示 0.30000000000000004 JavaScript 的浮点精度问题是老朋友了。
    但用户不关心这些,他们只看到“我 step=0.1,怎么变出一串小数?

解法:按 step 的小数位数四舍五入

_roundToStepPrecision(value, step) {
  if (Number.isInteger(step)) return Math.round(value);
  const decimalPlaces = step.toString().split('.')[1]?.length || 0;
  const factor = 10 ** decimalPlaces;
  return Math.round(value * factor) / factor;
}
  • step=0.1 → 保留 1 位 → 0.300000000000000040.3
  • step=0.01 → 保留 2 位 → 0.13
  • step=1 → 整数 → 4

所有赋值路径(手动输入、上下键、外部设置)都走这个修正,彻底告别脏数字。


监听一下回车作为“确认”。

这里直接不仅走了失焦的逻辑,还主动失焦,避免二次“失焦”。

this.numberEl.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') {
    this._finalizeInput();
    this.numberEl.blur(); // 自动失焦,统一交互
  }
});

其他细节打磨

  • 修复 typosteop="0.1"step="0.1"(别笑,真有人写错)
  • 移除无用代码:原始代码里声明了 rangeEl 但没用,删掉
  • 事件传数字value-changeddetail.valuenumber 类型,不是字符串
  • 外部设置值也修正setAttribute('value', '0.30000000000000004') 会自动转成 0.3

最终效果

✅ 自由输入 -.-12.
✅ 按 ↑/↓ 按 step 精确增减
✅ 按 Enter 或失焦自动校验+修正
✅ 支持 min/max 限制
✅ 移动端弹出数字键盘
✅ 事件传出干净的数字
✅ 完全 Web Component,零依赖


结语

这个组件最终代码比最初长了近一倍,但用户体验提升是质的飞跃

有时候,看似简单的功能,深挖下去全是坑。
但正是这些“小细节”,决定了产品是“能用”还是“好用”。

最后,女同事看了一眼说:“这个输入框终于不抽风了。”
我笑了笑,没告诉她,我让AI改了四版。

(完)


脑虎科技:公司“三全”脑机接口产品成功完成首例临床试验

2025年12月13日 17:43
在今日举行的2025天桥脑科学研究院脑机接口与人工智能论坛中,脑虎科技方面表示,公司自主研发的国内首款、国际第二款内置电池的全植入、全无线、全功能(“三全”)脑机接口产品,在复旦大学附属华山医院毛颖、陈亮教授团队的主持下,成功完成首例临床试验。(财联社)

金融时报:将坚持内需主导放在首位

2025年12月13日 17:38
金融时报评论员发布文章称,12月10日至11日,中央经济工作会议在京举行。会���聚焦“当前怎么看”和“明年怎么干”,为中国经济高质量发展把舵定向。站在“十四五”规划收官与“十五五”规划谋篇的历史交汇点,会议强调持续扩大内需、优化供给,将“坚持内需主导,建设强大国内市场”确定为明年经济工作重点任务之首。未来一个时期,我国国内市场主导国民经济循环的特征将更为明显。在内外部发展环境更趋严峻复杂的大背景下,只有坚持立足国内,全方位扩大内需、建设强大国内市场,增强发展主动性,才能够在国际风云变幻中,牢牢把握发展主动权。着眼明年经济社会发展目标任务,做强国内大循环,建设强大国内市场,以国内循环的稳定性对冲国际循环的不确定性,必须坚持内需的主导地位。坚持内需主导,全方位扩大国内需求,要大力提振居民消费。坚持内需主导,全方位扩大国内需求,要推动投资止跌回稳。将坚持内需主导放在首位,是党和国家对当前经济形势的深刻洞察。要全面贯彻明年经济工作的总体要求和政策取向,加快培育完整内需体系,形成消费和投资相互促进的良性循环,将超大规模市场的潜力转化为现实增长动力。

中国造高端工业母机在沈阳下线交付

2025年12月13日 17:23
记者13日从通用技术沈阳机床获悉,由通用技术集团与东方电气集团联合研发的4台高端五轴联动数控机床12日在沈阳下线交付。这一合作在取得重大技术突破的同时,还打破了“研用脱节”的产业困境,开创了国产工业母机研制的新模式。长期以来,国产高端数控机床面临“研发投入大、周期长、验证难”的系统性瓶颈——企业闭门研发与市场实际需求脱节,产品因缺乏真实工况下的长期验证,陷入“用户不愿用、不敢用,技术难迭代、难成熟”的恶性循环,严重制约产业高质量发展。(中新网)

应对老年护理刚需,四部门发布《老年护理服务能力提升行动方案》

2025年12月13日 16:59
近日,国家卫生健康委、国家医保局、国家中医药局和国家疾控局四部门发布《老年护理服务能力提升行动方案》(以下简称《行动方案》)提出,到2027年,老年护理资源有效扩容,覆盖机构、社区、居家的老年护理服务体系逐步完善,从业人员服务能力不断提升,老年护理服务持续改善,服务连续性、可及性、规范性持续提高,老年人获得感不断增强。提升老年护理服务能力是深入贯彻实施积极应对人口老龄化国家战略的具体举措。截至2024年底,我国60岁及以上人口数达3.1亿,占总人口的22%,老年人特别是失能老年人对医疗护理服务呈现迫切的刚性需求。(央视新闻)

斐波那契数列:从递归到缓存优化的极致拆解

作者 闲云ing
2025年12月13日 16:57
斐波那契数列:从递归到缓存优化的极致拆解 斐波那契数列是算法入门的经典案例,也是理解「递归」「缓存优化」「闭包」核心思想的绝佳载体。本文会从最基础的递归解法入手,逐步拆解重复计算的痛点,再通过哈希缓存

全球首台船用中压直流混合式断路器研制成功

2025年12月13日 16:42
近日,全球首台双极双向船用10千伏中压直流混合式断路器,顺利通过大电流短路开断试验。10千伏电压等级相当于普通居民用电(220伏)的45倍,是目前船舶电力系统的最高电压等级。这一重大突破不仅填补了船用中压直流断路器领域的技术空白,更标志着中国在船舶电力装备领域实现关键技术破局,跻身全球创新引领行列。该设备由中国船舶集团第七〇四研究所自主研发。(中新网)

异步互斥锁

作者 NuLL
2025年12月13日 16:27
节流防抖时间太短,而接口返回太久,导致节流防抖结束后仍然可以被重复提交?尝试换个思路——异步互斥锁,以互斥锁作为基石,将整个同名称的异步操作状态锁定,彻底防止接口数据重复提交!

如何正确实现圆角渐变边框?为什么 border-radius 对 border-image 不生效?

作者 三十_
2025年12月13日 14:51

在项目中需要实现一个圆角渐变边框效果。

image-20251212075318921.png

我的第一反应是使用 border-radiusborder-image。然而实践后发现 border-radiusborder-image 不生效效果是这样的:

image-20251201193414791.png

给 div 设置了 border-radius,但边框仍然是直角。


为什么 border-radiusborder-image 失效?

两者的工作层级不同

  • border-radius 作用在 div 元素上,它控制的是整个 div 轮廓的圆角。
  • border-image 绘制边框,是独立于 div 之外的,是脱离于 div 的。

所以,看到的效果就是边框依然是直角,而 div 是圆角。

实现方案

主要通过两点来实现:

  • 创建一个稍大于主元素的伪元素,并设置渐变背景。
  • 使用CSS遮罩"挖空"中间部分,只留下边框区域。

代码如下:

.gradient-border-box {
    width: 100px;
    height: 100px;
    border-radius: 6px;
    position: relative;
}

.gradient-border-box::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    border-radius: 6px; /* 和主元素相同的圆角 */
    padding: 1px; /* 边框宽度 */
    
    /* 渐变效果 */
    background: linear-gradient(360deg, 
        rgba(96, 161, 250, 0.5), 
        rgba(96, 161, 250, 1));
    
    /* CSS遮罩 */
    mask: 
        linear-gradient(#fff 0 0) content-box, 
        linear-gradient(#fff 0 0);
    mask-composite: exclude;
}

方案解析

  • 主元素: 负责内容区域和圆角,只设置 border-radius

  • 伪元素: 负责绘制渐变边框,它的位置与大小覆盖主元素,通过:

    • background 绘制渐变
    • padding 控制边框宽度
    • mask 挖空中间区域

伪元素中的遮罩详解

mask: 
    linear-gradient(#fff 0 0) content-box, 
    linear-gradient(#fff 0 0);
mask-composite: exclude;
  • -webkit-mask: 这行代码创建了两个完全相同的白色矩形遮罩,第一个仅作用于内容区域(content-box);第二个作用域整个元素区域(border-box)。

    第一个 linear-gradient(#fff 0 0) 创建一个纯白色矩形(线性渐变,从0%到0%);

    content-box 指定遮罩的参考框,作用域元素的内容区域(不包括padding、border);

    第二个 linear-gradient(#fff 0 0) 创建了一个白色矩形,默认是 border-box(包括内容+padding+border)。

  • mask-composite:exclude: 控制多个遮罩如何组合exclude 代表异或操作

    结合遮罩解释就是: 边框区域 = border-box(整个区域) - content-box(中心区域)

🎉 React 的 JSX 语法与组件思想:开启你的前端‘搭积木’之旅(深度对比 Vue 哲学)

2025年12月13日 14:46
嘿,未来的前端大神们!👋 欢迎来到 React 的世界!如果你正在寻找一个现代、高效、充满乐趣的前端框架,那么恭喜你,你找对地方了! 在 React 中,有两个核心概念你必须掌握:JSX 语法和组件化
❌
❌