diff --git a/src/cascader/README.md b/src/cascader/README.md new file mode 100644 index 000000000..7b2ffbd62 --- /dev/null +++ b/src/cascader/README.md @@ -0,0 +1,178 @@ +# Cascader + +### Install + +```js +import Vue from 'vue'; +import { Cascader } from 'vant'; + +Vue.use(Cascader); +``` + +## Usage + +### Basic Usage + +```html + + + + +``` + +```js +export default { + data() { + return { + show: false, + fieldValue: '', + cascaderValue: '', + options: [ + { + text: 'Zhejiang', + value: '330000', + children: [{ text: 'Hangzhou', value: '330100' }], + }, + { + text: 'Jiangsu', + value: '320000', + children: [{ text: 'Nanjing', value: '320100' }], + }, + ], + }; + }, + methods: { + onFinish({ selectedOptions }) { + this.show = false; + this.fieldValue = selectedOptions.map((option) => option.text).join('/'); + }, + }, +}; +``` + +### Custom Color + +```html + +``` + +### Async Options + +```html + + + + +``` + +```js +export default { + data() { + return { + show: false, + fieldValue: '', + cascaderValue: '', + options: [ + { + text: 'Zhejiang', + value: '330000', + children: [], + }, + ], + }; + }, + methods: { + onChange({ value }) { + if (value === this.options[0].value) { + setTimeout(() => { + this.options[0].children = [ + { text: 'Hangzhou', value: '330100' }, + { text: 'Ningbo', value: '330200' }, + ]; + }, 500); + } + }, + onFinish({ selectedOptions }) { + this.show = false; + this.fieldValue = selectedOptions.map((option) => option.text).join('/'); + }, + }, +}; +``` + +## API + +### Props + +| Attribute | Description | Type | Default | +| --- | --- | --- | --- | +| title | Title | _string_ | - | +| value | Value of selected option | _string \| number_ | - | +| options | Options | _Option[]_ | `[]` | +| placeholder | Placeholder of unselected tab | _string_ | `Select` | +| active-color | Active color | _string_ | `#ee0a24` | +| closeable | Whether to show close icon | _boolean_ | `true` | + +### Events + +| Event | Description | Arguments | +| --- | --- | --- | +| change | Emitted when active option changed | `{ value, selectedOptions, tabIndex }` | +| finish | Emitted when all options is selected | `{ value, selectedOptions, tabIndex }` | +| close | Emmitted when the close icon is clicked | - | + +### Slots + +| Name | Description | +| ----- | ------------ | +| title | Custom title | + +### Less Variables + +How to use: [Custom Theme](#/en-US/theme). + +| Name | Default Value | Description | +| --------------------------------- | --------------- | ----------- | +| @cascader-header-height | `48px` | - | +| @cascader-title-font-size | `@font-size-lg` | - | +| @cascader-title-line-height | `20px` | - | +| @cascader-close-icon-size | `22px` | - | +| @cascader-close-icon-color | `@gray-5` | - | +| @cascader-close-icon-active-color | `@gray-6` | - | +| @cascader-selected-icon-size | `18px` | - | +| @cascader-tabs-height | `48px` | - | +| @cascader-active-color | `@red` | - | +| @cascader-options-height | `384px` | - | +| @cascader-tab-color | `@text-color` | - | +| @cascader-unselected-tab-color | `@gray-6` | - | diff --git a/src/cascader/README.zh-CN.md b/src/cascader/README.zh-CN.md new file mode 100644 index 000000000..47f020c7a --- /dev/null +++ b/src/cascader/README.zh-CN.md @@ -0,0 +1,190 @@ +# Cascader 级联选择 + +### 介绍 + +级联选择框,用于多层级数据的选择,典型场景为省市区选择,2.12 版本开始支持此组件。 + +### 引入 + +```js +import Vue from 'vue'; +import { Cascader } from 'vant'; + +Vue.use(Cascader); +``` + +## 代码演示 + +### 基础用法 + +级联选择组件可以搭配 Field 和 Popup 组件使用,示例如下: + +```html + + + + +``` + +```js +export default { + data() { + return { + show: false, + fieldValue: '', + cascaderValue: '', + // 选项列表,children 代表子选项,支持多级嵌套 + options: [ + { + text: '浙江省', + value: '330000', + children: [{ text: '杭州市', value: '330100' }], + }, + { + text: '江苏省', + value: '320000', + children: [{ text: '南京市', value: '320100' }], + }, + ], + }; + }, + methods: { + // 全部选项选择完毕后,会触发 finish 事件 + onFinish({ selectedOptions }) { + this.show = false; + this.fieldValue = selectedOptions.map((option) => option.text).join('/'); + }, + }, +}; +``` + +### 自定义颜色 + +通过 `active-color` 属性来设置选中状态的高亮颜色。 + +```html + +``` + +### 异步加载选项 + +可以监听 `change` 事件并动态设置 `options`,实现异步加载选项。 + +```html + + + + +``` + +```js +export default { + data() { + return { + show: false, + fieldValue: '', + cascaderValue: '', + options: [ + { + text: '浙江省', + value: '330000', + children: [], + }, + ], + }; + }, + methods: { + onChange({ value }) { + if (value === this.options[0].value) { + setTimeout(() => { + this.options[0].children = [ + { text: '杭州市', value: '330100' }, + { text: '宁波市', value: '330200' }, + ]; + }, 500); + } + }, + onFinish({ selectedOptions }) { + this.show = false; + this.fieldValue = selectedOptions.map((option) => option.text).join('/'); + }, + }, +}; +``` + +## API + +### Props + +| 参数 | 说明 | 类型 | 默认值 | +| ------------ | ------------------ | ------------------ | --------- | +| title | 顶部标题 | _string_ | - | +| value | 选中项的值 | _string \| number_ | - | +| options | 可选项数据源 | _Option[]_ | `[]` | +| placeholder | 未选中时的提示文案 | _string_ | `请选择` | +| active-color | 选中状态的高亮颜色 | _string_ | `#ee0a24` | +| closeable | 是否显示关闭图标 | _boolean_ | `true` | + +### Events + +| 事件 | 说明 | 回调参数 | +| ------ | ---------------------- | -------------------------------------- | +| change | 选中项变化时触发 | `{ value, selectedOptions, tabIndex }` | +| finish | 全部选项选择完成后触发 | `{ value, selectedOptions, tabIndex }` | +| close | 点击关闭图标时触发 | - | + +### Slots + +| 名称 | 说明 | +| ----- | -------------- | +| title | 自定义顶部标题 | + +### 样式变量 + +组件提供了下列 Less 变量,可用于自定义样式,使用方法请参考[主题定制](#/zh-CN/theme)。 + +| 名称 | 默认值 | 描述 | +| --------------------------------- | --------------- | ---- | +| @cascader-header-height | `48px` | - | +| @cascader-title-font-size | `@font-size-lg` | - | +| @cascader-title-line-height | `20px` | - | +| @cascader-close-icon-size | `22px` | - | +| @cascader-close-icon-color | `@gray-5` | - | +| @cascader-close-icon-active-color | `@gray-6` | - | +| @cascader-selected-icon-size | `18px` | - | +| @cascader-tabs-height | `48px` | - | +| @cascader-active-color | `@red` | - | +| @cascader-options-height | `384px` | - | +| @cascader-tab-color | `@text-color` | - | +| @cascader-unselected-tab-color | `@gray-6` | - | diff --git a/src/cascader/demo/area-en-US.js b/src/cascader/demo/area-en-US.js new file mode 100644 index 000000000..8f0fa88be --- /dev/null +++ b/src/cascader/demo/area-en-US.js @@ -0,0 +1,122 @@ +export default [ + { + text: 'Zhejiang', + value: '330000', + children: [ + { + text: 'Hangzhou', + value: '330100', + children: [ + { + text: 'Shangcheng', + value: '330102', + }, + { + text: 'Xiacheng', + value: '330103', + }, + { + text: 'Jianggan', + value: '330104', + }, + ], + }, + { + text: 'Ningbo', + value: '330200', + children: [ + { + text: 'Haishu', + value: '330203', + }, + { + text: 'Jiangbei', + value: '330205', + }, + { + text: 'Beilun', + value: '330206', + }, + ], + }, + { + text: 'Wenzhou', + value: '330300', + children: [ + { + text: 'Lucheng', + value: '330302', + }, + { + text: 'Longwan', + value: '330303', + }, + { + text: 'Ouhai', + value: '330304', + }, + ], + }, + ], + }, + { + text: 'Jiangsu', + value: '320000', + children: [ + { + text: 'Nanjing', + value: '320100', + children: [ + { + text: 'Xuanwu', + value: '320102', + }, + { + text: 'Qinghuai', + value: '320104', + }, + { + text: 'Jianye', + value: '320105', + }, + ], + }, + { + text: 'Wuxi', + value: '320200', + children: [ + { + text: 'Xishan', + value: '320205', + }, + { + text: 'Huishan', + value: '320206', + }, + { + text: 'Binhu', + value: '320211', + }, + ], + }, + { + text: 'Xuzhou', + value: '320300', + children: [ + { + text: 'Gulou', + value: '320302', + }, + { + text: 'Yunlong', + value: '320303', + }, + { + text: 'Jiawang', + value: '320305', + }, + ], + }, + ], + }, +]; diff --git a/src/cascader/demo/area-zh-CN.js b/src/cascader/demo/area-zh-CN.js new file mode 100644 index 000000000..c9f297f14 --- /dev/null +++ b/src/cascader/demo/area-zh-CN.js @@ -0,0 +1,122 @@ +export default [ + { + text: '浙江省', + value: '330000', + children: [ + { + text: '杭州市', + value: '330100', + children: [ + { + text: '上城区', + value: '330102', + }, + { + text: '下城区', + value: '330103', + }, + { + text: '江干区', + value: '330104', + }, + ], + }, + { + text: '宁波市', + value: '330200', + children: [ + { + text: '海曙区', + value: '330203', + }, + { + text: '江北区', + value: '330205', + }, + { + text: '北仑区', + value: '330206', + }, + ], + }, + { + text: '温州市', + value: '330300', + children: [ + { + text: '鹿城区', + value: '330302', + }, + { + text: '龙湾区', + value: '330303', + }, + { + text: '瓯海区', + value: '330304', + }, + ], + }, + ], + }, + { + text: '江苏省', + value: '320000', + children: [ + { + text: '南京市', + value: '320100', + children: [ + { + text: '玄武区', + value: '320102', + }, + { + text: '秦淮区', + value: '320104', + }, + { + text: '建邺区', + value: '320105', + }, + ], + }, + { + text: '无锡市', + value: '320200', + children: [ + { + text: '锡山区', + value: '320205', + }, + { + text: '惠山区', + value: '320206', + }, + { + text: '滨湖区', + value: '320211', + }, + ], + }, + { + text: '徐州市', + value: '320300', + children: [ + { + text: '鼓楼区', + value: '320302', + }, + { + text: '云龙区', + value: '320303', + }, + { + text: '贾汪区', + value: '320305', + }, + ], + }, + ], + }, +]; diff --git a/src/cascader/demo/index.vue b/src/cascader/demo/index.vue new file mode 100644 index 000000000..e6d7bebf7 --- /dev/null +++ b/src/cascader/demo/index.vue @@ -0,0 +1,158 @@ + + + diff --git a/src/cascader/index.js b/src/cascader/index.js new file mode 100644 index 000000000..fba099ba4 --- /dev/null +++ b/src/cascader/index.js @@ -0,0 +1,252 @@ +import { createNamespace } from '../utils'; +import Tab from '../tab'; +import Tabs from '../tabs'; +import Icon from '../icon'; + +const [createComponent, bem, t] = createNamespace('cascader'); + +export default createComponent({ + props: { + title: String, + modelValue: [Number, String], + placeholder: String, + activeColor: String, + options: { + type: Array, + default: () => [], + }, + closeable: { + type: Boolean, + default: true, + }, + }, + + emits: ['close', 'change', 'finish', 'update:modelValue'], + + data() { + return { + tabs: [], + activeTab: 0, + }; + }, + + watch: { + options: { + deep: true, + handler: 'updateTabs', + }, + + modelValue(value) { + if (value || value === 0) { + const values = this.tabs.map((tab) => tab.selectedOption?.value); + if (values.indexOf(value) !== -1) { + return; + } + } + this.updateTabs(); + }, + }, + + created() { + this.updateTabs(); + }, + + methods: { + getSelectedOptionsByValue(options, value) { + for (let i = 0; i < options.length; i++) { + const option = options[i]; + + if (option.value === value) { + return [option]; + } + + if (option.children) { + const selectedOptions = this.getSelectedOptionsByValue( + option.children, + value + ); + if (selectedOptions) { + return [option, ...selectedOptions]; + } + } + } + }, + + updateTabs() { + if (this.modelValue || this.modelValue === 0) { + const selectedOptions = this.getSelectedOptionsByValue( + this.options, + this.modelValue + ); + + if (selectedOptions) { + let optionsCursor = this.options; + + this.tabs = selectedOptions.map((option) => { + const tab = { + options: optionsCursor, + selectedOption: option, + }; + + const next = optionsCursor.filter( + (item) => item.value === option.value + ); + if (next.length) { + optionsCursor = next[0].children; + } + + return tab; + }); + + if (optionsCursor) { + this.tabs.push({ + options: optionsCursor, + selectedOption: null, + }); + } + + this.$nextTick(() => { + this.activeTab = this.tabs.length - 1; + }); + + return; + } + } + + this.tabs = [ + { + options: this.options, + selectedOption: null, + }, + ]; + }, + + onSelect(option, tabIndex) { + this.tabs[tabIndex].selectedOption = option; + + if (this.tabs.length > tabIndex + 1) { + this.tabs = this.tabs.slice(0, tabIndex + 1); + } + + if (option.children) { + const nextTab = { + options: option.children, + selectedOption: null, + }; + + if (this.tabs[tabIndex + 1]) { + this.$set(this.tabs, tabIndex + 1, nextTab); + } else { + this.tabs.push(nextTab); + } + + this.$nextTick(() => { + this.activeTab++; + }); + } + + const selectedOptions = this.tabs + .map((tab) => tab.selectedOption) + .filter((item) => !!item); + + const eventParams = { + value: option.value, + tabIndex, + selectedOptions, + }; + this.$emit('update:modelValue', option.value); + this.$emit('change', eventParams); + + if (!option.children) { + this.$emit('finish', eventParams); + } + }, + + onClose() { + this.$emit('close'); + }, + + renderHeader() { + return ( +
+

