muzhiyu-ui 1.0.0

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.
Files changed (96) hide show
  1. package/README.md +202 -0
  2. package/components/muzhiyu/mu-action-sheet/mu-action-sheet.vue +335 -0
  3. package/components/muzhiyu/mu-alert/mu-alert.vue +220 -0
  4. package/components/muzhiyu/mu-back-top/mu-back-top.vue +161 -0
  5. package/components/muzhiyu/mu-badge/mu-badge.vue +150 -0
  6. package/components/muzhiyu/mu-barcode/mu-barcode.vue +100 -0
  7. package/components/muzhiyu/mu-blank/mu-blank.vue +30 -0
  8. package/components/muzhiyu/mu-bottom-nav/mu-bottom-nav.vue +331 -0
  9. package/components/muzhiyu/mu-bubble/mu-bubble.vue +316 -0
  10. package/components/muzhiyu/mu-button/mu-button.vue +431 -0
  11. package/components/muzhiyu/mu-calendar/mu-calendar.vue +429 -0
  12. package/components/muzhiyu/mu-car-input/mu-car-input.vue +209 -0
  13. package/components/muzhiyu/mu-card/mu-card.vue +317 -0
  14. package/components/muzhiyu/mu-cascader/mu-cascader.vue +336 -0
  15. package/components/muzhiyu/mu-cell/mu-cell.vue +368 -0
  16. package/components/muzhiyu/mu-checkbox/mu-checkbox.vue +116 -0
  17. package/components/muzhiyu/mu-checkbox-group/mu-checkbox-group.vue +40 -0
  18. package/components/muzhiyu/mu-code-input/mu-code-input.vue +264 -0
  19. package/components/muzhiyu/mu-collapse/mu-collapse.vue +316 -0
  20. package/components/muzhiyu/mu-color-picker/mu-color-picker.vue +466 -0
  21. package/components/muzhiyu/mu-config/index.js +34 -0
  22. package/components/muzhiyu/mu-config/mu-config.vue +54 -0
  23. package/components/muzhiyu/mu-countdown/mu-countdown.vue +338 -0
  24. package/components/muzhiyu/mu-cropper/mu-cropper.vue +415 -0
  25. package/components/muzhiyu/mu-date-picker/mu-date-picker.vue +290 -0
  26. package/components/muzhiyu/mu-datetime/mu-datetime.vue +262 -0
  27. package/components/muzhiyu/mu-divider/mu-divider.vue +229 -0
  28. package/components/muzhiyu/mu-drawer/mu-drawer.vue +172 -0
  29. package/components/muzhiyu/mu-dropdown/mu-dropdown.vue +215 -0
  30. package/components/muzhiyu/mu-dropdown-top/mu-dropdown-top.vue +243 -0
  31. package/components/muzhiyu/mu-empty/mu-empty.vue +217 -0
  32. package/components/muzhiyu/mu-fab/mu-fab.vue +324 -0
  33. package/components/muzhiyu/mu-footer/mu-footer.vue +21 -0
  34. package/components/muzhiyu/mu-form/mu-form.vue +170 -0
  35. package/components/muzhiyu/mu-grid/mu-grid.vue +95 -0
  36. package/components/muzhiyu/mu-grid-item/mu-grid-item.vue +247 -0
  37. package/components/muzhiyu/mu-html/mu-html.vue +145 -0
  38. package/components/muzhiyu/mu-icon/mu-icon.vue +822 -0
  39. package/components/muzhiyu/mu-image-grid/mu-image-grid.vue +225 -0
  40. package/components/muzhiyu/mu-image-preview/mu-image-preview.vue +286 -0
  41. package/components/muzhiyu/mu-index-list/mu-index-list.vue +381 -0
  42. package/components/muzhiyu/mu-input/mu-input.vue +570 -0
  43. package/components/muzhiyu/mu-input-number/mu-input-number.vue +193 -0
  44. package/components/muzhiyu/mu-keyboard/mu-keyboard.vue +415 -0
  45. package/components/muzhiyu/mu-list/mu-list.vue +89 -0
  46. package/components/muzhiyu/mu-loading/mu-loading.vue +124 -0
  47. package/components/muzhiyu/mu-loading-page/mu-loading-page.vue +529 -0
  48. package/components/muzhiyu/mu-loadmore/mu-loadmore.vue +26 -0
  49. package/components/muzhiyu/mu-modal/mu-modal.vue +298 -0
  50. package/components/muzhiyu/mu-navbar/mu-navbar.vue +223 -0
  51. package/components/muzhiyu/mu-nomore/mu-nomore.vue +107 -0
  52. package/components/muzhiyu/mu-notice-bar/mu-notice-bar.vue +284 -0
  53. package/components/muzhiyu/mu-notify/mu-notify.vue +202 -0
  54. package/components/muzhiyu/mu-overlay/mu-overlay.vue +94 -0
  55. package/components/muzhiyu/mu-pagination/mu-pagination.vue +330 -0
  56. package/components/muzhiyu/mu-password-input/mu-password-input.vue +119 -0
  57. package/components/muzhiyu/mu-picker/mu-picker.vue +297 -0
  58. package/components/muzhiyu/mu-popup/mu-popup.vue +297 -0
  59. package/components/muzhiyu/mu-progress/mu-progress.vue +156 -0
  60. package/components/muzhiyu/mu-pull-refresh/mu-pull-refresh.vue +54 -0
  61. package/components/muzhiyu/mu-qrcode/mu-qrcode.vue +340 -0
  62. package/components/muzhiyu/mu-qrcode/qrcode.js +2237 -0
  63. package/components/muzhiyu/mu-radio/mu-radio.vue +125 -0
  64. package/components/muzhiyu/mu-radio-group/mu-radio-group.vue +37 -0
  65. package/components/muzhiyu/mu-rate/mu-rate.vue +103 -0
  66. package/components/muzhiyu/mu-search/mu-search.vue +320 -0
  67. package/components/muzhiyu/mu-section/mu-section.vue +206 -0
  68. package/components/muzhiyu/mu-select/mu-select.vue +440 -0
  69. package/components/muzhiyu/mu-share-sheet/mu-share-sheet.vue +269 -0
  70. package/components/muzhiyu/mu-sidebar/mu-sidebar.vue +304 -0
  71. package/components/muzhiyu/mu-signature/mu-signature.vue +449 -0
  72. package/components/muzhiyu/mu-skeleton/mu-skeleton.vue +225 -0
  73. package/components/muzhiyu/mu-slider/mu-slider.vue +228 -0
  74. package/components/muzhiyu/mu-space/mu-space.vue +84 -0
  75. package/components/muzhiyu/mu-steps/mu-steps.vue +311 -0
  76. package/components/muzhiyu/mu-sticky/mu-sticky.vue +57 -0
  77. package/components/muzhiyu/mu-swipe-cell/mu-swipe-cell.vue +281 -0
  78. package/components/muzhiyu/mu-swiper/mu-swiper.vue +107 -0
  79. package/components/muzhiyu/mu-switch/mu-switch.vue +99 -0
  80. package/components/muzhiyu/mu-tabbar/mu-tabbar.vue +382 -0
  81. package/components/muzhiyu/mu-table/mu-table.vue +420 -0
  82. package/components/muzhiyu/mu-tabs/mu-tabs.vue +339 -0
  83. package/components/muzhiyu/mu-tag/mu-tag.vue +65 -0
  84. package/components/muzhiyu/mu-text-ellipsis/mu-text-ellipsis.vue +54 -0
  85. package/components/muzhiyu/mu-timeline/mu-timeline.vue +166 -0
  86. package/components/muzhiyu/mu-tips/mu-tips.vue +36 -0
  87. package/components/muzhiyu/mu-toast/mu-toast.vue +151 -0
  88. package/components/muzhiyu/mu-transfer/mu-transfer.vue +114 -0
  89. package/components/muzhiyu/mu-tree/mu-tree.vue +263 -0
  90. package/components/muzhiyu/mu-upload/mu-upload.vue +415 -0
  91. package/components/muzhiyu/mu-waterfall/mu-waterfall.vue +321 -0
  92. package/components/muzhiyu/mu-watermark/mu-watermark.vue +60 -0
  93. package/index.js +83 -0
  94. package/package.json +38 -0
  95. package/utils/index.js +600 -0
  96. package/utils/request.js +265 -0
