第十二节:完整用户管理 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 内部写法影响。

相关推荐
高级程序源8 小时前
django大学生创新创业项目管理系统94923-计算机课程设计、毕业设计
javascript·vue.js·spring boot·后端·python·django·课程设计
八荒启·交互动画8 小时前
# Web特效020—让 Web 特效真正动起来:时间循环应该怎么接
前端·webgl·网页特效·八荒启-交互动画·八荒启
动恰客流统计8 小时前
线下零售数字化浪潮下,客流统计的3个核心发展趋势
大数据·前端·人工智能
lizz22768 小时前
Babel插件手册
javascript
用户938515635078 小时前
从小米前端面试题,彻底搞懂闭包、作用域链与 useState 惰性初始化
前端·面试
粥里有勺糖9 小时前
视野修炼第133期 | Native 回春了?
前端·github·agent
aichitang202410 小时前
前端小skill
前端·人工智能·算法·ai·前端框架
前端·柱子10 小时前
q-floodfill白边锯齿?一招搞定抗锯齿边缘问题
前端·html·canva可画
葡萄城技术团队11 小时前
SpreadJS V19.2新特性揭秘:功能区键盘提示
前端
计算机魔术师11 小时前
AI 幻觉导致的错误情报险些引发美军拦截中国船只
前端