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
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CrudDataSource, SchemaFormItem, SchemaTableColumn, SchemaTableRow } from '@lumal/core/components';
|
|
3
|
+
import type { Option } from '../../../shared/api';
|
|
4
|
+
import type { User, UserQuery } from './types';
|
|
5
|
+
import { LumalCrudTable, LumalPage } from '@lumal/core/components';
|
|
6
|
+
import { ElAlert, ElButton, ElMessage, ElMessageBox } from 'element-plus';
|
|
7
|
+
import { computed, shallowRef, useTemplateRef, watch } from 'vue';
|
|
8
|
+
import { permissionStore, service } from '../../../app/services/session';
|
|
9
|
+
import { options } from '../../../shared/api';
|
|
10
|
+
import { useAsyncData } from '../../../shared/composables/use-async-data';
|
|
11
|
+
import { statusOptions } from '../../../shared/status';
|
|
12
|
+
import { createUserService, parseUser, userInput } from './api';
|
|
13
|
+
import UserOrganizationFilter from './components/UserOrganizationFilter.vue';
|
|
14
|
+
import UserRoleDialog from './components/UserRoleDialog.vue';
|
|
15
|
+
|
|
16
|
+
/***********************用户列表与引用选项*********************/
|
|
17
|
+
const api = createUserService(service);
|
|
18
|
+
const table = useTemplateRef<InstanceType<typeof LumalCrudTable>>('table');
|
|
19
|
+
const organizationId = shallowRef('');
|
|
20
|
+
const selected = shallowRef<SchemaTableRow[]>([]);
|
|
21
|
+
const actionError = shallowRef('');
|
|
22
|
+
let listError: unknown;
|
|
23
|
+
const acting = shallowRef(false);
|
|
24
|
+
const roleDialog = shallowRef(false);
|
|
25
|
+
const assigning = shallowRef<User[]>([]);
|
|
26
|
+
const batchAssignment = shallowRef(false);
|
|
27
|
+
const can = (action: string) => permissionStore.hasPermission(`system:user:${action}`);
|
|
28
|
+
const references = useAsyncData(
|
|
29
|
+
async () => {
|
|
30
|
+
const [roles, organizations] = await Promise.all([
|
|
31
|
+
service.request('GET', '/system/roles/options'),
|
|
32
|
+
service.request('GET', '/system/organizations/options'),
|
|
33
|
+
]);
|
|
34
|
+
return { roles: options(roles), organizations: options(organizations) };
|
|
35
|
+
},
|
|
36
|
+
{ roles: [] as Option[], organizations: [] as Option[] },
|
|
37
|
+
);
|
|
38
|
+
function flatten(options: Option[]): Option[] {
|
|
39
|
+
return options.flatMap((item) => [item, ...flatten(item.children ?? [])]);
|
|
40
|
+
}
|
|
41
|
+
const orgLabels = computed(
|
|
42
|
+
() => new Map(flatten(references.data.value.organizations).map((item) => [item.value, item.label])),
|
|
43
|
+
);
|
|
44
|
+
watch(organizationId, () => {
|
|
45
|
+
void table.value?.reload();
|
|
46
|
+
});
|
|
47
|
+
const dataSource: CrudDataSource = {
|
|
48
|
+
async fetch(input) {
|
|
49
|
+
listError = undefined;
|
|
50
|
+
try {
|
|
51
|
+
return await api.list({ ...input, query: { ...input.query, organizationId: organizationId.value } as UserQuery });
|
|
52
|
+
} catch (reason) {
|
|
53
|
+
listError = reason;
|
|
54
|
+
throw reason;
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
create: (model) => api.create(userInput(model)),
|
|
58
|
+
update: (row, model) => api.update(parseUser(row).id, userInput(model)),
|
|
59
|
+
remove: (row) => api.remove(parseUser(row).id),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/***********************用户独立表单与查询契约*********************/
|
|
63
|
+
const columns = computed<SchemaTableColumn[]>(() => [
|
|
64
|
+
{ field: 'username', label: '用户名', minWidth: 130 },
|
|
65
|
+
{ field: 'nickname', label: '昵称', minWidth: 120 },
|
|
66
|
+
{
|
|
67
|
+
field: 'organizationId',
|
|
68
|
+
label: '机构',
|
|
69
|
+
minWidth: 140,
|
|
70
|
+
formatter: (value) => orgLabels.value.get(String(value)) ?? '-',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
field: 'roles',
|
|
74
|
+
label: '角色',
|
|
75
|
+
minWidth: 150,
|
|
76
|
+
formatter: (value) =>
|
|
77
|
+
Array.isArray(value)
|
|
78
|
+
? value
|
|
79
|
+
.map((code) => references.data.value.roles.find((option) => option.value === code)?.label ?? code)
|
|
80
|
+
.join('、')
|
|
81
|
+
: '',
|
|
82
|
+
},
|
|
83
|
+
{ field: 'status', label: '状态', options: statusOptions, width: 90 },
|
|
84
|
+
{ field: 'phone', label: '手机号', minWidth: 130 },
|
|
85
|
+
]);
|
|
86
|
+
const schemas = computed<SchemaFormItem[]>(() => [
|
|
87
|
+
{ field: 'username', label: '用户名', component: 'input', required: true, editDisabled: true },
|
|
88
|
+
{
|
|
89
|
+
field: 'nickname',
|
|
90
|
+
label: '昵称',
|
|
91
|
+
component: 'input',
|
|
92
|
+
required: true,
|
|
93
|
+
rules: [{ max: 32, message: '昵称最多 32 个字符' }],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
field: 'organizationId',
|
|
97
|
+
label: '所属机构',
|
|
98
|
+
component: 'tree-select',
|
|
99
|
+
options: references.data.value.organizations,
|
|
100
|
+
componentProps: {
|
|
101
|
+
data: references.data.value.organizations,
|
|
102
|
+
checkStrictly: true,
|
|
103
|
+
defaultExpandAll: true,
|
|
104
|
+
props: { children: 'children', label: 'label', value: 'value', disabled: 'disabled' },
|
|
105
|
+
},
|
|
106
|
+
defaultValue: references.data.value.organizations[0]?.value ?? '',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
field: 'roles',
|
|
110
|
+
label: '角色',
|
|
111
|
+
component: 'select',
|
|
112
|
+
required: true,
|
|
113
|
+
options: references.data.value.roles,
|
|
114
|
+
defaultValue: ['guest'],
|
|
115
|
+
componentProps: { multiple: true },
|
|
116
|
+
disabled: ({ mode }) => mode === 'edit' && !can('assign-roles'),
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
field: 'status',
|
|
120
|
+
label: '状态',
|
|
121
|
+
component: 'select',
|
|
122
|
+
options: statusOptions,
|
|
123
|
+
defaultValue: 'enabled',
|
|
124
|
+
disabled: ({ mode, model }) => model.username === 'admin' || (mode === 'edit' && !can('status')),
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
field: 'phone',
|
|
128
|
+
label: '手机号',
|
|
129
|
+
component: 'input',
|
|
130
|
+
rules: [{ pattern: /^1\d{10}$/, message: '请输入 11 位手机号' }],
|
|
131
|
+
},
|
|
132
|
+
]);
|
|
133
|
+
const querySchemas = computed<SchemaFormItem[]>(() => [
|
|
134
|
+
{ field: 'keyword', label: '关键词', component: 'input' },
|
|
135
|
+
{ field: 'roles', label: '角色', component: 'select', options: references.data.value.roles },
|
|
136
|
+
{ field: 'status', label: '状态', component: 'select', options: statusOptions },
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
/***********************状态、批量操作与密码重置*********************/
|
|
140
|
+
async function run(action: () => Promise<unknown>): Promise<void> {
|
|
141
|
+
if (acting.value) return;
|
|
142
|
+
acting.value = true;
|
|
143
|
+
actionError.value = '';
|
|
144
|
+
let persisted = false;
|
|
145
|
+
try {
|
|
146
|
+
await action();
|
|
147
|
+
persisted = true;
|
|
148
|
+
await table.value?.reload();
|
|
149
|
+
if (listError) throw listError;
|
|
150
|
+
table.value?.clearSelection();
|
|
151
|
+
selected.value = [];
|
|
152
|
+
ElMessage.success('操作成功');
|
|
153
|
+
} catch (reason) {
|
|
154
|
+
actionError.value = `${persisted ? '操作已保存,但刷新失败:' : ''}${reason instanceof Error ? reason.message : '操作失败'}`;
|
|
155
|
+
} finally {
|
|
156
|
+
acting.value = false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function assign(users: User[], batch = false): void {
|
|
160
|
+
assigning.value = users;
|
|
161
|
+
batchAssignment.value = batch;
|
|
162
|
+
roleDialog.value = true;
|
|
163
|
+
}
|
|
164
|
+
async function resetPassword(row: SchemaTableRow): Promise<void> {
|
|
165
|
+
try {
|
|
166
|
+
await ElMessageBox.confirm(`重置 ${parseUser(row).nickname} 的密码?`, '密码重置', {
|
|
167
|
+
confirmButtonText: '重置',
|
|
168
|
+
cancelButtonText: '取消',
|
|
169
|
+
});
|
|
170
|
+
} catch {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
await run(async () => {
|
|
174
|
+
const result = await api.resetPassword(parseUser(row).id);
|
|
175
|
+
await ElMessageBox.alert(`临时密码:${result.temporaryPassword}`, '密码已重置');
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
async function rolesSaved(): Promise<void> {
|
|
179
|
+
table.value?.clearSelection();
|
|
180
|
+
selected.value = [];
|
|
181
|
+
await table.value?.reload();
|
|
182
|
+
if (listError) actionError.value = '角色分配已保存,但列表读取失败,请重新加载';
|
|
183
|
+
}
|
|
184
|
+
</script>
|
|
185
|
+
|
|
186
|
+
<template>
|
|
187
|
+
<LumalPage class="lumal-admin-page" title="用户管理" no-padding>
|
|
188
|
+
<ElAlert v-if="references.error.value" :title="references.error.value" type="error" :closable="false">
|
|
189
|
+
<ElButton link @click="references.reload"> 重试选项加载 </ElButton>
|
|
190
|
+
</ElAlert>
|
|
191
|
+
<ElAlert v-if="actionError" :title="actionError" type="error" role="alert" closable @close="actionError = ''" />
|
|
192
|
+
<div class="user-layout">
|
|
193
|
+
<UserOrganizationFilter v-model="organizationId" :options="references.data.value.organizations" />
|
|
194
|
+
<LumalCrudTable
|
|
195
|
+
ref="table"
|
|
196
|
+
:data-source="dataSource"
|
|
197
|
+
:form-schemas="schemas"
|
|
198
|
+
:table="{ columns, rowKey: 'id', selection: true, showColumnSettings: true, actionWidth: 430 }"
|
|
199
|
+
:query="{ schemas: querySchemas, columns: 3, collapsible: true, mobileDefaultVisible: false }"
|
|
200
|
+
:toolbar="{ create: can('create') && !references.error.value, batchDelete: false, refresh: true }"
|
|
201
|
+
:actions="{ view: false, edit: false, remove: false }"
|
|
202
|
+
:editor="{ columns: 2 }"
|
|
203
|
+
@selection-change="selected = $event"
|
|
204
|
+
@operation-error="actionError = $event instanceof Error ? $event.message : String($event)"
|
|
205
|
+
>
|
|
206
|
+
<template #toolbar-actions="{ selectedRows }">
|
|
207
|
+
<ElButton
|
|
208
|
+
v-if="can('status')"
|
|
209
|
+
:disabled="!selectedRows.length || acting"
|
|
210
|
+
@click="
|
|
211
|
+
run(() =>
|
|
212
|
+
api.batchStatus(
|
|
213
|
+
selectedRows.map((row) => parseUser(row).id),
|
|
214
|
+
'enabled',
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
"
|
|
218
|
+
>
|
|
219
|
+
批量启用
|
|
220
|
+
</ElButton>
|
|
221
|
+
<ElButton
|
|
222
|
+
v-if="can('status')"
|
|
223
|
+
:disabled="!selectedRows.length || acting"
|
|
224
|
+
@click="
|
|
225
|
+
run(() =>
|
|
226
|
+
api.batchStatus(
|
|
227
|
+
selectedRows.map((row) => parseUser(row).id),
|
|
228
|
+
'disabled',
|
|
229
|
+
),
|
|
230
|
+
)
|
|
231
|
+
"
|
|
232
|
+
>
|
|
233
|
+
批量停用
|
|
234
|
+
</ElButton>
|
|
235
|
+
<ElButton
|
|
236
|
+
v-if="can('assign-roles')"
|
|
237
|
+
:disabled="!selectedRows.length || !!references.error.value"
|
|
238
|
+
@click="assign(selectedRows.map(parseUser), true)"
|
|
239
|
+
>
|
|
240
|
+
批量分配角色
|
|
241
|
+
</ElButton>
|
|
242
|
+
</template>
|
|
243
|
+
<template #row-actions="{ row }">
|
|
244
|
+
<ElButton link @click="table?.openView(row)"> 查看 </ElButton>
|
|
245
|
+
<ElButton v-if="can('update')" link @click="table?.openEdit(row)"> 编辑 </ElButton>
|
|
246
|
+
<ElButton
|
|
247
|
+
v-if="can('status')"
|
|
248
|
+
link
|
|
249
|
+
:disabled="row.username === 'admin' || acting"
|
|
250
|
+
@click="run(() => api.setStatus(parseUser(row).id, row.status === 'enabled' ? 'disabled' : 'enabled'))"
|
|
251
|
+
>
|
|
252
|
+
{{ row.status === 'enabled' ? '停用' : '启用' }}
|
|
253
|
+
</ElButton>
|
|
254
|
+
<ElButton
|
|
255
|
+
v-if="can('assign-roles')"
|
|
256
|
+
link
|
|
257
|
+
:disabled="!!references.error.value"
|
|
258
|
+
@click="assign([parseUser(row)])"
|
|
259
|
+
>
|
|
260
|
+
分配角色
|
|
261
|
+
</ElButton>
|
|
262
|
+
<ElButton v-if="can('reset-password')" link @click="resetPassword(row)"> 重置密码 </ElButton>
|
|
263
|
+
<ElButton
|
|
264
|
+
v-if="can('delete')"
|
|
265
|
+
link
|
|
266
|
+
type="danger"
|
|
267
|
+
:disabled="row.username === 'admin'"
|
|
268
|
+
@click="table?.removeRow(row)"
|
|
269
|
+
>
|
|
270
|
+
删除
|
|
271
|
+
</ElButton>
|
|
272
|
+
</template>
|
|
273
|
+
</LumalCrudTable>
|
|
274
|
+
</div>
|
|
275
|
+
<UserRoleDialog
|
|
276
|
+
v-model="roleDialog"
|
|
277
|
+
:users="assigning"
|
|
278
|
+
:options="references.data.value.roles"
|
|
279
|
+
:batch="batchAssignment"
|
|
280
|
+
@saved="rolesSaved"
|
|
281
|
+
/>
|
|
282
|
+
</LumalPage>
|
|
283
|
+
</template>
|
|
284
|
+
|
|
285
|
+
<style scoped>
|
|
286
|
+
.user-layout {
|
|
287
|
+
display: grid;
|
|
288
|
+
grid-template-columns: 220px minmax(0, 1fr);
|
|
289
|
+
gap: 16px;
|
|
290
|
+
min-width: 0;
|
|
291
|
+
}
|
|
292
|
+
@media (max-width: 768px) {
|
|
293
|
+
.user-layout {
|
|
294
|
+
grid-template-columns: minmax(0, 1fr);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
</style>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { SchemaFormModel } from '@lumal/core/components';
|
|
2
|
+
import type { ApplicationService } from '../../../app/services/contracts';
|
|
3
|
+
import type { PageInput } from '../../../shared/api';
|
|
4
|
+
import type { User, UserBatchResult, UserInput, UserQuery } from './types';
|
|
5
|
+
import { formStatus, object, stringArray, text } from '../../../shared/api';
|
|
6
|
+
|
|
7
|
+
export function userInput(model: SchemaFormModel): UserInput {
|
|
8
|
+
return {
|
|
9
|
+
username: text(model.username, '用户名', true),
|
|
10
|
+
nickname: text(model.nickname, '昵称', true),
|
|
11
|
+
organizationId: text(model.organizationId, '所属机构'),
|
|
12
|
+
roles: stringArray(model.roles),
|
|
13
|
+
phone: text(model.phone, '手机号'),
|
|
14
|
+
status: formStatus(model),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function parseUser(value: unknown): User {
|
|
18
|
+
const raw = object(value);
|
|
19
|
+
return { ...userInput(raw), id: text(raw.id, '用户 ID', true), createdAt: text(raw.createdAt, '创建时间') };
|
|
20
|
+
}
|
|
21
|
+
export function createUserService(transport: Pick<ApplicationService, 'list' | 'request'>) {
|
|
22
|
+
const path = (id: string) => `/system/users/${encodeURIComponent(id)}`;
|
|
23
|
+
return {
|
|
24
|
+
async list(input: PageInput<UserQuery>) {
|
|
25
|
+
const result = await transport.list('users', { ...input, query: { ...input.query } });
|
|
26
|
+
return { items: result.items.map(parseUser), total: result.total };
|
|
27
|
+
},
|
|
28
|
+
create: async (input: UserInput) => parseUser(await transport.request('POST', '/system/users', { body: input })),
|
|
29
|
+
update: async (id: string, input: UserInput) =>
|
|
30
|
+
parseUser(await transport.request('PUT', path(id), { body: input })),
|
|
31
|
+
remove: (id: string) => transport.request<void>('DELETE', path(id)),
|
|
32
|
+
setStatus: (id: string, status: User['status']) =>
|
|
33
|
+
transport.request<User>('PATCH', `${path(id)}/status`, { body: { status } }),
|
|
34
|
+
assignRoles: (id: string, roles: string[]) =>
|
|
35
|
+
transport.request<User>('PUT', `${path(id)}/roles`, { body: { roles } }),
|
|
36
|
+
batchStatus: (ids: string[], status: User['status']) =>
|
|
37
|
+
transport.request<UserBatchResult>('PATCH', '/system/users/batch/status', { body: { ids, status } }),
|
|
38
|
+
batchRoles: (ids: string[], roles: string[], mode: 'append' | 'replace') =>
|
|
39
|
+
transport.request<UserBatchResult>('PUT', '/system/users/batch/roles', { body: { ids, roles, mode } }),
|
|
40
|
+
async resetPassword(id: string) {
|
|
41
|
+
const result = object(await transport.request('POST', `${path(id)}/reset-password`));
|
|
42
|
+
return { temporaryPassword: text(result.temporaryPassword, '临时密码', true) };
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
package/templates/features/system/src/modules/system/user/components/UserOrganizationFilter.vue
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Option } from '../../../../shared/api';
|
|
3
|
+
import { ElInput, ElTree } from 'element-plus';
|
|
4
|
+
import { shallowRef, useTemplateRef, watch } from 'vue';
|
|
5
|
+
|
|
6
|
+
/***********************机构筛选契约*********************/
|
|
7
|
+
defineProps<{ options: Option[] }>();
|
|
8
|
+
const value = defineModel<string>({ required: true });
|
|
9
|
+
const keyword = shallowRef('');
|
|
10
|
+
const tree = useTemplateRef<InstanceType<typeof ElTree>>('tree');
|
|
11
|
+
watch(keyword, (text) => tree.value?.filter(text));
|
|
12
|
+
function filter(text: string, data: Record<string, unknown>): boolean {
|
|
13
|
+
return !text || String(data.label).includes(text);
|
|
14
|
+
}
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<details class="user-organizations" open>
|
|
19
|
+
<summary>机构筛选</summary>
|
|
20
|
+
<ElInput v-model="keyword" aria-label="搜索机构" placeholder="搜索机构" clearable />
|
|
21
|
+
<ElTree
|
|
22
|
+
ref="tree"
|
|
23
|
+
:data="[{ label: '全部机构', value: '', children: options }]"
|
|
24
|
+
node-key="value"
|
|
25
|
+
default-expand-all
|
|
26
|
+
highlight-current
|
|
27
|
+
:current-node-key="value"
|
|
28
|
+
:expand-on-click-node="false"
|
|
29
|
+
:filter-node-method="filter"
|
|
30
|
+
:props="{ label: 'label' }"
|
|
31
|
+
@node-click="(node: Option) => (value = node.value)"
|
|
32
|
+
/>
|
|
33
|
+
</details>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<style scoped>
|
|
37
|
+
.user-organizations {
|
|
38
|
+
min-width: 0;
|
|
39
|
+
border-right: 1px solid var(--el-border-color-light);
|
|
40
|
+
padding-right: 16px;
|
|
41
|
+
}
|
|
42
|
+
.user-organizations summary {
|
|
43
|
+
font-weight: 600;
|
|
44
|
+
margin-bottom: 12px;
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
}
|
|
47
|
+
.user-organizations :deep(.el-tree) {
|
|
48
|
+
margin-top: 12px;
|
|
49
|
+
max-height: 60dvh;
|
|
50
|
+
overflow: auto;
|
|
51
|
+
}
|
|
52
|
+
@media (max-width: 768px) {
|
|
53
|
+
.user-organizations {
|
|
54
|
+
border: 0;
|
|
55
|
+
padding: 0;
|
|
56
|
+
}
|
|
57
|
+
.user-organizations :deep(.el-tree) {
|
|
58
|
+
max-height: 160px;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
</style>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { SchemaFormItem, SchemaFormModel } from '@lumal/core/components';
|
|
3
|
+
import type { Option } from '../../../../shared/api';
|
|
4
|
+
import type { User } from '../types';
|
|
5
|
+
import { LumalSchemaForm } from '@lumal/core/components';
|
|
6
|
+
import { ElAlert, ElDialog, ElMessage } from 'element-plus';
|
|
7
|
+
import { computed, shallowRef, watch } from 'vue';
|
|
8
|
+
import { service } from '../../../../app/services/session';
|
|
9
|
+
import { stringArray } from '../../../../shared/api';
|
|
10
|
+
import { createUserService } from '../api';
|
|
11
|
+
|
|
12
|
+
/***********************角色分配契约*********************/
|
|
13
|
+
const props = defineProps<{ users: User[]; options: Option[]; batch: boolean }>();
|
|
14
|
+
const emit = defineEmits<{ saved: [] }>();
|
|
15
|
+
const visible = defineModel<boolean>({ required: true });
|
|
16
|
+
const api = createUserService(service);
|
|
17
|
+
const model = shallowRef<SchemaFormModel>({});
|
|
18
|
+
const saving = shallowRef(false);
|
|
19
|
+
const error = shallowRef('');
|
|
20
|
+
watch(visible, (open) => {
|
|
21
|
+
if (open) {
|
|
22
|
+
model.value = { roles: props.batch ? [] : (props.users[0]?.roles ?? []), mode: 'append' };
|
|
23
|
+
error.value = '';
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
const schemas = computed<SchemaFormItem[]>(() => [
|
|
27
|
+
...(props.batch
|
|
28
|
+
? [
|
|
29
|
+
{
|
|
30
|
+
field: 'mode',
|
|
31
|
+
label: '分配方式',
|
|
32
|
+
component: 'radio' as const,
|
|
33
|
+
options: [
|
|
34
|
+
{ label: '追加', value: 'append' },
|
|
35
|
+
{ label: '替换', value: 'replace' },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
]
|
|
39
|
+
: []),
|
|
40
|
+
{
|
|
41
|
+
field: 'roles',
|
|
42
|
+
label: '角色',
|
|
43
|
+
component: 'select',
|
|
44
|
+
componentProps: { multiple: true },
|
|
45
|
+
options: props.options,
|
|
46
|
+
required: true,
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
/***********************授权请求与失败恢复*********************/
|
|
51
|
+
async function save(values: SchemaFormModel): Promise<void> {
|
|
52
|
+
if (saving.value || !props.users.length) return;
|
|
53
|
+
saving.value = true;
|
|
54
|
+
error.value = '';
|
|
55
|
+
try {
|
|
56
|
+
const roles = stringArray(values.roles);
|
|
57
|
+
if (props.batch)
|
|
58
|
+
await api.batchRoles(
|
|
59
|
+
props.users.map((user) => user.id),
|
|
60
|
+
roles,
|
|
61
|
+
values.mode === 'replace' ? 'replace' : 'append',
|
|
62
|
+
);
|
|
63
|
+
else await api.assignRoles(props.users[0]!.id, roles);
|
|
64
|
+
visible.value = false;
|
|
65
|
+
emit('saved');
|
|
66
|
+
ElMessage.success('角色分配已保存');
|
|
67
|
+
} catch (reason) {
|
|
68
|
+
error.value = reason instanceof Error ? reason.message : '角色分配失败';
|
|
69
|
+
} finally {
|
|
70
|
+
saving.value = false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<ElDialog
|
|
77
|
+
v-model="visible"
|
|
78
|
+
:title="batch ? '批量分配角色' : '分配角色'"
|
|
79
|
+
width="min(520px, 94vw)"
|
|
80
|
+
append-to-body
|
|
81
|
+
:close-on-click-modal="!saving"
|
|
82
|
+
>
|
|
83
|
+
<ElAlert v-if="error" :title="error" type="error" :closable="false" role="alert" />
|
|
84
|
+
<LumalSchemaForm
|
|
85
|
+
v-model="model"
|
|
86
|
+
:schemas="schemas"
|
|
87
|
+
:submit-loading="saving"
|
|
88
|
+
show-actions
|
|
89
|
+
submit-text="保存角色"
|
|
90
|
+
@submit="save"
|
|
91
|
+
/>
|
|
92
|
+
</ElDialog>
|
|
93
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AppModule } from '../../../app/module';
|
|
2
|
+
|
|
3
|
+
export const menus: AppModule['menus'] = [
|
|
4
|
+
{
|
|
5
|
+
path: '/system/users',
|
|
6
|
+
name: 'SystemUsers',
|
|
7
|
+
component: 'system/users',
|
|
8
|
+
meta: { title: '用户管理', icon: 'lumal:user', authority: ['system:user:list'], keepAlive: true, order: 1 },
|
|
9
|
+
},
|
|
10
|
+
];
|
|
11
|
+
export const views: AppModule['views'] = { 'system/users': () => import('./UserView.vue') };
|
|
12
|
+
// 只声明真实存在的操作,不根据菜单前缀生成伪权限。
|
|
13
|
+
export const permissions: AppModule['permissions'] = [
|
|
14
|
+
{
|
|
15
|
+
id: 'system-user',
|
|
16
|
+
label: '用户管理',
|
|
17
|
+
children: [
|
|
18
|
+
{ id: 'system:user:list', label: '查询用户', permission: 'system:user:list' },
|
|
19
|
+
{ id: 'system:user:create', label: '新增用户', permission: 'system:user:create' },
|
|
20
|
+
{ id: 'system:user:update', label: '修改用户', permission: 'system:user:update' },
|
|
21
|
+
{ id: 'system:user:delete', label: '删除用户', permission: 'system:user:delete' },
|
|
22
|
+
{ id: 'system:user:status', label: '启用与停用', permission: 'system:user:status' },
|
|
23
|
+
{ id: 'system:user:assign-roles', label: '分配角色', permission: 'system:user:assign-roles' },
|
|
24
|
+
{ id: 'system:user:reset-password', label: '重置密码', permission: 'system:user:reset-password' },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Query, Status } from '../../../shared/api';
|
|
2
|
+
|
|
3
|
+
export interface User {
|
|
4
|
+
id: string;
|
|
5
|
+
username: string;
|
|
6
|
+
nickname: string;
|
|
7
|
+
organizationId: string;
|
|
8
|
+
roles: string[];
|
|
9
|
+
phone: string;
|
|
10
|
+
status: Status;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
}
|
|
13
|
+
export interface UserInput {
|
|
14
|
+
username: string;
|
|
15
|
+
nickname: string;
|
|
16
|
+
organizationId: string;
|
|
17
|
+
roles: string[];
|
|
18
|
+
phone: string;
|
|
19
|
+
status: Status;
|
|
20
|
+
}
|
|
21
|
+
export interface UserQuery extends Query {
|
|
22
|
+
organizationId?: string;
|
|
23
|
+
roles?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface UserBatchResult {
|
|
26
|
+
items: User[];
|
|
27
|
+
updated: number;
|
|
28
|
+
}
|