uni-app微信小程序车牌号输入组件实现

uni-app 微信小程序实现车牌号输入功能

在移动端开发中,车牌号输入是一个常见的需求。本文将介绍如何在 uni-app 微信小程序中实现一个功能完善的车牌号输入组件,支持燃油车和新能源车两种格式。


1. 实现逻辑

1.1 需求分析

车牌号输入需要考虑以下关键点:

需求点 说明
格式差异 燃油车7位,新能源车6位
字符类型 第1位为省份简称,第2位为字母,后续为字母或数字
自定义键盘 需要自定义输入键盘,按位置切换不同字符集
交互体验 点击输入框弹出键盘,自动跳转下一位,输入完成自动关闭

1.2 架构设计

复制代码
┌─────────────────────────────────────────────────────────┐
│                    addCarNo 页面                         │
│  ┌──────────────────────────────────────────────────┐   │
│  │ 模式切换:燃油车 / 新能源车                        │   │
│  └──────────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────────┐   │
│  │           PlateInput 组件                         │   │
│  │  ┌─────────────────────────────────────────────┐  │   │
│  │  │ 车牌显示区域:京 · A · 1 · 2 · 3 · 4 · 5    │  │   │
│  │  └─────────────────────────────────────────────┘  │   │
│  │  ┌─────────────────────────────────────────────┐  │   │
│  │  │ 自定义键盘(底部弹出)                       │  │   │
│  │  │ - 省份键盘:京、津、沪、粤...                 │  │   │
│  │  │ - 字母键盘:A、B、C...Z                      │  │   │
│  │  │ - 混合键盘:1、2...0、A、B...Z               │  │   │
│  │  └─────────────────────────────────────────────┘  │   │
│  └──────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

1.3 核心逻辑

1.3.1 键盘切换逻辑

根据输入位置自动切换键盘类型:

typescript 复制代码
function updateKeyboardType(index: number) {
    switch (index) {
        case 0: // 第1位:省份简称
            keyboardType.value = 'province'
            break
        case 1: // 第2位:字母位
            keyboardType.value = 'letter'
            break
        default: // 第3位及以后:混合键盘
            keyboardType.value = 'mixed'
            break
    }
}
1.3.2 输入处理逻辑
typescript 复制代码
function updatePlateNumber(char: string, index: number) {
    const maxLength = props.isElectric ? 6 : 7
    const actualIndex = getActualIndex(index)
    
    // 检查是否达到最大长度
    if (plateNumber.value.length >= maxLength && actualIndex >= plateNumber.value.length) {
        return
    }

    // 更新车牌号
    const currentPlate = plateNumber.value
    let newPlate = ''
    
    if (actualIndex < currentPlate.length) {
        // 替换已有字符
        newPlate = currentPlate.slice(0, actualIndex) + char + currentPlate.slice(actualIndex + 1)
    } else {
        // 添加新字符
        newPlate = currentPlate + char
    }
    
    plateNumber.value = newPlate

    // 自动跳转到下一位
    const nextIndex = findNextCharIndex(index)
    if (nextIndex !== -1) {
        activeIndex.value = nextIndex
        updateKeyboardType(nextIndex)
    }
}
1.3.3 模式切换处理

当切换燃油车/新能源车模式时,需要调整输入框位数:

typescript 复制代码
watch(() => props.isElectric, (newVal) => {
    const maxLength = newVal ? 6 : 7
    // 如果当前输入超过新模式的最大长度,截断
    if (plateNumber.value.length > maxLength) {
        plateNumber.value = plateNumber.value.slice(0, maxLength)
    }
    activeIndex.value = Math.min(activeIndex.value, plateArray.value.length - 1)
})

2. 具体代码

2.1 PlateInput 组件

