Files
cmgd-mini-app/uni_modules/nutui-uni/components/cascaderitem/cascaderitem.vue
2026-01-05 12:47:14 +08:00

340 lines
8.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts" setup>
import { computed, defineComponent, ref, watch } from 'vue'
import { CHANGE_EVENT, PREFIX, UPDATE_MODEL_EVENT } from '../_constants'
import { getMainClass } from '../_utils'
import { useTranslate } from '../../locale'
import { convertListToOptions } from '../cascader/helper'
import Tree from '../cascader/tree'
import type { CascaderOption, CascaderPane, CascaderTabs, CascaderValue, ConvertConfig } from '../cascader/types'
import NutIcon from '../icon/icon.vue'
import NutTabPane from '../tabpane/tabpane.vue'
import NutTabs from '../tabs/tabs.vue'
import { cascaderitemEmits, cascaderitemProps } from './cascaderitem'
const props = defineProps(cascaderitemProps)
const emit = defineEmits(cascaderitemEmits)
const classes = computed(() => {
return getMainClass(props, componentName)
})
const configs = computed(() => ({
lazy: props.lazy,
lazyLoad: props.lazyLoad,
valueKey: props.valueKey,
textKey: props.textKey,
childrenKey: props.childrenKey,
convertConfig: props.convertConfig,
}))
const tabsCursor = ref(0)
const initLoading = ref(false)
const innerValue = ref(props.modelValue as CascaderValue)
const tree = ref(new Tree([], {}))
const panes = ref<CascaderPane[]>([])
const isLazy = computed(() => configs.value.lazy && Boolean(configs.value.lazyLoad))
const lazyLoadMap = new Map()
let currentProcessNode: CascaderOption | null
async function init() {
lazyLoadMap.clear()
panes.value = []
tabsCursor.value = 0
initLoading.value = false
currentProcessNode = null
let { options } = props
if (configs.value.convertConfig) {
options = convertListToOptions(options as CascaderOption[], configs.value.convertConfig as ConvertConfig)
}
tree.value = new Tree(options as CascaderOption[], {
value: configs.value.valueKey,
text: configs.value.textKey,
children: configs.value.childrenKey,
})
if (isLazy.value && !tree.value.nodes.length) {
await invokeLazyLoad({
root: true,
loading: true,
text: '',
value: '',
})
}
panes.value = [{ nodes: tree.value.nodes, selectedNode: null }]
syncValue()
}
const methods = {
// 选中一个节点,静默模式不触发事件
async handleNode(node: CascaderOption, silent?: boolean) {
const { disabled, loading } = node
if ((!silent && disabled) || !panes.value[tabsCursor.value])
return
if (tree.value.isLeaf(node, isLazy.value)) {
node.leaf = true
panes.value[tabsCursor.value].selectedNode = node
panes.value = panes.value.slice(0, (node.level as number) + 1)
if (!silent) {
const pathNodes = panes.value.map(pane => pane.selectedNode)
emitChange(pathNodes as CascaderOption[])
emit('pathChange', pathNodes as CascaderOption[])
}
return
}
if (tree.value.hasChildren(node, isLazy.value)) {
const level = (node.level as number) + 1
panes.value[tabsCursor.value].selectedNode = node
panes.value = panes.value.slice(0, level)
panes.value.push({
nodes: node.children || [],
selectedNode: null,
})
tabsCursor.value = level
if (!silent) {
const pathNodes = panes.value.map(pane => pane.selectedNode)
emit('pathChange', pathNodes as CascaderOption[])
}
return
}
currentProcessNode = node
if (loading)
return
await invokeLazyLoad(node)
if (currentProcessNode === node) {
panes.value[tabsCursor.value].selectedNode = node
methods.handleNode(node, silent)
}
},
handleTabClick(tab: CascaderTabs) {
currentProcessNode = null
tabsCursor.value = Number(tab.paneKey)
},
isSelected(pane: CascaderPane, node: CascaderOption) {
return pane?.selectedNode?.value === node.value
},
}
async function syncValue() {
const currentValue = innerValue.value
if (currentValue === undefined || !tree.value.nodes.length) {
return
}
if (currentValue.length === 0) {
tabsCursor.value = 0
panes.value = [{ nodes: tree.value.nodes, selectedNode: null }]
return
}
let needToSync = currentValue
if (isLazy.value && Array.isArray(currentValue) && currentValue.length) {
needToSync = []
const parent = tree.value.nodes.find(node => node.value === currentValue[0])
if (parent) {
needToSync = [parent.value]
initLoading.value = true
const last = await currentValue.slice(1).reduce(async (p: Promise<CascaderOption | void>, value) => {
const parent = await p
await invokeLazyLoad(parent)
const node = parent?.children?.find(item => item.value === value)
if (node)
needToSync.push(value)
return Promise.resolve(node)
}, Promise.resolve(parent))
await invokeLazyLoad(last)
initLoading.value = false
}
}
if (needToSync.length && currentValue === props.modelValue) {
const pathNodes = tree.value.getPathNodesByValue(needToSync)
pathNodes.forEach((node, index) => {
tabsCursor.value = index
methods.handleNode(node, true)
})
}
}
async function invokeLazyLoad(node?: CascaderOption | void) {
if (!node)
return
if (!configs.value.lazyLoad) {
node.leaf = true
return
}
if (tree.value.isLeaf(node, isLazy.value) || tree.value.hasChildren(node, isLazy.value))
return
node.loading = true
const parent = node.root ? null : node
let lazyLoadPromise = lazyLoadMap.get(node)
if (!lazyLoadPromise) {
lazyLoadPromise = new Promise((resolve) => {
// 外部必须resolve
configs.value.lazyLoad?.(node, resolve)
})
lazyLoadMap.set(node, lazyLoadPromise)
}
const nodes: CascaderOption[] | void = await lazyLoadPromise
if (Array.isArray(nodes) && nodes.length > 0) {
tree.value.updateChildren(nodes, parent)
}
else {
// 如果加载完成后没有提供子节点,作为叶子节点处理
node.leaf = true
}
node.loading = false
lazyLoadMap.delete(node)
}
function emitChange(pathNodes: CascaderOption[]) {
const emitValue = pathNodes.map(node => node.value)
innerValue.value = emitValue
emit(UPDATE_MODEL_EVENT, emitValue)
emit(CHANGE_EVENT, emitValue, pathNodes)
}
function formatTabTitle(pane: CascaderPane) {
return pane.selectedNode ? pane.selectedNode.text : translate('select')
}
watch(() => [configs.value, props.options], () => {
init()
}, {
deep: true,
immediate: true,
})
watch(() => props.modelValue, (value) => {
if (value !== innerValue.value) {
innerValue.value = value as CascaderValue
syncValue()
}
})
watch(() => props.visible, (value) => {
// TODO: value为空时保留上次选择记录修复单元测试问题
if (value && Array.isArray(innerValue.value) && innerValue.value.length > 0) {
syncValue()
}
})
</script>
<script lang="ts">
const componentName = `${PREFIX}-cascader-item`
const { translate } = useTranslate(componentName)
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<NutTabs
v-model="tabsCursor"
:custom-class="classes"
:custom-style="props.customStyle"
:type="props.titleType"
:size="props.titleSize"
:title-gutter="props.titleGutter"
:ellipsis="props.titleEllipsis"
title-scroll
@click="methods.handleTabClick"
>
<template v-if="!initLoading && panes.length">
<NutTabPane v-for="(pane, index) in panes" :key="index" :title="formatTabTitle(pane)">
<view class="nut-cascader-pane" role="menu">
<scroll-view style="height: 100%" :scroll-y="true">
<template v-for="node in pane.nodes" :key="node.value">
<view
class="nut-cascader-item__inner"
:class="{ active: methods.isSelected(pane, node), disabled: node.disabled }"
role="menuitemradio"
:aria-checked="methods.isSelected(pane, node)"
:aria-disabled="node.disabled || undefined"
@click="methods.handleNode(node, false)"
>
<view class="nut-cascader-item__title">
{{ node.text }}
</view>
<NutIcon
v-if="node.loading"
custom-class="nut-cascader-item__icon-loading"
loading
name="loading"
/>
<NutIcon
v-else
custom-class="nut-cascader-item__icon-check"
name="checklist"
/>
</view>
</template>
</scroll-view>
</view>
</NutTabPane>
</template>
<template v-else>
<NutTabPane title="Loading...">
<view class="nut-cascader-pane" />
</NutTabPane>
</template>
</NutTabs>
</template>
<style lang="scss">
@import "./index";
</style>