create-yoya-ui 0.4.1 → 0.6.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/package.json +1 -1
- package/templates/admin/RULES.md +22 -9
- package/templates/admin/package.json +1 -1
- package/templates/admin/src/api/domain.api.d.ts +1 -1
- package/templates/admin/src/api/domain.api.js +1 -1
- package/templates/admin/src/api/fetch.api.d.ts +1 -1
- package/templates/admin/src/features/dashboard/overview/pages/dashboard-overview-page.js +2 -2
- package/templates/admin/src/features/system/dicts/api/dict.mgr.d.ts +1 -1
- package/templates/admin/src/features/system/dicts/api/dict.mgr.js +1 -1
- package/templates/admin/src/features/system/dicts/api/dict.state.d.ts +14 -11
- package/templates/admin/src/features/system/dicts/api/dict.state.js +35 -70
- package/templates/admin/src/features/system/dicts/api/dict.views.d.ts +36 -23
- package/templates/admin/src/features/system/dicts/api/dict.views.js +36 -14
- package/templates/admin/src/features/system/dicts/components/dict-editor-dialog.js +53 -73
- package/templates/admin/src/features/system/dicts/components/dict-item-form-dialog.js +9 -6
- package/templates/admin/src/features/system/dicts/pages/dict-list-page.js +57 -59
- package/templates/admin/src/features/system/members/api/member.mgr.d.ts +1 -1
- package/templates/admin/src/features/system/members/api/member.mgr.js +1 -1
- package/templates/admin/src/features/system/members/api/member.req.d.ts +1 -1
- package/templates/admin/src/features/system/members/api/member.req.js +1 -1
- package/templates/admin/src/features/system/members/api/member.state.d.ts +10 -8
- package/templates/admin/src/features/system/members/api/member.state.js +24 -55
- package/templates/admin/src/features/system/members/api/member.views.d.ts +19 -5
- package/templates/admin/src/features/system/members/api/member.views.js +22 -8
- package/templates/admin/src/features/system/members/components/member-form-dialog.js +10 -6
- package/templates/admin/src/features/system/members/components/member-table.js +44 -54
- package/templates/admin/src/features/system/members/pages/member-list-page.js +19 -14
- package/templates/admin/src/features/system/permissions/api/permission.mgr.d.ts +1 -1
- package/templates/admin/src/features/system/permissions/api/permission.mgr.js +1 -1
- package/templates/admin/src/features/system/permissions/api/permission.state.d.ts +5 -3
- package/templates/admin/src/features/system/permissions/api/permission.state.js +4 -18
- package/templates/admin/src/features/system/permissions/api/permission.views.js +2 -1
- package/templates/admin/src/features/system/permissions/components/permission-form-dialog.js +1 -1
- package/templates/admin/src/features/system/permissions/pages/permission-list-page.js +9 -6
- package/templates/admin/src/features/system/roles/api/role.mgr.d.ts +1 -1
- package/templates/admin/src/features/system/roles/api/role.mgr.js +1 -1
- package/templates/admin/src/features/system/roles/api/role.state.d.ts +10 -8
- package/templates/admin/src/features/system/roles/api/role.state.js +22 -55
- package/templates/admin/src/features/system/roles/api/role.views.d.ts +24 -15
- package/templates/admin/src/features/system/roles/api/role.views.js +22 -9
- package/templates/admin/src/features/system/roles/components/role-form-dialog.js +11 -7
- package/templates/admin/src/features/system/roles/components/role-table.js +35 -47
- package/templates/admin/src/features/system/roles/pages/role-list-page.js +18 -14
- package/templates/admin/src/shared/state.rows.js +17 -0
- package/templates/admin/src/shell/api/shell.req.js +1 -1
- package/templates/admin/src/shell/api/shell.state.js +23 -55
- package/templates/admin/src/shell/components/admin-shell.js +4 -15
- package/templates/admin/src/shell/components/app-navbar.js +48 -56
- package/templates/admin/src/shell/components/app-sidebar.js +20 -31
- package/templates/admin/src/shell/components/sidebar-menu.js +1 -1
- package/templates/basic/package.json +1 -1
- package/templates/ssr/README.md +1 -1
- package/templates/ssr/package.json +1 -1
- package/templates/ssr/src/server.mjs +3 -0
|
@@ -1,17 +1,30 @@
|
|
|
1
|
-
//
|
|
1
|
+
// 角色结果结构(纯数据):行内展示且会原地变化的字段用句柄,其余保持普通值。
|
|
2
|
+
import { ref } from '@yoyaflow/yoya-ui';
|
|
3
|
+
import { fieldValue } from '../../../../shared/state.rows.js';
|
|
4
|
+
|
|
2
5
|
class ListItem {
|
|
3
|
-
constructor(
|
|
4
|
-
this.id = id;
|
|
5
|
-
this.name = name;
|
|
6
|
-
this.code = code;
|
|
7
|
-
this.description = description;
|
|
8
|
-
this.status = status;
|
|
9
|
-
this.sort = sort;
|
|
6
|
+
constructor(row = {}) {
|
|
7
|
+
this.id = row.id; // key:普通值,不能是句柄
|
|
8
|
+
this.name = ref(fieldValue(row.name) ?? '');
|
|
9
|
+
this.code = ref(fieldValue(row.code) ?? '');
|
|
10
|
+
this.description = ref(fieldValue(row.description) ?? '');
|
|
11
|
+
this.status = ref(fieldValue(row.status) ?? 'active');
|
|
12
|
+
this.sort = fieldValue(row.sort) ?? 0; // 冷字段:表格不展示,随行结构变化即可
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 合并而非重建:实例身份不变,keyed 复用行节点,只刷句柄绑定的位置。 */
|
|
16
|
+
apply(row) {
|
|
17
|
+
this.name.value = fieldValue(row.name) ?? '';
|
|
18
|
+
this.code.value = fieldValue(row.code) ?? '';
|
|
19
|
+
this.description.value = fieldValue(row.description) ?? '';
|
|
20
|
+
this.status.value = fieldValue(row.status) ?? 'active';
|
|
21
|
+
this.sort = fieldValue(row.sort) ?? 0;
|
|
22
|
+
return this;
|
|
10
23
|
}
|
|
11
24
|
}
|
|
12
25
|
|
|
13
26
|
class Detail {
|
|
14
|
-
constructor({ id, name, code, description = '', status = 'active', sort = 0 }) {
|
|
27
|
+
constructor({ id, name, code, description = '', status = 'active', sort = 0 } = {}) {
|
|
15
28
|
this.id = id;
|
|
16
29
|
this.name = name;
|
|
17
30
|
this.code = code;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { vDialog } from '@yoyaflow/yoya-ui';
|
|
2
|
+
import { fieldValue } from '../../../../shared/state.rows.js';
|
|
2
3
|
import { statusOptions } from '../utils/options.js';
|
|
3
4
|
|
|
4
5
|
const rowStyle = {
|
|
@@ -15,6 +16,11 @@ export function RoleFormDialog({ onSubmit }) {
|
|
|
15
16
|
|
|
16
17
|
function open(role = null) {
|
|
17
18
|
editingId = role?.id ?? null;
|
|
19
|
+
const name = fieldValue(role?.name) ?? '';
|
|
20
|
+
const code = fieldValue(role?.code) ?? '';
|
|
21
|
+
const description = fieldValue(role?.description) ?? '';
|
|
22
|
+
const status = fieldValue(role?.status) ?? 'active';
|
|
23
|
+
const sort = fieldValue(role?.sort) ?? 0;
|
|
18
24
|
dialog.content((content) => {
|
|
19
25
|
content.vForm((form) => {
|
|
20
26
|
form.styles({
|
|
@@ -28,19 +34,17 @@ export function RoleFormDialog({ onSubmit }) {
|
|
|
28
34
|
form.vFormItem((item) => {
|
|
29
35
|
item.styles(rowStyle);
|
|
30
36
|
item.label('名称').name('name').required({ message: '请填写名称' });
|
|
31
|
-
item.control((editor) => editor.vInput({ name: 'name', value:
|
|
37
|
+
item.control((editor) => editor.vInput({ name: 'name', value: name }));
|
|
32
38
|
});
|
|
33
39
|
form.vFormItem((item) => {
|
|
34
40
|
item.styles(rowStyle);
|
|
35
41
|
item.label('角色标识').name('code').required({ message: '请填写角色标识' });
|
|
36
|
-
item.control((editor) => editor.vInput({ name: 'code', value:
|
|
42
|
+
item.control((editor) => editor.vInput({ name: 'code', value: code }));
|
|
37
43
|
});
|
|
38
44
|
form.vFormItem((item) => {
|
|
39
45
|
item.styles(rowStyle);
|
|
40
46
|
item.label('描述').name('description');
|
|
41
|
-
item.control((editor) =>
|
|
42
|
-
editor.vInput({ name: 'description', value: role?.description ?? '' })
|
|
43
|
-
);
|
|
47
|
+
item.control((editor) => editor.vInput({ name: 'description', value: description }));
|
|
44
48
|
});
|
|
45
49
|
form.vFormItem((item) => {
|
|
46
50
|
item.styles(rowStyle);
|
|
@@ -49,14 +53,14 @@ export function RoleFormDialog({ onSubmit }) {
|
|
|
49
53
|
editor.vSelect({
|
|
50
54
|
name: 'status',
|
|
51
55
|
options: statusOptions,
|
|
52
|
-
value:
|
|
56
|
+
value: status
|
|
53
57
|
})
|
|
54
58
|
);
|
|
55
59
|
});
|
|
56
60
|
form.vFormItem((item) => {
|
|
57
61
|
item.styles(rowStyle);
|
|
58
62
|
item.label('排序').name('sort');
|
|
59
|
-
item.control((editor) => editor.vInput({ name: 'sort', value: String(
|
|
63
|
+
item.control((editor) => editor.vInput({ name: 'sort', value: String(sort) }));
|
|
60
64
|
});
|
|
61
65
|
form.vButton('保存', (btn) => {
|
|
62
66
|
btn.variant('primary');
|
|
@@ -1,57 +1,45 @@
|
|
|
1
|
-
import { vTable } from '@yoyaflow/yoya-ui';
|
|
1
|
+
import { computed, vTable, vText, vTr } from '@yoyaflow/yoya-ui';
|
|
2
2
|
import { RowActionButton } from '../../../../shared/ui.buttons.js';
|
|
3
3
|
import { statusText } from '../utils/options.js';
|
|
4
4
|
|
|
5
|
+
/** 角色表格:rows 是状态里的 ref 句柄,写入即按 key 对账,无需手动重建行。 */
|
|
5
6
|
export function RoleTable({ rows, onEdit, onRemove }) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
});
|
|
7
|
+
return vTable((table) => {
|
|
8
|
+
table.vThead((head) => {
|
|
9
|
+
head.vTr((row) => {
|
|
10
|
+
row.vTh('名称');
|
|
11
|
+
row.vTh('角色标识');
|
|
12
|
+
row.vTh('描述');
|
|
13
|
+
row.vTh('状态');
|
|
14
|
+
row.vTh('操作');
|
|
30
15
|
});
|
|
31
16
|
});
|
|
32
|
-
|
|
17
|
+
table.vTbody((body) => {
|
|
18
|
+
body.keyed(
|
|
19
|
+
rows,
|
|
20
|
+
(role) => role.id,
|
|
21
|
+
(role) => buildRow(role)
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
33
25
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
26
|
+
function buildRow(role) {
|
|
27
|
+
return vTr((row) => {
|
|
28
|
+
row.vTd(vText(role.name));
|
|
29
|
+
row.vTd(vText(role.code));
|
|
30
|
+
row.vTd(vText(role.description));
|
|
31
|
+
row.vTd(vText(computed(() => statusText[role.status.value] ?? role.status.value)));
|
|
32
|
+
row.vTd((cell) => {
|
|
33
|
+
cell.hstack({ gap: '8px' }, (actions) => {
|
|
34
|
+
actions.child(RowActionButton('编辑', (btn) => btn.on('click', () => onEdit(role))));
|
|
35
|
+
actions.child(
|
|
36
|
+
RowActionButton('删除', (btn) => {
|
|
37
|
+
btn.variant('danger');
|
|
38
|
+
btn.on('click', () => onRemove(role));
|
|
39
|
+
})
|
|
40
|
+
);
|
|
49
41
|
});
|
|
50
42
|
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
renderRows();
|
|
54
|
-
return this;
|
|
55
|
-
}
|
|
56
|
-
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
57
45
|
}
|
|
@@ -9,7 +9,7 @@ export function RoleListPage() {
|
|
|
9
9
|
const dialog = RoleFormDialog({ onSubmit: saveRole });
|
|
10
10
|
const toolbar = RoleToolbar({ onSearch: applyFilters, onAdd: () => dialog.open(null) });
|
|
11
11
|
const table = RoleTable({
|
|
12
|
-
rows:
|
|
12
|
+
rows: state.items,
|
|
13
13
|
onEdit: (role) => dialog.open(role),
|
|
14
14
|
onRemove: askRemove
|
|
15
15
|
});
|
|
@@ -19,22 +19,26 @@ export function RoleListPage() {
|
|
|
19
19
|
onChange({ page: nextPage, pageSize: nextSize }) {
|
|
20
20
|
state.setPage(nextPage);
|
|
21
21
|
state.setPageSize(nextSize);
|
|
22
|
-
|
|
22
|
+
load();
|
|
23
23
|
}
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
load();
|
|
27
|
+
|
|
28
|
+
// vPagination 是命令式组件(不是信号感知的):数据到位后由动作同步一次
|
|
29
|
+
async function load() {
|
|
30
|
+
await state.load();
|
|
28
31
|
syncPagination();
|
|
29
|
-
}
|
|
30
|
-
state.load();
|
|
32
|
+
}
|
|
31
33
|
|
|
32
34
|
function syncPagination() {
|
|
35
|
+
const total = state.total.value;
|
|
36
|
+
const pageSize = state.pageSize.value;
|
|
33
37
|
pagination.update({
|
|
34
|
-
page: state.page
|
|
35
|
-
pageSize
|
|
36
|
-
total
|
|
37
|
-
totalPages: Math.max(1, Math.ceil(
|
|
38
|
+
page: state.page.value,
|
|
39
|
+
pageSize,
|
|
40
|
+
total,
|
|
41
|
+
totalPages: Math.max(1, Math.ceil(total / pageSize))
|
|
38
42
|
});
|
|
39
43
|
}
|
|
40
44
|
|
|
@@ -42,13 +46,13 @@ export function RoleListPage() {
|
|
|
42
46
|
state.setKeyword(values.keyword ?? '');
|
|
43
47
|
state.setStatus(values.status ?? '');
|
|
44
48
|
state.setPage(1);
|
|
45
|
-
|
|
49
|
+
load();
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
async function askRemove(role) {
|
|
49
53
|
const ok = await vConfirm({
|
|
50
54
|
title: '删除角色',
|
|
51
|
-
content: `确定删除角色「${role.name}」?`,
|
|
55
|
+
content: `确定删除角色「${role.name.value}」?`,
|
|
52
56
|
danger: true,
|
|
53
57
|
confirmText: '删除'
|
|
54
58
|
});
|
|
@@ -56,7 +60,7 @@ export function RoleListPage() {
|
|
|
56
60
|
return;
|
|
57
61
|
}
|
|
58
62
|
await state.remove(role.id);
|
|
59
|
-
toast.success(`已删除 ${role.name}`);
|
|
63
|
+
toast.success(`已删除 ${role.name.value}`);
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
async function saveRole(editingId, payload) {
|
|
@@ -87,7 +91,7 @@ export function RoleListPage() {
|
|
|
87
91
|
});
|
|
88
92
|
},
|
|
89
93
|
refresh() {
|
|
90
|
-
return
|
|
94
|
+
return load();
|
|
91
95
|
}
|
|
92
96
|
};
|
|
93
97
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// 跨模块共享的纯状态工具:keyed 列表刷新时按 key 合并行模型。
|
|
2
|
+
// 复用判据是行引用——能复用的行要原地 apply(),否则 keyed 会整行重建。
|
|
3
|
+
|
|
4
|
+
/** 按 key 合并:命中的行原地 apply()(实例身份不变),新 key 才用入参里的新实例。 */
|
|
5
|
+
export function mergeRowsByKey(previous, incoming, keyOf = (row) => row.id) {
|
|
6
|
+
const reusable = new Map(previous.map((row) => [keyOf(row), row]));
|
|
7
|
+
|
|
8
|
+
return incoming.map((row) => {
|
|
9
|
+
const current = reusable.get(keyOf(row));
|
|
10
|
+
return current && typeof current.apply === 'function' ? current.apply(row) : row;
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** 读字段值:行内热字段是句柄,裸行是普通值——apply() 与表单回填两种输入都吃。 */
|
|
15
|
+
export function fieldValue(value) {
|
|
16
|
+
return value && typeof value.peek === 'function' ? value.peek() : value;
|
|
17
|
+
}
|
|
@@ -1,73 +1,47 @@
|
|
|
1
1
|
// 外壳状态类:会话(用户/角色/权限)+ 菜单 + 导航状态(当前模块 / 当前路径)的唯一事实源。
|
|
2
|
+
// 导航字段用 ref 持有——组件绑句柄 / 读信号,写句柄即完成通知,不需要额外订阅转发。
|
|
2
3
|
// load() 先加载会话并注入全局权限上下文,再按权限过滤菜单、创建路由并订阅导航。
|
|
4
|
+
import { ref } from '@yoyaflow/yoya-ui';
|
|
3
5
|
import { createAccess, currentAccess, installAccess } from '@yoyaflow/yoya-ui';
|
|
4
6
|
import ShellReq from './shell.req.js';
|
|
5
7
|
import { createAppRouter } from '../router.js';
|
|
6
8
|
|
|
7
9
|
export default class ShellState {
|
|
8
10
|
constructor() {
|
|
9
|
-
|
|
11
|
+
// 视图字段:顶栏 / 侧栏从这里派生高亮
|
|
12
|
+
this.menus = ref([]);
|
|
13
|
+
this.activeModuleKey = ref(null);
|
|
14
|
+
this.activePath = ref(null);
|
|
15
|
+
// 会话数据不参与渲染,按普通值持有
|
|
16
|
+
this.user = null;
|
|
17
|
+
this.permissions = [];
|
|
18
|
+
this.roles = [];
|
|
10
19
|
this._router = null;
|
|
11
|
-
this._activeModuleKey = null;
|
|
12
|
-
this._activePath = null;
|
|
13
|
-
this._user = null;
|
|
14
|
-
this._permissions = [];
|
|
15
|
-
this._roles = [];
|
|
16
|
-
this._listeners = new Set();
|
|
17
20
|
this._unsubscribeRouter = null;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
menus() {
|
|
21
|
-
return this._menus;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
user() {
|
|
25
|
-
return this._user;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
permissions() {
|
|
29
|
-
return this._permissions.slice();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
roles() {
|
|
33
|
-
return this._roles.slice();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
23
|
router() {
|
|
37
24
|
return this._router;
|
|
38
25
|
}
|
|
39
26
|
|
|
40
|
-
activeModuleKey() {
|
|
41
|
-
return this._activeModuleKey;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
activePath() {
|
|
45
|
-
return this._activePath;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
subscribe(listener) {
|
|
49
|
-
this._listeners.add(listener);
|
|
50
|
-
return () => this._listeners.delete(listener);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
27
|
async load() {
|
|
54
28
|
await this.loadSession();
|
|
55
29
|
|
|
56
30
|
const result = await ShellReq.QueryMenus().submit();
|
|
57
|
-
this.
|
|
58
|
-
this._router = createAppRouter(this.
|
|
31
|
+
this.menus.value = filterMenusByAccess(result.data);
|
|
32
|
+
this._router = createAppRouter(this.menus.value);
|
|
33
|
+
// 路由是外部事件源:订阅它把路径写回导航信号(组件读信号,不再需要二次通知)
|
|
59
34
|
this._unsubscribeRouter = this._router.subscribe((context) => this.syncFromPath(context.path));
|
|
60
|
-
this._emit();
|
|
61
35
|
return result;
|
|
62
36
|
}
|
|
63
37
|
|
|
64
38
|
// 加载会话:当前用户 / 角色 / 权限码,并注入全局权限上下文。
|
|
65
39
|
async loadSession() {
|
|
66
40
|
const result = await ShellReq.Me().submit();
|
|
67
|
-
this.
|
|
68
|
-
this.
|
|
69
|
-
this.
|
|
70
|
-
installAccess(createAccess({ permissions: this.permissions
|
|
41
|
+
this.user = result.data.user;
|
|
42
|
+
this.permissions = [...(result.data.permissions || [])];
|
|
43
|
+
this.roles = [...(result.data.roles || [])];
|
|
44
|
+
installAccess(createAccess({ permissions: this.permissions, roles: this.roles }));
|
|
71
45
|
return result;
|
|
72
46
|
}
|
|
73
47
|
|
|
@@ -91,26 +65,20 @@ export default class ShellState {
|
|
|
91
65
|
return this;
|
|
92
66
|
}
|
|
93
67
|
|
|
68
|
+
// 路由回调 → 写入导航信号(值相同不通知,重复同步无副作用)。
|
|
94
69
|
syncFromPath(path) {
|
|
95
|
-
const module = this.
|
|
96
|
-
|
|
97
|
-
|
|
70
|
+
const module = this.menus
|
|
71
|
+
.peek()
|
|
72
|
+
.find((entry) => entry.routes.some((route) => route.path === path));
|
|
73
|
+
this.activePath.value = path;
|
|
98
74
|
if (module) {
|
|
99
|
-
this.
|
|
100
|
-
}
|
|
101
|
-
if (changed) {
|
|
102
|
-
this._emit();
|
|
75
|
+
this.activeModuleKey.value = module.key;
|
|
103
76
|
}
|
|
104
77
|
return this;
|
|
105
78
|
}
|
|
106
79
|
|
|
107
80
|
destroy() {
|
|
108
81
|
this._unsubscribeRouter?.();
|
|
109
|
-
this._listeners.clear();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
_emit() {
|
|
113
|
-
this._listeners.forEach((listener) => listener());
|
|
114
82
|
}
|
|
115
83
|
}
|
|
116
84
|
|
|
@@ -2,17 +2,9 @@ import { vBody, vRouterViews } from '@yoyaflow/yoya-ui';
|
|
|
2
2
|
import { AppNavbar } from './app-navbar.js';
|
|
3
3
|
import { AppSidebar } from './app-sidebar.js';
|
|
4
4
|
|
|
5
|
-
//
|
|
6
|
-
// URL 变化 → state.syncFromPath()
|
|
5
|
+
// 外壳装配:只做布局组装。路由与导航动作都在 ShellState,
|
|
6
|
+
// URL 变化 → state.syncFromPath() 写入导航信号 → 顶栏 / 侧栏区域自行更新。
|
|
7
7
|
export function AdminShell({ state }) {
|
|
8
|
-
const navbar = AppNavbar({ state });
|
|
9
|
-
const sidebar = AppSidebar({ state });
|
|
10
|
-
|
|
11
|
-
const unsubscribe = state.subscribe(() => {
|
|
12
|
-
navbar.applyState();
|
|
13
|
-
sidebar.applyState();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
8
|
return {
|
|
17
9
|
render() {
|
|
18
10
|
return vBody((shell) => {
|
|
@@ -23,20 +15,17 @@ export function AdminShell({ state }) {
|
|
|
23
15
|
frame.viewport(true);
|
|
24
16
|
frame.vHeader({ height: 56 }, (header) => {
|
|
25
17
|
header.style('padding', '0');
|
|
26
|
-
header.child(
|
|
18
|
+
header.child(AppNavbar({ state }));
|
|
27
19
|
});
|
|
28
20
|
frame.vContainer((body) => {
|
|
29
21
|
body.style({ flex: '1 1 auto', minHeight: '0' });
|
|
30
|
-
body.child(
|
|
22
|
+
body.child(AppSidebar({ state }));
|
|
31
23
|
body.vMain((main) => {
|
|
32
24
|
main.child(vRouterViews(state.router(), { lockTitle: true, title: '内容区' }));
|
|
33
25
|
});
|
|
34
26
|
});
|
|
35
27
|
});
|
|
36
28
|
});
|
|
37
|
-
},
|
|
38
|
-
destroy() {
|
|
39
|
-
unsubscribe();
|
|
40
29
|
}
|
|
41
30
|
};
|
|
42
31
|
}
|
|
@@ -1,66 +1,58 @@
|
|
|
1
1
|
import { vAvatar, vNavbar } from '@yoyaflow/yoya-ui';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* 顶栏:模块页签从 state 派生。菜单区是区域——先声明 rebuildable() 再读信号,
|
|
5
|
+
* 当前模块变化时重建页签,模块内换路由不会动它。
|
|
6
|
+
*/
|
|
3
7
|
export function AppNavbar({ state }) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
background: 'var(--yoya-color-primary, #2563eb)',
|
|
24
|
-
borderRadius: '8px',
|
|
25
|
-
boxSizing: 'border-box',
|
|
26
|
-
color: '#ffffff',
|
|
27
|
-
display: 'inline-flex',
|
|
28
|
-
fontSize: '16px',
|
|
29
|
-
fontWeight: '800',
|
|
30
|
-
height: '32px',
|
|
31
|
-
justifyContent: 'center',
|
|
32
|
-
lineHeight: '1',
|
|
33
|
-
width: '32px'
|
|
34
|
-
});
|
|
35
|
-
logo.text('y');
|
|
36
|
-
});
|
|
37
|
-
row.strong((name) => {
|
|
38
|
-
name.style({
|
|
39
|
-
color: 'var(--yoya-color-text-strong, #0f172a)',
|
|
40
|
-
fontSize: '16px',
|
|
41
|
-
fontWeight: '700'
|
|
42
|
-
});
|
|
43
|
-
name.text('yoya 管理台');
|
|
44
|
-
});
|
|
8
|
+
return vNavbar((nav) => {
|
|
9
|
+
nav.sticky(true);
|
|
10
|
+
nav.brand((brand) => {
|
|
11
|
+
brand.hstack({ gap: '10px' }, (row) => {
|
|
12
|
+
row.style({ alignItems: 'center', marginLeft: '16px' });
|
|
13
|
+
row.span((logo) => {
|
|
14
|
+
logo.style({
|
|
15
|
+
alignItems: 'center',
|
|
16
|
+
background: 'var(--yoya-color-primary, #2563eb)',
|
|
17
|
+
borderRadius: '8px',
|
|
18
|
+
boxSizing: 'border-box',
|
|
19
|
+
color: '#ffffff',
|
|
20
|
+
display: 'inline-flex',
|
|
21
|
+
fontSize: '16px',
|
|
22
|
+
fontWeight: '800',
|
|
23
|
+
height: '32px',
|
|
24
|
+
justifyContent: 'center',
|
|
25
|
+
lineHeight: '1',
|
|
26
|
+
width: '32px'
|
|
45
27
|
});
|
|
28
|
+
logo.child('y');
|
|
46
29
|
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
item.on('click', () => state.switchModule(module));
|
|
53
|
-
});
|
|
30
|
+
row.strong((name) => {
|
|
31
|
+
name.style({
|
|
32
|
+
color: 'var(--yoya-color-text-strong, #0f172a)',
|
|
33
|
+
fontSize: '16px',
|
|
34
|
+
fontWeight: '700'
|
|
54
35
|
});
|
|
36
|
+
name.child('yoya 管理台');
|
|
55
37
|
});
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
nav.menuContent((menu) => {
|
|
41
|
+
menu.rebuildable();
|
|
42
|
+
const activeKey = state.activeModuleKey.value;
|
|
43
|
+
state.menus.value.forEach((module) => {
|
|
44
|
+
menu.vMenuItem((item) => {
|
|
45
|
+
item.child(module.label);
|
|
46
|
+
item.active(module.key === activeKey);
|
|
47
|
+
item.on('click', () => state.switchModule(module));
|
|
61
48
|
});
|
|
62
49
|
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
50
|
+
});
|
|
51
|
+
nav.actions((actions) => {
|
|
52
|
+
actions.vThemeModeSwitch({ persist: true });
|
|
53
|
+
const avatar = vAvatar({ color: '#2563eb', size: 'small', text: '管' });
|
|
54
|
+
avatar.style('marginRight', '16px');
|
|
55
|
+
actions.child(avatar);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
66
58
|
}
|
|
@@ -1,44 +1,33 @@
|
|
|
1
1
|
import { vSidebar } from '@yoyaflow/yoya-ui';
|
|
2
2
|
import { SidebarMenu } from './sidebar-menu.js';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* 左侧菜单:菜单区是区域——先声明 rebuildable() 再读信号,
|
|
6
|
+
* 当前模块或当前路径变化时重建菜单(菜单项很少,重建比逐项同步更直接)。
|
|
7
|
+
*/
|
|
4
8
|
export function AppSidebar({ state }) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
flex: '0 0 auto',
|
|
14
|
-
width: '220px',
|
|
15
|
-
gridTemplateRows: 'auto minmax(0, 1fr)'
|
|
16
|
-
});
|
|
9
|
+
return vSidebar((side) => {
|
|
10
|
+
side.ariaLabel('左侧菜单');
|
|
11
|
+
side.title('导航');
|
|
12
|
+
side.collapsible(false);
|
|
13
|
+
side.style({
|
|
14
|
+
flex: '0 0 auto',
|
|
15
|
+
width: '220px',
|
|
16
|
+
gridTemplateRows: 'auto minmax(0, 1fr)'
|
|
17
17
|
});
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
const module = state.menus().find((entry) => entry.key === state.activeModuleKey());
|
|
26
|
-
if (!module) {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
sidebar.menuContent((menu) => {
|
|
18
|
+
side.menuContent((menu) => {
|
|
19
|
+
menu.rebuildable();
|
|
20
|
+
const module = state.menus.value.find((entry) => entry.key === state.activeModuleKey.value);
|
|
21
|
+
if (!module) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
30
24
|
menu.child(
|
|
31
25
|
SidebarMenu({
|
|
32
26
|
module,
|
|
33
|
-
activePath: state.activePath
|
|
27
|
+
activePath: state.activePath.value,
|
|
34
28
|
onNavigate: (path) => state.navigate(path)
|
|
35
29
|
})
|
|
36
30
|
);
|
|
37
31
|
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
render,
|
|
42
|
-
applyState
|
|
43
|
-
};
|
|
32
|
+
});
|
|
44
33
|
}
|
|
@@ -5,7 +5,7 @@ export function SidebarMenu({ module, activePath, onNavigate }) {
|
|
|
5
5
|
group.label(module.label);
|
|
6
6
|
module.routes.forEach((route) => {
|
|
7
7
|
group.vMenuItem((item) => {
|
|
8
|
-
item.
|
|
8
|
+
item.child(route.title);
|
|
9
9
|
item.active(route.path === activePath);
|
|
10
10
|
item.on('click', () => onNavigate(route.path));
|
|
11
11
|
});
|