vue/cli构建 vue3项目

1.导入依赖

package.json

 "dependencies": {
    "@element-plus/icons-vue": "^2.1.0",
    "axios": "^0.27.2",
    "core-js": "^3.8.3",
    "echarts": "^5.4.3",
    "element-plus": "^2.2.17",
    "js-cookie": "^3.0.1",
    "less": "^4.1.3",
    "less-loader": "^11.0.0",
    "nprogress": "^0.2.0",
    "particles.vue3": "1.22.0",
    "pinia": "^2.1.7",
    "pinia-plugin-persistedstate": "^3.2.0",
    "save": "^2.9.0",
    "screenfull": "^5.1.0",
    "style-resources-loader": "^1.5.0",
    "svg-sprite-loader": "^6.0.11",
    "dayjs": "^1.11.10",
    "normalize.css": "^8.0.1",
    "qs": "^6.11.2",
    "vue": "^3.2.37",
    "vue-axios": "^3.4.1",
    "vue-router": "^4.1.5"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~5.0.0",
    "@vue/cli-service": "~5.0.0",
    "sass": "^1.32.7",
    "sass-loader": "^12.0.0",
    "scss": "^0.2.4",
    "mockjs": "^1.1.0",
    "svg-sprite-loader": "^6.0.11",
    "unplugin-auto-import": "^0.11.2",
    "unplugin-vue-components": "^0.22.7"
  },

2.全局声明

main.js

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// 路由
import router from '@/router';
app.use(router);

//添加路由守卫
import "@/permission"

// pinia
import pinia from '@/store'
app.use(pinia)

// element-plus
import ElementPlus from 'element-plus'; //引入Element-ui
import zhCN from "element-plus/dist/locale/zh-cn.mjs" //引入中文
import 'element-plus/dist/index.css'
app.use(ElementPlus, { locale: zhCN })

//elementplus的icon图标引入
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

// 导入svgicon
import SvgIcon from "@/assets/icons/index"
SvgIcon(app)

//导入normalize.css
import "normalize.css"

//消除默认样式
import "@/assets/styles/base.scss";

// 动态背景 particles
import Particles from "particles.vue3";

//引入mock
require("@/mock") //引入mock数据,关闭则注释该行

//导入时间格式化插件
import dayjs from "dayjs"
app.use(dayjs)//可以全局使用dayjs

//防抖
const debounce = (fn, delay) => {
  let timer = null;
  return function () {
    let context = this;
    let args = arguments;
    clearTimeout(timer);
    timer = setTimeout(function () {
      fn.apply(context, args);
    }, delay);
  }
}

const _ResizeObserver = window.ResizeObserver;
window.ResizeObserver = class ResizeObserver extends _ResizeObserver {
  constructor(callback) {
    callback = debounce(callback, 16);
    super(callback);
  }
}

// 分页组件
import Pagination from '@/components/Pagination/index.vue'
// 全局组件挂载
app.component('Pagination', Pagination)

import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi'
// 全局方法挂载
app.config.globalProperties.parseTime = parseTime
app.config.globalProperties.resetForm = resetForm
app.config.globalProperties.handleTree = handleTree
app.config.globalProperties.addDateRange = addDateRange
app.config.globalProperties.selectDictLabel = selectDictLabel
app.config.globalProperties.selectDictLabels = selectDictLabels


app.mount('#app')

3.修改index.html

<!DOCTYPE html>
<html lang="">

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <link rel="icon" href="<%= BASE_URL %>favicon.ico">
  <title>
    <%= htmlWebpackPlugin.options.title %>
  </title>
