vue+node+mysql全栈项目完整记录

vue+node+mysql全栈项目完整记录

写在前面

因工作需要近半年一直在写B/S架构的项目。博主作为门外汉coder其实刚开始会的语言只有Python和matlab。matlab作为博主研究生期间写论文的主要工具参加工作以后就很少用了。后面因为要做数据分析相关的工作所以自己学了一下Python包括qt界面开发相关的技术。个人觉得Python作为后端开发语言其实已经很强大了。现在在后端开发领域之所以java依旧是王者主要是因为很多项目都是java开发的用java维护起来更加方便。被大家诟病的弱类型语言其实Python3已经可以强制类型了。再者被大家诟病的运行效率低讲真除非那种非常大的项目Python操作起来还是没有压力的至少博主到目前为止没有发现任何问题。随着工作难度的加大后期博主也自学了java确实比Python难度大到目前为止还没学明白尤其是javaweb开发那一套学完就忘。再到后面学习JavaScript发现前后端都可以使用JavaScript进行开发于是就一门心思投入到JavaScript的学习中了。

博主谈一些JavaScript的学习体会吧。JavaScript也是一门弱类型语言不会强制数据的类型这和Python非常像。但是typescript已经解决这个问题了不过个人觉得没有必要硬要通过是否强制类型来区分程序语言的好坏。就小型web项目而言如果前后端都是使用JavaScript进行开发根本就无需对数据类型进行强制规定也不需要使用泛型可能个人的开发还比较基础目前来说没有需要使用泛型的地方。JavaScript有个优点那就是语法简单甚至比Python还要简单。而且JavaScript是前端必备三剑客之一。语法简单前端后端都能开发那还要啥自行车呢。所以博主选择将JavaScript作为主力语言进行web项目开发。

这半年博主跟着B站的up前前后后写了10来个web项目了。也来谈谈个人的体会吧

  • 很多web项目都只有前端后端随便给个接口地址而这些接口地址往往是不固定的过一段时间就没了尤其是那些培训机构给的接口
  • 有些web项目有后端开发但80%的后端都是java开发的比如非常火的若依后台管理系统。倒不是说java开发后端不好java是后端最主流的语言不过学习成本太大了我梳理了一下大致要学的内容有java基础+maven管理工具+SSM框架+springboot没有大半年的学习时间根本没法入门
  • 少部分web项目前后端都是JavaScript开发的不过现在倾向使用分布式数据库MongoDB虽然这个学习成本不大但博主是真用不到博主工作中都是用的关系型数据oraclepostgresql和MySQL都用过其中MySQL用的最多

这样一下来可供学习的资源就非常有限了。博主选定的技术栈如下

  • 后端node开发环境数据库使用MySQL使用express构建服务器框架
  • 前端vue框架axios前后端跨域访问vuex数据管理elementUI搭建页面

而且这半年在个人gitee上已经上传了很多前后端分离web项目B站上也录了一些项目的演示视频。不过一直想做的一件事就是把一个完整的项目开发记录下来离春节还有几天且这几天工作并没有那么忙鼓足勇气写起来吧

写完回过头来看感觉自己写的很乱确实不如视频课程清晰先就这样吧

项目最终界面展示

项目的演示视频如下

登录界面

image-20230116111948779

首页界面

image-20230116112019163

目前重点就写了这两个界面其他界面基本上都差不多了。比较复杂的是第二个界面上面有数据的增删改查功能以及页面的布局设计可以看出来整体上还是很有elementUI的风格的

项目框架搭建

后端

创建后端项目

首先搭建后端框架

新建文件wms>api

image-20230116112350502

api是后端接口的文件夹需要手动创建。app是前端页面的文件夹后面使用vue脚手架创建

使用vscode打开api文件夹并新建终端在终端输入npm init -y将会生成package.json文件

image-20230116112627839

需要安装的包有

"dependencies": {
    "bcryptjs": "^2.4.3",
    "cors": "^2.8.5",
    "express": "^4.17.1",
    "jsonwebtoken": "^8.5.1",
    "mysql": "^2.18.1",
    "svg-captcha": "^1.4.0"
  }

把这个内容拷贝到package.json文件中然后终端输入npm i即可安装这些包

编写入口文件

api根目录下创建app.js文件在app.js中填入以下代码

const express = require('express')
const cors = require('cors')
const bodyParser = require('body-parser')

const app = express()
app.use(cors())
app.use(express.urlencoded({extended: false}))
app.use(bodyParser.json())

// const userRouter = require('./router/user')
// app.use('/api/user', userRouter)

app.listen(3018, () => {
    console.log('api server running at http://127.0.0.1:3018')
})

我注释起来的两行代码是后面要用的创建项目之初不需要。全局安装nodemon终端输入nodemon app.js即可启动项目。

数据库及数据库使用

在mysql中创建wms数据库并创建user表可以自己写几条数据进去

image-20230116150552416

这没啥好说的在根目录下创建db/index.js文件填入如下代码

const mysql = require('mysql');

db = mysql.createPool({
    host: '127.0.0.1',
    user: 'root',
    password: 'xxxxxx',  // 这里使用自己数据库的密码
    database: 'wms'
})

module.exports = db

前端

创建前端项目

在wms根目录下进入终端输入vue create app 选择router、store、scss预处理器使用vue2开发

使用elementUI

终端输入

vue add element

选择全部引入

必要包安装

router、store和element在创建项目时已经引入安装还有以下几个包是必须要的

  • axios访问服务器
  • jwt-decode解码token需要

主要这两个

image-20230116134552819

设置跨域访问全局挂载axios

跨域我习惯在后端设置在前面的代码中后端app.jsapp.use(cors())即是设置跨域访问的

全局挂载axios在入口文件main.js中以下是main.js的全部代码

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './plugins/element.js'
import './assets/global.css'
import axios from 'axios'

Vue.config.productionTip = false
axios.defaults.baseURL = 'http://127.0.0.1:3018/api/'
Vue.prototype.$axios = axios

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

axios.defaults.baseURL = 'http://127.0.0.1:3018/api/'设置后端接口的通用部分地址

Vue.prototype.$axios = axios全局挂载

页面上使用this.$axios即可发起请求

删除无用的文件和代码

vue创建项目并使用router后会在component文件夹下生成About.vue和Home.vue两个文件这两个是无用的且helloworld.vue也没啥用可直接删除。主组件App.vue中很多代码都无用可直接删除删除后自己添加一个路由视图即可

App.vue中的代码

<template>
  <div id="app">
    <router-view></router-view>  
  </div>
</template>

<script>

</script>

<style>
html,
body {
  margin: 0;
  padding: 0;
}

#app {
  height: 100vh;

}
</style>

设置统一的页面样式

我看很多大佬在写的时候都是搞一大堆通用的css代码放进去主要是设置所有页面的页边距、字体等内容然后在main.js中引入即可全局使用在上面的main.js中import './assets/global.css'这行代码就是引入全局样式的代码global.css是我自己写的很简单两行代码

global.css

* {
    margin: 0;
    padding: 0;
}

至此基础的框架已经差不多了接下来将进入主页面的编写

主页面

页面设计

总体页面设计如下

image-20230116140307301

分成三个组件当然这三个组件也可以写在一个文件中但违背了vue框架的初衷分别是侧边栏Aside.vue头部Header.vue主体部分Main.vue

所以第一步应该是写一个主页页面。在views文件夹下创建Main.vue文件代码如下

