menuManagement.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. <!-- 模板区域 -->
  2. <template>
  3. <div>
  4. <PageMain class="vh100">
  5. <el-form :inline="true" :model="formInline" class="demo-form-inline">
  6. <el-form-item label="菜单名称">
  7. <el-input v-model="formInline.user" placeholder="请输入菜单名称" clearable />
  8. </el-form-item>
  9. <el-form-item>
  10. <el-button type="primary" @click="Menutree">
  11. <template #icon>
  12. <SvgIcon name="i-ep:search" />
  13. </template>
  14. 查询</el-button>
  15. </el-form-item>
  16. </el-form>
  17. <el-button type="primary" @click="menuAdd">
  18. <template #icon>
  19. <el-icon>
  20. <Plus />
  21. </el-icon>
  22. </template>
  23. 添加菜单</el-button>
  24. <div :style="{ height: tableHeight, marginTop: '20px' }">
  25. <el-auto-resizer>
  26. <template #default="{ height, width }">
  27. <el-table-v2 :columns="columns" :data="tableData" :width="width" :height="height" v-loading="loading"
  28. ref="multipleTableRef" :expand-column-key="expandColumnKey" header-class="header-class">
  29. <template #cell="scope">
  30. <template v-if="scope.column.key == 'option'" class="flex j-s a-c">
  31. <el-button type="primary" link @click="menuAddSub(scope.rowData)"> 添加子菜单
  32. </el-button>
  33. <el-dropdown placement="bottom-start">
  34. <span class="el-dropdown-link">
  35. 更 多
  36. </span>
  37. <template #dropdown>
  38. <el-dropdown-menu>
  39. <el-dropdown-item @click="menuEdit(scope.rowData)">编辑</el-dropdown-item>
  40. <el-dropdown-item @click="menuDel(scope.rowData)">删除</el-dropdown-item>
  41. </el-dropdown-menu>
  42. </template>
  43. </el-dropdown>
  44. </template>
  45. <template v-if="scope.column.key == 'type'">
  46. <span>{{typelist.find(item => item.value == scope.rowData.type)?.label || ''}}</span>
  47. </template>
  48. <template v-if="scope.column.key == 'icon'">
  49. <SvgIcon :name="scope.rowData.icon || ''" />
  50. </template>
  51. <template v-if="scope.column.key == 'menu'">
  52. <el-tag v-if="scope.rowData.menu == 1" type="primary">显示</el-tag>
  53. <el-tag v-if="scope.rowData.menu == 0" type="danger">隐藏</el-tag>
  54. </template>
  55. </template>
  56. </el-table-v2>
  57. </template>
  58. </el-auto-resizer>
  59. </div>
  60. </PageMain>
  61. <!-- 添加菜单弹窗 -->
  62. <el-drawer v-model="menuAddShow" direction="rtl" size="800px">
  63. <template #header>
  64. <h4>菜单配置</h4>
  65. </template>
  66. <template #default>
  67. <el-form :model="menuAddForm" :rules="menuAddRules" ref="menuAddFormRef" label-width="120px">
  68. <el-form-item label="菜单类型" prop="type">
  69. <el-radio-group v-model="menuAddForm.type" fill="#409eff">
  70. <el-radio-button v-for="item in typelist" :label="item.label" :value="item.value"
  71. :key="item.value"></el-radio-button>
  72. </el-radio-group>
  73. </el-form-item>
  74. <el-form-item :label="`${typeLabel}名称`" prop="title">
  75. <el-input v-model="menuAddForm.title" :placeholder="`请输入${typeLabel}名称`" clearable />
  76. </el-form-item>
  77. <el-form-item label="上级菜单" prop="parentId" v-if="menuAddForm.type != 0">
  78. <el-tree-select v-model="menuAddForm.parentId" :data="tableData" node-key="id" :props="defaultProps"
  79. check-strictly :render-after-expand="false" />
  80. </el-form-item>
  81. <el-form-item label="路由标识" prop="name" v-if="menuAddForm.type == 1">
  82. <el-input v-model="menuAddForm.name" placeholder="请输入路由标识" clearable />
  83. </el-form-item>
  84. <el-form-item label="是否缓存页面" prop="keepAlive" v-if="menuAddForm.type == 1">
  85. <el-switch v-model="menuAddForm.keepAlive" active-value="1" inactive-value="0" />
  86. </el-form-item>
  87. <el-form-item label="访问路径" prop="path" v-if="menuAddForm.type == 1">
  88. <el-input v-model="menuAddForm.path" placeholder="请输入访问路径" clearable
  89. @blur="menuAddForm.name = generateRouteName(menuAddForm.path)" />
  90. </el-form-item>
  91. <el-form-item label="组件" prop="component" v-if="menuAddForm.type == 1">
  92. <el-input v-model="menuAddForm.component" placeholder="请输入组件" clearable />
  93. </el-form-item>
  94. <el-form-item label="菜单图标" v-if="menuAddForm.type != 2">
  95. <el-input v-model="menuAddForm.icon" placeholder="请选择菜单图标" clearable >
  96. <template #append>
  97. <el-button :icon="Search" @click="iconVisible = true" />
  98. </template>
  99. </el-input>
  100. </el-form-item>
  101. <el-form-item label="按钮权限标识" prop="perms" v-if="menuAddForm.type == 2">
  102. <el-input v-model="menuAddForm.perms" placeholder="请输入按钮权限标识" clearable />
  103. </el-form-item>
  104. <el-form-item label="菜单高亮地址" v-if="menu">
  105. <el-input v-model="menuAddForm.activeMenu" placeholder="请输入菜单高亮地址" clearable />
  106. </el-form-item>
  107. <el-form-item label="隐藏路由" prop="menu" v-if="menuAddForm.type != 2">
  108. <el-switch v-model="menu" @change="handleMenuChange" />
  109. </el-form-item>
  110. </el-form>
  111. </template>
  112. <template #footer>
  113. <div style="flex: auto;">
  114. <el-button @click="menuAddShow = false">取消</el-button>
  115. <el-button type="primary" @click="confirmClick(menuAddFormRef)">确认</el-button>
  116. </div>
  117. </template>
  118. </el-drawer>
  119. <!-- 图标弹窗 -->
  120. <el-dialog v-model="iconVisible" title="选择图标" width="1200px" height="800px " top="8vh">
  121. <div>
  122. <!-- 图标搜索框 -->
  123. <el-input v-model="iconSearchQuery" placeholder="搜索图标名称" class="mb-4" clearable @input="currentPage = 1">
  124. <template #prefix>
  125. <el-icon>
  126. <Search />
  127. </el-icon>
  128. </template>
  129. </el-input>
  130. <el-tabs v-model="activeName" class="demo-tabs" @tab-change="currentPage = 1">
  131. <el-tab-pane :label="icon.info.name + '图标'" :name="icon.prefix" v-for="(icon, key) in icons" :key="key">
  132. <!-- 计算当前标签页的过滤和分页后的图标 -->
  133. <div v-if="getFilteredIcons(icon).length === 0" class="text-center py-10 text-gray-500">
  134. 未找到匹配的图标
  135. </div>
  136. <div v-else>
  137. <div class="icon-container flex f-wrap">
  138. <div class="icon-item flex a-c j-c " v-for="(item, index) in getPaginatedIcons(getFilteredIcons(icon))"
  139. :key="index" :class="{ 'icon-selected': selectedIcon === `${icon.prefix}:${item}` }"
  140. @click="selectedIcon = `${icon.prefix}:${item}`">
  141. <SvgIcon :name="`${icon.prefix}:${item}`" class="icon" />
  142. </div>
  143. </div>
  144. <!-- 分页组件 -->
  145. <div class="flex justify-center mt-4">
  146. <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize"
  147. :page-sizes="[20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
  148. :total="getFilteredIcons(icon).length" @size-change="currentPage = 1" />
  149. </div>
  150. </div>
  151. </el-tab-pane>
  152. </el-tabs>
  153. </div>
  154. <template #footer>
  155. <div class="dialog-footer">
  156. <el-button @click="iconVisible = false">取消</el-button>
  157. <el-button type="primary" @click="confirmSelectIcon">
  158. 确定
  159. </el-button>
  160. </div>
  161. </template>
  162. </el-dialog>
  163. </div>
  164. </template>
  165. <!-- 行为区域 -->
  166. <script lang='ts' setup>
  167. import { ref, reactive, computed } from 'vue'
  168. import { FormRules, FormInstance, ComponentSize, ElMessageBox } from 'element-plus';
  169. import api from '@/api'
  170. import { ElTag, ElMessage } from 'element-plus';
  171. import { icons } from '@/iconify'
  172. import { Search, Plus } from '@element-plus/icons-vue'
  173. interface Column {
  174. key?: string,
  175. dataKey?: string,
  176. title: string,
  177. width: number,
  178. class?: string,
  179. cellRenderer?: any
  180. align?: "left" | "center" | "right"; // align 是可选的,且只能是这三个值之一
  181. }
  182. const defaultProps = {
  183. children: 'children',
  184. label: 'title',
  185. }
  186. onMounted(() => {
  187. setTimeout(() => {
  188. const tableElement = multipleTableRef.value?.$el as HTMLElement;
  189. if (tableElement) {
  190. const offsetTop = tableElement.offsetTop; // 获取 offsetTop
  191. let height = window.innerHeight;
  192. tableHeight.value = height - offsetTop - 190 + "px";
  193. }
  194. }, 10);
  195. })
  196. onBeforeMount(() => {
  197. Menutree()//获取菜单树
  198. })
  199. const menuAddFormRef = ref<FormInstance>()
  200. const multipleTableRef = ref();//多个表格引用
  201. const iconVisible = ref(false);//图标弹窗显示
  202. const menuAddShow = ref(false);//添加菜单弹窗显示
  203. const activeName = ref('ep');//当前选中的tab
  204. const selectedIcon = ref('');//当前选中的图标
  205. const iconSearchQuery = ref('');//图标搜索关键词
  206. const currentPage = ref(1);//当前页码
  207. const pageSize = ref(50);//每页显示图标数量
  208. const tableHeight = ref(''); // 初始化表格高度
  209. const loading = ref(false)
  210. const expandColumnKey = 'title'//标记可展开列
  211. const menu = ref(false);
  212. // 定义查询表单数据
  213. const formInline = reactive({
  214. user: '',
  215. });
  216. // 定义添加菜单表单数据
  217. const menuAddForm = reactive({
  218. path: '',//路径
  219. keepAlive: '0',//是否缓存页面
  220. component: '',//组件
  221. parentId: '',//上级菜单
  222. name: '',//菜单名称
  223. title: '',//菜单标题
  224. icon: '',//图标
  225. type: 0,//菜单类型
  226. activeMenu: '',//高亮菜单路由地址
  227. perms: '',//按钮权限标识
  228. menu: 1,
  229. });
  230. // 定义添加菜单表单验证规则
  231. const menuAddRules = reactive({
  232. path: [{ required: true, message: '请输入路径', trigger: 'blur' }],
  233. component: [{ required: true, message: '请输入组件', trigger: 'blur' }],
  234. redirect: [{ required: false, message: '请输入重定向路径', trigger: 'blur' }],
  235. name: [{ required: true, message: '路由标识必填', trigger: 'blur' }],
  236. title: [{ required: true, message: '请输入菜单标题', trigger: 'blur' }],
  237. perms: [{ required: true, message: '请输入按钮权限标识', trigger: 'blur' }],
  238. type: [{ required: true, message: '请选择菜单类型', trigger: 'change' }],
  239. parentId: [{ required: true, message: '请选择上级菜单', trigger: 'change' }],
  240. });
  241. const columns: Column[] = [
  242. { key: 'title', dataKey: "title", title: "菜单名称", width: 200, },
  243. { key: 'type', dataKey: "type", title: "菜单类型", width: 100, align: "center", },
  244. { key: 'icon', dataKey: "icon", title: "图标", width: 100, align: "center" },
  245. { key: 'component', dataKey: "component", title: "组件", width: 300, align: "center" },
  246. { key: 'path', dataKey: "path", title: "页面路径", width: 300, align: "center" },
  247. {
  248. key: 'menu', dataKey: "menu", title: "路由是否隐藏", width: 150, align: "center",
  249. },
  250. {
  251. key: 'option',
  252. title: "操作",
  253. width: 250,
  254. align: 'center',
  255. },
  256. ]
  257. const tableData = ref([]);//菜单列表
  258. const typeLabel = ref('');//菜单类型标签
  259. const typelist = [
  260. { label: '目录', value: 0 },
  261. { label: '菜单', value: 1 },
  262. { label: '按钮', value: 2 },
  263. ]
  264. // 根据path生成驼峰命名的路由标识
  265. const generateRouteName = (path: string): string => {
  266. if (!path || path === '/') return ''
  267. // 移除开头的斜杠和所有非字母数字字符
  268. const cleanPath = path.replace(/^\/+/, '').replace(/[^a-zA-Z0-9_\-\/]/g, '')
  269. // 按斜杠分割并转换为驼峰命名
  270. return cleanPath.split('/')
  271. .map(segment => {
  272. // 处理下划线或连字符分隔的单词
  273. return segment.split(/[-_]/)
  274. .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
  275. .join('')
  276. })
  277. .join('')
  278. }
  279. // 处理菜单隐藏路由切换
  280. const handleMenuChange = (val: any) => {
  281. console.log(val)
  282. if (val) {
  283. menuAddForm.menu = 0;
  284. } else {
  285. menuAddForm.menu = 1;
  286. }
  287. }
  288. // 菜单添加子菜单
  289. const menuAddSub = (item: any) => {
  290. menuAddForm.parentId = item.id;
  291. menuAddForm.type = item.type;
  292. menuAddShow.value = true;
  293. }
  294. //编辑菜单
  295. const menuEdit = (item: any) => {
  296. console.log(item)
  297. const info = item;
  298. Object.assign(menuAddForm, info)
  299. console.log(menuAddForm)
  300. if (menuAddForm.menu == 1) {
  301. menu.value = false
  302. } else {
  303. menu.value = true
  304. }
  305. menuAddShow.value = true
  306. }
  307. // 删除菜单
  308. const menuDel = (item: any) => {
  309. ElMessageBox.confirm('确定删除该菜单吗?', '提示', {
  310. confirmButtonText: '确定',
  311. cancelButtonText: '取消',
  312. type: 'warning',
  313. }).then(() => {
  314. api.menuApi.deleteMenu(item.id).then((res: any) => {
  315. if (res.code === 200) {
  316. ElMessage({
  317. type: 'success',
  318. message: '删除成功',
  319. plain: true,
  320. })
  321. Menutree()//获取菜单树
  322. }
  323. })
  324. }).catch(() => {
  325. ElMessage({
  326. type: 'info',
  327. message: '已取消删除',
  328. plain: true,
  329. })
  330. })
  331. }
  332. // 过滤图标
  333. const getFilteredIcons = (icon: any) => {
  334. if (!iconSearchQuery.value) {
  335. return icon.icons;
  336. }
  337. const query = iconSearchQuery.value.toLowerCase();
  338. return icon.icons.filter((item: string) =>
  339. item.toLowerCase().includes(query)
  340. );
  341. }
  342. // 分页处理
  343. const getPaginatedIcons = (icons: string[]) => {
  344. const start = (currentPage.value - 1) * pageSize.value;
  345. const end = start + pageSize.value;
  346. return icons.slice(start, end);
  347. }
  348. // 确认选择图标
  349. const confirmSelectIcon = () => {
  350. if (selectedIcon.value) {
  351. menuAddForm.icon = selectedIcon.value;
  352. }
  353. iconVisible.value = false;
  354. }
  355. // 菜单类型改变时处理
  356. // watch(
  357. // () => menuAddForm.type,
  358. // (newType) => {
  359. // console.log(newType);
  360. // typeLabel.value = typelist.find(item => item.value === newType)?.label || '';
  361. // switch (newType) {
  362. // case 0:
  363. // menuAddForm.component = '';
  364. // break;
  365. // case 1:
  366. // menuAddForm.component = '';
  367. // break;
  368. // case 2:
  369. // menuAddForm.component = '';
  370. // break;
  371. // default:
  372. // }
  373. // },
  374. // { immediate: true } // 初始化时立即执行一次
  375. // )
  376. // 添加菜单
  377. const menuAdd = () => {
  378. menuAddShow.value = true;
  379. }
  380. // 获取菜单树
  381. const Menutree = () => {
  382. loading.value = true;
  383. api.menuApi.findMenuTree().then((res: any) => {
  384. if (res.code === 200) {
  385. tableData.value = res.data
  386. loading.value = false;
  387. }
  388. })
  389. }
  390. // 确认添加菜单
  391. const confirmClick = async (formEl: FormInstance | undefined) => {
  392. if (!formEl) return
  393. await formEl.validate((valid) => {
  394. if (valid) {
  395. // 提交表单数据
  396. api.menuApi.addMenu(menuAddForm).then((res: any) => {
  397. if (res.code === 200) {
  398. ElMessage({
  399. type: 'success',
  400. message: '添加成功',
  401. plain: true,
  402. })
  403. menuAddShow.value = false;
  404. Menutree();//刷新菜单树
  405. }
  406. })
  407. } else {
  408. ElMessage({
  409. type: 'error',
  410. message: '信息不完整',
  411. plain: true,
  412. })
  413. }
  414. })
  415. }
  416. </script>
  417. <!-- 样式区域 -->
  418. <style lang='scss' scoped>
  419. :deep(.header-class .el-table-v2__header-cell) {
  420. padding: 10px 20px;
  421. font-size: 15px;
  422. color: #333;
  423. background-color: #eef1f6;
  424. border-bottom: 1px solid #eef1f6;
  425. }
  426. :deep(.el-table-v2__header-row) {
  427. justify-content: space-between;
  428. background-color: #eef1f6;
  429. }
  430. :deep(.el-table-v2__row) {
  431. justify-content: space-between;
  432. }
  433. :deep(.el-table-v2__expand-icon) {
  434. margin: 0;
  435. }
  436. :deep(.el-table-v2__cell-text) {
  437. margin-left: 10px;
  438. }
  439. :deep(.el-dropdown-link) {
  440. display: flex;
  441. align-items: center;
  442. margin-left: 15px;
  443. color: var(--el-color-primary);
  444. cursor: pointer;
  445. }
  446. .disabled :deep(.el-upload--picture-card) {
  447. opacity: 0;
  448. }
  449. .icon-container {
  450. max-height: 400px;
  451. padding: 10px;
  452. overflow-y: auto;
  453. }
  454. .icon-item {
  455. width: 60px;
  456. height: 60px;
  457. margin: 8px;
  458. cursor: pointer;
  459. border: 2px solid transparent;
  460. border-radius: 4px;
  461. transition: all 0.3s;
  462. }
  463. .icon-selected {
  464. background-color: rgb(64 158 255 / 10%);
  465. border-color: #409eff;
  466. }
  467. .icon {
  468. font-size: 24px;
  469. }
  470. </style>