效果图
2-1 创建TagsView组件
src/layout/components/TagsView/index.vue
<template>
<div class="tags-view-container">
<div class="tags-view-wrapper">
<!-- 一个个tag view就是router-link -->
<router-link
class="tags-view-item"
:class="{
active: isActive(tag)
}"
v-for="(tag, index) in visitedTags"
:key="index"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
tag="span"
>
{{ tag.meta.title }}
<span
class="el-icon-close"
@click.prevent.stop="closeSelectedTag(tag)"
></span>
</router-link>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, watch, onMounted } from 'vue'
import { useRoute, RouteRecordRaw } from 'vue-router'
import { useStore } from '@/store'
export default defineComponent({
name: 'TagsView',
setup() {
const store = useStore()
const route = useRoute()
// 从store里获取 可显示的tags view
const visitedTags = computed(() => store.state.tagsView.visitedViews)
// 添加tag
const addTags = () => {
const { name } = route
if (name) {
store.dispatch('tagsView/addView', route)
}
}
// 路径发生变化追加tags view
watch(() => route.path, () => {
addTags()
})
// 最近当前router到tags view
onMounted(() => {
addTags()
})
// 是否是当前应该激活的tag
const isActive = (tag: RouteRecordRaw) => {
return tag.path === route.path
}
// 关闭当前右键的tag路由
const closeSelectedTag = (view: RouteRecordRaw) => {
store.dispatch('tagsView/delView', view)
}
return {
visitedTags,
isActive,
closeSelectedTag
}
}
})
</script>
<style lang="scss" scoped>
.tags-view-container {
width: 100%;
height: 34px;
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 {
.tags-view-item {
display: inline-block;
height: 26px;
line-height: 26px;
border: 1px solid #d8dce5;
background: #fff;
color: #495060;
padding: 0 8px;
box-sizing: border-box;
font-size: 12px;
margin-left: 5px;
margin-top: 4px;
&:first-of-type {
margin-left: 15px;
}
&:last-of-type {
margin-right: 15px;
}
&.active {
background-color: #42b983;
color: #fff;
border-color: #42b983;
&::before {
position: relative;
display: inline-block;
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 5px;
background: #fff;
}
}
}
}
}
</style>
<style lang="scss">
.tags-view-container {
.el-icon-close {
width: 16px;
height: 16px;
position: relative;
left: 2px;
border-radius: 50%;
text-align: center;
transition: all .3s cubic-bezier(.645, .045, .355, 1);
transform-origin: 100% 50%;
&:before {
transform: scale(.6);
display: inline-block;
vertical-align: -1px;
}
&:hover {
background-color: #b4bccc;
color: #fff;
}
}
}
</style>
2-2 定义store
定义tagsView module
src/store/modules/tagsView.ts
import { Module, ActionTree, MutationTree } from 'vuex'
import { RouteRecordRaw } from 'vue-router'
import { IRootState } from '@/store'
export interface ITagsViewState {
// 存放当前显示的tags view集合
visitedViews: RouteRecordRaw[];
}
// 定义mutations
const mutations: MutationTree<ITagsViewState> = {
// 添加可显示tags view
ADD_VISITED_VIEW(state, view) {
// 过滤去重
if (state.visitedViews.some(v => v.path === view.path)) return
// 没有title时处理
state.visitedViews.push(Object.assign({}, view, {
title: view.meta.title || 'tag-name'
}))
},
DEL_VISITED_VIEW(state, view) {
const i = state.visitedViews.indexOf(view)
if (i > -1) {
state.visitedViews.splice(i, 1)
}
}
}
// 定义actions
const actions: ActionTree<ITagsViewState, IRootState> = {
// 添加tags view
addView({ dispatch }, view: RouteRecordRaw) {
dispatch('addVisitedView', view)
},
// 添加可显示的tags view 添加前commit里需要进行去重过滤
addVisitedView({ commit }, view: RouteRecordRaw) {
commit('ADD_VISITED_VIEW', view)
},
// 删除tags view
delView({ dispatch }, view: RouteRecordRaw) {
dispatch('delVisitedView', view)
},
// 从可显示的集合中 删除tags view
delVisitedView({ commit }, view: RouteRecordRaw) {
commit('DEL_VISITED_VIEW', view)
}
}
const tagsView: Module<ITagsViewState, IRootState> = {
namespaced: true,
state: {
visitedViews: []
},
mutations,
actions
}
export default tagsView
修改store导入module
src/store/index.ts
import { InjectionKey } from 'vue'
import { createStore, Store, useStore as baseUseStore } from 'vuex'
import createPersistedState from 'vuex-persistedstate'
import app, { IAppState } from '@/store/modules/app'
import tagsView, { ITagsViewState } from '@/store/modules/tagsView'
import getters from './getters'
// 模块声明在根状态下
export interface IRootState {
app: IAppState;
tagsView: ITagsViewState;
}
// 通过下面方式使用 TypeScript 定义 store 能正确地为 store 提供类型声明。
// https://next.vuex.vuejs.org/guide/typescript-support.html#simplifying-usestore-usage
// eslint-disable-next-line symbol-description
export const key: InjectionKey<Store<IRootState>> = Symbol()
// 对于getters在组件使用时没有类型提示
// 有人提交了pr #1896 为getters创建泛型 应该还未发布
// https://github.com/vuejs/vuex/pull/1896
// 代码pr内容详情
// https://github.com/vuejs/vuex/pull/1896/files#diff-093ad82a25aee498b11febf1cdcb6546e4d223ffcb49ed69cc275ac27ce0ccce
// vuex store持久化 默认使用localstorage持久化
const persisteAppState = createPersistedState({
storage: window.sessionStorage, // 指定storage 也可自定义
key: 'vuex_app', // 存储名 默认都是vuex 多个模块需要指定 否则会覆盖
// paths: ['app'] // 针对app这个模块持久化
// 只针对app模块下sidebar.opened状态持久化
paths: ['app.sidebar.opened', 'app.size'] // 通过点连接符指定state路径
})
export default createStore<IRootState>({
plugins: [
persisteAppState
],
getters,
modules: {
app,
tagsView
}
})
// 定义自己的 `useStore` 组合式函数
// https://next.vuex.vuejs.org/zh/guide/typescript-support.html#%E7%AE%80%E5%8C%96-usestore-%E7%94%A8%E6%B3%95
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function useStore () {
return baseUseStore(key)
}
// vuex持久化 vuex-persistedstate文档说明
// https://www.npmjs.com/package/vuex-persistedstate
修改navbar 样式
<template>
<div class="navbar">
<hambuger @toggleClick="toggleSidebar" :is-active="sidebar.opened"/>
<breadcrumb />
<div class="right-menu">
<!-- 全屏 -->
<screenfull id="screefull" class="right-menu-item hover-effect" />
<!-- element组件size切换 -->
<el-tooltip content="Global Size" effect="dark" placement="bottom">
<size-select class="right-menu-item hover-effect" />
</el-tooltip>
<!-- 用户头像 -->
<avatar />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue'
import Breadcrumb from '@/components/Breadcrumb/index.vue'
import Hambuger from '@/components/Hambuger/index.vue'
import { useStore } from '@/store/index'
import Screenfull from '@/components/Screenfull/index.vue'
import SizeSelect from '@/components/SizeSelect/index.vue'
import Avatar from './avatar/index.vue'
export default defineComponent({
name: 'Navbar',
components: {
Breadcrumb,
Hambuger,
Screenfull,
SizeSelect,
Avatar
},
setup() {
// 使用我们自定义的useStore 具备类型提示
// store.state.app.sidebar 对于getters里的属性没有类型提示
const store = useStore()
const toggleSidebar = () => {
store.dispatch('app/toggleSidebar')
}
// 从getters中获取sidebar
const sidebar = computed(() => store.getters.sidebar)
return {
toggleSidebar,
sidebar
}
}
})
</script>
<style lang="scss">
.navbar {
display: flex;
background: #fff;
border-bottom: 1px solid rgba(0, 21, 41, .08);
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
.right-menu {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 15px;
&-item {
padding: 0 8px;
font-size: 18px;
color: #5a5e66;
vertical-align: text-bottom;
&.hover-effect {
cursor: pointer;
transition: background .3s;
&:hover {
background: rgba(0, 0, 0, .025);
}
}
}
}
}
</style>
2-3 导入到layout组件
src/layout/index.vue
<template>
<div class="app-wrapper">
<div class="sidebar-container">
<Sidebar />
</div>
<div class="main-container">
<div class="header">
<navbar />
<tags-view />
</div>
<!-- AppMain router-view -->
<app-main />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Sidebar from './components/Sidebar/index.vue'
import AppMain from './components/AppMain.vue'
import Navbar from './components/Navbar.vue'
import TagsView from './components/TagsView/index.vue'
export default defineComponent({
components: {
Sidebar,
AppMain,
Navbar,
TagsView
}
})
</script>
<style lang="scss" scoped>
.app-wrapper {
display: flex;
width: 100%;
height: 100%;
.main-container {
flex: 1;
display: flex;
flex-direction: column;
.app-main {
/* 50= navbar 50 如果有tagsview + 34 */
min-height: calc(100vh - 84px);
}
}
}
</style>
本节参考源码
https://gitee.com/brolly/vue3-element-admin/commit/7bd90896ab32f9295d3f1864fba75952a47d1435