重构tagview

This commit is contained in:
zouzhibing 2022-06-07 17:54:21 +08:00
parent 14358031e7
commit c72188cc91
12 changed files with 247 additions and 180 deletions

View File

@ -8,6 +8,7 @@
"build": "vue-cli-service build"
},
"dependencies": {
"@better-scroll/core": "^2.4.2",
"clipboard": "^2.0.10",
"core-js": "^3.6.5",
"echarts": "^5.3.1",

View File

@ -3,7 +3,7 @@
<router-view v-slot="{ Component,route }">
<transition name="fade-slide" mode="out-in" appear>
<keep-alive :include="cachedViews">
<component :is="Component" :key="route.name" />
<component :is="Component" :key="route.name" v-if="isReload"/>
</keep-alive>
</transition>
</router-view>
@ -20,6 +20,10 @@
const cachedViews = computed(()=>{
return store.state.tagsView.cachedViews
})
const isReload = computed(()=>{
return store.state.app.isReload
})
</script>
<style lang="scss" scoped>

View File

@ -1,85 +0,0 @@
<template>
<el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
<slot />
</el-scrollbar>
</template>
<script lang="ts" setup>
import {computed, onMounted, ref,getCurrentInstance} from "vue";
const tagAndTagSpacing = 4 // tagAndTagSpacing
const left = ref(0)
const scrollContainer = ref()
const scrollWrapper = computed(()=>{
return scrollContainer.value.$refs.wrap
})
const handleScroll = (e)=>{
const eventDelta = e.wheelDelta || -e.deltaY * 40
const $scrollWrapper = scrollWrapper.value
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
}
const moveToTarget = (currentTag,tagLists)=>{
const $container = scrollContainer.value.$el
const $containerWidth = $container.offsetWidth
const $scrollWrapper = scrollContainer.value.$refs.wrap$
const _this = getCurrentInstance()
const tagList = tagLists
let firstTag = null
let lastTag = null
if (tagList.length > 0) {
firstTag = tagList[0]
lastTag = tagList[tagList.length - 1]
}
if (firstTag === currentTag) {
$scrollWrapper.scrollLeft = 0
} else if (lastTag === currentTag) {
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
} else {
// find preTag and nextTag
const currentIndex = tagList.findIndex(item => item === currentTag)
const prevTag = tagList[currentIndex - 1]
const nextTag = tagList[currentIndex + 1]
// the tag's offsetLeft after of nextTag
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
// the tag's offsetLeft before of prevTag
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
}
}
}
defineExpose({
moveToTarget
})
onMounted(()=>{
})
</script>
<style lang="scss" scoped>
.scroll-container {
white-space: nowrap;
position: relative;
overflow: hidden;
width: 100%;
::v-deep(.el-scrollbar__bar){
bottom: 0px;
}
::v-deep(.el-scrollbar__wrap){
height: 35px;
}
}
</style>

View File

@ -0,0 +1,58 @@
<template>
<div ref="bsWrap" class="tags-scroll-wrap">
<div ref="bsContent" class="tags-scroll" >
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted,nextTick } from 'vue';
import { useElementSize } from '@vueuse/core';
import BScroll from '@better-scroll/core';
import type { Options } from '@better-scroll/core';
interface Props {
/** better-scroll的配置: https://better-scroll.github.io/docs/zh-CN/guide/base-scroll-options.html */
options: Options;
}
const props = defineProps<Props>();
const bsWrap = ref<HTMLElement>();
const instance = ref<BScroll>();
const bsContent = ref<HTMLElement>();
function initBetterScroll() {
nextTick(()=>{
instance.value = new BScroll(bsWrap.value, props.options);
})
}
// BS
const { width: wrapWidth } = useElementSize(bsWrap);
const { width, height } = useElementSize(bsContent);
watch([() => wrapWidth.value, () => width.value, () => height.value], () => {
if (instance.value) {
instance.value.refresh();
}
});
onMounted(() => {
initBetterScroll();
});
defineExpose({ instance });
</script>
<style scoped>
.tags-scroll-wrap{
width: 100%;
}
.tags-scroll-inner{
/*display: flex;*/
}
.tags-scroll{
display: inline-block;
}
</style>

