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,34 +1,45 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { createApplicationSession } from '../src/services/session-runtime'
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createApplicationSession } from '../src/app/services/session-runtime';
3
3
 
4
4
  describe('应用会话', () => {
5
5
  it('统一清理认证、权限、菜单与标签', () => {
6
- const values = new Map<string, string>()
6
+ const values = new Map<string, string>();
7
7
  const storage = {
8
8
  getItem: (key: string) => values.get(key) ?? null,
9
- setItem: (key: string, value: string) => { values.set(key, value) },
10
- removeItem: (key: string) => { values.delete(key) },
9
+ setItem: (key: string, value: string) => {
10
+ values.set(key, value);
11
+ },
12
+ removeItem: (key: string) => {
13
+ values.delete(key);
14
+ },
11
15
  clear: () => values.clear(),
12
16
  key: (index: number) => [...values.keys()][index] ?? null,
13
- get length() { return values.size },
14
- }
15
- const app = createApplicationSession(() => ({
16
- login: async () => ({ accessToken: 'test-only' }),
17
- getUser: async () => ({ id: '1', username: 'test', name: 'Test' }),
18
- getAccess: async () => ({ roles: ['admin'], permissions: ['read'] }),
19
- getMenus: async () => [],
20
- logout: async () => {},
21
- clearCache: () => {},
22
- list: async () => ({ items: [], total: 0 }),
23
- save: async () => {},
24
- remove: async () => {},
25
- }), storage, storage)
26
- app.session.setSession({ accessToken: 'test-only' })
27
- app.permissionStore.setPermissions(['read'])
28
- storage.setItem(app.tabStorageKey, 'old-tabs')
29
- app.reset()
30
- expect(app.isAuthenticated()).toBe(false)
31
- expect(app.permissionStore.permissions).toEqual([])
32
- expect(storage.getItem(app.tabStorageKey)).toBeNull()
33
- })
34
- })
17
+ get length() {
18
+ return values.size;
19
+ },
20
+ };
21
+ const app = createApplicationSession(
22
+ () => ({
23
+ login: async () => ({ accessToken: 'test-only' }),
24
+ getUser: async () => ({ id: '1', username: 'test', name: 'Test' }),
25
+ getAccess: async () => ({ roles: ['admin'], permissions: ['read'] }),
26
+ getMenus: async () => [],
27
+ logout: async () => {},
28
+ clearCache: () => {},
29
+ request: async <T>() => undefined as T,
30
+ list: async () => ({ items: [], total: 0 }),
31
+ save: async () => {},
32
+ remove: async () => {},
33
+ }),
34
+ storage,
35
+ storage,
36
+ );
37
+ app.session.setSession({ accessToken: 'test-only' });
38
+ app.permissionStore.setPermissions(['read']);
39
+ storage.setItem(app.tabStorageKey, 'old-tabs');
40
+ app.reset();
41
+ expect(app.isAuthenticated()).toBe(false);
42
+ expect(app.permissionStore.permissions).toEqual([]);
43
+ expect(storage.getItem(app.tabStorageKey)).toBeNull();
44
+ });
45
+ });
@@ -1,6 +1,6 @@
1
- import { fileURLToPath, URL } from 'node:url'
2
- import vue from '@vitejs/plugin-vue'
3
- import { defineConfig } from 'vite'
1
+ import { fileURLToPath, URL } from 'node:url';
2
+ import vue from '@vitejs/plugin-vue';
3
+ import { defineConfig } from 'vite';
4
4
 
5
5
  /***********************应用开发配置*********************/