<template>
    <div>
        <el-container style="height: 100vh; border: 1px solid #eee">
            <el-aside :width="asideWidth" style="background-color: rgb(238, 241, 246);height: 100vh;margin-left: -1px">
                <Aside></Aside>
            </el-aside>

            <el-container style="height: 100vh;">
                <el-header
                    style="text-align: right; font-size: 12px;border-bottom: rgba(168, 168, 168, 0.3) 1px solid;">
                    <Header></Header>
                </el-header>

                <el-main>
                    <router-view></router-view>
                </el-main>
            </el-container>
        </el-container>
    </div>
</template>

<script>
import Aside from '../components/Aside.vue';
import Header from '../components/Header.vue'
// import Main from '../components/Main.vue';
export default {
    data() {
        return {
            
        }
    },
    components: { Aside, Header },
    methods: {
        
    },
    computed: {
        asideWidth(){        
            return this.$store.state.asideWidth
        }
    },
    
}
</script>

<style scoped>
.el-header {
    /* background-color: #B3C0D1; */
    color: #333;
    line-height: 60px;
}

.el-main {
    padding: 5px;
}

.el-aside {
    color: #333;
}
</style>

先别看script和style部分先看template的框架

最外面那个div其实可以不要我写习惯了。因为里面就只有一个el-container大标签如果还有其他标签的话div就必须要写了这个和vue3差别挺大

el-container标签下有块内容分别对应侧边栏Aside.vue、顶部栏Header.vue和页面主体部分el-main标签其中顶部栏和页面主体部分又是组合在一起放在同一个el-container标签下的

而这里最重要的“页面主体部分”并没有写死而是给了一个router-view标签这是vue的路由视图标签也就是说这里的内容会根据路由的变化而变化

至此已经出现两个router-view了这里不太好理解至少是对于我来说很不好理解我是写了十来个项目后才稍微搞清楚了点

  1. 第一个router-view出现在主组件App.vue中也就是说进入http://localhost:8080/这个路由就应该由视图来提供页面
  2. 第二个router-view出现在Main.vue组件中也就是说Main.vue作为App.vue的一个组件为App.vue提供一个页面而Main.vue应该有其他组件为其提供页面

既然到了这里那么自然而然的进入了路由接下来将进行路由的设置

路由设计

路由设计的思路理一下

首先是为App.vue提供视图的路由有两个一个是登录页面一个是主界面

其次是为主界面Main.vue提供视图的路由也就是侧边栏上“首页”、”导航一“、”导航二“以及还有一个显示个人信息的页面

App.vue

基本路由就这样了

看一下路由文件中的代码router/index.js

import Vue from "vue";
import VueRouter from "vue-router";
import Main from "../views/Main.vue";

Vue.use(VueRouter);

const routes = [
  {
    path: "/",
    redirect: "/home",
    component: Main,
    children: [
      {
        path: "/home",
        name: "home",
        component: () => import("../views/Home.vue"),
      },
      {
        path: "/userinfo",
        component: () => import("../views/UserInfo.vue"),
      },
      {
        path: "/admin",
        name: "admin",
        component: () => import("../views/AdminManage.vue"),
      },
      {
        path: "/user",
        name: "user",
        component: () => import("../views/UserManage.vue"),
      },
    ],
  },
 
  {
    path: "/login",
    name: "login",
    component: () => import("../views/Login.vue"),
  },
];

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

export default router;

router数组中的内容是自己写的对照上面的图片其实就挺好理解了

登录页面

再看一下登录页面登录页面的路由是http://localhost:8080/login当然如果token过期访问主页时会直接跳转到这个登录页面这是后话

登录页面有两个注意点

image-20230116145154434

其实挺简单的一个页面就是一个表单但是表单的需要设计一下里面还有个验证码

前端

先上前端代码

<template>
    <div class="login">
        <el-form class="login-form" :model="loginForm" ref="loginForm" :rules="rules">
            <h3 class="title">仓库管理系统-登录</h3>
            <el-form-item prop="no">
                <el-input placeholder="账号" type="text" prefix-icon="el-icon-user-solid" v-model="loginForm.no">
                </el-input>
            </el-form-item>
            <el-form-item prop="password">
                <el-input placeholder="密码" type="password" prefix-icon="el-icon-lock"
                    v-model="loginForm.password">
                </el-input>
            </el-form-item>
            <el-form-item prop="checkCode">
                <el-input placeholder="验证码" prefix-icon="el-icon-picture-outline-round"
                    v-model="loginForm.checkCode" style="width: 63%" @keyup.enter.native="submit('loginForm')">
                </el-input>
                <div class="login-code">
                    <!-- <img :src="codeUrl" class="login-code-imgs" @click="getCode"/> 
          -->
                    <div @click="getCode" v-html="code" style="vertical-align:middle"></div>
                </div>
            </el-form-item>
            <el-checkbox v-model="loginForm.rememberme" style="margin: 0 0 15px 0;">记住我</el-checkbox>
            <el-form-item>
                <el-button style="width: 100%" @click="submit('loginForm')" type="primary">登录</el-button>
            </el-form-item>
        </el-form>
        <div class="el-login-footer">
            <span>Copyright © 2018-2022 huanggang All Rights Reserved.</span>
        </div>
    </div>
</template>

<script>
// import { getCodeImg } from '@/api/login'
import jwt_decode from 'jwt-decode'
export default {
    data() {
        return {
            loginForm: {
                no: '',
                password: '',
                checkCode: '',
                rememberme: false
            },
            codeUrl: '',
            code: '',
            codeText: '',
            redirect: undefined,

            rules: {
                no: [
                    { required: true, message: "用户名不能为空", trigger: "blur" },
                    { min: 2, max: 30, message: "长度在 2 到 30 个字符", trigger: "blur" }
                ],
                password: [
                    { required: true, message: "密码不能为空", trigger: "blur" },
                    { min: 3, max: 30, message: "长度在 6 到 30 个字符", trigger: "blur" }
                ],
                checkCode: [{ required: true, message: "验证码不能为空", trigger: "blur" }]
            }
        }
    },
    watch: {
        $route: {
            handler: function (route) {
                // console.log('gsdrouter', route)
                this.redirect = route.query && route.query.redirect
            },
            immediate: true,
        }
    },
    methods: {
        submit(formName) {
            this.$refs[formName].validate(valid => {
                if (valid) {
                    if (this.loginForm.checkCode.toLowerCase() == this.codeText.toLowerCase()) {
                        this.$axios.post('/user/login', this.loginForm)
                            .then(res => {
                                // console.log(res)
                                const { token } = res.data
                                localStorage.setItem('wmstoken', token)

                                const decode = jwt_decode(token)
                                // console.log(decode)
                                const userInfo = {
                                    no: decode.no,
                                    name: decode.name,
                                    token
                                }
                                // console.log(userInfo)
                                this.$store.commit('setUser', userInfo)

                                this.$message({
                                    type: "success",
                                    message: '登录成功'
                                })

                                this.$router.push({ path: this.redirect || '/' })
                            })
                            .catch(err => {
                                console.log(err)
                                this.$message.error('登录失败请检查用户名及密码是否正确')
                            })
                    } else { this.$message.error('验证码错误') }

                } else {
                    this.$message({
                        type: "error",
                        message: '错误提交申请'
                    })
                    return false
                }
            })
           
        },
        getCode() {
            this.$axios.get('/user/checkCode')
                .then(res => {
                    // console.log(res)
                    this.code = res.data.data
                    this.codeText = res.data.text
                })
        }
    },
    created() {
        this.getCode()
    }
}
</script>

