create-lumal-admin 0.0.4-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 (185) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +98 -12
  3. package/dist/cli.d.ts +18 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +308 -28
  6. package/dist/cli.js.map +1 -1
  7. package/dist/create.d.ts +11 -1
  8. package/dist/create.d.ts.map +1 -1
  9. package/dist/create.js +98 -2
  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 +41 -0
  16. package/dist/presets.d.ts.map +1 -0
  17. package/dist/presets.js +58 -0
  18. package/dist/presets.js.map +1 -0
  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/scaffold.d.ts +2 -11
  28. package/dist/scaffold.d.ts.map +1 -1
  29. package/dist/scaffold.js +2 -1602
  30. package/dist/scaffold.js.map +1 -1
  31. package/dist/templates.d.ts +7 -0
  32. package/dist/templates.d.ts.map +1 -0
  33. package/dist/templates.js +91 -0
  34. package/dist/templates.js.map +1 -0
  35. package/dist/types.d.ts +64 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/dist/types.js +2 -0
  38. package/dist/types.js.map +1 -0
  39. package/package.json +13 -4
  40. package/templates/base/.env.example +4 -0
  41. package/templates/base/API-CONTRACT.md +84 -0
  42. package/templates/base/README.md +59 -0
  43. package/templates/base/index.html +12 -0
  44. package/templates/base/package.json +38 -0
  45. package/templates/base/src/App.vue +206 -0
  46. package/templates/base/src/app/components/AppHeaderActions.vue +46 -0
  47. package/templates/base/src/app/components/AppSettingsDrawer.vue +37 -0
  48. package/templates/base/src/app/menu-policy.ts +163 -0
  49. package/templates/base/src/app/module.ts +46 -0
  50. package/templates/base/src/app/router/index.ts +107 -0
  51. package/templates/base/src/app/router/views.ts +12 -0
  52. package/templates/base/src/app/services/access.ts +2 -0
  53. package/templates/base/src/app/services/contracts.ts +54 -0
  54. package/templates/base/src/app/services/local-contracts.ts +18 -0
  55. package/templates/base/src/app/services/preferences.ts +41 -0
  56. package/templates/base/src/app/services/session-runtime.ts +186 -0
  57. package/templates/base/src/app/services/session.ts +14 -0
  58. package/templates/base/src/app/stored-menus.ts +72 -0
  59. package/templates/base/src/app/views/SafeIframeView.vue +34 -0
  60. package/templates/base/src/app/views/auth/LoginView.vue +75 -0
  61. package/templates/base/src/app/views/error/ForbiddenView.vue +20 -0
  62. package/templates/base/src/app/views/error/NotFoundView.vue +16 -0
  63. package/templates/base/src/main.ts +40 -0
  64. package/templates/base/src/modules/business/README.md +33 -0
  65. package/templates/base/src/modules/dashboard/DashboardView.vue +27 -0
  66. package/templates/base/src/modules/dashboard/module.ts +14 -0
  67. package/templates/base/src/shared/api.ts +53 -0
  68. package/templates/base/src/shared/components/BaseEntityDialog.vue +39 -0
  69. package/templates/base/src/shared/composables/use-async-data.ts +35 -0
  70. package/templates/base/src/shared/composables/use-entity-editor.ts +77 -0
  71. package/templates/base/src/shared/status.ts +4 -0
  72. package/templates/base/src/shared/tree.ts +41 -0
  73. package/templates/base/src/styles.scss +173 -0
  74. package/templates/base/tests/menu-policy.test.ts.template +89 -0
  75. package/templates/base/tests/session.test.ts.template +45 -0
  76. package/templates/base/vite.config.ts +54 -0
  77. package/templates/blank/README.md +6 -0
  78. package/templates/blank/src/App.vue +11 -0
  79. package/templates/common/AGENTS.md +13 -0
  80. package/templates/common/_editorconfig +15 -0
  81. package/templates/common/_gitattributes +6 -0
  82. package/templates/common/_gitignore +7 -0
  83. package/templates/common/_prettierignore +6 -0
  84. package/templates/common/docs/DEVELOPMENT.md +26 -0
  85. package/templates/common/eslint.config.mjs +60 -0
  86. package/templates/common/package.json +39 -0
  87. package/templates/common/prettier.config.mjs +8 -0
  88. package/templates/common/tsconfig.json +19 -0
  89. package/templates/common/vitest.config.ts +3 -0
  90. package/templates/features/examples/src/modules/examples/CrudExampleView.vue +51 -0
  91. package/templates/features/examples/src/modules/examples/FormExampleView.vue +32 -0
  92. package/templates/features/examples/src/modules/examples/module.ts +27 -0
  93. package/templates/features/system/src/modules/system/config/ConfigView.vue +104 -0
  94. package/templates/features/system/src/modules/system/config/api.ts +38 -0
  95. package/templates/features/system/src/modules/system/config/routes.ts +21 -0
  96. package/templates/features/system/src/modules/system/dictionary/DictionaryItemView.vue +141 -0
  97. package/templates/features/system/src/modules/system/dictionary/DictionaryTypeView.vue +107 -0
  98. package/templates/features/system/src/modules/system/dictionary/api.ts +61 -0
  99. package/templates/features/system/src/modules/system/dictionary/cache.ts +9 -0
  100. package/templates/features/system/src/modules/system/dictionary/routes.ts +32 -0
  101. package/templates/features/system/src/modules/system/dictionary/types.ts +22 -0
  102. package/templates/features/system/src/modules/system/local.ts +348 -0
  103. package/templates/features/system/src/modules/system/menu/MenuView.vue +133 -0
  104. package/templates/features/system/src/modules/system/menu/api.ts +73 -0
  105. package/templates/features/system/src/modules/system/menu/components/MenuEditorDialog.vue +106 -0
  106. package/templates/features/system/src/modules/system/menu/routes.ts +23 -0
  107. package/templates/features/system/src/modules/system/menu/types.ts +18 -0
  108. package/templates/features/system/src/modules/system/module.ts +22 -0
  109. package/templates/features/system/src/modules/system/organization/OrganizationView.vue +134 -0
  110. package/templates/features/system/src/modules/system/organization/api.ts +38 -0
  111. package/templates/features/system/src/modules/system/organization/routes.ts +23 -0
  112. package/templates/features/system/src/modules/system/organization/types.ts +17 -0
  113. package/templates/features/system/src/modules/system/role/RoleView.vue +90 -0
  114. package/templates/features/system/src/modules/system/role/api.ts +40 -0
  115. package/templates/features/system/src/modules/system/role/components/RoleAuthorizationDialog.vue +110 -0
  116. package/templates/features/system/src/modules/system/role/permission-tree.ts +31 -0
  117. package/templates/features/system/src/modules/system/role/routes.ts +24 -0
  118. package/templates/features/system/src/modules/system/role/types.ts +12 -0
  119. package/templates/features/system/src/modules/system/user/UserView.vue +297 -0
  120. package/templates/features/system/src/modules/system/user/api.ts +45 -0
  121. package/templates/features/system/src/modules/system/user/components/UserOrganizationFilter.vue +61 -0
  122. package/templates/features/system/src/modules/system/user/components/UserRoleDialog.vue +93 -0
  123. package/templates/features/system/src/modules/system/user/routes.ts +27 -0
  124. package/templates/features/system/src/modules/system/user/types.ts +28 -0
  125. package/templates/features/system/tests/system.test.ts.template +266 -0
  126. package/templates/modes/http/src/app/services/data-source.ts +124 -0
  127. package/templates/modes/http/src/app/services/http-adapter.ts +128 -0
  128. package/templates/modes/http/tests/http.test.ts.template +218 -0
  129. package/templates/modes/local/src/app/services/data-source.ts +113 -0
  130. package/templates/modes/local/src/app/services/local-database.ts +67 -0
  131. package/templates/modes/local/tests/local.test.ts.template +132 -0
  132. package/templates/presets/standard/src/modules/dashboard/DashboardView.vue +52 -0
  133. package/templates/presets/standard/src/modules/dashboard/components/DashboardSessionStatus.vue +41 -0
  134. package/templates/presets/standard/src/modules/dashboard/components/DashboardShortcuts.vue +51 -0
  135. package/templates/presets/standard/src/modules/profile/ProfileView.vue +109 -0
  136. package/templates/presets/standard/src/modules/profile/api.ts +49 -0
  137. package/templates/presets/standard/src/modules/profile/components/ProfilePasswordForm.vue +68 -0
  138. package/templates/presets/standard/src/modules/profile/local.ts +40 -0
  139. package/templates/presets/standard/src/modules/profile/module.ts +11 -0
  140. package/templates/presets/standard/tests/profile.test.ts.template +72 -0
  141. package/templates/screen/README.md +6 -0
  142. package/templates/screen/src/App.vue +7 -0
  143. package/templates/screen/src/screen/ScreenEntry.vue +18 -0
  144. package/templates/screen/src/screen/ScreenView.vue +48 -0
  145. package/templates/screen/src/screen/components/ScreenMetrics.vue +40 -0
  146. package/templates/screen-auth/src/screen/ScreenEntry.vue +52 -0
  147. package/templates/screen-auth/src/screen/components/ScreenLoginForm.vue +44 -0
  148. package/templates/screen-auth/src/services/session.ts +61 -0
  149. package/templates/screen-auth/tests/session.test.ts.template +54 -0
  150. package/templates/screen-auth-http/src/services/auth-adapter.ts +14 -0
  151. package/templates/screen-auth-http/tests/auth-adapter.test.ts.template +17 -0
  152. package/templates/screen-auth-local-demo/src/services/auth-adapter.ts +15 -0
  153. package/templates/screen-auth-local-demo/tests/auth-adapter.test.ts.template +18 -0
  154. package/templates/screen-data/src/screen/ScreenView.vue +69 -0
  155. package/templates/screen-data/src/services/metrics-types.ts +4 -0
  156. package/templates/screen-data-http/src/services/metrics.ts +20 -0
  157. package/templates/screen-data-http/src/services/request.ts +3 -0
  158. package/templates/screen-data-http/tests/metrics.test.ts.template +21 -0
  159. package/templates/screen-data-local/src/services/metrics.ts +11 -0
  160. package/templates/screen-data-local/tests/metrics.test.ts.template +9 -0
  161. package/templates/screen-http/API-CONTRACT.md +20 -0
  162. package/templates/screen-http/src/services/http.ts +17 -0
  163. package/templates/screen-request-auth/src/services/request.ts +15 -0
  164. package/templates/screen-router/src/App.vue +7 -0
  165. package/templates/screen-router/src/main.ts +13 -0
  166. package/templates/screen-router/src/router.ts +7 -0
  167. package/templates/screen-router/tests/app.test.ts.template +20 -0
  168. package/templates/screen-viewport/src/screen/ScreenViewport.vue +30 -0
  169. package/templates/screen-visualization-charts/src/screen/components/ScreenVisualization.vue +22 -0
  170. package/templates/screen-visualization-cockpit/src/screen/components/ScreenVisualization.vue +33 -0
  171. package/templates/screen-visualization-cockpit/src/screen/components/ScreenWidget.vue +10 -0
  172. package/templates/screen-visualization-datav/src/screen/components/ScreenVisualization.vue +11 -0
  173. package/templates/vben-compatible/MIGRATION.md +30 -0
  174. package/templates/vben-compatible/src/modules/vben-migration/VbenFormView.vue +11 -0
  175. package/templates/vben-compatible/src/modules/vben-migration/VbenUsersView.vue +12 -0
  176. package/templates/vben-compatible/src/modules/vben-migration/components/VbenFormExample.vue +64 -0
  177. package/templates/vben-compatible/src/modules/vben-migration/components/VbenUsers.vue +144 -0
  178. package/templates/vben-compatible/src/modules/vben-migration/local.ts +68 -0
  179. package/templates/vben-compatible/src/modules/vben-migration/module.ts +42 -0
  180. package/templates/vue/index.html +12 -0
  181. package/templates/vue/src/main.ts +5 -0
  182. package/templates/vue/src/styles.css +19 -0
  183. package/templates/vue/tests/app.test.ts.template +10 -0
  184. package/templates/vue/vite.config.ts +9 -0
  185. package/templates/vue/vitest.config.ts +4 -0
