20230707----重返学习-物美管理系统-登录流程-页面路由跳转-面包屑导航-访问历史列表

day-107-one-hundred-and-seven-20230707-物美管理系统-登录流程-页面路由跳转-面包屑导航-访问历史列表

物美管理系统

登录流程

  1. 客户端:
    1. 表单校验。
      • 避免无效请求。
      • 防止SQL注入。
    2. 密码进行加密-MD。
      • 非对称性加密。
    3. 获取表单信息,向服务器发送。
      • 请求-post。
  2. 服务器端:
    1. 获取请求主体传递的信息-建议二次校验。
    2. 根据传递的验证码及uuid,到数据库去查询,校验验证码的准确性。
      • 不准确:反馈给客户端错误。
      • 准确:下一步。
    3. 验证帐号和密码的准确度-去数据库查询。
      • 不准确:反馈给客户端错误。
      • 准确:下一步。
    4. 找到登录者信息的相关信息,基于JWT(json web token)算法,根据登录者信息、密钥、时间,算出一个Token值!
      • 返回给客户端成功的消息-携带Token。
  3. 客户端:
    1. 接收服务器返回的结果。
      • 失败:直接做提示,重新获取验证码-重置验证码。
      • 成功:下一步。
    2. 如果登录成功,我们也可以获取Token信息。
      • 把Token存储到本地-localStorage。
      • 向服务器发送请求,把登录者信息和权限信息获取到,存储到全局状态管理vuex中!
        • 为了做前端的登录态校验与权限校验。
      • 如果有记住密码的功能,则需要把帐号密码存储到本地。
      • 提示
      • 跳转-细节。
数据库
  • 数据库
    • node.js --> mongodb、MySQL、SQLServer、Oracle…
Token
  • Token是我们后期发请求与登录态校验的有效凭证。
    • 一般除登录/获取验证码两个接口外,其余所有的接口请求,都要求把token基于请求头传递给服务器-因为服务器需要知道你是谁、你是否登录。
    • 在后端接收到每一次请求的时候:
      1. 首先获取Token,如果没有,则直接反馈给客户端错误。
      2. 基于JWT算法进行反解析。
        • 先校验时间,看Token是否失效,如果失效,则证明登录过期了。
        • 如果没有失败,则获取登录者信息,根据登录者信息,再返回相应的数据!
登录态校验
  • 登录态校验:
  1. 当在浏览器前端,当访问除登录页或404页面以外的其它页面。
    1. 第一件事:校验用户是否登录:
      • 登录了,则进入指定的页面;
      • 没登录,跳转到登录页(提示)。
  • 登录态校验核心:
    1. 核心1:从服务器获取登录者信息,并且把信息存储起来。
      • 一般不存在localStorage中。
        • 防止服务器端登录者信息更新了,但是本地存储的信息还在有效期,这样不能获取最新的登录者信息。
        • 防止有人在本地恶意仿造登录者信息。
      • 我们一般会使用vuex来存储:
        • 只要页面刷新,或者页面关闭重新打开,vuex中存储的登录者信息已经没有了,此时我们需要重新向服务器发送请求获取!保证实时性、和安全性。
      • 一般做这件事的场景:
        • 登录成功后。
        • 每一次路由跳转(含页面第一次加载/刷新)
          1. 先看vuex中是否有登录者信息。
            • 有:说明此用户是登录的,直接想去哪就去哪即可!
            • 没有:需要从服务器获取。
          2. 如果获取到了:存储到vuex,说明也是登录的,想去哪就去哪即可!如果发送请求都没拿到:用户压根没有登录,此时提示、跳转到登录页。
          • 这件事一般就是在router.beforeEach()路由全局前置守卫里。
登录态校验信息注意事项
  1. 不能单纯的以本地是否存储了Token,来判断是否登录:Token可能是非法的、也可能早已经过期了。
    • 服务器一般都会提供一个接口:获取登录者信息和权限信息-这个接口请求,需要客户端传递Token。
具体代码
  • fang/f20230705/ManageSystem/src/views/user/Login.vue
<script>
import ut from "@/assets/utils";

