灌篮少年 发表于 2024-12-10 19:20:24

(系列十四)Vue3+WebApi 搭建动态菜单

阐明

    该文章是属于OverallAuth2.0系列文章,每周更新一篇该系列文章(从0到1完成体系开发)。
    该体系文章,我会尽量说的非常具体,做到不管新手、老手都能看懂。
    阐明:OverallAuth2.0 是一个简单、易懂、功能强大的权限+可视化流程管理体系。
友情提醒:本篇文章是属于系列文章,看该文章前,建议先看之前文章,可以更好理解项目结构。
qq群:801913255,进群有什么不懂的只管问,群主都会耐心解答。
有爱好的朋侪,请关注我吧(*^▽^*)。
https://img2024.cnblogs.com/blog/1158526/202408/1158526-20240824140446786-404771438.png
关注我,学不会你来打我
问题修复
  阐明:不是跟着系列文章搭建体系的请自行忽略,往下看搭建动态菜单的过程。
  问题1:
https://img2024.cnblogs.com/blog/1158526/202412/1158526-20241204131959863-1859974101.png
修改代码如下
路径:framework->index.vue
https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gifhttps://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif<el-main>
          <el-affix :offset="60">
            <el-tabs
            v-if="tabsList.length > 0"
            v-model="defaultActive"
            class="demo-tabs"
            @click="tabsClick(defaultActive)"
            @tab-remove="tabRemoveClick"
            >
            <el-tab-pane
                v-for="item in tabsList"
                :label="item.name"
                :name="item.path"
                :key="item.path"
                :closable="item.path == '/panel' ? false : true"
                style="font-size: 16px"
            >
            </el-tab-pane>
            </el-tabs>
          </el-affix>
          <router-view></router-view>
      </el-main>View Code样式.demo-tabs中参加白色背景样式:background-color: white;
  问题2:
中国地图中,波纹会随着各省的数值变大而变大
https://img2024.cnblogs.com/blog/1158526/202412/1158526-20241204132755037-1894610160.png
路径:echarts.ts
把value: chinaGeoCoordMap.name].concat(.value])修改成value: chinaGeoCoordMap.name].concat(0)
实现功能
    把以下菜单换成动态菜单
  不是跟着系列走的朋侪,可直接观看动态路由菜单的关键代码 ,在后面的第三步中!!!
  注意:该篇文章是实现OverallAuth2.0 功能级权限之一,菜单权限的紧张篇幅。
https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gifhttps://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gifroutes.push(
{
    path: '/framework',
    component: Framework,
    name: "架构",

},
{
    path: '/login',
    component: Login,
    name: "登录页面",
},
{
    path: '/panel',
    redirect: '/panel/index',
    meta: { title: '工作空间' },
    name: "工作空间",
    component: Framework,
    children: [
      {
      path: '/panel',
      name: '工作台',
      component: () => import('../../views/panel/index.vue'),
      meta: { title: '工作台', requireAuth: true, affix: true, closable: false },
      }
    ]
},
{
    path: '/menu',
    redirect: '/menu/index',
    meta: { title: '菜单管理' },
    name: "菜单管理",
    component: Framework,
    children: [
      {
      path: '/menu',
      name: '菜单',
      component: () => import('../../views/menu/index.vue'),
      meta: { title: '菜单', requireAuth: true, affix: true, closable: false },
      }
    ]
},
{
    path: '/user',
    meta: { title: '用户管理' },
    name: "用户管理",
    component: Framework,
    children: [
      {
      path: '/user',
      name: '用户',
      component: () => import('../../views/user/index.vue'),
      meta: { title: '用户' },
      }]
},
)View Code创建数据库表
   根据菜单格式创建数据库表,并创建初始值。
https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gifhttps://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gifCREATE TABLE .(
    NOT NULL,
    (50) NOT NULL,
    (50) NOT NULL,
    (50) NOT NULL,
    (50) NOT NULL,
    (50) NULL,
    (50) NOT NULL,
    (500) NOT NULL,
    NOT NULL,
    NOT NULL,
    NOT NULL,
    (50) NOT NULL,
    NULL,
    (500) NULL,