</head>
<style>
  #loader-wrapper {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 999999;
  }

  #loader {
    display: block;
    position: relative;
    left: 50%;
    top: 50%;
    width: 120px;
    height: 120px;
    margin: -75px 0 0 -75px;
    border-radius: 50%;
    border: 3px solid transparent;
    /* COLOR 1 */
    border-top-color: #FFF;
    -webkit-animation: spin 2s linear infinite;
    /* Chrome, Opera 15+, Safari 5+ */
    -ms-animation: spin 2s linear infinite;
    /* Chrome, Opera 15+, Safari 5+ */
    -moz-animation: spin 2s linear infinite;
    /* Chrome, Opera 15+, Safari 5+ */
    -o-animation: spin 2s linear infinite;
    /* Chrome, Opera 15+, Safari 5+ */
    animation: spin 2s linear infinite;
    /* Chrome, Firefox 16+, IE 10+, Opera */
    z-index: 1001;
  }

  #loader:before {
    content: "";
    position: absolute;
    top: 5px;
    left: 5px;
    right: 5px;
    bottom: 5px;
    border-radius: 50%;
    border: 3px solid transparent;
    /* COLOR 2 */
    border-top-color: #FFF;
    -webkit-animation: spin 3s linear infinite;
    /* Chrome, Opera 15+, Safari 5+ */
    -moz-animation: spin 3s linear infinite;
    /* Chrome, Opera 15+, Safari 5+ */
    -o-animation: spin 3s linear infinite;
    /* Chrome, Opera 15+, Safari 5+ */
    -ms-animation: spin 3s linear infinite;
    /* Chrome, Opera 15+, Safari 5+ */
    animation: spin 3s linear infinite;
    /* Chrome, Firefox 16+, IE 10+, Opera */
  }

  #loader:after {
    content: "";
    position: absolute;
    top: 15px;
    left: 15px;
    right: 15px;
    bottom: 15px;
    border-radius: 50%;
    border: 3px solid transparent;
    border-top-color: #FFF;
    /* COLOR 3 */
    -moz-animation: spin 1.5s linear infinite;
    /* Chrome, Opera 15+, Safari 5+ */
    -o-animation: spin 1.5s linear infinite;
    /* Chrome, Opera 15+, Safari 5+ */
    -ms-animation: spin 1.5s linear infinite;
    /* Chrome, Opera 15+, Safari 5+ */
    -webkit-animation: spin 1.5s linear infinite;
    /* Chrome, Opera 15+, Safari 5+ */
    animation: spin 1.5s linear infinite;
    /* Chrome, Firefox 16+, IE 10+, Opera */
  }

  @-webkit-keyframes spin {
    0% {
      -webkit-transform: rotate(0deg);
      /* Chrome, Opera 15+, Safari 3.1+ */
      -ms-transform: rotate(0deg);
      /* IE 9 */
      transform: rotate(0deg);
      /* Firefox 16+, IE 10+, Opera */
    }

    100% {
      -webkit-transform: rotate(360deg);
      /* Chrome, Opera 15+, Safari 3.1+ */
      -ms-transform: rotate(360deg);
      /* IE 9 */
      transform: rotate(360deg);
      /* Firefox 16+, IE 10+, Opera */
    }
  }

  @keyframes spin {
    0% {
      -webkit-transform: rotate(0deg);
      /* Chrome, Opera 15+, Safari 3.1+ */
      -ms-transform: rotate(0deg);
      /* IE 9 */
      transform: rotate(0deg);
      /* Firefox 16+, IE 10+, Opera */
    }

    100% {
      -webkit-transform: rotate(360deg);
      /* Chrome, Opera 15+, Safari 3.1+ */
      -ms-transform: rotate(360deg);
      /* IE 9 */
      transform: rotate(360deg);
      /* Firefox 16+, IE 10+, Opera */
    }
  }

  #loader-wrapper .loader-section {
    position: fixed;
    top: 0;
    width: 51%;
    height: 100%;
    background: #1890FF;
    /* Old browsers */
    z-index: 1000;
    -webkit-transform: translateX(0);
    /* Chrome, Opera 15+, Safari 3.1+ */
    -ms-transform: translateX(0);
    /* IE 9 */
    transform: translateX(0);
    /* Firefox 16+, IE 10+, Opera */
  }

  #loader-wrapper .loader-section.section-left {
    left: 0;
  }

  #loader-wrapper .loader-section.section-right {
    right: 0;
  }

  /* Loaded */
  .loaded #loader-wrapper .loader-section.section-left {
    -webkit-transform: translateX(-100%);
    /* Chrome, Opera 15+, Safari 3.1+ */
    -ms-transform: translateX(-100%);
    /* IE 9 */
    transform: translateX(-100%);
    /* Firefox 16+, IE 10+, Opera */
    -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
    transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
  }

  .loaded #loader-wrapper .loader-section.section-right {
    -webkit-transform: translateX(100%);
    /* Chrome, Opera 15+, Safari 3.1+ */
    -ms-transform: translateX(100%);
    /* IE 9 */
    transform: translateX(100%);
    /* Firefox 16+, IE 10+, Opera */
    -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
    transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
  }

  .loaded #loader {
    opacity: 0;
    -webkit-transition: all 0.3s ease-out;
    transition: all 0.3s ease-out;
  }

  .loaded #loader-wrapper {
    visibility: hidden;
    -webkit-transform: translateY(-100%);
    /* Chrome, Opera 15+, Safari 3.1+ */
    -ms-transform: translateY(-100%);
    /* IE 9 */
    transform: translateY(-100%);
    /* Firefox 16+, IE 10+, Opera */
    -webkit-transition: all 0.3s 1s ease-out;
    transition: all 0.3s 1s ease-out;
  }

  /* JavaScript Turned Off */
  .no-js #loader-wrapper {
    display: none;
  }

  .no-js h1 {
    color: #222222;
  }

  #loader-wrapper .load_title {
    font-family: 'Open Sans';
    color: #FFF;
    font-size: 14px;
    width: 100%;
    text-align: center;
    z-index: 9999999999999;
    position: absolute;
    top: 60%;
    opacity: 1;
    line-height: 30px;
  }

  #loader-wrapper .load_title span {
    font-weight: normal;
    font-style: italic;
    font-size: 14px;
    color: #FFF;
    opacity: 0.5;
  }

  /* 滚动条优化 start */
  ::-webkit-scrollbar {
    width: 8px;
    height: 8px;
  }

  ::-webkit-scrollbar-track {
    background: #f6f6f6;
    border-radius: 2px;
  }

  ::-webkit-scrollbar-thumb {
    background: #cdcdcd;
    border-radius: 2px;
  }

  ::-webkit-scrollbar-thumb:hover {
    background: #747474;
  }

  ::-webkit-scrollbar-corner {
    background: #f6f6f6;
  }

  /* 滚动条优化 end */