const place = "********"; //占位符:不能和用户自己输入的一致、需要能通过Form表单校验。
export default {
  data() {
    // 校验密码的格式。
    const validatePassword = (_, value, callback) => {
      // 和特殊占位符一致,直接通过。
      if (value === place) {
        callback();
      }
      if (value.length === 0) {
        return callback(new Error("密码是必填项哦"));
      }
      /* //项目中:
      let reg = /^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&*? ])\S*$/;
      if (!reg.test(value)) {
        return callback(new Error("密码格式有误"));
      } */
      callback();
    };
    return {
      //控制选项卡。
      activeName: "account",
      // 验证码相关状态
      captcha: {
        img: "",
        uuid: "",
        loading: false,
      },
      //表单相关状态。
      ruleForm: {
        username: "",
        password: "",
        code: "",
        remember: true,
      },
      rules: {
        username: [
          { required: true, message: "帐号是必填项", trigger: "blur" },
        ],
        password: [
          { required: true, message: "密码是必填项", trigger: "blur" },
        ],
        code: [
          // { required: true, message: "验证码是必填项哦~~", trigger: "blur" },
          { validtor: validatePassword, trigger: "blur" },
        ],
      },
    };
  },
  methods: {
    //获取验证码
    async queryCaptcha() {
      this.captcha.loading = true;
      try {
        let { code, img, uuid } = await this.$API.queryCaptchaImage();
        if (+code !== 200) {
          this.$message.error(`网络出现异常,获取验证码失败`);
        } else {
          this.captcha.img = `data:image/jpeg;base64,${img}`; //因为服务器返回的图片默认没前缀。
          this.captcha.uuid = uuid;
        }
      } catch (error) {
        console.log(`error:-->`, error);
      }
      this.captcha.loading = false;
    },
    // 登录校验。
    async submit() {
      try {
        //1. 先进行表单校验。
        await this.$refs.formIns.validate();
        // this.$message.success('哈哈')

        //2. 获取表单中的数据,向服务器发送请求。
        let { username, password, code, remember } = this.ruleForm;
        if (password === place) {
          //说明用户没有改过密码。
          password = this.remberOldPass;
        }
        let {
          code: resultCode,
          token,
          msg,
        } = await this.$API.checkUserLogin({
          username,
          password,
          code,
          uuid: this.captcha.uuid,
        });//前提:API中设置了对应的接口。
        if (+resultCode !== 200) {
          //登录失败。
          this.$message.error(msg);
          // 重新获取验证码。
          this.queryCaptcha();
          this.ruleForm.code = "";
          return;
        }
        //登录成功。
        ut.storage.set("TK", token); //把token存储到本地上。
        await this.$store.dispatch("setProfileAsync"); //获取登录者信息。前提:API中设置了对应的接口。同时有token-请求拦截器中放置会把当前接口带上token。在vuex中设置了关于用户信息的异步请求接口。

        // 登录成功后,如果有记住密码,就存储帐号密码到本地。
        if (remember) {
          ut.storage.set("REMBER", {
            username,
            password, //真实开发中一定要MD5加密。
          });
        } else {
          ut.storage.remove("REMBER");
        }

        this.$message.success("恭喜你,登录成功了!");
        this.$router.push("/");
      } catch (error) {
        console.log(`error:-->`, error);
      }
    },
  },
  created() {
    //第一次渲染组件:立即获取验证码
    this.queryCaptcha();

    // 第一次渲染组件:验证是否有记住帐号密码,如果有记住,则赋值给对应的框。
    const remberInfo = ut.storage.get("REMBER");
    if (remberInfo) {
      this.ruleForm.username = remberInfo.username;
      this.ruleForm.password = place;
      this.remberOldPass = remberInfo.password;
    }
  },
};
</script>

<template>
  <div class="main">
    <el-form
      :model="ruleForm"
      :rules="rules"
      ref="formIns"
      class="user-layout-login"
    >
      <el-form-item prop="username">
        <el-input
          v-model.trim="ruleForm.username"
          placeholder="请输入账号"
          prefix-icon="el-icon-user"
        ></el-input>
      </el-form-item>
      <el-form-item prop="password">
        <el-input
          v-model.trim="ruleForm.password"
          placeholder="请输入密码"
          prefix-icon="el-icon-key"
          show-password
        ></el-input>
      </el-form-item>
      <el-row :gutter="16">
        <el-col :span="16">
          <el-form-item prop="code">
            <el-input
              v-model.trim="ruleForm.code"
              placeholder="请输入验证码"
              prefix-icon="el-icon-mobile"
            ></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="8">
          <div
            v-loading="captcha.loading"
            class="captcha"
            element-loading-spinner="el-icon-loading"
            @click="queryCaptcha"
          >
            <img :src="captcha.img" alt="" />
          </div>
        </el-col>
      </el-row>
      <el-form-item prop="remember">
        <el-checkbox v-model="ruleForm.remember">记住密码</el-checkbox>
      </el-form-item>
      <!-- <el-tabs v-model="activeName">
        <el-tab-pane label="账号密码登录" name="account">
          <el-form-item>
            <el-input
              placeholder="请输入账号"
              prefix-icon="el-icon-user"
            ></el-input>
          </el-form-item>
          <el-form-item>
            <el-input
              placeholder="请输入密码"
              prefix-icon="el-icon-key"
              show-password
            ></el-input>
          </el-form-item>
          <el-row :gutter="16">
            <el-col :span="16">
              <el-form-item>
                <el-input
                  placeholder="请输入验证码"
                  prefix-icon="el-icon-mobile"
                ></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <div
                class="captcha"
                v-loading="true"
                element-loading-spinner="el-icon-loading"
              >
                <img src="" alt="" />
              </div>
            </el-col>
          </el-row>
          <el-form-item>
            <el-checkbox>记住密码</el-checkbox>
          </el-form-item>
        </el-tab-pane>

        <el-tab-pane label="手机号登录" name="phone" disabled>
          <el-form-item>
            <el-input
              placeholder="请输入手机号"
              prefix-icon="el-icon-phone"
            ></el-input>
          </el-form-item>
          <el-row :gutter="16">
            <el-col :span="16">
              <el-form-item>
                <el-input
                  placeholder="请输入验证码"
                  prefix-icon="el-icon-mobile"
                ></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <button-again class="getCaptcha">发送验证码</button-again>
            </el-col>
          </el-row>
        </el-tab-pane>
      </el-tabs> -->

      <button-again type="primary" class="login-button" @click="submit">
        立即登录
      </button-again>
    </el-form>
  </div>
