Vue3+Vite+Pinia+Naive后台管理系统搭建之七:utils 工具构建

前言

如果对 vue3 的语法不熟悉的,可以移步Vue3.0 基础入门,快速入门。

github 开源库:Vue3-Vite-Pinia-Naive-Js

gitee   开源库:Vue3-Vite-Pinia-Naive-Js

1. 新增 cookie 存储 token 需缓存数据

1.1 安装依赖
yarn add js-cookie
// or
npm install js-cookie

 1.2 新增 src/utils/cookie.js
import Cookies from "js-cookie";

const TokenKey = "User-Token";
const ExpiresInKey = "User-Expires-In";

export function getToken() {
  return Cookies.get(TokenKey);
}

export function setToken(token) {
  return Cookies.set(TokenKey, token);
}

export function removeToken() {
  return Cookies.remove(TokenKey);
}

2. 新增 src/utils/common.js 共用方法

/**
 * 参数处理
 * @param {*} params  参数
 */
export function tansParams(params) {
  let result = "";
  for (const propName of Object.keys(params)) {
    const value = params[propName];
    var part = encodeURIComponent(propName) + "=";
    if (value !== null && value !== "" && typeof value !== "undefined") {
      if (typeof value === "object") {
        for (const key of Object.keys(value)) {
          if (
            value[key] !== null &&
            value[key] !== "" &&
            typeof value[key] !== "undefined"
          ) {
            let params = propName + "[" + key + "]";
            var subPart = encodeURIComponent(params) + "=";
            result += subPart + encodeURIComponent(value[key]) + "&";
          }
        }
      } else {
        result += part + encodeURIComponent(value) + "&";
      }
    }
  }
  return result;
}

// 日期格式化
export function parseTime(time, pattern) {
  if (arguments.length === 0 || !time) {
    return null;
  }
  const format = pattern || "{y}-{m}-{d} {h}:{i}:{s}";
  let date;
  if (typeof time === "object") {
    date = time;
  } else {
    if (typeof time === "string" && /^[0-9]+$/.test(time)) {
      time = parseInt(time);
    } else if (typeof time === "string") {
      time = time
        .replace(new RegExp(/-/gm), "/")
        .replace("T", " ")
        .replace(new RegExp(/\.[\d]{3}/gm), "");
    }
    if (typeof time === "number" && time.toString().length === 10) {
      time = time * 1000;
    }
    date = new Date(time);
  }
  const formatObj = {
    y: date.getFullYear(),
    m: date.getMonth() + 1,
    d: date.getDate(),
    h: date.getHours(),
    i: date.getMinutes(),
    s: date.getSeconds(),
    a: date.getDay(),
  };
  const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
    let value = formatObj[key];
    if (key === "a") {
      return ["日", "一", "二", "三", "四", "五", "六"][value];
    }
    if (result.length > 0 && value < 10) {
      value = "0" + value;
    }
    return value || 0;
  });
  return time_str;
}

// 添加日期范围
export function addDateRange(params, dateRange, propName) {
  let search = params;
  search.params =
    typeof search.params === "object" &&
    search.params !== null &&
    !Array.isArray(search.params)
      ? search.params
      : {};
  dateRange = Array.isArray(dateRange) ? dateRange : [];
  if (typeof propName === "undefined") {
    search.params["beginTime"] = dateRange[0];
    search.params["endTime"] = dateRange[1];
  } else {
    search.params["begin" + propName] = dateRange[0];
    search.params["end" + propName] = dateRange[1];
  }
  return search;
}

/**
 * 构造树型结构数据
 * @param {*} data 数据源
 * @param {*} id id字段 默认 'id'
 * @param {*} parentId 父节点字段 默认 'parentId'
 * @param {*} children 孩子节点字段 默认 'children'
 */
export function handleTree(data, id, parentId, children) {
  let config = {
    id: id || "id",
    parentId: parentId || "parentId",
    childrenList: children || "children",
  };

  var childrenListMap = {};
  var nodeIds = {};
  var tree = [];

  for (let d of data) {
    let parentId = d[config.parentId];
    if (childrenListMap[parentId] == null) {
      childrenListMap[parentId] = [];
    }
    nodeIds[d[config.id]] = d;
    childrenListMap[parentId].push(d);
  }

  for (let d of data) {
    let parentId = d[config.parentId];
    if (nodeIds[parentId] == null) {
      tree.push(d);
    }
  }

  function adaptToChildrenList(o) {
    if (childrenListMap[o[config.id]] !== null) {
      o[config.childrenList] = childrenListMap[o[config.id]];
    }
    if (o[config.childrenList]) {
      for (let c of o[config.childrenList]) {
        adaptToChildrenList(c);
      }
    }
  }

  for (let t of tree) {
    adaptToChildrenList(t);
  }

  return tree;
}

3. 新增 src/utils/errorCode.js 错误码转换

export default {
  '401': '认证失败,无法访问系统资源',
  '403': '当前操作没有权限',
  '404': '访问资源不存在',
  'default': '系统未知错误,请反馈给管理员'
}

4. 新增 jsencrypt 数据加密

