增删改查源码解析通透篇、表单视图、列表视图
# 表单和列表的快速分解
# 列表视图
<template>
<div>
<!-- 表格组件 -->
<BasicTable @register="registerTable">
<!-- 表格标题插槽 -->
<template #tableTitle>
<Icon :icon="getTitle.icon" class="m-1 pr-1" />
<span> {{ getTitle.value }} </span>
</template>
<!-- 表格右侧按钮插槽,其中 v-auth 是按钮权限控制 -->
<template #toolbar>
<a-button type="primary" @click="handleForm({})" v-auth="'test:testData:edit'">
<Icon icon="i-fluent:add-12-filled" /> {{ t('新增') }}
</a-button>
</template>
<!-- 首列插槽 -->
<template #firstColumn="{ record }">
<a @click="handleForm({ id: record.id })">
{{ record.testInput }}
</a>
</template>
</BasicTable>
<!-- 点击表格行进入的输入表单弹窗 -->
<InputForm @register="registerDrawer" @success="handleSuccess" />
</div>
</template>
<script lang="ts">
export default defineComponent({
// 当前组件名称(与路由名一致,如果不一致会页面缓存失效)
name: 'ViewsTestTestDataList',
});
</script>
<script lang="ts" setup>
// 导入当前用到的对象,部分省略
import { defineComponent } from 'vue';
import InputForm from './form.vue';
// 国际化方法调用,参数是国际化编码的根路径
const { t } = useI18n('test.testData');
// 消息弹窗方法
const { showMessage } = useMessage();
// 当前页面标题定义,来自菜单管理定义
const getTitle = {
icon: router.currentRoute.value.meta.icon || 'i-ant-design:book-outlined',
value: router.currentRoute.value.meta.title || t('数据管理'),
};
// 表格搜索表单控件定义
const searchForm: FormProps = {
baseColProps: { lg: 6, md: 8 }, // 表单栅格布局
labelWidth: 90, // 表单标签宽度
schemas: [
{
label: t('单行文本'), // 表单标签
field: 'testInput', // 字段提交参数名
component: 'Input', // 表单控件
},
{
label: t('下拉框'),
field: 'testSelect',
component: 'Select', // 选择框还有 RadioGroup、CheckboxGroup
componentProps: {
dictType: 'sys_menu_type', // 下拉框选项数据(支持直接指定字典类型)
allowClear: true, // 启用空选项,可清空选择
mode: 'multiple', // 下拉框模块,启用多选
},
},
// 更多控件,再次不展示了,和上一节表单视图一致
],
};
// 表格列定义
const tableColumns: BasicColumn[] = [
{
title: t('单行文本'), // 表头标题
dataIndex: 'testInput', // 表列实体属性名
key: 'a.test_input', // 排序数据库字段名
sorter: true, // 点击表头是否可排序
width: 230, // 列宽
align: 'left', // 列的对齐方式
// 个性化列,可定义插槽(如样式,增加控件等)
slots: { customRender: 'firstColumn' }, // 5.1.0 及之前
slot: 'firstColumn', // 5.2.0+ (Antdv 3.x 不再使用 slots)
},
{
title: t('下拉框'),
dataIndex: 'testSelect',
key: 'a.test_select',
sorter: true,
width: 130,
align: 'center',
dictType: 'sys_menu_type', // 字典列,快速显示字典标签
},
];
// 表格操作列定义
const actionColumn: BasicColumn = {
width: 160, // 操作列宽
actions: (record: Recordable) => [
{
icon: 'i-clarity:note-edit-line',
title: t('编辑数据'),
onClick: handleForm.bind(this, { id: record.id }),
// 按钮权限控制,指定权限字符串
auth: 'test:testData:edit',
},
{
icon: 'i-ant-design:stop-outlined',
color: 'error',
title: t('停用数据'),
// 是否需要启用确认框
popConfirm: {
title: t('是否确认停用数据'),
confirm: handleDisable.bind(this, { id: record.id }),
},
// 按钮权限控制,指定权限字符串
auth: 'test:testData:edit',
// 通过业务数据控制,按钮是否显示
ifShow: () => record.status === '0',
},
],
// 操作列更多按钮定义
dropDownActions: (record: Recordable) => [
{
icon: 'i-ant-design:reload-outlined',
label: t('重置密码'),
onClick: handleResetpwd.bind(this, { userCode: record.userCode }),
auth: 'sys:empUser:resetpwd',
},
],
};
// 点击首列或编辑按钮是的抽屉弹窗定义
const [registerDrawer, { openDrawer }] = useDrawer();
// 表格定义
const [registerTable, { reload }] = useTable({
api: testDataListData, // 表格数据源 API
beforeFetch: (params) => {
return params; // API 提交之前的参数修改
},
columns: tableColumns, // 表格列
actionColumn: actionColumn,// 操作列
formConfig: searchForm, // 搜索表单
showTableSetting: true, // 是否显示右上角的设置按钮
useSearchForm: true, // 是否显示搜索表单
canResize: true, // 是否自适应表单高度
});
// 弹窗操作方法
function handleForm(record: Recordable) {
openDrawer(true, record);
}
// 操作列停用按钮方法
async function handleDisable(record: Recordable) {
const res = await testDataDisable(record);
showMessage(res.message);
handleSuccess();
}
// 刷新表格数据(含表单回调)
function handleSuccess() {
reload();
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# 表单视图
<template>
<!-- 弹出抽屉组件,如果想改为弹窗,Drawer 换为 Modal 即可快速替换 -->
<BasicDrawer
v-bind="$attrs" -- 传递来自父组件的属性
:showFooter="true" -- 显示弹窗底部按钮组
:okAuth="'test:testData:edit'" -- 提交按钮权限,控制按钮是否显示
@register="registerDrawer" -- 弹窗后的回调方法
@ok="handleSubmit" -- 提交按钮调用方法
width="60%" -- 弹窗宽度,支持按比例
>
<!-- 弹窗标题 -->
<template #title>
<Icon :icon="getTitle.icon" class="pr-1 m-1" /> -- 图标
<span> {{ getTitle.value }} </span> -- 标题名称
</template>
<!-- 表单组件 -->
<BasicForm @register="registerForm">
<!-- 定义表单控件插槽、个性化表单控件,如:这是一个表单子表插槽 -->
<template #testDataChildList>
<BasicTable
@register="registerTestDataChildTable"
@row-click="handleTestDataChildRowClick"
/>
<!-- 子表新增按钮 -->
<a-button class="mt-2" @click="handleTestDataChildAdd">
<Icon icon="i-ant-design:plus-circle-outlined" /> {{ t('新增') }}
</a-button>
</template>
</BasicForm>
</BasicDrawer>
</template>
<script lang="ts">
export default defineComponent({
// 当前组件名称(与路由名一致,如果不一致会页面缓存失效)
name: 'ViewsTestTestDataForm',
});
</script>
<script lang="ts" setup>
// 导入当前用到的对象,部分省略
import { defineComponent, ref, computed } from 'vue';
import { officeTreeData } from '/@/api/sys/office';
import { areaTreeData } from '/@/api/sys/area';
// 页面事件定义
const emit = defineEmits(['success', 'register']);
// 国际化方法调用,参数是国际化编码的根路径
const { t } = useI18n('test.testData');
// 消息弹窗方法
const { showMessage } = useMessage();
// 当前页面数据记录
const record = ref<Recordable>({});
// 当前页面标题定义,来自菜单管理定义
const getTitle = computed(() => ({
icon: router.currentRoute.value.meta.icon || 'i-ant-design:book-outlined',
value: record.value.isNewRecord ? t('新增数据') : t('编辑数据'),
}));
// 输入表单控件定义
const inputFormSchemas: FormSchema[] = [
{
label: t('单行文本'), // 控件前面的页签
field: 'testInput', // 字段提交参数名
component: 'Input', // 控件类型(可自定义,更多查看 componentMap.ts )
componentProps: { // 组件属性定义
maxlength: 200,
},
required: true, // 表单验证,是否必填(快速定义)
rules: [ // 如果不只是必填,需要通过 rules 定义,举例:
{ required: true },
{ min: 4, max: 20, message: t('请输入长度在 4 到 20 个字符之间') },
{ pattern: /^[\u0391-\uFFE5\w]+$/, message: t('不能输入特殊字符') },
{
validator(_rule, value) {
return new Promise((resolve, reject) => {
if (!value || value === '') return resolve();
// 远程验证,访问后台校验数据是否重复
checkTestInput(record.value.testInput || '', value)
.then((res) => (res ? resolve() : reject(t('数据已存在'))))
.catch((err) => reject(err.message || t('验证失败')));
});
},
trigger: 'blur', // 如果是远程验证,可以减少请求频率
},
],
colProps: { lg: 24, md: 24 }, // 栅格布局(遵循 Ant Design 风格)
// 控制控件是否显示(区别:show 是显示或隐藏;ifShow 是显示或移除)
show: () => record.status === '0',
ifShow: () => record.status === '0',
},
{
label: t('下拉框'),
field: 'testSelect',
component: 'Select', // 选择框还有 RadioGroup、CheckboxGroup
componentProps: {
dictType: 'sys_menu_type', // 下拉框选项数据(支持直接指定字典类型)
allowClear: true, // 启用空选项,可清空选择
mode: 'multiple', // 下拉框模块,启用多选
},
},
{
label: t('日期选择'),
field: 'testDate',
component: 'DatePicker',
componentProps: {
format: 'YYYY-MM-DD', // 日期选择
showTime: false, // 关闭时间选择
},
},
{
label: t('日期时间'),
field: 'testDatetime',
component: 'DatePicker',
componentProps: {
format: 'YYYY-MM-DD HH:mm', // 日期时间选择
showTime: { format: 'HH:mm' }, // 设置时间的格式
},
},
{
label: t('用户选择'),
field: 'testUser.userCode',
fieldLabel: 'testUser.userName', //【支持返回,如下拉框或树选择的节点名】
component: 'TreeSelect', // 树选择控件
componentProps: {
api: officeTreeData, // 数据源 API 定义,支持 ztree 格式
params: { isLoadUser: true, userIdPrefix: '' }, // API 参数
canSelectParent: false, // 是否允许选择父级
allowClear: true,
},
},
{
label: t('子表数据'),
field: 'testDataChildList',
component: 'Input',
colProps: { lg: 24, md: 24 },
slot: 'testDataChildList', // 指定插槽、个性化控件内容
},
];
// 当前表单的参数定义
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
labelWidth: 120, // 控件前面的标签宽度
schemas: inputFormSchemas, // 控件定义列表
baseColProps: { lg: 12, md: 24 }, // 控件默认栅格布局方式(响应式)
});
// 当前表单子表格定义
const [registerTestDataChildTable, testDataChildTable] = useTable({
actionColumn: { // 子表的操作列定义
width: 60, // 操作列宽度
actions: (record: Recordable) => [
{
icon: 'i-ant-design:delete-outlined',
color: 'error',
popConfirm: { // 是否需要启用确认框
title: '是否确认删除',
confirm: handleTestDataChildDelete.bind(this, record),
},
auth: 'sys:empUser:edit', // 按钮权限(可控制按钮是否显示)
},
],
},
rowKey: 'id', // 子表主键名
pagination: false,// 关闭分页
bordered: true, // 开启表格边框
size: 'small', // 单元格间距
inset: true, // 是否内嵌(去除一些边距)
});
// 当前表单子表自动定义
async function setTestDataChildTableData(_res: Recordable) {
testDataChildTable.setColumns([
{
title: t('单行文本'),
dataIndex: 'testInput',
width: 230,
align: 'left',
editRow: true, // 是否启用编辑
editComponent: 'Input', // 编辑控件(可自定义,更多查看 componentMap.ts )
editRule: true, // 控件验证(是否必填)
},
{
title: t('下拉框'),
dataIndex: 'testSelect',
width: 130,
align: 'left',
dictType: 'sys_menu_type', // 指定字典类型,自动显示字典标签
editRow: true,
editComponent: 'Select',
editComponentProps: { // 控件属性
dictType: 'sys_menu_type', // 下拉框的字段类型
allowClear: true,
},
editRule: false,
},
// 更多组件控件不举例了,同表单控件 ...
]);
// 设定子表数据
testDataChildTable.setTableData(record.value.testDataChildList || []);
}
// 点击行,启用编辑
function handleTestDataChildRowClick(record: Recordable) {
record.onEdit?.(true, false);
}
// 添加编辑行,可指定初始数据
function handleTestDataChildAdd() {
testDataChildTable.insertTableDataRecord({
id: new Date().getTime(),
isNewRecord: true,
editable: true,
});
}
// 删除编辑行方法
function handleTestDataChildDelete(record: Recordable) {
testDataChildTable.deleteTableDataRecord(record);
}
// 获取子表数据(支持返回删除未提交的数据)
async function getTestDataChildList() {
let testDataChildListValid = true;
let testDataChildList: Recordable[] = [];
for (const record of testDataChildTable.getDataSource()) {
// 验证控件内容,并取消行的编辑状态(如果验证失败返回false)
if (!(await record.onEdit?.(false, true))) {
testDataChildListValid = false;
}
testDataChildList.push({
...record,
id: !!record.isNewRecord ? '' : record.id,
});
}
for (const record of testDataChildTable.getDelDataSource()) {
if (!!record.isNewRecord) continue;
testDataChildList.push({
...record,
status: '1',
});
}
// 子表验证事件,抛出异常消息
if (!testDataChildListValid) {
throw new Error('testDataChildList valid.');
}
return testDataChildList;
}
// 弹窗后的回调事件,进行一些表单数据初始化等操作
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
resetFields(); // 重置表单数据
setDrawerProps({ loading: true }); // 显示加载框
const res = await testDataForm(data); // 查询表单数据
record.value = (res.testData || {}) as Recordable;
setFieldsValue(record.value); // 设置字段值
setTestDataChildTableData(res); // 设置子表数据(没有子表可不写)
setDrawerProps({ loading: false }); // 隐藏加载框
});
// 表单提交按钮方法
async function handleSubmit() {
try {
const data = await validate(); // 验证表单,并返回数据
setDrawerProps({ confirmLoading: true }); // 显示提交加载中
// 设置提交的参数(QueryString,后台 Controller 的 get 接受)
const params: any = {
isNewRecord: record.value.isNewRecord,
id: record.value.id,
};
// 获取并设置子表数据
data.testDataChildList = await getTestDataChildList();
// console.log('submit', params, data, record);
// 将数据提交给后台(如果失败跳转到 catch)
const res = await testDataSave(params, data);
showMessage(res.message); // 显示提交结果
setTimeout(closeDrawer); // 隐藏抽屉弹窗
emit('success', data); // 触发事件,列表数据刷新
} catch (error: any) {
if (error && error.errorFields) {
showMessage(t('您填写的信息有误,请根据提示修正。'));
}
console.log('error', error);
} finally {
setDrawerProps({ confirmLoading: false }); // 隐藏提交加载中
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291