create-yoya-ui 0.1.2 → 0.2.1

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 +212 -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,23 @@
1
+ // 字典结果结构(纯数据)。
2
+ class DictTypeItem {
3
+ constructor({ id, name = '', code = '', status = 'active', remark = '' } = {}) {
4
+ this.id = id;
5
+ this.name = name;
6
+ this.code = code;
7
+ this.status = status;
8
+ this.remark = remark;
9
+ }
10
+ }
11
+
12
+ class DictItem {
13
+ constructor({ id, typeId = null, label = '', value = '', sort = 0, status = 'active' } = {}) {
14
+ this.id = id;
15
+ this.typeId = typeId;
16
+ this.label = label;
17
+ this.value = value;
18
+ this.sort = sort;
19
+ this.status = status;
20
+ }
21
+ }
22
+
23
+ export default { DictTypeItem, DictItem };
@@ -0,0 +1,218 @@
1
+ import { toast, vDialog, vTable } from '@yoyaflow/yoya-ui';
2
+ import { RowActionButton } from '../../../ui/row-action-button.js';
3
+ import { DictItemFormDialog } from './dict-item-form-dialog.js';
4
+ import { statusOptions, statusText } from '../utils/options.js';
5
+
6
+ const rowStyle = {
7
+ alignItems: 'center',
8
+ display: 'grid',
9
+ gap: '12px',
10
+ gridTemplateColumns: '88px minmax(0, 1fr)',
11
+ minWidth: '0'
12
+ };
13
+
14
+ // 字典编辑弹窗:上半部分是字典基本信息表单,下半部分维护该字典的字典值表格。
15
+ export function DictEditorDialog({ state, onSubmit }) {
16
+ const dialog = vDialog();
17
+ const itemDialog = DictItemFormDialog({ onSubmit: saveItem });
18
+ const confirmDialog = vDialog();
19
+ let editingId = null;
20
+ let itemsBody = null;
21
+ let itemsCountText = null;
22
+ let addItemButton = null;
23
+ let form = null;
24
+
25
+ dialog.style('maxWidth', 'min(92vw, 880px)');
26
+
27
+ function open(type = null) {
28
+ editingId = type?.id ?? null;
29
+ state.selectType(editingId).then(() => renderItems());
30
+
31
+ dialog.content((content) => {
32
+ content.vstack({ gap: '16px' }, (stack) => {
33
+ stack.h3(type ? `编辑字典「${type.name}」` : '新增字典');
34
+
35
+ stack.vForm((editor) => {
36
+ form = editor;
37
+ editor.styles({
38
+ border: '1px solid var(--yoya-color-border, #d8dee8)',
39
+ borderRadius: '8px',
40
+ display: 'grid',
41
+ gap: '16px',
42
+ gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
43
+ padding: '16px'
44
+ });
45
+ editor.vFormItem((item) => {
46
+ item.styles(rowStyle);
47
+ item.label('名称').name('name').required({ message: '请填写名称' });
48
+ item.control((control) => control.vInput({ name: 'name', value: type?.name ?? '' }));
49
+ });
50
+ editor.vFormItem((item) => {
51
+ item.styles(rowStyle);
52
+ item.label('编码').name('code').required({ message: '请填写编码' });
53
+ item.control((control) => control.vInput({ name: 'code', value: type?.code ?? '' }));
54
+ });
55
+ editor.vFormItem((item) => {
56
+ item.styles(rowStyle);
57
+ item.label('状态').name('status');
58
+ item.control((control) =>
59
+ control.vSelect({
60
+ name: 'status',
61
+ options: statusOptions,
62
+ value: type?.status ?? 'active'
63
+ })
64
+ );
65
+ });
66
+ editor.vFormItem((item) => {
67
+ item.styles(rowStyle);
68
+ item.label('备注').name('remark');
69
+ item.control((control) =>
70
+ control.vInput({ name: 'remark', value: type?.remark ?? '' })
71
+ );
72
+ });
73
+ });
74
+
75
+ stack.section((section) => {
76
+ section.vstack({ gap: '10px' }, (items) => {
77
+ items.hstack({ alignItems: 'center', gap: '10px' }, (toolbar) => {
78
+ toolbar.vButton('新增字典值', (btn) => {
79
+ addItemButton = btn;
80
+ btn.variant('primary');
81
+ btn.disabled(editingId === null);
82
+ btn.on('click', () => itemDialog.open(null));
83
+ });
84
+ toolbar.spacer();
85
+ toolbar.span((count) => {
86
+ count.style('color', 'var(--yoya-color-text-muted, #64748b)');
87
+ itemsCountText = count;
88
+ syncItemsCount();
89
+ });
90
+ });
91
+ items.p((hint) => {
92
+ hint.style('color', 'var(--yoya-color-text-muted, #64748b)');
93
+ hint.text(
94
+ editingId === null ? '保存字典类型后即可添加字典值。' : '维护当前字典的字典值。'
95
+ );
96
+ });
97
+ items.child(
98
+ vTable((table) => {
99
+ table.vThead((head) => {
100
+ head.vTr((row) => {
101
+ row.vTh('标签');
102
+ row.vTh('值');
103
+ row.vTh('排序');
104
+ row.vTh('状态');
105
+ row.vTh('操作');
106
+ });
107
+ });
108
+ table.vTbody((body) => {
109
+ itemsBody = body;
110
+ renderItems();
111
+ });
112
+ })
113
+ );
114
+ });
115
+ });
116
+
117
+ stack.hstack({ gap: '10px', justifyContent: 'flex-end' }, (footer) => {
118
+ footer.vButton('取消', (btn) => btn.on('click', () => dialog.close()));
119
+ footer.vButton('保存', (btn) => {
120
+ btn.variant('primary');
121
+ btn.on('click', () => save());
122
+ });
123
+ });
124
+
125
+ stack.child(itemDialog);
126
+ stack.child(confirmDialog);
127
+ });
128
+ });
129
+
130
+ dialog.open(true);
131
+ }
132
+
133
+ function renderItems() {
134
+ if (!itemsBody) {
135
+ return;
136
+ }
137
+ itemsBody.children().forEach((child) => child.destroy());
138
+ state.items().forEach((item) => {
139
+ itemsBody.vTr((row) => {
140
+ row.vTd(item.label);
141
+ row.vTd(item.value);
142
+ row.vTd(String(item.sort));
143
+ row.vTd(statusText[item.status] ?? item.status);
144
+ row.vTd((cell) => {
145
+ cell.hstack({ gap: '8px' }, (actions) => {
146
+ actions.child(
147
+ RowActionButton('编辑', (btn) => btn.on('click', () => itemDialog.open(item)))
148
+ );
149
+ actions.child(
150
+ RowActionButton('删除', (btn) => {
151
+ btn.variant('danger');
152
+ btn.on('click', () => askRemoveItem(item));
153
+ })
154
+ );
155
+ });
156
+ });
157
+ });
158
+ });
159
+ syncItemsCount();
160
+ }
161
+
162
+ function syncItemsCount() {
163
+ if (itemsCountText) {
164
+ itemsCountText.textContent(`共 ${state.items().length} 条`);
165
+ }
166
+ if (addItemButton) {
167
+ addItemButton.disabled(editingId === null);
168
+ }
169
+ }
170
+
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
+ });
187
+ });
188
+ confirmDialog.open(true);
189
+ }
190
+
191
+ async function saveItem(itemId, payload) {
192
+ if (itemId === null) {
193
+ await state.addItem(payload);
194
+ toast.success('已新增字典值');
195
+ } else {
196
+ await state.editItem(itemId, payload);
197
+ toast.success('已更新字典值');
198
+ }
199
+ renderItems();
200
+ }
201
+
202
+ function save() {
203
+ if (!form.validate()) {
204
+ return;
205
+ }
206
+ onSubmit(editingId, form.values()).then(() => dialog.close());
207
+ }
208
+
209
+ return {
210
+ render() {
211
+ return dialog;
212
+ },
213
+ open,
214
+ close() {
215
+ dialog.close();
216
+ }
217
+ };
218
+ }
@@ -0,0 +1,79 @@
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 DictItemFormDialog({ onSubmit }) {
13
+ const dialog = vDialog();
14
+ let editingId = null;
15
+
16
+ function open(item = null) {
17
+ editingId = item?.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((field) => {
29
+ field.styles(rowStyle);
30
+ field.label('标签').name('label').required({ message: '请填写标签' });
31
+ field.control((editor) => editor.vInput({ name: 'label', value: item?.label ?? '' }));
32
+ });
33
+ form.vFormItem((field) => {
34
+ field.styles(rowStyle);
35
+ field.label('值').name('value').required({ message: '请填写值' });
36
+ field.control((editor) => editor.vInput({ name: 'value', value: item?.value ?? '' }));
37
+ });
38
+ form.vFormItem((field) => {
39
+ field.styles(rowStyle);
40
+ field.label('排序').name('sort');
41
+ field.control((editor) =>
42
+ editor.vInput({ name: 'sort', value: String(item?.sort ?? 0) })
43
+ );
44
+ });
45
+ form.vFormItem((field) => {
46
+ field.styles(rowStyle);
47
+ field.label('状态').name('status');
48
+ field.control((editor) =>
49
+ editor.vSelect({ name: 'status', options: statusOptions, value: item?.status ?? 'active' })
50
+ );
51
+ });
52
+ form.vButton('保存', (btn) => {
53
+ btn.variant('primary');
54
+ btn.style({ gridColumn: '1 / -1', justifySelf: 'end', marginTop: '16px' });
55
+ btn.on('click', () => save(form));
56
+ });
57
+ });
58
+ });
59
+ dialog.open(true);
60
+ }
61
+
62
+ async function save(form) {
63
+ if (!form.validate()) {
64
+ return;
65
+ }
66
+ await onSubmit(editingId, form.values());
67
+ dialog.close();
68
+ }
69
+
70
+ return {
71
+ render() {
72
+ return dialog;
73
+ },
74
+ open,
75
+ close() {
76
+ dialog.close();
77
+ }
78
+ };
79
+ }
@@ -0,0 +1,123 @@
1
+ import { toast, vDialog, vTable, vstack } from '@yoyaflow/yoya-ui';
2
+ import { RowActionButton } from '../../../ui/row-action-button.js';
3
+ import DictsPageState from '../api/dict.state.js';
4
+ import { DictEditorDialog } from '../components/dict-editor-dialog.js';
5
+ import { statusText } from '../utils/options.js';
6
+
7
+ export function DictListPage() {
8
+ const state = new DictsPageState();
9
+ const editorDialog = DictEditorDialog({ state, onSubmit: saveType });
10
+ const confirmDialog = vDialog();
11
+ let typesBody = null;
12
+
13
+ state.subscribe(render);
14
+ state.loadTypes();
15
+
16
+ function render() {
17
+ renderTypes();
18
+ }
19
+
20
+ function renderTypes() {
21
+ if (!typesBody) {
22
+ return;
23
+ }
24
+ typesBody.children().forEach((child) => child.destroy());
25
+ state.types().forEach((type) => {
26
+ typesBody.vTr((row) => {
27
+ row.vTd(type.name);
28
+ row.vTd(type.code);
29
+ row.vTd(statusText[type.status] ?? type.status);
30
+ row.vTd(type.remark || '—');
31
+ row.vTd((cell) => {
32
+ cell.hstack({ gap: '8px' }, (actions) => {
33
+ actions.child(
34
+ RowActionButton('编辑', (btn) => btn.on('click', () => editorDialog.open(type)))
35
+ );
36
+ actions.child(
37
+ RowActionButton('删除', (btn) => {
38
+ btn.variant('danger');
39
+ btn.on('click', () => askRemoveType(type));
40
+ })
41
+ );
42
+ });
43
+ });
44
+ });
45
+ });
46
+ }
47
+
48
+ function askRemoveType(type) {
49
+ confirmDialog.content((content) => {
50
+ content.p(`确定删除字典「${type.name}」及其所有字典值?`);
51
+ content.hstack({ gap: '8px' }, (row) => {
52
+ row.spacer();
53
+ row.vButton('取消', (btn) => btn.on('click', () => confirmDialog.close()));
54
+ row.vButton('删除', (btn) => {
55
+ btn.variant('danger');
56
+ btn.on('click', async () => {
57
+ confirmDialog.close();
58
+ await state.removeType(type.id);
59
+ toast.success(`已删除 ${type.name}`);
60
+ });
61
+ });
62
+ });
63
+ });
64
+ confirmDialog.open(true);
65
+ }
66
+
67
+ async function saveType(editingId, payload) {
68
+ if (editingId === null) {
69
+ await state.addType(payload);
70
+ toast.success('已新增字典');
71
+ } else {
72
+ await state.editType(editingId, payload);
73
+ toast.success('已更新字典');
74
+ }
75
+ }
76
+
77
+ return {
78
+ render() {
79
+ return vstack({ gap: '16px' }, (stack) => {
80
+ stack.h2('字典管理');
81
+ stack.vCard((card) => {
82
+ card.vCardHeader((header) => {
83
+ header.hstack({ alignItems: 'center', gap: '10px' }, (toolbar) => {
84
+ toolbar.vButton('新增字典', (btn) => {
85
+ btn.variant('primary');
86
+ btn.on('click', () => editorDialog.open(null));
87
+ });
88
+ toolbar.spacer();
89
+ toolbar.span((hint) => {
90
+ hint.style('color', 'var(--yoya-color-text-muted, #64748b)');
91
+ hint.text('点击「编辑」可在弹窗中维护基本信息与字典值');
92
+ });
93
+ });
94
+ });
95
+ card.vCardBody((body) => {
96
+ body.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
+ });
106
+ });
107
+ table.vTbody((bodyNode) => {
108
+ typesBody = bodyNode;
109
+ renderTypes();
110
+ });
111
+ })
112
+ );
113
+ });
114
+ });
115
+ stack.child(editorDialog);
116
+ stack.child(confirmDialog);
117
+ });
118
+ },
119
+ refresh() {
120
+ return state.loadTypes();
121
+ }
122
+ };
123
+ }
@@ -0,0 +1,5 @@
1
+ export const statusOptions = [
2
+ ['active', '启用'],
3
+ ['disabled', '禁用']
4
+ ];
5
+ export const statusText = { active: '启用', disabled: '禁用' };
@@ -0,0 +1,47 @@
1
+ /** 管理请求命令类型声明(对应 member.mgr.js)。 */
2
+ import { RequestBase } from '@yoyaflow/yoya-ui';
3
+ import Members from './member.views.js';
4
+
5
+ type MemberListItem = InstanceType<typeof Members.ListItem>;
6
+ type MemberDetail = InstanceType<typeof Members.Detail>;
7
+
8
+ declare class Query extends RequestBase {
9
+ constructor(init?: { page?: number; pageSize?: number; keyword?: string; status?: string });
10
+ address(): string;
11
+ params(): { page: number; pageSize: number; keyword: string; status: string };
12
+ toItem(row: Record<string, unknown>): MemberListItem;
13
+ }
14
+
15
+ declare class Create extends RequestBase {
16
+ constructor(init?: { name?: string; email?: string; role?: string; status?: string });
17
+ method(): 'POST';
18
+ address(): string;
19
+ body(): { name: string; email: string; role: string; status: string };
20
+ toDetail(member: Record<string, unknown>): MemberDetail;
21
+ }
22
+
23
+ declare class Update extends RequestBase {
24
+ constructor(
25
+ id: number,
26
+ init?: { name?: string; email?: string; role?: string; status?: string }
27
+ );
28
+ method(): 'PUT';
29
+ address(): string;
30
+ body(): { name: string; email: string; role: string; status: string };
31
+ toDetail(member: Record<string, unknown>): MemberDetail;
32
+ }
33
+
34
+ declare class Remove extends RequestBase {
35
+ constructor(id: number);
36
+ method(): 'DELETE';
37
+ address(): string;
38
+ }
39
+
40
+ declare const MemberMgr: {
41
+ Query: typeof Query;
42
+ Create: typeof Create;
43
+ Update: typeof Update;
44
+ Remove: typeof Remove;
45
+ };
46
+
47
+ export default MemberMgr;
@@ -0,0 +1,95 @@
1
+ // 管理请求命令:字段在构造器声明,方法由 RequestBase 基类提供、子类覆写。
2
+ import { RequestBase } from '@yoyaflow/yoya-ui';
3
+ import Members from './member.views.js';
4
+
5
+ class Query extends RequestBase {
6
+ constructor({ page = 1, pageSize = 10, keyword = '', status = '' } = {}) {
7
+ super();
8
+ this.page = page;
9
+ this.pageSize = pageSize;
10
+ this.keyword = keyword;
11
+ this.status = status;
12
+ }
13
+
14
+ address() {
15
+ return '/members';
16
+ }
17
+
18
+ params() {
19
+ return { page: this.page, pageSize: this.pageSize, keyword: this.keyword, status: this.status };
20
+ }
21
+
22
+ toItem(row) {
23
+ return new Members.ListItem(row);
24
+ }
25
+ }
26
+
27
+ class Create extends RequestBase {
28
+ constructor({ name = '', email = '', role = 'viewer', status = 'active' } = {}) {
29
+ super();
30
+ this.name = name;
31
+ this.email = email;
32
+ this.role = role;
33
+ this.status = status;
34
+ }
35
+
36
+ method() {
37
+ return 'POST';
38
+ }
39
+
40
+ address() {
41
+ return '/members';
42
+ }
43
+
44
+ body() {
45
+ return { name: this.name, email: this.email, role: this.role, status: this.status };
46
+ }
47
+
48
+ toDetail(member) {
49
+ return new Members.Detail(member);
50
+ }
51
+ }
52
+
53
+ class Update extends RequestBase {
54
+ constructor(id, { name = '', email = '', role = 'viewer', status = 'active' } = {}) {
55
+ super();
56
+ this.id = id;
57
+ this.name = name;
58
+ this.email = email;
59
+ this.role = role;
60
+ this.status = status;
61
+ }
62
+
63
+ method() {
64
+ return 'PUT';
65
+ }
66
+
67
+ address() {
68
+ return `/members/${this.id}`;
69
+ }
70
+
71
+ body() {
72
+ return { name: this.name, email: this.email, role: this.role, status: this.status };
73
+ }
74
+
75
+ toDetail(member) {
76
+ return new Members.Detail(member);
77
+ }
78
+ }
79
+
80
+ class Remove extends RequestBase {
81
+ constructor(id) {
82
+ super();
83
+ this.id = id;
84
+ }
85
+
86
+ method() {
87
+ return 'DELETE';
88
+ }
89
+
90
+ address() {
91
+ return `/members/${this.id}`;
92
+ }
93
+ }
94
+
95
+ export default { Query, Create, Update, Remove };
@@ -0,0 +1,74 @@
1
+ // 演示用内存 mock(接入真实后端后删除本文件)。
2
+ import { mockRequest } from '../../../api/domain.api.js';
3
+
4
+ let members = [
5
+ { id: 1, name: '张伟', email: 'zhangwei@example.com', role: 'admin', status: 'active' },
6
+ { id: 2, name: '李娜', email: 'lina@example.com', role: 'editor', status: 'active' },
7
+ { id: 3, name: '王强', email: 'wangqiang@example.com', role: 'viewer', status: 'disabled' },
8
+ { id: 4, name: '赵敏', email: 'zhaomin@example.com', role: 'editor', status: 'active' },
9
+ { id: 5, name: '刘洋', email: 'liuyang@example.com', role: 'viewer', status: 'active' },
10
+ { id: 6, name: '陈静', email: 'chenjing@example.com', role: 'editor', status: 'disabled' },
11
+ { id: 7, name: '杨帆', email: 'yangfan@example.com', role: 'admin', status: 'active' },
12
+ { id: 8, name: '黄磊', email: 'huanglei@example.com', role: 'viewer', status: 'active' },
13
+ { id: 9, name: '周婷', email: 'zhouting@example.com', role: 'editor', status: 'active' },
14
+ { id: 10, name: '吴迪', email: 'wudi@example.com', role: 'viewer', status: 'disabled' },
15
+ { id: 11, name: '徐静', email: 'xujing@example.com', role: 'editor', status: 'active' },
16
+ { id: 12, name: '孙鹏', email: 'sunpeng@example.com', role: 'admin', status: 'active' },
17
+ { id: 13, name: '马丽', email: 'mali@example.com', role: 'viewer', status: 'active' }
18
+ ];
19
+
20
+ const delay = (ms = 200) => new Promise((resolve) => setTimeout(resolve, ms));
21
+
22
+ function filterMembers({ keyword = '', status = '' } = {}) {
23
+ const text = String(keyword).trim().toLowerCase();
24
+ return members.filter((member) => {
25
+ const matchKeyword =
26
+ !text ||
27
+ member.name.toLowerCase().includes(text) ||
28
+ member.email.toLowerCase().includes(text);
29
+ const matchStatus = !status || member.status === status;
30
+ return matchKeyword && matchStatus;
31
+ });
32
+ }
33
+
34
+ mockRequest('GET', '/members', async (req) => {
35
+ await delay();
36
+ const { page = 1, pageSize = 5 } = req.params();
37
+ const filtered = filterMembers(req.params());
38
+ const start = (page - 1) * pageSize;
39
+ return {
40
+ ok: true,
41
+ code: '0',
42
+ data: filtered.slice(start, start + pageSize),
43
+ pageNum: page,
44
+ pageSize,
45
+ total: filtered.length
46
+ };
47
+ });
48
+
49
+ mockRequest('GET', '/members/available', async () => {
50
+ await delay();
51
+ return { ok: true, code: '0', data: members.filter((member) => member.status === 'active') };
52
+ });
53
+
54
+ mockRequest('POST', '/members', async (req) => {
55
+ await delay();
56
+ const member = { id: Math.max(0, ...members.map((item) => item.id)) + 1, ...req.body() };
57
+ members = [member, ...members];
58
+ return { ok: true, code: '0', data: member };
59
+ });
60
+
61
+ mockRequest('PUT', /^\/members\/\d+$/, async (req) => {
62
+ await delay();
63
+ const id = Number(req.address().split('/').pop());
64
+ const member = { ...members.find((item) => item.id === id), ...req.body(), id };
65
+ members = members.map((item) => (item.id === id ? member : item));
66
+ return { ok: true, code: '0', data: member };
67
+ });
68
+
69
+ mockRequest('DELETE', /^\/members\/\d+$/, async (req) => {
70
+ await delay();
71
+ const id = Number(req.address().split('/').pop());
72
+ members = members.filter((item) => item.id !== id);
73
+ return { ok: true, code: '0', data: null };
74
+ });
@@ -0,0 +1,16 @@
1
+ /** 外部请求命令类型声明(对应 member.req.js)。 */
2
+ import { RequestBase } from '@yoyaflow/yoya-ui';
3
+ import Members from './member.views.js';
4
+
5
+ type MemberListItem = InstanceType<typeof Members.ListItem>;
6
+
7
+ declare class QueryAvailable extends RequestBase {
8
+ address(): string;
9
+ toItem(row: Record<string, unknown>): MemberListItem;
10
+ }
11
+
12
+ declare const MemberReq: {
13
+ QueryAvailable: typeof QueryAvailable;
14
+ };
15
+
16
+ export default MemberReq;