mao-mobile 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,86 @@
1
+ # ProPlateKeyboard 车牌输入键盘组件
2
+
3
+ ## 组件介绍
4
+
5
+ ProPlateKeyboard 是一个专为车牌输入场景设计的虚拟键盘组件,支持普通车牌、新能源车牌输入,内置类型切换、输入高亮、删除、完成等功能,适用于需要车牌输入的表单或弹窗。
6
+
7
+ ## 功能特点
8
+
9
+ - 支持普通车牌与新能源车牌类型切换
10
+ - 车牌输入高亮、自动切换输入框
11
+ - 虚拟键盘支持省份、字母、数字、特殊字符
12
+ - 操作按钮:取消、删除、完成
13
+ - 主题色可自定义
14
+ - 可与 ProPopup 弹窗组件配合使用
15
+
16
+ ## 属性(Props)
17
+
18
+ | 属性名 | 类型 | 默认值 | 说明 |
19
+ | ---------- | ------- | --------- | -------------------------- |
20
+ | modelValue | String | "" | 车牌初始值(支持 v-model) |
21
+ | maskClick | Boolean | false | 点击遮罩层是否关闭弹窗 |
22
+ | themeColor | String | '#0BC8C8' | 主题色 |
23
+
24
+ ## 事件(Events)
25
+
26
+ | 事件名 | 说明 | 回调参数 |
27
+ | ----------------- | ------------------ | ---------- |
28
+ | update:modelValue | 车牌输入完成时触发 | 车牌字符串 |
29
+ | change | 车牌输入完成时触发 | 车牌字符串 |
30
+
31
+ ## 方法(Expose)
32
+
33
+ | 方法名 | 说明 |
34
+ | ------ | ------------ |
35
+ | open | 打开键盘弹窗 |
36
+
37
+ ## 使用示例
38
+
39
+ ```vue
40
+ <template>
41
+ <ProPlateKeyboard
42
+ v-model="plateNumber"
43
+ :theme-color="'#0BC8C8'"
44
+ :maskClick="true"
45
+ ref="plateKeyboardRef"
46
+ @change="onPlateChange"
47
+ />
48
+ <button @click="openKeyboard">打开车牌键盘</button>
49
+ </template>
50
+
51
+ <script setup>
52
+ import { ref } from "vue";
53
+ import ProPlateKeyboard from "./index.vue";
54
+
55
+ const plateNumber = ref("");
56
+ const plateKeyboardRef = ref(null);
57
+
58
+ const openKeyboard = () => {
59
+ plateKeyboardRef.value.open();
60
+ };
61
+
62
+ const onPlateChange = (val) => {
63
+ console.log("车牌输入完成:", val);
64
+ };
65
+ </script>
66
+ ```
67
+
68
+ ## 样式定制
69
+
70
+ - `.pro-plate-keyboard`:键盘整体容器
71
+ - `.pro-plate-type`:车牌类型选择区域
72
+ - `.pro-plate-body`:车牌输入框区域
73
+ - `.pro-plate-keyboard-container`:键盘区域
74
+ - `.pro-plate-btn-group`:操作按钮区域
75
+ - `.pro-plate-btn--cancel`:取消按钮
76
+ - `.pro-plate-btn--delete`:删除按钮
77
+ - `.pro-plate-btn--submit`:完成按钮
78
+
79
+ 可通过覆盖这些类名自定义样式。
80
+
81
+ ## 注意事项
82
+
83
+ - 组件默认配合 ProPopup 弹窗使用,需保证 ProPopup 可用。
84
+ - 主题色通过 `themeColor` 属性传递,影响高亮、按钮等样式。
85
+ - 车牌输入长度根据类型自动切换(普通 7 位,新能源 8 位)。
86
+ - 组件内部已处理输入高亮、切换、删除、完成等交互。
@@ -0,0 +1,510 @@
1
+ <template>
2
+ <view>
3
+ <pro-popup
4
+ ref="plateKeyboardPopupRef"
5
+ position="bottom"
6
+ backgroundColor="#eee"
7
+ :maskClick="maskClick"
8
+ borderRadius="0"
9
+ :is-show-header="false"
10
+ @change="handlePopupChange"
11
+ customClass="pro-plate-popup"
12
+ >
13
+ <view class="pro-plate-keyboard">
14
+ <!-- 车牌类型选择 -->
15
+ <view class="pro-plate-type">
16
+ <view
17
+ v-for="item in typeList"
18
+ :key="item.label"
19
+ :class="[
20
+ 'pro-plate-type__item',
21
+ type === item.value
22
+ ? 'pro-plate-type__item--active'
23
+ : 'pro-plate-type__item--unactive',
24
+ ]"
25
+ @tap="handleTypeChange(item.value)"
26
+ >
27
+ <text>{{ item.label }}</text>
28
+ <view
29
+ v-show="type === item.value"
30
+ class="pro-plate-type__underline"
31
+ ></view>
32
+ </view>
33
+ </view>
34
+ <!-- 车牌输入框 -->
35
+ <view class="pro-plate-body">
36
+ <template v-for="(_, index) in plateLength" :key="index">
37
+ <view
38
+ :class="[
39
+ 'pro-plate-word',
40
+ { 'pro-plate-word--active': currentInputIndex === index },
41
+ ]"
42
+ @tap="handleInputSwitch(index)"
43
+ >
44
+ <text>{{ currentInputValue[index] }}</text>
45
+ </view>
46
+ <view v-if="index === 1" class="pro-plate-dot" />
47
+ </template>
48
+ </view>
49
+ <!-- 车牌键盘 -->
50
+ <view class="pro-plate-keys-container">
51
+ <view class="pro-plate-keyboard__keys">
52
+ <template v-if="inputType === 1">
53
+ <view
54
+ class="pro-plate-key"
55
+ v-for="el of provinceText"
56
+ :key="el"
57
+ @tap="handleKeyChoose(el)"
58
+ >
59
+ <view class="pro-plate-key-text">
60
+ <text>{{ el }}</text>
61
+ </view>
62
+ </view>
63
+ </template>
64
+ <template v-if="inputType >= 3">
65
+ <view
66
+ class="pro-plate-key"
67
+ v-for="el of numberText"
68
+ :key="el"
69
+ @tap="handleKeyChoose(el)"
70
+ >
71
+ <view class="pro-plate-key-text">
72
+ <text>{{ el }}</text>
73
+ </view>
74
+ </view>
75
+ </template>
76
+ <template v-if="inputType >= 2">
77
+ <view
78
+ class="pro-plate-key"
79
+ v-for="el of wordText"
80
+ :key="el"
81
+ @tap="handleKeyChoose(el)"
82
+ >
83
+ <view class="pro-plate-key-text">
84
+ <text>{{ el }}</text>
85
+ </view>
86
+ </view>
87
+ </template>
88
+ <template v-if="inputType >= 3">
89
+ <view
90
+ class="pro-plate-key"
91
+ v-for="el of lastWordText"
92
+ :key="el"
93
+ @tap="handleKeyChoose(el, 'lastWordText')"
94
+ :style="{ 'pointer-events': inputType !== 4 ? 'none' : 'auto' }"
95
+ >
96
+ <view
97
+ class="pro-plate-key-text"
98
+ :style="{
99
+ color: inputType !== 4 ? '#ddd' : '',
100
+ }"
101
+ >
102
+ <text>{{ el }}</text>
103
+ </view>
104
+ </view>
105
+ </template>
106
+ </view>
107
+ </view>
108
+ <!-- 操作按钮 -->
109
+ <view class="pro-plate-btn-group">
110
+ <view class="pro-plate-btn-group__left">
111
+ <view
112
+ class="pro-plate-btn pro-plate-btn--cancel"
113
+ @tap="handleClose"
114
+ >
115
+ <text>取消</text>
116
+ </view>
117
+ </view>
118
+ <view class="pro-plate-btn-group__right">
119
+ <view
120
+ class="pro-plate-btn pro-plate-btn--delete"
121
+ @tap="handleDelete"
122
+ >
123
+ <text>删除</text>
124
+ </view>
125
+ <view
126
+ class="pro-plate-btn pro-plate-btn--submit"
127
+ @tap="handleSubmit"
128
+ >
129
+ <text>完成</text>
130
+ </view>
131
+ </view>
132
+ </view>
133
+ </view>
134
+ </pro-popup>
135
+ </view>
136
+ </template>
137
+
138
+ <script setup>
139
+ import { ref, computed } from "vue";
140
+ import ProPopup from "../ProPopup/index.vue";
141
+
142
+ // Props 定义
143
+ const props = defineProps({
144
+ // 默认选中的值
145
+ modelValue: {
146
+ type: String,
147
+ default: "",
148
+ },
149
+ maskClick: {
150
+ type: Boolean,
151
+ default: true,
152
+ },
153
+ // 主题色
154
+ themeColor: {
155
+ type: String,
156
+ default: "#0BC8C8",
157
+ },
158
+ });
159
+
160
+ // Emits 定义
161
+ const emit = defineEmits(["update:modelValue", "change"]);
162
+
163
+ // Refs
164
+ const plateKeyboardPopupRef = ref(null);
165
+ const type = ref("1");
166
+ const currentInputIndex = ref(0);
167
+ const currentInputValue = ref(["", "", "", "", "", "", ""]);
168
+
169
+ // 常量定义
170
+ const typeList = [
171
+ { label: "普通车牌", value: "1" },
172
+ { label: "新能源车牌", value: "2" },
173
+ ];
174
+
175
+ const provinceText = [
176
+ "粤",
177
+ "京",
178
+ "冀",
179
+ "沪",
180
+ "津",
181
+ "晋",
182
+ "蒙",
183
+ "辽",
184
+ "吉",
185
+ "黑",
186
+ "苏",
187
+ "浙",
188
+ "皖",
189
+ "闽",
190
+ "赣",
191
+ "鲁",
192
+ "豫",
193
+ "鄂",
194
+ "湘",
195
+ "桂",
196
+ "琼",
197
+ "渝",
198
+ "川",
199
+ "贵",
200
+ "云",
201
+ "藏",
202
+ "陕",
203
+ "甘",
204
+ "青",
205
+ "宁",
206
+ "新",
207
+ ];
208
+
209
+ const numberText = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"];
210
+
211
+ const wordText = [
212
+ "A",
213
+ "B",
214
+ "C",
215
+ "D",
216
+ "E",
217
+ "F",
218
+ "G",
219
+ "H",
220
+ "J",
221
+ "K",
222
+ "L",
223
+ "M",
224
+ "N",
225
+ "P",
226
+ "Q",
227
+ "R",
228
+ "S",
229
+ "T",
230
+ "U",
231
+ "V",
232
+ "W",
233
+ "X",
234
+ "Y",
235
+ "Z",
236
+ ];
237
+
238
+ const lastWordText = ["港", "澳", "学", "领", "警"];
239
+
240
+ // 计算属性
241
+ const plateLength = computed(() => (type.value === "1" ? 7 : 8));
242
+
243
+ const inputType = computed(() => {
244
+ switch (currentInputIndex.value) {
245
+ case 0:
246
+ return 1;
247
+ case 1:
248
+ return 2;
249
+ case 2:
250
+ case 3:
251
+ case 4:
252
+ case 5:
253
+ return 3;
254
+ case 6:
255
+ return type.value === "2" ? 3 : 4;
256
+ case 7:
257
+ return 4;
258
+ default:
259
+ return 1;
260
+ }
261
+ });
262
+
263
+ // 方法定义
264
+ const resetState = () => {
265
+ type.value = "1";
266
+ currentInputIndex.value = 0;
267
+ currentInputValue.value = ["", "", "", "", "", "", ""];
268
+ };
269
+
270
+ const open = () => {
271
+ // 如果有 modelValue,进行回显处理
272
+ if (props.modelValue) {
273
+ const plateKey = props.modelValue.split("");
274
+ if (plateKey.length === 7) {
275
+ type.value = "1";
276
+ } else if (plateKey.length === 8) {
277
+ type.value = "2";
278
+ }
279
+ if (plateKey.length === 7 || plateKey.length === 8) {
280
+ currentInputValue.value = plateKey;
281
+ currentInputIndex.value = plateKey.length - 1;
282
+ }
283
+ }
284
+ plateKeyboardPopupRef.value.open();
285
+ };
286
+
287
+ const handlePopupChange = (e) => {
288
+ if (!e.show) {
289
+ resetState();
290
+ }
291
+ };
292
+
293
+ const handleClose = () => {
294
+ resetState();
295
+ plateKeyboardPopupRef.value.close();
296
+ };
297
+
298
+ const handleTypeChange = (val) => {
299
+ type.value = val;
300
+ currentInputIndex.value = 0;
301
+ currentInputValue.value =
302
+ val === "1"
303
+ ? ["", "", "", "", "", "", ""]
304
+ : ["", "", "", "", "", "", "", ""];
305
+ };
306
+
307
+ const handleInputSwitch = (index) => {
308
+ currentInputIndex.value = index;
309
+ };
310
+
311
+ const handleKeyChoose = (value, keyType) => {
312
+ if (keyType === "lastWordText" && inputType.value !== 4) return;
313
+ currentInputValue.value[currentInputIndex.value] = value;
314
+ if (type.value === "1") {
315
+ if (currentInputIndex.value < 6) {
316
+ currentInputIndex.value++;
317
+ }
318
+ } else {
319
+ if (currentInputIndex.value < 7) {
320
+ currentInputIndex.value++;
321
+ }
322
+ }
323
+ };
324
+
325
+ const handleDelete = () => {
326
+ currentInputValue.value[currentInputIndex.value] = "";
327
+ if (currentInputIndex.value !== 0) {
328
+ currentInputIndex.value--;
329
+ }
330
+ };
331
+
332
+ const handleSubmit = () => {
333
+ const plate = currentInputValue.value.join("");
334
+ const isValidLength =
335
+ type.value === "1" ? plate.length === 7 : plate.length === 8;
336
+
337
+ if (!isValidLength) {
338
+ uni.showToast({ title: "请输入完整的车牌号码", icon: "none" });
339
+ return;
340
+ }
341
+ emit("update:modelValue", plate);
342
+ emit("change", plate);
343
+ handleClose();
344
+ };
345
+
346
+ // 暴露方法
347
+ defineExpose({
348
+ open,
349
+ });
350
+ </script>
351
+ <style lang="scss">
352
+ .pro-plate-popup {
353
+ :deep(.pro-popup__content--body) {
354
+ margin: 0 !important;
355
+ }
356
+ }
357
+ </style>
358
+ <style lang="scss" scoped>
359
+ .pro-plate-keyboard {
360
+ font-family: PingFang SC, sans-serif;
361
+ color: #000000;
362
+ background: #eeeeee;
363
+
364
+ // 车牌类型选择
365
+ .pro-plate-type {
366
+ display: flex;
367
+ flex: 1;
368
+ padding: 28rpx 0 24rpx;
369
+ font-weight: 500;
370
+ font-size: 32rpx;
371
+ line-height: 48rpx;
372
+
373
+ &__item {
374
+ display: flex;
375
+ flex: 1;
376
+ align-items: center;
377
+ justify-content: center;
378
+ position: relative;
379
+ &--active {
380
+ font-weight: 500;
381
+ color: v-bind("themeColor");
382
+ }
383
+ &--unactive {
384
+ font-weight: 400;
385
+ color: #888;
386
+ }
387
+ }
388
+ &__underline {
389
+ position: absolute;
390
+ left: 50%;
391
+ bottom: -24rpx;
392
+ transform: translateX(-50%);
393
+ width: 116rpx;
394
+ height: 6rpx;
395
+ border-radius: 3rpx;
396
+ background: v-bind("themeColor");
397
+ }
398
+ }
399
+
400
+ // 车牌输入框
401
+ .pro-plate-body {
402
+ box-sizing: border-box;
403
+ padding: 40rpx 0 24rpx;
404
+ display: flex;
405
+ justify-content: space-between;
406
+ align-items: center;
407
+
408
+ .pro-plate-word {
409
+ flex: 1;
410
+ background: #ffffff;
411
+ border-radius: 8rpx;
412
+ height: 0;
413
+ margin: 0 6rpx;
414
+ box-sizing: border-box;
415
+ padding-bottom: 96rpx;
416
+ position: relative;
417
+ &--active {
418
+ border: 2rpx solid v-bind("themeColor");
419
+ }
420
+
421
+ text {
422
+ position: absolute;
423
+ top: 50%;
424
+ left: 50%;
425
+ transform: translateX(-50%) translateY(-50%);
426
+ font-weight: 600;
427
+ font-size: 48rpx;
428
+ color: #333;
429
+ }
430
+ }
431
+
432
+ .pro-plate-dot {
433
+ width: 8rpx;
434
+ height: 8rpx;
435
+ background: #333;
436
+ border-radius: 50%;
437
+ margin: 0 5rpx;
438
+ flex-shrink: 0;
439
+ }
440
+ }
441
+ // 键盘区域
442
+ .pro-plate-keys-container {
443
+ height: 432rpx;
444
+ .pro-plate-keyboard__keys {
445
+ box-sizing: border-box;
446
+ transition: all 0.3s;
447
+ display: flex;
448
+ flex-wrap: wrap;
449
+
450
+ .pro-plate-key {
451
+ padding: 12rpx 6rpx;
452
+ box-sizing: border-box;
453
+ width: 10%;
454
+ line-height: 84rpx;
455
+
456
+ .pro-plate-key-text {
457
+ box-sizing: border-box;
458
+ background: #fff;
459
+ border-radius: 10rpx;
460
+ display: flex;
461
+ align-items: center;
462
+ justify-content: center;
463
+ font-weight: 400;
464
+ font-size: 44rpx;
465
+ color: #000;
466
+ &:active {
467
+ background: v-bind("themeColor");
468
+ color: #fff;
469
+ }
470
+ }
471
+ }
472
+ }
473
+ }
474
+
475
+ // 按钮组
476
+ .pro-plate-btn-group {
477
+ display: flex;
478
+ justify-content: space-between;
479
+ padding: 28rpx 0 48rpx;
480
+ &__right {
481
+ display: flex;
482
+ }
483
+
484
+ .pro-plate-btn {
485
+ height: 84rpx;
486
+ width: 182rpx;
487
+ display: flex;
488
+ align-items: center;
489
+ justify-content: center;
490
+ background: #fff;
491
+ font-size: 32rpx;
492
+ border-radius: 10rpx;
493
+ margin: 0 6rpx;
494
+
495
+ &--cancel {
496
+ color: #666;
497
+ }
498
+
499
+ &--delete {
500
+ color: #ff0000;
501
+ }
502
+
503
+ &--submit {
504
+ color: #ffffff;
505
+ background: v-bind("themeColor");
506
+ }
507
+ }
508
+ }
509
+ }
510
+ </style>