CONSTRAINT PRIMARY KEY CLUSTERED
(
    ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON
) ON
GO
INSERT . (, , , , , , , , , , , , , ) VALUES (N'380ca40b-8b62-4ebe-86d7-91ae48292f43', N'0', N'6a75ec49-2093-4b89-950f-65e6e72746da', N'6e746ed0-12e9-4002-9887-d84a19142304', N'/panel', N'layui-icon-engine', N'工作空间', N'frameWork', 1, 1, CAST(N'2024-12-05T00:00:00.000' AS DateTime), N'1', 1, NULL)
INSERT . (, , , , , , , , , , , , , ) VALUES (N'380ca40b-8b62-4ebe-86d7-91ae48292f44', N'380ca40b-8b62-4ebe-86d7-91ae48292f43', N'6a75ec49-2093-4b89-950f-65e6e72746da', N'6e746ed0-12e9-4002-9887-d84a19142304', N'/panel', N'layui-icon-engine', N'工作台', N'../views/panel/index', 1, 1, CAST(N'2024-12-05T00:00:00.000' AS DateTime), N'1', 1, NULL)
INSERT . (, , , , , , , , , , , , , ) VALUES (N'380ca40b-8b62-4ebe-86d7-91ae48292f45', N'0', N'6a75ec49-2093-4b89-950f-65e6e72746da', N'6e746ed0-12e9-4002-9887-d84a19142304', N'/menu', N'layui-icon-engine', N'菜单管理', N'frameWork', 1, 1, CAST(N'2024-12-05T00:00:00.000' AS DateTime), N'1', 1, NULL)
INSERT . (, , , , , , , , , , , , , ) VALUES (N'380ca40b-8b62-4ebe-86d7-91ae48292f46', N'380ca40b-8b62-4ebe-86d7-91ae48292f45', N'6a75ec49-2093-4b89-950f-65e6e72746da', N'6e746ed0-12e9-4002-9887-d84a19142304', N'/menu', N'layui-icon-engine', N'菜单', N'../views/menu/index', 1, 1, CAST(N'2024-12-05T00:00:00.000' AS DateTime), N'1', 1, NULL)
INSERT . (, , , , , , , , , , , , , ) VALUES (N'380ca40b-8b62-4ebe-86d7-91ae48292f47', N'0', N'6a75ec49-2093-4b89-950f-65e6e72746da', N'6e746ed0-12e9-4002-9887-d84a19142304', N'/user', N'layui-icon-engine', N'用户管理', N'frameWork', 1, 1, CAST(N'2024-12-05T00:00:00.000' AS DateTime), N'1', 1, NULL)
INSERT . (, , , , , , , , , , , , , ) VALUES (N'380ca40b-8b62-4ebe-86d7-91ae48292f48', N'380ca40b-8b62-4ebe-86d7-91ae48292f47', N'6a75ec49-2093-4b89-950f-65e6e72746da', N'6e746ed0-12e9-4002-9887-d84a19142304', N'/user', N'layui-icon-engine', N'用户', N'../views/user/index', 1, 1, CAST(N'2024-12-05T00:00:00.000' AS DateTime), N'1', 1, NULL)
ALTER TABLE . ADDCONSTRAINT DEFAULT ((0)) FOR
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'菜单id' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'Sys_Menu', @level2type=N'COLUMN',@level2name=N'Id'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'父级id' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'Sys_Menu', @level2type=N'COLUMN',@level2name=N'Pid'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'公司Key' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'Sys_Menu', @level2type=N'COLUMN',@level2name=N'CorporationKey'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'系统Key' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'Sys_Menu', @level2type=N'COLUMN',@level2name=N'SystemKey'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'菜单路径' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'Sys_Menu', @level2type=N'COLUMN',@level2name=N'MenuUrl'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'菜单图标' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'Sys_Menu', @level2type=N'COLUMN',@level2name=N'MenuIcon'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'菜单标题' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'Sys_Menu', @level2type=N'COLUMN',@level2name=N'MenuTitle'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'排序' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'Sys_Menu', @level2type=N'COLUMN',@level2name=N'Sort'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'是否开启菜单' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'Sys_Menu', @level2type=N'COLUMN',@level2name=N'IsOpen'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'创建时间' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'Sys_Menu', @level2type=N'COLUMN',@level2name=N'CreateTime'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'创建人员' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'Sys_Menu', @level2type=N'COLUMN',@level2name=N'CreateUser'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'是否验证' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'Sys_Menu', @level2type=N'COLUMN',@level2name=N'RequireAuth'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'重定向' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'Sys_Menu', @level2type=N'COLUMN',@level2name=N'Redirect'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'菜单表' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'Sys_Menu'
GOView Code编写后端代码
  注意:底层仓储我已经搭建好,只需要实现菜单查询的业务逻辑即可,假如需要了解底层仓储的,请查看《dapper搭建底层仓储》
  1:创建模子(我的创建路径:Model->DomainModel->Sys)
