ACS
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<div class="admin-layout">
|
||||
<!-- 侧边栏 -->
|
||||
<div class="sidebar">
|
||||
<div class="logo-container">
|
||||
<h1 class="title">零零七云计算后台控制面板</h1>
|
||||
</div>
|
||||
<el-scrollbar>
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
class="sidebar-menu"
|
||||
background-color="#ffffff"
|
||||
text-color="#333333"
|
||||
active-text-color="#1890ff"
|
||||
:unique-opened="true"
|
||||
router
|
||||
>
|
||||
<sidebar-menu-item v-for="menu in menus" :key="menu.path" :menu="menu" />
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
|
||||
<!-- 主区域 -->
|
||||
<div class="main-container">
|
||||
<!-- 顶部导航 -->
|
||||
<div class="navbar">
|
||||
<div class="navbar-left">
|
||||
<breadcrumb />
|
||||
</div>
|
||||
<div class="navbar-right">
|
||||
<div class="navbar-item">
|
||||
<el-tooltip content="全屏" placement="bottom">
|
||||
<el-button type="text" class="header-btn" @click="toggleFullScreen">
|
||||
<el-icon :size="18"><full-screen /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="navbar-item">
|
||||
<el-dropdown trigger="click">
|
||||
<div class="avatar-container">
|
||||
<el-avatar :size="32" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" />
|
||||
<span class="username">{{ userStore.userInfo.user_name }}</span>
|
||||
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="router.push('/profile')">
|
||||
<el-icon><user /></el-icon>个人信息
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="router.push('/change-password')">
|
||||
<el-icon><key /></el-icon>修改密码
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided @click="handleLogout">
|
||||
<el-icon><switch-button /></el-icon>退出登录
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 标签页 -->
|
||||
<tags-view />
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="content-container">
|
||||
<el-config-provider :locale="zhCn">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<keep-alive>
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</el-config-provider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import SidebarMenuItem from './SidebarMenuItem.vue'
|
||||
import Breadcrumb from './Breadcrumb.vue'
|
||||
import TagsView from './TagsView.vue'
|
||||
import { menus as menuConfig } from '@/config/menus'
|
||||
import {
|
||||
FullScreen,
|
||||
ArrowDown,
|
||||
User,
|
||||
Key,
|
||||
SwitchButton
|
||||
} from '@element-plus/icons-vue'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import {useUserStore} from "@/store/userStore.js";
|
||||
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 侧边栏菜单数据
|
||||
const menus = ref(menuConfig)
|
||||
|
||||
// 获取当前激活的菜单项
|
||||
const activeMenu = computed(() => {
|
||||
return route.path
|
||||
})
|
||||
|
||||
// 切换全屏
|
||||
const toggleFullScreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen()
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
localStorage.removeItem('token')
|
||||
router.push('/login')
|
||||
}).catch(() => {})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 侧边栏样式 */
|
||||
.sidebar {
|
||||
width: 240px;
|
||||
height: 100%;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
color: #333;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
border-right: none;
|
||||
height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
/* 主容器样式 */
|
||||
.main-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f0f2f5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 顶部导航栏样式 */
|
||||
.navbar {
|
||||
height: 60px;
|
||||
padding: 0 15px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.navbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-item {
|
||||
padding: 0 10px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-btn {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.header-btn:hover {
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 0 12px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.avatar-container:hover {
|
||||
background-color: rgba(0, 0, 0, 0.025);
|
||||
}
|
||||
|
||||
.username {
|
||||
margin: 0 8px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
/* 内容区域样式 */
|
||||
.content-container {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
background-color: #f0f2f5;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 过渡动画 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item i) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="index" :to="item.path">
|
||||
<el-icon v-if="item.icon" class="breadcrumb-icon">
|
||||
<component :is="item.icon" />
|
||||
</el-icon>
|
||||
{{ item.title }}
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
HomeFilled,
|
||||
Setting,
|
||||
User,
|
||||
Document,
|
||||
List,
|
||||
PieChart,
|
||||
DataAnalysis,
|
||||
DataBoard
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 图标映射
|
||||
const iconMap = {
|
||||
'dashboard': 'DataBoard',
|
||||
'system': 'Setting',
|
||||
'users': 'User',
|
||||
'content': 'Document',
|
||||
'articles': 'Document',
|
||||
'categories': 'List',
|
||||
'tags': 'List',
|
||||
'statistics': 'PieChart',
|
||||
'visits': 'DataAnalysis',
|
||||
'performance': 'DataAnalysis'
|
||||
}
|
||||
|
||||
// 生成面包屑数据
|
||||
const breadcrumbs = computed(() => {
|
||||
// 当前路由的完整路径
|
||||
const currentPath = route.path
|
||||
|
||||
// 按照路径层级生成面包屑
|
||||
const pathSegments = currentPath.split('/').filter(segment => segment !== '')
|
||||
const result = []
|
||||
|
||||
// 添加首页
|
||||
result.push({
|
||||
path: '/dashboard',
|
||||
title: '首页',
|
||||
icon: 'HomeFilled'
|
||||
})
|
||||
|
||||
// 构建剩余的面包屑
|
||||
let currentPathBuilder = ''
|
||||
|
||||
for (let i = 0; i < pathSegments.length; i++) {
|
||||
const segment = pathSegments[i]
|
||||
currentPathBuilder += '/' + segment
|
||||
|
||||
// 查找匹配的路由
|
||||
const matchedRoute = router.getRoutes().find(route => route.path === currentPathBuilder)
|
||||
const segmentIcon = iconMap[segment]
|
||||
|
||||
if (matchedRoute && matchedRoute.meta.title) {
|
||||
result.push({
|
||||
path: currentPathBuilder,
|
||||
title: matchedRoute.meta.title,
|
||||
icon: segmentIcon
|
||||
})
|
||||
} else if (segment) {
|
||||
// 如果没有匹配的路由,但有路径段,也添加到面包屑
|
||||
result.push({
|
||||
path: currentPathBuilder,
|
||||
title: segment.charAt(0).toUpperCase() + segment.slice(1),
|
||||
icon: segmentIcon
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.breadcrumb-icon {
|
||||
margin-right: 6px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb__item) {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb__inner) {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb__inner a) {
|
||||
color: #606266;
|
||||
font-weight: normal;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb__inner a:hover) {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb__separator) {
|
||||
margin: 0 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<el-sub-menu v-if="hasChildren" :index="menu.path">
|
||||
<template #title>
|
||||
<el-icon v-if="menu.icon || menu.meta?.icon">
|
||||
<component :is="menu.icon || menu.meta?.icon" />
|
||||
</el-icon>
|
||||
<span>{{ menu.title || menu.meta?.title }}</span>
|
||||
</template>
|
||||
<sidebar-menu-item
|
||||
v-for="child in menu.children"
|
||||
:key="child.path"
|
||||
:menu="child"
|
||||
/>
|
||||
</el-sub-menu>
|
||||
<el-menu-item v-else :index="menu.path">
|
||||
<el-icon v-if="menu.icon || menu.meta?.icon">
|
||||
<component :is="menu.icon || menu.meta?.icon" />
|
||||
</el-icon>
|
||||
<template #title>{{ menu.title || menu.meta?.title }}</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import * as ElementPlusIcons from '@element-plus/icons-vue'
|
||||
|
||||
// 接收菜单项属性
|
||||
const props = defineProps({
|
||||
menu: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// 判断是否有子菜单
|
||||
const hasChildren = computed(() => {
|
||||
return props.menu.children && props.menu.children.length > 0
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-menu-item, :deep(.el-sub-menu__title) {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu .el-menu-item) {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
padding-left: 55px !important;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
margin-right: 10px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
/* 激活菜单项特效 */
|
||||
.el-menu-item.is-active {
|
||||
position: relative;
|
||||
background-color: #e6f7ff !important;
|
||||
color: #1890ff !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.el-menu-item.is-active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu.is-active > .el-sub-menu__title) {
|
||||
color: #1890ff !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.el-menu-item:hover, :deep(.el-sub-menu__title:hover) {
|
||||
background-color: #f5f7fa !important;
|
||||
}
|
||||
|
||||
/* 修复图标颜色 */
|
||||
.el-menu-item.is-active .el-icon, :deep(.el-sub-menu.is-active > .el-sub-menu__title .el-icon) {
|
||||
color: #1890ff !important;
|
||||
}
|
||||
|
||||
/* 修复箭头颜色 */
|
||||
:deep(.el-sub-menu.is-active > .el-sub-menu__title .el-sub-menu__icon-arrow) {
|
||||
color: #1890ff !important;
|
||||
}
|
||||
|
||||
/* 子菜单样式 */
|
||||
:deep(.el-menu--inline) {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<div class="tags-view-container">
|
||||
<div class="tags-view-wrapper">
|
||||
<div class="tags-view-scroll">
|
||||
<router-link
|
||||
v-for="tag in visitedViews"
|
||||
:key="tag.path"
|
||||
:class="isActive(tag) ? 'active-tag' : 'tag'"
|
||||
:to="{ path: tag.path, query: tag.query }"
|
||||
@contextmenu.prevent="openMenu($event, tag)"
|
||||
>
|
||||
<el-icon v-if="tag.meta.icon" class="tag-icon">
|
||||
<component :is="tag.meta.icon" />
|
||||
</el-icon>
|
||||
<span class="tag-title">{{ tag.meta.title }}</span>
|
||||
<el-icon
|
||||
class="tag-close"
|
||||
@click.prevent.stop="closeSelectedTag(tag)"
|
||||
v-if="!isAffix(tag)"
|
||||
>
|
||||
<close />
|
||||
</el-icon>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ul v-show="visible" :style="{left: left+'px', top: top+'px'}" class="contextmenu">
|
||||
<li @click="refreshSelectedTag(selectedTag)">
|
||||
<el-icon><refresh /></el-icon>
|
||||
<span>刷新页面</span>
|
||||
</li>
|
||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
|
||||
<el-icon><close /></el-icon>
|
||||
<span>关闭当前</span>
|
||||
</li>
|
||||
<li @click="closeOthersTags">
|
||||
<el-icon><circle-close /></el-icon>
|
||||
<span>关闭其他</span>
|
||||
</li>
|
||||
<li @click="closeLeftTags">
|
||||
<el-icon><back /></el-icon>
|
||||
<span>关闭左侧</span>
|
||||
</li>
|
||||
<li @click="closeRightTags">
|
||||
<el-icon><right /></el-icon>
|
||||
<span>关闭右侧</span>
|
||||
</li>
|
||||
<li @click="closeAllTags">
|
||||
<el-icon><remove /></el-icon>
|
||||
<span>关闭所有</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Close, Refresh, CircleClose, Back, Right, Remove } from '@element-plus/icons-vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// 固定标签
|
||||
const affixTags = ref([])
|
||||
// 访问过的标签
|
||||
const visitedViews = ref([])
|
||||
// 右键菜单
|
||||
const visible = ref(false)
|
||||
const top = ref(0)
|
||||
const left = ref(0)
|
||||
const selectedTag = ref({})
|
||||
|
||||
// 初始化标签
|
||||
const initTags = () => {
|
||||
// 如果当前路由不在访问过的标签中,添加它
|
||||
if (route.name) {
|
||||
addVisitedView(route)
|
||||
}
|
||||
// 添加固定标签(仪表盘)
|
||||
const dashboardRoute = router.getRoutes().find(r => r.name === 'Dashboard')
|
||||
if (dashboardRoute) {
|
||||
affixTags.value.push(dashboardRoute)
|
||||
addVisitedView(dashboardRoute)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加访问过的标签
|
||||
const addVisitedView = (view) => {
|
||||
if (visitedViews.value.some(v => v.path === view.path)) return
|
||||
|
||||
// 过滤404和登录页
|
||||
if (view.name === 'NotFound' || view.name === 'Login') return
|
||||
|
||||
visitedViews.value.push(
|
||||
Object.assign({}, view, {
|
||||
title: view.meta.title || 'unknown'
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// 刷新选中的标签
|
||||
const refreshSelectedTag = (view) => {
|
||||
// 路由刷新的原理是先获取当前路由的全部信息,然后将路由重定向到一个空白页,
|
||||
// 然后立即再将路由重定向回原路由,实现刷新效果
|
||||
const { fullPath } = view
|
||||
router.replace('/redirect' + fullPath)
|
||||
}
|
||||
|
||||
// 关闭选中的标签
|
||||
const closeSelectedTag = (view) => {
|
||||
// 从访问过的标签中移除
|
||||
const index = visitedViews.value.findIndex(v => v.path === view.path)
|
||||
if (index > -1) {
|
||||
visitedViews.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 如果关闭的是当前标签,则跳转到下一个标签
|
||||
if (isActive(view)) {
|
||||
toLastView(visitedViews.value, view)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭其他标签
|
||||
const closeOthersTags = () => {
|
||||
// 保留固定标签和当前选中的标签
|
||||
visitedViews.value = visitedViews.value.filter(v => {
|
||||
return isAffix(v) || v.path === selectedTag.value.path
|
||||
})
|
||||
|
||||
if (!isActive(selectedTag.value)) {
|
||||
router.push(selectedTag.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭左侧标签
|
||||
const closeLeftTags = () => {
|
||||
const selectedIndex = visitedViews.value.findIndex(v => v.path === selectedTag.value.path)
|
||||
if (selectedIndex === -1) return
|
||||
|
||||
// 保留固定标签和右侧标签
|
||||
visitedViews.value = visitedViews.value.filter((v, i) => {
|
||||
return isAffix(v) || i >= selectedIndex
|
||||
})
|
||||
|
||||
if (!isActive(selectedTag.value)) {
|
||||
router.push(selectedTag.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭右侧标签
|
||||
const closeRightTags = () => {
|
||||
const selectedIndex = visitedViews.value.findIndex(v => v.path === selectedTag.value.path)
|
||||
if (selectedIndex === -1) return
|
||||
|
||||
// 保留固定标签和左侧标签
|
||||
visitedViews.value = visitedViews.value.filter((v, i) => {
|
||||
return isAffix(v) || i <= selectedIndex
|
||||
})
|
||||
|
||||
if (!isActive(selectedTag.value)) {
|
||||
router.push(selectedTag.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭所有标签
|
||||
const closeAllTags = () => {
|
||||
// 仅保留固定标签
|
||||
visitedViews.value = visitedViews.value.filter(v => isAffix(v))
|
||||
|
||||
// 跳转到第一个标签或首页
|
||||
toLastView(visitedViews.value)
|
||||
}
|
||||
|
||||
// 跳转到最后一个标签或首页
|
||||
const toLastView = (visitedViews, view) => {
|
||||
const latestView = visitedViews.slice(-1)[0]
|
||||
if (latestView) {
|
||||
router.push(latestView)
|
||||
} else {
|
||||
// 如果没有标签,则跳转到首页
|
||||
if (view && view.name === 'Dashboard') {
|
||||
// 如果当前是首页,则刷新页面
|
||||
router.push('/redirect' + '/dashboard')
|
||||
} else {
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否是当前激活的标签
|
||||
const isActive = (tag) => {
|
||||
return tag.path === route.path
|
||||
}
|
||||
|
||||
// 判断是否是固定标签
|
||||
const isAffix = (tag) => {
|
||||
return affixTags.value.some(t => t.path === tag.path)
|
||||
}
|
||||
|
||||
// 打开右键菜单
|
||||
const openMenu = (e, tag) => {
|
||||
const menuMinWidth = 125
|
||||
const offsetLeft = e.clientX
|
||||
const offsetWidth = document.body.clientWidth
|
||||
const maxLeft = offsetWidth - menuMinWidth
|
||||
|
||||
left.value = offsetLeft > maxLeft ? maxLeft : offsetLeft
|
||||
top.value = e.clientY
|
||||
|
||||
visible.value = true
|
||||
selectedTag.value = tag
|
||||
}
|
||||
|
||||
// 关闭右键菜单
|
||||
const closeMenu = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
// 监听路由变化,添加标签
|
||||
watch(route, (newRoute) => {
|
||||
if (newRoute.name) {
|
||||
addVisitedView(newRoute)
|
||||
}
|
||||
})
|
||||
|
||||
// 点击其他区域关闭右键菜单
|
||||
const handleClickOutside = () => {
|
||||
closeMenu()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initTags()
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tags-view-container {
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tags-view-wrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tags-view-wrapper::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tags-view-scroll {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tag, .active-tag {
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
margin-right: 5px;
|
||||
border-radius: 2px;
|
||||
font-size: 12px;
|
||||
color: #333333;
|
||||
background-color: #f4f4f5;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.tag:hover {
|
||||
color: #1890ff;
|
||||
background-color: #e6f7ff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.active-tag {
|
||||
color: #1890ff;
|
||||
background-color: #e6f7ff;
|
||||
border-color: #1890ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tag-icon {
|
||||
margin-right: 4px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.tag-title {
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tag-close {
|
||||
margin-left: 5px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tag:hover .tag-close {
|
||||
color: #666;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.active-tag .tag-close {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.active-tag:hover .tag-close {
|
||||
background-color: rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
.contextmenu {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
background-color: #fff;
|
||||
list-style-type: none;
|
||||
padding: 6px 0;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
font-size: 12px;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.contextmenu li {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.contextmenu li:hover {
|
||||
background-color: #f5f7fa;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.contextmenu li .el-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user