create-yoya-ui 0.2.3 → 0.3.3

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 (38) hide show
  1. package/bin/create-yoya-ui.js +3 -1
  2. package/package.json +1 -1
  3. package/templates/admin/README.md +10 -0
  4. package/templates/admin/RULES.md +11 -2
  5. package/templates/admin/node_modules/.vite/deps/_metadata.json +8 -0
  6. package/templates/admin/node_modules/.vite/deps/package.json +3 -0
  7. package/templates/admin/package.json +1 -1
  8. package/templates/admin/src/features/dashboard/overview/components/dashboard-chart.js +7 -1
  9. package/templates/admin/src/features/dashboard/overview/pages/dashboard-overview-page.js +9 -5
  10. package/templates/admin/src/features/system/dicts/api/dict.mgr.d.ts +48 -21
  11. package/templates/admin/src/features/system/dicts/components/dict-editor-dialog.js +13 -20
  12. package/templates/admin/src/features/system/dicts/components/dict-item-form-dialog.js +5 -1
  13. package/templates/admin/src/features/system/dicts/pages/dict-list-page.js +12 -19
  14. package/templates/admin/src/features/system/members/api/member.mgr.d.ts +8 -6
  15. package/templates/admin/src/features/system/members/components/member-form-dialog.js +5 -1
  16. package/templates/admin/src/features/system/members/components/member-table.js +7 -1
  17. package/templates/admin/src/features/system/members/components/member-toolbar.js +2 -3
  18. package/templates/admin/src/features/system/permissions/api/permission.mgr.d.ts +35 -13
  19. package/templates/admin/src/features/system/permissions/api/permission.mgr.js +7 -1
  20. package/templates/admin/src/features/system/permissions/api/permission.mock.js +24 -3
  21. package/templates/admin/src/features/system/permissions/api/permission.views.js +9 -1
  22. package/templates/admin/src/features/system/permissions/pages/permission-list-page.js +12 -19
  23. package/templates/admin/src/features/system/roles/api/role.mgr.d.ts +24 -26
  24. package/templates/admin/src/features/system/roles/api/role.mock.js +9 -4
  25. package/templates/admin/src/features/system/roles/components/role-form-dialog.js +6 -4
  26. package/templates/admin/src/features/system/roles/components/role-toolbar.js +1 -3
  27. package/templates/admin/src/features/system/roles/pages/role-list-page.js +12 -19
  28. package/templates/admin/src/main.js +2 -0
  29. package/templates/admin/src/shell/api/auth.mock.js +23 -0
  30. package/templates/admin/src/shell/api/shell.mock.js +48 -8
  31. package/templates/admin/src/shell/api/shell.req.js +8 -1
  32. package/templates/admin/src/shell/api/shell.state.js +48 -8
  33. package/templates/admin/src/shell/api/shell.views.js +2 -1
  34. package/templates/admin/src/shell/router.js +4 -1
  35. package/templates/admin/vite.config.js +16 -0
  36. package/templates/basic/package.json +1 -1
  37. package/templates/basic/src/main.js +1 -1
  38. package/templates/ssr/package.json +1 -1
@@ -10,7 +10,9 @@ const targetDir = resolve(process.cwd(), targetName);
10
10
  const templateArgIndex = args.indexOf('--template');
11
11
  const templateName =
12
12
  templateArgIndex !== -1 && args[templateArgIndex + 1] ? args[templateArgIndex + 1] : 'basic';
13
- const templateRoot = resolve(fileURLToPath(new URL(`../templates/${templateName}`, import.meta.url)));
13
+ const templateRoot = resolve(
14
+ fileURLToPath(new URL(`../templates/${templateName}`, import.meta.url))
15
+ );
14
16
 