https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gifhttps://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif/// <summary>
/// 菜单表模型
/// </summary>
public class SysMenu
{
    /// <summary>
    /// 菜单主键
    /// </summary>
    public Guid Id { get; set; }

    /// <summary>
    /// 上级菜单
    /// </summary>
    public string? Pid { get; set; }

    /// <summary>
    /// 公司key
    /// </summary>
    public string? CorporationKey { get; set; }

    /// <summary>
    /// 系统Key
    /// </summary>
    public string? SystemKey { get; set; }

    /// <summary>
    /// 菜单路径
    /// </summary>
    public string? MenuUrl { get; set; }

    /// <summary>
    /// 菜单图标
    /// </summary>
    public string? MenuIcon { get; set; }

    /// <summary>
    /// 菜单标题
    /// </summary>
    public string? MenuTitle { get; set; }

    /// <summary>
    /// 菜单模板
    /// </summary>
    public string? Component { get; set; }

    /// <summary>
    /// 是否开启
    /// </summary>
    public bool IsOpen { get; set; }

    /// <summary>
    /// 排序
    /// </summary>
    public int Sort { get; set; }

    /// <summary>
    /// 创建时间
    /// </summary>
    public DateTime CreateTime { get; set; }

    /// <summary>
    /// 创建人员
    /// </summary>
    public string? CreateUser { get; set; }

    /// <summary>
    /// 是否验证
    /// </summary>
    public bool RequireAuth { get; set; }

    /// <summary>
    /// 重定向目录
    /// </summary>
    public string? Redirect { get; set; }
}View Code  2:创建菜单表的仓储(我的创建路径:Infrastructure->IRepository和Repository->Sys)

/// <summary>
/// 系统菜单仓储接口
/// </summary>
public interface ISysMenuRepository : IRepository<SysMenu>
{
}/// <summary>
/// 系统菜单仓储接口实现
/// </summary>
public class SysMenuRepository : Repository<SysMenu>, ISysMenuRepository
{
}  3:创建领域服务(我的创建路径:DomainService->IService和Service->Sys)
/// <summary>
/// 菜单服务接口
/// </summary>
public interface ISysMenuService
{
    /// <summary>
    /// 获取树形菜单
    /// </summary>
    /// <returns></returns>
    List<SysMenuOutPut> GetMenuTreeList();
} /// <summary>
/// 菜单服务实现
/// </summary>
public class SysMenuService : ISysMenuService
{
   #region 构造实例化

   /// <summary>
   /// 菜单仓储接口
   /// </summary>
   private readonly ISysMenuRepository _menuRepository;

   /// <summary>
   /// 构造函数
   /// </summary>
   /// <param name="menuRepository"></param>
   public SysMenuService(ISysMenuRepository menuRepository)
   {
         _menuRepository = menuRepository;
   }

   #endregion


   #region 业务逻辑

   /// <summary>
   /// 获取树形菜单
   /// </summary>
   /// <returns></returns>
   public List<SysMenuOutPut> GetMenuTreeList()
   {
          return new List<SysMenuOutPut>();
   }