</style>

<body>
  <div id="app">
    <div id="loader-wrapper">
      <div id="loader"></div>
      <div class="loader-section section-left"></div>
      <div class="loader-section section-right"></div>
      <div class="load_title">正在加载 后台管理系统 请耐心等待...
      </div>
    </div>
  </div>
  <script type="module" src="/src/main.js"></script>
</body>

</html>

4.修改App.vue

<template>
  <div>
    <router-view></router-view>
  </div>
</template>
<script setup>

</script>
<style lang="less" scoped>

</style>

5.新建路由

router/index.js

import { createRouter, createWebHistory } from 'vue-router';
export const routes = [
    {
        path: "/Login",
        name: "Login",
        component: () => import("@/views/Login/Index.vue"),
        meta: {
            hide: true
        }
    },
    {
        path: "/404",
        name: "404",
        component: () => import("@/views/404/Index.vue"),
        meta: {
            hide: true
        }
    }
]
const router = createRouter({
    routes: routes,
    history: createWebHistory()
})
export default router;

6.request封装

utils/request.js

import axios from "axios"
import { useUserStore } from "@/store"
import { tansParams } from '@/utils/ruoyi'
import { ElMessage, ElMessageBox } from "element-plus"

axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({
	// axios中请求配置有baseURL选项,表示请求URL公共部分
	// baseURL: process.env.VUE_APP_BASE_URL,
	// 超时
	timeout: 10000
})

//请求拦截器
service.interceptors.request.use(
	config => {
		// 是否需要设置 token
		const isToken = (config.headers || {}).isToken === false
		const hasToken = useUserStore().token
		if (hasToken && !isToken) {
			config.headers['Authorization'] = 'Bearer ' + hasToken // 让每个请求携带自定义token 请根据实际情况自行修改
		}
		// 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
	},
	error => {
		return Promise.reject(new Error(error))
	})

//响应拦截器
service.interceptors.response.use(res => {
    // 未设置状态码则默认成功状态
    const code = res.data.code || 200;
    // 获取错误信息
    const msg = res.data.msg
    if (code === 401) {
		ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' })
		.then(() => {
			useUserStore().Logout().then(() => {
			  location.href = '/';
			})
		})
		.catch(() => {
		});
		return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
    }
      else if (code === 500) {
      ElMessage({ message: msg, type: 'error' })
      return Promise.reject(new Error(msg))
    } else if (code === 601) {
      ElMessage({ message: msg, type: 'warning' })
      return Promise.reject(new Error(msg))
    } else if (code !== 200) {
      ElNotification.error({ title: msg })
      return Promise.reject('error')
    } else {
      return  Promise.resolve(res.data)
    }
  },
  error => {
    console.log('err' + error)
    let { message } = error;
    if (message == "Network Error") {
      message = "后端接口连接异常";
    } else if (message.includes("timeout")) {
      message = "系统接口请求超时";
    } else if (message.includes("Request failed with status code")) {
      message = "系统接口" + message.substr(message.length - 3) + "异常";
    }
    ElMessage({ message: message, type: 'error', duration: 5 * 1000 })
    return Promise.reject(error)
  }
)
export default service

7.登录页面

views/Lgoin/Index.vue

<template>
  <div class="login_body">
    <Particles id="tsparticles" :options="particles_login"> </Particles>
    <div class="login_form">
      <h1 class="login_form_title">后台管理系统</h1>
      <el-form
        ref="loginRef"
        :model="loginForm"
        :rules="loginRules"
        label-width="auto"
        label-position="top"
        status-icon
        size="large"
      >
        <div class="login_form_item">
          <el-form-item label="用户名:" prop="username">
            <el-input v-model="loginForm.username" placeholder="请输入用户名" />
          </el-form-item>
          <el-form-item label="密码:" prop="password">
            <el-input
              v-model="loginForm.password"
              type="password"
              placeholder="请输入密码"
            />
          </el-form-item>

          <div class="login_form_button">
            <el-button type="primary" @click="handleLogin">登录</el-button>
          </div>
        </div>
      </el-form>
    </div>
    <!--  底部  -->
    <div class="el-login-footer">
      <span>Copyright © 2018-2023 tayo.com All Rights Reserved.</span>
    </div>
  </div>
