Learn Springboot & Vue 2 增删改查、导入导出、登录和注册

204 阅读2分钟

分页查询显示

在之前实现Mybatis-plus的分页查询+模糊查询之后,自动返回的JSON格式如下:


{
    "records": [
        {
            "id": 1,
            "username": "admin",
            "password": "admin",
            "nickname": "管理员",
            "email": "admin@qq.com",
            "phone": "12345678910",
            "address": "Mars",
            "avatar": null
        },
        {
            "id": 2,
            "username": "admin2",
            "password": null,
            "nickname": "管理员2",
            "email": "admin2@163.com",
            "phone": null,
            "address": null,
            "avatar": null
        },
        {
            "id": 3,
            "username": "ddd",
            "password": null,
            "nickname": null,
            "email": null,
            "phone": null,
            "address": null,
            "avatar": null
        },
        {
            "id": 4,
            "username": "dmiut",
            "password": null,
            "nickname": null,
            "email": null,
            "phone": null,
            "address": "mark",
            "avatar": null
        },
        {
            "id": 5,
            "username": "4",
            "password": null,
            "nickname": null,
            "email": null,
            "phone": null,
            "address": "",
            "avatar": null
        }
    ],
    "total": 11,
    "size": 5,
    "current": 1,
    "orders": [],
    "optimizeCountSql": true,
    "searchCount": true,
    "countId": null,
    "maxLimit": null,
    "pages": 3
}

所以前端就没有res.data这一栏,在前端修改成res.records

load(){
  fetch( "http://localhost:9090/user/page?pageNum="+this.pageNum+"&pageSize="+this.pageSize).then(res => res.json())
      .then( res => {
        this.tableData = res.records
        this.total = res.total
        console.log(res)
      })
},

又一次在前端获取到数据

image.png

axios

在前端文件夹目录的terminal下输入:

npm i axios -S

在src下新建utils/request.js

import axios from "axios";

const request = axios.create({
    baseURL:'http://localhost',
    timeout:5000
})

// request 拦截器
request.interceptors.request.use( config => {
    config.headers['Content-Type'] = 'application/json;charset=utf-8';

    // config.headers['token'] = user.token;
    return config
}, error => {
    return Promise.reject(error)
});

// response拦截器
// 在api响应后统一处理结果
request.interceptors.response.use(
    response =>{
        let res = response.data;

        //如果返回的是文件
        if (response.config.responseType === 'blob'){
            return res;
        }

        //兼容服务端返回的字符串数据
        if (typeof res === 'string'){
            res = res ? JSON.parse(res) : res
        }

        return res;
    },
    error => {
        console.log('err: ' + error)
        return Promise.reject(error)
    }
)

export default request

main.js中将request设为全局变量

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import './assets/globle.css'
import request from "@/utils/request";

Vue.config.productionTip = false

Vue.use(ElementUI,{size:"mini"});

Vue.prototype.axios = request

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

HomeView.vue中重写load()方法

load(){
  request.get("http://localhost:9090/user/page?" +
      "pageNum="+ this.pageNum+
      "&pageSize=" + this.pageSize+
      "&username="+this.username+
      "&email="+this.email+
      "&address="+this.address)
      .then(res => {
        console.log(res)
      })
},

成功在前端log出来res

image.png

不过前端默认写了模糊查询的变量为""空字符串,所以在后端加一个空字符串的if判断

@GetMapping("/page")
@ResponseBody
public IPage<User> findPage(@RequestParam Integer pageNum,
                            @RequestParam Integer pageSize,
                            @RequestParam(required = false, defaultValue = "") String username,
                            @RequestParam(required = false, defaultValue = "") String email,
                            @RequestParam(required = false, defaultValue = "") String address){

    final String cmp = "";

    IPage<User> page = new Page<>(pageNum, pageSize);

    QueryWrapper<User> queryWrapper = new QueryWrapper<>();

    if (!cmp.equals(username)){
        queryWrapper.like("username",username);
    }
    if (!cmp.equals(email)) {
        queryWrapper.like("email",email);
    }
    if (!cmp.equals(address)){
        queryWrapper.like("address",address);
    }

    return userService.page(page,queryWrapper);
}

又调试了一下发现全局变量设置的有问题,还是得import request

模糊查询

在前端中添加placeholderv-model,之后就会自动读取输入的变量

