省市区级联、日期区间、预约时段……移动端表单里最难写的往往不是输入框,而是选择器。Vant Picker 与 DatePicker 在 Vant 4 里分工明确:Picker 负责通用列与级联数据,DatePicker 专注年月日选择,两者都常与 Popup 组合成底部弹层。本文用四个可直接运行的例子,讲清 columns 数据结构、confirm 回调参数、级联字段名映射、日期列配置与范围联动,最后附上从 Vant 3 迁移时的差异清单。

Vant 4 里两个选择器的分工

Vant 3 用一个 van-datetime-picker 包办日期时间;Vant 4 把它拆成了独立的 van-date-picker 与 van-time-picker,而 van-picker 回归通用选择器。这个变化直接决定了 API 形态:

对比项van-pickervan-date-picker
数据来源columns(自定义选项)按 min-date / max-date 自动生成
v-model选中值数组 number[] | string[]日期片段数组 string[],如 ['2026','11','07']
列控制columns 嵌套数组 / childrencolumns-type,如 ['year','month']
confirm 参数{ selectedValues, selectedOptions, selectedIndexes }(Vant 4 统一为对象)
典型搭配van-field 触发 + van-popup position="bottom" 承载

Picker 的数据结构:别再传字符串数组

最常见的错误是把 columns 写成 ['杭州', '宁波']。Vant 4 要求每项是 PickerOption 对象,至少包含 text 与 value;多列独立选择时传「数组的数组」,级联选择时则用 children:

typescript src/data/columns.ts
// 单列
const cityColumns = [
  { text: '杭州', value: '330100' },
  { text: '宁波', value: '330200' },
  { text: '温州', value: '330300', disabled: true },
];

// 多列独立(星期 + 时段)
const weekColumns = [
  [{ text: '周一', value: '1' }, { text: '周二', value: '2' }],
  [{ text: '上午', value: 'am' }, { text: '下午', value: 'pm' }],
];

// 级联(children 逐层嵌套)
const areaColumns = [
  {
    text: '浙江', value: '33',
    children: [
      { text: '杭州', value: '3301',
        children: [{ text: '西湖区', value: '330106' }, { text: '余杭区', value: '330110' }] },
      { text: '温州', value: '3303',
        children: [{ text: '鹿城区', value: '330302' }] },
    ],
  },
];

实战一:底部弹出的城市选择

标准三件套:Field 做只读展示、Popup 做容器、Picker 做选择。注意 Vant 4 的 confirm 回调拿到的是一个对象,从里面解构 selectedValues 和 selectedOptions:

vue src/components/CitySelect.vue
<script setup lang="ts">
import { ref } from 'vue';
import { cityColumns } from '@/data/columns';

const showPicker = ref(false);
const fieldValue = ref('');
const pickerValue = ref<string[]>([]);

const onConfirm = ({ selectedValues, selectedOptions }: {
  selectedValues: (string | number)[];
  selectedOptions: { text?: string | number }[];
}) => {
  showPicker.value = false;
  pickerValue.value = selectedValues.map(String);
  fieldValue.value = String(selectedOptions[0]?.text ?? '');
};
</script>

<template>
  <van-field
    v-model="fieldValue"
    is-link readonly label="城市"
    placeholder="请选择城市"
    @click="showPicker = true"
  />
  <van-popup v-model:show="showPicker" destroy-on-close round position="bottom">
    <van-picker
      :model-value="pickerValue"
      title="选择城市"
      :columns="cityColumns"
      @cancel="showPicker = false"
      @confirm="onConfirm"
    />
  </van-popup>
</template>
💡 小技巧:弹层里用 :model-value 手动回填选中值,配合 destroy-on-close 可以避免「取消后再次打开仍停在上次位置」的交互问题。若希望关闭弹层时完全重置,直接把 pickerValue 清空即可。

实战二:后端数据字段名不叫 text / value

真实项目的省市区接口通常返回 name / code / districts,硬转数据不如用 columns-field-names 声明映射。三个键都要给全,否则级联层会解析失败:

vue src/components/AreaSelect.vue(片段)
<script setup lang="ts">
const areaColumns = [
  {
    cityName: '浙江', code: '33',
    cities: [{ cityName: '杭州', code: '3301', cities: [{ cityName: '西湖区', code: '330106' }] }],
  },
];
const fieldNames = { text: 'cityName', value: 'code', children: 'cities' };

