init
This commit is contained in:
205
src/component/tree/TreeNode.vue
Normal file
205
src/component/tree/TreeNode.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "TreeNode",
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LayIcon } from "@layui/icons-vue";
|
||||
import LayCheckbox from "../checkbox/index.vue";
|
||||
import { computed, Ref, useSlots } from "vue";
|
||||
import { Tree } from "./tree";
|
||||
import { Nullable } from "../../types";
|
||||
import LayTransition from "../transition/index.vue";
|
||||
import { StringOrNumber, CustomKey, CustomString } from "./tree.type";
|
||||
|
||||
export interface TreeData {
|
||||
id: CustomKey;
|
||||
title: CustomString;
|
||||
children: TreeData[];
|
||||
parentKey: Nullable<StringOrNumber>;
|
||||
isRoot: boolean;
|
||||
isChecked: boolean;
|
||||
isDisabled: boolean;
|
||||
isLeaf: boolean;
|
||||
hasNextSibling: boolean;
|
||||
parentNode: Nullable<TreeData>;
|
||||
}
|
||||
|
||||
export interface TreeNodeProps {
|
||||
tree: Tree;
|
||||
nodeList: TreeData[];
|
||||
showCheckbox: boolean;
|
||||
showLine: boolean;
|
||||
selectedKey: any;
|
||||
checkStrictly: boolean | string;
|
||||
collapseTransition: boolean;
|
||||
onlyIconControl: boolean;
|
||||
}
|
||||
|
||||
interface TreeNodeEmits {
|
||||
(e: "node-click", node: TreeData): void;
|
||||
}
|
||||
|
||||
const slots = useSlots();
|
||||
const props = defineProps<TreeNodeProps>();
|
||||
const emit = defineEmits<TreeNodeEmits>();
|
||||
|
||||
function renderLineShort(node: TreeData) {
|
||||
return (
|
||||
!node.hasNextSibling &&
|
||||
node.parentNode &&
|
||||
// 外层最后一个
|
||||
(!node.parentNode.hasNextSibling ||
|
||||
//上一层父级有延伸线
|
||||
(node.parentNode.hasNextSibling && !node.parentNode.children))
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 展开收起 icon样式
|
||||
* @param node
|
||||
*/
|
||||
const nodeIconType = (node: TreeData): string => {
|
||||
if (!props.showLine) {
|
||||
if (node.children.length > 0) {
|
||||
return "layui-tree-iconArrow ";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
if (node.children.length !== 0) {
|
||||
return !node.isLeaf ? "layui-icon-addition" : "layui-icon-subtraction";
|
||||
}
|
||||
return "layui-icon-file";
|
||||
};
|
||||
|
||||
function recursiveNodeClick(node: TreeData) {
|
||||
emit("node-click", node);
|
||||
}
|
||||
|
||||
function handleChange(checked: boolean, node: TreeData) {
|
||||
props.tree.setCheckedKeys(checked, props.checkStrictly, node);
|
||||
}
|
||||
|
||||
function handleIconClick(node: TreeData) {
|
||||
node.isLeaf = !node.isLeaf;
|
||||
}
|
||||
|
||||
function handleTitleClick(node: TreeData) {
|
||||
if (!props.onlyIconControl) {
|
||||
handleIconClick(node);
|
||||
}
|
||||
if (!node.isDisabled) {
|
||||
emit("node-click", node);
|
||||
}
|
||||
}
|
||||
|
||||
function handleRowClick(node: TreeData) {
|
||||
if (!props.showLine) {
|
||||
handleTitleClick(node);
|
||||
}
|
||||
}
|
||||
|
||||
//判断是否半选
|
||||
const isChildAllSelected = computed(() => {
|
||||
function _isChildAllSelected(node: TreeData): boolean {
|
||||
if (!props.showCheckbox) {
|
||||
return false;
|
||||
}
|
||||
let childSelectNum = 0;
|
||||
let res = false; // true为半选 false为全选
|
||||
for (const item of node.children) {
|
||||
if (item.isChecked) childSelectNum++;
|
||||
}
|
||||
if (childSelectNum > 0) node.isChecked = true; //此处的处理与 checkedKeys 有关联
|
||||
if (childSelectNum == node.children.length) {
|
||||
//继续递归向下判断
|
||||
for (const item of node.children) {
|
||||
res = _isChildAllSelected(item);
|
||||
if (res) break;
|
||||
}
|
||||
} else {
|
||||
res = true;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
return (node: TreeData): boolean => {
|
||||
if (props.checkStrictly) {
|
||||
return false;
|
||||
} else {
|
||||
let res = _isChildAllSelected(node);
|
||||
return res;
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-for="(node, nodeIndex) in nodeList"
|
||||
:key="nodeIndex"
|
||||
:class="{
|
||||
'layui-tree-set': true,
|
||||
'layui-tree-setLineShort': renderLineShort(node),
|
||||
'layui-tree-setHide': node.isRoot,
|
||||
}"
|
||||
>
|
||||
<div class="layui-tree-entry" @click="handleRowClick(node)">
|
||||
<div class="layui-tree-main">
|
||||
<span
|
||||
:class="[
|
||||
showLine && node.children.length > 0 ? 'layui-tree-icon' : '',
|
||||
{ 'layui-tree-iconClick': true },
|
||||
]"
|
||||
>
|
||||
<lay-icon
|
||||
:type="nodeIconType(node)"
|
||||
@click.stop="handleIconClick(node)"
|
||||
/>
|
||||
</span>
|
||||
<lay-checkbox
|
||||
value=""
|
||||
skin="primary"
|
||||
:modelValue="node.isChecked"
|
||||
:disabled="node.isDisabled"
|
||||
:isIndeterminate="isChildAllSelected(node)"
|
||||
@change="(checked) => handleChange(checked, node)"
|
||||
v-if="showCheckbox"
|
||||
/>
|
||||
<span
|
||||
:class="{
|
||||
'layui-tree-txt': true,
|
||||
'layui-disabled': node.isDisabled,
|
||||
'layui-this': selectedKey === node.id,
|
||||
}"
|
||||
@click.stop="handleTitleClick(node)"
|
||||
>
|
||||
<slot name="title" :data="node">{{ node.title }}</slot>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<lay-transition :enable="collapseTransition">
|
||||
<div
|
||||
v-if="node.isLeaf"
|
||||
class="layui-tree-pack layui-tree-showLine"
|
||||
style="display: block"
|
||||
>
|
||||
<tree-node
|
||||
:tree="tree"
|
||||
:node-list="node.children"
|
||||
:show-checkbox="showCheckbox"
|
||||
:show-line="showLine"
|
||||
:selected-key="selectedKey"
|
||||
:collapse-transition="collapseTransition"
|
||||
:checkStrictly="checkStrictly"
|
||||
:only-icon-control="onlyIconControl"
|
||||
@node-click="recursiveNodeClick"
|
||||
>
|
||||
<template v-if="$slots.title" v-slot:title="slotProp: { data: any }">
|
||||
<slot name="title" :data="slotProp.data"></slot>
|
||||
</template>
|
||||
</tree-node>
|
||||
</div>
|
||||
</lay-transition>
|
||||
</div>
|
||||
</template>
|
||||
185
src/component/tree/index.less
Normal file
185
src/component/tree/index.less
Normal file
@@ -0,0 +1,185 @@
|
||||
@import "../checkbox/index.less";
|
||||
|
||||
.layui-tree {
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.layui-tree .layui-form-checkbox {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.layui-tree-set {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.layui-tree-txt.layui-this {
|
||||
color: var(--global-checked-color)!important;
|
||||
}
|
||||
|
||||
.layui-tree-pack {
|
||||
display: none;
|
||||
padding-left: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.layui-tree-iconClick,
|
||||
.layui-tree-main {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.layui-tree-line .layui-tree-pack {
|
||||
padding-left: 27px;
|
||||
}
|
||||
|
||||
.layui-tree-line .layui-tree-set .layui-tree-set:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: -9px;
|
||||
width: 17px;
|
||||
height: 0;
|
||||
border-top: 1px dotted #c0c4cc;
|
||||
}
|
||||
|
||||
.layui-tree-entry {
|
||||
position: relative;
|
||||
padding: 3px 0;
|
||||
height: 20px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.layui-tree-entry:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.layui-tree-line .layui-tree-entry:hover {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.layui-tree-line .layui-tree-entry:hover .layui-tree-txt {
|
||||
color: #999;
|
||||
text-decoration: underline;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.layui-tree-main {
|
||||
cursor: pointer;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.layui-tree-line .layui-tree-set:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -9px;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
border-left: 1px dotted #c0c4cc;
|
||||
}
|
||||
|
||||
.layui-tree-line .layui-tree-set.layui-tree-setLineShort:before {
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.layui-tree-line .layui-tree-set.layui-tree-setHide:before {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.layui-tree-iconClick {
|
||||
position: relative;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin: 0 10px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.layui-tree-icon {
|
||||
height: 12px;
|
||||
line-height: 12px;
|
||||
width: 12px;
|
||||
text-align: center;
|
||||
border: 1px solid #c0c4cc;
|
||||
}
|
||||
|
||||
.layui-tree-iconClick .layui-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.layui-tree-icon .layui-icon {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.layui-tree-iconArrow {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.layui-tree-iconArrow:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 3px;
|
||||
z-index: 100;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent transparent #c0c4cc;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.layui-tree-btnGroup,
|
||||
.layui-tree-editInput {
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.layui-tree-spread
|
||||
> .layui-tree-entry
|
||||
> .layui-tree-iconClick
|
||||
> .layui-tree-iconArrow:after {
|
||||
transform: rotate(90deg) translate(3px, 4px);
|
||||
}
|
||||
|
||||
.layui-tree-txt {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.layui-tree-search {
|
||||
margin-bottom: 15px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.layui-tree-btnGroup .layui-icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.layui-tree-btnGroup .layui-icon:hover {
|
||||
color: #999;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.layui-tree-entry:hover .layui-tree-btnGroup {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.layui-tree-editInput {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
padding: 0 3px;
|
||||
border: none;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.layui-tree-emptyText {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
5
src/component/tree/index.ts
Normal file
5
src/component/tree/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { withInstall, WithInstallType } from "../../utils";
|
||||
import Component from "./index.vue";
|
||||
|
||||
const component: WithInstallType<typeof Component> = withInstall(Component);
|
||||
export default component;
|
||||
153
src/component/tree/index.vue
Normal file
153
src/component/tree/index.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "LayTree",
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import TreeNode from "./TreeNode.vue";
|
||||
import { computed, useSlots, watch, ref, onMounted, nextTick } from "vue";
|
||||
import { useTree } from "./useTree";
|
||||
import { TreeData } from "./tree";
|
||||
import { StringFn, StringOrNumber, KeysType, EditType } from "./tree.type";
|
||||
import "./index.less";
|
||||
|
||||
export interface OriginalTreeData {
|
||||
title: StringFn | string;
|
||||
id: StringOrNumber;
|
||||
field: StringFn | string;
|
||||
children?: OriginalTreeData[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface ReplaceFieldsOptions {
|
||||
id?: string;
|
||||
children?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface TreeProps {
|
||||
data: OriginalTreeData;
|
||||
disabled?: boolean;
|
||||
edit?: EditType;
|
||||
checkedKeys?: KeysType;
|
||||
checkStrictly?: boolean | string;
|
||||
collapseTransition?: boolean;
|
||||
onlyIconControl?: boolean;
|
||||
selectedKey?: any;
|
||||
showLine?: boolean;
|
||||
showCheckbox?: boolean;
|
||||
replaceFields?: ReplaceFieldsOptions;
|
||||
}
|
||||
|
||||
interface TreeEmits {
|
||||
(e: "update:checkedKeys", keys: KeysType): void;
|
||||
(e: "update:expandKeys", keys: KeysType): void;
|
||||
(e: "node-click", node: OriginalTreeData): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<TreeProps>(), {
|
||||
checkedKeys: () => {
|
||||
return [];
|
||||
},
|
||||
showCheckbox: false,
|
||||
edit: false,
|
||||
collapseTransition: true,
|
||||
checkStrictly: false,
|
||||
onlyIconControl: false,
|
||||
disabled: false,
|
||||
showLine: true,
|
||||
replaceFields: () => {
|
||||
return {
|
||||
id: "id",
|
||||
children: "children",
|
||||
title: "title",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
|
||||
const emit = defineEmits<TreeEmits>();
|
||||
|
||||
const className = computed(() => {
|
||||
return {
|
||||
"layui-tree": true,
|
||||
"layui-form": props.showCheckbox,
|
||||
"layui-tree-line": props.showLine,
|
||||
};
|
||||
});
|
||||
|
||||
let tree = ref();
|
||||
let nodeList = ref();
|
||||
const unWatch = ref(false);
|
||||
const initStatus = ref(false);
|
||||
|
||||
const loadNodeList = () => {
|
||||
let { tree: _tree, nodeList: _nodeList } = useTree(props, emit);
|
||||
tree.value = _tree;
|
||||
nodeList.value = _nodeList.value;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
() => {
|
||||
loadNodeList();
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.checkedKeys,
|
||||
() => {
|
||||
if (!unWatch.value) {
|
||||
loadNodeList();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
tree,
|
||||
() => {
|
||||
if (initStatus.value) {
|
||||
const { checkedKeys } = tree.value.getKeys();
|
||||
unWatch.value = true;
|
||||
emit("update:checkedKeys", checkedKeys);
|
||||
setTimeout(() => {
|
||||
unWatch.value = false;
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initStatus.value = true;
|
||||
});
|
||||
});
|
||||
|
||||
function handleClick(node: TreeData) {
|
||||
const originNode = tree.value.getOriginData(node.id);
|
||||
emit("node-click", originNode);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div :class="className">
|
||||
<tree-node
|
||||
:tree="tree"
|
||||
:node-list="nodeList"
|
||||
:show-checkbox="showCheckbox"
|
||||
:show-line="showLine"
|
||||
:selectedKey="selectedKey"
|
||||
:check-strictly="checkStrictly"
|
||||
:collapse-transition="collapseTransition"
|
||||
:only-icon-control="onlyIconControl"
|
||||
@node-click="handleClick"
|
||||
>
|
||||
<template v-if="$slots.title" v-slot:title="{ data }">
|
||||
<slot name="title" :data="data"></slot>
|
||||
</template>
|
||||
</tree-node>
|
||||
</div>
|
||||
</template>
|
||||
224
src/component/tree/tree.ts
Normal file
224
src/component/tree/tree.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import { OriginalTreeData, StringOrNumber } from "./tree.type";
|
||||
import { Nullable } from "../../types";
|
||||
import { Ref, ref } from "vue";
|
||||
import { check } from "prettier";
|
||||
|
||||
type CustomKey = string | number;
|
||||
type CustomString = (() => string) | string;
|
||||
|
||||
export interface TreeData {
|
||||
id: CustomKey;
|
||||
title: CustomString;
|
||||
children: TreeData[];
|
||||
parentKey: Nullable<StringOrNumber>;
|
||||
isRoot: boolean;
|
||||
isChecked: boolean;
|
||||
isDisabled: boolean;
|
||||
isLeaf: boolean;
|
||||
hasNextSibling: boolean;
|
||||
parentNode: Nullable<TreeData>;
|
||||
}
|
||||
|
||||
interface ReplaceFields {
|
||||
id: string;
|
||||
title: string;
|
||||
children: string;
|
||||
}
|
||||
|
||||
interface TreeConfig {
|
||||
checkStrictly: boolean | string;
|
||||
showCheckbox: boolean;
|
||||
checkedKeys: StringOrNumber[];
|
||||
expandKeys: StringOrNumber[];
|
||||
nodeMap: Map<StringOrNumber, TreeData>;
|
||||
originMap: Map<StringOrNumber, OriginalTreeData>;
|
||||
replaceFields: ReplaceFields;
|
||||
}
|
||||
|
||||
class Tree {
|
||||
protected config: TreeConfig;
|
||||
protected treeData: TreeData[];
|
||||
|
||||
constructor(
|
||||
config: TreeConfig,
|
||||
origin: OriginalTreeData | OriginalTreeData[]
|
||||
) {
|
||||
this.config = config;
|
||||
this.treeData = [];
|
||||
this.init(origin);
|
||||
}
|
||||
|
||||
init(origin: OriginalTreeData | OriginalTreeData[]): void {
|
||||
const tree = this.createTree(origin);
|
||||
this.treeData = tree;
|
||||
}
|
||||
|
||||
createTree(
|
||||
origin: OriginalTreeData | OriginalTreeData[],
|
||||
parentKey: StringOrNumber = ""
|
||||
): TreeData[] {
|
||||
let data;
|
||||
if (!Array.isArray(origin)) {
|
||||
data = Array.of(Object.assign({}, origin));
|
||||
} else {
|
||||
data = origin;
|
||||
}
|
||||
const nodeList: TreeData[] = [];
|
||||
const { children } = this.config.replaceFields;
|
||||
|
||||
const len = data.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
const node = this.getNode(data[i], parentKey, i < len - 1);
|
||||
const nodeChildren = Reflect.get(node, children);
|
||||
const nodeHasChildren = !!Reflect.get(node, children);
|
||||
|
||||
if (nodeHasChildren) {
|
||||
Reflect.set(node, children, this.createTree(nodeChildren, node.id));
|
||||
}
|
||||
|
||||
nodeList.push(node);
|
||||
}
|
||||
return nodeList;
|
||||
}
|
||||
|
||||
getNode(
|
||||
origin: OriginalTreeData,
|
||||
parentKey: StringOrNumber,
|
||||
hasNextSibling: boolean
|
||||
): TreeData {
|
||||
const {
|
||||
nodeMap,
|
||||
originMap,
|
||||
checkedKeys,
|
||||
expandKeys,
|
||||
checkStrictly,
|
||||
replaceFields: { children, id, title },
|
||||
} = this.config;
|
||||
|
||||
const nodeKey = Reflect.get(origin, id);
|
||||
const nodeTitle = Reflect.get(origin, title);
|
||||
const nodeChildren = Reflect.get(origin, children);
|
||||
const nodeDisabled = !!Reflect.get(origin, "disabled");
|
||||
const nodeIsLeaf = !!Reflect.get(origin, "spread");
|
||||
const parentNode = nodeMap.get(parentKey);
|
||||
|
||||
const node = Object.assign({}, origin, {
|
||||
id: nodeKey,
|
||||
title: nodeTitle,
|
||||
children: nodeChildren ? nodeChildren : [],
|
||||
parentKey: parentKey,
|
||||
isRoot: parentKey === "",
|
||||
isDisabled: false,
|
||||
isChecked: false,
|
||||
isLeaf: false,
|
||||
hasNextSibling: hasNextSibling,
|
||||
parentNode: parentNode || null,
|
||||
});
|
||||
|
||||
node.isDisabled = nodeDisabled;
|
||||
node.isChecked = checkedKeys.includes(nodeKey);
|
||||
node.isLeaf = parentNode ? parentNode.isLeaf : expandKeys.includes(nodeKey);
|
||||
node.isLeaf = nodeIsLeaf;
|
||||
|
||||
if (!nodeMap.has(nodeKey)) {
|
||||
nodeMap.set(nodeKey, node);
|
||||
}
|
||||
if (!originMap.has(nodeKey)) {
|
||||
originMap.set(nodeKey, origin);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
treeForeach(tree: any, func: Function) {
|
||||
tree.forEach((data: any) => {
|
||||
data.children && this.treeForeach(data.children, func);
|
||||
func(data);
|
||||
});
|
||||
}
|
||||
|
||||
setChildrenChecked(checked: boolean, nodes: TreeData[]) {
|
||||
var ableCount = 0;
|
||||
var checkCount = 0;
|
||||
const len = nodes.length;
|
||||
this.treeForeach(nodes, (node: any) => {
|
||||
if (!node.isDisabled) {
|
||||
ableCount = ableCount + 1;
|
||||
if (node.isChecked) {
|
||||
checkCount = checkCount + 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
checkCount < ableCount ? (checked = true) : (checked = false);
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (
|
||||
!nodes[i].isDisabled ||
|
||||
(nodes[i].isDisabled && nodes[i].children.length > 0)
|
||||
) {
|
||||
nodes[i].isChecked = checked;
|
||||
}
|
||||
nodes[i].children &&
|
||||
nodes[i].children.length > 0 &&
|
||||
this.setChildrenChecked(checked, nodes[i].children);
|
||||
}
|
||||
}
|
||||
|
||||
setParentChecked(checked: boolean, parent: TreeData) {
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
parent.isChecked = checked;
|
||||
const pChild = parent.children;
|
||||
const pChildChecked = pChild.some((c) => c.isChecked);
|
||||
if (pChildChecked) {
|
||||
parent.isChecked = true;
|
||||
}
|
||||
if (parent.parentNode) {
|
||||
this.setParentChecked(checked, parent.parentNode);
|
||||
}
|
||||
}
|
||||
|
||||
setCheckedKeys(
|
||||
checked: boolean,
|
||||
checkStrictly: boolean | string,
|
||||
node: TreeData
|
||||
) {
|
||||
node.isChecked = checked;
|
||||
if (!checkStrictly) {
|
||||
if (node.parentNode) {
|
||||
this.setParentChecked(checked, node.parentNode);
|
||||
}
|
||||
if (node.children) {
|
||||
this.setChildrenChecked(checked, node.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getData() {
|
||||
return this.treeData;
|
||||
}
|
||||
|
||||
getKeys() {
|
||||
const checkedKeys = [];
|
||||
const expandKeys = [];
|
||||
const iterator = this.config.nodeMap[Symbol.iterator]();
|
||||
let next = iterator.next();
|
||||
while (!next.done) {
|
||||
const [, node] = next.value;
|
||||
const id = Reflect.get(node, this.config.replaceFields.id);
|
||||
if (node.isChecked) {
|
||||
checkedKeys.push(id);
|
||||
}
|
||||
if (node.isLeaf) {
|
||||
expandKeys.push(id);
|
||||
}
|
||||
next = iterator.next();
|
||||
}
|
||||
return { checkedKeys, expandKeys };
|
||||
}
|
||||
|
||||
getOriginData(key: StringOrNumber): OriginalTreeData {
|
||||
return this.config.originMap.get(key)!;
|
||||
}
|
||||
}
|
||||
|
||||
export { Tree };
|
||||
39
src/component/tree/tree.type.ts
Normal file
39
src/component/tree/tree.type.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export type StringFn = () => string;
|
||||
export type StringOrNumber = string | number;
|
||||
export type KeysType = (number | string)[];
|
||||
export type EditType = boolean | ("add" | "update" | "delete");
|
||||
|
||||
export interface OriginalTreeData {
|
||||
title: StringFn | string;
|
||||
id: StringOrNumber;
|
||||
field: StringFn | string;
|
||||
children?: OriginalTreeData[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface ReplaceFieldsOptions {
|
||||
id?: string;
|
||||
children?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface TreeProps {
|
||||
checkedKeys?: KeysType;
|
||||
expandKeys?: KeysType;
|
||||
data: OriginalTreeData;
|
||||
checkStrictly?: boolean | string;
|
||||
showCheckbox?: boolean;
|
||||
edit?: EditType;
|
||||
collapseTransition?: boolean;
|
||||
onlyIconControl?: boolean;
|
||||
showLine?: boolean;
|
||||
replaceFields?: ReplaceFieldsOptions;
|
||||
}
|
||||
export interface TreeEmits {
|
||||
(e: "update:checkedKeys", keys: KeysType): void;
|
||||
(e: "update:expandKeys", keys: KeysType): void;
|
||||
(e: "node-click", node: OriginalTreeData, event: Event): void;
|
||||
}
|
||||
|
||||
export type CustomKey = string | number;
|
||||
export type CustomString = (() => string) | string;
|
||||
40
src/component/tree/useTree.ts
Normal file
40
src/component/tree/useTree.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { TreeEmits, TreeProps } from "./tree.type";
|
||||
import { computed, ComputedRef, watch } from "vue";
|
||||
import { Tree, TreeData } from "./tree";
|
||||
|
||||
export declare type UseTree = (
|
||||
props: TreeProps,
|
||||
emit: TreeEmits
|
||||
) => {
|
||||
tree: Tree;
|
||||
nodeList: ComputedRef<TreeData[]>;
|
||||
};
|
||||
|
||||
export const useTree: UseTree = (props: TreeProps, emit: TreeEmits) => {
|
||||
const tree = new Tree(
|
||||
{
|
||||
nodeMap: new Map(),
|
||||
originMap: new Map(),
|
||||
replaceFields: {
|
||||
id: "id",
|
||||
title: "title",
|
||||
children: "children",
|
||||
},
|
||||
showCheckbox: props.showCheckbox ?? false,
|
||||
checkedKeys: props.checkedKeys ?? [],
|
||||
expandKeys: props.expandKeys ?? [],
|
||||
checkStrictly: props.checkStrictly ?? false,
|
||||
},
|
||||
props.data
|
||||
);
|
||||
|
||||
const nodeList = computed(() => {
|
||||
const nodes = tree.getData();
|
||||
return nodes;
|
||||
});
|
||||
|
||||
return {
|
||||
tree,
|
||||
nodeList,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user