依赖包: 

 路由

router/index.js:

import Vue from "vue";
import VueRouter from "vue-router";
import { Message } from "element-ui";
import jwt_decode from "jwt-decode";

Vue.use(VueRouter);
const WHITE_LIST = ["/login"];

const routes = [
  {
    path: "/",
    name: "home",
    component: () => import("@/views/home.vue"),
    redirect: "/userStatistics",
    children: [
      {
        path: "/official_website_userlist",
        name: "official_website_userlist",
        component: () => import("@/views/official_website_userlist"),
      },
      {
        path: "/userStatistics",
        name: "userStatistics",
        component: () => import("@/views/userStatistics"),
      },
      {
        path: "/orderStatistics",
        name: "orderStatistics",
        component: () => import("@/views/orderStatistics"),
      },
    ],
  },
  {
    path: "/login",
    name: "login",
    component: () => import("@/views/login"),
  },
  {
    path: "*",
    component: () => import("@/views/exception/NotFound"),
  },
];

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

// 路由守卫
router.beforeEach((to, from, next) => {
  let token = localStorage.getItem("token");

  if (WHITE_LIST.includes(to.path)) {
    next();
  } else if (token) {
    if (jwt_decode(token).exp < parseInt(new Date().getTime() / 1000)) {
      // 已过期
      Message.warning("登录状态已过期, 请重新登录");

      localStorage.clear();

      next({
        path: "/login",
        query: to.path === "/" ? {} : { from: to.path },
      });
    } else {
      next();
    }
  } else {
    next({
      path: "/login",
      query: to.path === "/" ? {} : { from: to.path },
    });
  }
});

export default router;

vuex

store/index.js:

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

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    // 当前登录用户信息
    user_info: {
      username: "",
    },
  },
  mutations: {
    // 设置登录用户的信息
    setUserInfo(state, user_info) {
      state.user_info = {
        username: user_info.username,
      };
    },
  },
  actions: {},
  modules: {},
});

axios 

api/axios.js: 

/**
 * axios实例
 */
import axios from "axios";
import { Message } from "element-ui";
import router from "@/router";

const service = axios.create({
  baseURL: `${config_settings.address_node}`,
});
service.defaults.timeout = 1000 * 60;
service.defaults.headers.post["Content-Type"] =
  "application/x-www-form-urlencoded;charset=utf-8";

