| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- <template>
- <view class="nana-form-cascader">
- <Tabs
- :currentIndex="headerTabCurrent"
- :tabs="headerTabs"
- :autoItemWidth="false"
- :innerStyle="headerStyle"
- v-bind="tabProps"
- @update:currentIndex="handleTabChange"
- />
- <slot name="header" :currentIndex="headerTabCurrent" />
- <SimpleList
- mode="single-check"
- virtual
- :data="currentDataDynamicLoading ? [] : currentData"
- :dataDisplayProp="textKey"
- colorProp="color"
- disabledProp="disabled"
- :innerStyle="{
- width: '100%',
- height: themeContext.resolveThemeSize(props.listHeight),
- }"
- :checkedItems="[ currentCheckedItem ]"
- @itemClick="handleItemClick"
- >
- <template #empty>
- <LoadingPage v-if="currentDataDynamicLoading" />
- <Empty v-else :description="currentDataDynamicLoadErrorText || '暂无数据'" :image="currentDataDynamicLoadErrorText ? 'error' : 'default'" />
- </template>
- </SimpleList>
- </view>
- </template>
- <script setup lang="ts">
- import { computed, onMounted, ref, watch } from 'vue';
- import { useTheme } from '../theme/ThemeDefine';
- import Tabs, { type TabsItemData, type TabsProps } from '../nav/Tabs.vue';
- import SimpleList from '../list/SimpleList.vue';
- import { DynamicSize } from '../theme/ThemeTools';
- import LoadingPage from '../display/loading/LoadingPage.vue';
- import Empty from '../feedback/Empty.vue';
- import { getCascaderText } from './CascaderUtils';
- const themeContext = useTheme();
- const headerStyle = themeContext.useThemeStyle({
- marginLeft: DynamicSize('SimpleListItemPaddingHorizontal', 35),
- marginRight: DynamicSize('SimpleListItemPaddingHorizontal', 35),
- })
- export interface CascaderItem extends Record<string, any> {
- /**
- * 选项的文本
- */
- text: string;
- /**
- * 选项的值
- */
- value: string|number;
- /**
- * 选项是否禁用
- */
- disabled?: boolean;
- /**
- * 选项的颜色
- */
- color?: string;
- /**
- * 子选项
- */
- children?: CascaderItem[];
- }
- export interface CascaderProps {
- /**
- * 选中的值
- */
- modelValue: (number|string)[],
- /**
- * 数据
- */
- data: CascaderItem[];
- /**
- * 是否在选择完成后直接关闭。
- * @default true
- */
- autoConfirm?: boolean,
- /**
- * 列表的高度rpx
- * @default 700
- */
- listHeight?: number|string;
- /**
- * 选项的文本键名
- * @default 'text'
- */
- textKey?: string,
- /**
- * 选项的值键名
- * @default 'value'
- */
- valueKey?: string,
- /**
- * 选项的子选项键名
- * @default 'children'
- */
- childrenKey?: string,
- /**
- * Tab组件的自定义选项
- */
- tabProps?: Omit<TabsProps, 'tabs' | 'currentIndex'>,
- /**
- * 异步加载数据。
- * 如果你需要异步加载数据,当选项打开后并且 children 为空数组时,会调用该函数加载数据。
- * * 注:当一个条目的数据已加载后,它会缓存到 children 数组中再次点击不会再加载数据,若要刷新,可手动清空 children 数组。
- */
- asyncLoadData?: (group: CascaderItem, level: number) => Promise<CascaderItem[]>;
- /**
- * 最大选择层级。在异步加载数据时建议设置最大层级,否则将尝试每一级都继续加载数据。
- * @default undefined
- */
- maxSelectLevel?: number;
- }
- const emit = defineEmits([ 'update:modelValue', 'selectTextChange', 'pickEnd' ]);
- const props = withDefaults(defineProps<CascaderProps>(), {
- autoConfirm: true,
- listHeight: 700,
- tabProps: () => ({
- width: 750 - 70,
- }),
- textKey: 'text',
- valueKey: 'value',
- childrenKey: 'children',
- });
- const headerTabCurrent = ref(0);
- const headerTabs = computed<TabsItemData[]>(() => {
- const tabs: TabsItemData[] = [];
- let currentGroup : CascaderItem[]|undefined = props.data;
- let i = 0;
- for (; i < props.modelValue.length; i++) {
- const item : CascaderItem|undefined = currentGroup?.find(item => item[props.valueKey] === props.modelValue[i]);
- if (item) {
- tabs.push({
- text: item[props.textKey],
- data: item[props.valueKey],
- width: -1,
- })
- currentGroup = item[props.childrenKey];
- }
- }
- if ((currentGroup !== undefined && currentGroup.length > 0) || canLoadLevel(i)) {
- tabs.push({
- text: '请选择',
- data: null,
- width: -1,
- });
- }
- return tabs;
- });
- function canLoadLevel(level: number) {
- return (props.maxSelectLevel === undefined || level < props.maxSelectLevel) && props.asyncLoadData;
- }
- const currentDataDynamicLoading = ref(false);
- const currentDataDynamicLoadErrorText = ref('');
- const currentDataDynamicLoad = ref<CascaderItem[]>([]);
- const currentData = computed(() => {
- let currentGroup = props.data;
- for (let i = 0; i < headerTabCurrent.value; i++) {
- const item = currentGroup.find(item => item[props.valueKey] === props.modelValue[i]);
- if (item) {
- currentGroup = item[props.childrenKey] || [];
- if (currentGroup.length === 0 && props.asyncLoadData) {
- loadAsyncData(item);
- return currentDataDynamicLoad.value;
- }
- }
- }
- return currentGroup;
- });
- const currentCheckedItem = computed(() => {
- return currentData.value.find(item => item[props.valueKey] === props.modelValue[headerTabCurrent.value]);
- });
- function updateText(value: (number|string)[]) {
- emit('selectTextChange', getCascaderText(
- value,
- props.valueKey,
- props.textKey,
- props.childrenKey,
- props.data
- ));
- }
- async function loadAsyncData(group: CascaderItem) {
- if (props.asyncLoadData) {
- currentDataDynamicLoading.value = true;
- props.asyncLoadData(group, headerTabCurrent.value)
- .then(data => {
- group[props.childrenKey] = data;
- currentDataDynamicLoad.value = data;
- currentDataDynamicLoadErrorText.value = '';
- console.log(group);
- }).catch((e) => {
- console.error('asyncLoadData failed', e);
- currentDataDynamicLoadErrorText.value = '' + e;
- })
- .finally(() => {
- currentDataDynamicLoading.value = false;
- });
- }
- }
- function loadValue() {
- headerTabCurrent.value = Math.min(props.modelValue.length, headerTabs.value.length - 1);
- }
- function handleItemClick(item: CascaderItem) {
- let values = props.modelValue;
- if (values.length > headerTabCurrent.value)
- values = values.slice(0, headerTabCurrent.value);
- values = values.concat([item[props.valueKey]]);
- emit('update:modelValue', values);
- updateText(values);
- const nextGroup = item[props.childrenKey];
- if (
- (!nextGroup || nextGroup.length === 0)
- && !canLoadLevel(headerTabCurrent.value + 1)
- )
- emit('pickEnd');
- }
- function handleTabChange(v: number) {
- headerTabCurrent.value = v;
- }
- watch(() => props.modelValue, (v) => {
- loadValue();
- updateText(v);
- })
- onMounted(() => {
- loadValue();
- updateText(props.modelValue);
- })
- defineOptions({
- options: {
- styleIsolation: "shared",
- virtualHost: true,
- }
- })
- </script>
- <style lang="scss">
- .nana-form-cascader {
- position: relative;
- display: flex;
- flex-direction: column;
- }
- </style>
|