在前面幾篇文章裡,提到這次專案選擇透過外購套版來處理 UI/UX,藉此省下不少介面設計與元件開發的時間。本篇就延續這個話題,來分享如何利用模板進行改造與引用。Day7 已經整理過套版的專案結構,因此只要依照分類,就能快速找到需要的元件或頁面。接下來,先談談這次實際操作套版的心得,專案檔案主要分成 dashboard 與 components 兩大區塊,並逐步說明我們如何在專案中加以應用。
dashboard:代表整體頁面,包含各種 page、form、table、chart 與 layout 排版設計範例。可依照上方對應的 Route 路徑,在 MainRoutes.ts 中找到原始的 Vue 檔案。
圖22-1:Dashboard 分類頁:可瀏覽頁面、表單、表格、圖表與版面配置等排版範例。
圖22-2:在 MainRoutes.ts
中查找對應的 Dashboard 範例路由設定。
components:著重於獨立元件的使用方式,提供如 Autocomplete、Button、Chip、Progress 等物件的使用與調用範例。
圖22-3:Components 分類總覽:Autocomplete、Button、Chip、Progress 等獨立元件示例。
圖22-4:在 ComponentRoutes.ts
中查找各元件範例的路由設定位置。
starter-kit
資料夾複製到專案中,接著啟動服務。從這個基礎環境開始,我們就能一步一步進行改造,逐漸把畫面調整成理想的樣子。本篇的第一步,先來實作左側的功能列。npm install // 首次執行就可以了
npm run dev
圖22-5:starter-kit
啟動後首頁;紅框為本篇要改造的左側功能清單區域。
在專案中找到左側功能列的模板 sidebarItem.ts
,並檢視其綁定的 interface
物件結構。
了解套版中左側功能列的物件結構與對應的模擬資料,先用來測試功能列的呈現效果,確保排版與互動符合預期。
圖22-6 : sidebarItem.ts
的功能列物件結構與模擬資料,用於先行驗證排版與互動。
將物件結構移至後端,由 backend API 負責處理。當使用者登入時,API 會依照身份與功能授權動態產生功能清單,並回傳給前端;前端再依據回應內容呈現功能列表。
//UserAccessDto.cs
namespace Core.Models.Dto
{
public class UserAccessDto
{
public List<Menu> Menu { get; set; } = new();// 新增這個屬性
}
public class Menu
{
public string? Header { get; set; } // 功能群組
public string? Title { get; set; } // 功能名稱
public string? Icon { get; set; } //使用的 Icon
public string? To { get; set; } // route 導頁 vue 標的
public bool? Divider { get; set; }
public string? Chip { get; set; }
public string? ChipColor { get; set; }
public string? ChipVariant { get; set; }
public string? ChipIcon { get; set; }
public List<Menu>? Children { get; set; }
public bool? Disabled { get; set; }
public string? Type { get; set; }
public string? SubCaption { get; set; }
}
}
PermissionController.cs
,作為處理功能授權與回應清單的控制器。//PermissionController.cs
using Core.Models.Dto;
using backendAPI.Services;
using Microsoft.AspNetCore.Authorization;
namespace backendAPI.Controllers
{
[Authorize] // 需有基本身分驗證
[ApiController]
[Route("api/[controller]")]
[EnableCors("AllowSpecificOrigin")]
public class PermissionController : ControllerBase
{
private readonly UserAccessService _userAccessService;
public PermissionController(UserAccessService userAccessService)
{
//服務取得登入帳號授權清單的service
_userAccessService = userAccessService ?? throw new ArgumentNullException(nameof(userAccessService))
}
[HttpGet]
[ProducesResponseType(typeof(UserAccessDto), 200)]
public async Task<IActionResult> GetPermission()
{
var emailClaim = User.FindFirst(ClaimTypes.Upn)?.Value;
UserAccessDto ?userAccess = new UserAccessDto();
//至資料庫撈取帳號被授權的功能清單,並且將資料組何符合套版設計的物件結構存放資料
List<Menu> menusList = await _userAccessService.GetSideBarItems(emailClaim);
userAccess.Menu = menusList;
return Ok(userAccess);
}
}
}
補充說明:其實資料庫的欄位設計有依照套版進行微幅調整,目的是在組合功能列資料時,能更容易匯整成前端套版所需的樣式。不過,並非所有功能都會完全配合套版,仍需依需求與實務設計來決定。以下資料欄位為例,新增了
RoutePath
欄位,讓前端的Menu.To
能正確導向指定的 Vue Page。
AppId | AppName | RoutePath | OrderNo | GroupType | ShowOnMenu |
---|---|---|---|---|---|
1 | 一般硬體 | App1 | 1 | 新增資產 | 1 |
2 | 軟體 | App2 | 5 | 新增資產 | 1 |
3 | 資料庫 | App3 | 4 | 新增資產 | 1 |
4 | 伺服器 | App4 | 2 | 新增資產 | 1 |
5 | 網路設備 | App5 | 3 | 新增資產 | 1 |
6 | 出入機房維護作業 | App6 | 1 | 機房管理 | 1 |
7 | 進出機房查詢 | App7 | 2 | ServerRoom | 1 |
Menu DTO 與實際 table 的對照:
Menu DTO屬性 | Table欄位 | 說明 |
---|---|---|
Title | AppName | 應用程式名稱 |
To | RoutePath | 路由路徑 |
Header | GroupType | 群組類型 |
OrderNo | OrderNo | 排序(List 會依此數值排序) |
Disabled | ShowOnMenu | 是否顯示(僅會顯示 Disabled 的功能) |
GroupName | GroupType | 功能群組名稱 |
auth.ts
,用來呼叫後端 API,並將回傳結果存放於 LocalStorage
。import { defineStore } from 'pinia';
import { router } from '@/router';
import type { AuthenticationResult } from '@azure/msal-browser';
import msalInstance from '@/stores/msalConfig';
import { fetchWrapper ,type ApiRspMessage } from '@/utils/helpers/fetch-wrapper';//Day10 建立的 fetch-wrapper.ts
const baseUrl = `${import.meta.env.VITE_API_URL}/Permission`;
export interface menu {
header?: string;
title?: string;
icon?: string;
to?: string;
divider?: boolean;
chip?: string;
chipColor?: string;
chipVariant?: string;
chipIcon?: string;
children?: menu[];
disabled?: boolean;
type?: string;
subCaption?: string;
}
export const useAuthStore = defineStore({
id: 'auth',
state: () => ({
user : JSON.parse(localStorage.getItem('user') || 'null') ,
}),
actions: {
async feachUserInfo() {
try {
const respData: UserAccessDto = await fetchWrapper.get(baseUrl) as UserAccessDto;
this.user.menu = respData.menu;
localStorage.setItem('user', JSON.stringify(this.user));
} catch (error: any) {
console.error('Failed to fetch user info', error);
}
} ,
}
});
VerticalSidebar.vue
,在此頁面實際引用 sidebarItem
元件。onMounted
):執行 auth
,同步後端的功能清單,並將資料透過 authStore
回寫到套版中的 sidebarMenu
。<script setup lang="ts">
import { onMounted,ref } from 'vue';
import { useCustomizerStore } from '../../../stores/customizer';
import { useAuthStore ,type menu} from '@/stores/auth';
import NavGroup from './NavGroup/NavGroup.vue';
import NavItem from './NavItem/NavItem.vue';
import NavCollapse from './NavCollapse/NavCollapse.vue';
import Logo from '../logo/LogoMain.vue';
const customizer = useCustomizerStore();
const userStore = useAuthStore();
const sidebarMenu = ref(<menu[]>([]))
const authStore = useAuthStore();
onMounted(async () => {
try {
await authStore.fetchUserAccessDto(); // 觸發authStore.fetchUserAccessDto 同步後端授權的功能清單
sidebarMenu.value = userStore.user.menu; // 取用authStore中的user.menu 資料
} catch (error) {
console.error('Failed to fetch menu:', error);
}
});
</script>
<template>
<v-navigation-drawer
left
v-model="customizer.sidebarDrawer"
elevation="0"
rail-width="90"
mobile-breakpoint="lg"
app
class="leftSidebar"
width="279"
:rail="customizer.miniSidebar"
expand-on-hover
>
<!---Logo part -->
<div class="pa-5">
<Logo />
</div>
<!-- ---------------------------------------------- -->
<!---Navigation -->
<!-- ---------------------------------------------- -->
<perfect-scrollbar class="scrollnavbar">
<v-list aria-busy="true" class="px-2" aria-label="menu list">
<!---Menu Loop -->
<template v-for="(item, i) in userStore.user.menu" :key="i">
<!---Item Sub Header -->
<NavGroup :item="item" v-if="item.header" :key="item.title" />
<!---Item Divider -->
<v-divider class="my-3" v-else-if="item.divider" />
<!---If Has Child -->
<NavCollapse class="leftPadding" :item="item" :level="0" v-else-if="item.children" />
<!---Single Item-->
<NavItem :item="item" v-else />
<!---End Single Item-->
</template>
</v-list>
<!-- <div class="pa-4">
<ExtraBox />
</div> -->
</perfect-scrollbar>
</v-navigation-drawer>
</template>
圖22-7:登入後由 API 回傳權限,前端動態產生左側功能列(不同帳號顯示不同選單)。
本日的示範展示了如何將套版中原本的靜態選單改造成由後端權限驅動的動態選單。透過 Menu.To
與 RoutePath
的對應,以及 DTO 與資料表欄位的設計調整,前端能正確渲染對應的頁面。整體改造遵循「最小改動原則」,沿用套版的渲染邏輯,僅需在資料結構與 API 串接上下功夫,即可實現依使用者身份動態生成的功能清單。
最後,以圖示化呈現整體流程:從前端 VerticalSidebar.vue
在初始化時發出請求,經由 auth.ts
呼叫後端 API,由 UserAccessService
連接資料庫取得授權功能清單,再回傳給前端並寫入 LocalStorage,最終由前端完成功能列的動態渲染。這個流程清楚地展示了前後端資料如何流轉與互動。
圖22-8:功能清單初始化與授權流程圖