</template>
<script setup>
import { ref, getCurrentInstance, watch } from "vue";
import { particles_login } from "@/utils/particles-config.js";
import {useUserStore} from "@/store";
import { useRoute, useRouter } from "vue-router";

const userStore = useUserStore();
const route = useRoute();
const router = useRouter();
const { proxy } = getCurrentInstance();
const redirect = ref(undefined);

const loginForm = ref({
  username: "admin",
  password: "admin123",
});
const loginRules = ref({
  username: { required: true, message: "请输入用户名", trigger: "change" },
  password: { required: true, message: "请输入密码", trigger: "change" },
});

watch(
  route,
  (newRoute) => {
    redirect.value = newRoute.query && newRoute.query.redirect;
  },
  { immediate: true }
);

const handleLogin = () => {
  proxy.$refs.loginRef.validate((valid) => {
    if (valid) {
      // 调用action的登录方法
      userStore
        .Login(loginForm.value)
        .then(() => {
          router.push({ path: redirect.value || "/" });
        })
        .catch(() => {
          router.push("/Login")
        });
    }
  });
};
</script>
<style lang="less" scoped>
.login_body {
  height: 100vh;
  width: 100vw;
  background-size: cover;
  background-repeat: no-repeat;
  background-image: url("../../assets/images/login-background.png");
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  .login_form {
    z-index: 2;
    position: absolute;
    width: 35vw;
    padding: 30px 15px;
    background: #fff;
    border-radius: 5px;
    box-shadow: 0 2px 12px 0 black;
    .login_form_title {
      margin-bottom: 13px;
      text-align: center;
    }
    .login_form_item {
      margin: 0 16px;
    }
    .login_form_button {
      width: 100%;
      .el-button {
        width: 100%;
      }
    }
  }
}
.el-login-footer {
  height: 40px;
  line-height: 40px;
  position: fixed;
  bottom: 0;
  width: 100%;
  text-align: center;
  color: #fff;
  font-family: Arial;
  font-size: 12px;
  letter-spacing: 1px;
}
</style>

状态管理和发送请求

store/modules/user.js

import { defineStore } from 'pinia';
import { login, logout } from "@/api/login"
import { getCookies, setCookies, removeCookies } from "@/utils/cookie.js";
// 用户模块
export const useUserStore = defineStore('user', {
    state: () => ({
        token: getCookies("token"),
        username: "",
        avatar: ""
    }),
    actions: {
        // 登录
        Login(userInfo) {
            const username = userInfo.username.trim()
            const password = userInfo.password
            return new Promise((resolve, reject) => {
                login({ username, password }).then((res) => {
                    setCookies("token", res.data.token)
                    this.token = res.data.token
                    resolve(res)
                }).catch((err) => {
                    reject(err)
                });
            })
        },

        // 获取用户信息
        GetInfo() {
            return new Promise((resolve, reject) => {
                getInfo().then(res => {
                    const user = res.data.user
                    this.username = user.userName
                    this.avatar = avatar;
                    resolve(res)
                }).catch(error => {
                    reject(error)
                })
            })
        },

        // 退出系统
        Logout() {
            return new Promise((resolve, reject) => {
                logout().then(() => {
                    this.token = ''
                    removeCookies(token)
                    resolve()
                }).catch(error => {
                    reject(error)
                })
            })
        },
    }
})

8.前置路由拦截

permission.js

import { useUserStore, usePermissionStore } from '@/store'
import { start, close } from "@/utils/nprogress.js";
import router from "@/router"
import { setRouterPackag } from "@/utils/user"

// 记录路由
let hasRoles = true
// 白名单(不需要登录就可以访问的名单)
const whiteList = ['/Login'];

router.beforeEach(async (to, from, next) => {
    start();
    //获取token
    const token = useUserStore().token;

    //判断是否登录
    if (token) {
        //如果等录还去登录页,直接返回首页
        if (to.path === '/Login') {
            next({ path: '/' })
        } else {
            //调用获取信息的action,重新获取动态路由
            await usePermissionStore().SetDynamicRoute()
            //拿到动态添加的路由
            let routes = usePermissionStore().menuList
            if (hasRoles) {
                //重新添加动态路由
                setRouterPackag(routes);
                hasRoles = false
                next({ ...to, replace: true })
            } else {
                next()
            }
        }
    } else {
        //如果没有登录,如果要去登录页,直接放行
        if (whiteList.includes(to.path)) {
            next()
        } else {
            next(`/Login`)
        }
    }
})

