create-simpleadmin-ui 2.0.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 (55) hide show
  1. package/README.md +53 -0
  2. package/bin/create-simpleadmin-ui.mjs +2 -0
  3. package/package.json +35 -0
  4. package/src/index.mjs +557 -0
  5. package/src/sync-template.mjs +64 -0
  6. package/template/.env.development +4 -0
  7. package/template/.env.production.example +4 -0
  8. package/template/.vscode/extensions.json +3 -0
  9. package/template/README.md +26 -0
  10. package/template/index.html +19 -0
  11. package/template/package.json +30 -0
  12. package/template/public/vite.svg +1 -0
  13. package/template/src/App.vue +3 -0
  14. package/template/src/components/AppBreadcrumb.vue +132 -0
  15. package/template/src/components/AppPage.vue +23 -0
  16. package/template/src/components/ChangePasswordDialog.vue +97 -0
  17. package/template/src/components/ProfileEditDialog.vue +142 -0
  18. package/template/src/components/RichTextEditor.vue +121 -0
  19. package/template/src/components/SidebarMenuItem.vue +56 -0
  20. package/template/src/components/TagsView.vue +172 -0
  21. package/template/src/constants/menuIcons.ts +205 -0
  22. package/template/src/directives/permission.ts +13 -0
  23. package/template/src/env.d.ts +22 -0
  24. package/template/src/layouts/MainLayout.vue +410 -0
  25. package/template/src/main.ts +27 -0
  26. package/template/src/router/index.ts +198 -0
  27. package/template/src/stores/tagsView.ts +161 -0
  28. package/template/src/stores/theme.ts +68 -0
  29. package/template/src/stores/user.ts +191 -0
  30. package/template/src/styles/global.css +314 -0
  31. package/template/src/utils/datetime.ts +34 -0
  32. package/template/src/utils/request.ts +324 -0
  33. package/template/src/utils/requestSign.ts +162 -0
  34. package/template/src/views/error/NotFound.vue +49 -0
  35. package/template/src/views/home/HomeView.vue +1177 -0
  36. package/template/src/views/login/LoginView.vue +576 -0
  37. package/template/src/views/monitor/loginlog/index.vue +366 -0
  38. package/template/src/views/monitor/online/index.vue +298 -0
  39. package/template/src/views/monitor/operlog/index.vue +492 -0
  40. package/template/src/views/system/config/index.vue +407 -0
  41. package/template/src/views/system/dept/index.vue +517 -0
  42. package/template/src/views/system/dict/index.vue +747 -0
  43. package/template/src/views/system/file/index.vue +1031 -0
  44. package/template/src/views/system/menu/index.vue +822 -0
  45. package/template/src/views/system/message/index.vue +422 -0
  46. package/template/src/views/system/notice/index.vue +578 -0
  47. package/template/src/views/system/openApp/index.vue +596 -0
  48. package/template/src/views/system/post/index.vue +495 -0
  49. package/template/src/views/system/role/index.vue +546 -0
  50. package/template/src/views/system/user/index.vue +373 -0
  51. package/template/src/vite-env.d.ts +1 -0
  52. package/template/tsconfig.app.json +30 -0
  53. package/template/tsconfig.json +7 -0
  54. package/template/tsconfig.node.json +24 -0
  55. package/template/vite.config.ts +22 -0
