第十二节:完整用户管理 CRUD 页面(表格分页、新增 / 编辑弹窗、删除、Mock 接口)

第十二节:完整用户管理 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>

✅第十二节测试

  1. 重启pnpm dev,登录,打开【用户管理】
  2. 页面加载自动请求 mock 接口渲染表格数据 ✔
  3. 分页切换正常 ✔
  4. 查询、重置搜索 ✔
  5. 点击新增:弹窗表单,提交后刷新列表 ✔
  6. 点击编辑:回显数据,提交更新 ✔
  7. 点击删除:弹出确认框,删除后刷新表格 ✔

到这里,一套 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:'删除成功' }
  }
}

重要说明

  1. 你这套 mock 规则:key 写接口路径,函数接收params,get 请求 query、post 请求 body 都放在 params 里,和教程数组版的{query,body}参数结构不一样,不能混用。
  2. 保存文件,重启 pnpm dev
  3. 访问用户管理页面,表格就可以拿到 mock 返回的数据,新增 / 编辑 / 删除弹窗提交也会正常返回成功提示。

注意:src/api/user.js 不需要改动,api 层不受 mock 内部写法影响。

相关推荐
深念Y11 小时前
NativeScript 移动端开发踩坑记录
前端·ui·vue·安卓·移动端·native·原生
YHHLAI11 小时前
[特殊字符] 从 JSX 到 TypeScript:React 开发的进化之路
javascript·react.js·typescript
HackTwoHub11 小时前
Butter_Cookie 黄油曲奇|浏览器渗透插件,云存储检测、XSS、SQL 注入一站式 Web 安全测试工具
前端·sql·安全·web安全·网络安全·自动化·xss
2601_9620662311 小时前
golang超详细基础入门教程_golang教程
前端·python·golang
单线程1213811 小时前
从案例分析 Vue3 Tokenizer 源码二
前端·javascript·vue.js
单线程_0111 小时前
从案例分析 Vue3 Tokenizer 源码二
前端·javascript·vue.js
山甫aa12 小时前
【从零开始的 Web 后端学习】文件上传一篇搞定(MultipartFile + 本地存储 + 阿里云 OSS 保姆级教程)
前端·学习·阿里云
IT毕设实战小研12 小时前
基于安卓的考研资讯系统设计与实现
android·大数据·vue.js·爬虫·python·考研·课程设计
风骏时光牛马12 小时前
AIRAGDebug智能检索链路异常定位与调试分析
前端