create-lumal-admin 0.0.4-beta → 0.0.5-beta

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 (73) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +67 -10
  3. package/dist/cli.d.ts +12 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +91 -28
  6. package/dist/cli.js.map +1 -1
  7. package/dist/create.d.ts +2 -1
  8. package/dist/create.d.ts.map +1 -1
  9. package/dist/create.js +85 -2
  10. package/dist/create.js.map +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/presets.d.ts +15 -0
  14. package/dist/presets.d.ts.map +1 -0
  15. package/dist/presets.js +30 -0
  16. package/dist/presets.js.map +1 -0
  17. package/dist/scaffold.d.ts +2 -11
  18. package/dist/scaffold.d.ts.map +1 -1
  19. package/dist/scaffold.js +2 -1602
  20. package/dist/scaffold.js.map +1 -1
  21. package/dist/templates.d.ts +5 -0
  22. package/dist/templates.d.ts.map +1 -0
  23. package/dist/templates.js +62 -0
  24. package/dist/templates.js.map +1 -0
  25. package/dist/types.d.ts +25 -0
  26. package/dist/types.d.ts.map +1 -0
  27. package/dist/types.js +2 -0
  28. package/dist/types.js.map +1 -0
  29. package/package.json +4 -3
  30. package/templates/base/.env.example +4 -0
  31. package/templates/base/API-CONTRACT.md +37 -0
  32. package/templates/base/README.md +42 -0
  33. package/templates/base/_gitignore +6 -0
  34. package/templates/base/eslint.config.mjs +8 -0
  35. package/templates/base/index.html +12 -0
  36. package/templates/base/package.json +38 -0
  37. package/templates/base/src/App.vue +214 -0
  38. package/templates/base/src/components/app/AppHeaderActions.vue +34 -0
  39. package/templates/base/src/components/app/AppSettingsDrawer.vue +37 -0
  40. package/templates/base/src/main.ts +41 -0
  41. package/templates/base/src/router/index.ts +86 -0
  42. package/templates/base/src/router/views.ts +16 -0
  43. package/templates/base/src/services/access.ts +2 -0
  44. package/templates/base/src/services/contracts.ts +29 -0
  45. package/templates/base/src/services/preferences.ts +41 -0
  46. package/templates/base/src/services/session-runtime.ts +137 -0
  47. package/templates/base/src/services/session.ts +14 -0
  48. package/templates/base/src/styles.scss +178 -0
  49. package/templates/base/src/views/auth/LoginView.vue +80 -0
  50. package/templates/base/src/views/dashboard/DashboardView.vue +27 -0
  51. package/templates/base/src/views/error/ForbiddenView.vue +25 -0
  52. package/templates/base/src/views/error/NotFoundView.vue +18 -0
  53. package/templates/base/tests/session.test.ts.template +34 -0
  54. package/templates/base/tsconfig.json +22 -0
  55. package/templates/base/vite.config.ts +54 -0
  56. package/templates/base/vitest.config.ts +3 -0
  57. package/templates/features/examples/src/modules/examples.ts +10 -0
  58. package/templates/features/examples/src/views/examples/CrudExampleView.vue +30 -0
  59. package/templates/features/examples/src/views/examples/FormExampleView.vue +25 -0
  60. package/templates/features/system/src/business/system/entities.ts +93 -0
  61. package/templates/features/system/src/modules/system.ts +28 -0
  62. package/templates/features/system/src/views/system/SystemEntityView.vue +85 -0
  63. package/templates/modes/http/src/services/data-source.ts +120 -0
  64. package/templates/modes/http/src/services/http-adapter.ts +124 -0
  65. package/templates/modes/http/tests/http.test.ts.template +165 -0
  66. package/templates/modes/local/src/services/data-source.ts +176 -0
  67. package/templates/modes/local/tests/local.test.ts.template +92 -0
  68. package/templates/vben-compatible/MIGRATION.md +28 -0
  69. package/templates/vben-compatible/src/components/migration/VbenFormExample.vue +34 -0
  70. package/templates/vben-compatible/src/components/migration/VbenUsers.vue +135 -0
  71. package/templates/vben-compatible/src/modules/vben.ts +17 -0
  72. package/templates/vben-compatible/src/views/migration/VbenFormView.vue +10 -0
  73. package/templates/vben-compatible/src/views/migration/VbenUsersView.vue +11 -0
