第十二节:完整用户管理 CRUD 页面(表格分页、新增 / 编辑弹窗、删除、Mock 接口)
串联前面全部技术:
pnpm+vue3+pinia+element‑plus;实现:查询、分页、新增、编辑、删除,mock 模拟后端接口返回数据。
步骤 1:增加 mock 用户接口,修改src/mock/index.js
// src/mock/index.js
export default [
// 原有登录接口保留
{
url: '/api/login',
method: 'post',
response: ({ body }) => {
if(body.username === 'admin' && body.password === '123456'){
return { code:200, data:{token:'admin‑token‑123'} }
}else{
return { code:500, msg:'账号密码错误' }
}
}
},
// 获取用户列表分页
{
url: '/api/user/list',
method: 'get',
response: ({ query }) => {
const page = Number(query.pageNum) || 1
const pageSize = Number(query.pageSize) || 10
const allUser = [
{id:1,username:'admin',nickName:'超级管理员',phone:'13800138000'},
{id:2,username:'test',nickName:'测试用户',phone:'13800138001'},
{id:3,username:'zhangsan',nickName:'张三',phone:'13800138002'},
{id:4,username:'lisi',nickName:'李四',phone:'13800138003'},
]
const total = allUser.length
const start = (page‑1)*pageSize
const records = allUser.slice(start, start+pageSize)
return { code:200, data:{records, total, pageNum:page, pageSize } }
}
},
// 新增用户
{
url: '/api/user/add',
method: 'post',
response: ()=>{
return {code:200,msg:'新增成功'}
}
},
// 编辑用户
{
url: '/api/user/edit',
method: 'post',
response: ()=>{
return {code:200,msg:'编辑成功'}
}
},
// 删除用户
{
url: '/api/user/delete',
method: 'post',
response: ()=>{
return {code:200,msg:'删除成功'}
}
}
]
步骤 2:新建用户 api src/api/user.js
import request from '@/utils/request'
// 获取用户分页列表
export function getUserList(params){
return request({
url:'/api/user/list',
method:'get',
params
})
}
// 新增用户
export function addUser(data){
return request({
url:'/api/user/add',
method:'post',
data
})
}
// 编辑用户
export function editUser(data){
return request({
url:'/api/user/edit',
method:'post',
data
})
}
// 删除用户
export function deleteUser(data){
return request({
url:'/api/user/delete',
method:'post',
data
})
}
步骤 3:完整重写 src/views/system/user.vue
包含:搜索栏、表格、分页、新增编辑弹窗表单、删除确认
<template>
<PageCard title="用户管理">
<!--搜索区域-->
<el-form
:model="queryParams"
inline>
<el-form-item label="用户名">
<el-input
v-model="queryParams.username"
placeholder="请输入用户名"
clearable />
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="getList"
>查询</el-button
>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!--操作按钮-->
<div class="mb-10">
<el-button
type="primary"
@click="openDialog()"
>新增用户</el-button
>
</div>
<!--表格-->
<el-table
:data="tableData"
border>
<el-table-column
prop="id"
label="ID"
width="80" />
<el-table-column
prop="username"
label="用户名" />
<el-table-column
prop="nickName"
label="昵称" />
<el-table-column
prop="phone"
label="手机号" />
<el-table-column
label="操作"
width="200">
<template #default="scope">
<el-button
link
type="primary"
@click="openDialog(scope.row)"
>编辑</el-button
>
<el-button
link
type="danger"
@click="handleDelete(scope.row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<!--分页-->
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="getList"
@current-change="getList" />
</PageCard>
<!--新增/编辑弹窗-->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="500px">
<el-form
ref="formRef"
:model="form"
label-width="80px">
<el-form-item
label="用户名"
prop="username">
<el-input v-model="form.username" />
</el-form-item>
<el-form-item
label="昵称"
prop="nickName">
<el-input v-model="form.nickName" />
</el-form-item>
<el-form-item
label="手机号"
prop="phone">
<el-input v-model="form.phone" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button
type="primary"
@click="submitForm"
>确定</el-button
>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getUserList, addUser, editUser, deleteUser } from '@/api/user'
import { ElMessage, ElMessageBox } from 'element-plus'
// 查询参数
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
username: ''
})
const tableData = ref([])
const total = ref(0)
//弹窗
const dialogVisible = ref(false)
const dialogTitle = ref('新增用户')
const formRef = ref(null)
const form = reactive({
id: null,
username: '',
nickName: '',
phone: ''
})
// 获取列表
const getList = async () => {
const res = await getUserList(queryParams)
if (res.code === 200) {
tableData.value = res.data.records
total.value = res.data.total
}
}
//重置搜索
const resetQuery = () => {
queryParams.username = ''
queryParams.pageNum = 1
getList()
}
//打开弹窗
const openDialog = (row) => {
formRef.value?.resetFields()
if (row) {
dialogTitle.value = '编辑用户'
Object.assign(form, row)
} else {
dialogTitle.value = '新增用户'
form.id = null
form.username = ''
form.nickName = ''
form.phone = ''
}
dialogVisible.value = true
}
//提交
const submitForm = async () => {
try {
await formRef.value.validate()
let res
if (form.id) {
res = await editUser(form)
} else {
res = await addUser(form)
}
if (res.code === 200) {
ElMessage.success(res.msg)
dialogVisible.value = false
getList()
}
} catch (error) {
//表单校验失败不执行后续逻辑
}
}
//删除
const handleDelete = (row) => {
ElMessageBox.confirm('确定删除该用户?', '提示', { type: 'warning' })
.then(async () => {
const res = await deleteUser({ id: row.id })
if (res.code === 200) {
ElMessage.success(res.msg)
getList()
}
})
.catch(() => {
//点击取消,捕获异常
})
}
onMounted(() => {
getList()
})
</script>
<style scoped>
.mb-10 {
margin-bottom: 10px;
}
</style>
✅第十二节测试
- 重启
pnpm dev,登录,打开【用户管理】 - 页面加载自动请求 mock 接口渲染表格数据 ✔
- 分页切换正常 ✔
- 查询、重置搜索 ✔
- 点击新增:弹窗表单,提交后刷新列表 ✔
- 点击编辑:回显数据,提交更新 ✔
- 点击删除:弹出确认框,删除后刷新表格 ✔
到这里,一套 Vue3 管理系统基础架子全部完成:登录、token、路由守卫、pinia 状态、侧边动态菜单、面包屑、tag 标签页、CRUD 表格弹窗、mock 模拟后端。
问题修复
你的 mock 是对象格式写法(key 为接口路径),和前面教程的数组格式不一样,不能直接复制那套数组代码,需要在你原有对象基础上追加用户 CRUD 接口,保持语法统一。
完整修改后的 src/mock/index.js,直接覆盖你的文件:
// 手写模拟接口数据
export default {
// 登录接口
'/api/login': (params) => {
const { username, password } = params
if (username === 'admin' && password === '123456') {
return {
code: 200,
msg: '登录成功',
data: {
token: 'mock-token-123456'
}
}
} else {
return {
code: 500,
msg: '账号密码错误',
data: null
}
}
},
// 获取用户信息接口
'/api/getUserInfo': () => {
return {
code: 200,
msg: '获取成功',
data: {
username: 'admin',
roles: ['admin']
}
}
},
// ============新增下面4个用户管理mock接口============
// 用户分页列表
'/api/user/list': (params) => {
const { pageNum = 1, pageSize = 10 } = params
const allUser = [
{id:1,username:'admin',nickName:'超级管理员',phone:'13800138000'},
{id:2,username:'test',nickName:'测试用户',phone:'13800138001'},
{id:3,username:'zhangsan',nickName:'张三',phone:'13800138002'},
{id:4,username:'lisi',nickName:'李四',phone:'13800138003'},
]
const total = allUser.length
const start = (pageNum - 1) * pageSize
const records = allUser.slice(start, start + pageSize)
return {
code:200,
data:{ records, total, pageNum, pageSize }
}
},
// 新增用户
'/api/user/add': () => {
return { code:200, msg:'新增成功' }
},
// 编辑用户
'/api/user/edit': () => {
return { code:200, msg:'编辑成功' }
},
// 删除用户
'/api/user/delete': () => {
return { code:200, msg:'删除成功' }
}
}
重要说明
- 你这套 mock 规则:key 写接口路径,函数接收
params,get 请求 query、post 请求 body 都放在 params 里,和教程数组版的{query,body}参数结构不一样,不能混用。 - 保存文件,重启
pnpm dev - 访问用户管理页面,表格就可以拿到 mock 返回的数据,新增 / 编辑 / 删除弹窗提交也会正常返回成功提示。
注意:
src/api/user.js不需要改动,api 层不受 mock 内部写法影响。

