@yss123/cage-admin 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +49 -2
- package/dist/bootstrap.d.ts +17 -0
- package/dist/bootstrap.js +43 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/catalog.d.ts +43 -0
- package/dist/catalog.js +91 -0
- package/dist/catalog.js.map +1 -0
- package/dist/domain/constraints.d.ts +9 -0
- package/dist/domain/constraints.js +61 -0
- package/dist/domain/constraints.js.map +1 -0
- package/dist/domain/index.d.ts +1 -0
- package/dist/domain/index.js +5 -0
- package/dist/domain/index.js.map +1 -0
- package/dist/domain/org.d.ts +387 -0
- package/dist/domain/org.js +238 -0
- package/dist/domain/org.js.map +1 -0
- package/dist/http.d.ts +4 -0
- package/dist/http.js +150 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -3
- package/ui/dist/app/shell.js +110 -0
- package/ui/dist/app.js +24 -0
- package/ui/dist/modules/audit/audit-page.js +95 -0
- package/ui/dist/modules/audit/index.js +1 -0
- package/ui/dist/modules/auth/index.js +1 -0
- package/ui/dist/modules/auth/login-page.js +30 -0
- package/ui/dist/modules/operations/index.js +2 -0
- package/ui/dist/modules/operations/operations-page.js +95 -0
- package/ui/dist/modules/operations/sync-page.js +48 -0
- package/ui/dist/modules/org/index.js +1 -0
- package/ui/dist/modules/org/org-page.js +213 -0
- package/ui/dist/modules/roles/index.js +1 -0
- package/ui/dist/modules/roles/roles-page.js +308 -0
- package/ui/dist/modules/tasks/index.js +1 -0
- package/ui/dist/modules/tasks/tasks-page.js +60 -0
- package/ui/dist/shared/api.js +84 -0
- package/ui/dist/shared/dom.js +103 -0
- package/ui/dist/shared/labels.js +55 -0
- package/ui/dist/shared/navigation.js +7 -0
- package/ui/dist/shared/types.js +1 -0
- package/ui/index.html +14 -0
- package/ui/style.css +1577 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { api, can, run } from '../../shared/api.js';
|
|
2
|
+
import { badge, field, formatTime, h, icon, modal, panel, table } from '../../shared/dom.js';
|
|
3
|
+
import { capabilityName, eventName, jobName, statusName } from '../../shared/labels.js';
|
|
4
|
+
import { navigate } from '../../shared/navigation.js';
|
|
5
|
+
export async function renderOperations(main) {
|
|
6
|
+
const [summary, dead, jobs] = await Promise.all([
|
|
7
|
+
api('GET', '/api/sys/ops/summary'),
|
|
8
|
+
api('GET', '/api/sys/ops/dead-letters'),
|
|
9
|
+
api('GET', '/api/sys/ops/jobs'),
|
|
10
|
+
]);
|
|
11
|
+
const manageable = can('Sys.ops.manage');
|
|
12
|
+
main.append(h('div', { class: 'stats-row' }, stat('待投递事件', summary.outbox.reduce((sum, item) => sum + item.n, 0), 'inbox'), stat('待处理死信', summary.deadLetters, 'info', summary.deadLetters > 0), stat('运行中定时任务', jobs.filter((job) => job.enabled).length, 'clock')));
|
|
13
|
+
main.append(panel('事件投递队列', '事件由后台 worker 按顺序投递,失败后自动退避重试。', table([
|
|
14
|
+
[
|
|
15
|
+
'处理状态',
|
|
16
|
+
(item) => badge(statusName(item.status), item.status === 'pending' ? 'amber' : 'neutral'),
|
|
17
|
+
],
|
|
18
|
+
['事件数量', (item) => h('strong', {}, item.n)],
|
|
19
|
+
['最早进入时间', (item) => formatTime(item.oldest)],
|
|
20
|
+
], summary.outbox, '投递队列已清空,没有积压事件')));
|
|
21
|
+
main.append(panel('需要处理的死信', '请先排查失败原因再重放。同一单据的后续事件会等待这条事件处理完成。', table([
|
|
22
|
+
[
|
|
23
|
+
'业务事件',
|
|
24
|
+
(item) => h('div', { class: 'stack-cell' }, h('strong', {}, eventName(item.event)), h('code', {}, item.event)),
|
|
25
|
+
],
|
|
26
|
+
['关联单据', (item) => h('code', { title: item.aggregate_id }, item.aggregate_id.slice(0, 12))],
|
|
27
|
+
['尝试次数', (item) => badge(`${item.attempts} 次`, 'amber')],
|
|
28
|
+
[
|
|
29
|
+
'失败原因',
|
|
30
|
+
(item) => h('details', {}, h('summary', { class: 'failure-summary' }, (item.error || '无错误说明').slice(0, 65)), h('pre', {}, item.error || '无错误说明')),
|
|
31
|
+
],
|
|
32
|
+
['进入时间', (item) => formatTime(item.created_at)],
|
|
33
|
+
[
|
|
34
|
+
'处理',
|
|
35
|
+
(item) => manageable
|
|
36
|
+
? h('div', { class: 'row-actions' }, h('button', {
|
|
37
|
+
class: 'small primary',
|
|
38
|
+
onclick: async (event) => {
|
|
39
|
+
if (await run(() => api('POST', `/api/sys/ops/dead-letters/${item.id}/replay`), '事件已重新进入投递队列', event.currentTarget))
|
|
40
|
+
navigate('ops');
|
|
41
|
+
},
|
|
42
|
+
}, '重新投递'), h('button', { class: 'small danger', onclick: () => discard(item) }, '丢弃'))
|
|
43
|
+
: h('span', { class: 'muted' }, '仅查看'),
|
|
44
|
+
],
|
|
45
|
+
], dead, '当前没有待处理的死信')));
|
|
46
|
+
main.append(panel('定时任务', '按业务声明的时间执行;停用后不会继续触发新的调度。', table([
|
|
47
|
+
[
|
|
48
|
+
'任务名称',
|
|
49
|
+
(job) => h('div', { class: 'stack-cell' }, h('strong', {}, jobName(job.name)), h('code', {}, job.name)),
|
|
50
|
+
],
|
|
51
|
+
['执行计划', (job) => h('code', {}, job.cron)],
|
|
52
|
+
[
|
|
53
|
+
'业务操作',
|
|
54
|
+
(job) => h('div', { class: 'tag-list' }, job.calls.map((name) => badge(capabilityName(name)))),
|
|
55
|
+
],
|
|
56
|
+
[
|
|
57
|
+
'上次运行',
|
|
58
|
+
(job) => h('div', { class: 'stack-cell' }, formatTime(job.lastRunAt), job.lastStatus &&
|
|
59
|
+
badge(statusName(job.lastStatus), job.lastStatus === 'ok' ? 'green' : 'amber')),
|
|
60
|
+
],
|
|
61
|
+
[
|
|
62
|
+
'下次计划',
|
|
63
|
+
(job) => (job.enabled ? formatTime(job.nextRunAt) : h('span', { class: 'muted' }, '已暂停调度')),
|
|
64
|
+
],
|
|
65
|
+
[
|
|
66
|
+
'状态与操作',
|
|
67
|
+
(job) => h('div', { class: 'row-actions' }, badge(job.enabled ? '运行中' : '已停用', job.enabled ? 'green' : 'neutral'), manageable &&
|
|
68
|
+
h('button', {
|
|
69
|
+
class: 'small',
|
|
70
|
+
onclick: async (event) => {
|
|
71
|
+
if (await run(() => api('POST', `/api/sys/ops/jobs/${encodeURIComponent(job.name)}/${job.enabled ? 'disable' : 'enable'}`), '定时任务状态已更新', event.currentTarget))
|
|
72
|
+
navigate('ops');
|
|
73
|
+
},
|
|
74
|
+
}, job.enabled ? '停用' : '启用')),
|
|
75
|
+
],
|
|
76
|
+
], jobs, '当前业务没有声明定时任务')));
|
|
77
|
+
function discard(item) {
|
|
78
|
+
const confirm = h('input', { type: 'checkbox', required: true });
|
|
79
|
+
const save = h('button', { class: 'danger', type: 'submit' }, '确认丢弃事件');
|
|
80
|
+
const form = h('form', {
|
|
81
|
+
class: 'stack-form',
|
|
82
|
+
onsubmit: async (event) => {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
if (await run(() => api('POST', `/api/sys/ops/dead-letters/${item.id}/discard`), '事件已丢弃', save)) {
|
|
85
|
+
dialog.close();
|
|
86
|
+
navigate('ops');
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
}, h('p', {}, '丢弃后,这条事件将不再重试。请确认业务补偿或人工处理已经完成。'), field('我已了解此操作会停止该事件的后续处理', confirm), h('div', { class: 'form-actions' }, save));
|
|
90
|
+
const dialog = modal('丢弃死信事件', form);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function stat(label, value, glyph, warning = false) {
|
|
94
|
+
return h('div', { class: `stat-card ${warning ? 'attention' : ''}` }, h('span', { class: 'stat-icon' }, icon(glyph)), h('div', {}, h('span', {}, label), h('strong', {}, value)));
|
|
95
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { api, run, state } from '../../shared/api.js';
|
|
2
|
+
import { badge, field, formatTime, h, panel, table } from '../../shared/dom.js';
|
|
3
|
+
export async function renderSync(main) {
|
|
4
|
+
const connector = h('input', { placeholder: '连接器标识,留空查看全部' });
|
|
5
|
+
const status = h('select', {}, h('option', { value: '' }, '全部结果'), h('option', { value: 'ok' }, '同步成功'), h('option', { value: 'failed' }, '同步失败'));
|
|
6
|
+
const query = h('button', { class: 'primary', type: 'submit' }, '查询记录');
|
|
7
|
+
const out = h('div');
|
|
8
|
+
main.append(panel('外部系统同步记录', '查看出站请求的结果、重试次数与失败原因,辅助第三方联调与排障。', h('form', {
|
|
9
|
+
class: 'sync-filters',
|
|
10
|
+
onsubmit: (event) => {
|
|
11
|
+
event.preventDefault();
|
|
12
|
+
void load();
|
|
13
|
+
},
|
|
14
|
+
}, field('连接器', connector), field('同步结果', status), query), out));
|
|
15
|
+
async function load() {
|
|
16
|
+
const params = new URLSearchParams();
|
|
17
|
+
if (connector.value)
|
|
18
|
+
params.set('connector', connector.value);
|
|
19
|
+
if (status.value)
|
|
20
|
+
params.set('status', status.value);
|
|
21
|
+
const result = await run(() => api('GET', `/api/sys/ops/sync-records?${params}`), undefined, query);
|
|
22
|
+
if (!result)
|
|
23
|
+
return;
|
|
24
|
+
out.replaceChildren(table([
|
|
25
|
+
[
|
|
26
|
+
'连接器 / 操作',
|
|
27
|
+
(item) => h('div', { class: 'stack-cell' }, h('strong', {}, state.catalog.labels[`connector:connector:${item.connector}`] ||
|
|
28
|
+
state.catalog.labels[`connector:${item.connector}`] ||
|
|
29
|
+
'外部系统连接器'), h('code', {}, `${item.connector}.${item.operation}`)),
|
|
30
|
+
],
|
|
31
|
+
['业务单据', (item) => h('code', {}, item.ref_id || '—')],
|
|
32
|
+
[
|
|
33
|
+
'同步结果',
|
|
34
|
+
(item) => badge(item.status === 'ok' ? '同步成功' : '同步失败', item.status === 'ok' ? 'green' : 'red'),
|
|
35
|
+
],
|
|
36
|
+
['响应状态', (item) => item.http_status ?? '未收到响应'],
|
|
37
|
+
['尝试次数', (item) => `${item.attempts} 次`],
|
|
38
|
+
[
|
|
39
|
+
'错误详情',
|
|
40
|
+
(item) => item.error
|
|
41
|
+
? h('details', {}, h('summary', {}, '查看原因'), h('pre', {}, item.error))
|
|
42
|
+
: h('span', { class: 'muted' }, '—'),
|
|
43
|
+
],
|
|
44
|
+
['最近更新', (item) => formatTime(item.updated_at)],
|
|
45
|
+
], result, '暂无符合条件的同步记录'));
|
|
46
|
+
}
|
|
47
|
+
await load();
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { renderOrg } from './org-page.js';
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { api, can, run } from '../../shared/api.js';
|
|
2
|
+
import { badge, field, h, icon, modal, panel, searchInput, table } from '../../shared/dom.js';
|
|
3
|
+
import { navigate } from '../../shared/navigation.js';
|
|
4
|
+
/** 组织编辑继续走生成命令;负责人和角色是两份配置,在界面上明确区分。 */
|
|
5
|
+
export async function renderOrg(main) {
|
|
6
|
+
const [departments, people, roleResult] = await Promise.all([
|
|
7
|
+
can('Dept.read')
|
|
8
|
+
? api('GET', '/api/dept?pageSize=200&sort=createdAt')
|
|
9
|
+
: Promise.resolve({ items: [], total: 0 }),
|
|
10
|
+
can('User.read')
|
|
11
|
+
? api('GET', '/api/user?pageSize=200&status=active,disabled')
|
|
12
|
+
: Promise.resolve({ items: [], total: 0 }),
|
|
13
|
+
can('Role.read')
|
|
14
|
+
? api('GET', '/api/role?pageSize=200')
|
|
15
|
+
: Promise.resolve({ items: [] }),
|
|
16
|
+
]);
|
|
17
|
+
const depts = departments.items, users = people.items, roles = roleResult.items;
|
|
18
|
+
const departmentName = new Map(depts.map((dept) => [dept.id, dept.name]));
|
|
19
|
+
const userName = new Map(users.map((user) => [user.id, `${user.name}(${user.username})`]));
|
|
20
|
+
const roleName = new Map(roles.map((role) => [role.id, role.name]));
|
|
21
|
+
const depth = (dept) => Math.max(0, dept.ancestors.split('/').filter(Boolean).length - 1);
|
|
22
|
+
const selectDepartment = (value) => h('select', { ...(value ? { value } : {}) }, depts.map((dept) => h('option', { value: dept.id }, `${' '.repeat(depth(dept))}${dept.name}`)));
|
|
23
|
+
main.append(h('div', { class: 'stats-row' }, stat('部门', departments.total, 'grid'), stat('用户', people.total, 'users'), stat('当前列表已启用', users.filter((user) => user.status === 'active').length, 'check')));
|
|
24
|
+
const deptPanel = panel('部门与负责人', '部门负责人用于审批选人;给用户分配经理角色后,仍需在这里指定负责人。');
|
|
25
|
+
const departmentActions = h('div', { class: 'section-actions' });
|
|
26
|
+
if (can('Dept.create') && depts.length)
|
|
27
|
+
departmentActions.append(h('button', { class: 'small', onclick: createDepartment }, icon('plus'), '新建部门'));
|
|
28
|
+
deptPanel.querySelector('.panel-heading').append(departmentActions);
|
|
29
|
+
deptPanel.append(table([
|
|
30
|
+
[
|
|
31
|
+
'部门名称',
|
|
32
|
+
(dept) => h('div', { class: 'entity-cell', style: `padding-left:${depth(dept) * 20}px` }, h('span', { class: 'entity-icon' }, icon('grid')), h('strong', {}, dept.name)),
|
|
33
|
+
],
|
|
34
|
+
['部门编码', (dept) => h('code', { class: 'muted' }, dept.code || '—')],
|
|
35
|
+
[
|
|
36
|
+
'部门负责人',
|
|
37
|
+
(dept) => {
|
|
38
|
+
if (!can('Dept.update') || !can('User.read'))
|
|
39
|
+
return userName.get(dept.leaderId ?? '') || '未设置';
|
|
40
|
+
const select = h('select', {
|
|
41
|
+
value: dept.leaderId ?? '',
|
|
42
|
+
'aria-label': `${dept.name}负责人`,
|
|
43
|
+
onchange: async () => {
|
|
44
|
+
select.disabled = true;
|
|
45
|
+
const updated = await run(() => api('POST', `/api/dept/${dept.id}/update`, { leaderId: select.value || null }), '部门负责人已保存');
|
|
46
|
+
if (updated)
|
|
47
|
+
dept.leaderId = updated.leaderId;
|
|
48
|
+
else
|
|
49
|
+
select.value = dept.leaderId ?? '';
|
|
50
|
+
select.disabled = false;
|
|
51
|
+
},
|
|
52
|
+
}, h('option', { value: '' }, '请选择负责人'), users
|
|
53
|
+
.filter((user) => user.status === 'active')
|
|
54
|
+
.map((user) => h('option', { value: user.id }, userName.get(user.id))));
|
|
55
|
+
return select;
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
[
|
|
59
|
+
'状态',
|
|
60
|
+
(dept) => badge(dept.status === 'active' ? '已启用' : '已停用', dept.status === 'active' ? 'green' : 'neutral'),
|
|
61
|
+
],
|
|
62
|
+
], depts));
|
|
63
|
+
if (can('Dept.read'))
|
|
64
|
+
main.append(deptPanel);
|
|
65
|
+
if (can('User.read')) {
|
|
66
|
+
const out = h('div');
|
|
67
|
+
let query = '';
|
|
68
|
+
const toolbar = h('div', { class: 'table-toolbar' }, searchInput('搜索姓名、账号或部门', (value) => {
|
|
69
|
+
query = value.toLowerCase();
|
|
70
|
+
renderUsers();
|
|
71
|
+
}));
|
|
72
|
+
if (can('User.create'))
|
|
73
|
+
toolbar.append(h('button', { class: 'primary', disabled: !depts.length, onclick: createUser }, icon('plus'), '新建用户'));
|
|
74
|
+
const userPanel = panel('用户与角色', '通过角色分配操作权限;账号停用后将无法继续访问系统。', toolbar, out);
|
|
75
|
+
main.append(userPanel);
|
|
76
|
+
function renderUsers() {
|
|
77
|
+
const visible = users.filter((user) => `${user.name} ${user.username} ${departmentName.get(user.deptId) ?? ''}`
|
|
78
|
+
.toLowerCase()
|
|
79
|
+
.includes(query));
|
|
80
|
+
out.replaceChildren(table([
|
|
81
|
+
[
|
|
82
|
+
'用户',
|
|
83
|
+
(user) => h('div', { class: 'person-cell' }, h('span', { class: 'person-avatar' }, user.name.slice(0, 1)), h('div', {}, h('strong', {}, user.name), h('small', {}, user.username))),
|
|
84
|
+
],
|
|
85
|
+
['所属部门', (user) => departmentName.get(user.deptId) || '未获取部门名称'],
|
|
86
|
+
[
|
|
87
|
+
'拥有角色',
|
|
88
|
+
(user) => h('div', { class: 'tag-list' }, user.roleIds.length
|
|
89
|
+
? user.roleIds.map((id) => badge(roleName.get(id) || '未获取角色名称'))
|
|
90
|
+
: h('span', { class: 'muted' }, '尚未分配'), can('User.assignRoles') && can('Role.read') && user.status === 'active'
|
|
91
|
+
? h('button', { class: 'text-button', onclick: () => assignRoles(user) }, '配置')
|
|
92
|
+
: null),
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
'状态',
|
|
96
|
+
(user) => badge(user.status === 'active' ? '已启用' : '已停用', user.status === 'active' ? 'green' : 'neutral'),
|
|
97
|
+
],
|
|
98
|
+
[
|
|
99
|
+
'账号操作',
|
|
100
|
+
(user) => h('div', { class: 'row-actions' }, can('User.setPassword') && user.status === 'active'
|
|
101
|
+
? h('button', { class: 'text-button', onclick: () => resetPassword(user) }, '重置密码')
|
|
102
|
+
: null, user.status === 'active' && can('User.disable')
|
|
103
|
+
? h('button', {
|
|
104
|
+
class: 'text-button danger',
|
|
105
|
+
onclick: async (event) => {
|
|
106
|
+
if (await run(() => api('POST', `/api/user/${user.id}/disable`), '账号已停用', event.currentTarget))
|
|
107
|
+
navigate('org');
|
|
108
|
+
},
|
|
109
|
+
}, '停用')
|
|
110
|
+
: null, user.status === 'disabled' && can('User.enable')
|
|
111
|
+
? h('button', {
|
|
112
|
+
class: 'text-button',
|
|
113
|
+
onclick: async (event) => {
|
|
114
|
+
if (await run(() => api('POST', `/api/user/${user.id}/enable`), '账号已启用', event.currentTarget))
|
|
115
|
+
navigate('org');
|
|
116
|
+
},
|
|
117
|
+
}, '启用')
|
|
118
|
+
: null),
|
|
119
|
+
],
|
|
120
|
+
], visible));
|
|
121
|
+
}
|
|
122
|
+
renderUsers();
|
|
123
|
+
}
|
|
124
|
+
if (people.total > users.length || departments.total > depts.length)
|
|
125
|
+
main.append(h('p', { class: 'muted' }, '当前仅展示前 200 条可见组织记录;超出部分请通过业务组织接口查询。'));
|
|
126
|
+
function createDepartment() {
|
|
127
|
+
const name = h('input', { required: true, maxlength: 100, placeholder: '例如:采购部' });
|
|
128
|
+
const parent = selectDepartment();
|
|
129
|
+
const save = h('button', { type: 'submit', class: 'primary' }, '创建部门');
|
|
130
|
+
const form = h('form', {
|
|
131
|
+
class: 'stack-form',
|
|
132
|
+
onsubmit: async (event) => {
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
if (await run(() => api('POST', '/api/dept/create', { name: name.value, parentId: parent.value }), '部门已创建', save)) {
|
|
135
|
+
dialog.close();
|
|
136
|
+
navigate('org');
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
}, field('部门名称', name), field('上级部门', parent), h('div', { class: 'form-actions' }, save));
|
|
140
|
+
const dialog = modal('新建部门', form);
|
|
141
|
+
name.focus();
|
|
142
|
+
}
|
|
143
|
+
function createUser() {
|
|
144
|
+
const username = h('input', { required: true, placeholder: '用于登录的账号', autocomplete: 'off' });
|
|
145
|
+
const name = h('input', { required: true, placeholder: '用户真实姓名' });
|
|
146
|
+
const department = selectDepartment();
|
|
147
|
+
const save = h('button', { type: 'submit', class: 'primary' }, '创建用户');
|
|
148
|
+
const form = h('form', {
|
|
149
|
+
class: 'stack-form',
|
|
150
|
+
onsubmit: async (event) => {
|
|
151
|
+
event.preventDefault();
|
|
152
|
+
if (await run(() => api('POST', '/api/user/create', {
|
|
153
|
+
username: username.value,
|
|
154
|
+
name: name.value,
|
|
155
|
+
deptId: department.value,
|
|
156
|
+
}), '用户已创建,请设置密码并分配角色', save)) {
|
|
157
|
+
dialog.close();
|
|
158
|
+
navigate('org');
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
}, field('登录账号', username), field('用户姓名', name), field('所属部门', department), h('p', { class: 'inline-info' }, icon('info'), '新用户默认没有角色。创建后请设置登录密码,并按职责配置角色。'), h('div', { class: 'form-actions' }, save));
|
|
162
|
+
const dialog = modal('新建用户', form);
|
|
163
|
+
username.focus();
|
|
164
|
+
}
|
|
165
|
+
function assignRoles(user) {
|
|
166
|
+
const selected = new Set(user.roleIds);
|
|
167
|
+
const save = h('button', { class: 'primary', type: 'submit' }, '保存角色');
|
|
168
|
+
const form = h('form', {
|
|
169
|
+
class: 'stack-form',
|
|
170
|
+
onsubmit: async (event) => {
|
|
171
|
+
event.preventDefault();
|
|
172
|
+
if (await run(() => api('POST', `/api/user/${user.id}/assign-roles`, { roleIds: [...selected] }), '用户角色已保存', save)) {
|
|
173
|
+
dialog.close();
|
|
174
|
+
navigate('org');
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
}, h('p', { class: 'muted' }, `为 ${user.name} 选择角色,多个角色的有效权限会共同生效。`), h('div', { class: 'choice-list' }, roles
|
|
178
|
+
.filter((role) => role.status === 'active')
|
|
179
|
+
.map((role) => h('label', {}, h('input', {
|
|
180
|
+
type: 'checkbox',
|
|
181
|
+
checked: selected.has(role.id),
|
|
182
|
+
onchange: (event) => {
|
|
183
|
+
event.currentTarget.checked
|
|
184
|
+
? selected.add(role.id)
|
|
185
|
+
: selected.delete(role.id);
|
|
186
|
+
},
|
|
187
|
+
}), h('span', {}, h('strong', {}, role.name), h('small', {}, role.code))))), h('div', { class: 'form-actions' }, save));
|
|
188
|
+
const dialog = modal('配置用户角色', form);
|
|
189
|
+
}
|
|
190
|
+
function resetPassword(user) {
|
|
191
|
+
const password = h('input', {
|
|
192
|
+
type: 'password',
|
|
193
|
+
minlength: 8,
|
|
194
|
+
required: true,
|
|
195
|
+
autocomplete: 'new-password',
|
|
196
|
+
placeholder: '至少 8 位',
|
|
197
|
+
});
|
|
198
|
+
const save = h('button', { type: 'submit', class: 'primary' }, '保存新密码');
|
|
199
|
+
const form = h('form', {
|
|
200
|
+
class: 'stack-form',
|
|
201
|
+
onsubmit: async (event) => {
|
|
202
|
+
event.preventDefault();
|
|
203
|
+
if (await run(() => api('POST', `/api/user/${user.id}/set-password`, { password: password.value }), '密码已重置', save))
|
|
204
|
+
dialog.close();
|
|
205
|
+
},
|
|
206
|
+
}, h('p', { class: 'muted' }, `正在为 ${user.name}(${user.username})设置新密码。`), field('新密码', password), h('div', { class: 'form-actions' }, save));
|
|
207
|
+
const dialog = modal('重置登录密码', form);
|
|
208
|
+
password.focus();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function stat(label, value, glyph) {
|
|
212
|
+
return h('div', { class: 'stat-card' }, h('span', { class: 'stat-icon' }, icon(glyph)), h('div', {}, h('span', {}, label), h('strong', {}, value)));
|
|
213
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { renderRoles } from './roles-page.js';
|