我做过好几个跟车相关的微信小程序,停车缴费、洗车预约、加油开票,绕来绕去都躲不开一个需求:让用户把车牌号码输进来。很多刚上手的朋友第一反应是"放个输入框不就行了",真做起来才发现事情没那么简单。车牌里夹着汉字省份简称,第二位是字母,后面几位字母数字混着来,新能源车牌还多一位,再加上大小写、易混字母、位数判断这些问题,用户输错一个字符,后面的订单、缴费、查询全卡住。这个输入车牌号码的功能,看起来是个小模块,但它直接决定了整个流程能不能顺畅跑通,也是很多汽车后市场类小程序绕不开的基础能力。这篇文章我打算把整套实现思路摊开讲一遍:从车牌的编码规则、自定义键盘的设计、输入校验的写法,到组件封装和实际踩过的坑。不管你是刚开始接触小程序开发,还是已经做过几个项目想把这套东西沉淀成组件,应该都能拿到能直接抄的东西。
1. 车牌号码的底层编码规则与输入场景拆解
1.1 普通车牌和新能源车牌的结构差异
车牌这东西表面看就是几个字符,实际上有一套很严格的编码逻辑,你不把规则吃透,校验就写不对,用户也就永远在输错和改错之间来回折腾。先看普通燃油车牌,一共七位:第一位是省份简称,是一个汉字,常见的比如京、津、沪、粤、川、浙这些,全国省级行政区简称加起来三十多个;第二位是发牌机关代号,是一个英文字母,代表该省下面的具体城市或区域,比如粤B是深圳、粤A是广州、京A是北京市区;第三到第七位一共五位,由字母和数字混合组成,而且字母里明确规定不含 I 和 O。这一点特别关键,因为 I 和 1、O 和 0 在视觉上极容易混,相关部门干脆从编码字符集里把它俩去掉了。你写键盘的时候如果不把这些字母剔除,用户点出来的本身就是非法字符。
再看新能源车牌,它是八位。第一位还是省份简称,第二位还是发牌机关代号字母,区别在第三位和后续:小型新能源车第三位是 D 或 F,大型新能源车则在第八位是 D 或 F,D 代表纯电动,F 代表非纯电动也就是混动。也就是说,同样是"粤B"开头,你光看前两位根本判断不出是普通牌还是新能源牌,必须结合位数和第三位的字符一起判断。这就引出一个问题——用户输入的过程中,你什么时候知道他到底要输七位还是八位?我的做法是动态判断:当用户输完前两位、准备输第三位的时候,看第三位是不是 D 或 F,如果是,就按八位新能源处理,否则按七位普通处理。这样键盘位数和校验规则可以自适应切换,用户不用自己去选类型。
还有一类特殊车牌,比如挂车、警车、学车、港澳入出境的车辆,它们的末位或者特定位置会有"挂""学""警""港""澳"这样的汉字。这些在普通民用场景里碰得少,但你要是做的是货运、驾校、物流类小程序,就必须考虑进去。我个人的经验是,普通业务场景(停车、洗车、加油)只需要支持普通牌和新能源牌,特殊号段可以放到配置里按需开启,没必要一上来就把所有情况都堆进去,键盘会变得特别臃肿。
1.2 从真实使用场景倒推用户为什么会输错
我做停车缴费的时候专门统计过一批用户行为,输错车牌的原因其实很集中。第一大类是字母数字混淆,尤其是把数字键上的字母漏掉,比如把"粤B"输成"粤8",因为不少人对发牌机关代号是字母这件事没概念。第二大类是新能源位数搞不清,七位输完了才发现还要再输一位。第三大类是省份简称记不住或打不对,用户心里想的是"我们这是鲁",结果选了"豫",两个字形近。第四大类是大小写问题,有人用系统键盘敲了小写字母,校验直接没过。
这些问题倒推回来,其实给了设计上的明确指引:键盘上必须把字母和数字都摆出来,不能只给数字;省份简称必须用汉字让用户点选,不能让他打字;输入位数要实时提示当前是第几位、还剩几位;字母统一按大写处理,用户敲小写也自动转大写。这些看着都是小细节,但它们决定了用户是三次搞定还是折腾半分钟。你如果有线下门店扫码或者收银台代客输入的场景,还要考虑大按钮、防误触,因为很多时候是店员拿着手机帮客户输,手指粗、屏幕小,按错概率更高。
1.3 一个能打的输入组件应该具备哪些能力
我把这些年做下来认为必备的能力列一下,方便你对照检查自己的实现。首先是省份选择区和字母数字键盘区要能联动切换,最开始显示省份,选完省份自动切到字母数字键盘。其次是输入位数的动态判断,普通牌七位、新能源八位要自动识别。第三是实时校验,每输入一位就判断当前字符是否合法,不合法直接不让上屏,而不是等用户输完再报错,这种"边输边拦"的体验比事后报错好太多。第四是回填和编辑能力,用户从历史记录或者拍照识别结果里带回来的车牌要能显示、能修改、能定位到某一位再改。第五是对外暴露清晰的接口,比如bind:change返回当前车牌、bind:complete在输满时触发,方便父页面调用。这几条看着简单,但真要把每一步都打磨顺,代码量和细节思考一点都不少。
2. 方案选型:为什么我最终放弃了系统键盘
2.1 直接用系统输入框会遇到哪些麻烦
一开始我也图省事,直接放了个 input,想着配合校验正则就完事了,结果上线后被用户教做人。系统键盘的问题主要三个。第一个是汉字输入太麻烦,车牌首位的省份简称是汉字,用户要么用拼音打要么用手写,光这一个字就能耗掉好几秒,还经常打错。第二个是键盘类型不匹配,车牌里既有字母又有数字,手机的数字键盘没有字母,字母键盘切数字又要来回切,操作成本极高。第三个是没法做"边输边拦",系统键盘输入的内容你要在bindinput里过滤,用户输入了非法字符再被删掉,光标会跳,体验很割裂,尤其在 iOS 上光标跳动问题特别明显。
我做测试的时候请了几个朋友体验,用系统输入框平均要花十几到二十秒才能输对,还得有人提醒他第二位是字母。换成自定义键盘之后,熟练的人五六秒就输完了。这个差距在缴费、开票这种高频场景里非常明显,用户等待时间直接影响转化。所以只要你的业务对车牌输入有稳定需求,自定义键盘基本是唯一合理的解法。
2.2 自定义键盘组件的整体设计思路
我的组件拆成三层。最外面是组件容器,负责接收父页面的属性(比如是否自动聚焦、是否只读、初始值),向内暴露change和complete两个事件。中间是显示层,用一个个格子把当前已输入的字符显示出来,没输入的位置显示占位符,光标所在的位置高亮。最下面是键盘层,分省份键盘和字母数字键盘两套,根据输入进度自动切换。
省份键盘我做成一个宫格,把三十多个省份简称按顺序铺开,每个格子是一个按钮。字母数字键盘则按类似手机全键盘的布局排,但去掉了 I 和 O,同时把数字单独放大或者标红,让用户一眼能区分字母和数字。键盘上还单独放了删除键,长按可以连续删除,这个小细节体验提升很明显——用户输错一串的时候不用一下一下点。
组件和父页面的通信我用事件来做,这样组件可以被多个页面复用。数据在上层用properties传进来,下层的变化用triggerEvent抛出去,父子解耦,后面维护起来也清爽。这套结构做出来之后,我在三个项目里直接复用,基本没怎么改。
2.3 和拍照识别的配合怎么做
现在很多小程序都会加拍照识别车牌的功能,用户对着车牌拍一张,自动识别出号码回填到输入框。这个功能很香,但识别结果不能直接信,因为光线、角度、遮挡都会导致识别错位。我的处理方式是:识别结果回填到键盘组件里,用户可以逐位检查,发现错了直接定位到那一位改。组件必须支持"部分回填+继续编辑",而不是识别完就锁死。识别回来之后,我还会再跑一遍本地校验,如果位数或者字符集不对,直接在对应位置标红提示用户确认。
这一套组合拳下来,识别准的用户一步到位,识别错的用户也能快速修正,比纯手输和纯识别都靠谱。要注意的是拍照识别涉及到相机权限和图片上传,这块记得做权限申请失败的降级处理,别让用户卡在授权弹窗上动弹不得,回归到手动输入也能完成流程。
3. 从零构建车牌输入组件的核心实现
3.1 先把数据准备好:省份简称和键盘字符集
所有逻辑的起点是数据。省份简称我整理成一个数组,顺序上把热门省份放前面还是按行政区划顺序,这个看业务,我一般按常用度稍微调整一下,把业务量大的地区往前放。因为我在好几个项目里都遇到用户抱怨"翻半天找不到我这个省",把高频省份前置能省不少事。
// 省份简称数据 const PROVINCE_LIST = [ '京', '津', '冀', '晋', '蒙', '辽', '吉', '黑', '沪', '苏', '浙', '皖', '闽', '赣', '鲁', '豫', '鄂', '湘', '粤', '桂', '琼', '渝', '川', '贵', '云', '藏', '陕', '甘', '青', '宁', '新' ]; // 字母数字字符集(去掉 I 和 O) const LETTERS = ['Q','W','E','R','T','Y','U','P','A','S','D','F','G','H','J','K','L','Z','X','C','V','B','N','M']; const NUMBERS = ['1','2','3','4','5','6','7','8','9','0'];注意我把 I 和 O 从字母里彻底去掉了,这是很多新手容易忽略的点。有人会问那用户要是真想输 I 怎么办,答案是车牌里本来就不存在 I 和 O,去掉才是对的。字符集准备好之后,键盘布局用二维数组组织,一行一个子数组,渲染的时候双层循环就行,后面想加特殊字符也方便。
3.2 键盘布局与输入逻辑
键盘的渲染我用 WXML 循环铺开,每个键上绑一个点击事件,把当前键值传进去。输入逻辑是核心,我写一个handleKeyTap函数统一处理。第一步先判断当前处于省份段还是字母数字段,如果是省份段并且点击的是省份键,那就把省份写进第一位,同时把键盘切到字母数字模式,光标移到第二位。第二步,如果是字母数字段,先判断这一位的类型要求,比如第二位必须是字母,那用户点数字就不上屏并给个轻微提示。
handleKeyTap(e) { const key = e.currentTarget.dataset.key; const { value, activeIndex } = this.data; // 省份段处理 if (activeIndex === 0) { if (this.isProvince(key)) { const arr = value.split(''); arr[0] = key; this.setData({ value: arr.join(''), activeIndex: 1, keyboardType: 'letter' }); this.emitChange(); } return; } // 第二位必须为字母 if (activeIndex === 1 && !this.isLetter(key)) { this.shakeTip('第二位应为字母'); return; } // 已经输满则不再接受输入 if (activeIndex >= this.getMaxLength()) return; const arr = value.split(''); arr[activeIndex] = key.toUpperCase(); const nextIndex = activeIndex + 1; this.setData({ value: arr.join(''), activeIndex: nextIndex }); this.emitChange(); // 输满时触发完成事件 if (nextIndex >= this.getMaxLength()) { this.triggerEvent('complete', { value: arr.join('') }); } }这里有几个关键点。长度判断用getMaxLength()动态返回七或八,判断依据是第三位字符,如果第三位是 D 或 F,就返回八,否则返回七。字母统一toUpperCase(),用户就算点到了小写也自动转大写。输满的时候触发complete,父页面可以在这里做提交或者查询。整个输入过程每次按键都校验,非法字符直接拦截,不会跑到显示层。
3.3 校验算法的实现细节
校验分成两层,一层是实时输入校验,前面说的边输边拦就是这层;另一层是最终格式校验,在complete事件里跑。实时校验保证每一位的字符集正确,最终校验用正则确认整个车牌合法。普通车牌和新能源车牌我写了两条正则。
// 普通燃油车牌(七位) const NORMAL_REG = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]$/; // 新能源车牌(八位) const NEW_ENERGY_REG = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼][A-HJ-NP-Z](([DF][A-HJ-NP-Z0-9]\d{4})|(\d{5}[DF]))$/;这两条正则里[A-HJ-NP-Z]的意思就是从 A 到 Z 去掉 I 和 O 的字母集合,正好对应车牌规则。新能源正则里(([DF][A-HJ-NP-Z0-9]\d{4})|(\d{5}[DF]))分开处理了小型车第三位是 D/F 和大型车第八位是 D/F 两种情况。你直接拿去用的话,记得把省份列表和字符集保持一致,别正则里认的省份和键盘里给的省份对不上,那样就会出现"键盘能输但校验不过"的诡异问题,我早期就踩过这个坑。
3.4 组件的对外接口设计
组件封装好之后,对外我只暴露几个必要的属性。value用来传入初始值,实现回填;disabled控制只读;autoFocus控制是否自动弹出键盘。事件上暴露change(每次变化抛出当前值)和complete(输满触发)。父页面用起来就像这样:
<car-plate-input value="{{plate}}" bind:change="onPlateChange" bind:complete="onPlateComplete" />这种设计的好处是组件不关心业务逻辑,只管输入和校验,父页面拿到完整车牌去做自己的事。后面如果你要加拍照识别按钮,放在父页面就行,识别结果通过value传回组件,组件自动显示并允许继续编辑。职责清晰,复用性高。
4. 避坑实录:只有真做过才知道的那些细节
4.1 车牌规则里的"隐藏条款"和易混点
第一个坑是新能源的位数判断时机。如果你在用户输完第二位就开始判断,那第三位还没输进来你怎么知道是不是新能源?正确做法是等第三位输入之后再锁定长度,或者在第三位输入时动态判断。我在线上见过有实现是让用户先选"普通/新能源"类型再输入的,多一步选择用户就多一层烦恼,能自动判断就别让用户选。
第二个坑是字母数字的视觉混淆。除了 I 和 O,实际用车牌里 1 和 7、8 和 B、0 和 D 也有认错的情况,但车牌规则本身不禁止这些,你不能替用户做过滤。我的做法是在显示层把字符间距拉开一点、字体用等宽字体,减少误读。如果有条件,在字母键上把字母做得和数字有明显视觉区分,比如不同颜色。
第三个坑是挂车、使领馆、警用等特殊号段的尾号汉字。这些车牌在做物流、驾校、租赁类小程序时确实会遇到,但它们的规则各不相同。我的建议是把特殊号段做成可配置的开关,默认关闭,需要的时候再打开,避免键盘被特殊字符撑爆。你如果业务里覆盖不到,直接不支持就行,不要为了少数场景牺牲大多数用户的体验。
4.2 光标定位与输入回退的处理
组件做编辑的时候,用户点中间某一位去改,这时光标要能定位到那一位,并且后续输入要覆盖而不是往后插。我的实现是把已输入的每个字符当成一个独立格子,点击哪个格子就把activeIndex设为那个位置,下一次按键就替换这一位。删除键的逻辑是如果当前位置有字符先清空当前位,如果没有就把光标往前移一格再清空。这个逻辑看着简单,但顺序写反了就会出现"删了半天删不掉"的问题。
长按删除连续清空这个功能,我用bindlongpress触发一个定时器,每隔一小段时间删一位,松手就清除定时器。要注意页面销毁或者组件隐藏时把定时器清掉,不然会内存泄漏,这种小疏忽在小程序里排查起来特别费劲,因为不一定每次都能复现。
4.3 常见问题速查表
我把实际项目里遇到的问题整理成一张表,方便你对照排查。
| 问题现象 | 可能原因 | 处理方式 |
|---|---|---|
| 键盘能点但校验不过 | 键盘省份列表与正则省份列表不一致 | 统一维护一份省份常量,两边共用 |
| iOS 上输入后光标跳动 | 使用系统 input 做过滤 | 改用自定义键盘,避免过滤引起的重排 |
| 新能源车牌输不满八位 | 第三位判断逻辑写成了第二位 | 在第三位输入后再判定长度 |
| 小写字母被拒绝 | 未做大小写转换 | 输入时统一 toUpperCase |
| 删除键长按失效 | 定时器未清除或事件未绑定 | 检查 bindlongpress 和清定时器逻辑 |
| 回填后无法编辑 | 只读状态未随输入切换 | 回填时把组件设为可编辑并重置光标 |
| 键盘挡住了输入框 | 页面滚动未处理 | 键盘弹出时让容器上移或滚动到可视区 |
注意:省份列表、字符集、正则这三样东西必须同源,建议抽成一个单独的常量文件,所有地方引用同一份,这是我在多个项目里反复吃亏之后总结出来的铁律。
5. 落地场景与后续可以扩展的方向
5.1 哪些业务会用到这个组件
停车缴费是最典型的场景,用户进场扫码缴费用车牌匹配,输入准确率直接影响缴费效率。洗车预约、加油开票也是一样,很多门店系统靠车牌来关联会员和订单。保险、二手车、租车这类小程序,车牌是查询和下单的关键字段。还有一类是有车一族的社群或者工具类应用,让用户输入车牌做违章查询、限行提醒。这些场景对输入组件的诉求高度一致:快、准、可编辑。所以我一直说这个组件值得做成通用能力沉淀下来,别每个项目都重新写一遍。
5.2 可以继续深挖的功能点
组件做扎实之后,往上还能挂不少东西。比如接入车牌识别,识别失败时再降级到手动输入,两条路配合体验最顺。比如把用户最近输入过的车牌存到本地,下次进入页面直接展示供快速选择,复购场景特别有用。再比如加入车牌归属地的展示,输入完自动显示"粤B 广东深圳",用户能顺带确认自己没输错。还有一点是校验要有容错,用户输错的常见字符可以给出智能提示,比如输成"粤I"的时候提示"车牌中不含字母 I",这样比单纯拒绝要友好得多。
我个人在实际操作中的体会是,这个组件最值钱的地方不在代码本身,而在于把车牌规则、用户习惯和交互细节三者结合起来的那些判断。代码谁都能写,但知道为什么第二位必须是字母、为什么 I 和 O 要去掉、为什么长度要动态判断,这些才是让一个组件从"能跑"变成"好用"的关键。你要是正准备做这类功能,建议先把本章的速查表和常量统一这两件事落实了,能省掉后面一半的调试时间。