feat(cascader): 新增 cascader 组件 (#4992)

This commit is contained in:
yangjinfeng 2022-12-05 16:53:29 +08:00
parent 5b96c7acb0
commit d86d4a6a5f
22 changed files with 1536 additions and 18 deletions

View File

@ -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"
}

View File

@ -0,0 +1,3 @@
import Page from '../../common/page';
Page();

View File

@ -0,0 +1,3 @@
{
"navigationBarTitleText": "Cascader 级联选择"
}

View File

@ -0,0 +1 @@
<van-cascader-demo />

View File

@ -37,7 +37,8 @@
"packNpmManually": false,
"packNpmRelationList": [],
"minifyWXSS": true,
"showES6CompileOption": false
"showES6CompileOption": false,
"ignoreUploadUnusedFiles": true
},
"compileType": "miniprogram",
"libVersion": "2.6.5",

243
packages/cascader/README.md Normal file
View File

@ -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
<van-field
value="{{ fieldValue }}"
is-link
readonly
label="地区"
placeholder="请选择所在地区"
bind:tap="onClick"
/>
<van-popup show="{{ show }}" round position="bottom">
<van-cascader
wx:if="{{ show }}"
value="{{ cascaderValue }}"
title="请选择所在地区"
options="{{ options }}"
bind:close="onClose"
bind:finish="onFinish"
/>
</van-popup>
```
```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
<van-cascader
value="{{ cascaderValue }}"
title="请选择所在地区"
options="{{ options }}"
active-color="#ee0a24"
bind:close="onClose"
bind:finish="onFinish"
/>
```
### 异步加载选项
可以监听 `change` 事件并动态设置 `options`,实现异步加载选项。
```html
<van-field
value="{{ fieldValue }}"
is-link
readonly
label="地区"
placeholder="请选择所在地区"
bind:tap="onClick"
/>
<van-popup show="{{ show }}" round position="bottom">
<van-cascader
wx:if="{{ show }}"
value="{{ cascaderValue }}"
title="请选择所在地区"
options="{{ options }}"
bind:close="onClose"
bind:change="onChange"
bind:finish="onFinish"
/>
</van-popup>
```
```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
<van-cascader
value="{{ code }}"
title="请选择所在地区"
options="{{ options }}"
field-names="{{ fieldNames }}"
/>
```
```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 | 自定义顶部标题 | - |

View File

@ -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"
}
}

View File

@ -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);
}
},
},
});

View File

@ -0,0 +1,112 @@
<demo-block title="基础用法">
<van-field
value="{{ baseState.result }}"
is-link
readonly
label="地区"
placeholder="{{ selectArea }}"
data-state-key="baseState"
bind:tap="onClick"
/>
<van-popup
show="{{ baseState.show }}"
round
position="bottom"
>
<van-cascader
wx:if="{{ baseState.show }}"
value="{{ baseState.value }}"
title="{{ selectArea }}"
options="{{ options }}"
data-state-key="baseState"
bind:close="onClose"
bind:finish="onFinish"
/>
</van-popup>
</demo-block>
<demo-block title="自定义颜色">
<van-field
value="{{ customColorState.result }}"
is-link
readonly
label="地区"
placeholder="{{ selectArea }}"
data-state-key="customColorState"
bind:tap="onClick"
/>
<van-popup
show="{{ customColorState.show }}"
round
position="bottom"
>
<van-cascader
wx:if="{{ customColorState.show }}"
value="{{ customColorState.value }}"
title="{{ selectArea }}"
options="{{ options }}"
data-state-key="customColorState"
active-color="#ee0a24"
bind:close="onClose"
bind:finish="onFinish"
/>
</van-popup>
</demo-block>
<demo-block title="异步加载选项">
<van-field
value="{{ asyncState.result }}"
is-link
readonly
label="地区"
placeholder="{{ selectArea }}"
data-state-key="asyncState"
bind:tap="onClick"
/>
<van-popup
show="{{ asyncState.show }}"
round
position="bottom"
>
<van-cascader
wx:if="{{ asyncState.show }}"
value="{{ asyncState.value }}"
title="{{ selectArea }}"
options="{{ asyncState.options }}"
data-state-key="asyncState"
bind:close="onClose"
bind:change="loadDynamicOptions"
bind:finish="onFinish"
/>
</van-popup>
</demo-block>
<demo-block title="自定义字段名">
<van-field
value="{{ customFieldState.result }}"
is-link
readonly
label="地区"
placeholder="{{ selectArea }}"
data-state-key="customFieldState"
bind:tap="onClick"
/>
<van-popup
show="{{ customFieldState.show }}"
round
position="bottom"
safe-area-inset-bottom
>
<van-cascader
wx:if="{{ customFieldState.show }}"
data-state-key="customFieldState"
value="{{ customFieldState.value }}"
title="{{ selectArea }}"
options="{{ customFieldOptions }}"
field-names="{{ fieldNames }}"
data-state-key="customFieldState"
bind:close="onClose"
bind:finish="onFinish"
/>
</van-popup>
</demo-block>

