refactor: 系统权限模块重构,迁移至PostgreSQL并对齐新设计
- 切换数据库为PostgreSQL,更新依赖与配置 - 重构权限体系,使用sys_user/sys_role/sys_menu等新实体替代旧模型 - 移除旧的用户角色/权限/系统设置相关代码与实体 - 新增字典、菜单管理模块与基础CRUD接口 - 优化鉴权逻辑,基于菜单权限标识进行接口校验 - 前端同步更新API、路由与权限控制逻辑 - 删除冗余的v-permission指令与相关代码
This commit is contained in:
1 parent
98c22f6afb
commit
0a8103f33d
67 files changed
+3263
-1924
No files matched your search
@@ -0,0 +1,335 @@
|
||||
<template>
|
||||
<div class="menu-management">
|
||||
<a-card title="菜单管理">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="handleAdd()">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增菜单
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="menuList"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
:default-expand-all-rows="true"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'menuType'">
|
||||
<a-tag :color="record.menuType === 1 ? 'blue' : 'green'">
|
||||
{{ record.menuType === 1 ? '目录' : '菜单' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.dataIndex === 'visible'">
|
||||
<a-tag :color="record.visible === 1 ? 'success' : 'default'">
|
||||
{{ record.visible === 1 ? '显示' : '隐藏' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleAdd(record)">
|
||||
<PlusOutlined />新增子菜单
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<EditOutlined />编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定删除该菜单?"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="handleDelete(record)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>
|
||||
<DeleteOutlined />删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
width="640px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="上级菜单" name="parentId">
|
||||
<a-tree-select
|
||||
v-model:value="formData.parentId"
|
||||
:tree-data="parentOptions"
|
||||
:field-names="{ label: 'menuName', value: 'id', children: 'children' }"
|
||||
allow-clear
|
||||
tree-default-expand-all
|
||||
placeholder="请选择上级菜单"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="菜单类型" name="menuType">
|
||||
<a-radio-group v-model:value="formData.menuType">
|
||||
<a-radio :value="1">目录</a-radio>
|
||||
<a-radio :value="2">菜单</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="菜单名称" name="menuName">
|
||||
<a-input v-model:value="formData.menuName" placeholder="请输入菜单名称" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="路由路径" name="menuPath">
|
||||
<a-input v-model:value="formData.menuPath" placeholder="如 /system/user" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="组件" name="component">
|
||||
<a-input v-model:value="formData.component" placeholder="如 system/user/index" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="图标" name="icon">
|
||||
<a-input v-model:value="formData.icon" placeholder="如 UserOutlined" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="排序" name="sortOrder">
|
||||
<a-input-number v-model:value="formData.sortOrder" :min="0" :max="999" style="width: 100%" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="权限标识" name="permission">
|
||||
<a-input v-model:value="formData.permission" placeholder="如 user:list" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="可见性" name="visible">
|
||||
<a-switch
|
||||
:checked="formData.visible === 1"
|
||||
checked-children="显示"
|
||||
un-checked-children="隐藏"
|
||||
@change="(checked: boolean) => (formData.visible = checked ? 1 : 0)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue';
|
||||
import { menuApi } from '@/api/menu';
|
||||
|
||||
interface MenuItem {
|
||||
id: number;
|
||||
parentId?: number | null;
|
||||
menuName: string;
|
||||
menuPath?: string;
|
||||
component?: string;
|
||||
icon?: string;
|
||||
menuType: number;
|
||||
sortOrder: number;
|
||||
permission?: string;
|
||||
visible: number;
|
||||
children?: MenuItem[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const menuList = ref<MenuItem[]>([]);
|
||||
|
||||
const columns = [
|
||||
{ title: '菜单名称', dataIndex: 'menuName', key: 'menuName' },
|
||||
{ title: '路由路径', dataIndex: 'menuPath', key: 'menuPath', width: 160 },
|
||||
{ title: '组件', dataIndex: 'component', key: 'component', width: 180 },
|
||||
{ title: '图标', dataIndex: 'icon', key: 'icon', width: 120 },
|
||||
{ title: '类型', dataIndex: 'menuType', key: 'menuType', width: 80, align: 'center' as const },
|
||||
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 80, align: 'center' as const },
|
||||
{ title: '权限标识', dataIndex: 'permission', key: 'permission', width: 150 },
|
||||
{ title: '可见性', dataIndex: 'visible', key: 'visible', width: 90, align: 'center' as const },
|
||||
{ title: '操作', key: 'action', width: 240, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
// 上级菜单树(编辑时排除自身及后代节点,防止循环引用)
|
||||
const parentOptions = computed(() => {
|
||||
const excludeId = isEdit.value ? editId.value : null;
|
||||
const filtered = excludeId ? filterTree(menuList.value, excludeId) : menuList.value;
|
||||
return [{ id: 0, menuName: '根目录', children: filtered }];
|
||||
});
|
||||
|
||||
function filterTree(nodes: MenuItem[], excludeId: number): MenuItem[] {
|
||||
const result: MenuItem[] = [];
|
||||
for (const node of nodes) {
|
||||
if (node.id === excludeId) continue;
|
||||
const children = node.children?.length ? filterTree(node.children, excludeId) : undefined;
|
||||
if (children) {
|
||||
result.push({ ...node, children });
|
||||
} else {
|
||||
result.push(node);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const modalVisible = ref(false);
|
||||
const modalLoading = ref(false);
|
||||
const modalTitle = ref('');
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const editId = ref<number | null>(null);
|
||||
|
||||
const formData = reactive({
|
||||
parentId: 0,
|
||||
menuType: 2,
|
||||
menuName: '',
|
||||
menuPath: '',
|
||||
component: '',
|
||||
icon: '',
|
||||
sortOrder: 0,
|
||||
permission: '',
|
||||
visible: 1,
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
menuName: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchMenuTree();
|
||||
});
|
||||
|
||||
async function fetchMenuTree() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await menuApi.getMenuTree();
|
||||
if (res.code === 200) {
|
||||
menuList.value = res.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取菜单列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd(parent?: MenuItem) {
|
||||
isEdit.value = false;
|
||||
editId.value = null;
|
||||
modalTitle.value = '新增菜单';
|
||||
resetForm();
|
||||
if (parent) {
|
||||
formData.parentId = parent.id;
|
||||
formData.menuType = 2;
|
||||
}
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: MenuItem) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑菜单';
|
||||
formData.parentId = record.parentId || 0;
|
||||
formData.menuType = record.menuType;
|
||||
formData.menuName = record.menuName;
|
||||
formData.menuPath = record.menuPath || '';
|
||||
formData.component = record.component || '';
|
||||
formData.icon = record.icon || '';
|
||||
formData.sortOrder = record.sortOrder ?? 0;
|
||||
formData.permission = record.permission || '';
|
||||
formData.visible = record.visible ? 1 : 0;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.parentId = 0;
|
||||
formData.menuType = 2;
|
||||
formData.menuName = '';
|
||||
formData.menuPath = '';
|
||||
formData.component = '';
|
||||
formData.icon = '';
|
||||
formData.sortOrder = 0;
|
||||
formData.permission = '';
|
||||
formData.visible = 1;
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value?.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const payload: any = {
|
||||
parentId: formData.parentId || null,
|
||||
menuType: formData.menuType,
|
||||
menuName: formData.menuName,
|
||||
menuPath: formData.menuPath,
|
||||
component: formData.component,
|
||||
icon: formData.icon,
|
||||
sortOrder: formData.sortOrder,
|
||||
permission: formData.permission,
|
||||
visible: formData.visible,
|
||||
};
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await menuApi.updateMenu(editId.value, payload);
|
||||
} else {
|
||||
res = await menuApi.createMenu(payload);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
fetchMenuTree();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(record: MenuItem) {
|
||||
try {
|
||||
const res: any = await menuApi.deleteMenu(record.id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
fetchMenuTree();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.menu-management {
|
||||
padding: 20px;
|
||||
|
||||
:deep(.ant-btn-link) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user