4.1 安装依赖
yarn add jsencrypt
// or
npm install jsencrypt

 4.2  新增 src/utils/jsencrypt.js
import JSEncrypt from "jsencrypt/bin/jsencrypt.min";

const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' +
  'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='

const privateKey = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' +
  '7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' +
  'PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n' +
  'kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n' +
  'cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n' +
  'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' +
  'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' +
  'UP8iWi1Qw0Y='

  // 加密
export function encrypt(txt) {
  const encryptor = new JSEncrypt()
  encryptor.setPublicKey(publicKey) // 设置公钥
  return encryptor.encrypt(txt) // 对数据进行加密
}

// 解密
export function decrypt(txt) {
  const encryptor = new JSEncrypt()
  encryptor.setPrivateKey(privateKey) // 设置私钥
  return encryptor.decrypt(txt) // 对数据进行解密
}

5. 新增 src/utils/validate.js 数据验证

/**
 * 判断是否外部网址
 * @param {string} path
 * @returns {Boolean}
 */
export function isExternal(path) {
  return /^(https?:|mailto:|tel:)/.test(path);
}

/**
 * 有效用户名
 * @param {string} str
 * @returns {Boolean}
 */
export function validUsername(str) {
  const valid_map = ["admin", "editor"];
  return valid_map.indexOf(str.trim()) >= 0;
}

// 判断是否电话
export function isPhone(str) {
  return /^1[0-9]{10}$/.test(str);
}

/**
 * 是否email
 * @param {string} email
 * @returns {Boolean}
 */
export function isEmail(email) {
  const reg =
    /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
  return reg.test(email);
}

/**
 * 判断是否网址
 * @param {string} url
 * @returns {Boolean}
 */
export function isURL(url) {
  const reg =
    /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/;
  return reg.test(url);
}

/**
 * 有效小写字母
 * @param {string} str
 * @returns {Boolean}
 */
export function validLowerCase(str) {
  const reg = /^[a-z]+$/;
  return reg.test(str);
}

/**
 * 有效大写字母
 * @param {string} str
 * @returns {Boolean}
 */
export function validUpperCase(str) {
  const reg = /^[A-Z]+$/;
  return reg.test(str);
}

/**
 * 是否字母
 * @param {string} str
 * @returns {Boolean}
 */
export function isAlphabets(str) {
  const reg = /^[A-Za-z]+$/;
  return reg.test(str);
}

/**
 * 是否字符串
 * @param {string} str
 * @returns {Boolean}
 */
export function isString(str) {
  if (typeof str === "string" || str instanceof String) {
    return true;
  }
  return false;
}

/**
 * 是否数组
 * @param {Array} arg
 * @returns {Boolean}
 */
export function isArray(arg) {
  if (typeof Array.isArray === "undefined") {
    return Object.prototype.toString.call(arg) === "[object Array]";
  }
  return Array.isArray(arg);
}

 6. 编辑 src/api/http.js 完善接口逻辑

import axios from "axios";
import errorCode from "@/utils/errorCode.js";
import { getToken } from "@/utils/cookie.js";
import { tansParams } from "@/utils/common.js";
import { useUserStore } from "@/store/user.js";

// 请求和响应的消息主体用什么方式编码
axios.defaults.headers["Content-Type"] = "application/json;charset=utf-8";
const service = axios.create({
  baseURL: import.meta.env.VITE_APP_BASE_API,
  timeout: 10000,
});

// 请求拦截器
service.interceptors.request.use((config) => {
  // 是否需要 token
  const isToken = (config.headers || {}).isToken === true;
  if (getToken() && !isToken) {
    // 让请求携带token
    config.headers["Authorization"] = "Bearer " + getToken();
  }
  // get请求映射params参数
  if (config.method === "get" && config.params) {
    let url = config.url + "?" + tansParams(config.params);
    url = url.slice(0, -1);
    config.params = {};
    config.url = url;
  }
  return config;
});

// 响应拦截器
service.interceptors.response.use((res) => {
  let userStore = useUserStore();
  // 未设置返回码默认200
  let code = res.data.code || 200;
  // 获取错误信息
  let msg = errorCode[code] || res.data.msg || errorCode["default"];
  // 二进制数据直接返回
  if (
    res.request.responseType === "blob" ||
    res.request.responseType === "arraybuffer"
  ) {
    return res.data;
  }
  if (code === 401) {
    window.$msg.info("登录状态已过期,请重新登录");
    userStore.logout().then(() => {
      window.location.href = "/home";
    });
  }
  if (code !== 200) {
    window.$msg.error(msg);
  }
  return res.data;
});

export default service;

 7. 编辑 src/api/login.js 完善登录接口

import api from "./http.js";

// 登录
export function login(data) {
  return api({
    url: `/auth/login`,
    headers: {
      isToken: false,
    },
    method: "post",
    data,
  });
}

// 退出
export function logout() {
  return api({
    url: `/auth/logout`,
    method: "delete",
  });
}

// 获取用户详细信息
export function getInfo() {
  return api({
    url: `/system/user/getInfo`,
    method: "get",
  });
}

