create-yoya-ui 0.1.2 → 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/README.md +1 -1
- package/package.json +1 -1
- package/templates/admin/README.md +54 -2
- package/templates/admin/node_modules/.vite/deps/_metadata.json +8 -0
- package/templates/admin/node_modules/.vite/deps/package.json +3 -0
- package/templates/admin/package.json +4 -2
- package/templates/admin/src/api/domain.api.d.ts +10 -0
- package/templates/admin/src/api/domain.api.js +31 -0
- package/templates/admin/src/api/fetch.api.d.ts +4 -0
- package/templates/admin/src/api/fetch.api.js +20 -0
- package/templates/admin/src/app/admin-shell.js +51 -0
- package/templates/admin/src/app/modules.js +37 -0
- package/templates/admin/src/app/router.js +20 -0
- package/templates/admin/src/features/dashboard/components/dashboard-chart.js +205 -0
- package/templates/admin/src/features/dashboard/pages/dashboard-overview-page.js +197 -0
- package/templates/admin/src/features/dicts/api/dict.mgr.d.ts +79 -0
- package/templates/admin/src/features/dicts/api/dict.mgr.js +160 -0
- package/templates/admin/src/features/dicts/api/dict.mock.js +81 -0
- package/templates/admin/src/features/dicts/api/dict.state.d.ts +23 -0
- package/templates/admin/src/features/dicts/api/dict.state.js +103 -0
- package/templates/admin/src/features/dicts/api/dict.views.d.ts +41 -0
- package/templates/admin/src/features/dicts/api/dict.views.js +23 -0
- package/templates/admin/src/features/dicts/components/dict-editor-dialog.js +218 -0
- package/templates/admin/src/features/dicts/components/dict-item-form-dialog.js +79 -0
- package/templates/admin/src/features/dicts/pages/dict-list-page.js +123 -0
- package/templates/admin/src/features/dicts/utils/options.js +5 -0
- package/templates/admin/src/features/members/api/member.mgr.d.ts +47 -0
- package/templates/admin/src/features/members/api/member.mgr.js +95 -0
- package/templates/admin/src/features/members/api/member.mock.js +74 -0
- package/templates/admin/src/features/members/api/member.req.d.ts +16 -0
- package/templates/admin/src/features/members/api/member.req.js +15 -0
- package/templates/admin/src/features/members/api/member.state.d.ts +23 -0
- package/templates/admin/src/features/members/api/member.state.js +97 -0
- package/templates/admin/src/features/members/api/member.views.d.ts +33 -0
- package/templates/admin/src/features/members/api/member.views.js +23 -0
- package/templates/admin/src/features/members/components/member-form-dialog.js +83 -0
- package/templates/admin/src/features/members/components/member-table.js +58 -0
- package/templates/admin/src/features/members/components/member-toolbar.js +58 -0
- package/templates/admin/src/features/members/pages/member-list-page.js +85 -0
- package/templates/admin/src/features/members/utils/options.d.ts +4 -0
- package/templates/admin/src/features/members/utils/options.js +6 -0
- package/templates/admin/src/features/permissions/api/permission.mgr.d.ts +45 -0
- package/templates/admin/src/features/permissions/api/permission.mgr.js +79 -0
- package/templates/admin/src/features/permissions/api/permission.mock.js +68 -0
- package/templates/admin/src/features/permissions/api/permission.state.d.ts +14 -0
- package/templates/admin/src/features/permissions/api/permission.state.js +46 -0
- package/templates/admin/src/features/permissions/api/permission.views.d.ts +27 -0
- package/templates/admin/src/features/permissions/api/permission.views.js +14 -0
- package/templates/admin/src/features/permissions/components/permission-form-dialog.js +98 -0
- package/templates/admin/src/features/permissions/pages/permission-list-page.js +98 -0
- package/templates/admin/src/features/roles/api/role.mgr.d.ts +57 -0
- package/templates/admin/src/features/roles/api/role.mgr.js +101 -0
- package/templates/admin/src/features/roles/api/role.mock.js +68 -0
- package/templates/admin/src/features/roles/api/role.state.d.ts +23 -0
- package/templates/admin/src/features/roles/api/role.state.js +97 -0
- package/templates/admin/src/features/roles/api/role.views.d.ts +43 -0
- package/templates/admin/src/features/roles/api/role.views.js +24 -0
- package/templates/admin/src/features/roles/components/role-form-dialog.js +86 -0
- package/templates/admin/src/features/roles/components/role-table.js +57 -0
- package/templates/admin/src/features/roles/components/role-toolbar.js +58 -0
- package/templates/admin/src/features/roles/pages/role-list-page.js +100 -0
- package/templates/admin/src/features/roles/utils/options.js +5 -0
- package/templates/admin/src/main.js +17 -165
- package/templates/admin/src/ui/app-navbar.js +67 -0
- package/templates/admin/src/ui/app-sidebar.js +27 -0
- package/templates/admin/src/ui/placeholder-page.js +17 -0
- package/templates/admin/src/ui/row-action-button.js +12 -0
- package/templates/admin/src/ui/sidebar-menu.js +14 -0
- package/templates/admin/tsconfig.json +18 -0
- package/templates/basic/package.json +1 -1
- package/templates/ssr/package.json +1 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// 外部请求命令:其他模块调用本域能力的入口。
|
|
2
|
+
import { RequestBase } from '@yoyaflow/yoya-ui';
|
|
3
|
+
import Members from './member.views.js';
|
|
4
|
+
|
|
5
|
+
class QueryAvailable extends RequestBase {
|
|
6
|
+
address() {
|
|
7
|
+
return '/members/available';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
toItem(row) {
|
|
11
|
+
return new Members.ListItem(row);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default { QueryAvailable };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** 页面数据状态类类型声明(对应 member.state.js)。 */
|
|
2
|
+
import Members from './member.views.js';
|
|
3
|
+
|
|
4
|
+
type MemberListItem = InstanceType<typeof Members.ListItem>;
|
|
5
|
+
|
|
6
|
+
export default class MembersPageState {
|
|
7
|
+
constructor();
|
|
8
|
+
items(): MemberListItem[];
|
|
9
|
+
keyword(): string;
|
|
10
|
+
status(): string;
|
|
11
|
+
page(): number;
|
|
12
|
+
pageSize(): number;
|
|
13
|
+
total(): number;
|
|
14
|
+
setKeyword(value: string): void;
|
|
15
|
+
setStatus(value: string): void;
|
|
16
|
+
setPage(value: number): void;
|
|
17
|
+
setPageSize(value: number): void;
|
|
18
|
+
subscribe(listener: () => void): () => void;
|
|
19
|
+
load(): Promise<unknown>;
|
|
20
|
+
add(payload: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
21
|
+
edit(id: number, patch: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
22
|
+
remove(id: number): Promise<void>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// 页面数据状态类:持有数据与筛选,动作构造命令并 submit 后写入状态、通知订阅者。
|
|
2
|
+
import MemberMgr from './member.mgr.js';
|
|
3
|
+
|
|
4
|
+
export default class MembersPageState {
|
|
5
|
+
constructor() {
|
|
6
|
+
this._items = [];
|
|
7
|
+
this._keyword = '';
|
|
8
|
+
this._status = '';
|
|
9
|
+
this._page = 1;
|
|
10
|
+
this._pageSize = 5;
|
|
11
|
+
this._total = 0;
|
|
12
|
+
this._listeners = new Set();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
items() {
|
|
16
|
+
return this._items;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
keyword() {
|
|
20
|
+
return this._keyword;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
status() {
|
|
24
|
+
return this._status;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
page() {
|
|
28
|
+
return this._page;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
pageSize() {
|
|
32
|
+
return this._pageSize;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
total() {
|
|
36
|
+
return this._total;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
setKeyword(value) {
|
|
40
|
+
this._keyword = value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setStatus(value) {
|
|
44
|
+
this._status = value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setPage(value) {
|
|
48
|
+
this._page = Math.max(1, Number(value) || 1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setPageSize(value) {
|
|
52
|
+
this._pageSize = Math.max(1, Number(value) || 5);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
subscribe(listener) {
|
|
56
|
+
this._listeners.add(listener);
|
|
57
|
+
return () => this._listeners.delete(listener);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async load() {
|
|
61
|
+
const result = await new MemberMgr.Query({
|
|
62
|
+
page: this._page,
|
|
63
|
+
pageSize: this._pageSize,
|
|
64
|
+
keyword: this._keyword,
|
|
65
|
+
status: this._status
|
|
66
|
+
}).submit();
|
|
67
|
+
this._items = result.data;
|
|
68
|
+
this._total = result.total;
|
|
69
|
+
this._emit();
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async add(payload) {
|
|
74
|
+
await new MemberMgr.Create(payload).submit();
|
|
75
|
+
this._page = 1;
|
|
76
|
+
await this.load();
|
|
77
|
+
return payload;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async edit(id, patch) {
|
|
81
|
+
await new MemberMgr.Update(id, patch).submit();
|
|
82
|
+
await this.load();
|
|
83
|
+
return patch;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async remove(id) {
|
|
87
|
+
await new MemberMgr.Remove(id).submit();
|
|
88
|
+
if (this._items.length === 1 && this._page > 1) {
|
|
89
|
+
this._page -= 1;
|
|
90
|
+
}
|
|
91
|
+
await this.load();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_emit() {
|
|
95
|
+
this._listeners.forEach((listener) => listener());
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** 领域结果结构类型声明(对应 member.views.js)。 */
|
|
2
|
+
export class ListItem {
|
|
3
|
+
id: number;
|
|
4
|
+
name: string;
|
|
5
|
+
email: string;
|
|
6
|
+
role: string;
|
|
7
|
+
status: string;
|
|
8
|
+
constructor(init: { id: number; name: string; email: string; role: string; status: string });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class Detail {
|
|
12
|
+
id: number;
|
|
13
|
+
name: string;
|
|
14
|
+
email: string;
|
|
15
|
+
role: string;
|
|
16
|
+
status: string;
|
|
17
|
+
remark: string;
|
|
18
|
+
constructor(init: {
|
|
19
|
+
id: number;
|
|
20
|
+
name: string;
|
|
21
|
+
email: string;
|
|
22
|
+
role: string;
|
|
23
|
+
status: string;
|
|
24
|
+
remark?: string;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
declare const Members: {
|
|
29
|
+
ListItem: typeof ListItem;
|
|
30
|
+
Detail: typeof Detail;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default Members;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// 领域结果结构(纯数据):类不单独 export,统一由默认导出命名空间提供。
|
|
2
|
+
class ListItem {
|
|
3
|
+
constructor({ id, name, email, role, status }) {
|
|
4
|
+
this.id = id;
|
|
5
|
+
this.name = name;
|
|
6
|
+
this.email = email;
|
|
7
|
+
this.role = role;
|
|
8
|
+
this.status = status;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class Detail {
|
|
13
|
+
constructor({ id, name, email, role, status, remark = '' }) {
|
|
14
|
+
this.id = id;
|
|
15
|
+
this.name = name;
|
|
16
|
+
this.email = email;
|
|
17
|
+
this.role = role;
|
|
18
|
+
this.status = status;
|
|
19
|
+
this.remark = remark;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default { ListItem, Detail };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { vDialog } from '@yoyaflow/yoya-ui';
|
|
2
|
+
import { roleOptions, statusOptions } from '../utils/options.js';
|
|
3
|
+
|
|
4
|
+
const rowStyle = {
|
|
5
|
+
alignItems: 'center',
|
|
6
|
+
display: 'grid',
|
|
7
|
+
gap: '12px',
|
|
8
|
+
gridTemplateColumns: '88px minmax(0, 1fr)',
|
|
9
|
+
minWidth: '0'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function MemberFormDialog({ onSubmit }) {
|
|
13
|
+
const dialog = vDialog();
|
|
14
|
+
let editingId = null;
|
|
15
|
+
|
|
16
|
+
function buildFormFields(form, member) {
|
|
17
|
+
form.styles({
|
|
18
|
+
border: '1px solid var(--yoya-color-border, #d8dee8)',
|
|
19
|
+
borderRadius: '8px',
|
|
20
|
+
display: 'grid',
|
|
21
|
+
gap: '16px',
|
|
22
|
+
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
|
|
23
|
+
padding: '16px'
|
|
24
|
+
});
|
|
25
|
+
form.vFormItem((item) => {
|
|
26
|
+
item.styles(rowStyle);
|
|
27
|
+
item.label('姓名').name('name').required({ message: '请填写姓名' });
|
|
28
|
+
item.control((editor) => editor.vInput({ name: 'name', value: member?.name ?? '' }));
|
|
29
|
+
});
|
|
30
|
+
form.vFormItem((item) => {
|
|
31
|
+
item.styles(rowStyle);
|
|
32
|
+
item.label('邮箱').name('email').required({ message: '请填写邮箱' });
|
|
33
|
+
item.control((editor) => editor.vInput({ name: 'email', value: member?.email ?? '' }));
|
|
34
|
+
});
|
|
35
|
+
form.vFormItem((item) => {
|
|
36
|
+
item.styles(rowStyle);
|
|
37
|
+
item.label('角色').name('role');
|
|
38
|
+
item.control((editor) =>
|
|
39
|
+
editor.vSelect({ name: 'role', options: roleOptions, value: member?.role ?? 'viewer' })
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
form.vFormItem((item) => {
|
|
43
|
+
item.styles(rowStyle);
|
|
44
|
+
item.label('状态').name('status');
|
|
45
|
+
item.control((editor) =>
|
|
46
|
+
editor.vSelect({ name: 'status', options: statusOptions, value: member?.status ?? 'active' })
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function open(member = null) {
|
|
52
|
+
editingId = member?.id ?? null;
|
|
53
|
+
dialog.content((content) => {
|
|
54
|
+
content.vForm((form) => {
|
|
55
|
+
buildFormFields(form, member);
|
|
56
|
+
form.vButton('保存', (btn) => {
|
|
57
|
+
btn.variant('primary');
|
|
58
|
+
btn.style({ gridColumn: '1 / -1', justifySelf: 'end', marginTop: '16px' });
|
|
59
|
+
btn.on('click', () => save(form));
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
dialog.open(true);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function save(form) {
|
|
67
|
+
if (!form.validate()) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
await onSubmit(editingId, form.values());
|
|
71
|
+
dialog.close();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
render() {
|
|
76
|
+
return dialog;
|
|
77
|
+
},
|
|
78
|
+
open,
|
|
79
|
+
close() {
|
|
80
|
+
dialog.close();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { vTable } from '@yoyaflow/yoya-ui';
|
|
2
|
+
import { RowActionButton } from '../../../ui/row-action-button.js';
|
|
3
|
+
import { statusText } from '../utils/options.js';
|
|
4
|
+
|
|
5
|
+
export function MemberTable({ rows, onEdit, onRemove }) {
|
|
6
|
+
let tbody = null;
|
|
7
|
+
|
|
8
|
+
function renderRows() {
|
|
9
|
+
if (!tbody) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
tbody.children().forEach((child) => child.destroy());
|
|
14
|
+
rows().forEach((member) => {
|
|
15
|
+
tbody.vTr((row) => {
|
|
16
|
+
row.vTd(member.name);
|
|
17
|
+
row.vTd(member.email);
|
|
18
|
+
row.vTd(member.role);
|
|
19
|
+
row.vTd(statusText[member.status] ?? member.status);
|
|
20
|
+
row.vTd((cell) => {
|
|
21
|
+
cell.hstack({ gap: '8px' }, (actions) => {
|
|
22
|
+
actions.child(RowActionButton('编辑', (btn) => btn.on('click', () => onEdit(member))));
|
|
23
|
+
actions.child(
|
|
24
|
+
RowActionButton('删除', (btn) => {
|
|
25
|
+
btn.variant('danger');
|
|
26
|
+
btn.on('click', () => onRemove(member));
|
|
27
|
+
})
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
render() {
|
|
37
|
+
return vTable((table) => {
|
|
38
|
+
table.vThead((head) => {
|
|
39
|
+
head.vTr((row) => {
|
|
40
|
+
row.vTh('姓名');
|
|
41
|
+
row.vTh('邮箱');
|
|
42
|
+
row.vTh('角色');
|
|
43
|
+
row.vTh('状态');
|
|
44
|
+
row.vTh('操作');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
table.vTbody((body) => {
|
|
48
|
+
tbody = body;
|
|
49
|
+
renderRows();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
refresh() {
|
|
54
|
+
renderRows();
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { vForm } from '@yoyaflow/yoya-ui';
|
|
2
|
+
import { statusOptions } from '../utils/options.js';
|
|
3
|
+
|
|
4
|
+
export function MemberToolbar({ onSearch, onAdd }) {
|
|
5
|
+
let form = null;
|
|
6
|
+
|
|
7
|
+
function values() {
|
|
8
|
+
return form ? form.values() : {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function search() {
|
|
12
|
+
onSearch(values());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function reset() {
|
|
16
|
+
if (form) {
|
|
17
|
+
form.values({ keyword: '', status: '' });
|
|
18
|
+
}
|
|
19
|
+
search();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
render() {
|
|
24
|
+
return vForm((f) => {
|
|
25
|
+
form = f;
|
|
26
|
+
f.style({ display: 'flex', flexWrap: 'wrap', gap: '12px', alignItems: 'center' });
|
|
27
|
+
f.vFormItem((item) => {
|
|
28
|
+
item.style({ display: 'flex', alignItems: 'center', gap: '8px' });
|
|
29
|
+
item.label('关键词').name('keyword');
|
|
30
|
+
item.control((editor) =>
|
|
31
|
+
editor.vInput({ name: 'keyword', placeholder: '姓名 / 邮箱' })
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
f.vFormItem((item) => {
|
|
35
|
+
item.style({ display: 'flex', alignItems: 'center', gap: '8px' });
|
|
36
|
+
item.label('状态').name('status');
|
|
37
|
+
item.control((editor) =>
|
|
38
|
+
editor.vSelect({ name: 'status', options: statusOptions, placeholder: '全部' })
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
f.vButton('查询', (btn) => {
|
|
42
|
+
btn.variant('primary');
|
|
43
|
+
btn.on('click', search);
|
|
44
|
+
});
|
|
45
|
+
f.vButton('重置', (btn) => btn.on('click', reset));
|
|
46
|
+
f.vButton('新增成员', (btn) => {
|
|
47
|
+
btn.variant('primary');
|
|
48
|
+
btn.style('marginLeft', 'auto');
|
|
49
|
+
btn.on('click', onAdd);
|
|
50
|
+
});
|
|
51
|
+
f.on('submit', (event) => {
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
search();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { toast, vPagination, vstack } from '@yoyaflow/yoya-ui';
|
|
2
|
+
import MembersPageState from '../api/member.state.js';
|
|
3
|
+
import { MemberToolbar } from '../components/member-toolbar.js';
|
|
4
|
+
import { MemberTable } from '../components/member-table.js';
|
|
5
|
+
import { MemberFormDialog } from '../components/member-form-dialog.js';
|
|
6
|
+
|
|
7
|
+
// 页面只做编排:组合工具栏、表格、弹窗与分页,逻辑在组件 / service / state 中
|
|
8
|
+
export function MemberListPage() {
|
|
9
|
+
const state = new MembersPageState();
|
|
10
|
+
const dialog = MemberFormDialog({ onSubmit: saveMember });
|
|
11
|
+
const toolbar = MemberToolbar({ onSearch: applyFilters, onAdd: () => dialog.open(null) });
|
|
12
|
+
const table = MemberTable({
|
|
13
|
+
rows: () => state.items(),
|
|
14
|
+
onEdit: (member) => dialog.open(member),
|
|
15
|
+
onRemove: removeMemberAction
|
|
16
|
+
});
|
|
17
|
+
const pagination = vPagination({
|
|
18
|
+
pageSize: 5,
|
|
19
|
+
pageSizes: [5, 10, 20],
|
|
20
|
+
onChange({ page: nextPage, pageSize: nextSize }) {
|
|
21
|
+
state.setPage(nextPage);
|
|
22
|
+
state.setPageSize(nextSize);
|
|
23
|
+
state.load();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
state.subscribe(() => {
|
|
28
|
+
table.refresh();
|
|
29
|
+
syncPagination();
|
|
30
|
+
});
|
|
31
|
+
state.load();
|
|
32
|
+
|
|
33
|
+
function syncPagination() {
|
|
34
|
+
pagination.update({
|
|
35
|
+
page: state.page(),
|
|
36
|
+
pageSize: state.pageSize(),
|
|
37
|
+
total: state.total(),
|
|
38
|
+
totalPages: Math.max(1, Math.ceil(state.total() / state.pageSize()))
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function applyFilters(values) {
|
|
43
|
+
state.setKeyword(values.keyword ?? '');
|
|
44
|
+
state.setStatus(values.status ?? '');
|
|
45
|
+
state.setPage(1);
|
|
46
|
+
state.load();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function removeMemberAction(member) {
|
|
50
|
+
await state.remove(member.id);
|
|
51
|
+
toast.success(`已删除 ${member.name}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function saveMember(editingId, payload) {
|
|
55
|
+
if (editingId === null) {
|
|
56
|
+
await state.add(payload);
|
|
57
|
+
toast.success('已创建成员');
|
|
58
|
+
} else {
|
|
59
|
+
await state.edit(editingId, payload);
|
|
60
|
+
toast.success('已更新成员');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
render() {
|
|
66
|
+
return vstack({ gap: '16px' }, (stack) => {
|
|
67
|
+
stack.h2('成员管理');
|
|
68
|
+
stack.vCard((card) => {
|
|
69
|
+
card.vCardHeader('成员列表');
|
|
70
|
+
card.vCardBody((body) => {
|
|
71
|
+
body.vstack({ gap: '12px' }, (content) => {
|
|
72
|
+
content.child(toolbar);
|
|
73
|
+
content.child(table);
|
|
74
|
+
content.child(pagination);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
stack.child(dialog);
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
refresh() {
|
|
82
|
+
return state.load();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** 权限管理请求命令类型声明(对应 permission.mgr.js)。 */
|
|
2
|
+
import { RequestBase } from '@yoyaflow/yoya-ui';
|
|
3
|
+
import Permissions, { PermissionType } from './permission.views.js';
|
|
4
|
+
|
|
5
|
+
type PermissionNodeInstance = InstanceType<typeof Permissions.PermissionNode>;
|
|
6
|
+
|
|
7
|
+
declare class QueryTree extends RequestBase {
|
|
8
|
+
address(): string;
|
|
9
|
+
toItem(node: Record<string, unknown>): PermissionNodeInstance;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
declare class Create extends RequestBase {
|
|
13
|
+
constructor(
|
|
14
|
+
parentId: number | null,
|
|
15
|
+
init?: { name?: string; code?: string; type?: PermissionType; sort?: number }
|
|
16
|
+
);
|
|
17
|
+
method(): 'POST';
|
|
18
|
+
address(): string;
|
|
19
|
+
body(): { parentId: number | null; name: string; code: string; type: PermissionType; sort: number };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare class Update extends RequestBase {
|
|
23
|
+
constructor(
|
|
24
|
+
id: number,
|
|
25
|
+
init?: { name?: string; code?: string; type?: PermissionType; sort?: number }
|
|
26
|
+
);
|
|
27
|
+
method(): 'PUT';
|
|
28
|
+
address(): string;
|
|
29
|
+
body(): { name: string; code: string; type: PermissionType; sort: number };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
declare class Remove extends RequestBase {
|
|
33
|
+
constructor(id: number);
|
|
34
|
+
method(): 'DELETE';
|
|
35
|
+
address(): string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
declare const PermissionMgr: {
|
|
39
|
+
QueryTree: typeof QueryTree;
|
|
40
|
+
Create: typeof Create;
|
|
41
|
+
Update: typeof Update;
|
|
42
|
+
Remove: typeof Remove;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default PermissionMgr;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// 权限管理请求命令:查询树、新增、编辑、删除。
|
|
2
|
+
import { RequestBase } from '@yoyaflow/yoya-ui';
|
|
3
|
+
import Permissions from './permission.views.js';
|
|
4
|
+
|
|
5
|
+
class QueryTree extends RequestBase {
|
|
6
|
+
address() {
|
|
7
|
+
return '/permissions/tree';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
toItem(node) {
|
|
11
|
+
return new Permissions.PermissionNode({
|
|
12
|
+
...node,
|
|
13
|
+
children: (node.children ?? []).map((child) => this.toItem(child))
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class Create extends RequestBase {
|
|
19
|
+
constructor(parentId, { name = '', code = '', type = 'menu', sort = 0 } = {}) {
|
|
20
|
+
super();
|
|
21
|
+
this.parentId = parentId ?? null;
|
|
22
|
+
this.name = name;
|
|
23
|
+
this.code = code;
|
|
24
|
+
this.type = type;
|
|
25
|
+
this.sort = sort;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
method() {
|
|
29
|
+
return 'POST';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
address() {
|
|
33
|
+
return '/permissions';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
body() {
|
|
37
|
+
return { parentId: this.parentId, name: this.name, code: this.code, type: this.type, sort: this.sort };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class Update extends RequestBase {
|
|
42
|
+
constructor(id, { name = '', code = '', type = 'menu', sort = 0 } = {}) {
|
|
43
|
+
super();
|
|
44
|
+
this.id = id;
|
|
45
|
+
this.name = name;
|
|
46
|
+
this.code = code;
|
|
47
|
+
this.type = type;
|
|
48
|
+
this.sort = sort;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
method() {
|
|
52
|
+
return 'PUT';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
address() {
|
|
56
|
+
return `/permissions/${this.id}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
body() {
|
|
60
|
+
return { name: this.name, code: this.code, type: this.type, sort: this.sort };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class Remove extends RequestBase {
|
|
65
|
+
constructor(id) {
|
|
66
|
+
super();
|
|
67
|
+
this.id = id;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
method() {
|
|
71
|
+
return 'DELETE';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
address() {
|
|
75
|
+
return `/permissions/${this.id}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default { QueryTree, Create, Update, Remove };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// 权限演示用内存 mock(接入真实后端后删除本文件)。
|
|
2
|
+
import { mockRequest } from '../../../api/domain.api.js';
|
|
3
|
+
|
|
4
|
+
let permissions = [
|
|
5
|
+
{ id: 1, parentId: null, name: '工作台', code: 'dashboard', type: 'menu', sort: 1 },
|
|
6
|
+
{ id: 11, parentId: 1, name: '数据概览', code: 'dashboard:overview', type: 'menu', sort: 1 },
|
|
7
|
+
{ id: 12, parentId: 1, name: '待办审批', code: 'dashboard:todos', type: 'menu', sort: 2 },
|
|
8
|
+
{ id: 2, parentId: null, name: '系统管理', code: 'system', type: 'menu', sort: 2 },
|
|
9
|
+
{ id: 21, parentId: 2, name: '成员管理', code: 'system:member', type: 'menu', sort: 1 },
|
|
10
|
+
{ id: 211, parentId: 21, name: '新增成员', code: 'system:member:create', type: 'button', sort: 1 },
|
|
11
|
+
{ id: 212, parentId: 21, name: '编辑成员', code: 'system:member:update', type: 'button', sort: 2 },
|
|
12
|
+
{ id: 213, parentId: 21, name: '删除成员', code: 'system:member:remove', type: 'button', sort: 3 },
|
|
13
|
+
{ id: 22, parentId: 2, name: '角色管理', code: 'system:role', type: 'menu', sort: 2 },
|
|
14
|
+
{ id: 23, parentId: 2, name: '权限管理', code: 'system:permission', type: 'menu', sort: 3 },
|
|
15
|
+
{ id: 24, parentId: 2, name: '字典管理', code: 'system:dict', type: 'menu', sort: 4 }
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const delay = (ms = 200) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
|
+
|
|
20
|
+
function buildTree(parentId = null) {
|
|
21
|
+
return permissions
|
|
22
|
+
.filter((permission) => (permission.parentId ?? null) === parentId)
|
|
23
|
+
.sort((a, b) => a.sort - b.sort)
|
|
24
|
+
.map((permission) => ({ ...permission, children: buildTree(permission.id) }));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function collectIds(id) {
|
|
28
|
+
const ids = new Set([id]);
|
|
29
|
+
let changed = true;
|
|
30
|
+
while (changed) {
|
|
31
|
+
changed = false;
|
|
32
|
+
permissions.forEach((permission) => {
|
|
33
|
+
if (ids.has(permission.parentId) && !ids.has(permission.id)) {
|
|
34
|
+
ids.add(permission.id);
|
|
35
|
+
changed = true;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return ids;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
mockRequest('GET', '/permissions/tree', async () => {
|
|
43
|
+
await delay();
|
|
44
|
+
return { ok: true, code: '0', data: buildTree(null) };
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
mockRequest('POST', '/permissions', async (req) => {
|
|
48
|
+
await delay();
|
|
49
|
+
const permission = { id: Math.max(0, ...permissions.map((item) => item.id)) + 1, ...req.body() };
|
|
50
|
+
permissions.push(permission);
|
|
51
|
+
return { ok: true, code: '0', data: permission };
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
mockRequest('PUT', /^\/permissions\/\d+$/, async (req) => {
|
|
55
|
+
await delay();
|
|
56
|
+
const id = Number(req.address().split('/').pop());
|
|
57
|
+
const permission = { ...permissions.find((item) => item.id === id), ...req.body(), id };
|
|
58
|
+
permissions = permissions.map((item) => (item.id === id ? permission : item));
|
|
59
|
+
return { ok: true, code: '0', data: permission };
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
mockRequest('DELETE', /^\/permissions\/\d+$/, async (req) => {
|
|
63
|
+
await delay();
|
|
64
|
+
const id = Number(req.address().split('/').pop());
|
|
65
|
+
const ids = collectIds(id);
|
|
66
|
+
permissions = permissions.filter((item) => !ids.has(item.id));
|
|
67
|
+
return { ok: true, code: '0', data: null };
|
|
68
|
+
});
|