Files
vue3-element-admin/src/views/system/menu/index.vue
2025-03-24 10:36:52 +08:00

532 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="app-container">
<div class="search-bar">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="关键字" prop="keywords">
<el-input
v-model="queryParams.keywords"
placeholder="菜单名称"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-card shadow="never">
<div class="mb-10px">
<el-button
v-hasPerm="['sys:menu:add']"
type="success"
icon="plus"
@click="handleOpenDialog('0')"
>
新增
</el-button>
</div>
<el-table
v-loading="loading"
:data="menuTableData"
highlight-current-row
row-key="id"
:tree-props="{
children: 'children',
hasChildren: 'hasChildren',
}"
@row-click="handleRowClick"
>
<el-table-column label="菜单名称" min-width="200">
<template #default="scope">
<template v-if="scope.row.icon && scope.row.icon.startsWith('el-icon')">
<el-icon style="vertical-align: -0.15em">
<component :is="scope.row.icon.replace('el-icon-', '')" />
</el-icon>
</template>
<template v-else-if="scope.row.icon">
<div :class="`i-svg:${scope.row.icon}`" />
</template>
{{ scope.row.name }}
</template>
</el-table-column>
<el-table-column label="类型" align="center" width="80">
<template #default="scope">
<el-tag v-if="scope.row.type === MenuTypeEnum.CATALOG" type="warning">目录</el-tag>
<el-tag v-if="scope.row.type === MenuTypeEnum.MENU" type="success">菜单</el-tag>
<el-tag v-if="scope.row.type === MenuTypeEnum.BUTTON" type="danger">按钮</el-tag>
<el-tag v-if="scope.row.type === MenuTypeEnum.EXTLINK" type="info">外链</el-tag>
</template>
</el-table-column>
<el-table-column label="路由名称" align="left" width="150" prop="routeName" />
<el-table-column label="路由路径" align="left" width="150" prop="routePath" />
<el-table-column label="组件路径" align="left" width="250" prop="component" />
<el-table-column label="权限标识" align="center" width="200" prop="perm" />
<el-table-column label="状态" align="center" width="80">
<template #default="scope">
<el-tag v-if="scope.row.visible === 1" type="success">显示</el-tag>
<el-tag v-else type="info">隐藏</el-tag>
</template>
</el-table-column>
<el-table-column label="排序" align="center" width="80" prop="sort" />
<el-table-column fixed="right" align="center" label="操作" width="220">
<template #default="scope">
<el-button
v-if="scope.row.type == MenuTypeEnum.CATALOG || scope.row.type == MenuTypeEnum.MENU"
v-hasPerm="['sys:menu:add']"
type="primary"
link
size="small"
icon="plus"
@click.stop="handleOpenDialog(scope.row.id)"
>
新增
</el-button>
<el-button
v-hasPerm="['sys:menu:edit']"
type="primary"
link
size="small"
icon="edit"
@click.stop="handleOpenDialog(undefined, scope.row.id)"
>
编辑
</el-button>
<el-button
v-hasPerm="['sys:menu:delete']"
type="danger"
link
size="small"
icon="delete"
@click.stop="handleDelete(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-drawer v-model="dialog.visible" :title="dialog.title" size="50%" @close="handleCloseDialog">
<el-form ref="menuFormRef" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="父级菜单" prop="parentId">
<el-tree-select
v-model="formData.parentId"
placeholder="选择上级菜单"
:data="menuOptions"
filterable
check-strictly
:render-after-expand="false"
/>
</el-form-item>
<el-form-item label="菜单名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入菜单名称" />
</el-form-item>
<el-form-item label="菜单类型" prop="type">
<el-radio-group v-model="formData.type" @change="handleMenuTypeChange">
<el-radio :value="MenuTypeEnum.CATALOG">目录</el-radio>
<el-radio :value="MenuTypeEnum.MENU">菜单</el-radio>
<el-radio :value="MenuTypeEnum.BUTTON">按钮</el-radio>
<el-radio :value="MenuTypeEnum.EXTLINK">外链</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="formData.type == MenuTypeEnum.EXTLINK" label="外链地址" prop="path">
<el-input v-model="formData.routePath" placeholder="请输入外链完整路径" />
</el-form-item>
<el-form-item v-if="formData.type == MenuTypeEnum.MENU" prop="routeName">
<template #label>
<div class="flex-y-center">
路由名称
<el-tooltip placement="bottom" effect="light">
<template #content>
如果需要开启缓存需保证页面 defineOptions 中的 name 与此处一致建议使用驼峰
</template>
<el-icon class="ml-1 cursor-pointer">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<el-input v-model="formData.routeName" placeholder="User" />
</el-form-item>
<el-form-item
v-if="formData.type == MenuTypeEnum.CATALOG || formData.type == MenuTypeEnum.MENU"
prop="routePath"
>
<template #label>
<div class="flex-y-center">
路由路径
<el-tooltip placement="bottom" effect="light">
<template #content>
定义应用中不同页面对应的 URL 路径目录需以 / 开头菜单项不用例如系统管理目录
/system系统管理下的用户管理菜单 user
</template>
<el-icon class="ml-1 cursor-pointer">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<el-input
v-if="formData.type == MenuTypeEnum.CATALOG"
v-model="formData.routePath"
placeholder="system"
/>
<el-input v-else v-model="formData.routePath" placeholder="user" />
</el-form-item>
<el-form-item v-if="formData.type == MenuTypeEnum.MENU" prop="component">
<template #label>
<div class="flex-y-center">
组件路径
<el-tooltip placement="bottom" effect="light">
<template #content>
组件页面完整路径相对于 src/views/ system/user/index缺省后缀 .vue
</template>
<el-icon class="ml-1 cursor-pointer">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<el-input v-model="formData.component" placeholder="system/user/index" style="width: 95%">
<template v-if="formData.type == MenuTypeEnum.MENU" #prepend>src/views/</template>
<template v-if="formData.type == MenuTypeEnum.MENU" #append>.vue</template>
</el-input>
</el-form-item>
<el-form-item v-if="formData.type == MenuTypeEnum.MENU">
<template #label>
<div class="flex-y-center">
路由参数
<el-tooltip placement="bottom" effect="light">
<template #content>
组件页面使用 `useRoute().query.参数名` 获取路由参数值
</template>
<el-icon class="ml-1 cursor-pointer">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<div v-if="!formData.params || formData.params.length === 0">
<el-button type="success" plain @click="formData.params = [{ key: '', value: '' }]">
添加路由参数
</el-button>
</div>
<div v-else>
<div v-for="(item, index) in formData.params" :key="index">
<el-input v-model="item.key" placeholder="参数名" style="width: 100px" />
<span class="mx-1">=</span>
<el-input v-model="item.value" placeholder="参数值" style="width: 100px" />
<el-icon
v-if="formData.params.indexOf(item) === formData.params.length - 1"
class="ml-2 cursor-pointer color-[var(--el-color-success)]"
style="vertical-align: -0.15em"
@click="formData.params.push({ key: '', value: '' })"
>
<CirclePlusFilled />
</el-icon>
<el-icon
class="ml-2 cursor-pointer color-[var(--el-color-danger)]"
style="vertical-align: -0.15em"
@click="formData.params.splice(formData.params.indexOf(item), 1)"
>
<DeleteFilled />
</el-icon>
</div>
</div>
</el-form-item>
<el-form-item v-if="formData.type !== MenuTypeEnum.BUTTON" prop="visible" label="显示状态">
<el-radio-group v-model="formData.visible">
<el-radio :value="1">显示</el-radio>
<el-radio :value="0">隐藏</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="formData.type === MenuTypeEnum.CATALOG || formData.type === MenuTypeEnum.MENU"
>
<template #label>
<div class="flex-y-center">
始终显示
<el-tooltip placement="bottom" effect="light">
<template #content>
选择即使目录或菜单下只有一个子节点也会显示父节点
<br />
选择如果目录或菜单下只有一个子节点则只显示该子节点隐藏父节点
<br />
如果是叶子节点请选择
</template>
<el-icon class="ml-1 cursor-pointer">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<el-radio-group v-model="formData.alwaysShow">
<el-radio :value="1"></el-radio>
<el-radio :value="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="formData.type === MenuTypeEnum.MENU" label="缓存页面">
<el-radio-group v-model="formData.keepAlive">
<el-radio :value="1">开启</el-radio>
<el-radio :value="0">关闭</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number
v-model="formData.sort"
style="width: 100px"
controls-position="right"
:min="0"
/>
</el-form-item>
<!-- 权限标识 -->
<el-form-item v-if="formData.type == MenuTypeEnum.BUTTON" label="权限标识" prop="perm">
<el-input v-model="formData.perm" placeholder="sys:user:add" />
</el-form-item>
<el-form-item v-if="formData.type !== MenuTypeEnum.BUTTON" label="图标" prop="icon">
<!-- 图标选择器 -->
<icon-select v-model="formData.icon" />
</el-form-item>
<el-form-item v-if="formData.type == MenuTypeEnum.CATALOG" label="跳转路由">
<el-input v-model="formData.redirect" placeholder="跳转路由" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit"> </el-button>
<el-button @click="handleCloseDialog"> </el-button>
</div>
</template>
</el-drawer>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: "SysMenu",
inheritAttrs: false,
});
import MenuAPI, { MenuQuery, MenuForm, MenuVO } from "@/api/system/menu.api";
import { MenuTypeEnum } from "@/enums/system/menu.enum";
const queryFormRef = ref();
const menuFormRef = ref();
const loading = ref(false);
const dialog = reactive({
title: "新增菜单",
visible: false,
});
// 查询参数
const queryParams = reactive<MenuQuery>({});
// 菜单表格数据
const menuTableData = ref<MenuVO[]>([]);
// 顶级菜单下拉选项
const menuOptions = ref<OptionType[]>([]);
// 初始菜单表单数据
const initialMenuFormData = ref<MenuForm>({
id: undefined,
parentId: "0",
visible: 1,
sort: 1,
type: MenuTypeEnum.MENU, // 默认菜单
alwaysShow: 0,
keepAlive: 1,
params: [],
});
// 菜单表单数据
const formData = ref({ ...initialMenuFormData.value });
// 表单验证规则
const rules = reactive({
parentId: [{ required: true, message: "请选择父级菜单", trigger: "blur" }],
name: [{ required: true, message: "请输入菜单名称", trigger: "blur" }],
type: [{ required: true, message: "请选择菜单类型", trigger: "blur" }],
routeName: [{ required: true, message: "请输入路由名称", trigger: "blur" }],
routePath: [{ required: true, message: "请输入路由路径", trigger: "blur" }],
component: [{ required: true, message: "请输入组件路径", trigger: "blur" }],
visible: [{ required: true, message: "请选择显示状态", trigger: "change" }],
});
// 选择表格的行菜单ID
const selectedMenuId = ref<string | undefined>();
// 查询菜单
function handleQuery() {
loading.value = true;
MenuAPI.getList(queryParams)
.then((data) => {
menuTableData.value = data;
})
.finally(() => {
loading.value = false;
});
}
// 重置查询
function handleResetQuery() {
queryFormRef.value.resetFields();
handleQuery();
}
// 行点击事件
function handleRowClick(row: MenuVO) {
selectedMenuId.value = row.id;
}
/**
* 打开表单弹窗
*
* @param parentId 父菜单ID
* @param menuId 菜单ID
*/
function handleOpenDialog(parentId?: string, menuId?: string) {
MenuAPI.getOptions(true)
.then((data) => {
menuOptions.value = [{ value: "0", label: "顶级菜单", children: data }];
})
.then(() => {
dialog.visible = true;
if (menuId) {
dialog.title = "编辑菜单";
MenuAPI.getFormData(menuId).then((data) => {
initialMenuFormData.value = { ...data };
formData.value = data;
});
} else {
dialog.title = "新增菜单";
formData.value.parentId = parentId?.toString();
}
});
}
// 菜单类型切换
function handleMenuTypeChange() {
// 如果菜单类型改变
if (formData.value.type !== initialMenuFormData.value.type) {
if (formData.value.type === MenuTypeEnum.MENU) {
// 目录切换到菜单时,清空组件路径
if (initialMenuFormData.value.type === MenuTypeEnum.CATALOG) {
formData.value.component = "";
} else {
// 其他情况,保留原有的组件路径
formData.value.routePath = initialMenuFormData.value.routePath;
formData.value.component = initialMenuFormData.value.component;
}
}
}
}
/**
* 提交表单
*/
function handleSubmit() {
menuFormRef.value.validate((isValid: boolean) => {
if (isValid) {
const menuId = formData.value.id;
if (menuId) {
//修改时父级菜单不能为当前菜单
if (formData.value.parentId == menuId) {
ElMessage.error("父级菜单不能为当前菜单");
return;
}
MenuAPI.update(menuId, formData.value).then(() => {
ElMessage.success("修改成功");
handleCloseDialog();
handleQuery();
});
} else {
MenuAPI.create(formData.value).then(() => {
ElMessage.success("新增成功");
handleCloseDialog();
handleQuery();
});
}
}
});
}
// 删除菜单
function handleDelete(menuId: number) {
if (!menuId) {
ElMessage.warning("请勾选删除项");
return false;
}
ElMessageBox.confirm("确认删除已选中的数据项?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(
() => {
loading.value = true;
MenuAPI.deleteById(menuId)
.then(() => {
ElMessage.success("删除成功");
handleQuery();
})
.finally(() => {
loading.value = false;
});
},
() => {
ElMessage.info("已取消删除");
}
);
}
function resetForm() {
menuFormRef.value.resetFields();
menuFormRef.value.clearValidate();
formData.value = {
id: undefined,
parentId: "0",
visible: 1,
sort: 1,
type: MenuTypeEnum.MENU, // 默认菜单
alwaysShow: 0,
keepAlive: 1,
params: [],
};
}
// 关闭弹窗
function handleCloseDialog() {
dialog.visible = false;
resetForm();
}
onMounted(() => {
handleQuery();
});
</script>