Files
mayfly-go/frontend/src/views/system/resource/ResourceList.vue

424 lines
16 KiB
Vue
Raw Normal View History

<template>
2023-12-14 13:05:21 +08:00
<div class="card system-resouce-list">
2024-03-26 21:46:03 +08:00
<Splitpanes class="default-theme">
<Pane size="25" min-size="20" max-size="30">
<div class="card pd5 mr5">
<el-input v-model="filterResource" clearable placeholder="输入关键字过滤(右击操作)" style="width: 200px; margin-right: 10px" />
<el-button v-auth="perms.addResource" type="primary" icon="plus" @click="addResource(false)"></el-button>
<div class="fr">
<el-tooltip placement="top">
<template #content> 红色橙色字体表示禁用状态 (右击资源进行操作) </template>
<span> <SvgIcon name="question-filled" /> </span>
</el-tooltip>
</div>
</div>
<el-scrollbar class="tree-data">
<el-tree
ref="resourceTreeRef"
class="none-select"
:indent="24"
node-key="id"
:props="props"
:data="data"
highlight-current
@node-expand="handleNodeExpand"
@node-collapse="handleNodeCollapse"
@node-contextmenu="nodeContextmenu"
@node-click="treeNodeClick"
:default-expanded-keys="defaultExpandedKeys"
:expand-on-click-node="false"
draggable
:allow-drop="allowDrop"
@node-drop="handleDrop"
:filter-node-method="filterNode"
>
<template #default="{ data }">
<span class="custom-tree-node">
<span style="font-size: 13px" v-if="data.type === menuTypeValue">
<span style="color: #3c8dbc"></span>
<span v-if="data.status == 1">{{ data.name }}</span>
<span v-if="data.status == -1" style="color: #e6a23c">{{ data.name }}</span>
<span style="color: #3c8dbc"></span>
<el-tag v-if="data.children !== null" size="small">{{ data.children.length }}</el-tag>
</span>
<span style="font-size: 13px" v-if="data.type === permissionTypeValue">
<span style="color: #3c8dbc"></span>
<span :style="data.status == 1 ? 'color: #67c23a;' : 'color: #f67c6c;'">{{ data.name }}</span>
<span style="color: #3c8dbc"></span>
</span>
</span>
</template>
</el-tree>
</el-scrollbar>
</Pane>
<Pane min-size="40">
<div class="ml10">
<el-tabs v-model="state.activeTabName" v-if="currentResource">
<el-tab-pane label="菜单资源详情" :name="ResourceDetail">
<el-descriptions title="资源信息" :column="2" border>
<el-descriptions-item label="类型">
<enum-tag :enums="ResourceTypeEnum" :value="currentResource?.type" />
</el-descriptions-item>
<el-descriptions-item label="名称">{{ currentResource.name }}</el-descriptions-item>
<el-descriptions-item label="code[菜单path]">{{ currentResource.code }}</el-descriptions-item>
<el-descriptions-item v-if="currentResource.type == menuTypeValue" label="图标">
<SvgIcon :name="currentResource.meta.icon" />
</el-descriptions-item>
<el-descriptions-item v-if="currentResource.type == menuTypeValue" label="路由名">
{{ currentResource.meta.routeName }}
</el-descriptions-item>
<el-descriptions-item v-if="currentResource.type == menuTypeValue" label="组件路径">
{{ currentResource.meta.component }}
</el-descriptions-item>
<el-descriptions-item v-if="currentResource.type == menuTypeValue" label="是否缓存">
{{ currentResource.meta.isKeepAlive ? '是' : '否' }}
</el-descriptions-item>
<el-descriptions-item v-if="currentResource.type == menuTypeValue" label="是否隐藏">
{{ currentResource.meta.isHide ? '是' : '否' }}
</el-descriptions-item>
<el-descriptions-item v-if="currentResource.type == menuTypeValue" label="tag不可删除">
{{ currentResource.meta.isAffix ? '是' : '否' }}
</el-descriptions-item>
<el-descriptions-item v-if="currentResource.type == menuTypeValue" label="外链">
{{ currentResource.meta.linkType ? '是' : '否' }}
</el-descriptions-item>
<el-descriptions-item v-if="currentResource.type == menuTypeValue && currentResource.meta.linkType > 0" label="外链">
{{ currentResource.meta.link }}
</el-descriptions-item>
<el-descriptions-item label="创建者">{{ currentResource.creator }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatDate(currentResource.createTime) }} </el-descriptions-item>
2024-03-26 21:46:03 +08:00
<el-descriptions-item label="修改者">{{ currentResource.modifier }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ formatDate(currentResource.updateTime) }} </el-descriptions-item>
2024-03-26 21:46:03 +08:00
</el-descriptions>
</el-tab-pane>
</el-tabs>
</div>
</Pane>
</Splitpanes>
<ResourceEdit
:title="dialogForm.title"
v-model:visible="dialogForm.visible"
v-model:data="dialogForm.data"
:typeDisabled="dialogForm.typeDisabled"
:departTree="data"
:type="dialogForm.type"
@val-change="valChange"
/>
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
</div>
</template>
<script lang="ts" setup>
2023-12-14 13:05:21 +08:00
import { ref, toRefs, reactive, onMounted, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import ResourceEdit from './ResourceEdit.vue';
import { ResourceTypeEnum } from '../enums';
import { resourceApi } from '../api';
import { formatDate } from '@/common/utils/format';
2024-03-26 21:46:03 +08:00
import EnumTag from '@/components/enumtag/EnumTag.vue';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
2024-03-26 21:46:03 +08:00
import { Splitpanes, Pane } from 'splitpanes';
import { isPrefixSubsequence } from '@/common/utils/string';
const menuTypeValue = ResourceTypeEnum.Menu.value;
const permissionTypeValue = ResourceTypeEnum.Permission.value;
2023-08-16 17:37:33 +08:00
const perms = {
addResource: 'resource:add',
delResource: 'resource:delete',
updateResource: 'resource:update',
changeStatus: 'resource:changeStatus',
};
const props = {
label: 'name',
children: 'children',
};
const contextmenuRef = ref();
2023-12-14 13:05:21 +08:00
const filterResource = ref();
const resourceTreeRef = ref();
2024-03-26 21:46:03 +08:00
const ResourceDetail = 'resourceDetail';
const contextmenuAdd = new ContextmenuItem('add', '添加子资源')
.withIcon('circle-plus')
.withPermission(perms.addResource)
.withHideFunc((data: any) => data.type !== menuTypeValue)
.withOnClick((data: any) => addResource(data));
const contextmenuEdit = new ContextmenuItem('edit', '编辑')
.withIcon('edit')
.withPermission(perms.updateResource)
.withOnClick((data: any) => editResource(data));
const contextmenuEnable = new ContextmenuItem('enable', '启用')
.withIcon('circle-check')
.withPermission(perms.updateResource)
.withHideFunc((data: any) => data.status === 1)
.withOnClick((data: any) => changeStatus(data, 1));
const contextmenuDisable = new ContextmenuItem('disable', '禁用')
.withIcon('circle-close')
.withPermission(perms.updateResource)
.withHideFunc((data: any) => data.status === -1)
.withOnClick((data: any) => changeStatus(data, -1));
const contextmenuDel = new ContextmenuItem('delete', '删除')
.withIcon('delete')
.withPermission(perms.delResource)
.withOnClick((data: any) => deleteMenu(data));
const state = reactive({
contextmenu: {
dropdown: {
x: 0,
y: 0,
},
2024-03-26 21:46:03 +08:00
items: [contextmenuAdd, contextmenuEdit, contextmenuEnable, contextmenuDisable, contextmenuDel],
},
//弹出框对象
dialogForm: {
type: null,
title: '',
visible: false,
data: { pid: 0, type: 1 },
// 资源类型选择是否选
typeDisabled: true,
},
data: [],
// 展开的节点
defaultExpandedKeys: [] as any[],
2024-03-26 21:46:03 +08:00
activeTabName: ResourceDetail,
currentResource: null as any,
});
2024-03-26 21:46:03 +08:00
const { currentResource, dialogForm, data, defaultExpandedKeys } = toRefs(state);
onMounted(() => {
search();
});
2023-12-14 13:05:21 +08:00
watch(filterResource, (val) => {
resourceTreeRef.value!.filter(val);
});
const filterNode = (value: string, data: any) => {
return !value || isPrefixSubsequence(value, data.name);
2023-12-14 13:05:21 +08:00
};
const search = async () => {
let res = await resourceApi.list.request(null);
state.data = res;
};
// 树节点右击事件
const nodeContextmenu = (event: any, data: any) => {
const { clientX, clientY } = event;
state.contextmenu.dropdown.x = clientX;
state.contextmenu.dropdown.y = clientY;
contextmenuRef.value.openContextmenu(data);
};
2024-03-26 21:46:03 +08:00
const treeNodeClick = async (data: any) => {
// 关闭可能存在的右击菜单
contextmenuRef.value.closeContextmenu();
2024-03-26 21:46:03 +08:00
let info = await resourceApi.detail.request({ id: data.id });
state.currentResource = info;
if (info.meta && info.meta != '') {
state.currentResource.meta = JSON.parse(info.meta);
}
};
const deleteMenu = (data: any) => {
ElMessageBox.confirm(`此操作将删除 [${data.name}], 是否继续?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
resourceApi.del
.request({
id: data.id,
})
.then(() => {
ElMessage.success('删除成功!');
search();
});
});
};
const addResource = (data: any) => {
let dialog = state.dialogForm;
dialog.data = { pid: 0, type: 1 };
// 添加顶级菜单情况
if (!data) {
dialog.typeDisabled = true;
dialog.data.type = menuTypeValue;
dialog.title = '添加顶级菜单';
dialog.visible = true;
return;
}
// 添加子菜单把当前菜单id作为新增菜单pid
dialog.data.pid = data.id;
dialog.title = '添加“' + data.name + '”的子资源 ';
if (data.children === null || data.children.length === 0) {
// 如果子节点不存在,则资源类型可选择
dialog.typeDisabled = false;
} else {
dialog.typeDisabled = true;
let hasPermission = false;
for (let c of data.children) {
if (c.type === permissionTypeValue) {
hasPermission = true;
break;
}
}
// 如果子节点中存在权限资源,则只能新增权限资源,否则只能新增菜单资源
if (hasPermission) {
dialog.data.type = permissionTypeValue;
} else {
dialog.data.type = menuTypeValue;
}
}
dialog.visible = true;
};
const editResource = async (data: any) => {
const res = await resourceApi.detail.request({
id: data.id,
});
if (res.meta) {
res.meta = JSON.parse(res.meta);
}
state.dialogForm.data = res;
state.dialogForm.typeDisabled = true;
state.dialogForm.title = '修改“' + data.name + '”菜单';
2023-03-16 16:40:57 +08:00
state.dialogForm.visible = true;
};
const valChange = () => {
search();
state.dialogForm.visible = false;
};
const changeStatus = async (data: any, status: any) => {
await resourceApi.changeStatus.request({
id: data.id,
status: status,
});
search();
ElMessage.success((status === 1 ? '启用' : '禁用') + '成功!');
};
// 节点被展开时触发的事件
const handleNodeExpand = (data: any, node: any) => {
const id: any = node.data.id;
if (!state.defaultExpandedKeys.includes(id)) {
state.defaultExpandedKeys.push(id);
}
};
// 关闭节点
const handleNodeCollapse = (data: any, node: any) => {
removeDeafultExpandId(node.data.id);
let childNodes = node.childNodes;
for (let cn of childNodes) {
if (cn.data.type == 2) {
return;
}
if (cn.expanded) {
removeDeafultExpandId(cn.data.id);
}
// 递归删除展开的子节点节点id
handleNodeCollapse(data, cn);
}
};
const allowDrop = (draggingNode: any, dropNode: any, type: any) => {
// 如果是插入至目标节点
if (type === 'inner') {
// 只有目标节点下没有子节点才允许移动
if (!dropNode.data.children || dropNode.data.children == 0) {
// 只有权限节点可移动至菜单节点下 或者移动菜单
return (
(draggingNode.data.type == permissionTypeValue && dropNode.data.type == menuTypeValue) ||
(draggingNode.data.type == menuTypeValue && dropNode.data.type == menuTypeValue)
);
}
return false;
}
return draggingNode.data.type === dropNode.data.type;
};
const handleDrop = async (draggingNode: any, dropNode: any, dropType: any) => {
const draggingData = draggingNode.data;
const dropData = dropNode.data;
if (draggingData.pid !== dropData.pid) {
draggingData.pid = dropData.pid;
}
if (dropType === 'inner') {
draggingData.weight = 1;
draggingData.pid = dropData.id;
}
if (dropType === 'before') {
draggingData.weight = dropData.weight - 1;
}
if (dropType === 'after') {
draggingData.weight = dropData.weight + 1;
}
await resourceApi.sort.request([
{
id: draggingData.id,
name: draggingData.name,
pid: draggingData.pid,
weight: draggingData.weight,
},
]);
};
const removeDeafultExpandId = (id: any) => {
let index = state.defaultExpandedKeys.indexOf(id);
if (index > -1) {
state.defaultExpandedKeys.splice(index, 1);
}
};
</script>
<style lang="scss">
2023-12-14 13:05:21 +08:00
.system-resouce-list {
.el-tree-node__content {
height: 40px;
line-height: 40px;
}
2023-12-14 13:05:21 +08:00
.tree-data {
2023-12-29 16:48:15 +08:00
height: calc(100vh - 202px);
2023-12-14 13:05:21 +08:00
}
2024-03-26 21:46:03 +08:00
.el-tree {
display: inline-block;
min-width: 100%;
}
}
.none-select {
moz-user-select: -moz-none;
-moz-user-select: none;
-o-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
</style>