</template>

<style lang="less" scoped>
.main {
  min-width: 260px;
  width: 368px;
  margin: 0 auto;

  .el-form-item {
    margin-bottom: 18px;
  }

  .login-button {
    font-size: 16px;
    width: 100%;
  }

  .getCaptcha {
    display: block;
    width: 100%;
    height: 40px;
  }

  .captcha {
    position: relative;
    height: 40px;
    background: #ddd;
    cursor: pointer;

    img {
      display: block;
      width: 100%;
      height: 100%;

      &[src=""] {
        display: none;
      }
    }
  }

  :deep(.el-loading-mask) {
    background: transparent;

    .el-icon-loading {
      font-size: 26px;
    }

    .el-loading-spinner {
      margin-top: -13px;
    }
  }
}
</style>
  • fang/f20230705/ManageSystem/src/api/index.js
import http from "./http";

//获取验证码
const queryCaptchaImage = () => http.get("/captchaImage");

/* //扒到的接口信息:
  /login
  POST
  {"username":"fastbee","password":"123456","code":"7","uuid":"31615b2fb707485db349886991b526c5"}
  ---
  {
      "msg": "操作成功",
      "code": 200,
      "token": "eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6Ijk5M2NmYTAyLTM2NTUtNGQwMy1iZDZiLTI2N2Q3MjRiZmNhMiJ9.sKVIlXZByTs58KNOUXbsseULwQl0rLE_Jl4M8sFVJPguasPJTpOcJfIpp7ITnMjAtRbQCMWoaoGnVjyjBo1MAQ"
  }
*/
// 用户登录校验:
// body => {"username":"fastbee","password":"123456","code":"7","uuid":"31615b2fb707485db349886991b526c5"}
const checkUserLogin = (body) => {
  return http.post("/login", body);
};


/* //扒到的接口:
/getInfo
GET
无参数「但是要求,在请求头中,基于 Authorization 把存储的token,传递给服务器才可以(以后所有请求都这样)」===> 请求拦截器中处理
=====
.....
*/
// 获取登录者信息-含权限信息。
// 还在得请求拦截器中做处理,把存的Token放到该请求的请求头中。
const queryUserProfile = ()=>http.get('/getInfo')


/* 暴露API */
const API = {
  queryCaptchaImage,
  checkUserLogin,
  queryUserProfile,
};
export default API;
  • fang/f20230705/ManageSystem/src/api/http.js
import axios from "axios";
import { Message } from "element-ui";
import _ from "@/assets/utils";

const http = axios.create({
  baseURL: "/api",
  timeout: 60000,
});

//对于除登录或获取验证码的接口外,其余所有接口请求,都需要基于请求头把Token传递给服务器。
const exclude = ["/captchaImage", "/login"];
http.interceptors.request.use((config) => {
  const token = _.storage.get("TK"); //存储的名称越不语义化越好!
  if (token && !exclude.includes(config.url)) {
    config.headers["Authorization"] = token;
  }

  return config;
});

http.interceptors.response.use(
  (response) => {
    return response.data;
  },
  (reason) => {
    Message.error("网络繁忙,稍后再试~");
    return Promise.reject(reason);
  }
);
export default http;
  • fang/f20230705/ManageSystem/src/store/index.js
import Vue from "vue";
import API from "@/api";

import Vuex, { createLogger } from "vuex";
import VuexPersistence from "vuex-persist";
Vue.use(Vuex);

/* 配置插件 */
const vuexLocal = new VuexPersistence({
  key: "vuex",
  storage: window.localStorage,
  /* reducer(state) {
    //指定部分vuex状态持久化存储。
    return {};
  }, */
});
const env = process.env.NODE_ENV;
const plugins = [vuexLocal.plugin];
if (env === "development") {
  plugins.push(createLogger());
}