<style scoped lang="scss">
.login {
    display: flex;
    height: 100%;
    background: url(../assets/images/login-background.jpg);
    background-size: cover;
    justify-content: center;
    align-items: center;
}

.title {
    text-align: center;
    color: #707070;
    margin: 0 auto 30px;
}

.login-form {
    box-sizing: border-box;
    width: 400px;
    border-radius: 6px;
    background-color: rgba(248, 248, 255, 0.9);
    padding: 25px 25px 5px 25px;
}

.el-login-footer {
    height: 40px;
    line-height: 40px;
    position: fixed;
    bottom: 0;
    color: #fff;
    font-size: 12px;
    letter-spacing: 1px;
    font-family: Arial;
}

.login-code {
    width: 33%;
    float: right;

    div {
        cursor: pointer;
    }
}

.login-code-imgs {}
</style>

看代码表单的输入框都设置了校验规则这是elementUI提供的还是很好用的

验证码由后端提供 getCode()方法即为获取验证码的方法看下我前面两篇博客应该还挺好理解

后端

应有两个接口一个是登录的接口一个是验证码的接口

登录接口

我习惯把接口函数和接口请求分开写

  • 创建router文件夹新建user.js文件里面编写user相关的接口请求
  • 创建router_handle文件夹新建user.js文件里面编写user相关的请求函数

直接上代码

router_handle/user.js

const db = require("../db/index");
const svgCaptcha = require("svg-captcha");
const config = require("../config");
const jwt = require("jsonwebtoken");

// 登录
exports.login = (req, res) => {
  const user = req.body;
  const sql = "select * from user where no = ?";
  db.query(sql, user.no, (err, results) => {
    if (err) return res.status(400).json(err);
    if (results.length !== 1) return res.status(400).json("用户不存在");
    if (results[0].password != user.password)
      return res.status(400).json("用户名或密码错误请重新输入");

    const user_info = {
      id: results[0].id,
      no: results[0].no,
      name: results[0].name,
    };
    const tokenStr = jwt.sign(user_info, config.jwtSecretKey, {
      expiresIn: "10h",
    });
    res.json({
      status: 200,
      message: "登录成功",
      token: "Bearer " + tokenStr,
    });
  });
};


// 验证码
exports.checkCode = (req, res) => {
  const img = svgCaptcha.create({
    size: 4,
    ignoreChars: "0o1l",
    color: true,
    noise: 2,
    background: "#FFF0F5",
    height: 38,
    width: 113,
  });
  res.send(img);
};

写法基本固定登录成功后向前端传递信息最重要的就是token信息这里的token是加密了的秘钥是我自己随便写的一个字符串存在根目录下的config.js文件中

验证码接口

代码已经写在上面了写法也基本固定根据自己的需求设置要忽略的字符、噪声的条数、背景颜色、宽高等

接口请求

即router/user.js中的代码

const { urlencoded } = require('express');
const express = require('express');
const router = express.Router();

const userHandler = require('../router_handler/user.js')
// router.get('/list', userHandler.list)
// router.post('/add', userHandler.add)
// router.post('/mod/:id', userHandler.mod)
// router.post('/delete/:id', userHandler.delete)
// router.post('/searchByName', userHandler.searchByName)

router.get('/checkCode', userHandler.checkCode)
router.post('/login', userHandler.login)

module.exports = router

我注释起来的那几条是后面要用到的

前后端数据交互

真不知道怎么写说简单其实也挺复杂的就是要理解这么个过程

前端发送ajax(axios)请求点击登录按钮的时候触发submit(formName) 方法本节的前端代码里面

逻辑如下

image-20230116154320704

第一次用draw.io画的太丑请见谅

这个图结合前面的代码看基本上就能看明白是怎么回事了

图上有一个向vuex提交数据代码中有this.$store.commit('setUser', userInfo)这是怎么一回事接下来进入vuex的相关内容

vuex数据共享

前面说了创建vue项目时需要使用store就是这里的vuex了这个vuex是一个全局数据管理工具。怎么个意思呢就是组件之间要传递数据共享数据可以用vuex。当然也可以不用vuex不用的原因就是太复杂了有简单的数据共享方式可以实现父子传值、兄弟传值参见我的博客——vue组件之间的数据共享

怎么说呢如果只是简单地数据共享那么就使用上面的父传子、子传父好了有些项目数据相当复杂组件之间的父子关系也很复杂那么传来传去就很繁琐了因此需要有个空间来存储这个值vuex就是干这个事情的创建项目时引入了store这个单词本身就有仓库、存储的意思是不是很巧妙

登录成功后前端将用户信息保存下来并把数据保存到vuex中到底是怎么进行的呢

其实vuex还挺复杂的我自己也有一篇博客记录参见–vuex笔记看完这个博客应该还是一脸懵逼很正常必须得自己写才能领悟其中的奥义

总体来说就5个步骤最常用的是state和mutations当然很多人喜欢用actions我理解的actions就是异步的mutations

回到代码登录时有几行关键代码涉及vuex如下

const { token } = res.data
const decode = jwt_decode(token)
const userInfo = {
                  	no: decode.no,
                  	name: decode.name,
                  	token
                  }
this.$store.commit('setUser', userInfo)

jwt_decode把token解析出来然后创建个userInfo对象来存储解析后的用户账号和姓名并把原始的token也放进去

然后调用store的commit方法this.$store.commit('setUser', userInfo)这个commit对应的mutations而actions对应的是dispatch这里要注意区分。另外commit方法有两个参数前面的setUser是mutations中的方法名userinfo是要传递的数据

已经写到这里了那不得不把store/index.js放出来了看一下代码

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

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    isCollapse: false,
    asideWidth: "200px",
    iconDirection: "el-icon-s-fold",
    userInfo: {
      no: "",
      name: "",
      token: "",
    },
  },
  getters: {
    userInfo: state => state.userInfo
  },
  mutations: {
    collapseMenu(state) {
      state.isCollapse = !state.isCollapse;
      if (state.isCollapse) {
        state.asideWidth = "64px";
        state.iconDirection = "el-icon-s-unfold";
      } else {
        state.asideWidth = "200px";
        state.iconDirection = "el-icon-s-fold";
      }
    },
    setUser(state, payload) {
      state.userInfo = payload;
    },
    clearUser(state) {
      state.userInfo = {
        no: "",
        name: "",
        token: "",
      };
    },
  },
  actions: {},
  modules: {},
});

这里的store是我写完所有项目后的内容因此只需要关注mutations中的setUser和state中的userInfo这里就和组件中的commit关联起来了setUser其实是将组件中传递过来的userinfo写入到state中那么其他页面想用的时候随时用就可以了

主页面-侧边栏

登录页面相对来说比较简单但主页面涉及路由跳转而且分成了三块所以拆分进行记录

先介绍侧边栏Aside.vue

先上代码

<template>
    <el-menu style="height: 100vh" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b"
        default-active="/home" :collapse="isCollapse" :collapse-transition="false" router>
        <el-menu-item index="/home">
            <i class="el-icon-s-home"></i>
            <span slot="title">首页</span>
        </el-menu-item>

        <el-menu-item index="/admin">
            <i class="el-icon-s-flag"></i>
            <span slot="title">导航一</span>
        </el-menu-item>
        <el-menu-item index="/user">
            <i class="el-icon-s-opportunity"></i>
            <span slot="title">导航二</span>
        </el-menu-item>
    </el-menu>
</template>

<script>
export default {
    data() {
        return {

        }
    },
    computed: {
        isCollapse() {
            return this.$store.state.isCollapse
        }
    }
}
</script>