vue 复制代码
<template>
    <view class="plate-input-container">
        <!-- 车牌号显示区域 -->
        <view class="plate-display">
            <view class="plate-input-items">
                <view v-for="(item, index) in plateArray" :key="index"
                    :class="['plate-item', { 'active': activeIndex === index, 'dot': item.type === 'dot' }]"
                    @click="focusInput(index)">
                    <template v-if="item.type === 'char'">
                        <text class="plate-char">{{ item.value || ' ' }}</text>
                    </template>
                    <template v-else>
                        <text class="dot-text">{{ item.value }}</text>
                    </template>
                </view>
            </view>
        </view>

        <!-- 自定义键盘 - 从底部弹出 -->
        <view class="keyboard-overlay" v-if="showKeyboard" @click="handleBlur">
            <view class="keyboard-container" @click.stop>
                <!-- 键盘标题 -->
                <view class="keyboard-header">
                    <text class="keyboard-title">{{ keyboardTitle }}</text>
                    <view class="keyboard-close" @click="handleBlur">✕</view>
                </view>
                
                <!-- 键盘内容 -->
                <view v-for="(row, rowIndex) in keyboardRows" :key="rowIndex" class="keyboard-row">
                    <view v-for="key in row" :key="key" class="keyboard-key" @click="handleKeyPress(key)">
                        <text>{{ key }}</text>
                    </view>
                </view>
                
                <!-- 功能键行 -->
                <view class="keyboard-row">
                    <view class="keyboard-key keyboard-key-delete" @click="handleDelete">
                        <text>删除</text>
                    </view>
                    <view class="keyboard-key keyboard-key-confirm" @click="handleConfirm">
                        <text>完成</text>
                    </view>
                </view>
            </view>
        </view>
    </view>
</template>

<script setup lang="ts">
import { ref, computed, watch } from 'vue'

const props = defineProps({
    modelValue: {
        type: String,
        default: ''
    },
    placeholder: {
        type: String,
        default: '请输入车牌号'
    },
    isElectric: {
        type: Boolean,
        default: false
    }
})

const emit = defineEmits(['update:modelValue', 'confirm'])

const plateNumber = ref(props.modelValue)
const activeIndex = ref(0)
const showKeyboard = ref(false)

// 省份键盘布局
const provinceKeyboard = [
    ['京', '津', '沪', '粤', '苏', '浙', '鲁', '川'],
    ['渝', '冀', '豫', '湘', '鄂', '赣', '闽', '陕'],
    ['甘', '晋', '辽', '吉', '黑', '皖', '琼', '贵'],
    ['云', '蒙', '桂', '宁', '新', '藏', '青']
]

