400 lines
13 KiB
Vue
400 lines
13 KiB
Vue
<template>
|
|
<div class="group-user-tree">
|
|
<ul class="opera-ul">
|
|
<li class="add-node-li" @click="showAddNode">
|
|
<Tooltip content="新增子组" placement="top" transfer>
|
|
<SvgIcon name="tree-icon-1" />
|
|
</Tooltip>
|
|
</li>
|
|
<Tooltip content="编辑组" placement="top" transfer>
|
|
<li @click="showEditNode">
|
|
<SvgIcon name="tree-icon-2" />
|
|
</li>
|
|
</Tooltip>
|
|
<Tooltip content="子组排序" placement="top" transfer>
|
|
<li @click="showSortNode">
|
|
<SvgIcon name="tree-icon-3" />
|
|
</li>
|
|
</Tooltip>
|
|
<Tooltip content="移动组" placement="top" transfer>
|
|
<li @click="showMoveNode">
|
|
<SvgIcon name="tree-icon-4" />
|
|
</li>
|
|
</Tooltip>
|
|
<Tooltip content="删除组" placement="top" transfer>
|
|
<li @click="showDeleteNode">
|
|
<SvgIcon name="tree-icon-5" />
|
|
</li>
|
|
</Tooltip>
|
|
<Tooltip content="搜索组" placement="top" transfer>
|
|
<li class="search-node-li" @click="refTreePanel?.openSearch()">
|
|
<Icon type="md-search" />
|
|
</li>
|
|
</Tooltip>
|
|
</ul>
|
|
|
|
<GroupTreeSearchPanel ref="refTreePanel" v-model:curTreeNode="curTreeNode" :treeApi="treeApi"
|
|
:defaultSearchVisible="false" :showCloseButton="true" @on-change="treeLink" @on-tree-data="onTreeData" />
|
|
|
|
<Modal v-model="editNodeModal.modal" :title="editNodeModal.title" :width="450" :mask-closable="false">
|
|
<Form ref="refFormData" :label-width="100" :model="editNodeModal" class="myform mb-20 mr-20">
|
|
<Form-Item label="当前位置:">
|
|
{{ curNodePath.join(' > ') }}
|
|
</Form-Item>
|
|
<Form-Item label="组类型:" prop="type"
|
|
:rules="{ required: true, type: 'number', message: '请选择组类型', trigger: 'change' }">
|
|
<Select v-model="editNodeModal.type" placeholder="请选择组类型" transfer>
|
|
<Option v-for="t in groupTypeOptions" :key="t.value" :value="t.value">{{ t.label }}</Option>
|
|
</Select>
|
|
</Form-Item>
|
|
<Form-Item label="节点名称:" prop="name" :rules="{ required: true, message: '请输入节点名称', trigger: 'blur' }">
|
|
<Input ref="refNodeNameInput" maxlength="30" v-model="editNodeModal.name" />
|
|
</Form-Item>
|
|
</Form>
|
|
<template #footer>
|
|
<Button type="link" v-if="!editNodeModal.id" @click="switchToBatch">切换至批量添加</Button>
|
|
<Button type="primary" :loading="editNodeModal.loading" @click="saveEditNode">确认</Button>
|
|
<Button @click="editNodeModal.modal = false">取消</Button>
|
|
</template>
|
|
</Modal>
|
|
|
|
<BatchAddGroup v-model:visible="batchModal" :tree-data="treeData" :default-pid="curTreeNode?.id"
|
|
:default-type="batchDefaultType" @success="onBatchSuccess" />
|
|
|
|
<GroupSort v-model:visible="sortModal" :parent-id="sortParentId" :parent-name="sortParentName"
|
|
:children="sortChildrenSource" @saved="onSortSaved" />
|
|
|
|
<GroupMove v-model:visible="moveModal" :tree-data="treeData" :move-id="moveNodeId" :move-name="moveNodeName"
|
|
:move-path="moveNodePath" @saved="onMoveSaved" />
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { computed, nextTick, onMounted, reactive, ref, useTemplateRef } from "vue";
|
|
import { cloneDeep } from "lodash";
|
|
import { Message } from "view-ui-plus";
|
|
import SvgIcon from "@/components/SvgIcon.vue";
|
|
import GroupTreeSearchPanel from '@/components/GroupTreeSearchPanel.vue'
|
|
import { useDefaultStore } from "@/store";
|
|
import MyConfirm from "@/components/MyConfirm";
|
|
import Apis from "@/api";
|
|
import BatchAddGroup from "./BatchAddGroup.vue";
|
|
import GroupSort from "./GroupSort.vue";
|
|
import GroupMove from "./GroupMove.vue";
|
|
import Utils from "@/utils/index.ts";
|
|
|
|
const props = withDefaults(defineProps<{
|
|
treeApi?: () => Promise<any[]>;
|
|
}>(), {
|
|
treeApi: () => Apis.SysGroup.getTree(),
|
|
})
|
|
const treeApi = props.treeApi
|
|
|
|
const emit = defineEmits<{
|
|
'on-change': [treeNode: any]
|
|
'on-tree-data': [tree: any[]]
|
|
}>()
|
|
|
|
const curTreeNode = defineModel<any>('curTreeNode', { default: {} })
|
|
|
|
const store = useDefaultStore();
|
|
|
|
const refTreePanel = useTemplateRef('refTreePanel');
|
|
const refFormData = useTemplateRef('refFormData')
|
|
const refNodeNameInput = useTemplateRef('refNodeNameInput')
|
|
|
|
const treeData = ref<any[]>([])
|
|
|
|
const batchModal = ref(false)
|
|
const batchDefaultType = ref<number | null>(null)
|
|
|
|
const sortModal = ref(false)
|
|
const sortParentId = ref<number | null>(null)
|
|
const sortParentName = ref('')
|
|
const sortChildrenSource = ref<any[]>([])
|
|
|
|
const moveModal = ref(false)
|
|
const moveNodeId = ref<number | null>(null)
|
|
const moveNodeName = ref('')
|
|
const moveNodePath = ref('')
|
|
|
|
const editNodeModal = reactive({
|
|
modal: false,
|
|
title: '',
|
|
loading: false,
|
|
name: null as any,
|
|
id: null as any,
|
|
parentId: null as any,
|
|
type: null as any,
|
|
})
|
|
|
|
const groupTypeOptions = computed(() => {
|
|
const dict: any = (store.getSysDict as any)?.grouptype || [];
|
|
const map = (x: any) => ({ value: Number(x.key ?? x.k), label: String(x.value ?? x.v ?? x.name) });
|
|
const list = Array.isArray(dict) ? dict.map(map) : Object.entries(dict).map(([k, v]) => ({ value: Number(k), label: String(v) }));
|
|
if (list.length === 0) list.push({ value: 4, label: '班级组' });
|
|
return list;
|
|
})
|
|
|
|
const curNodePath = computed(() => {
|
|
if (!curTreeNode.value || !curTreeNode.value.getPath) return [];
|
|
return curTreeNode.value.getPath().map((x: any) => x.name)
|
|
})
|
|
|
|
function onTreeData(tree: any[]) {
|
|
treeData.value = tree;
|
|
emit('on-tree-data', cloneDeep(tree));
|
|
}
|
|
|
|
function treeLink(treeNode: any) {
|
|
const copyNode = cloneDeep(treeNode)
|
|
copyNode.children = [];
|
|
curTreeNode.value = copyNode;
|
|
emit('on-change', copyNode)
|
|
}
|
|
|
|
function currentNode() {
|
|
return curTreeNode.value?.id ? curTreeNode.value : (treeData.value[0] || {});
|
|
}
|
|
|
|
async function showAddNode() {
|
|
const node = currentNode();
|
|
editNodeModal.title = '新增子组';
|
|
refFormData.value?.resetFields();
|
|
await Utils.sleep(0);
|
|
editNodeModal.modal = true;
|
|
editNodeModal.id = null;
|
|
editNodeModal.name = null;
|
|
editNodeModal.parentId = node?.id ?? null;
|
|
editNodeModal.type = node?.type ?? groupTypeOptions.value[0]?.value ?? null;
|
|
setTimeout(() => {
|
|
|
|
refNodeNameInput.value?.focus();
|
|
}, 300);
|
|
|
|
}
|
|
|
|
async function showEditNode() {
|
|
const node = currentNode();
|
|
editNodeModal.title = '编辑组';
|
|
refFormData.value?.resetFields();
|
|
await Utils.sleep(0);
|
|
editNodeModal.modal = true;
|
|
editNodeModal.id = node?.id ?? null;
|
|
editNodeModal.name = node?.name ?? '';
|
|
editNodeModal.parentId = node?.parentId ?? null;
|
|
editNodeModal.type = node?.type ?? null;
|
|
|
|
nextTick().then(() => refNodeNameInput.value?.focus())
|
|
}
|
|
|
|
function switchToBatch() {
|
|
batchDefaultType.value = editNodeModal.type ?? null;
|
|
editNodeModal.modal = false;
|
|
batchModal.value = true;
|
|
}
|
|
|
|
function findNode(list: any[], id: number): any {
|
|
for (const n of list || []) {
|
|
if (n.id === id) return n;
|
|
if (n.children?.length) {
|
|
const r = findNode(n.children, id);
|
|
if (r) return r;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function showSortNode() {
|
|
const node = currentNode();
|
|
if (!node?.id) {
|
|
Message.warning('请先选择一个组');
|
|
return;
|
|
}
|
|
const found = findNode(treeData.value, node.id);
|
|
sortChildrenSource.value = found?.children ?? [];
|
|
sortParentId.value = node.id;
|
|
sortParentName.value = node.name ?? '';
|
|
sortModal.value = true;
|
|
}
|
|
|
|
function showMoveNode() {
|
|
const node = currentNode();
|
|
if (!node?.id) {
|
|
Message.warning('请先选择一个组');
|
|
return;
|
|
}
|
|
moveNodeId.value = node.id;
|
|
moveNodeName.value = node.name ?? '';
|
|
moveNodePath.value = node.getPath ? node.getPath().map((x: any) => x.name).join('/') : (node.name ?? '');
|
|
moveModal.value = true;
|
|
}
|
|
|
|
function detachNode(list: any[], id: number): any {
|
|
for (let i = 0; i < list.length; i++) {
|
|
if (list[i].id === id) {
|
|
const [n] = list.splice(i, 1);
|
|
return n;
|
|
}
|
|
if (list[i].children?.length) {
|
|
const r = detachNode(list[i].children, id);
|
|
if (r) {
|
|
if (list[i].children.length === 0) {
|
|
delete list[i].children;
|
|
list[i].isParent = false;
|
|
}
|
|
return r;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function attachNode(list: any[], pid: number, node: any): boolean {
|
|
for (const n of list) {
|
|
if (n.id === pid) {
|
|
n.children = n.children || [];
|
|
n.children.push(node);
|
|
n.isParent = true;
|
|
return true;
|
|
}
|
|
if (n.children?.length && attachNode(n.children, pid, node)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function onMoveSaved({ movedId, newPid }: { movedId: number; newPid: number }) {
|
|
const full = findNode(treeData.value, movedId);
|
|
const isParent = !!(full?.children && full.children.length);
|
|
const name = full?.name ?? moveNodeName.value;
|
|
const children = isParent ? cloneDeep(full?.children) : undefined;
|
|
|
|
const detached = detachNode(treeData.value, movedId);
|
|
if (detached) {
|
|
attachNode(treeData.value, newPid, { ...detached, isParent, children: isParent ? detached.children : undefined });
|
|
}
|
|
|
|
refTreePanel.value?.removeNode(movedId);
|
|
refTreePanel.value?.addNote(newPid, movedId, name, isParent, children);
|
|
refTreePanel.value?.selectNode(newPid, true);
|
|
}
|
|
|
|
function showDeleteNode() {
|
|
const node = currentNode();
|
|
if (!node?.id) {
|
|
Message.warning('请先选择一个组');
|
|
return;
|
|
}
|
|
MyConfirm({
|
|
title: '删除组',
|
|
msg: `确定删除名为【${node?.name}】的组?`,
|
|
info: '删除后不可恢复!',
|
|
async onOk() {
|
|
try {
|
|
await Apis.SysGroup.sysGroupRemove({ pathParams: { id: node?.id } });
|
|
Message.success('删除成功!');
|
|
refTreePanel.value?.removeNode(node?.id);
|
|
} catch (e: any) {
|
|
console.warn(e?.message || '删除失败');
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
async function saveEditNode() {
|
|
const validate = await refFormData.value?.validate();
|
|
if (!validate) {
|
|
Message.error('请填写红色星号内容!');
|
|
return;
|
|
}
|
|
const { id, name } = editNodeModal;
|
|
editNodeModal.loading = true;
|
|
try {
|
|
if (!id) {
|
|
const parentId = editNodeModal.parentId;
|
|
const data: any = { name, parentId, type: Number(editNodeModal.type) };
|
|
await Apis.SysGroup.sysGroupAdd({ data });
|
|
Message.success('新增成功');
|
|
await refTreePanel.value?.refreshTree(parentId);
|
|
} else {
|
|
await Apis.SysGroup.sysGroupEdit({ pathParams: { id }, data: { name, type: Number(editNodeModal.type) } });
|
|
Message.success('编辑成功');
|
|
await refTreePanel.value?.refreshTree(id);
|
|
}
|
|
editNodeModal.modal = false;
|
|
} catch (e: any) {
|
|
console.warn(e?.message || '保存失败');
|
|
} finally {
|
|
editNodeModal.loading = false;
|
|
}
|
|
}
|
|
|
|
function onBatchSuccess() {
|
|
refTreePanel.value?.refreshTree(curTreeNode.value?.id);
|
|
}
|
|
|
|
function onSortSaved() {
|
|
refTreePanel.value?.refreshTree(curTreeNode.value?.id);
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await nextTick();
|
|
refTreePanel.value?.refreshTree(curTreeNode.value?.id);
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.group-user-tree {
|
|
width: 300px !important;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
}
|
|
|
|
.opera-ul {
|
|
display: flex;
|
|
justify-content: space-around;
|
|
align-items: center;
|
|
border-bottom: 1px solid #ebeef5;
|
|
padding-bottom: 10px;
|
|
|
|
li {
|
|
padding: 10px;
|
|
height: 38px;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
|
|
&.active,
|
|
&:hover {
|
|
background-color: #f5f5f5;
|
|
}
|
|
|
|
svg {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
|
|
.ivu-icon {
|
|
font-size: 18px;
|
|
}
|
|
}
|
|
|
|
li.disabled {
|
|
cursor: not-allowed;
|
|
opacity: 0.4;
|
|
|
|
&:hover {
|
|
background-color: #fff;
|
|
}
|
|
}
|
|
|
|
.add-node-li {
|
|
position: relative;
|
|
}
|
|
|
|
.search-node-li {
|
|
position: relative;
|
|
}
|
|
}
|
|
</style>
|