first commit
This commit is contained in:
519
uni_modules/tdesign-uniapp/components/cascader/cascader.vue
Normal file
519
uni_modules/tdesign-uniapp/components/cascader/cascader.vue
Normal file
@@ -0,0 +1,519 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user