(component): 处理 tree 组件 n 多问题

This commit is contained in:
就眠儀式 2022-10-02 22:25:02 +08:00
parent 15372347a3
commit 49fcb42d9e
14 changed files with 737 additions and 52 deletions

View File

@ -6,7 +6,7 @@ export default {
<script setup lang="ts"> <script setup lang="ts">
import { LayIcon } from "@layui/icons-vue"; import { LayIcon } from "@layui/icons-vue";
import { computed, inject } from "vue"; import { computed, inject, useSlots } from "vue";
import "./index.less"; import "./index.less";
export interface LayCheckboxProps { export interface LayCheckboxProps {
@ -38,6 +38,8 @@ const isGroup = computed(() => {
const emit = defineEmits(["update:modelValue", "change"]); const emit = defineEmits(["update:modelValue", "change"]);
const slots = useSlots();
const isChecked = computed({ const isChecked = computed({
get() { get() {
if (isGroup.value) { if (isGroup.value) {
@ -123,7 +125,7 @@ const isDisabled = computed(() => {
}" }"
:lay-skin="skin" :lay-skin="skin"
> >
<span class="layui-checkbox-label"> <span class="layui-checkbox-label" v-if="slots.default || label">
<slot>{{ label }}</slot> <slot>{{ label }}</slot>
</span> </span>
<lay-icon <lay-icon

View File

@ -31,6 +31,7 @@ export interface TreeNodeProps {
nodeList: TreeData[]; nodeList: TreeData[];
showCheckbox: boolean; showCheckbox: boolean;
showLine: boolean; showLine: boolean;
checkStrictly: boolean;
collapseTransition: boolean; collapseTransition: boolean;
onlyIconControl: boolean; onlyIconControl: boolean;
} }
@ -75,7 +76,7 @@ function recursiveNodeClick(node: TreeData) {
} }
function handleChange(checked: boolean, node: TreeData) { function handleChange(checked: boolean, node: TreeData) {
props.tree.setCheckedKeys(checked, node); props.tree.setCheckedKeys(checked, props.checkStrictly, node);
} }
function handleIconClick(node: TreeData) { function handleIconClick(node: TreeData) {
@ -86,8 +87,11 @@ function handleTitleClick(node: TreeData) {
if (!props.onlyIconControl) { if (!props.onlyIconControl) {
handleIconClick(node); handleIconClick(node);
} }
emit("node-click", node); if (!node.isDisabled) {
emit("node-click", node);
}
} }
function handleRowClick(node: TreeData) { function handleRowClick(node: TreeData) {
if (!props.showLine) { if (!props.showLine) {
handleTitleClick(node); handleTitleClick(node);
@ -96,6 +100,7 @@ function handleRowClick(node: TreeData) {
// //
const isChildAllSelected = computed(() => { const isChildAllSelected = computed(() => {
function _isChildAllSelected(node: TreeData): boolean { function _isChildAllSelected(node: TreeData): boolean {
if (!props.showCheckbox) { if (!props.showCheckbox) {
return false; return false;
@ -117,9 +122,14 @@ const isChildAllSelected = computed(() => {
} }
return res; return res;
} }
return function (node: TreeData): boolean { return function (node: TreeData): boolean {
let res = _isChildAllSelected(node); if(props.checkStrictly) {
return res; return false;
} else {
let res = _isChildAllSelected(node);
return res;
}
}; };
}); });
</script> </script>
@ -147,18 +157,14 @@ const isChildAllSelected = computed(() => {
@click.stop="handleIconClick(node)" @click.stop="handleIconClick(node)"
/> />
</span> </span>
<lay-checkbox <lay-checkbox
v-if="showCheckbox" value="miss"
skin="primary"
:modelValue="node.isChecked" :modelValue="node.isChecked"
:disabled="node.isDisabled" :disabled="node.isDisabled"
skin="primary"
value=""
@change="
(checked) => {
handleChange(checked, node);
}
"
:isIndeterminate="isChildAllSelected(node)" :isIndeterminate="isChildAllSelected(node)"
@change="(checked) => handleChange(checked, node)"
v-if="showCheckbox"
/> />
<span <span
:class="{ :class="{
@ -183,11 +189,12 @@ const isChildAllSelected = computed(() => {
style="display: block" style="display: block"
> >
<tree-node <tree-node
:tree="tree"
:node-list="node.children" :node-list="node.children"
:show-checkbox="showCheckbox" :show-checkbox="showCheckbox"
:show-line="showLine" :show-line="showLine"
:collapse-transition="collapseTransition" :collapse-transition="collapseTransition"
:tree="tree" :checkStrictly="checkStrictly"
:only-icon-control="onlyIconControl" :only-icon-control="onlyIconControl"
@node-click="recursiveNodeClick" @node-click="recursiveNodeClick"
> >

View File

@ -24,6 +24,7 @@ export interface TreeProps {
checkedKeys?: KeysType; checkedKeys?: KeysType;
data: OriginalTreeData; data: OriginalTreeData;
showCheckbox?: boolean; showCheckbox?: boolean;
checkStrictly: boolean;
edit?: EditType; edit?: EditType;
collapseTransition?: boolean; collapseTransition?: boolean;
onlyIconControl?: boolean; onlyIconControl?: boolean;
@ -49,6 +50,7 @@ const props = withDefaults(defineProps<TreeProps>(), {
showCheckbox: false, showCheckbox: false,
edit: false, edit: false,
collapseTransition: true, collapseTransition: true,
checkStrictly: false,
onlyIconControl: false, onlyIconControl: false,
disabled: false, disabled: false,
showLine: true, showLine: true,
@ -77,11 +79,13 @@ let tree = ref();
let nodeList = ref(); let nodeList = ref();
const unWatch = ref(false); const unWatch = ref(false);
const initStatus = ref(false); const initStatus = ref(false);
const loadNodeList = () => { const loadNodeList = () => {
let { tree: _tree, nodeList: _nodeList } = useTree(props, emit); let { tree: _tree, nodeList: _nodeList } = useTree(props, emit);
tree.value = _tree; tree.value = _tree;
nodeList.value = _nodeList.value; nodeList.value = _nodeList.value;
}; };
watch( watch(
() => props.data, () => props.data,
() => { () => {
@ -89,6 +93,7 @@ watch(
}, },
{ deep: true, immediate: true } { deep: true, immediate: true }
); );
watch( watch(
() => props.checkedKeys, () => props.checkedKeys,
() => { () => {
@ -97,6 +102,7 @@ watch(
} }
} }
); );
watch( watch(
tree, tree,
() => { () => {
@ -130,6 +136,7 @@ function handleClick(node: TreeData) {
:node-list="nodeList" :node-list="nodeList"
:show-checkbox="showCheckbox" :show-checkbox="showCheckbox"
:show-line="showLine" :show-line="showLine"
:check-strictly="checkStrictly"
:collapse-transition="collapseTransition" :collapse-transition="collapseTransition"
:only-icon-control="onlyIconControl" :only-icon-control="onlyIconControl"
@node-click="handleClick" @node-click="handleClick"

View File

@ -25,6 +25,7 @@ interface ReplaceFields {
} }
interface TreeConfig { interface TreeConfig {
checkStrictly: boolean;
showCheckbox: boolean; showCheckbox: boolean;
checkedKeys: StringOrNumber[]; checkedKeys: StringOrNumber[];
expandKeys: StringOrNumber[]; expandKeys: StringOrNumber[];
@ -156,15 +157,15 @@ class Tree {
} }
} }
setCheckedKeys(checked: boolean, node: TreeData) { setCheckedKeys(checked: boolean, checkStrictly: boolean, node: TreeData) {
node.isChecked = checked; node.isChecked = checked;
// 处理上级 if (!checkStrictly) {
if (node.parentNode) { if (node.parentNode) {
this.setParentChecked(checked, node.parentNode); this.setParentChecked(checked, node.parentNode);
} }
// 处理下级 if (node.children) {
if (node.children) { this.setChildrenChecked(checked, node.children);
this.setChildrenChecked(checked, node.children); }
} }
} }

View File

@ -15,6 +15,7 @@ export interface TreeProps {
checkedKeys?: KeysType; checkedKeys?: KeysType;
expandKeys?: KeysType; expandKeys?: KeysType;
data: OriginalTreeData; data: OriginalTreeData;
checkStrictly?: boolean;
showCheckbox?: boolean; showCheckbox?: boolean;
edit?: EditType; edit?: EditType;
collapseTransition?: boolean; collapseTransition?: boolean;
@ -26,7 +27,6 @@ export interface TreeProps {
title?: string; title?: string;
}; };
} }
export interface TreeEmits { export interface TreeEmits {
(e: "update:checkedKeys", keys: KeysType): void; (e: "update:checkedKeys", keys: KeysType): void;
(e: "update:expandKeys", keys: KeysType): void; (e: "update:expandKeys", keys: KeysType): void;

View File

@ -23,6 +23,7 @@ export const useTree: UseTree = (props: TreeProps, emit: TreeEmits) => {
showCheckbox: props.showCheckbox ?? false, showCheckbox: props.showCheckbox ?? false,
checkedKeys: props.checkedKeys ?? [], checkedKeys: props.checkedKeys ?? [],
expandKeys: props.expandKeys ?? [], expandKeys: props.expandKeys ?? [],
checkStrictly: props.checkStrictly ?? false
}, },
props.data props.data
); );

View File

@ -0,0 +1,17 @@
.layui-tree-select {
width: 220px;
}
.layui-tree-select-content {
padding: 10px;
}
.layui-tree-select .layui-icon-triangle-d {
transition: all 0.3s;
-webkit-transition: all 0.3s;
color: var(--global-neutral-color-8);
}
.layui-tree-select .layui-icon-triangle-d.triangle {
transform: rotate(180deg);
}

View File

@ -5,12 +5,20 @@ export default {
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from "vue"; import "./index.less";
import { computed, ref, watch } from "vue";
import { getNode } from "../../utils/treeUtil";
export interface LayTreeSelect { export interface LayTreeSelect {
data: any, data: any;
modelValue: string; modelValue: any;
disabled: boolean; disabled?: boolean;
placeholder?: string;
multiple?: boolean;
allowClear?: boolean;
collapseTagsTooltip?: boolean;
minCollapsedNum?: number;
size?: string;
} }
export interface TreeSelectEmits { export interface TreeSelectEmits {
@ -20,13 +28,21 @@ export interface TreeSelectEmits {
} }
const props = withDefaults(defineProps<LayTreeSelect>(), { const props = withDefaults(defineProps<LayTreeSelect>(), {
disabled: false disabled: false,
placeholder: "请选择",
multiple: false,
allowClear: false,
collapseTagsTooltip: true,
minCollapsedNum: 3,
size: "md",
}); });
const singleValue = ref(); const singleValue = ref();
const multipleValue = ref(["1"]);
const dropdownRef = ref(); const dropdownRef = ref();
const openState = ref(false); const openState = ref(false);
const emits = defineEmits<TreeSelectEmits>(); const emits = defineEmits<TreeSelectEmits>();
const selectedValue = computed({ const selectedValue = computed({
get() { get() {
return props.modelValue; return props.modelValue;
@ -34,40 +50,85 @@ const selectedValue = computed({
set(value) { set(value) {
emits("update:modelValue", value); emits("update:modelValue", value);
emits("change", value); emits("change", value);
} },
}) });
watch(
[selectedValue],
() => {
if (props.multiple) {
multipleValue.value = selectedValue.value.map((value: any) => {
const node: any = getNode(props.data, value);
node.label = node.title;
return node;
});
} else {
singleValue.value = props.data.find((item: any) => {
return item.value === selectedValue.value;
})?.label;
}
},
{ immediate: true, deep: true }
);
const handleClick = (node: any) => { const handleClick = (node: any) => {
dropdownRef.value.hide(); dropdownRef.value.hide();
selectedValue.value = node.id; selectedValue.value = node.id;
singleValue.value = node.title; };
}
</script> </script>
<template> <template>
<div class="layui-tree-select"> <div class="layui-tree-select" :class="{ 'layui-disabled': disabled }">
<lay-dropdown <lay-dropdown
ref="dropdownRef" ref="dropdownRef"
:disabled="disabled" :disabled="disabled"
:update-at-scroll="true" :update-at-scroll="true"
@show="openState = true" @show="openState = true"
@hide="openState = false"> @hide="openState = false"
<lay-input v-model="singleValue"></lay-input> >
<lay-tag-input
v-if="multiple"
v-model="multipleValue"
:size="size"
:allow-clear="allowClear"
:placeholder="placeholder"
:collapseTagsTooltip="collapseTagsTooltip"
:minCollapsedNum="minCollapsedNum"
:disabledInput="true"
>
<template #suffix>
<lay-icon
type="layui-icon-triangle-d"
:class="{ triangle: openState }"
></lay-icon>
</template>
</lay-tag-input>
<lay-input
v-else
v-model="singleValue"
:size="size"
:placeholder="placeholder"
:disabled="disabled"
:readonly="true"
>
<template #suffix>
<lay-icon
type="layui-icon-triangle-d"
:class="{ triangle: openState }"
></lay-icon>
</template>
</lay-input>
<template #content> <template #content>
<div class="layui-tree-select-content"> <div class="layui-tree-select-content">
<lay-tree :data="data" :onlyIconControl="true" @node-click="handleClick"></lay-tree> <lay-tree
:data="data"
:onlyIconControl="true"
:show-checkbox="multiple"
v-model:checkedKeys="selectedValue"
@node-click="handleClick"
></lay-tree>
</div> </div>
</template> </template>
</lay-dropdown> </lay-dropdown>
</div> </div>
</template> </template>
<style scoped>
.layui-tree-select {
width: 220px;
}
.layui-tree-select-content {
padding: 10px;
}
</style>

View File

@ -181,7 +181,7 @@ const components: Record<string, Plugin> = {
LaySpace, LaySpace,
LayTag, LayTag,
LayTagInput, LayTagInput,
LayTreeSelect LayTreeSelect,
}; };
const install = (app: App, options?: InstallOptions): void => { const install = (app: App, options?: InstallOptions): void => {

View File

@ -0,0 +1,34 @@
export function getNode(root: any, id: string) {
let resultNode = null;
findNode(root, id);
function findNode(root: any, id: string){
if(!!root) {
let type = Object.prototype.toString.call(root);
if(type === '[object Object]') {
if(root.id && root.id === id) {
resultNode = root
} else {
let node = root.children || null
findNode(node, id);
}
}else if(type === '[object Array]') {
let needNode = root.find((i:any) => !!i === true && i.id === id);
if(!!needNode) {
resultNode = needNode;
} else {
if(root.length) {
root.forEach((item: any)=>{
if(item && item.children) {
let node = item.children
if(node && node.length){
findNode(node, id);
}
}
})
}
}
}
}
}
return resultNode;
}

View File

@ -381,6 +381,191 @@ const showCheckbox = ref(true)
::: :::
::: title 禁用级联
:::
::: demo 使用 `showCheckbox` 属性开启复选框
<template>
<lay-tree
v-model:checkedKeys="checkedKeys"
:showCheckbox="showCheckbox"
:checkStrictly="true"
:collapse-transition="true"
:data="data2"
>
</lay-tree>
</template>
<script setup>
import { ref } from 'vue';
const data2 = ref([{
title: '一级1',
id: 1,
field: 'name1',
checked: true,
spread: true,
children: [{
title: '二级1-1 可允许跳转',
id: 3,
field: 'name11',
href: 'https://www.layui.com/',
children: [{
title: '三级1-1-3',
id: 23,
field: '',
children: [{
title: '四级1-1-3-1',
id: 24,
field: '',
children: [{
title: '五级1-1-3-1-1',
id: 30,
field: ''
},
{
title: '五级1-1-3-1-2',
id: 31,
field: ''
}]
}]
},
{
title: '三级1-1-1',
id: 7,
field: '',
children: [{
title: '四级1-1-1-1 可允许跳转',
id: 15,
field: '',
href: 'https://www.layui.com/doc/'
}]
},
{
title: '三级1-1-2',
id: 8,
field: '',
children: [{
title: '四级1-1-2-1',
id: 32,
field: ''
}]
}]
},
{
title: '二级1-2',
id: 4,
spread: true,
children: [{
title: '三级1-2-1',
id: 9,
field: '',
disabled: true
},
{
title: '三级1-2-2',
id: 10,
field: ''
}]
},
{
title: '二级1-3',
id: 20,
field: '',
children: [{
title: '三级1-3-1',
id: 21,
field: ''
},
{
title: '三级1-3-2',
id: 22,
field: ''
}]
}]
},
{
title: '一级2',
id: 2,
field: '',
spread: true,
children: [{
title: '二级2-1',
id: 5,
field: '',
spread: true,
children: [{
title: '三级2-1-1',
id: 11,
field: ''
},
{
title: '三级2-1-2',
id: 12,
field: ''
}]
},
{
title: '二级2-2',
id: 6,
field: '',
children: [{
title: '三级2-2-1',
id: 13,
field: ''
},
{
title: '三级2-2-2',
id: 14,
field: '',
disabled: true
}]
}]
},
{
title: '一级3',
id: 16,
field: '',
children: [{
title: '二级3-1',
id: 17,
field: '',
fixed: true,
children: [{
title: '三级3-1-1',
id: 18,
field: ''
},
{
title: '三级3-1-2',
id: 19,
field: ''
}]
},
{
title: '二级3-2',
id: 27,
field: '',
children: [{
title: '三级3-2-1',
id: 28,
field: ''
},
{
title: '三级3-2-2',
id: 29,
field: ''
}]
}]
}]);
const checkedKeys = ref([2,3])
const showCheckbox = ref(true)
</script>
:::
::: title 关闭连线 ::: title 关闭连线
::: :::

View File

@ -188,6 +188,360 @@ function handleClick(node) {
::: :::
::: title 启用多选
:::
::: demo 使用 `lay-tree` 标签, 创建树形组件, @node-click 监听节点点击。
<template>
<lay-tree-select v-model="value1" :multiple="true" :data="data1" ></lay-tree-select> {{ value1 }}
</template>
<script setup>
import { ref } from 'vue';
const value1 = ref([]);
const data1 = ref([{
title: '一级1',
id: 1,
field: 'name1',
checked: true,
spread: true,
children: [{
title: '二级1-1 可允许跳转',
id: 3,
field: 'name11',
href: 'https://www.layui.com/',
children: [{
title: '三级1-1-3',
id: 23,
field: '',
children: [{
title: '四级1-1-3-1',
id: 24,
field: '',
children: [{
title: '五级1-1-3-1-1',
id: 30,
field: ''
},
{
title: '五级1-1-3-1-2',
id: 31,
field: ''
}]
}]
},
{
title: '三级1-1-1',
id: 7,
field: '',
children: [{
title: '四级1-1-1-1 可允许跳转',
id: 15,
field: '',
href: 'https://www.layui.com/doc/'
}]
},
{
title: '三级1-1-2',
id: 8,
field: '',
children: [{
title: '四级1-1-2-1',
id: 32,
field: ''
}]
}]
},
{
title: '二级1-2',
id: 4,
spread: true,
children: [{
title: '三级1-2-1',
id: 9,
field: '',
disabled: true
},
{
title: '三级1-2-2',
id: 10,
field: ''
}]
},
{
title: '二级1-3',
id: 20,
field: '',
children: [{
title: '三级1-3-1',
id: 21,
field: ''
},
{
title: '三级1-3-2',
id: 22,
field: ''
}]
}]
},
{
title: '一级2',
id: 2,
field: '',
spread: true,
children: [{
title: '二级2-1',
id: 5,
field: '',
spread: true,
children: [{
title: '三级2-1-1',
id: 11,
field: ''
},
{
title: '三级2-1-2',
id: 12,
field: ''
}]
},
{
title: '二级2-2',
id: 6,
field: '',
children: [{
title: '三级2-2-1',
id: 13,
field: ''
},
{
title: '三级2-2-2',
id: 14,
field: '',
disabled: true
}]
}]
},
{
title: '一级3',
id: 16,
field: '',
children: [{
title: '二级3-1',
id: 17,
field: '',
fixed: true,
children: [{
title: '三级3-1-1',
id: 18,
field: ''
},
{
title: '三级3-1-2',
id: 19,
field: ''
}]
},
{
title: '二级3-2',
id: 27,
field: '',
children: [{
title: '三级3-2-1',
id: 28,
field: ''
},
{
title: '三级3-2-2',
id: 29,
field: ''
}]
}]
}]);
</script>
:::
::: title 禁用选择
:::
::: demo 使用 `lay-tree` 标签, 创建树形组件, @node-click 监听节点点击。
<template>
<lay-tree-select v-model="value" :data="data" :disabled="true"></lay-tree-select>
</template>
<script setup>
import { ref } from 'vue';
const value = ref();
const data = ref([{
title: '一级1',
id: 1,
field: 'name1',
checked: true,
spread: true,
children: [{
title: '二级1-1 可允许跳转',
id: 3,
field: 'name11',
href: 'https://www.layui.com/',
children: [{
title: '三级1-1-3',
id: 23,
field: '',
children: [{
title: '四级1-1-3-1',
id: 24,
field: '',
children: [{
title: '五级1-1-3-1-1',
id: 30,
field: ''
},
{
title: '五级1-1-3-1-2',
id: 31,
field: ''
}]
}]
},
{
title: '三级1-1-1',
id: 7,
field: '',
children: [{
title: '四级1-1-1-1 可允许跳转',
id: 15,
field: '',
href: 'https://www.layui.com/doc/'
}]
},
{
title: '三级1-1-2',
id: 8,
field: '',
children: [{
title: '四级1-1-2-1',
id: 32,
field: ''
}]
}]
},
{
title: '二级1-2',
id: 4,
spread: true,
children: [{
title: '三级1-2-1',
id: 9,
field: '',
disabled: true
},
{
title: '三级1-2-2',
id: 10,
field: ''
}]
},
{
title: '二级1-3',
id: 20,
field: '',
children: [{
title: '三级1-3-1',
id: 21,
field: ''
},
{
title: '三级1-3-2',
id: 22,
field: ''
}]
}]
},
{
title: '一级2',
id: 2,
field: '',
spread: true,
children: [{
title: '二级2-1',
id: 5,
field: '',
spread: true,
children: [{
title: '三级2-1-1',
id: 11,
field: ''
},
{
title: '三级2-1-2',
id: 12,
field: ''
}]
},
{
title: '二级2-2',
id: 6,
field: '',
children: [{
title: '三级2-2-1',
id: 13,
field: ''
},
{
title: '三级2-2-2',
id: 14,
field: '',
disabled: true
}]
}]
},
{
title: '一级3',
id: 16,
field: '',
children: [{
title: '二级3-1',
id: 17,
field: '',
fixed: true,
children: [{
title: '三级3-1-1',
id: 18,
field: ''
},
{
title: '三级3-1-2',
id: 19,
field: ''
}]
},
{
title: '二级3-2',
id: 27,
field: '',
children: [{
title: '三级3-2-1',
id: 28,
field: ''
},
{
title: '三级3-2-2',
id: 29,
field: ''
}]
}]
}]);
</script>
:::
::: title Tree 属性 ::: title Tree 属性
::: :::

View File

@ -10,6 +10,21 @@
::: demo ::: demo
<template> <template>
<lay-timeline> <lay-timeline>
<lay-timeline-item title="1.6.x">
<ul>
<a name="1-6-0"></a>
<li>
<h3>1.6.0 <span class="layui-badge-rim">2022-10-08</span></h3>
<ul>
<li>[新增] tree-select 下拉选择树组件, 提供树结构数据选择。</li>
<li>[新增] tree 组件 checkStrictly 属性, 开启复选框时解除父子联动关系, 默认为 false。</li>
<li>[修复] tree 组件 node 配置 disabled 启用时, @node-click 事件仍触发的问题。</li>
<li>[修复] checkbox 组件 label 属性与 default 插槽不设置, layui-checkbox-label 元素仍存在的问题。</li>
<li>[修复] tree 组件 show-checkbox 为 true 时, 复选框与标题间距过宽的问题。</li>
</ul>
</li>
</ul>
</lay-timeline-item>
<lay-timeline-item title="1.5.x"> <lay-timeline-item title="1.5.x">
<ul> <ul>
<a name="1-5-1"></a> <a name="1-5-1"></a>

View File

@ -238,7 +238,8 @@ const zhCN = [
}, },
{ {
path: "/zh-CN/components/treeSelect", path: "/zh-CN/components/treeSelect",
component: () => import("../document/zh-CN/components/treeSelect.md"), component: () =>
import("../document/zh-CN/components/treeSelect.md"),
meta: { title: "下拉树组件" }, meta: { title: "下拉树组件" },
}, },
{ {