create-lumal-admin 0.0.5-beta → 0.0.6-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 (194) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +39 -10
  3. package/dist/cli.d.ts +9 -3
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +236 -19
  6. package/dist/cli.js.map +1 -1
  7. package/dist/create.d.ts +10 -1
  8. package/dist/create.d.ts.map +1 -1
  9. package/dist/create.js +18 -5
  10. package/dist/create.js.map +1 -1
  11. package/dist/index.d.ts +2 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/presets.d.ts +29 -3
  16. package/dist/presets.d.ts.map +1 -1
  17. package/dist/presets.js +38 -10
  18. package/dist/presets.js.map +1 -1
  19. package/dist/project-cli.d.ts +3 -0
  20. package/dist/project-cli.d.ts.map +1 -0
  21. package/dist/project-cli.js +13 -0
  22. package/dist/project-cli.js.map +1 -0
  23. package/dist/projects.d.ts +21 -0
  24. package/dist/projects.d.ts.map +1 -0
  25. package/dist/projects.js +124 -0
  26. package/dist/projects.js.map +1 -0
  27. package/dist/templates.d.ts +2 -0
  28. package/dist/templates.d.ts.map +1 -1
  29. package/dist/templates.js +42 -13
  30. package/dist/templates.js.map +1 -1
  31. package/dist/types.d.ts +39 -0
  32. package/dist/types.d.ts.map +1 -1
  33. package/package.json +11 -3
  34. package/templates/base/API-CONTRACT.md +67 -20
  35. package/templates/base/README.md +20 -3
  36. package/templates/base/src/App.vue +71 -79
  37. package/templates/base/src/app/components/AppHeaderActions.vue +46 -0
  38. package/templates/base/src/{components/app → app/components}/AppSettingsDrawer.vue +15 -15
  39. package/templates/base/src/app/menu-policy.ts +163 -0
  40. package/templates/base/src/app/module.ts +46 -0
  41. package/templates/base/src/app/router/index.ts +107 -0
  42. package/templates/base/src/app/router/views.ts +12 -0
  43. package/templates/base/src/{services → app/services}/access.ts +1 -1
  44. package/templates/base/src/app/services/contracts.ts +54 -0
  45. package/templates/base/src/app/services/local-contracts.ts +18 -0
  46. package/templates/base/src/{services → app/services}/preferences.ts +8 -8
  47. package/templates/base/src/app/services/session-runtime.ts +186 -0
  48. package/templates/base/src/{services → app/services}/session.ts +4 -4
  49. package/templates/base/src/app/stored-menus.ts +72 -0
  50. package/templates/base/src/app/views/SafeIframeView.vue +34 -0
  51. package/templates/base/src/{views → app/views}/auth/LoginView.vue +29 -34
  52. package/templates/base/src/app/views/error/ForbiddenView.vue +20 -0
  53. package/templates/base/src/{views → app/views}/error/NotFoundView.vue +5 -7
  54. package/templates/base/src/main.ts +21 -22
  55. package/templates/base/src/modules/business/README.md +33 -0
  56. package/templates/base/src/{views → modules}/dashboard/DashboardView.vue +3 -3
  57. package/templates/base/src/modules/dashboard/module.ts +14 -0
  58. package/templates/base/src/shared/api.ts +53 -0
  59. package/templates/base/src/shared/components/BaseEntityDialog.vue +39 -0
  60. package/templates/base/src/shared/composables/use-async-data.ts +35 -0
  61. package/templates/base/src/shared/composables/use-entity-editor.ts +77 -0
  62. package/templates/base/src/shared/status.ts +4 -0
  63. package/templates/base/src/shared/tree.ts +41 -0
  64. package/templates/base/src/styles.scss +1 -6
  65. package/templates/base/tests/menu-policy.test.ts.template +89 -0
  66. package/templates/base/tests/session.test.ts.template +38 -27
  67. package/templates/base/vite.config.ts +4 -4
  68. package/templates/blank/README.md +6 -0
  69. package/templates/blank/src/App.vue +11 -0
  70. package/templates/common/AGENTS.md +13 -0
  71. package/templates/common/_editorconfig +15 -0
  72. package/templates/common/_gitattributes +6 -0
  73. package/templates/{base → common}/_gitignore +1 -0
  74. package/templates/common/_prettierignore +6 -0
  75. package/templates/common/docs/DEVELOPMENT.md +26 -0
  76. package/templates/common/eslint.config.mjs +60 -0
  77. package/templates/common/package.json +39 -0
  78. package/templates/common/prettier.config.mjs +8 -0
  79. package/templates/{base → common}/tsconfig.json +2 -5
  80. package/templates/common/vitest.config.ts +3 -0
  81. package/templates/features/examples/src/modules/examples/CrudExampleView.vue +51 -0
  82. package/templates/features/examples/src/{views → modules}/examples/FormExampleView.vue +15 -8
  83. package/templates/features/examples/src/modules/examples/module.ts +27 -0
  84. package/templates/features/system/src/modules/system/config/ConfigView.vue +104 -0
  85. package/templates/features/system/src/modules/system/config/api.ts +38 -0
  86. package/templates/features/system/src/modules/system/config/routes.ts +21 -0
  87. package/templates/features/system/src/modules/system/dictionary/DictionaryItemView.vue +141 -0
  88. package/templates/features/system/src/modules/system/dictionary/DictionaryTypeView.vue +107 -0
  89. package/templates/features/system/src/modules/system/dictionary/api.ts +61 -0
  90. package/templates/features/system/src/modules/system/dictionary/cache.ts +9 -0
  91. package/templates/features/system/src/modules/system/dictionary/routes.ts +32 -0
  92. package/templates/features/system/src/modules/system/dictionary/types.ts +22 -0
  93. package/templates/features/system/src/modules/system/local.ts +348 -0
  94. package/templates/features/system/src/modules/system/menu/MenuView.vue +133 -0
  95. package/templates/features/system/src/modules/system/menu/api.ts +73 -0
  96. package/templates/features/system/src/modules/system/menu/components/MenuEditorDialog.vue +106 -0
  97. package/templates/features/system/src/modules/system/menu/routes.ts +23 -0
  98. package/templates/features/system/src/modules/system/menu/types.ts +18 -0
  99. package/templates/features/system/src/modules/system/module.ts +22 -0
  100. package/templates/features/system/src/modules/system/organization/OrganizationView.vue +134 -0
  101. package/templates/features/system/src/modules/system/organization/api.ts +38 -0
  102. package/templates/features/system/src/modules/system/organization/routes.ts +23 -0
  103. package/templates/features/system/src/modules/system/organization/types.ts +17 -0
  104. package/templates/features/system/src/modules/system/role/RoleView.vue +90 -0
  105. package/templates/features/system/src/modules/system/role/api.ts +40 -0
  106. package/templates/features/system/src/modules/system/role/components/RoleAuthorizationDialog.vue +110 -0
  107. package/templates/features/system/src/modules/system/role/permission-tree.ts +31 -0
  108. package/templates/features/system/src/modules/system/role/routes.ts +24 -0
  109. package/templates/features/system/src/modules/system/role/types.ts +12 -0
  110. package/templates/features/system/src/modules/system/user/UserView.vue +297 -0
  111. package/templates/features/system/src/modules/system/user/api.ts +45 -0
  112. package/templates/features/system/src/modules/system/user/components/UserOrganizationFilter.vue +61 -0
  113. package/templates/features/system/src/modules/system/user/components/UserRoleDialog.vue +93 -0
  114. package/templates/features/system/src/modules/system/user/routes.ts +27 -0
  115. package/templates/features/system/src/modules/system/user/types.ts +28 -0
  116. package/templates/features/system/tests/system.test.ts.template +266 -0
  117. package/templates/modes/http/src/app/services/data-source.ts +124 -0
  118. package/templates/modes/http/src/app/services/http-adapter.ts +128 -0
  119. package/templates/modes/http/tests/http.test.ts.template +186 -133
  120. package/templates/modes/local/src/app/services/data-source.ts +113 -0
  121. package/templates/modes/local/src/app/services/local-database.ts +67 -0
  122. package/templates/modes/local/tests/local.test.ts.template +117 -77
  123. package/templates/presets/standard/src/modules/dashboard/DashboardView.vue +52 -0
  124. package/templates/presets/standard/src/modules/dashboard/components/DashboardSessionStatus.vue +41 -0
  125. package/templates/presets/standard/src/modules/dashboard/components/DashboardShortcuts.vue +51 -0
  126. package/templates/presets/standard/src/modules/profile/ProfileView.vue +109 -0
  127. package/templates/presets/standard/src/modules/profile/api.ts +49 -0
  128. package/templates/presets/standard/src/modules/profile/components/ProfilePasswordForm.vue +68 -0
  129. package/templates/presets/standard/src/modules/profile/local.ts +40 -0
  130. package/templates/presets/standard/src/modules/profile/module.ts +11 -0
  131. package/templates/presets/standard/tests/profile.test.ts.template +72 -0
  132. package/templates/screen/README.md +6 -0
  133. package/templates/screen/src/App.vue +7 -0
  134. package/templates/screen/src/screen/ScreenEntry.vue +18 -0
  135. package/templates/screen/src/screen/ScreenView.vue +48 -0
  136. package/templates/screen/src/screen/components/ScreenMetrics.vue +40 -0
  137. package/templates/screen-auth/src/screen/ScreenEntry.vue +52 -0
  138. package/templates/screen-auth/src/screen/components/ScreenLoginForm.vue +44 -0
  139. package/templates/screen-auth/src/services/session.ts +61 -0
  140. package/templates/screen-auth/tests/session.test.ts.template +54 -0
  141. package/templates/screen-auth-http/src/services/auth-adapter.ts +14 -0
  142. package/templates/screen-auth-http/tests/auth-adapter.test.ts.template +17 -0
  143. package/templates/screen-auth-local-demo/src/services/auth-adapter.ts +15 -0
  144. package/templates/screen-auth-local-demo/tests/auth-adapter.test.ts.template +18 -0
  145. package/templates/screen-data/src/screen/ScreenView.vue +69 -0
  146. package/templates/screen-data/src/services/metrics-types.ts +4 -0
  147. package/templates/screen-data-http/src/services/metrics.ts +20 -0
  148. package/templates/screen-data-http/src/services/request.ts +3 -0
  149. package/templates/screen-data-http/tests/metrics.test.ts.template +21 -0
  150. package/templates/screen-data-local/src/services/metrics.ts +11 -0
  151. package/templates/screen-data-local/tests/metrics.test.ts.template +9 -0
  152. package/templates/screen-http/API-CONTRACT.md +20 -0
  153. package/templates/screen-http/src/services/http.ts +17 -0
  154. package/templates/screen-request-auth/src/services/request.ts +15 -0
  155. package/templates/screen-router/src/App.vue +7 -0
  156. package/templates/screen-router/src/main.ts +13 -0
  157. package/templates/screen-router/src/router.ts +7 -0
  158. package/templates/screen-router/tests/app.test.ts.template +20 -0
  159. package/templates/screen-viewport/src/screen/ScreenViewport.vue +30 -0
  160. package/templates/screen-visualization-charts/src/screen/components/ScreenVisualization.vue +22 -0
  161. package/templates/screen-visualization-cockpit/src/screen/components/ScreenVisualization.vue +33 -0
  162. package/templates/screen-visualization-cockpit/src/screen/components/ScreenWidget.vue +10 -0
  163. package/templates/screen-visualization-datav/src/screen/components/ScreenVisualization.vue +11 -0
  164. package/templates/vben-compatible/MIGRATION.md +2 -0
  165. package/templates/vben-compatible/src/{views/migration → modules/vben-migration}/VbenFormView.vue +2 -1
  166. package/templates/vben-compatible/src/{views/migration → modules/vben-migration}/VbenUsersView.vue +3 -2
  167. package/templates/vben-compatible/src/modules/vben-migration/components/VbenFormExample.vue +64 -0
  168. package/templates/vben-compatible/src/{components/migration → modules/vben-migration/components}/VbenUsers.vue +73 -64
  169. package/templates/vben-compatible/src/modules/vben-migration/local.ts +68 -0
  170. package/templates/vben-compatible/src/modules/vben-migration/module.ts +42 -0
  171. package/templates/vue/index.html +12 -0
  172. package/templates/vue/src/main.ts +5 -0
  173. package/templates/vue/src/styles.css +19 -0
  174. package/templates/vue/tests/app.test.ts.template +10 -0
  175. package/templates/vue/vite.config.ts +9 -0
  176. package/templates/vue/vitest.config.ts +4 -0
  177. package/templates/base/eslint.config.mjs +0 -8
  178. package/templates/base/src/components/app/AppHeaderActions.vue +0 -34
  179. package/templates/base/src/router/index.ts +0 -86
  180. package/templates/base/src/router/views.ts +0 -16
  181. package/templates/base/src/services/contracts.ts +0 -29
  182. package/templates/base/src/services/session-runtime.ts +0 -137
  183. package/templates/base/src/views/error/ForbiddenView.vue +0 -25
  184. package/templates/base/vitest.config.ts +0 -3
  185. package/templates/features/examples/src/modules/examples.ts +0 -10
  186. package/templates/features/examples/src/views/examples/CrudExampleView.vue +0 -30
  187. package/templates/features/system/src/business/system/entities.ts +0 -93
  188. package/templates/features/system/src/modules/system.ts +0 -28
  189. package/templates/features/system/src/views/system/SystemEntityView.vue +0 -85
  190. package/templates/modes/http/src/services/data-source.ts +0 -120
  191. package/templates/modes/http/src/services/http-adapter.ts +0 -124
  192. package/templates/modes/local/src/services/data-source.ts +0 -176
  193. package/templates/vben-compatible/src/components/migration/VbenFormExample.vue +0 -34
  194. package/templates/vben-compatible/src/modules/vben.ts +0 -17