router.afterEach(() => {
    close();
});

router.onError((err) => {
    close();
});

utils/user.js

import Layout from "@/layout/Index.vue";
import router from "@/router";
// 添加动态路由
export const setRouterPackag = (list = []) => {
  const routers = getRoutes(JSON.parse(JSON.stringify(list)));
  if (routers && routers.length > 0) {
    for (let i = 0; i < routers.length; i++) {
      if (isRouter(routers[i])) {
        router.addRoute(routers[i]);
      }
    }
  }
  // 404页面
  const notPath = { path: "/:catchAll(.*)", redirect: "/404" };
  if (isRouter(notPath)) {
    router.addRoute(notPath);
  }
};

// 处理获取到路由的参数
const getRoutes = (list) => {
  for (let i = 0; i < list.length; i++) {
    if (list[i].component === "Layout") {
      list[i].component = Layout;
    } else if (list[i].component) {
      list[i].component = import(`@/views${list[i].component}.vue`)
    }
    if (list[i].children && list[i].children.length > 0) {
      getRoutes(list[i].children)
    }
  }
  return list
};

// 判断路由是否有重复值
const isRouter = (item = {}) => {
  const routers = router.getRoutes();
  if (item && Object.keys(item).length > 0) {
    const index = routers.findIndex((x) => x.path === item.path);
    if (index !== -1) {
      return false;
    }
    return true;
  }
  return false;
};

mock.js

const Mock = require('mockjs')

const Random = Mock.Random//获取随机数

//定义返回数据格式
let Result = {
    code: 200,
    msg: "操作成功",
    data: null
}

var getQuery = (url, name) => {
    const index = url.indexOf("?");
    if (index !== -1) {
        const queryStrArr = url.substr(index + 1).split("&");
        for (var i = 0; i < queryStrArr.length; i++) {
            const itemArr = queryStrArr[i].split("=");
            if (itemArr[0] === name) {
                return itemArr[1];
            }
        }
    }
    return null;
};

//登录操作处理
Mock.mock(/\login/, "post", (data) => {
    const username = getQuery(data.url, "username");
    const password = getQuery(data.url, "password");

    Result.data = {
        token: Random.string(32),
        username,
        password
    }
    return Result
})

//获取用户信息
Mock.mock("/getInfo", "get", () => {
    Result.data = {
        user: {
            avatar: "@/assets/images/profile.jpg",
            username: "杨"
        }
    }
    return Result
})

//退出
Mock.mock("/logout", "get", () => {
    return Result
})

//获取路由信息
Mock.mock("/getRouters", "get", () => {
    Result.data = [
        {
            path: "/",
            name: "Home",
            redirect: "/Home",
            component: "Layout",
            children: [
                {
                    path: "/Home",
                    component: "/Home/Index",
                    meta: {
                        title: "首页",
                        icon: "HomeFilled",
                        affix: true
                    },
                },
                {
                    path: "/System",
                    redirect: "/System/User",
                    meta: {
                        title: "系统管理",
                        icon: "Operation",

                    },
                    children: [
                        {
                            path: "/System/User",
                            name: "UserIndex",
                            component: "/System/User/Index",
                            meta: {
                                title: "用户管理",
                                icon: "UserFilled",
                            },
                        },
                        {
                            path: "/System/Role",
                            name: "RoleIndex",
                            component: "/System/Role/Index",
                            meta: {
                                title: "角色管理",
                                icon: "Memo",
                            },
                        },
                        {
                            path: "/System/Menu",
                            name: "MenuIndex",
                            component: "/System/Menu/Index",
                            meta: {
                                title: "菜单管理",
                                icon: "Menu",
                            },
                        },
                    ],
                },
                {
                    path: "/food",
                    redirect: "/food/Dish",
                    meta: {
                        title: "饭堂管理",
                        icon: "Tools",
                    },
                    children: [
                        {
                            path: "/food/ChefCommission",
                            name: "ChefCommission",
                            component: "/food/ChefCommission",
                            meta: {
                                title: "日菜谱管理",
                                icon: "Memo",
                            },
                        },
                        {
                            path: "/food/Dish",
                            name: "Dish",
                            component: "/food/Dish",
                            meta: {
                                title: "小炒管理",
                                icon: "Memo",
                            },
                        },
                        {
                            path: "/food/Menu",
                            name: "Menu",
                            component: "/food/Menu",
                            meta: {
                                title: "菜谱管理",
                                icon: "Memo",
                            },
                        },
                        {
                            path: "/food/Notice",
                            name: "Notice",
                            component: "/food/Notice",
                            meta: {
                                title: "公告管理",
                                icon: "Memo",
                            },
                        },
                        {
                            path: "/food/Order",
                            name: "Order",
                            component: "/food/Order",
                            meta: {
                                title: "订单管理",
                                icon: "Memo",
                            },
                        },
                        {
                            path: "/food/WorkingMeal",
                            name: "WorkingMeal",
                            component: "/food/WorkingMeal",
                            meta: {
                                title: "工作餐管理",
                                icon: "Memo",
                            },
                        },
                        {
                            path: "/food/Comment",
                            name: "Comment",
                            component: "/food/Comment",
                            meta: {
                                title: "评论管理",
                                icon: "Memo",
                            },
                        },
                    ]
                },

            ],
        }
    ]
    return Result
})

