收货地址、门店归属、服务网点……只要涉及「省 / 市 / 区」三级选择,Vant 4 就有两条现成的路:Cascader 级联选择通用灵活,Area 省市区选择开箱即用。很多人直接把 @vant/area-data 塞给 Cascader,结果发现数据结构完全对不上。本文把两条路线的数据结构、事件参数和回填方式拆开讲,最后给出一份把行政区划数据转换成 Cascader options 的完整代码。

先选路线:通用树 vs 行政区划

对比项CascaderArea
数据形态嵌套树 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,不是路径数组。

ts options.ts
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 直接映射:

html 自定义字段名
<van-cascader
  v-model="code"
  title="请选择地区"
  :options="rawOptions"
  :field-names="{ text: 'name', value: 'code', children: 'items' }"
  @finish="onFinish"
/>
js 后端原始数据
const rawOptions = [
  {
    name: '浙江省',
    code: '330000',
    items: [{ name: '杭州市', code: '330100' }],
  },
];

实战:Popup + Field 完成选择与回填

finish 事件在选择到最后一级时触发,参数是 { value, selectedOptions, tabIndex }。回填文本靠 selectedOptions 拼路径,提交给后端则应该用 value(最后一个节点的编码)。

vue CascaderField.vue
<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 }。

js 按需加载子级
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 包。

bash 安装行政区划数据
npm i @vant/area-data
vue AreaField.vue
<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>
ℹ️
Area 的关键参数
v-model 绑定的是当前选中地区的编码字符串;columns-num 控制显示 2 级还是 3 级;confirm 事件一次性给出 selectedValues(编码)、selectedOptions(含 text)、selectedIndexes。实例上还有 confirm() 与 getSelectedOptions(),适合自定义工具栏时调用。

把 area-data 转成 Cascader 能吃的树

当产品要求「级联 tab 样式 + 自定义选项渲染」时,就得把三张字典拼回树。依据是编码前缀:市编码前 2 位等于省编码前 2 位,区县编码前 4 位等于市编码前 4 位。下面这份工具函数可直接放进项目。

ts area-to-cascader.ts
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)) ?? [],
  }));
}
js 使用方式
import { areaList } from '@vant/area-data';
import { areaListToCascaderOptions } from './area-to-cascader';

const options = areaListToCascaderOptions(areaList);
// 直接传给 <van-cascader :options="options" />
⚠️
叶子节点不要留空 children
转换时如果给所有区县都补一个空 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 位编码的前缀规则,就能在需要时把官方行政区划数据无损地转换成任意层级的级联树。

先看清组件要的是树还是字典,再决定要不要写转换器。
—— 多数「组件不显示」的问题都止于这一步