View File

@ -0,0 +1,8 @@
{
"component": true,
"usingComponents": {
"van-icon": "../icon/index",
"van-tab": "../tab/index",
"van-tabs": "../tabs/index"
}
}

View File

@ -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;
}
}

252
packages/cascader/index.ts Normal file
View File

@ -0,0 +1,252 @@
import { VantComponent } from '../common/component';
enum FieldName {
TEXT = 'text',
VALUE = 'value',
CHILDREN = 'children',
}
type Option = Record<string, any>;
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);
}
},
},
});

View File

@ -0,0 +1,53 @@
<wxs src="./index.wxs" module="utils" />
<view wx:if="{{ showHeader }}" class="van-cascader__header">
<text class="van-cascader__title"><slot name="title"></slot>{{ title }}</text>
<van-icon
wx:if="{{ closeable }}"
name="{{ closeIcon }}"
class="van-cascader__close-icon"
bind:tap="onClose"
/>
</view>
<van-tabs
active="{{ activeTab }}"
custom-class="van-cascader__tabs"
wrap-class="van-cascader__tabs-wrap"
tab-class="van-cascader__tab"
color="{{ activeColor }}"
border="{{ false }}"
swipeable="{{ swipeable }}"
bind:click="onClickTab"
>
<van-tab
wx:for="{{ tabs }}"
wx:for-item="tab"
wx:for-index="tabIndex"
wx:key="tabIndex"
title="{{ tab.selected ? tab.selected[textKey] : placeholder }}"
style="width: 100%;"
title-style="{{ !tab.selected ? 'color: #969799;font-weight:normal;' : '' }}"
>
<!-- 暂不支持 -->
<!-- <slot name="options-top"></slot> -->
<view class="van-cascader__options">
<view
wx:for="{{ tab.options }}"
wx:for-item="option"
wx:key="index"
class="{{ option.className }} {{ utils.optionClass(tab, textKey, option) }}"
style="{{ utils.optionStyle({ tab, textKey, option, activeColor }) }}"
data-option="{{ option }}"
data-tab-index="{{ tabIndex }}"
bind:tap="onSelect"
>
<text>{{ option[textKey] }}</text>
<van-icon wx:if="{{ utils.isSelected(tab, textKey, option) }}" name="success" custom-class="van-cascader__selected-icon" />
</view>
</view>
<!-- 暂不支持 -->
<!-- <slot name="options-bottom"></slot> -->
</van-tab>
</van-tabs>

View File

@ -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,
};

View File