<div style="margin: 10px 0;">
  <el-input style="width: 200px" placeholder="请输入名称" suffix-icon="el-icon-search" v-model="username"></el-input>
  <el-input style="width: 200px;margin-left: 5px" placeholder="请输入邮箱" suffix-icon="el-icon-message" v-model="email"></el-input>
  <el-input style="width: 200px;margin-left: 5px" placeholder="请输入地址" suffix-icon="el-icon-position" v-model="address"></el-input>
  <el-button style="margin-left: 5px" type="primary" @click="load">搜索</el-button>
</div>

image.png

Insert

添加新增用户表单:

<el-dialog title="用户信息" :visible.sync="dialogFormVisible" width="30%">
  <el-form label-width="80px" size="small">
    <el-form-item label="用户名" :label-width="formLabelWidth">
      <el-input v-model="form.username" autocomplete="off"></el-input>
    </el-form-item>
    <el-form-item label="昵称" :label-width="formLabelWidth">
      <el-input v-model="form.nickname" autocomplete="off"></el-input>
    </el-form-item>
    <el-form-item label="邮箱" :label-width="formLabelWidth">
      <el-input v-model="form.email" autocomplete="off"></el-input>
    </el-form-item>
    <el-form-item label="电话" :label-width="formLabelWidth">
      <el-input v-model="form.phone" autocomplete="off"></el-input>
    </el-form-item>
    <el-form-item label="地址" :label-width="formLabelWidth">
      <el-input v-model="form.address" autocomplete="off"></el-input>
    </el-form-item>

  </el-form>
  <div slot="footer" class="dialog-footer">
    <el-button @click="dialogFormVisible = false">取 消</el-button>
    <el-button type="primary" @click="save">确 定</el-button>
  </div>
</el-dialog>
<el-button type="primary" @click="handleAdd">新增 <i class="el-icon-circle-plus-outline"></i></el-button>

在新增按钮上添加handleAdd方法,确定按钮上添加save方法

handleAdd(){
  this.dialogFormVisible = true
  this.form = {}
},
save(){
  request.post("/user",this.form).then(res => {
    if (res){
      this.$message.success("保存成功!")
      this.dialogFormVisible = false
      this.load()
    }else {
      this.$message.success("error!")
    }
  })
},

Edit

利用Insert中添加的表单实现信息更新

<template slot-scope="scope">
  <el-button type="info" @click="handleEdit(scope.row)">编辑 <i class="el-icon-edit"></i> </el-button>
  <el-button type="danger">删除 <i class="el-icon-delete"></i> </el-button>
</template>

在编辑按钮上添加handleEdit方法

handleEdit(row){
  this.form = Object.assign({},row)
  this.dialogFormVisible = true
  this.load()
}

注意使用Object.assigh({},row)来给from赋值,不然会出现点击取消后依然会保存数据的bug

image.png

image.png

delete

<el-button type="danger" @click="handleDelete(scope.row.id)">删除 <i class="el-icon-delete"></i> </el-button>

在删除按钮中添加handleDelete方法,直接将row.id传进去

handleDelete(id){
  request.delete("/user/"+id)
      .then(res => {
        if (res){
          this.$message.success("删除成功!")
          this.load()
        }else {
          this.$message.error("删除失败!")
        }
      })
},

考虑到这种情况不太安全,再加一个气泡悬浮

<el-popconfirm
    confirm-button-text='确定'
    cancel-button-text='取消'
    icon="el-icon-info"
    icon-color="red"
    title="确定删除吗?"
    @confirm="handleDelete(scope.row.id)"
>
  <el-button type="danger" slot="reference" style="margin-left: 5px">删除 <i class="el-icon-delete"></i> </el-button>
</el-popconfirm>

将删除方法改为放在popconfirm中的@confirm

批量删除

批量删除后端方法:

@PostMapping("/del/batch")
@ResponseBody
public boolean deleteBatch(@RequestBody List<Integer> ids){
    return userService.removeBatchByIds(ids);
}

前端在表格中添加selection

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

同时在selection中接收数据:

<el-table :data="tableData" border stripe header-cell-class-name="headerBg"  @selection-change="handleSelectionChange">

之后写这两个方法

handleSelectionChange(val){
  this.multiSelection = val
},
delBatch(){
  let ids = this.multiSelection.map( v => v.id)

  request.post("/user/del/batch",ids)
      .then( res => {
        if (res){
          this.$message.success("批量删除成功!")
          this.load()
        }else{
          this.$message.error("批量删除失败!")
        }
      })
},

image.png

image.png

Rename

考虑到目前写的页面是Admin的管理界面,所以将HomeView rename为Manage

同时修改路由

path: '/',
name: 'Manage',
component: ()=>import('../views/Manage'),

组件分发

Aside