6
6
  export default defineConfig({
@@ -51,4 +51,4 @@ export default defineConfig({
51
51
  '@': fileURLToPath(new URL('./src', import.meta.url)),
52
52
  },
53
53
  },
54
- })
54
+ });
@@ -0,0 +1,6 @@
1
+ # Vue 项目
2
+
3
+ Vue 3 + TypeScript + Vite。运行时仅依赖 Vue,无登录、路由、Store 或后台框架。
4
+
5
+ 首次运行 `pnpm install`,开发使用 `pnpm dev`,提交前执行 `pnpm check`。
6
+ 工程规范见 [开发约定](docs/DEVELOPMENT.md)。
@@ -0,0 +1,11 @@
1
+ <template>
2
+ <main class="app">
3
+ <h1>Vue</h1>
4
+ </main>
5
+ </template>
6
+
7
+ <style scoped>
8
+ .app {
9
+ padding: 24px;
10
+ }
11
+ </style>
@@ -0,0 +1,13 @@
1
+ # 项目开发指令
2
+
3
+ 先阅读 [开发约定](docs/DEVELOPMENT.md),再修改代码。
4
+
5
+ - 使用 Vue 3、Composition API、`<script setup lang="ts">` 和明确的 TypeScript 类型。
6
+ - 组合式逻辑按 `/***********************说明*********************/` 分块,模板引用使用 `useTemplateRef`。
7
+ - 只使用依赖包的公开 API,不导入 Lumal Monorepo 私有源码。
8
+ - 组件负责呈现,Composable 负责复用状态与生命周期,API Adapter 负责后端字段转换。
9
+ - 路由负责页面装配,Store 只保存跨页面状态;业务规则留在业务模块。
10
+ - 前端认证、菜单和按钮权限不能替代服务端鉴权。
11
+ - 新增页面、修改接口或公共行为时补充对应测试,禁止将接口失败伪装成演示成功。
12
+ - 不添加未使用的重依赖,不批量改写无关文件,不提交凭据、私有数据或构建产物。
13
+ - 完成前执行 `pnpm check`,分别报告通过、失败和未执行的检查。
@@ -0,0 +1,15 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ indent_style = space
7
+ indent_size = 2
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
10
+
11
+ [*.md]
12
+ trim_trailing_whitespace = false
13
+
14
+ [*.{yml,yaml}]
15
+ indent_size = 2
@@ -0,0 +1,6 @@
1
+ * text=auto eol=lf
2
+ *.png binary
3
+ *.jpg binary
4
+ *.jpeg binary
5
+ *.webp binary
6
+ *.woff2 binary
@@ -4,3 +4,4 @@ coverage/
4
4
  .env.local
5
5
  .env.*.local
6
6
  *.log