<style>

</style>

注意看el-menu标签中的参数一个个介绍吧

  1. style="height: 100vh"侧边栏应垂直铺满屏幕

  2. background-color="#545c64"侧边栏的背景色就像下面这个颜色

    这里的背景色是#545c64
  3. text-color="#fff"文字字体是白色

  4. active-text-color="#ffd04b"激活状态下的颜色

  5. default-active="/home"默认激活的路由

  6. :collapse="isCollapse"这玩意儿挺重要就是说是不是要展开侧边栏true是折叠false是展开**点击Header.vue中的一个图标就会切换状态看这就又用到了vuex了**这也是写到store中用来全局共享的数据

  7. :collapse-transition="false"展开/折叠动画要不要false就是不要

  8. router这个相当重要!important=10000这关系到点击侧边栏上的文字能不能实现路由的跳转这里如果不写就得在每个item上面去写一个router-link了。写了router后就可以直接在item中直接写上index="/home"这种路由了这里的路由要和router/index.js中的路由保持一致

然后看script吧

script中只有一个计算属性就是控制侧边栏是否展开的

对比store/index.js中的state.iscollapse和组件中的写法这里不需要用到mutations直接取值就好更改值在Header.vue组件中

主页面–头部导航栏

先上代码

<template>
    <div style="display: flex; justify-content: space-between; align-items: center;">
        <div style="margin-top: 8px">
            <i :class="iconDirection" @click="handleCollapse" style="font-size:24px;cursor: pointer;"></i>
        </div>
        <h1 class="middle" style="font-size: 24px">欢迎来到仓库管理系统</h1>
        <div class="right">
            <el-dropdown trigger="click">
                <span>{{ user.name }}</span>
                <i class="el-icon-arrow-down" style="margin-left: 5px"></i>
                <el-dropdown-menu slot="dropdown">
                    <el-dropdown-item @click.native="toUser">个人资料</el-dropdown-item>
                    <el-dropdown-item @click.native="logout">退出</el-dropdown-item>
                </el-dropdown-menu>
            </el-dropdown>
        </div>
    </div>
</template>

<script>
export default {
    date() {
        return {}
    },
    methods: {
        toUser() {
            this.$router.push('/userinfo')
        },
        logout() {
            localStorage.removeItem('wmstoken')
            this.$store.commit('clearUser')
            this.$router.push('/login')
        },
        handleCollapse() {
            this.$store.commit('collapseMenu')
        }
    },
    computed: {
        iconDirection() {
            return this.$store.state.iconDirection
        },
        user() {
            return this.$store.state.userInfo
        }
    }
}
</script>

<style>

</style>

头部导航栏还挺复杂的实现了这么几个功能

  1. 侧边栏是否展开根据状态更改控制图标的类型其实就是箭头的方向
  2. 下拉用户信息点击用户姓名可以跳转查看用户名和注销

计算属性的说明

这里最关键的是两个计算属性

  • iconDirection图标方向有两个值el-icon-s-fold和el-icon-s-unfold切换图标方向的图示这里从store/index.js中可以看明白

    回顾一下代码

    export default new Vuex.Store({
      state: {
        isCollapse: false,
        asideWidth: "200px",
        iconDirection: "el-icon-s-fold",
        
      },
      getters: {},
      mutations: {
        collapseMenu(state) {
          state.isCollapse = !state.isCollapse;
          if (state.isCollapse) {
            state.asideWidth = "64px";
            state.iconDirection = "el-icon-s-unfold";
          } else {
            state.asideWidth = "200px";
            state.iconDirection = "el-icon-s-fold";
          }
        },
        setUser(state, payload) {
          state.userInfo = payload;
        },
        clearUser(state) {
          state.userInfo = {
            no: "",
            name: "",
            token: "",
          };
        },
      },
      actions: {},
      modules: {},
    });
    

    点击折叠侧边栏后还要更改侧边栏的宽度当然不是写在这里的写在我们的Main.vue组件中因此要回顾一下Main.vue主页面上的代码asideWidth在这个组件中使用的

    <el-aside :width="asideWidth" style="background-color: rgb(238, 241, 246);height: 100vh;margin-left: -1px">
       <Aside></Aside>
    </el-aside>
    

    看完整的Main.vue代码发现script中也有个计算属性计算的就是asideWidth值

用户信息的使用

用户信息userinfouserinfo.name放在顶部栏的右侧下拉中

作为计算属性在标签中直接使用模板语法就可以了

<span>{{ user.name }}</span>

当使用不同的账户进行登录时能显示不同的用户名

image-20230116165315515

image-20230116165428297

主页面–动态页面之home页面

其实这个项目就一个home页面也是点击首页后跳转的页面创建views/Home.vue文件还是先上代码

<template>
    <div>
        <div class="search" style="margin-bottom: 5px">
            <el-input placeholder="请输入姓名" style="width: 200px" suffix-icon="el-icon-search" v-model="searchContent"
                @keyup.enter.native="handleSearch"></el-input>
            <el-select v-model="sex" placeholder="请选择" style="margin-left: 5px; width: 200px">
                <el-option v-for="item in sexs" :key="item.value" :label="item.label" :value="item.value">
                </el-option>
            </el-select>
            <el-button type="primary" style="margin-left: 5px" size="small" @click="handleSearch">搜索</el-button>
            <el-button type="success" style="margin-left: 5px" size="small" @click="handleReset">重置</el-button>

            <el-button type="primary" style="margin-left: 5px" size="small" @click="handleAdd" round>新增</el-button>

        </div>
        <el-table :data="tableData" :header-cell-style="{ background: '#f3f6fd', color: '#555' }" border>
            <el-table-column prop="id" label="ID" width="70">
            </el-table-column>
            <el-table-column prop="no" label="账号">
            </el-table-column>
            <el-table-column prop="name" label="姓名">
            </el-table-column>
            <el-table-column prop="role_id" label="角色">
                <template slot-scope="scope">
                    <el-tag
                        :type="scope.row.role_id === 0 ? 'danger' : (scope.row.role_id === 1 ? 'primary' : 'success')"
                        disable-transitions>{{
                            scope.row.role_id === 0 ? '超级管理员' : (scope.row.role_id === 1 ? '管理员'
                                : '用户')
                        }}</el-tag>
                </template>
            </el-table-column>
            <el-table-column prop="sex" label="性别">
                <template slot-scope="scope">
                    <el-tag :type="scope.row.sex === 1 ? 'primary' : 'success'" disable-transitions>{{
                        scope.row.sex ===
                            1 ? '男' : '女'
                    }}</el-tag>
                </template>
            </el-table-column>
            <el-table-column prop="age" label="年龄">
            </el-table-column>
            <el-table-column prop="phone" label="电话">
            </el-table-column>
            <el-table-column prop="operate" label="操作">
                <template slot-scope="scope">
                    <el-button type="primary" icon="el-icon-edit-outline" title="编辑" size="small"
                        @click="handleEdit(scope.$index, scope.row)" circle>
                    </el-button>
                    <el-button type="danger" icon="el-icon-delete-solid" size="small" title="删除"
                        @click="handleDelete(scope.$index, scope.row)" circle></el-button>
                </template>
            </el-table-column>
        </el-table>
        <!-- 分页 -->
        <div class="pagination">
            <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange"
                :current-page.sync="paginations.page_index" :page-sizes="paginations.page_sizes"
                :page-size="paginations.page_size" :layout="paginations.layout" :total="paginations.total">
            </el-pagination>
        </div>

        <el-dialog :title="title" :visible.sync="centerDialogVisible" width="30%" center>
            <el-form ref="form" :model="form" label-width="100px" :rules="rules">
                <el-form-item label="账号" prop="no">
                    <el-col :span="20">
                        <el-input v-model="form.no" :disabled="isAbled"></el-input>
                    </el-col>
                </el-form-item>
                <el-form-item label="姓名" prop="name">
                    <el-col :span="20">
                        <el-input v-model="form.name"></el-input>
                    </el-col>
                </el-form-item>
                <el-form-item label="密码" prop="password">
                    <el-col :span="20">
                        <el-input v-model="form.password"></el-input>
                    </el-col>
                </el-form-item>
                <el-form-item label="年龄" prop="age">
                    <el-col :span="20">
                        <el-input v-model="form.age"></el-input>
                    </el-col>
                </el-form-item>
                <el-form-item label="性别">
                    <el-radio-group v-model="form.sex">
                        <el-radio label="1"></el-radio>
                        <el-radio label="2"></el-radio>
                    </el-radio-group>
                </el-form-item>
                <el-form-item label="电话" prop="phone">
                    <el-col :span="20">
                        <el-input v-model="form.phone"></el-input>
                    </el-col>
                </el-form-item>

            </el-form>
            <span slot="footer" class="dialog-footer">
                <el-button @click="centerDialogVisible = false" size="small">取 消</el-button>
                <el-button type="primary" @click="submitForm('form')" size="small">确 定</el-button>
            </span>
        </el-dialog>
    </div>
