Files
NoticeFront/src/components/GroupTreeSearchPanel.vue
T

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>