(系列十四)Vue3+WebApi 搭建動態選單

陈逸子风發表於2024-12-10

說明

該文章是屬於OverallAuth2.0系列文章,每週更新一篇該系列文章(從0到1完成系統開發)。

該系統文章,我會盡量說的非常詳細,做到不管新手、老手都能看懂。

說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+視覺化流程管理系統。

友情提醒:本篇文章是屬於系列文章,看該文章前,建議先看之前文章,可以更好理解專案結構。

qq群:801913255,進群有什麼不懂的儘管問,群主都會耐心解答。

有興趣的朋友,請關注我吧(*^▽^*)。

關注我,學不會你來打我

問題修復

  說明:不是跟著系列文章搭建系統的請自行忽略,往下看搭建動態選單的過程。

  問題1:

修改程式碼如下

路徑:framework->index.vue

(系列十四)Vue3+WebApi 搭建動態選單
<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:

中國地圖中,波紋會隨著各省的數值變大而變大

路徑:echarts.ts

把value: chinaGeoCoordMap[dataItem[0].name].concat([dataItem[0].value])修改成value: chinaGeoCoordMap[dataItem[0].name].concat(0)

實現功能

  把以下選單換成動態選單

  不是跟著系列走的朋友,可直接觀看動態路由選單的關鍵程式碼 ,在後面的第三步中!!!

  注意:該篇文章是實現OverallAuth2.0 功能級許可權之一,選單許可權的重要篇幅。

(系列十四)Vue3+WebApi 搭建動態選單
routes.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

建立資料庫表

  根據選單格式建立資料庫表,並建立初始值。

(系列十四)Vue3+WebApi 搭建動態選單
CREATE TABLE [dbo].[Sys_Menu](
    [Id] [uniqueidentifier] NOT NULL,
    [Pid] [varchar](50) NOT NULL,
    [CorporationKey] [varchar](50) NOT NULL,
    [SystemKey] [varchar](50) NOT NULL,
    [MenuUrl] [varchar](50) NOT NULL,
    [MenuIcon] [varchar](50) NULL,
    [MenuTitle] [nvarchar](50) NOT NULL,
    [Component] [varchar](500) NOT NULL,
    [Sort] [int] NOT NULL,
    [IsOpen] [bit] NOT NULL,
    [CreateTime] [datetime] NOT NULL,
    [CreateUser] [varchar](50) NOT NULL,
    [RequireAuth] [bit] NULL,
    [Redirect] [varchar](500) NULL,
 CONSTRAINT [PK_Menu] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
INSERT [dbo].[Sys_Menu] ([Id], [Pid], [CorporationKey], [SystemKey], [MenuUrl], [MenuIcon], [MenuTitle], [Component], [Sort], [IsOpen], [CreateTime], [CreateUser], [RequireAuth], [Redirect]) 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 [dbo].[Sys_Menu] ([Id], [Pid], [CorporationKey], [SystemKey], [MenuUrl], [MenuIcon], [MenuTitle], [Component], [Sort], [IsOpen], [CreateTime], [CreateUser], [RequireAuth], [Redirect]) 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 [dbo].[Sys_Menu] ([Id], [Pid], [CorporationKey], [SystemKey], [MenuUrl], [MenuIcon], [MenuTitle], [Component], [Sort], [IsOpen], [CreateTime], [CreateUser], [RequireAuth], [Redirect]) 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 [dbo].[Sys_Menu] ([Id], [Pid], [CorporationKey], [SystemKey], [MenuUrl], [MenuIcon], [MenuTitle], [Component], [Sort], [IsOpen], [CreateTime], [CreateUser], [RequireAuth], [Redirect]) 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 [dbo].[Sys_Menu] ([Id], [Pid], [CorporationKey], [SystemKey], [MenuUrl], [MenuIcon], [MenuTitle], [Component], [Sort], [IsOpen], [CreateTime], [CreateUser], [RequireAuth], [Redirect]) 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 [dbo].[Sys_Menu] ([Id], [Pid], [CorporationKey], [SystemKey], [MenuUrl], [MenuIcon], [MenuTitle], [Component], [Sort], [IsOpen], [CreateTime], [CreateUser], [RequireAuth], [Redirect]) 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 [dbo].[Sys_Menu] ADD  CONSTRAINT [DF_Sys_Menu_Sort]  DEFAULT ((0)) FOR [Sort]
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'
GO
View Code

編寫後端程式碼

  注意:底層倉儲我已經搭建好,只需要實現選單查詢的業務邏輯即可,如果需要了解底層倉儲的,請檢視《dapper搭建底層倉儲

  1:建立模型(我的建立路徑:Model->DomainModel->Sys)

(系列十四)Vue3+WebApi 搭建動態選單
/// <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)。

(系列十四)Vue3+WebApi 搭建動態選單
/// <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)

(系列十四)Vue3+WebApi 搭建動態選單
/// <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)

(系列十四)Vue3+WebApi 搭建動態選單
 /// <summary>
 /// 系統模組
 /// </summary>
 [ApiController]
 [Route("api/[controller]/[action]")]
 [ApiExplorerSettings(GroupName = nameof(ModeuleGroupEnum.SysMenu))]
 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>
     [HttpGet]
     public ReceiveStatus<SysMenuOutPut> GetMenuTreeList()
     {
         ReceiveStatus<SysMenuOutPut> receiveStatus = new();
         var list = _sysMenuService.GetMenuTreeList();
         receiveStatus.data = list;
         return receiveStatus;
     }

     #endregion

 }
View Code

  6:測試介面

編寫前端程式碼

  後端介面已經準備就緒,那麼接下來就要編寫前端的程式碼,把靜態的json資料轉換成介面返回的動態資料。

第一步:修改靜態路由

把base-routes.ts檔案中的路由換成

(系列十四)Vue3+WebApi 搭建動態選單
routes.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');
 }

該介面是上面我們編寫的獲取選單介面

第三步:遞迴選單,並動態新增到路由中

  在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[1]}/${pathArray[2]}.vue`);
        if (pathArray.length === 4)
          url.value = import(`../${pathArray[1]}/${pathArray[2]}/${pathArray[3]}.vue`);
        if (pathArray.length === 5)
          url.value = import(`../${pathArray[1]}/${pathArray[2]}/${pathArray[3]}/${pathArray[4]}.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[item.component]
          : () => 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完整程式碼如下(動態路由選單的核心)

(系列十四)Vue3+WebApi 搭建動態選單
import { 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[1]}/${pathArray[2]}.vue`);
        if (pathArray.length === 4)
          url.value = import(`../${pathArray[1]}/${pathArray[2]}/${pathArray[3]}.vue`);
        if (pathArray.length === 5)
          url.value = import(`../${pathArray[1]}/${pathArray[2]}/${pathArray[3]}/${pathArray[4]}.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[item.component]
          : () => 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()方法即可。

第四步:登入後獲取動態路由選單

如圖

第五步:傳入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

關注公眾號:傳送【許可權】,獲取前後端程式碼

有興趣的朋友,請關注我微信公眾號吧(*^▽^*)。

關注我:一個全棧多端的寶藏博主,定時分享技術文章,不定時分享開源專案。關注我,帶你認識不一樣的程式世界

相關文章