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) => {
|
Object.keys(selectedModulesMaps.value).forEach((key) => {
|
||||||
delete selectedModulesMaps.value[key];
|
delete selectedModulesMaps.value[key];
|
||||||
});
|
});
|
||||||
|
checkedKeys.value = [];
|
||||||
isCheckedAll.value = false;
|
isCheckedAll.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -322,7 +322,7 @@
|
||||||
filterTreeData.value = data.value;
|
filterTreeData.value = data.value;
|
||||||
}
|
}
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
treeRef.value?.expandAll(false);
|
treeRef.value?.expandAll(props.defaultExpandAll ?? false);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
updateDebouncedSearch();
|
updateDebouncedSearch();
|
||||||
|
|
|
@ -1,80 +1,168 @@
|
||||||
<template>
|
<template>
|
||||||
<a-tooltip
|
<a-tooltip
|
||||||
:content="getTreeSelectTooltip()"
|
:content="getTreeSelectTooltip()"
|
||||||
:disabled="!(modelValue ?? []).length"
|
:disabled="!(checkedKeys ?? []).length"
|
||||||
position="top"
|
position="top"
|
||||||
content-class="tree-select-content"
|
content-class="tree-select-content"
|
||||||
:mouse-enter-delay="300"
|
:mouse-enter-delay="300"
|
||||||
>
|
>
|
||||||
<a-tree-select
|
<a-trigger
|
||||||
ref="treeSelectRef"
|
v-model:popup-visible="viewSelectOptionVisible"
|
||||||
v-model:model-value="selectValue"
|
:trigger="['click']"
|
||||||
v-model:input-value="inputValue"
|
:click-to-close="false"
|
||||||
:data="props.data"
|
:popup-translate="[0, 4]"
|
||||||
:disabled="props.disabled"
|
content-class="arco-trigger-menu tree-select-trigger-content"
|
||||||
:multiple="props.multiple"
|
:content-style="{ width: `${triggerWidth}px` }"
|
||||||
: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"
|
|
||||||
>
|
>
|
||||||
<template #label="{ data: slotData }">
|
<a-tree-select
|
||||||
<div class="one-line-text">{{ slotData.label }}</div>
|
: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>
|
||||||
<template #tree-slot-title="node">
|
</a-trigger>
|
||||||
<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-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 useSelect from '@/hooks/useSelect';
|
||||||
import { filterTreeNode, findNodeByKey } from '@/utils';
|
import { findNodeByKey, getGenerateId } from '@/utils';
|
||||||
|
|
||||||
import type { TreeFieldNames, TreeNodeData } from '@arco-design/web-vue';
|
import type { TreeFieldNames, TreeNodeData } from '@arco-design/web-vue';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
data: TreeNodeData[];
|
data: TreeNodeData[];
|
||||||
fieldNames?: TreeFieldNames;
|
fieldNames?: TreeFieldNames | MsTreeFieldNames;
|
||||||
disabled?: boolean;
|
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
shouldCalculateMaxTag?: boolean;
|
shouldCalculateMaxTag?: boolean;
|
||||||
|
treeCheckStrictly?: boolean;
|
||||||
|
treeCheckable?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
shouldCalculateMaxTag: true,
|
shouldCalculateMaxTag: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const selectValue = defineModel<any>('modelValue', { required: true });
|
|
||||||
|
|
||||||
const inputValue = ref('');
|
const { t } = useI18n();
|
||||||
const tempInputValue = ref('');
|
|
||||||
|
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 treeSelectRef = ref();
|
||||||
const { maxTagCount, calculateMaxTag } = useSelect({
|
const { maxTagCount, calculateMaxTag } = useSelect({
|
||||||
selectRef: treeSelectRef,
|
selectRef: treeSelectRef,
|
||||||
selectVal: selectValue,
|
selectVal: checkedKeys,
|
||||||
});
|
});
|
||||||
watch(
|
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) {
|
if (props.shouldCalculateMaxTag !== false && props.multiple) {
|
||||||
calculateMaxTag();
|
calculateMaxTag();
|
||||||
}
|
}
|
||||||
|
@ -95,10 +183,10 @@
|
||||||
const getTreeSelectTooltip = computed(() => {
|
const getTreeSelectTooltip = computed(() => {
|
||||||
return () => {
|
return () => {
|
||||||
let treeSelectTooltip = '';
|
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) {
|
if (props.data) {
|
||||||
treeSelectTooltip = values
|
treeSelectTooltip = values
|
||||||
?.map((valueItem: string) => {
|
?.map((valueItem: string | number) => {
|
||||||
const optItem = findNodeByKey<MsTreeNodeData>(
|
const optItem = findNodeByKey<MsTreeNodeData>(
|
||||||
props.data as MsTreeNodeData[],
|
props.data as MsTreeNodeData[],
|
||||||
valueItem,
|
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 搜索值
|
* @param val 搜索值
|
||||||
*/
|
*/
|
||||||
function handleInputValueChange(val: string) {
|
async function handleInputValueChange(val: string) {
|
||||||
inputValue.value = val;
|
// 搜索后选中一个节点,treeSelect组件会触发popupVisibleChange,进而清空inputValue,这时需要手动将inputValue还原
|
||||||
if (val !== '') {
|
if (isPopupVisibleChanging.value) {
|
||||||
// 只存储有值的搜索值,因为当搜索完选中一个选项后,arco-tree-select 会自动清空输入框,这里需要过滤掉
|
inputValue.value = tempInputValue.value;
|
||||||
tempInputValue.value = val;
|
} else {
|
||||||
}
|
inputValue.value = val;
|
||||||
}
|
if (val !== '') {
|
||||||
function handlePopupVisibleChange(val: boolean) {
|
// 只存储有值的搜索值
|
||||||
if (!val) {
|
tempInputValue.value = val;
|
||||||
inputValue.value = '';
|
}
|
||||||
tempInputValue.value = '';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function handleChange() {
|
function handleChange() {
|
||||||
|
@ -145,7 +243,37 @@
|
||||||
}
|
}
|
||||||
function handleClear() {
|
function handleClear() {
|
||||||
tempInputValue.value = '';
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
|
@ -154,4 +282,8 @@
|
||||||
max-height: 150px;
|
max-height: 150px;
|
||||||
.ms-scroll-bar();
|
.ms-scroll-bar();
|
||||||
}
|
}
|
||||||
|
.tree-select-trigger-content {
|
||||||
|
max-height: 300px;
|
||||||
|
.ms-scroll-bar();
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
:data="moduleTree"
|
:data="moduleTree"
|
||||||
allow-clear
|
allow-clear
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
|
tree-check-strictly
|
||||||
:tree-checkable="true"
|
:tree-checkable="true"
|
||||||
:placeholder="t('common.pleaseSelect')"
|
:placeholder="t('common.pleaseSelect')"
|
||||||
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||||
|
|
Loading…
Reference in New Issue