简介:这份资源聚焦于HTML5环境下的微信支付页面金额输入交互,面向需要实现移动端支付键盘的前端开发者与页面重构人员。它解决的是在微信内置浏览器中调用自定义数字键盘、完成金额录入与校验的常见需求,适合具备一定HTML5与jQuery基础、希望快速复用成熟交互逻辑的开发者参考。压缩包共5个文件,包含2个html页面、1个js脚本、1个txt说明及1个url快捷方式,整体约33KB,体积轻量,便于直接嵌入现有项目或作为独立示例调试。其中html负责页面结构与键盘布局,js承担金额输入、小数点控制与按键响应等核心逻辑,txt与url则提供必要的使用说明和素材来源指引。目前已有1557人学习下载,说明该交互方案在移动支付场景中具有一定参考价值。读者可从中获取一套可直接运行的键盘输入金额实现,理解按键事件绑定、金额格式限制与页面适配思路,并在此基础上按自身业务调整样式与校验规则。
1. 从一次支付页面的“键盘失灵”说起:这个项目到底解决什么问题
移动端支付页面里,金额输入框是最容易翻车的地方。用户点开数字键盘,输入“19.9”,光标乱跳、小数点重复、退格删不掉、键盘弹起遮住按钮——这些在 PC 浏览器上根本不会出现的问题,在手机 WebView 里全冒出来了。这个项目就是冲着这个场景来的:用纯 HTML5 + CSS + JavaScript 实现一个微信支付风格的金额输入页面,自带自定义数字键盘,不依赖系统键盘,输入逻辑完全可控。
它适合两类人:一类是正在做 H5 支付页、收银台、打赏页的前端开发者,需要一套能直接嵌入的金额输入组件;另一类是想学习移动端自定义键盘实现思路的人,代码量不大,但把输入校验、光标控制、金额格式化这几个硬骨头都啃了。整个项目就是几个静态文件,浏览器打开就能跑,不需要后端,也不需要任何构建工具。
2. 项目结构与技术选型:为什么不用系统键盘
2.1 目录结构与文件职责
拿到源码后,先看目录。这个项目没有用任何框架,就是最朴素的 HTML + CSS + JS 三件套,结构非常扁平:
wechat-pay-keyboard/ ├── index.html # 支付页面主结构 ├── css/ │ └── style.css # 页面布局与键盘样式 ├── js/ │ ├── keyboard.js # 自定义键盘核心逻辑 │ └── amount.js # 金额格式化与校验 └── images/ └── (图标资源)index.html负责页面骨架,包含金额显示区、键盘容器和确认按钮。style.css用 flex 布局把键盘固定在底部,同时处理了安全区域适配。keyboard.js是核心,监听键盘按钮点击、维护输入状态、控制光标位置。amount.js单独抽出来做金额的格式化与合法性判断,方便复用和测试。
这种拆分方式的好处是职责清晰:键盘只管“按了什么键”,金额模块只管“这个值合不合法”。二次开发时想换 UI 只动 CSS,想改校验规则只动 amount.js,不会牵一发动全身。
2.2 为什么放弃 input 原生键盘
常见做法是直接用<input type="number">或type="tel",让系统弹键盘。但在支付场景里这有三个致命问题:第一,系统键盘类型不可控,iOS 弹的是带字母的键盘,Android 各家定制 ROM 弹出来的布局都不一样;第二,系统键盘弹起时页面会被顶上去,金额显示区可能被遮住;第三,用户可以通过长按粘贴、拖拽等方式绕过你的输入限制,把非法字符塞进来。
所以这个项目选择完全屏蔽系统键盘,用readonly属性让 input 不触发系统键盘,然后自己画一个数字键盘。核心代码就一行:
<input type="text" id="amountInput" readonly placeholder="请输入金额">readonly而不是disabled,是因为disabled的输入框无法获得焦点,光标和选中状态都没法控制。readonly保留了焦点能力,但阻止了系统键盘弹出。这是整个方案能成立的前提。
2.3 键盘布局与事件绑定
键盘用一组div或button拼出来,布局是经典的 4 列 4 行:1-9 数字、小数点、0、退格、确认。每个按键通过><div class="keyboard"> <div class="key">document.querySelector('.keyboard').addEventListener('click', function(e) { const key = e.target.dataset.key; if (!key) return; handleKeyPress(key); });
handleKeyPress里根据 key 的值分派:数字和小数点走插入逻辑,del走删除逻辑,confirm走提交逻辑。这里要注意移动端 300ms 点击延迟的问题,常见做法是引入fastclick或者用touchstart代替click。这个项目用的是touchstart+preventDefault,避免延迟的同时也阻止了页面滚动。
3. 核心逻辑拆解:金额输入、光标与校验
3.1 输入状态管理与光标位置
金额输入和普通文本输入最大的区别是:光标不能随便放。用户输入“19.9”,光标如果在“1”后面,再按“9”就变成“199.9”,这显然不对。所以这个项目维护了一个cursorIndex变量,记录光标在字符串中的位置,每次插入或删除后更新它。
插入逻辑的核心代码:
function insertChar(char) { const value = input.value; const index = input.selectionStart; // 获取当前光标位置 const newValue = value.slice(0, index) + char + value.slice(index); // 校验新值是否合法 if (!isValidAmount(newValue)) return; input.value = newValue; const newIndex = index + char.length; input.setSelectionRange(newIndex, newIndex); // 恢复光标 updateDisplay(); }selectionStart和setSelectionRange是这里的关键 API。selectionStart拿到光标位置,setSelectionRange在值更新后把光标放回去。如果不做这一步,input 的值一变,光标就会跳到末尾,用户连续输入时体验极差。
参数说明:char是要插入的字符,可能是数字也可能是小数点。isValidAmount是校验函数,返回 false 时直接 return,不修改 input 的值。updateDisplay负责把金额同步到显示区,比如加上“¥”符号或千分位分隔。
3.2 金额合法性校验规则
金额校验看起来简单,其实边界情况很多。这个项目的isValidAmount函数处理了以下几类规则:
function isValidAmount(value) { // 空值允许(用户可能全删了) if (value === '') return true; // 不能有多个小数点 if ((value.match(/\./g) || []).length > 1) return false; // 小数点后最多两位 if (/\.\d{3,}/.test(value)) return false; // 不能以小数点开头 if (value.startsWith('.')) return false; // 整数部分不能有前导零(除非就是0) if (/^0\d+/.test(value)) return false; // 总长度限制 if (value.replace('.', '').length > 10) return false; return true; }逐条解释:多个小数点直接拒绝,这是最常见的误操作;小数点后超过两位拒绝,因为人民币最小单位是分;以小数点开头拒绝,避免出现“.5”这种不规范的写法;前导零拒绝,防止“007”这种输入;总长度限制是防止用户输入一个天文数字导致后端溢出。
这里有个细节:value.replace('.', '')只替换第一个小数点,但因为前面已经保证了最多一个小数点,所以没问题。如果校验规则更复杂,建议用正则一次性判断,可读性更好。
3.3 删除逻辑与边界处理
删除比插入更麻烦,因为要处理光标在末尾、光标在中间、光标在小数点前后等不同情况。核心代码:
function deleteChar() { const value = input.value; const index = input.selectionStart; // 光标在最前面,没东西可删 if (index === 0) return; const newValue = value.slice(0, index - 1) + value.slice(index); input.value = newValue; input.setSelectionRange(index - 1, index - 1); updateDisplay(); }这段逻辑看起来简单,但有个坑:如果用户选中了一段文本再按删除,selectionStart和selectionEnd不相等,上面的代码只会删掉光标前一个字符,而不是选中的整段。完整的做法是先判断是否有选中:
const start = input.selectionStart; const end = input.selectionEnd; if (start !== end) { // 有选中,删除选中部分 const newValue = value.slice(0, start) + value.slice(end); input.value = newValue; input.setSelectionRange(start, start); } else { // 无选中,删除光标前一个字符 // ... }这个项目在移动端场景下,用户很少做选中操作,所以简化处理也能接受。但如果要做成通用组件,这段必须补上。
3.4 金额格式化与显示同步
输入框里的值是纯数字字符串,但显示区通常要带“¥”符号和千分位。格式化函数:
function formatAmount(value) { if (!value) return '0.00'; const parts = value.split('.'); let integer = parts[0]; let decimal = parts[1] || ''; // 整数部分加千分位 integer = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ','); // 小数部分补齐两位 decimal = decimal.padEnd(2, '0').slice(0, 2); return integer + '.' + decimal; }\B(?=(\d{3})+(?!\d))这个正则的意思是:在非单词边界处,且后面跟着 3 的倍数个数字的位置,插入逗号。这是千分位格式化的标准写法。padEnd(2, '0')把“9”补成“90”,slice(0, 2)防止超过两位。
注意:格式化只用于显示,不要写回 input 的值。input 里始终保持用户输入的原始字符串,否则光标位置会乱掉。这是很多新手容易犯的错误——把格式化后的值塞回输入框,结果用户一输入光标就跳。
4. 避坑与常见问题排查
4.1 键盘弹起遮住输入框
现象:在部分 Android 机型上,点击输入框后系统键盘弹起,页面被顶上去,金额显示区被遮住,用户看不到自己输了什么。
原因:系统键盘弹起时,浏览器会调整可视区域高度,但页面没有做响应式适配,固定定位的元素位置没变。
解决:监听window.resize或visualViewport的变化,动态调整键盘容器的位置。常见做法是用position: fixed; bottom: 0把键盘固定在底部,同时给页面主体加padding-bottom,值等于键盘高度。如果用了visualViewport,可以这样:
if (window.visualViewport) { window.visualViewport.addEventListener('resize', function() { const keyboardHeight = window.innerHeight - window.visualViewport.height; document.querySelector('.keyboard').style.bottom = keyboardHeight + 'px'; }); }4.2 光标位置在 iOS 上不准确
现象:在 iOS Safari 或微信 WebView 里,调用setSelectionRange后光标位置和预期不一致,有时跳到末尾,有时停在中间。
原因:iOS 对readonly输入框的光标控制有延迟,值更新和光标设置如果同步执行,浏览器可能还没完成渲染。
解决:把setSelectionRange放到requestAnimationFrame或setTimeout(fn, 0)里异步执行:
input.value = newValue; requestAnimationFrame(function() { input.setSelectionRange(newIndex, newIndex); });这个坑很隐蔽,PC 上调试完全正常,一到真机就出问题。血泪经验是:所有涉及光标操作的代码,都要在真机上验证,模拟器不可信。
4.3 快速连点导致输入错乱
现象:用户快速连续点击数字键,偶尔会出现字符丢失或顺序错乱。
原因:touchstart事件触发频率高,如果处理函数里有异步操作(比如上面的requestAnimationFrame),多次点击的回调可能交错执行,导致状态不一致。
解决:加一个简单的节流,或者用状态锁。更稳妥的做法是把输入状态维护在一个对象里,每次操作基于最新状态计算,而不是依赖 DOM 的当前值:
const state = { value: '', cursor: 0 }; function handleKeyPress(key) { // 基于 state 计算新值和新光标 // 然后一次性更新 DOM }这样即使事件触发很快,每次计算都是基于内存中的 state,不会出现 DOM 读取滞后的问题。
4.4 小数点后补零导致光标跳动
现象:用户输入“19.”,显示区格式化成“19.00”,但输入框里的光标位置变了。
原因:格式化函数修改了显示区的值,但输入框的值没变,两者不同步。如果误把格式化后的值写回输入框,光标就会跳。
解决:严格区分“输入值”和“显示值”。输入框只存原始字符串,显示区用格式化后的字符串。两者通过updateDisplay同步,但updateDisplay只改显示区的textContent,不碰输入框的value。
4.5 确认按钮重复提交
现象:用户点击确认后,网络请求还没返回,又点了一次,导致重复提交。
原因:确认按钮没有做防重复点击处理。
解决:点击后立即禁用按钮,或者加一个isSubmitting标志位:
let isSubmitting = false; function handleConfirm() { if (isSubmitting) return; isSubmitting = true; // 提交逻辑 submitAmount(state.value).finally(function() { isSubmitting = false; }); }这个坑和金额输入本身无关,但在支付场景里是致命的。很多项目在开发阶段忽略了,上线后才发现有用户重复支付。
5. 二次开发与改造思路:从 Demo 到可复用组件
5.1 把键盘封装成独立组件
当前代码是面向页面的,键盘逻辑和页面逻辑混在一起。如果要复用到多个页面,建议封装成一个构造函数或类:
class AmountKeyboard { constructor(options) { this.input = document.querySelector(options.inputSelector); this.keyboard = document.querySelector(options.keyboardSelector); this.maxLength = options.maxLength || 10; this.onConfirm = options.onConfirm || function() {}; this.state = { value: '', cursor: 0 }; this.init(); } init() { this.bindEvents(); this.render(); } bindEvents() { const self = this; this.keyboard.addEventListener('touchstart', function(e) { const key = e.target.dataset.key; if (!key) return; e.preventDefault(); self.handleKey(key); }); } handleKey(key) { // 输入逻辑 } render() { // 更新 DOM } }使用时就一行:
new AmountKeyboard({ inputSelector: '#amountInput', keyboardSelector: '.keyboard', maxLength: 8, onConfirm: function(value) { console.log('确认金额:', value); } });封装的好处是配置项集中,不同页面可以传不同的maxLength和onConfirm,互不影响。
5.2 增加金额上限与下限校验
实际业务里通常有金额限制,比如单笔最低 0.01 元,最高 50000 元。在isValidAmount里加两条规则:
function isValidAmount(value) { // ... 前面的规则 const num = parseFloat(value); if (num < 0.01) return false; if (num > 50000) return false; return true; }注意parseFloat('')返回NaN,所以空值要在前面就 return true,否则会被后面的比较拦住。另外,上限校验最好在确认时再做一次,因为用户可能输入到一半就点确认。
5.3 支持自定义键盘布局
不同业务对键盘的需求不一样。有的只需要数字和删除,不需要小数点;有的需要“全部清空”键。改造思路是把键盘布局抽成配置:
const layout = [ ['1', '2', '3', 'del'], ['4', '5', '6', 'clear'], ['7', '8', '9', '.'], ['0', 'confirm'] ];然后根据 layout 动态生成键盘 DOM。这样换布局只改配置,不动逻辑代码。clear键的处理就是清空 state 和 input,del就是删除,逻辑上很容易扩展。
5.4 验证改造是否成功的三个方法
改完之后怎么确认没改坏?我一般走这三步:
第一,边界值测试。输入“0”、“0.01”、“50000”、“50000.01”,看校验是否符合预期。特别是“0.01”这种最小值,很容易被前导零规则误杀。
第二,光标测试。在“19.9”的中间插入和删除,看光标是否停在正确位置。这个必须在真机上测,Chrome DevTools 的手机模拟器不够准。
第三,快速操作测试。快速连点数字键和小数点,看最终值是否和预期一致。如果出现字符丢失,说明状态管理有问题,需要回到 4.3 节检查。
从那以后我每次改完键盘逻辑,都强制在真机上把这三个测试走一遍,不再偷懒只跑模拟器。希望帮到你。
本文还有配套的精品资源,点击获取