|
|
@@ -0,0 +1,486 @@
|
|
|
+<!-- 模板区域 -->
|
|
|
+<template>
|
|
|
+ <div>
|
|
|
+ <PageMain class="vh100">
|
|
|
+ <el-form :inline="true" :model="formInline" class="demo-form-inline">
|
|
|
+ <el-form-item label="菜单名称">
|
|
|
+ <el-input v-model="formInline.user" placeholder="请输入菜单名称" clearable />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" @click="Menutree">
|
|
|
+ <template #icon>
|
|
|
+ <SvgIcon name="i-ep:search" />
|
|
|
+ </template>
|
|
|
+ 查询</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <el-button type="primary" @click="menuAdd">
|
|
|
+ <template #icon>
|
|
|
+ <el-icon>
|
|
|
+ <Plus />
|
|
|
+ </el-icon>
|
|
|
+ </template>
|
|
|
+ 添加菜单</el-button>
|
|
|
+ <div :style="{ height: tableHeight, marginTop: '20px' }">
|
|
|
+ <el-auto-resizer>
|
|
|
+ <template #default="{ height, width }">
|
|
|
+ <el-table-v2 :columns="columns" :data="tableData" :width="width" :height="height" v-loading="loading"
|
|
|
+ ref="multipleTableRef" :expand-column-key="expandColumnKey" header-class="header-class">
|
|
|
+ <template #cell="scope">
|
|
|
+ <template v-if="scope.column.key == 'option'" class="flex j-s a-c">
|
|
|
+ <el-button type="primary" link @click="menuAddSub(scope.rowData)"> 添加子菜单
|
|
|
+ </el-button>
|
|
|
+ <el-dropdown placement="bottom-start">
|
|
|
+ <span class="el-dropdown-link">
|
|
|
+ 更 多
|
|
|
+ </span>
|
|
|
+ <template #dropdown>
|
|
|
+ <el-dropdown-menu>
|
|
|
+ <el-dropdown-item @click="menuEdit(scope.rowData)">编辑</el-dropdown-item>
|
|
|
+ <el-dropdown-item @click="menuDel(scope.rowData)">删除</el-dropdown-item>
|
|
|
+ </el-dropdown-menu>
|
|
|
+ </template>
|
|
|
+ </el-dropdown>
|
|
|
+ </template>
|
|
|
+ <template v-if="scope.column.key == 'type'">
|
|
|
+ <span>{{typelist.find(item => item.value == scope.rowData.type)?.label || ''}}</span>
|
|
|
+ </template>
|
|
|
+ <template v-if="scope.column.key == 'icon'">
|
|
|
+ <SvgIcon :name="scope.rowData.icon || ''" />
|
|
|
+ </template>
|
|
|
+ <template v-if="scope.column.key == 'menu'">
|
|
|
+ <el-tag v-if="scope.rowData.menu==1" type="primary">显示</el-tag>
|
|
|
+ <el-tag v-if="scope.rowData.menu==0" type="danger">隐藏</el-tag>
|
|
|
+
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ </el-table-v2>
|
|
|
+ </template>
|
|
|
+ </el-auto-resizer>
|
|
|
+ </div>
|
|
|
+ </PageMain>
|
|
|
+ <!-- 添加菜单弹窗 -->
|
|
|
+ <el-drawer v-model="menuAddShow" direction="rtl" size="800px">
|
|
|
+ <template #header>
|
|
|
+ <h4>菜单配置</h4>
|
|
|
+ </template>
|
|
|
+ <template #default>
|
|
|
+ <el-form :model="menuAddForm" :rules="menuAddRules" ref="menuAddFormRef" label-width="120px">
|
|
|
+ <el-form-item label="菜单类型" prop="type">
|
|
|
+ <el-radio-group v-model="menuAddForm.type" fill="#409eff">
|
|
|
+ <el-radio-button v-for="item in typelist" :label="item.label" :value="item.value"
|
|
|
+ :key="item.value"></el-radio-button>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item :label="`${typeLabel}名称`" prop="title">
|
|
|
+ <el-input v-model="menuAddForm.title" :placeholder="`请输入${typeLabel}名称`" clearable />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="上级菜单" prop="parentId" v-if="menuAddForm.type != 0">
|
|
|
+ <el-tree-select v-model="menuAddForm.parentId" :data="tableData" node-key="id" :props="defaultProps"
|
|
|
+ check-strictly :render-after-expand="false" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="路由标识" prop="name" v-if="menuAddForm.type == 1">
|
|
|
+ <el-input v-model="menuAddForm.name" placeholder="请输入路由标识" clearable />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="访问路径" prop="path" v-if="menuAddForm.type == 1">
|
|
|
+ <el-input v-model="menuAddForm.path" placeholder="请输入访问路径" clearable
|
|
|
+ @blur="menuAddForm.name = generateRouteName(menuAddForm.path)" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="组件" prop="component" v-if="menuAddForm.type == 1">
|
|
|
+ <el-input v-model="menuAddForm.component" placeholder="请输入组件" clearable />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="菜单图标" v-if="menuAddForm.type != 2">
|
|
|
+ <el-input v-model="menuAddForm.icon" placeholder="请选择菜单图标" @click="iconVisible = true" clearable readonly />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="按钮权限标识" prop="perms" v-if="menuAddForm.type == 2">
|
|
|
+ <el-input v-model="menuAddForm.perms" placeholder="请输入按钮权限标识" clearable />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="菜单高亮地址" v-if="menu">
|
|
|
+ <el-input v-model="menuAddForm.activeMenu" placeholder="请输入菜单高亮地址" clearable />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="隐藏路由" prop="menu" v-if="menuAddForm.type != 2">
|
|
|
+ <el-switch v-model="menu" @change="handleMenuChange" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </template>
|
|
|
+ <template #footer>
|
|
|
+ <div style="flex: auto;">
|
|
|
+ <el-button @click="menuAddShow = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="confirmClick(menuAddFormRef)">确认</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-drawer>
|
|
|
+ <!-- 图标弹窗 -->
|
|
|
+ <el-dialog v-model="iconVisible" title="选择图标" width="1200px" height="800px " top="8vh">
|
|
|
+ <div>
|
|
|
+ <!-- 图标搜索框 -->
|
|
|
+ <el-input v-model="iconSearchQuery" placeholder="搜索图标名称" class="mb-4" clearable @input="currentPage = 1">
|
|
|
+ <template #prefix>
|
|
|
+ <el-icon>
|
|
|
+ <Search />
|
|
|
+ </el-icon>
|
|
|
+ </template>
|
|
|
+ </el-input>
|
|
|
+ <el-tabs v-model="activeName" class="demo-tabs" @tab-change="currentPage = 1">
|
|
|
+ <el-tab-pane :label="icon.info.name + '图标'" :name="icon.prefix" v-for="(icon, key) in icons" :key="key">
|
|
|
+ <!-- 计算当前标签页的过滤和分页后的图标 -->
|
|
|
+ <div v-if="getFilteredIcons(icon).length === 0" class="text-center py-10 text-gray-500">
|
|
|
+ 未找到匹配的图标
|
|
|
+ </div>
|
|
|
+ <div v-else>
|
|
|
+ <div class="icon-container flex f-wrap">
|
|
|
+ <div class="icon-item flex a-c j-c " v-for="(item, index) in getPaginatedIcons(getFilteredIcons(icon))"
|
|
|
+ :key="index" :class="{ 'icon-selected': selectedIcon === `${icon.prefix}:${item}` }"
|
|
|
+ @click="selectedIcon = `${icon.prefix}:${item}`">
|
|
|
+ <SvgIcon :name="`${icon.prefix}:${item}`" class="icon" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- 分页组件 -->
|
|
|
+ <div class="flex justify-center mt-4">
|
|
|
+ <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize"
|
|
|
+ :page-sizes="[20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ :total="getFilteredIcons(icon).length" @size-change="currentPage = 1" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+ </div>
|
|
|
+ <template #footer>
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button @click="iconVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="confirmSelectIcon">
|
|
|
+ 确定
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<!-- 行为区域 -->
|
|
|
+<script lang='ts' setup>
|
|
|
+import { ref, reactive, computed } from 'vue'
|
|
|
+import { FormRules, FormInstance, ComponentSize, ElMessageBox } from 'element-plus';
|
|
|
+import api from '@/api'
|
|
|
+import { ElTag, ElMessage } from 'element-plus';
|
|
|
+import { icons } from '@/iconify'
|
|
|
+import { Search, Plus } from '@element-plus/icons-vue'
|
|
|
+interface Column {
|
|
|
+ key?: string,
|
|
|
+ dataKey?: string,
|
|
|
+ title: string,
|
|
|
+ width: number,
|
|
|
+ class?: string,
|
|
|
+ cellRenderer?: any
|
|
|
+ align?: "left" | "center" | "right"; // align 是可选的,且只能是这三个值之一
|
|
|
+}
|
|
|
+const defaultProps = {
|
|
|
+ children: 'children',
|
|
|
+ label: 'title',
|
|
|
+}
|
|
|
+onMounted(() => {
|
|
|
+ setTimeout(() => {
|
|
|
+ const tableElement = multipleTableRef.value?.$el as HTMLElement;
|
|
|
+ if (tableElement) {
|
|
|
+ const offsetTop = tableElement.offsetTop; // 获取 offsetTop
|
|
|
+ let height = window.innerHeight;
|
|
|
+ tableHeight.value = height - offsetTop - 190 + "px";
|
|
|
+ }
|
|
|
+ }, 10);
|
|
|
+})
|
|
|
+onBeforeMount(() => {
|
|
|
+ Menutree()//获取菜单树
|
|
|
+
|
|
|
+})
|
|
|
+const menuAddFormRef = ref<FormInstance>()
|
|
|
+const multipleTableRef = ref();//多个表格引用
|
|
|
+const iconVisible = ref(false);//图标弹窗显示
|
|
|
+const menuAddShow = ref(false);//添加菜单弹窗显示
|
|
|
+const activeName = ref('ep');//当前选中的tab
|
|
|
+const selectedIcon = ref('');//当前选中的图标
|
|
|
+const iconSearchQuery = ref('');//图标搜索关键词
|
|
|
+const currentPage = ref(1);//当前页码
|
|
|
+const pageSize = ref(50);//每页显示图标数量
|
|
|
+const tableHeight = ref(''); // 初始化表格高度
|
|
|
+const loading = ref(false)
|
|
|
+const expandColumnKey = 'title'//标记可展开列
|
|
|
+const menu = ref(false);
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+// 定义查询表单数据
|
|
|
+const formInline = reactive({
|
|
|
+ user: '',
|
|
|
+});
|
|
|
+
|
|
|
+// 定义添加菜单表单数据
|
|
|
+const menuAddForm = reactive({
|
|
|
+ path: '',//路径
|
|
|
+ component: '',//组件
|
|
|
+ parentId: '',//上级菜单
|
|
|
+ name: '',//菜单名称
|
|
|
+ title: '',//菜单标题
|
|
|
+ icon: '',//图标
|
|
|
+ type: 0,//菜单类型
|
|
|
+ activeMenu: '',//高亮菜单路由地址
|
|
|
+ perms: '',//按钮权限标识
|
|
|
+ menu: 1,
|
|
|
+});
|
|
|
+
|
|
|
+// 定义添加菜单表单验证规则
|
|
|
+const menuAddRules = reactive({
|
|
|
+ path: [{ required: true, message: '请输入路径', trigger: 'blur' }],
|
|
|
+ component: [{ required: true, message: '请输入组件', trigger: 'blur' }],
|
|
|
+ redirect: [{ required: false, message: '请输入重定向路径', trigger: 'blur' }],
|
|
|
+ name: [{ required: true, message: '路由标识必填', trigger: 'blur' }],
|
|
|
+ title: [{ required: true, message: '请输入菜单标题', trigger: 'blur' }],
|
|
|
+ perms: [{ required: true, message: '请输入按钮权限标识', trigger: 'blur' }],
|
|
|
+ type: [{ required: true, message: '请选择菜单类型', trigger: 'change' }],
|
|
|
+ parentId: [{ required: true, message: '请选择上级菜单', trigger: 'change' }],
|
|
|
+});
|
|
|
+const columns: Column[] = [
|
|
|
+ { key: 'title', dataKey: "title", title: "菜单名称", width: 200, },
|
|
|
+ { key: 'type', dataKey: "type", title: "菜单类型", width: 100, align: "center", },
|
|
|
+ { key: 'icon', dataKey: "icon", title: "图标", width: 100, align: "center" },
|
|
|
+ { key: 'component', dataKey: "component", title: "组件", width: 300, align: "center" },
|
|
|
+ { key: 'path', dataKey: "path", title: "页面路径", width: 300, align: "center" },
|
|
|
+ {
|
|
|
+ key: 'menu', dataKey: "menu", title: "路由是否隐藏", width: 150, align: "center",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'option',
|
|
|
+ title: "操作",
|
|
|
+ width: 250,
|
|
|
+ align: 'center',
|
|
|
+ },
|
|
|
+]
|
|
|
+const tableData = ref([]);//菜单列表
|
|
|
+const typeLabel = ref('');//菜单类型标签
|
|
|
+const typelist = [
|
|
|
+ { label: '目录', value: 0 },
|
|
|
+ { label: '菜单', value: 1 },
|
|
|
+ { label: '按钮', value: 2 },
|
|
|
+]
|
|
|
+// 根据path生成驼峰命名的路由标识
|
|
|
+const generateRouteName = (path: string): string => {
|
|
|
+ if (!path || path === '/') return ''
|
|
|
+
|
|
|
+ // 移除开头的斜杠和所有非字母数字字符
|
|
|
+ const cleanPath = path.replace(/^\/+/, '').replace(/[^a-zA-Z0-9_\-\/]/g, '')
|
|
|
+
|
|
|
+ // 按斜杠分割并转换为驼峰命名
|
|
|
+ return cleanPath.split('/')
|
|
|
+ .map(segment => {
|
|
|
+ // 处理下划线或连字符分隔的单词
|
|
|
+ return segment.split(/[-_]/)
|
|
|
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
|
+ .join('')
|
|
|
+ })
|
|
|
+ .join('')
|
|
|
+}
|
|
|
+// 处理菜单隐藏路由切换
|
|
|
+const handleMenuChange = (val: any) => {
|
|
|
+ console.log(val)
|
|
|
+ if (val) {
|
|
|
+ menuAddForm.menu = 0;
|
|
|
+ } else {
|
|
|
+ menuAddForm.menu = 1;
|
|
|
+ }
|
|
|
+}
|
|
|
+// 菜单添加子菜单
|
|
|
+const menuAddSub = (item: any) => {
|
|
|
+ menuAddForm.parentId = item.id;
|
|
|
+ menuAddForm.type = item.type;
|
|
|
+ menuAddShow.value = true;
|
|
|
+}
|
|
|
+//编辑菜单
|
|
|
+const menuEdit = (item: any) => {
|
|
|
+ const info = item;
|
|
|
+ Object.assign(menuAddForm, info)
|
|
|
+ console.log(menuAddForm)
|
|
|
+ if (menuAddForm.menu == 1) {
|
|
|
+ menu.value = false
|
|
|
+ } else {
|
|
|
+ menu.value = true
|
|
|
+ }
|
|
|
+ menuAddShow.value = true
|
|
|
+}
|
|
|
+// 删除菜单
|
|
|
+const menuDel = (item: any) => {
|
|
|
+ ElMessageBox.confirm('确定删除该菜单吗?', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning',
|
|
|
+ }).then(() => {
|
|
|
+ api.menuApi.deleteMenu(item.id).then((res: any) => {
|
|
|
+ if (res.code === 200) {
|
|
|
+ ElMessage({
|
|
|
+ type: 'success',
|
|
|
+ message: '删除成功',
|
|
|
+ plain: true,
|
|
|
+ })
|
|
|
+ Menutree()//获取菜单树
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }).catch(() => {
|
|
|
+ ElMessage({
|
|
|
+ type: 'info',
|
|
|
+ message: '已取消删除',
|
|
|
+ plain: true,
|
|
|
+ })
|
|
|
+ })
|
|
|
+}
|
|
|
+// 过滤图标
|
|
|
+const getFilteredIcons = (icon: any) => {
|
|
|
+ if (!iconSearchQuery.value) {
|
|
|
+ return icon.icons;
|
|
|
+ }
|
|
|
+ const query = iconSearchQuery.value.toLowerCase();
|
|
|
+ return icon.icons.filter((item: string) =>
|
|
|
+ item.toLowerCase().includes(query)
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+// 分页处理
|
|
|
+const getPaginatedIcons = (icons: string[]) => {
|
|
|
+ const start = (currentPage.value - 1) * pageSize.value;
|
|
|
+ const end = start + pageSize.value;
|
|
|
+ return icons.slice(start, end);
|
|
|
+}
|
|
|
+
|
|
|
+// 确认选择图标
|
|
|
+const confirmSelectIcon = () => {
|
|
|
+ if (selectedIcon.value) {
|
|
|
+ menuAddForm.icon = selectedIcon.value;
|
|
|
+ }
|
|
|
+ iconVisible.value = false;
|
|
|
+}
|
|
|
+
|
|
|
+// 菜单类型改变时处理
|
|
|
+watch(
|
|
|
+ () => menuAddForm.type,
|
|
|
+ (newType) => {
|
|
|
+ console.log(newType);
|
|
|
+ typeLabel.value = typelist.find(item => item.value === newType)?.label || '';
|
|
|
+ switch (newType) {
|
|
|
+ case 0:
|
|
|
+ menuAddForm.component = '';
|
|
|
+ break;
|
|
|
+ case 1:
|
|
|
+ menuAddForm.component = 'Layout';
|
|
|
+ break;
|
|
|
+ case 2:
|
|
|
+ menuAddForm.component = '';
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ }
|
|
|
+ },
|
|
|
+ { immediate: true } // 初始化时立即执行一次
|
|
|
+)
|
|
|
+// 添加菜单
|
|
|
+const menuAdd = () => {
|
|
|
+ menuAddShow.value = true;
|
|
|
+}
|
|
|
+
|
|
|
+// 获取菜单树
|
|
|
+const Menutree = () => {
|
|
|
+ loading.value = true;
|
|
|
+ api.menuApi.findMenuTree().then((res: any) => {
|
|
|
+ if (res.code === 200) {
|
|
|
+ tableData.value = res.data
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+// 确认添加菜单
|
|
|
+const confirmClick = async (formEl: FormInstance | undefined) => {
|
|
|
+ if (!formEl) return
|
|
|
+ await formEl.validate((valid) => {
|
|
|
+ if (valid) {
|
|
|
+ // 提交表单数据
|
|
|
+ api.menuApi.addMenu(menuAddForm).then((res: any) => {
|
|
|
+ if (res.code === 200) {
|
|
|
+ ElMessage({
|
|
|
+ type: 'success',
|
|
|
+ message: '添加成功',
|
|
|
+ plain: true,
|
|
|
+ })
|
|
|
+ menuAddShow.value = false;
|
|
|
+ Menutree();//刷新菜单树
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ ElMessage({
|
|
|
+ type: 'error',
|
|
|
+ message: '信息不完整',
|
|
|
+ plain: true,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<!-- 样式区域 -->
|
|
|
+<style lang='scss' scoped>
|
|
|
+:deep(.header-class .el-table-v2__header-cell) {
|
|
|
+ padding: 10px 20px;
|
|
|
+ font-size: 15px;
|
|
|
+ color: #333;
|
|
|
+ background-color: #eef1f6;
|
|
|
+ border-bottom: 1px solid #eef1f6;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table-v2__header-row) {
|
|
|
+ justify-content: space-between;
|
|
|
+ background-color: #eef1f6;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table-v2__row) {
|
|
|
+ justify-content: space-between;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table-v2__expand-icon) {
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table-v2__cell-text) {
|
|
|
+ margin-left: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-dropdown-link) {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-left: 15px;
|
|
|
+ color: var(--el-color-primary);
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.disabled :deep(.el-upload--picture-card) {
|
|
|
+ opacity: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.icon-container {
|
|
|
+ max-height: 400px;
|
|
|
+ padding: 10px;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.icon-item {
|
|
|
+ width: 60px;
|
|
|
+ height: 60px;
|
|
|
+ margin: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+ border: 2px solid transparent;
|
|
|
+ border-radius: 4px;
|
|
|
+ transition: all 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.icon-selected {
|
|
|
+ background-color: rgb(64 158 255 / 10%);
|
|
|
+ border-color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.icon {
|
|
|
+ font-size: 24px;
|
|
|
+}
|
|
|
+</style>
|