Files
HTFX-CRM-APP/pages/components/baseTree/baseTree.vue
2025-07-07 15:55:44 +08:00

411 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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