1.TagsView.vue
1 <template> 2 <div class="tags-view-container"> 3 <scroll-pane class="tags-view-wrapper" ref="scrollPane"> 4 <router-link ref="tag" class="isActive(tag)?'active':''" :to="tag" @contextmenu.prevent.native="openMenu(tag,$event)" v-for="tag in Array.from(visitedViews)" :key="tag.path" > 5 {{tag.title}} 6 <span class="el-icon-close" @click.prevent.stop='closeSelectedTag(tag)'></span> 7 </router-link> 8 </scroll-pane> 9 <ul class='contextmenu' v-show="visible" :style="{left:left+'px',top:top+'px'}"> 10 <li @click="closeSelectedTag(selectedTag)">关闭</li> 11 <li @click="closeOthersTags">关闭其他</li> 12 <li @click="closeAllTags">关闭所有</li> 13 </ul> 14 </div> 15 </template> 16 17 <script> 18 import ScrollPane from '@/components/ScrollPane' 19 export default { 20 name: "tags-view", 21 components: { ScrollPane }, 22 data(){ 23 return{ 24 visible: false, 25 top: 0, 26 left: 0, 27 selectedTag: {} 28 } 29 }, 30 computed:{ 31 visitedViews(){ 32 console.log('tabView') 33 console.log(this.$store.state.tagsView) 34 console.log(this.$store.state) 35 return this.$store.state.tagsView.visitedViews 36 } 37 }, 38 watch:{ 39 $route(){ 40 this.addViewTags() 41 this.moveToCurrentTag() 42 }, 43 visible(value) { 44 if (value) { 45 document.body.addEventListener('click', this.closeMenu) 46 } else { 47 document.body.removeEventListener('click', this.closeMenu) 48 } 49 } 50 }, 51 mounted() { 52 this.addViewTags() 53 }, 54 methods:{ 55 generateRoute(){ 56 if (this.$route.name) { 57 return this.$route 58 } 59 return false 60 }, 61 isActive(route) { 62 return route.path === this.$route.path 63 }, 64 addViewTags() { 65 const route = this.generateRoute() 66 if (!route) { 67 return false 68 } 69 this.$store.dispatch('addVisitedViews', route) 70 }, 71 moveToCurrentTag() { 72 const tags = this.$refs.tag 73 this.$nextTick(() => { 74 for (const tag of tags) { 75 if (tag.to.path === this.$route.path) { 76 this.$refs.scrollPane.moveToTarget(tag.$el) 77 break 78 } 79 } 80 }) 81 }, 82 closeSelectedTag(view) { 83 this.$store.dispatch('delVisitedViews', view).then((views) => { 84 if (this.isActive(view)) { 85 const latestView = views.slice(-1)[0] 86 if (latestView) { 87 this.$router.push(latestView) 88 } else { 89 this.$router.push('/') 90 } 91 } 92 }) 93 }, 94 closeOthersTags() { 95 this.$router.push(this.selectedTag) 96 this.$store.dispatch('delOthersViews', this.selectedTag).then(() => { 97 this.moveToCurrentTag() 98 }) 99 }, 100 closeAllTags() { 101 this.$store.dispatch('delAllViews') 102 this.$router.push('/') 103 }, 104 openMenu(tag, e) { 105 this.visible = true 106 this.selectedTag = tag 107 const offsetLeft = this.$el.getBoundingClientRect().left // container margin left 108 this.left = e.clientX - offsetLeft + 15 // 15: margin right 109 this.top = e.clientY 110 }, 111 closeMenu() { 112 this.visible = false 113 } 114 } 115 } 116 </script> 117 118 <style rel="stylesheet/scss" lang="scss" scoped> 119 .tags-view-container { 120 .tags-view-wrapper { 121 background: #fff; 122 height: 34px; 123 border-bottom: 1px solid #d8dce5; 124 box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04); 125 .tags-view-item { 126 display: inline-block; 127 position: relative; 128 height: 26px; 129 line-height: 26px; 130 border: 1px solid #d8dce5; 131 color: #495060; 132 background: #fff; 133 padding: 0 8px; 134 font-size: 12px; 135 margin-left: 5px; 136 margin-top: 4px; 137 &:first-of-type { 138 margin-left: 15px; 139 } 140 &.active { 141 background-color: #42b983; 142 color: #fff; 143 border-color: #42b983; 144 &::before { 145 content: ''; 146 background: #fff; 147 display: inline-block; 148 8px; 149 height: 8px; 150 border-radius: 50%; 151 position: relative; 152 margin-right: 2px; 153 } 154 } 155 } 156 } 157 .contextmenu { 158 margin: 0; 159 background: #fff; 160 z-index: 100; 161 position: absolute; 162 list-style-type: none; 163 padding: 5px 0; 164 border-radius: 4px; 165 font-size: 12px; 166 font-weight: 400; 167 color: #333; 168 box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3); 169 li { 170 margin: 0; 171 padding: 7px 16px; 172 cursor: pointer; 173 &:hover { 174 background: #eee; 175 } 176 } 177 } 178 } 179 </style> 180 181 <style rel="stylesheet/scss" lang="scss"> 182 //reset element css of el-icon-close 183 .tags-view-wrapper { 184 .tags-view-item { 185 .el-icon-close { 186 16px; 187 height: 16px; 188 vertical-align: 2px; 189 border-radius: 50%; 190 text-align: center; 191 transition: all .3s cubic-bezier(.645, .045, .355, 1); 192 transform-origin: 100% 50%; 193 &:before { 194 transform: scale(.6); 195 display: inline-block; 196 vertical-align: -3px; 197 } 198 &:hover { 199 background-color: #b4bccc; 200 color: #fff; 201 } 202 } 203 } 204 } 205 </style>
2.ScrollPane.vue:是当tagView内容超出一行时候的滚动(将鼠标悬浮在那一行上,不出现滚动条,该行就可以滚动)
1 <template> 2 <div class="scroll-container" ref="scrollContainer" @wheel.prevent="handleScroll"> 3 <div class="scroll-wrapper" ref="scrollWrapper" :style="{left: left + 'px'}"> 4 <slot></slot> 5 </div> 6 </div> 7 </template> 8 9 <script> 10 const padding = 15 // tag's padding 11 12 export default { 13 name: 'scrollPane', 14 data() { 15 return { 16 left: 0 17 } 18 }, 19 methods: { 20 handleScroll(e) { 21 const eventDelta = e.wheelDelta || -e.deltaY * 3//wheelDelta:-120;deltaY:-120 22 const $container = this.$refs.scrollContainer//外面的container 23 const $containerWidth = $container.offsetWidth//外面的container的宽度 24 const $wrapper = this.$refs.scrollWrapper//里面 25 const $wrapperWidth = $wrapper.offsetWidth//里面的宽度 26 27 if (eventDelta > 0) { 28 this.left = Math.min(0, this.left + eventDelta)//min() 方法可返回指定的数字中带有最低值的数字。 29 } else { 30 if ($containerWidth - padding < $wrapperWidth) { 31 if (this.left < -($wrapperWidth - $containerWidth + padding)) { 32 this.left = this.left 33 } else { 34 this.left = Math.max(this.left + eventDelta, $containerWidth - $wrapperWidth - padding) 35 } 36 } else { 37 this.left = 0 38 } 39 } 40 }, 41 moveToTarget($target) { 42 const $container = this.$refs.scrollContainer 43 const $containerWidth = $container.offsetWidth 44 const $targetLeft = $target.offsetLeft 45 const $targetWidth = $target.offsetWidth 46 47 if ($targetLeft < -this.left) { 48 // tag in the left 49 this.left = -$targetLeft + padding 50 } else if ($targetLeft + padding > -this.left && $targetLeft + $targetWidth < -this.left + $containerWidth - padding) { 51 // tag in the current view 52 // eslint-disable-line 53 } else { 54 // tag in the right 55 this.left = -($targetLeft - ($containerWidth - $targetWidth) + padding) 56 } 57 } 58 } 59 } 60 </script> 61 62 <style rel="stylesheet/scss" lang="scss" scoped> 63 .scroll-container { 64 white-space: nowrap; 65 position: relative; 66 overflow: hidden; 67 100%; 68 .scroll-wrapper { 69 position: absolute; 70 } 71 } 72 </style>
3.Store/tagsView.js
1 const tagsView = { 2 state: { 3 visitedViews: [], 4 cachedViews: [] 5 }, 6 mutations: { 7 ADD_VISITED_VIEWS: (state, view) => { 8 if (state.visitedViews.some(v => v.path === view.path)) return 9 state.visitedViews.push(Object.assign({}, view, { 10 title: view.meta.title || 'no-name' 11 })) 12 if (!view.meta.noCache) { 13 state.cachedViews.push(view.name) 14 } 15 }, 16 DEL_VISITED_VIEWS: (state, view) => { 17 for (const [i, v] of state.visitedViews.entries()) { 18 if (v.path === view.path) { 19 state.visitedViews.splice(i, 1) 20 break 21 } 22 } 23 for (const i of state.cachedViews) { 24 if (i === view.name) { 25 const index = state.cachedViews.indexOf(i) 26 state.cachedViews.splice(index, 1) 27 break 28 } 29 } 30 }, 31 DEL_OTHERS_VIEWS: (state, view) => { 32 for (const [i, v] of state.visitedViews.entries()) { 33 if (v.path === view.path) { 34 state.visitedViews = state.visitedViews.slice(i, i + 1) 35 break 36 } 37 } 38 for (const i of state.cachedViews) { 39 if (i === view.name) { 40 const index = state.cachedViews.indexOf(i) 41 state.cachedViews = state.cachedViews.slice(index, i + 1) 42 break 43 } 44 } 45 }, 46 DEL_ALL_VIEWS: (state) => { 47 state.visitedViews = [] 48 state.cachedViews = [] 49 } 50 }, 51 actions: { 52 addVisitedViews({ commit }, view) { 53 commit('ADD_VISITED_VIEWS', view) 54 }, 55 delVisitedViews({ commit, state }, view) { 56 return new Promise((resolve) => { 57 commit('DEL_VISITED_VIEWS', view) 58 resolve([...state.visitedViews]) 59 }) 60 }, 61 delOthersViews({ commit, state }, view) { 62 return new Promise((resolve) => { 63 commit('DEL_OTHERS_VIEWS', view) 64 resolve([...state.visitedViews]) 65 }) 66 }, 67 delAllViews({ commit, state }) { 68 return new Promise((resolve) => { 69 commit('DEL_ALL_VIEWS') 70 resolve([...state.visitedViews]) 71 }) 72 } 73 } 74 } 75 76 export default tagsView
4.将tagsView.js引入到store/index.js中
1 import Vue from 'vue' 2 import Vuex from 'vuex' 3 import user from './modules/user' 4 import app from './modules/app' 5 import permission from './modules/permission' 6 import tagsView from './modules/tagsView' 7 8 import getters from './getters' 9 10 Vue.use(Vuex) 11 const store= new Vuex.Store({ 12 modules:{ 13 user, 14 app, 15 permission, 16 tagsView 17 18 }, 19 getters 20 21 }) 22 23 export default store
总体功能分析:
1.选择左侧菜单的时候,通过监听路由变化,将左侧菜单的选中项缓存到store中,并显示在tagsView中
2.有右侧菜单事件:textmenu
3.点击名字时候,跳转到相应页面
4.点击关闭时候,关闭当前选项卡