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.
Files changed (71) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/templates/admin/README.md +54 -2
  4. package/templates/admin/node_modules/.vite/deps/_metadata.json +8 -0
  5. package/templates/admin/node_modules/.vite/deps/package.json +3 -0
  6. package/templates/admin/package.json +4 -2
  7. package/templates/admin/src/api/domain.api.d.ts +10 -0
  8. package/templates/admin/src/api/domain.api.js +31 -0
  9. package/templates/admin/src/api/fetch.api.d.ts +4 -0
  10. package/templates/admin/src/api/fetch.api.js +20 -0
  11. package/templates/admin/src/app/admin-shell.js +51 -0
  12. package/templates/admin/src/app/modules.js +37 -0
  13. package/templates/admin/src/app/router.js +20 -0
  14. package/templates/admin/src/features/dashboard/components/dashboard-chart.js +205 -0
  15. package/templates/admin/src/features/dashboard/pages/dashboard-overview-page.js +197 -0
  16. package/templates/admin/src/features/dicts/api/dict.mgr.d.ts +79 -0
  17. package/templates/admin/src/features/dicts/api/dict.mgr.js +160 -0
  18. package/templates/admin/src/features/dicts/api/dict.mock.js +81 -0
  19. package/templates/admin/src/features/dicts/api/dict.state.d.ts +23 -0
  20. package/templates/admin/src/features/dicts/api/dict.state.js +103 -0
  21. package/templates/admin/src/features/dicts/api/dict.views.d.ts +41 -0
  22. package/templates/admin/src/features/dicts/api/dict.views.js +23 -0
  23. package/templates/admin/src/features/dicts/components/dict-editor-dialog.js +218 -0
  24. package/templates/admin/src/features/dicts/components/dict-item-form-dialog.js +79 -0
  25. package/templates/admin/src/features/dicts/pages/dict-list-page.js +123 -0
  26. package/templates/admin/src/features/dicts/utils/options.js +5 -0
  27. package/templates/admin/src/features/members/api/member.mgr.d.ts +47 -0
  28. package/templates/admin/src/features/members/api/member.mgr.js +95 -0
  29. package/templates/admin/src/features/members/api/member.mock.js +74 -0
  30. package/templates/admin/src/features/members/api/member.req.d.ts +16 -0
  31. package/templates/admin/src/features/members/api/member.req.js +15 -0
  32. package/templates/admin/src/features/members/api/member.state.d.ts +23 -0
  33. package/templates/admin/src/features/members/api/member.state.js +97 -0
  34. package/templates/admin/src/features/members/api/member.views.d.ts +33 -0
  35. package/templates/admin/src/features/members/api/member.views.js +23 -0
  36. package/templates/admin/src/features/members/components/member-form-dialog.js +83 -0
  37. package/templates/admin/src/features/members/components/member-table.js +58 -0
  38. package/templates/admin/src/features/members/components/member-toolbar.js +58 -0
  39. package/templates/admin/src/features/members/pages/member-list-page.js +85 -0
  40. package/templates/admin/src/features/members/utils/options.d.ts +4 -0
  41. package/templates/admin/src/features/members/utils/options.js +6 -0
  42. package/templates/admin/src/features/permissions/api/permission.mgr.d.ts +45 -0
  43. package/templates/admin/src/features/permissions/api/permission.mgr.js +79 -0
  44. package/templates/admin/src/features/permissions/api/permission.mock.js +68 -0
  45. package/templates/admin/src/features/permissions/api/permission.state.d.ts +14 -0
  46. package/templates/admin/src/features/permissions/api/permission.state.js +46 -0
  47. package/templates/admin/src/features/permissions/api/permission.views.d.ts +27 -0
  48. package/templates/admin/src/features/permissions/api/permission.views.js +14 -0
  49. package/templates/admin/src/features/permissions/components/permission-form-dialog.js +98 -0
  50. package/templates/admin/src/features/permissions/pages/permission-list-page.js +98 -0
  51. package/templates/admin/src/features/roles/api/role.mgr.d.ts +57 -0
  52. package/templates/admin/src/features/roles/api/role.mgr.js +101 -0
  53. package/templates/admin/src/features/roles/api/role.mock.js +68 -0
  54. package/templates/admin/src/features/roles/api/role.state.d.ts +23 -0
  55. package/templates/admin/src/features/roles/api/role.state.js +97 -0
  56. package/templates/admin/src/features/roles/api/role.views.d.ts +43 -0
  57. package/templates/admin/src/features/roles/api/role.views.js +24 -0
  58. package/templates/admin/src/features/roles/components/role-form-dialog.js +86 -0
  59. package/templates/admin/src/features/roles/components/role-table.js +57 -0
  60. package/templates/admin/src/features/roles/components/role-toolbar.js +58 -0
  61. package/templates/admin/src/features/roles/pages/role-list-page.js +100 -0
  62. package/templates/admin/src/features/roles/utils/options.js +5 -0
  63. package/templates/admin/src/main.js +17 -165
  64. package/templates/admin/src/ui/app-navbar.js +67 -0
  65. package/templates/admin/src/ui/app-sidebar.js +27 -0
  66. package/templates/admin/src/ui/placeholder-page.js +17 -0
  67. package/templates/admin/src/ui/row-action-button.js +12 -0
  68. package/templates/admin/src/ui/sidebar-menu.js +14 -0
  69. package/templates/admin/tsconfig.json +18 -0
  70. package/templates/basic/package.json +1 -1
  71. 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
+ }
@@ -0,0 +1,5 @@
1
+ export const statusOptions = [
2
+ ['active', '启用'],
3
+ ['disabled', '禁用']
4
+ ];
5
+ export const statusText = { active: '启用', disabled: '禁用' };
@@ -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
- function pageView(title, text) {
15
- return vstack({ gap: '16px' }, (stack) => {
16
- stack.h2(title);
17
- stack.vCard((card) => {
18
- card.vCardHeader(title);
19
- card.vCardBody((body) => {
20
- body.p(text);
21
- body.vButton('操作', (btn) => {
22
- btn.variant('primary');
23
- btn.on('click', () => toast.success(`${title} 操作成功`));
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
+ }
@@ -9,7 +9,7 @@
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "dependencies": {
12
- "@yoyaflow/yoya-ui": "^0.2.0"
12
+ "@yoyaflow/yoya-ui": "^0.3.0"
13
13
  },
14
14
  "devDependencies": {
15
15
  "vite": "^8.2.0"