create-modsemi 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +99 -0
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +155 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +38 -0
  7. package/template/.browserslistrc +4 -0
  8. package/template/.env.example +9 -0
  9. package/template/.github/workflows/ci.yml +36 -0
  10. package/template/.nvmrc +2 -0
  11. package/template/README.md +199 -0
  12. package/template/_gitignore +33 -0
  13. package/template/_package.json +36 -0
  14. package/template/biome.json +37 -0
  15. package/template/modern.config.ts +38 -0
  16. package/template/orval.config.ts +98 -0
  17. package/template/src/api/instance.ts +87 -0
  18. package/template/src/components/Access/index.tsx +32 -0
  19. package/template/src/components/AppBreadcrumb/index.tsx +34 -0
  20. package/template/src/components/UserAvatar/index.less +66 -0
  21. package/template/src/components/UserAvatar/index.tsx +96 -0
  22. package/template/src/config/global.tsx +59 -0
  23. package/template/src/config/navigation.tsx +91 -0
  24. package/template/src/hooks/useAccess.ts +53 -0
  25. package/template/src/hooks/useMenuData.ts +171 -0
  26. package/template/src/hooks/usePageTitle.ts +26 -0
  27. package/template/src/layouts/ProLayout/DoubleLayout.tsx +157 -0
  28. package/template/src/layouts/ProLayout/LayoutBreadcrumb.tsx +32 -0
  29. package/template/src/layouts/ProLayout/MixLayout.tsx +134 -0
  30. package/template/src/layouts/ProLayout/SideLayout.tsx +108 -0
  31. package/template/src/layouts/ProLayout/TopLayout.tsx +98 -0
  32. package/template/src/layouts/ProLayout/index.tsx +75 -0
  33. package/template/src/layouts/SettingDrawer/index.tsx +390 -0
  34. package/template/src/modern-app-env.d.ts +1 -0
  35. package/template/src/modern.runtime.ts +3 -0
  36. package/template/src/pages/Dashboard/Workplace/index.tsx +7 -0
  37. package/template/src/pages/Error/NotFound/index.less +211 -0
  38. package/template/src/pages/Error/NotFound/index.tsx +64 -0
  39. package/template/src/pages/Login/index.less +491 -0
  40. package/template/src/pages/Login/index.tsx +204 -0
  41. package/template/src/pages/Welcome/index.less +351 -0
  42. package/template/src/pages/Welcome/index.tsx +164 -0
  43. package/template/src/routes/$.tsx +14 -0
  44. package/template/src/routes/dashboard/workplace/page.tsx +3 -0
  45. package/template/src/routes/layout.tsx +53 -0
  46. package/template/src/routes/login/page.tsx +3 -0
  47. package/template/src/routes/page.tsx +3 -0
  48. package/template/src/store/authStore.ts +61 -0
  49. package/template/src/store/layoutStore.ts +82 -0
  50. package/template/src/store/pageTitleStore.ts +12 -0
  51. package/template/src/styles/global.less +80 -0
  52. package/template/swagger/sample.json +263 -0
  53. package/template/tsconfig.json +16 -0