// 字母键盘布局
const letterKeyboard = [
    ['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 mixedKeyboard = [
    ['1', '2', '3', '4', '5', '6', '7', '8'],
    ['9', '0', '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 keyboardType = ref('province')

// 当前键盘布局
const keyboardRows = computed(() => {
    switch (keyboardType.value) {
        case 'province':
            return provinceKeyboard
        case 'letter':
            return letterKeyboard
        case 'mixed':
            return mixedKeyboard
        default:
            return provinceKeyboard
    }
})

// 键盘标题
const keyboardTitle = computed(() => {
    switch (keyboardType.value) {
        case 'province':
            return '请选择省份简称'
        case 'letter':
            return '请选择字母'
        case 'mixed':
            return '请输入数字或字母'
        default:
            return ''
    }
})

// 车牌号数组(用于显示)
const plateArray = computed(() => {
    const plate = plateNumber.value
    const array = []
    const totalLength = props.isElectric ? 6 : 7

    for (let i = 0; i < totalLength + 1; i++) {
        if (i === 2) {
            array.push({ type: 'dot', value: '·' })
        } else if (i < 2) {
            array.push({ type: 'char', value: plate[i] || '' })
        } else {
            const actualIndex = i - 1
            if (actualIndex < totalLength) {
                array.push({ type: 'char', value: plate[actualIndex] || '' })
            }
        }
    }
    return array
})

// 监听外部传入的modelValue
watch(() => props.modelValue, (newVal) => {
    plateNumber.value = newVal
})

// 监听外部传入的isElectric变化
watch(() => props.isElectric, (newVal) => {
    const maxLength = newVal ? 6 : 7
    if (plateNumber.value.length > maxLength) {
        plateNumber.value = plateNumber.value.slice(0, maxLength)
    }
    activeIndex.value = Math.min(activeIndex.value, plateArray.value.length - 1)
})

// 监听内部plateNumber变化
watch(plateNumber, (newVal) => {
    emit('update:modelValue', newVal)
})

function focusInput(index: number) {
    if (plateArray.value[index]?.type === 'dot') {
        const nextIndex = findNextCharIndex(index)
        if (nextIndex !== -1) {
            activeIndex.value = nextIndex
        }
        return
    }

    activeIndex.value = index
    showKeyboard.value = true

    switch (index) {
        case 0:
            keyboardType.value = 'province'
            break
        case 1:
            keyboardType.value = 'letter'
            break
        default:
            keyboardType.value = 'mixed'
            break
    }
}

function handleBlur() {
    activeIndex.value = -1
    showKeyboard.value = false
}

function handleKeyPress(key: string) {
    if (activeIndex.value === -1) return
    updatePlateNumber(key, activeIndex.value)
}

function handleDelete() {
    const currentPlate = plateNumber.value
    const maxLength = props.isElectric ? 6 : 7

    if (currentPlate.length > 0) {
        const newPlate = currentPlate.slice(0, -1)
        plateNumber.value = newPlate

        if (newPlate.length > 0) {
            const newLastCharIndex = newPlate.length - 1
            let displayIndex = newLastCharIndex
            if (displayIndex >= 2) {
                displayIndex = displayIndex + 1
            }
            activeIndex.value = displayIndex
            updateKeyboardType(displayIndex)
        } else {
            activeIndex.value = 0
            updateKeyboardType(0)
        }
    }
}

function updatePlateNumber(char: string, index: number) {
    const maxLength = props.isElectric ? 6 : 7
    const actualIndex = getActualIndex(index)
    
    if (plateNumber.value.length >= maxLength && actualIndex >= plateNumber.value.length) {
        return
    }

    const currentPlate = plateNumber.value
    let newPlate = ''
    
    if (actualIndex < currentPlate.length) {
        newPlate = currentPlate.slice(0, actualIndex) + char + currentPlate.slice(actualIndex + 1)
    } else {
        newPlate = currentPlate + char
    }
    
    plateNumber.value = newPlate

    const nextIndex = findNextCharIndex(index)
    if (nextIndex !== -1) {
        activeIndex.value = nextIndex
        updateKeyboardType(nextIndex)
    }
}

function getActualIndex(displayIndex: number) {
    if (displayIndex < 2) {
        return displayIndex
    } else if (displayIndex > 2) {
        return displayIndex - 1
    }
    return -1
}

function updateKeyboardType(index: number) {
    switch (index) {
        case 0:
            keyboardType.value = 'province'
            break
        case 1:
            keyboardType.value = 'letter'
            break
        default:
            keyboardType.value = 'mixed'
            break
    }
}

function handleConfirm() {
    emit('confirm', plateNumber.value)
    showKeyboard.value = false
}

function findNextCharIndex(currentIndex: number) {
    for (let i = currentIndex + 1; i < plateArray.value.length; i++) {
        if (plateArray.value[i]?.type === 'char') {
            return i
        }
    }
    return -1
}
</script>

<style scoped lang="scss">
.plate-input-container {
    width: 100%;
}

.plate-display {
    width: 100%;
    background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
    border-radius: 12px;
    padding: 16rpx;
    box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.2);
}

.plate-input-items {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    gap: 4rpx;
}

.plate-item {
    width: 72rpx;
    height: 88rpx;
    border: 2rpx solid rgba(255, 255, 255, 0.3);
    border-radius: 8rpx;
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    background-color: rgba(255, 255, 255, 0.05);
    transition: all 0.3s ease;
}

.plate-item.active {
    border-color: #f39c12;
    background-color: rgba(243, 156, 18, 0.15);
    box-shadow: 0 0 12rpx rgba(243, 156, 18, 0.4);
}

.plate-item.dot {
    border: none;
    width: 32rpx;
    background-color: transparent;
}

.plate-char {
    font-size: 32rpx;
    font-weight: 600;
    color: #ffffff;
    font-family: 'DIN Alternate', 'Arial Black', sans-serif;
}

.dot-text {
    font-size: 28rpx;
    font-weight: bold;
    color: rgba(255, 255, 255, 0.4);
}

.keyboard-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(0, 0, 0, 0.5);
    z-index: 9999;
    display: flex;
    align-items: flex-end;
}

.keyboard-container {
    width: 100%;
    background-color: #ffffff;
    padding: 16rpx;
    border-top-left-radius: 24rpx;
    border-top-right-radius: 24rpx;
    max-width: 500px;
    box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.15);
}

.keyboard-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 16rpx 24rpx;
    border-bottom: 1rpx solid #f0f0f0;
    margin-bottom: 16rpx;
    
    .keyboard-title {
        font-size: 28rpx;
        color: #333;
        font-weight: 500;
    }
    
    .keyboard-close {
        font-size: 32rpx;
        color: #999;
        padding: 8rpx;
    }
}