将侧边栏单独拎出来放到Aside.vue

<template>
  <el-menu :default-openeds="[]" style="height: 100%; overflow-x: hidden"
           background-color="rgb(48,65,86)"
           text-color="#fff"
           active-text-color="#409EFF"
           :collapse-transition="false"
           :collapse="isCollapse"
  >
    <div style="height: 60px; line-height: 60px; text-align: center">
      <img src="../assets/logo.png" alt="" style="width: 20px; position: relative;top: 5px;margin-right: 5px">
      <b style="color: white" v-show="logoTextShow">后台管理系统</b>
    </div>
    <el-submenu index="1">
      <template slot="title">
        <i class="el-icon-message"></i>
        <span>导航一</span>
      </template>
      <el-menu-item-group>
        <template slot="title">分组一</template>
        <el-menu-item index="1-1">选项1</el-menu-item>
        <el-menu-item index="1-2">选项2</el-menu-item>
      </el-menu-item-group>
      <el-menu-item-group title="分组2">
        <el-menu-item index="1-3">选项3</el-menu-item>
      </el-menu-item-group>
    </el-submenu>
    <el-submenu index="2">
      <template slot="title">
        <i class="el-icon-menu"></i>
        <span>导航二</span>
      </template>
      <el-menu-item-group>
        <template slot="title">分组一</template>
        <el-menu-item index="2-1">选项1</el-menu-item>
        <el-menu-item index="2-2">选项2</el-menu-item>
      </el-menu-item-group>
      <el-menu-item-group title="分组2">
        <el-menu-item index="2-3">选项3</el-menu-item>
      </el-menu-item-group>
    </el-submenu>
  </el-menu>
</template>

<script>
export default {
  name: "Aside",
  props:{
    isCollapse:Boolean,
    logoTextShow:Boolean,
  }
}
</script>

<style scoped>

</style>

如果是在外层中需要使用的变量的话需要在props中提前写好接收的类型

同时在外层调用时指定调用的方法/类型/对象

<el-aside :width="sideWidth + 'px'" style="background-color: rgb(238, 241, 246);height: 100%">
  <Aside :isCollapse="isCollapse" :logoTextShow="logoTextShow" />
</el-aside>

Header

将上页中的个人信息单独放到Header.vue

<template>
  <div style="font-size: 12px; line-height: 60px;display: flex">
    <div style="flex: 1;font-size: 25px">
      <span :class="collapseBtnClass" style="cursor: pointer" @click="collapse"></span>
    </div>
    <el-dropdown style="width: 70px;cursor: pointer">
      <span>王小虎</span><i class="el-icon-arrow-down" style="margin-left: 5px"></i>
      <el-dropdown-menu slot="dropdown">
        <el-dropdown-item>个人信息</el-dropdown-item>
        <el-dropdown-item>退出</el-dropdown-item>
      </el-dropdown-menu>
    </el-dropdown>
  </div>
</template>

<script>
export default {
  name: "Header",
  props: {
    collapseBtnClass: String,
    collapse:Function,
  }
}
</script>

<style scoped>

</style>

如果调用指定的是一个方法,那么就要在props中指定collapse: Function

而对于按钮组件这样的类型只需要传String类型

User

将查看用户信息栏放入User.vue

实现分发路由

{
  path: '/',
  name: 'Manage',
  component: Manage,
  children:[
    {path: '/user', name: 'User', component: User}
  ]
},

将之前的变量和方法放入User.vue