@@ -0,0 +1,38 @@
1
+ import { appTools, defineConfig } from '@modern-js/app-tools';
2
+
3
+ // https://modernjs.dev/en/configure/app/usage
4
+ export default defineConfig({
5
+ plugins: [appTools()],
6
+ tools: {
7
+ // 啟用 Less 支援(Modern.js 內建,安裝 less 即可)
8
+ styleLoader: {},
9
+ },
10
+ dev: {
11
+ server: {
12
+ proxy: {
13
+ /**
14
+ * API Proxy — 解決本地開發的 CORS 問題
15
+ *
16
+ * 所有 /api/** 請求會在 dev server 端轉發給後端,
17
+ * 瀏覽器只看到同源的 /api,不會觸發 CORS。
18
+ *
19
+ * 使用方式:
20
+ * 將 target 改為後端實際位址,例如:
21
+ * 'http://localhost:3001'
22
+ * 'https://staging-api.example.com'
23
+ *
24
+ * 注意:此設定僅在 `pnpm dev` 時生效;
25
+ * 正式環境請改用 Nginx / CDN 的 proxy 設定。
26
+ */
27
+ '/api': {
28
+ target: 'http://localhost:3001',
29
+ changeOrigin: true,
30
+ // 將 /api 前綴從轉發路徑中移除
31
+ // 例如:前端呼叫 /api/users → 後端收到 /users
32
+ // 若後端 route 本身已包含 /api,請刪除下面這行
33
+ pathRewrite: { '^/api': '' },
34
+ },
35
+ },
36
+ },
37
+ },
38
+ });
@@ -0,0 +1,98 @@
1
+ import { defineConfig } from 'orval';
2
+
3
+ /**
4
+ * Orval 設定檔 — API 程式碼自動生成
5
+ *
6
+ * 使用方式:
7
+ * pnpm api:gen → 依 swagger/*.json 生成所有 API
8
+ * pnpm api:gen --watch → 監聽 spec 變更並自動重新生成
9
+ *
10
+ * 生成結果輸出至 src/api/generated/
11
+ * ⚠️ 請勿手動編輯 src/api/generated/ 內的任何檔案
12
+ *
13
+ * 文件:https://orval.dev/docs
14
+ */
15
+ export default defineConfig({
16
+ /**
17
+ * ── 主要 API(範例使用 swagger/sample.json)────────────────────
18
+ * 開發者只需將 Swagger / OpenAPI JSON 放到 swagger/ 目錄下,
19
+ * 並在此新增對應設定區塊即可。
20
+ */
21
+ sampleApi: {
22
+ input: {
23
+ /**
24
+ * target:Swagger spec 來源
25
+ * 支援:本地檔案路徑 | 遠端 URL
26
+ *
27
+ * 本地範例:'./swagger/sample.json'
28
+ * 遠端範例:'https://api.example.com/swagger.json'
29
+ * 'https://api.example.com/v3/api-docs'
30
+ */
31
+ target: './swagger/sample.json',
32
+ },
33
+ output: {
34
+ /**
35
+ * mode:
36
+ * 'single' — 所有 API 生成到一個檔案(小型 spec 適用)
37
+ * 'split' — models 與 functions 分開(預設推薦)
38
+ * 'tags' — 依 OpenAPI tags 拆分成多個檔案(大型 spec 推薦)
39
+ * 'tags-split'— tags + split 的組合
40
+ */
41
+ mode: 'tags',
42
+
43
+ /** 生成的 API 請求函式輸出目錄 */
44
+ target: './src/api/generated',
45
+
46
+ /** 生成的 TypeScript 型別定義輸出目錄 */
47
+ schemas: './src/api/generated/model',
48
+
49
+ /**
50
+ * client:生成的客戶端類型
51
+ * 'axios-functions' — 純函式(預設,無框架依賴)
52
+ * 'axios' — 工廠函式(支援注入自訂 instance)
53
+ * 'react-query' — 生成 useQuery / useMutation hooks
54
+ * 'swr' — 生成 SWR hooks
55
+ * 'fetch' — 使用原生 Fetch API
56
+ */
57
+ client: 'axios',
58
+
59
+ override: {
60
+ /**
61
+ * mutator:使用自訂的 axios instance 取代預設的 axios
62
+ * 讓所有生成的 API 都走 src/api/instance.ts 中的
63
+ * 攔截器(自動注入 Token、統一錯誤處理)
64
+ */
65
+ mutator: {
66
+ path: './src/api/instance.ts',
67
+ name: 'customInstance',
68
+ },
69
+ },
70
+ },
71
+ // hooks: {
72
+ // afterAllFilesWrite: 'prettier --write',
73
+ // },
74
+ // 注意:src/api/generated/ 已加入 .gitignore,無需格式化
75
+ },
76
+
77
+ /**
78
+ * ── 新增其他 API 服務的範例 ────────────────────────────────
79
+ * 若後端有多個 micro-service,各自有不同的 Swagger spec,
80
+ * 在此複製上方 sampleApi 區塊並修改 input.target 即可。
81
+ *
82
+ * anotherService: {
83
+ * input: { target: './swagger/another.json' },
84
+ * output: {
85
+ * mode: 'tags',
86
+ * target: './src/api/generated/another',
87
+ * schemas: './src/api/generated/another/model',
88
+ * client: 'axios',
89
+ * override: {
90
+ * mutator: {
91
+ * path: './src/api/instance.ts',
92
+ * name: 'customInstance',
93
+ * },
94
+ * },
95
+ * },
96
+ * },
97
+ */
98
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Axios Instance — ModSemi API 統一請求入口
3
+ *
4
+ * 所有 orval 生成的 API 函式都使用此 instance 發出請求。
5
+ * 在此集中管理:baseURL、逾時、請求攔截器(注入 Token)、回應攔截器(錯誤處理)。
6
+ */
7
+
8
+ import axios, {
9
+ type AxiosError,
10
+ type AxiosRequestConfig,
11
+ type AxiosResponse,
12
+ } from 'axios';
13
+
14
+ // ── 環境設定 ────────────────────────────────────────────────
15
+ // Modern.js 透過 .env 檔案注入環境變數,於 modern.config.ts 中設定 source.define
16
+ const BASE_URL = (typeof process !== 'undefined' && process.env.MODERN_APP_API_BASE_URL) || '/api';
17
+
18
+ // ── 建立 instance ──────────────────────────────────────────
19
+ export const apiInstance = axios.create({
20
+ baseURL: BASE_URL,
21
+ timeout: 15000,
22
+ headers: {
23
+ 'Content-Type': 'application/json',
24
+ },
25
+ });
26
+
27
+ // ── 請求攔截器:自動注入 Bearer Token ─────────────────────
28
+ apiInstance.interceptors.request.use(
29
+ config => {
30
+ // 從 localStorage 讀取 authStore 持久化的 token
31
+ // 若日後 authStore 新增 token 欄位,在此取用
32
+ const raw = localStorage.getItem('modsemi-auth');
33
+ if (raw) {
34
+ try {
35
+ const state = JSON.parse(raw) as { state?: { token?: string } };
36
+ const token = state?.state?.token;
37
+ if (token) {
38
+ config.headers.Authorization = `Bearer ${token}`;
39
+ }
40
+ } catch {
41
+ // JSON 解析失敗時忽略,不影響請求
42
+ }
43
+ }
44
+ return config;
45
+ },
46
+ error => Promise.reject(error),
47
+ );
48
+
49
+ // ── 回應攔截器:統一錯誤處理 ──────────────────────────────
50
+ apiInstance.interceptors.response.use(
51
+ (response: AxiosResponse) => response,
52
+ (error: AxiosError) => {
53
+ if (error.response) {
54
+ const { status } = error.response;
55
+
56
+ if (status === 401) {
57
+ // Token 過期或無效 → 清除本地登入狀態並導向登入頁
58
+ localStorage.removeItem('modsemi-auth');
59
+ window.location.href = '/login';
60
+ }
61
+
62
+ if (status === 403) {
63
+ console.warn('[API] 403 Forbidden — 無此操作權限');
64
+ }
65
+
66
+ if (status >= 500) {
67
+ console.error('[API] 伺服器錯誤', error.response.data);
68
+ }
69
+ } else if (error.request) {
70
+ console.error('[API] 網路錯誤,請檢查連線', error.message);
71
+ }
72
+
73
+ return Promise.reject(error);
74
+ },
75
+ );
76
+
77
+ /**
78
+ * orval mutator — 供 orval.config.ts 的 `mutator` 欄位引用。
79
+ * orval 生成的每個 API 函式會呼叫此函式發送請求。
80
+ */
81
+ export const customInstance = <T>(
82
+ config: AxiosRequestConfig,
83
+ ): Promise<T> => {
84
+ return apiInstance(config).then(response => response.data as T);
85
+ };
86
+
87
+ export default apiInstance;
@@ -0,0 +1,32 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ interface AccessProps {
4
+ /** 是否有存取權限(通常來自 useAccess() 的某個 key) */
5
+ accessible: boolean;
6
+ /** 無權限時顯示的替代內容,預設為 null(不渲染任何東西) */
7
+ fallback?: ReactNode;
8
+ children: ReactNode;
9
+ }
10
+
11
+ /**
12
+ * 條件渲染包裝組件。根據 `accessible` 決定是否渲染 `children`。
13
+ *
14
+ * @example 基本用法
15
+ * ```tsx
16
+ * const access = useAccess();
17
+ *
18
+ * <Access accessible={access.isAdmin}>
19
+ * <DeleteButton />
20
+ * </Access>
21
+ * ```
22
+ *
23
+ * @example 搭配 fallback
24
+ * ```tsx
25
+ * <Access accessible={access.canViewSettings} fallback={<NoPermissionTip />}>
26
+ * <SettingsPanel />
27
+ * </Access>
28
+ * ```
29
+ */
30
+ export function Access({ accessible, fallback = null, children }: AccessProps) {
31
+ return accessible ? <>{children}</> : <>{fallback}</>;
32
+ }
@@ -0,0 +1,34 @@
1
+ import { Breadcrumb } from '@douyinfe/semi-ui-19';
2
+ import { useNavigate } from '@modern-js/runtime/router';
3
+ import type { BreadcrumbItem } from '../../hooks/useMenuData';
4
+
5
+ export interface BreadcrumbComponentProps {
6
+ breadcrumbs: BreadcrumbItem[];
7
+ }
8
+
9
+ /**
10
+ * 預設 Breadcrumb 元件,使用 Semi Design 的 Breadcrumb.Item 子元素方式渲染。
11
+ * 非最後一項可點擊並導航至對應路徑。
12
+ * 可在 src/config/global.tsx 的 breadcrumbComponent 欄位替換為自訂元件。
13
+ */
14
+ export function AppBreadcrumb({ breadcrumbs }: BreadcrumbComponentProps) {
15
+ const navigate = useNavigate();
16
+ const lastIndex = breadcrumbs.length - 1;
17
+
18
+ return (
19
+ <Breadcrumb>
20
+ {breadcrumbs.map((b, i) => {
21
+ const isLast = i === lastIndex;
22
+ return (
23
+ <Breadcrumb.Item
24
+ key={b.path}
25
+ onClick={isLast ? undefined : () => navigate(b.path)}
26
+ style={isLast ? undefined : { cursor: 'pointer' }}
27
+ >
28
+ {b.text}
29
+ </Breadcrumb.Item>
30
+ );
31
+ })}
32
+ </Breadcrumb>
33
+ );
34
+ }
@@ -0,0 +1,66 @@
1
+ // ── UserAvatar 觸發器 ─────────────────────────────────────
2
+
3
+ .ua-trigger {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ justify-content: center;
7
+ border-radius: 50%;
8
+ cursor: pointer;
9
+ outline: none;
10
+ transition: box-shadow 0.2s ease, opacity 0.15s ease;
11
+
12
+ &:hover {
13
+ opacity: 0.85;
14
+ box-shadow: 0 0 0 3px var(--semi-color-primary-light-hover);
15
+ }
16
+
17
+ &:focus-visible {
18
+ box-shadow: 0 0 0 3px var(--semi-color-primary-light-active);
19
+ }
20
+ }
21
+
22
+ .ua-avatar {
23
+ cursor: pointer;
24
+ }
25
+
26
+ // ── Dropdown 選單樣式 ─────────────────────────────────────
27
+
28
+ .ua-menu {
29
+ min-width: 200px !important;
30
+ }
31
+
32
+ // 使用者資訊區塊
33
+ .ua-user-info {
34
+ display: flex;
35
+ align-items: center;
36
+ gap: 12px;
37
+ padding: 14px 16px 12px;
38
+ }
39
+
40
+ .ua-user-avatar {
41
+ flex-shrink: 0;
42
+ }
43
+
44
+ .ua-user-text {
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: 2px;
48
+ overflow: hidden;
49
+ }
50
+
51
+ .ua-user-name {
52
+ font-size: 14px;
53
+ font-weight: 600;
54
+ color: var(--semi-color-text-0);
55
+ white-space: nowrap;
56
+ overflow: hidden;
57
+ text-overflow: ellipsis;
58
+ }
59
+
60
+ .ua-user-email {
61
+ font-size: 12px;
62
+ color: var(--semi-color-text-2);
63
+ white-space: nowrap;
64
+ overflow: hidden;
65
+ text-overflow: ellipsis;
66
+ }
@@ -0,0 +1,96 @@
1
+ import {
2
+ IconExit,
3
+ IconMoon,
4
+ IconSettingStroked,
5
+ IconSun,
6
+ } from '@douyinfe/semi-icons';
7
+ import { Avatar, Dropdown } from '@douyinfe/semi-ui-19';
8
+ import { useNavigate } from '@modern-js/runtime/router';
9
+ import { useAuthStore } from '../../store/authStore';
10
+ import { useLayoutStore } from '../../store/layoutStore';
11
+ import './index.less';
12
+
13
+ function getInitials(name: string) {
14
+ return name.trim().slice(0, 1).toUpperCase();
15
+ }
16
+
17
+ /** Header 右側使用者頭像 + Dropdown 選單 */
18
+ export function UserAvatar() {
19
+ const navigate = useNavigate();
20
+ const { currentUser, logout } = useAuthStore();
21
+ const { themeMode, toggleTheme } = useLayoutStore();
22
+
23
+ const name = currentUser?.name ?? '使用者';
24
+ const email = currentUser?.email;
25
+
26
+ const handleLogout = () => {
27
+ logout();
28
+ navigate('/login', { replace: true });
29
+ };
30
+
31
+ const menu = (
32
+ <Dropdown.Menu className="ua-menu">
33
+ {/* 使用者資訊區塊(非可點選) */}
34
+ <div className="ua-user-info">
35
+ <Avatar
36
+ size="default"
37
+ color="blue"
38
+ src={currentUser?.avatar}
39
+ className="ua-user-avatar"
40
+ >
41
+ {getInitials(name)}
42
+ </Avatar>
43
+ <div className="ua-user-text">
44
+ <div className="ua-user-name">{name}</div>
45
+ {email && <div className="ua-user-email">{email}</div>}
46
+ </div>
47
+ </div>
48
+
49
+ <Dropdown.Divider />
50
+
51
+ <Dropdown.Item
52
+ icon={<IconSettingStroked />}
53
+ onClick={() => navigate('/settings/account')}
54
+ >
55
+ 個人設定
56
+ </Dropdown.Item>
57
+
58
+ <Dropdown.Item
59
+ icon={themeMode === 'dark' ? <IconSun /> : <IconMoon />}
60
+ onClick={toggleTheme}
61
+ >
62
+ {themeMode === 'dark' ? '切換亮色模式' : '切換暗色模式'}
63
+ </Dropdown.Item>
64
+
65
+ <Dropdown.Divider />
66
+
67
+ <Dropdown.Item
68
+ type="danger"
69
+ icon={<IconExit />}
70
+ onClick={handleLogout}
71
+ >
72
+ 登出
73
+ </Dropdown.Item>
74
+ </Dropdown.Menu>
75
+ );
76
+
77
+ return (
78
+ <Dropdown
79
+ trigger="click"
80
+ position="bottomRight"
81
+ render={menu}
82
+ clickToHide
83
+ >
84
+ <div className="ua-trigger" role="button" tabIndex={0} aria-label="使用者選單">
85
+ <Avatar
86
+ size="small"
87
+ color="blue"
88
+ src={currentUser?.avatar}
89
+ className="ua-avatar"
90
+ >
91
+ {getInitials(name)}
92
+ </Avatar>
93
+ </div>
94
+ </Dropdown>
95
+ );
96
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * ModSemi 全域設定
3
+ *
4
+ * 這是框架開放給使用者自訂的主要介面。
5
+ * 修改此檔案的欄位以替換預設的框架行為,無需深入框架內部。
6
+ *
7
+ * 目前支援的自訂項目:
8
+ * - errorPages.notFound — 404 頁面元件
9
+ * - breadcrumbComponent — 麵包屑元件
10
+ */
11
+
12
+ import type { ComponentType } from 'react';
13
+ import {
14
+ AppBreadcrumb,
15
+ type BreadcrumbComponentProps,
16
+ } from '../components/AppBreadcrumb';
17
+ import { NotFoundPage } from '../pages/Error/NotFound';
18
+
19
+ // ── 型別定義 ─────────────────────────────────────────────
20
+
21
+ export type { BreadcrumbComponentProps };
22
+
23
+ /** 錯誤頁面元件映射表 */
24
+ export interface GlobalErrorConfig {
25
+ /** 訪問不存在的路徑時顯示的頁面(HTTP 404)*/
26
+ notFound: ComponentType;
27
+ }
28
+
29
+ /** ModSemi 全域設定型別 */
30
+ export interface GlobalConfig {
31
+ errorPages: GlobalErrorConfig;
32
+ /**
33
+ * 麵包屑元件。接收 `breadcrumbs: BreadcrumbItem[]`,自行決定渲染方式。
34
+ *
35
+ * @example 使用自訂 Breadcrumb
36
+ * ```tsx
37
+ * import type { BreadcrumbComponentProps } from './global';
38
+ *
39
+ * function MyBreadcrumb({ breadcrumbs }: BreadcrumbComponentProps) {
40
+ * return <div>{breadcrumbs.map(b => b.text).join(' › ')}</div>;
41
+ * }
42
+ *
43
+ * export const globalConfig: GlobalConfig = {
44
+ * ...
45
+ * breadcrumbComponent: MyBreadcrumb,
46
+ * };
47
+ * ```
48
+ */
49
+ breadcrumbComponent: ComponentType<BreadcrumbComponentProps>;
50
+ }
51
+
52
+ // ── 設定本體 ─────────────────────────────────────────────
53
+
54
+ export const globalConfig: GlobalConfig = {
55
+ errorPages: {
56
+ notFound: NotFoundPage,
57
+ },
58
+ breadcrumbComponent: AppBreadcrumb,
59
+ };
@@ -0,0 +1,91 @@
1
+ import {
2
+ IconBarChartVStroked,
3
+ IconChecklistStroked,
4
+ IconFlowChartStroked,
5
+ IconHome,
6
+ IconLayers,
7
+ IconSettingStroked,
8
+ } from '@douyinfe/semi-icons';
9
+ import type { ReactNode } from 'react';
10
+
11
+ export interface RouteItem {
12
+ text: string;
13
+ itemKey: string;
14
+ icon?: ReactNode;
15
+ children?: RouteItem[];
16
+ /** 是否隱藏於導航(如詳情頁) */
17
+ hideInMenu?: boolean;
18
+ /** 跳轉路徑,預設同 itemKey */
19
+ path?: string;
20
+ /**
21
+ * 存取權限 key,對應 `AccessConfig` 中的欄位名稱。
22
+ * 設定後,使用者須擁有對應權限才能在選單中看到此項目。
23
+ * 未設定則所有登入使用者均可見。
24
+ *
25
+ * @example
26
+ * // 只有管理員(isAdmin)才能看到此項目
27
+ * access: 'isAdmin'
28
+ *
29
+ * // 只有可查看設定的使用者才能看到此項目
30
+ * access: 'canViewSettings'
31
+ */
32
+ access?: string;
33
+ }
34
+
35
+ export const routesConfig: RouteItem[] = [
36
+ {
37
+ text: '歡迎',
38
+ itemKey: '/',
39
+ icon: <IconHome />,
40
+ hideInMenu: true,
41
+ },
42
+ {
43
+ text: '儀表板',
44
+ itemKey: '/dashboard',
45
+ icon: <IconBarChartVStroked />,
46
+ children: [
47
+ { text: '工作台', itemKey: '/dashboard/workplace' },
48
+ { text: '分析頁', itemKey: '/dashboard/analysis' },
49
+ { text: '監控頁', itemKey: '/dashboard/monitor' },
50
+ ],
51
+ },
52
+ {
53
+ text: '表單頁面',
54
+ itemKey: '/form',
55
+ icon: <IconChecklistStroked />,
56
+ children: [
57
+ { text: '基礎表單', itemKey: '/form/basic' },
58
+ { text: '步驟表單', itemKey: '/form/step' },
59
+ { text: '進階表單', itemKey: '/form/advanced' },
60
+ ],
61
+ },
62
+ {
63
+ text: '列表頁面',
64
+ itemKey: '/list',
65
+ icon: <IconLayers />,
66
+ children: [
67
+ { text: '查詢列表', itemKey: '/list/query' },
68
+ { text: '標準列表', itemKey: '/list/standard' },
69
+ { text: '卡片列表', itemKey: '/list/card' },
70
+ ],
71
+ },
72
+ {
73
+ text: '詳情頁面',
74
+ itemKey: '/detail',
75
+ icon: <IconFlowChartStroked />,
76
+ children: [
77
+ { text: '基礎詳情', itemKey: '/detail/basic' },
78
+ { text: '進階詳情', itemKey: '/detail/advanced' },
79
+ ],
80
+ },
81
+ {
82
+ text: '系統設定',
83
+ itemKey: '/settings',
84
+ icon: <IconSettingStroked />,
85
+ access: 'canViewSettings',
86
+ children: [
87
+ { text: '帳號設定', itemKey: '/settings/account' },
88
+ { text: '權限管理', itemKey: '/settings/permission' },
89
+ ],
90
+ },
91
+ ];
@@ -0,0 +1,53 @@
1
+ import { useAuthStore } from '../store/authStore';
2
+
3
+ /**
4
+ * 系統權限定義。
5
+ * 每個 key 代表一項存取能力,值為 boolean。
6
+ * 在 navigation.tsx 的 `access` 欄位填入對應 key,
7
+ * 可自動控制選單的顯示 / 隱藏。
8
+ */
9
+ export interface AccessConfig {
10
+ /** 是否為系統管理員 */
11
+ isAdmin: boolean;
12
+ /** 是否可查看儀表板 */
13
+ canViewDashboard: boolean;
14
+ /** 是否可查看表單頁面 */
15
+ canViewForm: boolean;
16
+ /** 是否可查看列表頁面 */
17
+ canViewList: boolean;
18
+ /** 是否可查看詳情頁面 */
19
+ canViewDetail: boolean;
20
+ /** 是否可查看系統設定 */
21
+ canViewSettings: boolean;
22
+ }
23
+
24
+ /**
25
+ * 根據當前登入使用者的角色(roles),計算各功能的存取權限。
26
+ *
27
+ * **設計原則:**
28
+ * - 「角色 → 權限」的對映邏輯集中於此 hook,業務程式碼只消費語意化 key。
29
+ * - 新增角色或調整權限範圍,只需修改此檔案。
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * function AdminButton() {
34
+ * const access = useAccess();
35
+ * if (!access.isAdmin) return null;
36
+ * return <Button>管理員功能</Button>;
37
+ * }
38
+ * ```
39
+ */
40
+ export function useAccess(): AccessConfig {
41
+ const { currentUser } = useAuthStore();
42
+ const roles = currentUser?.roles ?? [];
43
+ const isAdmin = roles.includes('admin');
44
+
45
+ return {
46
+ isAdmin,
47
+ canViewDashboard: true, // 所有登入使用者皆可存取
48
+ canViewForm: true,
49
+ canViewList: true,
50
+ canViewDetail: true,
51
+ canViewSettings: isAdmin, // 僅管理員可見
52
+ };
53
+ }