.keyboard-row {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    margin-bottom: 8rpx;
}

.keyboard-key {
    flex: 1;
    height: 80rpx;
    background-color: #ffffff;
    border-radius: 12rpx;
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    margin: 6rpx 4rpx;
    border: 1rpx solid #e8e8e8;
    font-size: 30rpx;
    font-weight: 500;
    color: #333;
    transition: all 0.2s ease;
    
    &:active {
        background-color: #f0f0f0;
        transform: scale(0.95);
    }
}

.keyboard-key-delete {
    background-color: #f8f9fa;
    color: #666;
    flex: 1.2;
    font-size: 36rpx;
}

.keyboard-key-confirm {
    flex: 1.2;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: #ffffff;
    border-color: transparent;
    
    &:active {
        opacity: 0.9;
    }
}
</style>

2.2 addCarNo 页面

vue 复制代码
<template>
  <view class="page-container">
    <!-- 顶部装饰区域 -->
    <view class="header">
      <view class="header-bg"></view>
      <view class="header-content">
        <view class="logo-icon">🚗</view>
        <view class="title">添加车牌</view>
        <view class="subtitle">请输入您的车牌号码</view>
      </view>
    </view>
    
    <!-- 车牌输入卡片 -->
    <view class="card">
      <view class="card-header">
        <view class="icon-wrapper">📋</view>
        <view class="card-title">车牌信息</view>
      </view>
      
      <!-- 模式切换 -->
      <view class="mode-switch">
        <view 
          :class="['mode-btn', { active: !isElectric }]" 
          @click="isElectric = false"
        >
          🚗 燃油车
        </view>
        <view 
          :class="['mode-btn', { active: isElectric }]" 
          @click="isElectric = true"
        >
          ⚡ 新能源车
        </view>
      </view>
      
      <view class="input-area">
        <PlateInput v-model="plateNumber" :isElectric="isElectric" />
      </view>
      
      <view class="tip-text">
        <text class="tip-icon">💡</text>
        <text>{{ isElectric ? '新能源车车牌格式,如:京A123456' : '燃油车车牌格式,如:京A12345' }}</text>
      </view>
    </view>
    
    <!-- 操作按钮 -->
    <view class="btn-area">
      <view class="submit-btn" @click="submitPlate">
        <text class="btn-icon">✓</text>
        <text>确认添加</text>
      </view>
    </view>
    
    <!-- 底部装饰 -->
    <view class="footer-decoration">
      <view class="deco-circle deco-1"></view>
      <view class="deco-circle deco-2"></view>
      <view class="deco-circle deco-3"></view>
    </view>
  </view>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import PlateInput from '@/components/PlateInput/index.vue';

const plateNumber = ref('')
const isElectric = ref(false)

function submitPlate() {
  const requiredLength = isElectric.value ? 6 : 7
  
  if (!plateNumber.value) {
    uni.showToast({
      title: '请输入车牌号码',
      icon: 'none'
    })
    return
  }
  
  if (plateNumber.value.length !== requiredLength) {
    const typeText = isElectric.value ? '新能源车' : '燃油车'
    uni.showToast({
      title: `${typeText}车牌需输入${requiredLength}位`,
      icon: 'none'
    })
    return
  }
  
  uni.showToast({
    title: `车牌 ${plateNumber.value} 添加成功`,
    icon: 'success'
  })
}
</script>

<style lang="scss">
.page-container {
  min-height: 100vh;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
  padding: 0 32rpx 60rpx;
  box-sizing: border-box;
  position: relative;
  overflow: hidden;
}

.header {
  padding: 80rpx 32rpx 60rpx;
  position: relative;
  
  .header-bg {
    position: absolute;
    top: -100rpx;
    right: -50rpx;
    width: 400rpx;
    height: 400rpx;
    background: rgba(255, 255, 255, 0.1);
    border-radius: 50%;
    filter: blur(40px);
  }
  
  .header-content {
    position: relative;
    z-index: 1;
    text-align: center;
  }
  
  .logo-icon {
    font-size: 80rpx;
    margin-bottom: 20rpx;
    animation: bounce 2s ease-in-out infinite;
  }
  
  .title {
    font-size: 48rpx;
    font-weight: 700;
    color: #fff;
    margin-bottom: 12rpx;
    text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2);
  }
  
  .subtitle {
    font-size: 28rpx;
    color: rgba(255, 255, 255, 0.85);
  }
}