7
+ .pnpm-store/
@@ -0,0 +1,6 @@
1
+ node_modules/
2
+ dist/
3
+ coverage/
4
+ .pnpm-store/
5
+ pnpm-lock.yaml
6
+ **/*.d.ts
@@ -0,0 +1,26 @@
1
+ # 开发约定
2
+
3
+ ## 通用
4
+
5
+ 组件使用 PascalCase 文件名,目录、普通 TypeScript 文件使用 kebab-case。
6
+ 页面负责组合功能组件;组件通过 props/emits 通信,真正跨页面的状态才放入 Store。
7
+ 异步副作用必须处理加载、失败、取消和卸载,不用假成功替代后端错误。
8
+ 公共逻辑优先使用稳定生态或已安装包的公开 API,禁止导入 `packages/*/src`。
9
+ 业务响应在 API Adapter 中转成稳定类型,校验与业务规则不散落到模板。
10
+
11
+ ESLint 负责质量与依赖约束,Prettier 负责格式。不要同时开启其他格式化插件。
12
+ `src/shared` 不反向依赖业务模块;存在 admin/screen 双域时两者互不依赖。
13
+ 只有当前项目存在相应目录时才应用这些边界,不要求 Blank 建立业务架构。
14
+
15
+ ## 测试与检查
16
+
17
+ - 新增页面:覆盖关键交互、加载失败和权限边界。
18
+ - 修改接口:验证响应转换、错误、取消和会话切换,不只验证成功路径。
19
+ - 修改公共能力:增加契约回归测试,同时检查已有消费者。
20
+ - 布局、图表、Canvas、地图或点击坐标变化:必要时运行真实浏览器检查,DOM 模拟不能证明布局正确。
21
+
22
+ 统一命令:`pnpm lint`、`pnpm format:check`、`pnpm typecheck`、`pnpm test`、`pnpm build`。
23
+ 提交前运行 `pnpm check`。初次安装生成 lockfile,之后使用 `pnpm install --frozen-lockfile`。
24
+ `pnpm format` 应可重复运行且不产生新变化;依赖、产物与声明文件不参与格式化。
25
+
26
+ <!-- PROJECT_CONVENTIONS -->
@@ -0,0 +1,60 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import antfu from '@antfu/eslint-config';
4
+ import prettier from 'eslint-config-prettier/flat';
5
+ import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript';
6
+ import importX from 'eslint-plugin-import-x';
7
+
8
+ const root = fileURLToPath(new URL('.', import.meta.url));
9
+ const hybrid =
10
+ existsSync(new URL('./src/admin', import.meta.url)) && existsSync(new URL('./src/screen', import.meta.url));
11
+
12
+ export default antfu(
13
+ {
14
+ vue: true,
15
+ typescript: true,
16
+ // Prettier 独占格式化;不启用 Antfu 的格式插件。
17
+ stylistic: false,
18
+ formatters: false,
19
+ ignores: ['**/dist/**', '**/coverage/**', '**/*.d.ts', 'pnpm-lock.yaml', '**/*.md'],
20
+ rules: {
21
+ 'antfu/if-newline': 'off',
22
+ 'antfu/curly': 'off',
23
+ 'vue/block-order': ['error', { order: ['script', 'template', 'style'] }],
24
+ 'vue/component-name-in-template-casing': ['error', 'PascalCase', { registeredComponentsOnly: true }],
25
+ 'vue/component-api-style': ['error', ['script-setup']],
26
+ 'unused-imports/no-unused-imports': 'error',
27
+ 'unused-imports/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
28
+ 'node/prefer-global/process': 'off',
29
+ },
30
+ },
31
+ {
32
+ files: ['src/**/*.{ts,vue}'],
33
+ plugins: { 'import-x': importX },
34
+ settings: {
35
+ 'import-x/resolver-next': [
36
+ createTypeScriptImportResolver({ project: fileURLToPath(new URL('./tsconfig.json', import.meta.url)) }),
37
+ ],
38
+ },
39
+ rules: {
40
+ 'import-x/no-cycle': ['error', { ignoreExternal: true }],
41
+ 'import-x/no-restricted-paths': [
42
+ 'error',
43
+ {
44
+ basePath: root,
45
+ zones: [
46
+ { target: './src/shared', from: './src/modules', message: '共享层不得反向依赖业务模块。' },
47
+ ...(hybrid
48
+ ? [
49
+ { target: './src/screen', from: './src/admin' },
50
+ { target: './src/admin', from: './src/screen' },
51
+ { target: './src/shared', from: ['./src/admin', './src/screen'] },
52
+ ]
53
+ : []),
54
+ ],
55
+ },
56
+ ],
57
+ },
58
+ },
59
+ prettier,
60
+ );
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "template-base",
3
+ "type": "module",
4
+ "version": "0.0.0",
5
+ "private": true,
6
+ "packageManager": "pnpm@10.33.0",
7
+ "engines": {
8
+ "node": ">=22.12.0"
9
+ },
10
+ "scripts": {
11
+ "dev": "vite --host 127.0.0.1",
12
+ "lint": "eslint .",
13
+ "lint:fix": "eslint . --fix",
14
+ "format": "prettier --write .",
15
+ "format:check": "prettier --check .",
16
+ "typecheck": "vue-tsc --noEmit -p tsconfig.json",
17
+ "test": "vitest run",
18
+ "build": "vite build",
19
+ "preview": "vite preview --host 127.0.0.1",
20
+ "check": "pnpm lint && pnpm format:check && pnpm typecheck && pnpm test && pnpm build"
21
+ },
22
+ "dependencies": {
23
+ "vue": "^3.5.0"
24
+ },
25
+ "devDependencies": {
26
+ "@antfu/eslint-config": "^8.2.0",
27
+ "@types/node": "^24.6.1",
28
+ "@vitejs/plugin-vue": "^6.0.1",
29
+ "eslint": "^9.37.0",
30
+ "eslint-config-prettier": "^10.1.8",
31
+ "eslint-import-resolver-typescript": "^4.4.4",
32
+ "eslint-plugin-import-x": "^4.16.1",
33
+ "prettier": "^3.6.2",
34
+ "typescript": "^6.0.0",
35
+ "vite": "^8.0.0",
36
+ "vitest": "^4.1.5",
37
+ "vue-tsc": "^3.1.1"
38
+ }
39
+ }
@@ -0,0 +1,8 @@
1
+ export default {
2
+ semi: true,
3
+ singleQuote: true,
4
+ tabWidth: 2,
5
+ printWidth: 120,
6
+ trailingComma: 'all',
7
+ endOfLine: 'lf',
8
+ };
@@ -11,12 +11,9 @@
11
11
  "ignoreDeprecations": "6.0",
12
12
  "types": ["vite/client", "node"],
13
13
  "strict": true,
14
+ "noEmit": true,
14
15
  "isolatedModules": true,
15
16
  "skipLibCheck": true
16
17
  },
17
- "include": [
18
- "src/**/*.ts",
19
- "src/**/*.vue",
20
- "vite.config.ts"
21
- ]
18
+ "include": ["src/**/*.ts", "src/**/*.vue", "tests/**/*.ts", "vite.config.ts", "vitest.config.ts"]
22
19
  }
@@ -0,0 +1,3 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({ test: { environment: 'node' } });
@@ -0,0 +1,51 @@
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 = [
8
+ { field: 'name', label: '名称' },
9
+ { field: 'status', label: '状态' },
10
+ ];
11
+ const dataSource: CrudDataSource = {
12
+ fetch: async ({ page, pageSize, query }) => {
13
+ const filtered = rows.filter((row) => String(row.name).includes(String(query.keyword ?? '')));
14
+ return { items: filtered.slice((page - 1) * pageSize, page * pageSize), total: filtered.length };
15
+ },
16
+ create: async (model) => {
17
+ rows = [...rows, { ...model, id: crypto.randomUUID() }];
18
+ },
19
+ update: async (row, model) => {
20
+ rows = rows.map((item) => (item.id === row.id ? { ...model, id: row.id } : item));
21
+ },
22
+ remove: async (row) => {
23
+ rows = rows.filter((item) => item.id !== row.id);
24
+ },
25
+ };
26
+ </script>
27
+
28
+ <template>
29
+ <main class="lumal-admin-page">
30
+ <LumalCrudTable
31
+ title="CRUD 示例"
32
+ :data-source="dataSource"
33
+ :table="{ columns, rowKey: 'id', showColumnSettings: true, selection: true }"
34
+ :query="{ schemas: [{ field: 'keyword', label: '关键词', component: 'input' }] }"
35
+ :form-schemas="[
36
+ { field: 'name', label: '名称', component: 'input', required: true },
37
+ {
38
+ field: 'status',
39
+ label: '状态',
40
+ component: 'select',
41
+ options: [
42
+ { label: '启用', value: 'enabled' },
43
+ { label: '停用', value: 'disabled' },
44
+ ],
45
+ defaultValue: 'enabled',
46
+ },
47
+ ]"
48
+ :toolbar="{ export: true, refresh: true }"
49
+ />
50
+ </main>
51
+ </template>
@@ -1,19 +1,26 @@
1
1
  <script setup lang="ts">
2
- import type { SchemaFormItem, SchemaFormModel } from '@lumal/core/components'
3
- import { LumalSchemaForm } from '@lumal/core/components'
4
- import { ElMessage } from 'element-plus'
5
- import { shallowRef } from 'vue'
2
+ import type { SchemaFormItem, SchemaFormModel } from '@lumal/core/components';
3
+ import { LumalSchemaForm } from '@lumal/core/components';
4
+ import { ElMessage } from 'element-plus';
5
+ import { shallowRef } from 'vue';
6
6
 
7
7
  /***********************Schema 与联动*********************/
8
- const model = shallowRef<SchemaFormModel>({})
8
+ const model = shallowRef<SchemaFormModel>({});
9
9
  const schemas: SchemaFormItem[] = [
10
10
  { field: 'name', label: '名称', required: true, component: 'input' },
11
11
  { field: 'date', label: '日期', component: 'date', componentProps: { valueFormat: 'YYYY-MM-DD' } },
12
12
  { field: 'notify', label: '邮件通知', component: 'switch' },
13
- { field: 'email', label: '邮箱', component: 'input', hidden: ({ model }) => !model.notify, required: ({ model }) => Boolean(model.notify), rules: [{ type: 'email', message: '请输入有效邮箱' }] },
14
- ]
13
+ {
14
+ field: 'email',
15
+ label: '邮箱',
16
+ component: 'input',
17
+ hidden: ({ model }) => !model.notify,
18
+ required: ({ model }) => Boolean(model.notify),
19
+ rules: [{ type: 'email', message: '请输入有效邮箱' }],
20
+ },
21
+ ];
15
22
  function submit(): void {
16
- ElMessage.success('保存成功')
23
+ ElMessage.success('保存成功');
17
24
  }
18
25
  </script>
19
26
 
@@ -0,0 +1,27 @@
1
+ import type { AppModule } from '../../app/module';
2
+
3
+ export const menus: AppModule['menus'] = [
4
+ {
5
+ path: '/examples',
6
+ name: 'Examples',
7
+ meta: { title: '开发示例', icon: 'lumal:code', order: 80 },
8
+ children: [
9
+ {
10
+ path: '/examples/form',
11
+ name: 'SchemaFormExample',
12
+ component: 'examples/form',
13
+ meta: { title: 'Schema 表单', keepAlive: true },
14
+ },
15
+ {
16
+ path: '/examples/crud',
17
+ name: 'CrudExample',
18
+ component: 'examples/crud',
19
+ meta: { title: 'CRUD 示例', keepAlive: true },
20
+ },
21
+ ],
22
+ },
23
+ ];
24
+ export const views: AppModule['views'] = {
25
+ 'examples/form': () => import('./FormExampleView.vue'),
26
+ 'examples/crud': () => import('./CrudExampleView.vue'),
27
+ };
@@ -0,0 +1,104 @@
1
+ <script setup lang="ts">
2
+ import type { SchemaFormItem, SchemaFormModel } from '@lumal/core/components';
3
+ import { LumalPage, LumalSchemaForm } from '@lumal/core/components';
4
+ import { ElAlert, ElButton, ElColorPicker, ElMessage, ElMessageBox } from 'element-plus';
5
+ import { shallowRef, watch } from 'vue';
6
+ import { permissionStore, service } from '../../../app/services/session';
7
+ import { useAsyncData } from '../../../shared/composables/use-async-data';
8
+ import { configInput, createConfigService } from './api';
9
+
10
+ /***********************配置加载与独立表单*********************/
11
+ const api = createConfigService(service);
12
+ const state = useAsyncData(api.get, null);
13
+ const model = shallowRef<SchemaFormModel>({});
14
+ const saving = shallowRef(false);
15
+ const failure = shallowRef('');
16
+ const canSave = () => permissionStore.hasPermission('system:config:update');
17
+ watch(state.data, (value) => {
18
+ if (value) model.value = { ...value };
19
+ });
20
+ const schemas: SchemaFormItem[] = [
21
+ { field: 'appName', label: '应用名称', required: true, rules: [{ max: 64, message: '应用名称最多 64 个字符' }] },
22
+ { field: 'colorPrimary', label: '主题色', required: true },
23
+ {
24
+ field: 'layout',
25
+ label: '导航布局',
26
+ component: 'select',
27
+ required: true,
28
+ options: [
29
+ { label: '侧边导航', value: 'sidebar-nav' },
30
+ { label: '顶部导航', value: 'top-nav' },
31
+ { label: '混合导航', value: 'mixed-nav' },
32
+ ],
33
+ },
34
+ { field: 'tabbarEnable', label: '标签栏', component: 'switch' },
35
+ { field: 'transitionEnable', label: '页面动画', component: 'switch' },
36
+ ];
37
+
38
+ /***********************保存、重置与失败恢复*********************/
39
+ async function persist(values: SchemaFormModel, restore = false): Promise<void> {
40
+ if (saving.value || state.loading.value || !canSave()) return;
41
+ saving.value = true;
42
+ failure.value = '';
43
+ try {
44
+ const next = restore ? await api.restore() : await api.save(configInput(values));
45
+ model.value = { ...next };
46
+ ElMessage.success(restore ? '已恢复默认配置' : '配置已保存');
47
+ } catch (reason) {
48
+ failure.value = reason instanceof Error ? reason.message : '配置保存失败';
49
+ } finally {
50
+ saving.value = false;
51
+ }
52
+ }
53
+ async function restore(): Promise<void> {
54
+ try {
55
+ await ElMessageBox.confirm('恢复系统默认配置?', '恢复默认', { type: 'warning' });
56
+ } catch {
57
+ return;
58
+ }
59
+ await persist({}, true);
60
+ }
61
+ async function reload(): Promise<void> {
62
+ if (saving.value || state.loading.value) return;
63
+ failure.value = '';
64
+ await state.reload();
65
+ }
66
+ </script>
67
+
68
+ <template>
69
+ <LumalPage class="lumal-admin-page" title="系统配置">
70
+ <ElAlert v-if="state.error.value || failure" :title="state.error.value || failure" type="error" :closable="false">
71
+ <ElButton link :loading="state.loading.value" :disabled="saving" @click="reload"> 重新加载 </ElButton>
72
+ </ElAlert>
73
+ <div v-if="state.data.value" class="config-form">
74
+ <LumalSchemaForm
75
+ v-model="model"
76
+ :schemas="schemas"
77
+ :columns="2"
78
+ :mode="canSave() ? 'edit' : 'view'"
79
+ :show-actions="canSave()"
80
+ :disabled="saving || state.loading.value"
81
+ :submit-disabled="state.loading.value"
82
+ :submit-loading="saving"
83
+ submit-text="保存配置"
84
+ @submit="persist"
85
+ >
86
+ <template #field-colorPrimary="{ value }">
87
+ <ElColorPicker
88
+ :model-value="String(value || '')"
89
+ :disabled="!canSave() || saving || state.loading.value"
90
+ @update:model-value="model = { ...model, colorPrimary: $event }"
91
+ />
92
+ </template>
93
+ </LumalSchemaForm>
94
+ <ElButton :disabled="saving" :loading="state.loading.value" @click="reload"> 重读配置 </ElButton>
95
+ <ElButton v-if="canSave()" :disabled="saving" @click="restore"> 恢复默认配置 </ElButton>
96
+ </div>
97
+ </LumalPage>
98
+ </template>
99
+
100
+ <style scoped>
101
+ .config-form {
102
+ max-width: 900px;
103
+ }
104
+ </style>
@@ -0,0 +1,38 @@
1
+ import type { SchemaFormModel } from '@lumal/core/components';
2
+ import type { BusinessTransport } from '../../../app/services/contracts';
3
+ import { object, text } from '../../../shared/api';
4
+
5
+ export interface SystemConfig {
6
+ appName: string;
7
+ colorPrimary: string;
8
+ layout: 'sidebar-nav' | 'top-nav' | 'mixed-nav';
9
+ tabbarEnable: boolean;
10
+ transitionEnable: boolean;
11
+ }
12
+ export function configInput(model: SchemaFormModel): SystemConfig {
13
+ const appName = text(model.appName, '应用名称', true);
14
+ const colorPrimary = text(model.colorPrimary, '主题色', true);
15
+ if (
16
+ appName.length > 64 ||
17
+ !/^#[\da-f]{6}$/i.test(colorPrimary) ||
18
+ !['sidebar-nav', 'top-nav', 'mixed-nav'].includes(String(model.layout))
19
+ )
20
+ throw new Error('应用名称、主题色或导航布局无效');
21
+ if (typeof model.tabbarEnable !== 'boolean' || typeof model.transitionEnable !== 'boolean')
22
+ throw new Error('标签栏和动画设置必须是布尔值');
23
+ return {
24
+ appName,
25
+ colorPrimary,
26
+ layout: model.layout as SystemConfig['layout'],
27
+ tabbarEnable: model.tabbarEnable,
28
+ transitionEnable: model.transitionEnable,
29
+ };
30
+ }
31
+ export function createConfigService(transport: BusinessTransport) {
32
+ return {
33
+ get: async () => configInput(object(await transport.request('GET', '/system/config'))),
34
+ save: async (input: SystemConfig) =>
35
+ configInput(object(await transport.request('PUT', '/system/config', { body: input }))),
36
+ restore: async () => configInput(object(await transport.request('POST', '/system/config/restore'))),
37
+ };
38
+ }
@@ -0,0 +1,21 @@
1
+ import type { AppModule } from '../../../app/module';
2
+
3
+ export const menus: AppModule['menus'] = [
4
+ {
5
+ path: '/system/config',
6
+ name: 'SystemConfig',
7
+ component: 'system/config',
8
+ meta: { title: '系统配置', authority: ['system:config:view'], keepAlive: true, order: 7 },
9
+ },
10
+ ];
11
+ export const views: AppModule['views'] = { 'system/config': () => import('./ConfigView.vue') };
12
+ export const permissions: AppModule['permissions'] = [
13
+ {
14
+ id: 'system-config',
15
+ label: '系统配置',
16
+ children: [
17
+ { id: 'system:config:view', label: '查看配置', permission: 'system:config:view' },
18
+ { id: 'system:config:update', label: '保存配置', permission: 'system:config:update' },
19
+ ],
20
+ },
21
+ ];