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,98 @@
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, Content } = Layout;
10
+
11
+ interface TopLayoutProps {
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 TopLayout({
28
+ children,
29
+ logo,
30
+ title = 'ModSemi',
31
+ headerExtra,
32
+ }: TopLayoutProps) {
33
+ const navigate = useNavigate();
34
+ const { firstLevelMenus, selectedKeys, breadcrumbs } = useMenuData();
35
+ const { fixedHeader, showBreadcrumb } = useLayoutStore();
36
+
37
+ const navItems = toNavItems(firstLevelMenus);
38
+
39
+ return (
40
+ <Layout style={{ minHeight: '100vh' }}>
41
+ <Header
42
+ style={{
43
+ backgroundColor: 'var(--semi-color-bg-1)',
44
+ borderBottom: '1px solid var(--semi-color-border)',
45
+ padding: '0 24px',
46
+ display: 'flex',
47
+ alignItems: 'center',
48
+ gap: 16,
49
+ height: 56,
50
+ position: fixedHeader ? 'sticky' : 'relative',
51
+ top: 0,
52
+ zIndex: 100,
53
+ }}
54
+ >
55
+ <div
56
+ style={{
57
+ display: 'flex',
58
+ alignItems: 'center',
59
+ gap: 8,
60
+ flexShrink: 0,
61
+ }}
62
+ >
63
+ {logo}
64
+ <span
65
+ style={{
66
+ fontWeight: 700,
67
+ fontSize: 16,
68
+ color: 'var(--semi-color-text-0)',
69
+ }}
70
+ >
71
+ {title}
72
+ </span>
73
+ </div>
74
+
75
+ <Nav
76
+ mode="horizontal"
77
+ items={navItems}
78
+ selectedKeys={selectedKeys}
79
+ style={{ flex: 1, borderBottom: 'none' }}
80
+ onSelect={({ itemKey }) => navigate(itemKey as string)}
81
+ />
82
+
83
+ <div style={{ flexShrink: 0 }}>{headerExtra}</div>
84
+ </Header>
85
+
86
+ <Content
87
+ style={{
88
+ padding: 24,
89
+ backgroundColor: 'var(--semi-color-bg-0)',
90
+ minHeight: 'calc(100vh - 56px)',
91
+ }}
92
+ >
93
+ <LayoutBreadcrumb breadcrumbs={breadcrumbs} show={showBreadcrumb} />
94
+ {children}
95
+ </Content>
96
+ </Layout>
97
+ );
98
+ }
@@ -0,0 +1,75 @@
1
+ import { IconToken } from '@douyinfe/semi-icons-lab';
2
+ import { Tooltip } from '@douyinfe/semi-ui-19';
3
+ import type { ReactNode } from 'react';
4
+ import { UserAvatar } from '../../components/UserAvatar';
5
+ import { useLayoutStore } from '../../store/layoutStore';
6
+ import { DoubleLayout } from './DoubleLayout';
7
+ import { MixLayout } from './MixLayout';
8
+ import { SideLayout } from './SideLayout';
9
+ import { TopLayout } from './TopLayout';
10
+
11
+ export interface ProLayoutProps {
12
+ children: ReactNode;
13
+ logo?: ReactNode;
14
+ title?: string;
15
+ }
16
+
17
+ function SettingTrigger() {
18
+ const { setSettingDrawerOpen } = useLayoutStore();
19
+ return (
20
+ <Tooltip content="佈局設定" position="bottom">
21
+ <button
22
+ type="button"
23
+ onClick={() => setSettingDrawerOpen(true)}
24
+ aria-label="佈局設定"
25
+ style={{
26
+ display: 'inline-flex',
27
+ alignItems: 'center',
28
+ justifyContent: 'center',
29
+ width: 32,
30
+ height: 32,
31
+ borderRadius: 8,
32
+ border: '1px solid var(--semi-color-primary-light-active)',
33
+ background: 'var(--semi-color-primary-light-default)',
34
+ color: 'var(--semi-color-primary)',
35
+ cursor: 'pointer',
36
+ fontSize: 16,
37
+ transition: 'background 0.2s, border-color 0.2s',
38
+ }}
39
+ >
40
+ <IconToken />
41
+ </button>
42
+ </Tooltip>
43
+ );
44
+ }
45
+
46
+ function HeaderExtra() {
47
+ return (
48
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
49
+ <SettingTrigger />
50
+ <UserAvatar />
51
+ </div>
52
+ );
53
+ }
54
+
55
+ export function ProLayout({ children, logo, title }: ProLayoutProps) {
56
+ const { layoutMode } = useLayoutStore();
57
+
58
+ const sharedProps = {
59
+ children,
60
+ logo,
61
+ title,
62
+ headerExtra: <HeaderExtra />,
63
+ };
64
+
65
+ switch (layoutMode) {
66
+ case 'top':
67
+ return <TopLayout {...sharedProps} />;
68
+ case 'mix':
69
+ return <MixLayout {...sharedProps} />;
70
+ case 'double':
71
+ return <DoubleLayout {...sharedProps} />;
72
+ default:
73
+ return <SideLayout {...sharedProps} />;
74
+ }
75
+ }
@@ -0,0 +1,390 @@
1
+ import {
2
+ IconColumnsStroked,
3
+ IconGridView,
4
+ IconSidebar,
5
+ IconVersionStroked,
6
+ } from '@douyinfe/semi-icons';
7
+ import { Divider, SideSheet, Switch, Tooltip, Typography } from '@douyinfe/semi-ui-19';
8
+ import { useEffect } from 'react';
9
+ import { type ColorTheme, type LayoutMode, useLayoutStore } from '../../store/layoutStore';
10
+
11
+ const { Title, Text } = Typography;
12
+
13
+ // ── 色系定義 ─────────────────────────────────────────────
14
+
15
+ interface ColorScale {
16
+ primary: string;
17
+ primaryHover: string;
18
+ primaryActive: string;
19
+ primaryDisabled: string;
20
+ primaryLightDefault: string;
21
+ primaryLightHover: string;
22
+ primaryLightActive: string;
23
+ }
24
+
25
+ interface ColorThemeTokens {
26
+ label: string;
27
+ swatch: string; // 色票展示色(永遠用亮色版)
28
+ light: ColorScale;
29
+ dark: ColorScale;
30
+ }
31
+
32
+ const COLOR_THEMES: Record<ColorTheme, ColorThemeTokens> = {
33
+ blue: {
34
+ label: '靛藍',
35
+ swatch: '#4165D7',
36
+ light: {
37
+ primary: '#4165D7',
38
+ primaryHover: '#3355C3',
39
+ primaryActive: '#2747AD',
40
+ primaryDisabled: '#A8B6EC',
41
+ primaryLightDefault: '#EEF0FB',
42
+ primaryLightHover: '#DCE1F8',
43
+ primaryLightActive: '#CAD1F4',
44
+ },
45
+ dark: {
46
+ primary: '#6B8EFF',
47
+ primaryHover: '#7A9DFF',
48
+ primaryActive: '#89AAFF',
49
+ primaryDisabled: '#273A7A',
50
+ primaryLightDefault: '#141B38',
51
+ primaryLightHover: '#182044',
52
+ primaryLightActive: '#1C264F',
53
+ },
54
+ },
55
+ violet: {
56
+ label: '紫羅蘭',
57
+ swatch: '#7C3AED',
58
+ light: {
59
+ primary: '#7C3AED',
60
+ primaryHover: '#6D31D4',
61
+ primaryActive: '#5E28BB',
62
+ primaryDisabled: '#C4A3F6',
63
+ primaryLightDefault: '#F3EFFE',
64
+ primaryLightHover: '#E8DCFC',
65
+ primaryLightActive: '#DCC9FA',
66
+ },
67
+ dark: {
68
+ primary: '#A78BFA',
69
+ primaryHover: '#B89DFB',
70
+ primaryActive: '#C9AFFC',
71
+ primaryDisabled: '#3D207F',
72
+ primaryLightDefault: '#1A1030',
73
+ primaryLightHover: '#1F1438',
74
+ primaryLightActive: '#241840',
75
+ },
76
+ },
77
+ teal: {
78
+ label: '青碧',
79
+ swatch: '#0891B2',
80
+ light: {
81
+ primary: '#0891B2',
82
+ primaryHover: '#0779A0',
83
+ primaryActive: '#05618E',
84
+ primaryDisabled: '#7BCFE6',
85
+ primaryLightDefault: '#ECFCFF',
86
+ primaryLightHover: '#D0F4FB',
87
+ primaryLightActive: '#B4ECF7',
88
+ },
89
+ dark: {
90
+ primary: '#22D3EE',
91
+ primaryHover: '#38DAEF',
92
+ primaryActive: '#4EE0F0',
93
+ primaryDisabled: '#073E50',
94
+ primaryLightDefault: '#051A20',
95
+ primaryLightHover: '#071F28',
96
+ primaryLightActive: '#08252F',
97
+ },
98
+ },
99
+ rose: {
100
+ label: '玫瑰',
101
+ swatch: '#E11D48',
102
+ light: {
103
+ primary: '#E11D48',
104
+ primaryHover: '#C91940',
105
+ primaryActive: '#B21538',
106
+ primaryDisabled: '#F18FA4',
107
+ primaryLightDefault: '#FFF0F3',
108
+ primaryLightHover: '#FFE0E7',
109
+ primaryLightActive: '#FFD0DB',
110
+ },
111
+ dark: {
112
+ primary: '#FB7185',
113
+ primaryHover: '#FC8595',
114
+ primaryActive: '#FDA0AD',
115
+ primaryDisabled: '#6B0F22',
116
+ primaryLightDefault: '#250912',
117
+ primaryLightHover: '#2C0C16',
118
+ primaryLightActive: '#340F1A',
119
+ },
120
+ },
121
+ };
122
+
123
+ function applyColorTheme(theme: ColorTheme, isDark: boolean) {
124
+ const scale = COLOR_THEMES[theme][isDark ? 'dark' : 'light'];
125
+ const b = document.body;
126
+ b.style.setProperty('--semi-color-primary', scale.primary);
127
+ b.style.setProperty('--semi-color-primary-hover', scale.primaryHover);
128
+ b.style.setProperty('--semi-color-primary-active', scale.primaryActive);
129
+ b.style.setProperty('--semi-color-primary-disabled', scale.primaryDisabled);
130
+ b.style.setProperty('--semi-color-primary-light-default', scale.primaryLightDefault);
131
+ b.style.setProperty('--semi-color-primary-light-hover', scale.primaryLightHover);
132
+ b.style.setProperty('--semi-color-primary-light-active', scale.primaryLightActive);
133
+ b.style.setProperty('--semi-color-focus-border', scale.primary);
134
+ b.style.setProperty('--semi-color-link', scale.primary);
135
+ b.style.setProperty('--semi-color-link-hover', scale.primaryHover);
136
+ b.style.setProperty('--semi-color-link-active', scale.primaryActive);
137
+ }
138
+
139
+ // ── 佈局模式選項 ─────────────────────────────────────────
140
+
141
+ interface LayoutOption {
142
+ value: LayoutMode;
143
+ label: string;
144
+ icon: React.ReactNode;
145
+ description: string;
146
+ }
147
+
148
+ const LAYOUT_OPTIONS: LayoutOption[] = [
149
+ { value: 'side', label: '側邊導航', icon: <IconSidebar size="large" />, description: '左側固定選單' },
150
+ { value: 'top', label: '頂部導航', icon: <IconColumnsStroked size="large" />, description: '頂部水平選單' },
151
+ { value: 'mix', label: '混合導航', icon: <IconGridView size="large" />, description: '頂部一級 + 左側二級' },
152
+ { value: 'double', label: '雙欄導航', icon: <IconVersionStroked size="large" />, description: '圖示欄 + 文字欄' },
153
+ ];
154
+
155
+ // ── 子元件 ───────────────────────────────────────────────
156
+
157
+ function LayoutCard({
158
+ option,
159
+ active,
160
+ onClick,
161
+ }: {
162
+ option: LayoutOption;
163
+ active: boolean;
164
+ onClick: () => void;
165
+ }) {
166
+ return (
167
+ <button
168
+ type="button"
169
+ onClick={onClick}
170
+ style={{
171
+ display: 'flex',
172
+ flexDirection: 'column',
173
+ alignItems: 'center',
174
+ gap: 6,
175
+ padding: '12px 8px',
176
+ borderRadius: 8,
177
+ border: `2px solid ${active ? 'var(--semi-color-primary)' : 'var(--semi-color-border)'}`,
178
+ backgroundColor: active ? 'var(--semi-color-primary-light-default)' : 'var(--semi-color-bg-1)',
179
+ cursor: 'pointer',
180
+ transition: 'all 0.2s',
181
+ flex: 1,
182
+ }}
183
+ >
184
+ <span style={{ color: active ? 'var(--semi-color-primary)' : 'var(--semi-color-text-2)', fontSize: 20 }}>
185
+ {option.icon}
186
+ </span>
187
+ <Text
188
+ strong={active}
189
+ style={{
190
+ fontSize: 12,
191
+ color: active ? 'var(--semi-color-primary)' : 'var(--semi-color-text-1)',
192
+ textAlign: 'center',
193
+ lineHeight: 1.3,
194
+ }}
195
+ >
196
+ {option.label}
197
+ </Text>
198
+ </button>
199
+ );
200
+ }
201
+
202
+ function ColorSwatch({
203
+ themeKey,
204
+ tokens,
205
+ active,
206
+ onClick,
207
+ }: {
208
+ themeKey: ColorTheme;
209
+ tokens: ColorThemeTokens;
210
+ active: boolean;
211
+ onClick: () => void;
212
+ }) {
213
+ return (
214
+ <Tooltip content={tokens.label} position="top">
215
+ <button
216
+ type="button"
217
+ onClick={onClick}
218
+ aria-label={tokens.label}
219
+ style={{
220
+ display: 'flex',
221
+ flexDirection: 'column',
222
+ alignItems: 'center',
223
+ gap: 5,
224
+ background: 'none',
225
+ border: 'none',
226
+ cursor: 'pointer',
227
+ padding: '4px 6px',
228
+ borderRadius: 6,
229
+ transition: 'background 0.15s',
230
+ }}
231
+ >
232
+ <span
233
+ style={{
234
+ display: 'block',
235
+ width: 28,
236
+ height: 28,
237
+ borderRadius: '50%',
238
+ background: tokens.swatch,
239
+ boxShadow: active
240
+ ? `0 0 0 2px var(--semi-color-bg-0), 0 0 0 4px ${tokens.swatch}`
241
+ : `0 0 0 2px transparent`,
242
+ transition: 'box-shadow 0.2s',
243
+ }}
244
+ />
245
+ <Text
246
+ style={{
247
+ fontSize: 11,
248
+ color: active ? 'var(--semi-color-primary)' : 'var(--semi-color-text-2)',
249
+ fontWeight: active ? 600 : 400,
250
+ whiteSpace: 'nowrap',
251
+ }}
252
+ >
253
+ {tokens.label}
254
+ </Text>
255
+ </button>
256
+ </Tooltip>
257
+ );
258
+ }
259
+
260
+ function SectionTitle({ children }: { children: React.ReactNode }) {
261
+ return (
262
+ <Text
263
+ type="tertiary"
264
+ style={{ fontSize: 12, fontWeight: 600, letterSpacing: '0.05em', textTransform: 'uppercase' }}
265
+ >
266
+ {children}
267
+ </Text>
268
+ );
269
+ }
270
+
271
+ function SettingRow({
272
+ label,
273
+ description,
274
+ children,
275
+ }: {
276
+ label: string;
277
+ description?: string;
278
+ children: React.ReactNode;
279
+ }) {
280
+ return (
281
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 0' }}>
282
+ <div>
283
+ <Text style={{ fontSize: 14 }}>{label}</Text>
284
+ {description && (
285
+ <Text type="tertiary" style={{ fontSize: 12, display: 'block' }}>
286
+ {description}
287
+ </Text>
288
+ )}
289
+ </div>
290
+ {children}
291
+ </div>
292
+ );
293
+ }
294
+
295
+ // ── 主元件 ───────────────────────────────────────────────
296
+
297
+ export function SettingDrawer() {
298
+ const {
299
+ settingDrawerOpen,
300
+ setSettingDrawerOpen,
301
+ layoutMode,
302
+ setLayoutMode,
303
+ themeMode,
304
+ toggleTheme,
305
+ colorTheme,
306
+ setColorTheme,
307
+ fixedHeader,
308
+ setFixedHeader,
309
+ showBreadcrumb,
310
+ setShowBreadcrumb,
311
+ } = useLayoutStore();
312
+
313
+ // 同步暗色模式 + 色系 CSS 變數
314
+ useEffect(() => {
315
+ const isDark = themeMode === 'dark';
316
+ if (isDark) {
317
+ document.body.setAttribute('theme-mode', 'dark');
318
+ } else {
319
+ document.body.removeAttribute('theme-mode');
320
+ }
321
+ applyColorTheme(colorTheme, isDark);
322
+ }, [themeMode, colorTheme]);
323
+
324
+ return (
325
+ <SideSheet
326
+ title={<Title heading={5} style={{ margin: 0 }}>佈局設定</Title>}
327
+ visible={settingDrawerOpen}
328
+ onCancel={() => setSettingDrawerOpen(false)}
329
+ footer={null}
330
+ width={300}
331
+ placement="right"
332
+ >
333
+ {/* 佈局模式 */}
334
+ <SectionTitle>佈局模式</SectionTitle>
335
+ <div style={{ display: 'flex', gap: 8, marginTop: 12, marginBottom: 4 }}>
336
+ {LAYOUT_OPTIONS.map(option => (
337
+ <LayoutCard
338
+ key={option.value}
339
+ option={option}
340
+ active={layoutMode === option.value}
341
+ onClick={() => setLayoutMode(option.value)}
342
+ />
343
+ ))}
344
+ </div>
345
+
346
+ <Divider style={{ margin: '20px 0' }} />
347
+
348
+ {/* 色系主題 */}
349
+ <SectionTitle>色系主題</SectionTitle>
350
+ <div style={{ display: 'flex', justifyContent: 'space-around', marginTop: 14, marginBottom: 4 }}>
351
+ {(Object.entries(COLOR_THEMES) as [ColorTheme, ColorThemeTokens][]).map(([key, tokens]) => (
352
+ <ColorSwatch
353
+ key={key}
354
+ themeKey={key}
355
+ tokens={tokens}
356
+ active={colorTheme === key}
357
+ onClick={() => setColorTheme(key)}
358
+ />
359
+ ))}
360
+ </div>
361
+
362
+ <Divider style={{ margin: '20px 0' }} />
363
+
364
+ {/* 主題切換 */}
365
+ <SectionTitle>主題模式</SectionTitle>
366
+ <SettingRow
367
+ label={themeMode === 'dark' ? '暗色模式' : '亮色模式'}
368
+ description="切換介面整體配色主題"
369
+ >
370
+ <Switch
371
+ checked={themeMode === 'dark'}
372
+ onChange={toggleTheme}
373
+ checkedText="暗"
374
+ uncheckedText="亮"
375
+ />
376
+ </SettingRow>
377
+
378
+ <Divider style={{ margin: '20px 0' }} />
379
+
380
+ {/* 介面配置 */}
381
+ <SectionTitle>介面配置</SectionTitle>
382
+ <SettingRow label="固定頂欄" description="頁面捲動時 Header 保持固定">
383
+ <Switch checked={fixedHeader} onChange={checked => setFixedHeader(checked)} />
384
+ </SettingRow>
385
+ <SettingRow label="顯示麵包屑" description="在內容區頂部顯示路徑導覽">
386
+ <Switch checked={showBreadcrumb} onChange={checked => setShowBreadcrumb(checked)} />
387
+ </SettingRow>
388
+ </SideSheet>
389
+ );
390
+ }
@@ -0,0 +1 @@
1
+ /// <reference types='@modern-js/app-tools/types' />
@@ -0,0 +1,3 @@
1
+ import { defineRuntimeConfig } from '@modern-js/runtime';
2
+
3
+ export default defineRuntimeConfig({});
@@ -0,0 +1,7 @@
1
+ export default function Workplace() {
2
+ return (
3
+ <div>
4
+ <h1>Workplace</h1>
5
+ </div>
6
+ )
7
+ }