create-yoya-ui 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +3 -0
  2. package/bin/create-yoya-ui.js +4 -2
  3. package/package.json +1 -1
  4. package/templates/admin/README.md +77 -0
  5. package/templates/admin/index.html +12 -0
  6. package/templates/admin/node_modules/.vite/deps/_metadata.json +8 -0
  7. package/templates/admin/node_modules/.vite/deps/package.json +3 -0
  8. package/templates/admin/package.json +19 -0
  9. package/templates/admin/src/api/domain.api.d.ts +10 -0
  10. package/templates/admin/src/api/domain.api.js +31 -0
  11. package/templates/admin/src/api/fetch.api.d.ts +4 -0
  12. package/templates/admin/src/api/fetch.api.js +20 -0
  13. package/templates/admin/src/app/admin-shell.js +51 -0
  14. package/templates/admin/src/app/modules.js +37 -0
  15. package/templates/admin/src/app/router.js +20 -0
  16. package/templates/admin/src/features/dashboard/components/dashboard-chart.js +205 -0
  17. package/templates/admin/src/features/dashboard/pages/dashboard-overview-page.js +197 -0
  18. package/templates/admin/src/features/dicts/api/dict.mgr.d.ts +79 -0
  19. package/templates/admin/src/features/dicts/api/dict.mgr.js +160 -0
  20. package/templates/admin/src/features/dicts/api/dict.mock.js +81 -0
  21. package/templates/admin/src/features/dicts/api/dict.state.d.ts +23 -0
  22. package/templates/admin/src/features/dicts/api/dict.state.js +103 -0
  23. package/templates/admin/src/features/dicts/api/dict.views.d.ts +41 -0
  24. package/templates/admin/src/features/dicts/api/dict.views.js +23 -0
  25. package/templates/admin/src/features/dicts/components/dict-editor-dialog.js +218 -0
  26. package/templates/admin/src/features/dicts/components/dict-item-form-dialog.js +79 -0
  27. package/templates/admin/src/features/dicts/pages/dict-list-page.js +123 -0
  28. package/templates/admin/src/features/dicts/utils/options.js +5 -0
  29. package/templates/admin/src/features/members/api/member.mgr.d.ts +47 -0
  30. package/templates/admin/src/features/members/api/member.mgr.js +95 -0
  31. package/templates/admin/src/features/members/api/member.mock.js +74 -0
  32. package/templates/admin/src/features/members/api/member.req.d.ts +16 -0
  33. package/templates/admin/src/features/members/api/member.req.js +15 -0
  34. package/templates/admin/src/features/members/api/member.state.d.ts +23 -0
  35. package/templates/admin/src/features/members/api/member.state.js +97 -0
  36. package/templates/admin/src/features/members/api/member.views.d.ts +33 -0
  37. package/templates/admin/src/features/members/api/member.views.js +23 -0
  38. package/templates/admin/src/features/members/components/member-form-dialog.js +83 -0
  39. package/templates/admin/src/features/members/components/member-table.js +58 -0
  40. package/templates/admin/src/features/members/components/member-toolbar.js +58 -0
  41. package/templates/admin/src/features/members/pages/member-list-page.js +85 -0
  42. package/templates/admin/src/features/members/utils/options.d.ts +4 -0
  43. package/templates/admin/src/features/members/utils/options.js +6 -0
  44. package/templates/admin/src/features/permissions/api/permission.mgr.d.ts +45 -0
  45. package/templates/admin/src/features/permissions/api/permission.mgr.js +79 -0
  46. package/templates/admin/src/features/permissions/api/permission.mock.js +68 -0
  47. package/templates/admin/src/features/permissions/api/permission.state.d.ts +14 -0
  48. package/templates/admin/src/features/permissions/api/permission.state.js +46 -0
  49. package/templates/admin/src/features/permissions/api/permission.views.d.ts +27 -0
  50. package/templates/admin/src/features/permissions/api/permission.views.js +14 -0
  51. package/templates/admin/src/features/permissions/components/permission-form-dialog.js +98 -0
  52. package/templates/admin/src/features/permissions/pages/permission-list-page.js +98 -0
  53. package/templates/admin/src/features/roles/api/role.mgr.d.ts +57 -0
  54. package/templates/admin/src/features/roles/api/role.mgr.js +101 -0
  55. package/templates/admin/src/features/roles/api/role.mock.js +68 -0
  56. package/templates/admin/src/features/roles/api/role.state.d.ts +23 -0
  57. package/templates/admin/src/features/roles/api/role.state.js +97 -0
  58. package/templates/admin/src/features/roles/api/role.views.d.ts +43 -0
  59. package/templates/admin/src/features/roles/api/role.views.js +24 -0
  60. package/templates/admin/src/features/roles/components/role-form-dialog.js +86 -0
  61. package/templates/admin/src/features/roles/components/role-table.js +57 -0
  62. package/templates/admin/src/features/roles/components/role-toolbar.js +58 -0
  63. package/templates/admin/src/features/roles/pages/role-list-page.js +100 -0
  64. package/templates/admin/src/features/roles/utils/options.js +5 -0
  65. package/templates/admin/src/main.js +18 -0
  66. package/templates/admin/src/ui/app-navbar.js +67 -0
  67. package/templates/admin/src/ui/app-sidebar.js +27 -0
  68. package/templates/admin/src/ui/placeholder-page.js +17 -0
  69. package/templates/admin/src/ui/row-action-button.js +12 -0
  70. package/templates/admin/src/ui/sidebar-menu.js +14 -0
  71. package/templates/admin/tsconfig.json +18 -0
  72. package/templates/admin/vite.config.js +7 -0
  73. package/templates/basic/package.json +1 -1
  74. package/templates/ssr/package.json +1 -1
  75. package/templates/ssr/src/server.mjs +0 -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;