@@ -0,0 +1,86 @@
1
+ import type { LumalLayoutTabItem } from '@lumal/core/layout'
2
+ import type { LumalMenuInputRecord, SidebarMenuItem } from '@lumal/core/router'
3
+ import type { RouteRecordRaw, RouterHistory } from 'vue-router'
4
+ import { setupPermissionGuard } from '@lumal/core/permission'
5
+ import { createMenuRouteRuntime } from '@lumal/core/router'
6
+ import { shallowRef } from 'vue'
7
+ import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
8
+ import { appConfig } from '../app.config'
9
+ import { application, permissionStore } from '../services/session'
10
+ import { viewComponents } from './views'
11
+
12
+ /***********************路由初始化*********************/
13
+ export const routeError = shallowRef('')
14
+ export function createAdminRouter(history?: RouterHistory) {
15
+ const targetRouter = createRouter({
16
+ history: history ?? (appConfig.routerMode === 'history' ? createWebHistory(import.meta.env.BASE_URL) : createWebHashHistory(import.meta.env.BASE_URL)),
17
+ routes: [
18
+ { path: '/login', name: 'Login', component: () => import('../views/auth/LoginView.vue'), meta: { layout: 'public', requireLogin: false, title: '登录' } },
19
+ { path: '/', redirect: '/dashboard' },
20
+ { path: '/403', component: () => import('../views/error/ForbiddenView.vue'), meta: { title: '无权限' } },
21
+ { path: '/404', component: () => import('../views/error/NotFoundView.vue'), meta: { title: '页面不存在' } },
22
+ { path: '/:pathMatch(.*)*', name: 'CatchAll', component: () => import('../views/error/NotFoundView.vue') },
23
+ ],
24
+ })
25
+ const runtime = createMenuRouteRuntime({
26
+ router: {
27
+ addRoute: route => targetRouter.addRoute(route as RouteRecordRaw),
28
+ getRoutes: () => targetRouter.getRoutes(),
29
+ hasRoute: name => targetRouter.hasRoute(name),
30
+ removeRoute: name => targetRouter.removeRoute(name),
31
+ },
32
+ componentResolver: name => viewComponents[name],
33
+ hasPermission: values => permissionStore.hasPermission(values, 'every'),
34
+ hasRole: values => permissionStore.hasRole(values, 'some'),
35
+ loadRemoteMenus: async () => {
36
+ await application.initialize()
37
+ return application.menus.value as unknown as LumalMenuInputRecord[]
38
+ },
39
+ })
40
+ application.onReset((expired) => {
41
+ runtime.resetRemote()
42
+ if (expired && targetRouter.currentRoute.value.path !== '/login')
43
+ void targetRouter.replace({ path: '/login', query: { redirect: targetRouter.currentRoute.value.fullPath } })
44
+ })
45
+
46
+ targetRouter.beforeEach(async (to) => {
47
+ if (!application.isAuthenticated())
48
+ return to.path === '/login' ? true : { path: '/login', query: { redirect: to.fullPath } }
49
+ const rematch = !runtime.remoteLoaded
50
+ try {
51
+ await runtime.loadRemote()
52
+ routeError.value = ''
53
+ }
54
+ catch (reason) {
55
+ routeError.value = reason instanceof Error ? reason.message : '菜单加载失败'
56
+ application.reset()
57
+ return { path: '/login', query: { redirect: to.fullPath } }
58
+ }
59
+ if (to.path === '/login' || to.path === '/')
60
+ return runtime.firstAccessiblePath || '/403'
61
+ if (rematch)
62
+ return to.fullPath
63
+ return to.name === 'CatchAll' ? '/404' : true
64
+ })
65
+ setupPermissionGuard(targetRouter, permissionStore, {
66
+ isAuthenticated: application.isAuthenticated,
67
+ mode: 'every',
68
+ roleMode: 'some',
69
+ requireLoginByDefault: true,
70
+ noAccessRedirect: '/403',
71
+ })
72
+ return { router: targetRouter, runtime }
73
+ }
74
+
75
+ const { router, runtime } = createAdminRouter()
76
+ export { router }
77
+ export function createAdminSidebarMenus(): SidebarMenuItem[] {
78
+ // ready 使非响应式菜单运行时在退出和初始化完成后重新计算。
79
+ return application.ready.value ? runtime.sidebarMenus : []
80
+ }
81
+ export function createAdminTabs(): LumalLayoutTabItem[] {
82
+ return runtime.routes.filter(route => route.meta.affixTab === true
83
+ && permissionStore.hasPermission(route.meta.permissions, 'every')
84
+ && permissionStore.hasRole(route.meta.roles, 'some'))
85
+ .map(route => ({ path: route.path, title: route.meta.title, closable: false }))
86
+ }
@@ -0,0 +1,16 @@
1
+ import type { LumalMenuRecord } from '@lumal/core/router'
2
+
3
+ export interface FeatureModule {
4
+ menus: LumalMenuRecord[]
5
+ views: Record<string, () => Promise<unknown>>
6
+ }
7
+
8
+ const modules = import.meta.glob<FeatureModule>('../modules/*.ts', { eager: true })
9
+ export const localMenus: LumalMenuRecord[] = [
10
+ { path: '/dashboard', name: 'Dashboard', component: 'dashboard/index', meta: { title: '工作台', icon: 'app:dashboard', affixTab: true, keepAlive: true } },
11
+ ...Object.values(modules).flatMap(module => module.menus),
12
+ ]
13
+ export const viewComponents: Record<string, () => Promise<unknown>> = {
14
+ 'dashboard/index': () => import('../views/dashboard/DashboardView.vue'),
15
+ ...Object.assign({}, ...Object.values(modules).map(module => module.views)),
16
+ }
@@ -0,0 +1,2 @@
1
+ // 单一权限 Store,由实际会话响应填充,不能持久化演示权限。
2
+ export { permissionStore } from './session'
@@ -0,0 +1,29 @@
1
+ import type { AuthSessionData } from '@lumal/core/auth'
2
+ import type { LumalMenuRecord } from '@lumal/core/router'
3
+
4
+ export interface LoginInput { username: string, password: string }
5
+ export interface CurrentUser { id: string, name: string, username: string }
6
+ export interface AccessSnapshot { permissions: string[], roles: string[] }
7
+ export type DataRow = Record<string, unknown>
8
+ export interface ListInput { page: number, pageSize: number, query: DataRow }
9
+ export interface PageResult { items: DataRow[], total: number }
10
+
11
+ // 应用协议由 Local/HTTP 共同实现,后端包装和字段转换不进入 Core。
12
+ export interface ApplicationService {
13
+ login: (input: LoginInput, signal: AbortSignal) => Promise<AuthSessionData>
14
+ getUser: (signal: AbortSignal) => Promise<CurrentUser>
15
+ getAccess: (signal: AbortSignal) => Promise<AccessSnapshot>
16
+ getMenus: (signal: AbortSignal) => Promise<LumalMenuRecord[]>
17
+ logout: (accessToken: string) => Promise<void>
18
+ clearCache: () => void
19
+ list: (resource: string, input: ListInput) => Promise<PageResult>
20
+ save: (resource: string, model: DataRow, id?: string) => Promise<unknown>
21
+ remove: (resource: string, id: string) => Promise<void>
22
+ }
23
+
24
+ export interface ServiceContext {
25
+ getSession: () => AuthSessionData | null
26
+ getSignal: () => AbortSignal
27
+ setSession: (value: AuthSessionData) => void
28
+ expire: () => Promise<void>
29
+ }
@@ -0,0 +1,41 @@
1
+ import type { LumalPreferencesDefaults } from '@lumal/core/theme'
2
+ import { createPreferencesStore } from '@lumal/core/theme'
3
+ import { appConfig } from '../app.config'
4
+
5
+ export const adminPreferenceDefaults = {
6
+ app: { dynamicTitle: true, layout: 'sidebar-nav' },
7
+ breadcrumb: { enable: true, hideOnlyOne: false, showHome: true, showIcon: true },
8
+ header: { globalSearch: true },
9
+ shortcutKeys: { globalSearch: true },
10
+ sidebar: { width: 220 },
11
+ tabbar: {
12
+ cache: appConfig.preset !== 'minimal',
13
+ draggable: true,
14
+ enable: appConfig.preset !== 'minimal',
15
+ maxCount: 0,
16
+ middleClickToClose: true,
17
+ persist: true,
18
+ showIcon: true,
19
+ showMaximize: true,
20
+ showMore: true,
21
+ showRefresh: true,
22
+ styleType: 'chrome',
23
+ visitHistory: true,
24
+ wheelable: true,
25
+ },
26
+ theme: { fontSize: 14 },
27
+ transition: { enable: true, name: 'fade-side' },
28
+ } satisfies LumalPreferencesDefaults
29
+
30
+ const preferencesStore = createPreferencesStore({
31
+ defaults: adminPreferenceDefaults,
32
+ storage: localStorage,
33
+ storageKey: 'lumal-admin:preferences',
34
+ })
35
+
36
+ export const adminPreferences = preferencesStore.state
37
+ export const adminResolvedThemeMode = preferencesStore.resolvedThemeMode
38
+
39
+ export function patchAdminPreferences(preferences: Parameters<typeof preferencesStore.patch>[0]): void {
40
+ preferencesStore.patch(preferences)
41
+ }
@@ -0,0 +1,137 @@
1
+ import type { LumalMenuRecord } from '@lumal/core/router'
2
+ import type { ApplicationService, CurrentUser, LoginInput, ServiceContext } from './contracts'
3
+ import { createAuthSession } from '@lumal/core/auth'
4
+ import { createPermissionStore } from '@lumal/core/permission'
5
+ import { readonly, shallowRef } from 'vue'
6
+
7
+ export function createApplicationSession(
8
+ createService: (context: ServiceContext) => ApplicationService,
9
+ storage: Storage,
10
+ tabStorage?: Storage,
11
+ ) {
12
+ const permissionStore = createPermissionStore()
13
+ const user = shallowRef<CurrentUser | null>(null)
14
+ const menus = shallowRef<LumalMenuRecord[]>([])
15
+ const ready = shallowRef(false)
16
+ const error = shallowRef('')
17
+ const resetHandlers = new Set<(expired: boolean) => void>()
18
+ const tabStorageKey = 'lumal-admin:tabs'
19
+ let generation = 0
20
+ let controller = new AbortController()
21
+ let initializing: Promise<void> | undefined
22
+ const session = createAuthSession({ storage, tokenKey: 'lumal-admin:session' })
23
+ let service: ApplicationService
24
+
25
+ function reset(expired = false): void {
26
+ generation += 1
27
+ controller.abort()
28
+ controller = new AbortController()
29
+ initializing = undefined
30
+ ready.value = false
31
+ user.value = null
32
+ menus.value = []
33
+ permissionStore.clear()
34
+ session.clearToken()
35
+ service.clearCache()
36
+ tabStorage?.removeItem(tabStorageKey)
37
+ resetHandlers.forEach(handler => handler(expired))
38
+ }
39
+
40
+ service = createService({
41
+ getSession: session.getSession,
42
+ getSignal: () => controller.signal,
43
+ setSession: session.setSession,
44
+ expire: async () => reset(true),
45
+ })
46
+
47
+ /***********************会话初始化与隔离*********************/
48
+ function initialize(): Promise<void> {
49
+ if (ready.value || !session.isAuthenticated())
50
+ return Promise.resolve()
51
+ if (initializing)
52
+ return initializing
53
+ const version = generation
54
+ const signal = controller.signal
55
+ const pending = (async () => {
56
+ try {
57
+ const [nextUser, access, nextMenus] = await Promise.all([
58
+ service.getUser(signal),
59
+ service.getAccess(signal),
60
+ service.getMenus(signal),
61
+ ])
62
+ signal.throwIfAborted()
63
+ if (version !== generation)
64
+ throw new Error('会话已切换')
65
+ user.value = nextUser
66
+ permissionStore.setPermissions(access.permissions)
67
+ permissionStore.setRoles(access.roles)
68
+ menus.value = nextMenus
69
+ ready.value = true
70
+ error.value = ''
71
+ }
72
+ catch (reason) {
73
+ if (version === generation) {
74
+ reset()
75
+ error.value = reason instanceof Error ? reason.message : '会话初始化失败'
76
+ }
77
+ throw reason
78
+ }
79
+ })()
80
+ initializing = pending
81
+ void pending.finally(() => {
82
+ if (initializing === pending)
83
+ initializing = undefined
84
+ }).catch(() => {}) // 原始 Promise 仍向调用方传播异常,只消费 finally 分支。
85
+ return pending
86
+ }
87
+
88
+ async function login(input: LoginInput): Promise<void> {
89
+ reset()
90
+ const version = generation
91
+ const signal = controller.signal
92
+ error.value = ''
93
+ try {
94
+ const result = await service.login(input, signal)
95
+ signal.throwIfAborted()
96
+ if (version !== generation)
97
+ throw new Error('会话已切换')
98
+ session.setSession(result)
99
+ await initialize()
100
+ }
101
+ catch (reason) {
102
+ if (version === generation) {
103
+ reset()
104
+ error.value = reason instanceof Error ? reason.message : '登录失败'
105
+ }
106
+ throw reason
107
+ }
108
+ }
109
+
110
+ async function logout(): Promise<void> {
111
+ const token = session.getToken()
112
+ reset()
113
+ // 即使远端撤销失败,本地状态也已清理;错误交由界面反馈。
114
+ if (token)
115
+ await service.logout(token)
116
+ }
117
+
118
+ return {
119
+ currentUser: readonly(user),
120
+ error: readonly(error),
121
+ menus: readonly(menus),
122
+ ready: readonly(ready),
123
+ permissionStore,
124
+ session,
125
+ service,
126
+ tabStorageKey,
127
+ initialize,
128
+ isAuthenticated: session.isAuthenticated,
129
+ login,
130
+ logout,
131
+ reset,
132
+ onReset(handler: (expired: boolean) => void) {
133
+ resetHandlers.add(handler)
134
+ return () => resetHandlers.delete(handler)
135
+ },
136
+ }
137
+ }
@@ -0,0 +1,14 @@
1
+ import { createBackend } from './data-source'
2
+ import { createApplicationSession } from './session-runtime'
3
+
4
+ export const application = createApplicationSession(createBackend, localStorage, sessionStorage)
5
+ export const {
6
+ currentUser,
7
+ initialize,
8
+ isAuthenticated,
9
+ login,
10
+ logout,
11
+ permissionStore,
12
+ service,
13
+ tabStorageKey: adminTabSnapshotStorageKey,
14
+ } = application
@@ -0,0 +1,178 @@
1
+ :root {
2
+ color: var(--el-text-color-primary);
3
+ background: var(--el-bg-color-page);
4
+ font-family:
5
+ Inter,
6
+ "Segoe UI",
7
+ "Microsoft YaHei",
8
+ Arial,
9
+ sans-serif;
10
+ }
11
+
12
+ body {
13
+ margin: 0;
14
+ min-width: 320px;
15
+ overflow-x: hidden;
16
+ }
17
+
18
+ .lumal-admin-home {
19
+ display: grid;
20
+ gap: var(--lumal-page-gutter, 20px);
21
+ box-sizing: border-box;
22
+ padding: var(--lumal-page-gutter, 20px);
23
+ }
24
+
25
+ .lumal-admin-home__metrics {
26
+ display: grid;
27
+ grid-template-columns: repeat(4, minmax(0, 1fr));
28
+ gap: 16px;
29
+ }
30
+
31
+ .lumal-admin-home__metric {
32
+ display: grid;
33
+ min-width: 0;
34
+ gap: 8px;
35
+ padding: 18px;
36
+ border: 1px solid var(--el-border-color-lighter);
37
+ border-radius: var(--lumal-radius-base, 8px);
38
+ background: var(--el-bg-color);
39
+ }
40
+
41
+ .lumal-admin-home__metric strong {
42
+ color: var(--el-color-primary);
43
+ font-size: 28px;
44
+ }
45
+
46
+ .lumal-admin-home__metric small {
47
+ color: var(--el-text-color-secondary);
48
+ }
49
+
50
+ .lumal-admin-page {
51
+ box-sizing: border-box;
52
+ padding: var(--lumal-page-gutter, 20px);
53
+ }
54
+
55
+ .lumal-admin-page__list {
56
+ display: grid;
57
+ gap: 10px;
58
+ margin: 0;
59
+ padding-left: 18px;
60
+ }
61
+
62
+ .lumal-admin-page__item {
63
+ color: var(--el-text-color-regular);
64
+ font-size: 14px;
65
+ line-height: 1.6;
66
+ }
67
+
68
+ .lumal-admin-header-actions {
69
+ display: flex;
70
+ min-width: 0;
71
+ align-items: center;
72
+ gap: 4px;
73
+ }
74
+
75
+ .lumal-admin-header-actions .el-button {
76
+ min-width: 44px;
77
+ min-height: 44px;
78
+ margin: 0;
79
+ }
80
+
81
+ .lumal-admin-header-actions__user {
82
+ max-width: 120px;
83
+ overflow: hidden;
84
+ color: var(--el-text-color-regular);
85
+ font-size: 13px;
86
+ text-overflow: ellipsis;
87
+ white-space: nowrap;
88
+ }
89
+
90
+ .lumal-admin-login {
91
+ display: grid;
92
+ min-height: 100dvh;
93
+ place-items: center;
94
+ padding: 24px;
95
+ background: var(--el-bg-color-page);
96
+ }
97
+
98
+ .lumal-admin-login__panel {
99
+ box-sizing: border-box;
100
+ width: min(400px, 100%);
101
+ padding: 32px;
102
+ border: 1px solid var(--el-border-color-lighter);
103
+ border-radius: var(--lumal-radius-large, 12px);
104
+ background: var(--el-bg-color);
105
+ box-shadow: var(--lumal-shadow-dialog);
106
+ }
107
+
108
+ .lumal-admin-login__brand {
109
+ display: grid;
110
+ width: 48px;
111
+ height: 48px;
112
+ place-items: center;
113
+ border-radius: var(--lumal-radius-base, 8px);
114
+ color: var(--el-color-white);
115
+ background: var(--el-color-primary);
116
+ font-size: 24px;
117
+ font-weight: 800;
118
+ }
119
+
120
+ .lumal-admin-login h1 {
121
+ margin: 20px 0 6px;
122
+ }
123
+
124
+ .lumal-admin-login p,
125
+ .lumal-admin-login small {
126
+ color: var(--el-text-color-secondary);
127
+ }
128
+
129
+ .lumal-admin-login .el-form {
130
+ margin: 24px 0 16px;
131
+ }
132
+
133
+ .lumal-admin-login .el-button {
134
+ width: 100%;
135
+ min-height: 44px;
136
+ }
137
+
138
+ .lumal-admin-login__error {
139
+ color: var(--el-color-danger) !important;
140
+ font-size: 13px;
141
+ }
142
+
143
+ @media (max-width: 900px) {
144
+ .lumal-admin-home__metrics {
145
+ grid-template-columns: repeat(2, minmax(0, 1fr));
146
+ }
147
+ }
148
+
149
+ @media (max-width: 640px) {
150
+ .lumal-admin-home,
151
+ .lumal-admin-page {
152
+ padding: 12px;
153
+ }
154
+
155
+ .lumal-admin-home__metrics {
156
+ grid-template-columns: minmax(0, 1fr);
157
+ }
158
+
159
+ .lumal-admin-header-actions__user {
160
+ display: none;
161
+ }
162
+
163
+ .lumal-admin-login {
164
+ padding: 16px;
165
+ }
166
+
167
+ .lumal-admin-login__panel {
168
+ padding: 24px 20px;
169
+ }
170
+ }
171
+
172
+ @media (prefers-reduced-motion: reduce) {
173
+ *,
174
+ *::before,
175
+ *::after {
176
+ scroll-behavior: auto !important;
177
+ }
178
+ }
@@ -0,0 +1,80 @@
1
+ <script setup lang="ts">
2
+ import type { FormInstance } from 'element-plus'
3
+ import { ElButton, ElForm, ElFormItem, ElInput } from 'element-plus'
4
+ import { reactive, shallowRef, useTemplateRef } from 'vue'
5
+ import { useRoute, useRouter } from 'vue-router'
6
+ import { routeError } from '../../router'
7
+ import { demoAccount } from '../../services/data-source'
8
+ import { application, login } from '../../services/session'
9
+
10
+ /***********************登录状态*********************/
11
+ const route = useRoute()
12
+ const router = useRouter()
13
+ const formRef = useTemplateRef<FormInstance>('formRef')
14
+ const loading = shallowRef(false)
15
+ const errorMessage = shallowRef('')
16
+ const form = reactive({ username: demoAccount?.username ?? '', password: demoAccount?.password ?? '' })
17
+
18
+ /***********************登录提交*********************/
19
+ function resolveRedirect(): string {
20
+ const redirect = Array.isArray(route.query.redirect) ? route.query.redirect[0] : route.query.redirect
21
+ return redirect && redirect.startsWith('/') && !redirect.startsWith('//') && !redirect.startsWith('/login') ? redirect : '/dashboard'
22
+ }
23
+
24
+ async function handleSubmit(): Promise<void> {
25
+ if (loading.value) {
26
+ return
27
+ }
28
+
29
+ loading.value = true
30
+ errorMessage.value = ''
31
+
32
+ try {
33
+ const valid = await formRef.value?.validate()
34
+ if (valid === false) {
35
+ return
36
+ }
37
+
38
+ await login(form)
39
+ await router.replace(resolveRedirect())
40
+ }
41
+ catch (error) {
42
+ errorMessage.value = error instanceof Error ? error.message : '登录失败'
43
+ }
44
+ finally {
45
+ loading.value = false
46
+ }
47
+ }
48
+ </script>
49
+
50
+ <template>
51
+ <main class="lumal-admin-login">
52
+ <section class="lumal-admin-login__panel">
53
+ <div class="lumal-admin-login__brand" aria-hidden="true">
54
+ L
55
+ </div>
56
+ <h1>Lumal Admin</h1>
57
+ <p>轻量后台基座</p>
58
+
59
+ <ElForm ref="formRef" :model="form" label-position="top" @submit.prevent="handleSubmit">
60
+ <ElFormItem label="用户名" prop="username" :rules="[{ required: true, message: '请输入用户名' }]">
61
+ <ElInput v-model="form.username" autocomplete="username" />
62
+ </ElFormItem>
63
+ <ElFormItem label="密码" prop="password" :rules="[{ required: true, message: '请输入密码' }]">
64
+ <ElInput v-model="form.password" type="password" show-password autocomplete="current-password" />
65
+ </ElFormItem>
66
+ <p v-if="errorMessage" class="lumal-admin-login__error" role="alert">
67
+ {{ errorMessage }}
68
+ </p>
69
+ <ElButton native-type="submit" type="primary" :loading="loading">
70
+ 登录
71
+ </ElButton>
72
+ </ElForm>
73
+
74
+ <p v-if="routeError || application.error.value" role="alert">
75
+ {{ routeError || application.error.value }}
76
+ </p>
77
+ <small v-if="demoAccount">本地演示账号:{{ demoAccount.username }} / {{ demoAccount.password }}</small>
78
+ </section>
79
+ </main>
80
+ </template>
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ import { ElDescriptions, ElDescriptionsItem } from 'element-plus'
3
+ import { currentUser, permissionStore } from '../../services/session'
4
+
5
+ /***********************会话摘要*********************/
6
+ const startedAt = new Date().toLocaleDateString()
7
+ </script>
8
+
9
+ <template>
10
+ <main class="lumal-admin-page">
11
+ <h1>工作台</h1>
12
+ <ElDescriptions :column="1" border>
13
+ <ElDescriptionsItem label="当前用户">
14
+ {{ currentUser?.name }}
15
+ </ElDescriptionsItem>
16
+ <ElDescriptionsItem label="角色">
17
+ {{ permissionStore.roles.join('、') }}
18
+ </ElDescriptionsItem>
19
+ <ElDescriptionsItem label="权限数">
20
+ {{ permissionStore.permissions.length }}
21
+ </ElDescriptionsItem>
22
+ <ElDescriptionsItem label="日期">
23
+ {{ startedAt }}
24
+ </ElDescriptionsItem>
25
+ </ElDescriptions>
26
+ </main>
27
+ </template>
@@ -0,0 +1,25 @@
1
+ <script setup lang="ts">
2
+ import { LumalPage } from '@lumal/core/components'
3
+ import { ElButton } from 'element-plus'
4
+ import { useRouter } from 'vue-router'
5
+
6
+ /***********************路由操作*********************/
7
+ const router = useRouter()
8
+
9
+ function handleBackDashboardClick(): void {
10
+ void router.push('/dashboard')
11
+ }
12
+ </script>
13
+
14
+ <template>
15
+ <main class="lumal-admin-page">
16
+ <LumalPage
17
+ title="无权限"
18
+ description="当前账号没有访问该页面所需的权限。"
19
+ >
20
+ <ElButton type="primary" @click="handleBackDashboardClick">
21
+ 返回工作台
22
+ </ElButton>
23
+ </LumalPage>
24
+ </main>
25
+ </template>
@@ -0,0 +1,18 @@
1
+ <script setup lang="ts">
2
+ import { LumalPage } from '@lumal/core/components'
3
+ import { ElButton } from 'element-plus'
4
+ import { useRouter } from 'vue-router'
5
+
6
+ /***********************异常页导航*********************/
7
+ const router = useRouter()
8
+ </script>
9
+
10
+ <template>
11
+ <main class="lumal-admin-page">
12
+ <LumalPage title="页面不存在" description="请检查地址,或返回工作台继续操作。">
13
+ <ElButton type="primary" @click="router.push('/dashboard')">
14
+ 返回工作台
15
+ </ElButton>
16
+ </LumalPage>
17
+ </main>
18
+ </template>