feat: 组件MsTreeSelect支持选择当前
This commit is contained in:
parent
407639f7e2
commit
1d08e57664
|
@ -171,6 +171,7 @@ export default function useTreeSelection(selectedModuleProps: SelectedModuleProp
|
|||
Object.keys(selectedModulesMaps.value).forEach((key) => {
|
||||
delete selectedModulesMaps.value[key];
|
||||
});
|
||||
checkedKeys.value = [];
|
||||
isCheckedAll.value = false;
|
||||
}
|
||||
|
||||
|
|
|
@ -322,7 +322,7 @@
|
|||
filterTreeData.value = data.value;
|
||||
}
|
||||
nextTick(() => {
|
||||
treeRef.value?.expandAll(false);
|
||||
treeRef.value?.expandAll(props.defaultExpandAll ?? false);
|
||||
});
|
||||
} else {
|
||||
updateDebouncedSearch();
|
||||
|
|
|
@ -1,80 +1,168 @@
|
|||
<template>
|
||||
<a-tooltip
|
||||
:content="getTreeSelectTooltip()"
|
||||
:disabled="!(modelValue ?? []).length"
|
||||
:disabled="!(checkedKeys ?? []).length"
|
||||
position="top"
|
||||
content-class="tree-select-content"
|
||||
:mouse-enter-delay="300"
|
||||
>
|
||||
<a-tree-select
|
||||
ref="treeSelectRef"
|
||||
v-model:model-value="selectValue"
|
||||
v-model:input-value="inputValue"
|
||||
:data="props.data"
|
||||
:disabled="props.disabled"
|
||||
:multiple="props.multiple"
|
||||
:field-names="props.fieldNames"
|
||||
allow-search
|
||||
:filter-tree-node="filterTreeNode"
|
||||
:tree-props="{
|
||||
virtualListProps: {
|
||||
height: 200,
|
||||
threshold: 200,
|
||||
},
|
||||
}"
|
||||
v-bind="$attrs"
|
||||
:max-tag-count="maxTagCount"
|
||||
@input-value-change="handleInputValueChange"
|
||||
@popup-visible-change="handlePopupVisibleChange"
|
||||
@change="handleChange"
|
||||
@keyup="handleKeyup"
|
||||
@clear="handleClear"
|
||||
<a-trigger
|
||||
v-model:popup-visible="viewSelectOptionVisible"
|
||||
:trigger="['click']"
|
||||
:click-to-close="false"
|
||||
:popup-translate="[0, 4]"
|
||||
content-class="arco-trigger-menu tree-select-trigger-content"
|
||||
:content-style="{ width: `${triggerWidth}px` }"
|
||||
>
|
||||
<template #label="{ data: slotData }">
|
||||
<div class="one-line-text">{{ slotData.label }}</div>
|
||||
<a-tree-select
|
||||
:id="treeSelectId"
|
||||
ref="treeSelectRef"
|
||||
v-model:model-value="checkedKeys"
|
||||
v-model:input-value="inputValue"
|
||||
:data="treeData"
|
||||
:max-tag-count="maxTagCount"
|
||||
:multiple="props.multiple"
|
||||
allow-search
|
||||
disable-filter
|
||||
allow-clear
|
||||
:tree-props="{
|
||||
virtualListProps: virtualListProps,
|
||||
}"
|
||||
:field-names="props.fieldNames"
|
||||
:placeholder="t('common.pleaseSelect')"
|
||||
:trigger-props="{ contentClass: 'view-select-trigger' }"
|
||||
@popup-visible-change="handlePopupVisibleChange"
|
||||
@input-value-change="handleInputValueChange"
|
||||
@change="handleChange"
|
||||
@keyup="handleKeyup"
|
||||
@clear="handleClear"
|
||||
>
|
||||
<template #label="{ data: slotData }">
|
||||
<div class="one-line-text">{{ slotData.label }}</div>
|
||||
</template>
|
||||
</a-tree-select>
|
||||
<template #content>
|
||||
<MsTree
|
||||
v-model:checked-keys="checkedKeys"
|
||||
v-model:halfCheckedKeys="halfCheckedKeys"
|
||||
:selectable="false"
|
||||
:data="treeData"
|
||||
:keyword="inputValue"
|
||||
:empty-text="t('common.noData')"
|
||||
:virtual-list-props="virtualListProps"
|
||||
:field-names="props.fieldNames as MsTreeFieldNames"
|
||||
default-expand-all
|
||||
block-node
|
||||
title-tooltip-position="tr"
|
||||
:multiple="props.multiple"
|
||||
:checkable="props.treeCheckable"
|
||||
:check-strictly="props.treeCheckStrictly"
|
||||
v-bind="$attrs"
|
||||
@check="checkNode"
|
||||
>
|
||||
<template #title="nodeData">
|
||||
<div
|
||||
class="one-line-text w-full cursor-pointer text-[var(--color-text-1)]"
|
||||
@click="checkNode(checkedKeys, { checked: !checkedKeys.includes(nodeData.id), node: nodeData })"
|
||||
>
|
||||
{{ nodeData.name }}
|
||||
</div>
|
||||
</template>
|
||||
<template #extra="nodeData">
|
||||
<MsButton
|
||||
v-if="nodeData.children && nodeData.children.length"
|
||||
@click="selectParent(nodeData, !!checkedKeys.includes(nodeData.id))"
|
||||
>
|
||||
{{
|
||||
checkedKeys.includes(nodeData.id)
|
||||
? t('ms.case.associate.cancelCurrent')
|
||||
: t('ms.case.associate.selectCurrent')
|
||||
}}
|
||||
</MsButton>
|
||||
</template>
|
||||
</MsTree>
|
||||
</template>
|
||||
<template #tree-slot-title="node">
|
||||
<a-tooltip :content="`${node[props?.fieldNames?.title || 'title']}`" position="tr">
|
||||
<div class="one-line-text max-w-[170px]">{{ node[props?.fieldNames?.title || 'title'] }}</div>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-tree-select>
|
||||
</a-trigger>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import useTreeSelection from '@/components/business/ms-associate-case/useTreeSelection';
|
||||
import MsTree from '@/components/business/ms-tree/index.vue';
|
||||
import type { MsTreeFieldNames, MsTreeNodeData } from '@/components/business/ms-tree/types';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useSelect from '@/hooks/useSelect';
|
||||
import { filterTreeNode, findNodeByKey } from '@/utils';
|
||||
import { findNodeByKey, getGenerateId } from '@/utils';
|
||||
|
||||
import type { TreeFieldNames, TreeNodeData } from '@arco-design/web-vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
data: TreeNodeData[];
|
||||
fieldNames?: TreeFieldNames;
|
||||
disabled?: boolean;
|
||||
fieldNames?: TreeFieldNames | MsTreeFieldNames;
|
||||
multiple?: boolean;
|
||||
shouldCalculateMaxTag?: boolean;
|
||||
treeCheckStrictly?: boolean;
|
||||
treeCheckable?: boolean;
|
||||
}>(),
|
||||
{
|
||||
shouldCalculateMaxTag: true,
|
||||
}
|
||||
);
|
||||
const selectValue = defineModel<any>('modelValue', { required: true });
|
||||
|
||||
const inputValue = ref('');
|
||||
const tempInputValue = ref('');
|
||||
const { t } = useI18n();
|
||||
|
||||
const treeSelectId = ref(getGenerateId()); // 区分同时有两个组件的情况
|
||||
const viewSelectOptionVisible = ref(false);
|
||||
|
||||
const selectValue = defineModel<Array<string | number>>('modelValue', { required: true, default: [] });
|
||||
const treeData = defineModel<TreeNodeData[]>('data', { required: true });
|
||||
const selectedModuleProps = ref({
|
||||
modulesTree: treeData.value,
|
||||
moduleCount: {},
|
||||
});
|
||||
const { selectedModulesMaps, checkedKeys, halfCheckedKeys, selectParent, checkNode, clearSelector } =
|
||||
useTreeSelection(selectedModuleProps.value);
|
||||
|
||||
const skipSelectValueWatch = ref(false);
|
||||
watch(
|
||||
() => selectValue.value,
|
||||
(newValue) => {
|
||||
if (!skipSelectValueWatch.value && !isEqual(checkedKeys.value, newValue)) {
|
||||
clearSelector();
|
||||
(newValue ?? []).forEach((id) => {
|
||||
selectedModulesMaps.value[id] = {
|
||||
selectAll: true,
|
||||
selectIds: new Set(),
|
||||
excludeIds: new Set(),
|
||||
count: 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
skipSelectValueWatch.value = false;
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
const treeSelectRef = ref();
|
||||
const { maxTagCount, calculateMaxTag } = useSelect({
|
||||
selectRef: treeSelectRef,
|
||||
selectVal: selectValue,
|
||||
selectVal: checkedKeys,
|
||||
});
|
||||
watch(
|
||||
() => selectValue.value,
|
||||
() => {
|
||||
() => checkedKeys.value,
|
||||
(newValue) => {
|
||||
if (!isEqual(selectValue.value, newValue)) {
|
||||
// 赋值selectValue,但是不触发watch selectValue
|
||||
skipSelectValueWatch.value = true;
|
||||
selectValue.value = [...newValue];
|
||||
}
|
||||
if (props.shouldCalculateMaxTag !== false && props.multiple) {
|
||||
calculateMaxTag();
|
||||
}
|
||||
|
@ -95,10 +183,10 @@
|
|||
const getTreeSelectTooltip = computed(() => {
|
||||
return () => {
|
||||
let treeSelectTooltip = '';
|
||||
const values = Array.isArray(selectValue.value) ? selectValue.value : [selectValue.value];
|
||||
const values = Array.isArray(checkedKeys.value) ? checkedKeys.value : [checkedKeys.value];
|
||||
if (props.data) {
|
||||
treeSelectTooltip = values
|
||||
?.map((valueItem: string) => {
|
||||
?.map((valueItem: string | number) => {
|
||||
const optItem = findNodeByKey<MsTreeNodeData>(
|
||||
props.data as MsTreeNodeData[],
|
||||
valueItem,
|
||||
|
@ -114,21 +202,31 @@
|
|||
};
|
||||
});
|
||||
|
||||
const inputValue = ref('');
|
||||
const tempInputValue = ref('');
|
||||
|
||||
const isPopupVisibleChanging = ref(false);
|
||||
function handlePopupVisibleChange() {
|
||||
isPopupVisibleChanging.value = true;
|
||||
setTimeout(() => {
|
||||
isPopupVisibleChanging.value = false;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理输入框搜索值变化
|
||||
* @param val 搜索值
|
||||
*/
|
||||
function handleInputValueChange(val: string) {
|
||||
inputValue.value = val;
|
||||
if (val !== '') {
|
||||
// 只存储有值的搜索值,因为当搜索完选中一个选项后,arco-tree-select 会自动清空输入框,这里需要过滤掉
|
||||
tempInputValue.value = val;
|
||||
}
|
||||
}
|
||||
function handlePopupVisibleChange(val: boolean) {
|
||||
if (!val) {
|
||||
inputValue.value = '';
|
||||
tempInputValue.value = '';
|
||||
async function handleInputValueChange(val: string) {
|
||||
// 搜索后选中一个节点,treeSelect组件会触发popupVisibleChange,进而清空inputValue,这时需要手动将inputValue还原
|
||||
if (isPopupVisibleChanging.value) {
|
||||
inputValue.value = tempInputValue.value;
|
||||
} else {
|
||||
inputValue.value = val;
|
||||
if (val !== '') {
|
||||
// 只存储有值的搜索值
|
||||
tempInputValue.value = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleChange() {
|
||||
|
@ -145,7 +243,37 @@
|
|||
}
|
||||
function handleClear() {
|
||||
tempInputValue.value = '';
|
||||
clearSelector();
|
||||
}
|
||||
|
||||
const triggerWidth = ref<number>(280); // 下拉框的宽度
|
||||
function updateTriggerWidth() {
|
||||
const treeSelectInput = document.getElementById(treeSelectId.value) as HTMLElement;
|
||||
if (treeSelectInput) {
|
||||
triggerWidth.value = treeSelectInput.offsetWidth; // 设置成输入框的宽度
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => viewSelectOptionVisible.value,
|
||||
(val: boolean) => {
|
||||
if (!val) {
|
||||
inputValue.value = '';
|
||||
tempInputValue.value = '';
|
||||
} else {
|
||||
updateTriggerWidth();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const virtualListProps = computed(() => {
|
||||
return {
|
||||
threshold: 300,
|
||||
height: 280,
|
||||
fixedSize: true,
|
||||
buffer: 15, // 缓冲区默认 10 的时候,虚拟滚动的底部 padding 计算有问题
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
|
@ -154,4 +282,8 @@
|
|||
max-height: 150px;
|
||||
.ms-scroll-bar();
|
||||
}
|
||||
.tree-select-trigger-content {
|
||||
max-height: 300px;
|
||||
.ms-scroll-bar();
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
:data="moduleTree"
|
||||
allow-clear
|
||||
:multiple="true"
|
||||
tree-check-strictly
|
||||
:tree-checkable="true"
|
||||
:placeholder="t('common.pleaseSelect')"
|
||||
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||
|
|
Loading…
Reference in New Issue