+ {this.$slots.title ? this.$slots.title() : this.title} +

+ {this.closeable ? ( + + ) : null} +
+ ); + }, + + renderOptions(options, selectedOption, tabIndex) { + const renderOption = (option) => { + const isSelected = + selectedOption && option.value === selectedOption.value; + + return ( +
  • { + this.onSelect(option, tabIndex); + }} + > + {option.text} + {isSelected ? ( + + ) : null} +
  • + ); + }; + + return
      {options.map(renderOption)}
    ; + }, + + renderTab(item, tabIndex) { + const { options, selectedOption } = item; + const title = selectedOption + ? selectedOption.text + : this.placeholder || t('select'); + + return ( + + {this.renderOptions(options, selectedOption, tabIndex)} + + ); + }, + + renderTabs() { + return ( + + {this.tabs.map(this.renderTab)} + + ); + }, + }, + + render() { + return ( +
    + {this.renderHeader()} + {this.renderTabs()} +
    + ); + }, +}); diff --git a/src/cascader/index.less b/src/cascader/index.less new file mode 100644 index 000000000..cc43b2750 --- /dev/null +++ b/src/cascader/index.less @@ -0,0 +1,76 @@ +@import '../style/var'; + +.van-cascader { + &__header { + display: flex; + align-items: center; + justify-content: space-between; + height: @cascader-header-height; + padding: 0 @padding-md; + } + + &__title { + font-weight: @font-weight-bold; + font-size: @cascader-title-font-size; + line-height: @cascader-title-line-height; + } + + &__close-icon { + color: @cascader-close-icon-color; + font-size: @cascader-close-icon-color; + + &:active { + color: @cascader-close-icon-active-color; + } + } + + &__tabs { + .van-tab { + flex: none; + padding: 0 10px; + } + + &.van-tabs--line .van-tabs__wrap { + height: @cascader-tabs-height; + padding: 0 6px; + } + } + + &__tab { + color: @cascader-tab-color; + font-weight: @font-weight-bold; + + &--unselected { + color: @cascader-unselected-tab-color; + } + } + + &__option { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px @padding-md; + font-size: @font-size-md; + line-height: @line-height-md; + + &:active { + background-color: @active-color; + } + + &--selected { + color: @cascader-active-color; + } + } + + &__selected-icon { + font-size: @cascader-selected-icon-size; + } + + &__options { + box-sizing: border-box; + height: @cascader-options-height; + padding-top: 6px; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + } +} diff --git a/src/cascader/test/__snapshots__/demo.spec.js.snap b/src/cascader/test/__snapshots__/demo.spec.js.snap new file mode 100644 index 000000000..e0cce98f3 --- /dev/null +++ b/src/cascader/test/__snapshots__/demo.spec.js.snap @@ -0,0 +1,85 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render demo and match snapshot 1`] = ` +
    +
    +
    + + Area + +
    +
    +
    + +
    +
    + + +
    + + + + +
    +
    +
    +
    + + Area + +
    +
    +
    + +
    +
    + + +
    + + + + +
    +
    +
    +
    + + Area + +
    +
    +
    + +
    +
    + + +
    + + + + +
    +`; diff --git a/src/cascader/test/demo.spec.js b/src/cascader/test/demo.spec.js new file mode 100644 index 000000000..5c70922b5 --- /dev/null +++ b/src/cascader/test/demo.spec.js @@ -0,0 +1,4 @@ +import Demo from '../demo'; +import { snapshotDemo } from '../../../test/demo'; + +snapshotDemo(Demo); diff --git a/src/cascader/test/index.legacy.js b/src/cascader/test/index.legacy.js new file mode 100644 index 000000000..25b87094e --- /dev/null +++ b/src/cascader/test/index.legacy.js @@ -0,0 +1,124 @@ +import Cascader from '..'; +import { mount, later } from '../../../test'; +import options from '../demo/area-en-US'; + +test('should emit change event when active option changed', async () => { + const wrapper = mount(Cascader, { + propsData: { + options, + }, + }); + + await later(); + wrapper.find('.van-cascader__option').trigger('click'); + + const firstOption = options[0]; + expect(wrapper.emitted('change')[0]).toEqual([ + { + value: firstOption.value, + tabIndex: 0, + selectedOptions: [firstOption], + }, + ]); + + await later(); + wrapper + .findAll('.van-cascader__options') + .at(1) + .find('.van-cascader__option') + .trigger('click'); + const secondOption = options[0].children[0]; + expect(wrapper.emitted('change')[1]).toEqual([ + { + value: secondOption.value, + tabIndex: 1, + selectedOptions: [firstOption, secondOption], + }, + ]); +}); + +test('should emit finish event when all options is selected', async () => { + const option = { value: '1', text: 'foo' }; + const wrapper = mount(Cascader, { + propsData: { + options: [option], + }, + }); + + await later(); + wrapper.find('.van-cascader__option').trigger('click'); + expect(wrapper.emitted('finish')[0]).toEqual([ + { + value: option.value, + tabIndex: 0, + selectedOptions: [option], + }, + ]); +}); + +test('should emit close event when close icon is clicked', () => { + const wrapper = mount(Cascader); + wrapper.find('.van-cascader__close-icon').trigger('click'); + expect(wrapper.emitted('close')[0]).toBeTruthy(); +}); + +test('should not render close icon when closeable is false', () => { + const wrapper = mount(Cascader, { + propsData: { + closeable: false, + }, + }); + expect(wrapper.contains('.van-cascader__close-icon')).toBeFalsy(); +}); + +test('should render title slot correctly', () => { + const wrapper = mount(Cascader, { + scopedSlots: { + title: () => 'Custom Title', + }, + }); + expect(wrapper.find('.van-cascader__title').html()).toMatchSnapshot(); +}); + +test('should select correct option when value changed', async () => { + const wrapper = mount(Cascader, { + propsData: { + options, + }, + }); + + await later(); + wrapper.setProps({ value: '330304' }); + await later(); + const selectedOptions = wrapper.findAll('.van-cascader__option--selected'); + const lastSelectedOption = selectedOptions.at(selectedOptions.length - 1); + expect(lastSelectedOption).toMatchSnapshot(); +}); + +test('should reset selected options when value is set to emtpy', async () => { + const wrapper = mount(Cascader, { + propsData: { + value: '330304', + options, + }, + }); + + await later(); + wrapper.setProps({ value: '' }); + await later(); + expect(wrapper.contains('.van-cascader__option--selected')).toBeFalsy(); +}); + +test('should update tabs when previous tab is clicked', async () => { + const wrapper = mount(Cascader, { + propsData: { + value: '330304', + options, + }, + }); + + await later(); + wrapper.findAll('.van-cascader__option').at(1).trigger('click'); + await later(); + expect(wrapper.html()).toMatchSnapshot(); +}); diff --git a/src/locale/lang/de-DE-formal.ts b/src/locale/lang/de-DE-formal.ts index 38d1d1c25..5ea45622e 100644 --- a/src/locale/lang/de-DE-formal.ts +++ b/src/locale/lang/de-DE-formal.ts @@ -21,6 +21,9 @@ export default { monthTitle: (year: number, month: number) => `${year}/${month}`, rangePrompt: (maxRange: number) => `Wähle nicht mehr als ${maxRange} Tage`, }, + vanCascader: { + select: 'Wählen', + }, vanContactCard: { addText: 'Kontaktinformationen hinzufügen', }, diff --git a/src/locale/lang/de-DE.ts b/src/locale/lang/de-DE.ts index 2acd455ae..137ce3b87 100644 --- a/src/locale/lang/de-DE.ts +++ b/src/locale/lang/de-DE.ts @@ -21,6 +21,9 @@ export default { monthTitle: (year: number, month: number) => `${year}/${month}`, rangePrompt: (maxRange: number) => `Wähle nicht mehr als ${maxRange} Tage`, }, + vanCascader: { + select: 'Wählen', + }, vanContactCard: { addText: 'Kontaktinformationen hinzufügen', }, diff --git a/src/locale/lang/en-US.ts b/src/locale/lang/en-US.ts index 99268eae0..51555ce8d 100644 --- a/src/locale/lang/en-US.ts +++ b/src/locale/lang/en-US.ts @@ -21,6 +21,9 @@ export default { monthTitle: (year: number, month: number) => `${year}/${month}`, rangePrompt: (maxRange: number) => `Choose no more than ${maxRange} days`, }, + vanCascader: { + select: 'Select', + }, vanContactCard: { addText: 'Add contact info', }, diff --git a/src/locale/lang/es-ES.ts b/src/locale/lang/es-ES.ts index 85dc38d7f..262e1c03b 100644 --- a/src/locale/lang/es-ES.ts +++ b/src/locale/lang/es-ES.ts @@ -21,6 +21,9 @@ export default { monthTitle: (year: number, month: number) => `${year}/${month}`, rangePrompt: (maxRange: number) => `Elija no más de ${maxRange} días`, }, + vanCascader: { + select: 'Seleccione', + }, vanContactCard: { addText: 'Añadir información de contacto', }, diff --git a/src/locale/lang/ja-JP.ts b/src/locale/lang/ja-JP.ts index 83e2b9497..d37c26632 100644 --- a/src/locale/lang/ja-JP.ts +++ b/src/locale/lang/ja-JP.ts @@ -22,6 +22,9 @@ export default { monthTitle: (year: number, month: number) => `${year}年${month}月`, rangePrompt: (maxRange: number) => `${maxRange}日以内を選択してください`, }, + vanCascader: { + select: '選択する', + }, vanContactCard: { addText: '連絡先を追加', }, diff --git a/src/locale/lang/nb-NO.ts b/src/locale/lang/nb-NO.ts index 35df25da2..21d808cdf 100644 --- a/src/locale/lang/nb-NO.ts +++ b/src/locale/lang/nb-NO.ts @@ -21,6 +21,9 @@ export default { monthTitle: (year: number, month: number) => `${year}/${month}`, rangePrompt: (maxRange: number) => `Maks. ${maxRange} dager`, }, + vanCascader: { + select: 'Plukke ut', + }, vanContactCard: { addText: 'Legg til kontakt info', }, diff --git a/src/locale/lang/ro-RO.ts b/src/locale/lang/ro-RO.ts index 95ec534db..562705967 100644 --- a/src/locale/lang/ro-RO.ts +++ b/src/locale/lang/ro-RO.ts @@ -21,6 +21,9 @@ export default { monthTitle: (year: number, month: number) => `${year}/${month}`, rangePrompt: (maxRange: number) => `Alege maxim ${maxRange} zile`, }, + vanCascader: { + select: 'Selectați', + }, vanContactCard: { addText: 'Adaugă informațiile de contact', }, diff --git a/src/locale/lang/tr-TR.ts b/src/locale/lang/tr-TR.ts index 76218b8fa..96e6b005f 100644 --- a/src/locale/lang/tr-TR.ts +++ b/src/locale/lang/tr-TR.ts @@ -21,6 +21,9 @@ export default { monthTitle: (year: number, month: number) => `${year}/${month}`, rangePrompt: (maxRange: number) => `En fazla ${maxRange} gün seçin`, }, + vanCascader: { + select: 'Seçiniz', + }, vanContactCard: { addText: 'Kişi bilgisi ekle', }, diff --git a/src/locale/lang/zh-CN.ts b/src/locale/lang/zh-CN.ts index c9010ff82..35692b09e 100644 --- a/src/locale/lang/zh-CN.ts +++ b/src/locale/lang/zh-CN.ts @@ -22,6 +22,9 @@ export default { monthTitle: (year: number, month: number) => `${year}年${month}月`, rangePrompt: (maxRange: number) => `选择天数不能超过 ${maxRange} 天`, }, + vanCascader: { + select: '请选择', + }, vanContactCard: { addText: '添加联系人', }, diff --git a/src/locale/lang/zh-HK.ts b/src/locale/lang/zh-HK.ts index 3d567b08f..5f2ab68e7 100644 --- a/src/locale/lang/zh-HK.ts +++ b/src/locale/lang/zh-HK.ts @@ -22,6 +22,9 @@ export default { monthTitle: (year: number, month: number) => `${year}年${month}月`, rangePrompt: (maxRange: number) => `選擇天數不能超過 ${maxRange} 天`, }, + vanCascader: { + select: '請選擇', + }, vanContactCard: { addText: '添加聯系人', }, diff --git a/src/locale/lang/zh-TW.ts b/src/locale/lang/zh-TW.ts index aecda17ec..7d20dcc66 100644 --- a/src/locale/lang/zh-TW.ts +++ b/src/locale/lang/zh-TW.ts @@ -22,6 +22,9 @@ export default { monthTitle: (year: number, month: number) => `${year}年${month}月`, rangePrompt: (maxRange: number) => `選擇天數不能超過 ${maxRange} 天`, }, + vanCascader: { + select: '請選擇', + }, vanContactCard: { addText: '新增聯絡人', }, diff --git a/src/style/var.less b/src/style/var.less index e3b56f13e..928dbaf1e 100644 --- a/src/style/var.less +++ b/src/style/var.less @@ -201,6 +201,20 @@ @card-price-integer-font-size: @font-size-lg; @card-price-font-family: @price-integer-font-family; +// Cascader +@cascader-header-height: 48px; +@cascader-title-font-size: @font-size-lg; +@cascader-title-line-height: 20px; +@cascader-close-icon-size: 22px; +@cascader-close-icon-color: @gray-5; +@cascader-close-icon-active-color: @gray-6; +@cascader-selected-icon-size: 18px; +@cascader-tabs-height: 48px; +@cascader-active-color: @red; +@cascader-options-height: 384px; +@cascader-tab-color: @text-color; +@cascader-unselected-tab-color: @gray-6; + // Cell @cell-font-size: @font-size-md; @cell-line-height: 24px; diff --git a/src/tab/README.md b/src/tab/README.md index 2423818b0..be8281dcf 100644 --- a/src/tab/README.md +++ b/src/tab/README.md @@ -267,6 +267,7 @@ export default { | to | Target route of the link, same as to of vue-router | _string \| object_ | - | | replace | If true, the navigation will not leave a history record | _boolean_ | `false` | | title-style | Custom title style | _any_ | - | +| title-class | Custom title class name | _any_ | - | ### Tabs Events diff --git a/src/tab/README.zh-CN.md b/src/tab/README.zh-CN.md index c193ee021..60c41d265 100644 --- a/src/tab/README.zh-CN.md +++ b/src/tab/README.zh-CN.md @@ -274,6 +274,7 @@ export default { | to | 点击后跳转的目标路由对象,同 vue-router 的 [to 属性](https://router.vuejs.org/zh/api/#to) | _string \| object_ | - | | replace | 是否在跳转时替换当前页面历史 | _boolean_ | `false` | | title-style | 自定义标题样式 | _any_ | - | +| title-class | 自定义标题类名 | _any_ | - | ### Tabs Events diff --git a/src/tab/index.js b/src/tab/index.js index 03bff277a..ac2a79955 100644 --- a/src/tab/index.js +++ b/src/tab/index.js @@ -19,6 +19,7 @@ export default createComponent({ badge: [Number, String], title: String, titleStyle: null, + titleClass: null, disabled: Boolean, }, @@ -56,6 +57,7 @@ export default createComponent({ () => props.title, () => { parent.setLine(); + parent.scrollIntoView(); } ); diff --git a/src/tabs/index.js b/src/tabs/index.js index 343bdf820..a78c700f4 100644 --- a/src/tabs/index.js +++ b/src/tabs/index.js @@ -317,6 +317,7 @@ export default createComponent({ title={item.title} color={props.color} style={item.titleStyle} + class={item.titleClass} isActive={index === state.currentIndex} disabled={item.disabled} scrollable={scrollable.value} @@ -418,6 +419,7 @@ export default createComponent({ props, setLine, currentName, + scrollIntoView, }); return () => ( diff --git a/types/index.d.ts b/types/index.d.ts index f258b1306..ced16e008 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -37,6 +37,7 @@ export class AddressList extends VanComponent {} export class Badge extends VanComponent {} export class Button extends VanComponent {} export class Card extends VanComponent {} +export class Cascader extends VanComponent {} export class Cell extends VanComponent {} export class CellGroup extends VanComponent {} export class Circle extends VanComponent {} diff --git a/vant.config.js b/vant.config.js index 881d4fcde..3d7babc4a 100644 --- a/vant.config.js +++ b/vant.config.js @@ -149,6 +149,10 @@ module.exports = { path: 'calendar', title: 'Calendar 日历', }, + { + path: 'cascader', + title: 'Cascader 级联选择', + }, { path: 'checkbox', title: 'Checkbox 复选框', @@ -498,6 +502,10 @@ module.exports = { path: 'calendar', title: 'Calendar', }, + { + path: 'cascader', + title: 'Cascader', + }, { path: 'checkbox', title: 'Checkbox',