/* 创建store容器 */
const store = new Vuex.Store({
  strict: true,
  plugins,
  state: {
    profile: null, //为null只能说明当前没存用户信息,而不能判断用户是否已经登录。
  },
  mutations: {
    setProfile(state, profile) {
      state.profile = profile;
    },
  },
  actions: {
    async setProfileAsync({ commit }) {
      let profile = null;
      try {
        let { code, permissions, roles, user } = await API.queryUserProfile();
        if (+code === 200) {
          profile = {
            permissions,
            roles,
            user,
          };
          commit("serProfile", profile);
        }
      } catch (error) {
        console.log(`error:-->`, error);
      }
      return profile;
    },
  },
  modules: {},
});
export default store;
记住密码的占位符
  • 核心代码:
<script>
//import ut from "@/assets/utils";
// 具备有效期的LocalStorage存储
const storage = {
  set(key, value) {
    localStorage.setItem(
      key,
      JSON.stringify({
        time: +new Date(),
        value,
      })
    );
  },
  get(key, cycle = 2592000000) {
    cycle = +cycle;
    if (isNaN(cycle)) cycle = 2592000000;
    let data = localStorage.getItem(key);
    if (!data) return null;
    let { time, value } = JSON.parse(data);
    if (+new Date() - time > cycle) {
      storage.remove(key);
      return null;
    }
    return value;
  },
  remove(key) {
    localStorage.removeItem(key);
  },
};
const ut = {storage}
const place = "********"; //占位符:不能和用户自己输入的一致、需要能通过Form表单校验。
export default {
  data() {
    // 校验密码的格式。
    const validatePassword = (_, value, callback) => {
      // 和特殊占位符一致,直接通过。
      if (value === place) {
        callback();
      }
      //....其它校验
      callback();
    };
    return {
      //表单相关状态。
      ruleForm: {
        username: "",
        password: "",
        remember: true,
      },
      rules: {
        code: [
          // { required: true, message: "验证码是必填项哦~~", trigger: "blur" },
          { validtor: validatePassword, trigger: "blur" },
        ],
      },
    };
  },
  methods: {
    // 登录校验。
    async submit() {
      try {

        //2. 获取表单中的数据,向服务器发送请求。
        let { username, password, code, remember } = this.ruleForm;
        if (password === place) {
          //说明用户没有改过密码。
          password = this.remberOldPass;
        }

        // 登录成功后,如果有记住密码,就存储帐号密码到本地。
        if (remember) {
          ut.storage.set("REMBER", {
            username,
            password, //真实开发中一定要MD5加密。
          });
        } else {
          ut.storage.remove("REMBER");
        }
    },
  },
  created() {

    // 第一次渲染组件:验证是否有记住帐号密码,如果有记住,则赋值给对应的框。
    const remberInfo = ut.storage.get("REMBER");
    if (remberInfo) {
      this.ruleForm.username = remberInfo.username;
      this.ruleForm.password = place;
      this.remberOldPass = remberInfo.password;
    }
  },
};
</script>
获取用户token
  • fang/f20230705/ManageSystem/src/views/user/Login.vue
<script>
export default {
  data() {
    // 校验密码的格式。
    const validatePassword = (_, value, callback) => {
      callback();
    };
    return {
      //控制选项卡。
      activeName: "account",
      // 验证码相关状态
      captcha: {
        img: "",
        uuid: "",
        loading: false,
      },
      //表单相关状态。
      ruleForm: {
        username: "",
        password: "",
        code: "",
        remember: true,
      },
      rules: {
        username: [
          { required: true, message: "帐号是必填项", trigger: "blur" },
        ],
        password: [
          { required: true, message: "密码是必填项", trigger: "blur" },
        ],
        code: [
          // { required: true, message: "验证码是必填项哦~~", trigger: "blur" },
          { validtor: validatePassword, trigger: "blur" },
        ],
      },
    };
  },
  methods: {
    //获取验证码
    async queryCaptcha() {
      this.captcha.loading = true;
      try {
        let { code, img, uuid } = await this.$API.queryCaptchaImage();
        if (+code !== 200) {
          this.$message.error(`网络出现异常,获取验证码失败`);
        } else {
          this.captcha.img = `data:image/jpeg;base64,${img}`; //因为服务器返回的图片默认没前缀。
          this.captcha.uuid = uuid;
        }
      } catch (error) {
        console.log(`error:-->`, error);
      }
      this.captcha.loading = false;
    },
    // 登录校验。
    async submit() {
      try {
        //1. 先进行表单校验。
        await this.$refs.formIns.validate();

        //2. 获取表单中的数据,向服务器发送请求。
        let { username, password, code, remember } = this.ruleForm;
        if (password === place) {
          //说明用户没有改过密码。
          password = this.remberOldPass;
        }
        let {
          code: resultCode,
          token,
          msg,
        } = await this.$API.checkUserLogin({
          username,
          password,
          code,
          uuid: this.captcha.uuid,
        }); //前提:API中设置了对应的接口。
        if (+resultCode !== 200) {
          //登录失败。
          this.$message.error(msg);
          // 重新获取验证码。
          this.queryCaptcha();
          this.ruleForm.code = "";
          return;
        }
        //登录成功。
        ut.storage.set("TK", token); //把token存储到本地上。

        this.$message.success("恭喜你,登录成功了!");

        // 跳转后的细节:
        this.$router.push("/");
      } catch (error) {
        console.log(`error:-->`, error);
      }
    },
  },
  created() {
    //第一次渲染组件:立即获取验证码
    this.queryCaptcha();
  },
};
</script>

<template>
  <div class="main">
    <el-form
      :model="ruleForm"
      :rules="rules"
      ref="formIns"
      class="user-layout-login"
    >
      <el-form-item prop="username">
        <el-input
          v-model.trim="ruleForm.username"
          placeholder="请输入账号"
          prefix-icon="el-icon-user"
        ></el-input>
      </el-form-item>
      <el-form-item prop="password">
        <el-input
          v-model.trim="ruleForm.password"
          placeholder="请输入密码"
          prefix-icon="el-icon-key"
          show-password
        ></el-input>
      </el-form-item>
      <el-row :gutter="16">
        <el-col :span="16">
          <el-form-item prop="code">
            <el-input
              v-model.trim="ruleForm.code"
              placeholder="请输入验证码"
              prefix-icon="el-icon-mobile"
            ></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="8">
          <div
            v-loading="captcha.loading"
            class="captcha"
            element-loading-spinner="el-icon-loading"
            @click="queryCaptcha"
          >
            <img :src="captcha.img" alt="" />
          </div>
        </el-col>
      </el-row>
      <el-form-item prop="remember">
        <el-checkbox v-model="ruleForm.remember">记住密码</el-checkbox>
      </el-form-item>

      <button-again type="primary" class="login-button" @click="submit">
        立即登录
      </button-again>
    </el-form>
  </div>
</template>

  • fang/f20230705/ManageSystem/src/api/index.js
import http from "./http";

//获取验证码
const queryCaptchaImage = () => http.get("/captchaImage");

/* //扒到的接口信息:
  /login
  POST
  {"username":"fastbee","password":"123456","code":"7","uuid":"31615b2fb707485db349886991b526c5"}
  ---
  {
      "msg": "操作成功",
      "code": 200,
      "token": "eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6Ijk5M2NmYTAyLTM2NTUtNGQwMy1iZDZiLTI2N2Q3MjRiZmNhMiJ9.sKVIlXZByTs58KNOUXbsseULwQl0rLE_Jl4M8sFVJPguasPJTpOcJfIpp7ITnMjAtRbQCMWoaoGnVjyjBo1MAQ"
  }
*/
// 用户登录校验:
// body => {"username":"fastbee","password":"123456","code":"7","uuid":"31615b2fb707485db349886991b526c5"}
const checkUserLogin = (body) => {
  return http.post("/login", body);
};


/* 暴露API */
const API = {
  queryCaptchaImage,
  checkUserLogin,
};
export default API;

全局守卫做登录态校验

异步获取登录者信息
页面路由跳转间的Loading
修改页面的标题

回退功能

  1. 可以进入到登录页的情况:
    1. 手动输入/user/login进来的。
      • 登录成功:首页 - push();
    2. 我想进入/iot/template,但是因为没有登录,跳转到登录页。
      • 登录成功:目的地/iot/template - replace。
      • 需要:
        1. 跳转到登录页的时候,需要告知登录页目的地的地址-问号传参。
          • /user/login?target=/iot/template
    3. 点击退出登录,进入登录页。可以和2保持一致,跳转回之前的。也可以和1保持一致,跳转到首页。
回退功能代码
有了回退后的全部代码
  • fang/f20230705/ManageSystem/src/views/user/Login.vue
<script>
import ut from "@/assets/utils";
// 具备有效期的LocalStorage存储
const storage = {
  set(key, value) {
    localStorage.setItem(
      key,
      JSON.stringify({
        time: +new Date(),
        value,
      })
    );
  },
  get(key, cycle = 2592000000) {
    cycle = +cycle;
    if (isNaN(cycle)) cycle = 2592000000;
    let data = localStorage.getItem(key);
    if (!data) return null;
    let { time, value } = JSON.parse(data);
    if (+new Date() - time > cycle) {
      storage.remove(key);
      return null;
    }
    return value;
  },
  remove(key) {
    localStorage.removeItem(key);
  },
};

const place = "********"; //占位符:不能和用户自己输入的一致、需要能通过Form表单校验。
export default {
  data() {
    // 校验密码的格式。
    const validatePassword = (_, value, callback) => {
      // 和特殊占位符一致,直接通过。
      if (value === place) {
        callback();
      }
      if (value.length === 0) {
        return callback(new Error("密码是必填项哦"));
      }
      /* //项目中:
      let reg = /^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&*? ])\S*$/;
      if (!reg.test(value)) {
        return callback(new Error("密码格式有误"));
      } */
      callback();
    };
    return {
      //控制选项卡。
      activeName: "account",
      // 验证码相关状态
      captcha: {
        img: "",
        uuid: "",
        loading: false,
      },
      //表单相关状态。
      ruleForm: {
        username: "",
        password: "",
        code: "",
        remember: true,
      },
      rules: {
        username: [
          { required: true, message: "帐号是必填项", trigger: "blur" },
        ],
        password: [
          { required: true, message: "密码是必填项", trigger: "blur" },
        ],
        code: [
          // { required: true, message: "验证码是必填项哦~~", trigger: "blur" },
          { validtor: validatePassword, trigger: "blur" },
        ],
      },
    };
  },
  methods: {
    //获取验证码
    async queryCaptcha() {
      this.captcha.loading = true;
      try {
        let { code, img, uuid } = await this.$API.queryCaptchaImage();
        if (+code !== 200) {
          this.$message.error(`网络出现异常,获取验证码失败`);
        } else {
          this.captcha.img = `data:image/jpeg;base64,${img}`; //因为服务器返回的图片默认没前缀。
          this.captcha.uuid = uuid;
        }
      } catch (error) {
        console.log(`error:-->`, error);
      }
      this.captcha.loading = false;
    },
    // 登录校验。
    async submit() {
      try {
        //1. 先进行表单校验。
        await this.$refs.formIns.validate();
        // this.$message.success('哈哈')

        //2. 获取表单中的数据,向服务器发送请求。
        let { username, password, code, remember } = this.ruleForm;
        if (password === place) {
          //说明用户没有改过密码。
          password = this.remberOldPass;
        }
        let {
          code: resultCode,
          token,
          msg,
        } = await this.$API.checkUserLogin({
          username,
          password,
          code,
          uuid: this.captcha.uuid,
        }); //前提:API中设置了对应的接口。
        if (+resultCode !== 200) {
          //登录失败。
          this.$message.error(msg);
          // 重新获取验证码。
          this.queryCaptcha();
          this.ruleForm.code = "";
          return;
        }
        //登录成功。
        ut.storage.set("TK", token); //把token存储到本地上。
        await this.$store.dispatch("setProfileAsync"); //获取登录者信息。前提:API中设置了对应的接口。同时有token-请求拦截器中放置会把当前接口带上token。在vuex中设置了关于用户信息的异步请求接口。

        // 登录成功后,如果有记住密码,就存储帐号密码到本地。
        if (remember) {
          ut.storage.set("REMBER", {
            username,
            password, //真实开发中一定要MD5加密。
          });
        } else {
          ut.storage.remove("REMBER");
        }

        this.$message.success("恭喜你,登录成功了!");

        // 跳转后的细节:
        let target = this.$route.query.target;
        target ? this.$router.replace(target) : this.$router.push("/");
      } catch (error) {
        console.log(`error:-->`, error);
      }
    },
  },
  created() {
    //第一次渲染组件:立即获取验证码
    this.queryCaptcha();

    // 第一次渲染组件:验证是否有记住帐号密码,如果有记住,则赋值给对应的框。
    const remberInfo = ut.storage.get("REMBER");
    if (remberInfo) {
      this.ruleForm.username = remberInfo.username;
      this.ruleForm.password = place;
      this.remberOldPass = remberInfo.password;
    }
  },
};
</script>

<template>
  <div class="main">
    <el-form
      :model="ruleForm"
      :rules="rules"
      ref="formIns"
      class="user-layout-login"
    >
      <el-form-item prop="username">
        <el-input
          v-model.trim="ruleForm.username"
          placeholder="请输入账号"
          prefix-icon="el-icon-user"
        ></el-input>
      </el-form-item>
      <el-form-item prop="password">
        <el-input
          v-model.trim="ruleForm.password"
          placeholder="请输入密码"
          prefix-icon="el-icon-key"
          show-password
        ></el-input>
      </el-form-item>
      <el-row :gutter="16">
        <el-col :span="16">
          <el-form-item prop="code">
            <el-input
              v-model.trim="ruleForm.code"
              placeholder="请输入验证码"
              prefix-icon="el-icon-mobile"
            ></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="8">
          <div
            v-loading="captcha.loading"
            class="captcha"
            element-loading-spinner="el-icon-loading"
            @click="queryCaptcha"
          >
            <img :src="captcha.img" alt="" />
          </div>
        </el-col>
      </el-row>
      <el-form-item prop="remember">
        <el-checkbox v-model="ruleForm.remember">记住密码</el-checkbox>
      </el-form-item>
      <!-- <el-tabs v-model="activeName">
        <el-tab-pane label="账号密码登录" name="account">
          <el-form-item>
            <el-input
              placeholder="请输入账号"
              prefix-icon="el-icon-user"
            ></el-input>
          </el-form-item>
          <el-form-item>
            <el-input
              placeholder="请输入密码"
              prefix-icon="el-icon-key"
              show-password
            ></el-input>
          </el-form-item>
          <el-row :gutter="16">
            <el-col :span="16">
              <el-form-item>
                <el-input
                  placeholder="请输入验证码"
                  prefix-icon="el-icon-mobile"
                ></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <div
                class="captcha"
                v-loading="true"
                element-loading-spinner="el-icon-loading"
              >
                <img src="" alt="" />
              </div>
            </el-col>
          </el-row>
          <el-form-item>
            <el-checkbox>记住密码</el-checkbox>
          </el-form-item>
        </el-tab-pane>

        <el-tab-pane label="手机号登录" name="phone" disabled>
          <el-form-item>
            <el-input
              placeholder="请输入手机号"
              prefix-icon="el-icon-phone"
            ></el-input>
          </el-form-item>
          <el-row :gutter="16">
            <el-col :span="16">
              <el-form-item>
                <el-input
                  placeholder="请输入验证码"
                  prefix-icon="el-icon-mobile"
                ></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <button-again class="getCaptcha">发送验证码</button-again>
            </el-col>
          </el-row>
        </el-tab-pane>
      </el-tabs> -->

      <button-again type="primary" class="login-button" @click="submit">
        立即登录
      </button-again>
    </el-form>
  </div>
</template>

<style lang="less" scoped>
.main {
  min-width: 260px;
  width: 368px;
  margin: 0 auto;

  .el-form-item {
    margin-bottom: 18px;
  }

  .login-button {
    font-size: 16px;
    width: 100%;
  }

  .getCaptcha {
    display: block;
    width: 100%;
    height: 40px;
  }

  .captcha {
    position: relative;
    height: 40px;
    background: #ddd;
    cursor: pointer;

    img {
      display: block;
      width: 100%;
      height: 100%;

      &[src=""] {
        display: none;
      }
    }
  }

  :deep(.el-loading-mask) {
    background: transparent;

    .el-icon-loading {
      font-size: 26px;
    }

    .el-loading-spinner {
      margin-top: -13px;
    }
  }
}
</style>
  • fang/f20230705/ManageSystem/src/router/routes.js
import BasicLayout from "@/layout/BasicLayout.vue";
import UserLayout from "@/layout/UserLayout.vue";
import childRoutes from "./childRoutes";

// 基础路由
const routes = [
  {
    path: "/user",
    name: "user",
    meta: {
      title: "",
      level: 1,
    },
    component: UserLayout,
    redirect: "/user/login",
    children: [
      {
        path: "login",
        name: "user_login",
        meta: {
          title: "用户登录",
          level: 2,
        },
        component: () =>
          import(/* webpackChunkName: "user" */ "@/views/user/Login.vue"),
      },
    ],
  },
  {
    path: "/",
    name: "home",
    meta: {
      title: "",
      level: 1,
    },
    component: BasicLayout,
    redirect: "/index/welcome",
    children: childRoutes,
  },
  {
    path: "*",
    name: "error",
    meta: {
      title: "404页面",
      level: 1,
    },
    component: () =>
      import(/* webpackChunkName: "error" */ "@/views/ErrorPage.vue"),
  },
];
export default routes;
  • fang/f20230705/ManageSystem/src/router/index.js
import Vue from "vue";
import store from "@/store";
import { Message, Loading } from "element-ui";

import VueRouter from "vue-router";
import routes from "./routes";
Vue.use(VueRouter);

/* 创建路由管理 */
const router = new VueRouter({
  mode: "hash",
  routes,
});

// 导航守卫。
// 全局前置守卫:登录态的校验。
let ignore = ["user_login", "user", "error"]; //根据路由对象的name来忽略对页面的校验。
let loadingIns = null;
router.beforeEach(async (to, from, next) => {
  //开启Loading。
  if (!loadingIns) {
    loadingIns = Loading.service({
      text: "正在加载中.....",
      spinner: "el-icon-loading",
      background: "rgba(0, 0, 0, 0.7)",
    });
  }

  //异步获取登录者信息。
  let profile = store.state.profile;
  //跳转的路由不是ignore中的,并且vuex中没有存储登录者信息,此时我们需要异步派发任务,实现登录态校验。
  if (!ignore.includes(to.name) && !profile) {
    profile = await store.dispatch("setProfileAsync");
    if (!profile) {
      //发送异步请求后,都没有获取登录者信息,则说明当前用户就是没登录。
      Message.warning("你还没登录,请你先登录");
      next({
        path: "/user/login",
        query: {
          target: to.fullPath,//用于登录成功后,直接进入想要进去的页面。
        },
      });
      return;
    }
  }
  next();
});
// 全局后置守卫。
router.afterEach((to, from) => {
  //关闭Loading。
  if (loadingIns) {
    loadingIns.close();
    loadingIns = null;
  }

  // 修改页面的标题
  let { title } = to.meta;
  document.title = title ? `${title} - 物联网管理系统` : `物联网管理系统`;
});
export default router;
  • fang/f20230705/ManageSystem/src/store/index.js
import Vue from "vue";
import API from "@/api";

import Vuex, { createLogger } from "vuex";
import VuexPersistence from "vuex-persist";
Vue.use(Vuex);

/* 配置插件 */
const vuexLocal = new VuexPersistence({
  key: "vuex",
  storage: window.localStorage,
  /* reducer(state) {
    //指定部分vuex状态持久化存储。
    return {};
  }, */
});
const env = process.env.NODE_ENV;
const plugins = [vuexLocal.plugin];
if (env === "development") {
  plugins.push(createLogger());
}

/* 创建store容器 */
const store = new Vuex.Store({
  strict: true,
  plugins,
  state: {
    profile: null, //为null只能说明当前没存用户信息,而不能判断用户是否已经登录。
  },
  mutations: {
    setProfile(state, profile) {
      state.profile = profile;
    },
  },
  actions: {
    async setProfileAsync({ commit }) {
      let profile = null;
      try {
        let { code, permissions, roles, user } = await API.queryUserProfile();
        if (+code === 200) {
          profile = {
            permissions,
            roles,
            user,
          };
          commit("serProfile", profile);
        }
      } catch (error) {
        console.log(`error:-->`, error);
      }
      return profile;
    },
  },
  modules: {},
});
export default store;
  • fang/f20230705/ManageSystem/src/api/index.js
import http from "./http";

//获取验证码
const queryCaptchaImage = () => http.get("/captchaImage");

/* //扒到的接口信息:
  /login
  POST
  {"username":"fastbee","password":"123456","code":"7","uuid":"31615b2fb707485db349886991b526c5"}
  ---
  {
      "msg": "操作成功",
      "code": 200,
      "token": "eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6Ijk5M2NmYTAyLTM2NTUtNGQwMy1iZDZiLTI2N2Q3MjRiZmNhMiJ9.sKVIlXZByTs58KNOUXbsseULwQl0rLE_Jl4M8sFVJPguasPJTpOcJfIpp7ITnMjAtRbQCMWoaoGnVjyjBo1MAQ"
  }
*/
// 用户登录校验:
// body => {"username":"fastbee","password":"123456","code":"7","uuid":"31615b2fb707485db349886991b526c5"}
const checkUserLogin = (body) => {
  return http.post("/login", body);
};


/* //扒到的接口:
/getInfo
GET
无参数「但是要求,在请求头中,基于 Authorization 把存储的token,传递给服务器才可以(以后所有请求都这样)」===> 请求拦截器中处理
=====
.....
*/
// 获取登录者信息-含权限信息。
// 还在得请求拦截器中做处理,把存的Token放到该请求的请求头中。
const queryUserProfile = ()=>http.get('/getInfo')


/* 暴露API */
const API = {
  queryCaptchaImage,
  checkUserLogin,
  queryUserProfile,
};
export default API;
  • fang/f20230705/ManageSystem/src/api/http.js
import axios from "axios";
import { Message } from "element-ui";
import _ from "@/assets/utils";

const http = axios.create({
  baseURL: "/api",
  timeout: 60000,
});

//对于除登录或获取验证码的接口外,其余所有接口请求,都需要基于请求头把Token传递给服务器。
const exclude = ["/captchaImage", "/login"];
http.interceptors.request.use((config) => {
  const token = _.storage.get("TK"); //存储的名称越不语义化越好!
  if (token && !exclude.includes(config.url)) {
    config.headers["Authorization"] = token;
  }

  return config;
});

http.interceptors.response.use(
  (response) => {
    return response.data;
  },
  (reason) => {
    Message.error("网络繁忙,稍后再试~");
    return Promise.reject(reason);
  }
);
export default http;

路由跳转

面包屑导航
处理头像
全局混入的方法
用户下拉方法
解除首页点击回退再到首页的loading问题

首页左侧菜单

  • 根据扁平化的路由表得到菜单选项,并循环出来对应的页面。

首页上方的访问历史列表

历史列表的事件委托

历史记录关掉的操作

历史记录新增的操作

进阶参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值