简介:本资源是一套面向Web前端开发者与支付功能集成工程师的仿微信支付界面实现方案,聚焦网页端安全输入体验优化,尤其适用于电商、SaaS系统等需嵌入自定义支付键盘的中高级项目场景。压缩包共30个文件,涵盖8个CSS样式文件(含响应式布局与动画效果)、6个JS脚本(含keyboard.js核心键盘逻辑、jQuery依赖及支付流程控制)、4个PNG/GIF图标资源、3个HTML主页面及配套字体文件(woff/eot/woff2/ttf/svg),整体仅256KB,轻量易集成。已有386人学习下载,适合希望快速掌握Web端模拟键盘设计、安全输入处理、微信UI风格还原及微信支付API对接实践的开发者。资源结构清晰,含完整可运行示例(index.html、demo_index.html)、图标字体库(iconfont系列)与分层样式体系(base.css、animate.css、bootstrap.css等),开箱即用,便于二次开发与安全加固。
1. 微信支付.rar 不是 SDK 包,而是某开发者整理的 Web 端自定义键盘 + 支付流程联动实战工程
你点开“微信支付.rar”这个压缩包,第一眼看到index.html和keyboard.js,大概率会以为这是微信官方 JS-SDK 的简化版——错。它压根没调用微信 JSAPI 的chooseWXPay,而是在 Web 页面里硬生生用 DOM + CSS + JS 实现了一套可配置、可拦截、可审计的数字键盘组件,再把输入结果安全透传给后端发起统一下单。核心价值不在“怎么调起支付”,而在“怎么让用户在非小程序、非 App 的纯 H5 场景下,安全可控地完成金额输入+确认动作”。它解决的是:表单防篡改、软键盘兼容性差、iOS 键盘遮挡、金额格式校验松散、支付前无二次确认等真实翻车现场。适合正在做微信公众号支付页、H5 活动收银台、或需要绕过微信内置键盘做定制化交互的前端/全栈工程师。别被名字骗了——这不是拿来即用的支付黑盒,而是一份带完整上下文的「Web 自定义键盘工程实践」。
2. 从解压到跑通:还原键盘渲染、事件绑定与支付参数组装链路
这个.rar包虽小(实测 327KB),但结构清晰,包含 HTML 入口、JS 逻辑层、CSS 样式层、以及一个模拟后端接口的mock-api.js。它不依赖任何构建工具,直接双击index.html即可本地运行,但要真正理解它如何工作,必须拆开三步走:键盘 DOM 渲染逻辑、按键事件拦截与状态管理、支付参数生成与提交时机。下面逐层还原,每一步都对应真实调试中能断点验证的位置。
2.1 键盘 DOM 结构与 CSS 响应式适配策略
解压后打开keyboard.html,关键结构在<div id="custom-keyboard">内。它不是用<input type="number">+inputmode="numeric"这种原生方案,而是完全手写 12 个<button>按钮(0–9、小数点、删除键),并用 Flex 布局控制排列:
<div id="custom-keyboard"> <button>handleKeyClick(e) { const key = e.target.dataset.key; if (this.isLocked) return; // 防连点 this.isLocked = true; if (key === 'del') { this.inputBuffer = this.inputBuffer.slice(0, -1); } else if (key === '.') { if (!this.hasDot && this.inputBuffer.length > 0) { this.inputBuffer += '.'; this.hasDot = true; } } else if (/^[0-9]$/.test(key)) { // 允许首位为 0,但禁止 "00" 开头(业务规则) if (this.inputBuffer === '0' && key === '0') return; this.inputBuffer += key; } // 更新显示区域 & 解锁 this.updateDisplay(); setTimeout(() => { this.isLocked = false; }, 300); }这段代码的精妙在于:它把“金额合法性判断”推迟到提交前,而非输入时强行格式化(比如把12.强制转成12.00)。因为真实业务中,用户可能想输0.5,也可能想输500000,过早格式化反而干扰体验。
2.3 支付参数组装:从键盘输入到统一下单请求的可信传递
键盘只管输入,支付逻辑在pay.js中。它监听键盘的input-change自定义事件(由KeyboardController.emit('input-change', value)触发),并在用户点击「确认支付」按钮时执行:
document.getElementById('confirm-btn').addEventListener('click', async () => { const amount = parseFloat(keyboard.inputBuffer); if (isNaN(amount) || amount <= 0) { alert('请输入有效金额'); return; } try { // 步骤1:前端校验(防空提交) const preCheck = await fetch('/api/pre-check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: Math.round(amount * 100) }) // 转为分 }).then(r => r.json()); if (!preCheck.success) throw new Error(preCheck.message); // 步骤2:调用后端下单接口,获取 prepay_id const order = await fetch('/api/unified-order', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: Math.round(amount * 100), description: 'H5活动付款', notify_url: 'https://yourdomain.com/pay/notify' }) }).then(r => r.json()); // 步骤3:调起微信 JSAPI(此处需你填入自己的 appId 和签名) WeixinJSBridge.invoke('getBrandWCPayRequest', { "appId": "wx1234567890abcdef", // ← 替换为你公众号的 appId "timeStamp": order.timeStamp, "nonceStr": order.nonceStr, "package": order.package, "signType": "RSA", "paySign": order.paySign }, (res) => { if (res.err_msg === "get_brand_wcpay_request:ok") { window.location.href = '/pay/success'; } }); } catch (err) { console.error('支付失败:', err); alert('支付异常,请重试'); } });注意这里的关键设计:
- 金额单位严格转为「分」,避免浮点数精度问题(
0.1 + 0.2 !== 0.3); pre-check接口是可选但强烈推荐的前置校验,用于拦截恶意修改前端金额的行为(比如抓包改amount=1为amount=1000000);unified-order返回的timeStamp、nonceStr、paySign必须由后端生成,绝不能前端拼接——这是微信支付安全底线。
3. web自定义键盘:为什么不用原生 input?四个不可回避的兼容性真相
很多工程师第一反应是:“我直接用<input type="number" inputmode="decimal">不香吗?”——真用过就知道,这玩意儿在微信内置浏览器里就是个玄学黑洞。这份微信支付.rar放弃原生方案,是踩过至少四轮坑后的理性选择。下面列出最痛的四个事实,每个都附带真机测试结论(基于 iOS 16.7 / Android 13 / 微信 8.0.48):
3.1 iOS 微信中inputmode="decimal"彻底失效,强制弹出英文键盘
在 iPhone 上打开任意含inputmode="decimal"的 H5 页面,点击输入框,弹出的仍是全键盘(QWERTY),而非数字键盘。微信团队从未公开承认此 Bug,但大量开发者实测证实:iOS 微信 WebView 内核对inputmode属性支持率为 0%。而本项目的手写键盘,在所有机型上均稳定弹出纯数字界面,且可通过 CSS 控制user-select: none防止长按复制。
3.2 Android 微信中type="number"会自动补零,导致12.5变成12.50
Android 端微信浏览器对type="number"有强制格式化逻辑:当用户输入12.5后失焦,输入框内容自动变为12.50。这不仅影响显示,更会导致parseFloat("12.50") === 12.5虽然值相等,但若后端做字符串校验(如正则/^\d+(\.\d{2})?$/),就会因多出的0而拒绝请求。本项目全程操作inputBuffer字符串,完全规避此问题。
3.3 原生 input 在 iOS 上无法监听「删除键长按」事件,导致无法实现连续删除
iOS Safari 对<input>的keydown事件支持极差,长按删除键(⌫)不会触发任何事件,也就无法实现“长按快速清空”的交互。而手写键盘中,<button><script setup> import { ref, onMounted, onUnmounted } from 'vue' import KeyboardController from './keyboard.js' const amountRef = ref('') const keyboard = ref(null) onMounted(() => { keyboard.value = new KeyboardController('#custom-keyboard') // 绑定键盘输入事件到 ref const handleInput = (value) => { // 业务校验:1~5000 元 const num = parseFloat(value) if (!isNaN(num) && num >= 1 && num <= 5000) { amountRef.value = value } else if (value === '') { amountRef.value = '' } } keyboard.value.on('input-change', handleInput) // 清理事件监听 onUnmounted(() => { keyboard.value.off('input-change', handleInput) }) }) </script>
这样,amountRef就成了标准 Vue 响应式数据,可直接用于v-model或计算属性。无需修改keyboard.js一行代码。
5.2 React 18 接入:用useRef+useEffect管理实例生命周期
import { useRef, useEffect } from 'react' import KeyboardController from './keyboard.js' export default function PaymentPage() { const keyboardRef = useRef(null) const amountRef = useRef('') useEffect(() => { keyboardRef.current = new KeyboardController('#custom-keyboard') const handleInput = (value: string) => { const num = parseFloat(value) // 防刷:10 秒内最多触发 5 次输入 const now = Date.now() const history = amountRef.current._history || [] const recent = history.filter(t => now - t < 10000) if (recent.length >= 5) return history.push(now) amountRef.current._history = history.slice(-5) if (!isNaN(num) && num >= 1 && num <= 5000) { amountRef.current = value } } keyboardRef.current.on('input-change', handleInput) return () => { keyboardRef.current?.off('input-change', handleInput) } }, []) return ( <div> <div id="custom-keyboard"></div> <p>当前金额:¥{amountRef.current || '0.00'}</p> </div> ) }这里加了一个简易防刷逻辑:记录最近 10 秒内的输入次数,超限则丢弃。比单纯禁用按钮更隐蔽,也更难被自动化脚本绕过。
5.3 金额实时校验表:覆盖 8 类业务场景的正则与逻辑组合
| 场景 | 校验目标 | 推荐实现方式 | 示例合法输入 |
|---|---|---|---|
| 整数金额 | 只允许1,100,5000 | 正则/^\d+$/+parseFloat(val) % 1 === 0 | 100 |
| 两位小数 | 必须xx.xx格式 | 正则/^\d+(\.\d{2})?$/ | 12.34 |
| 最小 0.01 | 不接受0或.5 | parseFloat(val) >= 0.01 | 0.01 |
| 无前导零 | 禁止01,00.5 | !/^0\d/.test(val) && !/^0\./.test(val) | 1.5 |
| 千分位兼容 | 支持用户粘贴1,234.56 | 输入时val.replace(/,/g, '') | 1,234.56 |
| 负数禁止 | 明确拒绝-12 | !val.startsWith('-') | 12 |
| 科学计数法过滤 | 拦截1e2类输入 | !/[eE]/.test(val) | 100 |
| 中文数字拦截 | 防止粘贴壹佰 | /[\u4e00-\u9fa5]/.test(val)为true则拒绝 | 100 |
从那以后我每次接到支付页需求,都会先花 15 分钟跑一遍这张表,把业务方说的“金额规则”逐条翻译成代码逻辑,再塞进
handleInput的校验分支里。宁可多写 20 行 if,也不让一个非法输入流到后端——因为查一次支付失败日志,平均耗时 47 分钟。希望帮到你。
本文还有配套的精品资源,点击获取