资讯动态

uni-app + Vue3 自定义车牌键盘组件开发实战

发布时间:2026/9/10 7:34:33 来源:尧图企业网站定制
做车辆相关业务的时候车牌输入这个需求几乎是躲不开的。不管是二手车估价、停车缴费、违章查询还是物流登记用户都需要在一个看起来像键盘的组件上一格一格地点车牌。早期我接过一个项目直接用系统键盘输入结果用户在车牌输入框里打了一串英文和数字格式乱得没法看最后还是下了重写方案。后来在 uni-app 里用 vue3 重构这套业务我索性自己写了一个车牌键盘组件把省份简称、字母、数字、新能源专用键位全部做成可配置的数据结构一次性解决了多端复用和格式校验的问题。这篇就聊聊这个组件从设计到落地的完整过程适合正在做停车、物流、车辆管理类项目的同学参考也适合想自己封装 uni-app 组件的开发者看看思路。1. 车牌键盘到底要解决什么1.1 车牌输入的两个硬性约束车牌输入和普通文本输入最大区别在于格式有固定规则。国内民用车牌一般是“省份简称 发牌机关代号 序号”比如“京A12345”新能源车牌比普通蓝色牌照多一位格式是“省 字母 6位序号”其中序号部分可以包含字母和数字但不能用“I”和“O”。这个规则在不同业务场景下还有变体教练车、警车、挂车等特殊牌照也都有不同位数字数。如果用系统键盘用户会非常自然地乱输你不仅要事后校验还得在输入过程中不断纠正交互体验很差。另外一个约束是平台差异。项目用了 uni-app意味着同一套代码要跑小程序、App 和 H5。系统自带键盘在三个端上的表现完全不同小程序里 input 聚焦会自动拉起键盘App 里不同机型的键盘高度不一致H5 在部分 Android 浏览器里还会顶起页面。而自定义组件用 view 模拟按键完全摆脱系统键盘的弹起逻辑这样在跨端开发里反而更稳定因为所有点击都由我们自己的 touch 事件处理不再依赖系统输入法。1.2 组件化的价值一次封装多处复用最开始我图省事直接在页面里写了几个array循环渲染按键代码确实不多。但等到第二个页面也要用第三个页面也要用问题就来了每个页面拷贝一次改一个键位要同步好几处输入逻辑跟页面业务混在一起后面加自动聚焦、车牌颜色选择、新能源判断改动越来越痛苦。所以最终决定封装成独立组件plate-keyboard.vue。组件对外只暴露一个v-model绑定当前输入值以及一个visible属性控制显示隐藏内部管理键盘类型切换、按键点击、删除、清空、完成事件。业务页面完全不需要关心键位怎么排、各省简称有多少个只要传一个值进去再监听输入事件就够了。这样的封装也方便后续扩展比如增加“港澳台”车牌、双行结构车牌等都只需要改组件内部的数据。2. 组件设计思路与整体架构2.1 键盘布局的数据驱动设计我把键盘拆成三种类型省份简称键盘、字母数字键盘、新能源专属键盘。省份简称键盘是纯汉字包含全国各省、直辖市、自治区简称有的键盘设计还会加上“使”“领”“学”“警”“港”“澳”这些特殊字符。字母数字键盘包含 24 个字母去掉 I 和 O、10 个数字以及“删除”“完成”“切换”功能键。新能源专属键盘的思路是前五位的序号部分用字母数字键盘最后一位只有数字少数地区允许字母但一般按数字处理所以当输入到新能源车牌最后一位时自动切到纯数字键盘。不把三种键盘独立写死而是定义成一个配置数组每个键位是一个对象// 键位对象结构 interface PlateKey { label: string; // 显示文本 type: province | letter | number | delete | complete | switch; value?: string; // 实际回填的值默认等于 label span?: number; // 占位宽度比例用于功能键 }这样键盘渲染就是纯循环后续键位增减只需要维护一份数据。实测下来这种结构也方便做多语言和自定义业务键比如新能源键盘里要把“充”字按键换成“电”字按键改配置即可不用动渲染逻辑。2.2 状态管理与输入值同步方案车牌键盘不是简单的一排按钮它需要根据当前输入位置动态决定键盘类型。普通车牌第 1 位只能是省份简称第 2 位是大写字母城市代码第 3 位及之后是字母或数字。新能源车牌第 1 位省份、第 2 位字母、第 3 到第 8 位是字母或数字但最后一位严格来说是纯数字。在这个前提下组件内部需要维护三个状态const keyboardType refprovince | letterNumber | number(province) const currentValue ref() const emit defineEmits([update:modelValue, input, complete, clear])currentValue是字符串类型长度对应车牌有效位数。每次用户点击字符键都先判断当前输入的是第几位然后决定是否能插入。我采用“携带当前值”的方式组件内部修改currentValue并立刻emit(update:modelValue, currentValue)这样可以支持v-model双向绑定。同时对外暴露input事件方便父组件在后端接口校验失败时回滚数据。这里有一个关键点组件内部不能直接用 props 里的 modelValue 作为唯一数据源否则父组件修改值的时候会触发更新但内部键盘类型不会自动切换。我让currentValue作为内部同步镜像监听 modelValue 变化来重置它。这样父组件可以随时强制清空也能在提交后重置。3. 完整实现从模板到逻辑逐步拆解3.1 模板结构三种键盘的渲染方案组件模板不复杂分成上下两层上面是输入预览框下面是键盘区。输入预览框不是必选项但加上以后交互更直观用户能看到当前已输了几位。预览框我用的是独立的面板不依赖原生 input这样避免弹出系统键盘。键盘区用一个根 view 包住根据keyboardType显示不同键盘。template view v-ifvisible classplate-keyboard-mask tapcloseKeyboard view classplate-keyboard tap.stop !-- 输入预览区可选 -- view classplate-preview v-ifshowPreview text v-for(item, index) in previewArray :keyindex :class[plate-preview-item, { active: index currentValue.length }] {{ item }}/text /view !-- 省份简称键盘 -- view v-ifkeyboardType province classkeyboard-grid province-grid view v-forkey in provinceKeys :keykey.label classkey-item hover-classkey-item-hover taphandleKeyTap(key) {{ key.label }}/view /view !-- 字母数字键盘 -- view v-else-ifkeyboardType letterNumber classkeyboard-grid letter-number-grid view v-forkey in letterNumberKeys :keykey.label :class[key-item, key.type delete || key.type complete ? key-item-func : ] hover-classkey-item-hover taphandleKeyTap(key) {{ key.label }}/view /view !-- 新能源纯数字键盘 -- view v-else-ifkeyboardType number classkeyboard-grid number-grid view v-forkey in numberKeys :keykey.label :class[key-item, key.type delete || key.type complete ? key-item-func : ] hover-classkey-item-hover taphandleKeyTap(key) {{ key.label }}/view /view /view /view /template省份键盘我用 8 列布局因为省份简称加上特殊字符大概是 48 个左右8 列刚好排满 6 行。字母数字键盘用 10 列26 个字母去掉 I/O 是 24 个数字 10 个一共 34 个字符加上删除、完成、切换功能键排成 4 行比较合理。纯数字键盘也是 10 列但功能键少一个切换整体更紧凑。列数和键位数据是匹配的改布局的时候要一起改。3.2 核心逻辑按键点击、删除、切换、完成逻辑部分全部放在setup里使用ref和emit。以下是核心函数的设计。handleKeyTap是统一入口根据key.type分发处理function handleKeyTap(key: PlateKey) { switch (key.type) { case province: case letter: case number: appendChar(key.value || key.label) break case delete: deleteChar() break case complete: completeInput() break case switch: switchKeyboard() break } }appendChar要解决的第一个问题是判断当前位上允许输入什么字符。判断规则我写成了一个函数function canAppend(char: string, index: number, isNewEnergy: boolean): boolean { // 第1位必须是省份简称 if (index 0) return /^[\u4e00-\u9fa5]$/.test(char) // 新能源第2位必须是大写字母 if (isNewEnergy index 1) return /^[A-Z]$/.test(char) // 新能源最后一位第8位只允许数字 if (isNewEnergy char.length 2 index 7) return /^[0-9]$/.test(char) // 其他位置允许字母和数字但字母去掉 I 和 O return /^[A-Z0-9]$/.test(char) ![I, O].includes(char) }这里char.length指的是车牌总长度。我会根据当前currentValue长度判断是第几位动态决定键盘类型但省份键盘只在第 1 位时出现。当用户点完省份简称后下一帧自动切到字母数字键盘。deleteChar比想象中要处理的点多。如果当前正在省份键盘删除后应该停留在省份键盘如果删掉了第 1 位的省份简称键盘要切回省份键盘。同时删除要限制不能把字符串删成负数。代码很简单但关键是切换逻辑要同步function deleteChar() { if (!currentValue.value.length) return currentValue.value currentValue.value.slice(0, -1) syncKeyboardType() emit(input, currentValue.value) } function syncKeyboardType() { const len currentValue.value.length if (len 0) { keyboardType.value province } else if (len 1) { keyboardType.value letterNumber } else if (isNewEnergyPlate() len 7) { keyboardType.value number } else { keyboardType.value letterNumber } }注意isNewEnergyPlate()的判断规则普通车牌长度是 7含省份和城市新能源汽车车牌长度是 8且第一位是省份简称后面跟字母和数字。判断方式可以简单点就是用户输入到第 2 位以后如果当前长度等于 7且用户之前没有显式声明车型可以按普通车处理但更好的办法是让外部传入一个isNewEnergy属性。我在项目里直接让父组件通过车辆类型燃油/新能源控制这个属性这样键盘切换更准确。completeInput要做的不仅是关闭键盘关键是做好输入校验。普通车牌 7 位新能源 8 位第 1 位省份第 2 位城市字母这些都要检查。校验不通过时我用emit(error, errorMsg)把错误消息发出去父组件可以用 toast 展示。如果校验通过就emit(complete, currentValue.value)并关闭键盘。这里我没用uni.showToast让父组件决定如何提示组件保持纯净。switchKeyboard就是手动切换键盘类型的交互比如用户在省份键盘想直接切到字母数字键盘。逻辑上是先判断当前输入是否满足最低切换条件然后切到letterNumber类型。如果当前输入为空切过去也没有意义我就加了个保护不给用户乱切。3.3 样式处理主题定制、弹层位置、安全区适配样式在 uni-app 里用scss写但要注意各端预处理器支持。为了简单我这里用纯 CSS在 App 端和小程序端都工作正常。键盘区我固定在底部遮罩半透明键盘外部点击关闭。字体大小和键位尺寸要考虑手指点击面积。官方推荐 44px 左右我这里用height: 88rpx在小屏幕上折叠成 96rpx 会比较挤实测 88rpx 是舒适区域。键位之间留 8rpx 间隔总宽度用 flex 布局不用担心 375px 和 414px 屏幕差异。安全区是移动端绕不开的坑。iPhone X 及以后机型底部有 home indicator键盘底部要留出env(safe-area-inset-bottom)的距离。我在键盘容器上加.plate-keyboard { padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); }constant是为了兼容 iOS 旧版本。App 端如果用了原生导航也建议提前适配否则键盘会遮挡底部返回手势区域。另外如果键盘上有悬浮收起按钮按钮位置要避开安全区。4. 在业务页面中接入组件4.1 父组件引入与 v-model 绑定组件封装好以后在页面里用起来就非常清爽。假设业务页面是停车缴费页面只有一个车牌输入框和一个“确认”按钮。父组件模板template view classparking-page view classplate-input-wrap tapopenKeyboard text classplate-label车牌号/text text classplate-value :class{ plate-empty: !plateNo }{{ plateNo || 点击输入车牌 }}/text /view button classsubmit-btn :disabled!canSubmit tapsubmit立即缴费/button plate-keyboard v-modelplateNo :visiblekeyboardVisible :is-new-energyisNewEnergy update:model-valueonPlateChange completeonPlateComplete clearonPlateClear closekeyboardVisible false / /view /templatev-model在 vue3 中默认绑定的是modelValue和update:modelValue。我在组件里没有直接修改 props 的 modelValue而是通过 emit 触发更新。父组件这里的plateNo会自动同步。注意一个陷阱在小程序端update:model-value这种事件写法没有问题但如果用input来监听每次输入在 H5 端可能存在事件重复触发的问题。我用的是update:model-value配合complete输入过程中只关心 final 值输入反馈由组件内部预览框处理这样更稳。4.2 与车牌颜色、车辆类型的联动现实中车牌类型不只有蓝牌和黄牌还有绿牌新能源、白牌军警、黑牌涉外。其中新能源车牌的键盘规则与普通车牌差异最大。我在组件里增加了一个plate-type属性或者直接叫isNewEnergy。因为车牌颜色和车辆类型是两套业务维度但在交互上可以合并逻辑用户先选择车牌颜色如果是“新能源渐变绿”则isNewEnergy true组件按 8 位规则运行如果是普通蓝牌则isNewEnergy false按 7 位规则运行。同时键盘的键位显示也可以多一个“新能源专用”数字键盘提示。我的父组件里这样联动const plateColor refblue | green(blue) const isNewEnergy computed(() plateColor.value green)这样用户改车牌颜色后键盘规则和长度限制会自动改变。避免了业务侧写一堆判断。5. 常见问题排查与体验优化5.1 键盘弹起遮挡输入框的坑很多人会问自定义键盘没有系统键盘为什么还会遮挡其实是键盘弹出动画和业务滚动没有协调好。比如表单里有多个字段用户点开键盘后键盘最上方正好遮住输入框用户看不见自己输到第几位。解决办法有两种。第一种是组件内部加“输入预览框”也就是我在模板里写的plate-preview它永远显示在键盘上方用户视线焦点不离开键盘区域这个问题自然不存在。第二种是父页面把滚动条往上顶但这种方式在 App 端和小程序端行为不一致很麻烦。我建议使用第一种因为键盘本来就是替代输入法的不要让输入框参与到系统键盘交互中。在停车的场景里整个页面只有一个输入框点击按钮弹键盘键盘上方直接显示车牌号码再优雅不过。5.2 快速连点与数据同步问题自定义键盘最怕用户手快在极短时间内连续点击两个字符键。v-model更新是异步的如果currentValue在事件处理函数内同步更新理论上没有问题但如果在uni.$emit或 Vuex 里再绕一圈就容易拿到旧值。我在组件外部没有引入全局状态所有状态都限定在内部所以天然是安全的。但有个小毛病用户快速点击时删除键和字符键交替按下可能出现删除逻辑还没执行完字符就已经追加上去了。我加了一个简单锁let lockInput false function appendChar(char: string) { if (lockInput) return lockInput true // 简化实际可能用 setTimeout 处理反馈 currentValue.value char syncKeyboardType() lockInput false }实际上 vue3 的事件循环是同步的不需要setTimeout锁的意义在于拦截重复触发。更稳妥的做法是用nextTick把键盘类型切换延后一帧视觉反馈更自然。5.3 不同端表现差异App、小程序、H5uni-app 自定义组件在三个端上有个坑view的hover-class在 App 端有一定延迟尤其是低端 android 上点击反馈不明显。我的优化办法是不依赖 hover-class而是用 CSS transition 控制点击后的背景色变化给按键做一个 0.1s 的过渡效果触摸反馈更灵敏。还有一个端差异是事件名。在 H5 端tap事件底层是click在某些浏览器上 300ms 延迟问题由 uni-app 处理过但不要自己再去监听click否则会触发两次。统一用tap。小程序端如果键盘层级不够高可能被其他原生组件覆盖比如map或video。解决方式是给键盘容器设置z-index一个很大的值但z-index在小程序端并不是万能的原生组件会盖在普通组件上方。这时要用cover-view重写键盘或者开启小程序原生组件的same-layer-rendering。通常车牌输入场景很少碰到 map所以这里只提一下遇到再处理。最后是字体问题。省份简称是汉字在不同端默认字体差异不大但字母数字键盘如果用了自定义字体记得在小程序端把 font-family 设置为系统内置字体否则会出现字符渲染不一致。写在最后这个车牌键盘组件我已经在三个线上项目里跑过了停车缴费、车辆登记、物流运单都在用。前后迭代了好几版最大的体会是不要为了炫技把键盘做成一行行复杂渲染越是输入类组件越要保证逻辑简单、数据可控。现在这个版本把省份、字母数字、新能源键盘全部收敛成一套状态机后面再加“港澳台通行证”车牌也只需要扩展配置和校验规则。如果你也在做类似组件建议先捋清楚自己的业务规则车牌一定是对齐国家标准的还是允许自定义格式有没有教练车、挂车这类特殊车牌确认清楚之后再动代码会少走很多弯路。另外键盘动画别做太复杂一个 slide-up 就够过度设计在这个场景里反而会拖慢输入速度。希望这篇拆解对你有帮助有问题可以评论区交流我尽量回复。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价