View File

@ -1,46 +1,69 @@
<template>
<div class="tags-view-container">
<scroll-pane ref="rollPane" class="tags-view-wrapper" >
<router-link
v-for="tag in visitedViews"
<div class="tags-wrap-container">
<div class="tags-view" ref="scrollContainer">
<better-scroll :options="{ scrollX: true, scrollY: false, }" ref="bsScroll">
<div class="tags-scroll-inner">
<div v-for="tag in visitedViews"
:ref="setTagRef"
:class="isActive(tag)?'active':''"
:key="tag.path"
:path= "tag.path"
:fullPath="tag.fullPath"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
tag="span"
class="tags-view-item"
>
<div class="tags-inner">
<span >{{ tag.title }}</span>
<el-icon @click.prevent.stop="closeSelectedTag(tag)"
class="tag-icon"
><circle-close-filled /></el-icon>
:key="tag.path"
class="item-tag-wrap"
:class="isActive(tag)?'active':''"
@click="routerGo(tag)">
<div class="tags-view-item" >{{ tag.title }}</div>
<el-icon @click.prevent.stop="closeSelectedTag(tag)" class="tag-icon">
<circle-close-filled /></el-icon>
</div>
</router-link>
</scroll-pane>
</div>
</better-scroll>
</div>
<el-tooltip
class="box-item"
effect="dark"
content="点击刷新"
placement="bottom-end"
>
<div class="refresh" @click="refresh">
<div class="refresh-inner">
<el-icon><Refresh />
</el-icon>
</div></div>
</el-tooltip>
</div>
</template>
<script lang="ts" setup>
import {computed, nextTick, onMounted, reactive, ref, watch} from "vue";
import ScrollPane from "./ScrollPane.vue";
import betterScroll from "./betterScroll.vue";
import { useStore } from 'vuex'
import {useRoute,useRouter} from 'vue-router'
const path = require('path')
const store = useStore()
const route = useRoute()
const router = useRouter()
const refresh = ()=>{
store.dispatch('app/setReload')
}
const routes = computed(()=>{
return store.state.permission.routes
})
const visitedViews = computed(()=>{
return store.state.tagsView.visitedViews
})
const bsScroll = ref<Expose.BetterScroll>();
let obj = new WeakMap()
let affixTags = ref([])
const tags = ref([])
// ref tag
const setTagRef = (el)=>{
if(el){
if(!obj.get(el)){
@ -48,10 +71,10 @@
}
obj.set(el,el)
}
}
const rollPane = ref()
const scrollContainer = ref()
function filterAffixTags(routes, basePath = '/') {
let tags = []
@ -76,7 +99,6 @@
}
const initTags = ()=>{
// let routesNew = routes.value.filter(item=>item.path!=='/login')
let routesNew = routes.value
let affixTag = affixTags.value = filterAffixTags(routesNew)
for (const tag of affixTag) {
@ -85,9 +107,6 @@
}
}
}
function handleScroll() {
closeMenu()
}
const isActive = (rou)=> {
return rou.path === route.path
@ -105,6 +124,7 @@
}
return false
}
function toLastView(visitedViews, view) {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
@ -126,12 +146,36 @@
})
}
const routerGo = (tag)=>{
router.push({
path: tag.path,
query: tag.query,
})
}
function handleScrollAction(currentTag,tagLists) {
const scrollContainerRect = scrollContainer.value.getBoundingClientRect()
let { left:currx, width:currentWidth } = currentTag.getBoundingClientRect()
const clientX = currx + currentWidth / 2;
const currentX = clientX - scrollContainerRect.left;
const deltaX = currentX - scrollContainerRect.width / 2;
if (bsScroll.value) {
const { maxScrollX, x: leftX } = bsScroll.value.instance;
const rightX = maxScrollX - leftX;
const update = deltaX > 0 ? Math.max(-deltaX, rightX) : Math.min(-deltaX, -leftX);
bsScroll.value?.instance.scrollBy(update, 0, 300);
}
}
function moveToCurrentTag(){
nextTick(() => {
for (const tag of tags.value) {
if (tag.to.path === route.path) {
rollPane.value.moveToTarget(tag,tags.value)
if (tag.to.fullPath !== route.fullPath) {
let path = tag.attributes.path.value
if (path === route.path) {
let fullPath = tag.attributes.fullPath.value
//
handleScrollAction(tag,tags.value)
if (fullPath !== route.fullPath) {
store.dispatch('tagsView/updateVisitedView', route)
}
break
@ -144,15 +188,23 @@
onMounted(()=>{
initTags()
addTags()
nextTick(()=>{
setTimeout(()=>{
moveToCurrentTag()
},50)
})
watch(route,()=>{
addTags()
nextTick(()=>{
setTimeout(()=>{
moveToCurrentTag()
},50)
})
})
router.beforeEach(async (to, from, next)=>{
if((from.fullPath==='/error/404'||from.fullPath==='/error/401')&&to.fullPath==="/home") {
let whiteList = ['/error/404','/error/401']
await store.dispatch('tagsView/removeView', whiteList)
@ -165,45 +217,45 @@
</script>
<style lang="scss" scoped>
.tags-view-item {
text-decoration: none;
display: inline-block;
position: relative;
cursor: pointer;
height: 26px;
line-height: 26px;
border: 1px solid #d8dce5;
color: #495060;
background: #fff;
font-size: 12px;
padding: 0 10px;
.tags-wrap-container{
display: flex;
justify-content: space-between;
align-items: center;
padding-left: 10px;
.tags-view{
background: white;
flex: 1;
box-sizing: border-box;
margin-left: 5px;
margin-top: 4px;
/*display: flex;*/
/*align-items: center;*/
&:first-of-type {
margin-left: 15px;
overflow: hidden;
}
&:last-of-type {
margin-right: 15px;
}
.tags-inner{
.refresh{
//margin-left: 20px;
cursor: pointer;
font-size: 22px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
.refresh-inner{
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 100%;
}
.tag-icon{
margin-left: 6px;
//box-shadow: 0 3px 6px -4px #0000001f, 0 6px 16px #00000014, 0 9px 28px 8px #0000000d;
}
//.tag-icon{
// //display: none;
//}
//&:hover{
// .tag-icon{
// display: block;
//
// }
//}
//padding-right: 10px;
}
.item-tag-wrap{
position: relative;
display: inline-flex;
align-items: center;
padding: 6px 10px;
font-size: 14px;
cursor: pointer;
margin-right: 10px;
border: 1px solid #d8dce5;
&.active .tag-icon{
display: block;
}
@ -211,16 +263,30 @@
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;*/
/* }*/
}
}
.item-tag-wrap:hover{
border-color: #42b983;
//color: #42b983;
}
.tags-scroll-inner{
display: flex;
flex-wrap: nowrap;
}
.tag-icon{
margin-left: 6px;
}
.tags-view-item {
position: relative;
z-index: 2;
white-space: nowrap;
.tags-inner{
display: flex;
align-items: center;
white-space: nowrap;
}
}
</style>

View File

@ -43,17 +43,17 @@ router.beforeEach(async(to, from, next) => {
} catch (error) {
next(`/login?redirect=${to.path}`)
}
NProgress.done()
}
}else{
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
next(`/login?redirect=${to.path}`)
NProgress.done()
}
NProgress.done()
}
})
router.afterEach(() => {
NProgress.done()
})

View File

@ -36,7 +36,7 @@ const excelRouter = {
path: 'upload-style-excel',
component: () => import('@/views/excel/export-style-excel.vue'),
name: 'upload-style-excel',
meta: { title: '自定义样式导出 Excel', }
meta: { title: '自定义样式导出 Excel' }
},
]
}

View File

@ -34,7 +34,7 @@ const othersRouter = {
path: 'cropper',
component: () => import('@/views/other/cropper/index.vue'),
name: 'cropper',
meta: { title: '头像裁剪' }
meta: { title: '头像裁剪' , }
},
{
path: 'grid-sorter',
@ -46,7 +46,7 @@ const othersRouter = {
path: 'splitpane',
component: () => import('@/views/other/splitpane.vue'),
name: 'splitpane',
meta: { title: '分割模块', }
meta: { title: '分割模块',}
},
{
path: 'qrcode',
@ -58,13 +58,13 @@ const othersRouter = {
path: 'right-menu',
component: () => import('@/views/other/right-menu.vue'),
name: 'right-menu',
meta: { title: '右键菜单', }
meta: { title: '右键菜单',}
},
{
path: 'count',
component: () => import('@/views/other/count.vue'),
name: 'count',
meta: { title: '数字自增长', }
meta: { title: '数字自增长',}
},
]
}

View File

@ -4,6 +4,7 @@ const state = {
isCollapse: true,
withoutAnimation:false,
device: 'desktop',
isReload:true,
}
const mutations = {
@ -21,6 +22,14 @@ const mutations = {
state.isCollapse = false
state.withoutAnimation = withoutAnimation
},
SET_RELOAD:(state) => {
state.isReload = false
setTimeout(()=>{
state.isReload = true
},50)
},
}
const actions = {
toggleDevice({ commit }, device) {
@ -29,6 +38,9 @@ const actions = {
closeSideBar({ commit }, { withoutAnimation }) {
commit('CLOSE_SIDEBAR', withoutAnimation)
},
setReload({commit}){
commit('SET_RELOAD')
}
}

View File

@ -32,9 +32,8 @@
<el-divider></el-divider>
<div style="margin-bottom: 15px"><h5>最喜欢的一句话</h5></div>
<div>---------- 开心最重要</div>
<el-divider></el-divider>
<div style="margin-bottom: 15px"><h5>如果对你有帮助的话可以麻烦点一颗 star!</h5></div>
<div style="margin-bottom: 15px"><h5>如果对你有帮助的话可以麻烦点一颗 star! 你的鼓励是我继续优化的动力~~</h5></div>
</div>
</el-card>
</el-col>

View File

@ -940,6 +940,18 @@
"@babel/helper-validator-identifier" "^7.16.7"
to-fast-properties "^2.0.0"
"@better-scroll/core@^2.4.2":
version "2.4.2"
resolved "https://registry.npmmirror.com/@better-scroll/core/-/core-2.4.2.tgz#e69470012d79923a18034c3e4931720fbb06eae5"
integrity sha512-IqVZLnh04YpaEAy9wJDxtFK/stxVQjB9A9Wcr3Uwkj7Av1TtFpin+t/TObl53diNDG5ZJ+vck/OAthphpuugLA==
dependencies:
"@better-scroll/shared-utils" "^2.4.2"
"@better-scroll/shared-utils@^2.4.2":
version "2.4.2"
resolved "https://registry.npmmirror.com/@better-scroll/shared-utils/-/shared-utils-2.4.2.tgz#1ac5c97495727093a22a8009560795dd5e3c18da"
integrity sha512-Gy/Jfbpu+hq0u+PcjkTqyXGqAf+0dexTzEZ5IDXEVwJVLmd3cx8A73oTcAZ8QZgk4wSHvlMjXecSaptkhnNPEw==
"@ctrl/tinycolor@^3.4.0":
version "3.4.0"
resolved "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.4.0.tgz#c3c5ae543c897caa9c2a68630bed355be5f9990f"