</template>

<script>
export default {
    data() {
        return {
            tableData: [],
            allTableData: [],  // 存放所有表格数据以及搜索后的数据
            filterTableData: [],  // 存放所有表格数据供搜索
            paginations: {
                page_index: 1,  // 当前页
                total: 0,  // 总数
                page_size: 10,  // 默认每页显示多少条
                page_sizes: [5, 10, 15, 20],  // 选择每页显示多少条
                layout: "total, sizes, prev, pager, next, jumper"
            },
            searchContent: '',
            sex: '',
            sexs: [
                { value: 1, label: '男' },
                { value: 2, label: '女' },

            ],
            centerDialogVisible: false,
            form: {
                no: '',
                name: '',
                password: '',
                age: '',
                phone: '',
                sex: '1',
                role_id: '2',
                id: ''
            },
            rules: {
                no: [
                    { required: true, message: '请输入账号', trigger: 'blur' },
                    { min: 2, max: 10, message: '长度在2-10个字符', trigger: 'blur' }
                ],
                name: [
                    { required: true, message: '请输入姓名', trigger: 'blur' },
                    { min: 2, max: 10, message: '长度在2-10个字符', trigger: 'blur' }
                ],
                password: [
                    { required: true, message: '请输入密码', trigger: 'blur' },
                    { min: 3, max: 10, message: '长度在3-10个字符', trigger: 'blur' }
                ],
                age: [
                    { required: true, message: '请输入年龄', trigger: 'blur' },
                    { pattern: /^(?:[1-9][0-9]?|1[01][0-9]|120)$/, message: '请输入正确的年龄', trigger: 'blur' }
                ],
                phone: [
                    { required: true, message: '请输入手机号', trigger: 'blur' },
                    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
                ]
            },
            title: '新增用户',
            operateType: 'add',
            isAbled: false,
        }
    },
    methods: {
        getUser() {
            this.$axios.get('/user/list')
                .then(res => {
                    if (res.data.status === 200) {
                        // this.tableData = res.data.results
                        this.allTableData = res.data.results
                        this.filterTableData = res.data.results
                        this.setPaginations()
                    }
                    // console.log(res)
                })
        },
        handleEdit(index, row) {
            // console.log(row)
            this.operateType = 'mod'
            this.centerDialogVisible = true
            this.isAbled = true
            this.title = '编辑用户'
            this.form = row
            this.form.sex = row.sex.toString()
            this.form.password = ''

        },
        handleDelete(index, row) {
            this.$confirm('此操作将永久删除该用户是否继续', "提示", {
                confirmButtonText: '确定',
                cancelButtonText: '取消',
                type: 'warning'
            }).then(() => {
                this.$axios.post(`/user/delete/${row.id}`)
                    .then(res => {
                        this.$message({
                            type: 'success',
                            message: '删除成功'
                        })
                        this.getUser()
                    })
            })
        },

        // 分页相关函数
        setPaginations() {
            this.paginations.total = this.allTableData.length
            this.paginations.page_index = 1
            this.tableData = this.allTableData.filter((item, index) => index < this.paginations.page_size)
            // console.log(this.allTableData[0])

        },
        handleSizeChange(page_size) {
            this.paginations.page_index = 1
            this.paginations.page_size = page_size
            this.tableData = this.allTableData.filter((item, index) => {
                return index < page_size
            })

        },
        // 页面跳转
        handleCurrentChange(page) {
            const index = this.paginations.page_size * (page - 1)
            const nums = this.paginations.page_size * page
            const tables = []
            for (let i = index; i < nums; i++) {
                if (this.allTableData[i]) tables.push(this.allTableData[i])
            }
            this.tableData = tables
        },

        // 搜索
        handleSearch() {
            if (!this.searchContent && !this.sex) {
                this.$message({ type: 'warning', message: '请输入用户姓名或性别进行查询' })
                this.getUser()
                return
            }
            // 这里有两种方法可以实现通过姓名的模糊查询
            // 第一种在返回的数据中通过filter函数模糊查询使用filter中的includes方法实现模糊查询
            // 加入性别后这里的查询显得有点啰嗦了后面还需要改进
            if (this.searchContent && this.sex) {
                this.allTableData = this.filterTableData.filter((item, index) => {
                    return item.name.includes(this.searchContent) && item.sex === this.sex
                })
                this.setPaginations()
            }
            if (this.searchContent && !this.sex) {
                this.allTableData = this.filterTableData.filter((item, index) => {
                    return item.name.includes(this.searchContent)
                })
                this.setPaginations()
            }
            if (!this.searchContent && this.sex) {
                this.allTableData = this.filterTableData.filter((item, index) => {
                    return item.sex === this.sex
                })
                this.setPaginations()
            }


            // 第二种后端写了模糊查询的接口接口为/user/searchByName然后传递name参数注意这里之前使用的get请求
            // 后来发现get请求不适用于传参的请求改成post请求后就可以正常获取数据了
            // 由于还有一个是通过性别来查询我在后端没有写性别查询的接口所以这里就使用第一种查询方法了
            // this.$axios.post('/user/searchByName', { name: this.searchContent })
            //     .then(res => {
            //         if (res.data.status === 200) {
            //             this.allTableData = res.data.results
            //             this.setPaginations()
            //         } else {
            //             this.$message.warning('请求数据出错')
            //         }
            //     })
        },

        // 重置按钮的点击函数
        handleReset() {
            this.searchContent = ''
            this.sex = ''
            this.getUser()
        },

        // 新增按钮的点击函数
        handleAdd() {
            this.centerDialogVisible = true
            this.operateType = 'add'
            this.isAbled = false
            this.resetForm()
        },


        // 提交新增或编辑按钮的回调函数
        submitForm(formName) {
            const url = this.operateType == 'add' ? '/user/add' : `/user/mod/${this.form.id}`
            this.$refs[formName].validate(valid => {
                if (valid) {
                    this.$axios.post(url, this.form)
                        .then(res => {
                            if (res.data.status === 200) {
                                this.$message.success('操作成功!')
                                this.centerDialogVisible = false
                                this.resetForm()
                                this.getUser()
                            } else {
                                this.$message.error('操作失败请更换账号重试')
                            }
                        })
                        .catch(err => {
                            this.$message.error('操作失败请更换账号重试')
                            this.form.no = ''
                        })
                } else {
                    this.$message.warning('请输入有效的数据')
                }
            })
        },

        // 重置表单中的内容
        resetForm() {
            this.form = {
                no: '',
                name: '',
                password: '',
                age: '',
                phone: '',
                sex: '1'
            }
            // this.$refs.form.resetFields()
        }

    },
    created() {
        this.getUser()
    },
}
</script>

