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,24 @@
|
|
|
1
|
+
// 角色结果结构(纯数据)。
|
|
2
|
+
class ListItem {
|
|
3
|
+
constructor({ id, name, code, description = '', status = 'active', sort = 0 }) {
|
|
4
|
+
this.id = id;
|
|
5
|
+
this.name = name;
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.description = description;
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.sort = sort;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class Detail {
|
|
14
|
+
constructor({ id, name, code, description = '', status = 'active', sort = 0 }) {
|
|
15
|
+
this.id = id;
|
|
16
|
+
this.name = name;
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.description = description;
|
|
19
|
+
this.status = status;
|
|
20
|
+
this.sort = sort;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default { ListItem, Detail };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { vDialog } from '@yoyaflow/yoya-ui';
|
|
2
|
+
import { 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 RoleFormDialog({ onSubmit }) {
|
|
13
|
+
const dialog = vDialog();
|
|
14
|
+
let editingId = null;
|
|
15
|
+
|
|
16
|
+
function open(role = null) {
|
|
17
|
+
editingId = role?.id ?? null;
|
|
18
|
+
dialog.content((content) => {
|
|
19
|
+
content.vForm((form) => {
|
|
20
|
+
form.styles({
|
|
21
|
+
border: '1px solid var(--yoya-color-border, #d8dee8)',
|
|
22
|
+
borderRadius: '8px',
|
|
23
|
+
display: 'grid',
|
|
24
|
+
gap: '16px',
|
|
25
|
+
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
|
|
26
|
+
padding: '16px'
|
|
27
|
+
});
|
|
28
|
+
form.vFormItem((item) => {
|
|
29
|
+
item.styles(rowStyle);
|
|
30
|
+
item.label('名称').name('name').required({ message: '请填写名称' });
|
|
31
|
+
item.control((editor) => editor.vInput({ name: 'name', value: role?.name ?? '' }));
|
|
32
|
+
});
|
|
33
|
+
form.vFormItem((item) => {
|
|
34
|
+
item.styles(rowStyle);
|
|
35
|
+
item.label('角色标识').name('code').required({ message: '请填写角色标识' });
|
|
36
|
+
item.control((editor) => editor.vInput({ name: 'code', value: role?.code ?? '' }));
|
|
37
|
+
});
|
|
38
|
+
form.vFormItem((item) => {
|
|
39
|
+
item.styles(rowStyle);
|
|
40
|
+
item.label('描述').name('description');
|
|
41
|
+
item.control((editor) =>
|
|
42
|
+
editor.vInput({ name: 'description', value: role?.description ?? '' })
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
form.vFormItem((item) => {
|
|
46
|
+
item.styles(rowStyle);
|
|
47
|
+
item.label('状态').name('status');
|
|
48
|
+
item.control((editor) =>
|
|
49
|
+
editor.vSelect({ name: 'status', options: statusOptions, value: role?.status ?? 'active' })
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
form.vFormItem((item) => {
|
|
53
|
+
item.styles(rowStyle);
|
|
54
|
+
item.label('排序').name('sort');
|
|
55
|
+
item.control((editor) =>
|
|
56
|
+
editor.vInput({ name: 'sort', value: String(role?.sort ?? 0) })
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
form.vButton('保存', (btn) => {
|
|
60
|
+
btn.variant('primary');
|
|
61
|
+
btn.style({ gridColumn: '1 / -1', justifySelf: 'end', marginTop: '16px' });
|
|
62
|
+
btn.on('click', () => save(form));
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
dialog.open(true);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function save(form) {
|
|
70
|
+
if (!form.validate()) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
await onSubmit(editingId, form.values());
|
|
74
|
+
dialog.close();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
render() {
|
|
79
|
+
return dialog;
|
|
80
|
+
},
|
|
81
|
+
open,
|
|
82
|
+
close() {
|
|
83
|
+
dialog.close();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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 RoleTable({ rows, onEdit, onRemove }) {
|
|
6
|
+
let tbody = null;
|
|
7
|
+
|
|
8
|
+
function renderRows() {
|
|
9
|
+
if (!tbody) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
tbody.children().forEach((child) => child.destroy());
|
|
13
|
+
rows().forEach((role) => {
|
|
14
|
+
tbody.vTr((row) => {
|
|
15
|
+
row.vTd(role.name);
|
|
16
|
+
row.vTd(role.code);
|
|
17
|
+
row.vTd(role.description);
|
|
18
|
+
row.vTd(statusText[role.status] ?? role.status);
|
|
19
|
+
row.vTd((cell) => {
|
|
20
|
+
cell.hstack({ gap: '8px' }, (actions) => {
|
|
21
|
+
actions.child(RowActionButton('编辑', (btn) => btn.on('click', () => onEdit(role))));
|
|
22
|
+
actions.child(
|
|
23
|
+
RowActionButton('删除', (btn) => {
|
|
24
|
+
btn.variant('danger');
|
|
25
|
+
btn.on('click', () => onRemove(role));
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
render() {
|
|
36
|
+
return vTable((table) => {
|
|
37
|
+
table.vThead((head) => {
|
|
38
|
+
head.vTr((row) => {
|
|
39
|
+
row.vTh('名称');
|
|
40
|
+
row.vTh('角色标识');
|
|
41
|
+
row.vTh('描述');
|
|
42
|
+
row.vTh('状态');
|
|
43
|
+
row.vTh('操作');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
table.vTbody((body) => {
|
|
47
|
+
tbody = body;
|
|
48
|
+
renderRows();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
refresh() {
|
|
53
|
+
renderRows();
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { vForm } from '@yoyaflow/yoya-ui';
|
|
2
|
+
import { statusOptions } from '../utils/options.js';
|
|
3
|
+
|
|
4
|
+
export function RoleToolbar({ 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,100 @@
|
|
|
1
|
+
import { toast, vDialog, vPagination, vstack } from '@yoyaflow/yoya-ui';
|
|
2
|
+
import RolesPageState from '../api/role.state.js';
|
|
3
|
+
import { RoleToolbar } from '../components/role-toolbar.js';
|
|
4
|
+
import { RoleTable } from '../components/role-table.js';
|
|
5
|
+
import { RoleFormDialog } from '../components/role-form-dialog.js';
|
|
6
|
+
|
|
7
|
+
export function RoleListPage() {
|
|
8
|
+
const state = new RolesPageState();
|
|
9
|
+
const dialog = RoleFormDialog({ onSubmit: saveRole });
|
|
10
|
+
const confirmDialog = vDialog();
|
|
11
|
+
const toolbar = RoleToolbar({ onSearch: applyFilters, onAdd: () => dialog.open(null) });
|
|
12
|
+
const table = RoleTable({
|
|
13
|
+
rows: () => state.items(),
|
|
14
|
+
onEdit: (role) => dialog.open(role),
|
|
15
|
+
onRemove: askRemove
|
|
16
|
+
});
|
|
17
|
+
const pagination = vPagination({
|
|
18
|
+
pageSize: 10,
|
|
19
|
+
pageSizes: [10, 20, 50],
|
|
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
|
+
function askRemove(role) {
|
|
50
|
+
confirmDialog.content((content) => {
|
|
51
|
+
content.p(`确定删除角色「${role.name}」?`);
|
|
52
|
+
content.hstack({ gap: '8px' }, (row) => {
|
|
53
|
+
row.spacer();
|
|
54
|
+
row.vButton('取消', (btn) => btn.on('click', () => confirmDialog.close()));
|
|
55
|
+
row.vButton('删除', (btn) => {
|
|
56
|
+
btn.variant('danger');
|
|
57
|
+
btn.on('click', async () => {
|
|
58
|
+
confirmDialog.close();
|
|
59
|
+
await state.remove(role.id);
|
|
60
|
+
toast.success(`已删除 ${role.name}`);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
confirmDialog.open(true);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function saveRole(editingId, payload) {
|
|
69
|
+
if (editingId === null) {
|
|
70
|
+
await state.add(payload);
|
|
71
|
+
toast.success('已新增角色');
|
|
72
|
+
} else {
|
|
73
|
+
await state.edit(editingId, payload);
|
|
74
|
+
toast.success('已更新角色');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
render() {
|
|
80
|
+
return vstack({ gap: '16px' }, (stack) => {
|
|
81
|
+
stack.h2('角色管理');
|
|
82
|
+
stack.vCard((card) => {
|
|
83
|
+
card.vCardHeader('角色列表');
|
|
84
|
+
card.vCardBody((body) => {
|
|
85
|
+
body.vstack({ gap: '12px' }, (content) => {
|
|
86
|
+
content.child(toolbar);
|
|
87
|
+
content.child(table);
|
|
88
|
+
content.child(pagination);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
stack.child(dialog);
|
|
93
|
+
stack.child(confirmDialog);
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
refresh() {
|
|
97
|
+
return state.load();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -1,166 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createRouter,
|
|
3
|
-
toast,
|
|
4
|
-
vBody,
|
|
5
|
-
vNavbar,
|
|
6
|
-
vRouterViews,
|
|
7
|
-
vSidebar,
|
|
8
|
-
vstack
|
|
9
|
-
} from '@yoyaflow/yoya-ui';
|
|
1
|
+
import { initYoyaTheme } from '@yoyaflow/yoya-ui';
|
|
10
2
|
import '@yoyaflow/yoya-ui/ui.css';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// ---------- 模块配置:顶级导航 → 左侧菜单 → 路由 ----------
|
|
31
|
-
|
|
32
|
-
const modules = [
|
|
33
|
-
{
|
|
34
|
-
key: 'dashboard',
|
|
35
|
-
label: '工作台',
|
|
36
|
-
icon: 'D',
|
|
37
|
-
routes: [
|
|
38
|
-
{ path: '/dashboard/overview', title: '数据概览', text: '关键指标、趋势与告警一览。' },
|
|
39
|
-
{ path: '/dashboard/todos', title: '待办审批', text: '待处理审批与任务。' }
|
|
40
|
-
]
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
key: 'ops',
|
|
44
|
-
label: '运维',
|
|
45
|
-
icon: 'O',
|
|
46
|
-
routes: [
|
|
47
|
-
{ path: '/ops/services', title: '服务清单', text: '服务列表与健康状态。' },
|
|
48
|
-
{ path: '/ops/deploys', title: '部署任务', text: '发布记录与执行状态。' }
|
|
49
|
-
]
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
key: 'system',
|
|
53
|
-
label: '系统',
|
|
54
|
-
icon: 'S',
|
|
55
|
-
routes: [
|
|
56
|
-
{ path: '/system/users', title: '成员管理', text: '成员列表与角色分配。' },
|
|
57
|
-
{ path: '/system/roles', title: '权限策略', text: '角色与权限规则。' }
|
|
58
|
-
]
|
|
59
|
-
}
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
// ---------- 路由与内容区 ----------
|
|
63
|
-
|
|
64
|
-
const appRouter = createRouter((r) => {
|
|
65
|
-
modules.forEach((module) => {
|
|
66
|
-
module.routes.forEach((route) => {
|
|
67
|
-
r.route(route.path, { title: route.title, view: () => pageView(route.title, route.text) });
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
r.notFound(() => pageView('未找到', '该页面不存在。'));
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// ---------- 左侧菜单:随顶级导航切换 ----------
|
|
74
|
-
|
|
75
|
-
let activeTopItem = null;
|
|
76
|
-
|
|
77
|
-
function renderSidebarMenu(module) {
|
|
78
|
-
sidebar.menuContent((menu) => {
|
|
79
|
-
menu.vMenuGroup((group) => {
|
|
80
|
-
group.label(module.label);
|
|
81
|
-
module.routes.forEach((route) => {
|
|
82
|
-
group.vMenuItem((item) => {
|
|
83
|
-
item.icon(module.icon);
|
|
84
|
-
item.text(route.title);
|
|
85
|
-
item.on('click', () => appRouter.navigate(route.path));
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function switchModule(module, topItem) {
|
|
93
|
-
if (activeTopItem) {
|
|
94
|
-
activeTopItem.active(false);
|
|
95
|
-
}
|
|
96
|
-
activeTopItem = topItem;
|
|
97
|
-
topItem.active(true);
|
|
98
|
-
renderSidebarMenu(module);
|
|
99
|
-
appRouter.navigate(module.routes[0].path);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const sidebar = vSidebar((side) => {
|
|
103
|
-
side.ariaLabel('左侧菜单');
|
|
104
|
-
side.title('导航');
|
|
105
|
-
});
|
|
106
|
-
renderSidebarMenu(modules[0]);
|
|
107
|
-
|
|
108
|
-
// ---------- 顶部导航:logo + 系统名 / 顶级导航 / 用户头像 ----------
|
|
109
|
-
|
|
110
|
-
const navbar = vNavbar((nav) => {
|
|
111
|
-
nav.sticky(true);
|
|
112
|
-
nav.brand((brand) => {
|
|
113
|
-
brand.hstack({ gap: '8px' }, (row) => {
|
|
114
|
-
row.style('alignItems', 'center');
|
|
115
|
-
row.span((logo) => {
|
|
116
|
-
logo.style({
|
|
117
|
-
color: 'var(--yoya-color-primary, #2563eb)',
|
|
118
|
-
fontSize: '18px',
|
|
119
|
-
fontWeight: '700'
|
|
120
|
-
});
|
|
121
|
-
logo.text('◆');
|
|
122
|
-
});
|
|
123
|
-
row.strong('yoya 管理台');
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
nav.menuContent((menu) => {
|
|
127
|
-
modules.forEach((module) => {
|
|
128
|
-
menu.vMenuItem((item) => {
|
|
129
|
-
item.text(module.label);
|
|
130
|
-
item.on('click', () => switchModule(module, item));
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
nav.actions((actions) => {
|
|
135
|
-
actions.vAvatar({ color: '#2563eb', size: 'small', text: '管' });
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// ---------- 页面壳:上方导航,下方左菜单右内容 ----------
|
|
140
|
-
|
|
141
|
-
const page = vBody((shell) => {
|
|
142
|
-
shell.gap(0);
|
|
143
|
-
shell.maxWidth('100%');
|
|
144
|
-
shell.padding(0);
|
|
145
|
-
shell.vContainer((frame) => {
|
|
146
|
-
frame.style('minHeight', '100vh');
|
|
147
|
-
frame.vHeader({ height: 56 }, (header) => {
|
|
148
|
-
header.style('padding', '0');
|
|
149
|
-
header.child(navbar);
|
|
150
|
-
});
|
|
151
|
-
frame.vContainer((body) => {
|
|
152
|
-
body.vAside({ width: 220 }, (aside) => {
|
|
153
|
-
aside.style('padding', '12px');
|
|
154
|
-
aside.child(sidebar);
|
|
155
|
-
});
|
|
156
|
-
body.vMain((main) => {
|
|
157
|
-
main.style('padding', '16px');
|
|
158
|
-
main.child(vRouterViews(appRouter, { lockTitle: true, title: '内容区' }));
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
page.bindTo('#app');
|
|
165
|
-
appRouter.start();
|
|
166
|
-
appRouter.navigate(modules[0].routes[0].path, { replace: true }); // 进入默认模块首页
|
|
3
|
+
import './api/domain.api.js';
|
|
4
|
+
import './features/members/api/member.mock.js';
|
|
5
|
+
import './features/permissions/api/permission.mock.js';
|
|
6
|
+
import './features/roles/api/role.mock.js';
|
|
7
|
+
import './features/dicts/api/dict.mock.js';
|
|
8
|
+
import { modules } from './app/modules.js';
|
|
9
|
+
import { createAppRouter } from './app/router.js';
|
|
10
|
+
import { AdminShell } from './app/admin-shell.js';
|
|
11
|
+
|
|
12
|
+
initYoyaTheme();
|
|
13
|
+
|
|
14
|
+
const router = createAppRouter();
|
|
15
|
+
const shell = AdminShell({ router, modules });
|
|
16
|
+
shell.render().bindTo('#app');
|
|
17
|
+
router.start();
|
|
18
|
+
shell.syncFromUrl();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { vAvatar, vNavbar } from '@yoyaflow/yoya-ui';
|
|
2
|
+
|
|
3
|
+
export function AppNavbar({ modules, onModuleChange }) {
|
|
4
|
+
const items = new Map();
|
|
5
|
+
let activeKey = null;
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
render() {
|
|
9
|
+
return vNavbar((nav) => {
|
|
10
|
+
nav.sticky(true);
|
|
11
|
+
nav.brand((brand) => {
|
|
12
|
+
brand.hstack({ gap: '10px' }, (row) => {
|
|
13
|
+
row.style({ alignItems: 'center', marginLeft: '16px' });
|
|
14
|
+
row.span((logo) => {
|
|
15
|
+
logo.style({
|
|
16
|
+
alignItems: 'center',
|
|
17
|
+
background: 'var(--yoya-color-primary, #2563eb)',
|
|
18
|
+
borderRadius: '8px',
|
|
19
|
+
boxSizing: 'border-box',
|
|
20
|
+
color: '#ffffff',
|
|
21
|
+
display: 'inline-flex',
|
|
22
|
+
fontSize: '16px',
|
|
23
|
+
fontWeight: '800',
|
|
24
|
+
height: '32px',
|
|
25
|
+
justifyContent: 'center',
|
|
26
|
+
lineHeight: '1',
|
|
27
|
+
width: '32px'
|
|
28
|
+
});
|
|
29
|
+
logo.text('y');
|
|
30
|
+
});
|
|
31
|
+
row.strong((name) => {
|
|
32
|
+
name.style({
|
|
33
|
+
color: 'var(--yoya-color-text-strong, #0f172a)',
|
|
34
|
+
fontSize: '16px',
|
|
35
|
+
fontWeight: '700'
|
|
36
|
+
});
|
|
37
|
+
name.text('yoya 管理台');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
nav.menuContent((menu) => {
|
|
42
|
+
modules.forEach((module) => {
|
|
43
|
+
menu.vMenuItem((item) => {
|
|
44
|
+
item.text(module.label);
|
|
45
|
+
items.set(module.key, item);
|
|
46
|
+
item.on('click', () => onModuleChange(module));
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
nav.actions((actions) => {
|
|
51
|
+
actions.vThemeModeSwitch({ persist: true });
|
|
52
|
+
const avatar = vAvatar({ color: '#2563eb', size: 'small', text: '管' });
|
|
53
|
+
avatar.style('marginRight', '16px');
|
|
54
|
+
actions.child(avatar);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
activate(key) {
|
|
59
|
+
if (activeKey && activeKey !== key) {
|
|
60
|
+
items.get(activeKey)?.active(false);
|
|
61
|
+
}
|
|
62
|
+
activeKey = key;
|
|
63
|
+
items.get(key)?.active(true);
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { vSidebar } from '@yoyaflow/yoya-ui';
|
|
2
|
+
import { SidebarMenu } from './sidebar-menu.js';
|
|
3
|
+
|
|
4
|
+
export function AppSidebar({ router }) {
|
|
5
|
+
const sidebar = vSidebar((side) => {
|
|
6
|
+
side.ariaLabel('左侧菜单');
|
|
7
|
+
side.title('导航');
|
|
8
|
+
side.collapsible(false);
|
|
9
|
+
});
|
|
10
|
+
sidebar.style({
|
|
11
|
+
flex: '0 0 auto',
|
|
12
|
+
width: '220px',
|
|
13
|
+
gridTemplateRows: 'auto minmax(0, 1fr)'
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
render() {
|
|
18
|
+
return sidebar;
|
|
19
|
+
},
|
|
20
|
+
showModule(module) {
|
|
21
|
+
sidebar.menuContent((menu) => {
|
|
22
|
+
menu.child(SidebarMenu({ module, router }));
|
|
23
|
+
});
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { toast, vstack } from '@yoyaflow/yoya-ui';
|
|
2
|
+
|
|
3
|
+
export function PlaceholderPage({ title, text }) {
|
|
4
|
+
return vstack({ gap: '16px' }, (stack) => {
|
|
5
|
+
stack.h2(title);
|
|
6
|
+
stack.vCard((card) => {
|
|
7
|
+
card.vCardHeader(title);
|
|
8
|
+
card.vCardBody((body) => {
|
|
9
|
+
body.p(text);
|
|
10
|
+
body.vButton('操作', (btn) => {
|
|
11
|
+
btn.variant('primary');
|
|
12
|
+
btn.on('click', () => toast.success(`${title} 操作成功`));
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { vButton } from '@yoyaflow/yoya-ui';
|
|
2
|
+
|
|
3
|
+
// 表格/树行内操作按钮:小尺寸、常规字重,避免行内文字显得粗重。
|
|
4
|
+
export function RowActionButton(text, setup = null) {
|
|
5
|
+
return vButton(text, (btn) => {
|
|
6
|
+
btn.size('small');
|
|
7
|
+
btn.style('fontWeight', '400');
|
|
8
|
+
if (typeof setup === 'function') {
|
|
9
|
+
setup(btn);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { vMenuGroup } from '@yoyaflow/yoya-ui';
|
|
2
|
+
|
|
3
|
+
export function SidebarMenu({ module, router }) {
|
|
4
|
+
return vMenuGroup((group) => {
|
|
5
|
+
group.label(module.label);
|
|
6
|
+
module.routes.forEach((route) => {
|
|
7
|
+
group.vMenuItem((item) => {
|
|
8
|
+
item.text(route.title);
|
|
9
|
+
item.active(route.path === router.currentPath());
|
|
10
|
+
item.on('click', () => router.navigate(route.path));
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"allowJs": true,
|
|
11
|
+
"checkJs": false,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"types": ["vite/client"]
|
|
16
|
+
},
|
|
17
|
+
"include": ["src"]
|
|
18
|
+
}
|