@@ -0,0 +1,576 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
3
+ import { useRoute, useRouter } from 'vue-router'
4
+ import { ElMessage } from 'element-plus'
5
+ import { useUserStore } from '@/stores/user'
6
+ import { resetDynamicRoutes, setupDynamicRoutes } from '@/router'
7
+
8
+ const router = useRouter()
9
+ const route = useRoute()
10
+ const store = useUserStore()
11
+
12
+ /** 登录方式:password | sms */
13
+ const loginMode = ref<'password' | 'sms'>('password')
14
+ /** 是否启用手机号登录(来自 SysConfig: sys.auth.enableSmsLogin) */
15
+ const enableSmsLogin = ref(true)
16
+ const optionsLoaded = ref(false)
17
+ const loading = ref(false)
18
+ const smsSending = ref(false)
19
+ const smsCountdown = ref(0)
20
+ let smsTimer: ReturnType<typeof setInterval> | null = null
21
+
22
+ const passwordForm = reactive({
23
+ userName: 'admin',
24
+ password: 'admin123',
25
+ captchaId: '',
26
+ captchaCode: '',
27
+ })
28
+
29
+ const smsForm = reactive({
30
+ phone: '13800000000',
31
+ smsCode: '',
32
+ })
33
+
34
+ const captchaImage = ref('')
35
+ const captchaLoading = ref(false)
36
+
37
+ const brandHint = computed(() =>
38
+ enableSmsLogin.value ? '选择账密或手机验证码登录' : '使用账号密码登录',
39
+ )
40
+
41
+ async function refreshCaptcha() {
42
+ captchaLoading.value = true
43
+ try {
44
+ const data = await store.fetchCaptcha()
45
+ passwordForm.captchaId = data.captchaId
46
+ passwordForm.captchaCode = data.devCode || ''
47
+ captchaImage.value = data.imageBase64
48
+ } catch (e) {
49
+ console.error(e)
50
+ passwordForm.captchaId = ''
51
+ captchaImage.value = ''
52
+ } finally {
53
+ captchaLoading.value = false
54
+ }
55
+ }
56
+
57
+ function clearSmsTimer() {
58
+ if (smsTimer) {
59
+ clearInterval(smsTimer)
60
+ smsTimer = null
61
+ }
62
+ }
63
+
64
+ function startSmsCountdown(seconds: number) {
65
+ clearSmsTimer()
66
+ smsCountdown.value = Math.max(0, seconds)
67
+ if (smsCountdown.value <= 0) return
68
+ smsTimer = setInterval(() => {
69
+ smsCountdown.value -= 1
70
+ if (smsCountdown.value <= 0) clearSmsTimer()
71
+ }, 1000)
72
+ }
73
+
74
+ async function onSendSms() {
75
+ if (!enableSmsLogin.value) {
76
+ ElMessage.warning('未启用手机号登录')
77
+ return
78
+ }
79
+ const phone = smsForm.phone.trim()
80
+ if (!/^1[3-9]\d{9}$/.test(phone)) {
81
+ ElMessage.warning('请输入正确的手机号')
82
+ return
83
+ }
84
+ smsSending.value = true
85
+ try {
86
+ const data = await store.sendSmsCode(phone)
87
+ startSmsCountdown(data.intervalSeconds || 60)
88
+ if (data.devCode) {
89
+ smsForm.smsCode = data.devCode
90
+ ElMessage.success(`验证码已发送(开发态:${data.devCode})`)
91
+ } else {
92
+ ElMessage.success('验证码已发送')
93
+ }
94
+ } catch (e) {
95
+ console.error(e)
96
+ } finally {
97
+ smsSending.value = false
98
+ }
99
+ }
100
+
101
+ async function afterLoginSuccess() {
102
+ resetDynamicRoutes()
103
+ try {
104
+ await setupDynamicRoutes()
105
+ } catch (e) {
106
+ console.error(e)
107
+ }
108
+ ElMessage.success('登录成功')
109
+ const redirect = (route.query.redirect as string) || '/index'
110
+ await router.replace(redirect === '/' ? '/index' : redirect)
111
+ }
112
+
113
+ async function onSubmit() {
114
+ loading.value = true
115
+ try {
116
+ if (loginMode.value === 'password') {
117
+ if (!passwordForm.userName.trim() || !passwordForm.password) {
118
+ ElMessage.warning('请输入账号和密码')
119
+ return
120
+ }
121
+ if (!passwordForm.captchaId) {
122
+ ElMessage.warning('验证码未加载,请点击图片刷新')
123
+ await refreshCaptcha()
124
+ return
125
+ }
126
+ if (!passwordForm.captchaCode.trim()) {
127
+ ElMessage.warning('请输入验证码')
128
+ return
129
+ }
130
+ await store.login({
131
+ loginType: 'password',
132
+ userName: passwordForm.userName.trim(),
133
+ password: passwordForm.password,
134
+ captchaId: passwordForm.captchaId,
135
+ captchaCode: passwordForm.captchaCode.trim(),
136
+ })
137
+ } else {
138
+ if (!enableSmsLogin.value) {
139
+ ElMessage.warning('未启用手机号登录')
140
+ loginMode.value = 'password'
141
+ return
142
+ }
143
+ const phone = smsForm.phone.trim()
144
+ if (!/^1[3-9]\d{9}$/.test(phone)) {
145
+ ElMessage.warning('请输入正确的手机号')
146
+ return
147
+ }
148
+ if (!smsForm.smsCode.trim()) {
149
+ ElMessage.warning('请输入短信验证码')
150
+ return
151
+ }
152
+ await store.login({
153
+ loginType: 'sms',
154
+ phone,
155
+ smsCode: smsForm.smsCode.trim(),
156
+ })
157
+ }
158
+ await afterLoginSuccess()
159
+ } catch (e: unknown) {
160
+ console.error(e)
161
+ if (loginMode.value === 'password') await refreshCaptcha()
162
+ } finally {
163
+ loading.value = false
164
+ }
165
+ }
166
+
167
+ function switchMode(mode: 'password' | 'sms') {
168
+ if (mode === 'sms' && !enableSmsLogin.value) return
169
+ loginMode.value = mode
170
+ if (mode === 'password' && !captchaImage.value) refreshCaptcha()
171
+ }
172
+
173
+ async function loadLoginOptions() {
174
+ try {
175
+ const data = await store.fetchLoginOptions()
176
+ enableSmsLogin.value = data?.enableSmsLogin !== false
177
+ } catch (e) {
178
+ console.error(e)
179
+ // 拉取失败时保守:仅展示账密,避免误开短信入口
180
+ enableSmsLogin.value = false
181
+ } finally {
182
+ optionsLoaded.value = true
183
+ if (!enableSmsLogin.value) loginMode.value = 'password'
184
+ }
185
+ }
186
+
187
+ onMounted(async () => {
188
+ await loadLoginOptions()
189
+ refreshCaptcha()
190
+ })
191
+
192
+ onUnmounted(() => {
193
+ clearSmsTimer()
194
+ })
195
+ </script>
196
+
197
+ <template>
198
+ <div class="login">
199
+ <div class="login__visual" aria-hidden="true">
200
+ <div class="login__orb login__orb--a" />
201
+ <div class="login__orb login__orb--b" />
202
+ <div class="login__copy">
203
+ <p class="login__eyebrow">SimpleAdmin</p>
204
+ <h1>现代后台<br />一处掌控</h1>
205
+ <p class="login__lead">.NET 8 API · Vue3 · Element Plus · EF Core + Dapper</p>
206
+ </div>
207
+ </div>
208
+
209
+ <div class="login__panel">
210
+ <div class="login__card">
211
+ <div class="login__brand">
212
+ <span class="login__mark">S</span>
213
+ <div>
214
+ <strong>欢迎回来</strong>
215
+ <p>{{ brandHint }}</p>
216
+ </div>
217
+ </div>
218
+
219
+ <div v-if="optionsLoaded && enableSmsLogin" class="login__tabs" role="tablist">
220
+ <button
221
+ type="button"
222
+ class="login__tab"
223
+ :class="{ 'is-active': loginMode === 'password' }"
224
+ role="tab"
225
+ @click="switchMode('password')"
226
+ >
227
+ 账密登录
228
+ </button>
229
+ <button
230
+ type="button"
231
+ class="login__tab"
232
+ :class="{ 'is-active': loginMode === 'sms' }"
233
+ role="tab"
234
+ @click="switchMode('sms')"
235
+ >
236
+ 手机登录
237
+ </button>
238
+ </div>
239
+
240
+ <el-form
241
+ v-if="loginMode === 'password'"
242
+ label-position="top"
243
+ class="login__form"
244
+ @submit.prevent="onSubmit"
245
+ >
246
+ <el-form-item label="账号">
247
+ <el-input v-model="passwordForm.userName" size="large" placeholder="admin" autocomplete="username" />
248
+ </el-form-item>
249
+ <el-form-item label="密码">
250
+ <el-input
251
+ v-model="passwordForm.password"
252
+ size="large"
253
+ type="password"
254
+ show-password
255
+ placeholder="密码"
256
+ autocomplete="current-password"
257
+ />
258
+ </el-form-item>
259
+ <el-form-item label="验证码">
260
+ <div class="login__captcha">
261
+ <el-input
262
+ v-model="passwordForm.captchaCode"
263
+ size="large"
264
+ maxlength="6"
265
+ placeholder="图形验证码"
266
+ @keyup.enter="onSubmit"
267
+ />
268
+ <button
269
+ type="button"
270
+ class="login__captcha-img"
271
+ title="点击刷新"
272
+ :disabled="captchaLoading"
273
+ @click="refreshCaptcha"
274
+ >
275
+ <img v-if="captchaImage" :src="captchaImage" alt="验证码" />
276
+ <span v-else>加载中</span>
277
+ </button>
278
+ </div>
279
+ </el-form-item>
280
+ <el-button
281
+ type="primary"
282
+ size="large"
283
+ native-type="submit"
284
+ :loading="loading"
285
+ class="login__submit"
286
+ >
287
+ 进入系统
288
+ </el-button>
289
+ </el-form>
290
+
291
+ <el-form
292
+ v-else-if="enableSmsLogin"
293
+ label-position="top"
294
+ class="login__form"
295
+ @submit.prevent="onSubmit"
296
+ >
297
+ <el-form-item label="手机号">
298
+ <el-input
299
+ v-model="smsForm.phone"
300
+ size="large"
301
+ maxlength="11"
302
+ placeholder="绑定账号的手机号"
303
+ autocomplete="tel"
304
+ />
305
+ </el-form-item>
306
+ <el-form-item label="短信验证码">
307
+ <div class="login__sms">
308
+ <el-input
309
+ v-model="smsForm.smsCode"
310
+ size="large"
311
+ maxlength="8"
312
+ placeholder="6 位验证码"
313
+ @keyup.enter="onSubmit"
314
+ />
315
+ <el-button
316
+ size="large"
317
+ :loading="smsSending"
318
+ :disabled="smsCountdown > 0"
319
+ @click="onSendSms"
320
+ >
321
+ {{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }}
322
+ </el-button>
323
+ </div>
324
+ </el-form-item>
325
+ <el-button
326
+ type="primary"
327
+ size="large"
328
+ native-type="submit"
329
+ :loading="loading"
330
+ class="login__submit"
331
+ >
332
+ 进入系统
333
+ </el-button>
334
+ </el-form>
335
+
336
+ <p class="login__hint">
337
+ <template v-if="enableSmsLogin">
338
+ 账密默认 admin / admin123;手机演示号 13800000000(开发态接口会回传验证码)
339
+ </template>
340
+ <template v-else>账密默认 admin / admin123</template>
341
+ </p>
342
+ </div>
343
+ </div>
344
+ </div>
345
+ </template>
346
+
347
+ <style scoped lang="scss">
348
+ .login {
349
+ min-height: 100vh;
350
+ display: grid;
351
+ grid-template-columns: 1.1fr 0.9fr;
352
+ background: #f8fafc;
353
+ }
354
+
355
+ .login__visual {
356
+ position: relative;
357
+ overflow: hidden;
358
+ padding: 56px;
359
+ color: #ecfeff;
360
+ background:
361
+ linear-gradient(145deg, #042f2e 0%, #0f766e 48%, #0e7490 100%);
362
+ display: flex;
363
+ align-items: flex-end;
364
+ }
365
+
366
+ .login__orb {
367
+ position: absolute;
368
+ border-radius: 50%;
369
+ filter: blur(2px);
370
+ animation: float 8s ease-in-out infinite;
371
+ }
372
+ .login__orb--a {
373
+ width: 280px;
374
+ height: 280px;
375
+ right: -40px;
376
+ top: -60px;
377
+ background: rgba(153, 246, 228, 0.25);
378
+ }
379
+ .login__orb--b {
380
+ width: 180px;
381
+ height: 180px;
382
+ left: 12%;
383
+ top: 28%;
384
+ background: rgba(56, 189, 248, 0.18);
385
+ animation-delay: -2.5s;
386
+ }
387
+
388
+ .login__copy {
389
+ position: relative;
390
+ z-index: 1;
391
+ max-width: 460px;
392
+ animation: sa-fade-up 0.5s ease both;
393
+ }
394
+
395
+ .login__eyebrow {
396
+ margin: 0 0 14px;
397
+ letter-spacing: 0.16em;
398
+ text-transform: uppercase;
399
+ font-size: 12px;
400
+ font-weight: 600;
401
+ color: rgba(204, 251, 241, 0.85);
402
+ }
403
+
404
+ .login__copy h1 {
405
+ margin: 0;
406
+ font-size: clamp(36px, 5vw, 52px);
407
+ line-height: 1.08;
408
+ font-weight: 800;
409
+ letter-spacing: -0.03em;
410
+ }
411
+
412
+ .login__lead {
413
+ margin: 18px 0 0;
414
+ font-size: 15px;
415
+ color: rgba(236, 254, 255, 0.78);
416
+ max-width: 34ch;
417
+ }
418
+
419
+ .login__panel {
420
+ display: grid;
421
+ place-items: center;
422
+ padding: 32px 24px;
423
+ background:
424
+ radial-gradient(circle at 80% 20%, rgba(45, 212, 191, 0.12), transparent 28%),
425
+ #f8fafc;
426
+ }
427
+
428
+ .login__card {
429
+ width: min(420px, 100%);
430
+ padding: 36px 32px;
431
+ background: #fff;
432
+ border: 1px solid var(--sa-border);
433
+ border-radius: 20px;
434
+ box-shadow: var(--sa-shadow);
435
+ animation: sa-fade-up 0.45s ease 0.08s both;
436
+ }
437
+
438
+ .login__brand {
439
+ display: flex;
440
+ gap: 14px;
441
+ align-items: center;
442
+ margin-bottom: 20px;
443
+ strong {
444
+ display: block;
445
+ font-size: 22px;
446
+ font-weight: 750;
447
+ letter-spacing: -0.02em;
448
+ }
449
+ p {
450
+ margin: 4px 0 0;
451
+ color: var(--sa-ink-secondary);
452
+ font-size: 13px;
453
+ }
454
+ }
455
+
456
+ .login__mark {
457
+ width: 44px;
458
+ height: 44px;
459
+ border-radius: 14px;
460
+ display: grid;
461
+ place-items: center;
462
+ font-weight: 800;
463
+ color: #042f2e;
464
+ background: linear-gradient(135deg, #99f6e4, #14b8a6);
465
+ }
466
+
467
+ .login__tabs {
468
+ display: grid;
469
+ grid-template-columns: 1fr 1fr;
470
+ gap: 6px;
471
+ margin-bottom: 18px;
472
+ padding: 4px;
473
+ border-radius: 12px;
474
+ background: #f1f5f9;
475
+ }
476
+
477
+ .login__tab {
478
+ height: 36px;
479
+ border: 0;
480
+ border-radius: 10px;
481
+ background: transparent;
482
+ color: var(--sa-ink-secondary);
483
+ font-size: 13px;
484
+ font-weight: 600;
485
+ cursor: pointer;
486
+ transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
487
+ }
488
+ .login__tab.is-active {
489
+ background: #fff;
490
+ color: #0f766e;
491
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
492
+ }
493
+
494
+ .login__captcha,
495
+ .login__sms {
496
+ display: flex;
497
+ gap: 10px;
498
+ width: 100%;
499
+ align-items: center;
500
+ }
501
+ .login__captcha :deep(.el-input),
502
+ .login__sms :deep(.el-input) {
503
+ flex: 1;
504
+ min-width: 0;
505
+ }
506
+
507
+ .login__captcha-img {
508
+ flex-shrink: 0;
509
+ width: 128px;
510
+ height: 40px;
511
+ padding: 0;
512
+ border: 1px solid var(--sa-border);
513
+ border-radius: 8px;
514
+ overflow: hidden;
515
+ background: #f0fdfa;
516
+ cursor: pointer;
517
+ display: grid;
518
+ place-items: center;
519
+ font-size: 12px;
520
+ color: var(--sa-ink-muted);
521
+ }
522
+ .login__captcha-img img {
523
+ width: 100%;
524
+ height: 100%;
525
+ object-fit: cover;
526
+ display: block;
527
+ }
528
+ .login__captcha-img:disabled {
529
+ opacity: 0.7;
530
+ cursor: wait;
531
+ }
532
+
533
+ .login__sms .el-button {
534
+ flex-shrink: 0;
535
+ min-width: 110px;
536
+ }
537
+
538
+ .login__submit {
539
+ width: 100%;
540
+ margin-top: 8px;
541
+ height: 44px;
542
+ font-size: 15px;
543
+ }
544
+
545
+ .login__hint {
546
+ margin: 18px 0 0;
547
+ text-align: center;
548
+ font-size: 12px;
549
+ color: var(--sa-ink-muted);
550
+ line-height: 1.5;
551
+ }
552
+
553
+ @keyframes float {
554
+ 0%,
555
+ 100% {
556
+ transform: translateY(0);
557
+ }
558
+ 50% {
559
+ transform: translateY(12px);
560
+ }
561
+ }
562
+
563
+ @media (max-width: 900px) {
564
+ .login {
565
+ grid-template-columns: 1fr;
566
+ }
567
+ .login__visual {
568
+ min-height: 220px;
569
+ padding: 32px 24px;
570
+ align-items: center;
571
+ }
572
+ .login__copy h1 {
573
+ font-size: 32px;
574
+ }
575
+ }
576
+ </style>