<style scoped>
.pagination {
    float: right;
    margin-top: 10px;
}
</style>

来回顾一下页面

image-20230116165841766

代码比较长分别解释一下我把它分成4个部分

  1. 顶部有一个两个搜索输入框一个搜索姓名一个搜索性别3个按钮一个搜索一个重置一个新增
  2. 主要部分是一个表格展示后端传递过来的数据
  3. 单条数据的删、改
  4. 分页

其中2中的新增和3中的单条数据的改共用一个el-dialog对话框点击的时候跳出对话框来新增或更改数据

顶部输入框与按钮

页面框架

<div class="search" style="margin-bottom: 5px">
	<el-input placeholder="请输入姓名" style="width: 200px" suffix-icon="el-icon-search" v-model="searchContent" @keyup.enter.native="handleSearch"></el-input>
    <el-select v-model="sex" placeholder="请选择" style="margin-left: 5px; width: 200px">
    	<el-option v-for="item in sexs" :key="item.value" :label="item.label" :value="item.value">
        </el-option>
	</el-select>
    <el-button type="primary" style="margin-left: 5px" size="small" @click="handleSearch">搜索</el-button>
    <el-button type="success" style="margin-left: 5px" size="small" @click="handleReset">重置</el-button>

    <el-button type="primary" style="margin-left: 5px" size="small" @click="handleAdd" round>新增</el-button>
</div>

