@ruixinkeji/prism-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 (48) hide show
  1. package/README.md +141 -0
  2. package/components/PrismAIAssist/PrismAIAssist.vue +98 -0
  3. package/components/PrismAddressInput/PrismAddressInput.vue +597 -0
  4. package/components/PrismCityCascadeSelect/PrismCityCascadeSelect.vue +793 -0
  5. package/components/PrismCityPicker/PrismCityPicker.vue +1008 -0
  6. package/components/PrismCitySelect/PrismCitySelect.vue +435 -0
  7. package/components/PrismCode/PrismCode.vue +749 -0
  8. package/components/PrismCodeInput/PrismCodeInput.vue +156 -0
  9. package/components/PrismDateTimePicker/PrismDateTimePicker.vue +953 -0
  10. package/components/PrismDropdown/PrismDropdown.vue +77 -0
  11. package/components/PrismGroupSticky/PrismGroupSticky.vue +352 -0
  12. package/components/PrismIdCardInput/PrismIdCardInput.vue +253 -0
  13. package/components/PrismImagePicker/PrismImagePicker.vue +457 -0
  14. package/components/PrismIndexBar/PrismIndexBar.vue +243 -0
  15. package/components/PrismLicensePlateInput/PrismLicensePlateInput.vue +1100 -0
  16. package/components/PrismMusicPlayer/PrismMusicPlayer.vue +530 -0
  17. package/components/PrismNavBar/PrismNavBar.vue +199 -0
  18. package/components/PrismSecureInput/PrismSecureInput.vue +360 -0
  19. package/components/PrismSticky/PrismSticky.vue +173 -0
  20. package/components/PrismSwiper/PrismSwiper.vue +339 -0
  21. package/components/PrismSwitch/PrismSwitch.vue +202 -0
  22. package/components/PrismTabBar/PrismTabBar.vue +147 -0
  23. package/components/PrismTabs/PrismTabs.vue +49 -0
  24. package/components/PrismVoiceInput/PrismVoiceInput.vue +529 -0
  25. package/index.d.ts +24 -0
  26. package/index.esm.js +25 -0
  27. package/index.js +25 -0
  28. package/package.json +89 -0
  29. package/styles/base.scss +227 -0
  30. package/styles/button.scss +120 -0
  31. package/styles/card.scss +306 -0
  32. package/styles/colors.scss +877 -0
  33. package/styles/data.scss +1229 -0
  34. package/styles/effects.scss +407 -0
  35. package/styles/feedback.scss +698 -0
  36. package/styles/form.scss +1574 -0
  37. package/styles/index.scss +46 -0
  38. package/styles/list.scss +184 -0
  39. package/styles/navigation.scss +554 -0
  40. package/styles/overlay.scss +182 -0
  41. package/styles/utilities.scss +134 -0
  42. package/styles/variables.scss +138 -0
  43. package/theme/blue.scss +36 -0
  44. package/theme/cyan.scss +32 -0
  45. package/theme/green.scss +32 -0
  46. package/theme/orange.scss +32 -0
  47. package/theme/purple.scss +32 -0
  48. package/theme/red.scss +32 -0