   #endregion
}  4:递归获取菜单,呈现上下级关系
  这块主要是把数据库取出的数据,转换成前端能识别的树形结构。要实现该功能,先要建立一个支持树形结构的输出模子(Model->BusinessModel->OutPut)。
https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gifhttps://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif/// <summary>
/// 菜单输出模型
/// </summary>
public class SysMenuOutPut
{
    /// <summary>
    /// 菜单主键
    /// </summary>
    public Guid Id { get; set; }

    /// <summary>
    /// 上级菜单
    /// </summary>
    public string? Pid { get; set; }

    /// <summary>
    /// 公司key
    /// </summary>
    public string? CorporationKey { get; set; }

    /// <summary>
    /// 系统Key
    /// </summary>
    public string? SystemKey { get; set; }

    /// <summary>
    /// 菜单路径
    /// </summary>
    public string? Path { get; set; }

    /// <summary>
    /// 菜单图标
    /// </summary>
    public string? MenuIcon { get; set; }

    /// <summary>
    /// 菜单标题
    /// </summary>
    public string? Name { get; set; }

    /// <summary>
    /// 菜单模板
    /// </summary>
    public string? Component { get; set; }

    /// <summary>
    /// 是否开启
    /// </summary>
    public bool IsOpen { get; set; }

    /// <summary>
    /// 排序
    /// </summary>
    public int Sort { get; set; }

    /// <summary>
    /// 创建时间
    /// </summary>
    public DateTime CreateTime { get; set; }

    /// <summary>
    /// 创建人员
    /// </summary>
    public string? CreateUser { get; set; }

    /// <summary>
    /// 是否验证
    /// </summary>
    public bool RequireAuth { get; set; }

    /// <summary>
    /// 重定向目录
    /// </summary>
    public string? Redirect { get; set; }

    /// <summary>
    /// 子节点
    /// </summary>
    public List<SysMenuOutPut>? Children { get; set; }
}View Code  然后我们要创建一个菜单的核心操作类,以便体系后续的使用(CoreDomain->BusinessCore)
https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gifhttps://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif/// <summary>
/// 菜单核心
/// </summary>
public static class MenuCore
{
    /// <summary>
    /// 递归获取菜单,组成树形结构
    /// </summary>
    /// <param name="menuList">菜单数据</param>
    /// <returns>返回菜单的树形结构</returns>
    public static List<SysMenuOutPut> GetMenuTreeList(List<SysMenu> menuList)
    {
      List<SysMenuOutPut> list = new();
      List<SysMenuOutPut> menuListDto = new();
      //模型的转换
      foreach (var item in menuList)
      {
            SysMenuOutPut model = new()
            {
                Id = item.Id,
                Pid = item.Pid,
                CorporationKey = item.CorporationKey,
                SystemKey = item.SystemKey,
                Path = item.MenuUrl,
                Name = item.MenuTitle,
                MenuIcon = item.MenuIcon,
                Component = item.Component,
                IsOpen = item.IsOpen,
                Sort = item.Sort,
                RequireAuth = item.RequireAuth,
                Redirect = item.Redirect,
                CreateTime = item.CreateTime,
                CreateUser = item.CreateUser,
            };
            list.Add(model);
      }
      //递归所有父级菜单
      foreach (var data in list.Where(f => f.Pid == "0" && f.IsOpen))
      {
            var childrenList = GetChildrenMenu(list, data.Id).OrderBy(f => f.Sort).ToList();
            data.Children = childrenList.Count == 0 ? null : childrenList;
            menuListDto.Add(data);
      }
      return menuListDto;
    }