相关的JavaScript代码

    data() {
        return {
            searchContent: '',
            sex: '',
            sexs: [
                { value: 1, label: '男' },
                { value: 2, label: '女' },

            ],
            
        }
    },       
		// 搜索
        handleSearch() {
            if (!this.searchContent && !this.sex) {
                this.$message({ type: 'warning', message: '请输入用户姓名或性别进行查询' })
                this.getUser()
                return
            }
            // 这里有两种方法可以实现通过姓名的模糊查询
            // 第一种在返回的数据中通过filter函数模糊查询使用filter中的includes方法实现模糊查询
            // 加入性别后这里的查询显得有点啰嗦了后面还需要改进
            if (this.searchContent && this.sex) {
                this.allTableData = this.filterTableData.filter((item, index) => {
                    return item.name.includes(this.searchContent) && item.sex === this.sex
                })
                this.setPaginations()
            }
            if (this.searchContent && !this.sex) {
                this.allTableData = this.filterTableData.filter((item, index) => {
                    return item.name.includes(this.searchContent)
                })
                this.setPaginations()
            }
            if (!this.searchContent && this.sex) {
                this.allTableData = this.filterTableData.filter((item, index) => {
                    return item.sex === this.sex
                })
                this.setPaginations()
            }


            // 第二种后端写了模糊查询的接口接口为/user/searchByName然后传递name参数注意这里之前使用的get请求
            // 后来发现get请求不适用于传参的请求改成post请求后就可以正常获取数据了
            // 由于还有一个是通过性别来查询我在后端没有写性别查询的接口所以这里就使用第一种查询方法了
            // this.$axios.post('/user/searchByName', { name: this.searchContent })
            //     .then(res => {
            //         if (res.data.status === 200) {
            //             this.allTableData = res.data.results
            //             this.setPaginations()
            //         } else {
            //             this.$message.warning('请求数据出错')
            //         }
            //     })
        },

        // 重置按钮的点击函数
        handleReset() {
            this.searchContent = ''
            this.sex = ''
            this.getUser()
        },

        // 新增按钮的点击函数
        handleAdd() {
            this.centerDialogVisible = true
            this.operateType = 'add'
            this.isAbled = false
            this.resetForm()
        },
  1. el-input输入框是通过姓名来查找用户suffix-icon="el-icon-search"后置搜索图标@keyup.enter.native="handleSearch"绑定原生回车键到handleSearch方法这和后面的搜索按钮功能是一样的

  2. el-select是选择器v-model的值为当前被选中的el-option的 value 属性值还有一个label值就是页面上显示的选择器的实际值

  3. 按钮1——搜索键根据选定条件name+sex查询数据绑定的是handleSearch()方法我这里写的有点复杂了写了4个if感觉有点low。比较好的选择是后端提供接口然后直接返回数据进行显示就行了实际上我也这么做了后来放弃了。但是我在写代码的时候逻辑上是name框和sex选择器有可能两个都有数据有可能只其中一个有数据有可能两个都没数据那么后端写接口的时候要分好几种情况去写所以我觉得后端可以把全部的数据的提供给前端实际上就只写一个接口然后前端要什么数据就自己取好了这种思路经常被我终端开发的LP鄙视她的想法是前端需要什么数据后端就应该提供什么而不是一股脑的都给前端增加前端的工作量。

    分这么几种情况

    • 两个框都没数据那么直接提示就可以了
    • 两个框都有数据使用JavaScript自己的filter函数匹配两个数据
    • 其中一个框有数据也是使用filter函数匹配一个数据
  4. 按钮2——重置按钮这个按钮相对简单把输入框里的数据清空同时重新获取一下后端的数据填充到表格里面即可

  5. 按钮3——新增按钮这个按钮比较复杂要和下面表格中的修改按钮联合起来使用怎么个逻辑呢来解释一下

    • 点击新增按钮跳出一个对话框这个对话框是个form表单结构如下图

    image-20230117093709070

    相应的代码如下

    html结构

            <el-dialog :title="title" :visible.sync="centerDialogVisible" width="30%" center>
                <el-form ref="form" :model="form" label-width="100px" :rules="rules">
                    <el-form-item label="账号" prop="no">
                        <el-col :span="20">
                            <el-input v-model="form.no" :disabled="isAbled"></el-input>
                        </el-col>
                    </el-form-item>
                    <el-form-item label="姓名" prop="name">
                        <el-col :span="20">
                            <el-input v-model="form.name"></el-input>
                        </el-col>
                    </el-form-item>
                    <el-form-item label="密码" prop="password">
                        <el-col :span="20">
                            <el-input v-model="form.password"></el-input>
                        </el-col>
                    </el-form-item>
                    <el-form-item label="年龄" prop="age">
                        <el-col :span="20">
                            <el-input v-model="form.age"></el-input>
                        </el-col>
                    </el-form-item>
                    <el-form-item label="性别">
                        <el-radio-group v-model="form.sex">
                            <el-radio label="1"></el-radio>
                            <el-radio label="2"></el-radio>
                        </el-radio-group>
                    </el-form-item>
                    <el-form-item label="电话" prop="phone">
                        <el-col :span="20">
                            <el-input v-model="form.phone"></el-input>
                        </el-col>
                    </el-form-item>
    
                </el-form>
                <span slot="footer" class="dialog-footer">
                    <el-button @click="centerDialogVisible = false" size="small">取 消</el-button>
                    <el-button type="primary" @click="submitForm('form')" size="small">确 定</el-button>
                </span>
            </el-dialog>
    

    JavaScript

    <script>
    export default {
        data() {
            return {
                
                centerDialogVisible: false,
                form: {
                    no: '',
                    name: '',
                    password: '',
                    age: '',
                    phone: '',
                    sex: '1',
                    role_id: '2',
                    id: ''
                },
                rules: {
                    no: [
                        { required: true, message: '请输入账号', trigger: 'blur' },
                        { min: 2, max: 10, message: '长度在2-10个字符', trigger: 'blur' }
                    ],
                    name: [
                        { required: true, message: '请输入姓名', trigger: 'blur' },
                        { min: 2, max: 10, message: '长度在2-10个字符', trigger: 'blur' }
                    ],
                    password: [
                        { required: true, message: '请输入密码', trigger: 'blur' },
                        { min: 3, max: 10, message: '长度在3-10个字符', trigger: 'blur' }
                    ],
                    age: [
                        { required: true, message: '请输入年龄', trigger: 'blur' },
                        { pattern: /^(?:[1-9][0-9]?|1[01][0-9]|120)$/, message: '请输入正确的年龄', trigger: 'blur' }
                    ],
                    phone: [
                        { required: true, message: '请输入手机号', trigger: 'blur' },
                        { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
                    ]
                },
                title: '新增用户',
                operateType: 'add',
                isAbled: false,
            }
        },
        methods: {
            getUser() {
                this.$axios.get('/user/list')
                    .then(res => {
                        if (res.data.status === 200) {
                            // this.tableData = res.data.results
                            this.allTableData = res.data.results
                            this.filterTableData = res.data.results
                            this.setPaginations()
                        }
                        // console.log(res)
                    })
            },
            // 提交新增或编辑按钮的回调函数
            submitForm(formName) {
                const url = this.operateType == 'add' ? '/user/add' : `/user/mod/${this.form.id}`
                this.$refs[formName].validate(valid => {
                    if (valid) {
                        this.$axios.post(url, this.form)
                            .then(res => {
                                if (res.data.status === 200) {
                                    this.$message.success('操作成功!')
                                    this.centerDialogVisible = false
                                    this.resetForm()
                                    this.getUser()
                                } else {
                                    this.$message.error('操作失败请更换账号重试')
                                }
                            })
                            .catch(err => {
                                this.$message.error('操作失败请更换账号重试')
                                this.form.no = ''
                            })
                    } else {
                        this.$message.warning('请输入有效的数据')
                    }
                })
            },
    
           
        },
        created() {
            this.getUser()
        },
    }
    </script>
    

    代码已经比较复杂了表单的title是可以变换的点击新增的时候是新增用户点击编辑的时候是编辑用户。新增时里面的数据是空的编辑时填充了数据。表单的提交无论是新增还是编辑都是使用的同一个方法不过接口要区分。因此点击新增按钮对应的方法如下

     // 新增按钮的点击函数
            handleAdd() {
                this.centerDialogVisible = true
                this.operateType = 'add'
                this.isAbled = false
                this.resetForm()
            },
    
    1. this.centerDialogVisible = true显示对话框
    2. this.operateType = 'add'将操作类型修改为’add’
    3. this.isAbled = false控制账号输入可用
    4. this.resetForm()将原始表单内容清空可以在最上面的代码中看一下resetForm()是怎样清空的

其实写到这里就应该开始介绍后端接口怎么写了因为顶部输入框中的搜索按钮、重置按钮和新增对话框都需要调用接口处理数据但后面也都要用到接口数据所以还是打算把主页面写完再一起写后端

中间的数据表格

逻辑很简单页面创建时即调用后端接口获取数据把数据渲染到表格中因此要使用vue提供的生命周期钩子相关代码如下

html结构

        <el-table :data="tableData" :header-cell-style="{ background: '#f3f6fd', color: '#555' }" border>
            <el-table-column prop="id" label="ID" width="70">
            </el-table-column>
            <el-table-column prop="no" label="账号">
            </el-table-column>
            <el-table-column prop="name" label="姓名">
            </el-table-column>
            <el-table-column prop="role_id" label="角色">
                <template slot-scope="scope">
                    <el-tag
                        :type="scope.row.role_id === 0 ? 'danger' : (scope.row.role_id === 1 ? 'primary' : 'success')"
                        disable-transitions>{{
                            scope.row.role_id === 0 ? '超级管理员' : (scope.row.role_id === 1 ? '管理员'
                                : '用户')
                        }}</el-tag>
                </template>
            </el-table-column>
            <el-table-column prop="sex" label="性别">
                <template slot-scope="scope">
                    <el-tag :type="scope.row.sex === 1 ? 'primary' : 'success'" disable-transitions>{{
                        scope.row.sex ===
                            1 ? '男' : '女'
                    }}</el-tag>
                </template>
            </el-table-column>
            <el-table-column prop="age" label="年龄">
            </el-table-column>
            <el-table-column prop="phone" label="电话">
            </el-table-column>
            <el-table-column prop="operate" label="操作">
                <template slot-scope="scope">
                    <el-button type="primary" icon="el-icon-edit-outline" title="编辑" size="small"
                        @click="handleEdit(scope.$index, scope.row)" circle>
                    </el-button>
                    <el-button type="danger" icon="el-icon-delete-solid" size="small" title="删除"
                        @click="handleDelete(scope.$index, scope.row)" circle></el-button>
                </template>
            </el-table-column>
        </el-table>

JavaScript

<script>
export default {
    data() {
        return {
            tableData: [],
            allTableData: [],  // 存放所有表格数据以及搜索后的数据
            filterTableData: [],  // 存放所有表格数据供搜索
            
        }
    },
    methods: {
        getUser() {
            this.$axios.get('/user/list')
                .then(res => {
                    if (res.data.status === 200) {
                        // this.tableData = res.data.results
                        this.allTableData = res.data.results
                        this.filterTableData = res.data.results
                        this.setPaginations()
                    }
                    // console.log(res)
                })
        },
        handleEdit(index, row) {
            // console.log(row)
            this.operateType = 'mod'
            this.centerDialogVisible = true
            this.isAbled = true
            this.title = '编辑用户'
            this.form = row
            this.form.sex = row.sex.toString()
            this.form.password = ''

        },
        handleDelete(index, row) {
            this.$confirm('此操作将永久删除该用户是否继续', "提示", {
                confirmButtonText: '确定',
                cancelButtonText: '取消',
                type: 'warning'
            }).then(() => {
                this.$axios.post(`/user/delete/${row.id}`)
                    .then(res => {
                        this.$message({
                            type: 'success',
                            message: '删除成功'
                        })
                        this.getUser()
                    })
            })
        },

        // 提交新增或编辑按钮的回调函数
        submitForm(formName) {
            const url = this.operateType == 'add' ? '/user/add' : `/user/mod/${this.form.id}`
            this.$refs[formName].validate(valid => {
                if (valid) {
                    this.$axios.post(url, this.form)
                        .then(res => {
                            if (res.data.status === 200) {
                                this.$message.success('操作成功!')
                                this.centerDialogVisible = false
                                this.resetForm()
                                this.getUser()
                            } else {
                                this.$message.error('操作失败请更换账号重试')
                            }
                        })
                        .catch(err => {
                            this.$message.error('操作失败请更换账号重试')
                            this.form.no = ''
                        })
                } else {
                    this.$message.warning('请输入有效的数据')
                }
            })
        },

        // 重置表单中的内容
        resetForm() {
            this.form = {
                no: '',
                name: '',
                password: '',
                age: '',
                phone: '',
                sex: '1'
            }
            // this.$refs.form.resetFields()
        }

    },
    created() {
        this.getUser()
    },
}
</script>

代码太长我只选择了相关的代码其实没有多大的难度主要的就是getUser()发起axios数据请求然后把数据渲染上去就行了当然还涉及分页后面再说

数据删除和编辑

就是每一行数据后面的两个按钮如下

image-20230117102832908

代码已经放在上面一小节中了

编辑

先看编辑点击那个蓝色的圆形按钮跳出编辑对话框如下

image-20230117102939213

对比一下上面的新增用户可以看出区别来首先顶部的title变成了编辑用户其次有数据填充了另外第一个输入框——账户是禁用的来看一下这个handleEdit(index, row)方法

        handleEdit(index, row) {
            // console.log(row)
            this.operateType = 'mod'
            this.centerDialogVisible = true
            this.isAbled = true
            this.title = '编辑用户'
            this.form = row
            this.form.sex = row.sex.toString()
            this.form.password = ''

        },

这个方法传递了两个参数第一个index是表格的索引第二个row是这一行的数据怎么传过来的呢看一下表格结构

            <el-table-column prop="phone" label="电话">
            </el-table-column>
            <el-table-column prop="operate" label="操作">
                <template slot-scope="scope">
                    <el-button type="primary" icon="el-icon-edit-outline" title="编辑" size="small"
                        @click="handleEdit(scope.$index, scope.row)" circle>
                    </el-button>
                    <el-button type="danger" icon="el-icon-delete-solid" size="small" title="删除"
                        @click="handleDelete(scope.$index, scope.row)" circle></el-button>
                </template>
            </el-table-column>

我这里特意多放了一个电话的el-table-column标签作为对比elementUI通过<template slot-scope="scope">这一标签为操作方法提供实参如果不需要传递参数简单写一下如电话那个table即可

删除

删除比编辑要简单逻辑是点击红色的圆形按钮后弹出确认框如下

image-20230117103620657

当继续点击确认后将通过post请求删除该条数据并调用getUser()方法刷新数据点击取消就关闭对话框

分页

分页的写法固定好多页面我都是这么写的可以回过头去看一下上面的代码

但是要注意我是前端做的分页很多大佬喜欢在后端分页。差异在于后端分页需要把分页的参数传给后端前端分页的话后端只需要写一个接口返回所有数据后匹配一下分页参数取回数据即可。所以争论又来了前端分页好还是后端分页好如果是全栈那无所谓如果是后端coder当然觉得前端分页好反之亦反无解

主页面–后端数据接口

终于要接着写后端了良心话node后端比前端写起来前端要纯粹都是JavaScript程序而且我还比较习惯es5语法。但也有头疼的地方就是各种异步查询异常复杂稍微复杂一点的数据处理接口写起来经常会返回不了数据基本原因都是因为异步查询导致的常规的解决方案是使用promise或者async/await遇到问题了再说吧

先上一下我的完整的后端接口实现数据的增删改查

router_handler/user.js文件

const db = require("../db/index");
const svgCaptcha = require("svg-captcha");
const config = require("../config");
const jwt = require("jsonwebtoken");

exports.list = (req, res) => {
  const sql = "select * from user";
  db.query(sql, (err, results) => {
    if (err) return res.status(400).json(err);
    res.json({
      status: 200,
      results,
    });
  });
};

exports.add = (req, res) => {
  const user = req.body;
  const searchSql = "select * from user where no = ?";
  db.query(searchSql, user.no, (err, results) => {
    if (results.length !== 0) {
      return res.status(400).json("账号已存在请更换账号");
    } else {
      const sql = "insert into user set ?";
      db.query(sql, user, (err, results) => {
        if (err) return res.status(400).json(err);
        res.json({
          status: 200,
          result: user,
        });
      });
    }
  });
};

exports.mod = (req, res) => {
  const user = req.body;
  const sql = "update user set ? where id = ?";
  db.query(sql, [user, req.params.id], (err, result) => {
    if (err) return res.status(400).json(err);
    if (result.affectedRows !== 1) return res.status(400).json("添加失败");
    res.json({
      status: 200,
      result: user,
    });
  });
};

exports.delete = (req, res) => {
  const sql = "delete from user where id = ?";
  db.query(sql, req.params.id, (err, result) => {
    if (err) return res.status(400).json(err);
    if (result.affectedRows !== 1) return res.status(400).json("删除失败");
    res.status(200).json("删除成功");
  });
};

exports.searchByName = (req, res) => {
  const name = req.body.name;
  if (!name) return res.status(400).json("请求体中无name");
  const seachParam = "%" + name + "%";
  const sql = "select * from user where name like ?";
  db.query(sql, seachParam, (err, results) => {
    if (err) return res.status(400).json(err);
    res.json({
      status: 200,
      results,
    });
  });
};

// 登录相关
// 验证码
exports.checkCode = (req, res) => {
  const img = svgCaptcha.create({
    size: 4,
    ignoreChars: "0o1l",
    color: true,
    noise: 2,
    background: "#FFF0F5",
    height: 38,
    width: 113,
  });
  res.send(img);
};

// 登录
exports.login = (req, res) => {
  const user = req.body;
  const sql = "select * from user where no = ?";
  db.query(sql, user.no, (err, results) => {
    if (err) return res.status(400).json(err);
    if (results.length !== 1) return res.status(400).json("用户不存在");
    if (results[0].password != user.password)
      return res.status(400).json("用户名或密码错误请重新输入");

    const user_info = {
      id: results[0].id,
      no: results[0].no,
      name: results[0].name,
    };
    const tokenStr = jwt.sign(user_info, config.jwtSecretKey, {
      expiresIn: "10h",
    });
    res.json({
      status: 200,
      message: "登录成功",
      token: "Bearer " + tokenStr,
    });
  });
};

看代码就7个处理函数有两个前面已经提到就是登录相关的验证码和登录查询登录这里写的不够好按理说密码不应该使用明文的但我懒得去搞了就用明文吧

接下来看一下后端的结构地址文件

router/user.js文件

const express = require('express');
const router = express.Router();

const userHandler = require('../router_handler/user.js')
router.get('/list', userHandler.list)
router.post('/add', userHandler.add)
router.post('/mod/:id', userHandler.mod)
router.post('/delete/:id', userHandler.delete)
router.post('/searchByName', userHandler.searchByName)

router.get('/checkCode', userHandler.checkCode)
router.post('/login', userHandler.login)

module.exports = router

分别对应上面7个路由处理函数

这里不想再过多的解释后端接口是怎么写的了总之要注意一点的就是可能会遇到异步查询的问题有几个思路

  • 写一个异步的db.query方法
  • db.query并行查询

我觉得第一个好

导航一页面

本来想重新写一下页面功能的但想了一下其实和主页面重复了很多后面就没写了

逻辑是点击侧边导航一item主界面main部分将跳转到导航一

简单写了一个vue组件代码如下

<template>
  <span>admin</span>
</template>

<script>
export default {

}
</script>

<style>

</style>

显示如下

image-20230117110237915

导航二页面

与导航一页面一样不再放了

写在后面

终于完整写完了虽然很多地方还不是很清楚但至少以后写项目时可参考的内容就多了一些

当然代码我也上传到gitee了

前端代码地址https://gitee.com/hgandzl/vue-wms-app

后端代码地址https://gitee.com/hgandzl/vue-wms-api

项目演示视频https://live.csdn.net/v/270576?spm=1001.2014.3001.5501

仓库管理系统

B站视频要清晰一点去B站看吧

保持学习保持进步完结~~

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: mysqlvue