feat: 初始化
This commit is contained in:
25
pages/components/baseTree/TreeProcessorVue.ts
Normal file
25
pages/components/baseTree/TreeProcessorVue.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { makeTreeProcessor, Options } from "./tree-utils";
|
||||
export * from "./tree-utils";
|
||||
import { reactive } from "./vue";
|
||||
|
||||
export function vueMakeTreeProcessor<T>(data: T[], options: Options = {}) {
|
||||
const opt = {
|
||||
...options,
|
||||
statHandler(input) {
|
||||
if (this["_statHandler2"]) {
|
||||
input = this["_statHandler2"](input);
|
||||
}
|
||||
return filter(options.statHandler, reactive(input));
|
||||
},
|
||||
statsHandler(input) {
|
||||
return filter(options.statsHandler, reactive(input));
|
||||
},
|
||||
statsFlatHandler(input) {
|
||||
return filter(options.statsFlatHandler, reactive(input));
|
||||
},
|
||||
};
|
||||
return makeTreeProcessor(data, opt);
|
||||
}
|
||||
function filter<T>(func: Function | null | undefined, input: T): T {
|
||||
return func ? func(input) : input;
|
||||
}
|
411
pages/components/baseTree/baseTree.vue
Normal file
411
pages/components/baseTree/baseTree.vue
Normal file
@ -0,0 +1,411 @@
|
||||
<template>
|
||||
<List
|
||||
class="he-tree"
|
||||
:id="nodeKey"
|
||||
ref="virtualizationList"
|
||||
:items="visibleStats"
|
||||
:enabled="virtualization"
|
||||
:class="{'he-tree-rtl': rtl}"
|
||||
>
|
||||
<template #prepend>
|
||||
<slot name="prepend" :tree="self"></slot>
|
||||
</template>
|
||||
<template #default="{ item: stat, index }">
|
||||
<TreeNode :vt-index="index" :class="[
|
||||
stat.class,
|
||||
{
|
||||
'drag-placeholder-wrapper': stat.data === placeholderData,
|
||||
'dragging-node': stat === dragNode,
|
||||
},
|
||||
]" :style="stat.style" :stat="stat" :rtl="rtl" :btt="btt" :indent="indent" :table="table" :treeLine="treeLine"
|
||||
:treeLineOffset="treeLineOffset" :processor="processor" @click="$emit('click:node', stat)"
|
||||
@open="$emit('open:node', $event)" @close="$emit('close:node', $event)" @check="$emit('check:node', $event)">
|
||||
<template #default="{ indentStyle }">
|
||||
<template v-if="stat.data === placeholderData">
|
||||
<view v-if="!table" class="drag-placeholder he-tree-drag-placeholder">
|
||||
<slot name="placeholder" :tree="self"></slot>
|
||||
</view>
|
||||
<td v-else :style="indentStyle" :colspan="placeholderColspan">
|
||||
<view class="drag-placeholder he-tree-drag-placeholder">
|
||||
<slot name="placeholder" :tree="self"></slot>
|
||||
</view>
|
||||
</td>
|
||||
</template>
|
||||
<slot v-else :node="stat.data" :stat="stat" :indentStyle="indentStyle" :tree="self">{{ stat.data[textKey] }}
|
||||
</slot>
|
||||
</template>
|
||||
</TreeNode>
|
||||
</template>
|
||||
<template #append>
|
||||
<slot name="append" :tree="self"></slot>
|
||||
</template>
|
||||
</List>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// 如果遇到滚动不流畅的情况,不用处理,因为Dev tool造成的。
|
||||
// If the scrolling is not smooth, do not deal with it, because it is caused by the Dev tool.
|
||||
import { PropType, defineComponent, reactive } from "./vue";
|
||||
import * as hp from "./helper-js";
|
||||
import List from "./list";
|
||||
import TreeNode from "./treeNode.vue";
|
||||
import { vueMakeTreeProcessor, Stat, TreeProcessor } from "./TreeProcessorVue";
|
||||
|
||||
export { walkTreeData } from "./helper-js";
|
||||
|
||||
const cpt = defineComponent({
|
||||
components: { List, TreeNode },
|
||||
props: {
|
||||
// for vue3
|
||||
modelValue: { required: true, type: Array as PropType<any[]> },
|
||||
updateBehavior: {
|
||||
type: String as PropType<"modify" | "new" | "disabled">,
|
||||
default: "modify",
|
||||
},
|
||||
processor: {
|
||||
type: Object as PropType<TreeProcessor>,
|
||||
default: () =>
|
||||
vueMakeTreeProcessor([], {
|
||||
noInitialization: true,
|
||||
}),
|
||||
},
|
||||
childrenKey: { type: String, default: "children" },
|
||||
/**
|
||||
* for default slot. 用于默认插槽
|
||||
*/
|
||||
textKey: { type: String, default: "text" },
|
||||
/**
|
||||
* node indent. 节点缩进
|
||||
*/
|
||||
indent: { type: Number, default: 20 },
|
||||
/**
|
||||
* Enable virtual list. 启用虚拟列表
|
||||
*/
|
||||
virtualization: { type: Boolean, default: false },
|
||||
/**
|
||||
* Render count for virtual list at start. 虚拟列表初始渲染数量.
|
||||
*/
|
||||
virtualizationPrerenderCount: { type: Number, default: 20 },
|
||||
/**
|
||||
* Open all nodes by default. 默认打开所有节点.
|
||||
*/
|
||||
defaultOpen: { type: Boolean, default: true },
|
||||
statHandler: { type: Function as PropType<(stat: Stat<any>) => Stat<any>> },
|
||||
/**
|
||||
* From right to left. 由右向左显示.
|
||||
*/
|
||||
rtl: { type: Boolean, default: false },
|
||||
/**
|
||||
* From bottom to top. 由下向上显示
|
||||
*/
|
||||
btt: { type: Boolean, default: false },
|
||||
/**
|
||||
* Display as table
|
||||
*/
|
||||
table: { type: Boolean, default: false },
|
||||
watermark: { type: Boolean, default: false },
|
||||
nodeKey: {
|
||||
type: [String, Function] as PropType<
|
||||
"index" | ((stat: Stat<any>, index: number) => any)
|
||||
>,
|
||||
default: 'index',
|
||||
},
|
||||
treeLine: { type: Boolean, default: false },
|
||||
treeLineOffset: { type: Number, default: 8 },
|
||||
},
|
||||
emits: [
|
||||
"update:modelValue",
|
||||
"click:node",
|
||||
"open:node",
|
||||
"close:node",
|
||||
"check:node",
|
||||
"beforeDragStart",
|
||||
"before-drag-start",
|
||||
"after-drop",
|
||||
"change",
|
||||
"enter",
|
||||
"leave",
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
treeID: hp.randString(),
|
||||
stats: [],
|
||||
statsFlat: [],
|
||||
dragNode: null,
|
||||
dragOvering: false,
|
||||
placeholderData: {},
|
||||
placeholderColspan: 1,
|
||||
batchUpdateWaiting: false,
|
||||
self: this,
|
||||
_ignoreValueChangeOnce: false,
|
||||
} as {
|
||||
treeID: any,
|
||||
stats: Exclude<TreeProcessor["stats"], null>;
|
||||
statsFlat: Exclude<TreeProcessor["statsFlat"], null>;
|
||||
dragNode: Stat<any> | null;
|
||||
dragOvering: boolean;
|
||||
placeholderData: {};
|
||||
placeholderColspan: number;
|
||||
batchUpdateWaiting: boolean;
|
||||
self: any;
|
||||
_ignoreValueChangeOnce: boolean;
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
valueComputed() {
|
||||
return (this.modelValue) || [];
|
||||
},
|
||||
visibleStats() {
|
||||
const { statsFlat, isVisible } = this;
|
||||
let items = statsFlat;
|
||||
if (this.btt) {
|
||||
items = items.slice();
|
||||
items.reverse();
|
||||
}
|
||||
return items.filter((stat) => isVisible(stat));
|
||||
},
|
||||
rootChildren() {
|
||||
return this.stats
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
_emitValue(value: any[]) {
|
||||
// @ts-ignore
|
||||
this.$emit("update:modelValue", value);
|
||||
},
|
||||
/**
|
||||
* private method
|
||||
* @param value
|
||||
*/
|
||||
_updateValue(value: any[]) {
|
||||
if (this.updateBehavior === "disabled") {
|
||||
return false;
|
||||
}
|
||||
// if value changed, ignore change once
|
||||
if (value !== this.valueComputed) {
|
||||
this._ignoreValueChangeOnce = true;
|
||||
}
|
||||
this._emitValue(value);
|
||||
return true;
|
||||
},
|
||||
getStat: reactiveFirstArg(
|
||||
processorMethodProxy("getStat")
|
||||
) as TreeProcessor["getStat"],
|
||||
has: reactiveFirstArg(processorMethodProxy("has")) as TreeProcessor["has"],
|
||||
updateCheck: processorMethodProxy(
|
||||
"updateCheck"
|
||||
) as TreeProcessor["updateCheck"],
|
||||
getChecked: processorMethodProxy(
|
||||
"getChecked"
|
||||
) as TreeProcessor["getChecked"],
|
||||
getUnchecked: processorMethodProxy(
|
||||
"getUnchecked"
|
||||
) as TreeProcessor["getUnchecked"],
|
||||
openAll: processorMethodProxy("openAll") as TreeProcessor["openAll"],
|
||||
closeAll: processorMethodProxy("closeAll") as TreeProcessor["closeAll"],
|
||||
openNodeAndParents: processorMethodProxy("openNodeAndParents") as TreeProcessor["openNodeAndParents"],
|
||||
isVisible: processorMethodProxy("isVisible") as TreeProcessor["isVisible"],
|
||||
move: processorMethodProxyWithBatchUpdate("move") as TreeProcessor["move"],
|
||||
add: reactiveFirstArg(
|
||||
processorMethodProxyWithBatchUpdate("add")
|
||||
) as TreeProcessor["add"],
|
||||
addMulti(
|
||||
dataArr: any[],
|
||||
parent?: Stat<any> | null,
|
||||
startIndex?: number | null
|
||||
) {
|
||||
this.batchUpdate(() => {
|
||||
let index = startIndex;
|
||||
for (const data of dataArr) {
|
||||
this.add(data, parent, index);
|
||||
if (index != null) {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
remove: processorMethodProxy("remove") as TreeProcessor["remove"],
|
||||
removeMulti(dataArr: any[]) {
|
||||
let cloned = [...dataArr];
|
||||
this.batchUpdate(() => {
|
||||
for (const data of cloned) {
|
||||
this.remove(data);
|
||||
}
|
||||
});
|
||||
},
|
||||
iterateParent: processorMethodProxy(
|
||||
"iterateParent"
|
||||
) as TreeProcessor["iterateParent"],
|
||||
getSiblings: processorMethodProxy(
|
||||
"getSiblings"
|
||||
) as TreeProcessor["getSiblings"],
|
||||
getData: processorMethodProxy("getData") as hp.ReplaceReturnType<
|
||||
TreeProcessor["getData"],
|
||||
any[]
|
||||
>,
|
||||
getRootEl() {
|
||||
// @ts-ignore
|
||||
return this.$refs.vtlist.listElRef as HTMLElement;
|
||||
},
|
||||
batchUpdate(task: () => any | Promise<any>) {
|
||||
const r = this.ignoreUpdate(task);
|
||||
if (!this.batchUpdateWaiting) {
|
||||
this._updateValue(
|
||||
this.updateBehavior === "new" ? this.getData() : this.valueComputed
|
||||
);
|
||||
}
|
||||
return r;
|
||||
},
|
||||
ignoreUpdate(task: () => any | Promise<any>) {
|
||||
const old = this.batchUpdateWaiting;
|
||||
this.batchUpdateWaiting = true;
|
||||
const r = task();
|
||||
this.batchUpdateWaiting = old;
|
||||
return r;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
processor: {
|
||||
immediate: true,
|
||||
handler(processor: typeof this.processor) {
|
||||
if (processor) {
|
||||
// hook
|
||||
const getNodeDataChildren = (nodeData: any): any[] => {
|
||||
if (!nodeData) {
|
||||
return this.valueComputed;
|
||||
} else {
|
||||
const { childrenKey } = this;
|
||||
if (!nodeData[childrenKey]) {
|
||||
nodeData[childrenKey] = [];
|
||||
}
|
||||
return nodeData[childrenKey];
|
||||
}
|
||||
};
|
||||
processor["_statHandler2"] = this.statHandler
|
||||
? (stat) => {
|
||||
if (stat.data === this.placeholderData) {
|
||||
return stat;
|
||||
}
|
||||
return this.statHandler!(stat);
|
||||
}
|
||||
: null;
|
||||
processor.afterSetStat = (stat, parent, index) => {
|
||||
const { childrenKey, updateBehavior } = this;
|
||||
let value = this.valueComputed;
|
||||
if (updateBehavior === "new") {
|
||||
if (this.batchUpdateWaiting) {
|
||||
return;
|
||||
}
|
||||
value = this.getData();
|
||||
} else if (updateBehavior === "modify") {
|
||||
const siblings = getNodeDataChildren(parent?.data);
|
||||
if (siblings.includes(stat.data)) {
|
||||
// when call add -> add child -> _setPositionm ignore because the child already in parent.children
|
||||
} else {
|
||||
siblings.splice(index, 0, stat.data);
|
||||
}
|
||||
} else if (updateBehavior === "disabled") {
|
||||
}
|
||||
if (this.batchUpdateWaiting) {
|
||||
return;
|
||||
}
|
||||
this._updateValue(value);
|
||||
};
|
||||
processor.afterRemoveStat = (stat) => {
|
||||
const { childrenKey, updateBehavior } = this;
|
||||
let value = this.valueComputed;
|
||||
if (updateBehavior === "new") {
|
||||
if (this.batchUpdateWaiting) {
|
||||
return;
|
||||
}
|
||||
value = this.getData();
|
||||
} else if (updateBehavior === "modify") {
|
||||
const siblings = getNodeDataChildren(stat.parent?.data);
|
||||
hp.arrayRemove(siblings, stat.data);
|
||||
} else if (updateBehavior === "disabled") {
|
||||
}
|
||||
if (this.batchUpdateWaiting) {
|
||||
return;
|
||||
}
|
||||
this._updateValue(value);
|
||||
};
|
||||
}
|
||||
if (!processor.initialized) {
|
||||
processor.data = this.valueComputed;
|
||||
Object.assign(
|
||||
processor,
|
||||
hp.objectOnly(this, ["childrenKey", "defaultOpen"])
|
||||
);
|
||||
processor.init();
|
||||
processor.updateCheck();
|
||||
}
|
||||
this.stats = processor.stats!;
|
||||
this.statsFlat = processor.statsFlat!;
|
||||
if (processor.data !== this.valueComputed) {
|
||||
this._updateValue(processor.data);
|
||||
}
|
||||
},
|
||||
},
|
||||
valueComputed: {
|
||||
handler(value) {
|
||||
// isDragging triggered in Vue2 because its array is not same with Vue3
|
||||
const isDragging = this.dragOvering || this.dragNode
|
||||
if (isDragging || this._ignoreValueChangeOnce) {
|
||||
this._ignoreValueChangeOnce = false;
|
||||
} else {
|
||||
const { processor } = this;
|
||||
processor.data = value;
|
||||
processor.init();
|
||||
this.stats = processor.stats!;
|
||||
this.statsFlat = processor.statsFlat!;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
created() { },
|
||||
mounted() {
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
export default cpt;
|
||||
export type BaseTreeType = InstanceType<typeof cpt>;
|
||||
|
||||
function processorMethodProxy(name: string) {
|
||||
return function (...args) {
|
||||
// @ts-ignore
|
||||
return this.processor[name](...args);
|
||||
};
|
||||
}
|
||||
function processorMethodProxyWithBatchUpdate(name: string) {
|
||||
return function (...args) {
|
||||
// @ts-ignore
|
||||
return this.batchUpdate(() => {
|
||||
// @ts-ignore
|
||||
return this.processor[name](...args);
|
||||
});
|
||||
};
|
||||
}
|
||||
function reactiveFirstArg(func: any) {
|
||||
return function (arg1, ...args) {
|
||||
if (arg1) {
|
||||
arg1 = reactive(arg1);
|
||||
}
|
||||
// @ts-ignore
|
||||
return func.call(this, arg1, ...args);
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.he-tree--rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.he-tree-drag-placeholder {
|
||||
background: #ddf2f9;
|
||||
border: 1px dashed #00d9ff;
|
||||
height: 22px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
3464
pages/components/baseTree/helper-js.ts
Normal file
3464
pages/components/baseTree/helper-js.ts
Normal file
File diff suppressed because it is too large
Load Diff
140
pages/components/baseTree/list.vue
Normal file
140
pages/components/baseTree/list.vue
Normal file
@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<view class="VirtualizationList virtualization-list" @scroll.passive="update" >
|
||||
<component
|
||||
:is="listTag"
|
||||
:class="listClass"
|
||||
>
|
||||
<template v-for="(info, i) in visibleItems" :key="info.item.$id">
|
||||
<slot
|
||||
:item="info.item"
|
||||
:index="info.index"
|
||||
:renderIndex="i"
|
||||
:itemStyle="{ marginBottom: gap + 'px' }"
|
||||
{{ info.item.text }}
|
||||
></slot>
|
||||
</template>
|
||||
</component>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, nextTick } from "./vue"
|
||||
import { obj } from "./types"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
items: { type: Array as PropType<obj[]>, default: () => [] },
|
||||
enabled: { type: Boolean, default: true },
|
||||
buffer: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
},
|
||||
minItemHeight: { type: Number, default: 20 },
|
||||
prerender: { type: Number, default: 20 },
|
||||
listTag: { type: String, default: "view" },
|
||||
listClass: { type: String },
|
||||
itemClass: { type: String, default: "vl-item" },
|
||||
gap: { type: Number, default: 0 },
|
||||
afterCalcTop2: { type: Function as PropType<(top2: number) => number> },
|
||||
isForceVisible: {
|
||||
type: Function as PropType<(node: obj, index: number) => boolean>,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
start: 0,
|
||||
end: -1,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
totalHeight: 0,
|
||||
itemsHeight: <number[]>[],
|
||||
mountedPromise: new Promise((resolve, reject) => {
|
||||
this._mountedPromise_resolve = resolve;
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
visibleItems(): { item: obj; index: number }[] {
|
||||
const r: { item: obj; index: number }[] = [];
|
||||
console.log(this.items)
|
||||
this.items.forEach((item: obj, index: number) => {
|
||||
if (!this.enabled) {
|
||||
r.push({ item, index });
|
||||
} else if (
|
||||
(index >= this.start && index <= this.end) ||
|
||||
(this.isForceVisible && this.isForceVisible(item, index))
|
||||
) {
|
||||
r.push({ item, index });
|
||||
}
|
||||
});
|
||||
console.log(r)
|
||||
return r;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
enabled: {
|
||||
immediate: true,
|
||||
handler() {
|
||||
if (!this.enabled) {
|
||||
// @ts-ignore
|
||||
this.totalHeight = undefined;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getItemElHeight(el: UniApp.NodesRef) {
|
||||
return new Promise((resolve, reject) => {
|
||||
el.boundingClientRect(res => {
|
||||
if(res) {
|
||||
resolve(res)
|
||||
} else {
|
||||
reject(res)
|
||||
}
|
||||
}).exec()
|
||||
})
|
||||
},
|
||||
update() {
|
||||
const task = async () => {
|
||||
this.start = 0;
|
||||
this.end = this.items.length;
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.bottom = this.prerender - 1;
|
||||
},
|
||||
mounted() {
|
||||
// @ts-ignore
|
||||
this._mountedPromise_resolve!(null);
|
||||
let updatedOnce = false;
|
||||
this.$watch(
|
||||
() => [this.items],
|
||||
() => {
|
||||
this.itemsHeight = [];
|
||||
nextTick(() => {
|
||||
this.update();
|
||||
if (!updatedOnce) {
|
||||
this.update();
|
||||
}
|
||||
updatedOnce = true;
|
||||
});
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
this.$watch(
|
||||
() => [this.buffer],
|
||||
() => {
|
||||
this.update();
|
||||
}
|
||||
);
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.vl-items {
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
1
pages/components/baseTree/style/default.css
Normal file
1
pages/components/baseTree/style/default.css
Normal file
@ -0,0 +1 @@
|
||||
.tree-node--with-tree-line{position:relative}.tree-line{position:absolute;background-color:#bbb}.tree-vline{width:1px;top:0;bottom:0}.tree-hline{height:1px;top:50%;width:10px}.he-tree--rtl{direction:rtl}.he-tree-drag-placeholder{background:#ddf2f9;border:1px dashed #00d9ff;height:22px;width:100%}.he-tree__open-icon{cursor:pointer;-webkit-user-select:none;user-select:none;display:inline-block}.he-tree__open-icon.open{transform:rotate(90deg)}.he-tree__open-icon svg{width:1em}
|
58
pages/components/baseTree/style/material-design.css
Normal file
58
pages/components/baseTree/style/material-design.css
Normal file
@ -0,0 +1,58 @@
|
||||
.mtl-tree .tree-node-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.mtl-tree .tree-node {
|
||||
padding: 1px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mtl-tree .tree-node:hover {
|
||||
background-color: #ededed;
|
||||
/* recommend: active #ddeff9, active & hover: #cfe6f2 */
|
||||
}
|
||||
|
||||
.mtl-checkbox {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.mtl-ml {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.mtl-mr {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mtl-tree table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
|
||||
.mtl-tree td,
|
||||
.mtl-tree th {
|
||||
border-bottom: 1px solid rgba(224, 224, 224, 1);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.mtl-tree tr:last-child td,
|
||||
.mtl-tree tr:last-child tr {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
.mtl-text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mtl-text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mtl-text-right {
|
||||
text-align: right;
|
||||
}
|
432
pages/components/baseTree/tree-utils.ts
Normal file
432
pages/components/baseTree/tree-utils.ts
Normal file
@ -0,0 +1,432 @@
|
||||
import * as hp from "./helper-js";
|
||||
|
||||
export const CHILDREN = "children"; // inner childrenKey
|
||||
/**
|
||||
* help to handle tree data. 帮助处理树形数据.
|
||||
*/
|
||||
export function makeTreeProcessor<T>(data: T[], opt: Options = {}) {
|
||||
const opt2 = opt as Required<Options>;
|
||||
const utilsBase = {
|
||||
...defaultOptions,
|
||||
...opt2,
|
||||
data,
|
||||
stats: null as Stat<T>[] | null,
|
||||
statsFlat: null as Stat<T>[] | null,
|
||||
_statsMap: null as Map<T, Stat<T>> | null,
|
||||
initialized: false,
|
||||
init() {
|
||||
const { data, childrenKey } = this;
|
||||
const td = new hp.TreeData([] as Stat<T>[]);
|
||||
this._statsMap = new Map();
|
||||
hp.walkTreeData(
|
||||
data,
|
||||
(nodeData, index, parent, path) => {
|
||||
const stat = this.statHandler({
|
||||
...statDefault(),
|
||||
data: nodeData,
|
||||
open: Boolean(this.defaultOpen),
|
||||
parent: td.getParent(path),
|
||||
children: [],
|
||||
level: path.length,
|
||||
});
|
||||
this._statsMap!.set(nodeData, stat);
|
||||
td.set(path, stat);
|
||||
},
|
||||
{ childrenKey }
|
||||
);
|
||||
const statsFlat: typeof td.rootChildren = [];
|
||||
td.walk((stat) => {
|
||||
statsFlat.push(stat);
|
||||
});
|
||||
this.stats = this.statsHandler(td.rootChildren);
|
||||
this.statsFlat = this.statsFlatHandler(statsFlat);
|
||||
this.initialized = true;
|
||||
},
|
||||
getStat(nodeData: T) {
|
||||
let r: Stat<T> = this._statsMap!.get(nodeData)!;
|
||||
if (!r) {
|
||||
throw new StatNotFoundError(`Stat not found`);
|
||||
}
|
||||
return r;
|
||||
},
|
||||
has(nodeData: T | Stat<T>) {
|
||||
if (nodeData["isStat"]) {
|
||||
// @ts-ignore
|
||||
return this.statsFlat.indexOf(nodeData) > -1;
|
||||
} else {
|
||||
try {
|
||||
// @ts-ignore
|
||||
let r = this.getStat(nodeData);
|
||||
return Boolean(r);
|
||||
} catch (error) {
|
||||
if (error instanceof StatNotFoundError) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
},
|
||||
_getPathByStat(stat: Stat<T> | null) {
|
||||
if (stat == null) {
|
||||
return [];
|
||||
}
|
||||
const siblings = this.getSiblings(stat);
|
||||
const index = siblings.indexOf(stat);
|
||||
return [...(stat.parent ? this._getPathByStat(stat.parent) : []), index];
|
||||
},
|
||||
/**
|
||||
* call it after a stat's `checked` changed
|
||||
* @param stat
|
||||
* @returns return false mean ignored
|
||||
*/
|
||||
afterOneCheckChanged(stat: Stat<T>) {
|
||||
const { checked } = stat;
|
||||
if (stat._ignoreCheckedOnce) {
|
||||
delete stat._ignoreCheckedOnce;
|
||||
return false;
|
||||
}
|
||||
|
||||
// change parent
|
||||
const checkParent = (stat: any) => {
|
||||
const { parent } = stat;
|
||||
if (parent) {
|
||||
let hasChecked;
|
||||
let hasUnchecked;
|
||||
for (const child of parent.children) {
|
||||
if (child.checked || child.checked === 0) {
|
||||
hasChecked = true;
|
||||
} else {
|
||||
hasUnchecked = true;
|
||||
if (hasChecked && hasUnchecked) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const parentChecked = !hasUnchecked ? true : hasChecked ? 0 : false;
|
||||
|
||||
if (parent.checked !== parentChecked) {
|
||||
this._ignoreCheckedOnce(parent);
|
||||
parent.checked = parentChecked;
|
||||
}
|
||||
checkParent(parent);
|
||||
}
|
||||
};
|
||||
checkParent(stat);
|
||||
// change children
|
||||
hp.walkTreeData(
|
||||
stat.children,
|
||||
(child) => {
|
||||
if (child.checked !== checked) {
|
||||
this._ignoreCheckedOnce(child);
|
||||
child.checked = checked;
|
||||
}
|
||||
},
|
||||
{ childrenKey: CHILDREN }
|
||||
);
|
||||
return true;
|
||||
},
|
||||
_ignoreCheckedOnce(stat: Stat<T>) {
|
||||
stat._ignoreCheckedOnce = true;
|
||||
// cancel ignore immediately if not triggered
|
||||
setTimeout(() => {
|
||||
if (stat._ignoreCheckedOnce) {
|
||||
stat._ignoreCheckedOnce = false;
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
isVisible(statOrNodeData: T | Stat<T>) {
|
||||
// @ts-ignore
|
||||
const stat: Stat<T> = statOrNodeData["isStat"] ? statOrNodeData : this.getStat(statOrNodeData); // prettier-ignore
|
||||
const walk = (stat: Stat<T> | null) => {
|
||||
return !stat || (!stat.hidden && stat.open && walk(stat.parent));
|
||||
};
|
||||
return Boolean(!stat.hidden && walk(stat.parent));
|
||||
},
|
||||
/**
|
||||
* call it to update all stats' `checked`
|
||||
*/
|
||||
updateCheck() {
|
||||
hp.walkTreeData(
|
||||
this.stats!,
|
||||
(stat) => {
|
||||
if (stat.children && stat.children.length > 0) {
|
||||
const checked = stat.children.every((v) => v.checked);
|
||||
if (stat.checked !== checked) {
|
||||
this._ignoreCheckedOnce(stat);
|
||||
stat.checked = checked;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ childFirst: true, childrenKey: CHILDREN }
|
||||
);
|
||||
},
|
||||
getChecked(withDemi = false) {
|
||||
return this.statsFlat!.filter((v) => {
|
||||
return v.checked || (withDemi && v.checked === 0);
|
||||
});
|
||||
},
|
||||
getUnchecked(withDemi = true) {
|
||||
return this.statsFlat!.filter((v) => {
|
||||
return withDemi ? !v.checked : v.checked === false;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* open all nodes
|
||||
*/
|
||||
openAll() {
|
||||
for (const stat of this.statsFlat!) {
|
||||
stat.open = true;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* close all nodes
|
||||
*/
|
||||
closeAll() {
|
||||
for (const stat of this.statsFlat!) {
|
||||
stat.open = false;
|
||||
}
|
||||
},
|
||||
openNodeAndParents(nodeOrStat: T | Stat<T>) {
|
||||
// @ts-ignore
|
||||
const stat:Stat<T> = nodeOrStat["isStat"] ? nodeOrStat : this.getStat(nodeOrStat) // prettier-ignore
|
||||
for (const parentStat of this.iterateParent(stat, {
|
||||
withSelf: true,
|
||||
})) {
|
||||
parentStat.open = true;
|
||||
}
|
||||
},
|
||||
// actions
|
||||
_calcFlatIndex(parent: Stat<T> | null, index: number) {
|
||||
let flatIndex = parent ? this.statsFlat!.indexOf(parent) + 1 : 0;
|
||||
const siblings = parent ? parent.children : this.stats!;
|
||||
for (let i = 0; i < index; i++) {
|
||||
flatIndex += this._count(siblings[i]);
|
||||
}
|
||||
return flatIndex;
|
||||
},
|
||||
add(nodeData: T, parent?: Stat<T> | null, index?: number | null) {
|
||||
if (this.has(nodeData)) {
|
||||
throw `Can't add because data exists in tree`;
|
||||
}
|
||||
const siblings = parent ? parent.children : this.stats!;
|
||||
if (index == null) {
|
||||
index = siblings.length;
|
||||
}
|
||||
const stat: Stat<T> = this.statHandler({
|
||||
...statDefault(),
|
||||
open: Boolean(this.defaultOpen),
|
||||
data: nodeData,
|
||||
parent: parent || null,
|
||||
children: [],
|
||||
level: parent ? parent.level + 1 : 1,
|
||||
});
|
||||
this._setPosition(stat, parent || null, index);
|
||||
const children = nodeData[this.childrenKey];
|
||||
if (children) {
|
||||
const childrenSnap = children.slice();
|
||||
for (const child of childrenSnap) {
|
||||
this.add(child, stat);
|
||||
}
|
||||
}
|
||||
},
|
||||
remove(stat: Stat<T>) {
|
||||
const siblings = this.getSiblings(stat);
|
||||
if (siblings.includes(stat)) {
|
||||
hp.arrayRemove(siblings, stat);
|
||||
const stats = this._flat(stat);
|
||||
this.statsFlat!.splice(this.statsFlat!.indexOf(stat), stats.length);
|
||||
for (const stat of stats) {
|
||||
this._statsMap!.delete(stat.data);
|
||||
}
|
||||
this.afterRemoveStat(stat);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
getSiblings(stat: Stat<T>) {
|
||||
const { parent } = stat;
|
||||
return parent ? parent.children : this.stats!;
|
||||
},
|
||||
/**
|
||||
* The node should not exsit.
|
||||
* @param node
|
||||
* @param parent
|
||||
* @param index
|
||||
*/
|
||||
_setPosition(stat: Stat<T>, parent: Stat<T> | null, index: number) {
|
||||
const siblings = parent ? parent.children : this.stats!;
|
||||
siblings.splice(index, 0, stat);
|
||||
stat.parent = parent;
|
||||
stat.level = parent ? parent.level + 1 : 1;
|
||||
const flatIndex = this._calcFlatIndex(parent, index);
|
||||
const stats = this._flat(stat);
|
||||
this.statsFlat!.splice(flatIndex, 0, ...stats);
|
||||
for (const stat of stats) {
|
||||
if (!this._statsMap!.has(stat.data)) {
|
||||
this._statsMap!.set(stat.data, stat);
|
||||
}
|
||||
}
|
||||
hp.walkTreeData(
|
||||
stat,
|
||||
(node, index, parent) => {
|
||||
if (parent) {
|
||||
node.level = parent.level + 1;
|
||||
}
|
||||
},
|
||||
{ childrenKey: CHILDREN }
|
||||
);
|
||||
this.afterSetStat(stat, parent, index);
|
||||
},
|
||||
*iterateParent(stat: Stat<T>, opt?: { withSelf: boolean }) {
|
||||
let t = opt?.withSelf ? stat : stat.parent;
|
||||
while (t) {
|
||||
yield t;
|
||||
t = t.parent;
|
||||
}
|
||||
},
|
||||
move(stat: Stat<T>, parent: Stat<T> | null, index: number) {
|
||||
if (this.has(stat)) {
|
||||
if (
|
||||
stat.parent === parent &&
|
||||
this.getSiblings(stat).indexOf(stat) === index
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// check if is self
|
||||
if (stat === parent) {
|
||||
// 不允许移动目标为自己
|
||||
throw new Error(`Can't move node to it self`);
|
||||
}
|
||||
// check if is descendant
|
||||
if (parent && stat.level < parent.level) {
|
||||
let t;
|
||||
for (const item of this.iterateParent(parent)) {
|
||||
if (item.level === stat.level) {
|
||||
t = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (stat === t) {
|
||||
// 不允许移动节点到其后代节点下
|
||||
throw new Error(`Can't move node to its descendant`);
|
||||
}
|
||||
}
|
||||
this.remove(stat);
|
||||
}
|
||||
this._setPosition(stat, parent, index);
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* convert stat and its children to one-dimensional array
|
||||
* 转换节点和其后代节点为一维数组
|
||||
* @param stat
|
||||
* @returns
|
||||
*/
|
||||
_flat(stat: Stat<T>) {
|
||||
const r: Stat<T>[] = [];
|
||||
hp.walkTreeData(
|
||||
stat,
|
||||
(child) => {
|
||||
r.push(child);
|
||||
},
|
||||
{ childrenKey: CHILDREN }
|
||||
);
|
||||
return r;
|
||||
},
|
||||
/**
|
||||
* get count of stat and its all children
|
||||
* 统计节点和其后代节点数量
|
||||
* @param stat
|
||||
*/
|
||||
_count(stat: Stat<T>) {
|
||||
return this._flat(stat).length;
|
||||
},
|
||||
getData(filter?: (data: T) => T, root?: Stat<T>) {
|
||||
const { childrenKey } = this;
|
||||
const td = new hp.TreeData<T>([]);
|
||||
td.childrenKey = childrenKey;
|
||||
hp.walkTreeData(
|
||||
root || this.stats!,
|
||||
(stat, index, parent, path) => {
|
||||
let newData = { ...stat.data, [childrenKey]: [] };
|
||||
if (filter) {
|
||||
// @ts-ignore
|
||||
newData = filter(newData);
|
||||
}
|
||||
td.set(path, newData);
|
||||
},
|
||||
{
|
||||
childrenKey: CHILDREN,
|
||||
}
|
||||
);
|
||||
return td.data;
|
||||
},
|
||||
};
|
||||
type Base = typeof utilsBase;
|
||||
const utils: Base & {} = utilsBase;
|
||||
if (!utilsBase.noInitialization) {
|
||||
utils.init();
|
||||
}
|
||||
return utils;
|
||||
}
|
||||
|
||||
export type TreeProcessor = ReturnType<typeof makeTreeProcessor>;
|
||||
|
||||
export const defaultOptions = {
|
||||
childrenKey: "children",
|
||||
defaultOpen: false,
|
||||
statsHandler(stats: Stat<any>[]) {
|
||||
return stats;
|
||||
},
|
||||
statsFlatHandler(statsFlat: Stat<any>[]) {
|
||||
return statsFlat;
|
||||
},
|
||||
afterSetStat(stat: Stat<any>, parent: Stat<any> | null, index: number) {},
|
||||
afterRemoveStat(stat: Stat<any>) {},
|
||||
statHandler(stat: Stat<any>) {
|
||||
return stat;
|
||||
},
|
||||
};
|
||||
|
||||
export interface Options extends Partial<typeof defaultOptions> {
|
||||
/**
|
||||
* don't call init. You can call it manually later.
|
||||
*/
|
||||
noInitialization?: boolean;
|
||||
}
|
||||
|
||||
export interface Stat<T> extends ReturnType<typeof statDefault> {
|
||||
[x: string]: any;
|
||||
data: T;
|
||||
open: boolean;
|
||||
parent: Stat<T> | null;
|
||||
children: Stat<T>[];
|
||||
level: number;
|
||||
}
|
||||
|
||||
export function statDefault() {
|
||||
return {
|
||||
isStat: true,
|
||||
hidden: false,
|
||||
checked: false,
|
||||
style: null,
|
||||
class: null,
|
||||
draggable: null,
|
||||
droppable: null,
|
||||
} as {
|
||||
isStat: true;
|
||||
hidden: boolean;
|
||||
checked: boolean | 0; // 0 mean just part of children checked
|
||||
draggable: boolean | null; //null mean inhert parent
|
||||
droppable: boolean | null; //null mean inhert parent
|
||||
style: any;
|
||||
class: any;
|
||||
};
|
||||
}
|
||||
|
||||
class StatNotFoundError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "StatNotFoundError";
|
||||
}
|
||||
}
|
158
pages/components/baseTree/treeNode.vue
Normal file
158
pages/components/baseTree/treeNode.vue
Normal file
@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<view v-if="!table" class="tree-node" :class="{ 'tree-node--with-tree-line': treeLine }" :style="indentStyle" ref="el">
|
||||
<template v-if="treeLine">
|
||||
<view v-for="line in vLines" class="tree-line tree-vline" :style="line.style"></view>
|
||||
<view v-if="stat.level > 1" class="tree-line tree-hline" :style="hLineStyle"></view>
|
||||
</template>
|
||||
<view class="tree-node-inner">
|
||||
<slot :indentStyle="indentStyle"></slot>
|
||||
</view>
|
||||
</view>
|
||||
<tr v-else class="tree-node" ref="el">
|
||||
<slot :indentStyle="indentStyle"></slot>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, watch } from "./vue"
|
||||
|
||||
let justToggleOpen = false
|
||||
const afterToggleOpen = () => {
|
||||
justToggleOpen = true
|
||||
setTimeout(() => {
|
||||
justToggleOpen = false
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const cpt = defineComponent({
|
||||
// components: {},
|
||||
props: ["stat", "rtl", "btt", "indent", "table", "treeLine", "treeLineOffset", "processor"],
|
||||
emits: ["open", "close", "check"],
|
||||
setup(props, { emit }) {
|
||||
const indentStyle = computed(() => {
|
||||
return {
|
||||
[!props.rtl ? "paddingLeft" : "paddingRight"]:
|
||||
props.indent * (props.stat.level - 1) + "px",
|
||||
};
|
||||
});
|
||||
// watch checked
|
||||
watch(
|
||||
() => props.stat.checked,
|
||||
(checked) => {
|
||||
// fix issue: https://github.com/phphe/he-tree/issues/98
|
||||
// when open/close above node, the after nodes' states 'checked' and 'open' will be updated. It should be caused by Vue's key. We don't use Vue's key prop.
|
||||
if (justToggleOpen) {
|
||||
return
|
||||
}
|
||||
if (props.processor.afterOneCheckChanged(props.stat)) {
|
||||
emit("check", props.stat);
|
||||
}
|
||||
}
|
||||
);
|
||||
// watch open
|
||||
watch(
|
||||
() => props.stat.open,
|
||||
(open) => {
|
||||
if (justToggleOpen) {
|
||||
return
|
||||
}
|
||||
if (open) {
|
||||
emit("open", props.stat);
|
||||
} else {
|
||||
emit("close", props.stat);
|
||||
}
|
||||
afterToggleOpen()
|
||||
}
|
||||
);
|
||||
// tree lines
|
||||
const vLines = computed(() => {
|
||||
const lines: { style: object }[] = [];
|
||||
const hasNextVisibleNode = (stat) => {
|
||||
if (stat.parent) {
|
||||
let i = stat.parent?.children.indexOf(stat);
|
||||
do {
|
||||
i++
|
||||
let next = stat.parent.children[i]
|
||||
if (next) {
|
||||
if (!next.hidden) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
return false
|
||||
}
|
||||
const leftOrRight = props.rtl ? 'right' : 'left'
|
||||
const bottomOrTop = props.btt ? 'top' : 'bottom'
|
||||
let current = props.stat
|
||||
while (current) {
|
||||
let left = (current.level - 2) * props.indent + props.treeLineOffset
|
||||
const hasNext = hasNextVisibleNode(current)
|
||||
const addLine = () => {
|
||||
lines.push({
|
||||
style: {
|
||||
[leftOrRight]: left + 'px',
|
||||
[bottomOrTop]: hasNext ? 0 : '50%',
|
||||
}
|
||||
})
|
||||
}
|
||||
if (current === props.stat) {
|
||||
if (current.level > 1) {
|
||||
addLine()
|
||||
}
|
||||
} else if (hasNext) {
|
||||
addLine()
|
||||
}
|
||||
current = current.parent
|
||||
}
|
||||
return lines
|
||||
})
|
||||
const hLineStyle = computed(() => {
|
||||
let left = (props.stat.level - 2) * props.indent + props.treeLineOffset
|
||||
const leftOrRight = props.rtl ? 'right' : 'left'
|
||||
return {
|
||||
[leftOrRight]: left + 'px',
|
||||
}
|
||||
})
|
||||
return { indentStyle, vLines, hLineStyle, }
|
||||
},
|
||||
// data() {
|
||||
// return {}
|
||||
// },
|
||||
// computed: {},
|
||||
// watch: {},
|
||||
// methods: {},
|
||||
// created() {},
|
||||
// mounted() {}
|
||||
});
|
||||
export default cpt;
|
||||
export type TreeNodeType = InstanceType<typeof cpt>;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* tree line start */
|
||||
.tree-node--with-tree-line {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tree-line {
|
||||
position: absolute;
|
||||
background-color: #bbbbbb;
|
||||
}
|
||||
|
||||
.tree-vline {
|
||||
width: 1px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.tree-hline {
|
||||
height: 1px;
|
||||
top: 50%;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
/* tree line end */
|
||||
</style>
|
22
pages/components/baseTree/types.ts
Normal file
22
pages/components/baseTree/types.ts
Normal file
@ -0,0 +1,22 @@
|
||||
export type obj = Record<string, unknown>; // equal to object
|
||||
|
||||
export interface BaseNode {
|
||||
$id: string | number;
|
||||
$pid?: string | number;
|
||||
$level: number; // 0 is root
|
||||
$hidden?: boolean;
|
||||
$folded?: boolean;
|
||||
$checked?: boolean | 0;
|
||||
$children: Node[];
|
||||
$childrenLoading?: boolean;
|
||||
$childrenLoadStaus?: obj; // private
|
||||
$draggable?: boolean;
|
||||
$droppable?: boolean;
|
||||
// style
|
||||
$nodeStyle?: string | Record<string, string> | unknown;
|
||||
$nodeClass?: string | unknown;
|
||||
$outerStyle?: string | Record<string, string> | unknown;
|
||||
$outerClass?: string | unknown;
|
||||
}
|
||||
|
||||
export type Node = obj & BaseNode;
|
91
pages/components/baseTree/utils.ts
Normal file
91
pages/components/baseTree/utils.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import * as hp from "./helper-js";
|
||||
import { obj, BaseNode } from "./types";
|
||||
|
||||
export function genNodeID() {
|
||||
return `ht_${hp.randString(12)}`;
|
||||
}
|
||||
|
||||
export function initNode(node: obj) {
|
||||
if (!node.$id) {
|
||||
node.$id = genNodeID();
|
||||
}
|
||||
if (!node.$children) {
|
||||
node.$children = [];
|
||||
}
|
||||
}
|
||||
|
||||
export function convertTreeDataToFlat<T extends obj>(
|
||||
data: T[],
|
||||
childrenKey = "children",
|
||||
idKey = "id"
|
||||
) {
|
||||
const flatData: T[] = [];
|
||||
const mapForPid = new Map();
|
||||
hp.walkTreeData(
|
||||
data,
|
||||
(node, index, parent) => {
|
||||
const newNode = { $id: node[idKey], $pid: "", ...node };
|
||||
initNode(newNode);
|
||||
mapForPid.set(node, newNode.$id);
|
||||
newNode.$pid = (parent && mapForPid.get(parent)) || null;
|
||||
flatData.push(newNode);
|
||||
},
|
||||
childrenKey
|
||||
);
|
||||
return convertFlatDataToStandard(flatData, "$id", "$pid");
|
||||
}
|
||||
|
||||
export function convertFlatDataToStandard<T extends obj>(
|
||||
data: T[],
|
||||
idKey = "id",
|
||||
pidKey = "parent_id"
|
||||
) {
|
||||
const nodesByID: Record<string, T & BaseNode> = {};
|
||||
let nodes = data.map((node) => {
|
||||
// @ts-ignore
|
||||
const newNode: T & BaseNode = {
|
||||
$id: <string>node[idKey],
|
||||
$pid: <string>node[pidKey],
|
||||
...node,
|
||||
$children: [],
|
||||
};
|
||||
initNode(newNode);
|
||||
nodesByID[<string>newNode.$id] = newNode;
|
||||
return newNode;
|
||||
});
|
||||
const top = [];
|
||||
for (const node of nodes) {
|
||||
if (node.$level == null) {
|
||||
node.$level = resolveLevel(node);
|
||||
}
|
||||
const parent = node.$pid && nodesByID[node.$pid];
|
||||
if (parent) {
|
||||
parent.$children.push(node);
|
||||
}
|
||||
if (node.$level === 1) {
|
||||
top.push(node);
|
||||
}
|
||||
}
|
||||
nodes = [];
|
||||
hp.walkTreeData(
|
||||
top,
|
||||
(node) => {
|
||||
nodes.push(node);
|
||||
},
|
||||
"$children"
|
||||
);
|
||||
return { nodes, nodesByID };
|
||||
//
|
||||
function resolveLevel(node: T & BaseNode): number {
|
||||
if (node.$level && node.$level > 0) {
|
||||
return node.$level;
|
||||
} else {
|
||||
const parent = nodesByID[node.$pid || ""];
|
||||
if (!parent) {
|
||||
return 1;
|
||||
} else {
|
||||
return resolveLevel(parent) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
pages/components/baseTree/vue/index.ts
Normal file
16
pages/components/baseTree/vue/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
// @ts-nocheck
|
||||
|
||||
// #ifdef VUE3
|
||||
export * from 'vue';
|
||||
// #endif
|
||||
|
||||
// #ifndef VUE3
|
||||
export * from '@vue/composition-api';
|
||||
|
||||
// #ifdef APP-NVUE
|
||||
import Vue from 'vue'
|
||||
import VueCompositionAPI from '@vue/composition-api'
|
||||
Vue.use(VueCompositionAPI)
|
||||
// #endif
|
||||
|
||||
// #endif
|
Reference in New Issue
Block a user