<template>
<div class="menu-container">
<el-card class="box-card">
<template #header>
<div class="card-header">
<h3>菜单管理</h3>
<div>
<el-button type="success" @click="handleMoveUp" :disabled="selectedRows.length !== 1">
上移 {{ selectedRows.length === 1 ? `(${selectedRows[0].displayName || selectedRows[0].name})` : '' }}
</el-button>
<el-button type="warning" @click="handleMoveDown" :disabled="selectedRows.length !== 1">
下移 {{ selectedRows.length === 1 ? `(${selectedRows[0].displayName || selectedRows[0].name})` : '' }}
</el-button>
<el-button type="primary" @click="handleAdd">新增菜单</el-button>
</div>
</div>
</template>
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="菜单名称">
<el-input v-model="searchForm.q" placeholder="请输入菜单名称" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<el-alert
:title="`共找到 ${total} 个菜单项,其中根菜单 ${tableData.length} 个。请选择一行进行上移/下移操作。`"
type="info"
:closable="false"
show-icon
style="margin-bottom: 16px;"
/>
<el-table :data="tableData" border style="width: 100%" v-loading="loading"
row-key="id"
default-expand-all
:tree-props="{ children: 'children' }"
@selection-change="handleSelectionChange"
:show-header="true"
:highlight-current-row="true">
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="编号" width="80" />
<el-table-column prop="name" label="名称" min-width="150" />
<el-table-column prop="displayName" label="显示名" min-width="150" />
<el-table-column prop="url" label="链接" min-width="150" />
<el-table-column prop="sort" label="排序" width="80" />
<el-table-column prop="icon" label="图标" width="100">
<template #default="scope">
<i v-if="scope.row.icon" :class="scope.row.icon"></i>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="updateTime" label="更新时间" min-width="150" />
<el-table-column label="可见" width="80">
<template #default="scope">
<el-tag :type="scope.row.visible ? 'success' : 'danger'">
{{ scope.row.visible ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="必要" width="80">
<template #default="scope">
<el-tag :type="scope.row.necessary ? 'warning' : ''">
{{ scope.row.necessary ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="新窗口" width="80">
<template #default="scope">
<el-tag :type="scope.row.newWindow ? 'info' : ''">
{{ scope.row.newWindow ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="scope">
<el-button type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 菜单表单对话框 -->
<el-dialog v-model="dialogVisible" :title="formType === 'add' ? '新增菜单' : '编辑菜单'" width="600px">
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
<el-form-item label="菜单名称" prop="name">
<el-input v-model="form.name" placeholder="请输入菜单名称" />
</el-form-item>
<el-form-item label="显示名称" prop="displayName">
<el-input v-model="form.displayName" placeholder="请输入显示名称" />
</el-form-item>
<el-form-item label="菜单全名" prop="fullName">
<el-input v-model="form.fullName" placeholder="请输入菜单全名" />
</el-form-item>
<el-form-item label="上级菜单" prop="parentID">
<el-select v-model="form.parentID" placeholder="请选择上级菜单" clearable filterable>
<el-option label="无上级" :value="0" />
<el-option v-for="menu in menuOptions" :key="menu.id" :label="menu.displayName || menu.name" :value="menu.id" />
</el-select>
</el-form-item>
<el-form-item label="链接地址" prop="url">
<el-input v-model="form.url" placeholder="请输入链接地址" />
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" :min="0" placeholder="同级内排序" />
</el-form-item>
<el-form-item label="图标" prop="icon">
<el-input v-model="form.icon" placeholder="请输入图标类名" />
</el-form-item>
<el-form-item label="可见状态" prop="visible">
<el-switch v-model="form.visible" :active-value="true" :inactive-value="false" />
</el-form-item>
<el-form-item label="必要菜单" prop="necessary">
<el-switch v-model="form.necessary" :active-value="true" :inactive-value="false" />
<span class="form-tip">必要的菜单,必须至少有角色拥有这些权限</span>
</el-form-item>
<el-form-item label="新窗口打开" prop="newWindow">
<el-switch v-model="form.newWindow" :active-value="true" :inactive-value="false" />
</el-form-item>
<el-form-item label="权限子项" prop="permission">
<el-input v-model="form.permission" type="textarea" placeholder="逗号分隔,每个权限子项名值竖线分隔" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { ElMessageBox } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
import { request } from '@core/utils/request';
// 定义菜单类型接口
interface Menu {
id: number;
name: string;
displayName: string;
fullName: string;
parentID: number;
url: string;
sort: number;
icon: string;
visible: boolean;
necessary: boolean;
newWindow: boolean;
permission: string;
ex1: number;
ex2: number;
ex3: number;
ex4: string;
ex5: string;
ex6: string;
createUser: string;
createUserID: number;
createIP: string;
createTime: string;
updateUser: string;
updateUserID: number;
updateIP: string;
updateTime: string;
remark: string;
children?: Menu[];
hasChildren?: boolean;
}
// 表格数据
const tableData = ref<Menu[]>([]);
const loading = ref(false);
const total = ref(0);
const menuOptions = ref<Menu[]>([]);
const selectedRows = ref<Menu[]>([]);
// 查询表单
const searchForm = reactive({
q: '',
});
// 表单相关
const dialogVisible = ref(false);
const formType = ref<'add' | 'edit'>('add');
const formRef = ref<FormInstance | null>(null);
const form = reactive<Menu>({
id: 0,
name: '',
displayName: '',
fullName: '',
parentID: 0,
url: '',
sort: 0,
icon: '',
visible: true,
necessary: false,
newWindow: false,
permission: '',
ex1: 0,
ex2: 0,
ex3: 0,
ex4: '',
ex5: '',
ex6: '',
createUser: '',
createUserID: 0,
createIP: '',
createTime: '',
updateUser: '',
updateUserID: 0,
updateIP: '',
updateTime: '',
remark: '',
});
// 表单验证规则
const formRules = reactive<FormRules>({
name: [
{ required: true, message: '请输入菜单名称', trigger: 'blur' }
],
displayName: [
{ required: true, message: '请输入显示名称', trigger: 'blur' }
]
});
// 构建树结构并排序(使用队列,非递归方式)
const buildTreeData = (data: Menu[]): Menu[] => {
if (!data || data.length === 0) {
return [];
}
// 创建节点映射表和根节点数组
const nodeMap = new Map<number, Menu>();
const roots: Menu[] = [];
// 步骤1:初始化所有节点
data.forEach(item => {
const node = { ...item };
// 确保清理之前的树状属性
node.children = [];
delete node.hasChildren;
nodeMap.set(item.id, node);
});
// 步骤2:建立父子关系
data.forEach(item => {
const node = nodeMap.get(item.id)!;
if (item.parentID === 0) {
// parentID为0的是根节点
roots.push(node);
} else {
// 找到父节点并建立关系
const parent = nodeMap.get(item.parentID);
if (parent) {
if (!parent.children) {
parent.children = [];
}
parent.children.push(node);
} else {
// 如果找不到父节点,作为根节点处理
roots.push(node);
}
}
});
// 步骤3:清理空的children数组并排序
const processNode = (node: Menu) => {
if (node.children && node.children.length === 0) {
delete node.children;
} else if (node.children && node.children.length > 0) {
// 对子节点排序
node.children.sort((a, b) => a.sort - b.sort);
// 递归处理子节点
node.children.forEach(child => processNode(child));
}
};
// 对根节点排序
roots.sort((a, b) => a.sort - b.sort);
// 处理所有节点
roots.forEach(root => processNode(root));
console.log('构建树状结构完成:', {
原始数据数量: data.length,
根节点数量: roots.length,
树结构: roots
});
return roots;
};
// 选择变化处理
const handleSelectionChange = (selection: Menu[]) => {
selectedRows.value = selection;
};
// 加载数据
const loadData = async () => {
loading.value = true;
try {
const response = await request.get('/Admin/Menu', {
params: {
pageIndex: 1,
pageSize: 10000, // 获取全部数据用于构建树
q: searchForm.q
}
});
let dataList: Menu[] = [];
// 处理API响应数据
if (Array.isArray(response)) {
// 直接返回数组的响应
dataList = response;
} else if (response && typeof response === 'object' && 'data' in response) {
// 标准API响应格式,从data字段取数据
dataList = Array.isArray(response.data) ? response.data : [];
} else {
dataList = [];
}
console.log('菜单原始数据:', dataList);
// 构建树结构并排序
tableData.value = buildTreeData(dataList);
total.value = dataList.length;
console.log('构建后的树状数据:', tableData.value);
// 同时保存平铺的数据用于菜单选项(排除当前编辑的菜单)
menuOptions.value = dataList.filter(item => item.id !== form.id);
} catch {
tableData.value = [];
menuOptions.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
// 搜索
const handleSearch = () => {
loadData();
};
// 重置搜索
const resetSearch = () => {
searchForm.q = '';
loadData();
};
// 上移
const handleMoveUp = async () => {
if (selectedRows.value.length !== 1) {
return;
}
try {
await request.post('/Admin/Menu/Up', null, {
params: { id: selectedRows.value[0].id }
});
loadData();
} catch {
// 错误提示已经在 request 拦截器中自动处理
}
};
// 下移
const handleMoveDown = async () => {
if (selectedRows.value.length !== 1) {
return;
}
try {
await request.post('/Admin/Menu/Down', null, {
params: { id: selectedRows.value[0].id }
});
loadData();
} catch {
// 错误提示已经在 request 拦截器中自动处理
}
};
// 新增
const handleAdd = () => {
formType.value = 'add';
Object.assign(form, {
id: 0,
name: '',
displayName: '',
fullName: '',
parentID: 0,
url: '',
sort: 0,
icon: '',
visible: true,
necessary: false,
newWindow: false,
permission: '',
ex1: 0,
ex2: 0,
ex3: 0,
ex4: '',
ex5: '',
ex6: '',
createUser: '',
createUserID: 0,
createIP: '',
createTime: '',
updateUser: '',
updateUserID: 0,
updateIP: '',
updateTime: '',
remark: '',
});
dialogVisible.value = true;
};
// 编辑
const handleEdit = (row: Menu) => {
formType.value = 'edit';
Object.assign(form, { ...row });
// 更新菜单选项,排除自己
menuOptions.value = menuOptions.value.filter(item => item.id !== row.id);
dialogVisible.value = true;
};
// 删除
const handleDelete = (row: Menu) => {
ElMessageBox.confirm('确认删除该菜单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
await request.delete('/Admin/Menu', {
params: { id: row.id }
});
loadData();
} catch {
// 错误提示已经在 request 拦截器中自动处理
}
})
.catch(() => { });
};
// 提交表单
const submitForm = async () => {
if (!formRef.value) return;
formRef.value.validate(async (valid: boolean) => {
if (valid) {
try {
if (formType.value === 'add') {
await request.post('/Admin/Menu', form);
} else {
await request.put('/Admin/Menu', form);
}
dialogVisible.value = false;
loadData();
} catch {
// 错误提示已经在 request 拦截器中自动处理
}
}
});
};
// 初始化加载数据
onMounted(() => {
loadData();
});
</script>
<style scoped>
.menu-container {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.search-form {
margin-bottom: 20px;
}
.form-tip {
font-size: 12px;
color: #999;
margin-left: 10px;
}
/* 树状表格样式优化 */
:deep(.el-table .el-table__row) {
cursor: pointer;
}
:deep(.el-table .el-table__row:hover) {
background-color: #f5f7fa;
}
:deep(.el-table__expand-icon) {
color: #409eff;
}
:deep(.el-table__indent) {
padding-left: 20px;
}
/* 选中行样式 */
:deep(.el-table__row.is-selected) {
background-color: #ecf5ff;
}
</style>
|