Mock.mock("/getMenuList", "get", () => {
    Result.data = [
        {
            id: 1,
            created: "2021-01-15T18:58:18",
            updated: "2021-01-15T18:58:20",
            statu: 1,
            parentId: 0,
            name: "系统管理",
            path: "",
            perms: "sys:manage",
            component: "",
            type: 0,
            icon: "el-icon-s-operation",
            ordernum: 1,
            children: [
                {
                    id: 2,
                    created: "2021-01-15T19:03:45",
                    updated: "2021-01-15T19:03:48",
                    statu: 1,
                    parentId: 1,
                    name: "用户管理",
                    path: "/sys/users",
                    perms: "sys:user:list",
                    component: "sys/User",
                    type: 1,
                    icon: "el-icon-s-custom",
                    ordernum: 1,
                    children: [
                        {
                            id: 9,
                            created: "2021-01-17T21:48:32",
                            updated: null,
                            statu: 1,
                            parentId: 2,
                            name: "添加用户",
                            path: null,
                            perms: "sys:user:save",
                            component: null,
                            type: 2,
                            icon: null,
                            ordernum: 1,
                            children: [],
                        },
                        {
                            id: 10,
                            created: "2021-01-17T21:49:03",
                            updated: "2021-01-17T21:53:04",
                            statu: 1,
                            parentId: 2,
                            name: "修改用户",
                            path: null,
                            perms: "sys:user:update",
                            component: null,
                            type: 2,
                            icon: null,
                            ordernum: 2,
                            children: [],
                        },
                        {
                            id: 11,
                            created: "2021-01-17T21:49:21",
                            updated: null,
                            statu: 1,
                            parentId: 2,
                            name: "删除用户",
                            path: null,
                            perms: "sys:user:delete",
                            component: null,
                            type: 2,
                            icon: null,
                            ordernum: 3,
                            children: [],
                        },
                        {
                            id: 12,
                            created: "2021-01-17T21:49:58",
                            updated: null,
                            statu: 1,
                            parentId: 2,
                            name: "分配角色",
                            path: null,
                            perms: "sys:user:role",
                            component: null,
                            type: 2,
                            icon: null,
                            ordernum: 4,
                            children: [],
                        },
                        {
                            id: 13,
                            created: "2021-01-17T21:50:36",
                            updated: null,
                            statu: 1,
                            parentId: 2,
                            name: "重置密码",
                            path: null,
                            perms: "sys:user:repass",
                            component: null,
                            type: 2,
                            icon: null,
                            ordernum: 5,
                            children: [],
                        },
                    ],
                },
                {
                    id: 3,
                    created: "2021-01-15T19:03:45",
                    updated: "2021-01-15T19:03:48",
                    statu: 1,
                    parentId: 1,
                    name: "角色管理",
                    path: "/sys/roles",
                    perms: "sys:role:list",
                    component: "sys/Role",
                    type: 1,
                    icon: "el-icon-rank",
                    ordernum: 2,
                    children: [],
                },
            ],
        },
        {
            id: 5,
            created: "2021-01-15T19:06:11",
            updated: null,
            statu: 1,
            parentId: 0,
            name: "系统工具",
            path: "",
            perms: "sys:tools",
            component: null,
            type: 0,
            icon: "el-icon-s-tools",
            ordernum: 2,
            children: [
                {
                    id: 6,
                    created: "2021-01-15T19:07:18",
                    updated: "2021-01-18T16:32:13",
                    statu: 1,
                    parentId: 5,
                    name: "数字字典",
                    path: "/sys/dicts",
                    perms: "sys:dict:list",
                    component: "sys/Dict",
                    type: 1,
                    icon: "el-icon-s-order",
                    ordernum: 1,
                    children: [],
                },
            ],
        },
    ]
    return Result
})

9.动态面包屑

TagsView/index.vue

