一步步使用SpringBoot结合Vue实现登录和用户管理功能——下

872 阅读8分钟

因为文章过长,所以分为了上下两部分 上半部分: juejin.cn/post/692204…

五、用户管理功能

上面我们已经写了一个简单的登录功能,通过这个功能,基本可以对SpringBoot+Vue前后端分离开发有有一个初步了解,在实际工作中,一般的工作都是基于基本框架已经成型的项目,登录、鉴权、动态路由、请求封装这些基础功能可能都已经成型。所以后端的日常工作就是写接口写业务 ,前端的日常工作就是 调接口写界面,通过接下来的用户管理功能,我们能熟悉这些日常的开发。

1、后端开发

后端开发,crud就完了。

1.1、自定义分页查询

按照官方文档,来进行MP的分页。

1.1.1、分页配置

首先需要对分页进行配置,创建分页配置类

/**
 * @Author 三分恶
 * @Date 2021/1/23
 * @Description MP分页设置
 */
@Configuration
@MapperScan("cn.fighter3.mapper.*.mapper*")
public class MybatisPlusConfig {
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
        // paginationInterceptor.setOverflow(false);
        // 设置最大单页限制数量,默认 500 条,-1 不受限制
        // paginationInterceptor.setLimit(500);
        // 开启 count 的 join 优化,只针对部分 left join
        paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
        return paginationInterceptor;
    }
}

1.1.2、自定义sql

作为Mybatis的增强工具,MP自然是支持自定义sql的。其实在MP中,单表操作基本上是不用自己写sql。这里只是为了演示MP的自定义sql,毕竟在实际应用中,批量操作、多表操作还是更适合自定义sql实现。

  • 修改pom.xml,在 <build>中添加:
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
  • 配置文件:在application.properties中添加mapper扫描路径及实体类别名包
# mybatis-plus
mybatis-plus.mapper-locations=classpath:cn/fighter3/mapper/*.xml
mybatis-plus.type-aliases-package=cn.fighter3.entity
  • 在UserMapper.java 中定义分页查询的方法
IPage<User> selectUserPage(Page<User> page,String keyword);
  • 在UserMapper.java 同级目录下新建 UserMapper.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.fighter3.mapper.UserMapper">
    <select id="selectUserPage" resultType="User">
        select * from user
        <where>
            <if test="keyword !=null and keyword !='' ">
                or login_name like CONCAT('%',#{keyword},'%')
                or user_name like CONCAT('%',#{keyword},'%')
                or email like CONCAT('%',#{keyword},'%')
                or address like CONCAT('%',#{keyword},'%')
            </if>
        </where>
    </select>
</mapper>

这个查询也比较简单,根据关键字查询用户。

OK,我们的自定义分页查询就完成了,可以写个单元测试测一下。

1.2、控制层

新建UserControler,里面也没什么东西,增删改查的接口:

/**
 * @Author 三分恶
 * @Date 2021/1/23
 * @Description 用户管理
 */
@RestController
public class UserController {
    @Autowired
    private UserService userService;

    /**
     * 分页查询
     * @param queryDTO
     * @return
     */
    @PostMapping("/api/user/list")
    public Result userList(@RequestBody QueryDTO queryDTO){
        return new Result(200,"",userService.selectUserPage(queryDTO));
    }

    /**
     * 添加
     * @param user
     * @return
     */
    @PostMapping("/api/user/add")
    public Result addUser(@RequestBody User user){
        return new Result(200,"",userService.addUser(user));
    }

    /**
     * 更新
     * @param user
     * @return
     */
    @PostMapping("/api/user/update")
    public Result updateUser(@RequestBody User user){
        return new Result(200,"",userService.updateUser(user));
    }

    /**
     * 删除
     * @param id
     * @return
     */
    @PostMapping("/api/user/delete")
    public Result deleteUser(Integer id){
        return new Result(200,"",userService.deleteUser(id));
    }

    /**
     * 批量删除
     * @param ids
     * @return
     */
    @PostMapping("/api/user/delete/batch")
    public Result batchDeleteUser(@RequestBody List<Integer> ids){
        userService.batchDelete(ids);
        return new Result(200,"","");
    }
}

这里写的也比较简单,直接调用服务层的方法。

1.3、服务层

接口这里就不再贴出了,实现类如下:

