first commit
This commit is contained in:
@@ -0,0 +1,563 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>前端导航管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button @click="expandAll">
|
||||
<el-icon>
|
||||
<FolderOpened />
|
||||
</el-icon>
|
||||
全部展开
|
||||
</el-button>
|
||||
<el-button @click="collapseAll">
|
||||
<el-icon>
|
||||
<Folder />
|
||||
</el-icon>
|
||||
全部折叠
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleAddMenu">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
添加菜单
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<el-icon>
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 树形表格 -->
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
:data="menuTree"
|
||||
style="width: 100%"
|
||||
row-key="id"
|
||||
border
|
||||
v-loading="loading"
|
||||
element-loading-text="正在加载..."
|
||||
:tree-props="{
|
||||
children: 'children',
|
||||
hasChildren: 'hasChildren',
|
||||
}"
|
||||
@row-click="handleRowClick"
|
||||
>
|
||||
<el-table-column prop="title" label="菜单名称" width="200">
|
||||
<template #default="scope">
|
||||
<div class="menu-item">
|
||||
<i
|
||||
v-if="scope.row.icon"
|
||||
:class="scope.row.icon"
|
||||
class="menu-icon"
|
||||
></i>
|
||||
<span>{{ scope.row.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="path" label="路由地址"></el-table-column>
|
||||
|
||||
<el-table-column label="图片" width="200" align="center">
|
||||
<template #default="scope">
|
||||
<el-image
|
||||
v-if="scope.row.image"
|
||||
:src="getImageUrl(scope.row.image)"
|
||||
:preview-src-list="[getImageUrl(scope.row.image)]"
|
||||
:preview-teleported="true"
|
||||
fit="cover"
|
||||
style="width: 50px; height: 50px; border-radius: 4px;"
|
||||
/>
|
||||
<span v-else style="color: #ccc;">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="MenuType"
|
||||
label="菜单类型"
|
||||
width="120"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-tag :type="getMenuTypeTagType(scope.row.type)">
|
||||
{{ getMenuTypeTitle(scope.row.type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="sort"
|
||||
label="排序"
|
||||
width="80"
|
||||
align="center"
|
||||
></el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="280" fixed="right" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
@click="handleAddSubMenu(scope.row)"
|
||||
:disabled="scope.row.type === 3"
|
||||
>
|
||||
<el-icon>
|
||||
<CirclePlus />
|
||||
</el-icon>
|
||||
<span>子菜单</span>
|
||||
</el-button>
|
||||
|
||||
<el-button size="small" text @click="handleEditMenu(scope.row)">
|
||||
<el-icon>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
<span>编辑</span>
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
type="danger"
|
||||
@click="handleDeleteMenu(scope.row)"
|
||||
>
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
<span>删除</span>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 引入编辑组件 -->
|
||||
<MenuEdit
|
||||
v-model:visible="dialogVisible"
|
||||
:menu="dialogMenu"
|
||||
:parent-menu-options="parentMenuOptions"
|
||||
:dialog-type="dialogType"
|
||||
:parent-title="dialogParentTitle"
|
||||
@save="handleMenuSave"
|
||||
@cancel="handleMenuCancel"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { ElMessage, ElMessageBox, ElForm } from "element-plus";
|
||||
import {
|
||||
Plus,
|
||||
CirclePlus,
|
||||
Edit,
|
||||
Delete,
|
||||
Refresh,
|
||||
FolderOpened,
|
||||
Folder,
|
||||
} from "@element-plus/icons-vue";
|
||||
import {
|
||||
getFrontMenus,
|
||||
createFrontMenu,
|
||||
editFrontMenu,
|
||||
deleteFrontMenu,
|
||||
} from "@/api/frontMenu";
|
||||
import MenuEdit from "./components/edit.vue";
|
||||
|
||||
// 定义菜单数据类型
|
||||
interface Menu {
|
||||
id: number;
|
||||
pid: number;
|
||||
title: string;
|
||||
type: number;
|
||||
path: string;
|
||||
component_path: string;
|
||||
image?: string;
|
||||
sort: number;
|
||||
desc: string;
|
||||
children?: Menu[];
|
||||
hasChildren?: boolean;
|
||||
}
|
||||
|
||||
// 菜单树形数据
|
||||
const menuTree = ref<Menu[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// 表格引用
|
||||
const tableRef = ref<any>(null);
|
||||
|
||||
// 对话框相关变量
|
||||
const dialogVisible = ref(false);
|
||||
const dialogMenu = ref<Partial<Menu> | null>(null);
|
||||
const dialogType = ref<"add" | "edit" | "addSub">("add");
|
||||
const dialogParentTitle = ref("");
|
||||
|
||||
// 父级菜单选项
|
||||
const parentMenuOptions = ref<Menu[]>([]);
|
||||
|
||||
let fetchMenusPromise: Promise<any> | null = null;
|
||||
|
||||
const fetchMenus = async () => {
|
||||
if (fetchMenusPromise) {
|
||||
return fetchMenusPromise;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
fetchMenusPromise = (async () => {
|
||||
try {
|
||||
const result = await getFrontMenus();
|
||||
if (result.code === 200) {
|
||||
menuTree.value = result.data;
|
||||
parentMenuOptions.value = [
|
||||
{
|
||||
id: 0,
|
||||
pid: -1,
|
||||
title: "顶级菜单",
|
||||
type: 1,
|
||||
path: "",
|
||||
component_path: "",
|
||||
sort: 0,
|
||||
desc: "",
|
||||
children: result.data,
|
||||
} as Menu,
|
||||
];
|
||||
} else {
|
||||
ElMessage.error("获取前端导航失败: " + result.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("获取前端导航数据失败: " + (error as Error).message);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
fetchMenusPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return fetchMenusPromise;
|
||||
};
|
||||
|
||||
// 刷新界面
|
||||
async function refresh() {
|
||||
loading.value = true;
|
||||
try {
|
||||
await fetchMenus();
|
||||
ElMessage.success("刷新成功");
|
||||
} catch (error) {
|
||||
ElMessage.error("刷新失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有前端导航行数据(包括子节点)
|
||||
function getAllMenuRows(menuList: Menu[]): Menu[] {
|
||||
const rows: Menu[] = [];
|
||||
menuList.forEach((frontMenu) => {
|
||||
rows.push(frontMenu);
|
||||
if (frontMenu.children && frontMenu.children.length > 0) {
|
||||
rows.push(...getAllMenuRows(frontMenu.children));
|
||||
}
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
// 全部展开
|
||||
function expandAll() {
|
||||
if (!tableRef.value) return;
|
||||
const allRows = getAllMenuRows(menuTree.value);
|
||||
allRows.forEach((row) => {
|
||||
if (row.children && row.children.length > 0) {
|
||||
tableRef.value.toggleRowExpansion(row, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 全部折叠
|
||||
function collapseAll() {
|
||||
if (!tableRef.value) return;
|
||||
const allRows = getAllMenuRows(menuTree.value);
|
||||
allRows.forEach((row) => {
|
||||
if (row.children && row.children.length > 0) {
|
||||
tableRef.value.toggleRowExpansion(row, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 处理行点击事件 - 展开/收缩
|
||||
function handleRowClick(row: Menu, column: any, event: Event) {
|
||||
// 如果点击的是操作列,不触发展开/收缩
|
||||
if (column && column.label === '操作') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果该行有子菜单,切换展开/收缩状态
|
||||
if (row.children && row.children.length > 0) {
|
||||
if (tableRef.value) {
|
||||
tableRef.value.toggleRowExpansion(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建菜单树(处理父子关系)
|
||||
const buildMenuTree = (menuList: Menu[]): Menu[] => {
|
||||
return menuList;
|
||||
};
|
||||
|
||||
// 获取菜单类型名称
|
||||
const getMenuTypeTitle = (type: number) => {
|
||||
const typeMap = { 1: "目录", 2: "页面", 3: "外链", 4: "单页" };
|
||||
return typeMap[type as keyof typeof typeMap] || "未知类型";
|
||||
};
|
||||
|
||||
// 获取菜单类型标签样式
|
||||
const getMenuTypeTagType = (type: number) => {
|
||||
const typeMap = { 1: "primary", 2: "success", 3: "info", 4: "warning" };
|
||||
return typeMap[type as keyof typeof typeMap] || "default";
|
||||
};
|
||||
|
||||
// 添加子菜单
|
||||
const handleAddSubMenu = (parentMenu: Menu) => {
|
||||
dialogType.value = "addSub";
|
||||
dialogParentTitle.value = parentMenu.title;
|
||||
dialogMenu.value = {
|
||||
id: 0, // 明确设置为 0,表示是新增
|
||||
pid: parentMenu.id,
|
||||
title: "",
|
||||
path: parentMenu.path || "", // 自动填充父级路径
|
||||
component_path: "",
|
||||
desc: "",
|
||||
sort: 0,
|
||||
type: parentMenu.type === 1 ? 2 : parentMenu.type, // 如果父菜单是目录,子菜单默认为页面
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑菜单
|
||||
const handleEditMenu = (menu: Menu) => {
|
||||
dialogType.value = "edit";
|
||||
dialogMenu.value = { ...menu };
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除菜单
|
||||
const handleDeleteMenu = (menu: Menu) => {
|
||||
ElMessageBox.confirm(
|
||||
`确定要删除菜单 "${menu.title}" 吗?${
|
||||
menu.hasChildren ? "其下所有子菜单也将被删除。" : ""
|
||||
}`,
|
||||
"确认删除",
|
||||
{
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}
|
||||
).then(async () => {
|
||||
try {
|
||||
const result = await deleteFrontMenu(menu.id);
|
||||
if (result.code === 200) {
|
||||
ElMessage.success("删除成功");
|
||||
fetchMenus();
|
||||
} else {
|
||||
ElMessage.error("删除失败: " + result.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("删除失败: " + (error as Error).message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 添加菜单
|
||||
const handleAddMenu = () => {
|
||||
dialogType.value = "add";
|
||||
dialogMenu.value = {
|
||||
id: 0,
|
||||
pid: 0,
|
||||
title: "",
|
||||
type: 1,
|
||||
path: "",
|
||||
component_path: "",
|
||||
sort: 0,
|
||||
desc: "",
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 处理菜单保存
|
||||
const handleMenuSave = async (menu: Partial<Menu>) => {
|
||||
try {
|
||||
// 解决后端时间字段问题:过滤掉不需要的字段
|
||||
const payload = { ...menu };
|
||||
|
||||
// 确保 pid 是整数类型(后端要求必须是整数)
|
||||
// 处理数组情况:如果 pid 是数组,取第一个元素
|
||||
let pidValue: any = payload.pid;
|
||||
if (Array.isArray(pidValue)) {
|
||||
pidValue = pidValue.length > 0 ? pidValue[0] : null;
|
||||
}
|
||||
|
||||
// 强制转换为整数
|
||||
if (pidValue === null || pidValue === undefined || pidValue === '') {
|
||||
payload.pid = 0;
|
||||
} else {
|
||||
const parsedPid = parseInt(String(pidValue), 10);
|
||||
if (isNaN(parsedPid)) {
|
||||
payload.pid = 0;
|
||||
} else {
|
||||
payload.pid = parsedPid;
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是新增还是编辑:没有 id 或 id 为 0 或 dialogType 为 add/addSub 时为新增
|
||||
if (!menu.id || menu.id === 0 || dialogType.value === 'add' || dialogType.value === 'addSub') {
|
||||
// 新增菜单(包括添加顶级菜单和添加子菜单)
|
||||
const result = await createFrontMenu(payload as Menu);
|
||||
if (result.code === 200) {
|
||||
ElMessage.success(result.msg || "菜单添加成功");
|
||||
dialogVisible.value = false;
|
||||
await fetchMenus();
|
||||
} else {
|
||||
ElMessage.error(result.msg || "添加失败");
|
||||
}
|
||||
} else {
|
||||
// 编辑已存在的菜单
|
||||
const result = await editFrontMenu(menu.id!, payload as Menu);
|
||||
if (result.code === 200) {
|
||||
ElMessage.success(result.msg || "更新成功");
|
||||
dialogVisible.value = false;
|
||||
await fetchMenus();
|
||||
} else {
|
||||
ElMessage.error(result.msg || "更新失败");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("操作失败: " + (error as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理菜单取消
|
||||
const handleMenuCancel = () => {
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
// 获取图片完整URL(用于显示)
|
||||
// @ts-ignore
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api';
|
||||
const getImageUrl = (imagePath: string) => {
|
||||
if (!imagePath) return '';
|
||||
// 如果是绝对路径,直接返回
|
||||
if (imagePath.startsWith('http')) return imagePath;
|
||||
// 如果是相对路径,拼接API基础地址
|
||||
return API_BASE_URL + imagePath;
|
||||
};
|
||||
|
||||
// 组件挂载时加载菜单
|
||||
onMounted(() => {
|
||||
fetchMenus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.header-bar h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f2f3f5;
|
||||
}
|
||||
|
||||
.card-header span {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 表格核心样式 */
|
||||
:deep(.el-table) {
|
||||
border-radius: 0;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body td) {
|
||||
padding: 12px 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 有子菜单的行显示手型光标 */
|
||||
:deep(.el-table__body tr.el-table__row--level-0) {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr) {
|
||||
&:has(.el-table__expand-icon:not(.el-table__expand-icon--hidden)) {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
/* 展开图标与菜单内容对齐 */
|
||||
:deep(.el-table__expand-icon) {
|
||||
margin: 0 !important;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
:deep(.el-table__expand-icon-cell) {
|
||||
padding: 0 8px !important;
|
||||
}
|
||||
|
||||
/* 菜单项样式 */
|
||||
.menu-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 16px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 隐藏无子女菜单的展开图标 */
|
||||
:deep(.el-table__expand-icon--hidden) {
|
||||
visibility: hidden;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
:deep(.el-table__expand-icon) {
|
||||
margin-right: 8px !important;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 对话框样式精简 */
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 20px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user