@@ -0,0 +1,1008 @@
1
+ <template>
2
+ <view class="prism-city-picker" :class="{ 'dark-mode': appStore.isDarkMode }">
3
+ <!-- 触发器 -->
4
+ <view class="picker-trigger" @click="openPicker">
5
+ <slot>
6
+ <view class="prism-select-box">
7
+ <text :class="{ 'placeholder': !displayValue, 'select-text': displayValue }">
8
+ {{ displayValue || placeholder }}
9
+ </text>
10
+ <text class="select-arrow fa fa-map-marker-alt"></text>
11
+ </view>
12
+ </slot>
13
+ </view>
14
+
15
+ <!-- 选择器弹窗 -->
16
+ <view class="picker-popup" v-if="showPicker" @click="closePicker">
17
+ <view class="picker-content" @click.stop>
18
+ <!-- 头部 -->
19
+ <view class="picker-header">
20
+ <view class="picker-cancel" @click="closePicker">取消</view>
21
+ <view class="picker-title">{{ title }}</view>
22
+ <view class="picker-confirm" @click="confirmSelect">确定</view>
23
+ </view>
24
+
25
+ <!-- 模式1: 滚轮选择(类似日期选择器) -->
26
+ <view class="picker-body" v-if="mode === 'picker'">
27
+ <picker-view
28
+ class="picker-view"
29
+ :value="pickerIndex"
30
+ @change="onPickerChange"
31
+ indicator-class="picker-indicator"
32
+ :indicator-style="indicatorStyle"
33
+ :style="{ height: pickerHeight }"
34
+ >
35
+ <!-- 省份列 -->
36
+ <picker-view-column>
37
+ <view
38
+ v-for="(item, idx) in provinceList"
39
+ :key="item.code"
40
+ class="picker-item"
41
+ :style="{
42
+ height: itemHeight + 'px',
43
+ lineHeight: itemHeight + 'px',
44
+ color: idx === pickerIndex[0] ? 'var(--prism-primary-color, #3478F6)' : 'var(--prism-text-secondary, #86909C)',
45
+ fontWeight: idx === pickerIndex[0] ? '600' : '400'
46
+ }"
47
+ >
48
+ {{ item.name }}
49
+ </view>
50
+ </picker-view-column>
51
+ <!-- 城市列 -->
52
+ <picker-view-column>
53
+ <view
54
+ v-for="(item, idx) in cityList"
55
+ :key="item.code"
56
+ class="picker-item"
57
+ :style="{
58
+ height: itemHeight + 'px',
59
+ lineHeight: itemHeight + 'px',
60
+ color: idx === pickerIndex[1] ? 'var(--prism-primary-color, #3478F6)' : 'var(--prism-text-secondary, #86909C)',
61
+ fontWeight: idx === pickerIndex[1] ? '600' : '400'
62
+ }"
63
+ >
64
+ {{ item.name }}
65
+ </view>
66
+ </picker-view-column>
67
+ <!-- 区县列 -->
68
+ <picker-view-column v-if="level >= 3">
69
+ <view
70
+ v-for="(item, idx) in districtList"
71
+ :key="item.code"
72
+ class="picker-item"
73
+ :style="{
74
+ height: itemHeight + 'px',
75
+ lineHeight: itemHeight + 'px',
76
+ color: idx === pickerIndex[2] ? 'var(--prism-primary-color, #3478F6)' : 'var(--prism-text-secondary, #86909C)',
77
+ fontWeight: idx === pickerIndex[2] ? '600' : '400'
78
+ }"
79
+ >
80
+ {{ item.name }}
81
+ </view>
82
+ </picker-view-column>
83
+ </picker-view>
84
+ </view>
85
+
86
+ <!-- 模式2: 多层级列表选择 -->
87
+ <view class="cascade-body" v-else-if="mode === 'cascade'">
88
+ <!-- 热门城市 -->
89
+ <view class="hot-cities" v-if="showHotCities && currentLevel === 0 && !tempSelected.length">
90
+ <view class="hot-cities-title">
91
+ <text class="fa fa-fire"></text>
92
+ <text>热门城市</text>
93
+ </view>
94
+ <view class="hot-cities-list">
95
+ <view
96
+ v-for="city in hotCities"
97
+ :key="city.code"
98
+ class="hot-city-item"
99
+ @click="selectHotCity(city)"
100
+ >
101
+ {{ city.name }}
102
+ </view>
103
+ </view>
104
+ <view class="hot-cities-divider">
105
+ <text>全部城市</text>
106
+ </view>
107
+ </view>
108
+
109
+ <!-- 已选择的面包屑 -->
110
+ <view class="cascade-breadcrumb" v-if="tempSelected.length">
111
+ <view
112
+ v-for="(item, index) in tempSelected"
113
+ :key="index"
114
+ class="breadcrumb-item"
115
+ @click="backToLevel(index)"
116
+ >
117
+ <text>{{ item.name }}</text>
118
+ <text class="fa fa-chevron-right" v-if="index < tempSelected.length - 1"></text>
119
+ </view>
120
+ <view class="breadcrumb-clear" @click="clearSelection">
121
+ <text class="fa fa-times"></text>
122
+ </view>
123
+ </view>
124
+
125
+ <!-- 当前层级列表 -->
126
+ <scroll-view
127
+ class="cascade-list"
128
+ scroll-y
129
+ :scroll-with-animation="true"
130
+ @touchmove.stop
131
+ >
132
+ <view
133
+ v-for="item in currentLevelList"
134
+ :key="item.code"
135
+ class="cascade-item"
136
+ :class="{ 'selected': isItemSelected(item) }"
137
+ hover-class="none"
138
+ @click="selectItem(item)"
139
+ >
140
+ <view class="item-content">
141
+ <text class="item-name">{{ item.name }}</text>
142
+ <text class="item-code" v-if="showCode">{{ item.code }}</text>
143
+ </view>
144
+ <text class="fa fa-chevron-right item-arrow" v-if="hasChildren(item)"></text>
145
+ <text class="fa fa-check item-check" v-else-if="isItemSelected(item)"></text>
146
+ </view>
147
+ <view class="cascade-empty" v-if="!currentLevelList.length">
148
+ <text class="fa fa-inbox"></text>
149
+ <text>暂无数据</text>
150
+ </view>
151
+ </scroll-view>
152
+ </view>
153
+ </view>
154
+ </view>
155
+ </view>
156
+ </template>
157
+
158
+ <script setup>
159
+ import { ref, computed, watch, onMounted } from 'vue';
160
+ import { useAppStore } from '@/store/app';
161
+ import { rpx2px } from '@/utils/system';
162
+
163
+ const props = defineProps({
164
+ modelValue: {
165
+ type: Array,
166
+ default: () => []
167
+ },
168
+ // 选择模式:picker(滚轮)或 cascade(多层级列表)
169
+ mode: {
170
+ type: String,
171
+ default: 'picker',
172
+ validator: (val) => ['picker', 'cascade'].includes(val)
173
+ },
174
+ // 选择层级:2(省市)或 3(省市区)
175
+ level: {
176
+ type: Number,
177
+ default: 3,
178
+ validator: (val) => [2, 3].includes(val)
179
+ },
180
+ // 城市数据
181
+ data: {
182
+ type: Array,
183
+ default: () => []
184
+ },
185
+ placeholder: {
186
+ type: String,
187
+ default: '请选择城市'
188
+ },
189
+ title: {
190
+ type: String,
191
+ default: '选择城市'
192
+ },
193
+ // 是否显示编码
194
+ showCode: {
195
+ type: Boolean,
196
+ default: false
197
+ },
198
+ // 分隔符
199
+ separator: {
200
+ type: String,
201
+ default: '/'
202
+ },
203
+ disabled: {
204
+ type: Boolean,
205
+ default: false
206
+ },
207
+ // 是否显示热门城市
208
+ showHotCities: {
209
+ type: Boolean,
210
+ default: false
211
+ },
212
+ // 热门城市列表
213
+ hotCities: {
214
+ type: Array,
215
+ default: () => [
216
+ { name: '北京', code: '110000' },
217
+ { name: '上海', code: '310000' },
218
+ { name: '广州', code: '440100' },
219
+ { name: '深圳', code: '440300' },
220
+ { name: '杭州', code: '330100' },
221
+ { name: '成都', code: '510100' },
222
+ { name: '武汉', code: '420100' },
223
+ { name: '南京', code: '320100' }
224
+ ]
225
+ }
226
+ });
227
+
228
+ const emit = defineEmits(['update:modelValue', 'change', 'confirm', 'cancel']);
229
+
230
+ const appStore = useAppStore();
231
+ const showPicker = ref(false);
232
+ const pickerIndex = ref([0, 0, 0]);
233
+ const tempSelected = ref([]);
234
+ const currentLevel = ref(0);
235
+
236
+ // picker 配置 - 使用 rpx2px 换算,避免精度问题
237
+ const itemHeight = rpx2px(96);
238
+ const pickerHeight = `${itemHeight * 5}px`;
239
+ const indicatorStyle = `height: ${itemHeight}px`;
240
+
241
+ // 使用内置数据或外部数据
242
+ const regionData = computed(() => {
243
+ if (props.data && props.data.length) {
244
+ return props.data;
245
+ }
246
+ return defaultRegionData.value;
247
+ });
248
+
249
+ // 默认地区数据(示例数据,实际使用时应传入完整数据)
250
+ const defaultRegionData = ref([
251
+ {
252
+ code: '110000',
253
+ name: '北京市',
254
+ children: [
255
+ {
256
+ code: '110100',
257
+ name: '北京市',
258
+ children: [
259
+ { code: '110101', name: '东城区' },
260
+ { code: '110102', name: '西城区' },
261
+ { code: '110105', name: '朝阳区' },
262
+ { code: '110106', name: '丰台区' },
263
+ { code: '110107', name: '石景山区' },
264
+ { code: '110108', name: '海淀区' },
265
+ { code: '110109', name: '门头沟区' },
266
+ { code: '110111', name: '房山区' },
267
+ { code: '110112', name: '通州区' },
268
+ { code: '110113', name: '顺义区' },
269
+ { code: '110114', name: '昌平区' },
270
+ { code: '110115', name: '大兴区' },
271
+ { code: '110116', name: '怀柔区' },
272
+ { code: '110117', name: '平谷区' },
273
+ { code: '110118', name: '密云区' },
274
+ { code: '110119', name: '延庆区' }
275
+ ]
276
+ }
277
+ ]
278
+ },
279
+ {
280
+ code: '120000',
281
+ name: '天津市',
282
+ children: [
283
+ {
284
+ code: '120100',
285
+ name: '天津市',
286
+ children: [
287
+ { code: '120101', name: '和平区' },
288
+ { code: '120102', name: '河东区' },
289
+ { code: '120103', name: '河西区' },
290
+ { code: '120104', name: '南开区' },
291
+ { code: '120105', name: '河北区' },
292
+ { code: '120106', name: '红桥区' },
293
+ { code: '120110', name: '东丽区' },
294
+ { code: '120111', name: '西青区' },
295
+ { code: '120112', name: '津南区' },
296
+ { code: '120113', name: '北辰区' },
297
+ { code: '120114', name: '武清区' },
298
+ { code: '120115', name: '宝坻区' },
299
+ { code: '120116', name: '滨海新区' },
300
+ { code: '120117', name: '宁河区' },
301
+ { code: '120118', name: '静海区' },
302
+ { code: '120119', name: '蓟州区' }
303
+ ]
304
+ }
305
+ ]
306
+ },
307
+ {
308
+ code: '310000',
309
+ name: '上海市',
310
+ children: [
311
+ {
312
+ code: '310100',
313
+ name: '上海市',
314
+ children: [
315
+ { code: '310101', name: '黄浦区' },
316
+ { code: '310104', name: '徐汇区' },
317
+ { code: '310105', name: '长宁区' },
318
+ { code: '310106', name: '静安区' },
319
+ { code: '310107', name: '普陀区' },
320
+ { code: '310109', name: '虹口区' },
321
+ { code: '310110', name: '杨浦区' },
322
+ { code: '310112', name: '闵行区' },
323
+ { code: '310113', name: '宝山区' },
324
+ { code: '310114', name: '嘉定区' },
325
+ { code: '310115', name: '浦东新区' },
326
+ { code: '310116', name: '金山区' },
327
+ { code: '310117', name: '松江区' },
328
+ { code: '310118', name: '青浦区' },
329
+ { code: '310120', name: '奉贤区' },
330
+ { code: '310151', name: '崇明区' }
331
+ ]
332
+ }
333
+ ]
334
+ },
335
+ {
336
+ code: '440000',
337
+ name: '广东省',
338
+ children: [
339
+ {
340
+ code: '440100',
341
+ name: '广州市',
342
+ children: [
343
+ { code: '440103', name: '荔湾区' },
344
+ { code: '440104', name: '越秀区' },
345
+ { code: '440105', name: '海珠区' },
346
+ { code: '440106', name: '天河区' },
347
+ { code: '440111', name: '白云区' },
348
+ { code: '440112', name: '黄埔区' },
349
+ { code: '440113', name: '番禺区' },
350
+ { code: '440114', name: '花都区' },
351
+ { code: '440115', name: '南沙区' },
352
+ { code: '440117', name: '从化区' },
353
+ { code: '440118', name: '增城区' }
354
+ ]
355
+ },
356
+ {
357
+ code: '440300',
358
+ name: '深圳市',
359
+ children: [
360
+ { code: '440303', name: '罗湖区' },
361
+ { code: '440304', name: '福田区' },
362
+ { code: '440305', name: '南山区' },
363
+ { code: '440306', name: '宝安区' },
364
+ { code: '440307', name: '龙岗区' },
365
+ { code: '440308', name: '盐田区' },
366
+ { code: '440309', name: '龙华区' },
367
+ { code: '440310', name: '坪山区' },
368
+ { code: '440311', name: '光明区' }
369
+ ]
370
+ },
371
+ {
372
+ code: '440400',
373
+ name: '珠海市',
374
+ children: [
375
+ { code: '440402', name: '香洲区' },
376
+ { code: '440403', name: '斗门区' },
377
+ { code: '440404', name: '金湾区' }
378
+ ]
379
+ }
380
+ ]
381
+ },
382
+ {
383
+ code: '330000',
384
+ name: '浙江省',
385
+ children: [
386
+ {
387
+ code: '330100',
388
+ name: '杭州市',
389
+ children: [
390
+ { code: '330102', name: '上城区' },
391
+ { code: '330105', name: '拱墅区' },
392
+ { code: '330106', name: '西湖区' },
393
+ { code: '330108', name: '滨江区' },
394
+ { code: '330109', name: '萧山区' },
395
+ { code: '330110', name: '余杭区' },
396
+ { code: '330111', name: '富阳区' },
397
+ { code: '330112', name: '临安区' },
398
+ { code: '330113', name: '临平区' },
399
+ { code: '330114', name: '钱塘区' }
400
+ ]
401
+ },
402
+ {
403
+ code: '330200',
404
+ name: '宁波市',
405
+ children: [
406
+ { code: '330203', name: '海曙区' },
407
+ { code: '330205', name: '江北区' },
408
+ { code: '330206', name: '北仑区' },
409
+ { code: '330211', name: '镇海区' },
410
+ { code: '330212', name: '鄞州区' },
411
+ { code: '330213', name: '奉化区' }
412
+ ]
413
+ }
414
+ ]
415
+ },
416
+ {
417
+ code: '320000',
418
+ name: '江苏省',
419
+ children: [
420
+ {
421
+ code: '320100',
422
+ name: '南京市',
423
+ children: [
424
+ { code: '320102', name: '玄武区' },
425
+ { code: '320104', name: '秦淮区' },
426
+ { code: '320105', name: '建邺区' },
427
+ { code: '320106', name: '鼓楼区' },
428
+ { code: '320111', name: '浦口区' },
429
+ { code: '320113', name: '栖霞区' },
430
+ { code: '320114', name: '雨花台区' },
431
+ { code: '320115', name: '江宁区' },
432
+ { code: '320116', name: '六合区' },
433
+ { code: '320117', name: '溧水区' },
434
+ { code: '320118', name: '高淳区' }
435
+ ]
436
+ },
437
+ {
438
+ code: '320500',
439
+ name: '苏州市',
440
+ children: [
441
+ { code: '320505', name: '虎丘区' },
442
+ { code: '320506', name: '吴中区' },
443
+ { code: '320507', name: '相城区' },
444
+ { code: '320508', name: '姑苏区' },
445
+ { code: '320509', name: '吴江区' }
446
+ ]
447
+ }
448
+ ]
449
+ }
450
+ ]);
451
+
452
+ // 省份列表
453
+ const provinceList = computed(() => regionData.value || []);
454
+
455
+ // 城市列表
456
+ const cityList = computed(() => {
457
+ const province = provinceList.value[pickerIndex.value[0]];
458
+ return province?.children || [];
459
+ });
460
+
461
+ // 区县列表
462
+ const districtList = computed(() => {
463
+ const city = cityList.value[pickerIndex.value[1]];
464
+ return city?.children || [];
465
+ });
466
+
467
+ // 当前层级列表(cascade模式)
468
+ const currentLevelList = computed(() => {
469
+ if (currentLevel.value === 0) {
470
+ return provinceList.value;
471
+ } else if (currentLevel.value === 1) {
472
+ return tempSelected.value[0]?.children || [];
473
+ } else if (currentLevel.value === 2) {
474
+ return tempSelected.value[1]?.children || [];
475
+ }
476
+ return [];
477
+ });
478
+
479
+ // 显示值
480
+ const displayValue = computed(() => {
481
+ if (!props.modelValue || !props.modelValue.length) return '';
482
+ return props.modelValue.map(item => item.name || item).join(props.separator);
483
+ });
484
+
485
+ // 打开选择器
486
+ function openPicker() {
487
+ if (props.disabled) return;
488
+
489
+ // 先初始化数据,再显示弹窗
490
+ if (props.mode === 'cascade') {
491
+ tempSelected.value = [...(props.modelValue || [])];
492
+ const selectedLen = tempSelected.value.length;
493
+ currentLevel.value = selectedLen >= props.level ? props.level - 1 : selectedLen;
494
+ } else {
495
+ initPickerIndex();
496
+ }
497
+ showPicker.value = true;
498
+ }
499
+
500
+ // 初始化picker索引
501
+ function initPickerIndex() {
502
+ if (!props.modelValue || !props.modelValue.length) {
503
+ pickerIndex.value = [0, 0, 0];
504
+ return;
505
+ }
506
+
507
+ const [province, city, district] = props.modelValue;
508
+
509
+ // 查找省份索引
510
+ const provinceIdx = provinceList.value.findIndex(
511
+ p => p.code === province?.code || p.name === province?.name
512
+ );
513
+ pickerIndex.value[0] = provinceIdx >= 0 ? provinceIdx : 0;
514
+
515
+ // 查找城市索引
516
+ const cities = provinceList.value[pickerIndex.value[0]]?.children || [];
517
+ const cityIdx = cities.findIndex(
518
+ c => c.code === city?.code || c.name === city?.name
519
+ );
520
+ pickerIndex.value[1] = cityIdx >= 0 ? cityIdx : 0;
521
+
522
+ // 查找区县索引
523
+ if (props.level >= 3) {
524
+ const districts = cities[pickerIndex.value[1]]?.children || [];
525
+ const districtIdx = districts.findIndex(
526
+ d => d.code === district?.code || d.name === district?.name
527
+ );
528
+ pickerIndex.value[2] = districtIdx >= 0 ? districtIdx : 0;
529
+ }
530
+ }
531
+
532
+ // 关闭选择器
533
+ function closePicker() {
534
+ showPicker.value = false;
535
+ emit('cancel');
536
+ }
537
+
538
+ // picker变化
539
+ function onPickerChange(e) {
540
+ const values = e.detail.value;
541
+
542
+ // 如果省份变化,重置城市和区县索引
543
+ if (values[0] !== pickerIndex.value[0]) {
544
+ values[1] = 0;
545
+ values[2] = 0;
546
+ } else if (values[1] !== pickerIndex.value[1]) {
547
+ // 如果城市变化,重置区县索引
548
+ values[2] = 0;
549
+ }
550
+
551
+ pickerIndex.value = values;
552
+ }
553
+
554
+ // 选择热门城市
555
+ function selectHotCity(city) {
556
+ const result = [{ code: city.code, name: city.name }];
557
+ emit('update:modelValue', result);
558
+ emit('change', result);
559
+ emit('confirm', result);
560
+ showPicker.value = false;
561
+ }
562
+
563
+ // 确认选择
564
+ function confirmSelect() {
565
+ let result = [];
566
+
567
+ if (props.mode === 'picker') {
568
+ const province = provinceList.value[pickerIndex.value[0]];
569
+ const city = cityList.value[pickerIndex.value[1]];
570
+
571
+ if (province) {
572
+ result.push({ code: province.code, name: province.name });
573
+ }
574
+ if (city) {
575
+ result.push({ code: city.code, name: city.name });
576
+ }
577
+ if (props.level >= 3) {
578
+ const district = districtList.value[pickerIndex.value[2]];
579
+ if (district) {
580
+ result.push({ code: district.code, name: district.name });
581
+ }
582
+ }
583
+ } else {
584
+ result = [...tempSelected.value];
585
+ }
586
+
587
+ emit('update:modelValue', result);
588
+ emit('change', result);
589
+ emit('confirm', result);
590
+ showPicker.value = false;
591
+ }
592
+
593
+ // cascade模式 - 选择项目
594
+ function selectItem(item) {
595
+ if (hasChildren(item) && currentLevel.value < props.level - 1) {
596
+ // 有子级且未到最后一级,进入下一级
597
+ tempSelected.value = tempSelected.value.slice(0, currentLevel.value);
598
+ tempSelected.value.push({ code: item.code, name: item.name, children: item.children });
599
+ currentLevel.value++;
600
+ } else {
601
+ // 最后一级,选中并自动确认
602
+ tempSelected.value = tempSelected.value.slice(0, currentLevel.value);
603
+ tempSelected.value.push({ code: item.code, name: item.name });
604
+ confirmSelect();
605
+ }
606
+ }
607
+
608
+ // 判断是否有子级
609
+ function hasChildren(item) {
610
+ return item.children && item.children.length > 0;
611
+ }
612
+
613
+ // 判断是否选中
614
+ function isItemSelected(item) {
615
+ const current = tempSelected.value[currentLevel.value];
616
+ return current && (current.code === item.code || current.name === item.name);
617
+ }
618
+
619
+ // 返回到某一级
620
+ function backToLevel(index) {
621
+ currentLevel.value = index;
622
+ tempSelected.value = tempSelected.value.slice(0, index);
623
+ }
624
+
625
+ // 清除选择
626
+ function clearSelection() {
627
+ tempSelected.value = [];
628
+ currentLevel.value = 0;
629
+ }
630
+
631
+ // 监听modelValue变化
632
+ watch(() => props.modelValue, (val) => {
633
+ if (props.mode === 'cascade' && showPicker.value) {
634
+ tempSelected.value = [...(val || [])];
635
+ }
636
+ }, { deep: true });
637
+ </script>
638
+
639
+ <style lang="scss">
640
+ .prism-city-picker {
641
+ .picker-trigger {
642
+ cursor: pointer;
643
+ }
644
+
645
+ .picker-popup {
646
+ position: fixed;
647
+ top: 0;
648
+ left: 0;
649
+ right: 0;
650
+ bottom: 0;
651
+ background: var(--prism-mask-bg, rgba(0, 0, 0, 0.5));
652
+ display: flex;
653
+ align-items: flex-end;
654
+ justify-content: center;
655
+ z-index: 9999;
656
+ }
657
+
658
+ .picker-content {
659
+ width: 100%;
660
+ background: var(--prism-bg-color-card, #FFFFFF);
661
+ border-radius: 24rpx 24rpx 0 0;
662
+ max-height: 70vh;
663
+ display: flex;
664
+ flex-direction: column;
665
+ }
666
+
667
+ .picker-header {
668
+ display: flex;
669
+ align-items: center;
670
+ justify-content: space-between;
671
+ padding: 24rpx 32rpx;
672
+ border-bottom: 1rpx solid var(--prism-border-color-light, #E5E6EB);
673
+ }
674
+
675
+ .picker-cancel {
676
+ font-size: 30rpx;
677
+ color: var(--prism-text-secondary, #86909C);
678
+ padding: 8rpx 16rpx;
679
+ }
680
+
681
+ .picker-title {
682
+ font-size: 32rpx;
683
+ font-weight: 600;
684
+ color: var(--prism-text-primary, #1D2129);
685
+ }
686
+
687
+ .picker-confirm {
688
+ font-size: 30rpx;
689
+ color: var(--prism-primary-color, #3478F6);
690
+ padding: 8rpx 16rpx;
691
+ font-weight: 500;
692
+ }
693
+
694
+ .picker-body {
695
+ // height 通过 JS 动态设置
696
+ padding-bottom: env(safe-area-inset-bottom);
697
+ box-sizing: content-box;
698
+ position: relative;
699
+ }
700
+
701
+ .picker-view {
702
+ // height 通过 JS 动态设置
703
+ }
704
+
705
+ .picker-item {
706
+ display: flex;
707
+ align-items: center;
708
+ justify-content: center;
709
+ font-size: 32rpx;
710
+ // height 和 lineHeight 通过 JS 动态设置
711
+ font-weight: 500;
712
+ }
713
+
714
+ // picker指示器样式 - 隐藏原有背景
715
+ :deep(.picker-indicator) {
716
+ // height 通过 indicatorStyle 动态设置
717
+ background: transparent !important;
718
+ background-color: transparent !important;
719
+ border: none !important;
720
+ border-width: 0 !important;
721
+ border-style: none !important;
722
+ border-color: transparent !important;
723
+ box-shadow: none !important;
724
+ z-index: 0 !important;
725
+ }
726
+
727
+ // 整体激活项背景 - 横跨所有列的圆角气泡(浅色模式)- 暂时禁用
728
+ // .picker-body::before {
729
+ // content: '';
730
+ // position: absolute;
731
+ // left: 16rpx;
732
+ // right: 16rpx;
733
+ // top: 50%;
734
+ // transform: translateY(-50%);
735
+ // height: 96rpx;
736
+ // background: var(--prism-primary-light, rgba(52, 120, 246, 0.08));
737
+ // border-radius: 12rpx;
738
+ // z-index: 0;
739
+ // pointer-events: none;
740
+ // }
741
+
742
+ // 确保文字在背景色上方
743
+ .picker-item {
744
+ position: relative;
745
+ z-index: 2;
746
+ }
747
+
748
+ // 浅色模式遮罩层 - 放在最下层
749
+ :deep(.uni-picker-view-mask) {
750
+ z-index: 1 !important;
751
+ background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0)),
752
+ linear-gradient(0deg, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0)) !important;
753
+ }
754
+
755
+ // 覆盖 uni-app 默认边框和背景
756
+ :deep(.uni-picker-view-indicator) {
757
+ background: transparent !important;
758
+ background-color: transparent !important;
759
+ border: none !important;
760
+ border-top: none !important;
761
+ border-bottom: none !important;
762
+ border-color: transparent !important;
763
+ }
764
+
765
+ // 多层级选择
766
+ .cascade-body {
767
+ display: flex;
768
+ flex-direction: column;
769
+ height: 500rpx;
770
+ }
771
+
772
+ // 热门城市
773
+ .hot-cities {
774
+ padding: 24rpx 32rpx;
775
+ background: var(--prism-bg-color-card, #FFFFFF);
776
+ }
777
+
778
+ .hot-cities-title {
779
+ display: flex;
780
+ align-items: center;
781
+ gap: 8rpx;
782
+ font-size: 26rpx;
783
+ color: var(--prism-text-secondary, #86909C);
784
+ margin-bottom: 20rpx;
785
+
786
+ .fa-fire {
787
+ color: #FF6B00;
788
+ }
789
+ }
790
+
791
+ .hot-cities-list {
792
+ display: flex;
793
+ flex-wrap: wrap;
794
+ gap: 16rpx;
795
+ }
796
+
797
+ .hot-city-item {
798
+ padding: 16rpx 28rpx;
799
+ background: var(--prism-bg-color-container, #F7F8FA);
800
+ border-radius: 8rpx;
801
+ font-size: 28rpx;
802
+ color: var(--prism-text-primary, #1D2129);
803
+
804
+ &:active {
805
+ background: var(--prism-primary-light, rgba(52, 120, 246, 0.08));
806
+ color: var(--prism-primary-color, #3478F6);
807
+ }
808
+ }
809
+
810
+ .hot-cities-divider {
811
+ display: flex;
812
+ align-items: center;
813
+ margin-top: 24rpx;
814
+ padding-top: 20rpx;
815
+ border-top: 1rpx solid var(--prism-border-color-light, #E5E6EB);
816
+ font-size: 26rpx;
817
+ color: var(--prism-text-secondary, #86909C);
818
+ }
819
+
820
+ .cascade-breadcrumb {
821
+ display: flex;
822
+ align-items: center;
823
+ padding: 20rpx 32rpx;
824
+ background: var(--prism-bg-color-container, #F7F8FA);
825
+ flex-wrap: wrap;
826
+ gap: 8rpx;
827
+ }
828
+
829
+ .breadcrumb-item {
830
+ display: flex;
831
+ align-items: center;
832
+ gap: 8rpx;
833
+
834
+ text:first-child {
835
+ font-size: 28rpx;
836
+ color: var(--prism-primary-color, #3478F6);
837
+ }
838
+
839
+ .fa {
840
+ font-size: 20rpx;
841
+ color: var(--prism-text-placeholder, #C9CDD4);
842
+ }
843
+ }
844
+
845
+ .breadcrumb-clear {
846
+ margin-left: auto;
847
+ padding: 8rpx;
848
+
849
+ .fa {
850
+ font-size: 28rpx;
851
+ color: var(--prism-text-secondary, #86909C);
852
+ }
853
+ }
854
+
855
+ .cascade-list {
856
+ flex: 1;
857
+ height: 400rpx;
858
+ }
859
+
860
+ .cascade-item {
861
+ display: flex;
862
+ align-items: center;
863
+ padding: 28rpx 32rpx;
864
+ border-bottom: 1rpx solid var(--prism-border-color-light, #E5E6EB);
865
+
866
+ &.selected {
867
+ .item-name {
868
+ color: var(--prism-primary-color, #3478F6);
869
+ font-weight: 500;
870
+ }
871
+ }
872
+ }
873
+
874
+ .item-content {
875
+ flex: 1;
876
+ }
877
+
878
+ .item-name {
879
+ font-size: 30rpx;
880
+ color: var(--prism-text-primary, #1D2129);
881
+ }
882
+
883
+ .item-code {
884
+ font-size: 24rpx;
885
+ color: var(--prism-text-secondary, #86909C);
886
+ margin-top: 8rpx;
887
+ }
888
+
889
+ .item-arrow {
890
+ font-size: 24rpx;
891
+ color: var(--prism-text-placeholder, #C9CDD4);
892
+ }
893
+
894
+ .item-check {
895
+ font-size: 28rpx;
896
+ color: var(--prism-primary-color, #3478F6);
897
+ }
898
+
899
+ .cascade-empty {
900
+ padding: 80rpx 32rpx;
901
+ display: flex;
902
+ flex-direction: column;
903
+ align-items: center;
904
+ gap: 16rpx;
905
+
906
+ .fa {
907
+ font-size: 64rpx;
908
+ color: var(--prism-text-placeholder, #C9CDD4);
909
+ }
910
+
911
+ text:last-child {
912
+ font-size: 28rpx;
913
+ color: var(--prism-text-secondary, #86909C);
914
+ }
915
+ }
916
+ }
917
+
918
+ // 深色模式
919
+ .dark-mode.prism-city-picker {
920
+ .picker-content {
921
+ background: var(--prism-bg-color-card, #1A1A1A);
922
+ }
923
+
924
+ // picker 滚轮模式
925
+ .picker-item {
926
+ color: var(--prism-text-primary, #E5E6EB);
927
+ }
928
+
929
+ .picker-body :deep(.picker-indicator),
930
+ :deep(.picker-indicator) {
931
+ background: transparent !important;
932
+ background-color: transparent !important;
933
+ border: none !important;
934
+ border-width: 0 !important;
935
+ border-style: none !important;
936
+ border-color: transparent !important;
937
+ }
938
+
939
+ // 整体激活项背景 - 横跨所有列的圆角气泡 - 暂时禁用
940
+ // .picker-body::before {
941
+ // content: '';
942
+ // position: absolute;
943
+ // left: 16rpx;
944
+ // right: 16rpx;
945
+ // top: 50%;
946
+ // transform: translateY(-50%);
947
+ // height: 96rpx;
948
+ // background: rgba(255, 255, 255, 0.08);
949
+ // border-radius: 12rpx;
950
+ // z-index: 0;
951
+ // pointer-events: none;
952
+ // }
953
+
954
+ :deep(.uni-picker-view-indicator) {
955
+ background: transparent !important;
956
+ border: none !important;
957
+ border-width: 0 !important;
958
+ border-color: transparent !important;
959
+ }
960
+
961
+ :deep(.uni-picker-view-indicator)::before,
962
+ :deep(.uni-picker-view-indicator)::after {
963
+ display: none !important;
964
+ border: none !important;
965
+ }
966
+
967
+ :deep(picker-view-column)::before,
968
+ :deep(picker-view-column)::after {
969
+ display: none !important;
970
+ }
971
+
972
+ // 修复 picker-view 上下遮罩渐变色
973
+ :deep(.uni-picker-view-mask) {
974
+ background-image: linear-gradient(180deg, rgba(26, 26, 26, 0.95), rgba(26, 26, 26, 0.6)),
975
+ linear-gradient(0deg, rgba(26, 26, 26, 0.95), rgba(26, 26, 26, 0.6)) !important;
976
+ }
977
+
978
+ // cascade 列表模式
979
+ .hot-cities {
980
+ background: var(--prism-bg-color-card, #1A1A1A);
981
+ }
982
+
983
+ .hot-city-item {
984
+ background: var(--prism-bg-color-container, #2A2A2A);
985
+ color: var(--prism-text-primary, #E5E6EB);
986
+ }
987
+
988
+ .hot-cities-divider {
989
+ border-top-color: var(--prism-border-color-base, #3A3A3A);
990
+ }
991
+
992
+ .cascade-breadcrumb {
993
+ background: var(--prism-bg-color-container, #2A2A2A);
994
+ }
995
+
996
+ .cascade-item {
997
+ &:active {
998
+ background: var(--prism-bg-color-container, #2A2A2A);
999
+ }
1000
+
1001
+ &.selected {
1002
+ .item-name {
1003
+ color: var(--prism-primary-color, #3478F6);
1004
+ }
1005
+ }
1006
+ }
1007
+ }
1008
+ </style>