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,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
+ };
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "jsx": "preserve",
5
+ "useDefineForClassFields": true,
6
+ "baseUrl": ".",
7
+ "module": "ESNext",
8
+ "moduleResolution": "Bundler",
9
+ "paths": { "@/*": ["src/*"] },
10
+ "resolveJsonModule": true,
11
+ "ignoreDeprecations": "6.0",
12
+ "types": ["vite/client", "node"],
13
+ "strict": true,
14
+ "noEmit": true,
15
+ "isolatedModules": true,
16
+ "skipLibCheck": true
17
+ },
18
+ "include": ["src/**/*.ts", "src/**/*.vue", "tests/**/*.ts", "vite.config.ts", "vitest.config.ts"]
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>
@@ -0,0 +1,32 @@
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';
6
+
7
+ /***********************Schema 与联动*********************/
8
+ const model = shallowRef<SchemaFormModel>({});
9
+ const schemas: SchemaFormItem[] = [
10
+ { field: 'name', label: '名称', required: true, component: 'input' },
11
+ { field: 'date', label: '日期', component: 'date', componentProps: { valueFormat: 'YYYY-MM-DD' } },
12
+ { field: 'notify', label: '邮件通知', component: 'switch' },
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
+ ];
22
+ function submit(): void {
23
+ ElMessage.success('保存成功');
24
+ }
25
+ </script>
26
+
27
+ <template>
28
+ <main class="lumal-admin-page">
29
+ <h1>Schema 表单</h1>
30
+ <LumalSchemaForm v-model="model" :schemas="schemas" :columns="2" show-actions show-reset @submit="submit" />
31
+ </main>
32
+ </template>
@@ -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
+ ];
@@ -0,0 +1,141 @@
1
+ <script setup lang="ts">
2
+ import type { SchemaFormItem, SchemaTableColumn } from '@lumal/core/components';
3
+ import type { DictionaryItem } from './types';
4
+ import { LumalPage, LumalSchemaTable } from '@lumal/core/components';
5
+ import { LumalIcon } from '@lumal/icons-vue';
6
+ import { ElAlert, ElButton, ElColorPicker, ElOption, ElSelect } from 'element-plus';
7
+ import { computed, shallowRef, watch } from 'vue';
8
+ import { useRoute } from 'vue-router';
9
+ import { permissionStore, service } from '../../../app/services/session';
10
+ import BaseEntityDialog from '../../../shared/components/BaseEntityDialog.vue';
11
+ import { useAsyncData } from '../../../shared/composables/use-async-data';
12
+ import { useEntityEditor } from '../../../shared/composables/use-entity-editor';
13
+ import { statusOptions } from '../../../shared/status';
14
+ import { createDictionaryService, dictionaryItemInput, parseItem } from './api';
15
+ import { dictionaryStore } from './cache';
16
+
17
+ /***********************分类筛选与实时字典缓存*********************/
18
+ const route = useRoute();
19
+ const typeCode = shallowRef(typeof route.query.typeCode === 'string' ? route.query.typeCode : '');
20
+ const api = createDictionaryService(service, () => dictionaryStore.clearDictionary());
21
+ const types = useAsyncData(api.types, []);
22
+ const state = useAsyncData(() => api.items(typeCode.value), [] as DictionaryItem[]);
23
+ const can = (action: string) => permissionStore.hasPermission(`system:dict:${action}`);
24
+ watch(
25
+ () => route.query.typeCode,
26
+ (value) => {
27
+ typeCode.value = typeof value === 'string' ? value : '';
28
+ },
29
+ );
30
+ watch(typeCode, () => {
31
+ void state.reload();
32
+ });
33
+ const editor = useEntityEditor<DictionaryItem, ReturnType<typeof dictionaryItemInput>>({
34
+ create: api.createItem,
35
+ update: api.updateItem,
36
+ remove: api.removeItem,
37
+ toInput: dictionaryItemInput,
38
+ reload: state.load,
39
+ });
40
+ const columns: SchemaTableColumn<DictionaryItem>[] = [
41
+ { field: 'label', label: '显示名称', minWidth: 160 },
42
+ { field: 'value', label: '字典值', minWidth: 150 },
43
+ { field: 'typeCode', label: '分类', minWidth: 150 },
44
+ { field: 'color', label: '颜色', width: 120 },
45
+ { field: 'order', label: '排序', width: 80 },
46
+ { field: 'status', label: '状态', options: statusOptions, width: 100 },
47
+ ];
48
+ const schemas = computed<SchemaFormItem[]>(() => [
49
+ {
50
+ field: 'typeCode',
51
+ label: '分类',
52
+ component: 'select',
53
+ required: true,
54
+ options: types.data.value.map((row) => ({ label: row.name, value: row.code })),
55
+ },
56
+ { field: 'label', label: '显示名称', required: true },
57
+ { field: 'value', label: '字典值', required: true },
58
+ { field: 'color', label: '颜色' },
59
+ { field: 'order', label: '排序', component: 'number', defaultValue: 0 },
60
+ { field: 'status', label: '状态', component: 'select', options: statusOptions, defaultValue: 'enabled' },
61
+ ]);
62
+ </script>
63
+
64
+ <template>
65
+ <LumalPage class="lumal-admin-page" title="字典项管理" no-padding>
66
+ <ElAlert
67
+ v-if="editor.error.value || types.error.value"
68
+ :title="editor.error.value || types.error.value"
69
+ type="error"
70
+ :closable="false"
71
+ >
72
+ <ElButton v-if="types.error.value" link @click="types.reload"> 重试分类加载 </ElButton>
73
+ </ElAlert>
74
+ <LumalSchemaTable
75
+ :rows="state.data.value"
76
+ :columns="columns"
77
+ :loading="state.loading.value"
78
+ :error="state.error.value"
79
+ :action-width="160"
80
+ @retry="state.reload"
81
+ >
82
+ <template #toolbar>
83
+ <ElSelect v-model="typeCode" clearable aria-label="字典分类" placeholder="全部分类" style="width: 240px">
84
+ <ElOption v-for="type in types.data.value" :key="type.id" :label="type.name" :value="type.code" />
85
+ </ElSelect>
86
+ <ElButton
87
+ v-if="can('create')"
88
+ type="primary"
89
+ :disabled="!!types.error.value || !types.data.value.length"
90
+ @click="
91
+ editor.openCreate({
92
+ typeCode: typeCode || types.data.value[0]?.code,
93
+ status: 'enabled',
94
+ order: 0,
95
+ color: '',
96
+ })
97
+ "
98
+ >
99
+ 新增字典项
100
+ </ElButton>
101
+ </template>
102
+ <template #toolbar-tools>
103
+ <ElButton title="重新加载" aria-label="重新加载" @click="state.reload">
104
+ <LumalIcon name="lumal:refresh" />
105
+ </ElButton>
106
+ </template>
107
+ <template #table-color="{ value }">
108
+ <span :style="{ color: String(value || 'inherit') }">{{ value || '-' }}</span>
109
+ </template>
110
+ <template #actions="{ row }">
111
+ <ElButton v-if="can('update')" link @click="editor.openEdit(parseItem(row))"> 编辑 </ElButton>
112
+ <ElButton
113
+ v-if="can('delete')"
114
+ link
115
+ type="danger"
116
+ :disabled="editor.saving.value"
117
+ @click="editor.remove(parseItem(row))"
118
+ >
119
+ 删除
120
+ </ElButton>
121
+ </template>
122
+ </LumalSchemaTable>
123
+ <BaseEntityDialog
124
+ v-model:visible="editor.visible.value"
125
+ v-model:model="editor.model.value"
126
+ :title="editor.editing.value ? '编辑字典项' : '新增字典项'"
127
+ :schemas="schemas"
128
+ :editing="!!editor.editing.value"
129
+ :saving="editor.saving.value"
130
+ :error="editor.error.value"
131
+ @submit="editor.submit"
132
+ >
133
+ <template #field-color="{ value }">
134
+ <ElColorPicker
135
+ :model-value="String(value || '')"
136
+ @update:model-value="editor.model.value = { ...editor.model.value, color: $event || '' }"
137
+ />
138
+ </template>
139
+ </BaseEntityDialog>
140
+ </LumalPage>
141
+ </template>
@@ -0,0 +1,107 @@
1
+ <script setup lang="ts">
2
+ import type { SchemaFormItem, SchemaTableColumn } from '@lumal/core/components';
3
+ import type { DictionaryType } from './types';
4
+ import { LumalPage, LumalSchemaTable } from '@lumal/core/components';
5
+ import { LumalIcon } from '@lumal/icons-vue';
6
+ import { ElAlert, ElButton, ElInput } from 'element-plus';
7
+ import { computed, shallowRef } from 'vue';
8
+ import { useRouter } from 'vue-router';
9
+ import { permissionStore, service } from '../../../app/services/session';
10
+ import BaseEntityDialog from '../../../shared/components/BaseEntityDialog.vue';
11
+ import { useAsyncData } from '../../../shared/composables/use-async-data';
12
+ import { useEntityEditor } from '../../../shared/composables/use-entity-editor';
13
+ import { statusOptions } from '../../../shared/status';
14
+ import { createDictionaryService, dictionaryTypeInput, parseType } from './api';
15
+ import { dictionaryStore } from './cache';
16
+
17
+ /***********************字典分类与关联项入口*********************/
18
+ const api = createDictionaryService(service, () => dictionaryStore.clearDictionary());
19
+ const state = useAsyncData(api.types, [] as DictionaryType[]);
20
+ const keyword = shallowRef('');
21
+ const router = useRouter();
22
+ const can = (action: string) => permissionStore.hasPermission(`system:dict:${action}`);
23
+ const displayed = computed(() => state.data.value.filter((row) => `${row.name} ${row.code}`.includes(keyword.value)));
24
+ const editor = useEntityEditor<DictionaryType, ReturnType<typeof dictionaryTypeInput>>({
25
+ create: api.createType,
26
+ update: api.updateType,
27
+ remove: api.removeType,
28
+ toInput: dictionaryTypeInput,
29
+ reload: state.load,
30
+ removeMessage: (row) => `删除分类「${row.name}」及其所有字典项?`,
31
+ });
32
+ const columns: SchemaTableColumn<DictionaryType>[] = [
33
+ { field: 'name', label: '字典名称', minWidth: 160 },
34
+ { field: 'code', label: '编码', minWidth: 150 },
35
+ { field: 'status', label: '状态', options: statusOptions, width: 100 },
36
+ { field: 'description', label: '说明', minWidth: 200 },
37
+ ];
38
+ const schemas: SchemaFormItem[] = [
39
+ { field: 'name', label: '字典名称', required: true },
40
+ {
41
+ field: 'code',
42
+ label: '编码',
43
+ required: true,
44
+ editDisabled: true,
45
+ rules: [{ pattern: /^[a-z][\w-]*$/, message: '使用字母开头的编码' }],
46
+ },
47
+ { field: 'status', label: '状态', component: 'select', options: statusOptions, defaultValue: 'enabled' },
48
+ { field: 'description', label: '说明', component: 'textarea', span: 24 },
49
+ ];
50
+ </script>
51
+
52
+ <template>
53
+ <LumalPage class="lumal-admin-page" title="字典分类" no-padding>
54
+ <ElAlert v-if="editor.error.value" :title="editor.error.value" type="error" :closable="false" />
55
+ <LumalSchemaTable
56
+ :rows="displayed"
57
+ :columns="columns"
58
+ :loading="state.loading.value"
59
+ :error="state.error.value"
60
+ :action-width="240"
61
+ @retry="state.reload"
62
+ >
63
+ <template #toolbar>
64
+ <ElInput
65
+ v-model="keyword"
66
+ aria-label="搜索字典分类"
67
+ placeholder="搜索名称或编码"
68
+ clearable
69
+ style="max-width: 260px"
70
+ />
71
+ <ElButton v-if="can('create')" type="primary" @click="editor.openCreate({ status: 'enabled' })">
72
+ 新增分类
73
+ </ElButton>
74
+ </template>
75
+ <template #toolbar-tools>
76
+ <ElButton title="重新加载" aria-label="重新加载" @click="state.reload">
77
+ <LumalIcon name="lumal:refresh" />
78
+ </ElButton>
79
+ </template>
80
+ <template #actions="{ row }">
81
+ <ElButton link @click="router.push({ path: '/system/dictionaries/items', query: { typeCode: row.code } })">
82
+ 字典项
83
+ </ElButton>
84
+ <ElButton v-if="can('update')" link @click="editor.openEdit(parseType(row))"> 编辑 </ElButton>
85
+ <ElButton
86
+ v-if="can('delete')"
87
+ link
88
+ type="danger"
89
+ :disabled="editor.saving.value"
90
+ @click="editor.remove(parseType(row))"
91
+ >
92
+ 删除
93
+ </ElButton>
94
+ </template>
95
+ </LumalSchemaTable>
96
+ <BaseEntityDialog
97
+ v-model:visible="editor.visible.value"
98
+ v-model:model="editor.model.value"
99
+ :title="editor.editing.value ? '编辑分类' : '新增分类'"
100
+ :schemas="schemas"
101
+ :editing="!!editor.editing.value"
102
+ :saving="editor.saving.value"
103
+ :error="editor.error.value"
104
+ @submit="editor.submit"
105
+ />
106
+ </LumalPage>
107
+ </template>