520 lines
14 KiB
Vue
520 lines
14 KiB
Vue
<template>
|
||
<view>
|
||
<TPopup
|
||
:class="tClass"
|
||
:visible="dataVisible"
|
||
placement="bottom"
|
||
@visible-change="onVisibleChange"
|
||
>
|
||
<view
|
||
:style="tools._style([customStyle])"
|
||
:class="name"
|
||
>
|
||
<view :class="name + '__title'">
|
||
<slot name="title" />
|
||
{{ title }}
|
||
</view>
|
||
<view
|
||
:class="name + '__close-btn'"
|
||
@click="onClose"
|
||
>
|
||
<slot name="close-btn" />
|
||
<TIcon
|
||
v-if="closeBtn"
|
||
size="48rpx"
|
||
name="close"
|
||
/>
|
||
</view>
|
||
|
||
<slot name="header" />
|
||
|
||
<view :class="name + '__content'">
|
||
<block v-if="steps && steps.length">
|
||
<view
|
||
v-if="theme == 'step'"
|
||
:class="name + '__steps'"
|
||
>
|
||
<view
|
||
v-for="(item, index) in steps"
|
||
:key="index"
|
||
:class="name + '__step'"
|
||
:data-index="index"
|
||
@click="() => onStepClick(index)"
|
||
>
|
||
<view
|
||
:class="
|
||
name +
|
||
'__step-dot ' +
|
||
name +
|
||
'__step-dot--' +
|
||
(item !== placeholder ? 'active' : '') +
|
||
' ' +
|
||
name +
|
||
'__step-dot--' +
|
||
(index === steps.length - 1 ? 'last' : '')
|
||
"
|
||
/>
|
||
|
||
<view :class="name + '__step-label ' + name + '__step-label--' + (index === stepIndex ? 'active' : '')">
|
||
{{ item }}
|
||
</view>
|
||
|
||
<TIcon
|
||
name="chevron-right"
|
||
size="44rpx"
|
||
:t-class="name + '__step-arrow'"
|
||
:custom-style="stepArrowCustomStyle"
|
||
style="margin-left: auto"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<TTabs
|
||
v-if="theme == 'tab'"
|
||
ref="tabs"
|
||
:value="stepIndex"
|
||
:space-evenly="false"
|
||
@change="({value}) => onTabChange(value)"
|
||
>
|
||
<TTabPanel
|
||
v-for="(item, index) in steps"
|
||
:key="index"
|
||
:ref="`tab-${index}`"
|
||
:value="index"
|
||
:label="item"
|
||
/>
|
||
</TTabs>
|
||
</block>
|
||
|
||
<slot name="middle-content" />
|
||
|
||
<view
|
||
v-if="subTitles && subTitles[stepIndex]"
|
||
:class="name + '__options-title'"
|
||
>
|
||
{{ subTitles[stepIndex] }}
|
||
</view>
|
||
|
||
<view
|
||
:class="name + '__options-container'"
|
||
:style="'width: ' + (items.length + 1) + '00vw; transform: translateX(-' + stepIndex + '00vw)'"
|
||
>
|
||
<scroll-view
|
||
v-for="(options, index) in items"
|
||
:key="index"
|
||
:class="name + '__options'"
|
||
scroll-y
|
||
:scroll-top="scrollTopList[index]"
|
||
type="list"
|
||
:style="'height: ' + _optionsHeight + 'px'"
|
||
>
|
||
<view :class="'cascader-radio-group-' + index">
|
||
<TRadioGroup
|
||
:value="selectedValue[index]"
|
||
:keys="keys"
|
||
:options="options"
|
||
:data-level="index"
|
||
placement="right"
|
||
icon="line"
|
||
borderless
|
||
@change="({ value }) => handleSelect($event, { level: index, value })"
|
||
/>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</TPopup>
|
||
</view>
|
||
</template>
|
||
<script>
|
||
import TIcon from '../icon/icon';
|
||
import TPopup from '../popup/popup';
|
||
import TTabs from '../tabs/tabs';
|
||
import TTabPanel from '../tab-panel/tab-panel';
|
||
import TRadioGroup from '../radio-group/radio-group';
|
||
import { uniComponent } from '../common/src/index';
|
||
import { prefix } from '../common/config';
|
||
import props from './props';
|
||
import { getRect, coalesce, nextTick } from '../common/utils';
|
||
|
||
import tools from '../common/utils.wxs';
|
||
|
||
|
||
const name = `${prefix}-cascader`;
|
||
|
||
function parseOptions(options, keys) {
|
||
const label = coalesce(keys?.label, 'label');
|
||
const value = coalesce(keys?.value, 'value');
|
||
const disabled = coalesce(keys?.disabled, 'disabled');
|
||
|
||
return options.map(item => ({
|
||
[label]: item[label],
|
||
[value]: item[value],
|
||
[disabled]: item[disabled],
|
||
}));
|
||
}
|
||
|
||
const defaultState = {
|
||
contentHeight: 0,
|
||
stepHeight: 0,
|
||
tabsHeight: 0,
|
||
subTitlesHeight: 0,
|
||
stepsInitHeight: 0,
|
||
};
|
||
|
||
export default uniComponent({
|
||
name,
|
||
options: {
|
||
styleIsolation: 'shared',
|
||
},
|
||
controlledProps: [
|
||
{
|
||
key: 'value',
|
||
event: 'change',
|
||
},
|
||
],
|
||
externalClasses: [
|
||
`${prefix}-class`,
|
||
],
|
||
components: {
|
||
TIcon,
|
||
TPopup,
|
||
TTabs,
|
||
TTabPanel,
|
||
TRadioGroup,
|
||
},
|
||
props: {
|
||
...props,
|
||
},
|
||
emits: [
|
||
'update:visible',
|
||
],
|
||
data() {
|
||
return {
|
||
prefix,
|
||
name,
|
||
stepIndex: 0,
|
||
selectedIndexes: [],
|
||
selectedValue: [],
|
||
scrollTopList: [],
|
||
steps: [],
|
||
_optionsHeight: 0,
|
||
tools,
|
||
|
||
dataVisible: this.visible,
|
||
dataValue: coalesce(this.value, this.defaultValue),
|
||
items: [],
|
||
};
|
||
},
|
||
computed: {
|
||
stepArrowCustomStyle() {
|
||
return tools._style({
|
||
color: 'var(--td-cascader-step-arrow-color, var(--td-text-color-placeholder, var(--td-font-gray-3, rgba(0, 0, 0, .4))))',
|
||
marginLeft: 'auto',
|
||
});
|
||
},
|
||
},
|
||
watch: {
|
||
visible: {
|
||
handler(v) {
|
||
this.dataVisible = v;
|
||
},
|
||
immediate: true,
|
||
},
|
||
dataVisible: {
|
||
handler(v) {
|
||
if (v) {
|
||
nextTick().then(() => {
|
||
const $tabs = this.$refs.tabs;
|
||
$tabs?.setTrack();
|
||
$tabs?.getTabHeight().then((res) => {
|
||
this.state.tabsHeight = res.height;
|
||
});
|
||
});
|
||
|
||
// 不能使用 this.$nextTick,在头条小程序下会报错
|
||
nextTick().then(() => {
|
||
this.initOptionsHeight(this.steps.length);
|
||
this.updateScrollTop();
|
||
this.initWithValue();
|
||
});
|
||
} else {
|
||
this.state = { ...defaultState };
|
||
}
|
||
},
|
||
immediate: true,
|
||
},
|
||
|
||
value: {
|
||
handler(v) {
|
||
this.dataValue = v;
|
||
},
|
||
immediate: true,
|
||
},
|
||
|
||
dataValue: {
|
||
handler() {
|
||
this.initWithValue();
|
||
},
|
||
immediate: true,
|
||
},
|
||
|
||
options: {
|
||
handler() {
|
||
const { selectedValue, steps, items } = this.genItems();
|
||
|
||
this.steps = steps;
|
||
this.items = items;
|
||
this.selectedValue = selectedValue;
|
||
this.stepIndex = items.length - 1;
|
||
this.setTabParent();
|
||
},
|
||
immediate: true,
|
||
deep: true,
|
||
},
|
||
selectedIndexes: {
|
||
handler() {
|
||
const { visible, theme } = this;
|
||
const { selectedValue, steps, items } = this.genItems();
|
||
|
||
this.steps = steps;
|
||
this.setTabParent();
|
||
this.selectedValue = selectedValue;
|
||
this.stepIndex = items.length - 1;
|
||
|
||
if (JSON.stringify(items) !== JSON.stringify(this.items)) {
|
||
this.items = items;
|
||
}
|
||
|
||
|
||
if (visible && theme === 'step') {
|
||
this.updateOptionsHeight(steps.length);
|
||
}
|
||
},
|
||
immediate: true,
|
||
deep: true,
|
||
},
|
||
|
||
stepIndex: {
|
||
handler() {
|
||
const { dataVisible: visible } = this;
|
||
|
||
if (visible) {
|
||
this.updateScrollTop();
|
||
}
|
||
},
|
||
immediate: true,
|
||
deep: true,
|
||
},
|
||
},
|
||
created() {
|
||
this.state = {
|
||
...defaultState,
|
||
};
|
||
},
|
||
mounted() {
|
||
|
||
},
|
||
methods: {
|
||
setTabParent() {
|
||
// #ifdef MP-TOUTIAO
|
||
nextTick().then(() => {
|
||
const tabsRef = this.$refs.tabs;
|
||
this.steps.forEach((tools, index) => {
|
||
const tabRef = this.$refs[`tab-${index}`];
|
||
tabRef?.[0]?.setParent(tabsRef);
|
||
});
|
||
});
|
||
// #endif
|
||
},
|
||
updateOptionsHeight(steps) {
|
||
const { contentHeight, stepsInitHeight, stepHeight, subTitlesHeight } = this.state;
|
||
this._optionsHeight = contentHeight - stepsInitHeight - subTitlesHeight - (steps - 1) * stepHeight;
|
||
},
|
||
|
||
async initOptionsHeight(steps) {
|
||
const { theme, subTitles } = this;
|
||
|
||
const { height } = await getRect(this, `.${name}__content`);
|
||
this.state.contentHeight = height;
|
||
|
||
if (theme === 'step') {
|
||
await Promise.all([
|
||
getRect(this, `.${name}__steps`),
|
||
getRect(this, `.${name}__step`),
|
||
])
|
||
.then(([stepsRect, stepRect]) => {
|
||
this.state.stepsInitHeight = stepsRect.height - (steps - 1) * stepRect.height;
|
||
this.state.stepHeight = stepRect.height;
|
||
})
|
||
.catch(() => {
|
||
});
|
||
}
|
||
|
||
if (subTitles.length > 0) {
|
||
const { height } = await getRect(this, `.${name}__options-title`);
|
||
this.state.subTitlesHeight = height;
|
||
}
|
||
|
||
const optionsInitHeight = this.state.contentHeight - this.state.subTitlesHeight;
|
||
this._optionsHeight = theme === 'step'
|
||
? optionsInitHeight - this.state.stepsInitHeight - (steps - 1) * this.state.stepHeight
|
||
: optionsInitHeight - this.state.tabsHeight;
|
||
},
|
||
|
||
initWithValue() {
|
||
if (this.dataValue != null && this.dataValue !== '') {
|
||
const selectedIndexes = this.getIndexesByValue(this.options, this.dataValue);
|
||
|
||
if (selectedIndexes) {
|
||
this.selectedIndexes = selectedIndexes;
|
||
}
|
||
} else {
|
||
this.selectedIndexes = [];
|
||
}
|
||
},
|
||
getIndexesByValue(options, value) {
|
||
const { keys } = this;
|
||
|
||
for (let i = 0, size = options.length; i < size; i += 1) {
|
||
const opt = options[i];
|
||
if (opt[coalesce(keys?.value, 'value')] === value) {
|
||
return [i];
|
||
}
|
||
if (opt[coalesce(keys?.children, 'children')]) {
|
||
const res = this.getIndexesByValue(opt[coalesce(keys?.children, 'children')], value);
|
||
if (res) {
|
||
return [i, ...res];
|
||
}
|
||
}
|
||
}
|
||
},
|
||
updateScrollTop() {
|
||
const { dataVisible: visible, items, selectedIndexes, stepIndex } = this;
|
||
|
||
if (visible) {
|
||
getRect(this, '.cascader-radio-group-0').then((rect) => {
|
||
const eachRadioHeight = rect.height / items[0]?.length;
|
||
|
||
this[`scrollTopList[${stepIndex}]`] = eachRadioHeight * selectedIndexes[stepIndex];
|
||
})
|
||
.catch(() => {
|
||
});
|
||
}
|
||
},
|
||
hide(trigger) {
|
||
this.dataVisible = false;
|
||
this.$emit('close', { trigger });
|
||
this.$emit('update:visible', false);
|
||
},
|
||
onVisibleChange() {
|
||
this.hide('overlay');
|
||
},
|
||
onClose() {
|
||
if (this.checkStrictly) {
|
||
this.triggerChange();
|
||
}
|
||
this.hide('close-btn');
|
||
},
|
||
onStepClick(index) {
|
||
this.stepIndex = index;
|
||
},
|
||
onTabChange(value) {
|
||
this.stepIndex = value;
|
||
},
|
||
genItems() {
|
||
const { options, selectedIndexes, keys, placeholder } = this;
|
||
const selectedValue = [];
|
||
const steps = [];
|
||
const items = [parseOptions(options, keys)];
|
||
|
||
if (options.length > 0) {
|
||
let current = options;
|
||
for (let i = 0, size = selectedIndexes.length; i < size; i += 1) {
|
||
const index = selectedIndexes[i];
|
||
const next = current[index];
|
||
current = next[coalesce(keys?.children, 'children')];
|
||
|
||
selectedValue.push(next[coalesce(keys?.value, 'value')]);
|
||
steps.push(next[coalesce(keys?.label, 'label')]);
|
||
|
||
if (next[coalesce(keys?.children, 'children')]) {
|
||
items.push(parseOptions(next[coalesce(keys?.children, 'children')], keys));
|
||
}
|
||
}
|
||
}
|
||
|
||
if (steps.length < items.length) {
|
||
steps.push(placeholder);
|
||
}
|
||
|
||
return {
|
||
selectedValue,
|
||
steps,
|
||
items,
|
||
};
|
||
},
|
||
handleSelect(tools, { level, value }) {
|
||
const { checkStrictly } = this;
|
||
const { selectedIndexes, items, keys, options, selectedValue } = this;
|
||
|
||
const index = items[level].findIndex(item => item[coalesce(keys?.value, 'value')] === value);
|
||
|
||
let item = selectedIndexes.slice(0, level).reduce((acc, item, index) => {
|
||
if (index === 0) {
|
||
return acc[item];
|
||
}
|
||
return acc[coalesce(keys?.children, 'children')][item];
|
||
}, options);
|
||
|
||
|
||
if (level === 0) {
|
||
item = item[index];
|
||
} else {
|
||
item = item[coalesce(keys?.children, 'children')][index];
|
||
}
|
||
|
||
if (item[coalesce(keys?.disabled, 'disabled')]) {
|
||
return;
|
||
}
|
||
this.$emit('pick', {
|
||
value: item[coalesce(keys?.value, 'value')],
|
||
label: item[coalesce(keys?.label, 'label')],
|
||
index,
|
||
level,
|
||
});
|
||
selectedIndexes[level] = index;
|
||
if (checkStrictly && selectedValue.includes(String(value))) {
|
||
selectedIndexes.length = level;
|
||
this.selectedIndexes = selectedIndexes;
|
||
return;
|
||
}
|
||
selectedIndexes.length = level + 1;
|
||
|
||
const { items: newItems } = this.genItems();
|
||
if (item?.[coalesce(keys?.children, 'children')]?.length >= 0) {
|
||
this.selectedIndexes = selectedIndexes;
|
||
this[`items[${level + 1}]`] = newItems[level + 1];
|
||
} else {
|
||
// setCascaderValue(item.value);
|
||
this.selectedIndexes = selectedIndexes;
|
||
setTimeout(this.triggerChange);
|
||
|
||
this.hide('finish');
|
||
}
|
||
},
|
||
triggerChange() {
|
||
const { items, selectedValue, selectedIndexes } = this;
|
||
this._trigger('change', {
|
||
value: coalesce(selectedValue[selectedValue.length - 1], ''),
|
||
selectedOptions: items.map((item, index) => item[selectedIndexes[index]]).filter(Boolean),
|
||
});
|
||
},
|
||
},
|
||
});
|
||
</script>
|
||
<style scoped>
|
||
@import './cascader.css';
|
||
</style>
|