feat:菜单完成修复

This commit is contained in:
fangyunong 2025-07-16 10:29:46 +08:00
parent 157491c6f4
commit 36a217aec9
9 changed files with 336 additions and 174 deletions

View File

@ -46,7 +46,7 @@ export type MenuNode = {
// 新增父级菜单 // 新增父级菜单
export function addParentMenu(data: Omit<RawMenu, "uuid" | "parentId">) { export function addParentMenu(data: Omit<RawMenu, "uuid" | "parentId">) {
return http({ return http({
url: "/api/Menu/create-parent", url: "/api/Menu/createParent",
method: "POST", method: "POST",
data, data,
}); });
@ -54,7 +54,7 @@ export function addParentMenu(data: Omit<RawMenu, "uuid" | "parentId">) {
// 编辑父级菜单 // 编辑父级菜单
export function editParentMenu(data: Omit<RawMenu, "parentId">) { export function editParentMenu(data: Omit<RawMenu, "parentId">) {
return http({ return http({
url: "/api/Menu/update-parent", url: "/api/Menu/updateParent",
method: "PUT", method: "PUT",
data, data,
}); });
@ -62,7 +62,7 @@ export function editParentMenu(data: Omit<RawMenu, "parentId">) {
// 新增子级菜单 // 新增子级菜单
export function addChildMenu(data:Omit<RawMenu, "uuid">){ export function addChildMenu(data:Omit<RawMenu, "uuid">){
return http({ return http({
url:'/api/Menu/create-child', url:'/api/Menu/createChild',
method:'POST', method:'POST',
data data
}) })
@ -70,7 +70,7 @@ export function addChildMenu(data:Omit<RawMenu, "uuid">){
// 编辑子级菜单 // 编辑子级菜单
export function eidtChildMenu(data:RawMenu){ export function eidtChildMenu(data:RawMenu){
return http({ return http({
url:'/api/Menu/update-child', url:'/api/Menu/updateChild',
method:'PUT', method:'PUT',
data data
}) })
@ -85,7 +85,7 @@ export function getAllMenu():Promise<MenuTree[]>{
// 递归删除菜单 // 递归删除菜单
export function deleteMenu(uuid:string){ export function deleteMenu(uuid:string){
return http({ return http({
url:`/api/Menu/all/${uuid}`, url:`/api/Menu/delete/${uuid}`,
method:'DELETE' method:'DELETE'
}) })
} }

View File

@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1751689452739" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6754" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M510.0032 492.9536c-12.16 0-24.3456-2.3808-35.9424-7.1168L71.7824 300.5952c-11.3408-4.6336-18.816-15.5648-19.072-27.8016s6.784-23.4496 17.92-28.544l400.1536-183.296a100.1728 100.1728 0 0 1 82.7648-0.3072l398.1056 178.8416c11.1616 5.0176 18.2784 16.1792 18.1248 28.416s-7.5776 23.2192-18.8672 27.9296l-404.3264 189.7216a94.26944 94.26944 0 0 1-36.5824 7.3984zM160.64 270.592l336.7424 158.3872a33.0496 33.0496 0 0 0 25.5232-0.1024l338.9952-162.3808-333.5424-149.8112a38.6688 38.6688 0 0 0-32 0.128L160.64 270.592z" fill="#3D8EFF" p-id="6755"></path><path d="M510.1568 722.7648c-14.4896 0-28.9536-3.3536-42.1632-10.0864L68.3264 509.0816c-8.576-4.3776-15.0784-12.2624-16.9472-21.6832a30.7072 30.7072 0 0 1 15.0272-32.8192l189.6704-106.9568c19.5584-11.0336 44.3136-4.1216 55.3472 15.4368l10.2144 18.1248-175.1808 98.7904L495.872 657.92a31.5392 31.5392 0 0 0 28.672-0.0512l354.0224-181.9136-175.9488-100.736 10.3424-18.0736c11.1616-19.4816 35.968-26.2144 55.4496-15.0784l189.7984 108.672c9.7792 5.6064 15.6928 16.1024 15.4368 27.3664s-6.656 21.4784-16.6656 26.624l-404.352 207.7696a92.53376 92.53376 0 0 1-42.4704 10.2656z" fill="#3D8EFF" p-id="6756"></path><path d="M512.2048 975.9744c-4.7872 0-9.5744-1.1264-13.952-3.3536L70.2464 754.5856c-8.576-4.3776-15.0784-12.2624-16.9472-21.7088a30.72512 30.72512 0 0 1 15.0272-32.8192l192.1536-108.3648c18.176-10.24 41.2416-3.8144 51.4816 14.3616l11.6224 20.608-175.1808 98.7904 363.7504 185.2928 368.3584-189.2608-175.9488-100.736 11.7504-20.5312c10.368-18.1248 33.4592-24.3968 51.584-14.0288l192.2816 110.08c9.7792 5.6064 15.6928 16.1024 15.4368 27.3664a30.72 30.72 0 0 1-16.6656 26.624l-432.6656 222.3104a31.0272 31.0272 0 0 1-14.08 3.4048z" fill="#3D8EFF" p-id="6757"></path></svg> <?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1751689452739" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6754" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M510.0032 492.9536c-12.16 0-24.3456-2.3808-35.9424-7.1168L71.7824 300.5952c-11.3408-4.6336-18.816-15.5648-19.072-27.8016s6.784-23.4496 17.92-28.544l400.1536-183.296a100.1728 100.1728 0 0 1 82.7648-0.3072l398.1056 178.8416c11.1616 5.0176 18.2784 16.1792 18.1248 28.416s-7.5776 23.2192-18.8672 27.9296l-404.3264 189.7216a94.26944 94.26944 0 0 1-36.5824 7.3984zM160.64 270.592l336.7424 158.3872a33.0496 33.0496 0 0 0 25.5232-0.1024l338.9952-162.3808-333.5424-149.8112a38.6688 38.6688 0 0 0-32 0.128L160.64 270.592z" p-id="6755"></path><path d="M510.1568 722.7648c-14.4896 0-28.9536-3.3536-42.1632-10.0864L68.3264 509.0816c-8.576-4.3776-15.0784-12.2624-16.9472-21.6832a30.7072 30.7072 0 0 1 15.0272-32.8192l189.6704-106.9568c19.5584-11.0336 44.3136-4.1216 55.3472 15.4368l10.2144 18.1248-175.1808 98.7904L495.872 657.92a31.5392 31.5392 0 0 0 28.672-0.0512l354.0224-181.9136-175.9488-100.736 10.3424-18.0736c11.1616-19.4816 35.968-26.2144 55.4496-15.0784l189.7984 108.672c9.7792 5.6064 15.6928 16.1024 15.4368 27.3664s-6.656 21.4784-16.6656 26.624l-404.352 207.7696a92.53376 92.53376 0 0 1-42.4704 10.2656z" p-id="6756"></path><path d="M512.2048 975.9744c-4.7872 0-9.5744-1.1264-13.952-3.3536L70.2464 754.5856c-8.576-4.3776-15.0784-12.2624-16.9472-21.7088a30.72512 30.72512 0 0 1 15.0272-32.8192l192.1536-108.3648c18.176-10.24 41.2416-3.8144 51.4816 14.3616l11.6224 20.608-175.1808 98.7904 363.7504 185.2928 368.3584-189.2608-175.9488-100.736 11.7504-20.5312c10.368-18.1248 33.4592-24.3968 51.584-14.0288l192.2816 110.08c9.7792 5.6064 15.6928 16.1024 15.4368 27.3664a30.72 30.72 0 0 1-16.6656 26.624l-432.6656 222.3104a31.0272 31.0272 0 0 1-14.08 3.4048z" p-id="6757"></path></svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1751689762805" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11546" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M720.213333 269.653333c30.72 0 51.2-20.48 54.613334-47.786666v-170.666667c0-27.306667-20.48-47.786667-51.2-47.786667-27.306667 0-51.2 20.48-51.2 47.786667v170.666667c0 23.893333 20.48 44.373333 47.786666 47.786666zM300.373333 269.653333c30.72 0 51.2-20.48 54.613334-47.786666v-170.666667C351.573333 23.893333 331.093333 0 303.786667 0 273.066667 0 252.586667 20.48 252.586667 47.786667v170.666666c0 27.306667 20.48 47.786667 47.786666 51.2zM409.6 85.333333h201.386667v102.4H409.6z" fill="#FFFFFF" p-id="11547"></path><path d="M890.88 85.333333h-61.44v102.4H887.466667c20.48 0 34.133333 17.066667 34.133333 34.133334v215.04l3.413333 447.146666c0 20.48-17.066667 37.546667-37.546666 37.546667H136.533333c-20.48 0-37.546667-17.066667-37.546666-37.546667L102.4 436.906667V221.866667c0-17.066667 13.653333-34.133333 34.133333-34.133334h58.026667v-102.4H133.12C61.44 85.333333 0 143.36 0 218.453333v672.426667C0 962.56 61.44 1024 133.12 1024h757.76c71.68 0 133.12-61.44 133.12-133.12V218.453333c0-75.093333-61.44-133.12-133.12-133.12z" fill="#FFFFFF" p-id="11548"></path><path d="M682.666667 624.64c27.306667 0 51.2-23.893333 51.2-51.2s-23.893333-51.2-51.2-51.2h-47.786667l81.92-81.92c20.48-20.48 20.48-51.2 0-71.68-20.48-20.48-51.2-20.48-71.68 0L512 498.346667l-133.12-133.12c-20.48-20.48-51.2-20.48-71.68 0-20.48 20.48-20.48 51.2 0 71.68l81.92 81.92H341.333333c-27.306667 0-51.2 23.893333-51.2 51.2s23.893333 51.2 51.2 51.2h119.466667V682.666667H341.333333c-27.306667 0-51.2 23.893333-51.2 51.2S314.026667 785.066667 341.333333 785.066667h119.466667v37.546666c0 27.306667 23.893333 51.2 51.2 51.2s51.2-23.893333 51.2-51.2V785.066667H682.666667c27.306667 0 51.2-23.893333 51.2-51.2S709.973333 682.666667 682.666667 682.666667h-119.466667v-61.44H682.666667z" fill="#FFFFFF" p-id="11549"></path></svg> <?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1751689762805" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11546" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M720.213333 269.653333c30.72 0 51.2-20.48 54.613334-47.786666v-170.666667c0-27.306667-20.48-47.786667-51.2-47.786667-27.306667 0-51.2 20.48-51.2 47.786667v170.666667c0 23.893333 20.48 44.373333 47.786666 47.786666zM300.373333 269.653333c30.72 0 51.2-20.48 54.613334-47.786666v-170.666667C351.573333 23.893333 331.093333 0 303.786667 0 273.066667 0 252.586667 20.48 252.586667 47.786667v170.666666c0 27.306667 20.48 47.786667 47.786666 51.2zM409.6 85.333333h201.386667v102.4H409.6z" p-id="11547"></path><path d="M890.88 85.333333h-61.44v102.4H887.466667c20.48 0 34.133333 17.066667 34.133333 34.133334v215.04l3.413333 447.146666c0 20.48-17.066667 37.546667-37.546666 37.546667H136.533333c-20.48 0-37.546667-17.066667-37.546666-37.546667L102.4 436.906667V221.866667c0-17.066667 13.653333-34.133333 34.133333-34.133334h58.026667v-102.4H133.12C61.44 85.333333 0 143.36 0 218.453333v672.426667C0 962.56 61.44 1024 133.12 1024h757.76c71.68 0 133.12-61.44 133.12-133.12V218.453333c0-75.093333-61.44-133.12-133.12-133.12z" p-id="11548"></path><path d="M682.666667 624.64c27.306667 0 51.2-23.893333 51.2-51.2s-23.893333-51.2-51.2-51.2h-47.786667l81.92-81.92c20.48-20.48 20.48-51.2 0-71.68-20.48-20.48-51.2-20.48-71.68 0L512 498.346667l-133.12-133.12c-20.48-20.48-51.2-20.48-71.68 0-20.48 20.48-20.48 51.2 0 71.68l81.92 81.92H341.333333c-27.306667 0-51.2 23.893333-51.2 51.2s23.893333 51.2 51.2 51.2h119.466667V682.666667H341.333333c-27.306667 0-51.2 23.893333-51.2 51.2S314.026667 785.066667 341.333333 785.066667h119.466667v37.546666c0 27.306667 23.893333 51.2 51.2 51.2s51.2-23.893333 51.2-51.2V785.066667H682.666667c27.306667 0 51.2-23.893333 51.2-51.2S709.973333 682.666667 682.666667 682.666667h-119.466667v-61.44H682.666667z" p-id="11549"></path></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1751687644150" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5527" width="24" height="24" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M701.3376 147.2c66.048 0 118.8608 55.5008 121.6512 123.904l0.128 5.7344v444.7232c0 69.12-51.0208 126.464-116.3008 129.5104l-5.4784 0.128H322.6624c-66.048 0-118.8608-55.5008-121.6512-123.904l-0.128-5.7344v-83.5328a32 32 0 0 1 63.8464-3.2768l0.1536 3.2768v83.5328c0 35.328 24.4224 63.3088 54.0416 65.5104l3.7376 0.128h378.6752c30.0288 0 55.6544-26.5472 57.6512-61.2608l0.128-4.3776V276.8384c0-35.328-24.4224-63.3088-54.0416-65.5104l-3.7376-0.128H322.6624c-30.0288 0-55.6544 26.5472-57.6512 61.2608l-0.128 4.3776v211.1488a32 32 0 0 1-63.8208 3.2768l-0.1792-3.2768v-211.1488c0-69.12 51.0208-126.464 116.3008-129.5104l5.4784-0.128h378.6752z" fill="#FFFFFF" p-id="5528"></path><path d="M440.2688 352.512a25.6 25.6 0 0 1 39.2448 32.7168l-2.048 2.4832-56.2176 59.4176a25.6 25.6 0 0 1-32.4096 3.968l-2.4832-1.8176-36.608-30.2592a25.6 25.6 0 0 1 30.0032-41.3696l2.5856 1.8944 18.1504 15.0016 39.7824-42.0352zM440.2688 561.664a25.6 25.6 0 0 1 39.2448 32.7168l-2.048 2.4576-56.2176 59.4432a25.6 25.6 0 0 1-32.4096 3.9424l-2.4832-1.792-36.608-30.2848a25.6 25.6 0 0 1 30.0032-41.344l2.5856 1.8944 18.1504 14.976 39.7824-42.0096zM668.5696 384a25.6 25.6 0 0 1 2.9952 51.0208l-2.9952 0.1792h-138.24a25.6 25.6 0 0 1-2.9696-51.0208l2.9696-0.1792h138.24zM668.5696 583.3728a25.6 25.6 0 0 1 2.9952 51.0208l-2.9952 0.1792h-142.08a25.6 25.6 0 0 1-2.9696-51.0464l2.9696-0.1536h142.08z" fill="#FFFFFF" p-id="5529"></path></svg> <?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1751687644150" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5527" width="24" height="24" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M701.3376 147.2c66.048 0 118.8608 55.5008 121.6512 123.904l0.128 5.7344v444.7232c0 69.12-51.0208 126.464-116.3008 129.5104l-5.4784 0.128H322.6624c-66.048 0-118.8608-55.5008-121.6512-123.904l-0.128-5.7344v-83.5328a32 32 0 0 1 63.8464-3.2768l0.1536 3.2768v83.5328c0 35.328 24.4224 63.3088 54.0416 65.5104l3.7376 0.128h378.6752c30.0288 0 55.6544-26.5472 57.6512-61.2608l0.128-4.3776V276.8384c0-35.328-24.4224-63.3088-54.0416-65.5104l-3.7376-0.128H322.6624c-30.0288 0-55.6544 26.5472-57.6512 61.2608l-0.128 4.3776v211.1488a32 32 0 0 1-63.8208 3.2768l-0.1792-3.2768v-211.1488c0-69.12 51.0208-126.464 116.3008-129.5104l5.4784-0.128h378.6752z" p-id="5528"></path><path d="M440.2688 352.512a25.6 25.6 0 0 1 39.2448 32.7168l-2.048 2.4832-56.2176 59.4176a25.6 25.6 0 0 1-32.4096 3.968l-2.4832-1.8176-36.608-30.2592a25.6 25.6 0 0 1 30.0032-41.3696l2.5856 1.8944 18.1504 15.0016 39.7824-42.0352zM440.2688 561.664a25.6 25.6 0 0 1 39.2448 32.7168l-2.048 2.4576-56.2176 59.4432a25.6 25.6 0 0 1-32.4096 3.9424l-2.4832-1.792-36.608-30.2848a25.6 25.6 0 0 1 30.0032-41.344l2.5856 1.8944 18.1504 14.976 39.7824-42.0096zM668.5696 384a25.6 25.6 0 0 1 2.9952 51.0208l-2.9952 0.1792h-138.24a25.6 25.6 0 0 1-2.9696-51.0208l2.9696-0.1792h138.24zM668.5696 583.3728a25.6 25.6 0 0 1 2.9952 51.0208l-2.9952 0.1792h-142.08a25.6 25.6 0 0 1-2.9696-51.0464l2.9696-0.1536h142.08z" p-id="5529"></path></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -24,4 +24,7 @@
&.right{ &.right{
justify-content: right; justify-content: right;
} }
&.between{
justify-content: space-between;
}
} }

View File

@ -39,7 +39,8 @@ const iconName = computed(() => `#icon-${props.iconClass}`);
const svgStyle = computed(() => ({ const svgStyle = computed(() => ({
width: typeof props.width === 'number' ? `${props.width}px` : props.width, width: typeof props.width === 'number' ? `${props.width}px` : props.width,
height: typeof props.height === 'number' ? `${props.height}px` : props.height, height: typeof props.height === 'number' ? `${props.height}px` : props.height,
fill: props.color fill: props.color + ' !important',
'--icon-color': props.color
})); }));
</script> </script>
@ -48,5 +49,12 @@ const svgStyle = computed(() => ({
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
overflow: hidden; overflow: hidden;
:deep(path) {
fill: var(--icon-color) !important;
}
:deep(use) {
fill: var(--icon-color) !important;
}
} }
</style> </style>

View File

@ -1,4 +1,9 @@
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"; import {
createRouter,
createWebHistory,
RouteRecordRaw,
RouterView,
} from "vue-router";
import NProgress from "nprogress"; // progress bar import NProgress from "nprogress"; // progress bar
import "nprogress/nprogress.css"; // progress bar style import "nprogress/nprogress.css"; // progress bar style
@ -36,9 +41,16 @@ const routes: Array<RouteRecordRaw> = [
component: Home, component: Home,
}, },
{ {
path: "role", // 匹配 /layout/role path: "role",
name: "roleAuth", name: "Role",
component: Auth, component: RouterView,
children: [
{
path: "auth", // 匹配 /layout/role
name: "roleAuth",
component: Auth,
},
],
}, },
{ {
path: "menu", path: "menu",

59
src/utils/permission.ts Normal file
View File

@ -0,0 +1,59 @@
import { RouteMeta, RouteRecordRaw, RouterView } from "vue-router";
import NotFound from "@/views/404/index.vue"
interface MenuItem {
uuid: string;
path: string;
label: string;
icon: string;
menuCode: string;
adaptability: string;
component: string;
sort: number;
status: string;
query: string;
parentId?: string;
children?: MenuItem[];
}
function getComponent(componentPath: string) {
// 处理view-router特殊路由
if (componentPath === 'view-router') {
return RouterView
}
// 动态导入组件,添加错误处理
return () => {
try {
// 尝试动态导入组件
const componentPromise = import(`@/views/${componentPath}`)
// 成功加载则返回组件
return componentPromise.catch(() => {
console.error(`组件加载失败: @/views/${componentPath}, 回退到404页面`)
return NotFound
})
} catch (error) {
console.error('动态导入组件时发生错误:', error)
return NotFound
}
}
}
export function generateRoutes(menuList: MenuItem[]): RouteRecordRaw[] {
return menuList.map((menu) => {
const route: RouteRecordRaw = {
path: menu.path,
name: menu.path.toUpperCase(), //路径转大写
meta: {
...(menu.query ? JSON.parse(menu.query) : {})
},
component: getComponent(menu.component),
children:[]
};
if (menu.children && menu.children.length > 0) {
route.children = generateRoutes(menu.children);
}
return route;
});
}

View File

@ -1,81 +1,82 @@
<template> <template>
<n-layout class="menu-management"> <n-layout class="menu-management">
<n-layout-header bordered class="menu-management__header"> <n-layout-header bordered class="menu-management__header">
<n-space justify="space-between" align="center"> <div class="title">菜单管理</div>
<n-h2>菜单管理</n-h2> <n-button type="primary" @click="handleAddParent" size="small">
<n-button type="primary" @click="handleAddParent"> <template #icon>
<template #icon> <n-icon>
<n-icon><PlusOutlined /></n-icon> <Add />
</template> </n-icon>
添加父级菜单 </template>
</n-button> 添加父级菜单
</n-space> </n-button>
</n-layout-header> </n-layout-header>
<n-layout-content class="menu-management__content"> <n-layout-content class="menu-management__content">
<n-spin :show="loading"> <n-spin :show="loading">
<n-tree <n-tree :data="menuTree" key-field="uuid" label-field="label" children-field="children"
block-line :expanded-keys="expandedKeys" :render-label="renderTreeLabel" :render-switcher-icon="renderSwitcherIcon" />
:data="menuTree"
:render-label="renderTreeLabel"
:render-switcher-icon="renderSwitcherIcon"
:expanded-keys="expandedKeys"
@update:expanded-keys="handleExpand"
/>
</n-spin> </n-spin>
</n-layout-content> </n-layout-content>
<!-- 父级菜单表单弹窗 --> <!-- 父级菜单表单弹窗 -->
<n-modal v-model:show="showParentModal" preset="dialog" :title="parentModalTitle"> <n-modal v-model:show="showParentModal" preset="dialog" style="width:640px" :title="parentModalTitle">
<n-form <n-card style="width: 600px;">
ref="parentFormRef" <n-form ref="parentFormRef" :model="parentForm" :rules="parentRules" label-placement="left" label-width="auto"
:model="parentForm" require-mark-placement="right-hanging">
:rules="parentRules" <n-form-item label="菜单名称" path="label">
label-placement="left" <n-input v-model:value="parentForm.label" placeholder="请输入菜单名称" />
label-width="auto" </n-form-item>
require-mark-placement="right-hanging" <n-form-item label="菜单路径" path="path">
> <n-input v-model:value="parentForm.path" placeholder="请输入菜单路径" />
<n-form-item label="菜单名称" path="label"> </n-form-item>
<n-input v-model:value="parentForm.label" placeholder="请输入菜单名称" /> <n-form-item label="菜单编码" path="menuCode">
</n-form-item> <n-input v-model:value="parentForm.menuCode" placeholder="请输入菜单编码" />
<n-form-item label="菜单路径" path="path"> </n-form-item>
<n-input v-model:value="parentForm.path" placeholder="请输入菜单路径" /> <n-form-item label="图标" path="icon">
</n-form-item> <n-select v-model:value="parentForm.icon" :options="iconOptions" filterable clearable placeholder="请选择图标名称"
<n-form-item label="菜单编码" path="menuCode"> :render-label="renderLabel" />
<n-input v-model:value="parentForm.menuCode" placeholder="请输入菜单编码" /> </n-form-item>
</n-form-item> <n-form-item label="组件路径" path="component">
<n-form-item label="图标" path="icon"> <template #label>
<n-input v-model:value="parentForm.icon" placeholder="请输入图标名称" /> 组件路径
</n-form-item> <n-tooltip trigger="hover">
<n-form-item label="组件路径" path="component"> <template #trigger>
<n-input v-model:value="parentForm.component" placeholder="请输入组件路径" /> <n-icon size="16">
</n-form-item> <HelpCircle />
<n-form-item label="适配方式" path="adaptability"> </n-icon>
<n-select </template>
v-model:value="parentForm.adaptability" /src/views//src/views/home/index.vuehome/index.vue
:options="adaptabilityOptions" </n-tooltip>
placeholder="请选择适配方式" </template>
/> <n-input-group>
</n-form-item> <n-input :style="{ width: '60%' }" v-model:value="parentForm.component" placeholder="请输入组件路径"
<n-form-item label="排序" path="sort"> :disabled="menuType === 'list'" />
<n-input-number v-model:value="parentForm.sort" :min="0" /> <n-radio-group :style="{ width: '40%' }" v-model:value="menuType">
</n-form-item> <n-radio-button label="菜单" value="menu" />
<n-form-item label="状态" path="status"> <n-radio-button label="目录" value="list"></n-radio-button>
<n-radio-group v-model:value="parentForm.status"> </n-radio-group>
<n-space> </n-input-group>
<n-radio value="enabled">启用</n-radio> </n-form-item>
<n-radio value="disabled">禁用</n-radio> <n-form-item label="适配方式" path="adaptability">
</n-space> <n-select v-model:value="parentForm.adaptability" :options="adaptabilityOptions" placeholder="请选择适配方式" />
</n-radio-group> </n-form-item>
</n-form-item> <n-form-item label="排序" path="sort">
<n-form-item label="查询参数" path="query"> <n-input-number v-model:value="parentForm.sort" :min="0" />
<n-input </n-form-item>
v-model:value="parentForm.query" <n-form-item label="状态" path="status">
type="textarea" <n-radio-group v-model:value="parentForm.status">
placeholder="请输入查询参数(JSON格式)" <n-space>
/> <n-radio value="enable">启用</n-radio>
</n-form-item> <n-radio value="disabled">禁用</n-radio>
</n-form> </n-space>
</n-radio-group>
</n-form-item>
<n-form-item label="查询参数" path="query">
<n-input v-model:value="parentForm.query" type="textarea" placeholder="请输入查询参数(JSON格式)" />
</n-form-item>
</n-form>
</n-card>
<template #action> <template #action>
<n-space justify="end"> <n-space justify="end">
<n-button @click="showParentModal = false">取消</n-button> <n-button @click="showParentModal = false">取消</n-button>
@ -85,64 +86,65 @@
</n-modal> </n-modal>
<!-- 子级菜单表单弹窗 --> <!-- 子级菜单表单弹窗 -->
<n-modal v-model:show="showChildModal" preset="dialog" :title="childModalTitle"> <n-modal v-model:show="showChildModal" preset="dialog" :title="childModalTitle" style="width:680px">
<n-form <n-card style="width: 640px;">
ref="childFormRef" <n-form ref="childFormRef" :model="childForm" :rules="childRules" label-placement="left" label-width="auto"
:model="childForm" require-mark-placement="right-hanging">
:rules="childRules" <n-form-item label="父级菜单" path="parentId">
label-placement="left" <n-select v-model:value="childForm.parentId" :options="parentMenuOptions" placeholder="请选择父级菜单"
label-width="auto" :disabled="isEditChild" />
require-mark-placement="right-hanging" </n-form-item>
> <n-form-item label="菜单名称" path="label">
<n-form-item label="父级菜单" path="parentId"> <n-input v-model:value="childForm.label" placeholder="请输入菜单名称" />
<n-select </n-form-item>
v-model:value="childForm.parentId" <n-form-item label="菜单路径" path="path">
:options="parentMenuOptions" <n-input v-model:value="childForm.path" placeholder="请输入菜单路径" />
placeholder="请选择父级菜单" </n-form-item>
:disabled="isEditChild" <n-form-item label="菜单编码" path="menuCode">
/> <n-input v-model:value="childForm.menuCode" placeholder="请输入菜单编码" />
</n-form-item> </n-form-item>
<n-form-item label="菜单名称" path="label"> <n-form-item label="图标" path="icon">
<n-input v-model:value="childForm.label" placeholder="请输入菜单名称" /> <n-select v-model:value="childForm.icon" :options="iconOptions" filterable clearable placeholder="请选择图标名称"
</n-form-item> :render-label="renderLabel" />
<n-form-item label="菜单路径" path="path"> </n-form-item>
<n-input v-model:value="childForm.path" placeholder="请输入菜单路径" /> <n-form-item path="component">
</n-form-item> <template #label>
<n-form-item label="菜单编码" path="menuCode"> 组件路径
<n-input v-model:value="childForm.menuCode" placeholder="请输入菜单编码" /> <n-tooltip trigger="hover">
</n-form-item> <template #trigger>
<n-form-item label="图标" path="icon"> <n-icon size="16">
<n-input v-model:value="childForm.icon" placeholder="请输入图标名称" /> <HelpCircle />
</n-form-item> </n-icon>
<n-form-item label="组件路径" path="component"> </template>
<n-input v-model:value="childForm.component" placeholder="请输入组件路径" /> /src/views//src/views/home/index.vuehome/index.vue
</n-form-item> </n-tooltip>
<n-form-item label="适配方式" path="adaptability"> </template>
<n-select <n-input :style="{ width: '60%' }" v-model:value="childForm.component" placeholder="请输入组件路径"
v-model:value="childForm.adaptability" :disabled="menuType === 'list'" />
:options="adaptabilityOptions" <n-radio-group :style="{ width: '40%' }" v-model:value="menuType">
placeholder="请选择适配方式" <n-radio-button label="菜单" value="menu" />
/> <n-radio-button label="目录" value="list"></n-radio-button>
</n-form-item> </n-radio-group>
<n-form-item label="排序" path="sort"> </n-form-item>
<n-input-number v-model:value="childForm.sort" :min="0" /> <n-form-item label="适配方式" path="adaptability">
</n-form-item> <n-select v-model:value="childForm.adaptability" :options="adaptabilityOptions" placeholder="请选择适配方式" />
<n-form-item label="状态" path="status"> </n-form-item>
<n-radio-group v-model:value="childForm.status"> <n-form-item label="排序" path="sort">
<n-space> <n-input-number v-model:value="childForm.sort" :min="0" />
<n-radio value="enabled">启用</n-radio> </n-form-item>
<n-radio value="disabled">禁用</n-radio> <n-form-item label="状态" path="status">
</n-space> <n-radio-group v-model:value="childForm.status">
</n-radio-group> <n-space>
</n-form-item> <n-radio value="enable">启用</n-radio>
<n-form-item label="查询参数" path="query"> <n-radio value="disabled">禁用</n-radio>
<n-input </n-space>
v-model:value="childForm.query" </n-radio-group>
type="textarea" </n-form-item>
placeholder="请输入查询参数(JSON格式)" <n-form-item label="查询参数" path="query">
/> <n-input v-model:value="childForm.query" type="textarea" placeholder="请输入查询参数(JSON格式)" />
</n-form-item> </n-form-item>
</n-form> </n-form>
</n-card>
<template #action> <template #action>
<n-space justify="end"> <n-space justify="end">
<n-button @click="showChildModal = false">取消</n-button> <n-button @click="showChildModal = false">取消</n-button>
@ -154,8 +156,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// @vicons/ionicons5 import { Add, Create, Trash, Document, FolderOpen, Folder, HelpCircle } from '@vicons/ionicons5'
import { Add, Create, Trash, Document, FolderOpen, Folder } from '@vicons/ionicons5'
import { import {
addParentMenu, addParentMenu,
editParentMenu, editParentMenu,
@ -167,7 +168,8 @@ import {
type MenuNode, type MenuNode,
type RawMenu type RawMenu
} from '@/api/menu' } from '@/api/menu'
import { FormInst, FormRules, NButton, NIcon, NSpace, SelectOption, TreeOption } from 'naive-ui' import { FormInst, FormRules, NButton, NIcon, NSpace, SelectOption, TreeOption, useMessage } from 'naive-ui'
import SvgIcon from '@/components/SvgIcon.vue'
// //
const loading = ref(false) const loading = ref(false)
@ -175,8 +177,9 @@ const menuTree = ref<MenuTree[]>([])
const expandedKeys = ref<string[]>([]) const expandedKeys = ref<string[]>([])
// //
const showParentModal = ref(false) const showParentModal = ref(false);
const parentFormRef = ref<FormInst | null>(null) const parentFormRef = ref<FormInst | null>(null);
const menuType = ref<'menu' | 'list'>('menu'); //
const parentForm = ref<Omit<RawMenu, 'uuid' | 'parentId'>>({ const parentForm = ref<Omit<RawMenu, 'uuid' | 'parentId'>>({
path: '', path: '',
label: '', label: '',
@ -185,17 +188,61 @@ const parentForm = ref<Omit<RawMenu, 'uuid' | 'parentId'>>({
adaptability: 'pc', adaptability: 'pc',
component: '', component: '',
sort: 0, sort: 0,
status: 'enabled', status: 'enable',
query: '' query: ''
}) })
const parentRules: FormRules = { const parentRules: FormRules = {
label: { required: true, message: '请输入菜单名称', trigger: 'blur' }, label: { required: true, message: '请输入菜单名称', trigger: 'blur' },
path: { required: true, message: '请输入菜单路径', trigger: 'blur' }, path: { required: true, message: '请输入菜单路径', trigger: 'blur' },
menuCode: { required: true, message: '请输入菜单编码', trigger: 'blur' } component: { required: true, message: '请输入组件路径', trigger: 'blur' },
} }
const isEditParent = ref(false) const isEditParent = ref(false)
const currentParentId = ref('') const currentParentId = ref('')
const message = useMessage();
// ICON
//
interface IconOption extends SelectOption {
value: string
label: string
icon: string
}
const svgIcons = ref<string[]>([])
// SVG
const loadSvgIcons = async () => {
try {
// 使 import.meta.glob SVG
const svgModules = import.meta.glob('/src/assets/icons/*.svg', { eager: true })
//
svgIcons.value = Object.keys(svgModules).map(path => {
const fileName = path.split('/').pop() || ''
return fileName.replace('.svg', '')
})
} catch (error) {
console.error('加载图标失败:', error)
svgIcons.value = []
}
}
//
const iconOptions = computed<IconOption[]>(() => {
return svgIcons.value.map(icon => ({
value: icon,
label: icon,
icon: icon
}))
})
//
const renderLabel = (option: IconOption) => {
return h('div', { class: 'flex-content between' }, [
h('span', { class: 'mr-2' }, option.label),
h(SvgIcon, {
'icon-class': option.icon,
width: '16',
height: '16',
color: '#4090EF'
})
])
}
// //
const showChildModal = ref(false) const showChildModal = ref(false)
const childFormRef = ref<FormInst | null>(null) const childFormRef = ref<FormInst | null>(null)
@ -208,14 +255,14 @@ const childForm = ref<Omit<RawMenu, 'uuid'>>({
adaptability: 'pc', adaptability: 'pc',
component: '', component: '',
sort: 0, sort: 0,
status: 'enabled', status: 'enable',
query: '' query: ''
}) })
const childRules: FormRules = { const childRules: FormRules = {
parentId: { required: true, message: '请选择父级菜单', trigger: 'blur' }, parentId: { required: true, message: '请选择父级菜单', trigger: 'blur' },
label: { required: true, message: '请输入菜单名称', trigger: 'blur' }, label: { required: true, message: '请输入菜单名称', trigger: 'blur' },
path: { required: true, message: '请输入菜单路径', trigger: 'blur' }, path: { required: true, message: '请输入菜单路径', trigger: 'blur' },
menuCode: { required: true, message: '请输入菜单编码', trigger: 'blur' } component: { required: true, message: '请输入组件路径', trigger: 'blur' },
} }
const isEditChild = ref(false) const isEditChild = ref(false)
const currentChildId = ref('') const currentChildId = ref('')
@ -239,18 +286,42 @@ const parentMenuOptions = computed<SelectOption[]>(() => {
})) }))
}) })
const { stop } = watch(menuType, (newVal) => {
if (newVal === 'list') {
parentForm.value.component = 'view-router';
childForm.value.component = 'view-router';
} else {
parentForm.value.component = '';
childForm.value.component = '';
}
})
// //
onMounted(() => { onMounted(() => {
fetchMenuData() loadSvgIcons();
fetchMenuData();
});
onBeforeUnmount(() => {
stop();
}) })
// const transformMenuData = (menuData: MenuTree[]): TreeOption[] => {
return menuData.map(item => ({
key: item.uuid,
label: item.label,
icon: item.icon,
children: item.children ? transformMenuData(item.children) : undefined,
isLeaf: !item.children || item.children.length === 0,
rawData: item //
}))
}
//
const fetchMenuData = async () => { const fetchMenuData = async () => {
try { try {
loading.value = true loading.value = true
const res = await getAllMenu() const res = await getAllMenu()
menuTree.value = res menuTree.value = transformMenuData(res) //
//
expandedKeys.value = getAllKeys(res) expandedKeys.value = getAllKeys(res)
} catch (error) { } catch (error) {
console.error('获取菜单数据失败:', error) console.error('获取菜单数据失败:', error)
@ -270,10 +341,6 @@ const getAllKeys = (tree: MenuTree[]): string[] => {
return keys return keys
} }
const handleExpand = (keys: string[]) => {
expandedKeys.value = keys
}
const handleAddParent = () => { const handleAddParent = () => {
isEditParent.value = false isEditParent.value = false
parentForm.value = { parentForm.value = {
@ -284,10 +351,11 @@ const handleAddParent = () => {
adaptability: 'pc', adaptability: 'pc',
component: '', component: '',
sort: 0, sort: 0,
status: 'enabled', status: 'enable',
query: '' query: ''
} }
showParentModal.value = true menuType.value = 'menu';
showParentModal.value = true;
} }
const handleEditParent = (menu: MenuTree) => { const handleEditParent = (menu: MenuTree) => {
@ -304,6 +372,7 @@ const handleEditParent = (menu: MenuTree) => {
status: menu.status, status: menu.status,
query: menu.query query: menu.query
} }
menuType.value = menu.component === 'view-router' ? 'list' : 'menu';
showParentModal.value = true showParentModal.value = true
} }
@ -322,7 +391,7 @@ const handleSubmitParent = () => {
showParentModal.value = false showParentModal.value = false
await fetchMenuData() await fetchMenuData()
} catch (error) { } catch (error) {
console.error('操作失败:', error) message.error(error.message || '操作失败!');
} }
} }
}) })
@ -339,9 +408,10 @@ const handleAddChild = (parentId: string) => {
adaptability: 'pc', adaptability: 'pc',
component: '', component: '',
sort: 0, sort: 0,
status: 'enabled', status: 'enable',
query: '' query: ''
} }
menuType.value = 'menu';
showChildModal.value = true showChildModal.value = true
} }
@ -360,6 +430,7 @@ const handleEditChild = (menu: MenuNode) => {
status: menu.status, status: menu.status,
query: menu.query query: menu.query
} }
menuType.value = menu.component === 'view-router' ? 'list' : 'menu';
showChildModal.value = true showChildModal.value = true
} }
@ -394,22 +465,22 @@ const handleDelete = async (uuid: string) => {
} }
const renderTreeLabel = ({ option }: { option: TreeOption }) => { const renderTreeLabel = ({ option }: { option: TreeOption }) => {
const menu = option as unknown as MenuTree | MenuNode const menu = option.rawData as MenuNode | MenuTree
return h('div', { class: 'menu-management__tree-node' }, [ return h('div', { class: 'menu-management__tree-node' }, [
h('span', { class: 'menu-management__tree-label' }, menu.label), h('span', { class: 'menu-management__tree-label' }, menu.label),
h(NSpace, { class: 'menu-management__tree-actions' }, { h(NSpace, { class: 'menu-management__tree-actions' }, {
default: () => [ default: () => [
!option.isLeaf !option.isLeaf
? h(NButton, { ? h(NButton, {
size: 'tiny', size: 'tiny',
tertiary: true, tertiary: true,
onClick: (e: MouseEvent) => { onClick: (e: MouseEvent) => {
e.stopPropagation() e.stopPropagation()
handleAddChild(menu.uuid) handleAddChild(menu.uuid)
} }
}, { }, {
icon: () => h(NIcon, { size: 14 }, () => h(Add)) icon: () => h(NIcon, { size: 14 }, () => h(Add))
}) })
: null, : null,
h(NButton, { h(NButton, {
size: 'tiny', size: 'tiny',
@ -443,10 +514,10 @@ const renderTreeLabel = ({ option }: { option: TreeOption }) => {
// //
const renderSwitcherIcon = ({ expanded, option }: { expanded: boolean; option: TreeOption }) => { const renderSwitcherIcon = ({ expanded, option }: { expanded: boolean; option: TreeOption }) => {
return h(NIcon, { size: 16 }, () => return h(NIcon, { size: 16 }, () =>
option.isLeaf option.isLeaf
? h(Document) ? h(Document)
: expanded : expanded
? h(FolderOpen) ? h(FolderOpen)
: h(Folder) : h(Folder)
) )
@ -460,8 +531,17 @@ const renderSwitcherIcon = ({ expanded, option }: { expanded: boolean; option: T
flex-direction: column; flex-direction: column;
&__header { &__header {
padding: 16px 24px; display: flex;
align-items: center;
justify-content: space-between;
background-color: var(--n-color); background-color: var(--n-color);
padding: $normolGap;
.title {
font-size: 18px;
font-weight: bold;
color: $titleTextColor;
}
} }
&__content { &__content {