@@ -1,8 +0,0 @@
1
- import antfu from '@antfu/eslint-config'
2
-
3
- export default antfu({
4
- vue: true,
5
- ignores: ['dist/**', 'coverage/**'],
6
- // 与 Lumal 仓库一致,保留 Composition API 功能分块注释。
7
- rules: { 'style/spaced-comment': 'off' },
8
- })
@@ -1,34 +0,0 @@
1
- <script setup lang="ts">
2
- import type { ResolvedThemeMode } from '@lumal/core/theme'
3
- import { LumalIcon } from '@lumal/icons-vue'
4
- import { ElButton } from 'element-plus'
5
-
6
- /***********************头部操作契约*********************/
7
- withDefaults(defineProps<{
8
- resolvedThemeMode: ResolvedThemeMode
9
- userName?: string
10
- }>(), {
11
- userName: '管理员',
12
- })
13
-
14
- defineEmits<{
15
- logout: []
16
- openSettings: []
17
- toggleTheme: []
18
- }>()
19
- </script>
20
-
21
- <template>
22
- <div class="lumal-admin-header-actions">
23
- <ElButton text :title="resolvedThemeMode === 'dark' ? '切换浅色模式' : '切换深色模式'" :aria-label="resolvedThemeMode === 'dark' ? '切换浅色模式' : '切换深色模式'" @click="$emit('toggleTheme')">
24
- <LumalIcon :name="resolvedThemeMode === 'dark' ? 'lumal:sun' : 'lumal:moon'" />
25
- </ElButton>
26
- <ElButton text title="偏好设置" aria-label="偏好设置" @click="$emit('openSettings')">
27
- <LumalIcon name="lumal:settings" />
28
- </ElButton>
29
- <span class="lumal-admin-header-actions__user">{{ userName }}</span>
30
- <ElButton text aria-label="退出登录" @click="$emit('logout')">
31
- 退出
32
- </ElButton>
33
- </div>
34
- </template>
@@ -1,86 +0,0 @@
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
- }
@@ -1,16 +0,0 @@
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
- }
@@ -1,29 +0,0 @@
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
- }
@@ -1,137 +0,0 @@
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
- }
@@ -1,25 +0,0 @@
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>
@@ -1,3 +0,0 @@
1
- import { defineConfig } from 'vitest/config'
2
-
3
- export default defineConfig({ test: { environment: 'node' } })
@@ -1,10 +0,0 @@
1
- import type { FeatureModule } from '../router/views'
2
-
3
- export const menus: FeatureModule['menus'] = [
4
- { path: '/examples/form', name: 'SchemaFormExample', component: 'examples/form', meta: { title: 'Schema 表单', keepAlive: true } },
5
- { path: '/examples/crud', name: 'CrudExample', component: 'examples/crud', meta: { title: 'CRUD 示例', keepAlive: true } },
6
- ]
7
- export const views: FeatureModule['views'] = {
8
- 'examples/form': () => import('../views/examples/FormExampleView.vue'),
9
- 'examples/crud': () => import('../views/examples/CrudExampleView.vue'),
10
- }
@@ -1,30 +0,0 @@
1
- <script setup lang="ts">
2
- import type { CrudDataSource, SchemaTableRow } from '@lumal/core/components'
3
- import { LumalCrudTable } from '@lumal/core/components'
4
-
5
- /***********************隔离的 CRUD 示例数据*********************/
6
- let rows: SchemaTableRow[] = [{ id: 'example', name: '示例记录', status: 'enabled' }]
7
- const columns = [{ field: 'name', label: '名称' }, { field: 'status', label: '状态' }]
8
- const dataSource: CrudDataSource = {
9
- fetch: async ({ page, pageSize, query }) => {
10
- const filtered = rows.filter(row => String(row.name).includes(String(query.keyword ?? '')))
11
- return { items: filtered.slice((page - 1) * pageSize, page * pageSize), total: filtered.length }
12
- },
13
- create: async (model) => { rows = [...rows, { ...model, id: crypto.randomUUID() }] },
14
- update: async (row, model) => { rows = rows.map(item => item.id === row.id ? { ...model, id: row.id } : item) },
15
- remove: async (row) => { rows = rows.filter(item => item.id !== row.id) },
16
- }
17
- </script>
18
-
19
- <template>
20
- <main class="lumal-admin-page">
21
- <LumalCrudTable
22
- title="CRUD 示例"
23
- :data-source="dataSource"
24
- :table="{ columns, rowKey: 'id', showColumnSettings: true, selection: true }"
25
- :query="{ schemas: [{ field: 'keyword', label: '关键词', component: 'input' }] }"
26
- :form-schemas="[{ field: 'name', label: '名称', component: 'input', required: true }, { field: 'status', label: '状态', component: 'select', options: [{ label: '启用', value: 'enabled' }, { label: '停用', value: 'disabled' }], defaultValue: 'enabled' }]"
27
- :toolbar="{ export: true, refresh: true }"
28
- />
29
- </main>
30
- </template>
@@ -1,93 +0,0 @@
1
- import type { SchemaFormItem, SchemaTableColumn } from '@lumal/core/components'
2
- import { localMenus } from '../../router/views'
3
-
4
- export interface EntityDefinition {
5
- title: string
6
- resource: string
7
- permission: string
8
- schemas: SchemaFormItem[]
9
- columns: SchemaTableColumn[]
10
- create?: boolean
11
- remove?: boolean
12
- }
13
- const status: SchemaFormItem = {
14
- field: 'status',
15
- label: '状态',
16
- component: 'select',
17
- defaultValue: 'enabled',
18
- options: [{ label: '启用', value: 'enabled' }, { label: '停用', value: 'disabled' }],
19
- }
20
- function input(field: string, label: string, required = true): SchemaFormItem {
21
- return { field, label, required, component: 'input' }
22
- }
23
- function define(resource: string, permission: string, title: string, schemas: SchemaFormItem[]): EntityDefinition {
24
- return {
25
- title,
26
- resource,
27
- permission,
28
- schemas,
29
- columns: schemas.map(schema => ({
30
- field: schema.field,
31
- label: schema.label,
32
- options: schema.component === 'select' && Array.isArray(schema.options) ? schema.options : undefined,
33
- })),
34
- }
35
- }
36
-
37
- // 保持应用侧字段契约;页面与 HTTP adapter 都不依赖演示应用源码。
38
- export const entities: EntityDefinition[] = [
39
- define('users', 'system:user', '用户管理', [
40
- { ...input('username', '用户名'), editDisabled: true },
41
- input('nickname', '昵称'),
42
- input('phone', '手机号', false),
43
- input('organizationId', '机构 ID', false),
44
- { field: 'roles', label: '角色', component: 'select', componentProps: { multiple: true }, defaultValue: ['guest'], options: [{ label: '管理员', value: 'admin' }, { label: '运营', value: 'operator' }, { label: '访客', value: 'guest' }] },
45
- status,
46
- ]),
47
- define('roles', 'system:role', '角色管理', [
48
- input('name', '角色名称'),
49
- { ...input('code', '角色编码'), editDisabled: true },
50
- input('description', '说明', false),
51
- status,
52
- {
53
- field: 'permissions',
54
- label: '授权权限',
55
- component: 'checkbox',
56
- defaultValue: [],
57
- options: [...new Set(localMenus.flatMap((menu) => {
58
- const prefix = String(menu.meta?.permissionPrefix ?? '')
59
- return prefix ? ['list', 'view', 'create', 'update', 'delete', 'authorize'].map(action => `${prefix}:${action}`) : []
60
- }))].map(value => ({ label: value, value })),
61
- },
62
- ]),
63
- define('menus', 'system:menu', '菜单管理', [
64
- input('title', '标题'),
65
- input('path', '路径'),
66
- input('name', '路由名'),
67
- { ...input('component', '组件白名单键'), required: ({ model }) => model.type === 'menu', hidden: ({ model }) => model.type !== 'menu' },
68
- input('parentId', '父节点 ID', false),
69
- input('permission', '权限标识', false),
70
- { field: 'type', label: '类型', component: 'select', defaultValue: 'menu', options: [{ label: '目录', value: 'directory' }, { label: '页面', value: 'menu' }, { label: '按钮', value: 'button' }] },
71
- { field: 'order', label: '排序', component: 'number', defaultValue: 0 },
72
- { field: 'hidden', label: '隐藏菜单', component: 'switch', defaultValue: false },
73
- ]),
74
- define('organizations', 'system:organization', '机构管理', [input('name', '机构名称'), input('code', '机构编码'), input('parentId', '父机构 ID', false), input('leader', '负责人', false), status]),
75
- define('dictionaries/types', 'system:dict', '字典管理', [input('name', '字典名称'), input('code', '字典编码'), input('description', '说明', false), status]),
76
- define('dictionaries/items', 'system:dict', '字典项管理', [
77
- input('typeCode', '字典编码'),
78
- input('label', '显示名称'),
79
- input('value', '值'),
80
- input('color', '颜色', false),
81
- { field: 'order', label: '排序', component: 'number', defaultValue: 0 },
82
- status,
83
- ]),
84
- {
85
- ...define('config', 'system:config', '系统配置', [
86
- { ...input('key', '配置键'), disabled: true },
87
- input('value', '配置值'),
88
- input('description', '说明', false),
89
- ]),
90
- create: false,
91
- remove: false,
92
- },
93
- ]
@@ -1,28 +0,0 @@
1
- import type { LumalMenuRecord } from '@lumal/core/router'
2
- import type { FeatureModule } from '../router/views'
3
-
4
- const definitions = [
5
- ['users', 'user', '用户管理'],
6
- ['roles', 'role', '角色管理'],
7
- ['menus', 'menu', '菜单管理'],
8
- ['organizations', 'organization', '机构管理'],
9
- ['dictionaries/types', 'dict', '字典管理'],
10
- ['dictionaries/items', 'dict', '字典项管理'],
11
- ['config', 'config', '系统配置'],
12
- ]
13
- export const menus: LumalMenuRecord[] = definitions.map(([resource, code, title]) => ({
14
- component: `system/${resource}`,
15
- name: `System-${resource!.replaceAll('/', '-')}`,
16
- path: `/system/${resource}`,
17
- meta: {
18
- title,
19
- resource,
20
- permissionPrefix: `system:${code}`,
21
- authority: [`system:${code}:${code === 'config' ? 'view' : 'list'}`],
22
- keepAlive: true,
23
- },
24
- }))
25
- export const views: FeatureModule['views'] = Object.fromEntries(definitions.map(([resource]) => [
26
- `system/${resource}`,
27
- () => import('../views/system/SystemEntityView.vue'),
28
- ]))
@@ -1,85 +0,0 @@
1
- <script setup lang="ts">
2
- import type { CrudDataSource, SchemaFormItem, SchemaFormOption, SchemaTableRow } from '@lumal/core/components'
3
- import { LumalCrudTable } from '@lumal/core/components'
4
- import { ElAlert } from 'element-plus'
5
- import { computed, shallowRef, useTemplateRef, watch } from 'vue'
6
- import { useRoute } from 'vue-router'
7
- import { entities } from '../../business/system/entities'
8
- import { permissionStore, service } from '../../services/session'
9
-
10
- /***********************业务模块配置*********************/
11
- const route = useRoute()
12
- // RouterView 按路径缓存实例;离开页面后不能读取新路由并切换本实例的数据源。
13
- const entity = entities.find(item => `/system/${item.resource}` === route.path)
14
- if (!entity)
15
- throw new Error('未注册的系统模块')
16
- const tableRef = useTemplateRef<InstanceType<typeof LumalCrudTable>>('tableRef')
17
- const can = (action: string) => permissionStore.hasPermission(`${entity.permission}:${action}`)
18
- const table = computed(() => ({ columns: entity.columns, showColumnSettings: true, rowKey: 'id', selection: true }))
19
- const actions = computed(() => ({ view: true, edit: can('update'), remove: entity.remove !== false && can('delete') }))
20
- const toolbar = computed(() => ({ create: entity.create !== false && can('create'), batchDelete: false, refresh: true, export: true }))
21
- const querySchemas = computed(() => [
22
- { field: 'keyword', label: '关键词', component: 'input' as const },
23
- ...(entity.resource === 'dictionaries/items' ? [{ field: 'typeCode', label: '字典编码', component: 'input' as const }] : []),
24
- ])
25
- const operationError = shallowRef('')
26
- function handleOperationError(error: unknown): void {
27
- operationError.value = error instanceof Error ? error.message : String(error)
28
- }
29
-
30
- /***********************业务引用数据*********************/
31
- const roleOptions = shallowRef<SchemaFormOption[]>()
32
- const schemas = computed<SchemaFormItem[]>(() => entity.schemas.map(schema =>
33
- schema.field === 'roles' && roleOptions.value ? { ...schema, options: roleOptions.value } : schema))
34
- watch(() => entity.resource, async (resource, _previous, onCleanup) => {
35
- let active = true
36
- onCleanup(() => {
37
- active = false
38
- })
39
- if (resource !== 'users' || !permissionStore.hasPermission('system:role:list'))
40
- return
41
- try {
42
- const result = await service.list('roles', { page: 1, pageSize: 1000, query: {} })
43
- if (active)
44
- roleOptions.value = result.items.filter(row => row.status !== 'disabled').map(row => ({ label: String(row.name ?? row.code), value: String(row.code) }))
45
- }
46
- catch (error) {
47
- // 引用数据失败应进入应用错误边界,不能把加载失败伪装成空角色。
48
- if (active)
49
- throw error
50
- }
51
- }, { immediate: true })
52
-
53
- /***********************真实应用数据源*********************/
54
- function id(row: SchemaTableRow): string {
55
- if (typeof row.id !== 'string' && typeof row.id !== 'number')
56
- throw new Error('业务记录缺少主键 id')
57
- return String(row.id)
58
- }
59
- const dataSource: CrudDataSource = {
60
- fetch: input => service.list(entity.resource, input),
61
- create: model => service.save(entity.resource, model),
62
- update: (row, model) => service.save(entity.resource, model, id(row)),
63
- remove: row => service.remove(entity.resource, id(row)),
64
- }
65
- defineExpose({ reload: () => tableRef.value?.reload() })
66
- </script>
67
-
68
- <template>
69
- <main class="lumal-admin-page">
70
- <ElAlert v-if="operationError" :title="operationError" type="error" role="alert" closable @close="operationError = ''" />
71
- <LumalCrudTable
72
- ref="tableRef"
73
- :key="entity.resource"
74
- :title="entity.title"
75
- :table="table"
76
- :form-schemas="schemas"
77
- :data-source="dataSource"
78
- :query="{ schemas: querySchemas, columns: 3 }"
79
- :actions="actions"
80
- :toolbar="toolbar"
81
- :editor="{ type: 'dialog', columns: 2 }"
82
- @operation-error="handleOperationError"
83
- />
84
- </main>
85
- </template>