create-yoya-ui 0.1.2 → 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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/templates/admin/README.md +54 -2
- package/templates/admin/node_modules/.vite/deps/_metadata.json +8 -0
- package/templates/admin/node_modules/.vite/deps/package.json +3 -0
- package/templates/admin/package.json +4 -2
- package/templates/admin/src/api/domain.api.d.ts +10 -0
- package/templates/admin/src/api/domain.api.js +31 -0
- package/templates/admin/src/api/fetch.api.d.ts +4 -0
- package/templates/admin/src/api/fetch.api.js +20 -0
- package/templates/admin/src/app/admin-shell.js +51 -0
- package/templates/admin/src/app/modules.js +37 -0
- package/templates/admin/src/app/router.js +20 -0
- package/templates/admin/src/features/dashboard/components/dashboard-chart.js +205 -0
- package/templates/admin/src/features/dashboard/pages/dashboard-overview-page.js +197 -0
- package/templates/admin/src/features/dicts/api/dict.mgr.d.ts +79 -0
- package/templates/admin/src/features/dicts/api/dict.mgr.js +160 -0
- package/templates/admin/src/features/dicts/api/dict.mock.js +81 -0
- package/templates/admin/src/features/dicts/api/dict.state.d.ts +23 -0
- package/templates/admin/src/features/dicts/api/dict.state.js +103 -0
- package/templates/admin/src/features/dicts/api/dict.views.d.ts +41 -0
- package/templates/admin/src/features/dicts/api/dict.views.js +23 -0
- package/templates/admin/src/features/dicts/components/dict-editor-dialog.js +218 -0
- package/templates/admin/src/features/dicts/components/dict-item-form-dialog.js +79 -0
- package/templates/admin/src/features/dicts/pages/dict-list-page.js +123 -0
- package/templates/admin/src/features/dicts/utils/options.js +5 -0
- package/templates/admin/src/features/members/api/member.mgr.d.ts +47 -0
- package/templates/admin/src/features/members/api/member.mgr.js +95 -0
- package/templates/admin/src/features/members/api/member.mock.js +74 -0
- package/templates/admin/src/features/members/api/member.req.d.ts +16 -0
- package/templates/admin/src/features/members/api/member.req.js +15 -0
- package/templates/admin/src/features/members/api/member.state.d.ts +23 -0
- package/templates/admin/src/features/members/api/member.state.js +97 -0
- package/templates/admin/src/features/members/api/member.views.d.ts +33 -0
- package/templates/admin/src/features/members/api/member.views.js +23 -0
- package/templates/admin/src/features/members/components/member-form-dialog.js +83 -0
- package/templates/admin/src/features/members/components/member-table.js +58 -0
- package/templates/admin/src/features/members/components/member-toolbar.js +58 -0
- package/templates/admin/src/features/members/pages/member-list-page.js +85 -0
- package/templates/admin/src/features/members/utils/options.d.ts +4 -0
- package/templates/admin/src/features/members/utils/options.js +6 -0
- package/templates/admin/src/features/permissions/api/permission.mgr.d.ts +45 -0
- package/templates/admin/src/features/permissions/api/permission.mgr.js +79 -0
- package/templates/admin/src/features/permissions/api/permission.mock.js +68 -0
- package/templates/admin/src/features/permissions/api/permission.state.d.ts +14 -0
- package/templates/admin/src/features/permissions/api/permission.state.js +46 -0
- package/templates/admin/src/features/permissions/api/permission.views.d.ts +27 -0
- package/templates/admin/src/features/permissions/api/permission.views.js +14 -0
- package/templates/admin/src/features/permissions/components/permission-form-dialog.js +98 -0
- package/templates/admin/src/features/permissions/pages/permission-list-page.js +98 -0
- package/templates/admin/src/features/roles/api/role.mgr.d.ts +57 -0
- package/templates/admin/src/features/roles/api/role.mgr.js +101 -0
- package/templates/admin/src/features/roles/api/role.mock.js +68 -0
- package/templates/admin/src/features/roles/api/role.state.d.ts +23 -0
- package/templates/admin/src/features/roles/api/role.state.js +97 -0
- package/templates/admin/src/features/roles/api/role.views.d.ts +43 -0
- package/templates/admin/src/features/roles/api/role.views.js +24 -0
- package/templates/admin/src/features/roles/components/role-form-dialog.js +86 -0
- package/templates/admin/src/features/roles/components/role-table.js +57 -0
- package/templates/admin/src/features/roles/components/role-toolbar.js +58 -0
- package/templates/admin/src/features/roles/pages/role-list-page.js +100 -0
- package/templates/admin/src/features/roles/utils/options.js +5 -0
- package/templates/admin/src/main.js +17 -165
- package/templates/admin/src/ui/app-navbar.js +67 -0
- package/templates/admin/src/ui/app-sidebar.js +27 -0
- package/templates/admin/src/ui/placeholder-page.js +17 -0
- package/templates/admin/src/ui/row-action-button.js +12 -0
- package/templates/admin/src/ui/sidebar-menu.js +14 -0
- package/templates/admin/tsconfig.json +18 -0
- package/templates/basic/package.json +1 -1
- 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,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;
|