Files
userbase/src/views/group-user/group/index.vue
T
2026-08-19 12:01:26 +08:00

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>