<template>
  <div class="tags-view-container">
    <scroll-pane ref="scrollPaneRef" class="tags-view-wrapper">
      <router-link
        v-for="tag in visitedViews"
        :key="tag.path"
        tag="span"
        :class="isActive(tag) ? 'active' : ''"
        :to="{ path: tag.path }"
        class="tags-view-item"
        :style="activeStyle(tag)"
        @contextmenu.prevent="openMenu(tag, $event)"
      >
        {{ tag.title }}
        <span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
          <close
            class="el-icon-close"
            style="width: 1em; height: 1em; vertical-align: middle"
          />
        </span>
      </router-link>
    </scroll-pane>
    <ul
      v-show="visible"
      :style="{ left: left + 'px', top: top + 'px' }"
      class="contextmenu"
    >
      <li @click="closeAllTags(selectedTag)">
        <circle-close style="width: 1em; height: 1em" /> 全部关闭
      </li>
    </ul>
  </div>
</template>
<script setup>
import ScrollPane from "./ScrollPane.vue";
import { ref, computed, watch, getCurrentInstance } from "vue";
import { useTagsViewStore, useStore } from "@/store";
import { useRoute, useRouter } from "vue-router";

const visible = ref(false);
const top = ref(0);
const left = ref(0);
const selectedTag = ref({});
const affixTags = ref([]);
const { proxy } = getCurrentInstance();
const route = useRoute();
const router = useRouter();
const visitedViews = computed(() => useTagsViewStore().visitedViews);
const theme = useStore().theme;
watch(route, () => {
  addTags();
});
watch(visible, (value) => {
  if (value) {
    document.body.addEventListener("click", closeMenu);
  } else {
    document.body.removeEventListener("click", closeMenu);
  }
});
function addTags() {
  const { name } = route;
  if (name) {
    useTagsViewStore().addView(route);
    if (route.meta.link) {
      useTagsViewStore().addIframeView(route);
    }
  }
  return false;
}
function isActive(r) {
  return r.path === route.path;
}
function activeStyle(tag) {
  if (!isActive(tag)) return {};
  return {
    "background-color": theme,
    "border-color": theme,
  };
}
function isAffix(tag) {
  return tag.meta && tag.meta.affix;
}
function closeSelectedTag(view) {
  useTagsViewStore()
    .delView(view)
    .then(({ visitedViews }) => {
      if (isActive(view)) {
        toLastView(visitedViews, view);
      }
    });
}
function toLastView(visitedViews, view) {
  const latestView = visitedViews.slice(-1)[0];
  if (latestView) {
    router.push(latestView.fullPath);
  } else {
    // now the default is to redirect to the home page if there is no tags-view,
    // you can adjust it according to your needs.
    if (view.name === "Home") {
      // to reload home page
      router.replace({ path: "/redirect" + view.fullPath });
    } else {
      router.push("/");
    }
  }
}
function openMenu(tag, e) {
  top.value = e.clientY;
  left.value = e.clientX;
  visible.value = true;
  selectedTag.value = tag;
}
function closeMenu() {
  visible.value = false;
}
function closeAllTags(view) {
  useTagsViewStore()
    .delAllViews()
    .then(({ visitedViews }) => {
      if (affixTags.value.some((tag) => tag.path === route.path)) {
        return;
      }
      toLastView(visitedViews, view);
    });
}
</script>
<style lang='scss' scoped>
.tags-view-container {
  height: 34px;
  width: 100%;
  background: #fff;
  z-index: 3000;
  border-bottom: 1px solid #d8dce5;
  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
  .tags-view-wrapper {
    .tags-view-item {
      display: inline-block;
      position: relative;
      overflow: hidden;
      cursor: pointer;
      height: 26px;
      line-height: 26px;
      border: 1px solid #d8dce5;
      color: #495060;
      background: #fff;
      padding: 0 8px;
      font-size: 12px;
      margin-left: 5px;
      margin-top: 4px;
      &:first-of-type {
        margin-left: 15px;
      }
      &:last-of-type {
        margin-right: 15px;
      }
      &.active {
        background-color: #42b983;
        color: #fff;
        border-color: #42b983;
        &::before {
          content: "";
          background: #fff;
          display: inline-block;
          width: 8px;
          height: 8px;
          border-radius: 50%;
          position: relative;
          margin-right: 2px;
        }
      }
    }
  }
  .contextmenu {
    margin: 15px;
    background: #fff;
    position: fixed;
    list-style-type: none;
    padding: 5px 0;
    border-radius: 4px;
    font-size: 12px;
    font-weight: 400;
    color: #333;
    box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
    white-space: nowrap;
    li {
      margin: 0;
      padding: 8px 16px;
      cursor: pointer;
      &:hover {
        background: #eee;
      }
    }
  }
}
</style>

<style lang="scss">
//reset element css of el-icon-close
.tags-view-wrapper {
  .tags-view-item {
    .el-icon-close {
      width: 16px;
      height: 16px;
      vertical-align: 2px;
      border-radius: 50%;
      text-align: center;
      transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
      transform-origin: 100% 50%;
      &:before {
        transform: scale(0.6);
        display: inline-block;
        vertical-align: -3px;
      }
      &:hover {
        background-color: #b4bccc;
        color: #fff;
        width: 12px !important;
        height: 12px !important;
      }
    }
  }
}
</style>