@@ -0,0 +1,54 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { authenticate, refreshAuth } from '../src/services/auth-adapter';
3
+ import {
4
+ authenticated,
5
+ getSessionSignal,
6
+ getSessionToken,
7
+ refreshSession,
8
+ signIn,
9
+ signOut,
10
+ } from '../src/services/session';
11
+
12
+ vi.mock('../src/services/auth-adapter', () => ({
13
+ authEnabled: true,
14
+ authenticate: vi.fn(),
15
+ refreshAuth: vi.fn(),
16
+ }));
17
+ afterEach(() => {
18
+ signOut();
19
+ vi.resetAllMocks();
20
+ vi.useRealTimers();
21
+ });
22
+
23
+ describe('独立 Screen 会话', () => {
24
+ it('登录、刷新、退出并取消旧请求', async () => {
25
+ vi.mocked(authenticate).mockResolvedValue({ accessToken: 'fixture', refreshToken: 'refresh' });
26
+ vi.mocked(refreshAuth).mockResolvedValue({ accessToken: 'renewed' });
27
+ await signIn('test', 'secret');
28
+ const previous = getSessionSignal();
29
+ await refreshSession();
30
+ expect(getSessionToken()).toBe('renewed');
31
+ signOut();
32
+ expect(previous.aborted).toBe(true);
33
+ expect(authenticated.value).toBe(false);
34
+ });
35
+ it('迟到登录不能恢复已退出的身份', async () => {
36
+ let finish!: (data: { accessToken: string }) => void;
37
+ vi.mocked(authenticate).mockImplementation(() => new Promise((resolve) => (finish = resolve)));
38
+ const pending = signIn('old', 'secret');
39
+ signOut();
40
+ finish({ accessToken: 'late' });
41
+ await expect(pending).rejects.toThrow('会话已切换');
42
+ expect(getSessionToken()).toBeUndefined();
43
+ });
44
+ it('登录失败不回退,过期清除凭据', async () => {
45
+ vi.mocked(authenticate).mockRejectedValueOnce(new Error('401'));
46
+ await expect(signIn('wrong', 'wrong')).rejects.toThrow('401');
47
+ expect(authenticated.value).toBe(false);
48
+ vi.useFakeTimers();
49
+ vi.mocked(authenticate).mockResolvedValue({ accessToken: 'short', expiresAt: Date.now() + 100 });
50
+ await signIn('user', 'secret');
51
+ vi.advanceTimersByTime(101);
52
+ expect(authenticated.value).toBe(false);
53
+ });
54
+ });
@@ -0,0 +1,14 @@
1
+ import { parseAuthSession } from '@lumal/core/auth';
2
+ import { publicRequest } from './http';
3
+
4
+ export const authDemo = false;
5
+ export const authEnabled = true;
6
+
7
+ export async function authenticate(username: string, password: string, signal: AbortSignal) {
8
+ return parseAuthSession(await publicRequest.post('/auth/login', { body: { username, password }, signal }));
9
+ }
10
+
11
+ export async function refreshAuth(refreshToken: string, signal: AbortSignal) {
12
+ if (!refreshToken) throw new Error('会话已过期,请重新登录');
13
+ return parseAuthSession(await publicRequest.post('/auth/refresh', { body: { refreshToken }, signal }));
14
+ }
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { authenticate } from '../src/services/auth-adapter';
3
+ import { publicRequest } from '../src/services/http';
4
+
5
+ vi.mock('../src/services/http', () => ({ publicRequest: { post: vi.fn() } }));
6
+ describe('接口登录契约', () => {
7
+ it('验证响应,401 不回退演示', async () => {
8
+ vi.mocked(publicRequest.post).mockResolvedValueOnce({ accessToken: 'fixture' });
9
+ await expect(authenticate('user', 'secret', new AbortController().signal)).resolves.toEqual({
10
+ accessToken: 'fixture',
11
+ });
12
+ vi.mocked(publicRequest.post).mockRejectedValueOnce(new Error('Unauthorized'));
13
+ await expect(authenticate('user', 'bad', new AbortController().signal)).rejects.toThrow('Unauthorized');
14
+ vi.mocked(publicRequest.post).mockResolvedValueOnce({ code: 403 });
15
+ await expect(authenticate('user', 'bad', new AbortController().signal)).rejects.toThrow();
16
+ });
17
+ });
@@ -0,0 +1,15 @@
1
+ import type { AuthSessionData } from '@lumal/core/auth';
2
+
3
+ export const authDemo = true;
4
+ export const authEnabled = import.meta.env.DEV;
5
+
6
+ export async function authenticate(username: string, password: string, signal: AbortSignal): Promise<AuthSessionData> {
7
+ signal.throwIfAborted();
8
+ if (!authEnabled) throw new Error('生产环境禁止 Local Demo 登录');
9
+ if (username !== 'demo' || password !== 'demo') throw new Error('演示凭据错误');
10
+ return { accessToken: 'local-demo-only', expiresAt: Date.now() + 3_600_000 };
11
+ }
12
+
13
+ export async function refreshAuth(_refreshToken: string, _signal: AbortSignal): Promise<AuthSessionData> {
14
+ throw new Error('Local Demo 过期后请重新登录');
15
+ }
@@ -0,0 +1,18 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ afterEach(() => {
4
+ vi.unstubAllEnvs();
5
+ vi.resetModules();
6
+ });
7
+ describe('本地 Demo 安全边界', () => {
8
+ it('开发显式认证,错误凭据不成功', async () => {
9
+ const { authenticate } = await import('../src/services/auth-adapter');
10
+ await expect(authenticate('demo', 'demo', new AbortController().signal)).resolves.toHaveProperty('accessToken');
11
+ await expect(authenticate('bad', 'bad', new AbortController().signal)).rejects.toThrow('演示凭据');
12
+ });
13
+ it('生产禁用,即使知道演示凭据', async () => {
14
+ vi.stubEnv('DEV', false);
15
+ const { authenticate } = await import('../src/services/auth-adapter');
16
+ await expect(authenticate('demo', 'demo', new AbortController().signal)).rejects.toThrow('生产环境禁止');
17
+ });
18
+ });
@@ -0,0 +1,69 @@
1
+ <script setup lang="ts">
2
+ import type { ScreenMetric } from '../services/metrics-types';
3
+ import { onBeforeUnmount, onMounted, shallowRef } from 'vue';
4
+ import { loadMetrics } from '../services/metrics';
5
+ import ScreenMetrics from './components/ScreenMetrics.vue';
6
+ // SCREEN_VISUALIZATION_IMPORT
7
+
8
+ /***********************指标加载*********************/
9
+ const metrics = shallowRef<ScreenMetric[]>([]);
10
+ const loading = shallowRef(false);
11
+ const error = shallowRef('');
12
+ let controller: AbortController | undefined;
13
+
14
+ async function reload() {
15
+ controller?.abort();
16
+ const current = new AbortController();
17
+ controller = current;
18
+ loading.value = true;
19
+ error.value = '';
20
+ try {
21
+ const result = await loadMetrics(current.signal);
22
+ if (!current.signal.aborted) metrics.value = result;
23
+ } catch (cause) {
24
+ if (!current.signal.aborted) error.value = cause instanceof Error ? cause.message : '数据加载失败';
25
+ } finally {
26
+ if (!current.signal.aborted) loading.value = false;
27
+ }
28
+ }
29
+
30
+ onMounted(() => void reload());
31
+ onBeforeUnmount(() => controller?.abort());
32
+ </script>
33
+
34
+ <template>
35
+ <main class="screen-view" :aria-busy="loading">
36
+ <header class="screen-view__header">
37
+ <h1>数据总览</h1>
38
+ <button type="button" :disabled="loading" @click="reload">刷新数据</button>
39
+ </header>
40
+ <p v-if="error" role="alert">{{ error }}</p>
41
+ <p v-else-if="loading" role="status">加载中</p>
42
+ <ScreenMetrics :metrics="metrics" />
43
+ <!-- SCREEN_VISUALIZATION -->
44
+ </main>
45
+ </template>
46
+
47
+ <style scoped>
48
+ .screen-view {
49
+ height: 100%;
50
+ min-width: 0;
51
+ min-height: 0;
52
+ flex: 1;
53
+ overflow: auto;
54
+ padding: 32px;
55
+ font-size: max(14px, var(--lumal-viewport-min-font-size, 14px));
56
+ }
57
+ .screen-view__header {
58
+ display: flex;
59
+ flex-wrap: wrap;
60
+ align-items: center;
61
+ justify-content: space-between;
62
+ gap: 16px;
63
+ margin-bottom: 32px;
64
+ }
65
+ .screen-view__header h1 {
66
+ margin: 0;
67
+ font-size: max(28px, var(--lumal-viewport-min-font-size, 14px));
68
+ }
69
+ </style>
@@ -0,0 +1,4 @@
1
+ export interface ScreenMetric {
2
+ label: string;
3
+ value: number;
4
+ }
@@ -0,0 +1,20 @@
1
+ import type { ScreenMetric } from './metrics-types';
2
+ import { request } from './request';
3
+
4
+ export async function loadMetrics(signal?: AbortSignal): Promise<ScreenMetric[]> {
5
+ const data = await request.get<unknown>('/screen/metrics', { signal });
6
+ if (
7
+ !Array.isArray(data) ||
8
+ !data.every(
9
+ (item) =>
10
+ item !== null &&
11
+ typeof item === 'object' &&
12
+ typeof item.label === 'string' &&
13
+ typeof item.value === 'number' &&
14
+ Number.isFinite(item.value),
15
+ )
16
+ ) {
17
+ throw new Error('指标响应不符合 API-CONTRACT.md');
18
+ }
19
+ return data as ScreenMetric[];
20
+ }
@@ -0,0 +1,3 @@
1
+ import { publicRequest } from './http';
2
+
3
+ export const request = publicRequest;
@@ -0,0 +1,21 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { loadMetrics } from '../src/services/metrics';
3
+ import { request } from '../src/services/request';
4
+
5
+ vi.mock('../src/services/request', () => ({ request: { get: vi.fn() } }));
6
+ beforeEach(() => vi.resetAllMocks());
7
+
8
+ describe('接口指标 Adapter', () => {
9
+ it('透传取消并验证响应', async () => {
10
+ vi.mocked(request.get).mockResolvedValue([{ label: '任务', value: 3 }]);
11
+ const signal = new AbortController().signal;
12
+ expect(await loadMetrics(signal)).toEqual([{ label: '任务', value: 3 }]);
13
+ expect(request.get).toHaveBeenCalledWith('/screen/metrics', { signal });
14
+ });
15
+ it('接口失败不能回退静态示例', async () => {
16
+ vi.mocked(request.get).mockRejectedValue(new Error('backend rejected'));
17
+ await expect(loadMetrics()).rejects.toThrow('backend rejected');
18
+ vi.mocked(request.get).mockResolvedValue({ code: 403 });
19
+ await expect(loadMetrics()).rejects.toThrow('指标响应');
20
+ });
21
+ });
@@ -0,0 +1,11 @@
1
+ import type { ScreenMetric } from './metrics-types';
2
+
3
+ /** 明确的本地示例,不会在 HTTP 失败时被调用。 */
4
+ export async function loadMetrics(signal?: AbortSignal): Promise<ScreenMetric[]> {
5
+ signal?.throwIfAborted();
6
+ return [
7
+ { label: '任务总数', value: 128 },
8
+ { label: '已完成', value: 96 },
9
+ { label: '进行中', value: 32 },
10
+ ];
11
+ }
@@ -0,0 +1,9 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { loadMetrics } from '../src/services/metrics';
3
+
4
+ describe('本地指标', () => {
5
+ it('返回明确的示例且尊重取消', async () => {
6
+ expect(await loadMetrics()).toHaveLength(3);
7
+ await expect(loadMetrics(AbortSignal.abort())).rejects.toBeDefined();
8
+ });
9
+ });
@@ -0,0 +1,20 @@
1
+ # Screen HTTP 契约
2
+
3
+ 仅为生成项目的应用 Adapter 约定,不是对任意真实后端的兼容承诺。
4
+ 先配置 `VITE_API_BASE_URL`。无配置明确报错,不使用演示数据兜底。
5
+
6
+ - 选择 HTTP 业务数据时:`GET /screen/metrics` 返回 `[{ label: string, value: number }]`。
7
+ - 选择 HTTP 认证时:`POST /auth/login` 接收 `{ username, password }`,
8
+ 返回 `{ accessToken, refreshToken?, expiresAt? }`。`expiresAt` 是毫秒时间戳。
9
+ - 配置了 refreshToken 时,认证失效通过 `POST /auth/refresh` 接收 `{ refreshToken }`,
10
+ 返回相同会话结构。刷新失败退出,不降级。未提供 refreshToken 时过期直接退出。
11
+ - `Authorization: Bearer <token>` 由 Core 注入。后端仍必须执行权限检查。
12
+ - 默认请求超时 15 秒;取消不会伪装为成功。退出、切换身份会取消旧会话请求。
13
+ - 无认证的 HTTP 数据请求 401 只展示错误,不假定需要登录。
14
+
15
+ `http.ts`、`auth-adapter.ts`、`metrics.ts` 是应用可以修改的 Adapter。
16
+ 后端有 `code/data/msg` 包装时,在客户端的 `onResponse` 显式接入
17
+ `createStandardResponseParser`;字段映射和业务上下文不得写进 Core。
18
+ Token 使用当前标签页的 sessionStorage;按部署安全要求调整,不把演示凭据用于真实后端。
19
+ 登录页只在原路径切换内容,路由不会来回跳转或丢失当前 Screen 路径。
20
+ 服务端 Token 撤销、单点登录和业务 Store 清理由应用按接口契约补充。
@@ -0,0 +1,17 @@
1
+ import { createRequestClient } from '@lumal/core/request';
2
+
3
+ /** 可在这里配置成功码或字段映射;默认协议直接返回 JSON,不猜测业务包装。 */
4
+ export const apiBaseURL = import.meta.env.VITE_API_BASE_URL?.trim() ?? '';
5
+
6
+ export function prepareRequest(init: RequestInit) {
7
+ if (!apiBaseURL) throw new Error('请配置 VITE_API_BASE_URL');
8
+ const signals = [AbortSignal.timeout(15_000)];
9
+ if (init.signal) signals.push(init.signal);
10
+ init.signal = AbortSignal.any(signals);
11
+ }
12
+
13
+ // 登录/刷新用独立客户端,401 不触发已登录会话的退出或递归刷新。
14
+ export const publicRequest = createRequestClient({
15
+ baseURL: apiBaseURL,
16
+ onRequest: ({ init }) => prepareRequest(init),
17
+ });
@@ -0,0 +1,15 @@
1
+ import { createRequestClient } from '@lumal/core/request';
2
+ import { apiBaseURL, prepareRequest } from './http';
3
+ import { getSessionScope, getSessionSignal, getSessionToken, refreshSession, signOut } from './session';
4
+
5
+ export const request = createRequestClient({
6
+ baseURL: apiBaseURL,
7
+ getToken: getSessionToken,
8
+ getCacheScope: getSessionScope,
9
+ onRequest: ({ init }) => {
10
+ prepareRequest(init);
11
+ init.signal = AbortSignal.any([init.signal!, getSessionSignal()]);
12
+ },
13
+ authRefresh: { refresh: refreshSession },
14
+ onSessionExpired: () => signOut(),
15
+ });
@@ -0,0 +1,7 @@
1
+ <script setup lang="ts">
2
+ import { RouterView } from 'vue-router';
3
+ </script>
4
+
5
+ <template>
6
+ <RouterView />
7
+ </template>
@@ -0,0 +1,13 @@
1
+ import { createApp } from 'vue';
2
+ import App from './App.vue';
3
+ import { router } from './router';
4
+ import './styles.css';
5
+
6
+ async function bootstrap() {
7
+ const app = createApp(App);
8
+ app.use(router);
9
+ await router.isReady();
10
+ app.mount('#app');
11
+ }
12
+
13
+ void bootstrap();
@@ -0,0 +1,7 @@
1
+ import { createRouter, createWebHashHistory } from 'vue-router';
2
+ import ScreenEntry from './screen/ScreenEntry.vue';
3
+
4
+ export const router = createRouter({
5
+ history: createWebHashHistory(import.meta.env.BASE_URL),
6
+ routes: [{ path: '/', name: 'Screen', component: ScreenEntry }],
7
+ });
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createSSRApp } from 'vue';
3
+ import { createMemoryHistory, createRouter } from 'vue-router';
4
+ import { renderToString } from 'vue/server-renderer';
5
+ import App from '../src/App.vue';
6
+ import ScreenEntry from '../src/screen/ScreenEntry.vue';
7
+
8
+ describe('应用路由', () => {
9
+ it('可以渲染首页', async () => {
10
+ const router = createRouter({
11
+ history: createMemoryHistory(),
12
+ routes: [{ path: '/', component: ScreenEntry }],
13
+ });
14
+ const app = createSSRApp(App);
15
+ app.use(router);
16
+ await router.push('/');
17
+ await router.isReady();
18
+ expect(await renderToString(app)).toContain('<main');
19
+ });
20
+ });
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import { LumalViewport } from '@lumal/viewport';
3
+
4
+ /***********************只适配实际内容区,登录栏独立占位*********************/
5
+ const policy = {
6
+ mode: /* VIEWPORT_MODE */ 'contain',
7
+ designWidth: 1920,
8
+ designHeight: 1080,
9
+ minFontSize: 12,
10
+ } as const;
11
+ </script>
12
+
13
+ <template>
14
+ <div class="screen-viewport">
15
+ <LumalViewport :policy="policy">
16
+ <slot />
17
+ </LumalViewport>
18
+ </div>
19
+ </template>
20
+
21
+ <style scoped>
22
+ .screen-viewport {
23
+ flex: 1;
24
+ min-width: 0;
25
+ min-height: 0;
26
+ }
27
+ .screen-viewport :deep([data-viewport-mode='vw'] .screen-view) {
28
+ padding: max(16px, calc(32 * var(--lumal-viewport-x-unit)));
29
+ }
30
+ </style>
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ import type { EChartsOption } from 'echarts';
3
+ import { LumalChart } from '@lumal/charts';
4
+ import { LineChart } from 'echarts/charts';
5
+ import { GridComponent, TooltipComponent } from 'echarts/components';
6
+ import { use } from 'echarts/core';
7
+ import { CanvasRenderer } from 'echarts/renderers';
8
+ import '@lumal/charts/style.css';
9
+
10
+ /***********************只注册示例实际使用的图表能力*********************/
11
+ use([LineChart, GridComponent, TooltipComponent, CanvasRenderer]);
12
+ const options: EChartsOption = {
13
+ grid: { top: 20, right: 20, left: 44, bottom: 24 },
14
+ xAxis: { type: 'category', data: ['A', 'B', 'C', 'D', 'E'] },
15
+ yAxis: { type: 'value' },
16
+ series: [{ type: 'line', data: [12, 24, 18, 36, 30] }],
17
+ };
18
+ </script>
19
+
20
+ <template>
21
+ <LumalChart :options="options" height="240px" />
22
+ </template>
@@ -0,0 +1,33 @@
1
+ <script setup lang="ts">
2
+ import { createDefaultCockpitConfig, createWidgetInstance } from '@lumal/cockpit';
3
+ import { createCockpitRegistry } from '@lumal/cockpit/registry';
4
+ import { LumalCockpit } from '@lumal/cockpit/runtime';
5
+ import '@lumal/cockpit/style.css';
6
+ import 'element-plus/dist/index.css';
7
+
8
+ /***********************最小运行时编排,不加载 Designer*********************/
9
+ const registry = createCockpitRegistry();
10
+ registry.registerWidget({ type: 'metric', label: '指标', component: () => import('./ScreenWidget.vue') });
11
+ const config = createDefaultCockpitConfig('screen-demo');
12
+ const layout = config.layouts[0]!;
13
+ layout.left.width = layout.right.width = 200;
14
+ layout.left.columns[0]!.width = layout.right.columns[0]!.width = 200;
15
+ layout.left.rows[0]!.cells[0]!.widget = createWidgetInstance('metric', '任务指标');
16
+ </script>
17
+
18
+ <template>
19
+ <div class="screen-cockpit">
20
+ <LumalCockpit :config="config" :registry="registry" viewport-mode="external">
21
+ <template #center><p>数据总览</p></template>
22
+ </LumalCockpit>
23
+ </div>
24
+ </template>
25
+
26
+ <style scoped>
27
+ .screen-cockpit {
28
+ width: 100%;
29
+ height: 360px;
30
+ min-width: 0;
31
+ overflow: auto;
32
+ }
33
+ </style>
@@ -0,0 +1,10 @@
1
+ <script setup lang="ts">
2
+ /***********************通用本地指标*********************/
3
+ const count = 128;
4
+ </script>
5
+
6
+ <template>
7
+ <p>
8
+ 任务总数 <strong>{{ count }}</strong>
9
+ </p>
10
+ </template>
@@ -0,0 +1,11 @@
1
+ <script setup lang="ts">
2
+ import LumalBorderBox from '@lumal/datav/border-box';
3
+ import LumalDigitalFlop from '@lumal/datav/digital-flop';
4
+ import '@lumal/datav/style.css';
5
+ </script>
6
+
7
+ <template>
8
+ <LumalBorderBox :variant="8" style="height: 180px">
9
+ <LumalDigitalFlop :value="128" suffix=" 项" />
10
+ </LumalBorderBox>
11
+ </template>
@@ -0,0 +1,30 @@
1
+ # Vben 5 Element Plus 迁移基准
2
+
3
+ 基准 v5.7.0,commit `63a38dce49ba109f61607994e21ba921d8e970e9`。
4
+ 本项目不加载 VXE Table / vee-validate / Vben Store。
5
+
6
+ 1. `#/adapter/form` 和 `#/adapter/vxe-table` 改为 `@lumal/vben-compat/v5`。
7
+ 2. 保留 `[Form, formApi]`、单数 `schema`、`[Grid, gridApi]`、顶层 `formOptions`。
8
+ 3. 业务 API 接到本项目 ApplicationService。query 仍接收
9
+ `{ page: { currentPage, pageSize }, sort }` 和第二参数 formValues。
10
+ 4. 默认结果为 `{ items, total }`,不同响应在 `proxyConfig.response` 明确配置。
11
+ 5. Element Plus 控件属性沿用;Tailwind 布局需转成项目 CSS,
12
+ `wrapperClass` 的基础 grid-cols-N 映射为原生响应式列数。
13
+ 6. 原 Vben Drawer/Modal 改为 Element Plus Dialog 或 Lumal CRUD 编辑器。
14
+ 7. `@vben/access` 的高频用法可改为本入口的 AccessControl / useAccess;
15
+ 路由数据先经 adaptVbenMenus,authority 按角色匹配。
16
+ 8. `#toolbar-actions` 与 `#toolbar-tools` 用于具名工具栏插槽;当前不注入完整 VXE 工具栏上下文,
17
+ 命令通过已获得的 `gridApi` 调用。兼容页面位于独立 `modules/vben-migration`,不混入系统管理。
18
+
19
+ 本模板 `migration/form` 对应 web-ele/demos/form/basic 与共享 rules/dynamic
20
+ 场景;`migration/users` 对应共享 vxe-table/form 和 system/user 的
21
+ 查询、分页、列配置、插槽操作、弹窗校验、增删改与按钮权限。
22
+ 不是声称原始页面仅换 import 就可以完整运行。
23
+
24
+ 不支持:任意自定义表单组件注册、VXE renderer、虚拟滚动、高级编辑、
25
+ 表单嵌套路径、fieldMappingTime、Vben 全局偏好 Store/Drawer/Modal。
26
+ 配置不在受支持类型内时会明确报错,不静默忽略。
27
+ Zod 校验器可由应用自行安装后传入 rules;兼容包不内置或导出 z。
28
+ Lumal Form 泛型约束模型字段,不是 Vben 的控件名称泛型;显式泛型需调整。
29
+ 配置通过 hook/setState 更新,不接受 schema/gridOptions 组件 props。
30
+ Zod transform 需在应用提交边界处理;Grid 的命令式 query/reload 会向调用方传播错误。
@@ -0,0 +1,11 @@
1
+ <script setup lang="ts">
2
+ /***********************迁移表单场景装配*********************/
3
+ import VbenFormExample from './components/VbenFormExample.vue';
4
+ import '@lumal/vben-compat/style.css';
5
+ </script>
6
+
7
+ <template>
8
+ <main class="lumal-admin-page">
9
+ <VbenFormExample />
10
+ </main>
11
+ </template>
@@ -0,0 +1,12 @@
1
+ <script setup lang="ts">
2
+ import { service } from '../../app/services/session';
3
+ /***********************迁移业务场景装配*********************/
4
+ import VbenUsers from './components/VbenUsers.vue';
5
+ import '@lumal/vben-compat/style.css';
6
+ </script>
7
+
8
+ <template>
9
+ <main class="lumal-admin-page">
10
+ <VbenUsers :service="service" />
11
+ </main>
12
+ </template>
@@ -0,0 +1,64 @@
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) {
11
+ submitted.value = JSON.stringify(values);
12
+ },
13
+ schema: [
14
+ { component: 'Input', fieldName: 'name', label: '姓名', rules: 'required' },
15
+ {
16
+ component: 'Select',
17
+ fieldName: 'role',
18
+ label: '角色',
19
+ rules: 'selectRequired',
20
+ componentProps: {
21
+ options: [
22
+ { label: '管理员', value: 'admin' },
23
+ { label: '访客', value: 'guest' },
24
+ ],
25
+ },
26
+ },
27
+ {
28
+ component: 'DatePicker',
29
+ fieldName: 'date',
30
+ label: '日期',
31
+ rules: 'selectRequired',
32
+ componentProps: { valueFormat: 'YYYY-MM-DD' },
33
+ },
34
+ { component: 'Switch', fieldName: 'enabled', label: '启用扩展', defaultValue: true },
35
+ {
36
+ component: 'Input',
37
+ fieldName: 'remark',
38
+ label: '备注',
39
+ dependencies: {
40
+ triggerFields: ['enabled', 'role'],
41
+ if: (values) => Boolean(values.enabled),
42
+ disabled: (values) => values.role === 'guest',
43
+ required: (values) => values.role === 'admin',
44
+ },
45
+ },
46
+ {
47
+ component: 'Input',
48
+ fieldName: 'cssHidden',
49
+ label: '保留值字段',
50
+ defaultValue: 'retained',
51
+ dependencies: { triggerFields: ['enabled'], show: (values) => Boolean(values.enabled) },
52
+ },
53
+ ],
54
+ });
55
+ defineExpose({ formApi });
56
+ </script>
57
+
58
+ <template>
59
+ <section>
60
+ <h2>Vben 5 表单</h2>
61
+ <Form />
62
+ <output aria-label="提交结果">{{ submitted }}</output>
63
+ </section>
64
+ </template>