@@ -0,0 +1,429 @@
1
+ <template>
2
+ <view v-if="visible" class="mu-calendar" :class="[panelShow && 'mu-calendar--open']">
3
+ <!-- 遮罩层 -->
4
+ <view class="mu-calendar__mask" @tap="handleClose"></view>
5
+
6
+ <!-- 底部面板 Panel -->
7
+ <view class="mu-calendar__panel" :class="[panelShow && 'mu-calendar__panel--show']" @tap.stop>
8
+ <!-- 头部 Header -->
9
+ <view class="mu-calendar__header">
10
+ <text class="mu-calendar__title">{{ title }}</text>
11
+ <view class="mu-calendar__close mu-touchable-flat" @tap="handleClose">
12
+ <mu-icon name="close" :size="16" color="#94A3B8" />
13
+ </view>
14
+ </view>
15
+
16
+ <!-- 星期标头 -->
17
+ <view class="mu-calendar__weekdays">
18
+ <text v-for="(w, wi) in weekdays" :key="wi" class="mu-calendar__weekday" :class="{ 'mu-calendar__weekday--weekend': wi === 0 || wi === 6 }">
19
+ {{ w }}
20
+ </text>
21
+ </view>
22
+
23
+ <!-- 日历滚动区 -->
24
+ <scroll-view scroll-y class="mu-calendar__body">
25
+ <view v-for="(month, mi) in monthList" :key="mi" class="mu-calendar__month">
26
+ <view class="mu-calendar__month-title">
27
+ <text class="mu-calendar__month-text">{{ month.year }}年{{ month.month }}月</text>
28
+ </view>
29
+
30
+ <view class="mu-calendar__days">
31
+ <view
32
+ v-for="(day, di) in month.days"
33
+ :key="di"
34
+ class="mu-calendar__day"
35
+ :class="getDayClasses(day)"
36
+ @tap="selectDay(day)"
37
+ >
38
+ <template v-if="day.day">
39
+ <view class="mu-calendar__day-inner">
40
+ <text class="mu-calendar__day-text">{{ day.day }}</text>
41
+ <text v-if="getDaySubText(day)" class="mu-calendar__sub-text">{{ getDaySubText(day) }}</text>
42
+ </view>
43
+ </template>
44
+ </view>
45
+ </view>
46
+ </view>
47
+ </scroll-view>
48
+
49
+ <!-- 底部确认按钮 -->
50
+ <view v-if="showConfirm" class="mu-calendar__footer">
51
+ <mu-button type="primary" block size="medium" @click="handleConfirm">
52
+ {{ confirmText }}
53
+ </mu-button>
54
+ </view>
55
+ </view>
56
+ </view>
57
+ </template>
58
+
59
+ <script setup>
60
+ import { ref, computed, watch } from 'vue'
61
+
62
+ const props = defineProps({
63
+ show: { type: Boolean, default: false },
64
+ modelValue: { type: [String, Array], default: '' },
65
+ defaultDate: { type: [String, Array], default: '' },
66
+ title: { type: String, default: '选择日期' },
67
+ type: { type: String, default: 'single' }, // single (单选) | range (范围选) | multiple (多选)
68
+ minDate: { type: String, default: '' },
69
+ maxDate: { type: String, default: '' },
70
+ maxMonth: { type: Number, default: 6 },
71
+ showConfirm: { type: Boolean, default: true },
72
+ confirmText: { type: String, default: '确定' }
73
+ })
74
+
75
+ const emit = defineEmits(['confirm', 'change', 'close', 'update:show', 'update:modelValue'])
76
+
77
+ const visible = ref(false)
78
+ const panelShow = ref(false)
79
+ const weekdays = ['日', '一', '二', '三', '四', '五', '六']
80
+ const selected = ref([])
81
+
82
+ const todayStr = computed(() => {
83
+ const now = new Date()
84
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
85
+ })
86
+
87
+ const startDateObj = computed(() => {
88
+ const d = props.minDate ? new Date(props.minDate) : new Date()
89
+ return { year: d.getFullYear(), month: d.getMonth() + 1 }
90
+ })
91
+
92
+ const monthList = computed(() => {
93
+ const list = []
94
+ let y = startDateObj.value.year
95
+ let m = startDateObj.value.month
96
+
97
+ for (let i = 0; i < props.maxMonth; i++) {
98
+ const firstDay = new Date(y, m - 1, 1).getDay()
99
+ const totalDays = new Date(y, m, 0).getDate()
100
+ const days = []
101
+
102
+ for (let j = 0; j < firstDay; j++) days.push({ day: 0, date: '' })
103
+
104
+ for (let d = 1; d <= totalDays; d++) {
105
+ const dateStr = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`
106
+ days.push({ day: d, date: dateStr, year: y, month: m })
107
+ }
108
+
109
+ list.push({ year: y, month: m, days })
110
+ m++
111
+ if (m > 12) { m = 1; y++ }
112
+ }
113
+ return list
114
+ })
115
+
116
+ function isDayDisabled(day) {
117
+ if (!day.date) return true
118
+ if (props.minDate && day.date < props.minDate) return true
119
+ if (props.maxDate && day.date > props.maxDate) return true
120
+ return false
121
+ }
122
+
123
+ function getDayClasses(day) {
124
+ if (!day.day) return ['mu-calendar__day--empty']
125
+ if (isDayDisabled(day)) return ['mu-calendar__day--disabled']
126
+
127
+ const cls = []
128
+ const isSel = selected.value.includes(day.date)
129
+
130
+ if (isSel) cls.push('mu-calendar__day--selected')
131
+
132
+ if (props.type === 'range' && selected.value.length === 2) {
133
+ if (day.date === selected.value[0]) cls.push('mu-calendar__day--start')
134
+ if (day.date === selected.value[1]) cls.push('mu-calendar__day--end')
135
+ if (day.date > selected.value[0] && day.date < selected.value[1]) {
136
+ cls.push('mu-calendar__day--between')
137
+ }
138
+ }
139
+
140
+ return cls
141
+ }
142
+
143
+ function getDaySubText(day) {
144
+ if (!day.day) return ''
145
+ if (props.type === 'range' && selected.value.length >= 1) {
146
+ if (day.date === selected.value[0]) return '开始'
147
+ if (day.date === selected.value[1]) return '结束'
148
+ }
149
+ if (day.date === todayStr.value) return '今天'
150
+ return ''
151
+ }
152
+
153
+ function selectDay(day) {
154
+ if (!day.day || isDayDisabled(day)) return
155
+
156
+ if (props.type === 'single') {
157
+ selected.value = [day.date]
158
+ if (!props.showConfirm) {
159
+ handleConfirm()
160
+ }
161
+ } else if (props.type === 'range') {
162
+ if (selected.value.length === 0 || selected.value.length === 2) {
163
+ selected.value = [day.date]
164
+ } else {
165
+ const start = selected.value[0]
166
+ selected.value = day.date >= start ? [start, day.date] : [day.date, start]
167
+ if (!props.showConfirm && selected.value.length === 2) {
168
+ handleConfirm()
169
+ }
170
+ }
171
+ } else if (props.type === 'multiple') {
172
+ const idx = selected.value.indexOf(day.date)
173
+ if (idx >= 0) selected.value.splice(idx, 1)
174
+ else selected.value.push(day.date)
175
+ }
176
+
177
+ const res = props.type === 'single' ? selected.value[0] : [...selected.value]
178
+ emit('change', res)
179
+ }
180
+
181
+ function handleConfirm() {
182
+ const res = props.type === 'single' ? (selected.value[0] || '') : [...selected.value]
183
+ emit('confirm', res)
184
+ emit('update:modelValue', res)
185
+ handleClose()
186
+ }
187
+
188
+ function handleClose() {
189
+ panelShow.value = false
190
+ emit('update:show', false)
191
+ setTimeout(() => {
192
+ visible.value = false
193
+ emit('close')
194
+ }, 250)
195
+ }
196
+
197
+ watch(() => props.show, (val) => {
198
+ if (val) {
199
+ visible.value = true
200
+ const initVal = props.modelValue || props.defaultDate
201
+ if (initVal) {
202
+ selected.value = Array.isArray(initVal) ? [...initVal] : [initVal]
203
+ } else {
204
+ selected.value = []
205
+ }
206
+ setTimeout(() => {
207
+ panelShow.value = true
208
+ }, 30)
209
+ } else {
210
+ panelShow.value = false
211
+ setTimeout(() => {
212
+ visible.value = false
213
+ }, 250)
214
+ }
215
+ }, { immediate: true })
216
+ </script>
217
+
218
+ <style lang="scss" scoped>
219
+ @import "@/uni.scss";
220
+
221
+ .mu-calendar {
222
+ position: fixed;
223
+ inset: 0;
224
+ z-index: $mu-z-popup;
225
+ display: flex;
226
+ flex-direction: column;
227
+ justify-content: flex-end;
228
+ box-sizing: border-box;
229
+
230
+ &__mask {
231
+ position: absolute;
232
+ inset: 0;
233
+ background: rgba(15, 23, 42, 0.45);
234
+ backdrop-filter: blur(8px);
235
+ opacity: 0;
236
+ transition: opacity 0.25s ease;
237
+ }
238
+
239
+ &--open &__mask {
240
+ opacity: 1;
241
+ }
242
+
243
+ &__panel {
244
+ position: relative;
245
+ background: #ffffff;
246
+ border-radius: 32rpx 32rpx 0 0;
247
+ max-height: 80vh;
248
+ display: flex;
249
+ flex-direction: column;
250
+ transform: translateY(100%);
251
+ transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1);
252
+ z-index: 2;
253
+ overflow: hidden;
254
+
255
+ &--show {
256
+ transform: translateY(0);
257
+ }
258
+ }
259
+
260
+ &__header {
261
+ display: flex;
262
+ align-items: center;
263
+ justify-content: space-between;
264
+ padding: 28rpx 32rpx;
265
+ border-bottom: 1rpx solid rgba(226, 232, 240, 0.8);
266
+ }
267
+
268
+ &__title {
269
+ font-size: 30rpx;
270
+ font-weight: 800;
271
+ color: $mu-text-primary;
272
+ letter-spacing: -0.5rpx;
273
+ }
274
+
275
+ &__close {
276
+ width: 44rpx;
277
+ height: 44rpx;
278
+ border-radius: 50%;
279
+ background: #F1F5F9;
280
+ display: flex;
281
+ align-items: center;
282
+ justify-content: center;
283
+ }
284
+
285
+ &__weekdays {
286
+ display: flex;
287
+ padding: 16rpx 0;
288
+ border-bottom: 1rpx solid rgba(226, 232, 240, 0.6);
289
+ background: #F8FAFC;
290
+ }
291
+
292
+ &__weekday {
293
+ flex: 1;
294
+ text-align: center;
295
+ font-size: 22rpx;
296
+ color: #64748B;
297
+ font-weight: 700;
298
+
299
+ &--weekend {
300
+ color: #F43F5E;
301
+ }
302
+ }
303
+
304
+ &__body {
305
+ flex: 1;
306
+ max-height: 58vh;
307
+ }
308
+
309
+ &__month-title {
310
+ padding: 24rpx 32rpx 12rpx;
311
+ }
312
+
313
+ &__month-text {
314
+ font-size: 26rpx;
315
+ font-weight: 800;
316
+ color: $mu-text-primary;
317
+ }
318
+
319
+ &__days {
320
+ display: flex;
321
+ flex-wrap: wrap;
322
+ padding: 0 12rpx;
323
+ }
324
+
325
+ &__day {
326
+ width: calc(100% / 7);
327
+ height: 90rpx;
328
+ display: flex;
329
+ align-items: center;
330
+ justify-content: center;
331
+ position: relative;
332
+ box-sizing: border-box;
333
+
334
+ &--empty { pointer-events: none; }
335
+ &--disabled {
336
+ opacity: 0.35;
337
+ pointer-events: none;
338
+ }
339
+
340
+ &-inner {
341
+ width: 72rpx;
342
+ height: 72rpx;
343
+ border-radius: 50%;
344
+ display: flex;
345
+ flex-direction: column;
346
+ align-items: center;
347
+ justify-content: center;
348
+ transition: all 0.2s ease;
349
+ }
350
+
351
+ &-text {
352
+ font-size: 26rpx;
353
+ font-weight: 600;
354
+ color: $mu-text-primary;
355
+ line-height: 1;
356
+ font-variant-numeric: tabular-nums;
357
+ }
358
+
359
+ .mu-calendar__sub-text {
360
+ font-size: 16rpx;
361
+ color: #94A3B8;
362
+ margin-top: 4rpx;
363
+ line-height: 1;
364
+ }
365
+
366
+ &--selected .mu-calendar__day-inner,
367
+ &--start .mu-calendar__day-inner,
368
+ &--end .mu-calendar__day-inner {
369
+ background: #171717 !important;
370
+ box-shadow: 0 4rpx 14rpx rgba(99, 102, 241, 0.35) !important;
371
+ border-radius: 50% !important;
372
+
373
+ .mu-calendar__day-text, .mu-calendar__sub-text {
374
+ color: #ffffff !important;
375
+ font-weight: 800 !important;
376
+ }
377
+ }
378
+
379
+ &--between {
380
+ background: rgba(99, 102, 241, 0.12);
381
+
382
+ .mu-calendar__day-text {
383
+ color: #171717;
384
+ font-weight: 700;
385
+ }
386
+ }
387
+ }
388
+
389
+ &__footer {
390
+ padding: 20rpx 32rpx;
391
+ border-top: 1rpx solid rgba(226, 232, 240, 0.8);
392
+ background: #ffffff;
393
+ }
394
+ }
395
+
396
+ .mu-dark, page.mu-dark {
397
+
398
+ .mu-calendar {
399
+ &__mask { background: rgba(0, 0, 0, 0.65); }
400
+ &__panel { background: $mu-dark-bg-base; }
401
+ &__header { border-color: rgba(255, 255, 255, 0.08); }
402
+ &__title { color: $mu-dark-text-primary; }
403
+ &__close { background: rgba(255, 255, 255, 0.1); }
404
+ &__weekdays { background: rgba(255, 255, 255, 0.04); border-color: rgba(255, 255, 255, 0.06); }
405
+ &__weekday { color: $mu-dark-text-secondary; }
406
+ &__month-text { color: $mu-dark-text-primary; }
407
+ &__day-text { color: $mu-dark-text-primary; }
408
+ &__day--between { background: rgba(99, 102, 241, 0.2); .mu-calendar__day-text { color: #FFFFFF; } }
409
+ &__footer { background: $mu-dark-bg-base; border-color: rgba(255, 255, 255, 0.08); }
410
+ }
411
+ }
412
+
413
+ @media (prefers-color-scheme: dark) {
414
+
415
+ .mu-calendar {
416
+ &__mask { background: rgba(0, 0, 0, 0.65); }
417
+ &__panel { background: $mu-dark-bg-base; }
418
+ &__header { border-color: rgba(255, 255, 255, 0.08); }
419
+ &__title { color: $mu-dark-text-primary; }
420
+ &__close { background: rgba(255, 255, 255, 0.1); }
421
+ &__weekdays { background: rgba(255, 255, 255, 0.04); border-color: rgba(255, 255, 255, 0.06); }
422
+ &__weekday { color: $mu-dark-text-secondary; }
423
+ &__month-text { color: $mu-dark-text-primary; }
424
+ &__day-text { color: $mu-dark-text-primary; }
425
+ &__day--between { background: rgba(99, 102, 241, 0.2); .mu-calendar__day-text { color: #FFFFFF; } }
426
+ &__footer { background: $mu-dark-bg-base; border-color: rgba(255, 255, 255, 0.08); }
427
+ }
428
+ }
429
+ </style>
@@ -0,0 +1,209 @@
1
+ <template>
2
+ <view class="mu-car-input">
3
+ <!-- 8 位车牌号分割输入框 (前 7 位燃油车,第 8 位新能源) -->
4
+ <view class="mu-car-input__boxes" @tap="focusInput">
5
+ <view
6
+ v-for="i in 8"
7
+ :key="i"
8
+ class="mu-car-input__box"
9
+ :class="[
10
+ `mu-car-input__box--${shape}`,
11
+ i === 8 && 'mu-car-input__box--new-energy',
12
+ getValueChar(i - 1) && 'mu-car-input__box--filled',
13
+ isCurrentFocus(i - 1) && 'mu-car-input__box--focus'
14
+ ]"
15
+ >
16
+ <!-- 已选车牌字符 -->
17
+ <text v-if="getValueChar(i - 1)" class="mu-car-input__text">
18
+ {{ getValueChar(i - 1) }}
19
+ </text>
20
+
21
+ <!-- 第 8 位新能源专属绿色徽章提示 -->
22
+ <view v-else-if="i === 8" class="mu-car-input__new-energy-badge">
23
+ <text class="mu-car-input__new-energy-plus">+</text>
24
+ <text class="mu-car-input__new-energy-text">新能源</text>
25
+ </view>
26
+
27
+ <!-- 当前聚焦输入光标 -->
28
+ <view v-else-if="isCurrentFocus(i - 1)" class="mu-car-input__cursor"></view>
29
+ </view>
30
+ </view>
31
+
32
+ <!-- 自动联动弹起的车牌号专用键盘 -->
33
+ <mu-keyboard
34
+ v-model:show="showKeyboard"
35
+ v-model:value="innerValue"
36
+ mode="car"
37
+ :maxlength="8"
38
+ :title="keyboardTitle || '请输入车牌号码'"
39
+ @change="onKeyboardChange"
40
+ />
41
+ </view>
42
+ </template>
43
+
44
+ <script setup>
45
+ import { ref, watch } from 'vue'
46
+
47
+ const props = defineProps({
48
+ modelValue: { type: String, default: '' },
49
+ shape: { type: String, default: 'radius' }, // square (微直角) | radius (圆角) | round (全圆) | underline (下划线)
50
+ disabled: { type: Boolean, default: false },
51
+ autoFocus: { type: Boolean, default: false },
52
+ keyboardTitle: { type: String, default: '请输入车牌号码' }
53
+ })
54
+
55
+ const emit = defineEmits(['update:modelValue', 'change', 'finish'])
56
+
57
+ const showKeyboard = ref(props.autoFocus)
58
+ const innerValue = ref(props.modelValue || '')
59
+
60
+ watch(() => props.modelValue, (val) => {
61
+ innerValue.value = val || ''
62
+ })
63
+
64
+ function getValueChar(index) {
65
+ return (props.modelValue || '').charAt(index) || ''
66
+ }
67
+
68
+ function isCurrentFocus(index) {
69
+ const len = (props.modelValue || '').length
70
+ return len === index
71
+ }
72
+
73
+ function focusInput() {
74
+ if (props.disabled) return
75
+ showKeyboard.value = true
76
+ }
77
+
78
+ function onKeyboardChange(val) {
79
+ innerValue.value = val
80
+ emit('update:modelValue', val)
81
+ emit('change', val)
82
+ if (val.length >= 7) {
83
+ emit('finish', val)
84
+ }
85
+ }
86
+ </script>
87
+
88
+ <style lang="scss" scoped>
89
+ @import "@/uni.scss";
90
+
91
+ .mu-car-input {
92
+ position: relative;
93
+ width: 100%;
94
+ display: flex;
95
+ justify-content: center;
96
+
97
+ &__boxes {
98
+ display: flex;
99
+ align-items: center;
100
+ justify-content: space-between;
101
+ gap: 12rpx;
102
+ width: 100%;
103
+ }
104
+
105
+ &__box {
106
+ flex: 1;
107
+ height: 96rpx;
108
+ background: #ffffff;
109
+ border: 2rpx solid $mu-border;
110
+ display: flex;
111
+ align-items: center;
112
+ justify-content: center;
113
+ transition: all 0.2s $mu-ease;
114
+ box-sizing: border-box;
115
+
116
+ &--square { border-radius: 8rpx; }
117
+ &--radius { border-radius: 16rpx; }
118
+ &--round { border-radius: 50%; max-width: 88rpx; }
119
+ &--underline {
120
+ border: none;
121
+ border-bottom: 4rpx solid $mu-border;
122
+ background: transparent;
123
+ border-radius: 0;
124
+ }
125
+
126
+ /* 新能源插槽 */
127
+ &--new-energy {
128
+ border-style: dashed;
129
+ border-color: #10B981;
130
+ background: rgba(16, 185, 129, 0.04);
131
+ }
132
+
133
+ &--filled {
134
+ border-color: $mu-primary;
135
+ background: #ffffff;
136
+ }
137
+
138
+ &--focus {
139
+ border-color: $mu-primary;
140
+ box-shadow: 0 0 16rpx rgba(99, 102, 241, 0.3);
141
+ animation: mu-car-pulse 1.5s infinite ease-in-out;
142
+ }
143
+ }
144
+
145
+ &__text {
146
+ font-size: 36rpx;
147
+ font-weight: 800;
148
+ color: $mu-text-primary;
149
+ }
150
+
151
+ &__new-energy-badge {
152
+ display: flex;
153
+ flex-direction: column;
154
+ align-items: center;
155
+ justify-content: center;
156
+ line-height: 1;
157
+ }
158
+
159
+ &__new-energy-plus {
160
+ font-size: 22rpx;
161
+ color: #10B981;
162
+ font-weight: 800;
163
+ }
164
+
165
+ &__new-energy-text {
166
+ font-size: 16rpx;
167
+ color: #10B981;
168
+ font-weight: 700;
169
+ margin-top: 4rpx;
170
+ }
171
+
172
+ &__cursor {
173
+ width: 4rpx;
174
+ height: 36rpx;
175
+ background: $mu-primary;
176
+ animation: mu-car-blink 1s infinite;
177
+ }
178
+ }
179
+
180
+ @keyframes mu-car-blink {
181
+ 0%, 100% { opacity: 1; }
182
+ 50% { opacity: 0; }
183
+ }
184
+
185
+ @keyframes mu-car-pulse {
186
+ 0%, 100% { border-color: $mu-primary; }
187
+ 50% { border-color: rgba(99, 102, 241, 0.4); }
188
+ }
189
+
190
+ .mu-dark, page.mu-dark {
191
+ .mu-car-input {
192
+ &__box {
193
+ background: rgba(255, 255, 255, 0.04);
194
+ border-color: $mu-dark-border;
195
+ }
196
+ &__text { color: $mu-dark-text-primary; }
197
+ }
198
+ }
199
+
200
+ @media (prefers-color-scheme: dark) {
201
+ .mu-car-input {
202
+ &__box {
203
+ background: rgba(255, 255, 255, 0.04);
204
+ border-color: $mu-dark-border;
205
+ }
206
+ &__text { color: $mu-dark-text-primary; }
207
+ }
208
+ }
209
+ </style>