// 获取验证码
export function getCodeImg() {
  return api({
    url: `/code`,
    headers: {
      isToken: false,
    },
    method: "get",
  });
}

8. 编辑 src/store/user.js 完善状态共享逻辑

import { defineStore } from "pinia";
import { login, logout, getInfo } from "@/api/login.js";
import { getToken, setToken, removeToken } from "@/utils/cookie.js";

export const useUserStore = defineStore({
  id: "userStore",
  state: () => {
    return {
      token: getToken() || "",
      user: {},
      roles: [],
    };
  },
  getters: {
    ageAfter(state) {
      return state.user.age + 10;
    },
  },
  actions: {
    // 登录接口
    async login(data) {
      return new Promise((resolve, reject) => {
        // 前端测试登录,下面代码为静态登录,调试登录请自己删除。
        if (data.username == "admin" && data.password == "admin123") {
          let token = "123456";
          setToken(token);
          this.token = token;
          resolve(true);
        } else {
          reject(false);
        }

        // 调用后端接口,联调接口后自己取消注释
        // login(data)
        //   .then((res) => {
        //     if (res.code !== 200) {
        //       reject(res);
        //     }

        //     const { data } = res;
        //     // 保存token
        //     setToken(data.access_token);
        //     this.token = data.access_token;
        //     resolve(data);
        //   })
        //   .catch((err) => {
        //     reject(err);
        //   });
      });
    },
    // 退出接口
    async logout() {
      return new Promise((resolve, reject) => {
        // 前端测试,下面代码为静态登出,调试接口时请自己删除。
        removeToken();
        this.token = "";
        this.roles = [];
        resolve();

        // 调用后端接口,联调接口后自己取消注释
        // logout()
        //   .then((res) => {
        //     if (res.code !== 200) {
        //       reject(res);
        //     }
        //     this.roles = [];
        //     this.token = "";
        //     removeToken();
        //     resolve();
        //   })
        //   .catch((err) => {
        //     reject(err);
        //   });
      });
    },
    // 获取用户信息
    async getInfo() {
      return new Promise((resolve, reject) => {
        // 前端测试,下面代码为静态获取用户信息,调试登录请自己删除。
        this.roles = ["admin"];
        resolve();

        // 调用后端接口,联调接口后自己取消注释
        // getInfo()
        //   .then((data) => {
        //     if (data.code !== 200) {
        //       reject(data);
        //     }
            
        //     this.roles = data.roles;
        //     this.user = data;
        //     resolve(data);
        //   })
        //   .catch((err) => {
        //     reject(err);
        //   });
      });
    },
  },
});

9. 编辑 src/router/index.js 完善路由跳转逻辑

9.1 安装依赖
// 路由跳转顶部进度条
yarn add nprogress
// or
npm install nprogress

 9.2 编辑 src/router/index.js 完善路由监听器
import { createRouter, createWebHistory } from "vue-router";
import baseRouters from "./baseRouter.js";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
import { getToken } from "@/utils/cookie.js";
const whiteList = ["/", "/login"];

const routes = [...baseRouters];
const _createRouter = () =>
  createRouter({
    history: createWebHistory(),
    routes,
    scrollBehavior() {
      return {
        el: "#app",
        top: 0,
        behavior: "smooth",
      };
    },
  });

export function resetRouter() {
  const newRouter = _createRouter();
  router.matcher = newRouter.matcher;
}

const router = _createRouter();

// 路由监听
router.beforeEach((to, from, next) => {
  NProgress.start();
  if (!!getToken()) {
    // 有token,跳转登录时重定向首页
    if (to.path === "/login") {
      next("");
      NProgress.done();
    }
    next();
    NProgress.done();
  } else {
    // 判断路由是否在白名单,是直接跳转
    if (whiteList.indexOf(to.path) !== -1) {
      next();
      // 未登录页面跳转,直接跳转到登录页
    } else {
      next(`/login?redirect=${to.fullPath}`);
    }
    NProgress.done();
  }
});

export default router;

10. 编辑 src/pages/login.vue 完善登录页,登录逻辑

<script setup>
import { reactive } from "vue";
import router from "@/router/index.js";
import { NButton, NInput } from "naive-ui";
import { useUserStore } from "@/store/user.js";

// 用户状态管理
let userStore = useUserStore();

let loginForm = reactive({
  username: "admin",
  password: "admin123",
});
let handleLogin = () => {
  userStore.login(loginForm).then(() => {
    router.push({ name: "home" });
  });
};
</script>

<template>
  <div class="login">
    <n-input v-model:value="loginForm.username"></n-input>
    <n-input v-model:value="loginForm.password"></n-input>
    <n-button type="primary" size="small" @click="handleLogin">登录</n-button>
  </div>
</template>

<style lang="scss" scoped></style>

综上

utils 工具构建完成。接口请求逻辑,页面跳转逻辑,状态共享逻辑处理完成。

下一章:Vue3+Vite+Pinia+Naive后台管理系统搭建之八:构建 login.vue 登录页

猜你喜欢

转载自blog.csdn.net/weixin_64684095/article/details/131685112
今日推荐