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,171 @@
1
+ import { useLocation } from '@modern-js/runtime/router';
2
+ import { useMemo } from 'react';
3
+ import { type RouteItem, routesConfig } from '../config/navigation';
4
+ import { type LayoutMode, useLayoutStore } from '../store/layoutStore';
5
+ import { useAccess } from './useAccess';
6
+
7
+ export interface BreadcrumbItem {
8
+ path: string;
9
+ text: string;
10
+ }
11
+
12
+ export interface MenuDataResult {
13
+ /** 一級選單(hideInMenu 與無權限的項目已過濾) */
14
+ firstLevelMenus: RouteItem[];
15
+ /** 二級選單(mix/double 模式下根據一級選中項計算) */
16
+ secondLevelMenus: RouteItem[];
17
+ /** 當前路徑對應的選中 key 列表 */
18
+ selectedKeys: string[];
19
+ /** 當前路徑對應的展開 key 列表(側邊欄用) */
20
+ openKeys: string[];
21
+ /** mix/double 模式下一級選中的 itemKey */
22
+ activeFirstKey: string;
23
+ /** 麵包屑資料,使用 routesConfig 的 text 顯示 */
24
+ breadcrumbs: BreadcrumbItem[];
25
+ }
26
+
27
+ /**
28
+ * 根據 pathname 從 routesConfig 建立麵包屑資料。
29
+ * 搜尋範圍包含 hideInMenu 的項目(如歡迎頁),以確保每個路徑都有正確的文字。
30
+ * 麵包屑不受權限過濾影響(已知路徑就顯示文字)。
31
+ */
32
+ function buildBreadcrumbs(pathname: string): BreadcrumbItem[] {
33
+ for (const item of routesConfig) {
34
+ if (pathname === item.itemKey) {
35
+ return [{ path: item.itemKey, text: item.text }];
36
+ }
37
+ if (item.children) {
38
+ for (const child of item.children) {
39
+ if (pathname === child.itemKey || pathname.startsWith(`${child.itemKey}/`)) {
40
+ return [
41
+ { path: item.itemKey, text: item.text },
42
+ { path: child.itemKey, text: child.text },
43
+ ];
44
+ }
45
+ }
46
+ }
47
+ }
48
+ return [{ path: pathname, text: pathname }];
49
+ }
50
+
51
+ /** 根據 pathname 找到匹配的一級父節點 itemKey */
52
+ function findFirstLevelKey(pathname: string, routes: RouteItem[]): string {
53
+ for (const item of routes) {
54
+ if (pathname === item.itemKey || pathname.startsWith(`${item.itemKey}/`)) {
55
+ return item.itemKey;
56
+ }
57
+ if (item.children) {
58
+ for (const child of item.children) {
59
+ if (
60
+ pathname === child.itemKey ||
61
+ pathname.startsWith(`${child.itemKey}/`)
62
+ ) {
63
+ return item.itemKey;
64
+ }
65
+ }
66
+ }
67
+ }
68
+ return routes[0]?.itemKey ?? '';
69
+ }
70
+
71
+ /** 根據 pathname 計算選中 key 與展開 key */
72
+ function computeSelectedAndOpen(
73
+ pathname: string,
74
+ routes: RouteItem[],
75
+ ): { selectedKeys: string[]; openKeys: string[] } {
76
+ const selectedKeys: string[] = [];
77
+ const openKeys: string[] = [];
78
+
79
+ for (const item of routes) {
80
+ if (pathname === item.itemKey) {
81
+ selectedKeys.push(item.itemKey);
82
+ break;
83
+ }
84
+ if (item.children) {
85
+ for (const child of item.children) {
86
+ if (
87
+ pathname === child.itemKey ||
88
+ pathname.startsWith(`${child.itemKey}/`)
89
+ ) {
90
+ selectedKeys.push(child.itemKey);
91
+ openKeys.push(item.itemKey);
92
+ break;
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ if (selectedKeys.length === 0) {
99
+ selectedKeys.push(pathname);
100
+ }
101
+
102
+ return { selectedKeys, openKeys };
103
+ }
104
+
105
+ function buildMenuData(
106
+ layoutMode: LayoutMode,
107
+ activeFirstKey: string,
108
+ selectedKeys: string[],
109
+ openKeys: string[],
110
+ doubleFirstKey: string,
111
+ routes: RouteItem[],
112
+ ): MenuDataResult {
113
+ const base = { breadcrumbs: [] as BreadcrumbItem[] };
114
+
115
+ switch (layoutMode) {
116
+ case 'side':
117
+ return { ...base, firstLevelMenus: routes, secondLevelMenus: [], selectedKeys, openKeys, activeFirstKey };
118
+
119
+ case 'top':
120
+ return { ...base, firstLevelMenus: routes, secondLevelMenus: [], selectedKeys, openKeys: [], activeFirstKey };
121
+
122
+ case 'mix': {
123
+ const activeParent = routes.find(r => r.itemKey === activeFirstKey);
124
+ return { ...base, firstLevelMenus: routes, secondLevelMenus: activeParent?.children ?? [], selectedKeys, openKeys, activeFirstKey };
125
+ }
126
+
127
+ case 'double': {
128
+ const activeParent = routes.find(r => r.itemKey === doubleFirstKey);
129
+ return { ...base, firstLevelMenus: routes, secondLevelMenus: activeParent?.children ?? [], selectedKeys, openKeys, activeFirstKey: doubleFirstKey };
130
+ }
131
+
132
+ default:
133
+ return { ...base, firstLevelMenus: routes, secondLevelMenus: [], selectedKeys, openKeys, activeFirstKey };
134
+ }
135
+ }
136
+
137
+ export function useMenuData(): MenuDataResult {
138
+ const { layoutMode, doubleFirstKey } = useLayoutStore();
139
+ const location = useLocation();
140
+ const pathname = location.pathname;
141
+ const access = useAccess();
142
+
143
+ /**
144
+ * 依序套用兩層過濾:
145
+ * 1. hideInMenu — 不顯示於選單(如歡迎頁)
146
+ * 2. access — 無對應權限的項目自動隱藏(一、二級均過濾)
147
+ */
148
+ const visibleRoutes = useMemo(() => {
149
+ const accessMap = access as unknown as Record<string, boolean>;
150
+ return routesConfig
151
+ .filter(item => !item.hideInMenu)
152
+ .filter(item => !item.access || Boolean(accessMap[item.access]))
153
+ .map(item => ({
154
+ ...item,
155
+ children: item.children?.filter(
156
+ child => !child.access || Boolean(accessMap[child.access]),
157
+ ),
158
+ }));
159
+ }, [access]);
160
+
161
+ return useMemo(() => {
162
+ const { selectedKeys, openKeys } = computeSelectedAndOpen(pathname, visibleRoutes);
163
+ const activeFirstKey = findFirstLevelKey(pathname, visibleRoutes);
164
+ const breadcrumbs = buildBreadcrumbs(pathname);
165
+
166
+ return {
167
+ ...buildMenuData(layoutMode, activeFirstKey, selectedKeys, openKeys, doubleFirstKey, visibleRoutes),
168
+ breadcrumbs,
169
+ };
170
+ }, [layoutMode, pathname, doubleFirstKey, visibleRoutes]);
171
+ }
@@ -0,0 +1,26 @@
1
+ import { useEffect } from 'react';
2
+ import { usePageTitleStore } from '../store/pageTitleStore';
3
+
4
+ /**
5
+ * 在頁面元件中呼叫此 Hook,可強制覆寫 `<head>` 的 `<title>`。
6
+ * 離開頁面時自動清除,恢復成 breadcrumb 自動推導的標題。
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * // src/pages/Dashboard/Workplace/index.tsx
11
+ * import { usePageTitle } from '../../hooks/usePageTitle';
12
+ *
13
+ * export default function Workplace() {
14
+ * usePageTitle('工作台 — 即時總覽');
15
+ * return <div>...</div>;
16
+ * }
17
+ * ```
18
+ */
19
+ export function usePageTitle(title: string) {
20
+ const setCustomTitle = usePageTitleStore(s => s.setCustomTitle);
21
+
22
+ useEffect(() => {
23
+ setCustomTitle(title);
24
+ return () => setCustomTitle(null);
25
+ }, [title, setCustomTitle]);
26
+ }
@@ -0,0 +1,157 @@
1
+ import { Layout, Nav, Tooltip } from '@douyinfe/semi-ui-19';
2
+ import { useNavigate } from '@modern-js/runtime/router';
3
+ import type { ReactNode } from 'react';
4
+ import { type RouteItem, routesConfig } from '../../config/navigation';
5
+ import { useMenuData } from '../../hooks/useMenuData';
6
+ import { useLayoutStore } from '../../store/layoutStore';
7
+ import { LayoutBreadcrumb } from './LayoutBreadcrumb';
8
+
9
+ const { Header, Sider, Content } = Layout;
10
+
11
+ interface DoubleLayoutProps {
12
+ children: ReactNode;
13
+ logo?: ReactNode;
14
+ title?: string;
15
+ headerExtra?: ReactNode;
16
+ }
17
+
18
+ function toNavItems(items: RouteItem[]): object[] {
19
+ return items.map(item => ({
20
+ itemKey: item.itemKey,
21
+ text: item.text,
22
+ icon: item.icon,
23
+ items: item.children ? toNavItems(item.children) : undefined,
24
+ }));
25
+ }
26
+
27
+ export function DoubleLayout({
28
+ children,
29
+ logo,
30
+ title = 'ModSemi',
31
+ headerExtra,
32
+ }: DoubleLayoutProps) {
33
+ const navigate = useNavigate();
34
+ const { secondLevelMenus, selectedKeys, openKeys, activeFirstKey, breadcrumbs } =
35
+ useMenuData();
36
+ const { fixedHeader, showBreadcrumb, doubleFirstKey, setDoubleFirstKey } =
37
+ useLayoutStore();
38
+
39
+ const sideItems = toNavItems(secondLevelMenus);
40
+
41
+ return (
42
+ <Layout style={{ minHeight: '100vh' }}>
43
+ <Header
44
+ style={{
45
+ backgroundColor: 'var(--semi-color-bg-1)',
46
+ borderBottom: '1px solid var(--semi-color-border)',
47
+ padding: '0 24px',
48
+ display: 'flex',
49
+ alignItems: 'center',
50
+ justifyContent: 'space-between',
51
+ height: 56,
52
+ position: fixedHeader ? 'sticky' : 'relative',
53
+ top: 0,
54
+ zIndex: 100,
55
+ }}
56
+ >
57
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
58
+ {logo}
59
+ <span
60
+ style={{
61
+ fontWeight: 700,
62
+ fontSize: 16,
63
+ color: 'var(--semi-color-text-0)',
64
+ }}
65
+ >
66
+ {title}
67
+ </span>
68
+ </div>
69
+ <div>{headerExtra}</div>
70
+ </Header>
71
+
72
+ <Layout style={{ flex: 1 }}>
73
+ {/* 第一欄:極窄的圖示欄 */}
74
+ <Sider
75
+ style={{
76
+ width: 56,
77
+ minWidth: 56,
78
+ backgroundColor: 'var(--semi-color-bg-2)',
79
+ borderRight: '1px solid var(--semi-color-border)',
80
+ display: 'flex',
81
+ flexDirection: 'column',
82
+ alignItems: 'center',
83
+ paddingTop: 8,
84
+ gap: 4,
85
+ }}
86
+ >
87
+ {routesConfig.map(item => {
88
+ const isActive = doubleFirstKey === item.itemKey;
89
+ return (
90
+ <Tooltip key={item.itemKey} content={item.text} position="right">
91
+ <button
92
+ type="button"
93
+ onClick={() => {
94
+ setDoubleFirstKey(item.itemKey);
95
+ const firstChild = item.children?.[0];
96
+ navigate(firstChild ? firstChild.itemKey : item.itemKey);
97
+ }}
98
+ style={{
99
+ width: 40,
100
+ height: 40,
101
+ display: 'flex',
102
+ alignItems: 'center',
103
+ justifyContent: 'center',
104
+ borderRadius: 8,
105
+ cursor: 'pointer',
106
+ border: 'none',
107
+ backgroundColor: isActive
108
+ ? 'var(--semi-color-primary-light-default)'
109
+ : 'transparent',
110
+ color: isActive
111
+ ? 'var(--semi-color-primary)'
112
+ : 'var(--semi-color-text-2)',
113
+ fontSize: 18,
114
+ transition: 'all 0.2s',
115
+ marginBottom: 4,
116
+ }}
117
+ title={item.text}
118
+ >
119
+ {item.icon ?? item.text.slice(0, 1)}
120
+ </button>
121
+ </Tooltip>
122
+ );
123
+ })}
124
+ </Sider>
125
+
126
+ {/* 第二欄:文字選單 */}
127
+ {sideItems.length > 0 && (
128
+ <Sider
129
+ style={{
130
+ backgroundColor: 'var(--semi-color-bg-1)',
131
+ borderRight: '1px solid var(--semi-color-border)',
132
+ }}
133
+ >
134
+ <Nav
135
+ items={sideItems}
136
+ selectedKeys={selectedKeys}
137
+ defaultOpenKeys={openKeys}
138
+ style={{ height: 'calc(100vh - 56px)', overflowY: 'auto' }}
139
+ onSelect={({ itemKey }) => navigate(itemKey as string)}
140
+ />
141
+ </Sider>
142
+ )}
143
+
144
+ <Content
145
+ style={{
146
+ padding: 24,
147
+ backgroundColor: 'var(--semi-color-bg-0)',
148
+ minHeight: 'calc(100vh - 56px)',
149
+ }}
150
+ >
151
+ <LayoutBreadcrumb breadcrumbs={breadcrumbs} show={showBreadcrumb} />
152
+ {children}
153
+ </Content>
154
+ </Layout>
155
+ </Layout>
156
+ );
157
+ }
@@ -0,0 +1,32 @@
1
+ import { useEffect } from 'react';
2
+ import { globalConfig } from '../../config/global';
3
+ import type { BreadcrumbItem } from '../../hooks/useMenuData';
4
+ import { usePageTitleStore } from '../../store/pageTitleStore';
5
+
6
+ interface LayoutBreadcrumbProps {
7
+ breadcrumbs: BreadcrumbItem[];
8
+ show: boolean;
9
+ }
10
+
11
+ /**
12
+ * 讀取 globalConfig.breadcrumbComponent 並渲染。
13
+ * 同時根據 breadcrumbs 自動同步 document.title,
14
+ * 若頁面元件呼叫了 usePageTitle(),則優先使用自訂標題。
15
+ */
16
+ export function LayoutBreadcrumb({ breadcrumbs, show }: LayoutBreadcrumbProps) {
17
+ const customTitle = usePageTitleStore(s => s.customTitle);
18
+
19
+ useEffect(() => {
20
+ const autoTitle = breadcrumbs[breadcrumbs.length - 1]?.text;
21
+ document.title = customTitle ?? autoTitle ?? document.title;
22
+ }, [breadcrumbs, customTitle]);
23
+
24
+ if (!show || breadcrumbs.length === 0) return null;
25
+
26
+ const BreadcrumbComp = globalConfig.breadcrumbComponent;
27
+ return (
28
+ <div style={{ marginBottom: 16 }}>
29
+ <BreadcrumbComp breadcrumbs={breadcrumbs} />
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,134 @@
1
+ import { Layout, Nav } from '@douyinfe/semi-ui-19';
2
+ import { useNavigate } from '@modern-js/runtime/router';
3
+ import type { ReactNode } from 'react';
4
+ import type { RouteItem } from '../../config/navigation';
5
+ import { useMenuData } from '../../hooks/useMenuData';
6
+ import { useLayoutStore } from '../../store/layoutStore';
7
+ import { LayoutBreadcrumb } from './LayoutBreadcrumb';
8
+
9
+ const { Header, Sider, Content } = Layout;
10
+
11
+ interface MixLayoutProps {
12
+ children: ReactNode;
13
+ logo?: ReactNode;
14
+ title?: string;
15
+ headerExtra?: ReactNode;
16
+ }
17
+
18
+ function toNavItems(items: RouteItem[]): object[] {
19
+ return items.map(item => ({
20
+ itemKey: item.itemKey,
21
+ text: item.text,
22
+ icon: item.icon,
23
+ items: item.children ? toNavItems(item.children) : undefined,
24
+ }));
25
+ }
26
+
27
+ export function MixLayout({
28
+ children,
29
+ logo,
30
+ title = 'ModSemi',
31
+ headerExtra,
32
+ }: MixLayoutProps) {
33
+ const navigate = useNavigate();
34
+ const {
35
+ firstLevelMenus,
36
+ secondLevelMenus,
37
+ selectedKeys,
38
+ openKeys,
39
+ activeFirstKey,
40
+ breadcrumbs,
41
+ } = useMenuData();
42
+ const { fixedHeader, showBreadcrumb } = useLayoutStore();
43
+
44
+ const topItems = firstLevelMenus.map(item => ({
45
+ itemKey: item.itemKey,
46
+ text: item.text,
47
+ icon: item.icon,
48
+ }));
49
+
50
+ const sideItems = toNavItems(secondLevelMenus);
51
+
52
+ return (
53
+ <Layout style={{ minHeight: '100vh' }}>
54
+ <Header
55
+ style={{
56
+ backgroundColor: 'var(--semi-color-bg-1)',
57
+ borderBottom: '1px solid var(--semi-color-border)',
58
+ padding: '0 24px',
59
+ display: 'flex',
60
+ alignItems: 'center',
61
+ gap: 16,
62
+ height: 56,
63
+ position: fixedHeader ? 'sticky' : 'relative',
64
+ top: 0,
65
+ zIndex: 100,
66
+ }}
67
+ >
68
+ <div
69
+ style={{
70
+ display: 'flex',
71
+ alignItems: 'center',
72
+ gap: 8,
73
+ flexShrink: 0,
74
+ }}
75
+ >
76
+ {logo}
77
+ <span
78
+ style={{
79
+ fontWeight: 700,
80
+ fontSize: 16,
81
+ color: 'var(--semi-color-text-0)',
82
+ }}
83
+ >
84
+ {title}
85
+ </span>
86
+ </div>
87
+
88
+ <Nav
89
+ mode="horizontal"
90
+ items={topItems}
91
+ selectedKeys={[activeFirstKey]}
92
+ style={{ flex: 1, borderBottom: 'none' }}
93
+ onSelect={({ itemKey }) => {
94
+ const found = firstLevelMenus.find(r => r.itemKey === itemKey);
95
+ const firstChild = found?.children?.[0];
96
+ navigate(firstChild?.itemKey ?? (itemKey as string));
97
+ }}
98
+ />
99
+
100
+ <div style={{ flexShrink: 0 }}>{headerExtra}</div>
101
+ </Header>
102
+
103
+ <Layout>
104
+ {sideItems.length > 0 && (
105
+ <Sider
106
+ style={{
107
+ backgroundColor: 'var(--semi-color-bg-1)',
108
+ borderRight: '1px solid var(--semi-color-border)',
109
+ }}
110
+ >
111
+ <Nav
112
+ items={sideItems}
113
+ selectedKeys={selectedKeys}
114
+ defaultOpenKeys={openKeys}
115
+ style={{ height: 'calc(100vh - 56px)', overflowY: 'auto' }}
116
+ onSelect={({ itemKey }) => navigate(itemKey as string)}
117
+ />
118
+ </Sider>
119
+ )}
120
+
121
+ <Content
122
+ style={{
123
+ padding: 24,
124
+ backgroundColor: 'var(--semi-color-bg-0)',
125
+ minHeight: 'calc(100vh - 56px)',
126
+ }}
127
+ >
128
+ <LayoutBreadcrumb breadcrumbs={breadcrumbs} show={showBreadcrumb} />
129
+ {children}
130
+ </Content>
131
+ </Layout>
132
+ </Layout>
133
+ );
134
+ }
@@ -0,0 +1,108 @@
1
+ import { Layout, Nav } from '@douyinfe/semi-ui-19';
2
+ import { useNavigate } from '@modern-js/runtime/router';
3
+ import type { ReactNode } from 'react';
4
+ import type { RouteItem } from '../../config/navigation';
5
+ import { useMenuData } from '../../hooks/useMenuData';
6
+ import { useLayoutStore } from '../../store/layoutStore';
7
+ import { LayoutBreadcrumb } from './LayoutBreadcrumb';
8
+
9
+ const { Header, Sider, Content } = Layout;
10
+
11
+ interface SideLayoutProps {
12
+ children: ReactNode;
13
+ logo?: ReactNode;
14
+ title?: string;
15
+ headerExtra?: ReactNode;
16
+ }
17
+
18
+ function toNavItems(items: RouteItem[]): object[] {
19
+ return items.map(item => ({
20
+ itemKey: item.itemKey,
21
+ text: item.text,
22
+ icon: item.icon,
23
+ items: item.children ? toNavItems(item.children) : undefined,
24
+ }));
25
+ }
26
+
27
+ export function SideLayout({
28
+ children,
29
+ logo,
30
+ title = 'ModSemi',
31
+ headerExtra,
32
+ }: SideLayoutProps) {
33
+ const navigate = useNavigate();
34
+ const { firstLevelMenus, selectedKeys, openKeys, breadcrumbs } = useMenuData();
35
+ const { fixedHeader, showBreadcrumb } = useLayoutStore();
36
+
37
+ const navItems = toNavItems(firstLevelMenus);
38
+
39
+ return (
40
+ <Layout style={{ minHeight: '100vh' }}>
41
+ <Sider
42
+ style={{
43
+ backgroundColor: 'var(--semi-color-bg-1)',
44
+ borderRight: '1px solid var(--semi-color-border)',
45
+ }}
46
+ >
47
+ <div
48
+ style={{
49
+ display: 'flex',
50
+ alignItems: 'center',
51
+ gap: 8,
52
+ padding: '16px 20px',
53
+ borderBottom: '1px solid var(--semi-color-border)',
54
+ }}
55
+ >
56
+ {logo}
57
+ <span
58
+ style={{
59
+ fontWeight: 700,
60
+ fontSize: 16,
61
+ color: 'var(--semi-color-text-0)',
62
+ whiteSpace: 'nowrap',
63
+ }}
64
+ >
65
+ {title}
66
+ </span>
67
+ </div>
68
+ <Nav
69
+ items={navItems}
70
+ selectedKeys={selectedKeys}
71
+ defaultOpenKeys={openKeys}
72
+ style={{ height: 'calc(100vh - 57px)', overflowY: 'auto' }}
73
+ onSelect={({ itemKey }) => navigate(itemKey as string)}
74
+ />
75
+ </Sider>
76
+
77
+ <Layout>
78
+ <Header
79
+ style={{
80
+ backgroundColor: 'var(--semi-color-bg-1)',
81
+ borderBottom: '1px solid var(--semi-color-border)',
82
+ padding: '0 24px',
83
+ display: 'flex',
84
+ alignItems: 'center',
85
+ justifyContent: 'flex-end',
86
+ height: 56,
87
+ position: fixedHeader ? 'sticky' : 'relative',
88
+ top: 0,
89
+ zIndex: 100,
90
+ }}
91
+ >
92
+ {headerExtra}
93
+ </Header>
94
+
95
+ <Content
96
+ style={{
97
+ padding: 24,
98
+ backgroundColor: 'var(--semi-color-bg-0)',
99
+ minHeight: 'calc(100vh - 56px)',
100
+ }}
101
+ >
102
+ <LayoutBreadcrumb breadcrumbs={breadcrumbs} show={showBreadcrumb} />
103
+ {children}
104
+ </Content>
105
+ </Layout>
106
+ </Layout>
107
+ );
108
+ }