gaoluyang
2026-06-29 27cd042df9aca0383a49f3514bc21958dd890912
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
<script lang="ts" setup>
import type { CascaderProps } from 'ant-design-vue';
 
import type { SystemAreaApi } from '#/api/system/area';
 
import { onMounted, ref, watch } from 'vue';
 
import { Cascader } from 'ant-design-vue';
 
import { getAreaTree } from '#/api/system/area';
 
defineOptions({ name: 'AreaCascader' });
 
const props = withDefaults(defineProps<Props>(), {
  allowClear: false,
  changeOnSelect: false,
  modelValue: undefined,
  placeholder: '请选择省市区',
  showSearch: false,
});
 
const emit = defineEmits<{
  'update:modelValue': [value?: number];
}>();
 
interface Props {
  allowClear?: boolean;
  changeOnSelect?: boolean;
  modelValue?: number;
  placeholder?: string;
  showSearch?: boolean;
}
 
type AreaTreeNode = SystemAreaApi.Area & {
  children?: AreaTreeNode[];
};
 
const areaTree = ref<AreaTreeNode[]>([]); // 地区树
const loading = ref(false); // 加载状态
const selectedPath = ref<number[]>(); // 选中的地区路径
 
const fieldNames = {
  children: 'children', // 子级字段
  label: 'name', // 标签字段
  value: 'id', // 值字段
};
 
/**
 * 查找地区编号对应的级联路径
 *
 * @param tree 地区树
 * @param areaId 地区编号
 * @returns 省市区编号路径
 */
function findAreaPath(
  tree: AreaTreeNode[],
  areaId?: number,
): number[] | undefined {
  if (areaId === undefined || areaId === null) {
    return undefined;
  }
 
  for (const area of tree) {
    if (area.id === undefined) {
      continue;
    }
    if (area.id === areaId) {
      return [area.id];
    }
 
    const childPath = area.children?.length
      ? findAreaPath(area.children, areaId)
      : undefined;
    if (childPath) {
      return [area.id, ...childPath];
    }
  }
}
 
/** 同步级联选择器展示路径 */
function syncSelectedPath() {
  selectedPath.value = findAreaPath(areaTree.value, props.modelValue);
}
 
/** 选择地区后回写最后一级地区编号 */
const handleChange: NonNullable<CascaderProps['onChange']> = (value) => {
  if (!value?.length) {
    emit('update:modelValue', undefined);
    return;
  }
 
  const path = Array.isArray(value[0]) ? value[0] : value;
  const leafValue = path.at(-1);
  const areaId =
    typeof leafValue === 'number' ? leafValue : Number(leafValue);
  emit('update:modelValue', Number.isNaN(areaId) ? undefined : areaId);
};
 
watch(() => props.modelValue, syncSelectedPath);
 
/** 初始化 */
onMounted(async () => {
  loading.value = true;
  try {
    areaTree.value = (await getAreaTree()) as AreaTreeNode[];
    syncSelectedPath();
  } finally {
    loading.value = false;
  }
});
</script>
 
<template>
  <Cascader
    v-model:value="selectedPath"
    :allow-clear="allowClear"
    :change-on-select="changeOnSelect"
    :field-names="fieldNames"
    :loading="loading"
    :options="areaTree"
    :placeholder="placeholder"
    :show-search="showSearch"
    @change="handleChange"
  />
</template>