265 lines
6.6 KiB
Vue
265 lines
6.6 KiB
Vue
<template>
|
|
<div class="group-tree-search-panel">
|
|
<div v-if="searchVisible" class="search-bar mt-8 mb-8">
|
|
<Input v-model="searchKeyword" :placeholder="searchPlaceholder" suffix="ios-search" search clearable
|
|
class="search-input" @on-search="onSearch" @on-clear="onClear" />
|
|
<Button v-if="showCloseButton" icon="md-close" @click="closeSearch" />
|
|
</div>
|
|
|
|
<div class="tree-body">
|
|
<MyTree v-if="!treeLoading" ref="refMyTree" :autoClick="autoClick" :treeData="treeData"
|
|
:setting="treeSetting" @on-tree-click="treeLink" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { ref, nextTick, useTemplateRef } from "vue";
|
|
import { cloneDeep } from "lodash";
|
|
import { Message } from "view-ui-plus";
|
|
import MyTree from '@/components/MyTree.vue'
|
|
|
|
const GROUP_TYPE = {
|
|
NORMAL_GROUP: 1,
|
|
SCHOOL_GROUP: 2,
|
|
GRADE_GROUP: 3,
|
|
CLASS_GROUP: 4,
|
|
AREA_GROUP: 5,
|
|
STAGE_GROUP: 6,
|
|
TEACHER_GROUP: 7,
|
|
STUDENT_GROUP: 8,
|
|
} as const
|
|
|
|
const DEFAULT_CACHE_KEY = 'bsch.group-tree.selected-node'
|
|
|
|
const props = withDefaults(defineProps<{
|
|
treeApi: () => Promise<any[]>;
|
|
autoClick?: boolean;
|
|
defaultSearchVisible?: boolean;
|
|
showCloseButton?: boolean;
|
|
searchPlaceholder?: string;
|
|
cacheKey?: string;
|
|
}>(), {
|
|
autoClick: false,
|
|
defaultSearchVisible: true,
|
|
showCloseButton: false,
|
|
searchPlaceholder: '输入组名称搜索',
|
|
cacheKey: DEFAULT_CACHE_KEY,
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
'on-change': [treeNode: any]
|
|
'on-tree-data': [tree: any[]]
|
|
}>()
|
|
|
|
const curTreeNode = defineModel<any>('curTreeNode', { default: {} })
|
|
|
|
const refMyTree = useTemplateRef('refMyTree')
|
|
const treeLoading = ref(false)
|
|
const treeData = ref<any[]>([])
|
|
const searchKeyword = ref('')
|
|
const searchVisible = ref(props.defaultSearchVisible)
|
|
|
|
const treeSetting = ref({
|
|
view: { dblClickExpand: false, showIcon: false },
|
|
edit: { enable: false, showRemoveBtn: false, showRenameBtn: false, drag: false },
|
|
check: { enable: false },
|
|
callback: {},
|
|
})
|
|
|
|
function formatTree(list: any[], depth = 1): any[] {
|
|
return (list || []).map((node) => {
|
|
const children = node.children;
|
|
const hasChildren = Array.isArray(children) && children.length > 0;
|
|
const formatted: any = { ...node, isParent: hasChildren };
|
|
if (depth === 1) formatted.open = true;
|
|
if (hasChildren) {
|
|
formatted.children = formatTree(children, depth + 1);
|
|
} else {
|
|
delete formatted.children;
|
|
}
|
|
return formatted;
|
|
});
|
|
}
|
|
|
|
function findNodeById(list: any[], id?: number | null): any {
|
|
if (id == null) return null;
|
|
for (const node of list || []) {
|
|
if (Number(node.id) === Number(id)) return node;
|
|
const child = findNodeById(node.children || [], id);
|
|
if (child) return child;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function findFirstNodeByType(list: any[], type: number): any {
|
|
for (const node of list || []) {
|
|
if (Number(node.type) === type) return node;
|
|
const child = findFirstNodeByType(node.children || [], type);
|
|
if (child) return child;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function getCachedSelectedNodeId() {
|
|
try {
|
|
const raw = sessionStorage.getItem(props.cacheKey);
|
|
if (!raw) return null;
|
|
const cached = JSON.parse(raw);
|
|
const id = Number(cached?.id);
|
|
return Number.isFinite(id) ? id : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function cacheSelectedNode(treeNode: any) {
|
|
if (!treeNode?.id) return;
|
|
sessionStorage.setItem(props.cacheKey, JSON.stringify({
|
|
id: Number(treeNode.id),
|
|
name: treeNode.name,
|
|
type: treeNode.type,
|
|
}));
|
|
}
|
|
|
|
function resolveInitialSelectId(list: any[], preferredId?: number | null) {
|
|
const preferred = findNodeById(list, preferredId);
|
|
if (preferred) return preferred.id;
|
|
|
|
const cached = findNodeById(list, getCachedSelectedNodeId());
|
|
if (cached) return cached.id;
|
|
|
|
const schoolNode = findFirstNodeByType(list, GROUP_TYPE.SCHOOL_GROUP);
|
|
if (schoolNode) return schoolNode.id;
|
|
|
|
if (props.autoClick && list.length) return list[0].id;
|
|
return list[0]?.id ?? null;
|
|
}
|
|
|
|
async function loadTree(preferredId?: number | null) {
|
|
treeLoading.value = true;
|
|
try {
|
|
const tree = await props.treeApi();
|
|
treeData.value = formatTree(tree || []);
|
|
emit('on-tree-data', cloneDeep(treeData.value));
|
|
} catch (e: any) {
|
|
Message.error(e?.message || '加载组织树失败');
|
|
} finally {
|
|
treeLoading.value = false;
|
|
}
|
|
|
|
if (!treeData.value.length) return;
|
|
|
|
await nextTick();
|
|
const selectId = resolveInitialSelectId(treeData.value, preferredId);
|
|
if (selectId != null) {
|
|
refMyTree.value?.reloadTree(selectId);
|
|
}
|
|
}
|
|
|
|
async function refreshTree(selectId?: number) {
|
|
await loadTree(selectId);
|
|
}
|
|
|
|
function treeLink(treeNode: any) {
|
|
const copyNode = cloneDeep(treeNode)
|
|
copyNode.children = [];
|
|
curTreeNode.value = copyNode;
|
|
cacheSelectedNode(copyNode);
|
|
emit('on-change', copyNode)
|
|
}
|
|
|
|
function onSearch() {
|
|
refMyTree.value?.search(searchKeyword.value);
|
|
}
|
|
|
|
function onClear() {
|
|
searchKeyword.value = '';
|
|
refMyTree.value?.search('');
|
|
}
|
|
|
|
function openSearch() {
|
|
searchVisible.value = true;
|
|
}
|
|
|
|
function closeSearch() {
|
|
searchVisible.value = false;
|
|
onClear();
|
|
}
|
|
|
|
function reloadTree(id?: number) {
|
|
refMyTree.value?.reloadTree(id);
|
|
}
|
|
|
|
function selectNode(id: number, isClick = true) {
|
|
refMyTree.value?.selectNode(id, isClick);
|
|
}
|
|
|
|
function removeNode(id: number) {
|
|
refMyTree.value?.removeNode(id);
|
|
}
|
|
|
|
function renameNode(id: number, name: string) {
|
|
refMyTree.value?.renameNode(id, name);
|
|
}
|
|
|
|
function getNodeById(id: number) {
|
|
return refMyTree.value?.getNodeById(id);
|
|
}
|
|
|
|
function addNote(pid: number, id: number, name: string, isParent?: boolean, children?: any[]) {
|
|
refMyTree.value?.addNote(pid, id, name, isParent, children);
|
|
}
|
|
|
|
function openTree(open: boolean) {
|
|
refMyTree.value?.openTree(open);
|
|
}
|
|
|
|
loadTree()
|
|
|
|
defineExpose({
|
|
openSearch,
|
|
closeSearch,
|
|
onSearch,
|
|
onClear,
|
|
loadTree,
|
|
refreshTree,
|
|
reloadTree,
|
|
selectNode,
|
|
removeNode,
|
|
renameNode,
|
|
getNodeById,
|
|
addNote,
|
|
openTree,
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.group-tree-search-panel {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
}
|
|
|
|
.search-bar {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.search-input {
|
|
width: 100%;
|
|
}
|
|
|
|
.tree-body {
|
|
flex: 1;
|
|
min-height: 0;
|
|
}
|
|
|
|
.tree-body :deep(.ztree) {
|
|
height: 100%;
|
|
overflow-y: auto;
|
|
}
|
|
</style>
|