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,120 +0,0 @@
1
- import type { ApplicationService, ServiceContext } from './contracts'
2
- import { createRequestClient, RequestError } from '@lumal/core/request'
3
- import { createHttpAdapter } from './http-adapter'
4
-
5
- export const demoAccount: { username: string, password: string } | null = null
6
-
7
- export function createHttpBackend(context: ServiceContext, baseURL: string, adapterName = 'standard'): ApplicationService {
8
- const adapter = createHttpAdapter(adapterName)
9
- const publicRequest = createRequestClient({
10
- baseURL,
11
- onRequest() {
12
- if (!baseURL.trim())
13
- throw new Error('未配置 VITE_API_BASE_URL,请先接入真实后端')
14
- },
15
- onResponse: <T>({ data, init }: { data: unknown, init: RequestInit }) => {
16
- init.signal?.throwIfAborted()
17
- return adapter.parseResponse(data) as T
18
- },
19
- })
20
- const request = createRequestClient({
21
- baseURL,
22
- duplicateSubmit: true,
23
- getToken: () => context.getSession()?.accessToken,
24
- onRequest({ init }) {
25
- if (!baseURL.trim())
26
- throw new Error('未配置 VITE_API_BASE_URL,请先接入真实后端')
27
- init.signal = init.signal ? AbortSignal.any([init.signal, context.getSignal()]) : context.getSignal()
28
- const token = context.getSession()?.accessToken
29
- if (!token || new Headers(init.headers).get('Authorization') !== `Bearer ${token}`)
30
- throw new RequestError('会话已切换', { kind: 'cancelled' })
31
- },
32
- onResponse: <T>({ data, init }: { data: unknown, init: RequestInit }) => {
33
- init.signal?.throwIfAborted()
34
- return adapter.parseResponse(data) as T
35
- },
36
- authRefresh: {
37
- async refresh({ init }) {
38
- const refreshToken = context.getSession()?.refreshToken
39
- if (!refreshToken)
40
- throw new Error('缺少刷新凭据')
41
- const result = await publicRequest.post('/auth/refresh', { body: { refreshToken }, signal: init.signal ?? undefined })
42
- init.signal?.throwIfAborted()
43
- if (context.getSession()?.refreshToken !== refreshToken)
44
- throw new DOMException('会话已切换', 'AbortError')
45
- context.setSession(adapter.parseSession(result))
46
- },
47
- },
48
- onSessionExpired: context.expire,
49
- })
50
- return {
51
- login: async (input, signal) => adapter.parseSession(await publicRequest.post('/auth/login', { body: { ...input }, signal })),
52
- getUser: async signal => adapter.parseUser(await request.get('/session/me', { signal })),
53
- getAccess: async signal => adapter.parseAccess(await request.get('/session/access', { signal })),
54
- getMenus: async signal => adapter.parseMenus(await request.get(adapter.menusPath, { signal })),
55
- logout: async (accessToken) => {
56
- await publicRequest.post('/auth/logout', { headers: { Authorization: `Bearer ${accessToken}` } })
57
- },
58
- clearCache() {
59
- publicRequest.clearCache()
60
- request.clearCache()
61
- },
62
- async list(resource, input) {
63
- const result = await request.get(`/system/${resource}`, {
64
- query: { page: input.page, pageSize: input.pageSize, ...input.query } as Record<string, string | number>,
65
- })
66
- if (resource === 'config' && adapter.configMode === 'object') {
67
- if (!result || typeof result !== 'object' || Array.isArray(result))
68
- throw new Error('系统配置响应必须是对象')
69
- const items = Object.entries(result).map(([key, value]) => ({ id: key, key, value: String(value) }))
70
- return { items, total: items.length }
71
- }
72
- // Mock 的树形 / 字典接口不分页,显式在应用 adapter 边界转换。
73
- if (adapterName === 'lumal-mock' && Array.isArray(result)) {
74
- const flatten = (rows: Record<string, unknown>[]): Record<string, unknown>[] => rows.flatMap(row => [
75
- { ...row, children: undefined },
76
- ...(Array.isArray(row.children) ? flatten(row.children) : []),
77
- ])
78
- const items = flatten(result)
79
- const keyword = String(input.query.keyword ?? '').trim()
80
- const filtered = items.filter(row => !keyword || Object.values(row).some(value => String(value).includes(keyword)))
81
- return { items: filtered.slice((input.page - 1) * input.pageSize, input.page * input.pageSize), total: filtered.length }
82
- }
83
- const page = adapter.parsePage(result)
84
- if (resource === 'roles' && adapterName === 'lumal-mock') {
85
- page.items = await Promise.all(page.items.map(async row => ({
86
- ...row,
87
- permissions: await request.get(`/system/roles/${encodeURIComponent(String(row.code))}/permissions`),
88
- })))
89
- }
90
- return page
91
- },
92
- async save(resource, model, id) {
93
- if (resource === 'config' && adapter.configMode === 'object') {
94
- if (!id)
95
- throw new Error('系统配置只允许更新已存在的配置键')
96
- const current = await request.get<Record<string, unknown>>('/system/config')
97
- if (!(id in current))
98
- throw new Error('配置键不存在')
99
- const original = current[id]
100
- const value = typeof original === 'boolean'
101
- ? String(model.value) === 'true'
102
- : typeof original === 'number' ? Number(model.value) : String(model.value ?? '')
103
- return request.put('/system/config', { body: { [id]: value } })
104
- }
105
- const saved = id
106
- ? await request.put(`/system/${resource}/${encodeURIComponent(id)}`, { body: model })
107
- : await request.post(`/system/${resource}`, { body: model })
108
- if (resource === 'roles' && adapterName === 'lumal-mock' && Array.isArray(model.permissions))
109
- await request.put(`/system/roles/${encodeURIComponent(String(model.code))}/permissions`, { body: { permissions: model.permissions } })
110
- return saved
111
- },
112
- remove: async (resource, id) => {
113
- await request.delete(`/system/${resource}/${encodeURIComponent(id)}`)
114
- },
115
- }
116
- }
117
-
118
- export function createBackend(context: ServiceContext): ApplicationService {
119
- return createHttpBackend(context, import.meta.env.VITE_API_BASE_URL ?? '', import.meta.env.VITE_API_ADAPTER || 'standard')
120
- }
@@ -1,124 +0,0 @@
1
- import type { AuthSessionData } from '@lumal/core/auth'
2
- import type { LumalMenuRecord } from '@lumal/core/router'
3
- import type { AccessSnapshot, CurrentUser, PageResult } from './contracts'
4
- import { parseAuthSession } from '@lumal/core/auth'
5
- import { createStandardResponseParser, parsePageResult } from '@lumal/core/request'
6
- import { localMenus, viewComponents } from '../router/views'
7
-
8
- export interface HttpAdapter {
9
- parseResponse: (data: unknown) => unknown
10
- parseSession: (data: unknown) => AuthSessionData
11
- parseUser: (data: unknown) => CurrentUser
12
- parseAccess: (data: unknown) => AccessSnapshot
13
- parseMenus: (data: unknown) => LumalMenuRecord[]
14
- parsePage: (data: unknown) => PageResult
15
- menusPath: string
16
- configMode?: 'object'
17
- }
18
-
19
- function record(value: unknown): Record<string, unknown> {
20
- if (!value || typeof value !== 'object' || Array.isArray(value))
21
- throw new Error('后端返回了无效对象')
22
- return value as Record<string, unknown>
23
- }
24
- function strings(value: unknown): string[] {
25
- if (!Array.isArray(value) || value.some(item => typeof item !== 'string'))
26
- throw new Error('角色和权限必须是字符串数组')
27
- return value
28
- }
29
- function parseUser(value: unknown): CurrentUser {
30
- const raw = record(value)
31
- if (!['id', 'name', 'username'].every(key => typeof raw[key] === 'string' && raw[key]))
32
- throw new Error('当前用户缺少 id/name/username')
33
- return { id: String(raw.id), name: String(raw.name), username: String(raw.username) }
34
- }
35
- function parseAccess(value: unknown): AccessSnapshot {
36
- const raw = record(value)
37
- return { roles: strings(raw.roles), permissions: strings(raw.permissions) }
38
- }
39
- function parseMenus(value: unknown): LumalMenuRecord[] {
40
- if (!Array.isArray(value))
41
- throw new Error('菜单响应必须是数组')
42
- const visit = (items: unknown[]): LumalMenuRecord[] => items.map((item) => {
43
- const raw = record(item)
44
- if (typeof raw.path !== 'string' || !raw.path.startsWith('/'))
45
- throw new Error('菜单 path 必须是站内绝对路径')
46
- if (raw.component && (typeof raw.component !== 'string' || !viewComponents[raw.component]))
47
- throw new Error(`菜单组件不在白名单中:${String(raw.component)}`)
48
- return {
49
- path: raw.path,
50
- name: typeof raw.name === 'string' ? raw.name : undefined,
51
- component: typeof raw.component === 'string' ? raw.component : undefined,
52
- redirect: typeof raw.redirect === 'string' ? raw.redirect : undefined,
53
- meta: raw.meta ? record(raw.meta) : undefined,
54
- children: Array.isArray(raw.children) ? visit(raw.children) : undefined,
55
- }
56
- })
57
- return visit(value)
58
- }
59
-
60
- // 默认直接消费标准 JSON。特定后端包装仅在显式选择的应用 adapter 中处理。
61
- export function createHttpAdapter(name = 'standard'): HttpAdapter {
62
- const standard: HttpAdapter = {
63
- parseResponse: value => value,
64
- parseSession: parseAuthSession,
65
- parseUser,
66
- parseAccess,
67
- parseMenus,
68
- parsePage: value => Array.isArray(value) ? { items: value, total: value.length } : parsePageResult(value),
69
- menusPath: '/menus',
70
- }
71
- if (name === 'standard')
72
- return standard
73
- if (name !== 'lumal-mock')
74
- throw new Error(`未知 HTTP adapter:${name}`)
75
- const unwrap = createStandardResponseParser({
76
- fieldNames: { code: 'statusCode', data: 'result', message: 'resultMsg' },
77
- successCodes: ['0000'],
78
- sessionExpiredCodes: ['AUTH_EXPIRED'],
79
- })
80
- return {
81
- ...standard,
82
- // Lumal Mock 是显式接入示例,不能影响标准模式的协议。
83
- parseResponse: data => unwrap({ data } as Parameters<typeof unwrap>[0]),
84
- parseSession: value => parseAuthSession(value, { fieldNames: { accessToken: 'access_token', refreshToken: 'refresh_token', expiresAt: 'expire_time' } }),
85
- parsePage: value => Array.isArray(value) ? { items: value, total: value.length } : parsePageResult(value, { fieldNames: { items: 'records', total: 'totalNum' } }),
86
- menusPath: '/menu',
87
- configMode: 'object',
88
- parseAccess(value) {
89
- const access = parseAccess(value)
90
- // 此后端以 view 权限管理配置更新;只在显式 adapter 中映射应用权限。
91
- return {
92
- ...access,
93
- permissions: access.permissions.includes('system:config:view')
94
- ? [...new Set([...access.permissions, 'system:config:update'])]
95
- : access.permissions,
96
- }
97
- },
98
- parseMenus(value) {
99
- // 参考 Mock 包含图表等额外演示页面,只映射当前预设声明的页面。
100
- if (!Array.isArray(value))
101
- throw new Error('Mock 菜单响应必须是数组')
102
- const supported = new Map(localMenus.map(menu => [menu.component, menu]))
103
- const visit = (items: unknown[]): LumalMenuRecord[] => items.flatMap((item) => {
104
- const raw = record(item)
105
- const component = raw.viewPath === 'system/dict' ? 'system/dictionaries/types' : String(raw.viewPath ?? '')
106
- const selected = supported.get(component)
107
- return [
108
- ...(selected
109
- ? [{
110
- ...selected,
111
- meta: {
112
- ...selected.meta,
113
- title: String(raw.menuName ?? selected.meta?.title),
114
- authority: strings(raw.authCode ?? []),
115
- },
116
- }]
117
- : []),
118
- ...(Array.isArray(raw.nodes) ? visit(raw.nodes) : []),
119
- ]
120
- })
121
- return visit(value)
122
- },
123
- }
124
- }
@@ -1,176 +0,0 @@
1
- import type { LumalMenuRecord } from '@lumal/core/router'
2
- import type { AccessSnapshot, ApplicationService, CurrentUser, DataRow, ServiceContext } from './contracts'
3
- import { localMenus } from '../router/views'
4
-
5
- export const demoAccount = import.meta.env.DEV ? { username: 'admin', password: 'lumal123' } : null
6
-
7
- // 本地演示数据独立于 HTTP 模板,生产构建默认拒绝演示认证。
8
- export function createBackend(context: ServiceContext): ApplicationService {
9
- const permissions = [...new Set(localMenus.flatMap((menu) => {
10
- const prefix = String(menu.meta?.permissionPrefix ?? '')
11
- return prefix ? ['list', 'view', 'create', 'update', 'delete', 'authorize'].map(action => `${prefix}:${action}`) : []
12
- }))]
13
- const initial: Record<string, DataRow[]> = {
14
- users: [
15
- { id: 'admin', username: 'admin', nickname: '管理员', roles: ['admin'], status: 'enabled' },
16
- { id: 'guest', username: 'guest', nickname: '访客', roles: ['guest'], status: 'enabled' },
17
- ],
18
- roles: [
19
- { id: 'admin', code: 'admin', name: '管理员', status: 'enabled', permissions },
20
- { id: 'guest', code: 'guest', name: '访客', status: 'enabled', permissions: [] },
21
- ],
22
- menus: localMenus.map(menu => ({
23
- id: menu.name ?? menu.path,
24
- title: menu.meta?.title,
25
- path: menu.path,
26
- name: menu.name,
27
- component: menu.component,
28
- permission: menu.meta?.authority,
29
- type: 'menu',
30
- hidden: false,
31
- })),
32
- config: [{ id: 'appName', key: 'appName', value: 'Lumal Admin' }, { id: 'pageSize', key: 'pageSize', value: '10' }],
33
- }
34
- function readRows(resource: string): DataRow[] {
35
- const value = localStorage.getItem(`demo:system:${resource}`)
36
- return value ? JSON.parse(value) as DataRow[] : structuredClone(initial[resource] ?? [])
37
- }
38
- function assertDemo(): void {
39
- if (import.meta.env.PROD && import.meta.env.VITE_ENABLE_LOCAL_DEMO !== 'true')
40
- throw new Error('生产环境已禁用本地演示,请使用 HTTP 模式或显式启用演示')
41
- }
42
- function getUser(): CurrentUser {
43
- assertDemo()
44
- const raw = sessionStorage.getItem(`demo:${context.getSession()?.accessToken ?? ''}`)
45
- if (!raw)
46
- throw new Error('本地演示会话已失效')
47
- return JSON.parse(raw) as CurrentUser
48
- }
49
- function getAccess(): AccessSnapshot {
50
- const user = getUser()
51
- const record = readRows('users').find(row => row.username === user.username)
52
- if (!record || record.status === 'disabled')
53
- throw new Error('本地账号已停用或删除,请重新登录')
54
- const roles = Array.isArray(record.roles) ? record.roles.map(String) : []
55
- return {
56
- roles,
57
- permissions: [...new Set(readRows('roles').filter(role => roles.includes(String(role.code)) && role.status !== 'disabled').flatMap(role => Array.isArray(role.permissions) ? role.permissions.map(String) : []))],
58
- }
59
- }
60
- function writeRows(resource: string, rows: DataRow[]): void {
61
- localStorage.setItem(`demo:system:${resource}`, JSON.stringify(rows))
62
- }
63
- function assertWrite(resource: string, action: string): void {
64
- const menu = localMenus.find(menu => menu.meta?.resource === resource)
65
- const prefix = String(menu?.meta?.permissionPrefix ?? '')
66
- if (!prefix || !getAccess().permissions.includes(`${prefix}:${action}`))
67
- throw new Error('无操作权限')
68
- }
69
- function buildMenus(rows: DataRow[]): LumalMenuRecord[] {
70
- const active = new Set<string>()
71
- const identities = new Set(rows.map(row => String(row.id)))
72
- const visit = (parentId = ''): LumalMenuRecord[] => rows
73
- .filter(row => String(row.parentId ?? '') === parentId && row.type !== 'button')
74
- .sort((left, right) => Number(left.order ?? 0) - Number(right.order ?? 0))
75
- .map((row) => {
76
- const id = String(row.id)
77
- if (active.has(id))
78
- throw new Error('菜单存在循环父节点')
79
- active.add(id)
80
- const original = localMenus.find(menu => menu.component === row.component)
81
- if (row.type !== 'directory' && !original)
82
- throw new Error(`菜单组件不在预设白名单中:${String(row.component)}`)
83
- const children = visit(id)
84
- active.delete(id)
85
- return {
86
- path: String(row.path),
87
- name: String(row.name),
88
- component: row.type === 'directory' ? undefined : original?.component,
89
- children,
90
- meta: { ...original?.meta, title: String(row.title), hideInMenu: Boolean(row.hidden), authority: row.permission as string | string[], order: Number(row.order ?? 0) },
91
- }
92
- })
93
- for (const row of rows) {
94
- const parents = new Set([String(row.id)])
95
- let parent = String(row.parentId ?? '')
96
- while (parent) {
97
- if (parents.has(parent) || !identities.has(parent))
98
- throw new Error('菜单父节点不存在或形成循环')
99
- parents.add(parent)
100
- parent = String(rows.find(item => String(item.id) === parent)?.parentId ?? '')
101
- }
102
- if (typeof row.path !== 'string' || !row.path.startsWith('/') || !row.name)
103
- throw new Error('菜单必须配置绝对路径与唯一名称')
104
- }
105
- return visit()
106
- }
107
- return {
108
- async login(input, signal) {
109
- assertDemo()
110
- signal.throwIfAborted()
111
- const account = readRows('users').find(row => row.username === input.username)
112
- if (!account || account.status === 'disabled' || input.password !== 'lumal123')
113
- throw new Error('账号或密码不正确')
114
- const accessToken = crypto.randomUUID()
115
- sessionStorage.setItem(`demo:${accessToken}`, JSON.stringify({ id: input.username, name: input.username, username: input.username }))
116
- return { accessToken }
117
- },
118
- getUser: async (signal) => {
119
- signal.throwIfAborted()
120
- return getUser()
121
- },
122
- getAccess: async (signal) => {
123
- signal.throwIfAborted()
124
- return getAccess()
125
- },
126
- getMenus: async (signal) => {
127
- signal.throwIfAborted()
128
- getUser()
129
- // 只解析本预设组件白名单;菜单可编辑层级,不接受任意远程 import。
130
- return buildMenus(readRows('menus'))
131
- },
132
- logout: async (token) => { sessionStorage.removeItem(`demo:${token}`) },
133
- clearCache() {},
134
- async list(resource, input) {
135
- assertWrite(resource, resource === 'config' ? 'view' : 'list')
136
- const keyword = String(input.query.keyword ?? '').trim()
137
- const rows = readRows(resource).filter(row => (!keyword || Object.values(row).some(value => String(value).includes(keyword)))
138
- && (!input.query.typeCode || row.typeCode === input.query.typeCode))
139
- if (typeof input.query.sortField === 'string' && input.query.sortOrder) {
140
- const field = input.query.sortField
141
- rows.sort((left, right) => String(left[field] ?? '').localeCompare(String(right[field] ?? ''), 'zh-CN', { numeric: true }) * (input.query.sortOrder === 'desc' ? -1 : 1))
142
- }
143
- return { items: rows.slice((input.page - 1) * input.pageSize, input.page * input.pageSize), total: rows.length }
144
- },
145
- async save(resource, model, id) {
146
- assertWrite(resource, id ? 'update' : 'create')
147
- if (resource === 'roles' && Array.isArray(model.permissions))
148
- assertWrite(resource, 'authorize')
149
- const rows = readRows(resource)
150
- if (id && !rows.some(row => row.id === id))
151
- throw new Error('记录不存在')
152
- const record: DataRow = { ...model, id: id ?? crypto.randomUUID() }
153
- const uniqueField = resource === 'users' ? 'username' : resource === 'roles' ? 'code' : undefined
154
- if (uniqueField && rows.some(row => row.id !== id && row[uniqueField] === record[uniqueField]))
155
- throw new Error(`${uniqueField} 重复`)
156
- if (resource === 'users' && id === 'admin' && (record.status === 'disabled' || !Array.isArray(record.roles) || !record.roles.includes('admin')))
157
- throw new Error('不能停用或移除内置管理员角色')
158
- const assignedPermissions = record.permissions
159
- if (resource === 'roles' && id === 'admin' && (record.code !== 'admin' || record.status === 'disabled' || !Array.isArray(assignedPermissions) || !permissions.every(code => assignedPermissions.includes(code))))
160
- throw new Error('不能停用或移除内置管理员权限')
161
- const next = id ? rows.map(row => row.id === id ? record : row) : [...rows, record]
162
- if (resource === 'menus')
163
- buildMenus(next)
164
- writeRows(resource, next)
165
- return record
166
- },
167
- async remove(resource, id) {
168
- assertWrite(resource, 'delete')
169
- if (['users', 'roles'].includes(resource) && id === 'admin')
170
- throw new Error('不能删除内置管理员')
171
- if (resource === 'menus' && readRows(resource).some(row => row.parentId === id))
172
- throw new Error('请先删除子菜单')
173
- writeRows(resource, readRows(resource).filter(row => row.id !== id))
174
- },
175
- }
176
- }
@@ -1,34 +0,0 @@
1
- <script setup lang="ts">
2
- import { useVbenForm } from '@lumal/vben-compat/v5'
3
- import { shallowRef } from 'vue'
4
-
5
- /***********************5.7.0 基础控件与动态依赖迁移*********************/
6
- const submitted = shallowRef('')
7
- const [Form, formApi] = useVbenForm({
8
- layout: 'horizontal',
9
- wrapperClass: 'grid-cols-2',
10
- handleSubmit(values) { submitted.value = JSON.stringify(values) },
11
- schema: [
12
- { component: 'Input', fieldName: 'name', label: '姓名', rules: 'required' },
13
- { component: 'Select', fieldName: 'role', label: '角色', rules: 'selectRequired', componentProps: { options: [{ label: '管理员', value: 'admin' }, { label: '访客', value: 'guest' }] } },
14
- { component: 'DatePicker', fieldName: 'date', label: '日期', rules: 'selectRequired', componentProps: { valueFormat: 'YYYY-MM-DD' } },
15
- { component: 'Switch', fieldName: 'enabled', label: '启用扩展', defaultValue: true },
16
- {
17
- component: 'Input',
18
- fieldName: 'remark',
19
- label: '备注',
20
- dependencies: { triggerFields: ['enabled', 'role'], if: values => Boolean(values.enabled), disabled: values => values.role === 'guest', required: values => values.role === 'admin' },
21
- },
22
- { component: 'Input', fieldName: 'cssHidden', label: '保留值字段', defaultValue: 'retained', dependencies: { triggerFields: ['enabled'], show: values => Boolean(values.enabled) } },
23
- ],
24
- })
25
- defineExpose({ formApi })
26
- </script>
27
-
28
- <template>
29
- <section>
30
- <h2>Vben 5 表单</h2>
31
- <Form />
32
- <output aria-label="提交结果">{{ submitted }}</output>
33
- </section>
34
- </template>
@@ -1,17 +0,0 @@
1
- import { adaptVbenMenus } from '@lumal/vben-compat/v5/router'
2
- import '@lumal/vben-compat/style.css'
3
-
4
- // Vben authority 是角色;按钮权限继续由应用 PermissionStore 管理。
5
- export const menus = adaptVbenMenus([
6
- { path: '/migration/form', name: 'VbenForm', component: 'migration/form', meta: { title: 'Vben 表单迁移', keepAlive: true } },
7
- { path: '/migration/users', name: 'VbenUsers', component: 'migration/users', meta: { title: 'Vben 业务迁移', authority: ['admin'], keepAlive: true } },
8
- ]).map((menu) => {
9
- if (menu.component !== 'migration/users')
10
- return menu
11
- // 迁移页也可单独生成,资源权限不能依赖可选的 system 模块。
12
- return { ...menu, meta: { ...menu.meta, permissionPrefix: 'system:user', resource: 'users' } }
13
- })
14
- export const views = {
15
- 'migration/form': () => import('../views/migration/VbenFormView.vue'),
16
- 'migration/users': () => import('../views/migration/VbenUsersView.vue'),
17
- }