@ -0,0 +1,439 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render demo and match snapshot 1`] = `
<main>
<demo-block>
<wx-view
class="custom-class demo-block van-clearfix "
>
<wx-view
class="demo-block__title"
>
基础用法
</wx-view>
<van-field
data-state-key="baseState"
bind:tap="onClick"
>
<van-cell
customClass="van-field"
>
<wx-view
class="custom-class van-cell van-cell--clickable"
hoverClass="van-cell--hover hover-class"
hoverStayTime="70"
style=""
bind:tap="onClick"
>
<wx-view
class="van-cell__title title-class"
style="max-width:6.2em;min-width:6.2em;margin-right: 12px;"
>
<wx-view
class="label-class van-field__label"
slot="title"
>
地区
</wx-view>
</wx-view>
<wx-view
class="van-cell__value value-class"
>
<wx-view
class="van-field__body van-field__body--text"
>
<wx-view
class="van-field__control van-field__control--custom"
bind:tap="onClickInput"
/>
<wx-input
adjustPosition="{{true}}"
alwaysEmbed="{{false}}"
autoFocus="{{false}}"
class="van-field__control input-class"
confirmHold="{{false}}"
confirmType=""
cursor="{{-1}}"
cursorSpacing="{{50}}"
disabled="{{true}}"
focus="{{false}}"
holdKeyboard="{{false}}"
maxlength="{{-1}}"
password="{{false}}"
placeholder="请选择地区"
placeholderClass="van-field__placeholder"
placeholderStyle=""
selectionEnd="{{-1}}"
selectionStart="{{-1}}"
type="text"
value=""
bind:blur="onBlur"
bind:confirm="onConfirm"
bind:focus="onFocus"
bind:input="onInput"
bind:keyboardheightchange="onKeyboardHeightChange"
bind:tap="onClickInput"
/>
<wx-view
class="van-field__icon-container"
bind:tap="onClickIcon"
/>
<wx-view
class="van-field__button"
/>
</wx-view>
</wx-view>
<van-icon
class="van-cell__right-icon-wrap right-icon-class"
customClass="van-cell__right-icon"
>
<wx-view
class="custom-class van-icon van-icon-arrow"
style=""
bind:tap="onClick"
/>
</van-icon>
</wx-view>
</van-cell>
</van-field>
<van-popup>
<van-overlay
bind:click="onClickOverlay"
>
<van-transition
customClass="van-overlay custom-class"
bind:tap="onClick"
catch:touchmove="noop"
/>
</van-overlay>
</van-popup>
</wx-view>
</demo-block>
<demo-block>
<wx-view
class="custom-class demo-block van-clearfix "
>
<wx-view
class="demo-block__title"
>
自定义颜色
</wx-view>
<van-field
data-state-key="customColorState"
bind:tap="onClick"
>
<van-cell
customClass="van-field"
>
<wx-view
class="custom-class van-cell van-cell--clickable"
hoverClass="van-cell--hover hover-class"
hoverStayTime="70"
style=""
bind:tap="onClick"
>
<wx-view
class="van-cell__title title-class"
style="max-width:6.2em;min-width:6.2em;margin-right: 12px;"
>
<wx-view
class="label-class van-field__label"
slot="title"
>
地区
</wx-view>
</wx-view>
<wx-view
class="van-cell__value value-class"
>
<wx-view
class="van-field__body van-field__body--text"
>
<wx-view
class="van-field__control van-field__control--custom"
bind:tap="onClickInput"
/>
<wx-input
adjustPosition="{{true}}"
alwaysEmbed="{{false}}"
autoFocus="{{false}}"
class="van-field__control input-class"
confirmHold="{{false}}"
confirmType=""
cursor="{{-1}}"
cursorSpacing="{{50}}"
disabled="{{true}}"
focus="{{false}}"
holdKeyboard="{{false}}"
maxlength="{{-1}}"
password="{{false}}"
placeholder="请选择地区"
placeholderClass="van-field__placeholder"
placeholderStyle=""
selectionEnd="{{-1}}"
selectionStart="{{-1}}"
type="text"
value=""
bind:blur="onBlur"
bind:confirm="onConfirm"
bind:focus="onFocus"
bind:input="onInput"
bind:keyboardheightchange="onKeyboardHeightChange"
bind:tap="onClickInput"
/>
<wx-view
class="van-field__icon-container"
bind:tap="onClickIcon"
/>
<wx-view
class="van-field__button"
/>
</wx-view>
</wx-view>
<van-icon
class="van-cell__right-icon-wrap right-icon-class"
customClass="van-cell__right-icon"
>
<wx-view
class="custom-class van-icon van-icon-arrow"
style=""
bind:tap="onClick"
/>
</van-icon>
</wx-view>
</van-cell>
</van-field>
<van-popup>
<van-overlay
bind:click="onClickOverlay"
>
<van-transition
customClass="van-overlay custom-class"
bind:tap="onClick"
catch:touchmove="noop"
/>
</van-overlay>
</van-popup>
</wx-view>
</demo-block>
<demo-block>
<wx-view
class="custom-class demo-block van-clearfix "
>
<wx-view
class="demo-block__title"
>
异步加载选项
</wx-view>
<van-field
data-state-key="asyncState"
bind:tap="onClick"
>
<van-cell
customClass="van-field"
>
<wx-view
class="custom-class van-cell van-cell--clickable"
hoverClass="van-cell--hover hover-class"
hoverStayTime="70"
style=""
bind:tap="onClick"
>
<wx-view
class="van-cell__title title-class"
style="max-width:6.2em;min-width:6.2em;margin-right: 12px;"
>
<wx-view
class="label-class van-field__label"
slot="title"
>
地区
</wx-view>
</wx-view>
<wx-view
class="van-cell__value value-class"
>
<wx-view
class="van-field__body van-field__body--text"
>
<wx-view
class="van-field__control van-field__control--custom"
bind:tap="onClickInput"
/>
<wx-input
adjustPosition="{{true}}"
alwaysEmbed="{{false}}"
autoFocus="{{false}}"
class="van-field__control input-class"
confirmHold="{{false}}"
confirmType=""
cursor="{{-1}}"
cursorSpacing="{{50}}"
disabled="{{true}}"
focus="{{false}}"
holdKeyboard="{{false}}"
maxlength="{{-1}}"
password="{{false}}"
placeholder="请选择地区"
placeholderClass="van-field__placeholder"
placeholderStyle=""
selectionEnd="{{-1}}"
selectionStart="{{-1}}"
type="text"
value=""
bind:blur="onBlur"
bind:confirm="onConfirm"
bind:focus="onFocus"
bind:input="onInput"
bind:keyboardheightchange="onKeyboardHeightChange"
bind:tap="onClickInput"
/>
<wx-view
class="van-field__icon-container"
bind:tap="onClickIcon"
/>
<wx-view
class="van-field__button"
/>
</wx-view>
</wx-view>
<van-icon
class="van-cell__right-icon-wrap right-icon-class"
customClass="van-cell__right-icon"
>
<wx-view
class="custom-class van-icon van-icon-arrow"
style=""
bind:tap="onClick"
/>
</van-icon>
</wx-view>
</van-cell>
</van-field>
<van-popup>
<van-overlay
bind:click="onClickOverlay"
>
<van-transition
customClass="van-overlay custom-class"
bind:tap="onClick"
catch:touchmove="noop"
/>
</van-overlay>
</van-popup>
</wx-view>
</demo-block>
<demo-block>
<wx-view
class="custom-class demo-block van-clearfix "
>
<wx-view
class="demo-block__title"
>
自定义字段名
</wx-view>
<van-field
data-state-key="customFieldState"
bind:tap="onClick"
>
<van-cell
customClass="van-field"
>
<wx-view
class="custom-class van-cell van-cell--clickable"
hoverClass="van-cell--hover hover-class"
hoverStayTime="70"
style=""
bind:tap="onClick"
>
<wx-view
class="van-cell__title title-class"
style="max-width:6.2em;min-width:6.2em;margin-right: 12px;"
>
<wx-view
class="label-class van-field__label"
slot="title"
>
地区
</wx-view>
</wx-view>
<wx-view
class="van-cell__value value-class"
>
<wx-view
class="van-field__body van-field__body--text"
>
<wx-view
class="van-field__control van-field__control--custom"
bind:tap="onClickInput"
/>
<wx-input
adjustPosition="{{true}}"
alwaysEmbed="{{false}}"
autoFocus="{{false}}"
class="van-field__control input-class"
confirmHold="{{false}}"
confirmType=""
cursor="{{-1}}"
cursorSpacing="{{50}}"
disabled="{{true}}"
focus="{{false}}"
holdKeyboard="{{false}}"
maxlength="{{-1}}"
password="{{false}}"
placeholder="请选择地区"
placeholderClass="van-field__placeholder"
placeholderStyle=""
selectionEnd="{{-1}}"
selectionStart="{{-1}}"
type="text"
value=""
bind:blur="onBlur"
bind:confirm="onConfirm"
bind:focus="onFocus"
bind:input="onInput"
bind:keyboardheightchange="onKeyboardHeightChange"
bind:tap="onClickInput"
/>
<wx-view
class="van-field__icon-container"
bind:tap="onClickIcon"
/>
<wx-view
class="van-field__button"
/>
</wx-view>
</wx-view>
<van-icon
class="van-cell__right-icon-wrap right-icon-class"
customClass="van-cell__right-icon"
>
<wx-view
class="custom-class van-icon van-icon-arrow"
style=""
bind:tap="onClick"
/>
</van-icon>
</wx-view>
</van-cell>
</van-field>
<van-popup>
<van-overlay
bind:click="onClickOverlay"
>
<van-transition
customClass="van-overlay custom-class"
bind:tap="onClick"
catch:touchmove="noop"
/>
</van-overlay>
</van-popup>
</demo-block>
</main>
`;

View File

@ -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();
});

View File

@ -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;

View File

@ -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<T>() {
const pages = getCurrentPages();
return pages[pages.length - 1] as T & WechatMiniprogram.Page.TrivialInstance;
}
export function deepClone<T extends Record<string, any> | 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<string, any>;
Object.keys(obj).forEach((key: string) => {
// @ts-ignore
to[key] = deepClone(obj[key]);
});
return to as T;
}
return obj;
}

View File

@ -20,7 +20,7 @@ exports[`should render demo and match snapshot 1`] = `
style="z-index:1"
>
<wx-view
class="van-tabs__wrap "
class="wrap-class van-tabs__wrap "
>
<wx-scroll-view
class="van-tabs__scroll van-tabs__scroll--line"