/**
 * @Author 三分恶
 * @Date 2021/1/23
 * @Description
 */
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    /**
    * 分页查询
    **/
    @Override
    public IPage<User> selectUserPage(QueryDTO queryDTO) {
        Page<User> page=new Page<>(queryDTO.getPageNo(),queryDTO.getPageSize());
        return userMapper.selectUserPage(page,queryDTO.getKeyword());
    }

    @Override
    public Integer addUser(User user) {
        return userMapper.insert(user);
    }

    @Override
    public Integer updateUser(User user) {
        return userMapper.updateById(user);
    }

    @Override
    public Integer deleteUser(Integer id) {
        return userMapper.deleteById(id);
    }

    @Override
    public void batchDelete(List<Integer> ids) {
        userMapper.deleteBatchIds(ids);
    }

}

这里也比较简单,也没什么业务逻辑。

实际上,业务层至少也会做一些参数校验的工作——我见过有的系统,只是在客户端进行了参数校验,实际上,服务端参数校验是必需的(如果不做,会被怼😔),因为客户端校验相比较服务端校验是不可靠的。

在分页查询 public IPage<User> selectUserPage(QueryDTO queryDTO) 里用了一个业务对象,这种写法,也可以用一些参数校验的插件。

1.4、业务实体

上面用到了一个业务实体对象,创建一个 业务实体类QueryDTO ,定义了一些参数,这个类主要用于前端向后端传输数据,可以可以使用一些参数校验插件添加参数校验规则。

/**
 * @Author 三分恶
 * @Date 2021/1/23
 * @Description 查询业务实体
 * 这里仅仅定义了三个参数,在实际应用中可以定义多个参数
 */
public class QueryDTO {
    private Integer pageNo;    //页码
    private Integer pageSize;  //页面大小
    private String keyword;    //关键字
    //省略getter、setter
}

简单测一下,后端👌

image-20210126172536248

2、前端开发

2.1、首页

在前面,登录之后,跳转到HelloWorld,还是比较简陋的。本来想直接跳到用户管理的视图,觉得不太好看,所以还是写了一个首页,当然这一部分不是重点。

见过一些后台管理系统的都知道,后台管理系统大概都是像下面的布局:

后台布局

在ElementUI中提供了这样的布局组件Container 布局容器:

image-20210126173415562

大家都知道根组件是 App.vue ,当然在App.vue中写整体布局是不合适的,因为还有登录页面,所以在 views 下新建 home.vue,采用Container 布局容器来进行布局,使用NavMenu 导航菜单来创建侧边栏。

当然,比较好的做法是home.vue里不写什么内容,将顶部和侧边栏都抽出来作为子页面(组件)。

<template>
  <el-container class="home-container">
    <!--顶部-->
    <el-header style="margin-right: 15px; width: 100%">
      <span class="nav-logo">😀</span>
      <span class="head-title">Just A Demo</span>
      <el-avatar
        icon="el-icon-user-solid"
        style="color: #222; float: right; padding: 20px"
        >{{ this.$store.state.user.userName }}</el-avatar
      >
    </el-header>
    <!-- 主体 -->
    <el-container>
      <!-- 侧边栏 -->
      <el-aside width="13%">
        <el-menu
          :default-active="$route.path"
          router
          text-color="black"
          active-text-color="red"
        >
          <el-menu-item
            v-for="(item, i) in navList"
            :key="i"
            :index="item.name"
          >
            <i :class="item.icon"></i>
            {{ item.title }}
          </el-menu-item>
        </el-menu>
      </el-aside>
      <el-main>
        <!--路由占位符-->
        <router-view></router-view>
      </el-main>
    </el-container>
  </el-container>
</template>

<script>
export default {
  name: "Home",
  data() {
    return {
      navList: [
        { name: "/index", title: "首页", icon: "el-icon-s-home" },
        { name: "/user", title: "用户管理",icon:"el-icon-s-custom" },
      ],
    };
  },
};
</script>

<style >
.nav-logo {
  position: absolute;
  padding-top: -1%;
  left: 5%;
  font-size: 40px;
}

.head-title {
  position: absolute;
  padding-top: 20px;
  left: 15%;
  font-size: 20px;
  font-weight: bold;
}


</style>

注意 <el-main> 用了路由占位符 <router-view></router-view> ,在路由src\router\index.js里进行配置,就可以加载我们的子路由了:

    {
      path: '/',
      name: 'Default',
      redirect: '/home',
      component: Home
    },
    {
      path: '/home',
      name: 'Home',
      component: Home,
      meta: {
        requireAuth: true
      },
      redirect: '/index',
      children:[
        {
          path:'/index',
          name:'Index',
          component:() => import('@/views/home/index'),
          meta:{
            requireAuth:true
          }
        },
        }
      ]
    },

首页本来不想放什么东西,后来想想,还是放了点大家爱看的——没别的意思,快过年了,各位姐夫过年好。🏮😀