const onConfirm = ({ selectedOptions }) => {
  console.log(selectedOptions.map((o) => o.cityName));
};
</script>

<template>
  <van-picker
    title="选择地区"
    :columns="areaColumns"
    :columns-field-names="fieldNames"
    :loading="loading"
    @confirm="onConfirm"
  />
</template>

异步拉取选项时把 loading 置为 true,数据到位再置回;选项为空可以用 #empty 插槽放一个 van-empty 占位,体验比空白滚轮好得多。

DatePicker:日期列与范围控制

min-date / max-date 接收的是 Date 对象而不是字符串;v-model 是与 columns-type 顺序一致的字符串数组。只选年月就传两个元素,别多给:

vue src/components/MonthSelect.vue
<script setup lang="ts">
import { ref } from 'vue';

const currentDate = ref(['2026', '11']);
const columnsType = ['year', 'month'];
const minDate = new Date(2020, 0, 1);
const maxDate = new Date(2030, 11, 31);

// 给选项加单位
const formatter = (type: string, option: { text: string }) => {
  if (type === 'year') option.text += '年';
  if (type === 'month') option.text += '月';
  return option;
};
</script>

<template>
  <van-date-picker
    v-model="currentDate"
    title="选择年月"
    :columns-type="columnsType"
    :min-date="minDate"
    :max-date="maxDate"
    :formatter="formatter"
    @confirm="({ selectedValues }) => console.log(selectedValues.join('-'))"
  />
</template>

日期范围:用两个弹层做联动

官方并没有「范围选择器」组件,推荐做法是开始 / 结束各一个 DatePicker,把结束日期的 min-date 动态绑定到开始日期。这样既不用自己造滚轮,也能保证结束不早于开始:

vue src/components/DateRange.vue(片段)
<script setup lang="ts">
import { ref, computed } from 'vue';

const start = ref(['2026', '11', '01']);
const end = ref(['2026', '11', '30']);
const active = ref<'start' | 'end' | null>(null);
const show = ref(false);

const current = computed({
  get: () => (active.value === 'start' ? start.value : end.value),
  set: (v) => (active.value === 'start' ? (start.value = v) : (end.value = v)),
});
const minDate = computed(() =>
  active.value === 'end'
    ? new Date(+start.value[0], +start.value[1] - 1, +start.value[2])
    : new Date(2020, 0, 1)
);
const maxDate = new Date(2030, 11, 31);

const open = (which: 'start' | 'end') => { active.value = which; show.value = true; };
</script>

<template>
  <van-field readonly is-link label="开始" :model-value="start.join('-')" @click="open('start')" />
  <van-field readonly is-link label="结束" :model-value="end.join('-')" @click="open('end')" />
  <van-popup v-model:show="show" round position="bottom">
    <van-date-picker
      v-model="current"
      title="选择日期"
      :min-date="minDate"
      :max-date="maxDate"
      @confirm="show = false"
      @cancel="show = false"
    />
  </van-popup>
</template>

常见坑速查

⚠️
六个高频问题
① columns 传了字符串数组,滚轮空白;② 沿用 Vant 3 的 confirm(value, index) 写法,Vant 4 已改成对象参数;③ v-model 绑成字符串而非数组;④ min-date 传字符串;⑤ 动态替换 columns 后忘记重置选中值,导致 selectedValues 与选项对不上;⑥ columns-field-names 只写了 text / value,漏了 children 导致级联只剩一层。

另外,change 事件会在滑动过程中频繁触发,别在里面发请求;需要按选中项拉下一级数据时,先做防抖或在 confirm 里处理。

✅
自查清单
上线前确认:取消时不污染已选值、异步数据到位后 loading 关闭、级联三级都能回显、结束日期不早于开始日期、键盘弹出时不遮挡确认按钮。

总结

选择器类组件的难点从来不在样式,而在数据与状态的同步:把 columns 结构、v-model 数组、confirm 对象参数这三者对齐,Popup 只是壳子,DatePicker 只是预置了列生成规则。理解了这一点,任意层级的选择需求都能用同一套模式拆出来。

把选中值当成数组来管理,级联和范围就都不再是特例。