    /// <summary>
    /// 实现递归
    /// </summary>
    /// <param name="moduleOutput">菜单数据</param>
    /// <param name="id">菜单ID</param>
    /// <returns></returns>
    private static List<SysMenuOutPut> GetChildrenMenu(List<SysMenuOutPut> moduleOutput, Guid id)
    {
      List<SysMenuOutPut> sysShowTempMenus = new();
      //得到子菜单
      var info = moduleOutput.Where(w => w.Pid == id.ToString() && w.IsOpen).ToList();
      //循环
      foreach (var sysMenuInfo in info)
      {
            var childrenList = GetChildrenMenu(moduleOutput, sysMenuInfo.Id);
            //把子菜单放到Children集合里
            sysMenuInfo.Children = childrenList.Count == 0 ? null : childrenList;
            //添加父级菜单
            sysShowTempMenus.Add(sysMenuInfo);
      }
      return sysShowTempMenus;
    }
}View Code  在领域服务中实现接口GetMenuTreeList();
/// <summary>
/// 获取树形菜单
/// </summary>
/// <returns></returns>
public List<SysMenuOutPut> GetMenuTreeList()
{
    var menuList = _menuRepository.GetAll(BaseSqlRepository.sysMenu_selectAllSql);
    var menuTreeList = MenuCore.GetMenuTreeList(menuList);
    return menuTreeList;
}注意:BaseSqlRepository.sysMenu_selectAllSql 是查询sql的语句,我放在了一个底子sql仓储中,统一管理
  5:编写接口(Controllers->Sys)
https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gifhttps://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif /// <summary>
/// 系统模块
/// </summary>

/")]

public class SysMenuController : BaseController
{

   #region 构造实列化

   /// <summary>
   /// 菜单服务服务
   /// </summary>
   public ISysMenuService _sysMenuService;

   /// <summary>
   /// 构造函数
   /// </summary>
   /// <param name="sysMenuService"></param>
   public SysMenuController(ISysMenuService sysMenuService)
   {
         _sysMenuService = sysMenuService;
   }

   #endregion

   #region 菜单接口

   /// <summary>
   /// 获取树形菜单
   /// </summary>
   /// <returns></returns>
   
   public ReceiveStatus<SysMenuOutPut> GetMenuTreeList()
   {
         ReceiveStatus<SysMenuOutPut> receiveStatus = new();
         var list = _sysMenuService.GetMenuTreeList();
         receiveStatus.data = list;
         return receiveStatus;
   }

   #endregion

}View Code  6:测试接口
https://img2024.cnblogs.com/blog/1158526/202412/1158526-20241205162448522-2076632676.png
编写前端代码
  后端接口已经预备停当,那么接下来就要编写前端的代码,把静态的json数据转换成接口返回的动态数据。
 第一步:修改静态路由
把base-routes.ts文件中的路由换成
https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gifhttps://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gifroutes.push(
{
    path: '/framework',
    component: frameWork,
    name: "架构",

},
{
    path: '/login',
    component: Login,
    name: "登录页面",
},
{
    path: '/panel',
    redirect: '/panel/index',
    meta: { title: '工作空间' },
    name: "工作空间",
    component: frameWork,
    children: [
      {
      path: '/panel',
      name: '工作台',
      component: () => import('../../views/panel/index.vue'),
      meta: { title: '工作台', requireAuth: true, affix: true, closable: false },
      }
    ]
})View Code只留下固定的路由菜单,把菜单管理、用户管理等菜单去掉,我们做动态获取。
修改路由守卫如下
router.beforeEach((to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
NProgress.start();
const userStore = useUserStore();
const endTime = new Date(userStore.expiresDate);
const currentTime = new Date();
to.path = to.path;
if (to.meta.requireAuth && endTime < currentTime) {
    router.push('/login')
}
if (to.meta.requireAuth) {
    next();
} else if (to.matched.length == 0) {
    next({ path: '/panel' })
} else {
    next();
}
})这块对比前次的代码,是把next({ path: '/login' })换成了next({ path: '/panel' })。
第二步:添加接口
新建文件api->menu->index.ts
import Http from '../http';
export const getMenuTreeData = async function() {
    return await Http.get('/api/SysMenu/GetMenuTreeList');
}<br>该接口是上面我们编写的获取菜单接口
第三步:递归菜单,并动态添加到路由中
  在store->user.ts文件如下,添加如下代码
  该代码是把后端获取的树形菜单数据,转换成路由能认识的菜单。
