|
|
@@ -0,0 +1,392 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import { computed, ref, watch } from 'vue'
|
|
|
+
|
|
|
+// Props 定义
|
|
|
+const props = defineProps({
|
|
|
+ plateNumber: {
|
|
|
+ type: Array as () => (string | number)[],
|
|
|
+ default: () => Array.from({ length: 8 }, () => ''),
|
|
|
+ },
|
|
|
+})
|
|
|
+
|
|
|
+// Emits 定义
|
|
|
+const emit = defineEmits<{
|
|
|
+ myPlateChange: [plateNumber: (string | number)[]]
|
|
|
+}>()
|
|
|
+
|
|
|
+// 响应式数据
|
|
|
+const show = ref(false)
|
|
|
+const index = ref(-1)
|
|
|
+const areaDatas: string[] = [
|
|
|
+ '京',
|
|
|
+ '津',
|
|
|
+ '渝',
|
|
|
+ '沪',
|
|
|
+ '冀',
|
|
|
+ '晋',
|
|
|
+ '辽',
|
|
|
+ '吉',
|
|
|
+ '黑',
|
|
|
+ '苏',
|
|
|
+ '浙',
|
|
|
+ '皖',
|
|
|
+ '闽',
|
|
|
+ '赣',
|
|
|
+ '鲁',
|
|
|
+ '豫',
|
|
|
+ '鄂',
|
|
|
+ '湘',
|
|
|
+ '粤',
|
|
|
+ '琼',
|
|
|
+ '川',
|
|
|
+ '贵',
|
|
|
+ '云',
|
|
|
+ '陕',
|
|
|
+ '甘',
|
|
|
+ '青',
|
|
|
+ '蒙',
|
|
|
+ '桂',
|
|
|
+ '宁',
|
|
|
+ '新',
|
|
|
+ '藏',
|
|
|
+ '使',
|
|
|
+ '领',
|
|
|
+ '',
|
|
|
+ '',
|
|
|
+ '',
|
|
|
+ '',
|
|
|
+ '',
|
|
|
+ '',
|
|
|
+]
|
|
|
+const characterDatas: (number | string)[] = [
|
|
|
+ 0,
|
|
|
+ 1,
|
|
|
+ 2,
|
|
|
+ 3,
|
|
|
+ 4,
|
|
|
+ 5,
|
|
|
+ 6,
|
|
|
+ 7,
|
|
|
+ 8,
|
|
|
+ 9,
|
|
|
+ 'A',
|
|
|
+ 'B',
|
|
|
+ 'C',
|
|
|
+ 'D',
|
|
|
+ 'E',
|
|
|
+ 'F',
|
|
|
+ 'G',
|
|
|
+ 'H',
|
|
|
+ 'J',
|
|
|
+ 'K',
|
|
|
+ 'L',
|
|
|
+ 'M',
|
|
|
+ 'N',
|
|
|
+ 'P',
|
|
|
+ 'Q',
|
|
|
+ 'R',
|
|
|
+ 'S',
|
|
|
+ 'T',
|
|
|
+ 'U',
|
|
|
+ 'V',
|
|
|
+ 'W',
|
|
|
+ 'X',
|
|
|
+ 'Y',
|
|
|
+ 'Z',
|
|
|
+ '挂',
|
|
|
+ '警',
|
|
|
+ '学',
|
|
|
+ '港',
|
|
|
+ '澳',
|
|
|
+]
|
|
|
+
|
|
|
+// 内部车牌数组(用于本地操作)
|
|
|
+const localPlateNumber = ref<(string | number)[]>([...props.plateNumber])
|
|
|
+
|
|
|
+// 计算属性
|
|
|
+const currentDatas = computed(() => {
|
|
|
+ return index.value === 0 ? areaDatas : characterDatas
|
|
|
+})
|
|
|
+
|
|
|
+// 方法
|
|
|
+function handleChange(idx: number) {
|
|
|
+ index.value = idx
|
|
|
+ show.value = true
|
|
|
+}
|
|
|
+
|
|
|
+function handleClickKeyBoard(item: string | number, idx: number) {
|
|
|
+ // 索引为1时不能选择数字(0-9)
|
|
|
+ if (index.value === 1 && idx < 10)
|
|
|
+ return
|
|
|
+ // 索引2-5时不能选择特殊字符(索引>33)
|
|
|
+ if (index.value > 1 && index.value < 6 && idx > 33)
|
|
|
+ return
|
|
|
+
|
|
|
+ if (index.value < 8) {
|
|
|
+ localPlateNumber.value[index.value] = item
|
|
|
+ emit('myPlateChange', [...localPlateNumber.value])
|
|
|
+ }
|
|
|
+ if (index.value < 7) {
|
|
|
+ index.value++
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 重置
|
|
|
+function handleReset() {
|
|
|
+ index.value = 0
|
|
|
+ for (let i = 0; i < 8; i++) {
|
|
|
+ localPlateNumber.value[i] = ''
|
|
|
+ }
|
|
|
+ emit('myPlateChange', [...localPlateNumber.value])
|
|
|
+}
|
|
|
+
|
|
|
+// 删除
|
|
|
+function handleDelete() {
|
|
|
+ localPlateNumber.value[index.value] = ''
|
|
|
+ emit('myPlateChange', [...localPlateNumber.value])
|
|
|
+ if (index.value > 0) {
|
|
|
+ index.value--
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 获取完整车牌号字符串
|
|
|
+function getPlateString() {
|
|
|
+ let plate = ''
|
|
|
+ localPlateNumber.value.forEach((item, idx) => {
|
|
|
+ if (idx === 1) {
|
|
|
+ plate = `${plate}${item}·`
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ plate += item
|
|
|
+ }
|
|
|
+ })
|
|
|
+ return plate
|
|
|
+}
|
|
|
+
|
|
|
+// 车牌号格式校验
|
|
|
+function validatePlateNumber() {
|
|
|
+ const plateNumber = getPlateString().replace(/·/g, '')
|
|
|
+
|
|
|
+ if (!plateNumber) {
|
|
|
+ return { valid: false, message: '请输入车牌号' }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 长度校验:普通车牌7位,新能源8位
|
|
|
+ if (plateNumber.length < 7) {
|
|
|
+ return { valid: false, message: '车牌号长度不足,请输入完整车牌号' }
|
|
|
+ }
|
|
|
+ if (plateNumber.length > 8) {
|
|
|
+ return { valid: false, message: '车牌号长度超出限制' }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 第一位必须是省份简称
|
|
|
+ const firstChar = plateNumber.charAt(0)
|
|
|
+ if (!areaDatas.includes(firstChar)) {
|
|
|
+ return { valid: false, message: '车牌号首位必须是省份简称' }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 第二位必须是大写字母
|
|
|
+ const secondChar = plateNumber.charAt(1)
|
|
|
+ if (!/^[A-Z]$/.test(secondChar)) {
|
|
|
+ return { valid: false, message: '车牌号第二位必须是字母' }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 普通车牌正则:省份+字母+5位字母数字(含特殊车牌字符)
|
|
|
+ const normalPlateReg = /^[\u4E00-\u9FA5][A-Z][A-Z0-9]{4}[A-Z0-9挂港澳使领警]$/
|
|
|
+ // 新能源车牌正则:省份+字母+6位字母数字
|
|
|
+ const newEnergyPlateReg = /^[\u4E00-\u9FA5][A-Z][A-Z0-9]{6}$/
|
|
|
+
|
|
|
+ if (!normalPlateReg.test(plateNumber) && !newEnergyPlateReg.test(plateNumber)) {
|
|
|
+ return { valid: false, message: '车牌号格式不正确' }
|
|
|
+ }
|
|
|
+
|
|
|
+ return { valid: true, message: '' }
|
|
|
+}
|
|
|
+
|
|
|
+// 暴露方法给父组件
|
|
|
+defineExpose({
|
|
|
+ validatePlateNumber,
|
|
|
+ getPlateString,
|
|
|
+ reset: handleReset,
|
|
|
+})
|
|
|
+
|
|
|
+// 监听 props 变化同步到本地
|
|
|
+watch(() => props.plateNumber, (newVal) => {
|
|
|
+ localPlateNumber.value = [...newVal]
|
|
|
+}, { deep: true })
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <view>
|
|
|
+ <view class="plate" :class="{ show }">
|
|
|
+ <view class="item" :class="{ active: index === 0 }" @click="handleChange(0)">
|
|
|
+ {{ localPlateNumber[0] }}
|
|
|
+ <text class="triangle" />
|
|
|
+ </view>
|
|
|
+ <view class="item" :class="{ active: index === 1 }" @click="handleChange(1)">
|
|
|
+ {{ localPlateNumber[1] }}
|
|
|
+ </view>
|
|
|
+ <view class="point">
|
|
|
+ ●
|
|
|
+ </view>
|
|
|
+ <view class="item" :class="{ active: index === 2 }" @click="handleChange(2)">
|
|
|
+ {{ localPlateNumber[2] }}
|
|
|
+ </view>
|
|
|
+ <view class="item" :class="{ active: index === 3 }" @click="handleChange(3)">
|
|
|
+ {{ localPlateNumber[3] }}
|
|
|
+ </view>
|
|
|
+ <view class="item" :class="{ active: index === 4 }" @click="handleChange(4)">
|
|
|
+ {{ localPlateNumber[4] }}
|
|
|
+ </view>
|
|
|
+ <view class="item" :class="{ active: index === 5 }" @click="handleChange(5)">
|
|
|
+ {{ localPlateNumber[5] }}
|
|
|
+ </view>
|
|
|
+ <view class="item" :class="{ active: index === 6 }" @click="handleChange(6)">
|
|
|
+ {{ localPlateNumber[6] }}
|
|
|
+ </view>
|
|
|
+ <view class="item new-energy" :class="{ active: index === 7 }" @click="handleChange(7)">
|
|
|
+ <view v-if="localPlateNumber[7] || localPlateNumber[7] === 0">
|
|
|
+ <text>{{ localPlateNumber[7] }}</text>
|
|
|
+ </view>
|
|
|
+ <wd-icon v-else name="add" size="13px" color="#9ED605" />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <section class="panel" :class="{ show }">
|
|
|
+ <view class="header">
|
|
|
+ <view @click="handleReset">
|
|
|
+ 重置
|
|
|
+ </view>
|
|
|
+ <view @click="show = false">
|
|
|
+ 完成
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="panelList">
|
|
|
+ <view v-for="(item, idx) of currentDatas" :key="idx" class="item">
|
|
|
+ <view
|
|
|
+ v-if="item !== ''"
|
|
|
+ :class="{ disabled: (index === 1 && idx < 10) || (index > 1 && index < 6 && idx > 33) }"
|
|
|
+ @click="handleClickKeyBoard(item, idx)"
|
|
|
+ >
|
|
|
+ {{ item }}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="item backspace" :class="{ special: index === 0 }" @click="handleDelete">
|
|
|
+ ×
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </section>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped lang='less'>
|
|
|
+ .plate {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ .item {
|
|
|
+ width: 64rpx;
|
|
|
+ height: 80rpx;
|
|
|
+ background-color: #F3F4F7;
|
|
|
+ border-radius: 8rpx;
|
|
|
+ text-align: center;
|
|
|
+ line-height: 80rpx;
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: rgba(0,0,0,0.90);
|
|
|
+ font-weight: bold;
|
|
|
+ position: relative;
|
|
|
+ &.active {
|
|
|
+ background-color: #bbbbbb;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .new-energy {
|
|
|
+ box-sizing: border-box;
|
|
|
+ border: 2rpx dashed #9ED605;
|
|
|
+ font-weight: bold;
|
|
|
+ uni-icons {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .point {
|
|
|
+ height: 80rpx;
|
|
|
+ text-align: center;
|
|
|
+ line-height: 80rpx;
|
|
|
+ color: #BDC4CC;
|
|
|
+ font-size: 18rpx;
|
|
|
+ }
|
|
|
+ .triangle {
|
|
|
+ width: 0;
|
|
|
+ height: 0;
|
|
|
+ border: 6rpx solid transparent;
|
|
|
+ border-right-color: #00C69D;
|
|
|
+ border-bottom-color: #00C69D;
|
|
|
+ border-radius: 1rpx 2rpx 1rpx;
|
|
|
+ position: absolute;
|
|
|
+ right: 6rpx;
|
|
|
+ bottom: 6rpx;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .panel {
|
|
|
+ position: fixed;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ bottom: 0;
|
|
|
+ z-index: 999;
|
|
|
+ box-sizing: border-box;
|
|
|
+ background-color: #F5F5F5;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ transform: translateY(100%);
|
|
|
+ &.show {
|
|
|
+ transform: translateX(0);
|
|
|
+ }
|
|
|
+ .header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 0 24rpx;
|
|
|
+ height: 96rpx;
|
|
|
+ color: #9ED605;
|
|
|
+ font-size: 34rpx;
|
|
|
+ }
|
|
|
+ .panelList {
|
|
|
+ padding: 0 19rpx 20rpx;
|
|
|
+ .item {
|
|
|
+ display: inline-block;
|
|
|
+ width: calc(~"(100% - 72rpx) / 10");
|
|
|
+ height: 84rpx;
|
|
|
+ margin-right: 8rpx;
|
|
|
+ margin-bottom: 8rpx;
|
|
|
+ vertical-align: top;
|
|
|
+ view {
|
|
|
+ width: 100%;
|
|
|
+ height: 84rpx;
|
|
|
+ line-height: 84rpx;
|
|
|
+ border-radius: 6rpx;
|
|
|
+ background: #FEFFFE;
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: rgba(0,0,0,0.90);
|
|
|
+ font-weight: bold;
|
|
|
+ text-align: center;
|
|
|
+ &.disabled {
|
|
|
+ background-color: rgba(254, 255, 254, 0.6);
|
|
|
+ color: rgba(0, 0, 0, 0.23);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ &:nth-of-type(10n) {
|
|
|
+ margin-right: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .backspace {
|
|
|
+ vertical-align: top;
|
|
|
+ font-size: 48rpx;
|
|
|
+ font-weight: bold;
|
|
|
+ text-align: center;
|
|
|
+ height: 84rpx;
|
|
|
+ line-height: 84rpx;
|
|
|
+ border-radius: 6rpx;
|
|
|
+ background: #FEFFFE;
|
|
|
+ color: rgba(0,0,0,0.90);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+</style>
|