<template>
  <div>
    <div style="margin-bottom: 30px">
      <el-breadcrumb separator="/">
        <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
        <el-breadcrumb-item><a href="/">用户管理</a></el-breadcrumb-item>
      </el-breadcrumb>
    </div>

    <div style="margin: 10px 0;">
      <el-input style="width: 200px" placeholder="请输入名称" suffix-icon="el-icon-search" v-model="username"></el-input>
      <el-input style="width: 200px;margin-left: 5px" placeholder="请输入邮箱" suffix-icon="el-icon-message" v-model="email"></el-input>
      <el-input style="width: 200px;margin-left: 5px" placeholder="请输入地址" suffix-icon="el-icon-position" v-model="address"></el-input>
      <el-button style="margin-left: 5px" type="primary" @click="load">搜索</el-button>
      <el-button style="margin-left: 5px" type="info" @click="reset">重置</el-button>
    </div>

    <div style="margin: 10px 0">
      <el-button type="primary" @click="handleAdd">新增 <i class="el-icon-circle-plus-outline"></i></el-button>

      <el-popconfirm
          confirm-button-text='确认'
          cancel-button-text='我再想想'
          icon="el-icon-info"
          icon-color="red"
          title="确定要批量删除吗?"
          @confirm="delBatch"
      >
        <el-button type="danger" slot="reference" style="margin-left: 5px">批量删除 <i class="el-icon-remove-outline"></i> </el-button></el-popconfirm>
      <el-button type="primary" style="margin-left: 5px">导入 <i class="el-icon-bottom"></i> </el-button>
      <el-button type="primary" style="margin-left: 5px">导出 <i class="el-icon-top"></i> </el-button>
    </div>

    <el-table :data="tableData" border stripe header-cell-class-name="'headerBg'"  @selection-change="handleSelectionChange">

      <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="username" label="用户名" width="120"></el-table-column>

      <el-table-column prop="nickname" label="昵称" width="120"></el-table-column>

      <el-table-column prop="email" label="邮箱" width="200"></el-table-column>

      <el-table-column prop="phone" label="电话" width="130"></el-table-column>

      <el-table-column prop="address" label="地址" width="300"></el-table-column>

      <el-table-column prop="action" label="操作">
        <template slot-scope="scope">
          <el-button type="info" @click="handleEdit(scope.row)">编辑 <i class="el-icon-edit"></i> </el-button>

          <el-popconfirm
              confirm-button-text='确定'
              cancel-button-text='取消'
              icon="el-icon-info"
              icon-color="red"
              title="确定删除吗?"
              @confirm="handleDelete(scope.row.id)"
          >
            <el-button type="danger" slot="reference" style="margin-left: 5px">删除 <i class="el-icon-delete"></i> </el-button>
          </el-popconfirm>

        </template>
      </el-table-column>
    </el-table>
    <div style="padding: 10px 0">
      <el-pagination
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
          :current-page="pageNum"
          :page-sizes="[5,10,20]"
          :page-size="pageSize"
          layout="total, sizes, prev, pager, next, jumper"
          :total="total">
      </el-pagination>
    </div>

    <el-dialog title="用户信息" :visible.sync="dialogFormVisible" width="30%">
      <el-form label-width="80px" size="small">
        <el-form-item label="用户名" :label-width="formLabelWidth">
          <el-input v-model="form.username" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="昵称" :label-width="formLabelWidth">
          <el-input v-model="form.nickname" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="邮箱" :label-width="formLabelWidth">
          <el-input v-model="form.email" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="电话" :label-width="formLabelWidth">
          <el-input v-model="form.phone" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="地址" :label-width="formLabelWidth">
          <el-input v-model="form.address" autocomplete="off"></el-input>
        </el-form-item>

      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogFormVisible = false">取 消</el-button>
        <el-button type="primary" @click="save">确 定</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
export default {
  name: "User",
  data(){
    return{
      tableData: [],
      total:0,
      pageNum : 1,
      pageSize : 10,
      username:"",
      email:"",
      address:"",
      dialogFormVisible:false,
      form:{},
      multiSelection:[],
    }
  },
  created() {
    this.load()
  },
  methods:{
    load(){
      this.request.get("/user/page",{
        params:{
          pageNum:this.pageNum,
          pageSize:this.pageSize,
          username:this.username,
          email:this.email,
          address:this.address,
        }
      })
          .then(res => {

            this.tableData = res.records;

            this.total = res.total;
          })
    },

    handleSizeChange(pageSize){
      console.log(pageSize)
      this.pageSize = pageSize
      this.load()
    },

    handleCurrentChange(pageNum){
      console.log(pageNum)
      this.pageNum = pageNum
      this.load()
    },

    reset(){
      this.username=""
      this.email=""
      this.address=""
      this.load()
    },

    handleAdd(){
      this.dialogFormVisible = true
      this.form = {}
    },

    save(){
      this.request.post("/user",this.form).then(res => {
        if (res){
          this.$message.success("保存成功!")
          this.dialogFormVisible = false
          this.load()
        }else {
          this.$message.error("error!")
        }
      })
    },

    handleEdit(row){
      this.form = Object.assign({},row)
      this.dialogFormVisible = true
      this.load()
    },

    handleDelete(id){
      this.request.delete("/user/"+id)
          .then(res => {
            if (res){
              this.$message.success("删除成功!")
              this.load()
            }else {
              this.$message.error("删除失败!")
            }
          })
    },

    handleSelectionChange(val){
      this.multiSelection = val
    },
    delBatch(){
      let ids = this.multiSelection.map( v => v.id)

      this.request.post("/user/del/batch",ids)
          .then( res => {
            if (res){
              this.$message.success("批量删除成功!")
              this.load()
            }else{
              this.$message.error("批量删除失败!")
            }
          })
    },
  }
}
</script>