const defineRouteComponents: Record<string, any> = {
frameWork: () => import('@/views/frameWork/index.vue')
};
const defineRouteComponentKeys = Object.keys(defineRouteComponents);
export const setMenuData = (
routeMap: any[],
) => {
return routeMap
    .map(item => {
      const pathArray = item.component.split('/');
      const url = ref<any>();
      if (pathArray.length > 0) {
      if (pathArray.length === 3)
          url.value = import(`../${pathArray}/${pathArray}.vue`);
      if (pathArray.length === 4)
          url.value = import(`../${pathArray}/${pathArray}/${pathArray}.vue`);
      if (pathArray.length === 5)
          url.value = import(`../${pathArray}/${pathArray}/${pathArray}/${pathArray}.vue`);
      };
      const { name, requireAuth, id } = item || {};
      const currentRouter: RouteRecordRaw = {
      // 如果路由设置了 path,则作为默认 path,否则 路由地址 动态拼接生成如 /dashboard/workplace
      path: item.path,
      // 路由名称,建议唯一
      //name: `${item.id}`,
      // meta: 页面标题, 菜单图标, 页面权限(供指令权限用,可去掉)
      meta: {
          name,
          requireAuth,
          id
      },
      name: item.name,
      children: [],
      // 该路由对应页面的 组件 (动态加载 @/views/ 下面的路径文件)
      component: item.component && defineRouteComponentKeys.includes(item.component)
          ? defineRouteComponents
          : () => url.value,

      };

      // 为了防止出现后端返回结果不规范,处理有可能出现拼接出两个 反斜杠
      if (!currentRouter.path.startsWith('http')) {
      currentRouter.path = currentRouter.path.replace('//', '/');
      }

      // 重定向
      item.redirect && (currentRouter.redirect = item.redirect);
      if (item.children != null) {
      // 子菜单,递归处理
      currentRouter.children = setMenuData(item.children);
      }
      if (currentRouter.children === undefined || currentRouter.children.length <= 0) {
      currentRouter.children;
      }
      return currentRouter;
    })
    .filter(item => item);
};然后在defineStore的actions中添加如下方法
actions: {
    //获取菜单数据,并递归实现动态路由菜单
    async loadMenus() {
      new Promise<any>(async (resolve, reject) => {
      const { data, code, msg } = await getMenuTreeData();
      if (code == 200) {
          this.menus = data;
          var menuList = setMenuData(data) as RouteRecordRaw[]
          menuList.map(d => {
            router.addRoute(d);
          })
          resolve(menuList);
      }
      else {
          this.menus = [];
          ElMessage({
            message: msg,
            type: "error",
          });
      }
      });
    },
},user.ts完备代码如下(动态路由菜单的核心)
https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gifhttps://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gifimport { getMenuTreeData } from '@/api/menu';import router from '@/router';import { ElMessage } from 'element-plus';import { defineStore } from 'pinia'import { ref } from 'vue';import { RouteRecordRaw } from 'vue-router';export const useUserStore = defineStore('user', {state: () => ({    token: '',    expiresDate: '',    userInfo: {},    menus: [] as any,}),actions: {
    //获取菜单数据,并递归实现动态路由菜单
    async loadMenus() {
      new Promise<any>(async (resolve, reject) => {
      const { data, code, msg } = await getMenuTreeData();
      if (code == 200) {
          this.menus = data;
          var menuList = setMenuData(data) as RouteRecordRaw[]
          menuList.map(d => {
            router.addRoute(d);
          })
          resolve(menuList);
      }
      else {
          this.menus = [];
          ElMessage({
            message: msg,
            type: "error",
          });
      }
      });
    },
},persist: {    enabled: true,    strategies: [      {      // 可以是localStorage或sessionStorage      storage: localStorage,      // 指定需要持久化的属性      paths: ['token', 'expiresDate', 'userInfo', 'menus']      }    ]},})const defineRouteComponents: Record<string, any> = {
frameWork: () => import('@/views/frameWork/index.vue')
};
const defineRouteComponentKeys = Object.keys(defineRouteComponents);
export const setMenuData = (
routeMap: any[],
) => {
return routeMap
    .map(item => {
      const pathArray = item.component.split('/');
      const url = ref<any>();
      if (pathArray.length > 0) {
      if (pathArray.length === 3)
          url.value = import(`../${pathArray}/${pathArray}.vue`);
      if (pathArray.length === 4)
          url.value = import(`../${pathArray}/${pathArray}/${pathArray}.vue`);
      if (pathArray.length === 5)
          url.value = import(`../${pathArray}/${pathArray}/${pathArray}/${pathArray}.vue`);
      };
      const { name, requireAuth, id } = item || {};
      const currentRouter: RouteRecordRaw = {
      // 如果路由设置了 path,则作为默认 path,否则 路由地址 动态拼接生成如 /dashboard/workplace
      path: item.path,
      // 路由名称,建议唯一
      //name: `${item.id}`,
      // meta: 页面标题, 菜单图标, 页面权限(供指令权限用,可去掉)
      meta: {
          name,
          requireAuth,
          id
      },
      name: item.name,
      children: [],
      // 该路由对应页面的 组件 (动态加载 @/views/ 下面的路径文件)
      component: item.component && defineRouteComponentKeys.includes(item.component)
          ? defineRouteComponents
          : () => url.value,

      };

      // 为了防止出现后端返回结果不规范,处理有可能出现拼接出两个 反斜杠
      if (!currentRouter.path.startsWith('http')) {
      currentRouter.path = currentRouter.path.replace('//', '/');
      }

      // 重定向
      item.redirect && (currentRouter.redirect = item.redirect);
      if (item.children != null) {
      // 子菜单,递归处理
      currentRouter.children = setMenuData(item.children);
      }
      if (currentRouter.children === undefined || currentRouter.children.length <= 0) {
      currentRouter.children;
      }
      return currentRouter;
    })
    .filter(item => item);
};View Code编写完以上代码,我们已经获取到后端的菜单,并且已添加到动态路由中。接下来只需要在登录时,调用loadMenus()方法即可。
第四步:登录后获取动态路由菜单
如图
https://img2024.cnblogs.com/blog/1158526/202412/1158526-20241210152649123-1840775499.png
第五步:传入token
  当你辛劳完成以上步骤后,你迫不及待的想查看下效果。但是体系给你泼了一盆冷水,提示接口401错误。
没错,会出现错误,因为我们体系使用了jwt鉴权,所以我们需要把token传给后端,然后举行验证。只有通过后才气访问接口。
那么要怎样做才气把token传给后端呢。
在我们之前写好的请求拦截中,参加如下代码
ps:不清楚请求拦截的,请观看(系列十一)Vue3框架中路由守卫及请求拦截(实现前后端交互)
/* 请求拦截 */
      this.service.interceptors.request.use((config: InternalAxiosRequestConfig) => {
            //可以在这里做请求拦截处理   如:请求接口前,需要传入的token
            const userInfoStore = useUserStore();
            if (userInfoStore.token) {
                (config.headers as AxiosRequestHeaders).token = userInfoStore.token as string
                config.headers["Authorization"] = "Bearer " + userInfoStore.token;
            } else {
                if (router.currentRoute.value.path !== '/login') {
                  router.push('/login');
                }
            }
            return config
      }, (error: any) => {
            ElMessage({
                message: "接口调用失败",
                type: "error",
            });
            return error.message;
            //return Promise.reject(error);
      })然后运行项目,你会发现,你的菜单实现了动态路由,全部由数据库获取。
以上就是本篇文章的全部内容,感谢耐心观看
后端WebApi 预览地址:http://139.155.137.144:8880/swagger/index.html
前端vue 预览地址:http://139.155.137.144:8881
关注公众号:发送【权限】,获取前后端代码
有爱好的朋侪,请关注我微信公众号吧(*^▽^*)。
https://img2024.cnblogs.com/blog/1158526/202408/1158526-20240824140446786-404771438.png
关注我:一个全栈多端的宝藏博主,定时分享技能文章,不定时分享开源项目。关注我,带你认识不一样的程序世界

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: (系列十四)Vue3+WebApi 搭建动态菜单