RT :(サポートは、複数のページでルーティング、左のリンクにスクロールメニューへの切り替えの影響について:自己構成されたプロジェクトの要件に応じて)は
、コードにあまり話をしなかった--------------- I美しい分割線 *** ---------------------:
変更
が必要なファイルファイル1.ツールに必要なメソッドはすべてここにあります
ファイル2:ルートナビゲーションファイルの
ファイル3:入り口ページ参照(マルチプロジェクトとの互換性のために、参照ここではオンデマンドの変化に使用している場合、パッケージの契約を行うには)
以下は、各ファイルのコードである
ファイル1:
import { objEqual } from "@/libs/tools";
import { setStore, getStore } from '@/libs/utils/storeage';
export let routeNav = {
/**
* 判断打开的标签列表里是否已存在这个新添加的路由对象
*/
routeHasExist (tagNavList, routeItem) {
let len = tagNavList.length
let res = false
this.doCustomTimes(len, (index) => {
if (this.routeEqual(tagNavList[index], routeItem)) res = true
})
return res
},
/**
* @param {Number} times 回调函数需要执行的次数
* @param {Function} callback 回调函数
*/
doCustomTimes (times, callback) {
let i = -1
while (++i < times) {
callback(i)
}
},
/**
* 新增/关闭后重新赋值
*/
setTagNavList (name, homeName, router, ) {
// 空值处理
let tagList = [];
if (router) { tagList = [...router]; } else { tagList = JSON.parse(getStore(name)) || []; }
if (tagList[0] && tagList[0].name !== homeName) { tagList.shift(); } // 如果第一个不是 homeName 删除
let homeTagIndex = tagList.findIndex(item => item.name === homeName); // homeName判断
if (homeTagIndex > 0) {
let homeTag = tagList.splice(homeTagIndex, 1)[0];
tagList.unshift(homeTag);
}
this.setTagNavListInLocalstorage(name, [...tagList]);
},
/**
* @description 本地存储路由导航集合
*/
setTagNavListInLocalstorage (name, list) {
setStore(name, JSON.stringify(list))
},
getRouteTitleHandled (route) {
let router = { ...route };
let meta = { ...route.meta };
let title = "";
if (meta.title) {
if (typeof meta.title === "function") title = meta.title(router);
else title = meta.title;
}
meta.title = title;
router.meta = meta;
return router;
},
showTitle (item, vm) {
let title = item.meta.title;
if (!title) return;
return title;
},
/**
* @description 根据name/params/query判断两个路由对象是否相等
* @param {*} route1 路由对象
* @param {*} route2 路由对象
*/
routeEqual (route1, route2) {
const params1 = route1.params || {};
const params2 = route2.params || {};
const query1 = route1.query || {};
const query2 = route2.query || {};
return (route1.name === route2.name && objEqual(params1, params2) && objEqual(query1, query2));
},
/**
* @param {*} list 现有标签导航列表
* @param {*} newRoute 新添加的路由原信息对象
* @description 如果该newRoute已经存在则不再添加
*/
getNewTagList (list, newRoute) {
const { name, path, meta, query, params } = newRoute;
let newList = [...list];
// if (this.routeEqual(tagNavList[index], routeItem)) res = true
if (newList.findIndex(item => this.routeEqual(item, newRoute)) >= 0) { return newList; } else {
newList.push({ name, path, meta, query, params });
}
return newList;
},
/**
* @param {Array} list 标签列表
* @param {String} name 当前关闭的标签的name
*/
getNextRoute (homeName, list, route) {
let res = {};
if (list.length === 2) {
res = this.getHomeRoute(homeName, list);
} else {
const index = list.findIndex(item => this.routeEqual(item, route));
if (index === list.length - 1) { res = list[list.length - 2]; } else { res = list[index + 1]; }
}
return res;
},
/**
* @param {Array} routers 路由列表数组
* @description 用于找到路由列表中name为home的对象
*/
getHomeRoute (homeName, routers) {
let i = -1;
let len = routers.length;
let homeRoute = {};
while (++i < len) {
let item = routers[i];
if (item.children && item.children.length) {
let res = getHomeRoute(homeName, item.children);
if (res.name) return res;
} else {
if (item.name === homeName) homeRoute = item;
}
}
return homeRoute;
},
};
objEqual 方法
/**
* @param {*} obj1 对象
* @param {*} obj2 对象
* @description 判断两个对象是否相等,这两个对象的值只能是数字或字符串
*/
export const objEqual = (obj1, obj2) => {
const keysArr1 = Object.keys(obj1);
const keysArr2 = Object.keys(obj2);
if (keysArr1.length !== keysArr2.length) return false;
else if (keysArr1.length === 0 && keysArr2.length === 0) return true;
/* eslint-disable-next-line */ else
return !keysArr1.some(key => obj1[key] != obj2[key]);
};
ファイル2:コンポーネントコード(2つの入力パラメーターlocStorageRouteNameを受け取ります:ローカルストレージ名homeName:デフォルトでルーティングを開きます)
<template>
<div class="tags-nav">
<!-- 我是路由导航 -->
<div class="close-con">
<Dropdown transfer @on-click="handleTagsOption" style="margin-top:6px;">
<Button size="small" type="text">
<Icon :size="18" type="ios-close-circle-outline" />
</Button>
<DropdownMenu slot="list">
<DropdownItem name="close-all">关闭所有</DropdownItem>
<DropdownItem name="close-others">关闭其他</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
<div>
<div class="btn-con left-btn">
<Button type="text" @click="handleScroll(240)">
<Icon :size="18" type="ios-arrow-dropleft" />
</Button>
</div>
<div class="btn-con right-btn">
<Button type="text" @click="handleScroll(-240)">
<Icon :size="18" type="ios-arrow-dropright" />
</Button>
</div>
<div class="scroll-outer" ref="scrollOuter" @DOMMouseScroll="handlescroll" @mousewheel="handlescroll">
<div ref="scrollBody" class="scroll-body" :style="{left: tagBodyLeft + 'px'}">
<transition-group name="taglist-moving-animation">
<Tag type="dot" ref="tagsPageOpened" :closable="item.name !== homeName" v-for="(item, index) in routerList" :color="isCurrentTag(item) ? 'success' : 'default'" :key="`tag-nav-${index}`" :name="item.name" :data-route-item="item" @on-close="close(item)" @click.native="handleClick(item)" @contextmenu.prevent.native="contextMenu(item, $event)">
{{ showTitleInside(item) }}
</Tag>
</transition-group>
</div>
</div>
</div>
</div>
</template>
<script>
import { getStore } from "@/libs/utils/storeage";
export default {
name: "router-navigation",
props: {
homeName: {
type: String,
default () {
return ''
}
},
homeRoute: {
type: Object,
default () {
return {}
}
},
locStorageRouteName: {
type: String,
default () {
return ''
}
}
},
data () {
return {
routerList: [],
tagBodyLeft: 0,
rightOffset: 40,
outerPadding: 4,
contextMenuLeft: 0,
contextMenuTop: 0,
visible: false,
};
},
computed: {
currentRouteObj () {
const { name, params, query } = this.$route;
return { name, params, query };
}
},
watch: {
$route (newRoute, old) {
this.getTagElementByName(newRoute);
const { name, query, params, meta } = newRoute;
this.setRouteTag(this.$Utils.routeNav.getNewTagList(this.routerList, newRoute))
this.$emit('updateOpenName', newRoute.name);
},
visible (value) {
if (value) {
document.body.addEventListener("click", this.closeMenu());
} else {
document.body.removeEventListener("click", this.closeMenu());
}
}
},
mounted () {
this.setRouterList()
const { name, query, params, meta } = this.$route;
this.setRouteTag(this.$Utils.routeNav.getNewTagList(this.routerList, this.homeRoute)) // 初始化的时候默认把homeRoute加入路由导航(ps:兼容跨项目跳转子页面)
this.$nextTick(() => {
this.setRouteTag(this.$Utils.routeNav.getNewTagList(this.routerList, this.$route))
})
this.getTagElementByName(this.$route);
},
methods: {
setRouterList () { // 获取本地路由路径
this.routerList = JSON.parse(getStore(this.locStorageRouteName)) || [];
},
setRouteTag (route) {
this.$Utils.routeNav.setTagNavList(this.locStorageRouteName, this.homeName, route || this.$route);
this.setRouterList()
},
handleCloseTag (res, type, route) { // 关闭标签
if (type === "all") {
this.handleClick(this.homeName);
} else if (this.$Utils.routeNav.routeEqual(this.$route, route)) {
if (type !== "others") {
const nextRoute = this.$Utils.routeNav.getNextRoute(this.homeName, this.routerList, route);
this.$router.push(nextRoute);
}
}
this.setRouteTag(res)
},
// 是否选中
isCurrentTag (item) {
return this.$Utils.routeNav.routeEqual(this.currentRouteObj, item);
},
showTitleInside (item) { // 显示的title
return this.$Utils.routeNav.showTitle(item, this);
},
contextMenu (item, e) { //contextmenu事件控制右键菜单
if (item.name === this.homeName) { return; }
this.visible = true;
const offsetLeft = this.$el.getBoundingClientRect().left;
this.contextMenuLeft = e.clientX - offsetLeft + 10;
this.contextMenuTop = e.clientY - 64;
},
// 移动事件
handlescroll (e) {
var type = e.type;
let delta = 0;
if (type === "DOMMouseScroll" || type === "mousewheel") {
delta = e.wheelDelta ? e.wheelDelta : -(e.detail || 0) * 40;
}
this.handleScroll(delta);
},
handleScroll (offset) {
const outerWidth = this.$refs.scrollOuter.offsetWidth;
const bodyWidth = this.$refs.scrollBody.offsetWidth;
if (offset > 0) {
this.tagBodyLeft = Math.min(0, this.tagBodyLeft + offset);
} else {
if (outerWidth < bodyWidth) {
if (this.tagBodyLeft < -(bodyWidth - outerWidth)) {
this.tagBodyLeft = this.tagBodyLeft;
} else {
this.tagBodyLeft = Math.max(this.tagBodyLeft + offset, outerWidth - bodyWidth);
}
} else {
this.tagBodyLeft = 0;
}
}
},
handleTagsOption (type) { // 关闭按钮事件
if (type.includes("all")) { // 关闭所有,除了home
let res = this.routerList.filter(item => item.name === this.homeName);
this.handleCloseTag(res, "all")
} else if (type.includes("others")) {// 关闭除当前页和home页的其他页
let res = this.routerList.filter(
item => this.$Utils.routeNav.routeEqual(this.currentRouteObj, item) || item.name === this.homeName
);
this.handleCloseTag(res, "others", this.currentRouteObj)
setTimeout(() => { this.getTagElementByName(this.currentRouteObj.name); }, 100);
}
},
getTagElementByName (route) {
this.$nextTick(() => {
this.refsTag = this.$refs.tagsPageOpened || [];
this.refsTag.forEach((item, index) => {
if (this.$Utils.routeNav.routeEqual(route, item.$attrs["data-route-item"])) {
let tag = this.refsTag[index].$el;
this.moveToView(tag);
}
});
});
},
moveToView (tag) {// 移动
const outerWidth = this.$refs.scrollOuter.offsetWidth;
const bodyWidth = this.$refs.scrollBody.offsetWidth;
if (bodyWidth < outerWidth) {
this.tagBodyLeft = 0;
} else if (tag.offsetLeft < -this.tagBodyLeft) {
// 标签在可视区域左侧
this.tagBodyLeft = -tag.offsetLeft + this.outerPadding;
} else if (
tag.offsetLeft > -this.tagBodyLeft && tag.offsetLeft + tag.offsetWidth < -this.tagBodyLeft + outerWidth
) {
// 标签在可视区域
this.tagBodyLeft = Math.min(0, outerWidth - tag.offsetWidth - tag.offsetLeft - this.outerPadding);
} else {
// 标签在可视区域右侧
this.tagBodyLeft = -(tag.offsetLeft - (outerWidth - this.outerPadding - tag.offsetWidth));
}
},
close (route) { // 关闭 列表内 某个 标签
let res = this.routerList.filter(item => !this.$Utils.routeNav.routeEqual(route, item));
this.handleCloseTag(res, undefined, route)
},
handleClick (route) { // 点击切换 列表内 某个 标签
let { name, params, query } = {};
if (typeof route === "string") { name = route; } else {
name = route.name;
params = route.params;
query = route.query;
}
if (this.$Utils.routeNav.routeEqual(this.$route, route)) {
console.log('请勿重复点击当前页面导航,如有需要请刷新当前页面!')
} else {
this.$router.push({ name, params, query });
}
},
closeMenu () {
this.visible = false;
}
},
};
</script>
<style lang="less">
@import "./index.less";
</style>
.no-select{
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.size{
width: 100%;
height: 100%;
}
.tags-nav{
position: relative;
width: 100%;
border-top: 1px solid #F0F0F0;
border-bottom: 1px solid #F0F0F0;
.no-select;
.size;
.close-con{
position: absolute;
right: 0;
top: 0;
height: 100%;
width: 34px;
background: #fff;
text-align: center;
z-index: 10;
}
.btn-con{
position: absolute;
top: 0px;
height: 100%;
background: #fff;
padding-top: 3px;
z-index: 10;
button{
padding: 6px 4px;
line-height: 14px;
text-align: center;
}
&.left-btn{
left: 0px;
}
&.right-btn{
right: 32px;
border-right: 1px solid #F0F0F0;
}
}
.scroll-outer{
position: absolute;
left: 28px;
right: 61px;
top: 0;
bottom: 0;
box-shadow: 0px 0 3px 2px rgba(100,100,100,.1) inset;
background: #F6F6F6;
.scroll-body{
height: ~"calc(100% - 1px)";
display: inline-block;
padding: 1px 4px 0;
position: absolute;
overflow: visible;
white-space: nowrap;
transition: left .3s ease;
.ivu-tag-dot-inner{
transition: background .2s ease;
}
.ivu-tag-default {
background: #E8E8E8!important;
font-size: 14px;
color: #999999;
}
}
}
.contextmenu {
position: absolute;
margin: 0;
padding: 5px 0;
background: #fff;
z-index: 100;
list-style-type: none;
border-radius: 4px;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
li {
margin: 0;
padding: 5px 15px;
cursor: pointer;
&:hover {
background: #eee;
}
}
}
}
ファイル3:
<template>
<div class="app">
<frame-nav></frame-nav>
<div class="float">
<div class="float-left">
<page-list ref="sideMenu"></page-list>
</div>
<div class="float-right">
<div class="tag-nav-wrapper">
<tags-nav :locStorageRouteName="locStorageRouteName" :homeName="homeName" @updateOpenName="updateOpenName" />
</div>
<div v-if="reload">
<keep-alive :include="cacheList">
<router-view />
</keep-alive>
</div>
</div>
</div>
</div>
</template>
<script>
import frameNav from './components/frame-nav'
import pageList from './components/page-link-list'
import { mapState, mapActions } from "vuex";
import TagsNav from "../../../components/route-navigation";
import { getStore } from "@/libs/utils/storeage";
export default {
name: "Layout",
components: { frameNav, pageList, TagsNav }, //TagsNav
computed: {
cacheList () {
return [
...(this.tagNavList.length ? this.tagNavList.filter(item => !(item.meta && item.meta.notCache)).map(item => item.name) : [])
];
}
},
watch: {
$route (newRoute, old) {
this.tagNavList = JSON.parse(getStore(this.locStorageRouteName)) || [];
if (newRoute.name == old.name) {
this.reload = false
setTimeout(() => { this.reload = true }, 50)
}
},
},
data () {
return {
tagNavList: [],
reload: true,
locStorageRouteName: 'xxbDevOpsAdminRouteList',
homeName: 'shopList',
}
},
async created () { },
async mounted () { },
methods: {
...mapActions(["loginScheduler"]),
updateOpenName (name) {
this.$refs.sideMenu.updateOpenName(name);
},
}
}
</script>
<style scoped>
html,
body,
.app {
position: relative;
width: 100%;
height: 100%;
}
.float {
overflow: hidden;
height: calc(100% - 40px);
}
.float-left {
float: left;
width: 200px;
height: 100%;
}
.float-right {
float: right;
width: calc(100% - 200px);
overflow-y: auto;
overflow-x: hidden;
height: 100%;
}
.tag-nav-wrapper {
padding: 0;
height: 40px;
background: #f0f0f0;
}
</style>