create-yoya-ui 0.4.0 → 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.
Files changed (56) hide show
  1. package/package.json +3 -2
  2. package/templates/admin/RULES.md +22 -9
  3. package/templates/admin/package.json +1 -1
  4. package/templates/admin/src/api/domain.api.d.ts +1 -1
  5. package/templates/admin/src/api/domain.api.js +1 -1
  6. package/templates/admin/src/api/fetch.api.d.ts +1 -1
  7. package/templates/admin/src/features/dashboard/overview/pages/dashboard-overview-page.js +2 -2
  8. package/templates/admin/src/features/system/dicts/api/dict.mgr.d.ts +1 -1
  9. package/templates/admin/src/features/system/dicts/api/dict.mgr.js +1 -1
  10. package/templates/admin/src/features/system/dicts/api/dict.state.d.ts +14 -11
  11. package/templates/admin/src/features/system/dicts/api/dict.state.js +35 -70
  12. package/templates/admin/src/features/system/dicts/api/dict.views.d.ts +36 -23
  13. package/templates/admin/src/features/system/dicts/api/dict.views.js +36 -14
  14. package/templates/admin/src/features/system/dicts/components/dict-editor-dialog.js +53 -73
  15. package/templates/admin/src/features/system/dicts/components/dict-item-form-dialog.js +9 -6
  16. package/templates/admin/src/features/system/dicts/pages/dict-list-page.js +57 -59
  17. package/templates/admin/src/features/system/members/api/member.mgr.d.ts +1 -1
  18. package/templates/admin/src/features/system/members/api/member.mgr.js +1 -1
  19. package/templates/admin/src/features/system/members/api/member.req.d.ts +1 -1
  20. package/templates/admin/src/features/system/members/api/member.req.js +1 -1
  21. package/templates/admin/src/features/system/members/api/member.state.d.ts +10 -8
  22. package/templates/admin/src/features/system/members/api/member.state.js +24 -55
  23. package/templates/admin/src/features/system/members/api/member.views.d.ts +19 -5
  24. package/templates/admin/src/features/system/members/api/member.views.js +22 -8
  25. package/templates/admin/src/features/system/members/components/member-form-dialog.js +10 -6
  26. package/templates/admin/src/features/system/members/components/member-table.js +44 -54
  27. package/templates/admin/src/features/system/members/pages/member-list-page.js +19 -14
  28. package/templates/admin/src/features/system/permissions/api/permission.mgr.d.ts +1 -1
  29. package/templates/admin/src/features/system/permissions/api/permission.mgr.js +1 -1
  30. package/templates/admin/src/features/system/permissions/api/permission.state.d.ts +5 -3
  31. package/templates/admin/src/features/system/permissions/api/permission.state.js +4 -18
  32. package/templates/admin/src/features/system/permissions/api/permission.views.js +2 -1
  33. package/templates/admin/src/features/system/permissions/components/permission-form-dialog.js +1 -1
  34. package/templates/admin/src/features/system/permissions/pages/permission-list-page.js +9 -6
  35. package/templates/admin/src/features/system/roles/api/role.mgr.d.ts +1 -1
  36. package/templates/admin/src/features/system/roles/api/role.mgr.js +1 -1
  37. package/templates/admin/src/features/system/roles/api/role.state.d.ts +10 -8
  38. package/templates/admin/src/features/system/roles/api/role.state.js +22 -55
  39. package/templates/admin/src/features/system/roles/api/role.views.d.ts +24 -15
  40. package/templates/admin/src/features/system/roles/api/role.views.js +22 -9
  41. package/templates/admin/src/features/system/roles/components/role-form-dialog.js +11 -7
  42. package/templates/admin/src/features/system/roles/components/role-table.js +35 -47
  43. package/templates/admin/src/features/system/roles/pages/role-list-page.js +18 -14
  44. package/templates/admin/src/shared/state.rows.js +17 -0
  45. package/templates/admin/src/shell/api/shell.req.js +1 -1
  46. package/templates/admin/src/shell/api/shell.state.js +23 -55
  47. package/templates/admin/src/shell/components/admin-shell.js +4 -15
  48. package/templates/admin/src/shell/components/app-navbar.js +48 -56
  49. package/templates/admin/src/shell/components/app-sidebar.js +20 -31
  50. package/templates/admin/src/shell/components/sidebar-menu.js +1 -1
  51. package/templates/basic/package.json +1 -1
  52. package/templates/ssr/README.md +1 -1
  53. package/templates/ssr/package.json +1 -1
  54. package/templates/ssr/src/server.mjs +3 -0
  55. package/templates/admin/node_modules/.vite/deps/_metadata.json +0 -8
  56. package/templates/admin/node_modules/.vite/deps/package.json +0 -3