View File

@ -29,7 +29,7 @@ exports[`should render demo and match snapshot 1`] = `
style="z-index:1"
>
<wx-view
class="van-tabs__wrap "
class="wrap-class van-tabs__wrap "
>
<wx-scroll-view
class="van-tabs__scroll van-tabs__scroll--line"
@ -186,7 +186,7 @@ exports[`should render demo and match snapshot 1`] = `
style="z-index:1"
>
<wx-view
class="van-tabs__wrap "
class="wrap-class van-tabs__wrap "
>
<wx-scroll-view
class="van-tabs__scroll van-tabs__scroll--line"
@ -322,7 +322,7 @@ exports[`should render demo and match snapshot 1`] = `
style="z-index:1"
>
<wx-view
class="van-tabs__wrap van-tabs__wrap--scrollable "
class="wrap-class van-tabs__wrap van-tabs__wrap--scrollable "
>
<wx-scroll-view
class="van-tabs__scroll van-tabs__scroll--line"
@ -523,7 +523,7 @@ exports[`should render demo and match snapshot 1`] = `
style="z-index:1"
>
<wx-view
class="van-tabs__wrap "
class="wrap-class van-tabs__wrap "
>
<wx-scroll-view
class="van-tabs__scroll van-tabs__scroll--line"
@ -661,7 +661,7 @@ exports[`should render demo and match snapshot 1`] = `
style="z-index:1"
>
<wx-view
class="van-tabs__wrap "
class="wrap-class van-tabs__wrap "
>
<wx-scroll-view
class="van-tabs__scroll van-tabs__scroll--card"
@ -795,7 +795,7 @@ exports[`should render demo and match snapshot 1`] = `
style="z-index:1"
>
<wx-view
class="van-tabs__wrap "
class="wrap-class van-tabs__wrap "
>
<wx-scroll-view
class="van-tabs__scroll van-tabs__scroll--line"
@ -910,7 +910,7 @@ exports[`should render demo and match snapshot 1`] = `
style="z-index:1"
>
<wx-view
class="van-tabs__wrap "
class="wrap-class van-tabs__wrap "
>
<wx-scroll-view
class="van-tabs__scroll van-tabs__scroll--line"
@ -1067,7 +1067,7 @@ exports[`should render demo and match snapshot 1`] = `
style="z-index:1"
>
<wx-view
class="van-tabs__wrap "
class="wrap-class van-tabs__wrap "
>
<wx-scroll-view
class="van-tabs__scroll van-tabs__scroll--line"
@ -1224,7 +1224,7 @@ exports[`should render demo and match snapshot 1`] = `
style="z-index:1"
>
<wx-view
class="van-tabs__wrap "
class="wrap-class van-tabs__wrap "
>
<wx-scroll-view
class="van-tabs__scroll van-tabs__scroll--line"
@ -1385,7 +1385,7 @@ exports[`should render demo and match snapshot 1`] = `
style="z-index:1"
>
<wx-view
class="van-tabs__wrap "
class="wrap-class van-tabs__wrap "
>
<wx-scroll-view
class="van-tabs__scroll van-tabs__scroll--line"

View File

@ -15,7 +15,13 @@ type TrivialInstance = WechatMiniprogram.Component.TrivialInstance;
VantComponent({
mixins: [touch],
classes: ['nav-class', 'tab-class', 'tab-active-class', 'line-class'],
classes: [
'nav-class',
'tab-class',
'tab-active-class',
'line-class',
'wrap-class',
],
relation: useChildren('tab', function () {
this.updateTabs();

View File

@ -9,7 +9,7 @@
container="{{ container }}"
bind:scroll="onTouchScroll"
>
<view class="{{ utils.bem('tabs__wrap', { scrollable }) }} {{ type === 'line' && border ? 'van-hairline--top-bottom' : '' }}">
<view class="wrap-class {{ utils.bem('tabs__wrap', { scrollable }) }} {{ type === 'line' && border ? 'van-hairline--top-bottom' : '' }}">
<slot name="nav-left" />
<scroll-view