十九.如何动态添加后端返回的菜单实现动态路由的添加呢?
1. router/index.js 文件
import { createRouter, createWebHashHistory } from "vue-router";
import Admin from "~/Layout/admin.vue";
import Index from "~/pages/index.vue";
import Login from "~/pages/Login/login.vue";
import NotFound from "~/pages/NotFound/404.vue";
import GoodList from "~/pages/Goods/List.vue";
import CategoryList from "~/pages/Category/ListCategory.vue";
// const routes = [
// {
// path: "/",
// component: Admin,
// children: [
// {
// path: '/',
// component: Index,
// meta: {
// title: "后台首页"
// }
// },
// {
// path: '/goods/list',
// component: GoodList,
// meta: {
// title: "商品管理"
// }
// },
// {
// path: '/category/list',
// component: CategoryList,
// meta: {
// title: "分类管理"
// }
// }
// ]
// },
// {
// path: "/login",
// component: Login,
// meta: {
// title: "登录"
// }
// },
// {
// path: "/:pathMatch(.*)*",
// name: "NotFound",
// component: NotFound,
// meta: {
// title: "404-NotFound"
// }
// },
// ];
// 这个是公共路由 所有用户共享
const routes = [
{
path: "/",
name: "admin",
component: Admin,
},
{
path: "/login",
name: "login",
component: Login,
meta: {
title: "登录",
},
},
{
path: "/:pathMatch(.*)*",
name: "NotFound",
component: NotFound,
meta: {
title: "404-NotFound",
},
},
];
// 1. 动态路由,用于匹配菜单动态添加路由
const aysncRoutes = [
{
path: "/",
component: Index,
name: "/",
meta: {
title: "后台首页",
},
},
{
path: "/goods/list",
component: GoodList,
name: "/goods/list",
meta: {
title: "商品管理",
},
},
{
path: "/category/list",
component: CategoryList,
name: "/category/list",
meta: {
title: "分类管理",
},
},
];
export const router = createRouter({
history: createWebHashHistory(),
routes,
});
// 2. 动态添加路由的方法
export const addRoutes = (menus) => {
console.log(menus);
// 是否有新的路由
let hasNewRoutes = false;
const findAndAddRoutesByMenus = (arr) => {
arr.forEach((e) => {
let item = aysncRoutes.find((o) => o.path == e.frontpath);
// item: 是否有item; router.hasRoute():检查路由是否存在
// router.hasRoute(): 传递一个name,所以要在路由中写上name名称
if (item && !router.hasRoute(item.path)) { // !router.hasRoute(item.path): 没有注册过路由
// addRoute():参数一: 父级路由的name值,参数二:满足条件的对象
router.addRoute("admin", item);
hasNewRoutes = true;
}
// e: 是一个对象
if (e.child && e.child.length > 0) {
findAndAddRoutesByMenus(e.child);
}
});
};
findAndAddRoutesByMenus(menus);
return hasNewRoutes;
// console.log(router.getRoutes());
};
2.
// 专门处理权限相关的代码
// 1. 引入路由实例
import { router, addRoutes } from "~/router";
import { getToken } from "~/commonCookie/Auth";
import { Toast, showFullLoading, hideFullLoading } from "~/commonCookie/utils";
import store from "./store";
// 前置路由守卫
router.beforeEach(async (to, from, next) => {
// 只要路由发生了变化 要实现loading效果
showFullLoading();
const token = getToken();
if (!token && to.path !== "/login") {
Toast("请先登录", "error");
return next({ path: "/login" });
}
// 防止重复登录
if (token && to.path == "/login") {
Toast("请务重复登录", "error");
return next({ path: from.path ? from.path : "/" });
}
let hasNewRoutes = false;
// 如果用户登录了 ,自动获取用户信息, 并储存在vuex中,
// 这种是为了防止 第一次可以获取到数据, 但是在页面刷新以后数据会丢失
if (token) {
const res = await store.dispatch("getInfo");
// console.log(res);
// 动态添加路由 hasNewRoutes:返回值是Boolean当有新的路由的时候 值为true
hasNewRoutes = addRoutes(res.menus);
}
// 设置页面标题
// console.log(to.meta.title); // 显示的是当前路由的title
let title = to.meta?.title + "--振楚后台系统";
document.title = title;
console.log(to);
// 防止路由刷新页面消失,hasNewRoutes:返回值是Boolean当有新的路由的时候 值为true
hasNewRoutes ? next(to.fullPath) : next();
});
// 全局后置守卫
router.afterEach((to, from) => {
// 当路由加载完成以后 关闭全局的进度条
hideFullLoading();
});
二十. 封装一个完整版的标签导航
1. FTagListHooks.js
import { ref } from "vue";
import { useRoute, onBeforeRouteUpdate, useRouter } from "vue-router";
import { useCookies } from "@vueuse/integrations/useCookies";
export const useTabList = () => {
const route = useRoute();
const router = useRouter();
const cookie = useCookies();
const activeTab = ref(route.path);
const tabList = ref([
{
title: "后台首页",
path: "/",
},
]);
const changeTab = (t) => {
console.log(t);
activeTab.value = t;
router.push(t);
};
const removeTab = (t) => {
console.log(t); // 拿到path值
let tabs = tabList.value;
let a = activeTab.value;
if (a == t) {
tabs.forEach((tab, index) => {
if (tab.path == t) {
const nextTab = tabs[index + 1] || tabs[index - 1];
if (nextTab) {
a = nextTab.path; // 点击删除,匹配到当前上一个或者写一个的路径
}
}
});
}
activeTab.value = a;
tabList.value = tabList.value.filter((tab) => tab.path != t);
cookie.set("tabList", tabList.value);
};
// 监听当前路由改变
onBeforeRouteUpdate((to, from) => {
// console.log(to);
// console.log(from);
activeTab.value = to.path;
addTab({
title: to.meta.title,
path: to.path,
});
});
const addTab = (tabObj) => {
let noTab = tabList.value.findIndex((t) => t.path == tabObj.path) == -1; // 代表此时循环是数据是没有这个tab
if (noTab) {
tabList.value.push(tabObj);
}
cookie.set("tabList", tabList.value);
};
// 初始化标签导航列表,当你在刷新浏览器的时候 添加的tab标签没有了
const initTabList = () => {
let tabs = cookie.get("tabList");
if (tabs) {
tabList.value = tabs;
}
};
initTabList();
// 关闭标签
const handleClode = (c) => {
// closeOther
// closeAll
if (c == "closeAll") {
// 切换到首页
activeTab.value = "/";
// 过滤只剩下首页
tabList.value = [
{
title: "后台首页",
path: "/",
},
];
} else if (c == "closeOther") {
tabList.value = tabList.value.filter(
(tab) => tab.path == "/" || tab.path == activeTab.value
);
}
cookie.set("tabList", tabList.value);
};
return {
activeTab,
tabList,
changeTab,
removeTab,
handleClode
}
};
2. FTagList.vue
<template>
<div class="f-tag-list" :style="{ left: $store.state.asideWidth }">
<el-tabs v-model="activeTab" type="card" class="flex-1" @tab-remove="removeTab" @tab-change="changeTab"
style="min-width: 100px">
<el-tab-pane v-for="item in tabList" :closable="item.path != '/'" :key="item.path" :label="item.title"
:name="item.path">
</el-tab-pane>
</el-tabs>
<span class="tag-btn-class">
<el-dropdown @command="handleClode">
<span class="el-dropdown-link">
<el-icon>
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="closeOther">关闭其他</el-dropdown-item>
<el-dropdown-item command="closeAll">全部关闭</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</div>
<div style="height: 44px"></div>
</template>
<script setup>
import { useTabList } from "./CpnHook/FTagListHooks.js";
const {
activeTab,
tabList,
changeTab,
removeTab,
handleClode
} = useTabList()
</script>
<style>
.f-tag-list {
@apply fixed bg-gray-100 flex items-center px-2;
top: 64px;
right: 0;
height: 44px;
z-index: 100;
}
.tag-btn-class {
@apply bg-white rounded ml-auto flex items-center justify-center;
height: 32px;
width: 32px;
}
.el-tabs__header {
margin-bottom: 0px !important;
border: 0 !important;
}
.el-tabs__nav {
border: 0 !important;
}
.el-tabs__item {
border: 0 !important;
height: 32px;
line-height: 32px;
margin-top: 4px;
@apply bg-white mx-1 rounded;
}
.is-disabled {
cursor: not-allowed !important;
@apply text-gray-300 !important;
}
</style>
实际截图
image.png
二十一. 利用keep-alive 实现 全局 页面缓存
image.png
二十二.设置全局过渡动画
image.png
二十三.使用 animate.style 库来做动画效果
使用 animate.style 库来做动画效果
image.png
二十四.如何实现数字滚动 动画效果
gsap库的地址
1. 要是用一个第三方的gsap库安装命令: npm i gsap
2. 封装一个数字滚动组件 CountTo.vue组件
<template>
{{ d.num.toFixed(0) }}
</template>
<script setup>
import gsap from 'gsap';
import { reactive, watch } from 'vue';
// 接收父组件传递过来的数据
const props = defineProps({
value: {
type: Number,
default: 0
}
})
const d = reactive({
num: 0
})
const AnimateTovalue = () => {
/**
* 参数一: 需要变化的那个对象,目标对象,
* 参数二: 对象
* {
* // 动画秒数
* duration: 0.5
* // 当前内部的num: props.value 是父组件传递过来的数据
* num:props.value
* 此时他会从0 到父组件传递过来的数据 使用0.5s 的过渡效果
*
*
* }
*
* **/
gsap.to(d, {
duration: 3,
num: props.value
})
}
AnimateTovalue();
// 监听props.value中的值只要有变化的话, 就重新执行一下动画
watch(() => props.value, () => AnimateTovalue())
</script>
<style>
</style>
3. 在页面使用
// 3.1 引入
import CountTo from "~/components/CountTo/CountTo.vue"
// 3.2 标签上使用
<CountTo :value="item.value"></CountTo>
image.png
二十五.对Echarts图标封装
1. 安装Eacharts: cnpm install echarts --save
2. 封装一个Eacharts.vue文件
<template>
<el-card shadow="never" class="mt-4">
<template #header>
<div class="flex justify-between">
<span class="text-sm">订单统计</span>
<div>
<el-check-tag :checked="item.value === currentValue" v-model="currentValue" v-for="(item,index) in TagOptions"
class="mr-2" :key="index" @change="chartClick(item.value)">{{ item.text }}
</el-check-tag>
</div>
</div>
</template>
<div id="chart" style="width:100%;height:300px;" ref="el"></div>
</el-card>
</template>
<script setup>
import * as echarts from 'echarts';
import { ref, onMounted, onBeforeUnmount } from "vue";
import { getStatistics3 } from "~/api/Index/index.js";
// 当拖动缩小浏览器的窗口时对窗口的监听
import { useResizeObserver } from '@vueuse/core';
const currentValue = ref("week")
const TagOptions = [
{
text: "近1个月",
value: "month"
},
{
text: "近1周",
value: "week"
},
{
text: "近24小时",
value: "hour"
}
]
// Check Tag 选中事件
const chartClick = (type) => {
currentValue.value = type;
getList();
}
var myChart = null;
onMounted(() => {
var chartDom = document.getElementById('chart');
myChart = echarts.init(chartDom);
getList();// 在onMounted中获取数据
})
// 获取图表的数据方法
const getList = () => {
let option = {
xAxis: {
type: 'category',
data: []
},
yAxis: {
type: 'value'
},
series: [
{
data: [],
type: 'bar',
showBackground: true,
backgroundStyle: {
color: 'rgba(180, 180, 180, 0.2)'
}
}
]
};
// 调接口请求图标数据
myChart.showLoading()
getStatistics3(currentValue.value).then(({ x, y }) => {
// console.log(res, "resresresres");
option.xAxis.data = x
option.series[0].data = y
option && myChart.setOption(option);
}).finally(() => {
myChart.hideLoading()
})
}
// 当页面即将销毁的时候 要销毁图表
onBeforeUnmount(() => {
/**
* 官方: 在图表容器被销毁之后 调用echartsInstance.dispose 销毁实例,在图表容器重新被添加再次调用echarts.init初始化
*
* 为啥要调用echarts.dispose(): 销毁实例释放资源,避免内存泄漏
* **/
if (myChart) echarts.dispose(myChart);
})
// 优化: 当拖动缩小浏览器的窗口时对窗口的监听
const el = ref(null)
useResizeObserver(el, (entries) => {
// console.log(el); // 可以监听 对应 浏览器拖动
myChart.resize(); // 重新计算图表的大小
})
</script>
<style>
</style>
3. 在需要引用的页面中使用
import CheckEChart from "~/components/Compone/ComponentIndexEacharts.vue";
<el-row :gutter="20">
<el-col :span="12">
<CheckEChart />
</el-col>
<el-col :span="12"></el-col>
</el-row>
实际截图:
image.png
网友评论