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.
- package/CHANGELOG.md +13 -0
- package/README.md +39 -10
- package/dist/cli.d.ts +9 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +236 -19
- package/dist/cli.js.map +1 -1
- package/dist/create.d.ts +10 -1
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +18 -5
- package/dist/create.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/presets.d.ts +29 -3
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +38 -10
- package/dist/presets.js.map +1 -1
- package/dist/project-cli.d.ts +3 -0
- package/dist/project-cli.d.ts.map +1 -0
- package/dist/project-cli.js +13 -0
- package/dist/project-cli.js.map +1 -0
- package/dist/projects.d.ts +21 -0
- package/dist/projects.d.ts.map +1 -0
- package/dist/projects.js +124 -0
- package/dist/projects.js.map +1 -0
- package/dist/templates.d.ts +2 -0
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +42 -13
- package/dist/templates.js.map +1 -1
- package/dist/types.d.ts +39 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -3
- package/templates/base/API-CONTRACT.md +67 -20
- package/templates/base/README.md +20 -3
- package/templates/base/src/App.vue +71 -79
- package/templates/base/src/app/components/AppHeaderActions.vue +46 -0
- package/templates/base/src/{components/app → app/components}/AppSettingsDrawer.vue +15 -15
- package/templates/base/src/app/menu-policy.ts +163 -0
- package/templates/base/src/app/module.ts +46 -0
- package/templates/base/src/app/router/index.ts +107 -0
- package/templates/base/src/app/router/views.ts +12 -0
- package/templates/base/src/{services → app/services}/access.ts +1 -1
- package/templates/base/src/app/services/contracts.ts +54 -0
- package/templates/base/src/app/services/local-contracts.ts +18 -0
- package/templates/base/src/{services → app/services}/preferences.ts +8 -8
- package/templates/base/src/app/services/session-runtime.ts +186 -0
- package/templates/base/src/{services → app/services}/session.ts +4 -4
- package/templates/base/src/app/stored-menus.ts +72 -0
- package/templates/base/src/app/views/SafeIframeView.vue +34 -0
- package/templates/base/src/{views → app/views}/auth/LoginView.vue +29 -34
- package/templates/base/src/app/views/error/ForbiddenView.vue +20 -0
- package/templates/base/src/{views → app/views}/error/NotFoundView.vue +5 -7
- package/templates/base/src/main.ts +21 -22
- package/templates/base/src/modules/business/README.md +33 -0
- package/templates/base/src/{views → modules}/dashboard/DashboardView.vue +3 -3
- package/templates/base/src/modules/dashboard/module.ts +14 -0
- package/templates/base/src/shared/api.ts +53 -0
- package/templates/base/src/shared/components/BaseEntityDialog.vue +39 -0
- package/templates/base/src/shared/composables/use-async-data.ts +35 -0
- package/templates/base/src/shared/composables/use-entity-editor.ts +77 -0
- package/templates/base/src/shared/status.ts +4 -0
- package/templates/base/src/shared/tree.ts +41 -0
- package/templates/base/src/styles.scss +1 -6
- package/templates/base/tests/menu-policy.test.ts.template +89 -0
- package/templates/base/tests/session.test.ts.template +38 -27
- package/templates/base/vite.config.ts +4 -4
- package/templates/blank/README.md +6 -0
- package/templates/blank/src/App.vue +11 -0
- package/templates/common/AGENTS.md +13 -0
- package/templates/common/_editorconfig +15 -0
- package/templates/common/_gitattributes +6 -0
- package/templates/{base → common}/_gitignore +1 -0
- package/templates/common/_prettierignore +6 -0
- package/templates/common/docs/DEVELOPMENT.md +26 -0
- package/templates/common/eslint.config.mjs +60 -0
- package/templates/common/package.json +39 -0
- package/templates/common/prettier.config.mjs +8 -0
- package/templates/{base → common}/tsconfig.json +2 -5
- package/templates/common/vitest.config.ts +3 -0
- package/templates/features/examples/src/modules/examples/CrudExampleView.vue +51 -0
- package/templates/features/examples/src/{views → modules}/examples/FormExampleView.vue +15 -8
- package/templates/features/examples/src/modules/examples/module.ts +27 -0
- package/templates/features/system/src/modules/system/config/ConfigView.vue +104 -0
- package/templates/features/system/src/modules/system/config/api.ts +38 -0
- package/templates/features/system/src/modules/system/config/routes.ts +21 -0
- package/templates/features/system/src/modules/system/dictionary/DictionaryItemView.vue +141 -0
- package/templates/features/system/src/modules/system/dictionary/DictionaryTypeView.vue +107 -0
- package/templates/features/system/src/modules/system/dictionary/api.ts +61 -0
- package/templates/features/system/src/modules/system/dictionary/cache.ts +9 -0
- package/templates/features/system/src/modules/system/dictionary/routes.ts +32 -0
- package/templates/features/system/src/modules/system/dictionary/types.ts +22 -0
- package/templates/features/system/src/modules/system/local.ts +348 -0
- package/templates/features/system/src/modules/system/menu/MenuView.vue +133 -0
- package/templates/features/system/src/modules/system/menu/api.ts +73 -0
- package/templates/features/system/src/modules/system/menu/components/MenuEditorDialog.vue +106 -0
- package/templates/features/system/src/modules/system/menu/routes.ts +23 -0
- package/templates/features/system/src/modules/system/menu/types.ts +18 -0
- package/templates/features/system/src/modules/system/module.ts +22 -0
- package/templates/features/system/src/modules/system/organization/OrganizationView.vue +134 -0
- package/templates/features/system/src/modules/system/organization/api.ts +38 -0
- package/templates/features/system/src/modules/system/organization/routes.ts +23 -0
- package/templates/features/system/src/modules/system/organization/types.ts +17 -0
- package/templates/features/system/src/modules/system/role/RoleView.vue +90 -0
- package/templates/features/system/src/modules/system/role/api.ts +40 -0
- package/templates/features/system/src/modules/system/role/components/RoleAuthorizationDialog.vue +110 -0
- package/templates/features/system/src/modules/system/role/permission-tree.ts +31 -0
- package/templates/features/system/src/modules/system/role/routes.ts +24 -0
- package/templates/features/system/src/modules/system/role/types.ts +12 -0
- package/templates/features/system/src/modules/system/user/UserView.vue +297 -0
- package/templates/features/system/src/modules/system/user/api.ts +45 -0
- package/templates/features/system/src/modules/system/user/components/UserOrganizationFilter.vue +61 -0
- package/templates/features/system/src/modules/system/user/components/UserRoleDialog.vue +93 -0
- package/templates/features/system/src/modules/system/user/routes.ts +27 -0
- package/templates/features/system/src/modules/system/user/types.ts +28 -0
- package/templates/features/system/tests/system.test.ts.template +266 -0
- package/templates/modes/http/src/app/services/data-source.ts +124 -0
- package/templates/modes/http/src/app/services/http-adapter.ts +128 -0
- package/templates/modes/http/tests/http.test.ts.template +186 -133
- package/templates/modes/local/src/app/services/data-source.ts +113 -0
- package/templates/modes/local/src/app/services/local-database.ts +67 -0
- package/templates/modes/local/tests/local.test.ts.template +117 -77
- package/templates/presets/standard/src/modules/dashboard/DashboardView.vue +52 -0
- package/templates/presets/standard/src/modules/dashboard/components/DashboardSessionStatus.vue +41 -0
- package/templates/presets/standard/src/modules/dashboard/components/DashboardShortcuts.vue +51 -0
- package/templates/presets/standard/src/modules/profile/ProfileView.vue +109 -0
- package/templates/presets/standard/src/modules/profile/api.ts +49 -0
- package/templates/presets/standard/src/modules/profile/components/ProfilePasswordForm.vue +68 -0
- package/templates/presets/standard/src/modules/profile/local.ts +40 -0
- package/templates/presets/standard/src/modules/profile/module.ts +11 -0
- package/templates/presets/standard/tests/profile.test.ts.template +72 -0
- package/templates/screen/README.md +6 -0
- package/templates/screen/src/App.vue +7 -0
- package/templates/screen/src/screen/ScreenEntry.vue +18 -0
- package/templates/screen/src/screen/ScreenView.vue +48 -0
- package/templates/screen/src/screen/components/ScreenMetrics.vue +40 -0
- package/templates/screen-auth/src/screen/ScreenEntry.vue +52 -0
- package/templates/screen-auth/src/screen/components/ScreenLoginForm.vue +44 -0
- package/templates/screen-auth/src/services/session.ts +61 -0
- package/templates/screen-auth/tests/session.test.ts.template +54 -0
- package/templates/screen-auth-http/src/services/auth-adapter.ts +14 -0
- package/templates/screen-auth-http/tests/auth-adapter.test.ts.template +17 -0
- package/templates/screen-auth-local-demo/src/services/auth-adapter.ts +15 -0
- package/templates/screen-auth-local-demo/tests/auth-adapter.test.ts.template +18 -0
- package/templates/screen-data/src/screen/ScreenView.vue +69 -0
- package/templates/screen-data/src/services/metrics-types.ts +4 -0
- package/templates/screen-data-http/src/services/metrics.ts +20 -0
- package/templates/screen-data-http/src/services/request.ts +3 -0
- package/templates/screen-data-http/tests/metrics.test.ts.template +21 -0
- package/templates/screen-data-local/src/services/metrics.ts +11 -0
- package/templates/screen-data-local/tests/metrics.test.ts.template +9 -0
- package/templates/screen-http/API-CONTRACT.md +20 -0
- package/templates/screen-http/src/services/http.ts +17 -0
- package/templates/screen-request-auth/src/services/request.ts +15 -0
- package/templates/screen-router/src/App.vue +7 -0
- package/templates/screen-router/src/main.ts +13 -0
- package/templates/screen-router/src/router.ts +7 -0
- package/templates/screen-router/tests/app.test.ts.template +20 -0
- package/templates/screen-viewport/src/screen/ScreenViewport.vue +30 -0
- package/templates/screen-visualization-charts/src/screen/components/ScreenVisualization.vue +22 -0
- package/templates/screen-visualization-cockpit/src/screen/components/ScreenVisualization.vue +33 -0
- package/templates/screen-visualization-cockpit/src/screen/components/ScreenWidget.vue +10 -0
- package/templates/screen-visualization-datav/src/screen/components/ScreenVisualization.vue +11 -0
- package/templates/vben-compatible/MIGRATION.md +2 -0
- package/templates/vben-compatible/src/{views/migration → modules/vben-migration}/VbenFormView.vue +2 -1
- package/templates/vben-compatible/src/{views/migration → modules/vben-migration}/VbenUsersView.vue +3 -2
- package/templates/vben-compatible/src/modules/vben-migration/components/VbenFormExample.vue +64 -0
- package/templates/vben-compatible/src/{components/migration → modules/vben-migration/components}/VbenUsers.vue +73 -64
- package/templates/vben-compatible/src/modules/vben-migration/local.ts +68 -0
- package/templates/vben-compatible/src/modules/vben-migration/module.ts +42 -0
- package/templates/vue/index.html +12 -0
- package/templates/vue/src/main.ts +5 -0
- package/templates/vue/src/styles.css +19 -0
- package/templates/vue/tests/app.test.ts.template +10 -0
- package/templates/vue/vite.config.ts +9 -0
- package/templates/vue/vitest.config.ts +4 -0
- package/templates/base/eslint.config.mjs +0 -8
- package/templates/base/src/components/app/AppHeaderActions.vue +0 -34
- package/templates/base/src/router/index.ts +0 -86
- package/templates/base/src/router/views.ts +0 -16
- package/templates/base/src/services/contracts.ts +0 -29
- package/templates/base/src/services/session-runtime.ts +0 -137
- package/templates/base/src/views/error/ForbiddenView.vue +0 -25
- package/templates/base/vitest.config.ts +0 -3
- package/templates/features/examples/src/modules/examples.ts +0 -10
- package/templates/features/examples/src/views/examples/CrudExampleView.vue +0 -30
- package/templates/features/system/src/business/system/entities.ts +0 -93
- package/templates/features/system/src/modules/system.ts +0 -28
- package/templates/features/system/src/views/system/SystemEntityView.vue +0 -85
- package/templates/modes/http/src/services/data-source.ts +0 -120
- package/templates/modes/http/src/services/http-adapter.ts +0 -124
- package/templates/modes/local/src/services/data-source.ts +0 -176
- package/templates/vben-compatible/src/components/migration/VbenFormExample.vue +0 -34
- package/templates/vben-compatible/src/modules/vben.ts +0 -17
|
@@ -1,92 +1,132 @@
|
|
|
1
|
+
import type { LumalMenuRecord } from '@lumal/core/router';
|
|
1
2
|
// @vitest-environment node
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
-
import { localMenus } from '../src/router/views'
|
|
4
|
-
import { createBackend } from '../src/services/data-source'
|
|
5
|
-
import { createApplicationSession } from '../src/services/session-runtime'
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { localMenus } from '../src/app/router/views';
|
|
5
|
+
import { createBackend } from '../src/app/services/data-source';
|
|
6
|
+
import { createApplicationSession } from '../src/app/services/session-runtime';
|
|
6
7
|
|
|
7
8
|
function storage(): Storage {
|
|
8
|
-
const values = new Map<string, string>()
|
|
9
|
+
const values = new Map<string, string>();
|
|
9
10
|
return {
|
|
10
|
-
getItem: key => values.get(key) ?? null,
|
|
11
|
-
setItem: (key, value) => {
|
|
12
|
-
|
|
11
|
+
getItem: (key) => values.get(key) ?? null,
|
|
12
|
+
setItem: (key, value) => {
|
|
13
|
+
values.set(key, value);
|
|
14
|
+
},
|
|
15
|
+
removeItem: (key) => {
|
|
16
|
+
values.delete(key);
|
|
17
|
+
},
|
|
13
18
|
clear: () => values.clear(),
|
|
14
|
-
key: index => [...values.keys()][index] ?? null,
|
|
15
|
-
get length() {
|
|
16
|
-
|
|
19
|
+
key: (index) => [...values.keys()][index] ?? null,
|
|
20
|
+
get length() {
|
|
21
|
+
return values.size;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
17
24
|
}
|
|
18
25
|
beforeEach(() => {
|
|
19
|
-
vi.unstubAllEnvs()
|
|
20
|
-
vi.stubGlobal('localStorage', storage())
|
|
21
|
-
vi.stubGlobal('sessionStorage', storage())
|
|
22
|
-
})
|
|
26
|
+
vi.unstubAllEnvs();
|
|
27
|
+
vi.stubGlobal('localStorage', storage());
|
|
28
|
+
vi.stubGlobal('sessionStorage', storage());
|
|
29
|
+
});
|
|
23
30
|
afterEach(() => {
|
|
24
|
-
vi.unstubAllGlobals()
|
|
25
|
-
vi.unstubAllEnvs()
|
|
26
|
-
})
|
|
31
|
+
vi.unstubAllGlobals();
|
|
32
|
+
vi.unstubAllEnvs();
|
|
33
|
+
});
|
|
27
34
|
function application() {
|
|
28
|
-
return createApplicationSession(createBackend, localStorage, sessionStorage)
|
|
35
|
+
return createApplicationSession(createBackend, localStorage, sessionStorage);
|
|
29
36
|
}
|
|
30
37
|
describe('本地应用服务', () => {
|
|
31
38
|
it('认证、动态菜单和重新初始化使用同一服务契约', async () => {
|
|
32
|
-
const app = application()
|
|
33
|
-
await expect(app.login({ username: 'admin', password: 'wrong' })).rejects.toThrow('账号或密码')
|
|
34
|
-
await app.login({ username: 'admin', password: 'lumal123' })
|
|
35
|
-
expect(app.currentUser.value?.username).toBe('admin')
|
|
36
|
-
expect(app.menus.value.some(menu => menu.path === '/dashboard')).toBe(true)
|
|
37
|
-
const token = app.session.getToken()
|
|
38
|
-
const restored = application()
|
|
39
|
-
await restored.initialize()
|
|
40
|
-
expect(restored.session.getToken()).toBe(token)
|
|
41
|
-
expect(restored.ready.value).toBe(true)
|
|
42
|
-
await restored.logout()
|
|
43
|
-
expect(sessionStorage.getItem(`demo:${token}`)).toBeNull()
|
|
44
|
-
expect(restored.permissionStore.roles).toEqual([])
|
|
45
|
-
})
|
|
39
|
+
const app = application();
|
|
40
|
+
await expect(app.login({ username: 'admin', password: 'wrong' })).rejects.toThrow('账号或密码');
|
|
41
|
+
await app.login({ username: 'admin', password: 'lumal123' });
|
|
42
|
+
expect(app.currentUser.value?.username).toBe('admin');
|
|
43
|
+
expect(app.menus.value.some((menu) => menu.path === '/dashboard')).toBe(true);
|
|
44
|
+
const token = app.session.getToken();
|
|
45
|
+
const restored = application();
|
|
46
|
+
await restored.initialize();
|
|
47
|
+
expect(restored.session.getToken()).toBe(token);
|
|
48
|
+
expect(restored.ready.value).toBe(true);
|
|
49
|
+
await restored.logout();
|
|
50
|
+
expect(sessionStorage.getItem(`demo:${token}`)).toBeNull();
|
|
51
|
+
expect(restored.permissionStore.roles).toEqual([]);
|
|
52
|
+
});
|
|
46
53
|
it('生产默认拒绝演示账号,显式启用仅用于演示站点', async () => {
|
|
47
|
-
vi.stubEnv('PROD', true)
|
|
48
|
-
vi.stubEnv('VITE_ENABLE_LOCAL_DEMO', '')
|
|
49
|
-
await expect(application().login({ username: 'admin', password: 'lumal123' })).rejects.toThrow('生产环境')
|
|
50
|
-
vi.stubEnv('VITE_ENABLE_LOCAL_DEMO', 'true')
|
|
51
|
-
await expect(application().login({ username: 'admin', password: 'lumal123' })).resolves.toBeUndefined()
|
|
52
|
-
})
|
|
54
|
+
vi.stubEnv('PROD', true);
|
|
55
|
+
vi.stubEnv('VITE_ENABLE_LOCAL_DEMO', '');
|
|
56
|
+
await expect(application().login({ username: 'admin', password: 'lumal123' })).rejects.toThrow('生产环境');
|
|
57
|
+
vi.stubEnv('VITE_ENABLE_LOCAL_DEMO', 'true');
|
|
58
|
+
await expect(application().login({ username: 'admin', password: 'lumal123' })).resolves.toBeUndefined();
|
|
59
|
+
});
|
|
53
60
|
it('管理能力按预设启用,访客无写权限', async () => {
|
|
54
|
-
const app = application()
|
|
55
|
-
await app.login({ username: 'admin', password: 'lumal123' })
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
await app.service.
|
|
61
|
-
|
|
62
|
-
await
|
|
63
|
-
await expect(app.service.
|
|
64
|
-
await
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
await
|
|
71
|
-
await
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
await
|
|
75
|
-
|
|
61
|
+
const app = application();
|
|
62
|
+
await app.login({ username: 'admin', password: 'lumal123' });
|
|
63
|
+
const flatten = (menus: LumalMenuRecord[]): LumalMenuRecord[] =>
|
|
64
|
+
menus.flatMap((menu) => [menu, ...flatten(menu.children ?? [])]);
|
|
65
|
+
if (flatten(localMenus).some((menu) => menu.component === 'system/users')) {
|
|
66
|
+
await app.service.save('users', { username: 'new-user', nickname: '新增', roles: ['guest'], status: 'enabled' });
|
|
67
|
+
const result = await app.service.list('users', { page: 1, pageSize: 10, query: { keyword: 'new-user' } });
|
|
68
|
+
expect(result.total).toBe(1);
|
|
69
|
+
await app.service.remove('users', String(result.items[0]?.id));
|
|
70
|
+
await expect(app.service.remove('users', 'admin')).rejects.toThrow('内置管理员');
|
|
71
|
+
await expect(
|
|
72
|
+
app.service.save('users', { username: 'admin', nickname: '重复', roles: ['guest'] }),
|
|
73
|
+
).rejects.toThrow('重复');
|
|
74
|
+
await expect(
|
|
75
|
+
app.service.save('roles', { code: 'admin', name: '管理员', status: 'disabled' }, 'admin'),
|
|
76
|
+
).rejects.toThrow('内置角色');
|
|
77
|
+
await app.service.save('roles', { code: 'reader', name: '只读角色', status: 'enabled' });
|
|
78
|
+
await app.service.request('PUT', '/system/roles/reader/permissions', {
|
|
79
|
+
body: { permissions: ['system:user:list'] },
|
|
80
|
+
});
|
|
81
|
+
await app.service.save('users', {
|
|
82
|
+
username: 'reader',
|
|
83
|
+
nickname: '只读用户',
|
|
84
|
+
roles: ['reader'],
|
|
85
|
+
status: 'enabled',
|
|
86
|
+
});
|
|
87
|
+
await app.service.save('dictionaries/items', { typeCode: 'status', label: '待审核', value: 'pending' });
|
|
88
|
+
expect(await app.service.request('GET', '/system/dictionaries/items', { query: { typeCode: 'other' } })).toEqual(
|
|
89
|
+
[],
|
|
90
|
+
);
|
|
91
|
+
await app.service.save('menus', { title: '目录', path: '/group', name: 'Group', type: 'directory' });
|
|
92
|
+
const group = (await app.service.request<Record<string, unknown>[]>('GET', '/system/menus')).find(
|
|
93
|
+
(row) => row.name === 'Group',
|
|
94
|
+
)!;
|
|
95
|
+
await expect(app.service.save('menus', { ...group, parentId: group.id }, String(group.id))).rejects.toThrow(
|
|
96
|
+
'循环',
|
|
97
|
+
);
|
|
98
|
+
await expect(
|
|
99
|
+
app.service.save('menus', {
|
|
100
|
+
title: '恶意',
|
|
101
|
+
path: '/bad',
|
|
102
|
+
name: 'Bad',
|
|
103
|
+
type: 'menu',
|
|
104
|
+
component: 'https://evil.invalid/a.js',
|
|
105
|
+
}),
|
|
106
|
+
).rejects.toThrow('白名单');
|
|
107
|
+
await app.login({ username: 'reader', password: 'lumal123' });
|
|
108
|
+
expect(app.permissionStore.permissions).toEqual(['system:user:list']);
|
|
109
|
+
await expect(app.service.list('users', { page: 1, pageSize: 10, query: {} })).resolves.toHaveProperty('total');
|
|
110
|
+
await expect(app.service.save('users', { username: 'denied' })).rejects.toThrow('无操作权限');
|
|
111
|
+
} else if (flatten(localMenus).some((menu) => menu.component === 'migration/users')) {
|
|
112
|
+
expect(app.permissionStore.permissions).toContain('system:user:create');
|
|
113
|
+
await app.service.save('users', {
|
|
114
|
+
username: 'migration-only',
|
|
115
|
+
nickname: '迁移用户',
|
|
116
|
+
roles: ['guest'],
|
|
117
|
+
status: 'enabled',
|
|
118
|
+
});
|
|
119
|
+
const result = await app.service.list('users', { page: 1, pageSize: 10, query: { keyword: 'migration-only' } });
|
|
120
|
+
expect(result.total).toBe(1);
|
|
121
|
+
await app.service.remove('users', String(result.items[0]?.id));
|
|
122
|
+
expect(
|
|
123
|
+
(await app.service.list('users', { page: 1, pageSize: 10, query: { keyword: 'migration-only' } })).total,
|
|
124
|
+
).toBe(0);
|
|
125
|
+
} else {
|
|
126
|
+
await expect(app.service.list('users', { page: 1, pageSize: 10, query: {} })).rejects.toThrow('不支持');
|
|
76
127
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
await app.service.remove('users', String(result.items[0]?.id))
|
|
83
|
-
expect((await app.service.list('users', { page: 1, pageSize: 10, query: { keyword: 'migration-only' } })).total).toBe(0)
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
await expect(app.service.list('users', { page: 1, pageSize: 10, query: {} })).rejects.toThrow('无操作权限')
|
|
87
|
-
}
|
|
88
|
-
await app.login({ username: 'guest', password: 'lumal123' })
|
|
89
|
-
expect(app.permissionStore.permissions).toEqual([])
|
|
90
|
-
await expect(app.service.save('users', { username: 'denied' })).rejects.toThrow('无操作权限')
|
|
91
|
-
})
|
|
92
|
-
})
|
|
128
|
+
await app.login({ username: 'guest', password: 'lumal123' });
|
|
129
|
+
expect(app.permissionStore.permissions).toEqual([]);
|
|
130
|
+
await expect(app.service.save('users', { username: 'denied' })).rejects.toThrow();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { LumalPage } from '@lumal/core/components';
|
|
3
|
+
import { ElEmpty } from 'element-plus';
|
|
4
|
+
import { currentUser } from '../../app/services/session';
|
|
5
|
+
import DashboardSessionStatus from './components/DashboardSessionStatus.vue';
|
|
6
|
+
import DashboardShortcuts from './components/DashboardShortcuts.vue';
|
|
7
|
+
|
|
8
|
+
/***********************工作台装配*********************/
|
|
9
|
+
const today = new Intl.DateTimeFormat('zh-CN', { dateStyle: 'full' }).format(new Date());
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<LumalPage class="lumal-admin-page" title="工作台">
|
|
14
|
+
<header class="dashboard-welcome">
|
|
15
|
+
<div>
|
|
16
|
+
<h2>你好,{{ currentUser?.name }}</h2>
|
|
17
|
+
<p>{{ today }}</p>
|
|
18
|
+
</div>
|
|
19
|
+
</header>
|
|
20
|
+
<DashboardShortcuts />
|
|
21
|
+
<DashboardSessionStatus />
|
|
22
|
+
<section class="dashboard-section">
|
|
23
|
+
<h3>业务动态</h3>
|
|
24
|
+
<ElEmpty description="暂无业务动态" :image-size="72" />
|
|
25
|
+
</section>
|
|
26
|
+
</LumalPage>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<style scoped>
|
|
30
|
+
.dashboard-welcome {
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
justify-content: space-between;
|
|
34
|
+
padding: 8px 0 24px;
|
|
35
|
+
}
|
|
36
|
+
.dashboard-welcome h2 {
|
|
37
|
+
margin: 0 0 8px;
|
|
38
|
+
font-size: 22px;
|
|
39
|
+
}
|
|
40
|
+
.dashboard-welcome p {
|
|
41
|
+
margin: 0;
|
|
42
|
+
color: var(--el-text-color-secondary);
|
|
43
|
+
}
|
|
44
|
+
.dashboard-section {
|
|
45
|
+
padding-top: 24px;
|
|
46
|
+
border-top: 1px solid var(--el-border-color-lighter);
|
|
47
|
+
}
|
|
48
|
+
.dashboard-section h3 {
|
|
49
|
+
margin: 0;
|
|
50
|
+
font-size: 16px;
|
|
51
|
+
}
|
|
52
|
+
</style>
|
package/templates/presets/standard/src/modules/dashboard/components/DashboardSessionStatus.vue
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ElDescriptions, ElDescriptionsItem, ElTag } from 'element-plus';
|
|
3
|
+
import { appConfig } from '../../../app.config';
|
|
4
|
+
import { permissionStore } from '../../../app/services/session';
|
|
5
|
+
|
|
6
|
+
/***********************真实应用与会话状态*********************/
|
|
7
|
+
const source = appConfig.apiMode === 'local' ? '本地开发演示' : 'HTTP API';
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<section class="session-status">
|
|
12
|
+
<h3>应用状态</h3>
|
|
13
|
+
<ElDescriptions :column="1" border>
|
|
14
|
+
<ElDescriptionsItem label="数据源">
|
|
15
|
+
<ElTag :type="appConfig.apiMode === 'local' ? 'warning' : 'success'">
|
|
16
|
+
{{ source }}
|
|
17
|
+
</ElTag>
|
|
18
|
+
</ElDescriptionsItem>
|
|
19
|
+
<ElDescriptionsItem label="当前角色">
|
|
20
|
+
{{ permissionStore.roles.join('、') || '无' }}
|
|
21
|
+
</ElDescriptionsItem>
|
|
22
|
+
<ElDescriptionsItem label="可用操作权限">
|
|
23
|
+
{{ permissionStore.permissions.length }}
|
|
24
|
+
</ElDescriptionsItem>
|
|
25
|
+
<ElDescriptionsItem label="启用模块">
|
|
26
|
+
{{ appConfig.features.join('、') || '无可选模块' }}
|
|
27
|
+
</ElDescriptionsItem>
|
|
28
|
+
</ElDescriptions>
|
|
29
|
+
</section>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<style scoped>
|
|
33
|
+
.session-status {
|
|
34
|
+
padding: 24px 0;
|
|
35
|
+
border-top: 1px solid var(--el-border-color-lighter);
|
|
36
|
+
}
|
|
37
|
+
.session-status h3 {
|
|
38
|
+
margin: 0 0 16px;
|
|
39
|
+
font-size: 16px;
|
|
40
|
+
}
|
|
41
|
+
</style>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { SidebarMenuItem } from '@lumal/core/router';
|
|
3
|
+
import { LumalIcon } from '@lumal/icons-vue';
|
|
4
|
+
import { ElButton, ElEmpty } from 'element-plus';
|
|
5
|
+
import { computed } from 'vue';
|
|
6
|
+
import { useRouter } from 'vue-router';
|
|
7
|
+
import { createAdminSidebarMenus } from '../../../app/router';
|
|
8
|
+
|
|
9
|
+
/***********************当前权限下的快捷入口*********************/
|
|
10
|
+
const router = useRouter();
|
|
11
|
+
function flatten(items: SidebarMenuItem[]): SidebarMenuItem[] {
|
|
12
|
+
return items.flatMap((item) => (item.children?.length ? flatten(item.children) : [item]));
|
|
13
|
+
}
|
|
14
|
+
const shortcuts = computed(() =>
|
|
15
|
+
flatten(createAdminSidebarMenus())
|
|
16
|
+
.filter((item) => item.path !== '/dashboard' && !item.externalLink)
|
|
17
|
+
.slice(0, 6),
|
|
18
|
+
);
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<section class="dashboard-shortcuts">
|
|
23
|
+
<h3>常用入口</h3>
|
|
24
|
+
<div v-if="shortcuts.length" class="shortcut-list">
|
|
25
|
+
<ElButton v-for="item in shortcuts" :key="item.path" @click="router.push(item.path)">
|
|
26
|
+
<LumalIcon :name="item.icon || 'lumal:grid'" />{{ item.title }}
|
|
27
|
+
</ElButton>
|
|
28
|
+
</div>
|
|
29
|
+
<ElEmpty v-else description="暂无可用入口" :image-size="48" />
|
|
30
|
+
</section>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<style scoped>
|
|
34
|
+
.dashboard-shortcuts {
|
|
35
|
+
padding-bottom: 24px;
|
|
36
|
+
}
|
|
37
|
+
.dashboard-shortcuts h3 {
|
|
38
|
+
margin: 0 0 16px;
|
|
39
|
+
font-size: 16px;
|
|
40
|
+
}
|
|
41
|
+
.shortcut-list {
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-wrap: wrap;
|
|
44
|
+
gap: 12px;
|
|
45
|
+
}
|
|
46
|
+
.shortcut-list :deep(.el-button) {
|
|
47
|
+
min-height: 40px;
|
|
48
|
+
margin: 0;
|
|
49
|
+
gap: 8px;
|
|
50
|
+
}
|
|
51
|
+
</style>
|
|
@@ -0,0 +1,109 @@
|
|
|
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, ElMessage, ElTabPane, ElTabs } from 'element-plus';
|
|
5
|
+
import { shallowRef, watch } from 'vue';
|
|
6
|
+
import { application, service } from '../../app/services/session';
|
|
7
|
+
import { useAsyncData } from '../../shared/composables/use-async-data';
|
|
8
|
+
import { createProfileService, profileInput } from './api';
|
|
9
|
+
import ProfilePasswordForm from './components/ProfilePasswordForm.vue';
|
|
10
|
+
|
|
11
|
+
/***********************个人资料读取与保存*********************/
|
|
12
|
+
const api = createProfileService(service);
|
|
13
|
+
const state = useAsyncData(api.get, null);
|
|
14
|
+
const model = shallowRef<SchemaFormModel>({});
|
|
15
|
+
const saving = shallowRef(false);
|
|
16
|
+
const needsSessionRefresh = shallowRef(false);
|
|
17
|
+
const failure = shallowRef('');
|
|
18
|
+
const success = shallowRef('');
|
|
19
|
+
watch(state.data, (value) => {
|
|
20
|
+
if (value) model.value = { ...value };
|
|
21
|
+
});
|
|
22
|
+
const schemas: SchemaFormItem[] = [
|
|
23
|
+
{ field: 'username', label: '用户名', readonly: true },
|
|
24
|
+
{
|
|
25
|
+
field: 'roles',
|
|
26
|
+
label: '角色',
|
|
27
|
+
readonly: true,
|
|
28
|
+
formatter: (value) => (Array.isArray(value) ? value.join('、') : ''),
|
|
29
|
+
},
|
|
30
|
+
{ field: 'nickname', label: '昵称', required: true, rules: [{ max: 32, message: '昵称最多 32 个字符' }] },
|
|
31
|
+
{ field: 'phone', label: '手机号', rules: [{ pattern: /^1\d{10}$/, message: '手机号格式错误' }] },
|
|
32
|
+
];
|
|
33
|
+
async function save(values: SchemaFormModel): Promise<void> {
|
|
34
|
+
if (saving.value || needsSessionRefresh.value) return;
|
|
35
|
+
saving.value = true;
|
|
36
|
+
failure.value = '';
|
|
37
|
+
success.value = '';
|
|
38
|
+
let persisted = false;
|
|
39
|
+
try {
|
|
40
|
+
model.value = { ...(await api.save(profileInput(values))) };
|
|
41
|
+
persisted = true;
|
|
42
|
+
needsSessionRefresh.value = true;
|
|
43
|
+
await application.refreshAccess();
|
|
44
|
+
needsSessionRefresh.value = false;
|
|
45
|
+
success.value = '个人资料已更新';
|
|
46
|
+
ElMessage.success('个人资料已更新');
|
|
47
|
+
} catch (reason) {
|
|
48
|
+
failure.value = `${persisted ? '资料已保存,但会话刷新失败,请重新读取:' : ''}${reason instanceof Error ? reason.message : '保存失败'}`;
|
|
49
|
+
} finally {
|
|
50
|
+
saving.value = false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/***********************部分成功后的只读重试*********************/
|
|
55
|
+
async function reload(): Promise<void> {
|
|
56
|
+
if (saving.value || state.loading.value) return;
|
|
57
|
+
saving.value = true;
|
|
58
|
+
failure.value = '';
|
|
59
|
+
try {
|
|
60
|
+
// 已保存的资料不重复提交;恢复会话授权后再读取服务端资料。
|
|
61
|
+
if (needsSessionRefresh.value) {
|
|
62
|
+
await application.refreshAccess();
|
|
63
|
+
needsSessionRefresh.value = false;
|
|
64
|
+
}
|
|
65
|
+
await state.load();
|
|
66
|
+
} catch (reason) {
|
|
67
|
+
failure.value = `${needsSessionRefresh.value ? '资料已保存,但会话刷新失败,请重试:' : ''}${reason instanceof Error ? reason.message : '读取失败'}`;
|
|
68
|
+
} finally {
|
|
69
|
+
saving.value = false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<template>
|
|
75
|
+
<LumalPage class="lumal-admin-page" title="个人中心">
|
|
76
|
+
<ElAlert v-if="state.error.value || failure" :title="state.error.value || failure" type="error" :closable="false">
|
|
77
|
+
<ElButton link :loading="saving" @click="reload">
|
|
78
|
+
{{ needsSessionRefresh ? '重试会话刷新' : '重新读取' }}
|
|
79
|
+
</ElButton>
|
|
80
|
+
</ElAlert>
|
|
81
|
+
<ElAlert v-if="success" :title="success" type="success" :closable="false" />
|
|
82
|
+
<ElTabs class="profile-tabs">
|
|
83
|
+
<ElTabPane label="个人资料">
|
|
84
|
+
<LumalSchemaForm
|
|
85
|
+
v-if="state.data.value"
|
|
86
|
+
v-model="model"
|
|
87
|
+
:schemas="schemas"
|
|
88
|
+
:columns="1"
|
|
89
|
+
mode="edit"
|
|
90
|
+
show-actions
|
|
91
|
+
:disabled="needsSessionRefresh"
|
|
92
|
+
:submit-disabled="needsSessionRefresh"
|
|
93
|
+
:submit-loading="saving"
|
|
94
|
+
submit-text="保存资料"
|
|
95
|
+
@submit="save"
|
|
96
|
+
/>
|
|
97
|
+
</ElTabPane>
|
|
98
|
+
<ElTabPane label="修改密码" lazy>
|
|
99
|
+
<ProfilePasswordForm :api="api" />
|
|
100
|
+
</ElTabPane>
|
|
101
|
+
</ElTabs>
|
|
102
|
+
</LumalPage>
|
|
103
|
+
</template>
|
|
104
|
+
|
|
105
|
+
<style scoped>
|
|
106
|
+
.profile-tabs {
|
|
107
|
+
max-width: 720px;
|
|
108
|
+
}
|
|
109
|
+
</style>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { SchemaFormModel } from '@lumal/core/components';
|
|
2
|
+
import type { BusinessTransport } from '../../app/services/contracts';
|
|
3
|
+
import { object, stringArray, text } from '../../shared/api';
|
|
4
|
+
|
|
5
|
+
export interface Profile {
|
|
6
|
+
id: string;
|
|
7
|
+
username: string;
|
|
8
|
+
nickname: string;
|
|
9
|
+
phone: string;
|
|
10
|
+
roles: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface ProfileInput {
|
|
13
|
+
nickname: string;
|
|
14
|
+
phone: string;
|
|
15
|
+
}
|
|
16
|
+
export interface PasswordInput {
|
|
17
|
+
currentPassword: string;
|
|
18
|
+
newPassword: string;
|
|
19
|
+
}
|
|
20
|
+
export function profileInput(model: SchemaFormModel): ProfileInput {
|
|
21
|
+
const nickname = text(model.nickname, '昵称', true);
|
|
22
|
+
const phone = text(model.phone, '手机号');
|
|
23
|
+
if (nickname.length > 32 || (phone && !/^1\d{10}$/.test(phone)))
|
|
24
|
+
throw new Error('昵称最多 32 个字符,手机号为 11 位数字');
|
|
25
|
+
return { nickname, phone };
|
|
26
|
+
}
|
|
27
|
+
export function passwordInput(model: SchemaFormModel): PasswordInput {
|
|
28
|
+
const currentPassword = text(model.currentPassword, '当前密码', true);
|
|
29
|
+
const newPassword = text(model.newPassword, '新密码', true);
|
|
30
|
+
if (newPassword.length < 8 || newPassword.length > 32 || newPassword !== model.confirmPassword)
|
|
31
|
+
throw new Error('新密码为 8 到 32 位,且两次输入必须相同');
|
|
32
|
+
return { currentPassword, newPassword };
|
|
33
|
+
}
|
|
34
|
+
function parseProfile(value: unknown): Profile {
|
|
35
|
+
const raw = object(value);
|
|
36
|
+
return {
|
|
37
|
+
...profileInput(raw),
|
|
38
|
+
id: text(raw.id, '用户 ID', true),
|
|
39
|
+
username: text(raw.username, '用户名', true),
|
|
40
|
+
roles: stringArray(raw.roles),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function createProfileService(transport: BusinessTransport) {
|
|
44
|
+
return {
|
|
45
|
+
get: async () => parseProfile(await transport.request('GET', '/profile')),
|
|
46
|
+
save: async (input: ProfileInput) => parseProfile(await transport.request('PUT', '/profile', { body: input })),
|
|
47
|
+
changePassword: (input: PasswordInput) => transport.request<void>('PUT', '/profile/password', { body: input }),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { SchemaFormItem, SchemaFormModel } from '@lumal/core/components';
|
|
3
|
+
import type { createProfileService } from '../api';
|
|
4
|
+
import { LumalSchemaForm } from '@lumal/core/components';
|
|
5
|
+
import { ElAlert } from 'element-plus';
|
|
6
|
+
import { shallowRef } from 'vue';
|
|
7
|
+
import { passwordInput } from '../api';
|
|
8
|
+
|
|
9
|
+
/***********************真实密码更新接口*********************/
|
|
10
|
+
const props = defineProps<{ api: ReturnType<typeof createProfileService> }>();
|
|
11
|
+
const model = shallowRef<SchemaFormModel>({});
|
|
12
|
+
const saving = shallowRef(false);
|
|
13
|
+
const failure = shallowRef('');
|
|
14
|
+
const success = shallowRef('');
|
|
15
|
+
const schemas: SchemaFormItem[] = [
|
|
16
|
+
{
|
|
17
|
+
field: 'currentPassword',
|
|
18
|
+
label: '当前密码',
|
|
19
|
+
required: true,
|
|
20
|
+
component: 'input',
|
|
21
|
+
componentProps: { type: 'password', showPassword: true, autocomplete: 'current-password' },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
field: 'newPassword',
|
|
25
|
+
label: '新密码',
|
|
26
|
+
required: true,
|
|
27
|
+
component: 'input',
|
|
28
|
+
rules: [{ min: 8, max: 32, message: '密码长度为 8 到 32 位' }],
|
|
29
|
+
componentProps: { type: 'password', showPassword: true, autocomplete: 'new-password' },
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
field: 'confirmPassword',
|
|
33
|
+
label: '确认新密码',
|
|
34
|
+
required: true,
|
|
35
|
+
component: 'input',
|
|
36
|
+
componentProps: { type: 'password', showPassword: true, autocomplete: 'new-password' },
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
async function submit(values: SchemaFormModel): Promise<void> {
|
|
40
|
+
if (saving.value) return;
|
|
41
|
+
saving.value = true;
|
|
42
|
+
failure.value = '';
|
|
43
|
+
success.value = '';
|
|
44
|
+
try {
|
|
45
|
+
await props.api.changePassword(passwordInput(values));
|
|
46
|
+
model.value = {};
|
|
47
|
+
success.value = '密码已通过接口更新';
|
|
48
|
+
} catch (reason) {
|
|
49
|
+
failure.value = reason instanceof Error ? reason.message : '密码更新失败';
|
|
50
|
+
} finally {
|
|
51
|
+
saving.value = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<ElAlert v-if="failure" :title="failure" type="error" role="alert" :closable="false" />
|
|
58
|
+
<ElAlert v-if="success" :title="success" type="success" :closable="false" />
|
|
59
|
+
<LumalSchemaForm
|
|
60
|
+
v-model="model"
|
|
61
|
+
:schemas="schemas"
|
|
62
|
+
:columns="1"
|
|
63
|
+
show-actions
|
|
64
|
+
submit-text="更新密码"
|
|
65
|
+
:submit-loading="saving"
|
|
66
|
+
@submit="submit"
|
|
67
|
+
/>
|
|
68
|
+
</template>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { DataRow } from '../../app/services/contracts';
|
|
2
|
+
import type { LocalBusinessContext, LocalHandler } from '../../app/services/local-contracts';
|
|
3
|
+
import { object } from '../../app/menu-policy';
|
|
4
|
+
import { profileInput } from './api';
|
|
5
|
+
|
|
6
|
+
export function createLocalHandler({ db, user }: LocalBusinessContext): LocalHandler {
|
|
7
|
+
return {
|
|
8
|
+
handles: (path) => path === '/profile' || path === '/profile/password',
|
|
9
|
+
async request(method, path, input) {
|
|
10
|
+
const current = db.read('users').find((row) => row.id === user().id);
|
|
11
|
+
if (!current) throw new Error('当前用户不存在');
|
|
12
|
+
const clean = ({ password: _password, ...profile }: DataRow) => profile;
|
|
13
|
+
if (path === '/profile' && method === 'GET') return clean(current);
|
|
14
|
+
if (path === '/profile' && method === 'PUT') {
|
|
15
|
+
const next = { ...current, ...profileInput(object(input.body)) };
|
|
16
|
+
db.write(
|
|
17
|
+
'users',
|
|
18
|
+
db.read('users').map((row) => (row.id === current.id ? next : row)),
|
|
19
|
+
);
|
|
20
|
+
return clean(next);
|
|
21
|
+
}
|
|
22
|
+
if (path === '/profile/password' && method === 'PUT') {
|
|
23
|
+
const body = object(input.body);
|
|
24
|
+
if (
|
|
25
|
+
body.currentPassword !== current.password ||
|
|
26
|
+
typeof body.newPassword !== 'string' ||
|
|
27
|
+
body.newPassword.length < 8 ||
|
|
28
|
+
body.newPassword.length > 32
|
|
29
|
+
)
|
|
30
|
+
throw new Error('当前密码错误或新密码长度不符合要求');
|
|
31
|
+
db.write(
|
|
32
|
+
'users',
|
|
33
|
+
db.read('users').map((row) => (row.id === current.id ? { ...row, password: body.newPassword } : row)),
|
|
34
|
+
);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
throw new Error('不支持的个人资料操作');
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AppModule } from '../../app/module';
|
|
2
|
+
|
|
3
|
+
export const menus: AppModule['menus'] = [
|
|
4
|
+
{
|
|
5
|
+
path: '/profile',
|
|
6
|
+
name: 'Profile',
|
|
7
|
+
component: 'profile/index',
|
|
8
|
+
meta: { title: '个人中心', icon: 'lumal:user', hideInMenu: true, keepAlive: false },
|
|
9
|
+
},
|
|
10
|
+
];
|
|
11
|
+
export const views: AppModule['views'] = { 'profile/index': () => import('./ProfileView.vue') };
|