Files
HTFX-CRM-APP/pages/components/baseTree/baseTree.vue

411 lines
12 KiB
Vue
Raw Permalink Normal View History

2025-07-07 15:55:44 +08:00
<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>