<style scoped>
.headerBg{
  background: #eee!important;
}
</style>

之后进入/user

image.png

配置Vuex

方便面包屑的使用

在src目录下新建store文件夹,新建index.js配置vuex

因为vue的版本问题,需要先输入以下命令:

npm install --save vuex@3.6.2

index.js

import Vue from "vue";
import Vuex from 'vuex';

Vue.use(Vuex)

const index = new Vuex.Store({
    state:{
        currentPathName:''
    },
    mutations:{
        setPath(state){
            state.currentPathName = localStorage.getItem("currentPathName")
        }
    }
})

export default index

main.js中添加vuex的配置如下

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import './assets/globle.css'
import request from "@/utils/request";
import store from "@/store";

Vue.config.productionTip = false

Vue.use(ElementUI,{size:"mini"});

Vue.prototype.request = request

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

router/index.js下添加路由守卫

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

//路由守卫
router.beforeEach((to,from,next) => {
  localStorage.setItem("currentPathName",to.name)  //设置当前的路由名称,为了在Header组件中使用
  store.commit("setPath") //触发store的数据更新
  next() //放行路由
})

export default router

Header.vue中添加watchcomputed方法

<script>
export default {
  name: "Header",
  props: {
    collapseBtnClass: String,
    collapse:Function,
  },
  computed:{
    currentPathName(){
      return this.$store.state.currentPathName; //需要监听的数据
    }
  },
  watch:{
    currentPathName(newVal, oldVal){
      console.log(newVal)
    }
  }
}
</script>

在面包屑中添加路由:

<el-breadcrumb separator="/" style="display: inline-block; margin-left: 10px">
  <el-breadcrumb-item :to="'/'">首页</el-breadcrumb-item>
  <el-breadcrumb-item>{{ currentPathName }}</el-breadcrumb-item>
</el-breadcrumb>

至此就可以在面包屑中通过点击跳转路由

此时的router/index.js配置如下:

import Vue from 'vue'
import VueRouter from 'vue-router'
import User from "@/views/User";
import Manage from "@/views/Manage";
import Home from '@/views/Home'
import store from "@/store";

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: '首页',
    component: Manage,
    redirect:"/home",
    children:[
      {path: 'home', name: '首页', component: Home},
      {path: 'user', name: '用户管理', component: User},
    ]
  },
  {
    path: '/about',
    name: 'about',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

//路由守卫
router.beforeEach((to,from,next) => {
  localStorage.setItem("currentPathName",to.name)  //设置当前的路由名称,为了在Header组件中使用
  store.commit("setPath") //触发store的数据更新
  next() //放行路由
})

export default router

image.png

image.png

此时准备上床睡觉摆烂,于是懒得再开启后端了

明天看看把导入导出登陆注册完结了

Hutool

为了实现导入导出功能,需要在依赖中添加hutool包

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.20</version>
</dependency>

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>4.1.2</version>
</dependency>

export信息导出

    // 导出接口
    @GetMapping("/export")
    @ResponseBody
    public void export (HttpServletResponse response) throws Exception{
        // find all users from mysql
        List<User> list = userService.list();

        // 将信息导出的磁盘路径
//        ExcelWriter writer = ExcelUtil.getWriter(filesUploadPath)
        //在内存操作写出到浏览器
        ExcelWriter writer = ExcelUtil.getWriter(true);

        //自定义标题别名
        //反正都能看懂就不换别名了
        //writer.addHeaderAlias("username","用户名");

        // 一次性写出list内对象到excel,使用默认样式,强制输出标题
        writer.write(list,true);

        // 设置浏览器响应的格式
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
        String fileName = URLEncoder.encode("用户信息","UTF-8");
        response.setHeader("Content-Disposition","attachment;filename="+fileName+".xlsx");

        ServletOutputStream out = response.getOutputStream();
        writer.flush(out,true);
        out.close();
        writer.close();
    }

反正英文变量名都能看懂,就不做变量名转换了

设置浏览器相应格式只需要改一个filename就行,其他的都可以CV

在前端添加的导出标志上添加exp方法

<el-button type="primary" style="margin-left: 5px" @click="exp">导出 <i class="el-icon-top"></i> </el-button>
exp(){
  window.open("http://localhost:9090/user/export")
},

import信息导入

// 导入接口
@PostMapping("/import")
@ResponseBody
public Boolean imp(MultipartFile file) throws Exception{
    InputStream inputStream = file.getInputStream();
    ExcelReader reader = ExcelUtil.getReader(inputStream);
    List<User> list = reader.readAll(User.class);

    // 将获取的数据插入数据库
    userService.saveBatch(list);
    return true;
}

在导入后需要将读取的数据保存到数据库,有一个saveBatch方法批量保存数据

前端需要在导入中添加一个upload组件

<el-upload
    action="http://localhost:9090/user/import"
    style="display: inline-block"
    :show-file-list="false"
    accept="xlsx"
    :on-success="handleExcelImportSuccess">
<el-button type="primary" style="margin-left: 5px">导入 <i class="el-icon-bottom"></i> </el-button>
</el-upload>

同时写一个handleExcelImportSuccess的方法处理导入之后的数据

handleExcelImportSuccess(){
  this.$message.success("导入成功!")
  this.load()
}

登陆界面

<template>
  <div class="wrapper">
    <div style="margin: 200px auto; background-color: #fff; width: 350px; height: 300px; padding: 20px; border-radius: 10px">
      <div style="margin: 20px 0; text-align: center; font-size: 24px"><b>登 录</b></div>
      <el-input size="medium" style="margin: 10px 0" prefix-icon="el-icon-user" v-model="user.username"></el-input>
      <el-input size="medium" style="margin: 10px 0" prefix-icon="el-icon-lock" show-password v-model="user.password"></el-input>

      <div style="margin: 10px 0; text-align: right">
        <el-button type="primary" size="small" autocomplete="off">登录</el-button>
        <el-button type="warning" size="small" autocomplete="off">注册</el-button>
      </div>
    </div>

  </div>
</template>

<script>
export default {
  name: "Login",
  data() {
    return {
      user : {}
    }
  }
}
</script>

<style scoped>
.wrapper {
  height: 100vh;
  background-image: linear-gradient(to bottom right, #FC4668, #3F5EFB);
  overflow: hidden;
}
</style>

效果图:

image.png

后端Login

在Controller层中新建dto文件夹,新建UserDTO类表示接收的User对象

package com.example.springweb.contoller.dto;

import lombok.Data;

/**
 * 接收前端登录请求的参数
 */
@Data
public class UserDTO {
    private String username;
    private String password;

}

在Controller层中写login接口:

@PostMapping("/login")
@ResponseBody
public boolean login(@RequestBody UserDTO userDTO){
    String username = userDTO.getUsername();
    String password = userDTO.getPassword();
    if (StrUtil.isBlank(username) || StrUtil.isBlank(password)){
        return false;
    }
    return userService.login(userDTO);
}

在service层中定义login函数

public boolean login(UserDTO userDTO) {

    QueryWrapper<User> wrapper = new QueryWrapper<>();
    wrapper.eq("username", userDTO.getUsername());
    wrapper.eq("password", userDTO.getPassword());

    try{
        User one = getOne(wrapper);
        return one != null;
    } catch (Exception e){
        LOG.error(e);
        return false;
    }

}

前端login

使用axios获取后端接口,简单校验后跳转主菜单

methods : {
  login(){
    this.request.post("/user/login",this.user)
        .then(res => {
          if (!res){
            this.$message.error("用户名或密码错误")
          } else{
            this.$router.push("/")
          }
        })
  },
}

看之后能不能整一个拦截器+JWT登录认证

之后我想试试能不能权限登录进行跳转,加了一个下拉悬浮页的selection

<el-form :model="user" ref="userForm">
  <el-form-item>
  <el-select v-model="user.identity" placeholder="请选择登陆权限">
    <el-option value="user"></el-option>
    <el-option value="admin"></el-option>
  </el-select>
  </el-form-item>
<el-form-item>
  <el-input size="medium" style="margin: 10px 0" prefix-icon="el-icon-user" v-model="user.username"></el-input>
</el-form-item>

<el-form-item>
  <el-input size="medium" style="margin: 10px 0" prefix-icon="el-icon-lock" show-password v-model="user.password"></el-input>
</el-form-item>

</el-form>

click校验是否能够log出不同权限从而进行跳转:

login(){
  this.$refs['userForm'].validate((valid) => {
    if (valid){
      // if (this.user.identity === 'user'){
      //   console.log("user")
      // } else if (this.user.identity === 'admin'){
      //   console.log("admin")
      // }
      this.request.post("/user/login",this.user)
          .then(res => {
            if (!res){
              this.$message.error("用户名或密码错误")
            } else{
              this.$router.push("/")
            }
          })
    } else {
      console.log("error submit!")
      return false
    }
  })


},

事实证明分权限登录是可行的😋

课听到现在终于在注册那一P讲到封装接口了

Login封装

在开发过程中最好和前端程序员统一Result返回类型

Result中有code, messge, data三个变量

package com.example.springweb.common;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 *  接口统一返回包装类
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result {
    private String code;
    private String msg;
    private Object data;

    public static Result success(){
        return new Result(Constants.CODE_200, "",null);
    }

    public static Result success(Object data){
        return new Result(Constants.CODE_200, "",data);
    }

    public static Result error(String code, String msg){
        return new Result(code, msg,null);
    }

    public static Result error(){
        return new Result(Constants.CODE_500, "", null);
    }
}

同时为了接收Result中的code变量,定义一个Constants类

package com.example.springweb.common;

public interface Constants {
    String CODE_200 = "200"; // success
    String CODE_500 = "500"; // 系统错误
    String CODE_401 = "401"; // 权限不足
    String CODE_400 = "400"; // 参数不足
    String CODE_600 = "600"; //其他业务异常
}

重写login方法

@PostMapping("/login")
@ResponseBody
public Result login(@RequestBody UserDTO userDTO){
    String username = userDTO.getUsername();
    String password = userDTO.getPassword();
    if (StrUtil.isBlank(username) || StrUtil.isBlank(password)){
        return Result.error(Constants.CODE_400, "参数错误");
    }
    UserDTO dto = userService.login(userDTO);
    return Result.success(dto);
}

在Service层中修改login方法如下:

public UserDTO login(UserDTO userDTO) {

    QueryWrapper<User> wrapper = new QueryWrapper<>();
    wrapper.eq("username", userDTO.getUsername());
    wrapper.eq("password", userDTO.getPassword());

    User one;

    try{
        one = getOne(wrapper); // 从数据库查询用户信息
    } catch (Exception e){
        LOG.error(e);
        throw new ServiceException(Constants.CODE_500, "系统错误");
    }

    if (one != null){
        BeanUtil.copyProperties(one, userDTO, true);
        return userDTO;
    } else {
        throw new ServiceException(Constants.CODE_600, "用户名或密码错误");
    }

}

自定义一个异常:

package com.example.springweb.exception;

import com.example.springweb.common.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ServiceException.class)
    @ResponseBody
    public Result handle(ServiceException se){
        return Result.error(se.getCode(),se.getMessage());
    }
}

