收货地址、门店归属、服务网点……只要涉及「省 / 市 / 区」三级选择,Vant 4 就有两条现成的路:Cascader 级联选择通用灵活,Area 省市区选择开箱即用。很多人直接把 @vant/area-data 塞给 Cascader,结果发现数据结构完全对不上。本文把两条路线的数据结构、事件参数和回填方式拆开讲,最后给出一份把行政区划数据转换成 Cascader options 的完整代码。
先选路线:通用树 vs 行政区划
| 对比项 | Cascader | Area |
|---|---|---|
| 数据形态 | 嵌套树 options[].children | 三张扁平字典 province_list / city_list / county_list |
| 层级 | 任意层级,叶子可提前结束 | 固定最多三级,用 columns-num 取 2 或 3 |
| 值 | 自定义 value(字符串或数字) | 6 位行政区划代码字符串 |
| 异步加载 | change 事件里补 children | 需自行替换整个 area-list |
| 典型场景 | 商品分类、组织架构、自定义行业树 | 收货地址、户籍所在地 |
一句话结论:标准省市区用 Area,其它树形数据用 Cascader。只有「需要级联交互,但数据来自行政编码字典」时才需要文末那个转换函数。
Cascader:options 结构与字段映射
每个节点是 CascaderOption,核心字段为 text(显示文本)、value(值)、children(子节点);另有 disabled、color、className。v-model 绑定的是当前选中项的 value,不是路径数组。
import type { CascaderOption } from 'vant';
export const options: CascaderOption[] = [
{
text: '浙江省',
value: '330000',
children: [
{ text: '杭州市', value: '330100' },
{ text: '宁波市', value: '330200' },
],
},
{
text: '江苏省',
value: '320000',
children: [{ text: '南京市', value: '320100' }],
},
];
后端返回的字段名往往不是 text/value/children,不必在前端做一遍数据清洗,用 field-names 直接映射:
<van-cascader
v-model="code"
title="请选择地区"
:options="rawOptions"
:field-names="{ text: 'name', value: 'code', children: 'items' }"
@finish="onFinish"
/>
const rawOptions = [
{
name: '浙江省',
code: '330000',
items: [{ name: '杭州市', code: '330100' }],
},
];
实战:Popup + Field 完成选择与回填
finish 事件在选择到最后一级时触发,参数是 { value, selectedOptions, tabIndex }。回填文本靠 selectedOptions 拼路径,提交给后端则应该用 value(最后一个节点的编码)。
<script setup>
import { ref } from 'vue';
import type { CascaderOption } from 'vant';
import { options } from './options';
const show = ref(false);
const fieldValue = ref('');
const cascaderValue = ref('');
const submitCode = ref('');
const onFinish = ({ value, selectedOptions }: {
value: string | number;
selectedOptions: CascaderOption[];
tabIndex: number;
}) => {
show.value = false;
submitCode.value = String(value);
fieldValue.value = selectedOptions.map((o) => o.text).join('/');
};
</script>
<template>
<van-field
v-model="fieldValue"
is-link
readonly
label="所在地区"
placeholder="请选择"
@click="show = true"
/>
<van-popup v-model:show="show" round position="bottom">
<van-cascader
v-model="cascaderValue"
title="请选择所在地区"
:options="options"
@close="show = false"
@finish="onFinish"
/>
</van-popup>
</template>
finish 只有在选到最后一级时才触发。如果某些节点没有 children、某些又有,用户会在不同深度结束,selectedOptions 的长度就不固定,回填和校验都要按路径长度做兼容。异步加载下一级
大数据量时不要一次性拉全树:给待展开节点放一个空 children: [],在 change 事件里按 value 请求子级后原地赋值即可。事件参数是 { value, selectedOptions, tabIndex }。
import { ref } from 'vue';
import { showLoadingToast, closeToast } from 'vant';
const options = ref([{ text: '浙江省', value: '330000', children: [] }]);
const onChange = async ({ value, tabIndex, selectedOptions }) => {
const node = selectedOptions[tabIndex];
if (!node || node.children?.length) return;
showLoadingToast({ message: '加载中...', forbidClick: true });
try {
node.children = await fetchChildren(value);
} finally {
closeToast();
}
};
Area:三段编码字典才是它的输入
Area 不用树,而是三张以 6 位编码为键、名称为值的字典:前 2 位代表省,中间 2 位代表市,后 2 位代表区县;有下级时低位补 0。官方把全国数据单独拆成了 @vant/area-data 包。
npm i @vant/area-data
<script setup>
import { ref } from 'vue';
import { areaList } from '@vant/area-data';
const show = ref(false);
const areaCode = ref('330302'); // 预选中:浙江省 / 温州市 / 鹿城区
const fieldValue = ref('');
const onConfirm = ({ selectedValues, selectedOptions }) => {
// selectedValues: 三位编码数组;selectedOptions: 带 text / value 的选项数组
areaCode.value = selectedValues[selectedValues.length - 1] || '';
fieldValue.value = selectedOptions.map((o) => o.text).join('/');
show.value = false;
};
</script>
<template>
<van-field
v-model="fieldValue"
is-link
readonly
label="收货地区"
placeholder="请选择省 / 市 / 区"
@click="show = true"
/>
<van-popup v-model:show="show" position="bottom" round>
<van-area
v-model="areaCode"
title="选择地区"
:area-list="areaList"
:columns-num="3"
:columns-placeholder="['请选择', '请选择', '请选择']"
@confirm="onConfirm"
@cancel="show = false"
/>
</van-popup>
</template>
v-model 绑定的是当前选中地区的编码字符串;columns-num 控制显示 2 级还是 3 级;confirm 事件一次性给出 selectedValues(编码)、selectedOptions(含 text)、selectedIndexes。实例上还有 confirm() 与 getSelectedOptions(),适合自定义工具栏时调用。把 area-data 转成 Cascader 能吃的树
当产品要求「级联 tab 样式 + 自定义选项渲染」时,就得把三张字典拼回树。依据是编码前缀:市编码前 2 位等于省编码前 2 位,区县编码前 4 位等于市编码前 4 位。下面这份工具函数可直接放进项目。
import type { CascaderOption } from 'vant';
type AreaDict = Record<string, string>;
interface AreaList {
province_list: AreaDict;
city_list: AreaDict;
county_list: AreaDict;
}
export function areaListToCascaderOptions(list: AreaList): CascaderOption[] {
const cities = Object.entries(list.city_list);
const counties = Object.entries(list.county_list);
// 先按前 4 位给区县建索引,避免三重循环
const countyMap = new Map<string, CascaderOption[]>();
for (const [code, name] of counties) {
const key = code.slice(0, 4);
const bucket = countyMap.get(key) ?? [];
bucket.push({ text: name, value: code });
countyMap.set(key, bucket);
}
const cityMap = new Map<string, CascaderOption[]>();
for (const [code, name] of cities) {
const key = code.slice(0, 2);
const bucket = cityMap.get(key) ?? [];
bucket.push({
text: name,
value: code,
children: countyMap.get(code.slice(0, 4)) ?? [],
});
cityMap.set(key, bucket);
}
return Object.entries(list.province_list).map(([code, name]) => ({
text: name,
value: code,
children: cityMap.get(code.slice(0, 2)) ?? [],
}));
}
import { areaList } from '@vant/area-data';
import { areaListToCascaderOptions } from './area-to-cascader';
const options = areaListToCascaderOptions(areaList);
// 直接传给 <van-cascader :options="options" />
children: [],Cascader 会认为还有下一级,用户永远点不到 finish。区县层必须不带 children 字段(或为 undefined),把最后一级真正做成叶子。常见坑速查
| 现象 | 原因 | 处理 |
|---|---|---|
把 areaList 直接传给 Cascader,页面空白 | 字典结构与 options[] 树完全不同 | 要么改用 van-area,要么用上面的转换函数 |
选完不触发 finish | 节点挂了空 children,没有叶子 | 最后一级去掉 children 字段 |
| 回填只显示最后一个名称 | 只用了 value,没用 selectedOptions | 用 selectedOptions.map(o => o.text).join('/') |
| 编辑页无法回显 | 只恢复了显示文本,没恢复 v-model | 同时设置显示文本与 cascaderValue / areaCode |
| 直辖市只有两级 | 行政区划数据本身如此 | 回填逻辑按路径长度兼容,不要写死 3 段 |
总结
Cascader 的核心是「树 + 字段名映射 + finish 回填」,Area 的核心是「编码字典 + confirm 三件套」。两者的数据模型不能互换,但只要抓住 6 位编码的前缀规则,就能在需要时把官方行政区划数据无损地转换成任意层级的级联树。
先看清组件要的是树还是字典,再决定要不要写转换器。
—— 多数「组件不显示」的问题都止于这一步