diff --git a/example/app.json b/example/app.json
index 9063ba8c..05cc8b35 100644
--- a/example/app.json
+++ b/example/app.json
@@ -52,7 +52,8 @@
"pages/empty/index",
"pages/calendar/index",
"pages/share-sheet/index",
- "pages/config-provider/index"
+ "pages/config-provider/index",
+ "pages/cascader/index"
],
"window": {
"navigationBarBackgroundColor": "#f8f8f8",
@@ -114,7 +115,8 @@
"van-skeleton-demo": "./dist/skeleton/demo/index",
"van-calendar-demo": "./dist/calendar/demo/index",
"van-share-sheet-demo": "./dist/share-sheet/demo/index",
- "van-config-provider-demo": "./dist/config-provider/demo/index"
+ "van-config-provider-demo": "./dist/config-provider/demo/index",
+ "van-cascader-demo": "./dist/cascader/demo/index"
},
"sitemapLocation": "sitemap.json"
}
diff --git a/example/pages/cascader/index.js b/example/pages/cascader/index.js
new file mode 100644
index 00000000..cc11dfda
--- /dev/null
+++ b/example/pages/cascader/index.js
@@ -0,0 +1,3 @@
+import Page from '../../common/page';
+
+Page();
diff --git a/example/pages/cascader/index.json b/example/pages/cascader/index.json
new file mode 100644
index 00000000..176623c0
--- /dev/null
+++ b/example/pages/cascader/index.json
@@ -0,0 +1,3 @@
+{
+ "navigationBarTitleText": "Cascader 级联选择"
+}
\ No newline at end of file
diff --git a/example/pages/cascader/index.wxml b/example/pages/cascader/index.wxml
new file mode 100644
index 00000000..62614e77
--- /dev/null
+++ b/example/pages/cascader/index.wxml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/example/project.config.json b/example/project.config.json
index 2f445ee9..78d0df80 100644
--- a/example/project.config.json
+++ b/example/project.config.json
@@ -37,7 +37,8 @@
"packNpmManually": false,
"packNpmRelationList": [],
"minifyWXSS": true,
- "showES6CompileOption": false
+ "showES6CompileOption": false,
+ "ignoreUploadUnusedFiles": true
},
"compileType": "miniprogram",
"libVersion": "2.6.5",
diff --git a/packages/cascader/README.md b/packages/cascader/README.md
new file mode 100644
index 00000000..b0823bc1
--- /dev/null
+++ b/packages/cascader/README.md
@@ -0,0 +1,243 @@
+# Cascader 级联选择
+
+### 介绍
+
+级联选择框,用于多层级数据的选择,典型场景为省市区选择。
+
+### 引入
+
+在`app.json`或`index.json`中引入组件,详细介绍见[快速上手](#/quickstart#yin-ru-zu-jian)。
+
+```json
+"usingComponents": {
+ "van-cascader": "@vant/weapp/cascader/index"
+}
+```
+
+## 代码演示
+
+### 基础用法
+
+级联选择组件可以搭配 Field 和 Popup 组件使用,示例如下:
+
+```html
+
+
+
+
+```
+
+```js
+
+const options = [
+ {
+ text: '浙江省',
+ value: '330000',
+ children: [{ text: '杭州市', value: '330100' }],
+ },
+ {
+ text: '江苏省',
+ value: '320000',
+ children: [{ text: '南京市', value: '320100' }],
+ },
+];
+
+Page({
+ data: {
+ show: false,
+ options,
+ fieldValue: '',
+ cascaderValue: '',
+ },
+
+ onClick() {
+ this.setData({
+ show: true,
+ });
+ },
+
+ onClose() {
+ this.setData({
+ show: false,
+ });
+ },
+
+ onFinish(e) {
+ const { selectedOptions, value } = e.detail;
+ const fieldValue = selectedOptions
+ .map((option) => option.text || option.name)
+ .join('/');
+ this.setData({
+ fieldValue,
+ cascaderValue: value,
+ })
+ },
+});
+```
+
+### 自定义颜色
+
+通过 `active-color` 属性来设置选中状态的高亮颜色。
+
+```html
+
+```
+
+### 异步加载选项
+
+可以监听 `change` 事件并动态设置 `options`,实现异步加载选项。
+
+```html
+
+
+
+
+```
+
+```js
+Page({
+ data: {
+ options: [
+ {
+ text: '浙江省',
+ value: '330000',
+ children: [],
+ }
+ ];
+ },
+ onChange(e) {
+ const { value } = e.detail;
+ if (value === this.data.options[0].value) {
+ setTimeout(() => {
+ const children = [
+ { text: '杭州市', value: '330100' },
+ { text: '宁波市', value: '330200' },
+ ];
+ this.setData({
+ 'options[0].children': children,
+ })
+ }, 500);
+ }
+ },
+});
+
+```
+
+### 自定义字段名
+
+通过 `field-names` 属性可以自定义 `options` 里的字段名称。
+
+```html
+
+```
+
+```js
+Page({
+ data: {
+ code: '',
+ fieldNames: {
+ text: 'name',
+ value: 'code',
+ children: 'items',
+ },
+ options: [
+ {
+ name: '浙江省',
+ code: '330000',
+ items: [{ name: '杭州市', code: '330100' }],
+ },
+ {
+ name: '江苏省',
+ code: '320000',
+ items: [{ name: '南京市', code: '320100' }],
+ },
+ ],
+ },
+});
+```
+
+## API
+
+### Props
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| title | 顶部标题 | _string_ | - |
+| value | 选中项的值 | _string \| number_ | - |
+| options | 可选项数据源 | _CascaderOption[]_ | `[]` |
+| placeholder | 未选中时的提示文案 | _string_ | `请选择` |
+| active-color | 选中状态的高亮颜色 | _string_ | `#1989fa` |
+| swipeable | 是否开启手势左右滑动切换 | _boolean_ | `false` |
+| closeable | 是否显示关闭图标 | _boolean_ | `true` |
+| show-header | 是否展示标题栏 | _boolean_ | `true` |
+| close-icon | 关闭图标名称或图片链接,等同于 Icon 组件的 [name 属性](#/icon) | _string_ | `cross` |
+| field-names | 自定义 `options` 结构中的字段 | _CascaderFieldNames_ | `{ text: 'text', value: 'value', children: 'children' }` |
+
+### CascaderOption 数据结构
+
+`options` 属性是一个由对象构成的数组,数组中的每个对象配置一个可选项,对象可以包含以下值:
+
+| 键名 | 说明 | 类型 |
+| ------------------ | ------------------------ | --------------------------- |
+| text | 选项文字(必填) | _string_ |
+| value | 选项对应的值(必填) | _string \| number_ |
+| color | 选项文字颜色 | _string_ |
+| children | 子选项列表 | _CascaderOption[]_ |
+| disabled | 是否禁用选项 | _boolean_ |
+| className | 为对应列添加额外的 class | _string \| Array \| object_ |
+
+### Events
+
+| 事件 | 说明 | 回调参数 |
+| --- | --- | --- |
+| bind:change | 选中项变化时触发 | event.detail:_{ value: string \| number, selectedOptions: CascaderOption[], tabIndex: number }_ |
+| bind:finish | 全部选项选择完成后触发 | event.detail:_{ value: string \| number, selectedOptions: CascaderOption[], tabIndex: number }_ |
+| bind:close | 点击关闭图标时触发 | - |
+| bind:click-tab | 点击标签时触发 | event.detail:_{ tabIndex: number, title: string }_ |
+
+### Slots
+
+| 名称 | 说明 | 参数 |
+| --- | --- | --- |
+| title | 自定义顶部标题 | - |
diff --git a/packages/cascader/demo/index.json b/packages/cascader/demo/index.json
new file mode 100644
index 00000000..dbbdd2e7
--- /dev/null
+++ b/packages/cascader/demo/index.json
@@ -0,0 +1,9 @@
+{
+ "navigationBarTitleText": "Cell 单元格",
+ "usingComponents": {
+ "van-field": "../../field/index",
+ "van-popup": "../../popup/index",
+ "van-cascader": "../../cascader/index",
+ "demo-block": "../../../example/components/demo-block/index"
+ }
+}
diff --git a/packages/cascader/demo/index.ts b/packages/cascader/demo/index.ts
new file mode 100644
index 00000000..f82cfe2b
--- /dev/null
+++ b/packages/cascader/demo/index.ts
@@ -0,0 +1,226 @@
+import { VantComponent } from '../../common/component';
+import { deepClone } from '../../common/utils';
+
+const zhCNOptions = [
+ {
+ 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',
+ },
+ ],
+ },
+ ],
+ },
+];
+
+const asyncOptions1 = [
+ {
+ text: '浙江省',
+ value: '330000',
+ children: [],
+ },
+];
+const asyncOptions2 = [
+ { text: '杭州市', value: '330100' },
+ { text: '宁波市', value: '330200' },
+];
+
+function getCustomFieldOptions(options) {
+ const newOptions = deepClone(options);
+ const adjustFieldName = (item) => {
+ if ('text' in item) {
+ item.name = item.text;
+ delete item.text;
+ }
+ if ('value' in item) {
+ item.code = item.value;
+ delete item.value;
+ }
+ if ('children' in item) {
+ item.items = item.children;
+ delete item.children;
+ item.items.forEach(adjustFieldName);
+ }
+ };
+ newOptions.forEach(adjustFieldName);
+ return newOptions;
+}
+
+VantComponent({
+ data: {
+ area: '地区',
+ options: zhCNOptions,
+ selectArea: '请选择地区',
+ baseState: {
+ show: false,
+ value: '',
+ result: '',
+ },
+ asyncState: {
+ show: false,
+ value: undefined,
+ result: '',
+ options: asyncOptions1,
+ },
+ fieldNames: {
+ text: 'name',
+ value: 'code',
+ children: 'items',
+ },
+ customFieldOptions: getCustomFieldOptions(zhCNOptions),
+ customFieldState: {
+ show: false,
+ value: '',
+ result: '',
+ },
+ },
+
+ methods: {
+ onClick(e) {
+ const { stateKey } = e.currentTarget.dataset;
+ this.setData({
+ [`${stateKey}.show`]: true,
+ });
+ },
+ onClose(e) {
+ const { stateKey } = e.currentTarget.dataset;
+ this.setData({
+ [`${stateKey}.show`]: false,
+ });
+ },
+ onFinish(e) {
+ const { stateKey } = e.currentTarget.dataset;
+ const { selectedOptions, value } = e.detail;
+ const result = selectedOptions
+ .map((option) => option.text || option.name)
+ .join('/');
+
+ this.setData({
+ [`${stateKey}.value`]: value,
+ [`${stateKey}.result`]: result,
+ });
+ this.onClose(e);
+ },
+ loadDynamicOptions(e) {
+ const { value } = e.detail;
+ if (value === '330000') {
+ setTimeout(() => {
+ this.setData({
+ 'asyncState.options[0].children': asyncOptions2,
+ });
+ }, 500);
+ }
+ },
+ },
+});
diff --git a/packages/cascader/demo/index.wxml b/packages/cascader/demo/index.wxml
new file mode 100644
index 00000000..7908be37
--- /dev/null
+++ b/packages/cascader/demo/index.wxml
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/cascader/index.json b/packages/cascader/index.json
new file mode 100644
index 00000000..d0f75eb7
--- /dev/null
+++ b/packages/cascader/index.json
@@ -0,0 +1,8 @@
+{
+ "component": true,
+ "usingComponents": {
+ "van-icon": "../icon/index",
+ "van-tab": "../tab/index",
+ "van-tabs": "../tabs/index"
+ }
+}
\ No newline at end of file
diff --git a/packages/cascader/index.less b/packages/cascader/index.less
new file mode 100644
index 00000000..eeabef83
--- /dev/null
+++ b/packages/cascader/index.less
@@ -0,0 +1,82 @@
+@import '../common/style/var.less';
+
+.van-cascader {
+ &__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: @cascader-header-height;
+ padding: @cascader-header-padding;
+ }
+
+ &__title {
+ font-weight: 600;
+ font-size: @cascader-title-font-size;
+ line-height: @cascader-title-line-height;
+ }
+
+ &__close-icon {
+ color: @cascader-close-icon-color;
+ font-size: @cascader-close-icon-size;
+ height: 22px;
+ }
+
+ &__tabs {
+ &-wrap {
+ padding: 0 8px;
+ height: @cascader-tabs-height !important;
+ }
+ }
+
+ &__tab {
+ flex: none !important;
+ padding: 0 8px !important;
+ color: @cascader-tab-color !important;
+ font-weight: 600 !important;
+
+ &--unselected {
+ color: @cascader-unselected-tab-color !important;
+ font-weight: normal !important;
+ }
+ }
+
+ &__option {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px 16px;
+ font-size: 14px;
+ line-height: 20px;
+ cursor: pointer;
+
+ &:active {
+ background-color: #f2f3f5;
+ }
+
+ &--selected {
+ color: @cascader-active-color;
+ font-weight: 600;
+ }
+
+ &--disabled {
+ color: @cascader-option-disabled-color;
+ cursor: not-allowed;
+
+ &:active {
+ background-color: transparent;
+ }
+ }
+ }
+
+ &__selected-icon {
+ font-size: @cascader-selected-icon-size !important;
+ }
+
+ &__options {
+ box-sizing: border-box;
+ height: @cascader-options-height;
+ padding-top: 6px;
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+}
diff --git a/packages/cascader/index.ts b/packages/cascader/index.ts
new file mode 100644
index 00000000..b62b9532
--- /dev/null
+++ b/packages/cascader/index.ts
@@ -0,0 +1,252 @@
+import { VantComponent } from '../common/component';
+
+enum FieldName {
+ TEXT = 'text',
+ VALUE = 'value',
+ CHILDREN = 'children',
+}
+
+type Option = Record;
+
+interface ITab {
+ options: Option[];
+ selected: Option | null;
+}
+
+const defaultFieldNames = {
+ text: FieldName.TEXT,
+ value: FieldName.VALUE,
+ children: FieldName.CHILDREN,
+};
+
+VantComponent({
+ props: {
+ title: String,
+ value: {
+ type: String,
+ observer: 'updateValue',
+ },
+ placeholder: {
+ type: String,
+ value: '请选择',
+ },
+ activeColor: {
+ type: String,
+ value: '#1989fa',
+ },
+ options: {
+ type: Array,
+ value: [],
+ observer: 'updateOptions',
+ },
+ swipeable: {
+ type: Boolean,
+ value: false,
+ },
+ closeable: {
+ type: Boolean,
+ value: true,
+ },
+ showHeader: {
+ type: Boolean,
+ value: true,
+ },
+ closeIcon: {
+ type: String,
+ value: 'cross',
+ },
+ fieldNames: {
+ type: Object,
+ value: defaultFieldNames,
+ observer: 'updateFieldNames',
+ },
+ },
+
+ data: {
+ tabs: [] as ITab[],
+ activeTab: 0,
+ textKey: FieldName.TEXT,
+ valueKey: FieldName.VALUE,
+ childrenKey: FieldName.CHILDREN,
+ },
+
+ created() {
+ this.updateTabs();
+ },
+
+ methods: {
+ updateOptions(val, oldVal) {
+ const isAsync = !!(val.length && oldVal.length);
+ this.updateTabs(isAsync);
+ },
+ updateValue(val) {
+ if (val !== undefined) {
+ const values = this.data.tabs.map(
+ (tab: ITab) => tab.selected && tab.selected[this.data.valueKey]
+ );
+ if (values.indexOf(val) > -1) {
+ return;
+ }
+ }
+ this.updateTabs();
+ },
+ updateFieldNames() {
+ const {
+ text = 'text',
+ value = 'value',
+ children = 'children',
+ } = this.data.fieldNames || defaultFieldNames;
+ this.setData({
+ textKey: text,
+ valueKey: value,
+ childrenKey: children,
+ });
+ },
+ getSelectedOptionsByValue(options, value) {
+ for (let i = 0; i < options.length; i++) {
+ const option = options[i];
+
+ if (option[this.data.valueKey] === value) {
+ return [option];
+ }
+
+ if (option[this.data.childrenKey]) {
+ const selectedOptions = this.getSelectedOptionsByValue(
+ option[this.data.childrenKey],
+ value
+ );
+ if (selectedOptions) {
+ return [option, ...selectedOptions];
+ }
+ }
+ }
+ },
+ updateTabs(isAsync = false) {
+ const { options, value } = this.data;
+
+ if (value !== undefined) {
+ const selectedOptions = this.getSelectedOptionsByValue(options, value);
+
+ if (selectedOptions) {
+ let optionsCursor = options;
+
+ const tabs = selectedOptions.map((option) => {
+ const tab = {
+ options: optionsCursor,
+ selected: option,
+ };
+
+ const next = optionsCursor.find(
+ (item) => item[this.data.valueKey] === option[this.data.valueKey]
+ );
+ if (next) {
+ optionsCursor = next[this.data.childrenKey];
+ }
+
+ return tab;
+ });
+
+ if (optionsCursor) {
+ tabs.push({
+ options: optionsCursor,
+ selected: null,
+ });
+ }
+
+ this.setData({
+ tabs,
+ });
+
+ wx.nextTick(() => {
+ this.setData({
+ activeTab: tabs.length - 1,
+ });
+ });
+
+ return;
+ }
+ }
+
+ // 异步更新
+ if (isAsync) {
+ const { tabs } = this.data;
+ tabs[tabs.length - 1].options =
+ options[options.length - 1][this.data.childrenKey];
+ this.setData({
+ tabs,
+ });
+ return;
+ }
+
+ this.setData({
+ tabs: [
+ {
+ options,
+ selected: null,
+ },
+ ],
+ });
+ },
+ onClose() {
+ this.$emit('close');
+ },
+ onClickTab(e) {
+ const { index: tabIndex, title } = e.detail;
+ this.$emit('click-tab', { title, tabIndex });
+ },
+ // 选中
+ onSelect(e) {
+ const { option, tabIndex } = e.currentTarget.dataset;
+
+ if (option && option.disabled) {
+ return;
+ }
+
+ const { valueKey, childrenKey } = this.data;
+ let { tabs } = this.data;
+
+ tabs[tabIndex].selected = option;
+
+ if (tabs.length > tabIndex + 1) {
+ tabs = tabs.slice(0, tabIndex + 1);
+ }
+
+ if (option[childrenKey]) {
+ const nextTab = {
+ options: option[childrenKey],
+ selected: null,
+ };
+
+ if (tabs[tabIndex + 1]) {
+ tabs[tabIndex + 1] = nextTab;
+ } else {
+ tabs.push(nextTab);
+ }
+
+ wx.nextTick(() => {
+ this.setData({
+ activeTab: tabIndex + 1,
+ });
+ });
+ }
+
+ this.setData({
+ tabs,
+ });
+
+ const selectedOptions = tabs.map((tab) => tab.selected).filter(Boolean);
+
+ const params = {
+ value: option[valueKey],
+ tabIndex,
+ selectedOptions,
+ };
+
+ this.$emit('change', params);
+
+ if (!option[childrenKey]) {
+ this.$emit('finish', params);
+ }
+ },
+ },
+});
diff --git a/packages/cascader/index.wxml b/packages/cascader/index.wxml
new file mode 100644
index 00000000..45c1c42d
--- /dev/null
+++ b/packages/cascader/index.wxml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ option[textKey] }}
+
+
+
+
+
+
+
diff --git a/packages/cascader/index.wxs b/packages/cascader/index.wxs
new file mode 100644
index 00000000..cba6465c
--- /dev/null
+++ b/packages/cascader/index.wxs
@@ -0,0 +1,24 @@
+var utils = require('../wxs/utils.wxs');
+var style = require('../wxs/style.wxs');
+
+function isSelected(tab, textKey, option) {
+ return tab.selected && tab.selected[textKey] === option[textKey]
+}
+
+function optionClass(tab, textKey, option) {
+ return utils.bem('cascader__option', { selected: isSelected({ tab, textKey, option }), disabled: option.disabled })
+}
+
+function optionStyle(data) {
+ var color = data.option.color || (isSelected(data.tab, data.textKey, data.option) ? data.activeColor : undefined);
+ return style({
+ color
+ });
+}
+
+
+module.exports = {
+ isSelected: isSelected,
+ optionClass: optionClass,
+ optionStyle: optionStyle,
+};
\ No newline at end of file
diff --git a/packages/cascader/test/__snapshots__/demo.spec.ts.snap b/packages/cascader/test/__snapshots__/demo.spec.ts.snap
new file mode 100644
index 00000000..26e87ccf
--- /dev/null
+++ b/packages/cascader/test/__snapshots__/demo.spec.ts.snap
@@ -0,0 +1,439 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render demo and match snapshot 1`] = `
+
+
+
+
+ 基础用法
+
+
+
+
+
+
+ 地区
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 自定义颜色
+
+
+
+
+
+
+
+ 地区
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 异步加载选项
+
+
+
+
+
+
+
+ 地区
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 自定义字段名
+
+
+
+
+
+
+
+ 地区
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/packages/cascader/test/demo.spec.ts b/packages/cascader/test/demo.spec.ts
new file mode 100644
index 00000000..4c3798cb
--- /dev/null
+++ b/packages/cascader/test/demo.spec.ts
@@ -0,0 +1,11 @@
+import path from 'path';
+import simulate from 'miniprogram-simulate';
+
+test('should render demo and match snapshot', () => {
+ const id = simulate.load(path.resolve(__dirname, '../demo/index'), {
+ rootPath: path.resolve(__dirname, '../../'),
+ });
+ const comp = simulate.render(id);
+ comp.attach(document.createElement('parent-wrapper'));
+ expect(comp.toJSON()).toMatchSnapshot();
+});
diff --git a/packages/common/style/var.less b/packages/common/style/var.less
index 9cdd57c2..9ac06e0d 100644
--- a/packages/common/style/var.less
+++ b/packages/common/style/var.less
@@ -671,3 +671,18 @@
@skeleton-row-margin-top: @padding-sm;
@skeleton-avatar-background-color: @gray-2;
@skeleton-animation-duration: 1.2s;
+
+// Cascader
+@cascader-header-height: 48px;
+@cascader-header-padding: 0 16px;
+@cascader-title-font-size: 16px;
+@cascader-title-line-height: 20px;
+@cascader-close-icon-size: 22px;
+@cascader-close-icon-color: #c8c9cc;
+@cascader-selected-icon-size: 18px;
+@cascader-tabs-height: 48px;
+@cascader-active-color: @blue;
+@cascader-options-height: 384px;
+@cascader-option-disabled-color: #c8c9cc;
+@cascader-tab-color: #323233;
+@cascader-unselected-tab-color: #969799;
diff --git a/packages/common/utils.ts b/packages/common/utils.ts
index fe3f44e6..57d84dab 100644
--- a/packages/common/utils.ts
+++ b/packages/common/utils.ts
@@ -1,5 +1,9 @@
-import { isDef, isNumber, isPlainObject, isPromise } from './validator';
-import { canIUseGroupSetData, canIUseNextTick, getSystemInfoSync } from './version';
+import { isDef, isNumber, isPlainObject, isPromise, isObj } from './validator';
+import {
+ canIUseGroupSetData,
+ canIUseNextTick,
+ getSystemInfoSync,
+} from './version';
export { isDef } from './validator';
export { getSystemInfoSync } from './version';
@@ -112,3 +116,27 @@ export function getCurrentPage() {
const pages = getCurrentPages();
return pages[pages.length - 1] as T & WechatMiniprogram.Page.TrivialInstance;
}
+
+export function deepClone | null | undefined>(
+ obj: T
+): T {
+ if (!isDef(obj)) {
+ return obj;
+ }
+
+ if (Array.isArray(obj)) {
+ return obj.map((item) => deepClone(item)) as unknown as T;
+ }
+
+ if (isObj(obj)) {
+ const to = {} as Record;
+ Object.keys(obj).forEach((key: string) => {
+ // @ts-ignore
+ to[key] = deepClone(obj[key]);
+ });
+
+ return to as T;
+ }
+
+ return obj;
+}
diff --git a/packages/icon/test/__snapshots__/demo.spec.ts.snap b/packages/icon/test/__snapshots__/demo.spec.ts.snap
index a7ea542e..a1cf4c94 100644
--- a/packages/icon/test/__snapshots__/demo.spec.ts.snap
+++ b/packages/icon/test/__snapshots__/demo.spec.ts.snap
@@ -20,7 +20,7 @@ exports[`should render demo and match snapshot 1`] = `
style="z-index:1"
>
-
+