@@ -1,4 +1,5 @@
1
- import { toast, vConfirm, vDialog, vTable } from '@yoyaflow/yoya-ui';
1
+ import { computed, toast, vConfirm, vDialog, vText, vTr } from '@yoyaflow/yoya-ui';
2
+ import { fieldValue } from '../../../../shared/state.rows.js';
2
3
  import { RowActionButton } from '../../../../shared/ui.buttons.js';
3
4
  import { DictItemFormDialog } from './dict-item-form-dialog.js';
4
5
  import { statusOptions, statusText } from '../utils/options.js';
@@ -12,24 +13,28 @@ const rowStyle = {
12
13
  };
13
14
 
14
15
  // 字典编辑弹窗:上半部分是字典基本信息表单,下半部分维护该字典的字典值表格。
16
+ // 字典值表格绑 state.items 句柄,增删改写入信号后自动刷新,不需要手动重建行。
15
17
  export function DictEditorDialog({ state, onSubmit }) {
16
18
  const dialog = vDialog();
17
19
  const itemDialog = DictItemFormDialog({ onSubmit: saveItem });
18
20
  let editingId = null;
19
- let itemsBody = null;
20
- let itemsCountText = null;
21
- let addItemButton = null;
22
21
  let form = null;
23
22
 
24
23
  dialog.style('maxWidth', 'min(92vw, 880px)');
25
24
 
26
25
  function open(type = null) {
27
26
  editingId = type?.id ?? null;
28
- state.selectType(editingId).then(() => renderItems());
27
+ // 写入 state.items(ref)即刷新弹窗里的字典值表格
28
+ state.selectType(editingId);
29
+
30
+ const name = fieldValue(type?.name) ?? '';
31
+ const code = fieldValue(type?.code) ?? '';
32
+ const status = fieldValue(type?.status) ?? 'active';
33
+ const remark = fieldValue(type?.remark) ?? '';
29
34
 
30
35
  dialog.content((content) => {
31
36
  content.vstack({ gap: '16px' }, (stack) => {
32
- stack.h3(type ? `编辑字典「${type.name}」` : '新增字典');
37
+ stack.h3(type ? `编辑字典「${name}」` : '新增字典');
33
38
 
34
39
  stack.vForm((editor) => {
35
40
  form = editor;
@@ -44,30 +49,24 @@ export function DictEditorDialog({ state, onSubmit }) {
44
49
  editor.vFormItem((item) => {
45
50
  item.styles(rowStyle);
46
51
  item.label('名称').name('name').required({ message: '请填写名称' });
47
- item.control((control) => control.vInput({ name: 'name', value: type?.name ?? '' }));
52
+ item.control((control) => control.vInput({ name: 'name', value: name }));
48
53
  });
49
54
  editor.vFormItem((item) => {
50
55
  item.styles(rowStyle);
51
56
  item.label('编码').name('code').required({ message: '请填写编码' });
52
- item.control((control) => control.vInput({ name: 'code', value: type?.code ?? '' }));
57
+ item.control((control) => control.vInput({ name: 'code', value: code }));
53
58
  });
54
59
  editor.vFormItem((item) => {
55
60
  item.styles(rowStyle);
56
61
  item.label('状态').name('status');
57
62
  item.control((control) =>
58
- control.vSelect({
59
- name: 'status',
60
- options: statusOptions,
61
- value: type?.status ?? 'active'
62
- })
63
+ control.vSelect({ name: 'status', options: statusOptions, value: status })
63
64
  );
64
65
  });
65
66
  editor.vFormItem((item) => {
66
67
  item.styles(rowStyle);
67
68
  item.label('备注').name('remark');
68
- item.control((control) =>
69
- control.vInput({ name: 'remark', value: type?.remark ?? '' })
70
- );
69
+ item.control((control) => control.vInput({ name: 'remark', value: remark }));
71
70
  });
72
71
  });
73
72
 
@@ -75,7 +74,6 @@ export function DictEditorDialog({ state, onSubmit }) {
75
74
  section.vstack({ gap: '10px' }, (items) => {
76
75
  items.hstack({ alignItems: 'center', gap: '10px' }, (toolbar) => {
77
76
  toolbar.vButton('新增字典值', (btn) => {
78
- addItemButton = btn;
79
77
  btn.variant('primary');
80
78
  btn.disabled(editingId === null);
81
79
  btn.on('click', () => itemDialog.open(null));
@@ -83,33 +81,33 @@ export function DictEditorDialog({ state, onSubmit }) {
83
81
  toolbar.spacer();
84
82
  toolbar.span((count) => {
85
83
  count.style('color', 'var(--yoya-color-text-muted, #64748b)');
86
- itemsCountText = count;
87
- syncItemsCount();
84
+ count.child(vText(computed(() => `共 ${state.items.value.length} 条`)));
88
85
  });
89
86
  });
90
87
  items.p((hint) => {
91
88
  hint.style('color', 'var(--yoya-color-text-muted, #64748b)');
92
- hint.text(
89
+ hint.child(
93
90
  editingId === null ? '保存字典类型后即可添加字典值。' : '维护当前字典的字典值。'
94
91
  );
95
92
  });
96
- items.child(
97
- vTable((table) => {
98
- table.vThead((head) => {
99
- head.vTr((row) => {
100
- row.vTh('标签');
101
- row.vTh('值');
102
- row.vTh('排序');
103
- row.vTh('状态');
104
- row.vTh('操作');
105
- });
93
+ items.vTable((table) => {
94
+ table.vThead((head) => {
95
+ head.vTr((row) => {
96
+ row.vTh('标签');
97
+ row.vTh('值');
98
+ row.vTh('排序');
99
+ row.vTh('状态');
100
+ row.vTh('操作');
106
101
  });
107
- table.vTbody((body) => {
108
- itemsBody = body;
109
- renderItems();
110
- });
111
- })
112
- );
102
+ });
103
+ table.vTbody((tbody) => {
104
+ tbody.keyed(
105
+ state.items,
106
+ (item) => item.id,
107
+ (item) => buildItemRow(item)
108
+ );
109
+ });
110
+ });
113
111
  });
114
112
  });
115
113
 
@@ -128,48 +126,32 @@ export function DictEditorDialog({ state, onSubmit }) {
128
126
  dialog.open(true);
129
127
  }
130
128
 
131
- function renderItems() {
132
- if (!itemsBody) {
133
- return;
134
- }
135
- itemsBody.children().forEach((child) => child.destroy());
136
- state.items().forEach((item) => {
137
- itemsBody.vTr((row) => {
138
- row.vTd(item.label);
139
- row.vTd(item.value);
140
- row.vTd(String(item.sort));
141
- row.vTd(statusText[item.status] ?? item.status);
142
- row.vTd((cell) => {
143
- cell.hstack({ gap: '8px' }, (actions) => {
144
- actions.child(
145
- RowActionButton('编辑', (btn) => btn.on('click', () => itemDialog.open(item)))
146
- );
147
- actions.child(
148
- RowActionButton('删除', (btn) => {
149
- btn.variant('danger');
150
- btn.on('click', () => askRemoveItem(item));
151
- })
152
- );
153
- });
129
+ function buildItemRow(item) {
130
+ return vTr((row) => {
131
+ row.vTd(vText(item.label));
132
+ row.vTd(vText(item.value));
133
+ row.vTd(vText(computed(() => String(item.sort.value))));
134
+ row.vTd(vText(computed(() => statusText[item.status.value] ?? item.status.value)));
135
+ row.vTd((cell) => {
136
+ cell.hstack({ gap: '8px' }, (actions) => {
137
+ actions.child(
138
+ RowActionButton('编辑', (btn) => btn.on('click', () => itemDialog.open(item)))
139
+ );
140
+ actions.child(
141
+ RowActionButton('删除', (btn) => {
142
+ btn.variant('danger');
143
+ btn.on('click', () => askRemoveItem(item));
144
+ })
145
+ );
154
146
  });
155
147
  });
156
148
  });
157
- syncItemsCount();
158
- }
159
-
160
- function syncItemsCount() {
161
- if (itemsCountText) {
162
- itemsCountText.textContent(`共 ${state.items().length} 条`);
163
- }
164
- if (addItemButton) {
165
- addItemButton.disabled(editingId === null);
166
- }
167
149
  }
168
150
 
169
151
  async function askRemoveItem(item) {
170
152
  const ok = await vConfirm({
171
153
  title: '删除字典值',
172
- content: `确定删除字典值「${item.label}」?`,
154
+ content: `确定删除字典值「${item.label.value}」?`,
173
155
  danger: true,
174
156
  confirmText: '删除'
175
157
  });
@@ -177,8 +159,7 @@ export function DictEditorDialog({ state, onSubmit }) {
177
159
  return;
178
160
  }
179
161
  await state.removeItem(item.id);
180
- toast.success(`已删除 ${item.label}`);
181
- renderItems();
162
+ toast.success(`已删除字典值 ${item.label.value}`);
182
163
  }
183
164
 
184
165
  async function saveItem(itemId, payload) {
@@ -189,7 +170,6 @@ export function DictEditorDialog({ state, onSubmit }) {
189
170
  await state.editItem(itemId, payload);
190
171
  toast.success('已更新字典值');
191
172
  }
192
- renderItems();
193
173
  }
194
174
 
195
175
  function save() {
@@ -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,10 @@ export function DictItemFormDialog({ onSubmit }) {
15
16
 
16
17
  function open(item = null) {
17
18
  editingId = item?.id ?? null;
19
+ const label = fieldValue(item?.label) ?? '';
20
+ const value = fieldValue(item?.value) ?? '';
21
+ const sort = fieldValue(item?.sort) ?? 0;
22
+ const status = fieldValue(item?.status) ?? 'active';
18
23
  dialog.content((content) => {
19
24
  content.vForm((form) => {
20
25
  form.styles({
@@ -28,19 +33,17 @@ export function DictItemFormDialog({ onSubmit }) {
28
33
  form.vFormItem((field) => {
29
34
  field.styles(rowStyle);
30
35
  field.label('标签').name('label').required({ message: '请填写标签' });
31
- field.control((editor) => editor.vInput({ name: 'label', value: item?.label ?? '' }));
36
+ field.control((editor) => editor.vInput({ name: 'label', value: label }));
32
37
  });
33
38
  form.vFormItem((field) => {
34
39
  field.styles(rowStyle);
35
40
  field.label('值').name('value').required({ message: '请填写值' });
36
- field.control((editor) => editor.vInput({ name: 'value', value: item?.value ?? '' }));
41
+ field.control((editor) => editor.vInput({ name: 'value', value }));
37
42
  });
38
43
  form.vFormItem((field) => {
39
44
  field.styles(rowStyle);
40
45
  field.label('排序').name('sort');
41
- field.control((editor) =>
42
- editor.vInput({ name: 'sort', value: String(item?.sort ?? 0) })
43
- );
46
+ field.control((editor) => editor.vInput({ name: 'sort', value: String(sort) }));
44
47
  });
45
48
  form.vFormItem((field) => {
46
49
  field.styles(rowStyle);
@@ -49,7 +52,7 @@ export function DictItemFormDialog({ onSubmit }) {
49
52
  editor.vSelect({
50
53
  name: 'status',
51
54
  options: statusOptions,
52
- value: item?.status ?? 'active'
55
+ value: status
53
56
  })
54
57
  );
55
58
  });
@@ -1,4 +1,4 @@
1
- import { toast, vConfirm, vPagination, vTable, vstack } from '@yoyaflow/yoya-ui';
1
+ import { computed, toast, vConfirm, vPagination, vText, vTr, 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';
@@ -13,58 +13,33 @@ export function DictListPage() {
13
13
  onChange({ page: nextPage, pageSize: nextSize }) {
14
14
  state.setPage(nextPage);
15
15
  state.setPageSize(nextSize);
16
- state.loadTypes();
16
+ load();
17
17
  }
18
18
  });
19
- let typesBody = null;
20
19
 
21
- state.subscribe(() => {
22
- renderTypes();
20
+ load();
21
+
22
+ // vPagination 是命令式组件(不是信号感知的):数据到位后由动作同步一次
23
+ async function load() {
24
+ await state.loadTypes();
23
25
  syncPagination();
24
- });
25
- state.loadTypes();
26
+ }
26
27
 
27
28
  function syncPagination() {
29
+ const total = state.total.value;
30
+ const pageSize = state.pageSize.value;
28
31
  pagination.update({
29
- page: state.page(),
30
- pageSize: state.pageSize(),
31
- total: state.total(),
32
- totalPages: Math.max(1, Math.ceil(state.total() / state.pageSize()))
33
- });
34
- }
35
-
36
- function renderTypes() {
37
- if (!typesBody) {
38
- return;
39
- }
40
- typesBody.children().forEach((child) => child.destroy());
41
- state.types().forEach((type) => {
42
- typesBody.vTr((row) => {
43
- row.vTd(type.name);
44
- row.vTd(type.code);
45
- row.vTd(statusText[type.status] ?? type.status);
46
- row.vTd(type.remark || '—');
47
- row.vTd((cell) => {
48
- cell.hstack({ gap: '8px' }, (actions) => {
49
- actions.child(
50
- RowActionButton('编辑', (btn) => btn.on('click', () => editorDialog.open(type)))
51
- );
52
- actions.child(
53
- RowActionButton('删除', (btn) => {
54
- btn.variant('danger');
55
- btn.on('click', () => askRemoveType(type));
56
- })
57
- );
58
- });
59
- });
60
- });
32
+ page: state.page.value,
33
+ pageSize,
34
+ total,
35
+ totalPages: Math.max(1, Math.ceil(total / pageSize))
61
36
  });
62
37
  }
63
38
 
64
39
  async function askRemoveType(type) {
65
40
  const ok = await vConfirm({
66
41
  title: '删除字典',
67
- content: `确定删除字典「${type.name}」及其所有字典值?`,
42
+ content: `确定删除字典「${type.name.value}」及其所有字典值?`,
68
43
  danger: true,
69
44
  confirmText: '删除'
70
45
  });
@@ -72,7 +47,7 @@ export function DictListPage() {
72
47
  return;
73
48
  }
74
49
  await state.removeType(type.id);
75
- toast.success(`已删除 ${type.name}`);
50
+ toast.success(`已删除 ${type.name.value}`);
76
51
  }
77
52
 
78
53
  async function saveType(editingId, payload) {
@@ -85,6 +60,28 @@ export function DictListPage() {
85
60
  }
86
61
  }
87
62
 
63
+ function buildTypeRow(type) {
64
+ return vTr((row) => {
65
+ row.vTd(vText(type.name));
66
+ row.vTd(vText(type.code));
67
+ row.vTd(vText(computed(() => statusText[type.status.value] ?? type.status.value)));
68
+ row.vTd(vText(computed(() => type.remark.value || '—')));
69
+ row.vTd((cell) => {
70
+ cell.hstack({ gap: '8px' }, (actions) => {
71
+ actions.child(
72
+ RowActionButton('编辑', (btn) => btn.on('click', () => editorDialog.open(type)))
73
+ );
74
+ actions.child(
75
+ RowActionButton('删除', (btn) => {
76
+ btn.variant('danger');
77
+ btn.on('click', () => askRemoveType(type));
78
+ })
79
+ );
80
+ });
81
+ });
82
+ });
83
+ }
84
+
88
85
  return {
89
86
  render() {
90
87
  return vstack({ gap: '16px' }, (stack) => {
@@ -99,29 +96,30 @@ export function DictListPage() {
99
96
  toolbar.spacer();
100
97
  toolbar.span((hint) => {
101
98
  hint.style('color', 'var(--yoya-color-text-muted, #64748b)');
102
- hint.text('点击「编辑」可在弹窗中维护基本信息与字典值');
99
+ hint.child('点击「编辑」可在弹窗中维护基本信息与字典值');
103
100
  });
104
101
  });
105
102
  });
106
103
  card.vCardBody((body) => {
107
104
  body.vstack({ gap: '12px' }, (content) => {
108
- content.child(
109
- vTable((table) => {
110
- table.vThead((head) => {
111
- head.vTr((row) => {
112
- row.vTh('名称');
113
- row.vTh('编码');
114
- row.vTh('状态');
115
- row.vTh('备注');
116
- row.vTh('操作');
117
- });
105
+ content.vTable((table) => {
106
+ table.vThead((head) => {
107
+ head.vTr((row) => {
108
+ row.vTh('名称');
109
+ row.vTh('编码');
110
+ row.vTh('状态');
111
+ row.vTh('备注');
112
+ row.vTh('操作');
118
113
  });
119
- table.vTbody((bodyNode) => {
120
- typesBody = bodyNode;
121
- renderTypes();
122
- });
123
- })
124
- );
114
+ });
115
+ table.vTbody((tbody) => {
116
+ tbody.keyed(
117
+ state.types,
118
+ (type) => type.id,
119
+ (type) => buildTypeRow(type)
120
+ );
121
+ });
122
+ });
125
123
  content.child(pagination);
126
124
  });
127
125
  });
@@ -130,7 +128,7 @@ export function DictListPage() {
130
128
  });
131
129
  },
132
130
  refresh() {
133
- return state.loadTypes();
131
+ return load();
134
132
  }
135
133
  };
136
134
  }
@@ -1,5 +1,5 @@
1
1
  /** 管理请求命令类型声明(对应 member.mgr.js)。 */
2
- import { RequestBase } from '@yoyaflow/yoya-ui';
2
+ import { RequestBase } from '@yoyaflow/yoya-ui/api';
3
3
  import Members from './member.views.js';
4
4
 
5
5
  type MemberListItem = InstanceType<typeof Members.ListItem>;
@@ -1,5 +1,5 @@
1
1
  // 管理请求命令:字段在构造器声明,方法由 RequestBase 基类提供、子类覆写。
2
- import { RequestBase } from '@yoyaflow/yoya-ui';
2
+ import { RequestBase } from '@yoyaflow/yoya-ui/api';
3
3
  import Members from './member.views.js';
4
4
 
5
5
  class Query extends RequestBase {
@@ -1,5 +1,5 @@
1
1
  /** 外部请求命令类型声明(对应 member.req.js)。 */
2
- import { RequestBase } from '@yoyaflow/yoya-ui';
2
+ import { RequestBase } from '@yoyaflow/yoya-ui/api';
3
3
  import Members from './member.views.js';
4
4
 
5
5
  type MemberListItem = InstanceType<typeof Members.ListItem>;
@@ -1,5 +1,5 @@
1
1
  // 外部请求命令:其他模块调用本域能力的入口。
2
- import { RequestBase } from '@yoyaflow/yoya-ui';
2
+ import { RequestBase } from '@yoyaflow/yoya-ui/api';
3
3
  import Members from './member.views.js';
4
4
 
5
5
  class QueryAvailable extends RequestBase {
@@ -1,22 +1,24 @@
1
1
  /** 页面数据状态类类型声明(对应 member.state.js)。 */
2
+ import type { SignalHandle } from '@yoyaflow/yoya-ui';
3
+ import type { Result } from '@yoyaflow/yoya-ui/api';
2
4
  import Members from './member.views.js';
3
5
 
4
6
  type MemberListItem = InstanceType<typeof Members.ListItem>;
5
7
 
6
8
  export default class MembersPageState {
7
9
  constructor();
8
- items(): MemberListItem[];
9
- keyword(): string;
10
- status(): string;
11
- page(): number;
12
- pageSize(): number;
13
- total(): number;
10
+ /** 视图字段:页面与组件直接绑句柄,写入即更新 */
11
+ items: SignalHandle<MemberListItem[]>;
12
+ total: SignalHandle<number>;
13
+ page: SignalHandle<number>;
14
+ pageSize: SignalHandle<number>;
15
+ keyword: SignalHandle<string>;
16
+ status: SignalHandle<string>;
14
17
  setKeyword(value: string): void;
15
18
  setStatus(value: string): void;
16
19
  setPage(value: number): void;
17
20
  setPageSize(value: number): void;
18
- subscribe(listener: () => void): () => void;
19
- load(): Promise<unknown>;
21
+ load(): Promise<Result<MemberListItem[]>>;
20
22
  add(payload: Record<string, unknown>): Promise<Record<string, unknown>>;
21
23
  edit(id: number, patch: Record<string, unknown>): Promise<Record<string, unknown>>;
22
24
  remove(id: number): Promise<void>;
@@ -1,78 +1,51 @@
1
- // 页面数据状态类:持有数据与筛选,动作构造命令并 submit 后写入状态、通知订阅者。
1
+ // 页面数据状态类:要驱动视图的字段用 ref 持有——动作里写句柄即完成通知,
2
+ // 页面不需要订阅回调去手动 refresh。subscribe() 只留给非视图副作用(埋点 / 持久化)。
3
+ import { ref } from '@yoyaflow/yoya-ui';
4
+ import { mergeRowsByKey } from '../../../../shared/state.rows.js';
2
5
  import MemberMgr from './member.mgr.js';
3
6
 
4
7
  export default class MembersPageState {
5
8
  constructor() {
6
- this._items = [];
7
- this._keyword = '';
8
- this._status = '';
9
- this._page = 1;
10
- this._pageSize = 5;
11
- this._total = 0;
12
- this._listeners = new Set();
13
- }
14
-
15
- items() {
16
- return this._items;
17
- }
18
-
19
- keyword() {
20
- return this._keyword;
21
- }
22
-
23
- status() {
24
- return this._status;
25
- }
26
-
27
- page() {
28
- return this._page;
29
- }
30
-
31
- pageSize() {
32
- return this._pageSize;
33
- }
34
-
35
- total() {
36
- return this._total;
9
+ this.items = ref([]);
10
+ this.total = ref(0);
11
+ this.page = ref(1);
12
+ this.pageSize = ref(5);
13
+ this.keyword = ref('');
14
+ this.status = ref('');
37
15
  }
38
16
 
39
17
  setKeyword(value) {
40
- this._keyword = value;
18
+ this.keyword.value = value;
41
19
  }
42
20
 
43
21
  setStatus(value) {
44
- this._status = value;
22
+ this.status.value = value;
45
23
  }
46
24
 
47
25
  setPage(value) {
48
- this._page = Math.max(1, Number(value) || 1);
26
+ this.page.value = Math.max(1, Number(value) || 1);
49
27
  }
50
28
 
51
29
  setPageSize(value) {
52
- this._pageSize = Math.max(1, Number(value) || 5);
53
- }
54
-
55
- subscribe(listener) {
56
- this._listeners.add(listener);
57
- return () => this._listeners.delete(listener);
30
+ this.pageSize.value = Math.max(1, Number(value) || 5);
58
31
  }
59
32
 
60
33
  async load() {
61
34
  const result = await MemberMgr.Query({
62
- page: this._page,
63
- pageSize: this._pageSize,
64
- keyword: this._keyword,
65
- status: this._status
35
+ page: this.page.value,
36
+ pageSize: this.pageSize.value,
37
+ keyword: this.keyword.value,
38
+ status: this.status.value
66
39
  }).submit();
67
- this._items = result.data;
68
- this._total = result.total;
69
- this._emit();
40
+ // 按 key 合并:能复用的行保持实例身份,表格只刷变化的格子
41
+ this.items.value = mergeRowsByKey(this.items.peek(), result.data);
42
+ this.total.value = result.total;
70
43
  return result;
71
44
  }
72
45
 
73
46
  async add(payload) {
74
47
  await MemberMgr.Create(payload).submit();
75
- this._page = 1;
48
+ this.page.value = 1;
76
49
  await this.load();
77
50
  return payload;
78
51
  }
@@ -85,13 +58,9 @@ export default class MembersPageState {
85
58
 
86
59
  async remove(id) {
87
60
  await MemberMgr.Remove({ id }).submit();
88
- if (this._items.length === 1 && this._page > 1) {
89
- this._page -= 1;
61
+ if (this.items.peek().length === 1 && this.page.value > 1) {
62
+ this.page.value -= 1;
90
63
  }
91
64
  await this.load();
92
65
  }
93
-
94
- _emit() {
95
- this._listeners.forEach((listener) => listener());
96
- }
97
66
  }
@@ -1,11 +1,25 @@
1
1
  /** 领域结果结构类型声明(对应 member.views.js)。 */
2
+ import type { SignalHandle } from '@yoyaflow/yoya-ui';
3
+
4
+ export type MemberListRow = {
5
+ id: number;
6
+ name?: string;
7
+ email?: string;
8
+ role?: string;
9
+ status?: string;
10
+ };
11
+
2
12
  export class ListItem {
13
+ /** key:普通值,不能是句柄 */
3
14
  id: number;
4
- name: string;
5
- email: string;
6
- role: string;
7
- status: string;
8
- constructor(init: { id: number; name: string; email: string; role: string; status: string });
15
+ /** 行内热字段:句柄写入即刷新对应单元格 */
16
+ name: SignalHandle<string>;
17
+ email: SignalHandle<string>;
18
+ role: SignalHandle<string>;
19
+ status: SignalHandle<string>;
20
+ constructor(row?: MemberListRow);
21
+ /** 合并而非重建:实例身份不变 */
22
+ apply(row: MemberListRow): this;
9
23
  }
10
24
 
11
25
  export class Detail {