/* 添加请求拦截器 */
service.interceptors.request.use(
  (config) => {
    // 添加token到header
    const token = localStorage.getItem("token");
    if (token && config.headers) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

/* 添加响应拦截器 */
service.interceptors.response.use(
  (res) => {
    /**
     * 以下几种情况都会报401:
     * 1:无token
     * 2:token被篡改
     * 3:token过期
     */
    if (res.data.code === 401) {
      Message.warning("身份验证过期, 请重新登录");

      router.push({
        path: "/login",
        query: { from: window.location.pathname },
      });

      localStorage.clear();
    }
    return res;
  },
  (error) => {
    return Promise.resolve(error);
  }
);

export default service;

 main.js

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";

Vue.config.productionTip = false;

import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
Vue.use(ElementUI);

//全局注册axios
import http from "@/api/axios.js";
Vue.prototype.$axios = http;

import dayjs from "dayjs";
Vue.prototype.$dayjs = dayjs;

import Cookies from "js-cookie";
Vue.prototype.$cookie = Cookies;

import "xe-utils";
import VXETable from "vxe-table";
import "vxe-table/lib/style.css";
Vue.use(VXETable);
Vue.prototype.$VXETable = VXETable;

import "echarts";
import ECharts from "vue-echarts";
Vue.component("VueEcharts", ECharts);

Vue.filter("formatDate", function(value) {
  return dayjs(value).format("YYYY-MM-DD HH:mm:ss");
});

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

登录页

<template>
  <div class="login">
    <el-form
      ref="form"
      size="large"
      :model="form"
      :rules="rules"
      class="login-form"
      @keyup.enter.native="submit"
    >
      <h4
        style="
          color: black;
          font-weight: 400;
          text-align: center;
          margin: 0 0 25px 0;
        "
      >
        用户登录
      </h4>
      <el-form-item prop="username">
        <el-input
          clearable
          v-model="form.username"
          prefix-icon="el-icon-user"
          placeholder="用户名"
        />
      </el-form-item>
      <el-form-item prop="password">
        <el-input
          show-password
          v-model="form.password"
          prefix-icon="el-icon-lock"
          placeholder="密码"
        />
      </el-form-item>
      <div class="el-form-item">
        <el-checkbox v-model="form.remember"> 记住密码 </el-checkbox>
      </div>
      <div class="el-form-item">
        <el-button
          size="large"
          type="primary"
          :loading="loading"
          @click="submit"
          style="display: block; width: 100%"
        >
          {{ loading ? "登录中..." : "登录" }}
        </el-button>
      </div>
    </el-form>
  </div>
</template>

<script>
export default {
  name: "login",
  data() {
    return {
      loading: false,
      form: {
        username: "",
        password: "",
        remember: true,
      },
    };
  },
  mounted() {
    this.form.username = this.$cookie.get("username");
    this.form.password = this.$cookie.get("password");
  },
  computed: {
    // 表单验证规则
    rules() {
      return {
        username: [
          {
            required: true,
            message: "用户名不能为空",
            type: "string",
            trigger: "blur",
          },
        ],
        password: [
          {
            required: true,
            message: "密码不能为空",
            type: "string",
            trigger: "blur",
          },
        ],
      };
    },
    // 当前语言
  },
  created() {},
  methods: {
    /* 提交 */
    submit() {
      this.$refs.form.validate(async (valid) => {
        if (!valid) {
          return false;
        }
        this.loading = true;
        if (this.form.remember) {
          this.$cookie.set("username", this.form.username, {
            expires: 30,
          });
          this.$cookie.set("password", this.form.password, {
            expires: 30,
          });
        } else {
          this.$cookie.remove("username");
          this.$cookie.remove("password");
        }
        try {
          this.goHome();

          //登录
          let { data } = await this.$axios.post(`/login`, {
            username: this.form.username,
            // password: md5(this.form.password).toLowerCase(),
            password: this.form.password,
          });
          if (data.code == 0) {
            localStorage.setItem("token", data.token);

            this.$message({
              showClose: true,
              message: "登录成功!",
              type: "success",
            });
            this.goHome();
          } else {
            this.$message({
              showClose: true,
              message: data.message,
              type: "error",
            });
          }

          this.loading = false;
        } catch (error) {
          this.loading = false;
        }
      });
    },
    /* 跳转到首页 */
    goHome() {
      this.$router.push(this.$route?.query?.from ?? "/").catch(() => {});
    },
  },
};
</script>

<style lang="scss" scoped>
.login {
  box-sizing: border-box;
  // background-image: url(~@/assets/bg_login.jpg);
  background-image: url(https://file.rivermap.cn/lj/RivermapPanDownload/bg-login.jpg);
  background-repeat: no-repeat;
  background-size: cover;
  min-height: 100vh;

  .login-form {
    width: 360px;
    max-width: 100%;
    padding: 25px 30px;
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
    box-sizing: border-box;
    border-radius: 4px;
    z-index: 2;
    background-color: white;
  }
}
</style>

 布局

home.vue: 

<template>
  <el-container class="homeContainer">
    <el-header class="headerContainer">
      <div
        class="hidden-xs-only"
        style="
          display: flex;
          align-items: center;
          margin-left: 20px;
          cursor: pointer;
        "
      >
        <img
          src="http://file.rivermap.cn/lj/RivermapPanDownload/logo.svg"
          style="width: 30px; height: 30px; color: white"
        />
        <div style="margin: 0 10px; font-weight: bold; letter-spacing: 2px">
          xxx平台
        </div>
        <li
          style="margin-left: 5px;padding:0 20px"
          @click="isShowAside = !isShowAside"
        >
          <i v-if="isShowAside" class="el-icon-s-unfold"></i>
          <i v-else class="el-icon-s-fold"></i>
        </li>
      </div>
      <div
        style="
          margin-left: auto;
          padding: 0 10px;
          display: flex;
          align-items: center;
        "
      >
        <li style="margin-right: 5px" @click="screenfull()">
          <i class="vxe-icon-zoom-out  fullscreen"></i>
        </li>

        <el-dropdown style="margin-right: 15px" size="small">
          <div class="el-dropdown-link" style="color: white; cursor: pointer">
            <li>
              <span>{{ this.$store.state.user_info.username }}</span
              ><i class="el-icon-arrow-down el-icon--right"></i>
            </li>
          </div>
          <el-dropdown-menu slot="dropdown">
            <el-dropdown-item @click.native="loginOut()"
              ><i class="el-icon-switch-button"></i
              ><span>退出登录</span></el-dropdown-item
            >
          </el-dropdown-menu>
        </el-dropdown>
      </div>
    </el-header>
    <el-container class="bottomContainer">
      <el-aside
        class="asideContainer"
        style="width: 240px;"
        v-show="isShowAside"
      >
        <el-menu
          :default-active="$route.path"
          class="el-menu-vertical-demo"
          text-color="white"
          active-text-color="white"
        >
          <el-menu-item
            index="/official_website_userlist"
            @click="$router.push({ path: '/official_website_userlist' })"
          >
            <i
              class="el-icon-tickets"
              style="font-size: 32px; margin-right: 10px; color: white"
            ></i>
            <span slot="title">用户列表</span>
          </el-menu-item>
          <el-menu-item
            index="/userStatistics"
            @click="$router.push({ path: '/userStatistics' })"
          >
            <i
              class="el-icon-user"
              style="font-size: 32px; margin-right: 10px; color: white"
            ></i>
            <span slot="title">用户统计</span> </el-menu-item
          ><el-menu-item
            index="/orderStatistics"
            @click="$router.push({ path: '/orderStatistics' })"
          >
            <i
              class="el-icon-date"
              style="font-size: 32px; margin-right: 10px; color: white"
            ></i>
            <span slot="title">订单统计</span>
          </el-menu-item>
        </el-menu>
      </el-aside>
      <el-main class="mainContainer">
        <keep-alive :include="cacheRoute">
          <router-view></router-view>
        </keep-alive>
      </el-main>
    </el-container>
  </el-container>
</template>

<script>
import screenfull from "screenfull";

export default {
  components: {},
  data() {
    return {
      isShowAside: true,
      cacheRoute: [
        "userStatistics",
        "official_website_userlist",
        "orderStatistics",
      ],
    };
  },
  props: {},
  created() {},
  mounted() {
    // 获取并存储用户信息
    this.getUserInfo();
  },
  computed: {},
  methods: {
    async getUserInfo() {
      let data = await this.$axios.get("/getUserInfo");
      this.$store.commit("setUserInfo", data.data.data);
    },
    //  全屏事件
    screenfull() {
      screenfull.toggle();
    },
    //退出登录
    loginOut() {
      this.$confirm("确定要退出登录吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      }).then(async () => {
        localStorage.clear();
        this.$router.push({ path: "/login" });
      });
    },
  },
};
</script>

<style lang="scss" scoped>
.homeContainer {
  li {
    list-style-type: none;
    padding: 0 10px;
    cursor: pointer;
    &:hover {
      background-color: rgba(180, 180, 180, 0.26);
    }
  }
  .headerContainer {
    background-color: black;
    height: 60px;
    line-height: 60px;
    display: flex;
  }
  .bottomContainer {
    background-color: rgb(255, 255, 255);
    .asideContainer {
      background-color: #246cbb;
      height: calc(100vh - 60px);
      overflow-x: hidden;
      overflow-y: auto;
      //展开时宽度
      .el-menu-vertical-demo:not(.el-menu--collapse) {
        width: 240px;
      }
      .el-menu {
        border-right: solid 0px #e6e6e6;
        height: 100%;
        // background: url(~@/assets/bg_aside.png);
        background: #246cbb;

        background-repeat: no-repeat;
        background-size: cover;

        .el-menu-item {
          &:hover {
            background-color: rgba(5, 5, 5, 0.137) !important;
          }
        }
        .el-menu-item.is-active {
          border-bottom: 0 !important;
          background-color: #6295ce !important ;
        }
      }
    }
    .mainContainer {
      padding: 10px;
      height: calc(100vh - 60px);
      overflow-y: auto;
    }
  }
}
</style>

 404页面

 exception/NotFound.vue:

<template>
  <div class="wscn-http404-container">
    <div class="wscn-http404">
      <div class="pic-404">
        <img
          class="pic-404__parent"
          src="https://lj-common.oss-cn-chengdu.aliyuncs.com/404.png"
          alt="404"
        />
        <img
          class="pic-404__child left"
          src="https://lj-common.oss-cn-chengdu.aliyuncs.com/404_cloud.png"
          alt="404"
        />
        <img
          class="pic-404__child mid"
          src="https://lj-common.oss-cn-chengdu.aliyuncs.com/404_cloud.png"
          alt="404"
        />
        <img
          class="pic-404__child right"
          src="https://lj-common.oss-cn-chengdu.aliyuncs.com/404_cloud.png"
          alt="404"
        />
      </div>
      <div class="bullshit">
        <div class="bullshit__oops">OOPS!</div>
        <div class="bullshit__headline">{{ message }}</div>
        <div class="bullshit__info">
          Please check that the URL you entered is correct, or click the button
          below to return to the homepage.
        </div>
        <a class="bullshit__return-home" @click="$router.push({ path: '/' })"
          >Back to home</a
        >
      </div>
    </div>
  </div>
</template>
 
<script>
export default {
  name: "NotFound",
  computed: {
    message() {
      return "The webmaster said that you can not enter this page...";
    },
  },
};
</script>
 
<style lang="scss" scoped>
.wscn-http404-container {
  transform: translate(-50%, -50%);
  position: absolute;
  top: 40%;
  left: 50%;
}
.wscn-http404 {
  position: relative;
  width: 1200px;
  padding: 0 50px;
  overflow: hidden;
  .pic-404 {
    position: relative;
    float: left;
    width: 600px;
    overflow: hidden;
    &__parent {
      width: 100%;
    }
    &__child {
      position: absolute;
      &.left {
        width: 80px;
        top: 17px;
        left: 220px;
        opacity: 0;
        animation-name: cloudLeft;
        animation-duration: 2s;
        animation-timing-function: linear;
        animation-fill-mode: forwards;
        animation-delay: 1s;
      }
      &.mid {
        width: 46px;
        top: 10px;
        left: 420px;
        opacity: 0;
        animation-name: cloudMid;
        animation-duration: 2s;
        animation-timing-function: linear;
        animation-fill-mode: forwards;
        animation-delay: 1.2s;
      }
      &.right {
        width: 62px;
        top: 100px;
        left: 500px;
        opacity: 0;
        animation-name: cloudRight;
        animation-duration: 2s;
        animation-timing-function: linear;
        animation-fill-mode: forwards;
        animation-delay: 1s;
      }
      @keyframes cloudLeft {
        0% {
          top: 17px;
          left: 220px;
          opacity: 0;
        }
        20% {
          top: 33px;
          left: 188px;
          opacity: 1;
        }
        80% {
          top: 81px;
          left: 92px;
          opacity: 1;
        }
        100% {
          top: 97px;
          left: 60px;
          opacity: 0;
        }
      }
      @keyframes cloudMid {
        0% {
          top: 10px;
          left: 420px;
          opacity: 0;
        }
        20% {
          top: 40px;
          left: 360px;
          opacity: 1;
        }
        70% {
          top: 130px;
          left: 180px;
          opacity: 1;
        }
        100% {
          top: 160px;
          left: 120px;
          opacity: 0;
        }
      }
      @keyframes cloudRight {
        0% {
          top: 100px;
          left: 500px;
          opacity: 0;
        }
        20% {
          top: 120px;
          left: 460px;
          opacity: 1;
        }
        80% {
          top: 180px;
          left: 340px;
          opacity: 1;
        }
        100% {
          top: 200px;
          left: 300px;
          opacity: 0;
        }
      }
    }
  }
  .bullshit {
    position: relative;
    float: left;
    width: 300px;
    padding: 30px 0;
    overflow: hidden;
    &__oops {
      font-size: 32px;
      font-weight: bold;
      line-height: 40px;
      color: #1482f0;
      opacity: 0;
      margin-bottom: 20px;
      animation-name: slideUp;
      animation-duration: 0.5s;
      animation-fill-mode: forwards;
    }
    &__headline {
      font-size: 20px;
      line-height: 24px;
      color: #222;
      font-weight: bold;
      opacity: 0;
      margin-bottom: 10px;
      animation-name: slideUp;
      animation-duration: 0.5s;
      animation-delay: 0.1s;
      animation-fill-mode: forwards;
    }
    &__info {
      font-size: 13px;
      line-height: 21px;
      color: grey;
      opacity: 0;
      margin-bottom: 30px;
      animation-name: slideUp;
      animation-duration: 0.5s;
      animation-delay: 0.2s;
      animation-fill-mode: forwards;
    }
    &__return-home {
      display: block;
      float: left;
      width: 110px;
      height: 36px;
      background: #1482f0;
      border-radius: 100px;
      text-align: center;
      color: #ffffff;
      opacity: 0;
      font-size: 14px;
      line-height: 36px;
      cursor: pointer;
      animation-name: slideUp;
      animation-duration: 0.5s;
      animation-delay: 0.3s;
      animation-fill-mode: forwards;
    }
    @keyframes slideUp {
      0% {
        transform: translateY(60px);
        opacity: 0;
      }
      100% {
        transform: translateY(0);
        opacity: 1;
      }
    }
  }
}
</style>

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