ScrollPane.vue

<template>
  <el-scrollbar :vertical="false" class="scroll-container">
    <slot />
  </el-scrollbar>
</template>

store/tagsView.js

import { defineStore } from "pinia";

export const useTagsViewStore = defineStore("tagsView", {
  state: () => ({
    visitedViews: [],
  }),
  actions: {
    addView(view) {
      this.addVisitedView(view)
    },
    addVisitedView(view) {
      if (this.visitedViews.some(v => v.path === view.path)) return
      this.visitedViews.push(
        Object.assign({}, view, {
          title: view.meta.title || 'no-name'
        })
      )
    },
    delView(view) {
      return new Promise(resolve => {
        this.delVisitedView(view)
        resolve({
          visitedViews: [...this.visitedViews],
        })
      })
    },
    delVisitedView(view) {
      return new Promise(resolve => {
        for (const [i, v] of this.visitedViews.entries()) {
          if (v.path === view.path) {
            this.visitedViews.splice(i, 1)
            break
          }
        }
        resolve([...this.visitedViews])
      })
    },
    delAllViews(view) {
      return new Promise(resolve => {
        this.delAllVisitedViews(view)
        resolve({
          visitedViews: [...this.visitedViews],
        })
      })
    },
    delAllVisitedViews(view) {
      return new Promise(resolve => {
        const affixTags = this.visitedViews.filter(tag => tag.meta.affix)
        this.visitedViews = affixTags
        resolve([...this.visitedViews])
      })
    },
  }
});

全屏

npm install screenfull@5.1.0 save
<template>
  <div @click="handleFullScreen" id="screenFul">
    <svg-icon :icon="icon ? 'exit-fullscreen' : 'fullscreen'"></svg-icon>
  </div>
</template>

<script setup>
import screenfull from 'screenfull'
import { ref, onMounted, onBeforeMount } from 'vue'

const icon = ref(screenfull.isFullscreen)
const handleFullScreen = () => {
  if (screenfull.isEnabled) {
    screenfull.toggle()
  }
}

const changeIcon = () => {
  icon.value = screenfull.isFullscreen
}

onMounted(() => {
  screenfull.on('change', changeIcon)
})

onBeforeMount(() => {
  screenfull.off('change')
})
</script>

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

导入SvgIcon

components/SvgIcon/index.vue

<template>
  <svg class="svg-icon" aria-hidden="true">
    <use :xlink:href="iconName"></use>
  </svg>
</template>

<script setup>
import { defineProps, computed } from 'vue'
const props = defineProps({
  icon: {
    type: String,
    required: true
  }
})

const iconName = computed(() => {
  return `#icon-${props.icon}`
})
</script>

<style lang="scss" scoped>
.svg-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}
</style>

icons/index.js

import SvgIcon from '@/components/SvgIcon'

const svgRequired = require.context('./svg', false, /\.svg$/)
svgRequired.keys().forEach((item) => svgRequired(item))

export default (app) => {
  app.component('svg-icon', SvgIcon)
}

main.js中声明

// 导入svgicon
import SvgIcon from "@/icons/index"
SvgIcon(app)

安装svgloader依赖

npm install svg-sprite-loader --save-dev

在vue.config.js中配置

const path = require('path')
function resolve(dir) {
  return path.join(__dirname, dir)
}
const webpack = require('webpack')

module.exports = defineConfig({
     chainWebpack(config) {
    // 设置 svg-sprite-loader
    // config 为 webpack 配置对象
    // config.module 表示创建一个具名规则,以后用来修改规则
    config.module
      // 规则
      .rule('svg')
      // 忽略
      .exclude.add(resolve('src/icons'))
      // 结束
      .end()
    // config.module 表示创建一个具名规则,以后用来修改规则
    config.module
      // 规则
      .rule('icons')
      // 正则,解析 .svg 格式文件
      .test(/\.svg$/)
      // 解析的文件
      .include.add(resolve('src/icons'))
      // 结束
      .end()
      // 新增了一个解析的loader
      .use('svg-sprite-loader')
      // 具体的loader
      .loader('svg-sprite-loader')
      // loader 的配置
      .options({
        symbolId: 'icon-[name]'
      })
      // 结束
      .end()
    config
      .plugin('ignore')
      .use(new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /zh-cn$/))
    config.module
      .rule('icons')
      .test(/\.svg$/)
      .include.add(resolve('src/icons'))
      .end()
      .use('svg-sprite-loader')
      .loader('svg-sprite-loader')
      .options({
        symbolId: 'icon-[name]'
      })
      .end()
  },
}
  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值