表格 (curd) 封装
提示
表格(Table)组件官方文档:https://element.eleme.cn/#/zh-CN/component/table (opens new window)
# 1. 表格curd合集
这是一个集成了curd功能的表格,效果图如下:
<template>
<div>
<!-- 表格操作按钮区域 -->
<el-row style="padding-bottom: 5px;">
<el-col :span="4">
<!-- 删除选中行按钮 -->
<el-button type="danger" plain @click="deleteSelectedRows" :disabled="selectedRows.length === 0">
批量删除
</el-button>
<!-- 新增按钮 -->
<el-button type="primary" plain @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="20">
<div>
<!-- 查询输入框 -->
<el-input v-model="search" placeholder="请输入关键字查询" style="width: 200px;"></el-input>
<el-button type="info" plain @click="fetchFilteredData" style="margin-left: 10px">查询</el-button>
<el-button type="warning" plain @click="reset" style="margin-left: 10px">重置</el-button>
</div>
</el-col>
</el-row>
<!-- 表格组件 -->
<el-table :data="tableData" border stripe style="width: 100%" @selection-change="handleSelectionChange" ref="multipleTable">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="name" label="名称"></el-table-column>
<el-table-column prop="age" label="年龄"></el-table-column>
<!-- 头像列 -->
<el-table-column prop="avatar" label="头像" width="100">
<template slot-scope="scope">
<el-image :src="scope.row.avatar" alt="头像" fit="contain" style="width: 50px; height: 50px; border-radius: 50%;"></el-image>
</template>
</el-table-column>
<el-table-column prop="address" label="地址"></el-table-column>
<!-- 操作列 -->
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="mini" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<el-pagination
:current-page.sync="currentPage"
:page-size.sync="pageSize"
:total="total"
background
layout="->,total,sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange">
</el-pagination>
<!-- 编辑/新增对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="fromVisible" width="500px">
<el-form :model="form" label-width="100px">
<!-- 名称表单项 -->
<el-form-item label="名称">
<el-input v-model="form.name"></el-input>
</el-form-item>
<!-- 年龄表单项 -->
<el-form-item label="年龄">
<el-input type="number" v-model="form.age"></el-input>
</el-form-item>
<!-- 头像上传组件 -->
<el-form-item label="头像">
<avatar-uploader :action="'/uploadFile'" v-model="form.avatar"></avatar-uploader>
</el-form-item>
<!-- 地址表单项 -->
<el-form-item label="地址">
<el-input v-model="form.address"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="fromVisible = false">取消</el-button>
<el-button type="primary" @click="saveData">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import axios from 'axios';
import AvatarUploader from '@/components/AvatarUploader.vue';
export default {
components: { AvatarUploader },
data() {
return {
search: '', // 用于存储搜索条件
tableData: [], // 存储表格数据
currentPage: 1, // 当前页码
pageSize: 10, // 每页显示条目个数
total: 0, // 总条目数
fromVisible: false, // 控制对话框显示
dialogTitle: '', // 动态对话框标题
form: { // 表单数据(用于新增和编辑)
id: '',
name: '',
age: '',
address: '',
avatar: '' // 头像 URL
},
selectedRows: [] // 用于存储当前选中的多行数据
};
},
methods: {
handleAdd() { // 新增数据
this.form = { id: '', name: '', age: '', address: '', avatar: '' }; // 清空表单数据
this.dialogTitle = '新增信息'; // 设置对话框标题为“新增”
this.fromVisible = true; // 打开对话框
},
handleEdit(row) { // 编辑数据
this.form = { ...row }; // 将当前行的数据赋值给表单
this.dialogTitle = '编辑信息'; // 设置对话框标题为“编辑”
this.fromVisible = true; // 打开对话框
},
saveData() { // 保存数据
if (this.form.id) {
// 如果表单中有 ID,说明是编辑操作
axios.put(`/api/save/${this.form.id}`, this.form)
.then(() => {
this.$message.success('编辑成功');
this.fromVisible = false; // 关闭对话框
this.loadData(); // 重新加载数据
})
.catch(error => {
console.error('保存编辑失败:', error);
this.$message.error('保存失败');
});
} else {
// 如果表单中没有 ID,说明是新增操作
axios.post('/api/add', this.form)
.then(() => {
this.$message.success('新增成功');
this.fromVisible = false; // 关闭对话框
this.loadData(); // 重新加载数据
})
.catch(error => {
console.error('新增失败:', error);
this.$message.error('新增失败');
});
}
},
fetchFilteredData() { // 按关键词查询数据
if (this.search === '' || this.search === null) {
return false;
}
// 发起 GET 请求,请求参数为搜索条件
axios.get('/api/search', {
params: {
name: this.search
}
})
.then(response => {
// 更新表格数据为后端返回的查询结果
this.tableData = response.data;
console.log('查询结果:', this.tableData);
})
.catch(error => {
// 处理请求错误
this.$message.error('查询失败');
console.error('查询失败:', error);
});
},
reset() {
this.search = null;
this.loadData();
},
// 处理选择变化事件
handleSelectionChange(selection) {
this.selectedRows = selection; // 更新选中的行数据
},
// 删除选中的行
async deleteSelectedRows() {
try {
// 先进行确认操作
await this.$confirm('此操作将永久删除选中的记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
// 获取选中行的ID数组
const ids = this.selectedRows.map(row => row.id);
// 发送删除请求到后端
await axios.post('/api/deleteRows', { ids });
// 删除成功后,重新加载数据和更新分页信息
this.loadData(); // 重新加载数据,包括更新总条目数
// 清空表格的选择状态
this.$refs.multipleTable.clearSelection();
this.selectedRows = []; // 清空选中的行数据
// 显示成功消息
this.$message.success('删除成功');
} catch (error) {
// 判断是用户取消操作,还是请求失败
if (error === 'cancel') {
this.$message.info('已取消删除');
} else {
this.$message.error('删除失败');
}
}
},
// 初始化加载数据
loadData() {
const params = {
page: this.currentPage,
size: this.pageSize
};
// 使用 axios 发送 GET 请求,查询数据
axios.get('/api/data', { params })
.then(response => {
this.tableData = response.data.data; // 将返回的数据赋值给表格
this.total = response.data.total; // 设置总条目数
})
.catch(error => {
this.$message.error('数据加载失败');
console.error('加载数据失败:', error);
});
},
// 处理每页显示条数改变
handleSizeChange(newSize) {
this.pageSize = newSize;
this.loadData(); // 重新加载数据
},
// 处理当前页码改变
handleCurrentChange(newPage) {
this.currentPage = newPage;
this.loadData(); // 重新加载数据
},
// 处理删除操作
handleDelete(id) {
this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
axios.delete(`/api/delete/${id}`)
.then(() => {
this.$message.success('删除成功');
this.loadData(); // 重新加载数据
})
.catch(error => {
console.error('删除失败:', error);
this.$message.error('删除失败');
});
}).catch(() => {
this.$message.info('已取消删除');
});
}
},
created() {
this.loadData(); // 组件创建时加载数据
}
};
</script>
<style scoped>
/* 表单项布局美化 */
.el-form-item {
margin-bottom: 20px; /* 增加表单项之间的间距 */
}
.dialog-footer {
text-align: right; /* 使对话框底部的按钮右对齐 */
}
.el-button {
margin-left: 10px; /* 增加按钮之间的间距 */
}
</style>
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
# 2. 封装表格组件
封装这个表格组件 (CrudTable.vue
) 对我来说是为了提升开发效率和代码的可维护性。在项目中,很多页面都需要使用表格来展示数据,并且常常伴随着增删改查的操作。每次都从头实现这些功能不仅繁琐,还容易出现代码重复和不一致的问题。
通过封装,我把这些常见的功能统一到了一个组件里。这样,不管在哪个页面使用表格,我只需要配置好列信息和表单字段,其他的增删改查、分页等操作都由组件内部处理。这大大减少了我的工作量,让我可以更专注于业务逻辑的实现。
此外,封装组件也让我更容易维护代码。任何需要修改的地方,只要改动组件本身,所有使用这个组件的地方都会自动更新。这种集中管理的方式避免了重复修改不同页面代码的麻烦。
<template>
<div>
<!-- 表格操作按钮区域 -->
<el-row style="padding-bottom: 5px;">
<el-col :span="4">
<!-- 删除选中行按钮 -->
<el-button type="danger" plain @click="deleteSelectedRows" :disabled="selectedRows.length === 0">
批量删除
</el-button>
<!-- 新增按钮 -->
<el-button type="primary" plain @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="20">
<div>
<!-- 查询输入框 -->
<el-input v-model="search" placeholder="请输入关键字查询" style="width: 200px;"></el-input>
<el-button type="info" plain @click="fetchFilteredData" style="margin-left: 10px">查询</el-button>
<el-button type="warning" plain @click="reset" style="margin-left: 10px">重置</el-button>
</div>
</el-col>
</el-row>
<!-- 表格组件 -->
<el-table :data="tableData" border stripe style="width: 100%" @selection-change="handleSelectionChange" ref="multipleTable">
<el-table-column type="selection" width="55"></el-table-column>
<!-- 动态列渲染,根据传入的 columns 参数生成表格列 -->
<el-table-column v-for="column in columns" :key="column.prop" :prop="column.prop" :label="column.label" :width="column.width">
<template v-if="column.type === 'custom'" v-slot="scope">
<!-- 自定义渲染列 -->
<slot :name="column.slotName" :row="scope.row"></slot>
</template>
</el-table-column>
<!-- 操作列 -->
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="mini" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<el-pagination
:current-page.sync="currentPage"
:page-size.sync="pageSize"
:total="total"
background
layout="->,total,sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange">
</el-pagination>
<!-- 编辑/新增对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="fromVisible" width="500px">
<el-form :model="form" label-width="100px">
<!-- 动态生成表单项 -->
<el-form-item v-for="field in formFields" :key="field.prop" :label="field.label">
<!-- 使用动态组件渲染表单项 -->
<component :is="field.component" v-model="form[field.prop]" v-bind="field.attrs"></component>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="fromVisible = false">取消</el-button>
<el-button type="primary" @click="saveData">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import axios from 'axios';
export default {
props: {
columns: {
type: Array,
required: true,
},
formFields: {
type: Array,
required: true,
},
apiBaseUrl: {
type: String,
required: true,
},
},
data() {
return {
search: '', // 搜索条件
tableData: [], // 表格数据
currentPage: 1, // 当前页码
pageSize: 10, // 每页显示的条目数
total: 0, // 总条目数
fromVisible: false, // 控制对话框的显示状态
dialogTitle: '', // 对话框标题(根据新增/编辑切换)
form: {}, // 表单数据
selectedRows: [] // 存储当前选中的行
};
},
methods: {
handleAdd() { // 新增操作
this.form = {}; // 清空表单数据
this.dialogTitle = '新增信息'; // 设置对话框标题为“新增”
this.fromVisible = true; // 打开对话框
},
handleEdit(row) { // 编辑操作
this.form = { ...row }; // 将选中行的数据复制到表单
this.dialogTitle = '编辑信息'; // 设置对话框标题为“编辑”
this.fromVisible = true; // 打开对话框
},
saveData() { // 保存数据(新增/编辑)
if (this.form.id) {
// 编辑操作
axios.put(`${this.apiBaseUrl}/save/${this.form.id}`, this.form)
.then(() => {
this.$message.success('编辑成功');
this.fromVisible = false; // 关闭对话框
this.loadData(); // 重新加载数据
})
.catch(error => {
console.error('保存编辑失败:', error);
this.$message.error('保存失败');
});
} else {
// 新增操作
axios.post(`${this.apiBaseUrl}/add`, this.form)
.then(() => {
this.$message.success('新增成功');
this.fromVisible = false; // 关闭对话框
this.loadData(); // 重新加载数据
})
.catch(error => {
console.error('新增失败:', error);
this.$message.error('新增失败');
});
}
},
fetchFilteredData() { // 按关键词查询数据
if (this.search === '' || this.search === null) {
return false;
}
// 发起 GET 请求,请求参数为搜索条件
axios.get(`${this.apiBaseUrl}/search`, {
params: {
name: this.search
}
})
.then(response => {
// 更新表格数据为后端返回的查询结果
this.tableData = response.data;
console.log('查询结果:', this.tableData);
})
.catch(error => {
// 处理请求错误
this.$message.error('查询失败');
console.error('查询失败:', error);
});
},
reset() {
this.search = null;
this.loadData();
},
handleSelectionChange(selection) {
this.selectedRows = selection; // 更新选中的行数据
},
async deleteSelectedRows() { // 删除选中的行
try {
// 先进行确认操作
await this.$confirm('此操作将永久删除选中的记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
// 获取选中行的ID数组
const ids = this.selectedRows.map(row => row.id);
// 发送删除请求到后端
await axios.post(`${this.apiBaseUrl}/deleteRows`, { ids });
// 删除成功后,重新加载数据和更新分页信息
this.loadData(); // 重新加载数据,包括更新总条目数
// 清空表格的选择状态
this.$refs.multipleTable.clearSelection();
this.selectedRows = []; // 清空选中的行数据
// 显示成功消息
this.$message.success('删除成功');
} catch (error) {
// 判断是用户取消操作,还是请求失败
if (error === 'cancel') {
this.$message.info('已取消删除');
} else {
this.$message.error('删除失败');
}
}
},
// 初始化加载数据
loadData() {
const params = {
page: this.currentPage,
size: this.pageSize
};
// 使用 axios 发送 GET 请求,查询数据
axios.get(`${this.apiBaseUrl}/data`, { params })
.then(response => {
this.tableData = response.data.data; // 将返回的数据赋值给表格
this.total = response.data.total; // 设置总条目数
})
.catch(error => {
this.$message.error('数据加载失败');
console.error('加载数据失败:', error);
});
},
handleSizeChange(newSize) {
this.pageSize = newSize;
this.loadData(); // 重新加载数据
},
handleCurrentChange(newPage) {
this.currentPage = newPage;
this.loadData(); // 重新加载数据
},
handleDelete(id) { // 删除单个记录
this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
axios.delete(`${this.apiBaseUrl}/delete/${id}`)
.then(() => {
this.$message.success('删除成功');
this.loadData(); // 重新加载数据
})
.catch(error => {
console.error('删除失败:', error);
this.$message.error('删除失败');
});
}).catch(() => {
this.$message.info('已取消删除');
});
}
},
created() {
this.loadData(); // 组件创建时加载数据
}
};
</script>
<style scoped>
/* 表单项布局美化 */
.el-form-item {
margin-bottom: 20px; /* 增加表单项之间的间距 */
}
.dialog-footer {
text-align: right; /* 使对话框底部的按钮右对齐 */
}
.el-button {
margin-left: 10px; /* 增加按钮之间的间距 */
}
</style>
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
# 理解插槽名称和作用域
在 TableCrud
组件中,插槽名称是通过 column.slotName
传递的,具体的定义方式如下:
slot
标签:<slot :name="column.slotName" :row="scope.row"></slot>
用于在TableCrud
组件中定义插槽的位置,并传递作用域(即row
数据)。slot-scope
:这里使用了v-slot="scope"
来获取当前行的数据scope.row
,然后通过slot
标签将row
数据传递给父组件。- 插槽使用:在父组件中,通过
<template v-slot:avatarSlot="{ row }">
捕捉到这个插槽,并使用row
作用域数据来渲染具体内容。
# 表格渲染说明
v-for
渲染每一列:- 在
TableCrud
组件中,<el-table-column v-for="column in columns">
会遍历你传入的columns
数组,为每一列生成一个表格列组件 (el-table-column
)。 - 每一列都会根据
column.prop
映射到对应的数据字段,并展示在表格中。
- 在
- 插槽的使用:
- 如果某一列的
column.type
是'custom'
,就会使用你在父组件中定义的插槽内容来渲染这列的数据。 - 具体地说,
<slot :name="column.slotName" :row="scope.row"></slot>
这段代码会为每一行渲染这个插槽,并传递当前行的数据(通过scope.row
)。
- 如果某一列的
- 插槽作用域:
- 你在父组件中使用
v-slot:avatarSlot="{ row }"
来捕获插槽,并且row
会包含当前行的数据。 - 因此,对于表格中的每一行,都会调用一次插槽,并使用当前行的数据进行渲染。
- 你在父组件中使用
# 表格使用说明
Props (外部参数)
columns
: 列配置数组,包含每一列的prop
,label
, 和width
属性。formFields
: 表单字段配置数组,包含每个表单项的prop
,label
,component
, 以及绑定属性attrs
。apiBaseUrl
: API 基础 URL,用于发送增删改查请求。
组件功能
- 该组件提供表格展示、分页、增删改查等功能。
- 通过传入
columns
和formFields
参数可以灵活定制表格列和表单字段。 saveData
方法根据表单中的id
来判断是执行新增还是编辑操作。fetchFilteredData
方法根据输入的关键词来过滤表格数据。
如何使用
- 使用该组件时,需要传入
columns
、formFields
和apiBaseUrl
参数。 - 组件内部通过 Axios 进行 API 请求,使用时只需保证提供的 API 路径正确即可。
- 使用该组件时,需要传入
# 3. 使用表格组件
组件参数说明:
columns
: 定义表格的列配置,包括每一列的prop
、label
以及可选的width
和type
。如果某列需要自定义内容展示,比如头像,这里设置type
为'custom'
,并通过slotName
指定插槽名称。formFields
: 定义表单的字段配置,包括每个表单项的prop
、label
、组件类型component
和其他绑定属性attrs
。这里配置了名称、年龄、头像和地址的表单项。apiBaseUrl
: 设置 API 的基础 URL,用于组件内部发送增删改查请求。在这个示例中, API 的基础路径为/api
。自定义插槽: 如果有列配置了
type: 'custom'
,需要在使用组件的地方提供相应的插槽。例如,头像列可以通过如下方式提供插槽内容:
<template>
<div>
<!-- 使用封装的表格组件 -->
<table-crud :columns="columns" :formFields="formFields" apiBaseUrl="/api">
<!-- 定义头像列的自定义插槽 -->
<template v-slot:avatarSlot="{ row }"> <!-- 定义头像列的自定义插槽 -->
<el-image :src="row.avatar" alt="头像" fit="contain" style="width: 50px; height: 50px; border-radius: 50%;"></el-image>
</template>
</table-crud>
</div>
</template>
<script>
import TableCrud from '@/components/TableCrud.vue';
export default {
components: {
TableCrud
},
data() {
return {
// 定义表格每一列的配置(定义的是表格列)
columns: [
{ prop: 'id', label: 'ID', width: '80' },
{ prop: 'name', label: '名称' },
{ prop: 'age', label: '年龄' },
{ prop: 'avatar', label: '头像', width: '100', type: 'custom', slotName: 'avatarSlot' },
{ prop: 'address', label: '地址' }
],
// 定义对话框中的表单字段配置(定义的是表单行)
formFields: [
{ prop: 'name', label: '名称', component: 'el-input', attrs: { placeholder: '请输入名称' } },
{ prop: 'age', label: '年龄', component: 'el-input', attrs: { type: 'number', placeholder: '请输入年龄' } },
{ prop: 'avatar', label: '头像', component: 'AvatarUploader', attrs: { action: '/uploadFile' } },
{ prop: 'address', label: '地址', component: 'el-input', attrs: { placeholder: '请输入地址' } }
]
};
}
};
</script>
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
头像上传组件说明:这是我之前封装的一个头像上传组件,对外暴露了两个属性,上传地址
和回调的头像URL地址
。
<AvatarUploader :action="action" v-model="form.avatarUrl"></AvatarUploader>
# 4. 表格对话框中表单解析
当在 formFields
中配置了表单项,并通过 <component :is="field.component">
动态加载组件时,最终的 el-dialog
内容会根据 formFields
的配置被“还原”成一系列具体的组件实例。让我帮你解释一下这个过程,并给你一个还原后的代码示例。
1. formFields
数据结构:
我们需要传入的 formFields
是一个数组,其中每个对象代表一个表单项,包括以下属性:
prop
:对应于form
对象的字段名 (v-model绑定的字段名)label
:表单项的标签文本。component
:表单项中实际要渲染的组件(如el-input
或自定义组件AvatarUploader
)。attrs
:传递给组件的属性,通常用于设置组件的外观或行为。
2. <component :is="field.component">
:
component
是 Vue 提供的动态组件,:is
属性允许你根据formFields
中定义的component
属性动态选择渲染的组件。
3. 最终的渲染结果:
根据你提供的 formFields
,最终的 el-dialog
内部渲染出的表单项将替换为实际的 el-input
和 AvatarUploader
组件。
// 定义对话框中的表单字段配置
formFields: [
{ prop: 'name', label: '名称', component: 'el-input', attrs: { placeholder: '请输入名称' } },
{ prop: 'age', label: '年龄', component: 'el-input', attrs: { type: 'number', placeholder: '请输入年龄' } },
{ prop: 'avatar', label: '头像', component: 'AvatarUploader', attrs: { action: '/uploadFile' } },
{ prop: 'address', label: '地址', component: 'el-input', attrs: { placeholder: '请输入地址' } }
]
2
3
4
5
6
7
还原后的代码:
<template>
<el-dialog :title="dialogTitle" :visible.sync="fromVisible">
<el-form :model="form">
<!-- 还原第一个表单项:名称 -->
<el-form-item label="名称">
<el-input v-model="form.name" placeholder="请输入名称"></el-input>
</el-form-item>
<!-- 还原第二个表单项:年龄 -->
<el-form-item label="年龄">
<el-input v-model="form.age" type="number" placeholder="请输入年龄"></el-input>
</el-form-item>
<!-- 还原第三个表单项:上传头像组件 -->
<el-form-item label="头像">
<AvatarUploader v-model="form.avatar" action="/uploadFile"></AvatarUploader>
</el-form-item>
<!-- 还原第四个表单项:地址 -->
<el-form-item label="地址">
<el-input v-model="form.address" placeholder="请输入地址"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="fromVisible = false">取消</el-button>
<el-button type="primary" @click="saveData">保存</el-button>
</div>
</el-dialog>
</template>
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
传参替换过程:
prop
用于将form
对象中的数据与表单组件绑定 (v-model="form[prop]"
)。label
对应于表单项的标签文本。component
决定了渲染哪个组件,通过:is="field.component"
动态渲染。attrs
中的属性使用v-bind="field.attrs"
动态绑定到组件上。
通过这种方式,你的表单结构得以动态生成,可以在同一个组件中复用不同的表单字段和组件配置。
注意如果你传递的是组件,该组件需要在main.js
入口文件下进行全局注册:
// main.js
import Vue from 'vue'; // 导入 Vue
import App from './App.vue'; // 导入主组件 App
import GlobalComponent from './components/GlobalComponent.vue'; // 导入全局组件
// 注册全局组件
Vue.component('global-component', GlobalComponent);
new Vue({
render: h => h(App), // 渲染主组件 App
}).$mount('#app'); // 挂载 Vue 实例到 id 为 app 的元素上
2
3
4
5
6
7
8
9
10
11
# 5. 后端返回的数据格式
根据你的需求,后端 API 可以返回简化的数据结构,主要依赖于 HTTP 状态码来指示操作是否成功。以下是针对各种操作的后端返回数据格式建议:
# 1. 查询操作(GET 请求)
返回数据格式:
{
"data": [
{
"id": 1,
"name": "张三",
"age": 28,
"avatar": "https://example.com/avatar1.jpg",
"address": "北京市"
},
{
"id": 2,
"name": "李四",
"age": 35,
"avatar": "https://example.com/avatar2.jpg",
"address": "上海市"
}
// 其他数据项...
],
"total": 100 // 总记录数,用于分页
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
HTTP 状态码:
- 200 OK:表示查询成功并返回数据。
# 2. 新增操作(POST 请求)
返回数据格式:
对于新增操作,可以只返回状态码 200
表示成功,或者返回更具体的信息:
{
"status": "200",
"message": "新增成功"
}
2
3
4
HTTP 状态码:
- 200 OK:新增成功(简化处理)。
- 201 Created(可选):通常在 RESTful API 中表示资源创建成功。
- 400 Bad Request:请求数据无效或格式错误。
- 500 Internal Server Error:服务器错误。
# 3. 编辑操作(PUT 请求)
返回数据格式:
编辑操作可以与新增操作类似,只返回状态和信息:
{
"status": "200",
"message": "编辑成功"
}
2
3
4
HTTP 状态码:
- 200 OK:编辑成功。
- 400 Bad Request:请求数据无效或格式错误。
- 404 Not Found:要编辑的资源不存在。
- 500 Internal Server Error:服务器错误。
# 4. 删除操作(DELETE 请求)
返回数据格式:
删除操作的返回格式也可以简化:
{
"status": "200",
"message": "删除成功",
"data": {
"deletedCount": 1 // 删除的记录数量
}
}
2
3
4
5
6
7
HTTP 状态码:
- 200 OK:删除成功。
- 404 Not Found:要删除的资源不存在。
- 500 Internal Server Error:服务器错误。
# 5. 查询筛选数据(GET 请求,带参数)
返回数据格式:
与普通查询类似,返回符合条件的记录列表:
{
"data": [
{
"id": 1,
"name": "张三",
"age": 28,
"avatar": "https://example.com/avatar1.jpg",
"address": "北京市"
}
// 其他符合条件的数据项...
],
"total": 10 // 符合条件的总记录数
}
2
3
4
5
6
7
8
9
10
11
12
13
HTTP 状态码:
- 200 OK:查询成功并返回数据。
- 400 Bad Request:查询参数无效。
# 6. 操作失败的返回格式
无论是哪种操作,如果失败,建议返回类似如下格式的数据结构:
{
"status": "error",
"message": "操作失败,具体原因"
}
2
3
4
HTTP 状态码:
- 4xx 系列(如 400, 404):客户端错误。
- 5xx 系列(如 500):服务器错误。
结论
- 查询操作:需要返回数据和分页信息。
- 新增、编辑、删除操作:可以仅依赖 HTTP 状态码(如
200 OK
)来表示操作成功,附加简单的status
和message
信息。 - 筛选查询:返回符合条件的记录列表,格式与普通查询相同。
# 6.mock.js生成随机数据
在后端接口还没写好的情况下,我们可以基于mock.js拦截请求,去生成一些随机数据完成表格功能测试:
import Mock from 'mockjs';
// 模拟数据生成
const generateTestData = (count) => {
return Mock.mock({
[`data|${count}`]: [
{
'id|+1': 1, // ID 递增
'name': '@cname', // 随机生成中文姓名
'age|20-40': 1, // 生成 20 到 40 之间的随机年龄
'address': '@city(true)' // 生成随机地址,包含省、市、区
}
]
}).data;
};
// 生成 100 条测试数据
const testData = generateTestData(100);
// 拦截 GET 请求,返回分页数据
Mock.mock(/\/api\/data/, 'get', (options) => {
const url = new URL(options.url, window.location.origin);
const page = parseInt(url.searchParams.get('page')) || 1;
const size = parseInt(url.searchParams.get('size')) || 10;
const start = (page - 1) * size;
const end = start + size;
const paginatedData = testData.slice(start, end);
return {
total: testData.length,
data: paginatedData
};
});
// 拦截搜索请求
Mock.mock(/\/api\/search/, 'get', (options) => {
const url = new URL(options.url, window.location.origin);
const name = url.searchParams.get('name') || '';
const filteredData = testData.filter(item => item.name.includes(name));
return filteredData;
});
// 拦截删除多行请求
Mock.mock('/api/deleteRows', 'post', (options) => {
const { ids } = JSON.parse(options.body);
// 过滤掉删除的行
const remainingData = testData.filter(item => !ids.includes(item.id));
return {
message: '删除成功',
data: remainingData
};
});
// 拦截单个删除请求
Mock.mock(/\/api\/delete\/\d+/, 'delete', (options) => {
const id = parseInt(options.url.split('/').pop());
const index = testData.findIndex(item => item.id === id);
if (index !== -1) {
testData.splice(index, 1);
return {
message: '删除成功',
};
} else {
return {
message: '删除失败,未找到对应数据',
};
}
});
// 拦截保存编辑请求
Mock.mock(/\/api\/save\/\d+/, 'put', (options) => {
const id = parseInt(options.url.split('/').pop());
const updatedData = JSON.parse(options.body);
const index = testData.findIndex(item => item.id === id);
if (index !== -1) {
testData[index] = { ...testData[index], ...updatedData };
return {
message: '编辑成功',
};
} else {
return {
message: '编辑失败,未找到对应数据',
};
}
});
// 拦截新增请求
Mock.mock('/api/add', 'post', (options) => {
const newData = JSON.parse(options.body);
// 为新增的数据生成一个新的ID
const newId = testData.length ? testData[testData.length - 1].id + 1 : 1;
const newItem = { ...newData, id: newId };
// 将新数据添加到数组中
testData.push(newItem);
return {
message: '新增成功',
data: newItem
};
});
// 拦截文件上传请求
Mock.mock('/uploadFile', 'post', (options) => {
// 模拟返回上传后的图片路径
return {
message: '上传成功',
url: 'https://example.com/uploaded-image.png' // 模拟返回的图片URL
};
});
export default testData;
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