之后定义一个业务异常:

package com.example.springweb.exception;

import lombok.Getter;

/**
 *  自定义异常
 */
@Getter
public class ServiceException extends RuntimeException{
    private String code;

    public ServiceException(String code, String msg){
        super(msg);
        this.code = code;
    }
}

这样登录时就可以在后端抛出异常及时debug

使用Localstorage获取头像和昵称

login(){
  this.$refs['userForm'].validate((valid) => {
    if (valid){
      // if (this.user.identity === 'user'){
      //   console.log("user")
      // } else if (this.user.identity === 'admin'){
      //   console.log("admin")
      // }
      this.request.post("/user/login",this.user)
          .then(res => {
            if (res.code === '200'){
              localStorage.setItem("user", JSON.stringify(res.data)) // 存储用户信息到浏览器
              this.$router.push("/")
              this.$message.success("登录成功!")
            } else{
              this.$message.error(res.msg)
            }
          })
    } else {
      console.log("error submit!")
      return false
    }
  })


},

再一次改写login方法,此时使用localStorage将获取到的对象存储在前端

而在Header.vue中,可以使用getItem方法获取前端的存储的对象

data(){
  return {
    user : localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user")) : {}
  }
},

这样我们就能在前端中获取用户的昵称和头像

<el-dropdown style="width: 100px;cursor: pointer">
  <div style="display: inline-block">
    <img :src="user.avatarUrl" alt=""
         style="width: 30px; border-radius: 50%; position: relative; top: 10px; right: 5px">
  </div>
  <span>{{ user.nickname }}</span><i class="el-icon-arrow-down" style="margin-left: 5px;"></i>
  <el-dropdown-menu slot="dropdown">
    <el-dropdown-item style="font-size: 14px; padding: 10px">个人信息</el-dropdown-item>
    <el-dropdown-item style="font-size: 14px; padding: 10px">
      <router-link to="/login" style="text-decoration: none">退出</router-link>
    </el-dropdown-item>
  </el-dropdown-menu>
</el-dropdown>

image.png

但是在退出时我们也需要删除前端存储的变量

所以在退出按钮上绑定一个logout方法

<span to="/login" style="text-decoration: none" @click="logout">退出</span>

