车牌输入框 封装
- 小程序
- licenseNumber.js
- licenseNumber.json
- licenseNumber.wxml
- licenseNumber.wxss
- 样例
- vue
- vnp-input-box.vue
- vnp-input.vue
- vnp-keyboard.vue
- 样例
小程序
licenseNumber.js
const INPUT_NUM = 8;//车牌号输入框个数
const EmptyArray = new Array(INPUT_NUM).fill('');//['','','','','','','','']
// 车牌输入框的下标
const INPUT_INDEX = {
FIRST: 0,
SECOND: 1
};
Component({
data: {
// 键
provinceArr: ['京', '沪', '津', '苏', '粤', '冀', '晋', '蒙', '辽', '吉', '黑', '浙', '皖', '闽', '赣', '鲁', '豫', '鄂', '湘', '桂', '琼', '渝', '川', '贵', '云', '藏', '陕', '甘', '青', '宁', '新', '港', '澳', '台', '使', '领', "→"],
strArrOne: ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'P', '挂'],
strArrTwo: ['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', '港', '澳'],
strArrThree: ['Z', 'X', 'C', 'V', 'B', 'N', 'M', '学', '警'],
numArr: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'],
hiddenPro: false, // 隐藏省份键盘
hiddenStr: true, // 隐藏数字字母键盘
carNumArr: EmptyArray,
selectInputIndex: 0,
btnDisabled: true
},
methods: {
proTap(e) { //点击省份
let province = e.currentTarget.dataset.province;
const { carNumArr, selectInputIndex } = this.data;
this.setData({
hiddenPro: true,
hiddenStr: false
});
if(province != "→"){
carNumArr[selectInputIndex] = province;
// 选择车牌号时触发
this.setData({
carNumArr,
// 选中一个后,下一个输入框聚焦
selectInputIndex: selectInputIndex !== carNumArr.length - 1 ? selectInputIndex + 1 : selectInputIndex,
btnDisabled: this.btnDisabled()
});
}
},
strTap(e) { //点击字母数字
const str = e.currentTarget.dataset.str;
const { carNumArr, selectInputIndex } = this.data;
carNumArr[selectInputIndex] = str;
this.setData({
carNumArr,
// 选中一个后,下一个输入框聚焦
selectInputIndex: selectInputIndex !== carNumArr.length - 1 ? selectInputIndex + 1 : selectInputIndex,
btnDisabled: this.btnDisabled()
});
},
inputCarNum(e) {
const { index } = e.currentTarget.dataset;
this.setData({
showCarKeyboard: true,
selectInputIndex: index
});
if (index === INPUT_INDEX.FIRST) {
// 第一个输入框展示省份键盘,第二个展示字母数字输入框(数字不可点),以后就是数字字母输入框(都可点)
this.setData({
hiddenPro: false,
hiddenStr: true
});
} else if (index === INPUT_INDEX.SECOND) {
this.setData({
hiddenPro: true,
hiddenStr: false
});
} else {
this.setData({
hiddenPro: true,
hiddenStr: false
});
}
},
backSpace() { //删除
const { carNumArr, selectInputIndex } = this.data;
carNumArr[selectInputIndex] = '';
this.setData({
carNumArr,
selectInputIndex: selectInputIndex !== INPUT_INDEX.FIRST ? selectInputIndex - 1 : selectInputIndex,
btnDisabled: this.btnDisabled()
}, () => {
if (this.data.selectInputIndex === INPUT_INDEX.FIRST) { //这里必须要用this.data.selectInputIndex,用最新的
this.setData({
hiddenPro: false,
hiddenStr: true
});
}
});
},
// 只有输入内容的车牌号位数合法时,展示确认按钮
btnDisabled() {
const { carNumArr } = this.data;
const disabled = carNumArr.some((item, index) => {
if (index !== carNumArr.length - 1) {
return !item;
}
return false;
});
return disabled;
},
onCancel() {
this.setData({ carNumArr: EmptyArray });
this.triggerEvent('onCancel');
},
onOk() {
const carNum = this.data.carNumArr.join('');
this.triggerEvent('onOk', carNum);
}
},
});
licenseNumber.json
{
"component": true,
"usingComponents": {}
}
licenseNumber.wxml
<!--车牌-->
<view class="modal-box">
<view class="modal-wrapper">
<view class="modal-title">
<view class="titleWrapper"><text class="title-text">请录入车牌号</text></view>
<view class="iconWrapper"><image class="close-icon" bindtap="onCancel" src="../../chat/images/SC.png"/></view>
</view>
<view class="modal-content">
<view class="modal-input">
<block wx:for="{{8}}" wx:key="index">
<view data-index="{{index}}" class="input {{selectInputIndex===index?'activeInput':''}}" bindtap='inputCarNum'>
<text style="font-size: 30rpx;position: relative;" class="{{index == 7 && carNumArr[7] == '' ? 'XNY' : ''}}">
<text wx:if="{{index == 7 && carNumArr[7] == ''}}" style="position: absolute; color: green; display: flex;justify-content: center;align-items:center;font-size: 6px;margin-left: -10px; margin-top: 7px;">新能源</text>
<text wx:else="">{{carNumArr[index] || ''}}</text>
</text>
</view>
</block>
<view class="line"></view>
</view>
</view>
<view>
</view>
<view class="model-btn-group">
<button bindtap="onOk" class="btn confirm" disabled="{{btnDisabled}}">确认</button>
</view>
</view>
<!-- 车牌 -->
<view class='keyboard' >
<!-- 省键盘 -->
<view class="provinces" hidden='{{hiddenPro}}'>
<view class="pro-li fl" wx:for="{{provinceArr}}" wx:key="index" catchtap='proTap' data-province="{{item}}">{{item}}</view>
</view>
<!-- 号码键盘 -->
<view class="keyNums" hidden='{{hiddenStr}}'>
<view wx:if="{{selectInputIndex===1}}" class="row numRow">
<view class="pro-li disabled" wx:for="{{numArr}}" wx:key="index" data-str="{{item}}">{{item}}</view>
</view>
<view wx:else class="row numRow">
<view class="pro-li " wx:for="{{numArr}}" wx:key="index" catchtap='strTap' data-str="{{item}}">{{item}}</view>
</view>
<view class="strOne row">
<view class="pro-li " wx:for="{{strArrOne}}" wx:key="index" catchtap='strTap' data-str="{{item}}">{{item}}</view>
</view>
<view class="strTwo row">
<view class="pro-li " wx:for="{{strArrTwo}}"wx:key="index" catchtap='strTap' data-str="{{item}}">{{item}}</view>
</view>
<view class="strThree row">
<view class="pro-li " wx:for="{{strArrThree}}" wx:key="index" catchtap='strTap' data-str="{{item}}">{{item}}</view>
<view class='kb-icon pro-li' catchtap='backSpace'>
<image class='delete-icon' src="../../chat/images/SC.png" />
</view>
</view>
</view>
</view>
<view class="modal-cover"></view>
</view>
licenseNumber.wxss
/* 键盘 */
.keyboard {
width: 100%;
position: fixed;
bottom: 0;
left:0;
z-index: 1000;
background-color: rgba(210, 213, 219, 90);
}
.fl {
float: left
}
.carnum {
text-align: center;
height: 88rpx
}
.tel {
border-bottom: 2rpx solid #ddd;
height: 100rpx;
line-height: 100rpx;
}
.provinces {
overflow: hidden;
padding-top: 20rpx;
}
.pro-li {
font-size: 32rpx;
color: #353535;
height: 76rpx;
width: 62rpx;
line-height: 76rpx;
text-align: center;
margin-left: 12rpx;
margin-bottom: 20rpx;
background-color: #fff;
box-shadow: 0px 1rpx 2rpx 0 #979797;
border-radius: 5px;
flex: 1
}
.keyNums .disabled {
background-color: #F7F7F7;
color: #CCC
}
.keyNums {
overflow: hidden;
padding-top: 20rpx;
display: flex;
flex-direction: column;
}
.keyNums .row {
display: flex;
}
.keyNums .numRow {
padding: 0 10rpx;
}
.keyNums .strOne {
padding: 0 10rpx;
}
.keyNums .strOne .strOneItem {
flex: 1
}
.keyNums .strTwo {
padding: 0 40rpx;
}
.keyNums .strOne .strTwoItem {
flex: 1
}
.keyNums .strThree {
padding-left: 80rpx;
padding-right: 10rpx;
}
.keyNums .strOne .strThreeItem {
flex: 1
}
.keyNums .strOne .strThreeItem:nth-child(7) {
margin-left: 100px
}
.keyNums .pro-li:nth-child(16) {
color: red
}
.keyNums .strThree .kb-del {
margin-left: 12rpx
}
.keyNums .strThree .kb-icon {
flex: 1.5;
background: #ABB3BD;
margin-left: 20rpx;
}
/* modal样式 */
.modal-box {
width: 100%;
position: absolute;
top: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
}
.modal-wrapper {
margin: 30% 30rpx;
height: 380rpx;
padding: 30rpx;
background-color: #fff;
border-radius: 10rpx;
z-index: 300;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-content: space-between;
justify-content: space-between;
overflow: hidden;
text-align: left;
}
.modal-wrapper .model-btn-group {
display: flex;
box-sizing: border-box;
font-size: 32rpx;
}
.model-btn-group view {
width: 50%;
text-align: center;
box-sizing: border-box;
}
.model-btn-group .btn {
flex: 1;
font-size: 18px
}
.model-btn-group .cancel {
color: #999;
}
.model-btn-group .confirm {
color: #fff;
background-color: #ff5000;
}
.model-btn-group .confirm.active {
opacity: 1;
}
.modal-cover {
width: 100%;
background-color: #242424;
opacity: 0.5;
z-index: 299;
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin: auto;
overflow: hidden;
}
.modal-input {
display: flex;
}
.modal-input .input {
border: 1px solid #979797;
margin-right:6rpx;
border-radius: 3px;
flex: 1;
text-align: center;
padding: 8px;
height: 22px;
}
.modal-input .input:nth-child(1) {
border-right-width: 0;
margin-right: 0;
position: relative;
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
.modal-input .input:nth-child(1)::after {
content: "";
position: absolute;
right: -1px;
width: 1px;
height: 20px;
background-color: #979797;
z-index: -10
}
.modal-input .input:nth-child(2) {
position: relative;
border-left-width: 0;
margin-right:20rpx;
border-bottom-left-radius: 0;
border-top-left-radius: 0;
}
.modal-input .input:nth-child(2)::after {
content: "";
position: absolute;
right:-16rpx;
top: 45%;
width: 5px;
height: 5px;
border-radius: 50%;
background-color: #979797
}
.modal-input .input:nth-child(8) {
border: 1px dashed #18ca71;
}
.modal-input .activeInput {
border-radius: 3px !important;
border: 1px solid #FF5000 !important
}
.modal-input .text {
text-align: right;
color: #c5c5c5;
font-size: 28rpx;
}
.modal-placeholder-class {
color: #c5c5c5;
}
.modal-title {
display: flex;
font-size: 20px;
color: #333333
}
.titleWrapper {
flex: 1;
}
.title-text {
font-size: 18px;
font-weight: bold;
}
.iconWrapper {
flex: 1;
text-align: right;
}
.close-icon {
width: 35rpx;
height: 35rpx;
}
.delete-icon {
width: 40rpx;
height: 40rpx;
margin-top: 18rpx;
}
.XNY{
display: flex;
justify-content: center;
align-items:center;
}
样例
vue
vnp-input-box.vue
<template>
<div class="vnp-input-box">
<ul>
<li
v-for="(item, index) in val"
:key="index"
:class="{ active: activeIndex === index }"
@click="handleClickItem(index)"
>
<span>{{ item }}</span>
</li>
</ul>
</div>
</template>
<script>
export default {
props: {
value: {
type: String,
default: "",
},
editable: {
type: Boolean,
default: false,
},
},
data() {
return {
val: ["", "", "", "", "", "", "", ""],
activeIndex: this.editable ? 0 : undefined,
};
},
watch: {
activeIndex() {
this.$emit("activeChange", this.activeIndex);
},
value: {
immediate: true,
handler() {
this.value=this.value||'';
if (this.val.join("") === this.value) {
return;
}
const val = this.value.split("");
if (this.editable) {
this.activeIndex = val.length;
}
while (val.length < 8) {
val.push("");
}
this.val = val;
},
},
val() {
this.$emit("input", this.val.join(""));
},
},
methods: {
handleClickItem(index) {
if (!this.editable) {
return;
}
this.activeIndex = index;
},
setValue(val) {
this.$set(this.val, this.activeIndex, val);
if (this.activeIndex < 7) {
this.activeIndex += 1;
}
},
del() {
this.$set(this.val, this.activeIndex, "");
if (this.activeIndex > 0) {
this.activeIndex -= 1;
}
},
},
}
</script>
<style lang="less" scoped>
.vnp-input-box {
padding: 10px 0;
border: 1px solid #d8d8d8;
border-radius: 2px;
color: #8d8d8d;
font-size: 15px;
text-align: center;
ul {
display: flex;
}
li {
flex: 1;
border-right: 1px solid #eaeaea;
height: 28px;
line-height: 28px;
&:first-child {
border-color: #a6a6a6;
flex: 1.3;
}
&:last-child {
border: none;
}
&.active {
color: #1989fa;
> span {
height: 100%;
width: 20px;
display: inline-block;
border-bottom: 1px solid #1989fa;
}
}
}
}
</style>
vnp-input.vue
<template>
<div>
<vnp-input-box
:value="val"
@click.native="show = true"
></vnp-input-box>
<vnp-keyboard
:show.sync="show"
v-model="val"
></vnp-keyboard>
</div>
</template>
<script>
import Box from "./vnp-input-box";
import Keyboard from "./vnp-keyboard";
export default {
props: {
value: {
type: String,
default: "",
},
},
components: {
'vnp-input-box': Box,
'vnp-keyboard': Keyboard
},
data() {
return {
show: false,
};
},
computed: {
val: {
set(v) {
this.$emit("input", v);
},
get() {
return this.value;
},
},
},
};
</script>
vnp-keyboard.vue
<template>
<van-action-sheet v-model="visible" get-container="body">
<div class="vnp-header">
<button type="button" class="vnp-btn-finish" @click="finish">完成</button>
</div>
<div class="vnp-input-box-outer">
<vnp-input-box ref="inputBox" v-model="val" editable @activeChange="handleActiveChange"></vnp-input-box>
</div>
<div class="vnp-keys">
<div class="vnp-keys-row" v-for="(item, index) in list" :key="index">
<div
class="vnp-btn-key-wrapper"
v-for="(val, index) in item"
:key="index"
:class="{
'vnp-del-wrapper': val === 'del',
'vnp-type-wrapper': val === 'type'
}"
>
<van-button v-if="val === 'type'" class="vnp-btn-key" @click="handleChangeType">
<span v-if="type === 'cn'">中/<span class="vnp-smaller">英</span></span>
<span v-else><span class="vnp-smaller">中</span>/英</span>
</van-button>
<van-button v-else-if="val === 'del'" class="vnp-btn-key" @click="handleDel">
<svg
viewBox="0 0 32 22"
xmlns="http://www.w3.org/2000/svg"
class="vnp-delete-icon"
>
<path
d="M28.016 0A3.991 3.991 0 0132 3.987v14.026c0 2.2-1.787 3.987-3.98 3.987H10.382c-.509 0-.996-.206-1.374-.585L.89 13.09C.33 12.62 0 11.84 0 11.006c0-.86.325-1.62.887-2.08L9.01.585A1.936 1.936 0 0110.383 0zm0 1.947H10.368L2.24 10.28c-.224.226-.312.432-.312.73 0 .287.094.51.312.729l8.128 8.333h17.648a2.041 2.041 0 002.037-2.04V3.987c0-1.127-.915-2.04-2.037-2.04zM23.028 6a.96.96 0 01.678.292.95.95 0 01-.003 1.377l-3.342 3.348 3.326 3.333c.189.188.292.43.292.679 0 .248-.103.49-.292.679a.96.96 0 01-.678.292.959.959 0 01-.677-.292L18.99 12.36l-3.343 3.345a.96.96 0 01-.677.292.96.96 0 01-.678-.292.962.962 0 01-.292-.68c0-.248.104-.49.292-.679l3.342-3.348-3.342-3.348A.963.963 0 0114 6.971c0-.248.104-.49.292-.679A.96.96 0 0114.97 6a.96.96 0 01.677.292l3.358 3.348 3.345-3.348A.96.96 0 0123.028 6z"
fill="currentColor"
></path>
</svg>
</van-button>
<van-button v-else class="vnp-btn-key" :class="{'vnp-btn-empty': !val}" @click="handleClickKey(val)">
{{ val }}
</van-button
>
</div>
</div>
</div>
</van-action-sheet>
</template>
<script>
import Box from './vnp-input-box'
export default {
components: {
'vnp-input-box': Box
},
props: {
show: {
type: Boolean,
default: false
},
value: {
type: String,
default: ""
},
},
data() {
return {
val: this.value,
type: "cn",
cn: [
["京", "津", "沪", "渝", "冀", "豫", "云", "辽", "黑", "湘"],
["皖", "鲁", "新", "苏", "浙", "赣", "鄂", "桂", "甘", "晋"],
["蒙", "陕", "吉", "闽", "贵", "粤", "青", "藏", "川", "宁"],
["type", "琼", "使", "领", "学", "", "", "", "del"]
],
en: [
["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
["Q", "W", "E", "R", "T", "Y", "U", "O", "P"],
["A", "S", "D", "F", "G", "H", "J", "K", "L"],
["type", "Z", "X", "C", "V", "B", "N", "M", "del"]
],
}
},
computed: {
visible: {
set(val) {
this.$emit("update:show", val);
},
get() {
return this.show;
}
},
list() {
return this.type === "en" ? this.en : this.cn;
}
},
watch: {
show() {
if (this.show) {
this.val = this.value;
}
}
},
methods: {
handleChangeType() {
this.type = this.type === "en" ? "cn" : "en";
},
handleClickKey(val) {
if (val) {
this.$refs.inputBox.setValue(val);
}
},
handleActiveChange(activeIndex) {
if (activeIndex === 0) {
this.type = "cn";
} else {
this.type = "en";
}
},
handleDel() {
this.$refs.inputBox.del();
},
finish() {
this.$emit("input", this.val);
this.visible = false;
}
}
}
</script>
<style lang="less" scoped>
.vnp-header {
height: 40px;
padding-top: 6px;
position: relative;
.vnp-btn-finish {
position: absolute;
right: 0;
height: 100%;
padding: 0 16px;
color: #576b95;
font-size: 14px;
background-color: transparent;
border: none;
cursor: pointer;
}
}
.vnp-input-box-outer {
width: 82%;
max-width: 600px;
margin: 0 auto;
padding: 10px;
}
.vnp-keys {
padding: 3px;
background: #f2f3f5;
padding-bottom: 22px;
.vnp-keys-row {
display: flex;
justify-content: center;
}
.vnp-btn-key-wrapper {
flex: 0 1 calc((100% - 6px * 10) / 10);
padding: 3px;
box-sizing: content-box;
&.vnp-del-wrapper,
&.vnp-type-wrapper {
flex: 1;
}
&.vnp-type-wrapper {
.vnp-smaller {
color: #999;
font-size: 12px;
}
}
.vnp-btn-key {
padding: 0;
width: 100%;
border-radius: 4px;
}
.vnp-btn-empty {
background: transparent;
border: none;
}
.vnp-delete-icon {
width: 18px;
vertical-align: middle;
}
}
}
</style>>