This commit is contained in:
2022-11-14 11:56:21 +08:00
commit 0a63adba99
337 changed files with 25661 additions and 0 deletions

View 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>

View 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;
}

View 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;

View 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
View 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 };

View 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;

View 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,
};
};