@xlui/xux-ui 0.3.0 → 1.1.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.
@@ -0,0 +1,870 @@
1
+ <template>
2
+ <div ref="containerRef" class="region-cascader-container relative">
3
+ <!-- 输入框 -->
4
+ <div
5
+ @click="toggleDropdown"
6
+ :class="[
7
+ 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#1a1a1a] focus:border-[#1a1a1a] transition-colors duration-200 cursor-pointer bg-white flex items-center justify-between',
8
+ customClass,
9
+ { 'opacity-60 cursor-not-allowed': disabled }
10
+ ]"
11
+ :tabindex="disabled ? -1 : 0"
12
+ @keydown.enter="toggleDropdown"
13
+ @keydown.space="toggleDropdown"
14
+ @keydown.escape="closeDropdown"
15
+ >
16
+ <div class="flex items-center gap-2 flex-1 overflow-hidden">
17
+ <span v-if="displayValue" class="truncate">{{ displayValue }}</span>
18
+ <span v-else class="text-gray-400">{{ placeholder }}</span>
19
+ </div>
20
+ <svg
21
+ class="w-5 h-5 text-gray-400 transition-transform duration-200 flex-shrink-0"
22
+ :class="{ 'rotate-180': isOpen }"
23
+ fill="none"
24
+ stroke="currentColor"
25
+ viewBox="0 0 24 24"
26
+ >
27
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
28
+ </svg>
29
+ </div>
30
+
31
+ <!-- 级联下拉框 -->
32
+ <div
33
+ v-if="isOpen && !disabled"
34
+ class="absolute z-[9999] mt-1 bg-white border border-gray-300 rounded-md shadow-lg"
35
+ :style="{ minWidth: '100%', width: 'max-content' }"
36
+ >
37
+ <!-- 搜索框 -->
38
+ <div v-if="searchable" class="p-3 border-b border-gray-200">
39
+ <div class="relative">
40
+ <input
41
+ ref="searchInputRef"
42
+ v-model="searchKeyword"
43
+ type="text"
44
+ :placeholder="searchPlaceholder"
45
+ class="w-full px-3 py-2 pl-9 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#1a1a1a] focus:border-[#1a1a1a] transition-colors"
46
+ @input="handleSearch"
47
+ @keydown.stop
48
+ />
49
+ <svg
50
+ class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400"
51
+ fill="none"
52
+ stroke="currentColor"
53
+ viewBox="0 0 24 24"
54
+ >
55
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
56
+ </svg>
57
+ <button
58
+ v-if="searchKeyword"
59
+ @click="clearSearch"
60
+ class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
61
+ >
62
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
63
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
64
+ </svg>
65
+ </button>
66
+ </div>
67
+ <!-- 当前选择路径提示 -->
68
+ <div v-if="currentPath" class="mt-2 text-xs text-gray-500 flex items-center gap-1">
69
+ <span>当前:</span>
70
+ <span class="font-medium">{{ currentPath }}</span>
71
+ </div>
72
+ </div>
73
+
74
+ <!-- 全局搜索结果 -->
75
+ <div v-if="searchable && searchKeyword && searchResults.length > 0" class="max-h-80 overflow-y-auto" style="min-width: 600px;">
76
+ <div class="px-3 py-2 text-xs text-gray-500 bg-gray-50 sticky top-0">
77
+ 找到 {{ searchResults.length }} 个结果(点击自动定位)
78
+ </div>
79
+ <div
80
+ v-for="result in searchResults"
81
+ :key="result.value"
82
+ @click.stop="selectFromGlobalSearch(result)"
83
+ class="px-3 py-2 hover:bg-gray-50 cursor-pointer transition-colors border-b border-gray-100"
84
+ >
85
+ <div class="flex items-center justify-between">
86
+ <div class="flex-1">
87
+ <span v-html="highlightText(result.label, searchKeyword)" class="font-medium"></span>
88
+ <span class="ml-2 text-xs text-gray-400">
89
+ {{ ['', '省', '市', '区'][result.level || 0] }}
90
+ </span>
91
+ </div>
92
+ </div>
93
+ <div v-if="result.pathLabels && result.pathLabels.length > 1" class="text-xs text-gray-500 mt-1">
94
+ {{ result.pathLabels.slice(0, -1).join(' > ') }}
95
+ </div>
96
+ </div>
97
+ </div>
98
+
99
+ <!-- 级联列表 -->
100
+ <div v-if="!searchable || !searchKeyword || searchResults.length === 0" class="flex" style="min-width: 600px;">
101
+ <!-- 省份列表 -->
102
+ <div ref="provinceListRef" class="flex-1 border-r border-gray-200 max-h-60 overflow-y-auto min-w-[200px]">
103
+ <div
104
+ v-for="province in filteredProvinces"
105
+ :key="province.value"
106
+ :ref="el => { if (selectedProvince?.value === province.value) selectedProvinceRef = el as HTMLElement }"
107
+ @click.stop="selectProvince(province)"
108
+ class="px-3 py-2 hover:bg-gray-50 cursor-pointer transition-colors whitespace-nowrap"
109
+ :class="{ 'bg-blue-50 text-blue-600': selectedProvince?.value === province.value }"
110
+ v-html="highlightText(province.label, searchKeyword)"
111
+ >
112
+ </div>
113
+ </div>
114
+
115
+ <!-- 城市列表 -->
116
+ <div
117
+ v-if="filteredCities.length > 0"
118
+ ref="cityListRef"
119
+ class="flex-1 border-r border-gray-200 max-h-60 overflow-y-auto min-w-[200px]"
120
+ >
121
+ <div
122
+ v-for="city in filteredCities"
123
+ :key="city.value"
124
+ :ref="el => { if (selectedCity?.value === city.value) selectedCityRef = el as HTMLElement }"
125
+ @click.stop="selectCity(city)"
126
+ class="px-3 py-2 hover:bg-gray-50 cursor-pointer transition-colors whitespace-nowrap"
127
+ :class="{ 'bg-blue-50 text-blue-600': selectedCity?.value === city.value }"
128
+ v-html="highlightText(city.label, searchKeyword)"
129
+ >
130
+ </div>
131
+ </div>
132
+
133
+ <!-- 区县列表 -->
134
+ <div
135
+ v-if="filteredDistricts.length > 0"
136
+ ref="districtListRef"
137
+ class="flex-1 border-r border-gray-200 max-h-60 overflow-y-auto min-w-[200px]"
138
+ >
139
+ <div
140
+ v-for="district in filteredDistricts"
141
+ :key="district.value"
142
+ :ref="el => { if (selectedDistrict?.value === district.value) selectedDistrictRef = el as HTMLElement }"
143
+ @click.stop="selectDistrict(district)"
144
+ class="px-3 py-2 hover:bg-gray-50 cursor-pointer transition-colors whitespace-nowrap"
145
+ :class="{ 'bg-blue-50 text-blue-600': selectedDistrict?.value === district.value }"
146
+ v-html="highlightText(district.label, searchKeyword)"
147
+ >
148
+ </div>
149
+ </div>
150
+
151
+ </div>
152
+
153
+ <!-- 数据加载提示 -->
154
+ <div v-if="!workerReady" class="px-4 py-8 text-center text-gray-400">
155
+ <div class="mb-2">正在初始化地区数据...</div>
156
+ <div class="text-xs">请稍候</div>
157
+ </div>
158
+ <div v-else-if="provinces.length === 0" class="px-4 py-8 text-center text-gray-400">
159
+ <div class="mb-2">暂无数据</div>
160
+ </div>
161
+ </div>
162
+
163
+ <!-- 遮罩层 -->
164
+ <div
165
+ v-if="isOpen && !disabled"
166
+ class="fixed inset-0 z-[9998]"
167
+ @click="closeDropdown"
168
+ ></div>
169
+ </div>
170
+ </template>
171
+
172
+ <script setup lang="ts">
173
+ /**
174
+ * RegionCascader 中国地区级联选择器(支持5级)
175
+ * @displayName XRegionCascader
176
+ */
177
+ import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
178
+ import chinaAreasData from './data/china-areas.full.json'
179
+
180
+ interface AreaNode {
181
+ value: string
182
+ label: string
183
+ leaf: boolean
184
+ level?: number
185
+ path?: string[]
186
+ pathLabels?: string[]
187
+ }
188
+
189
+ export interface RegionCascaderProps {
190
+ modelValue?: string[]
191
+ placeholder?: string
192
+ disabled?: boolean
193
+ customClass?: string
194
+ separator?: string
195
+ maxLevel?: 1 | 2 | 3 // 最大级别:1-省,2-省市,3-省市区
196
+ searchable?: boolean // 是否启用搜索功能
197
+ }
198
+
199
+ const props = withDefaults(defineProps<RegionCascaderProps>(), {
200
+ modelValue: () => [],
201
+ placeholder: '请选择地区',
202
+ disabled: false,
203
+ customClass: '',
204
+ separator: ' / ',
205
+ maxLevel: 3,
206
+ searchable: false
207
+ })
208
+
209
+ const emit = defineEmits<{
210
+ 'update:modelValue': [value: string[]]
211
+ 'change': [value: string[], labels: string[]]
212
+ }>()
213
+
214
+ // 下拉框状态
215
+ const isOpen = ref(false)
216
+ const containerRef = ref<HTMLElement>()
217
+ const searchInputRef = ref<HTMLInputElement>()
218
+ const provinceListRef = ref<HTMLElement>()
219
+ const cityListRef = ref<HTMLElement>()
220
+ const districtListRef = ref<HTMLElement>()
221
+ const selectedProvinceRef = ref<HTMLElement>()
222
+ const selectedCityRef = ref<HTMLElement>()
223
+ const selectedDistrictRef = ref<HTMLElement>()
224
+ const workerReady = ref(false)
225
+
226
+ // 搜索关键词
227
+ const searchKeyword = ref('')
228
+ const searchResults = ref<AreaNode[]>([])
229
+ const isSearching = ref(false)
230
+
231
+ // Worker实例
232
+ let worker: Worker | null = null
233
+
234
+ // 缓存已加载的children
235
+ const childrenCache = new Map<string, AreaNode[]>()
236
+
237
+ // 各级选中的值
238
+ const selectedProvince = ref<AreaNode | null>(null)
239
+ const selectedCity = ref<AreaNode | null>(null)
240
+ const selectedDistrict = ref<AreaNode | null>(null)
241
+
242
+ // 各级列表数据
243
+ const provinces = ref<AreaNode[]>([])
244
+ const cities = ref<AreaNode[]>([])
245
+ const districts = ref<AreaNode[]>([])
246
+
247
+ // 显示的值
248
+ const displayValue = computed(() => {
249
+ const labels: string[] = []
250
+ if (selectedProvince.value) labels.push(selectedProvince.value.label)
251
+ if (selectedCity.value) labels.push(selectedCity.value.label)
252
+ if (selectedDistrict.value) labels.push(selectedDistrict.value.label)
253
+ return labels.length > 0 ? labels.join(props.separator) : ''
254
+ })
255
+
256
+ // 搜索占位符(全局搜索)
257
+ const searchPlaceholder = computed(() => {
258
+ return '搜索省/市/区,支持智能定位...'
259
+ })
260
+
261
+ // 当前选择路径
262
+ const currentPath = computed(() => {
263
+ const labels: string[] = []
264
+ if (selectedProvince.value) labels.push(selectedProvince.value.label)
265
+ if (selectedCity.value) labels.push(selectedCity.value.label)
266
+ if (selectedDistrict.value) labels.push(selectedDistrict.value.label)
267
+ return labels.length > 0 ? labels.join(' > ') : ''
268
+ })
269
+
270
+ // 过滤后的列表
271
+ const filteredProvinces = computed(() => {
272
+ if (!searchKeyword.value.trim()) return provinces.value
273
+ const keyword = searchKeyword.value.toLowerCase().trim()
274
+ return provinces.value.filter(p =>
275
+ p.label.toLowerCase().includes(keyword) || p.value.includes(keyword)
276
+ )
277
+ })
278
+
279
+ const filteredCities = computed(() => {
280
+ if (!searchKeyword.value.trim()) return cities.value
281
+ const keyword = searchKeyword.value.toLowerCase().trim()
282
+ return cities.value.filter(c =>
283
+ c.label.toLowerCase().includes(keyword) || c.value.includes(keyword)
284
+ )
285
+ })
286
+
287
+ const filteredDistricts = computed(() => {
288
+ if (!searchKeyword.value.trim()) return districts.value
289
+ const keyword = searchKeyword.value.toLowerCase().trim()
290
+ return districts.value.filter(d =>
291
+ d.label.toLowerCase().includes(keyword) || d.value.includes(keyword)
292
+ )
293
+ })
294
+
295
+ // 高亮文本
296
+ function highlightText(text: string, keyword: string): string {
297
+ if (!keyword || !keyword.trim()) return text
298
+
299
+ const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
300
+ const regex = new RegExp(`(${escapedKeyword})`, 'gi')
301
+ return text.replace(regex, '<span class="bg-yellow-200 font-semibold">$1</span>')
302
+ }
303
+
304
+ // 从Worker获取子节点
305
+ function loadChildren(parentValue: string): Promise<AreaNode[]> {
306
+ return new Promise((resolve) => {
307
+ if (!worker || !workerReady.value) {
308
+ resolve([])
309
+ return
310
+ }
311
+
312
+ // 检查缓存
313
+ if (childrenCache.has(parentValue)) {
314
+ resolve(childrenCache.get(parentValue)!)
315
+ return
316
+ }
317
+
318
+ const handler = (e: MessageEvent) => {
319
+ if (e.data?.type === 'children' && e.data.parent === parentValue) {
320
+ const list = e.data.list || []
321
+ childrenCache.set(parentValue, list)
322
+ resolve(list)
323
+ worker!.removeEventListener('message', handler)
324
+ }
325
+ }
326
+
327
+ worker.addEventListener('message', handler)
328
+ worker.postMessage({ type: 'children', payload: { parent: parentValue } })
329
+
330
+ // 超时处理
331
+ setTimeout(() => {
332
+ worker!.removeEventListener('message', handler)
333
+ resolve([])
334
+ }, 5000)
335
+ })
336
+ }
337
+
338
+ // 加载根节点(省份)
339
+ function loadRoots(): Promise<AreaNode[]> {
340
+ return new Promise((resolve) => {
341
+ if (!worker || !workerReady.value) {
342
+ resolve([])
343
+ return
344
+ }
345
+
346
+ const handler = (e: MessageEvent) => {
347
+ if (e.data?.type === 'roots') {
348
+ const list = e.data.list || []
349
+ resolve(list)
350
+ worker!.removeEventListener('message', handler)
351
+ }
352
+ }
353
+
354
+ worker.addEventListener('message', handler)
355
+ worker.postMessage({ type: 'roots' })
356
+
357
+ // 超时处理
358
+ setTimeout(() => {
359
+ worker!.removeEventListener('message', handler)
360
+ resolve([])
361
+ }, 5000)
362
+ })
363
+ }
364
+
365
+ // 切换下拉框
366
+ const toggleDropdown = async () => {
367
+ if (!props.disabled) {
368
+ isOpen.value = !isOpen.value
369
+
370
+ if (isOpen.value) {
371
+ // 打开时加载省份列表
372
+ if (provinces.value.length === 0) {
373
+ provinces.value = await loadRoots()
374
+ }
375
+
376
+ // 恢复之前的选择状态
377
+ await restoreSelection()
378
+
379
+ // 打开时聚焦搜索框(仅当启用搜索时)
380
+ if (props.searchable) {
381
+ setTimeout(() => {
382
+ searchInputRef.value?.focus()
383
+ }, 100)
384
+ }
385
+ }
386
+ }
387
+ }
388
+
389
+ // 恢复选择状态(关闭再打开时)
390
+ const restoreSelection = async () => {
391
+ // 如果有选中的省份,加载对应的城市列表
392
+ if (selectedProvince.value && props.maxLevel >= 2) {
393
+ const citiesList = await loadChildren(selectedProvince.value.value)
394
+ cities.value = citiesList
395
+ console.log('[RegionCascader] 恢复省份选择,加载城市:', citiesList.length)
396
+
397
+ // 滚动到选中的省份
398
+ await nextTick()
399
+ scrollToSelectedProvince()
400
+ }
401
+
402
+ // 如果有选中的城市,加载对应的区县列表
403
+ if (selectedCity.value && props.maxLevel >= 3) {
404
+ const districtsList = await loadChildren(selectedCity.value.value)
405
+ districts.value = districtsList
406
+ console.log('[RegionCascader] 恢复城市选择,加载区县:', districtsList.length)
407
+
408
+ // 滚动到选中的城市
409
+ await nextTick()
410
+ scrollToSelectedCity()
411
+ }
412
+
413
+ // 如果有选中的区县,也滚动到选中的区县
414
+ if (selectedDistrict.value && props.maxLevel >= 3) {
415
+ await nextTick()
416
+ scrollToSelectedDistrict()
417
+ }
418
+ }
419
+
420
+ // 通用滚动函数(无感滚动)
421
+ const scrollToSelected = (containerRef: HTMLElement | undefined, elementRef: HTMLElement | undefined, label: string) => {
422
+ if (!elementRef || !containerRef) return
423
+
424
+ // 计算滚动位置,让选中项居中显示
425
+ const containerHeight = containerRef.clientHeight
426
+ const elementTop = elementRef.offsetTop
427
+ const elementHeight = elementRef.clientHeight
428
+
429
+ const scrollTop = elementTop - (containerHeight / 2) + (elementHeight / 2)
430
+
431
+ // 使用 auto 实现瞬间滚动(无感)
432
+ containerRef.scrollTo({
433
+ top: Math.max(0, scrollTop),
434
+ behavior: 'auto'
435
+ })
436
+
437
+ console.log('[RegionCascader] 滚动到:', label)
438
+ }
439
+
440
+ // 滚动到选中的省份
441
+ const scrollToSelectedProvince = () => {
442
+ scrollToSelected(provinceListRef.value, selectedProvinceRef.value, selectedProvince.value?.label || '')
443
+ }
444
+
445
+ // 滚动到选中的城市
446
+ const scrollToSelectedCity = () => {
447
+ scrollToSelected(cityListRef.value, selectedCityRef.value, selectedCity.value?.label || '')
448
+ }
449
+
450
+ // 滚动到选中的区县
451
+ const scrollToSelectedDistrict = () => {
452
+ scrollToSelected(districtListRef.value, selectedDistrictRef.value, selectedDistrict.value?.label || '')
453
+ }
454
+
455
+ const closeDropdown = () => {
456
+ isOpen.value = false
457
+ searchKeyword.value = ''
458
+ }
459
+
460
+ // 搜索处理
461
+ let searchTimeout: ReturnType<typeof setTimeout> | null = null
462
+ const handleSearch = () => {
463
+ if (searchTimeout) {
464
+ clearTimeout(searchTimeout)
465
+ }
466
+
467
+ // 防抖处理
468
+ searchTimeout = setTimeout(() => {
469
+ performSearch()
470
+ }, 300)
471
+ }
472
+
473
+ // 执行搜索(全局搜索)
474
+ const performSearch = () => {
475
+ const keyword = searchKeyword.value.trim()
476
+ if (!keyword) {
477
+ searchResults.value = []
478
+ isSearching.value = false
479
+ return
480
+ }
481
+
482
+ isSearching.value = true
483
+
484
+ // 使用Worker进行全局搜索
485
+ if (worker && workerReady.value) {
486
+ const handler = (e: MessageEvent) => {
487
+ if (e.data?.type === 'searchResults' && e.data.keyword === keyword) {
488
+ searchResults.value = e.data.results || []
489
+ isSearching.value = false
490
+ worker!.removeEventListener('message', handler)
491
+ }
492
+ }
493
+
494
+ worker.addEventListener('message', handler)
495
+ // 全局搜索,不指定parentValue
496
+ worker.postMessage({
497
+ type: 'search',
498
+ payload: { keyword, parentValue: null }
499
+ })
500
+
501
+ // 超时处理
502
+ setTimeout(() => {
503
+ worker!.removeEventListener('message', handler)
504
+ isSearching.value = false
505
+ }, 3000)
506
+ } else {
507
+ isSearching.value = false
508
+ }
509
+ }
510
+
511
+ // 清除搜索
512
+ const clearSearch = () => {
513
+ searchKeyword.value = ''
514
+ searchResults.value = []
515
+ isSearching.value = false
516
+ searchInputRef.value?.focus()
517
+ }
518
+
519
+ // 选择省份
520
+ const selectProvince = async (province: AreaNode) => {
521
+ console.log('[RegionCascader] 选择省份:', province.label, 'maxLevel:', props.maxLevel)
522
+
523
+ selectedProvince.value = province
524
+ selectedCity.value = null
525
+ selectedDistrict.value = null
526
+
527
+ cities.value = []
528
+ districts.value = []
529
+
530
+ // 清除搜索,准备搜索下一级
531
+ searchKeyword.value = ''
532
+
533
+ // 立即滚动到选中的省份(无感滚动)
534
+ nextTick(() => {
535
+ scrollToSelectedProvince()
536
+ })
537
+
538
+ // 如果maxLevel为1,直接完成
539
+ if (props.maxLevel === 1) {
540
+ console.log('[RegionCascader] maxLevel=1,关闭下拉框')
541
+ emitValue()
542
+ closeDropdown()
543
+ return
544
+ }
545
+
546
+ // 加载下一级
547
+ if (!province.leaf && props.maxLevel >= 2) {
548
+ console.log('[RegionCascader] 加载城市列表...')
549
+ const citiesList = await loadChildren(province.value)
550
+ cities.value = citiesList
551
+ console.log('[RegionCascader] 城市列表加载完成:', citiesList.length, '个城市')
552
+ console.log('[RegionCascader] 下拉框状态 isOpen:', isOpen.value)
553
+
554
+ // 聚焦搜索框,准备搜索城市(仅当启用搜索时)
555
+ if (props.searchable) {
556
+ setTimeout(() => {
557
+ searchInputRef.value?.focus()
558
+ }, 100)
559
+ }
560
+ } else {
561
+ console.log('[RegionCascader] 省份是叶子节点或maxLevel<2,关闭下拉框')
562
+ emitValue()
563
+ closeDropdown()
564
+ }
565
+ }
566
+
567
+ // 选择城市
568
+ const selectCity = async (city: AreaNode) => {
569
+ selectedCity.value = city
570
+ selectedDistrict.value = null
571
+
572
+ districts.value = []
573
+
574
+ // 清除搜索,准备搜索下一级
575
+ searchKeyword.value = ''
576
+
577
+ // 立即滚动到选中的城市(无感滚动)
578
+ nextTick(() => {
579
+ scrollToSelectedCity()
580
+ })
581
+
582
+ // 如果maxLevel为2,直接完成
583
+ if (props.maxLevel === 2) {
584
+ emitValue()
585
+ closeDropdown()
586
+ return
587
+ }
588
+
589
+ // 加载下一级
590
+ if (!city.leaf && props.maxLevel >= 3) {
591
+ districts.value = await loadChildren(city.value)
592
+ // 聚焦搜索框,准备搜索区县(仅当启用搜索时)
593
+ if (props.searchable) {
594
+ setTimeout(() => {
595
+ searchInputRef.value?.focus()
596
+ }, 100)
597
+ }
598
+ } else {
599
+ emitValue()
600
+ closeDropdown()
601
+ }
602
+ }
603
+
604
+ // 选择区县
605
+ const selectDistrict = (district: AreaNode) => {
606
+ selectedDistrict.value = district
607
+
608
+ // 立即滚动到选中的区县(无感滚动)
609
+ nextTick(() => {
610
+ scrollToSelectedDistrict()
611
+ })
612
+
613
+ // 区县是最后一级,直接完成
614
+ emitValue()
615
+ closeDropdown()
616
+ }
617
+
618
+ // 从全局搜索结果中选择(智能反填)
619
+ const selectFromGlobalSearch = async (result: AreaNode) => {
620
+ if (!result.path || !result.pathLabels) return
621
+
622
+ // 清除搜索
623
+ searchKeyword.value = ''
624
+ searchResults.value = []
625
+
626
+ // 根据路径长度判断层级并智能反填
627
+ const path = result.path
628
+ const pathLabels = result.pathLabels
629
+ const level = result.level || path.length
630
+
631
+ // 确保provinces列表已加载
632
+ if (provinces.value.length === 0) {
633
+ provinces.value = await loadRoots()
634
+ }
635
+
636
+ // 根据选择的层级,调用对应的选择函数(复用现有逻辑)
637
+ if (level === 1) {
638
+ // 选择的是省份,调用selectProvince
639
+ const province: AreaNode = {
640
+ value: path[0],
641
+ label: pathLabels[0],
642
+ leaf: false
643
+ }
644
+ await selectProvince(province)
645
+ } else if (level === 2) {
646
+ // 选择的是城市,先设置省份,再调用selectCity
647
+ selectedProvince.value = {
648
+ value: path[0],
649
+ label: pathLabels[0],
650
+ leaf: false
651
+ }
652
+ cities.value = await loadChildren(path[0])
653
+
654
+ const city: AreaNode = {
655
+ value: path[1],
656
+ label: pathLabels[1],
657
+ leaf: false
658
+ }
659
+ await selectCity(city)
660
+ } else if (level === 3) {
661
+ // 选择的是区县,先设置省份和城市,再调用selectDistrict
662
+ selectedProvince.value = {
663
+ value: path[0],
664
+ label: pathLabels[0],
665
+ leaf: false
666
+ }
667
+ cities.value = await loadChildren(path[0])
668
+
669
+ selectedCity.value = {
670
+ value: path[1],
671
+ label: pathLabels[1],
672
+ leaf: false
673
+ }
674
+ districts.value = await loadChildren(path[1])
675
+
676
+ const district: AreaNode = {
677
+ value: path[2],
678
+ label: pathLabels[2],
679
+ leaf: true
680
+ }
681
+ selectDistrict(district)
682
+ }
683
+ }
684
+
685
+ // 发送选中的值
686
+ const emitValue = () => {
687
+ const values: string[] = []
688
+ const labels: string[] = []
689
+
690
+ if (selectedProvince.value) {
691
+ values.push(selectedProvince.value.value)
692
+ labels.push(selectedProvince.value.label)
693
+ }
694
+ if (selectedCity.value) {
695
+ values.push(selectedCity.value.value)
696
+ labels.push(selectedCity.value.label)
697
+ }
698
+ if (selectedDistrict.value) {
699
+ values.push(selectedDistrict.value.value)
700
+ labels.push(selectedDistrict.value.label)
701
+ }
702
+
703
+ emit('update:modelValue', values)
704
+ emit('change', values, labels)
705
+ }
706
+
707
+ // 初始化Worker
708
+ const initWorker = () => {
709
+ try {
710
+ // 创建Worker
711
+ worker = new Worker(
712
+ new URL('./areas.worker.ts', import.meta.url),
713
+ { type: 'module' }
714
+ )
715
+
716
+ worker.onmessage = (e: MessageEvent) => {
717
+ if (e.data?.type === 'ready') {
718
+ workerReady.value = true
719
+ console.log('[RegionCascader] Worker初始化完成')
720
+ } else if (e.data?.type === 'error') {
721
+ console.error('[RegionCascader] Worker错误:', e.data.error)
722
+ }
723
+ }
724
+
725
+ worker.onerror = (error) => {
726
+ console.error('[RegionCascader] Worker error:', error)
727
+ }
728
+
729
+ // 发送初始化数据
730
+ worker.postMessage({
731
+ type: 'init',
732
+ payload: { root: chinaAreasData }
733
+ })
734
+ } catch (error) {
735
+ console.error('[RegionCascader] Failed to create worker:', error)
736
+ }
737
+ }
738
+
739
+ // 点击外部关闭下拉框
740
+ onMounted(async () => {
741
+ // 初始化Worker
742
+ initWorker()
743
+
744
+ const handleClickOutside = (event: Event) => {
745
+ if (containerRef.value && !containerRef.value.contains(event.target as Node)) {
746
+ closeDropdown()
747
+ }
748
+ }
749
+
750
+ document.addEventListener('click', handleClickOutside)
751
+
752
+ onUnmounted(() => {
753
+ document.removeEventListener('click', handleClickOutside)
754
+ // 清理Worker
755
+ if (worker) {
756
+ worker.terminate()
757
+ worker = null
758
+ }
759
+ })
760
+ })
761
+
762
+ // 根据 modelValue 恢复选中状态
763
+ async function restoreFromModelValue(codes: string[]) {
764
+ if (!codes || codes.length === 0) {
765
+ // 清空选择
766
+ selectedProvince.value = null
767
+ selectedCity.value = null
768
+ selectedDistrict.value = null
769
+ provinces.value = []
770
+ cities.value = []
771
+ districts.value = []
772
+ return
773
+ }
774
+
775
+ console.log('[RegionCascader] 恢复选中状态:', codes)
776
+
777
+ try {
778
+ // 确保provinces列表已加载
779
+ if (provinces.value.length === 0) {
780
+ provinces.value = await loadRoots()
781
+ }
782
+
783
+ // 恢复省份
784
+ if (codes[0]) {
785
+ const province = provinces.value.find(p => p.value === codes[0])
786
+
787
+ if (province) {
788
+ selectedProvince.value = province
789
+
790
+ // 如果有市级代码,恢复城市
791
+ if (codes[1] && props.maxLevel >= 2) {
792
+ cities.value = await loadChildren(codes[0])
793
+ const city = cities.value.find(c => c.value === codes[1])
794
+
795
+ if (city) {
796
+ selectedCity.value = city
797
+
798
+ // 如果有区级代码,恢复区县
799
+ if (codes[2] && props.maxLevel >= 3) {
800
+ districts.value = await loadChildren(codes[1])
801
+ const district = districts.value.find(d => d.value === codes[2])
802
+
803
+ if (district) {
804
+ selectedDistrict.value = district
805
+ }
806
+ }
807
+ }
808
+ }
809
+ } else {
810
+ console.warn('[RegionCascader] 未找到省份代码:', codes[0])
811
+ }
812
+ }
813
+
814
+ console.log('[RegionCascader] 恢复完成:', {
815
+ province: selectedProvince.value?.label,
816
+ city: selectedCity.value?.label,
817
+ district: selectedDistrict.value?.label
818
+ })
819
+ } catch (error) {
820
+ console.error('[RegionCascader] 恢复选中状态失败:', error)
821
+ }
822
+ }
823
+
824
+ // 监听 modelValue 变化
825
+ watch(() => props.modelValue, async (newValue) => {
826
+ console.log('[RegionCascader] modelValue changed:', newValue)
827
+ await restoreFromModelValue(newValue)
828
+ }, { deep: true, immediate: false })
829
+
830
+ // 在 Worker 就绪后恢复初始值
831
+ watch(workerReady, async (ready) => {
832
+ if (ready && props.modelValue && props.modelValue.length > 0) {
833
+ console.log('[RegionCascader] Worker就绪,开始恢复初始值:', props.modelValue)
834
+ await restoreFromModelValue(props.modelValue)
835
+ }
836
+ }, { immediate: true })
837
+
838
+ // 暴露方法和数据
839
+ defineExpose({
840
+ worker,
841
+ workerReady,
842
+ loadChildren,
843
+ clearCache: () => childrenCache.clear()
844
+ })
845
+ </script>
846
+
847
+ <style scoped>
848
+ .region-cascader-container {
849
+ position: relative;
850
+ }
851
+
852
+ /* 自定义滚动条样式 */
853
+ .region-cascader-container ::-webkit-scrollbar {
854
+ width: 6px;
855
+ }
856
+
857
+ .region-cascader-container ::-webkit-scrollbar-track {
858
+ background: #f1f1f1;
859
+ border-radius: 3px;
860
+ }
861
+
862
+ .region-cascader-container ::-webkit-scrollbar-thumb {
863
+ background: #888;
864
+ border-radius: 3px;
865
+ }
866
+
867
+ .region-cascader-container ::-webkit-scrollbar-thumb:hover {
868
+ background: #555;
869
+ }
870
+ </style>