image-20210126174723686

图片来自冰冰微博,见水印。

2.2、用户列表

views下新建 user 目录,在 user 目录下新建 index.vue ,然后添加为home的子路由:

    {
      path: '/home',
      name: 'Home',
      component: Home,
      meta: {
        requireAuth: true
      },
      redirect: '/index',
      children:[
        {
          path:'/index',
          name:'Index',
          component:() => import('@/views/home/index'),
          meta:{
            requireAuth:true
          }
        },
        {
          path:'/user',
          name:'User',
          component:()=>import('@/views/user/index'),
          meta:{
            requireAuth:true
          }
        }
      ]
    },

接下来开始用户列表功能的编写。

  • 首先封装一下api,在user.js中添加调用分页查询接口的api
//获取用户列表
export function userList(data) {
  return request({
    url: '/user/list',
    method: 'post',
    data
  })
}
  • user/index.vue 中导入userList
import { userList} from "@/api/user";
  • 为了在界面初始化的时候加载用户列表,使用了生命周期钩子来调用接口获取用户列表,代码直接一锅炖了
export default {
  data() {
    return {
      userList: [], // 用户列表
      total: 0, // 用户总数
      // 获取用户列表的参数对象
      queryInfo: {
        keyword: "", // 查询参数
        pageNo: 1, // 当前页码
        pageSize: 5, // 每页显示条数
      },
    }
  created() { // 生命周期函数
    this.getUserList()
  },
    methods: {
    getUserList() {
      userList(this.queryInfo)
        .then((res) => {
          if (res.data.code === 200) {
            //用户列表
            this.userList = res.data.data.records;
            this.total = res.data.data.total;
          } else {
            this.$message.error(res.data.message);
          }
        })
        .catch((err) => {
          console.log(err);
        });
    },
    }
  • 取到的数据,我们用一个表格组件来进行绑定

            <!--表格-->
            <el-table
              :data="userList"
              border
              stripe
            >
              <el-table-column type="index" label="序号"></el-table-column>
              <el-table-column prop="userName" label="姓名"></el-table-column>
              <el-table-column prop="loginName" label="登录名"></el-table-column>
              <el-table-column prop="sex" label="性别"></el-table-column>
              <el-table-column prop="email" label="邮箱"></el-table-column>
              <el-table-column prop="address" label="地址"></el-table-column>
              <el-table-column label="操作">
              </el-table-column>
            </el-table>
    

效果如下,点击用户管理:

image-20210126184434700

2.3、分页

在上面的图里,我们看到了在最下面有分页栏,我们接下来看看分页栏的实现。

我们这里使用了 Pagination 分页组件:

image-20210126184833582

      <!--分页区域-->
      <el-pagination
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page="queryInfo.pageNo"
        :page-sizes="[1, 2, 5, 10]"
        :page-size="queryInfo.pageSize"
        layout="total, sizes, prev, pager, next, jumper"
        :total="total"
      >
      </el-pagination>

两个监听事件:

    // 监听 pageSize 改变的事件
    handleSizeChange(newSize) {
      // console.log(newSize)
      this.queryInfo.pageSize = newSize;
      // 重新发起请求用户列表
      this.getUserList();
    },
    // 监听 当前页码值 改变的事件
    handleCurrentChange(newPage) {
      // console.log(newPage)
      this.queryInfo.pageNo = newPage;
      // 重新发起请求用户列表
      this.getUserList();
    },

2.4、检索用户

搜索框已经绑定了queryInfo.keyword,只需要给顶部的搜索区域添加按钮点击和清空事件——重新获取用户列表:

            <!--搜索区域-->
            <el-input
              placeholder="请输入内容"
              v-model="queryInfo.keyword"
              clearable
              @clear="getUserList"
            >
              <el-button
                slot="append"
                icon="el-icon-search"
                @click="getUserList"
              ></el-button>
            </el-input>

效果如下:

image-20210126185429397

2.5、添加用户

  • 还是先写api,导入后面就略过了
//添加用户
export function userAdd(data) {
  return request({
    url: '/user/add',
    method: 'post',
    data
  })
}
  • 添加用户我们用到了两个组件 Dialog 对话框组件和 Form 表单组件。
    <!--添加用户的对话框-->
    <el-dialog
      title="添加用户"
      :visible.sync="addDialogVisible"
      width="30%"
      @close="addDialogClosed"
    >
      <!--内容主体区域-->
      <el-form :model="userForm" label-width="70px">
        <el-form-item label="登录名" prop="loginName">
          <el-input v-model="userForm.loginName"></el-input>
        </el-form-item>
        <el-form-item label="用户名" prop="userName">
          <el-input v-model="userForm.userName"></el-input>
        </el-form-item>
        <el-form-item label="密码" prop="password">
          <el-input v-model="userForm.password" show-password></el-input>
        </el-form-item>
        <el-form-item label="性别" prop="sex">
          <el-radio v-model="userForm.sex" label="男"></el-radio>
          <el-radio v-model="userForm.sex" label="女"></el-radio>
        </el-form-item>
        <el-form-item label="邮箱" prop="email">
          <el-input v-model="userForm.email"></el-input>
        </el-form-item>
        <el-form-item label="地址" prop="address">
          <el-input v-model="userForm.address"></el-input>
        </el-form-item>
      </el-form>
      <!--底部按钮区域-->
      <span slot="footer" class="dialog-footer">
        <el-button @click="addDialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="addUser">确 定</el-button>
      </span>
    </el-dialog>
  • 使用 addDialogVisible 控制对话框可见性,使用userForm 绑定修改用户表单:
      addDialogVisible: false, // 控制添加用户对话框是否显示
      userForm: {
        //用户
        loginName: "",
        userName: "",
        password: "",
        sex: "",
        email: "",
        address: "",
      },
  • 两个函数,addUser 添加用户,addDialogClosed 在对话框关闭时清空表单
    //添加用户
    addUser() {
      userAdd(this.userForm)
        .then((res) => {
          if (res.data.code === 200) {
            this.addDialogVisible = false;
            this.getUserList();
            this.$message({
              message: "添加用户成功",
              type: "success",
            });
          } else {
            this.$message.error("添加用户失败");
          }
        })
        .catch((err) => {
          this.$message.error("添加用户异常");
          console.log(err);
        });
    },

    // 监听 添加用户对话框的关闭事件
    addDialogClosed() {
      // 表单内容重置为空
      this.$refs.addFormRef.resetFields();
    },

效果:

image-20210126190500082

在最后一页可以看到我们添加的用户:

image-20210126190528809

2.6、修改用户

  • 先写api
//修改用户
export function userUpdate(data) {
  return request({
    url: '/user/update',
    method: 'post',
    data
  })
}
  • 在修改用户这里,我们用到一个作用域插槽,通过slot-scope="scope"接收了当前作用域的数据,然后通过scope.row拿到对应这一行的数据,再绑定具体的属性值就行了。
          <el-table-column label="操作">
            <!-- 作用域插槽 -->
            <template slot-scope="scope">
              <!--修改按钮-->
              <el-button
                type="primary"
                size="mini"
                icon="el-icon-edit"
                @click="showEditDialog(scope.row)"
              ></el-button>
            </template>
          </el-table-column>
  • 具体的修改仍然是用对话框加表单的形式
    <!--修改用户的对话框-->
    <el-dialog title="修改用户" :visible.sync="editDialogVisible" width="30%">
      <!--内容主体区域-->
      <el-form :model="editForm" label-width="70px">
        <el-form-item label="用户名" prop="userName">
          <el-input v-model="editForm.userName" :disabled="true"></el-input>
        </el-form-item>
        <el-form-item label="邮箱" prop="email">
          <el-input v-model="editForm.email"></el-input>
        </el-form-item>
        <el-form-item label="地址" prop="address">
          <el-input v-model="editForm.address"></el-input>
        </el-form-item>
      </el-form>
      <!--底部按钮区域-->
      <span slot="footer" class="dialog-footer">
        <el-button @click="editDialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="editUser">确 定</el-button>
      </span>
    </el-dialog>
  • editDialogVisible控制对话框显示,editForm 绑定修改用户表单
      editDialogVisible: false, // 控制修改用户信息对话框是否显示
      editForm: {
        id: "",
        loginName: "",
        userName: "",
        password: "",
        sex: "",
        email: "",
        address: "",
      },
  • showEditDialog 除了处理对话框显示,还绑定了修改用户对象。editUser 修改用户。
    // 监听 修改用户状态
    showEditDialog(userinfo) {
      this.editDialogVisible = true;
      console.log(userinfo);
      this.editForm = userinfo;
    },
    //修改用户
    editUser() {
      userUpdate(this.editForm)
        .then((res) => {
          if (res.data.code === 200) {
            this.editDialogVisible = false;
            this.getUserList();
            this.$message({
              message: "修改用户成功",
              type: "success",
            });
          } else {
            this.$message.error("修改用户失败");
          }
        })
        .catch((err) => {
          this.$message.error("修改用户异常");
          console.loge(err);
        });
    },

2.7、删除用户

  • api
//删除用户
export function userDelete(id) {
  return request({
    url: '/user/delete',
    method: 'post',
    params: {
      id
    }
  })
}
  • 在操作栏的作用域插槽里添加删除按钮,直接将作用域的id属性传递进去

              <el-table-column label="操作">
                <!-- 作用域插槽 -->
                <template slot-scope="scope">
                  <!--修改按钮-->
                  <el-button
                    type="primary"
                    size="mini"
                    icon="el-icon-edit"
                    @click="showEditDialog(scope.row)"
                  ></el-button>
                  <!--删除按钮-->
                  <el-button
                    type="danger"
                    size="mini"
                    icon="el-icon-delete"
                    @click="removeUserById(scope.row.id)"
                  ></el-button>
                </template>
              </el-table-column>
    
  • removeUserById 根据用户id删除用户

    // 根据ID删除对应的用户信息
    async removeUserById(id) {
      // 弹框 询问用户是否删除
      const confirmResult = await this.$confirm(
        "此操作将永久删除该用户, 是否继续?",
        "提示",
        {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
        }
      ).catch((err) => err);
      // 如果用户确认删除,则返回值为字符串 confirm
      // 如果用户取消删除,则返回值为字符串 cancel
      // console.log(confirmResult)
      if (confirmResult == "confirm") {
        //删除用户
        userDelete(id)
          .then((res) => {
            if (res.data.code === 200) {
              this.getUserList();
              this.$message({
                message: "删除用户成功",
                type: "success",
              });
            } else {
              this.$message.error("删除用户失败");
            }
          })
          .catch((err) => {
            this.$message.error("删除用户异常");
            console.loge(err);
          });
      }
    },

效果:

image-20210126192208197

2.8、批量删除用户

  • api
//批量删除用户
export function userBatchDelete(data) {
  return request({
    url: '/user/delete/batch',
    method: 'post',
    data
  })
}
  • 在ElementUI表格组件中有一个多选的方式,手动添加一个el-table-column,设type属性为selection即可

image-20210126192421265

<el-table-column type="selection" width="55"> </el-table-column>

在表格里添加事件:

@selection-change="handleSelectionChange"

下面是官方的示例:

export default {
    data() {
      return {
        multipleSelection: []
      }
    },

    methods: {
      handleSelectionChange(val) {
        this.multipleSelection = val;
      }
    }
  }

这个示例里取出的参数multipleSelection结构是这样的,我们只需要id,所以做一下处理:

image-20210126193018008

export default {
    data() {
      return {
        multipleSelection: [],
        ids: [],
      }
    },

    methods: {
      handleSelectionChange(val) {
        this.multipleSelection = val;
        //向被删除的ids赋值
        this.multipleSelection.forEach((item) => {
          this.ids.push(item.id);
          console.log(this.ids);
        });
      }
    }
  }
  • 接下来就简单了,批量删除操作直接cv上面的删除,改一下api函数和参数就可以了
   //批量删除用户
    async batchDeleteUser(){
     // 弹框 询问用户是否删除
      const confirmResult = await this.$confirm(
        "此操作将永久删除用户, 是否继续?",
        "提示",
        {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
        }
      ).catch((err) => err);
      // 如果用户确认删除,则返回值为字符串 confirm
      // 如果用户取消删除,则返回值为字符串 cancel
      if (confirmResult == "confirm") {
        //批量删除用户
        userBatchDelete(this.ids)
          .then((res) => {
            if (res.data.code === 200) {
              this.$message({
                message: "批量删除用户成功",
                type: "success",
              });
              this.getUserList();
            } else {
              this.$message.error("批量删除用户失败");
            }
          })
          .catch((err) => {
            this.$message.error("批量删除用户异常");
            console.log(err);
          });
      }

效果:

image-20210126193403139

完整代码有点长,就不贴了,请自行查看源码。

六、总结

通过这个示例,相信大家已经对 SpringBoot+Vue 前后端分离开发有了一个初步的掌握。

当然,由于这个示例并不是一个完整的项目,所以技术上和功能上都非常潦草😓

有兴趣的同学可以进一步地去扩展和完善这个示例。👏👏👏

源码地址:gitee.com/fighter3/sp…



参考:

【1】:Vue.js - 渐进式 JavaScript 框架

【2】:Element - 网站快速成型工具

【3】:how2j.cn

【4】:Vue + Spring Boot 项目实战

【5】:一看就懂!基于Springboot 拦截器的前后端分离式登录拦截

【6】:手摸手,带你用vue撸后台 系列一(基础篇

【7】:Vue + ElementUI的电商管理系统实例