.card {
  background: #fff;
  border-radius: 24rpx;
  padding: 40rpx;
  box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.15);
  margin-bottom: 40rpx;
  
  .card-header {
    display: flex;
    align-items: center;
    margin-bottom: 24rpx;
  }
  
  .mode-switch {
    display: flex;
    gap: 16rpx;
    margin-bottom: 24rpx;
    justify-content: center;
  }
  
  .mode-btn {
    flex: 1;
    height: 72rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 36rpx;
    font-size: 28rpx;
    color: #666;
    background: #f5f5f5;
    border: 2rpx solid #e0e0e0;
    transition: all 0.3s ease;
    
    &.active {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: #fff;
      border-color: transparent;
      box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.4);
    }
  }
  
  .icon-wrapper {
    width: 64rpx;
    height: 64rpx;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    border-radius: 16rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 32rpx;
    margin-right: 16rpx;
  }
  
  .card-title {
    font-size: 34rpx;
    font-weight: 600;
    color: #333;
  }
  
  .input-area {
    padding: 20rpx;
    background: #f8f9fa;
    border-radius: 16rpx;
  }
  
  .tip-text {
    display: flex;
    align-items: center;
    margin-top: 24rpx;
    padding: 16rpx 20rpx;
    background: #e8f5e9;
    border-radius: 12rpx;
    font-size: 26rpx;
    color: #4caf50;
    
    .tip-icon {
      font-size: 28rpx;
      margin-right: 12rpx;
    }
  }
}

.btn-area {
  padding: 0 16rpx;
  
  .submit-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 96rpx;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    border-radius: 48rpx;
    box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.4);
    transition: all 0.3s ease;
    
    &:active {
      transform: scale(0.98);
      box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
    }
    
    .btn-icon {
      font-size: 36rpx;
      color: #fff;
      margin-right: 12rpx;
    }
    
    text:last-child {
      font-size: 32rpx;
      font-weight: 600;
      color: #fff;
    }
  }
}

.footer-decoration {
  position: fixed;
  bottom: -60rpx;
  left: 0;
  right: 0;
  height: 200rpx;
  pointer-events: none;
  
  .deco-circle {
    position: absolute;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.08);
    filter: blur(30px);
  }
  
  .deco-1 {
    width: 200rpx;
    height: 200rpx;
    left: 50rpx;
    bottom: 20rpx;
  }
  
  .deco-2 {
    width: 150rpx;
    height: 150rpx;
    right: 100rpx;
    bottom: 40rpx;
  }
  
  .deco-3 {
    width: 120rpx;
    height: 120rpx;
    left: 50%;
    bottom: 0;
    transform: translateX(-50%);
  }
}

@keyframes bounce {
  0%, 100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-20rpx);
  }
}
</style>

3. 总结

本文介绍了如何在 uni-app 微信小程序中实现车牌号输入功能,核心要点包括:

  1. 自定义键盘:根据输入位置动态切换省份、字母、混合三种键盘
  2. 格式支持:同时支持燃油车(7位)和新能源车(6位)两种格式
  3. 交互优化:点击输入框弹出键盘,自动跳转下一位,输入完成自动关闭
  4. 视觉设计 :深色车牌样式,金色高亮选中框,渐变按钮等现代化设计
相关推荐
h_65432103 小时前
uniapp的app/h5实现地图连续定位
uni-app
真的不想写实验3 小时前
uniapp上传文件的载荷是个空对象
前端·uni-app
乌托邦16 小时前
uni-mini-ci:让 uniapp 小程序构建后自动预览和上传
前端·vue.js·uni-app
敲代码的鱼17 小时前
NFC读卡能力 支持安卓/iOS/鸿蒙 UTS插件
android·ios·uni-app
客场消音器18 小时前
如何使用codex进行UI重构,让AI开发的前端页面不再千篇一律
前端·后端·微信小程序
西洼工作室21 小时前
UniApp云开发笔记
前端·笔记·uni-app
Martin -Tang1 天前
uniapp 实现录音操作,长按录音,放开取消
前端·javascript·vue.js·uni-app·css3·录音
打瞌睡的朱尤1 天前
微信小程序126~160
微信小程序·小程序
腾讯云云开发1 天前
小程序成长计划正式接入Hy3 preview
微信小程序