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,491 @@
1
+ // ============================================================
2
+ // Login Page — Airy Enterprise Split Layout
3
+ // 登入頁:左側漸層品牌面板 + 右側乾淨表單
4
+ // ============================================================
5
+
6
+ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800;900&display=swap');
7
+
8
+ // ── 動畫定義 ──────────────────────────────────────────────
9
+
10
+ @keyframes lp-blob-float {
11
+ 0%, 100% { transform: translate(0, 0) scale(1); }
12
+ 33% { transform: translate(24px, -18px) scale(1.04); }
13
+ 66% { transform: translate(-16px, 12px) scale(0.97); }
14
+ }
15
+
16
+ @keyframes lp-fade-up {
17
+ from { opacity: 0; transform: translateY(20px); }
18
+ to { opacity: 1; transform: translateY(0); }
19
+ }
20
+
21
+ @keyframes lp-slide-right {
22
+ from { opacity: 0; transform: translateX(28px); }
23
+ to { opacity: 1; transform: translateX(0); }
24
+ }
25
+
26
+ @keyframes lp-shimmer {
27
+ 0% { background-position: -200% center; }
28
+ 100% { background-position: 200% center; }
29
+ }
30
+
31
+ @keyframes lp-pulse-dot {
32
+ 0%, 100% { opacity: 0.6; transform: scale(0.9); }
33
+ 50% { opacity: 1; transform: scale(1.1); }
34
+ }
35
+
36
+ // ── 根容器 ────────────────────────────────────────────────
37
+
38
+ .lp-root {
39
+ position: fixed;
40
+ inset: 0;
41
+ display: flex;
42
+ font-family: 'Outfit', -apple-system, BlinkMacSystemFont, sans-serif;
43
+ overflow: hidden;
44
+ z-index: 9999;
45
+ }
46
+
47
+ // ── 左側面板 ──────────────────────────────────────────────
48
+
49
+ .lp-left {
50
+ position: relative;
51
+ flex: 0 0 52%;
52
+ overflow: hidden;
53
+ background:
54
+ linear-gradient(145deg,
55
+ #dde8ff 0%,
56
+ #e6d9ff 35%,
57
+ #d8eeff 65%,
58
+ #e0f4ff 100%
59
+ );
60
+ display: flex;
61
+ flex-direction: column;
62
+
63
+ @media (max-width: 900px) {
64
+ display: none;
65
+ }
66
+ }
67
+
68
+ // 浮動裝飾圓球
69
+ .lp-blob {
70
+ position: absolute;
71
+ border-radius: 50%;
72
+ pointer-events: none;
73
+ filter: blur(72px);
74
+ opacity: 0.75;
75
+
76
+ &.lp-blob-1 {
77
+ width: 500px;
78
+ height: 500px;
79
+ background: radial-gradient(circle, rgba(140, 170, 255, 0.7) 0%, transparent 65%);
80
+ top: -160px;
81
+ left: -120px;
82
+ animation: lp-blob-float 9s ease-in-out infinite;
83
+ }
84
+
85
+ &.lp-blob-2 {
86
+ width: 420px;
87
+ height: 420px;
88
+ background: radial-gradient(circle, rgba(180, 145, 255, 0.6) 0%, transparent 65%);
89
+ bottom: -80px;
90
+ left: 60px;
91
+ animation: lp-blob-float 12s ease-in-out 3s infinite reverse;
92
+ }
93
+
94
+ &.lp-blob-3 {
95
+ width: 360px;
96
+ height: 360px;
97
+ background: radial-gradient(circle, rgba(120, 210, 255, 0.5) 0%, transparent 65%);
98
+ top: 180px;
99
+ right: -80px;
100
+ animation: lp-blob-float 15s ease-in-out 6s infinite;
101
+ }
102
+ }
103
+
104
+ // 細格紋紋理
105
+ .lp-grid {
106
+ position: absolute;
107
+ inset: 0;
108
+ background-image:
109
+ linear-gradient(rgba(100, 130, 220, 0.1) 1px, transparent 1px),
110
+ linear-gradient(90deg, rgba(100, 130, 220, 0.1) 1px, transparent 1px);
111
+ background-size: 36px 36px;
112
+ pointer-events: none;
113
+ }
114
+
115
+ // 左側內容區
116
+ .lp-left-inner {
117
+ position: relative;
118
+ z-index: 1;
119
+ flex: 1;
120
+ display: flex;
121
+ flex-direction: column;
122
+ justify-content: center;
123
+ padding: 48px 56px;
124
+ gap: 40px;
125
+ }
126
+
127
+ // 品牌識別
128
+ .lp-brand {
129
+ display: flex;
130
+ align-items: center;
131
+ gap: 10px;
132
+ animation: lp-fade-up 0.6s ease both;
133
+ }
134
+
135
+ .lp-brand-mark {
136
+ width: 36px;
137
+ height: 36px;
138
+ border-radius: 10px;
139
+ background: linear-gradient(135deg, #4f7dff 0%, #7b52ff 100%);
140
+ color: #fff;
141
+ font-weight: 800;
142
+ font-size: 18px;
143
+ display: flex;
144
+ align-items: center;
145
+ justify-content: center;
146
+ box-shadow: 0 4px 16px rgba(79, 125, 255, 0.4);
147
+ flex-shrink: 0;
148
+ }
149
+
150
+ .lp-brand-name {
151
+ font-size: 18px;
152
+ font-weight: 700;
153
+ color: #1a2340;
154
+ letter-spacing: -0.01em;
155
+ }
156
+
157
+
158
+ // 主標語
159
+ .lp-headline {
160
+ flex: 1;
161
+ display: flex;
162
+ flex-direction: column;
163
+ justify-content: center;
164
+ animation: lp-fade-up 0.6s ease 0.15s both;
165
+ }
166
+
167
+ .lp-headline-title {
168
+ font-size: clamp(36px, 3.5vw, 52px);
169
+ font-weight: 800;
170
+ line-height: 1.15;
171
+ letter-spacing: -0.03em;
172
+ color: #1a2340;
173
+ margin: 0 0 20px;
174
+ }
175
+
176
+ .lp-headline-accent {
177
+ background: linear-gradient(
178
+ 120deg,
179
+ #4f7dff 0%,
180
+ #9b6dff 40%,
181
+ #4fb8ff 80%,
182
+ #4f7dff 100%
183
+ );
184
+ background-size: 200% auto;
185
+ -webkit-background-clip: text;
186
+ -webkit-text-fill-color: transparent;
187
+ background-clip: text;
188
+ animation: lp-shimmer 5s linear infinite;
189
+ }
190
+
191
+ .lp-headline-sub {
192
+ font-size: 15px;
193
+ color: #4a5578;
194
+ line-height: 1.7;
195
+ font-weight: 400;
196
+ margin: 0;
197
+ }
198
+
199
+
200
+ // 左側底部版權
201
+ .lp-left-footer {
202
+ position: relative;
203
+ z-index: 1;
204
+ padding: 24px 56px;
205
+ font-size: 11px;
206
+ color: #6b789a;
207
+ font-weight: 500;
208
+ letter-spacing: 0.02em;
209
+ }
210
+
211
+ // ── 右側表單面板 ───────────────────────────────────────────
212
+
213
+ .lp-right {
214
+ flex: 1;
215
+ display: flex;
216
+ align-items: center;
217
+ justify-content: center;
218
+ background: #ffffff;
219
+ position: relative;
220
+ overflow: hidden;
221
+
222
+ // 極淡的點陣紋理
223
+ &::before {
224
+ content: '';
225
+ position: absolute;
226
+ inset: 0;
227
+ background-image: radial-gradient(circle, rgba(100, 130, 200, 0.08) 1px, transparent 1px);
228
+ background-size: 24px 24px;
229
+ pointer-events: none;
230
+ }
231
+ }
232
+
233
+ // 表單包裹容器
234
+ .lp-form-wrap {
235
+ position: relative;
236
+ z-index: 1;
237
+ width: 100%;
238
+ max-width: 400px;
239
+ padding: 0 40px;
240
+ animation: lp-slide-right 0.6s ease 0.2s both;
241
+
242
+ @media (max-width: 900px) {
243
+ max-width: 440px;
244
+ padding: 32px 24px;
245
+ }
246
+ }
247
+
248
+ // 表單頂部 Logo
249
+ .lp-form-logo {
250
+ display: flex;
251
+ align-items: center;
252
+ justify-content: center;
253
+ margin-bottom: 32px;
254
+ }
255
+
256
+ .lp-form-logo-mark {
257
+ width: 52px;
258
+ height: 52px;
259
+ border-radius: 16px;
260
+ background: linear-gradient(135deg, #4f7dff 0%, #7b52ff 100%);
261
+ color: #fff;
262
+ font-weight: 900;
263
+ font-size: 24px;
264
+ display: flex;
265
+ align-items: center;
266
+ justify-content: center;
267
+ box-shadow:
268
+ 0 8px 32px rgba(79, 125, 255, 0.35),
269
+ 0 2px 8px rgba(79, 125, 255, 0.2);
270
+ font-family: 'Outfit', sans-serif;
271
+ letter-spacing: -0.02em;
272
+ }
273
+
274
+ // 標題與副標題
275
+ .lp-form-title {
276
+ font-size: 28px;
277
+ font-weight: 800;
278
+ color: #0f1729;
279
+ text-align: center;
280
+ margin: 0 0 8px;
281
+ letter-spacing: -0.03em;
282
+ }
283
+
284
+ .lp-form-subtitle {
285
+ font-size: 14px;
286
+ color: #7a85a0;
287
+ text-align: center;
288
+ margin: 0 0 32px;
289
+ font-weight: 400;
290
+ line-height: 1.6;
291
+ }
292
+
293
+ // ── 表單欄位 ──────────────────────────────────────────────
294
+
295
+ .lp-fields {
296
+ display: flex;
297
+ flex-direction: column;
298
+ gap: 18px;
299
+ margin-bottom: 24px;
300
+ }
301
+
302
+ .lp-field-group {
303
+ display: flex;
304
+ flex-direction: column;
305
+ gap: 6px;
306
+ }
307
+
308
+ .lp-label-row {
309
+ display: flex;
310
+ align-items: center;
311
+ justify-content: space-between;
312
+ }
313
+
314
+ .lp-label {
315
+ font-size: 13px;
316
+ font-weight: 600;
317
+ color: #2d3a5a;
318
+ letter-spacing: 0.01em;
319
+ }
320
+
321
+ .lp-forgot {
322
+ all: unset;
323
+ font-size: 12px;
324
+ color: #4f7dff;
325
+ font-weight: 500;
326
+ cursor: pointer;
327
+ transition: opacity 0.15s ease;
328
+
329
+ &:hover {
330
+ opacity: 0.75;
331
+ text-decoration: underline;
332
+ }
333
+ }
334
+
335
+ // Semi Input 樣式覆蓋(強制淺色,不受主題影響)
336
+ .lp-input {
337
+ &.semi-input-wrapper {
338
+ border-radius: 10px !important;
339
+ border-color: #e0e6f0 !important;
340
+ background: #f8faff !important;
341
+ transition: border-color 0.2s ease, box-shadow 0.2s ease !important;
342
+
343
+ &:hover {
344
+ border-color: #b8c8f0 !important;
345
+ }
346
+
347
+ &.semi-input-wrapper-focus {
348
+ border-color: #4f7dff !important;
349
+ background: #fff !important;
350
+ box-shadow: 0 0 0 3px rgba(79, 125, 255, 0.12) !important;
351
+ }
352
+
353
+ // 強制文字顏色,避免暗色模式下白字不可見
354
+ .semi-input {
355
+ color: #0f1729 !important;
356
+ caret-color: #4f7dff !important;
357
+
358
+ &::placeholder {
359
+ color: #a8b2c8 !important;
360
+ opacity: 1 !important;
361
+ }
362
+ }
363
+
364
+ // 前綴 / 後綴 icon 顏色
365
+ .semi-input-prefix,
366
+ .semi-input-suffix {
367
+ color: #8a96b0 !important;
368
+ }
369
+ }
370
+ }
371
+
372
+ // 密碼顯示/隱藏按鈕
373
+ .lp-eye-btn {
374
+ all: unset;
375
+ display: inline-flex;
376
+ align-items: center;
377
+ color: #8a96b0;
378
+ cursor: pointer;
379
+ padding: 2px;
380
+ transition: color 0.15s ease;
381
+
382
+ &:hover {
383
+ color: #4f7dff;
384
+ }
385
+ }
386
+
387
+ // ── 登入按鈕 ──────────────────────────────────────────────
388
+
389
+ .lp-submit-btn {
390
+ &.semi-button {
391
+ border-radius: 10px !important;
392
+ font-size: 15px !important;
393
+ font-weight: 700 !important;
394
+ height: 48px !important;
395
+ background: linear-gradient(135deg, #4f7dff 0%, #7b52ff 100%) !important;
396
+ border: none !important;
397
+ box-shadow: 0 6px 24px rgba(79, 125, 255, 0.35) !important;
398
+ transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease !important;
399
+ letter-spacing: 0.02em !important;
400
+
401
+ &:hover:not(:disabled) {
402
+ transform: translateY(-1px) !important;
403
+ box-shadow: 0 8px 28px rgba(79, 125, 255, 0.45) !important;
404
+ }
405
+
406
+ &:active:not(:disabled) {
407
+ transform: translateY(0) !important;
408
+ }
409
+ }
410
+ }
411
+
412
+ // ── 分隔線 ────────────────────────────────────────────────
413
+
414
+ .lp-divider {
415
+ display: flex;
416
+ align-items: center;
417
+ gap: 12px;
418
+ margin: 24px 0;
419
+ color: #b0baca;
420
+ font-size: 12px;
421
+ font-weight: 500;
422
+
423
+ &::before,
424
+ &::after {
425
+ content: '';
426
+ flex: 1;
427
+ height: 1px;
428
+ background: #eaeff8;
429
+ }
430
+
431
+ span {
432
+ white-space: nowrap;
433
+ padding: 0 4px;
434
+ }
435
+ }
436
+
437
+ // ── Google 登入按鈕 ────────────────────────────────────────
438
+
439
+ .lp-google-btn {
440
+ all: unset;
441
+ display: flex;
442
+ align-items: center;
443
+ justify-content: center;
444
+ gap: 10px;
445
+ width: 100%;
446
+ height: 46px;
447
+ border-radius: 10px;
448
+ border: 1.5px solid #e0e6f0;
449
+ background: #fff;
450
+ color: #2d3a5a;
451
+ font-size: 14px;
452
+ font-weight: 600;
453
+ cursor: pointer;
454
+ transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
455
+ box-sizing: border-box;
456
+ letter-spacing: 0.01em;
457
+
458
+ &:hover {
459
+ border-color: #b8c8f0;
460
+ background: #f8faff;
461
+ box-shadow: 0 2px 12px rgba(79, 125, 255, 0.1);
462
+ }
463
+
464
+ &:active {
465
+ background: #f0f5ff;
466
+ }
467
+ }
468
+
469
+ // ── 測試帳號提示 ──────────────────────────────────────────
470
+
471
+ .lp-hint {
472
+ margin: 20px 0 0;
473
+ text-align: center;
474
+ font-size: 12px;
475
+ color: #a0aac0;
476
+ font-weight: 400;
477
+ line-height: 1.6;
478
+
479
+ code {
480
+ display: inline-block;
481
+ padding: 1px 6px;
482
+ border-radius: 4px;
483
+ background: #f0f4ff;
484
+ border: 1px solid #dde8ff;
485
+ color: #4f7dff;
486
+ font-size: 11px;
487
+ font-family: 'Outfit', monospace;
488
+ font-weight: 600;
489
+ margin: 0 2px;
490
+ }
491
+ }
@@ -0,0 +1,204 @@
1
+ import {
2
+ IconEyeClosedSolid,
3
+ IconEyeOpened,
4
+ IconLock,
5
+ IconUserStroked,
6
+ } from '@douyinfe/semi-icons';
7
+ import { Button, Input, Toast } from '@douyinfe/semi-ui-19';
8
+ import { useNavigate } from '@modern-js/runtime/router';
9
+ import { useEffect, useState } from 'react';
10
+ import { useAuthStore } from '../../store/authStore';
11
+ import './index.less';
12
+
13
+
14
+ function GoogleIcon() {
15
+ return (
16
+ <svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true">
17
+ <path
18
+ fill="#4285F4"
19
+ d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
20
+ />
21
+ <path
22
+ fill="#34A853"
23
+ d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
24
+ />
25
+ <path
26
+ fill="#FBBC05"
27
+ d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"
28
+ />
29
+ <path
30
+ fill="#EA4335"
31
+ d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
32
+ />
33
+ </svg>
34
+ );
35
+ }
36
+
37
+ export default function LoginPage() {
38
+ const navigate = useNavigate();
39
+ const { login } = useAuthStore();
40
+ const [username, setUsername] = useState('');
41
+ const [password, setPassword] = useState('');
42
+ const [showPwd, setShowPwd] = useState(false);
43
+ const [loading, setLoading] = useState(false);
44
+
45
+ const handleLogin = async () => {
46
+ if (!username.trim() || !password.trim()) {
47
+ Toast.warning({ content: '請填寫帳號與密碼', duration: 2 });
48
+ return;
49
+ }
50
+ setLoading(true);
51
+ await new Promise(r => setTimeout(r, 700));
52
+ const ok = login(username.trim(), password);
53
+ setLoading(false);
54
+ if (ok) {
55
+ Toast.success({ content: '登入成功,歡迎回來!', duration: 2 });
56
+ navigate('/', { replace: true });
57
+ } else {
58
+ Toast.error({ content: '帳號或密碼錯誤,請確認後重試', duration: 3 });
59
+ }
60
+ };
61
+
62
+ const handleKeyDown = (e: React.KeyboardEvent) => {
63
+ if (e.key === 'Enter') handleLogin();
64
+ };
65
+
66
+ useEffect(() => {
67
+ document.title = '登入';
68
+ }, []);
69
+
70
+ return (
71
+ <div className="lp-root">
72
+ {/* ── 左側裝飾面板 ── */}
73
+ <div className="lp-left" aria-hidden="true">
74
+ <div className="lp-blob lp-blob-1" />
75
+ <div className="lp-blob lp-blob-2" />
76
+ <div className="lp-blob lp-blob-3" />
77
+ <div className="lp-grid" />
78
+
79
+ <div className="lp-left-inner">
80
+ {/* 品牌識別 */}
81
+ <div className="lp-brand">
82
+ <div className="lp-brand-mark">M</div>
83
+ <span className="lp-brand-name">ModSemi</span>
84
+ </div>
85
+
86
+ {/* 主標語 */}
87
+ <div className="lp-headline">
88
+ <h1 className="lp-headline-title">
89
+ 打造高效
90
+ <br />
91
+ <span className="lp-headline-accent">企業中後台</span>
92
+ <br />
93
+ 的現代框架
94
+ </h1>
95
+ <p className="lp-headline-sub">
96
+ Modern.js · Semi Design · Zustand
97
+ <br />
98
+ 配置驅動,開箱即用,四種佈局隨心切換。
99
+ </p>
100
+ </div>
101
+
102
+ </div>
103
+
104
+ {/* 底部版權 */}
105
+ <div className="lp-left-footer">
106
+ © 2026 ModSemi · Enterprise Edition
107
+ </div>
108
+ </div>
109
+
110
+ {/* ── 右側登入面板 ── */}
111
+ <div className="lp-right">
112
+ <div className="lp-form-wrap">
113
+ {/* Logo */}
114
+ <div className="lp-form-logo">
115
+ <div className="lp-form-logo-mark">M</div>
116
+ </div>
117
+
118
+ <h2 className="lp-form-title">歡迎回來</h2>
119
+ <p className="lp-form-subtitle">請登入您的帳號以繼續使用系統</p>
120
+
121
+ {/* 表單欄位 */}
122
+ <div className="lp-fields" onKeyDown={handleKeyDown}>
123
+ <div className="lp-field-group">
124
+ <label className="lp-label" htmlFor="lp-username">
125
+ 帳號
126
+ </label>
127
+ <Input
128
+ id="lp-username"
129
+ size="large"
130
+ prefix={<IconUserStroked />}
131
+ placeholder="輸入您的帳號"
132
+ value={username}
133
+ onChange={v => setUsername(v)}
134
+ autoComplete="username"
135
+ className="lp-input"
136
+ />
137
+ </div>
138
+
139
+ <div className="lp-field-group">
140
+ <div className="lp-label-row">
141
+ <label className="lp-label" htmlFor="lp-password">
142
+ 密碼
143
+ </label>
144
+ <button type="button" className="lp-forgot">
145
+ 忘記密碼?
146
+ </button>
147
+ </div>
148
+ <Input
149
+ id="lp-password"
150
+ size="large"
151
+ prefix={<IconLock />}
152
+ suffix={
153
+ <button
154
+ type="button"
155
+ className="lp-eye-btn"
156
+ onClick={() => setShowPwd(v => !v)}
157
+ aria-label={showPwd ? '隱藏密碼' : '顯示密碼'}
158
+ >
159
+ {showPwd ? <IconEyeClosedSolid /> : <IconEyeOpened />}
160
+ </button>
161
+ }
162
+ type={showPwd ? 'text' : 'password'}
163
+ placeholder="輸入您的密碼"
164
+ value={password}
165
+ onChange={v => setPassword(v)}
166
+ autoComplete="current-password"
167
+ className="lp-input"
168
+ />
169
+ </div>
170
+ </div>
171
+
172
+ {/* 登入按鈕 */}
173
+ <Button
174
+ theme="solid"
175
+ type="primary"
176
+ size="large"
177
+ block
178
+ loading={loading}
179
+ onClick={handleLogin}
180
+ className="lp-submit-btn"
181
+ >
182
+ {loading ? '登入中...' : '登入'}
183
+ </Button>
184
+
185
+ {/* 分隔線 */}
186
+ <div className="lp-divider">
187
+ <span>或使用以下方式登入</span>
188
+ </div>
189
+
190
+ {/* Google 登入 */}
191
+ <button type="button" className="lp-google-btn">
192
+ <GoogleIcon />
193
+ <span>使用 Google 帳號登入</span>
194
+ </button>
195
+
196
+ {/* 提示文字 */}
197
+ <p className="lp-hint">
198
+ 測試帳號:<code>admin</code> / <code>admin.modsemi</code>
199
+ </p>
200
+ </div>
201
+ </div>
202
+ </div>
203
+ );
204
+ }