15
17
  if (!existsSync(templateRoot)) {
16
18
  console.error(`未知模板:${templateName}(可用:basic、ssr、admin)`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-yoya-ui",
3
- "version": "0.2.3",
3
+ "version": "0.3.3",
4
4
  "description": "Scaffold a new yoya-ui project with Vite: basic template (SPA) or SSR template (renderPage + hydrateOrMount).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,6 +15,16 @@ npm install
15
15
  npm run dev
16
16
  ```
17
17
 
18
+ ### 本地仓库联调(推荐在仓库内预览)
19
+
20
+ 模板通过 npm 安装线上 `@yoyaflow/yoya-ui`。若要在 yoya-ui 本地仓库中直接预览**最新源码**(如权限能力),模板的 `vite.config.js` 会自动把 `@yoyaflow/yoya-ui` 指到仓库 `src/`(对应路径不存在时回退 npm 包解析,脚手架生成的项目不受影响):
21
+
22
+ ```bash
23
+ cd create-yoya-ui/templates/admin
24
+ node ../../../node_modules/vite/bin/vite.js
25
+ # 打开 http://127.0.0.1:5173
26
+ ```
27
+
18
28
  ## TypeScript
19
29
 
20
30
  模板默认支持 TypeScript,非侵入接入:现有 `.js` 文件保持不动(`tsconfig.json` 已开启 `allowJs`),需要类型时直接新建或改写为 `.ts` 文件即可(Vite 原生支持)。运行 `npm run typecheck` 做类型检查。
@@ -61,6 +61,8 @@ export default {
61
61
  - **不深层 import**:只允许跨模块引用 `req.js`,不 import 业务域的 pages / components / state 内部实现
62
62
  - **自包含 UI 与状态**:弹窗、下拉、open/close 状态内聚在组件内,对外暴露工厂与必要方法(`open()/close()/value()`),不暴露内部状态实现
63
63
  - **浮层自洽**:弹窗/下拉类共享组件自带 fixed 定位逻辑,不依赖使用方容器样式
64
+ - **危险确认统一 vConfirm**:删除/危险操作调用 `vConfirm({ title, content, danger, confirmText })`
65
+ 返回 `Promise<boolean>`,不自建 vDialog 确认层(表单类编辑弹窗仍用 vDialog)
64
66
 
65
67
  ## 6. 外壳与导航状态
66
68
 
@@ -98,8 +100,15 @@ export default {
98
100
 
99
101
  `main.js` 固定顺序:
100
102
 
101
- 1. 导入并注册所有 mock(含 `shell.mock.js`)
102
- 2. `state.load()`:获取菜单 + 创建路由 + 订阅导航
103
+ 1. 导入并注册所有 mock(含 `shell.mock.js` / `auth.mock.js`)
104
+ 2. `state.load()`:内部先 `loadSession()` 加载会话并 `installAccess(...)` 注入全局权限,再获取菜单(按权限过滤)+ 创建路由 + 订阅导航
103
105
  3. `AdminShell({ state })` 装配外壳
104
106
  4. `shell.render().bindTo('#app')`
105
107
  5. `state.start()` 启动路由
108
+
109
+ ## 12. 权限接入
110
+
111
+ - **会话与权限**:会话请求命令并入 `shell.req.js`(`ShellReq.Me()`),`shell/api/auth.mock.js` 提供 `/auth/me` 演示数据;会话状态融合进 `ShellState`(`user()` / `roles()` / `permissions()`),`loadSession()` 内 `installAccess` 注入全局。
112
+ - **菜单 / 路由**:`shell.mock.js` 的每条路由声明 `permCode`(与权限管理页的 code 对齐),`ShellState.load()` 按 `currentAccess().canRead(permCode)` 过滤菜单;无读的路由不注册,直达 URL 落到未找到页。
113
+ - **按钮 / 操作**:直接在按钮上声明 `.access('system:member:create')` 等裸码,无写权限自动禁用、无读自动隐藏(见 member-toolbar / member-table)。
114
+ - **体验层**:前端只做显隐 / 禁用,真正的拦截必须由后端按权限码校验。
@@ -0,0 +1,8 @@
1
+ {
2
+ "hash": "854f049f",
3
+ "configHash": "1cca6c4d",
4
+ "lockfileHash": "7fb7e123",
5
+ "browserHash": "3ebf44f7",
6
+ "optimized": {},
7
+ "chunks": {}
8
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -10,7 +10,7 @@
10
10
  "typecheck": "tsc --noEmit"
11
11
  },
12
12
  "dependencies": {
13
- "@yoyaflow/yoya-ui": "^0.3.0"
13
+ "@yoyaflow/yoya-ui": "^0.3.3"
14
14
  },
15
15
  "devDependencies": {
16
16
  "typescript": "^7.0.2",
@@ -5,7 +5,13 @@ const PAD = { bottom: 34, left: 44, right: 16, top: 14 };
5
5
  const SERIES_COLORS = ['#2563eb', '#16a34a', '#f59e0b', '#dc2626', '#8b5cf6'];
6
6
 
7
7
  // vChart 内置 SVG 适配器:bar 柱状图 / line 折线图,零外部依赖。
8
- export function DashboardChart({ labels = [], series = [], title = '', type = 'bar', height = 280 } = {}) {
8
+ export function DashboardChart({
9
+ labels = [],
10
+ series = [],
11
+ title = '',
12
+ type = 'bar',
13
+ height = 280
14
+ } = {}) {
9
15
  const chart = vChart({
10
16
  adapter: createSvgAdapter(),
11
17
  data: { labels, series },
@@ -140,7 +140,10 @@ export function DashboardOverviewPage() {
140
140
  box.style({ textAlign: 'center' });
141
141
  box.child(vGauge({ unit: '%', value: 72, tone: 'warning' }));
142
142
  box.p((text) => {
143
- text.style({ color: 'var(--yoya-color-text-muted, #64748b)', fontSize: '12px' });
143
+ text.style({
144
+ color: 'var(--yoya-color-text-muted, #64748b)',
145
+ fontSize: '12px'
146
+ });
144
147
  text.text('CPU 使用率');
145
148
  });
146
149
  });
@@ -148,7 +151,10 @@ export function DashboardOverviewPage() {
148
151
  box.style({ textAlign: 'center' });
149
152
  box.child(vGauge({ unit: '%', value: 58, tone: 'primary' }));
150
153
  box.p((text) => {
151
- text.style({ color: 'var(--yoya-color-text-muted, #64748b)', fontSize: '12px' });
154
+ text.style({
155
+ color: 'var(--yoya-color-text-muted, #64748b)',
156
+ fontSize: '12px'
157
+ });
152
158
  text.text('内存使用率');
153
159
  });
154
160
  });
@@ -164,9 +170,7 @@ export function DashboardOverviewPage() {
164
170
  item.status('success');
165
171
  item.title('服务发布成功');
166
172
  item.time('09:32 · 2026-09-01');
167
- item.content((box) =>
168
- box.p('api-gateway v2.4.1 已上线,金丝雀批次全部通过。')
169
- );
173
+ item.content((box) => box.p('api-gateway v2.4.1 已上线,金丝雀批次全部通过。'));
170
174
  });
171
175
  timeline.vTimelineItem((item) => {
172
176
  item.status('processing');
@@ -20,9 +20,13 @@ declare class CreateType extends RequestBase {
20
20
  }
21
21
 
22
22
  declare class UpdateType extends RequestBase {
23
- constructor(
24
- init?: { id: number; name?: string; code?: string; status?: DictStatus; remark?: string }
25
- );
23
+ constructor(init?: {
24
+ id: number;
25
+ name?: string;
26
+ code?: string;
27
+ status?: DictStatus;
28
+ remark?: string;
29
+ });
26
30
  method(): 'PUT';
27
31
  address(): string;
28
32
  body(): { name: string; code: string; status: DictStatus; remark: string };
@@ -41,18 +45,26 @@ declare class QueryItems extends RequestBase {
41
45
  }
42
46
 
43
47
  declare class CreateItem extends RequestBase {
44
- constructor(
45
- init?: { typeId: number; label?: string; value?: string; sort?: number; status?: DictStatus }
46
- );
48
+ constructor(init?: {
49
+ typeId: number;
50
+ label?: string;
51
+ value?: string;
52
+ sort?: number;
53
+ status?: DictStatus;
54
+ });
47
55
  method(): 'POST';
48
56
  address(): string;
49
57
  body(): { label: string; value: string; sort: number; status: DictStatus };
50
58
  }
51
59
 
52
60
  declare class UpdateItem extends RequestBase {
53
- constructor(
54
- init?: { id: number; label?: string; value?: string; sort?: number; status?: DictStatus }
55
- );
61
+ constructor(init?: {
62
+ id: number;
63
+ label?: string;
64
+ value?: string;
65
+ sort?: number;
66
+ status?: DictStatus;
67
+ });
56
68
  method(): 'PUT';
57
69
  address(): string;
58
70
  body(): { label: string; value: string; sort: number; status: DictStatus };
@@ -66,20 +78,35 @@ declare class RemoveItem extends RequestBase {
66
78
 
67
79
  declare const DictMgr: {
68
80
  QueryTypes: (init?: { page?: number; pageSize?: number }) => QueryTypes;
69
- CreateType: (
70
- init?: { name?: string; code?: string; status?: DictStatus; remark?: string }
71
- ) => CreateType;
72
- UpdateType: (
73
- init?: { id: number; name?: string; code?: string; status?: DictStatus; remark?: string }
74
- ) => UpdateType;
81
+ CreateType: (init?: {
82
+ name?: string;
83
+ code?: string;
84
+ status?: DictStatus;
85
+ remark?: string;
86
+ }) => CreateType;
87
+ UpdateType: (init?: {
88
+ id: number;
89
+ name?: string;
90
+ code?: string;
91
+ status?: DictStatus;
92
+ remark?: string;
93
+ }) => UpdateType;
75
94
  RemoveType: (init?: { id: number }) => RemoveType;
76
95
  QueryItems: (init?: { typeId: number }) => QueryItems;
77
- CreateItem: (
78
- init?: { typeId: number; label?: string; value?: string; sort?: number; status?: DictStatus }
79
- ) => CreateItem;
80
- UpdateItem: (
81
- init?: { id: number; label?: string; value?: string; sort?: number; status?: DictStatus }
82
- ) => UpdateItem;
96
+ CreateItem: (init?: {
97
+ typeId: number;
98
+ label?: string;
99
+ value?: string;
100
+ sort?: number;
101
+ status?: DictStatus;
102
+ }) => CreateItem;
103
+ UpdateItem: (init?: {
104
+ id: number;
105
+ label?: string;
106
+ value?: string;
107
+ sort?: number;
108
+ status?: DictStatus;
109
+ }) => UpdateItem;
83
110
  RemoveItem: (init?: { id: number }) => RemoveItem;
84
111
  };
85
112
 
@@ -1,4 +1,4 @@
1
- import { toast, vDialog, vTable } from '@yoyaflow/yoya-ui';
1
+ import { toast, vConfirm, vDialog, vTable } from '@yoyaflow/yoya-ui';
2
2
  import { RowActionButton } from '../../../../shared/ui.buttons.js';
3
3
  import { DictItemFormDialog } from './dict-item-form-dialog.js';
4
4
  import { statusOptions, statusText } from '../utils/options.js';
@@ -15,7 +15,6 @@ const rowStyle = {
15
15
  export function DictEditorDialog({ state, onSubmit }) {
16
16
  const dialog = vDialog();
17
17
  const itemDialog = DictItemFormDialog({ onSubmit: saveItem });
18
- const confirmDialog = vDialog();
19
18
  let editingId = null;
20
19
  let itemsBody = null;
21
20
  let itemsCountText = null;
@@ -123,7 +122,6 @@ export function DictEditorDialog({ state, onSubmit }) {
123
122
  });
124
123
 
125
124
  stack.child(itemDialog);
126
- stack.child(confirmDialog);
127
125
  });
128
126
  });
129
127
 
@@ -168,24 +166,19 @@ export function DictEditorDialog({ state, onSubmit }) {
168
166
  }
169
167
  }
170
168
 
171
- function askRemoveItem(item) {
172
- confirmDialog.content((content) => {
173
- content.p(`确定删除字典值「${item.label}」?`);
174
- content.hstack({ gap: '8px' }, (row) => {
175
- row.spacer();
176
- row.vButton('取消', (btn) => btn.on('click', () => confirmDialog.close()));
177
- row.vButton('删除', (btn) => {
178
- btn.variant('danger');
179
- btn.on('click', async () => {
180
- confirmDialog.close();
181
- await state.removeItem(item.id);
182
- toast.success(`已删除 ${item.label}`);
183
- renderItems();
184
- });
185
- });
186
- });
169
+ async function askRemoveItem(item) {
170
+ const ok = await vConfirm({
171
+ title: '删除字典值',
172
+ content: `确定删除字典值「${item.label}」?`,
173
+ danger: true,
174
+ confirmText: '删除'
187
175
  });
188
- confirmDialog.open(true);
176
+ if (!ok) {
177
+ return;
178
+ }
179
+ await state.removeItem(item.id);
180
+ toast.success(`已删除 ${item.label}`);
181
+ renderItems();
189
182
  }
190
183
 
191
184
  async function saveItem(itemId, payload) {
@@ -46,7 +46,11 @@ export function DictItemFormDialog({ onSubmit }) {
46
46
  field.styles(rowStyle);
47
47
  field.label('状态').name('status');
48
48
  field.control((editor) =>
49
- editor.vSelect({ name: 'status', options: statusOptions, value: item?.status ?? 'active' })
49
+ editor.vSelect({
50
+ name: 'status',
51
+ options: statusOptions,
52
+ value: item?.status ?? 'active'
53
+ })
50
54
  );
51
55
  });
52
56
  form.vButton('保存', (btn) => {
@@ -1,4 +1,4 @@
1
- import { toast, vDialog, vPagination, vTable, vstack } from '@yoyaflow/yoya-ui';
1
+ import { toast, vConfirm, vPagination, vTable, vstack } from '@yoyaflow/yoya-ui';
2
2
  import { RowActionButton } from '../../../../shared/ui.buttons.js';
3
3
  import DictsPageState from '../api/dict.state.js';
4
4
  import { DictEditorDialog } from '../components/dict-editor-dialog.js';
@@ -7,7 +7,6 @@ import { statusText } from '../utils/options.js';
7
7
  export function DictListPage() {
8
8
  const state = new DictsPageState();
9
9
  const editorDialog = DictEditorDialog({ state, onSubmit: saveType });
10
- const confirmDialog = vDialog();
11
10
  const pagination = vPagination({
12
11
  pageSize: 10,
13
12
  pageSizes: [10, 20, 50],
@@ -62,23 +61,18 @@ export function DictListPage() {
62
61
  });
63
62
  }
64
63
 
65
- function askRemoveType(type) {
66
- confirmDialog.content((content) => {
67
- content.p(`确定删除字典「${type.name}」及其所有字典值?`);
68
- content.hstack({ gap: '8px' }, (row) => {
69
- row.spacer();
70
- row.vButton('取消', (btn) => btn.on('click', () => confirmDialog.close()));
71
- row.vButton('删除', (btn) => {
72
- btn.variant('danger');
73
- btn.on('click', async () => {
74
- confirmDialog.close();
75
- await state.removeType(type.id);
76
- toast.success(`已删除 ${type.name}`);
77
- });
78
- });
79
- });
64
+ async function askRemoveType(type) {
65
+ const ok = await vConfirm({
66
+ title: '删除字典',
67
+ content: `确定删除字典「${type.name}」及其所有字典值?`,
68
+ danger: true,
69
+ confirmText: '删除'
80
70
  });
81
- confirmDialog.open(true);
71
+ if (!ok) {
72
+ return;
73
+ }
74
+ await state.removeType(type.id);
75
+ toast.success(`已删除 ${type.name}`);
82
76
  }
83
77
 
84
78
  async function saveType(editingId, payload) {
@@ -133,7 +127,6 @@ export function DictListPage() {
133
127
  });
134
128
  });
135
129
  stack.child(editorDialog);
136
- stack.child(confirmDialog);
137
130
  });
138
131
  },
139
132
  refresh() {
@@ -35,13 +35,15 @@ declare class Remove extends RequestBase {
35
35
  }
36
36
 
37
37
  declare const MemberMgr: {
38
- Query: (
39
- init?: { page?: number; pageSize?: number; keyword?: string; status?: string }
40
- ) => Query;
38
+ Query: (init?: { page?: number; pageSize?: number; keyword?: string; status?: string }) => Query;
41
39
  Create: (init?: { name?: string; email?: string; role?: string; status?: string }) => Create;
42
- Update: (
43
- init?: { id: number; name?: string; email?: string; role?: string; status?: string }
44
- ) => Update;
40
+ Update: (init?: {
41
+ id: number;
42
+ name?: string;
43
+ email?: string;
44
+ role?: string;
45
+ status?: string;
46
+ }) => Update;
45
47
  Remove: (init?: { id: number }) => Remove;
46
48
  };
47
49
 
@@ -43,7 +43,11 @@ export function MemberFormDialog({ onSubmit }) {
43
43
  item.styles(rowStyle);
44
44
  item.label('状态').name('status');
45
45
  item.control((editor) =>
46
- editor.vSelect({ name: 'status', options: statusOptions, value: member?.status ?? 'active' })
46
+ editor.vSelect({
47
+ name: 'status',
48
+ options: statusOptions,
49
+ value: member?.status ?? 'active'
50
+ })
47
51
  );
48
52
  });
49
53
  }
@@ -19,10 +19,16 @@ export function MemberTable({ rows, onEdit, onRemove }) {
19
19
  row.vTd(statusText[member.status] ?? member.status);
20
20
  row.vTd((cell) => {
21
21
  cell.hstack({ gap: '8px' }, (actions) => {
22
- actions.child(RowActionButton('编辑', (btn) => btn.on('click', () => onEdit(member))));
22
+ actions.child(
23
+ RowActionButton('编辑', (btn) => {
24
+ btn.access('system:member:update');
25
+ btn.on('click', () => onEdit(member));
26
+ })
27
+ );
23
28
  actions.child(
24
29
  RowActionButton('删除', (btn) => {
25
30
  btn.variant('danger');
31
+ btn.access('system:member:remove');
26
32
  btn.on('click', () => onRemove(member));
27
33
  })
28
34
  );
@@ -27,9 +27,7 @@ export function MemberToolbar({ onSearch, onAdd }) {
27
27
  f.vFormItem((item) => {
28
28
  item.style({ display: 'flex', alignItems: 'center', gap: '8px' });
29
29
  item.label('关键词').name('keyword');
30
- item.control((editor) =>
31
- editor.vInput({ name: 'keyword', placeholder: '姓名 / 邮箱' })
32
- );
30
+ item.control((editor) => editor.vInput({ name: 'keyword', placeholder: '姓名 / 邮箱' }));
33
31
  });
34
32
  f.vFormItem((item) => {
35
33
  item.style({ display: 'flex', alignItems: 'center', gap: '8px' });
@@ -46,6 +44,7 @@ export function MemberToolbar({ onSearch, onAdd }) {
46
44
  f.vButton('新增成员', (btn) => {
47
45
  btn.variant('primary');
48
46
  btn.style('marginLeft', 'auto');
47
+ btn.access('system:member:create');
49
48
  btn.on('click', onAdd);
50
49
  });
51
50
  f.on('submit', (event) => {
@@ -10,18 +10,32 @@ declare class QueryTree extends RequestBase {
10
10
  }
11
11
 
12
12
  declare class Create extends RequestBase {
13
- constructor(
14
- init?: { parentId: number | null; name?: string; code?: string; type?: PermissionType; sort?: number }
15
- );
13
+ constructor(init?: {
14
+ parentId: number | null;
15
+ name?: string;
16
+ code?: string;
17
+ type?: PermissionType;
18
+ sort?: number;
19
+ });
16
20
  method(): 'POST';
17
21
  address(): string;
18
- body(): { parentId: number | null; name: string; code: string; type: PermissionType; sort: number };
22
+ body(): {
23
+ parentId: number | null;
24
+ name: string;
25
+ code: string;
26
+ type: PermissionType;
27
+ sort: number;
28
+ };
19
29
  }
20
30
 
21
31
  declare class Update extends RequestBase {
22
- constructor(
23
- init?: { id: number; name?: string; code?: string; type?: PermissionType; sort?: number }
24
- );
32
+ constructor(init?: {
33
+ id: number;
34
+ name?: string;
35
+ code?: string;
36
+ type?: PermissionType;
37
+ sort?: number;
38
+ });
25
39
  method(): 'PUT';
26
40
  address(): string;
27
41
  body(): { name: string; code: string; type: PermissionType; sort: number };
@@ -35,12 +49,20 @@ declare class Remove extends RequestBase {
35
49
 
36
50
  declare const PermissionMgr: {
37
51
  QueryTree: (init?: unknown) => QueryTree;
38
- Create: (
39
- init?: { parentId: number | null; name?: string; code?: string; type?: PermissionType; sort?: number }
40
- ) => Create;
41
- Update: (
42
- init?: { id: number; name?: string; code?: string; type?: PermissionType; sort?: number }
43
- ) => Update;
52
+ Create: (init?: {
53
+ parentId: number | null;
54
+ name?: string;
55
+ code?: string;
56
+ type?: PermissionType;
57
+ sort?: number;
58
+ }) => Create;
59
+ Update: (init?: {
60
+ id: number;
61
+ name?: string;
62
+ code?: string;
63
+ type?: PermissionType;
64
+ sort?: number;
65
+ }) => Update;
44
66
  Remove: (init?: { id: number }) => Remove;
45
67
  };
46
68
 
@@ -34,7 +34,13 @@ class Create extends RequestBase {
34
34
  }
35
35
 
36
36
  body() {
37
- return { parentId: this.parentId, name: this.name, code: this.code, type: this.type, sort: this.sort };
37
+ return {
38
+ parentId: this.parentId,
39
+ name: this.name,
40
+ code: this.code,
41
+ type: this.type,
42
+ sort: this.sort
43
+ };
38
44
  }
39
45
  }
40
46
 
@@ -7,9 +7,30 @@ let permissions = [
7
7
  { id: 12, parentId: 1, name: '待办审批', code: 'dashboard:todos', type: 'menu', sort: 2 },
8
8
  { id: 2, parentId: null, name: '系统管理', code: 'system', type: 'menu', sort: 2 },
9
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 },
10
+ {
11
+ id: 211,
12
+ parentId: 21,
13
+ name: '新增成员',
14
+ code: 'system:member:create',
15
+ type: 'button',
16
+ sort: 1
17
+ },
18
+ {
19
+ id: 212,
20
+ parentId: 21,
21
+ name: '编辑成员',
22
+ code: 'system:member:update',
23
+ type: 'button',
24
+ sort: 2
25
+ },
26
+ {
27
+ id: 213,
28
+ parentId: 21,
29
+ name: '删除成员',
30
+ code: 'system:member:remove',
31
+ type: 'button',
32
+ sort: 3
33
+ },
13
34
  { id: 22, parentId: 2, name: '角色管理', code: 'system:role', type: 'menu', sort: 2 },
14
35
  { id: 23, parentId: 2, name: '权限管理', code: 'system:permission', type: 'menu', sort: 3 },
15
36
  { id: 24, parentId: 2, name: '字典管理', code: 'system:dict', type: 'menu', sort: 4 }
@@ -1,6 +1,14 @@
1
1
  // 权限结果结构(纯数据)。
2
2
  class PermissionNode {
3
- constructor({ id, parentId = null, name = '', code = '', type = 'menu', sort = 0, children = [] } = {}) {
3
+ constructor({
4
+ id,
5
+ parentId = null,
6
+ name = '',
7
+ code = '',
8
+ type = 'menu',
9
+ sort = 0,
10
+ children = []
11
+ } = {}) {
4
12
  this.id = id;
5
13
  this.parentId = parentId;
6
14
  this.name = name;
@@ -1,4 +1,4 @@
1
- import { toast, vDialog, vTree, vstack } from '@yoyaflow/yoya-ui';
1
+ import { toast, vConfirm, vTree, vstack } from '@yoyaflow/yoya-ui';
2
2
  import { RowActionButton } from '../../../../shared/ui.buttons.js';
3
3
  import PermissionsPageState from '../api/permission.state.js';
4
4
  import { PermissionFormDialog } from '../components/permission-form-dialog.js';
@@ -6,7 +6,6 @@ import { PermissionFormDialog } from '../components/permission-form-dialog.js';
6
6
  export function PermissionListPage() {
7
7
  const state = new PermissionsPageState();
8
8
  const dialog = PermissionFormDialog({ onSubmit: savePermission });
9
- const confirmDialog = vDialog();
10
9
  const tree = vTree({ ariaLabel: '权限树' });
11
10
 
12
11
  state.subscribe(() => {
@@ -39,23 +38,18 @@ export function PermissionListPage() {
39
38
  }));
40
39
  }
41
40
 
42
- function askRemove(node) {
43
- confirmDialog.content((content) => {
44
- content.p(`确定删除权限「${node.name}」及其子权限?`);
45
- content.hstack({ gap: '8px' }, (row) => {
46
- row.spacer();
47
- row.vButton('取消', (btn) => btn.on('click', () => confirmDialog.close()));
48
- row.vButton('删除', (btn) => {
49
- btn.variant('danger');
50
- btn.on('click', async () => {
51
- confirmDialog.close();
52
- await state.remove(node.id);
53
- toast.success(`已删除 ${node.name}`);
54
- });
55
- });
56
- });
41
+ async function askRemove(node) {
42
+ const ok = await vConfirm({
43
+ title: '删除权限',
44
+ content: `确定删除权限「${node.name}」及其子权限?`,
45
+ danger: true,
46
+ confirmText: '删除'
57
47
  });
58
- confirmDialog.open(true);
48
+ if (!ok) {
49
+ return;
50
+ }
51
+ await state.remove(node.id);
52
+ toast.success(`已删除 ${node.name}`);
59
53
  }
60
54
 
61
55
  async function savePermission(editingId, parentId, payload) {
@@ -88,7 +82,6 @@ export function PermissionListPage() {
88
82
  });
89
83
  });
90
84
  stack.child(dialog);
91
- stack.child(confirmDialog);
92
85
  });
93
86
  },
94
87
  refresh() {
@@ -26,16 +26,14 @@ declare class Create extends RequestBase {
26
26
  }
27
27
 
28
28
  declare class Update extends RequestBase {
29
- constructor(
30
- init?: {
31
- id: number;
32
- name?: string;
33
- code?: string;
34
- description?: string;
35
- status?: RoleStatus;
36
- sort?: number;
37
- }
38
- );
29
+ constructor(init?: {
30
+ id: number;
31
+ name?: string;
32
+ code?: string;
33
+ description?: string;
34
+ status?: RoleStatus;
35
+ sort?: number;
36
+ });
39
37
  method(): 'PUT';
40
38
  address(): string;
41
39
  body(): { name: string; code: string; description: string; status: RoleStatus; sort: number };
@@ -48,22 +46,22 @@ declare class Remove extends RequestBase {
48
46
  }
49
47
 
50
48
  declare const RoleMgr: {
51
- Query: (
52
- init?: { page?: number; pageSize?: number; keyword?: string; status?: string }
53
- ) => Query;
54
- Create: (
55
- init?: { name?: string; code?: string; description?: string; status?: RoleStatus; sort?: number }
56
- ) => Create;
57
- Update: (
58
- init?: {
59
- id: number;
60
- name?: string;
61
- code?: string;
62
- description?: string;
63
- status?: RoleStatus;
64
- sort?: number;
65
- }
66
- ) => Update;
49
+ Query: (init?: { page?: number; pageSize?: number; keyword?: string; status?: string }) => Query;
50
+ Create: (init?: {
51
+ name?: string;
52
+ code?: string;
53
+ description?: string;
54
+ status?: RoleStatus;
55
+ sort?: number;
56
+ }) => Create;
57
+ Update: (init?: {
58
+ id: number;
59
+ name?: string;
60
+ code?: string;
61
+ description?: string;
62
+ status?: RoleStatus;
63
+ sort?: number;
64
+ }) => Update;
67
65
  Remove: (init?: { id: number }) => Remove;
68
66
  };
69
67
 
@@ -2,7 +2,14 @@
2
2
  import { mockRequest } from '../../../../api/domain.api.js';
3
3
 
4
4
  let roles = [
5
- { id: 1, name: '超级管理员', code: 'super_admin', description: '拥有全部权限', status: 'active', sort: 1 },
5
+ {
6
+ id: 1,
7
+ name: '超级管理员',
8
+ code: 'super_admin',
9
+ description: '拥有全部权限',
10
+ status: 'active',
11
+ sort: 1
12
+ },
6
13
  { id: 2, name: '管理员', code: 'admin', description: '后台管理操作', status: 'active', sort: 2 },
7
14
  { id: 3, name: '运维', code: 'ops', description: '运维与部署', status: 'active', sort: 3 },
8
15
  { id: 4, name: '审计员', code: 'auditor', description: '只读审计', status: 'disabled', sort: 4 },
@@ -22,9 +29,7 @@ function filterRoles({ keyword = '', status = '' } = {}) {
22
29
  const text = String(keyword).trim().toLowerCase();
23
30
  return roles.filter((role) => {
24
31
  const matchKeyword =
25
- !text ||
26
- role.name.toLowerCase().includes(text) ||
27
- role.code.toLowerCase().includes(text);
32
+ !text || role.name.toLowerCase().includes(text) || role.code.toLowerCase().includes(text);
28
33
  const matchStatus = !status || role.status === status;
29
34
  return matchKeyword && matchStatus;
30
35
  });
@@ -46,15 +46,17 @@ export function RoleFormDialog({ onSubmit }) {
46
46
  item.styles(rowStyle);
47
47
  item.label('状态').name('status');
48
48
  item.control((editor) =>
49
- editor.vSelect({ name: 'status', options: statusOptions, value: role?.status ?? 'active' })
49
+ editor.vSelect({
50
+ name: 'status',
51
+ options: statusOptions,
52
+ value: role?.status ?? 'active'
53
+ })
50
54
  );
51
55
  });
52
56
  form.vFormItem((item) => {
53
57
  item.styles(rowStyle);
54
58
  item.label('排序').name('sort');
55
- item.control((editor) =>
56
- editor.vInput({ name: 'sort', value: String(role?.sort ?? 0) })
57
- );
59
+ item.control((editor) => editor.vInput({ name: 'sort', value: String(role?.sort ?? 0) }));
58
60
  });
59
61
  form.vButton('保存', (btn) => {
60
62
  btn.variant('primary');
@@ -27,9 +27,7 @@ export function RoleToolbar({ onSearch, onAdd }) {
27
27
  f.vFormItem((item) => {
28
28
  item.style({ display: 'flex', alignItems: 'center', gap: '8px' });
29
29
  item.label('关键词').name('keyword');
30
- item.control((editor) =>
31
- editor.vInput({ name: 'keyword', placeholder: '名称 / 标识' })
32
- );
30
+ item.control((editor) => editor.vInput({ name: 'keyword', placeholder: '名称 / 标识' }));
33
31
  });
34
32
  f.vFormItem((item) => {
35
33
  item.style({ display: 'flex', alignItems: 'center', gap: '8px' });
@@ -1,4 +1,4 @@
1
- import { toast, vDialog, vPagination, vstack } from '@yoyaflow/yoya-ui';
1
+ import { toast, vConfirm, vPagination, vstack } from '@yoyaflow/yoya-ui';
2
2
  import RolesPageState from '../api/role.state.js';
3
3
  import { RoleToolbar } from '../components/role-toolbar.js';
4
4
  import { RoleTable } from '../components/role-table.js';
@@ -7,7 +7,6 @@ import { RoleFormDialog } from '../components/role-form-dialog.js';
7
7
  export function RoleListPage() {
8
8
  const state = new RolesPageState();
9
9
  const dialog = RoleFormDialog({ onSubmit: saveRole });
10
- const confirmDialog = vDialog();
11
10
  const toolbar = RoleToolbar({ onSearch: applyFilters, onAdd: () => dialog.open(null) });
12
11
  const table = RoleTable({
13
12
  rows: () => state.items(),
@@ -46,23 +45,18 @@ export function RoleListPage() {
46
45
  state.load();
47
46
  }
48
47
 
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
- });
48
+ async function askRemove(role) {
49
+ const ok = await vConfirm({
50
+ title: '删除角色',
51
+ content: `确定删除角色「${role.name}」?`,
52
+ danger: true,
53
+ confirmText: '删除'
64
54
  });
65
- confirmDialog.open(true);
55
+ if (!ok) {
56
+ return;
57
+ }
58
+ await state.remove(role.id);
59
+ toast.success(`已删除 ${role.name}`);
66
60
  }
67
61
 
68
62
  async function saveRole(editingId, payload) {
@@ -90,7 +84,6 @@ export function RoleListPage() {
90
84
  });
91
85
  });
92
86
  stack.child(dialog);
93
- stack.child(confirmDialog);
94
87
  });
95
88
  },
96
89
  refresh() {
@@ -6,12 +6,14 @@ import './features/system/permissions/api/permission.mock.js';
6
6
  import './features/system/roles/api/role.mock.js';
7
7
  import './features/system/dicts/api/dict.mock.js';
8
8
  import './shell/api/shell.mock.js';
9
+ import './shell/api/auth.mock.js';
9
10
  import ShellState from './shell/api/shell.state.js';
10
11
  import { AdminShell } from './shell/components/admin-shell.js';
11
12
 
12
13
  initYoyaTheme();
13
14
 
14
15
  async function bootstrap() {
16
+ // load() 内部先加载会话并注入全局权限,再按权限过滤菜单 / 创建路由。
15
17
  const state = new ShellState();
16
18
  await state.load();
17
19
  const shell = AdminShell({ state });
@@ -0,0 +1,23 @@
1
+ // 会话演示用内存 mock(接入真实后端后删除本文件)。
2
+ // 想观察权限效果:增删下方权限码即可。
3
+ // 例如去掉 'ops:deploy' 隐藏「部署任务」菜单;去掉 'system:member:remove' 禁用「删除」按钮。
4
+ import { mockRequest } from '../../api/domain.api.js';
5
+
6
+ mockRequest('GET', '/auth/me', async () => ({
7
+ ok: true,
8
+ data: {
9
+ user: { name: '管理员', account: 'admin' },
10
+ roles: ['admin'],
11
+ permissions: [
12
+ 'dashboard:overview',
13
+ 'dashboard:todos',
14
+ 'ops:service',
15
+ 'system:member',
16
+ 'system:member:create',
17
+ 'system:member:update',
18
+ 'system:role',
19
+ 'system:permission',
20
+ 'system:dict'
21
+ ]
22
+ }
23
+ }));
@@ -13,8 +13,18 @@ mockRequest('GET', '/menus', async () => {
13
13
  label: '工作台',
14
14
  icon: 'D',
15
15
  routes: [
16
- { path: '/dashboard/overview', title: '数据概览', viewKey: 'dashboard-overview' },
17
- { path: '/dashboard/todos', title: '待办审批', viewKey: 'todo-approval' }
16
+ {
17
+ path: '/dashboard/overview',
18
+ title: '数据概览',
19
+ viewKey: 'dashboard-overview',
20
+ permCode: 'dashboard:overview'
21
+ },
22
+ {
23
+ path: '/dashboard/todos',
24
+ title: '待办审批',
25
+ viewKey: 'todo-approval',
26
+ permCode: 'dashboard:todos'
27
+ }
18
28
  ]
19
29
  },
20
30
  {
@@ -22,8 +32,18 @@ mockRequest('GET', '/menus', async () => {
22
32
  label: '运维',
23
33
  icon: 'O',
24
34
  routes: [
25
- { path: '/ops/services', title: '服务清单', viewKey: 'service-list' },
26
- { path: '/ops/deploys', title: '部署任务', viewKey: 'deploy-list' }
35
+ {
36
+ path: '/ops/services',
37
+ title: '服务清单',
38
+ viewKey: 'service-list',
39
+ permCode: 'ops:service'
40
+ },
41
+ {
42
+ path: '/ops/deploys',
43
+ title: '部署任务',
44
+ viewKey: 'deploy-list',
45
+ permCode: 'ops:deploy'
46
+ }
27
47
  ]
28
48
  },
29
49
  {
@@ -31,10 +51,30 @@ mockRequest('GET', '/menus', async () => {
31
51
  label: '系统',
32
52
  icon: 'S',
33
53
  routes: [
34
- { path: '/system/members', title: '成员管理', viewKey: 'member-list' },
35
- { path: '/system/roles', title: '角色管理', viewKey: 'role-list' },
36
- { path: '/system/permissions', title: '权限管理', viewKey: 'permission-list' },
37
- { path: '/system/dicts', title: '字典管理', viewKey: 'dict-list' }
54
+ {
55
+ path: '/system/members',
56
+ title: '成员管理',
57
+ viewKey: 'member-list',
58
+ permCode: 'system:member'
59
+ },
60
+ {
61
+ path: '/system/roles',
62
+ title: '角色管理',
63
+ viewKey: 'role-list',
64
+ permCode: 'system:role'
65
+ },
66
+ {
67
+ path: '/system/permissions',
68
+ title: '权限管理',
69
+ viewKey: 'permission-list',
70
+ permCode: 'system:permission'
71
+ },
72
+ {
73
+ path: '/system/dicts',
74
+ title: '字典管理',
75
+ viewKey: 'dict-list',
76
+ permCode: 'system:dict'
77
+ }
38
78
  ]
39
79
  }
40
80
  ]
@@ -12,6 +12,13 @@ class QueryMenus extends RequestBase {
12
12
  }
13
13
  }
14
14
 
15
+ class Me extends RequestBase {
16
+ address() {
17
+ return '/auth/me';
18
+ }
19
+ }
20
+
15
21
  export default {
16
- QueryMenus: (init) => new QueryMenus(init)
22
+ QueryMenus: (init) => new QueryMenus(init),
23
+ Me: (init) => new Me(init)
17
24
  };
@@ -1,5 +1,6 @@
1
- // 外壳状态类:菜单数据 + 导航状态(当前模块 / 当前路径)的唯一事实源,
2
- // 加载时创建并持有路由,暴露导航动作(switchModule / navigate),由路由订阅驱动 syncFromPath。
1
+ // 外壳状态类:会话(用户/角色/权限)+ 菜单 + 导航状态(当前模块 / 当前路径)的唯一事实源。
2
+ // load() 先加载会话并注入全局权限上下文,再按权限过滤菜单、创建路由并订阅导航。
3
+ import { createAccess, currentAccess, installAccess } from '@yoyaflow/yoya-ui';
3
4
  import ShellReq from './shell.req.js';
4
5
  import { createAppRouter } from '../router.js';
5
6
 
@@ -9,6 +10,9 @@ export default class ShellState {
9
10
  this._router = null;
10
11
  this._activeModuleKey = null;
11
12
  this._activePath = null;
13
+ this._user = null;
14
+ this._permissions = [];
15
+ this._roles = [];
12
16
  this._listeners = new Set();
13
17
  this._unsubscribeRouter = null;
14
18
  }
@@ -17,6 +21,18 @@ export default class ShellState {
17
21
  return this._menus;
18
22
  }
19
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
+
20
36
  router() {
21
37
  return this._router;
22
38
  }
@@ -35,14 +51,26 @@ export default class ShellState {
35
51
  }
36
52
 
37
53
  async load() {
54
+ await this.loadSession();
55
+
38
56
  const result = await ShellReq.QueryMenus().submit();
39
- this._menus = result.data;
57
+ this._menus = filterMenusByAccess(result.data);
40
58
  this._router = createAppRouter(this._menus);
41
59
  this._unsubscribeRouter = this._router.subscribe((context) => this.syncFromPath(context.path));
42
60
  this._emit();
43
61
  return result;
44
62
  }
45
63
 
64
+ // 加载会话:当前用户 / 角色 / 权限码,并注入全局权限上下文。
65
+ async loadSession() {
66
+ const result = await ShellReq.Me().submit();
67
+ this._user = result.data.user;
68
+ this._permissions = [...(result.data.permissions || [])];
69
+ this._roles = [...(result.data.roles || [])];
70
+ installAccess(createAccess({ permissions: this.permissions(), roles: this.roles() }));
71
+ return result;
72
+ }
73
+
46
74
  start() {
47
75
  this._router?.start();
48
76
  return this;
@@ -64,11 +92,8 @@ export default class ShellState {
64
92
  }
65
93
 
66
94
  syncFromPath(path) {
67
- const module = this._menus.find((entry) =>
68
- entry.routes.some((route) => route.path === path)
69
- );
70
- const changed =
71
- this._activePath !== path || (module && this._activeModuleKey !== module.key);
95
+ const module = this._menus.find((entry) => entry.routes.some((route) => route.path === path));
96
+ const changed = this._activePath !== path || (module && this._activeModuleKey !== module.key);
72
97
  this._activePath = path;
73
98
  if (module) {
74
99
  this._activeModuleKey = module.key;
@@ -88,3 +113,18 @@ export default class ShellState {
88
113
  this._listeners.forEach((listener) => listener());
89
114
  }
90
115
  }
116
+
117
+ // 菜单按当前访问上下文过滤:无 permCode 的路由放行,无读权限的路由隐藏。
118
+ function filterMenusByAccess(menus) {
119
+ const access = currentAccess();
120
+ if (!access) {
121
+ return menus;
122
+ }
123
+
124
+ return menus
125
+ .map((module) => ({
126
+ ...module,
127
+ routes: module.routes.filter((route) => !route.permCode || access.canRead(route.permCode))
128
+ }))
129
+ .filter((module) => module.routes.length > 0);
130
+ }
@@ -1,9 +1,10 @@
1
1
  // 外壳结果结构(纯数据):菜单模块与路由,服务端返回后由 toItem 映射。
2
2
  class Route {
3
- constructor({ path, title, viewKey }) {
3
+ constructor({ path, title, viewKey, permCode = null }) {
4
4
  this.path = path;
5
5
  this.title = title;
6
6
  this.viewKey = viewKey;
7
+ this.permCode = permCode;
7
8
  }
8
9
  }
9
10
 
@@ -35,6 +35,9 @@ export function createAppRouter(menus) {
35
35
  });
36
36
  r.notFound(() => PlaceholderPage({ title: '未找到', text: '该页面不存在。' }));
37
37
  });
38
- router.default(menus[0].routes[0].path);
38
+ const firstPath = menus[0]?.routes?.[0]?.path;
39
+ if (firstPath) {
40
+ router.default(firstPath);
41
+ }
39
42
  return router;
40
43
  }
@@ -1,6 +1,22 @@
1
1
  import { defineConfig } from 'vite';
2
+ import { existsSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ // 本地仓库联调:模板直接使用 yoya-ui 的 src 源码(含最新权限能力)。
6
+ // 脚手架生成的项目(模板被复制出去)不存在该路径,自动回退到 npm 包解析。
7
+ const localUi = fileURLToPath(new URL('../../../src/index.js', import.meta.url));
8
+ const localCss = fileURLToPath(new URL('../../../src/yoya.ui.css', import.meta.url));
9
+ const useLocal = existsSync(localUi);
2
10
 
3
11
  export default defineConfig({
12
+ resolve: useLocal
13
+ ? {
14
+ alias: [
15
+ { find: /^@yoyaflow\/yoya-ui\/ui\.css$/, replacement: localCss },
16
+ { find: '@yoyaflow/yoya-ui', replacement: localUi }
17
+ ]
18
+ }
19
+ : undefined,
4
20
  server: {
5
21
  port: 5173
6
22
  }
@@ -9,7 +9,7 @@
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "dependencies": {
12
- "@yoyaflow/yoya-ui": "^0.3.0"
12
+ "@yoyaflow/yoya-ui": "^0.3.3"
13
13
  },
14
14
  "devDependencies": {
15
15
  "vite": "^8.2.0"
@@ -1,4 +1,4 @@
1
- import { div, toast, vBody, vButton, vCard, vForm, vFormItem, vInput, vSelect, vText } from '@yoyaflow/yoya-ui';
1
+ import { div, toast, vBody, vForm, vText } from '@yoyaflow/yoya-ui';
2
2
  import '@yoyaflow/yoya-ui/ui.css';
3
3
 
4
4
  // 页面壳:消费主题 token,明暗切换自动跟随
@@ -9,7 +9,7 @@
9
9
  "preview": "npm run build && npm start"
10
10
  },
11
11
  "dependencies": {
12
- "@yoyaflow/yoya-ui": "^0.3.0"
12
+ "@yoyaflow/yoya-ui": "^0.3.3"
13
13
  },
14
14
  "devDependencies": {
15
15
  "vite": "^8.2.0"