FrontendRefactoring:标签栏可以展示多个菜单
This commit is contained in:
parent
58632033b5
commit
3288829a01
@ -107,6 +107,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 顶部浏览器式标签栏存在时,侧边栏让出顶部高度,避免被标签栏遮住
|
||||
&.has-top-tabs {
|
||||
.sidebar-container {
|
||||
top: $top-tabs-height;
|
||||
height: calc(100% - #{$top-tabs-height});
|
||||
}
|
||||
}
|
||||
|
||||
.hideSidebar {
|
||||
.sidebar-container {
|
||||
width: 54px !important;
|
||||
|
||||
@ -38,6 +38,10 @@ $base-sub-menu-hover:#001528;
|
||||
|
||||
$base-sidebar-width: 200px;
|
||||
|
||||
// 顶部浏览器式标签栏高度(固定在页面最顶部,横跨侧边栏)
|
||||
// 取 50px 与 navbar 同高,才能放得下侧边栏原有的 42px logo + 24px 标题
|
||||
$top-tabs-height: 50px;
|
||||
|
||||
// the :export directive is the magic sauce for webpack
|
||||
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
|
||||
:export {
|
||||
|
||||
@ -27,6 +27,8 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/styles/variables.scss';
|
||||
|
||||
.app-main {
|
||||
/* 50= navbar 50 */
|
||||
min-height: calc(100vh - 50px);
|
||||
@ -39,14 +41,10 @@ export default {
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
.hasTagsView {
|
||||
.has-top-tabs {
|
||||
.app-main {
|
||||
/* 84 = navbar + tags-view = 50 + 34 */
|
||||
min-height: calc(100vh - 84px);
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
padding-top: 84px;
|
||||
/* 90 = 顶部标签栏 + navbar = 40 + 50 */
|
||||
min-height: calc(100vh - 50px - #{$top-tabs-height});
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="sidebar-logo-container" :class="{ collapse: collapse }" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
|
||||
<div class="sidebar-logo-container" :class="{ collapse: collapse, compact: compact }" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
|
||||
<transition name="sidebarLogoFade">
|
||||
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
|
||||
<img v-if="logo" :src="logo" class="sidebar-logo" />
|
||||
@ -24,6 +24,11 @@ export default {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
// 内联在顶部标签栏左侧展示(矮行,去掉侧边栏那套 50px 高 + 上边距的排布)
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
variables() {
|
||||
@ -47,6 +52,8 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/styles/variables.scss';
|
||||
|
||||
.sidebarLogoFade-enter-active {
|
||||
transition: opacity 1.5s;
|
||||
}
|
||||
@ -93,5 +100,40 @@ export default {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
// 顶部标签栏左侧的 logo 区:整行高度、背景色与标签栏/侧边栏一致(由 inline style 提供)
|
||||
// 尺寸与侧边栏内的 logo 保持一致(42px 图标 + 24px 标题),只是去掉侧边栏那 15px 上边距
|
||||
&.compact {
|
||||
height: $top-tabs-height;
|
||||
line-height: $top-tabs-height;
|
||||
margin: 0;
|
||||
|
||||
& .sidebar-logo-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 0 12px;
|
||||
|
||||
& .sidebar-logo {
|
||||
flex: 0 0 auto;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
& .sidebar-title {
|
||||
margin-left: 8px;
|
||||
line-height: $top-tabs-height;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
// 侧边栏收起时只留图标,居中显示
|
||||
&.collapse .sidebar-logo-link {
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -39,7 +39,15 @@ export default {
|
||||
return path;
|
||||
},
|
||||
showLogo() {
|
||||
return this.$store.state.settings.sidebarLogo;
|
||||
// 顶部标签栏开启时,logo 由标签栏左侧承载,侧边栏不再重复渲染(否则会被标签栏遮住);
|
||||
// 移动端标签栏不展示 logo,因此侧边栏保留
|
||||
if (!this.settings.sidebarLogo) {
|
||||
return false;
|
||||
}
|
||||
if (!this.settings.tagsView) {
|
||||
return true;
|
||||
}
|
||||
return this.$store.state.app.device === 'mobile';
|
||||
},
|
||||
variables() {
|
||||
return variables;
|
||||
|
||||
@ -77,6 +77,8 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/styles/variables.scss';
|
||||
|
||||
.scroll-container {
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
@ -87,7 +89,29 @@ export default {
|
||||
bottom: 0px;
|
||||
}
|
||||
.el-scrollbar__wrap {
|
||||
height: 49px;
|
||||
height: $top-tabs-height;
|
||||
// 关键:应用全局定义了 ::-webkit-scrollbar{width/height:6px}(index.scss),
|
||||
// element-ui 因此探测到"经典滚动条",会给 wrap 保留 overflow:scroll 并塞进
|
||||
// margin-bottom/right:-6px,于是底部出现占位 6px 的原生横向滚动条,
|
||||
// 把满高的标签盖掉 6px、露出条底色(表现为标签下方一条缝)。
|
||||
// 改 hidden 后既不留位也不画原生滚动条;横向滚动仍由 handleScroll/moveToTarget
|
||||
// 直接改 scrollLeft 驱动,功能不受影响。
|
||||
// margin 是 element-ui 写在内联 style 上的,必须 !important 才能覆盖。
|
||||
// 同源处理可参考 assets/styles/sidebar.scss 里的 .scrollbar-wrapper{overflow-x:hidden!important}
|
||||
overflow: hidden !important;
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
// wrap 未带 --hidden-default 时 element-ui 会补 15px 下内边距,
|
||||
// 会让内容盒只剩 35px,标签溢出让 element-ui 误算出竖向滚动条;这里一并清零
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
// 深色标签栏下默认的深色滚动条几乎看不见,跟随标签栏配色
|
||||
.el-scrollbar__thumb {
|
||||
background-color: var(--top-tabs-line, rgba(0, 0, 0, 0.3));
|
||||
|
||||
&:hover {
|
||||
background-color: var(--top-tabs-fg-dim, rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,28 +2,70 @@
|
||||
<div id="tags-view-container" class="tags-view-container">
|
||||
<scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll">
|
||||
<router-link
|
||||
v-for="tag in visitedViews"
|
||||
v-for="(tag, index) in visibleTags"
|
||||
ref="tag"
|
||||
:key="tag.path"
|
||||
:class="isActive(tag)?'active':''"
|
||||
:class="tagClass(tag, index)"
|
||||
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
|
||||
tag="span"
|
||||
class="tags-view-item"
|
||||
:style="activeStyle(tag)"
|
||||
@click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''"
|
||||
@contextmenu.prevent.native="openMenu(tag,$event)"
|
||||
>
|
||||
{{ tag.title }}
|
||||
<svg v-if="isAffix(tag)" class="tab-pin" viewBox="0 0 12 12" aria-hidden="true">
|
||||
<path d="M4 1h4v3.2l1.6 1.6v1.2H6.7v3.4l-.7.6-.7-.6V7H2.4V5.8L4 4.2V1z" />
|
||||
</svg>
|
||||
<span class="tab-title">{{ tag.title }}</span>
|
||||
<span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
|
||||
</router-link>
|
||||
<!-- 被挤出队列的老标签收进「更多」,按钮紧跟在最后一个可见标签右侧,
|
||||
随标签一起横向滚动。判据用 overflowTags 而不是「标签总数超上限」,
|
||||
保证按钮存在 ⇔ 面板里确实有内容 -->
|
||||
<div
|
||||
v-if="overflowTags.length"
|
||||
ref="moreBtn"
|
||||
class="tags-view-more"
|
||||
:class="{ 'is-open': moreVisible, 'is-active': activeInOverflow }"
|
||||
@click.stop="toggleMore"
|
||||
>
|
||||
更多
|
||||
<i class="el-icon-caret-bottom more-caret" />
|
||||
</div>
|
||||
</scroll-pane>
|
||||
<!-- 面板必须挂在滚动容器之外:scroll-pane 的 wrap 是 overflow:hidden,绝对定位的
|
||||
面板放在里面会被裁掉;left 由 toggleMore 按按钮实际位置算出来 -->
|
||||
<ul v-show="moreVisible" class="more-panel" :style="{ left: moreLeft + 'px' }">
|
||||
<li
|
||||
v-for="tag in overflowTags"
|
||||
:key="tag.path"
|
||||
:class="{ 'is-active': isActive(tag) }"
|
||||
:title="tag.title"
|
||||
@click="goTag(tag)"
|
||||
>
|
||||
<span class="more-title">{{ tag.title }}</span>
|
||||
<i v-if="!isAffix(tag)" class="el-icon-close" @click.stop="closeSelectedTag(tag)" />
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
|
||||
<li @click="refreshSelectedTag(selectedTag)"><i class="el-icon-refresh-right"></i> 刷新页面</li>
|
||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><i class="el-icon-close"></i> 关闭当前</li>
|
||||
<li @click="closeOthersTags"><i class="el-icon-circle-close"></i> 关闭其他</li>
|
||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><i class="el-icon-close"></i> 关闭当前标签</li>
|
||||
<li @click="closeOthersTags"><i class="el-icon-circle-close"></i> 关闭其他标签</li>
|
||||
<li v-if="!isFirstView()" @click="closeLeftTags"><i class="el-icon-back"></i> 关闭左侧</li>
|
||||
<li v-if="!isLastView()" @click="closeRightTags"><i class="el-icon-right"></i> 关闭右侧</li>
|
||||
<li @click="closeAllTags(selectedTag)"><i class="el-icon-circle-close"></i> 全部关闭</li>
|
||||
<li v-if="!isAffix(selectedTag) || isPinned(selectedTag)" class="menu-divider" />
|
||||
<li v-if="!isAffix(selectedTag)" @click="pinSelectedTag">
|
||||
<svg class="menu-pin" viewBox="0 0 12 12" aria-hidden="true">
|
||||
<path d="M4 1h4v3.2l1.6 1.6v1.2H6.7v3.4l-.7.6-.7-.6V7H2.4V5.8L4 4.2V1z" />
|
||||
</svg>
|
||||
固定标签到任务栏
|
||||
</li>
|
||||
<li v-else-if="isPinned(selectedTag)" @click="unpinSelectedTag">
|
||||
<svg class="menu-pin is-off" viewBox="0 0 12 12" aria-hidden="true">
|
||||
<path d="M4 1h4v3.2l1.6 1.6v1.2H6.7v3.4l-.7.6-.7-.6V7H2.4V5.8L4 4.2V1z" />
|
||||
</svg>
|
||||
取消固定
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
@ -32,6 +74,12 @@
|
||||
import ScrollPane from './ScrollPane'
|
||||
import path from 'path'
|
||||
|
||||
// 标签栏最多平铺多少个标签(含固定区)。超出的按队列规则挤进「更多」下拉:
|
||||
// 最近打开的留在栏上,最早打开的移入「更多」——固定区不参与淘汰
|
||||
const MAX_VISIBLE_TAGS = 14
|
||||
// 「更多」下拉面板宽度,用于把面板夹在标签栏可视范围内(与样式里的 width 保持一致)
|
||||
const MORE_PANEL_WIDTH = 200
|
||||
|
||||
export default {
|
||||
components: { ScrollPane },
|
||||
data() {
|
||||
@ -40,7 +88,11 @@ export default {
|
||||
top: 0,
|
||||
left: 0,
|
||||
selectedTag: {},
|
||||
affixTags: []
|
||||
affixTags: [],
|
||||
// 「更多」下拉是否展开
|
||||
moreVisible: false,
|
||||
// 「更多」下拉的横向位置(相对 #tags-view-container),展开时按按钮实际位置计算
|
||||
moreLeft: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -50,8 +102,39 @@ export default {
|
||||
routes() {
|
||||
return this.$store.state.permission.routes
|
||||
},
|
||||
theme() {
|
||||
return this.$store.state.settings.theme;
|
||||
// 固定区标签数(路由声明的 affix + 用户右键固定的)
|
||||
affixCount() {
|
||||
return this.visitedViews.filter(tag => this.isAffix(tag)).length
|
||||
},
|
||||
// 平铺在标签栏上的标签:固定区优先占位,剩余名额给「最近打开的标签」,
|
||||
// 名额之外的老标签按队列规则挤出到「更多」——新开的进栏、最老的移出,
|
||||
// 这样刚点开的页面永远看得见,符合标签栏的使用直觉
|
||||
visibleTags() {
|
||||
const all = this.visitedViews
|
||||
if (all.length <= MAX_VISIBLE_TAGS) {
|
||||
return all
|
||||
}
|
||||
const slots = MAX_VISIBLE_TAGS - this.affixCount
|
||||
// 名额为 0(固定标签自己就占满了)时不放任何普通标签;
|
||||
// 注意不能写成 Math.max(0, slots) + slice(-slots),slice(-0) 等价于 slice(0) 会返回全部
|
||||
const kept = new Set(slots > 0 ? all.filter(tag => !this.isAffix(tag)).slice(-slots) : [])
|
||||
// 用 filter 保序:栏上始终是「固定区 + 打开顺序」,不会因淘汰而重排
|
||||
return all.filter(tag => this.isAffix(tag) || kept.has(tag))
|
||||
},
|
||||
// 收进「更多」的标签:被挤出队列的那些,仍按打开顺序排列(最早的排最前)。
|
||||
// 空数组 = 没有东西被收起,它同时也是「更多」按钮的显隐判据
|
||||
overflowTags() {
|
||||
const visible = new Set(this.visibleTags)
|
||||
return this.visitedViews.filter(tag => !visible.has(tag))
|
||||
},
|
||||
// 标签数量:增删路径很多(关闭当前/其他/左侧/右侧/全部、固定、恢复),
|
||||
// 统一监听这个数字来收回「更多」面板,比在每个关闭方法里各写一次可靠
|
||||
visitedCount() {
|
||||
return this.visitedViews.length
|
||||
},
|
||||
// 当前页落到了「更多」里(点开一个老标签时会发生),按钮要给出提示
|
||||
activeInOverflow() {
|
||||
return this.overflowTags.some(tag => this.isActive(tag))
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@ -59,38 +142,65 @@ export default {
|
||||
this.addTags()
|
||||
this.moveToCurrentTag()
|
||||
},
|
||||
visitedCount() {
|
||||
this.syncMoreVisibility()
|
||||
},
|
||||
visible(value) {
|
||||
if (value) {
|
||||
document.body.addEventListener('click', this.closeMenu)
|
||||
} else {
|
||||
document.body.removeEventListener('click', this.closeMenu)
|
||||
}
|
||||
},
|
||||
moreVisible(value) {
|
||||
if (value) {
|
||||
document.body.addEventListener('click', this.closeMore)
|
||||
} else {
|
||||
document.body.removeEventListener('click', this.closeMore)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initTags()
|
||||
this.addTags()
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.body.removeEventListener('click', this.closeMenu)
|
||||
document.body.removeEventListener('click', this.closeMore)
|
||||
},
|
||||
methods: {
|
||||
isActive(route) {
|
||||
return route.path === this.$route.path
|
||||
},
|
||||
activeStyle(tag) {
|
||||
if (!this.isActive(tag)) return {};
|
||||
return {
|
||||
"background-color": this.theme,
|
||||
"border-color": this.theme
|
||||
};
|
||||
tagClass(tag, index) {
|
||||
return [
|
||||
this.isActive(tag) ? 'active' : '',
|
||||
this.isAffix(tag) ? 'is-fixed' : '',
|
||||
this.isFixedBlockEnd(index) ? 'fixed-block-end' : ''
|
||||
]
|
||||
},
|
||||
isAffix(tag) {
|
||||
return tag.meta && tag.meta.affix
|
||||
},
|
||||
// 用户右键固定到任务栏的标签
|
||||
isPinned(tag) {
|
||||
return !!(tag.meta && tag.meta.pinned)
|
||||
},
|
||||
// 固定区最后一个标签,右侧画分隔线,与普通标签区分
|
||||
// 注意:index 来自 visibleTags,这里必须用同一份列表取值,否则会错位
|
||||
isFixedBlockEnd(index) {
|
||||
const tags = this.visibleTags
|
||||
const next = tags[index + 1]
|
||||
return !!this.isAffix(tags[index]) && (!next || !this.isAffix(next))
|
||||
},
|
||||
isFirstView() {
|
||||
try {
|
||||
return this.selectedTag.fullPath === this.visitedViews[1].fullPath || this.selectedTag.fullPath === '/index'
|
||||
} catch (err) {
|
||||
return false
|
||||
// 可视区里第一个非固定标签,它左侧只剩固定区,所以「关闭左侧」对它无意义
|
||||
const tags = this.visibleTags
|
||||
const index = tags.findIndex(tag => !this.isAffix(tag))
|
||||
if (index === -1) {
|
||||
return true
|
||||
}
|
||||
return this.selectedTag.fullPath === tags[index].fullPath || this.selectedTag.fullPath === '/index'
|
||||
},
|
||||
isLastView() {
|
||||
try {
|
||||
@ -122,12 +232,22 @@ export default {
|
||||
},
|
||||
initTags() {
|
||||
const affixTags = this.affixTags = this.filterAffixTags(this.routes)
|
||||
const affixPaths = affixTags.map(tag => tag.path)
|
||||
// 1. 路由自身声明的 affix 标签(如首页),永远排在最左侧固定区
|
||||
for (const tag of affixTags) {
|
||||
// Must have tag name
|
||||
if (tag.name) {
|
||||
this.$store.dispatch('tagsView/addVisitedView', tag)
|
||||
}
|
||||
}
|
||||
// 2. 恢复上次会话「固定到任务栏」的标签(localStorage),保证刷新/重新登录后仍在固定区
|
||||
this.$store.state.tagsView.pinnedViews
|
||||
.filter(tag => tag.name && !affixPaths.includes(tag.path))
|
||||
.forEach(tag => {
|
||||
this.$store.dispatch('tagsView/addView', Object.assign({}, tag, {
|
||||
meta: Object.assign({}, tag.meta, { affix: true, pinned: true })
|
||||
}))
|
||||
})
|
||||
},
|
||||
addTags() {
|
||||
const { name } = this.$route
|
||||
@ -195,6 +315,15 @@ export default {
|
||||
this.toLastView(visitedViews, view)
|
||||
})
|
||||
},
|
||||
// 固定到标签栏最左侧的固定区,且不可关闭、刷新后仍存在
|
||||
pinSelectedTag() {
|
||||
this.$store.dispatch('tagsView/pinView', this.selectedTag)
|
||||
this.closeMenu()
|
||||
},
|
||||
unpinSelectedTag() {
|
||||
this.$store.dispatch('tagsView/unpinView', this.selectedTag)
|
||||
this.closeMenu()
|
||||
},
|
||||
toLastView(visitedViews, view) {
|
||||
const latestView = visitedViews.slice(-1)[0]
|
||||
if (latestView) {
|
||||
@ -212,6 +341,7 @@ export default {
|
||||
},
|
||||
openMenu(tag, e) {
|
||||
const menuMinWidth = 105
|
||||
const menuHeight = 260
|
||||
const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
|
||||
const offsetWidth = this.$el.offsetWidth // container width
|
||||
const maxLeft = offsetWidth - menuMinWidth // left boundary
|
||||
@ -223,64 +353,270 @@ export default {
|
||||
this.left = left
|
||||
}
|
||||
|
||||
this.top = e.clientY
|
||||
// 标签栏固定在页面顶部,clientY 即为容器内相对纵坐标;超屏时向上收
|
||||
this.top = Math.min(e.clientY, window.innerHeight - menuHeight)
|
||||
this.visible = true
|
||||
this.selectedTag = tag
|
||||
},
|
||||
closeMenu() {
|
||||
this.visible = false
|
||||
},
|
||||
closeMore() {
|
||||
this.moreVisible = false
|
||||
},
|
||||
// 溢出消失(标签被关到上限以内)时收回「更多」面板,避免留一个指向空列表的下拉
|
||||
syncMoreVisibility() {
|
||||
if (!this.overflowTags.length) {
|
||||
this.moreVisible = false
|
||||
}
|
||||
},
|
||||
toggleMore() {
|
||||
if (this.moreVisible) {
|
||||
this.moreVisible = false
|
||||
return
|
||||
}
|
||||
// 与右键菜单互斥,避免两个浮层叠在一起
|
||||
this.closeMenu()
|
||||
this.updateMorePosition()
|
||||
this.moreVisible = true
|
||||
},
|
||||
// 面板挂在滚动容器外,位置要按按钮当前所在位置实时算(按钮会随标签横向滚动)
|
||||
updateMorePosition() {
|
||||
const btn = this.$refs.moreBtn
|
||||
if (!btn) {
|
||||
return
|
||||
}
|
||||
const containerRect = this.$el.getBoundingClientRect()
|
||||
const btnRect = btn.getBoundingClientRect()
|
||||
const left = btnRect.left - containerRect.left
|
||||
// 按钮贴到右端时面板会顶出可视区,向左收回来
|
||||
this.moreLeft = Math.max(0, Math.min(left, containerRect.width - MORE_PANEL_WIDTH))
|
||||
},
|
||||
goTag(tag) {
|
||||
this.moreVisible = false
|
||||
if (this.isActive(tag)) {
|
||||
return
|
||||
}
|
||||
this.$router.push({ path: tag.path, query: tag.query, fullPath: tag.fullPath }).catch(() => {})
|
||||
},
|
||||
handleScroll() {
|
||||
this.closeMenu()
|
||||
this.closeMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/styles/variables.scss';
|
||||
|
||||
.tags-view-container {
|
||||
height: 34px;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #d8dce5;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
|
||||
|
||||
.tags-view-wrapper {
|
||||
height: 100%;
|
||||
// 行高归零,让标签以整行高度排布,不被行框撑出多余空白
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.tags-view-item {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
vertical-align: top;
|
||||
position: relative;
|
||||
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;
|
||||
box-sizing: border-box;
|
||||
height: $top-tabs-height;
|
||||
padding: 0 14px;
|
||||
color: var(--top-tabs-fg-dim, rgba(255, 255, 255, 0.7));
|
||||
font-size: 13px;
|
||||
// 外层 line-height 归零压制行框,标签自身要恢复行高,否则标题文字被压成 0 高
|
||||
line-height: 1.4;
|
||||
max-width: 180px;
|
||||
user-select: none;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--top-tabs-hover, rgba(0, 0, 0, 0.06));
|
||||
color: var(--top-tabs-fg, #fff);
|
||||
}
|
||||
&:last-of-type {
|
||||
margin-right: 15px;
|
||||
|
||||
.tab-pin {
|
||||
flex: 0 0 auto;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 5px;
|
||||
fill: currentColor;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tab-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 固定到任务栏的标签:不铺底色,只用主题色图钉 + 白字与普通标签区分
|
||||
&.is-fixed {
|
||||
color: var(--top-tabs-fg, #fff);
|
||||
max-width: 150px;
|
||||
|
||||
.tab-pin {
|
||||
fill: var(--current-color, #409eff);
|
||||
}
|
||||
}
|
||||
|
||||
// 当前标签:白底深字,与下方白色 navbar 连成一体
|
||||
&.active {
|
||||
background-color: #42b983;
|
||||
color: #fff;
|
||||
border-color: #42b983;
|
||||
background-color: var(--top-tabs-active-bg, #fff);
|
||||
color: var(--top-tabs-active-fg, #303133);
|
||||
|
||||
.tab-pin {
|
||||
fill: var(--current-color, #409eff);
|
||||
}
|
||||
}
|
||||
|
||||
// 固定区与普通标签之间的分隔线
|
||||
&.fixed-block-end::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 0;
|
||||
width: 1px;
|
||||
height: 26px;
|
||||
background: var(--top-tabs-line, rgba(255, 255, 255, 0.12));
|
||||
}
|
||||
}
|
||||
|
||||
// 「更多」入口:超出上限的标签收在这里,按钮紧跟在最后一个可见标签右侧
|
||||
.tags-view-more {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
height: $top-tabs-height;
|
||||
padding: 0 12px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
// 外层 .tags-view-wrapper 的 line-height 归零,这里要恢复,否则文字被压成 0 高
|
||||
line-height: 1.4;
|
||||
color: var(--top-tabs-fg-dim, rgba(255, 255, 255, 0.7));
|
||||
user-select: none;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
|
||||
&:hover,
|
||||
&.is-open {
|
||||
background: var(--top-tabs-hover, rgba(0, 0, 0, 0.06));
|
||||
color: var(--top-tabs-fg, #fff);
|
||||
}
|
||||
|
||||
.more-caret {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
&.is-open .more-caret {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
// 当前页正好被收进面板:用主题色圆点提示,否则栏上完全没有"当前页"的痕迹
|
||||
&.is-active {
|
||||
color: var(--top-tabs-fg, #fff);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
background: #fff;
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
flex: 0 0 auto;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin-right: 6px;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
margin-right: 2px;
|
||||
background: var(--current-color, #409eff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 「更多」下拉面板。挂在滚动容器之外(位置由内联 style 给 left),
|
||||
// 因为 scroll-pane 的 wrap 是 overflow:hidden,放里面会被裁掉
|
||||
.more-panel {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 3001;
|
||||
box-sizing: border-box;
|
||||
width: 200px; // 与 script 里的 MORE_PANEL_WIDTH 保持一致
|
||||
max-height: 320px;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
overflow-y: auto;
|
||||
list-style-type: none;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.16);
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: 7px 12px 7px 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.more-title {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.el-icon-close {
|
||||
flex: 0 0 auto;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 8px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
|
||||
&:before {
|
||||
transform: scale(0.6);
|
||||
display: inline-block;
|
||||
vertical-align: -3px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #dcdfe6;
|
||||
}
|
||||
}
|
||||
|
||||
// 当前项在面板里高亮,便于在长列表里一眼找到
|
||||
&.is-active {
|
||||
background: var(--current-color, #409eff);
|
||||
color: #fff;
|
||||
|
||||
.el-icon-close {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contextmenu {
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
@ -291,16 +627,40 @@ export default {
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 7px 16px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
&:hover {
|
||||
background: #eee;
|
||||
}
|
||||
}
|
||||
.menu-divider {
|
||||
padding: 0;
|
||||
margin: 5px 0;
|
||||
height: 1px;
|
||||
background: #ebeef5;
|
||||
cursor: default;
|
||||
&:hover {
|
||||
background: #ebeef5;
|
||||
}
|
||||
}
|
||||
.menu-pin {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 6px;
|
||||
fill: currentColor;
|
||||
vertical-align: -1px;
|
||||
|
||||
&.is-off {
|
||||
opacity: 0.5;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -310,11 +670,13 @@ export default {
|
||||
.tags-view-wrapper {
|
||||
.tags-view-item {
|
||||
.el-icon-close {
|
||||
flex: 0 0 auto;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: 2px;
|
||||
margin-left: 4px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
color: var(--top-tabs-close-fg, #909399);
|
||||
transition: all .3s cubic-bezier(.645, .045, .355, 1);
|
||||
transform-origin: 100% 50%;
|
||||
&:before {
|
||||
@ -323,8 +685,16 @@ export default {
|
||||
vertical-align: -3px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: #b4bccc;
|
||||
color: #fff;
|
||||
background-color: var(--top-tabs-close-hover, #dcdfe6);
|
||||
}
|
||||
}
|
||||
|
||||
// 选中标签(白底深字)上的关闭按钮
|
||||
&.active .el-icon-close {
|
||||
color: var(--top-tabs-active-close-fg, #909399);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--top-tabs-active-close-hover, #dcdfe6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,20 @@
|
||||
<template>
|
||||
<div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
|
||||
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
|
||||
<!-- 顶部浏览器式标签栏:固定在页面最上方,左侧是 logo 区(与侧边栏同宽同色) -->
|
||||
<div
|
||||
v-if="needTagsView"
|
||||
class="top-tabs-bar"
|
||||
:class="sideTheme === 'theme-dark' ? 'is-dark' : 'is-light'"
|
||||
:style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }"
|
||||
>
|
||||
<logo v-if="showLogo && !sidebar.hide" class="top-tabs-logo" :collapse="!sidebar.opened" compact />
|
||||
<tags-view class="top-tabs-view" />
|
||||
</div>
|
||||
<sidebar v-if="!sidebar.hide" class="sidebar-container" />
|
||||
<div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
|
||||
<div :class="{ sidebarHide: sidebar.hide }" class="main-container">
|
||||
<div :class="{ 'fixed-header': fixedHeader }">
|
||||
<navbar />
|
||||
<tags-view v-if="needTagsView" />
|
||||
</div>
|
||||
<app-main />
|
||||
<right-panel>
|
||||
@ -18,6 +27,7 @@
|
||||
<script>
|
||||
import RightPanel from '@/components/RightPanel';
|
||||
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components';
|
||||
import Logo from './components/Sidebar/Logo';
|
||||
import ResizeMixin from './mixin/ResizeHandler';
|
||||
import { mapState } from 'vuex';
|
||||
import variables from '@/assets/styles/variables.scss';
|
||||
@ -26,6 +36,7 @@ export default {
|
||||
name: 'Layout',
|
||||
components: {
|
||||
AppMain,
|
||||
Logo,
|
||||
Navbar,
|
||||
RightPanel,
|
||||
Settings,
|
||||
@ -41,6 +52,7 @@ export default {
|
||||
device: (state) => state.app.device,
|
||||
needTagsView: (state) => state.settings.tagsView,
|
||||
fixedHeader: (state) => state.settings.fixedHeader,
|
||||
showLogo: (state) => state.settings.sidebarLogo,
|
||||
}),
|
||||
classObj() {
|
||||
return {
|
||||
@ -48,6 +60,7 @@ export default {
|
||||
openSidebar: this.sidebar.opened,
|
||||
withoutAnimation: this.sidebar.withoutAnimation,
|
||||
mobile: this.device === 'mobile',
|
||||
'has-top-tabs': this.needTagsView,
|
||||
};
|
||||
},
|
||||
variables() {
|
||||
@ -88,6 +101,74 @@ export default {
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
// 顶部浏览器式标签栏:整行固定,左侧 logo 区与侧边栏同宽
|
||||
.top-tabs-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: $top-tabs-height;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
z-index: 1002;
|
||||
// 不加投影:选中标签是白底,要与下方白色 navbar 无缝连成一体,投影会在接缝处压出一条暗线
|
||||
|
||||
// 深色侧边栏主题:深蓝底白字,选中标签白底 —— 正好与下方白色 navbar 连成一体
|
||||
&.is-dark {
|
||||
--top-tabs-fg: #ffffff;
|
||||
--top-tabs-fg-dim: rgba(255, 255, 255, 0.7);
|
||||
--top-tabs-hover: rgba(0, 0, 0, 0.06);
|
||||
--top-tabs-line: rgba(255, 255, 255, 0.12);
|
||||
--top-tabs-close-fg: rgba(255, 255, 255, 0.75);
|
||||
--top-tabs-close-hover: rgba(255, 255, 255, 0.28);
|
||||
--top-tabs-active-bg: #ffffff;
|
||||
--top-tabs-active-fg: #303133;
|
||||
--top-tabs-active-close-fg: #909399;
|
||||
--top-tabs-active-close-hover: #dcdfe6;
|
||||
}
|
||||
|
||||
// 浅色侧边栏主题:白底深字,选中标签用主题色实心块保证对比
|
||||
&.is-light {
|
||||
--top-tabs-fg: #303133;
|
||||
--top-tabs-fg-dim: rgba(0, 0, 0, 0.65);
|
||||
--top-tabs-hover: rgba(0, 0, 0, 0.05);
|
||||
--top-tabs-line: rgba(0, 0, 0, 0.08);
|
||||
--top-tabs-close-fg: #909399;
|
||||
--top-tabs-close-hover: rgba(0, 0, 0, 0.12);
|
||||
--top-tabs-active-bg: var(--current-color, #409eff);
|
||||
--top-tabs-active-fg: #ffffff;
|
||||
--top-tabs-active-close-fg: rgba(255, 255, 255, 0.85);
|
||||
--top-tabs-active-close-hover: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.top-tabs-logo {
|
||||
flex: 0 0 auto;
|
||||
width: $base-sidebar-width;
|
||||
border-right: 1px solid var(--top-tabs-line);
|
||||
transition: width 0.28s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.top-tabs-view {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// 侧边栏收起时 logo 区同步收窄(与 sidebar.scss 的 54px 保持一致)
|
||||
.hideSidebar .top-tabs-logo {
|
||||
width: 54px;
|
||||
}
|
||||
|
||||
// 移动端屏宽有限,logo 区让位给标签(此时侧边栏内会重新显示 logo)
|
||||
.mobile .top-tabs-logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.withoutAnimation .top-tabs-logo {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.fixed-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@ -105,6 +186,17 @@ export default {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// 顶部标签栏占位:主内容整体下移,吸顶头部同步下移到标签栏下方
|
||||
.has-top-tabs {
|
||||
.main-container {
|
||||
padding-top: $top-tabs-height;
|
||||
}
|
||||
|
||||
.fixed-header {
|
||||
top: $top-tabs-height;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile .fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@ -17,9 +17,9 @@ export default {
|
||||
topNav: false,
|
||||
|
||||
/**
|
||||
* 是否显示 tagsView
|
||||
* 是否显示 tagsView(顶部浏览器式标签栏,默认开启)
|
||||
*/
|
||||
tagsView: false,
|
||||
tagsView: true,
|
||||
|
||||
/**
|
||||
* 是否固定头部
|
||||
|
||||
@ -3,7 +3,19 @@ import Cookies from 'js-cookie';
|
||||
|
||||
const { sideTheme, showSettings, topNav, tagsView, fixedHeader, sidebarLogo, dynamicTitle, language } = defaultSettings;
|
||||
|
||||
// 顶部浏览器式标签栏改为默认布局:存量用户 localStorage 里缓存的 tagsView=false 只强制覆盖一次,
|
||||
// 之后用户仍可在「布局设置」抽屉里自由开关(版本号变化才会再迁移一次)
|
||||
const LAYOUT_SETTING_VERSION = '2';
|
||||
const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || '';
|
||||
|
||||
if (localStorage.getItem('layout-setting-version') !== LAYOUT_SETTING_VERSION) {
|
||||
if (storageSetting && typeof storageSetting === 'object') {
|
||||
storageSetting.tagsView = true;
|
||||
localStorage.setItem('layout-setting', JSON.stringify(storageSetting));
|
||||
}
|
||||
localStorage.setItem('layout-setting-version', LAYOUT_SETTING_VERSION);
|
||||
}
|
||||
|
||||
const state = {
|
||||
title: '',
|
||||
theme: storageSetting.theme || '#409EFF',
|
||||
|
||||
@ -1,7 +1,47 @@
|
||||
const PINNED_STORAGE_KEY = 'pinned-tags'
|
||||
|
||||
// 只持久化「重新打开标签」所需的最小字段,避免把 meta 里的临时/函数数据写进 localStorage
|
||||
function toStorableView(view) {
|
||||
return {
|
||||
path: view.path,
|
||||
fullPath: view.fullPath,
|
||||
name: view.name,
|
||||
query: view.query || {},
|
||||
params: view.params || {},
|
||||
title: view.title || (view.meta && view.meta.title) || 'no-name',
|
||||
meta: Object.assign({}, view.meta, { affix: true, pinned: true })
|
||||
}
|
||||
}
|
||||
|
||||
function loadPinnedViews() {
|
||||
try {
|
||||
const list = JSON.parse(localStorage.getItem(PINNED_STORAGE_KEY))
|
||||
return Array.isArray(list) ? list : []
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function savePinnedViews(list) {
|
||||
try {
|
||||
localStorage.setItem(PINNED_STORAGE_KEY, JSON.stringify(list))
|
||||
} catch (e) {
|
||||
// localStorage 不可用(隐私模式/超配额)时降级为仅当前会话有效
|
||||
}
|
||||
}
|
||||
|
||||
// 固定区 = 排在普通标签之前的 affix 标签;新固定/取消固定的标签插到固定区末尾
|
||||
function moveToAffixBlockEnd(list, view) {
|
||||
const affixCount = list.filter(v => v.meta && v.meta.affix).length
|
||||
list.splice(affixCount, 0, view)
|
||||
}
|
||||
|
||||
const state = {
|
||||
visitedViews: [],
|
||||
cachedViews: [],
|
||||
iframeViews: []
|
||||
iframeViews: [],
|
||||
// 用户右键「固定标签到任务栏」的标签,刷新后仍然存在
|
||||
pinnedViews: loadPinnedViews()
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
@ -28,11 +68,10 @@ const mutations = {
|
||||
}
|
||||
},
|
||||
DEL_VISITED_VIEW: (state, view) => {
|
||||
for (const [i, v] of state.visitedViews.entries()) {
|
||||
if (v.path === view.path) {
|
||||
state.visitedViews.splice(i, 1)
|
||||
break
|
||||
}
|
||||
// 用户固定的标签不允许被其它逻辑(如 $tab.closeOpenPage)误删
|
||||
const index = state.visitedViews.findIndex(v => v.path === view.path && !(v.meta && v.meta.pinned))
|
||||
if (index > -1) {
|
||||
state.visitedViews.splice(index, 1)
|
||||
}
|
||||
state.iframeViews = state.iframeViews.filter(item => item.path !== view.path)
|
||||
},
|
||||
@ -75,6 +114,30 @@ const mutations = {
|
||||
}
|
||||
}
|
||||
},
|
||||
PIN_VIEW: (state, view) => {
|
||||
const index = state.visitedViews.findIndex(v => v.path === view.path)
|
||||
if (index === -1) return
|
||||
const target = state.visitedViews[index]
|
||||
// affix=true 复用现有关闭逻辑:不显示关闭按钮、关闭其他/全部关闭时都保留
|
||||
target.meta = Object.assign({}, target.meta, { affix: true, pinned: true })
|
||||
state.visitedViews.splice(index, 1)
|
||||
moveToAffixBlockEnd(state.visitedViews, target)
|
||||
if (!state.pinnedViews.some(v => v.path === view.path)) {
|
||||
state.pinnedViews.push(toStorableView(target))
|
||||
}
|
||||
savePinnedViews(state.pinnedViews)
|
||||
},
|
||||
UNPIN_VIEW: (state, view) => {
|
||||
state.pinnedViews = state.pinnedViews.filter(v => v.path !== view.path)
|
||||
savePinnedViews(state.pinnedViews)
|
||||
const index = state.visitedViews.findIndex(v => v.path === view.path)
|
||||
if (index === -1) return
|
||||
const target = state.visitedViews[index]
|
||||
// 取消固定后标签仍留在标签栏,只是恢复可关闭并移出固定区
|
||||
target.meta = Object.assign({}, target.meta, { affix: false, pinned: false })
|
||||
state.visitedViews.splice(index, 1)
|
||||
moveToAffixBlockEnd(state.visitedViews, target)
|
||||
},
|
||||
DEL_RIGHT_VIEWS: (state, view) => {
|
||||
const index = state.visitedViews.findIndex(v => v.path === view.path)
|
||||
if (index === -1) {
|
||||
@ -206,6 +269,14 @@ const actions = {
|
||||
updateVisitedView({ commit }, view) {
|
||||
commit('UPDATE_VISITED_VIEW', view)
|
||||
},
|
||||
// 固定标签到任务栏(标签栏最左侧固定区,刷新后仍存在)
|
||||
pinView({ commit }, view) {
|
||||
commit('PIN_VIEW', view)
|
||||
},
|
||||
// 取消固定
|
||||
unpinView({ commit }, view) {
|
||||
commit('UNPIN_VIEW', view)
|
||||
},
|
||||
delRightTags({ commit }, view) {
|
||||
return new Promise(resolve => {
|
||||
commit('DEL_RIGHT_VIEWS', view)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user