diff --git a/packages/vant/docs/markdown/migrate-from-v3.zh-CN.md b/packages/vant/docs/markdown/migrate-from-v3.zh-CN.md index c4ad4af85..f36489e60 100644 --- a/packages/vant/docs/markdown/migrate-from-v3.zh-CN.md +++ b/packages/vant/docs/markdown/migrate-from-v3.zh-CN.md @@ -4,22 +4,47 @@ 本文档提供了从 Vant 3 到 Vant 4 的升级指南。 -### 为什么会有 Vant 4.0 ? +## API 调整 -为了支持 **暗色模式**,我们对 Vant 中的 **样式变量** 进行了一些不兼容更新,因此发布了新的大版本。 +### Picker 组件重构 -如果你的项目没有使用主题定制,那样式变量的调整对你没有任何影响,只需要花几分钟去适配 API 调整,即可完成升级。 +在之前的版本中,Picker 组件的 API 设计存在较大问题,比如: -如果你的项目使用了主题定制,请完整阅读此文档,并进行迁移。 +- columns 数据格式定义不合理,容易产生误解 +- 数据流不清晰,暴露了过多的实例方法来对数据进行操作 -### API 调整 +为了解决上述问题,我们在 v4 版本中对 Picker 组件进行了重构。 -4.0 版本对少量 API 进行了不兼容调整: +#### 主要变更 -#### Picker +- 支持通过 `v-model` 绑定当前选中的值,移除 `default-index` 属性 +- 重新定义了 `columns` 属性的结构 +- 移除了操作内部数据的实例方法,仅保留 `confirm` 方法 +- 调整了 `confirm`、`cancel`、`change` 事件的参数 +- 重命名 `item-height` 属性为 `option-height` +- 重命名 `visible-item-count` 属性为 `visible-option-num` -- `default` 插槽重命名为 `toolbar` -- 移除了 `value-key` 属性,使用 `columnsFieldNames` 属性代替 +详细用法请参见 [Picker 组件文档](#/zh-CN/picker)。 + +### Area 组件重构 + +Area 组件是基于 Picker 组件进行封装的,因此本次升级也对 Area 组件进行了内部逻辑的重构,并优化了部分 API 设计。 + +#### 主要变更 + +- 支持通过 `v-model` 绑定当前选中的值 +- 移除 `reset` 方法,现在可以通过修改 `v-model` 来进行重置 +- 移除 `is-oversea-code` 属性 +- 调整所有事件的参数,与 Picker 组件保持一致 +- 重命名 `value` 属性我 `modelValue` +- 重命名 `item-height` 属性为 `option-height` +- 重命名 `visible-item-count` 属性为 `visible-option-num` + +详细用法请参见 [Area 组件文档](#/zh-CN/area)。 + +### 其他 API 调整 + +4.0 版本中,以下 API 进行了不兼容更新: #### Tabs diff --git a/packages/vant/src/area/Area.tsx b/packages/vant/src/area/Area.tsx index 629da5c72..2f7b2af62 100644 --- a/packages/vant/src/area/Area.tsx +++ b/packages/vant/src/area/Area.tsx @@ -2,16 +2,12 @@ import { ref, watch, computed, - reactive, - nextTick, - onMounted, defineComponent, type PropType, type ExtractPropTypes, } from 'vue'; // Utils -import { deepClone } from '../utils/deep-clone'; import { pick, extend, @@ -20,52 +16,24 @@ import { createNamespace, } from '../utils'; import { pickerSharedProps } from '../picker/Picker'; - -// Composables -import { useExpose } from '../composables/use-expose'; +import { INHERIT_PROPS, INHERIT_SLOTS, formatDataForCascade } from './utils'; // Components -import { Picker, PickerInstance } from '../picker'; +import { Picker } from '../picker'; // Types -import type { AreaList, AreaColumnType, AreaColumnOption } from './types'; +import type { AreaList } from './types'; const [name, bem] = createNamespace('area'); -const EMPTY_CODE = '000000'; -const INHERIT_SLOTS = [ - 'title', - 'cancel', - 'confirm', - 'toolbar', - 'columns-top', - 'columns-bottom', -] as const; -const INHERIT_PROPS = [ - 'title', - 'loading', - 'readonly', - 'itemHeight', - 'swipeDuration', - 'visibleItemCount', - 'cancelButtonText', - 'confirmButtonText', -] as const; - -const isOverseaCode = (code: string) => code[0] === '9'; - const areaProps = extend({}, pickerSharedProps, { - value: String, + modelValue: String, columnsNum: makeNumericProp(3), columnsPlaceholder: makeArrayProp(), areaList: { type: Object as PropType, default: () => ({}), }, - isOverseaCode: { - type: Function as PropType<(code: string) => boolean>, - default: isOverseaCode, - }, }); export type AreaProps = ExtractPropTypes; @@ -75,262 +43,54 @@ export default defineComponent({ props: areaProps, - emits: ['change', 'confirm', 'cancel'], + emits: ['change', 'confirm', 'cancel', 'update:modelValue'], setup(props, { emit, slots }) { - const pickerRef = ref(); - - const state = reactive({ - code: props.value, - columns: [{ values: [] }, { values: [] }, { values: [] }], - }); - - const areaList = computed(() => { - const { areaList } = props; - return { - province: areaList.province_list || {}, - city: areaList.city_list || {}, - county: areaList.county_list || {}, - }; - }); - - const placeholderMap = computed(() => { - const { columnsPlaceholder } = props; - return { - province: columnsPlaceholder[0] || '', - city: columnsPlaceholder[1] || '', - county: columnsPlaceholder[2] || '', - }; - }); - - const getDefaultCode = () => { - if (props.columnsPlaceholder.length) { - return EMPTY_CODE; - } - - const { county, city } = areaList.value; - - const countyCodes = Object.keys(county); - if (countyCodes[0]) { - return countyCodes[0]; - } - - const cityCodes = Object.keys(city); - if (cityCodes[0]) { - return cityCodes[0]; - } - - return ''; - }; - - const getColumnValues = (type: AreaColumnType, code?: string) => { - let column: AreaColumnOption[] = []; - if (type !== 'province' && !code) { - return column; - } - - const list = areaList.value[type]; - column = Object.keys(list).map((listCode) => ({ - code: listCode, - name: list[listCode], - })); - - if (code) { - // oversea code - if (type === 'city' && props.isOverseaCode(code)) { - code = '9'; - } - column = column.filter((item) => item.code.indexOf(code!) === 0); - } - - if (placeholderMap.value[type] && column.length) { - // set columns placeholder - let codeFill = ''; - if (type === 'city') { - codeFill = EMPTY_CODE.slice(2, 4); - } else if (type === 'county') { - codeFill = EMPTY_CODE.slice(4, 6); - } - - column.unshift({ - code: code + codeFill, - name: placeholderMap.value[type], - }); - } - - return column; - }; - - // get index by code - const getIndex = (type: AreaColumnType, code: string) => { - let compareNum = code.length; - if (type === 'province') { - compareNum = props.isOverseaCode(code) ? 1 : 2; - } - if (type === 'city') { - compareNum = 4; - } - - code = code.slice(0, compareNum); - - const list = getColumnValues( - type, - compareNum > 2 ? code.slice(0, compareNum - 2) : '' - ); - - for (let i = 0; i < list.length; i++) { - if (list[i].code.slice(0, compareNum) === code) { - return i; - } - } - - return 0; - }; - - const setValues = () => { - const picker = pickerRef.value; - - if (!picker) { - return; - } - - let code = state.code || getDefaultCode(); - const province = getColumnValues('province'); - const city = getColumnValues('city', code.slice(0, 2)); - picker.setColumnValues(0, province); - picker.setColumnValues(1, city); - - if ( - city.length && - code.slice(2, 4) === '00' && - !props.isOverseaCode(code) - ) { - [{ code }] = city; - } - - picker.setColumnValues(2, getColumnValues('county', code.slice(0, 4))); - picker.setIndexes([ - getIndex('province', code), - getIndex('city', code), - getIndex('county', code), - ]); - }; - - // parse output columns data - const parseValues = (values: AreaColumnOption[]) => - values.map((value, index) => { - if (value) { - value = deepClone(value); - - if (!value.code || value.name === props.columnsPlaceholder[index]) { - value.code = ''; - value.name = ''; - } - } - - return value; - }); - - const getValues = () => { - if (pickerRef.value) { - const values = pickerRef.value - .getValues() - .filter(Boolean); - return parseValues(values); - } - return []; - }; - - const getArea = () => { - const values = getValues(); - const area = { - code: '', - country: '', - province: '', - city: '', - county: '', - }; - - if (!values.length) { - return area; - } - - const names = values.map((item) => item.name); - const validValues = values.filter((value) => value.code); - - area.code = validValues.length - ? validValues[validValues.length - 1].code - : ''; - - if (props.isOverseaCode(area.code)) { - area.country = names[1] || ''; - area.province = names[2] || ''; - } else { - area.province = names[0] || ''; - area.city = names[1] || ''; - area.county = names[2] || ''; - } - - return area; - }; - - const reset = (newCode = '') => { - state.code = newCode; - setValues(); - }; - - const onChange = (values: AreaColumnOption[], index: number) => { - state.code = values[index].code; - setValues(); - - if (pickerRef.value) { - const parsedValues = parseValues(pickerRef.value.getValues()); - emit('change', parsedValues, index); - } - }; - - const onConfirm = (values: AreaColumnOption[], index: number) => { - setValues(); - emit('confirm', parseValues(values), index); - }; - + const codes = ref([]); + const columns = computed(() => formatDataForCascade(props)); + const onChange = (...args: unknown[]) => emit('change', ...args); const onCancel = (...args: unknown[]) => emit('cancel', ...args); - - onMounted(setValues); + const onConfirm = (...args: unknown[]) => emit('confirm', ...args); watch( - () => props.value, - (value) => { - state.code = value; - setValues(); - } + codes, + (newCodes) => { + const lastCode = newCodes.length ? newCodes[newCodes.length - 1] : ''; + if (lastCode && lastCode !== props.modelValue) { + emit('update:modelValue', lastCode); + } + }, + { deep: true } ); - watch(() => props.areaList, setValues, { deep: true }); - watch( - () => props.columnsNum, - () => nextTick(setValues) + () => props.modelValue, + (newCode) => { + const lastCode = codes.value.length + ? codes.value[codes.value.length - 1] + : ''; + if (newCode && newCode !== lastCode) { + codes.value = [ + `${newCode.slice(0, 2)}0000`, + `${newCode.slice(0, 4)}00`, + newCode, + ].slice(0, +props.columnsNum); + } + }, + { immediate: true } ); - useExpose({ reset, getArea, getValues }); - - return () => { - const columns = state.columns.slice(0, +props.columnsNum); - - return ( - - ); - }; + return () => ( + + ); }, }); diff --git a/packages/vant/src/area/README.md b/packages/vant/src/area/README.md index 014a1b11a..9917787d9 100644 --- a/packages/vant/src/area/README.md +++ b/packages/vant/src/area/README.md @@ -75,12 +75,23 @@ export default { }; ``` -### Initial Value +### Model Value -To have a selected value,simply pass the `code` of target area to `value` property. +Bind the currently selected area code via `v-model`. ```html - + +``` + +```js +import { ref } from 'vue'; + +export default { + setup() { + const value = ref('330302'); + return { value }; + }, +}; ``` ### Columns Number @@ -109,7 +120,7 @@ To have a selected value,simply pass the `code` of target area to `value` prop | Attribute | Description | Type | Default | | --- | --- | --- | --- | -| value | the `code` of selected area | _string_ | - | +| v-model | the `code` of selected area | _string_ | - | | title | Toolbar title | _string_ | - | | confirm-button-text | Text of confirm button | _string_ | `Confirm` | | cancel-button-text | Text of cancel button | _string_ | `Cancel` | @@ -117,40 +128,18 @@ To have a selected value,simply pass the `code` of target area to `value` prop | columns-placeholder | Placeholder of columns | _string[]_ | `[]` | | loading | Whether to show loading prompt | _boolean_ | `false` | | readonly | Whether to be readonly | _boolean_ | `false` | -| item-height | Option height, supports `px` `vw` `vh` `rem` unit, default `px` | _number \| string_ | `44` | +| option-height | Option height, supports `px` `vw` `vh` `rem` unit, default `px` | _number \| string_ | `44` | | columns-num | Level of picker | _number \| string_ | `3` | -| visible-item-count | Count of visible columns | _number \| string_ | `6` | +| visible-option-num | Count of visible columns | _number \| string_ | `6` | | swipe-duration | Duration of the momentum animation,unit `ms` | _number \| string_ | `1000` | -| is-oversea-code | The method to validate oversea code | _() => boolean_ | - | ### Events | Event | Description | Arguments | | --- | --- | --- | -| confirm | Emitted when the confirm button is clicked | _result: ConfirmResult_ | -| cancel | Emitted when the cancel button is clicked | - | -| change | Emitted when current option changed | current values,column index | - -### ConfirmResult - -An array that contains selected area objects. - -```js -[ - { - code: '330000', - name: 'Zhejiang Province', - }, - { - code: '330100', - name: 'Hangzhou', - }, - { - code: '330105', - name: 'Xihu District', - }, -]; -``` +| confirm | Emitted when the confirm button is clicked | _{ selectedValues, selectedOptions }_ | +| cancel | Emitted when the cancel button is clicked | _{ selectedValues, selectedOptions }_ | +| change | Emitted when current option is changed | _{ selectedValues, selectedOptions, columnIndex }_ | ### Slots @@ -163,14 +152,6 @@ An array that contains selected area objects. | columns-top | Custom content above columns | - | | columns-bottom | Custom content below columns | - | -### Methods - -Use [ref](https://v3.vuejs.org/guide/component-template-refs.html) to get Area instance and call instance methods. - -| Name | Description | Attribute | Return value | -| ----- | ------------------------- | --------------- | ------------ | -| reset | Reset all options by code | _code?: string_ | - | - ### Types The component exports the following type definitions: diff --git a/packages/vant/src/area/README.zh-CN.md b/packages/vant/src/area/README.zh-CN.md index cc9bc0edc..6357014ec 100644 --- a/packages/vant/src/area/README.zh-CN.md +++ b/packages/vant/src/area/README.zh-CN.md @@ -77,12 +77,23 @@ export default { }; ``` -### 选中省市区 +### 控制选中项 -如果想选中某个省市区,需要传入一个 `value` 属性,绑定对应的地区码。 +通过 `v-model` 绑定当前选中的地区码。 ```html - + +``` + +```js +import { ref } from 'vue'; + +export default { + setup() { + const value = ref('330302'); + return { value }; + }, +}; ``` ### 配置显示列 @@ -111,7 +122,7 @@ export default { | 参数 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | -| value | 当前选中项对应的地区码 | _string_ | - | +| v-model | 当前选中项对应的地区码 | _string_ | - | | title | 顶部栏标题 | _string_ | - | | confirm-button-text | 确认按钮文字 | _string_ | `确认` | | cancel-button-text | 取消按钮文字 | _string_ | `取消` | @@ -119,40 +130,18 @@ export default { | columns-placeholder | 列占位提示文字 | _string[]_ | `[]` | | loading | 是否显示加载状态 | _boolean_ | `false` | | readonly | 是否为只读状态,只读状态下无法切换选项 | _boolean_ | `false` | -| item-height | 选项高度,支持 `px` `vw` `vh` `rem` 单位,默认 `px` | _number \| string_ | `44` | +| option-height | 选项高度,支持 `px` `vw` `vh` `rem` 单位,默认 `px` | _number \| string_ | `44` | | columns-num | 显示列数,3-省市区,2-省市,1-省 | _number \| string_ | `3` | -| visible-item-count | 可见的选项个数 | _number \| string_ | `6` | +| visible-option-num | 可见的选项个数 | _number \| string_ | `6` | | swipe-duration | 快速滑动时惯性滚动的时长,单位 `ms` | _number \| string_ | `1000` | -| is-oversea-code | 根据地区码校验海外地址,海外地址会划分至单独的分类 | _() => boolean_ | - | ### Events -| 事件 | 说明 | 回调参数 | -| ------- | ------------------ | ------------------------------ | -| confirm | 点击完成按钮时触发 | _result: ConfirmResult_ | -| cancel | 点击取消按钮时触发 | - | -| change | 选项改变时触发 | 所有列选中值,当前列对应的索引 | - -### ConfirmResult 格式 - -confirm 事件返回的数据整体为一个数组,数组每一项对应一列选项中被选中的数据。 - -```js -[ - { - code: '110000', - name: '北京市', - }, - { - code: '110100', - name: '北京市', - }, - { - code: '110101', - name: '东城区', - }, -]; -``` +| 事件 | 说明 | 回调参数 | +| --- | --- | --- | +| confirm | 点击完成按钮时触发 | _{ selectedValues, selectedOptions }_ | +| cancel | 点击取消按钮时触发 | _{ selectedValues, selectedOptions }_ | +| change | 选项改变时触发 | _{ selectedValues, selectedOptions, columnIndex }_ | ### Slots @@ -165,20 +154,12 @@ confirm 事件返回的数据整体为一个数组,数组每一项对应一列 | columns-top | 自定义选项上方内容 | - | | columns-bottom | 自定义选项下方内容 | - | -### 方法 - -通过 ref 可以获取到 Area 实例并调用实例方法,详见[组件实例方法](#/zh-CN/advanced-usage#zu-jian-shi-li-fang-fa)。 - -| 方法名 | 说明 | 参数 | 返回值 | -| --- | --- | --- | --- | -| reset | 根据地区码重置所有选项,若不传地区码,则重置到第一项 | _code?: string_ | - | - ### 类型定义 组件导出以下类型定义: ```ts -import type { AreaProps, AreaList, AreaInstance, AreaColumnOption } from 'vant'; +import type { AreaProps, AreaList, AreaInstance } from 'vant'; ``` `AreaInstance` 是组件实例的类型,用法如下: diff --git a/packages/vant/src/area/demo/area.ts b/packages/vant/src/area/demo/area.ts deleted file mode 100644 index 3f2f8b507..000000000 --- a/packages/vant/src/area/demo/area.ts +++ /dev/null @@ -1 +0,0 @@ -// 已迁移至 https://github.com/youzan/vant/tree/dev/packages/vant-area-data diff --git a/packages/vant/src/area/demo/index.vue b/packages/vant/src/area/demo/index.vue index e63f7c2e6..6c9c7b6ac 100644 --- a/packages/vant/src/area/demo/index.vue +++ b/packages/vant/src/area/demo/index.vue @@ -7,14 +7,14 @@ import { useTranslate } from '../../../docs/site/use-translate'; const t = useTranslate({ 'zh-CN': { - title2: '选中省市区', + title2: '控制选中项', title3: '配置显示列', title4: '配置列占位提示文字', columnsPlaceholder: ['请选择', '请选择', '请选择'], areaList, }, 'en-US': { - title2: 'Initial Value', + title2: 'Model Value', title3: 'Columns Number', title4: 'Columns Placeholder', columnsPlaceholder: ['Choose', 'Choose', 'Choose'], @@ -31,7 +31,7 @@ const value = ref('330302'); - + diff --git a/packages/vant/src/area/index.ts b/packages/vant/src/area/index.ts index 43bba0dd7..b019703c0 100644 --- a/packages/vant/src/area/index.ts +++ b/packages/vant/src/area/index.ts @@ -4,7 +4,7 @@ import _Area from './Area'; export const Area = withInstall(_Area); export default Area; export type { AreaProps } from './Area'; -export type { AreaList, AreaInstance, AreaColumnOption } from './types'; +export type { AreaList, AreaInstance } from './types'; declare module 'vue' { export interface GlobalComponents { diff --git a/packages/vant/src/area/types.ts b/packages/vant/src/area/types.ts index d182ba9c9..c9c8f6478 100644 --- a/packages/vant/src/area/types.ts +++ b/packages/vant/src/area/types.ts @@ -8,23 +8,4 @@ export type AreaList = { province_list: Record; }; -export type AreaColumnOption = { - name: string; - code: string; -}; - -export type AreaColumnType = 'province' | 'county' | 'city'; - -export type AreaExpose = { - reset: (newCode?: string) => void; - getArea: () => { - code: string; - country: string; - province: string; - city: string; - county: string; - }; - getValues: () => AreaColumnOption[]; -}; - -export type AreaInstance = ComponentPublicInstance; +export type AreaInstance = ComponentPublicInstance; diff --git a/packages/vant/src/area/utils.ts b/packages/vant/src/area/utils.ts new file mode 100644 index 000000000..0af32ccea --- /dev/null +++ b/packages/vant/src/area/utils.ts @@ -0,0 +1,103 @@ +import type { AreaProps } from '.'; +import type { PickerOption } from '../picker'; + +const EMPTY_CODE = '000000'; + +export const INHERIT_SLOTS = [ + 'title', + 'cancel', + 'confirm', + 'toolbar', + 'columns-top', + 'columns-bottom', +] as const; +export const INHERIT_PROPS = [ + 'title', + 'loading', + 'readonly', + 'optionHeight', + 'swipeDuration', + 'visibleOptionNum', + 'cancelButtonText', + 'confirmButtonText', +] as const; + +const makeOption = ( + text = '', + value = EMPTY_CODE, + children?: PickerOption[] +): PickerOption => ({ + text, + value, + children, +}); + +export function formatDataForCascade({ + areaList, + columnsNum, + columnsPlaceholder: placeholder, +}: AreaProps) { + const { + city_list: city, + county_list: county, + province_list: province, + } = areaList; + const showCity = columnsNum > 1; + const showCounty = columnsNum > 2; + + const getProvinceChildren = () => { + if (showCity) { + return placeholder.length + ? [makeOption(placeholder[0], EMPTY_CODE, showCounty ? [] : undefined)] + : []; + } + }; + + const provinceMap = new Map(); + Object.keys(province).forEach((code) => { + provinceMap.set( + code.slice(0, 2), + makeOption(province[code], code, getProvinceChildren()) + ); + }); + + const cityMap = new Map(); + if (showCity) { + const getCityChildren = () => { + if (showCounty) { + return placeholder.length ? [makeOption(placeholder[1])] : []; + } + }; + + Object.keys(city).forEach((code) => { + const option = makeOption(city[code], code, getCityChildren()); + cityMap.set(code.slice(0, 4), option); + + const province = provinceMap.get(code.slice(0, 2)); + if (province) { + province.children!.push(option); + } + }); + } + + if (showCounty) { + Object.keys(county).forEach((code) => { + const city = cityMap.get(code.slice(0, 4)); + if (city) { + city.children!.push(makeOption(county[code], code)); + } + }); + } + + const options = Array.from(provinceMap.values()) as PickerOption[]; + + if (placeholder.length) { + const county = showCounty ? [makeOption(placeholder[2])] : undefined; + const city = showCity + ? [makeOption(placeholder[1], EMPTY_CODE, county)] + : undefined; + options.unshift(makeOption(placeholder[0], EMPTY_CODE, city)); + } + + return options; +} diff --git a/packages/vant/src/datetime-picker/README.md b/packages/vant/src/datetime-picker/README.md index cf049b6d9..d95713ab6 100644 --- a/packages/vant/src/datetime-picker/README.md +++ b/packages/vant/src/datetime-picker/README.md @@ -291,8 +291,8 @@ export default { | filter | Option filter | _(type: string, values: string[]) => string[]_ | - | | formatter | Option text formatter | _(type: string, value: string) => string_ | - | | columns-order | Array for ordering columns, where item can be set to
`year`, `month`, `day`, `hour` and `minute` | _string[]_ | - | -| item-height | Option height, supports `px` `vw` `vh` `rem` unit, default `px` | _number \| string_ | `44` | -| visible-item-count | Count of visible columns | _number \| string_ | `6` | +| option-height | Option height, supports `px` `vw` `vh` `rem` unit, default `px` | _number \| string_ | `44` | +| visible-option-num | Count of visible columns | _number \| string_ | `6` | | swipe-duration | Duration of the momentum animation,unit `ms` | _number \| string_ | `1000` | ### DatePicker Props diff --git a/packages/vant/src/datetime-picker/README.zh-CN.md b/packages/vant/src/datetime-picker/README.zh-CN.md index fee998c13..ad99fae8f 100644 --- a/packages/vant/src/datetime-picker/README.zh-CN.md +++ b/packages/vant/src/datetime-picker/README.zh-CN.md @@ -300,8 +300,8 @@ export default { | filter | 选项过滤函数 | _(type: string, values: string[]) => string[]_ | - | | formatter | 选项格式化函数 | _(type: string, value: string) => string_ | - | | columns-order | 自定义列排序数组, 子项可选值为
`year`、`month`、`day`、`hour`、`minute` | _string[]_ | - | -| item-height | 选项高度,支持 `px` `vw` `vh` `rem` 单位,默认 `px` | _number \| string_ | `44` | -| visible-item-count | 可见的选项个数 | _number \| string_ | `6` | +| option-height | 选项高度,支持 `px` `vw` `vh` `rem` 单位,默认 `px` | _number \| string_ | `44` | +| visible-option-num | 可见的选项个数 | _number \| string_ | `6` | | swipe-duration | 快速滑动时惯性滚动的时长,单位`ms` | _number \| string_ | `1000` | ### DatePicker Props diff --git a/packages/vant/src/picker/Picker.tsx b/packages/vant/src/picker/Picker.tsx index 922db050e..d9358f0dc 100644 --- a/packages/vant/src/picker/Picker.tsx +++ b/packages/vant/src/picker/Picker.tsx @@ -20,6 +20,14 @@ import { HAPTICS_FEEDBACK, BORDER_UNSET_TOP_BOTTOM, } from '../utils'; +import { + isValuesEqual, + getColumnsType, + findOptionByValue, + formatCascadeColumns, + getFirstEnabledOption, + assignDefaultFields, +} from './utils'; // Composables import { useChildren } from '@vant/use'; @@ -32,10 +40,9 @@ import Column, { PICKER_KEY } from './PickerColumn'; // Types import type { PickerColumn, - PickerOption, PickerExpose, + PickerOption, PickerFieldNames, - PickerObjectColumn, PickerToolbarPosition, } from './types'; @@ -46,17 +53,17 @@ export const pickerSharedProps = { loading: Boolean, readonly: Boolean, allowHtml: Boolean, - itemHeight: makeNumericProp(44), + optionHeight: makeNumericProp(44), showToolbar: truthProp, swipeDuration: makeNumericProp(1000), - visibleItemCount: makeNumericProp(6), + visibleOptionNum: makeNumericProp(6), cancelButtonText: String, confirmButtonText: String, }; const pickerProps = extend({}, pickerSharedProps, { columns: makeArrayProp(), - defaultIndex: makeNumericProp(0), + modelValue: makeArrayProp(), toolbarPosition: makeStringProp('top'), columnsFieldNames: Object as PropType, }); @@ -68,213 +75,77 @@ export default defineComponent({ props: pickerProps, - emits: ['confirm', 'cancel', 'change'], + emits: ['confirm', 'cancel', 'change', 'update:modelValue'], setup(props, { emit, slots }) { - const hasOptions = ref(false); - const formattedColumns = ref([]); - - const { - text: textKey, - values: valuesKey, - children: childrenKey, - } = extend( - { - text: 'text', - values: 'values', - children: 'children', - }, - props.columnsFieldNames - ); - + const selectedValues = ref(props.modelValue); const { children, linkChildren } = useChildren(PICKER_KEY); linkChildren(); - const itemHeight = computed(() => unitToPx(props.itemHeight)); + const fields = computed(() => assignDefaultFields(props.columnsFieldNames)); + const optionHeight = computed(() => unitToPx(props.optionHeight)); + const columnsType = computed(() => + getColumnsType(props.columns, fields.value) + ); - const dataType = computed(() => { - const firstColumn = props.columns[0]; - if (typeof firstColumn === 'object') { - if (childrenKey in firstColumn) { - return 'cascade'; - } - if (valuesKey in firstColumn) { - return 'object'; - } + const currentColumns = computed(() => { + const { columns } = props; + switch (columnsType.value) { + case 'multiple': + return columns as PickerColumn[]; + case 'cascade': + return formatCascadeColumns(columns, fields.value, selectedValues); + default: + return [columns]; } - return 'plain'; }); - const formatCascade = () => { - const formatted: PickerObjectColumn[] = []; + const hasOptions = computed(() => + currentColumns.value.some((options) => options.length) + ); - let cursor: PickerObjectColumn = { - [childrenKey]: props.columns, - }; + const selectedOptions = computed(() => + currentColumns.value.map((options, index) => + findOptionByValue(options, selectedValues.value[index], fields.value) + ) + ); - while (cursor && cursor[childrenKey]) { - const children = cursor[childrenKey]; - let defaultIndex = cursor.defaultIndex ?? +props.defaultIndex; + const onChange = (value: number | string, columnIndex: number) => { + selectedValues.value[columnIndex] = value; - while (children[defaultIndex] && children[defaultIndex].disabled) { - if (defaultIndex < children.length - 1) { - defaultIndex++; - } else { - defaultIndex = 0; - break; + if (columnsType.value === 'cascade') { + // reset values after cascading + selectedValues.value.forEach((value, index) => { + const options = currentColumns.value[index]; + if (!options.find((option) => option[fields.value.value] === value)) { + selectedValues.value[index] = options.length + ? options[0][fields.value.value] + : undefined; } - } - - formatted.push({ - [valuesKey]: cursor[childrenKey], - className: cursor.className, - defaultIndex, }); - - cursor = children[defaultIndex]; } - formattedColumns.value = formatted; - }; - - const format = () => { - const { columns } = props; - - if (dataType.value === 'plain') { - formattedColumns.value = [{ [valuesKey]: columns }]; - } else if (dataType.value === 'cascade') { - formatCascade(); - } else { - formattedColumns.value = columns as PickerObjectColumn[]; - } - - hasOptions.value = formattedColumns.value.some( - (item) => item[valuesKey] && item[valuesKey].length !== 0 - ); - }; - - // get indexes of all columns - const getIndexes = () => children.map((child) => child.state.index); - - // set options of column by index - const setColumnValues = (index: number, options: PickerOption[]) => { - const column = children[index]; - if (column) { - column.setOptions(options); - hasOptions.value = true; - } - }; - - const onCascadeChange = (columnIndex: number) => { - let cursor: PickerObjectColumn = { - [childrenKey]: props.columns, - }; - const indexes = getIndexes(); - - for (let i = 0; i <= columnIndex; i++) { - cursor = cursor[childrenKey][indexes[i]]; - } - - while (cursor && cursor[childrenKey]) { - columnIndex++; - setColumnValues(columnIndex, cursor[childrenKey]); - cursor = cursor[childrenKey][cursor.defaultIndex || 0]; - } - }; - - // get column instance by index - const getChild = (index: number) => children[index]; - - // get column value by index - const getColumnValue = (index: number) => { - const column = getChild(index); - if (column) { - return column.getValue(); - } - }; - - // set column value by index - const setColumnValue = (index: number, value: string) => { - const column = getChild(index); - if (column) { - column.setValue(value); - if (dataType.value === 'cascade') { - onCascadeChange(index); - } - } - }; - - // get column option index by column index - const getColumnIndex = (index: number) => { - const column = getChild(index); - if (column) { - return column.state.index; - } - }; - - // set column option index by column index - const setColumnIndex = (columnIndex: number, optionIndex: number) => { - const column = getChild(columnIndex); - if (column) { - column.setIndex(optionIndex); - if (dataType.value === 'cascade') { - onCascadeChange(columnIndex); - } - } - }; - - // get options of column by index - const getColumnValues = (index: number) => { - const column = getChild(index); - if (column) { - return column.state.options; - } - }; - - // get values of all columns - const getValues = () => children.map((child) => child.getValue()); - - // set values of all columns - const setValues = (values: string[]) => { - values.forEach((value, index) => { - setColumnValue(index, value); + emit('change', { + columnIndex, + selectedValues: selectedValues.value, + selectedOptions: selectedOptions.value, }); }; - // set indexes of all columns - const setIndexes = (indexes: number[]) => { - indexes.forEach((optionIndex, columnIndex) => { - setColumnIndex(columnIndex, optionIndex); - }); - }; - - const emitAction = (event: 'confirm' | 'cancel') => { - if (dataType.value === 'plain') { - emit(event, getColumnValue(0), getColumnIndex(0)); - } else { - emit(event, getValues(), getIndexes()); - } - }; - - const onChange = (columnIndex: number) => { - if (dataType.value === 'cascade') { - onCascadeChange(columnIndex); - } - - if (dataType.value === 'plain') { - emit('change', getColumnValue(0), getColumnIndex(0)); - } else { - emit('change', getValues(), columnIndex); - } - }; - const confirm = () => { children.forEach((child) => child.stopMomentum()); - emitAction('confirm'); + emit('confirm', { + selectedValues: selectedValues.value, + selectedOptions: selectedOptions.value, + }); }; - const cancel = () => emitAction('cancel'); + const cancel = () => + emit('cancel', { + selectedValues: selectedValues.value, + selectedOptions: selectedOptions.value, + }); const renderTitle = () => { if (slots.title) { @@ -324,27 +195,26 @@ export default defineComponent({ }; const renderColumnItems = () => - formattedColumns.value.map((item, columnIndex) => ( + currentColumns.value.map((options, columnIndex) => ( onChange(columnIndex)} + visibleOptionNum={props.visibleOptionNum} + onChange={(value: number | string) => onChange(value, columnIndex)} /> )); const renderMask = (wrapHeight: number) => { if (hasOptions.value) { - const frameStyle = { height: `${itemHeight.value}px` }; + const frameStyle = { height: `${optionHeight.value}px` }; const maskStyle = { - backgroundSize: `100% ${(wrapHeight - itemHeight.value) / 2}px`, + backgroundSize: `100% ${(wrapHeight - optionHeight.value) / 2}px`, }; return [
, @@ -357,7 +227,7 @@ export default defineComponent({ }; const renderColumns = () => { - const wrapHeight = itemHeight.value * +props.visibleItemCount; + const wrapHeight = optionHeight.value * +props.visibleOptionNum; const columnsStyle = { height: `${wrapHeight}px` }; return (
props.columns, format, { immediate: true }); + watch( + currentColumns, + (columns) => { + columns.forEach((options, index) => { + if (selectedValues.value[index] === undefined && options.length) { + selectedValues.value[index] = + getFirstEnabledOption(options)[fields.value.value]; + } + }); + }, + { immediate: true } + ); - useExpose({ - confirm, - getValues, - setValues, - getIndexes, - setIndexes, - getColumnIndex, - setColumnIndex, - getColumnValue, - setColumnValue, - getColumnValues, - setColumnValues, - }); + watch( + () => props.modelValue, + (newValues) => { + if (!isValuesEqual(newValues, selectedValues.value)) { + selectedValues.value = newValues; + } + }, + { deep: true } + ); + watch( + selectedValues, + (newValues) => { + if (!isValuesEqual(newValues, props.modelValue)) { + emit('update:modelValue', selectedValues.value); + } + }, + { deep: true } + ); + + useExpose({ confirm }); return () => (
diff --git a/packages/vant/src/picker/PickerColumn.tsx b/packages/vant/src/picker/PickerColumn.tsx index 2768c8dd7..bd4ad6a84 100644 --- a/packages/vant/src/picker/PickerColumn.tsx +++ b/packages/vant/src/picker/PickerColumn.tsx @@ -1,18 +1,21 @@ -import { ref, watch, reactive, defineComponent, type InjectionKey } from 'vue'; +import { + ref, + watchEffect, + defineComponent, + type PropType, + type InjectionKey, +} from 'vue'; // Utils -import { deepClone } from '../utils/deep-clone'; import { clamp, - isObject, - unknownProp, numericProp, makeArrayProp, - makeNumberProp, preventDefault, createNamespace, makeRequiredProp, } from '../utils'; +import { getElementTranslateY, findIndexOfEnabledOption } from './utils'; // Composables import { useParent } from '@vant/use'; @@ -20,42 +23,36 @@ import { useTouch } from '../composables/use-touch'; import { useExpose } from '../composables/use-expose'; // Types -import type { PickerOption, PickerColumnProvide } from './types'; +import type { + PickerOption, + PickerFieldNames, + PickerColumnProvide, +} from './types'; const DEFAULT_DURATION = 200; // 惯性滑动思路: -// 在手指离开屏幕时,如果和上一次 move 时的间隔小于 `MOMENTUM_LIMIT_TIME` 且 move -// 距离大于 `MOMENTUM_LIMIT_DISTANCE` 时,执行惯性滑动 -const MOMENTUM_LIMIT_TIME = 300; -const MOMENTUM_LIMIT_DISTANCE = 15; +// 在手指离开屏幕时,如果和上一次 move 时的间隔小于 `MOMENTUM_TIME` 且 move +// 距离大于 `MOMENTUM_DISTANCE` 时,执行惯性滑动 +const MOMENTUM_TIME = 300; +const MOMENTUM_DISTANCE = 15; const [name, bem] = createNamespace('picker-column'); -function getElementTranslateY(element: Element) { - const { transform } = window.getComputedStyle(element); - const translateY = transform.slice(7, transform.length - 1).split(', ')[5]; - return Number(translateY); -} - export const PICKER_KEY: InjectionKey = Symbol(name); -const isOptionDisabled = (option: PickerOption) => - isObject(option) && option.disabled; - export default defineComponent({ name, props: { - textKey: makeRequiredProp(String), + value: numericProp, + fields: makeRequiredProp(Object as PropType>), + options: makeArrayProp(), readonly: Boolean, allowHtml: Boolean, - className: unknownProp, - itemHeight: makeRequiredProp(Number), - defaultIndex: makeNumberProp(0), + optionHeight: makeRequiredProp(Number), swipeDuration: makeRequiredProp(numericProp), - initialOptions: makeArrayProp(), - visibleItemCount: makeRequiredProp(numericProp), + visibleOptionNum: makeRequiredProp(numericProp), }, emits: ['change'], @@ -68,61 +65,34 @@ export default defineComponent({ let transitionEndTrigger: null | (() => void); const wrapper = ref(); - - const state = reactive({ - index: props.defaultIndex, - offset: 0, - duration: 0, - options: deepClone(props.initialOptions), - }); - + const currentOffset = ref(0); + const currentDuration = ref(0); const touch = useTouch(); - const count = () => state.options.length; + const count = () => props.options.length; const baseOffset = () => - (props.itemHeight * (+props.visibleItemCount - 1)) / 2; + (props.optionHeight * (+props.visibleOptionNum - 1)) / 2; - const adjustIndex = (index: number) => { - index = clamp(index, 0, count()); + const updateValueByIndex = (index: number) => { + const enabledIndex = findIndexOfEnabledOption(props.options, index); + const offset = -enabledIndex * props.optionHeight; - for (let i = index; i < count(); i++) { - if (!isOptionDisabled(state.options[i])) return i; - } - for (let i = index - 1; i >= 0; i--) { - if (!isOptionDisabled(state.options[i])) return i; - } - }; - - const setIndex = (index: number, emitChange?: boolean) => { - index = adjustIndex(index) || 0; - - const offset = -index * props.itemHeight; const trigger = () => { - if (index !== state.index) { - state.index = index; - - if (emitChange) { - emit('change', index); - } + const value = props.options[enabledIndex][props.fields.value]; + if (value !== props.value) { + emit('change', value); } }; // trigger the change event after transitionend when moving - if (moving && offset !== state.offset) { + if (moving && offset !== currentOffset.value) { transitionEndTrigger = trigger; } else { trigger(); } - state.offset = offset; - }; - - const setOptions = (options: PickerOption[]) => { - if (JSON.stringify(options) !== JSON.stringify(state.options)) { - state.options = deepClone(options); - setIndex(props.defaultIndex); - } + currentOffset.value = offset; }; const onClickItem = (index: number) => { @@ -131,34 +101,28 @@ export default defineComponent({ } transitionEndTrigger = null; - state.duration = DEFAULT_DURATION; - setIndex(index, true); - }; - - const getOptionText = (option: PickerOption) => { - if (isObject(option) && props.textKey in option) { - return option[props.textKey]; - } - return option; + currentDuration.value = DEFAULT_DURATION; + updateValueByIndex(index); }; const getIndexByOffset = (offset: number) => - clamp(Math.round(-offset / props.itemHeight), 0, count() - 1); + clamp(Math.round(-offset / props.optionHeight), 0, count() - 1); const momentum = (distance: number, duration: number) => { const speed = Math.abs(distance / duration); - distance = state.offset + (speed / 0.003) * (distance < 0 ? -1 : 1); + distance = + currentOffset.value + (speed / 0.003) * (distance < 0 ? -1 : 1); const index = getIndexByOffset(distance); - state.duration = +props.swipeDuration; - setIndex(index, true); + currentDuration.value = +props.swipeDuration; + updateValueByIndex(index); }; const stopMomentum = () => { moving = false; - state.duration = 0; + currentDuration.value = 0; if (transitionEndTrigger) { transitionEndTrigger(); @@ -175,13 +139,11 @@ export default defineComponent({ if (moving) { const translateY = getElementTranslateY(wrapper.value!); - state.offset = Math.min(0, translateY - baseOffset()); - startOffset = state.offset; - } else { - startOffset = state.offset; + currentOffset.value = Math.min(0, translateY - baseOffset()); } - state.duration = 0; + currentDuration.value = 0; + startOffset = currentOffset.value; touchStartTime = Date.now(); momentumOffset = startOffset; transitionEndTrigger = null; @@ -199,16 +161,16 @@ export default defineComponent({ preventDefault(event, true); } - state.offset = clamp( + currentOffset.value = clamp( startOffset + touch.deltaY.value, - -(count() * props.itemHeight), - props.itemHeight + -(count() * props.optionHeight), + props.optionHeight ); const now = Date.now(); - if (now - touchStartTime > MOMENTUM_LIMIT_TIME) { + if (now - touchStartTime > MOMENTUM_TIME) { touchStartTime = now; - momentumOffset = state.offset; + momentumOffset = currentOffset.value; } }; @@ -217,23 +179,22 @@ export default defineComponent({ return; } - const distance = state.offset - momentumOffset; + const distance = currentOffset.value - momentumOffset; const duration = Date.now() - touchStartTime; - const allowMomentum = - duration < MOMENTUM_LIMIT_TIME && - Math.abs(distance) > MOMENTUM_LIMIT_DISTANCE; + const startMomentum = + duration < MOMENTUM_TIME && Math.abs(distance) > MOMENTUM_DISTANCE; - if (allowMomentum) { + if (startMomentum) { momentum(distance, duration); return; } - const index = getIndexByOffset(state.offset); - state.duration = DEFAULT_DURATION; - setIndex(index, true); + const index = getIndexByOffset(currentOffset.value); + currentDuration.value = DEFAULT_DURATION; + updateValueByIndex(index); // compatible with desktop scenario - // use setTimeout to skip the click event Emitted after touchstart + // use setTimeout to skip the click event emitted after touchstart setTimeout(() => { moving = false; }, 0); @@ -241,21 +202,24 @@ export default defineComponent({ const renderOptions = () => { const optionStyle = { - height: `${props.itemHeight}px`, + height: `${props.optionHeight}px`, }; - return state.options.map((option, index: number) => { - const text = getOptionText(option); - const disabled = isOptionDisabled(option); - + return props.options.map((option, index) => { + const text = option[props.fields.text]; + const { disabled } = option; + const value: string | number = option[props.fields.value]; const data = { role: 'button', style: optionStyle, tabindex: disabled ? -1 : 0, - class: bem('item', { - disabled, - selected: index === state.index, - }), + class: [ + bem('item', { + disabled, + selected: value === props.value, + }), + option.className, + ], onClick: () => onClickItem(index), }; @@ -272,39 +236,21 @@ export default defineComponent({ }); }; - const setValue = (value: string) => { - const { options } = state; - for (let i = 0; i < options.length; i++) { - if (getOptionText(options[i]) === value) { - return setIndex(i); - } - } - }; - - const getValue = (): PickerOption => state.options[state.index]; - - setIndex(state.index); - useParent(PICKER_KEY); - useExpose({ - state, - setIndex, - getValue, - setValue, - setOptions, - stopMomentum, + useExpose({ stopMomentum }); + + watchEffect(() => { + const index = props.options.findIndex( + (option) => option[props.fields.value] === props.value + ); + const enabledIndex = findIndexOfEnabledOption(props.options, index); + const offset = -enabledIndex * props.optionHeight; + currentOffset.value = offset; }); - watch(() => props.initialOptions, setOptions); - - watch( - () => props.defaultIndex, - (value) => setIndex(value) - ); - return () => (
{ - Toast(`Value: ${value}, Index: ${index}`); + const columns = [ + { text: 'Delaware', value: 'Delaware' }, + { text: 'Florida', value: 'Florida' }, + { text: 'Georqia', value: 'Georqia' }, + { text: 'Indiana', value: 'Indiana' }, + { text: 'Maine', value: 'Maine' }, + ]; + const onConfirm = ({ selectedValues }) => { + Toast(`Value: ${selectedValues.join(',')}`); }; - const onChange = (value, index) => { - Toast(`Value: ${value}, Index: ${index}`); + const onChange = ({ selectedValues }) => { + Toast(`Value: ${selectedValues.join(',')}`); }; const onCancel = () => Toast('Cancel'); @@ -55,161 +60,6 @@ export default { }; ``` -### Default Index - -```html - -``` - -### Multiple Columns - -```html - -``` - -```js -export default { - setup() { - const columns = [ - { - values: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'], - defaultIndex: 2, - }, - { - values: ['Morning', 'Afternoon', 'Evening'], - defaultIndex: 1, - }, - ]; - - return { columns }; - }, -}; -``` - -### Cascade - -```html - -``` - -```js -export default { - setup() { - const columns = [ - { - text: 'Zhejiang', - children: [ - { - text: 'Hangzhou', - children: [{ text: 'Xihu' }, { text: 'Yuhang' }], - }, - { - text: 'Wenzhou', - children: [{ text: 'Lucheng' }, { text: 'Ouhai' }], - }, - ], - }, - { - text: 'Fujian', - children: [ - { - text: 'Fuzhou', - children: [{ text: 'Gulou' }, { text: 'Taijiang' }], - }, - { - text: 'Xiamen', - children: [{ text: 'Siming' }, { text: 'Haicang' }], - }, - ], - }, - ]; - - return { columns }; - }, -}; -``` - -### Disable option - -```html - -``` - -```js -export default { - setup() { - const columns = [ - { text: 'Delaware', disabled: true }, - { text: 'Florida' }, - { text: 'Georqia' }, - ]; - - return { columns }; - }, -}; -``` - -### Set Column Values - -```html - -``` - -```js -import { ref } from 'vue'; - -export default { - setup() { - const picker = ref(null); - - const states = { - Group1: ['Delaware', 'Florida', 'Georqia', 'Indiana', 'Maine'], - Group2: ['Alabama', 'Kansas', 'Louisiana', 'Texas'], - }; - const columns = [ - { values: Object.keys(states) }, - { values: states.Group1 }, - ]; - - const onChange = (values) => { - picker.value.setColumnValues(1, states[values[0]]); - }; - - return { - picker, - columns, - onChange, - }; - }, -}; -``` - -### Loading - -When Picker columns data is acquired asynchronously, use `loading` prop to show loading prompt. - -```html - -``` - -```js -import { ref } from 'vue'; - -export default { - setup() { - const columns = ref([]); - const loading = ref(true); - - setTimeout(() => { - columns.value = ['Option']; - loading.value = false; - }, 1000); - - return { columns, loading }; - }, -}; -``` - ### With Popup ```html @@ -236,13 +86,19 @@ import { ref } from 'vue'; export default { setup() { - const columns = ['Delaware', 'Florida', 'Georqia', 'Indiana', 'Maine']; + const columns = [ + { text: 'Delaware', value: 'Delaware' }, + { text: 'Florida', value: 'Florida' }, + { text: 'Georqia', value: 'Georqia' }, + { text: 'Indiana', value: 'Indiana' }, + { text: 'Maine', value: 'Maine' }, + ]; const result = ref(''); const showPicker = ref(false); - const onConfirm = (value) => { - result.value = value; + const onConfirm = ({ selectedOptions }) => { showPicker.value = false; + fieldValue.value = selectedOptions[0].text; }; return { @@ -255,6 +111,141 @@ export default { }; ``` +### Multiple Columns + +```html + +``` + +```js +export default { + setup() { + const columns = [ + [ + { text: 'Monday', value: 'Monday' }, + { text: 'Tuesday', value: 'Tuesday' }, + { text: 'Wednesday', value: 'Wednesday' }, + { text: 'Thursday', value: 'Thursday' }, + { text: 'Friday', value: 'Friday' }, + ], + [ + { text: 'Morning', value: 'Morning' }, + { text: 'Afternoon', value: 'Afternoon' }, + { text: 'Evening', value: 'Evening' }, + ], + ]; + + return { columns }; + }, +}; +``` + +### Cascade + +```html + +``` + +```js +export default { + setup() { + const columns = [ + { + text: 'Zhejiang', + value: 'Zhejiang', + children: [ + { + text: 'Hangzhou', + value: 'Hangzhou', + children: [ + { text: 'Xihu', value: 'Xihu' }, + { text: 'Yuhang', value: 'Yuhang' }, + ], + }, + { + text: 'Wenzhou', + value: 'Wenzhou', + children: [ + { text: 'Lucheng', value: 'Lucheng' }, + { text: 'Ouhai', value: 'Ouhai' }, + ], + }, + ], + }, + { + text: 'Fujian', + value: 'Fujian', + children: [ + { + text: 'Fuzhou', + value: 'Fuzhou', + children: [ + { text: 'Gulou', value: 'Gulou' }, + { text: 'Taijiang', value: 'Taijiang' }, + ], + }, + { + text: 'Xiamen', + value: 'Xiamen', + children: [ + { text: 'Siming', value: 'Siming' }, + { text: 'Haicang', value: 'Haicang' }, + ], + }, + ], + }, + ]; + + return { columns }; + }, +}; +``` + +### Disable option + +```html + +``` + +```js +export default { + setup() { + const columns = [ + { text: 'Delaware', value: 'Delaware', disabled: true }, + { text: 'Florida', value: 'Florida' }, + { text: 'Georqia', value: 'Georqia' }, + ]; + return { columns }; + }, +}; +``` + +### Loading + +When Picker columns data is acquired asynchronously, use `loading` prop to show loading prompt. + +```html + +``` + +```js +import { ref } from 'vue'; + +export default { + setup() { + const columns = ref([]); + const loading = ref(true); + + setTimeout(() => { + columns.value = [{ text: 'Option', value: 'option' }]; + loading.value = false; + }, 1000); + + return { columns, loading }; + }, +}; +``` + ### Custom Columns Field ```html @@ -299,6 +290,7 @@ export default { const customFieldName = { text: 'cityName', + value: 'cityName', children: 'cities', }; @@ -316,8 +308,8 @@ export default { | Attribute | Description | Type | Default | | --- | --- | --- | --- | -| columns | Columns data | _Column[]_ | `[]` | -| columns-field-names | custom columns field | _object_ | `{ text: 'text', values: 'values', children: 'children' }` | +| columns | Columns data | _PickerOption[] \| PickerOption[][]_ | `[]` | +| columns-field-names | custom columns field | _object_ | `{ text: 'text', value: 'value', children: 'children' }` | | title | Toolbar title | _string_ | - | | confirm-button-text | Text of confirm button | _string_ | `Confirm` | | cancel-button-text | Text of cancel button | _string_ | `Cancel` | @@ -325,59 +317,47 @@ export default { | loading | Whether to show loading prompt | _boolean_ | `false` | | show-toolbar | Whether to show toolbar | _boolean_ | `true` | | allow-html | Whether to allow HTML in option text | _boolean_ | `false` | -| default-index | Default value index of single column picker | _number \| string_ | `0` | -| item-height | Option height, supports `px` `vw` `vh` `rem` unit, default `px` | _number \| string_ | `44` | -| visible-item-count | Count of visible columns | _number \| string_ | `6` | +| option-height | Option height, supports `px` `vw` `vh` `rem` unit, default `px` | _number \| string_ | `44` | +| visible-option-num | Count of visible columns | _number \| string_ | `6` | | swipe-duration | Duration of the momentum animation,unit `ms` | _number \| string_ | `1000` | ### Events -Picker events will pass different parameters according to the columns are single or multiple - | Event | Description | Arguments | | --- | --- | --- | -| confirm | Emitted when click confirm button | Single column:current value,current index
Multiple columns:current values,current indexes | -| cancel | Emitted when click cancel button | Single column:current value,current index
Multiple columns:current values,current indexes | -| change | Emitted when current option changed | Single column:Picker instance, current value,current index
Multiple columns:Picker instance, current values,column index | +| confirm | Emitted when the confirm button is clicked | _{ selectedValues, selectedOptions }_ | +| cancel | Emitted when the cancel button is clicked | _{ selectedValues, selectedOptions }_ | +| change | Emitted when current option is changed | _{ selectedValues, selectedOptions, columnIndex }_ | ### Slots -| Name | Description | SlotProps | -| --------------- | ---------------------------- | -------------------------- | -| toolbar `3.1.2` | Custom toolbar content | - | -| title | Custom title | - | -| confirm | Custom confirm button text | - | -| cancel | Custom cancel button text | - | -| option | Custom option content | _option: string \| object_ | -| columns-top | Custom content above columns | - | -| columns-bottom | Custom content below columns | - | +| Name | Description | SlotProps | +| --------------- | ---------------------------- | ---------------------- | +| toolbar `3.1.2` | Custom toolbar content | - | +| title | Custom title | - | +| confirm | Custom confirm button text | - | +| cancel | Custom cancel button text | - | +| option | Custom option content | _option: PickerOption_ | +| columns-top | Custom content above columns | - | +| columns-bottom | Custom content below columns | - | -### Data Structure of Column +### Data Structure of PickerOption -| Key | Description | Type | -| ------------ | ------------------------- | --------------------------- | -| values | Value of column | _Array_ | -| defaultIndex | Default value index | _number_ | -| className | ClassName for this column | _string \| Array \| object_ | -| children | Cascade children | _Column_ | +| Key | Description | Type | +| --------- | ------------------------- | --------------------------- | +| text | Text | _string \| number_ | +| value | Value of option | _string \| number_ | +| disabled | Whether to disable option | _boolean_ | +| children | Cascade children options | _PickerOption[]_ | +| className | ClassName for this option | _string \| Array \| object_ | ### Methods Use [ref](https://v3.vuejs.org/guide/component-template-refs.html) to get Picker instance and call instance methods. -| Name | Description | Attribute | Return value | -| --- | --- | --- | --- | -| getValues | Get current values of all columns | - | values | -| setValues | Set current values of all columns | values | - | -| getIndexes | Get current indexes of all columns | - | indexes | -| setIndexes | Set current indexes of all columns | indexes | - | -| getColumnValue | Get current value of the column | columnIndex | value | -| setColumnValue | Set current value of the column | columnIndex, value | - | -| getColumnIndex | Get current index of the column | columnIndex | optionIndex | -| setColumnIndex | Set current index of the column | columnIndex, optionIndex | - | -| getColumnValues | Get columns data of the column | columnIndex | values | -| setColumnValues | Set columns data of the column | columnIndex, values | - | -| confirm | Stop scrolling and emit confirm event | - | - | +| Name | Description | Attribute | Return value | +| ------- | ------------------------------------- | --------- | ------------ | +| confirm | Stop scrolling and emit confirm event | - | - | ### Types @@ -390,9 +370,10 @@ import type { PickerOption, PickerInstance, PickerFieldNames, - PickerObjectColumn, - PickerObjectOption, PickerToolbarPosition, + PickerCancelEventParams, + PickerChangeEventParams, + PickerConfirmEventParams, } from 'vant'; ``` diff --git a/packages/vant/src/picker/README.zh-CN.md b/packages/vant/src/picker/README.zh-CN.md index dca100c1e..5bb84113d 100644 --- a/packages/vant/src/picker/README.zh-CN.md +++ b/packages/vant/src/picker/README.zh-CN.md @@ -2,7 +2,7 @@ ### 介绍 -提供多个选项集合供用户选择,支持单列选择和多列级联,通常与[弹出层](#/zh-CN/popup)组件配合使用。 +提供多个选项集合供用户选择,支持单列选择、多列选择和级联选择,通常与[弹出层](#/zh-CN/popup)组件配合使用。 ### 引入 @@ -43,13 +43,18 @@ import { Toast } from 'vant'; export default { setup() { - const columns = ['杭州', '宁波', '温州', '绍兴', '湖州', '嘉兴', '金华']; - - const onConfirm = (value, index) => { - Toast(`当前值: ${value}, 当前索引: ${index}`); + const columns = [ + { text: '杭州', value: 'Hangzhou' }, + { text: '宁波', value: 'Ningbo' }, + { text: '温州', value: 'Wenzhou' }, + { text: '绍兴', value: 'Shaoxing' }, + { text: '湖州', value: 'Huzhou' }, + ]; + const onConfirm = ({ selectedValues }) => { + Toast(`当前值: ${selectedValues.join(',')}`); }; - const onChange = (value, index) => { - Toast(`当前值: ${value}, 当前索引: ${index}`); + const onChange = ({ selectedValues }) => { + Toast(`当前值: ${selectedValues.join(',')}`); }; const onCancel = () => Toast('取消'); @@ -63,175 +68,6 @@ export default { }; ``` -### 默认选中项 - -单列选择时,可以通过 `default-index` 属性设置初始选中项的索引。 - -```html - -``` - -### 多列选择 - -`columns` 属性可以通过对象数组的形式配置多列选择,对象中可以配置选项数据、初始选中项等,详细格式见[下方表格](#/zh-CN/picker#column-shu-ju-jie-gou)。 - -```html - -``` - -```js -export default { - setup() { - const columns = [ - // 第一列 - { - values: ['周一', '周二', '周三', '周四', '周五'], - defaultIndex: 2, - }, - // 第二列 - { - values: ['上午', '下午', '晚上'], - defaultIndex: 1, - }, - ]; - - return { columns }; - }, -}; -``` - -### 级联选择 - -使用 `columns` 的 `children` 字段可以实现选项级联的效果。如果级联层级较多,推荐使用 [Cascader 级联选项组件](#/zh-CN/cascader)。 - -```html - -``` - -```js -export default { - setup() { - const columns = [ - { - text: '浙江', - children: [ - { - text: '杭州', - children: [{ text: '西湖区' }, { text: '余杭区' }], - }, - { - text: '温州', - children: [{ text: '鹿城区' }, { text: '瓯海区' }], - }, - ], - }, - { - text: '福建', - children: [ - { - text: '福州', - children: [{ text: '鼓楼区' }, { text: '台江区' }], - }, - { - text: '厦门', - children: [{ text: '思明区' }, { text: '海沧区' }], - }, - ], - }, - ]; - - return { columns }; - }, -}; -``` - -> 级联选择的数据嵌套深度需要保持一致,如果部分选项没有子选项,可以使用空字符串进行占位。 - -### 禁用选项 - -选项可以为对象结构,通过设置 `disabled` 来禁用该选项。 - -```html - -``` - -```js -export default { - setup() { - const columns = [ - { text: '杭州', disabled: true }, - { text: '宁波' }, - { text: '温州' }, - ]; - - return { columns }; - }, -}; -``` - -### 动态设置选项 - -通过 Picker 上的实例方法可以更灵活地控制选择器,比如使用 `setColumnValues` 方法实现多列联动。 - -```html - -``` - -```js -import { ref } from 'vue'; - -export default { - setup() { - const picker = ref(null); - - const cities = { - 浙江: ['杭州', '宁波', '温州', '嘉兴', '湖州'], - 福建: ['福州', '厦门', '莆田', '三明', '泉州'], - }; - const columns = [ - { values: Object.keys(cities) }, - { values: cities['浙江'] }, - ]; - - const onChange = (values) => { - picker.value.setColumnValues(1, cities[values[0]]); - }; - - return { - picker, - columns, - onChange, - }; - }, -}; -``` - -### 加载状态 - -若选择器数据是异步获取的,可以通过 `loading` 属性显示加载提示。 - -```html - -``` - -```js -import { ref } from 'vue'; - -export default { - setup() { - const columns = ref([]); - const loading = ref(true); - - setTimeout(() => { - columns.value = ['选项']; - loading.value = false; - }, 1000); - - return { columns, loading }; - }, -}; -``` - ### 搭配弹出层使用 在实际场景中,Picker 通常作为用于辅助表单填写,可以搭配 Popup 和 Field 实现该效果。 @@ -259,13 +95,19 @@ import { ref } from 'vue'; export default { setup() { - const columns = ['杭州', '宁波', '温州', '绍兴', '湖州', '嘉兴', '金华']; + const columns = [ + { text: '杭州', value: 'Hangzhou' }, + { text: '宁波', value: 'Ningbo' }, + { text: '温州', value: 'Wenzhou' }, + { text: '绍兴', value: 'Shaoxing' }, + { text: '湖州', value: 'Huzhou' }, + ]; const result = ref(''); const showPicker = ref(false); - const onConfirm = (value) => { - result.value = value; + const onConfirm = ({ selectedOptions }) => { showPicker.value = false; + fieldValue.value = selectedOptions[0].text; }; return { @@ -278,6 +120,151 @@ export default { }; ``` +### 多列选择 + +`columns` 属性可以通过二维数组的形式配置多列选择。 + +```html + +``` + +```js +export default { + setup() { + const columns = [ + // 第一列 + [ + { text: '周一', value: 'Monday' }, + { text: '周二', value: 'Tuesday' }, + { text: '周三', value: 'Wednesday' }, + { text: '周四', value: 'Thursday' }, + { text: '周五', value: 'Friday' }, + ], + // 第二列 + [ + { text: '上午', value: 'Morning' }, + { text: '下午', value: 'Afternoon' }, + { text: '晚上', value: 'Evening' }, + ], + ]; + + return { columns }; + }, +}; +``` + +### 级联选择 + +使用 `columns` 的 `children` 字段可以实现选项级联的效果。如果级联层级较多,推荐使用 [Cascader 级联选项组件](#/zh-CN/cascader)。 + +```html + +``` + +```js +export default { + setup() { + const columns = [ + { + text: '浙江', + value: 'Zhejiang', + children: [ + { + text: '杭州', + value: 'Hangzhou', + children: [ + { text: '西湖区', value: 'Xihu' }, + { text: '余杭区', value: 'Yuhang' }, + ], + }, + { + text: '温州', + value: 'Wenzhou', + children: [ + { text: '鹿城区', value: 'Lucheng' }, + { text: '瓯海区', value: 'Ouhai' }, + ], + }, + ], + }, + { + text: '福建', + value: 'Fujian', + children: [ + { + text: '福州', + value: 'Fuzhou', + children: [ + { text: '鼓楼区', value: 'Gulou' }, + { text: '台江区', value: 'Taijiang' }, + ], + }, + { + text: '厦门', + value: 'Xiamen', + children: [ + { text: '思明区', value: 'Siming' }, + { text: '海沧区', value: 'Haicang' }, + ], + }, + ], + }, + ]; + + return { columns }; + }, +}; +``` + +> 级联选择的数据嵌套深度需要保持一致,如果部分选项没有子选项,可以使用空字符串进行占位。 + +### 禁用选项 + +选项可以为对象结构,通过设置 `disabled` 来禁用该选项。 + +```html + +``` + +```js +export default { + setup() { + const columns = [ + { text: '杭州', value: 'Hangzhou', disabled: true }, + { text: '宁波', value: 'Ningbo' }, + { text: '温州', value: 'Wenzhou' }, + ]; + return { columns }; + }, +}; +``` + +### 加载状态 + +若选择器数据是异步获取的,可以通过 `loading` 属性显示加载提示。 + +```html + +``` + +```js +import { ref } from 'vue'; + +export default { + setup() { + const columns = ref([]); + const loading = ref(true); + + setTimeout(() => { + columns.value = [{ text: '选项', value: 'option' }]; + loading.value = false; + }, 1000); + + return { columns, loading }; + }, +}; +``` + ### 自定义 Columns 的结构 ```html @@ -322,6 +309,7 @@ export default { const customFieldName = { text: 'cityName', + value: 'cityName', children: 'cities', }; @@ -339,8 +327,8 @@ export default { | 参数 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | -| columns | 对象数组,配置每一列显示的数据 | _Column[]_ | `[]` | -| columns-field-names | 自定义 `columns` 结构中的字段 | _object_ | `{ text: 'text', values: 'values', children: 'children' }` | +| columns | 对象数组,配置每一列显示的数据 | _PickerOption[] \| PickerOption[][]_ | `[]` | +| columns-field-names | 自定义 `columns` 结构中的字段 | _object_ | `{ text: 'text', value: 'value', children: 'children' }` | | title | 顶部栏标题 | _string_ | - | | confirm-button-text | 确认按钮文字 | _string_ | `确认` | | cancel-button-text | 取消按钮文字 | _string_ | `取消` | @@ -348,61 +336,47 @@ export default { | loading | 是否显示加载状态 | _boolean_ | `false` | | show-toolbar | 是否显示顶部栏 | _boolean_ | `true` | | allow-html | 是否允许选项内容中渲染 HTML | _boolean_ | `false` | -| default-index | 单列选择时,默认选中项的索引 | _number \| string_ | `0` | -| item-height | 选项高度,支持 `px` `vw` `vh` `rem` 单位,默认 `px` | _number \| string_ | `44` | -| visible-item-count | 可见的选项个数 | _number \| string_ | `6` | +| option-height | 选项高度,支持 `px` `vw` `vh` `rem` 单位,默认 `px` | _number \| string_ | `44` | +| visible-option-num | 可见的选项个数 | _number \| string_ | `6` | | swipe-duration | 快速滑动时惯性滚动的时长,单位 `ms` | _number \| string_ | `1000` | ### Events -当选择器有多列时,事件回调参数会返回数组。 - | 事件名 | 说明 | 回调参数 | | --- | --- | --- | -| confirm | 点击完成按钮时触发 | 单列:选中值,选中值对应的索引
多列:所有列选中值,所有列选中值对应的索引 | -| cancel | 点击取消按钮时触发 | 单列:选中值,选中值对应的索引
多列:所有列选中值,所有列选中值对应的索引 | -| change | 选项改变时触发 | 单列:选中值,选中值对应的索引
多列:所有列选中值,当前列对应的索引 | +| confirm | 点击完成按钮时触发 | _{ selectedValues, selectedOptions }_ | +| cancel | 点击取消按钮时触发 | _{ selectedValues, selectedOptions }_ | +| change | 选项改变时触发 | _{ selectedValues, selectedOptions, columnIndex }_ | ### Slots -| 名称 | 说明 | 参数 | -| ---------------- | ---------------------- | -------------------------- | -| toolbar `v3.1.2` | 自定义整个顶部栏的内容 | - | -| title | 自定义标题内容 | - | -| confirm | 自定义确认按钮内容 | - | -| cancel | 自定义取消按钮内容 | - | -| option | 自定义选项内容 | _option: string \| object_ | -| columns-top | 自定义选项上方内容 | - | -| columns-bottom | 自定义选项下方内容 | - | +| 名称 | 说明 | 参数 | +| ---------------- | ---------------------- | ---------------------- | +| toolbar `v3.1.2` | 自定义整个顶部栏的内容 | - | +| title | 自定义标题内容 | - | +| confirm | 自定义确认按钮内容 | - | +| cancel | 自定义取消按钮内容 | - | +| option | 自定义选项内容 | _option: PickerOption_ | +| columns-top | 自定义选项上方内容 | - | +| columns-bottom | 自定义选项下方内容 | - | -### Column 数据结构 +### PickerOption 数据结构 -当传入多列数据时,`columns` 为一个对象数组,数组中的每一个对象配置每一列,每一列有以下 `key`: - -| 键名 | 说明 | 类型 | -| ------------ | -------------------------- | --------------------------- | -| values | 列中对应的备选值 | _Array_ | -| defaultIndex | 初始选中项的索引,默认为 0 | _number_ | -| className | 为对应列添加额外的类名 | _string \| Array \| object_ | -| children | 级联选项 | _Column_ | +| 键名 | 说明 | 类型 | +| --------- | ------------ | --------------------------- | +| text | 选项文字内容 | _string \| number_ | +| value | 选项对应的值 | _string \| number_ | +| disabled | 是否禁用选项 | _boolean_ | +| children | 级联选项 | _PickerOption[]_ | +| className | 选项额外类名 | _string \| Array \| object_ | ### 方法 通过 ref 可以获取到 Picker 实例并调用实例方法,详见[组件实例方法](#/zh-CN/advanced-usage#zu-jian-shi-li-fang-fa)。 -| 方法名 | 说明 | 参数 | 返回值 | -| --- | --- | --- | --- | -| getValues | 获取所有列选中的值 | - | values | -| setValues | 设置所有列选中的值 | values | - | -| getIndexes | 获取所有列选中值对应的索引 | - | indexes | -| setIndexes | 设置所有列选中值对应的索引 | indexes | - | -| getColumnValue | 获取对应列选中的值 | columnIndex | value | -| setColumnValue | 设置对应列选中的值 | columnIndex, value | - | -| getColumnIndex | 获取对应列选中项的索引 | columnIndex | optionIndex | -| setColumnIndex | 设置对应列选中项的索引 | columnIndex, optionIndex | - | -| getColumnValues | 获取对应列中所有选项 | columnIndex | values | -| setColumnValues | 设置对应列中所有选项 | columnIndex, values | - | -| confirm | 停止惯性滚动并触发 confirm 事件 | - | - | +| 方法名 | 说明 | 参数 | 返回值 | +| ------- | --------------------------------- | ---- | ------ | +| confirm | 停止惯性滚动并触发 `confirm` 事件 | - | - | ### 类型定义 @@ -415,9 +389,10 @@ import type { PickerOption, PickerInstance, PickerFieldNames, - PickerObjectColumn, - PickerObjectOption, PickerToolbarPosition, + PickerCancelEventParams, + PickerChangeEventParams, + PickerConfirmEventParams, } from 'vant'; ``` diff --git a/packages/vant/src/picker/demo/WithPopup.vue b/packages/vant/src/picker/demo/WithPopup.vue new file mode 100644 index 000000000..32a232cae --- /dev/null +++ b/packages/vant/src/picker/demo/WithPopup.vue @@ -0,0 +1,59 @@ + + + diff --git a/packages/vant/src/picker/demo/data.ts b/packages/vant/src/picker/demo/data.ts index cd9b0b803..f89e1e72b 100644 --- a/packages/vant/src/picker/demo/data.ts +++ b/packages/vant/src/picker/demo/data.ts @@ -1,23 +1,48 @@ -export const dateColumns = { +export const basicColumns = { 'zh-CN': [ - { - values: ['周一', '周二', '周三', '周四', '周五'], - defaultIndex: 2, - }, - { - values: ['上午', '下午', '晚上'], - defaultIndex: 1, - }, + { text: '杭州', value: 'Hangzhou' }, + { text: '宁波', value: 'Ningbo' }, + { text: '温州', value: 'Wenzhou' }, + { text: '绍兴', value: 'Shaoxing' }, + { text: '湖州', value: 'Huzhou' }, ], 'en-US': [ - { - values: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'], - defaultIndex: 2, - }, - { - values: ['Morning', 'Afternoon', 'Evening'], - defaultIndex: 1, - }, + { text: 'Delaware', value: 'Delaware' }, + { text: 'Florida', value: 'Florida' }, + { text: 'Georqia', value: 'Georqia' }, + { text: 'Indiana', value: 'Indiana' }, + { text: 'Maine', value: 'Maine' }, + ], +}; + +export const dateColumns = { + 'zh-CN': [ + [ + { text: '周一', value: 'Monday' }, + { text: '周二', value: 'Tuesday' }, + { text: '周三', value: 'Wednesday' }, + { text: '周四', value: 'Thursday' }, + { text: '周五', value: 'Friday' }, + ], + [ + { text: '上午', value: 'Morning' }, + { text: '下午', value: 'Afternoon' }, + { text: '晚上', value: 'Evening' }, + ], + ], + 'en-US': [ + [ + { text: 'Monday', value: 'Monday' }, + { text: 'Tuesday', value: 'Tuesday' }, + { text: 'Wednesday', value: 'Wednesday' }, + { text: 'Thursday', value: 'Thursday' }, + { text: 'Friday', value: 'Friday' }, + ], + [ + { text: 'Morning', value: 'Morning' }, + { text: 'Afternoon', value: 'Afternoon' }, + { text: 'Evening', value: 'Evening' }, + ], ], }; @@ -25,27 +50,45 @@ export const cascadeColumns = { 'zh-CN': [ { text: '浙江', + value: 'Zhejiang', children: [ { text: '杭州', - children: [{ text: '西湖区' }, { text: '余杭区' }], + value: 'Hangzhou', + children: [ + { text: '西湖区', value: 'Xihu' }, + { text: '余杭区', value: 'Yuhang' }, + ], }, { text: '温州', - children: [{ text: '鹿城区' }, { text: '瓯海区' }], + value: 'Wenzhou', + children: [ + { text: '鹿城区', value: 'Lucheng' }, + { text: '瓯海区', value: 'Ouhai' }, + ], }, ], }, { text: '福建', + value: 'Fujian', children: [ { text: '福州', - children: [{ text: '鼓楼区' }, { text: '台江区' }], + value: 'Fuzhou', + children: [ + { text: '鼓楼区', value: 'Gulou' }, + { text: '台江区', value: 'Taijiang' }, + ], }, { text: '厦门', - children: [{ text: '思明区' }, { text: '海沧区' }], + value: 'Xiamen', + children: [ + { text: '思明区', value: 'Siming' }, + { text: '海沧区', value: 'Haicang' }, + ], }, ], }, @@ -53,34 +96,52 @@ export const cascadeColumns = { 'en-US': [ { text: 'Zhejiang', + value: 'Zhejiang', children: [ { text: 'Hangzhou', - children: [{ text: 'Xihu' }, { text: 'Yuhang' }], + value: 'Hangzhou', + children: [ + { text: 'Xihu', value: 'Xihu' }, + { text: 'Yuhang', value: 'Yuhang' }, + ], }, { text: 'Wenzhou', - children: [{ text: 'Lucheng' }, { text: 'Ouhai' }], + value: 'Wenzhou', + children: [ + { text: 'Lucheng', value: 'Lucheng' }, + { text: 'Ouhai', value: 'Ouhai' }, + ], }, ], }, { text: 'Fujian', + value: 'Fujian', children: [ { text: 'Fuzhou', - children: [{ text: 'Gulou' }, { text: 'Taijiang' }], + value: 'Fuzhou', + children: [ + { text: 'Gulou', value: 'Gulou' }, + { text: 'Taijiang', value: 'Taijiang' }, + ], }, { text: 'Xiamen', - children: [{ text: 'Siming' }, { text: 'Haicang' }], + value: 'Xiamen', + children: [ + { text: 'Siming', value: 'Siming' }, + { text: 'Haicang', value: 'Haicang' }, + ], }, ], }, ], }; -export const cascadeColumnsCustomKey = { +export const customKeyColumns = { 'zh-CN': [ { cityName: '浙江', @@ -138,3 +199,16 @@ export const cascadeColumnsCustomKey = { }, ], }; + +export const disabledColumns = { + 'zh-CN': [ + { text: '杭州', value: 'Hangzhou', disabled: true }, + { text: '宁波', value: 'Ningbo' }, + { text: '温州', value: 'Wenzhou' }, + ], + 'en-US': [ + { text: 'Delaware', value: 'Delaware', disabled: true }, + { text: 'Florida', value: 'Florida' }, + { text: 'Georqia', value: 'Georqia' }, + ], +}; diff --git a/packages/vant/src/picker/demo/index.vue b/packages/vant/src/picker/demo/index.vue index b90e41ee1..6215cefb1 100644 --- a/packages/vant/src/picker/demo/index.vue +++ b/packages/vant/src/picker/demo/index.vue @@ -1,146 +1,79 @@