同时将标签改为span,路由切换在方法中定义

logout(){
  this.$router.push("/login")
  localStorage.removeItem("user")
  this.$message.success("退出成功!")
},

后端register

@PostMapping("/register")
@ResponseBody
public Result register(@RequestBody UserDTO userDTO){
    String username = userDTO.getUsername();
    String password = userDTO.getPassword();
    if (StrUtil.isBlank(username) || StrUtil.isBlank(password)) {
        return Result.error(Constants.CODE_400, "参数错误");
    }
    return Result.success(userService.register(userDTO));
}

之后在userservice中添加register方法

public UserDTO register(UserDTO userDTO) {
    User one = getUserInfo(userDTO);
    if (one == null) {
        one = new User();
        BeanUtil.copyProperties(userDTO, one, true);
        save(one);
        return userDTO;
    } else {
        throw new ServiceException(Constants.CODE_600, "用户已存在");
    }
}

因为wrapper有代码冗余,所以将try catch和获取数据库对象封装成一个方法

private User getUserInfo(UserDTO userDTO){
    QueryWrapper<User> wrapper = new QueryWrapper<>();
    wrapper.eq("username", userDTO.getUsername());
    wrapper.eq("password", userDTO.getPassword());
    User one;

    try{
        one = getOne(wrapper); // 从数据库查询用户信息
    } catch (Exception e){
        LOG.error(e);
        throw new ServiceException(Constants.CODE_500, "系统错误");
    }

    return one;
}

重写后的Register和Login方法如下:

public UserDTO login(UserDTO userDTO) {

    User one=getUserInfo(userDTO);

    if (one != null){
        BeanUtil.copyProperties(one, userDTO, true);
        return userDTO;
    } else {
        throw new ServiceException(Constants.CODE_600, "用户名或密码错误");
    }

}

public UserDTO register(UserDTO userDTO) {
    User one = getUserInfo(userDTO);
    if (one == null) {
        one = new User();
        BeanUtil.copyProperties(userDTO, one, true);
        save(one);
        return userDTO;
    } else {
        throw new ServiceException(Constants.CODE_600, "用户已存在");
    }
}

private User getUserInfo(UserDTO userDTO){
    QueryWrapper<User> wrapper = new QueryWrapper<>();
    wrapper.eq("username", userDTO.getUsername());
    wrapper.eq("password", userDTO.getPassword());
    User one;

    try{
        one = getOne(wrapper); // 从数据库查询用户信息
    } catch (Exception e){
        LOG.error(e);
        throw new ServiceException(Constants.CODE_500, "系统错误");
    }

    return one;
}

前端register

直接CVLogin代码,稍作修改就可以作为注册页面

<template>
  <div class="wrapper">
    <div style="margin: 200px auto; background-color: #fff; width: 350px; height: 400px; padding: 20px; border-radius: 10px">
      <div style="margin: 20px 0; text-align: center; font-size: 24px"><b>注 册</b></div>

      <el-form :model="user" ref="userForm">
        <el-form-item>
          <el-input size="medium" style="margin: 10px 0" prefix-icon="el-icon-user" v-model="user.username"></el-input>
        </el-form-item>

        <el-form-item>
          <el-input size="medium" style="margin: 10px 0" prefix-icon="el-icon-lock" show-password v-model="user.password"></el-input>
        </el-form-item>

      </el-form>

      <div style="margin: 10px 0; text-align: right">
        <el-button type="primary" size="small" autocomplete="off" @click="register">注册</el-button>
        <el-button type="warning" size="small" autocomplete="off" @click="($router.push('/login'))">返回登录</el-button>
      </div>
    </div>

  </div>
</template>

<script>
export default {
  name: "Register",
  data() {
    return {
      user : {}
    }
  },

  methods : {
    register(){
      this.$refs['userForm'].validate((valid) => {
        if (valid){
          // if (this.user.identity === 'user'){
          //   console.log("user")
          // } else if (this.user.identity === 'admin'){
          //   console.log("admin")
          // }
          this.request.post("/user/register",this.user)
              .then(res => {
                if (res.code === '200'){
                  this.$message.success("注册成功!")
                } else{
                  this.$message.error(res.msg)
                }
              })
        } else {
          console.log("error submit!")
          return false
        }
      })


    },
  }
}
</script>

<style scoped>
.wrapper {
  height: 100vh;
  background-color: #FFDEE9;
  background-image: linear-gradient(0deg, #FFDEE9 0%, #B5FFFC 100%);

  overflow: hidden;
}
</style>

image.png