菜单导航优化

This commit is contained in:
bb_pan 2026-01-20 17:40:38 +08:00
parent adad544004
commit c2f41e979b
16 changed files with 1850 additions and 246 deletions

View File

@ -1,5 +1,5 @@
# 页面标题
VUE_APP_TITLE = 机械化施工装备管理平台
VUE_APP_TITLE = 机械化施工装备管理(共享)平台
# 开发环境配置
ENV = 'development'

BIN
src/assets/images/down.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

BIN
src/assets/images/star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

BIN
src/assets/images/up.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 B

View File

@ -40,7 +40,8 @@ $base-sub-menu-background:#000c17;
$base-sub-menu-hover:#001528;
*/
$base-sidebar-width: 230px;
// $base-sidebar-width: 230px;
$base-sidebar-width: 0px;
// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass

View File

@ -0,0 +1,198 @@
<template>
<div :class="{'show':show}" class="header-search">
<svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
<el-select
ref="headerSearchSelect"
v-model="search"
:remote-method="querySearch"
filterable
default-first-option
remote
placeholder="Search"
class="header-search-select"
@change="change"
>
<el-option v-for="option in options" :key="option.item.path" :value="option.item" :label="option.item.title.join(' > ')" />
</el-select>
</div>
</template>
<script>
// fuse is a lightweight fuzzy-search module
// make search results more in line with expectations
import Fuse from 'fuse.js/dist/fuse.min.js'
import path from 'path'
export default {
name: 'HeaderSearch',
data() {
return {
search: '',
options: [],
searchPool: [],
show: false,
fuse: undefined
}
},
computed: {
routes() {
return this.$store.getters.permission_routes
}
},
watch: {
routes() {
this.searchPool = this.generateRoutes(this.routes)
},
searchPool(list) {
this.initFuse(list)
},
show(value) {
if (value) {
document.body.addEventListener('click', this.close)
} else {
document.body.removeEventListener('click', this.close)
}
}
},
mounted() {
this.searchPool = this.generateRoutes(this.routes)
},
methods: {
click() {
this.show = !this.show
if (this.show) {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus()
}
},
close() {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur()
this.options = []
this.show = false
},
change(val) {
const path = val.path;
const query = val.query;
if(this.ishttp(val.path)) {
// http(s)://
const pindex = path.indexOf("http");
window.open(path.substr(pindex, path.length), "_blank");
} else {
if (query) {
this.$router.push({ path: path, query: JSON.parse(query) });
} else {
this.$router.push(path)
}
}
this.search = ''
this.options = []
this.$nextTick(() => {
this.show = false
})
},
initFuse(list) {
this.fuse = new Fuse(list, {
shouldSort: true,
threshold: 0.4,
location: 0,
distance: 100,
minMatchCharLength: 1,
keys: [{
name: 'title',
weight: 0.7
}, {
name: 'path',
weight: 0.3
}]
})
},
// Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title
generateRoutes(routes, basePath = '/', prefixTitle = []) {
let res = []
for (const router of routes) {
// skip hidden router
if (router.hidden) { continue }
const data = {
path: !this.ishttp(router.path) ? path.resolve(basePath, router.path) : router.path,
title: [...prefixTitle]
}
if (router.meta && router.meta.title) {
data.title = [...data.title, router.meta.title]
if (router.redirect !== 'noRedirect') {
// only push the routes with title
// special case: need to exclude parent router without redirect
res.push(data)
}
}
if (router.query) {
data.query = router.query
}
// recursive child routes
if (router.children) {
const tempRoutes = this.generateRoutes(router.children, data.path, data.title)
if (tempRoutes.length >= 1) {
res = [...res, ...tempRoutes]
}
}
}
return res
},
querySearch(query) {
if (query !== '') {
this.options = this.fuse.search(query)
} else {
this.options = []
}
},
ishttp(url) {
return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1
}
}
}
</script>
<style lang="scss" scoped>
.header-search {
font-size: 0 !important;
.search-icon {
cursor: pointer;
font-size: 18px;
vertical-align: middle;
}
.header-search-select {
font-size: 18px;
transition: width 0.2s;
width: 0;
overflow: hidden;
background: transparent;
border-radius: 0;
display: inline-block;
vertical-align: middle;
::v-deep .el-input__inner {
border-radius: 0;
border: 0;
padding-left: 0;
padding-right: 0;
box-shadow: none !important;
border-bottom: 1px solid #d9d9d9;
vertical-align: middle;
}
}
&.show {
.header-search-select {
width: 210px;
margin-left: 10px;
}
}
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<div :class="{'show':show}" class="header-search">
<svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
<!-- <svg-icon class-name="search-icon" icon-class="search" @click.stop="click" /> -->
<el-select
ref="headerSearchSelect"
v-model="search"
@ -8,10 +8,13 @@
filterable
default-first-option
remote
placeholder="Search"
placeholder="全局搜索"
class="header-search-select"
@change="change"
>
<template #prefix>
<img src="@/assets/images/search.png" style="width: 18px; height: 18px; margin-top: 17px" />
</template>
<el-option v-for="option in options" :key="option.item.path" :value="option.item" :label="option.item.title.join(' > ')" />
</el-select>
</div>
@ -168,23 +171,33 @@ export default {
}
.header-search-select {
font-size: 18px;
transition: width 0.2s;
width: 0;
overflow: hidden;
background: transparent;
border-radius: 0;
display: inline-block;
vertical-align: middle;
// font-size: 18px;
// transition: width 0.2s;
// width: 0;
// overflow: hidden;
// background: transparent;
// border-radius: 0;
// display: inline-block;
// vertical-align: middle;
::v-deep .el-input__inner {
border-radius: 0;
border: 0;
padding-left: 0;
padding-right: 0;
box-shadow: none !important;
border-bottom: 1px solid #d9d9d9;
vertical-align: middle;
// border-radius: 0;
// border: 0;
// padding-left: 0;
// padding-right: 0;
// box-shadow: none !important;
// border-bottom: 1px solid #d9d9d9;
// vertical-align: middle;
border: none;
background: rgba(255,255,255,0.2);
border-radius: 4px 4px 4px 4px;
font-family: Microsoft YaHei, Microsoft YaHei;
font-weight: 400;
font-size: 12px;
color: #EBEBEB;
text-align: left;
font-style: normal;
text-transform: none;
}
}

View File

@ -0,0 +1,200 @@
<template>
<div class="navbar">
<hamburger
id="hamburger-container"
:is-active="sidebar.opened"
class="hamburger-container"
@toggleClick="toggleSideBar"
/>
<breadcrumb
id="breadcrumb-container"
class="breadcrumb-container"
v-if="!topNav"
/>
<top-nav id="topmenu-container" class="topmenu-container" v-if="topNav" />
<div class="right-menu">
<template v-if="device !== 'mobile'">
<search id="header-search" class="right-menu-item" />
<screenfull id="screenfull" class="right-menu-item hover-effect" />
<el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip>
</template>
<el-dropdown
class="avatar-container right-menu-item hover-effect"
trigger="click"
>
<img :src="avatar" class="user-avatar" />
<el-dropdown-menu slot="dropdown">
<router-link to="/user/profile">
<el-dropdown-item>个人中心</el-dropdown-item>
</router-link>
<el-dropdown-item @click.native="setting = true">
<span>布局设置</span>
</el-dropdown-item>
<el-dropdown-item divided @click.native="logout">
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
import Breadcrumb from "@/components/Breadcrumb";
import TopNav from "@/components/TopNav";
import Hamburger from "@/components/Hamburger";
import Screenfull from "@/components/Screenfull";
import SizeSelect from "@/components/SizeSelect";
import Search from "@/components/HeaderSearch";
import bonusGit from "@/components/bonus/Git";
import bonusDoc from "@/components/bonus/Doc";
export default {
components: {
Breadcrumb,
TopNav,
Hamburger,
Screenfull,
SizeSelect,
Search,
bonusGit,
bonusDoc,
},
computed: {
...mapGetters(["sidebar", "avatar", "device"]),
setting: {
get() {
return this.$store.state.settings.showSettings;
},
set(val) {
this.$store.dispatch("settings/changeSetting", {
key: "showSettings",
value: val,
});
},
},
topNav: {
get() {
return this.$store.state.settings.topNav;
},
},
},
methods: {
toggleSideBar() {
this.$store.dispatch("app/toggleSideBar");
},
async logout() {
this.$confirm("确定注销并退出系统吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.$store.dispatch("LogOut").then((res) => {
console.log('🚀 ~ res-退出登录:', res)
console.log("logout", process.env.VUE_APP_BASE_API)
this.$store.dispatch('tagsView/delAllViews')
if (process.env.VUE_APP_BASE_API == '/iws/jxhzb-api') {
window.location.href = 'http://sgwpdm.ah.sgcc.com.cn/iws'
} else {
this.$router.push({ path: "/login" })
}
});
})
.catch((err) => {
console.log('🚀 ~ err-退出登录:', err)
});
},
},
};
</script>
<style lang="scss" scoped>
.navbar {
height: 50px;
overflow: hidden;
position: relative;
// background: #fff;
background-image: url('../../assets/images/titleGif.gif');
background-size: cover;
background-repeat: no-repeat;
background-position: center;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
.hamburger-container {
line-height: 46px;
height: 100%;
float: left;
cursor: pointer;
transition: background 0.3s;
-webkit-tap-highlight-color: transparent;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
.breadcrumb-container {
float: left;
}
.topmenu-container {
position: absolute;
left: 50px;
}
.errLog-container {
display: inline-block;
vertical-align: top;
}
.right-menu {
float: right;
height: 100%;
line-height: 50px;
&:focus {
outline: none;
}
.right-menu-item {
display: inline-block;
padding: 0 8px;
height: 100%;
font-size: 18px;
color: #bfbfbf; /* 纯黑色,比#262626更黑 */
vertical-align: text-bottom;
&.hover-effect {
cursor: pointer;
transition: background 0.3s;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
}
.avatar-container {
margin-right: 30px;
.user-avatar {
cursor: pointer;
width: 18px;
height: 18px;
vertical-align: text-bottom;
color:#262626;
/* 移除了opacity属性使用color属性实现颜色调整 */
}
}
}
}
</style>

View File

@ -1,61 +1,153 @@
<template>
<div class="navbar">
<hamburger
id="hamburger-container"
:is-active="sidebar.opened"
class="hamburger-container"
@toggleClick="toggleSideBar"
/>
<div>
<div class="navbar">
<div class="app-title">
<span>{{ title }}</span>
</div>
<breadcrumb
id="breadcrumb-container"
class="breadcrumb-container"
v-if="!topNav"
/>
<top-nav id="topmenu-container" class="topmenu-container" v-if="topNav" />
<div class="menus" @mouseleave="handleMenuLeave">
<div
v-for="(item, index) in menuList"
:key="index"
class="menu-item"
@mouseenter="handleMenuEnter(item)"
@click="handleMenuClick(item)"
>
<div class="menu-btn">
<span v-if="item.redirect == 'index' || item.redirect == 'index1'">
{{ item.children[0].meta.title }}
</span>
<span v-else>{{ item.meta.title }}</span>
<div class="tab-item-btn"></div>
</div>
</div>
</div>
<div class="right-menu">
<template v-if="device !== 'mobile'">
<search id="header-search" class="right-menu-item" />
<div class="right-menu">
<template v-if="device !== 'mobile'">
<search id="header-search" class="right-menu-item" />
<screenfull id="screenfull" class="right-menu-item hover-effect" />
<!-- <screenfull id="screenfull" class="right-menu-item hover-effect" />
<el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip> -->
</template>
<el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip>
</template>
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
<div>
<img :src="avatar" class="user-avatar" />
<span class="nick-name">{{ user.nickName }}</span>
<i class="el-icon-arrow-down" style="color: #fff"></i>
</div>
<el-dropdown-menu slot="dropdown">
<router-link to="/user/profile">
<el-dropdown-item>个人中心</el-dropdown-item>
</router-link>
<el-dropdown-item @click.native="setting = true">
<span>布局设置</span>
</el-dropdown-item>
<el-dropdown-item divided @click.native="logout">
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
<el-dropdown
class="avatar-container right-menu-item hover-effect"
trigger="click"
>
<img :src="avatar" class="user-avatar" />
<el-dropdown-menu slot="dropdown">
<router-link to="/user/profile">
<el-dropdown-item>个人中心</el-dropdown-item>
</router-link>
<el-dropdown-item @click.native="setting = true">
<span>布局设置</span>
</el-dropdown-item>
<el-dropdown-item divided @click.native="logout">
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<div class="menu-box" v-show="showMenuBox" @mouseenter="handleBoxEnter" @mouseleave="handleBoxLeave">
<div class="menu-list">
<div class="list-box">
<!-- 顶部标题 -->
<div class="menu-title-top" v-if="currentMenu && currentMenu.meta">
<div class="title-tip"></div>
<span style="font-weight: bold; font-size: 16px">{{ currentMenu.meta.title }}</span>
</div>
<!-- 一级 -->
<div v-for="(child, i) in currentMenu.children || []" :key="i" class="sub-menu-item" v-if="!child.hidden">
<!-- 一级有子级 -->
<div v-if="child.children && child.children.length">
<!-- 一级父节点点击展开 -->
<div class="menu-title" @click="toggleLevel1(child)">
<div class="children-title-tip"></div>
<span>
<span style="margin-right: 5px">{{ child.meta.title }}</span>
<img
v-if="openLevel1 == child.path"
src="@/assets/images/up.png"
style="width: 14px; height: 14px"
alt=""
/>
<img v-else src="@/assets/images/down.png" style="width: 14px; height: 14px" alt="" />
</span>
<span class="menu-star">
<img src="@/assets/images/star.png" style="width: 10px; height: 9px" alt="" />
</span>
</div>
<!-- 二级区域只在当前一级展开时显示 -->
<div v-show="openLevel1 === child.path">
<!-- 二级 -->
<div v-for="(c2, j) in child.children" :key="j" class="sub-menu-item" v-if="!c2.hidden">
<!-- 二级还有子级 -->
<div v-if="c2.children && c2.children.length">
<!-- 二级父节点 -->
<div class="menu-title" @click="toggleLevel2(c2)">
{{ c2.meta.title }}
</div>
<!-- 三级只展开当前二级 -->
<div v-show="openLevel2 === c2.path">
<div
v-for="(c3, k) in c2.children"
:key="k"
class="sub-menu-item"
v-if="!c3.hidden"
@click="goRoute(c3)"
>
{{ c3.meta.title }}
</div>
</div>
</div>
<!-- 二级叶子节点 -->
<div v-else class="menu-title" @click="goRoute(c2)">
<span class="child-left">- {{ c2.meta.title }}</span>
<span class="menu-star">
<img src="@/assets/images/star.png" style="width: 10px; height: 9px" alt="" />
</span>
</div>
</div>
</div>
</div>
<!-- 一级就是叶子节点 -->
<div v-else class="menu-title" @click="goRoute(child)">
<div class="children-title-tip"></div>
<span>{{ child.meta.title }}</span>
<span class="menu-star">
<img src="@/assets/images/star.png" style="width: 10px; height: 9px" alt="" />
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
import Breadcrumb from "@/components/Breadcrumb";
import TopNav from "@/components/TopNav";
import Hamburger from "@/components/Hamburger";
import Screenfull from "@/components/Screenfull";
import SizeSelect from "@/components/SizeSelect";
import Search from "@/components/HeaderSearch";
import bonusGit from "@/components/bonus/Git";
import bonusDoc from "@/components/bonus/Doc";
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav'
import Hamburger from '@/components/Hamburger'
import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect'
import Search from '@/components/HeaderSearch'
import bonusGit from '@/components/bonus/Git'
import bonusDoc from '@/components/bonus/Doc'
import { getUserProfile } from '@/api/system/user'
export default {
components: {
@ -68,53 +160,168 @@ export default {
bonusGit,
bonusDoc,
},
data() {
return {
title: process.env.VUE_APP_TITLE,
user: {},
onlyOneChild: null,
basePath: null,
basePath2: null,
menuList: [],
showMenuBox: false,
currentMenu: {}, // hover
openLevel1: '', // path
openLevel2: '', // path
}
},
computed: {
...mapGetters(["sidebar", "avatar", "device"]),
...mapGetters(['sidebarRouters', 'sidebar', 'avatar', 'device']),
setting: {
get() {
return this.$store.state.settings.showSettings;
return this.$store.state.settings.showSettings
},
set(val) {
this.$store.dispatch("settings/changeSetting", {
key: "showSettings",
this.$store.dispatch('settings/changeSetting', {
key: 'showSettings',
value: val,
});
})
},
},
topNav: {
get() {
return this.$store.state.settings.topNav;
return this.$store.state.settings.topNav
},
},
},
created() {
this.getUser()
// sidebarRouters
this.menuList = this.sidebarRouters.filter((route) => !route.hidden)
},
mounted() {},
methods: {
getUser() {
getUserProfile().then((response) => {
this.user = response.data
})
},
toggleSideBar() {
this.$store.dispatch("app/toggleSideBar");
this.$store.dispatch('app/toggleSideBar')
},
async logout() {
this.$confirm("确定注销并退出系统吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
this.$confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
this.$store.dispatch("LogOut").then((res) => {
this.$store.dispatch('LogOut').then((res) => {
console.log('🚀 ~ res-退出登录:', res)
console.log("logout", process.env.VUE_APP_BASE_API)
console.log('logout', process.env.VUE_APP_BASE_API)
this.$store.dispatch('tagsView/delAllViews')
if (process.env.VUE_APP_BASE_API == '/iws/jxhzb-api') {
window.location.href = 'http://sgwpdm.ah.sgcc.com.cn/iws'
} else {
this.$router.push({ path: "/login" })
this.$router.push({ path: '/login' })
}
});
})
})
.catch((err) => {
console.log('🚀 ~ err-退出登录:', err)
});
})
},
handleMenuClick(item) {
console.log('🚀 ~ item.redirect:', item.redirect)
if (item.redirect == 'index' || item.redirect == 'index1') {
this.$router.push({
path: '/' + item.redirect,
})
}
},
handleMenuEnter(item) {
if (item.redirect == 'index' || item.redirect == 'index1') {
this.showMenuBox = false
return
}
this.showMenuBox = true
// fullPath
const buildPath = (nodes, parentPath = '') => {
return nodes.map((n) => {
let currentPath = n.path || ''
// /
if (!currentPath.startsWith('/')) {
currentPath = parentPath.replace(/\/$/, '') + '/' + currentPath
}
const newNode = {
...n,
fullPath: currentPath,
}
if (n.children && n.children.length) {
newNode.children = buildPath(n.children, currentPath)
}
return newNode
})
}
this.currentMenu = {
...item,
fullPath: item.path,
children: buildPath(item.children || [], item.path),
}
},
handleMenuLeave() {
this.showMenuBox = false
},
handleBoxEnter() {
this.showMenuBox = true
},
handleBoxLeave() {
this.showMenuBox = false
},
goRoute(route) {
console.log('🚀 ~ route:', route)
if (route.fullPath) {
this.$router.push({
path: route.fullPath,
query: route.query,
})
this.showMenuBox = false
}
},
//
toggleLevel1(item) {
console.log('🚀 ~ item:', item)
if (this.openLevel1 === item.path) {
//
this.openLevel1 = ''
this.openLevel2 = ''
} else {
//
this.openLevel1 = item.path
this.openLevel2 = ''
}
},
//
toggleLevel2(item) {
console.log('🚀 ~ item:', item)
if (this.openLevel2 === item.path) {
this.openLevel2 = ''
} else {
this.openLevel2 = item.path
}
},
},
};
}
</script>
<style lang="scss" scoped>
@ -122,12 +329,65 @@ export default {
height: 50px;
overflow: hidden;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
// background: #fff;
background-image: url('../../assets/images/titleGif.gif');
background-size: cover;
background-repeat: no-repeat;
background-position: center;
// background-image: url('../../assets/images/titleGif.gif');
// background-size: cover;
// background-repeat: no-repeat;
// background-position: center;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
background: linear-gradient(90deg, #00d2be 0%, #4eacff 100%);
.app-title {
width: 314px;
height: 50px;
padding-left: 14px;
font-family: Microsoft YaHei, Microsoft YaHei;
font-weight: bold;
font-size: 20px;
color: #fff;
text-align: center;
display: flex;
align-items: center;
}
.menus {
min-width: 998px;
height: 50px;
line-height: 50px;
flex: 1;
display: flex;
align-items: center;
justify-content: space-around;
margin: 0 20px;
.menu-item {
height: 50px;
cursor: pointer;
font-family: Microsoft YaHei, Microsoft YaHei;
font-weight: bold;
font-size: 16px;
color: #ffffff;
text-align: center;
font-style: normal;
text-transform: none;
.menu-btn {
width: 100%;
text-align: center;
}
//
&:hover {
.tab-item-btn {
margin: 5px auto 0;
width: 15px;
height: 2px;
margin-top: -8px;
background: #fff;
}
}
}
}
.hamburger-container {
line-height: 46px;
@ -184,17 +444,103 @@ export default {
}
.avatar-container {
margin-right: 30px;
margin-right: 16px;
.user-avatar {
cursor: pointer;
width: 18px;
height: 18px;
vertical-align: text-bottom;
color:#262626;
color: #262626;
/* 移除了opacity属性使用color属性实现颜色调整 */
}
.nick-name {
margin: 0 3px;
font-family: Microsoft YaHei, Microsoft YaHei;
font-weight: 400;
font-size: 14px;
color: #ffffff;
text-align: center;
font-style: normal;
text-transform: none;
}
}
}
}
</style>
.menu-box {
position: absolute;
z-index: 99999999;
width: 100%;
background: linear-gradient(90deg, #00d2be 0%, #4eacff 100%);
.menu-list {
padding: 16px 20px;
width: 100%;
min-height: 287px;
background: #f8fdfc;
border-radius: 16px;
font-family: Microsoft YaHei, Microsoft YaHei;
font-weight: 400;
font-size: 14px;
color: #2cbab2;
line-height: 30px;
.list-box {
padding: 16px;
width: 290px;
height: 100%;
background: linear-gradient(0deg, rgba(44, 186, 178, 0.1) 0%, rgba(44, 186, 178, 0) 100%);
border-radius: 8px;
}
}
.menu-title-top {
width: 100%;
display: flex;
align-items: center;
}
.menu-title {
width: 100%;
display: flex;
align-items: center;
cursor: pointer;
//
&:hover {
background: rgba(44, 186, 178, 0.1);
border-radius: 4px;
}
}
.menu-star {
padding-right: 5px;
flex: 1;
display: flex;
justify-content: flex-end;
align-items: center;
opacity: 0; //
transition: opacity 0.2s;
}
/* hover 当前行时,显示星标 */
.menu-title:hover .menu-star {
opacity: 1;
}
.title-tip {
margin-right: 4px;
width: 8px;
height: 16px;
background: #2cbab2;
border-radius: 4px 4px 4px 4px;
}
.children-title-tip {
margin-right: 4px;
width: 8px;
height: 8px;
background: #2cbab2;
border-radius: 50%;
}
.child-left {
// 2
margin-left: 1rem;
}
}
</style>

View File

@ -0,0 +1,362 @@
<template>
<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"
ref="tag"
:key="tag.path"
:class="isActive(tag) ? 'active' : ''"
: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 }}
<span
v-if="!isAffix(tag)"
class="el-icon-close"
@click.prevent.stop="closeSelectedTag(tag)"
/>
</router-link>
</scroll-pane>
<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="!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>
</ul>
</div>
</template>
<script>
import ScrollPane from "./ScrollPane";
import path from "path";
export default {
components: { ScrollPane },
data() {
return {
visible: false,
top: 0,
left: 0,
selectedTag: {},
affixTags: [],
};
},
computed: {
visitedViews() {
return this.$store.state.tagsView.visitedViews;
},
routes() {
return this.$store.state.permission.routes;
},
theme() {
return this.$store.state.settings.theme;
},
},
watch: {
$route() {
this.addTags();
this.moveToCurrentTag();
},
visible(value) {
if (value) {
document.body.addEventListener("click", this.closeMenu);
} else {
document.body.removeEventListener("click", this.closeMenu);
}
},
},
mounted() {
this.initTags();
this.addTags();
},
methods: {
isActive(route) {
return route.path === this.$route.path;
},
activeStyle(tag) {
if (!this.isActive(tag)) return {};
return {
"background-color": "#00ad9d",
"border-color": "#00ad9d",
};
},
isAffix(tag) {
return tag.meta && tag.meta.affix;
},
isFirstView() {
try {
return (
this.selectedTag.fullPath === "/index" ||
this.selectedTag.fullPath === this.visitedViews[1].fullPath
);
} catch (err) {
return false;
}
},
isLastView() {
try {
return (
this.selectedTag.fullPath ===
this.visitedViews[this.visitedViews.length - 1].fullPath
);
} catch (err) {
return false;
}
},
filterAffixTags(routes, basePath = "/") {
let tags = [];
routes.forEach((route) => {
if (route.meta && route.meta.affix) {
const tagPath = path.resolve(basePath, route.path);
tags.push({
fullPath: tagPath,
path: tagPath,
name: route.name,
meta: { ...route.meta },
});
}
if (route.children) {
const tempTags = this.filterAffixTags(route.children, route.path);
if (tempTags.length >= 1) {
tags = [...tags, ...tempTags];
}
}
});
return tags;
},
initTags() {
const affixTags = (this.affixTags = this.filterAffixTags(this.routes));
for (const tag of affixTags) {
// Must have tag name
if (tag.name) {
this.$store.dispatch("tagsView/addVisitedView", tag);
}
}
},
addTags() {
const { name } = this.$route;
if (name) {
this.$store.dispatch("tagsView/addView", this.$route);
if (this.$route.meta.link) {
this.$store.dispatch("tagsView/addIframeView", this.$route);
}
}
return false;
},
moveToCurrentTag() {
const tags = this.$refs.tag;
this.$nextTick(() => {
for (const tag of tags) {
if (tag.to.path === this.$route.path) {
this.$refs.scrollPane.moveToTarget(tag);
// when query is different then update
if (tag.to.fullPath !== this.$route.fullPath) {
this.$store.dispatch("tagsView/updateVisitedView", this.$route);
}
break;
}
}
});
},
refreshSelectedTag(view) {
this.$tab.refreshPage(view);
if (this.$route.meta.link) {
this.$store.dispatch("tagsView/delIframeView", this.$route);
}
},
closeSelectedTag(view) {
this.$tab.closePage(view).then(({ visitedViews }) => {
if (this.isActive(view)) {
this.toLastView(visitedViews, view);
}
});
},
closeRightTags() {
this.$tab.closeRightPage(this.selectedTag).then((visitedViews) => {
if (!visitedViews.find((i) => i.fullPath === this.$route.fullPath)) {
this.toLastView(visitedViews);
}
});
},
closeLeftTags() {
this.$tab.closeLeftPage(this.selectedTag).then((visitedViews) => {
if (!visitedViews.find((i) => i.fullPath === this.$route.fullPath)) {
this.toLastView(visitedViews);
}
});
},
closeOthersTags() {
this.$router.push(this.selectedTag.fullPath).catch(() => {});
this.$tab.closeOtherPage(this.selectedTag).then(() => {
this.moveToCurrentTag();
});
},
closeAllTags(view) {
this.$tab.closeAllPage().then(({ visitedViews }) => {
if (this.affixTags.some((tag) => tag.path === this.$route.path)) {
return;
}
this.toLastView(visitedViews, view);
});
},
toLastView(visitedViews, view) {
const latestView = visitedViews.slice(-1)[0];
if (latestView) {
this.$router.push(latestView.fullPath);
} else {
// now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs.
if (view.name === "Dashboard") {
// to reload home page
this.$router.replace({ path: "/redirect" + view.fullPath });
} else {
this.$router.push("/");
}
}
},
openMenu(tag, e) {
const menuMinWidth = 105;
const offsetLeft = this.$el.getBoundingClientRect().left; // container margin left
const offsetWidth = this.$el.offsetWidth; // container width
const maxLeft = offsetWidth - menuMinWidth; // left boundary
const left = e.clientX - offsetLeft + 15; // 15: margin right
if (left > maxLeft) {
this.left = maxLeft;
} else {
this.left = left;
}
this.top = e.clientY;
this.visible = true;
this.selectedTag = tag;
},
closeMenu() {
this.visible = false;
},
handleScroll() {
this.closeMenu();
},
},
};
</script>
<style lang="scss" scoped>
.tags-view-container {
height: 34px;
width: 100%;
background: #fff;
border-bottom: 1px solid #d8dce5;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
.tags-view-wrapper {
.tags-view-item {
display: inline-block;
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;
}
&:last-of-type {
margin-right: 15px;
}
&.active {
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;
}
}
}
}
.contextmenu {
margin: 0;
background: #fff;
z-index: 3000;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
li {
margin: 0;
padding: 7px 16px;
cursor: pointer;
&:hover {
background: #eee;
}
}
}
}
</style>
<style lang="scss">
//reset element css of el-icon-close
.tags-view-wrapper {
.tags-view-item {
.el-icon-close {
width: 16px;
height: 16px;
vertical-align: 2px;
border-radius: 50%;
text-align: center;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transform-origin: 100% 50%;
&:before {
transform: scale(0.6);
display: inline-block;
vertical-align: -3px;
}
&:hover {
background-color: #b4bccc;
color: #fff;
}
}
}
}
</style>

View File

@ -1,60 +1,60 @@
<template>
<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"
ref="tag"
:key="tag.path"
:class="isActive(tag) ? 'active' : ''"
: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 }}
<span
v-if="!isAffix(tag)"
class="el-icon-close"
@click.prevent.stop="closeSelectedTag(tag)"
/>
</router-link>
</scroll-pane>
<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="!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>
</ul>
<div class="box-card">
<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"
ref="tag"
:key="tag.path"
:class="isActive(tag) ? 'active' : ''"
: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 }} -->
<span>
<i v-if="showBackHome(tag)" class="el-icon-arrow-left back-icon" />
{{ renderTitle(tag) }}
</span>
<span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
</router-link>
</scroll-pane>
<div class="right-tag-fixed">
<div class="line" />
<i class="el-icon-arrow-down right-icon" @click.stop="toggleDropDown" />
<div class="line" />
<i class="el-icon-close right-icon" @click="handleClose" />
</div>
<transition name="ant-popover-br">
<div v-if="showDropDown" class="drop-down" ref="dropDown">
<div class="drop-item" v-for="(item, index) in dropDownViews" :key="index">
<div style="cursor: pointer" :class="{ 'active-title': isActive(item) }" @click="handleDownJump(item)">
{{ item.title }}
</div>
<i class="el-icon-close" style="cursor: pointer" @click="closeSelectedTag(item)" />
</div>
</div>
</transition>
<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="!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>
</ul>
</div>
</div>
</template>
<script>
import ScrollPane from "./ScrollPane";
import path from "path";
import ScrollPane from './ScrollPane'
import path from 'path'
export default {
components: { ScrollPane },
@ -65,229 +65,296 @@ export default {
left: 0,
selectedTag: {},
affixTags: [],
};
showDropDown: false,
}
},
computed: {
visitedViews() {
return this.$store.state.tagsView.visitedViews;
return this.$store.state.tagsView.visitedViews
},
dropDownViews() {
return this.visitedViews.filter((item) => {
return item.path !== '/index' && item.path !== '/index1'
})
},
routes() {
return this.$store.state.permission.routes;
return this.$store.state.permission.routes
},
theme() {
return this.$store.state.settings.theme;
return this.$store.state.settings.theme
},
},
watch: {
$route() {
this.addTags();
this.moveToCurrentTag();
this.addTags()
this.moveToCurrentTag()
},
visible(value) {
if (value) {
document.body.addEventListener("click", this.closeMenu);
document.body.addEventListener('click', this.closeMenu)
} else {
document.body.removeEventListener("click", this.closeMenu);
document.body.removeEventListener('click', this.closeMenu)
}
},
},
mounted() {
this.initTags();
this.addTags();
document.addEventListener('click', this.handleClickOutside)
this.initTags()
this.addTags()
},
beforeDestroy() {
document.removeEventListener('click', this.handleClickOutside)
},
methods: {
toggleDropDown() {
this.showDropDown = !this.showDropDown
},
handleClickOutside(e) {
//
if (!this.showDropDown) return
const drop = this.$refs.dropDown
//
if (!drop) return
//
if (drop.contains(e.target)) return
//
this.showDropDown = false
},
//
showBackHome(tag) {
const isHome = tag.path === '/index' && tag.name === 'Index'
const isCurrentHome = this.$route.path === '/index'
//
return isHome && !isCurrentHome
},
//
renderTitle(tag) {
if (this.showBackHome(tag)) {
return '返回首页'
}
return tag.title
},
isActive(route) {
return route.path === this.$route.path;
return route.path === this.$route.path
},
activeStyle(tag) {
if (!this.isActive(tag)) return {};
if (!this.isActive(tag)) return {}
return {
"background-color": "#00ad9d",
"border-color": "#00ad9d",
};
// 'background-color': '#00ad9d',
// 'border-color': '#00ad9d',
}
},
isAffix(tag) {
return tag.meta && tag.meta.affix;
return tag.meta && tag.meta.affix
},
isFirstView() {
try {
return (
this.selectedTag.fullPath === "/index" ||
this.selectedTag.fullPath === this.visitedViews[1].fullPath
);
console.log(
'🚀 ~ this.selectedTag.fullPath === this.visitedViews[1].fullPath:',
this.selectedTag.fullPath === this.visitedViews[1].fullPath,
)
return this.selectedTag.fullPath === '/index' || this.selectedTag.fullPath === this.visitedViews[1].fullPath
} catch (err) {
return false;
return false
}
},
isLastView() {
try {
return (
this.selectedTag.fullPath ===
this.visitedViews[this.visitedViews.length - 1].fullPath
);
return this.selectedTag.fullPath === this.visitedViews[this.visitedViews.length - 1].fullPath
} catch (err) {
return false;
return false
}
},
filterAffixTags(routes, basePath = "/") {
let tags = [];
filterAffixTags(routes, basePath = '/') {
let tags = []
routes.forEach((route) => {
if (route.meta && route.meta.affix) {
const tagPath = path.resolve(basePath, route.path);
const tagPath = path.resolve(basePath, route.path)
tags.push({
fullPath: tagPath,
path: tagPath,
name: route.name,
meta: { ...route.meta },
});
})
}
if (route.children) {
const tempTags = this.filterAffixTags(route.children, route.path);
const tempTags = this.filterAffixTags(route.children, route.path)
if (tempTags.length >= 1) {
tags = [...tags, ...tempTags];
tags = [...tags, ...tempTags]
}
}
});
return tags;
})
return tags
},
initTags() {
const affixTags = (this.affixTags = this.filterAffixTags(this.routes));
const affixTags = (this.affixTags = this.filterAffixTags(this.routes))
for (const tag of affixTags) {
// Must have tag name
if (tag.name) {
this.$store.dispatch("tagsView/addVisitedView", tag);
this.$store.dispatch('tagsView/addVisitedView', tag)
}
}
},
addTags() {
const { name } = this.$route;
const { name } = this.$route
if (name) {
this.$store.dispatch("tagsView/addView", this.$route);
this.$store.dispatch('tagsView/addView', this.$route)
if (this.$route.meta.link) {
this.$store.dispatch("tagsView/addIframeView", this.$route);
this.$store.dispatch('tagsView/addIframeView', this.$route)
}
}
return false;
return false
},
moveToCurrentTag() {
const tags = this.$refs.tag;
const tags = this.$refs.tag
this.$nextTick(() => {
for (const tag of tags) {
if (tag.to.path === this.$route.path) {
this.$refs.scrollPane.moveToTarget(tag);
this.$refs.scrollPane.moveToTarget(tag)
// when query is different then update
if (tag.to.fullPath !== this.$route.fullPath) {
this.$store.dispatch("tagsView/updateVisitedView", this.$route);
this.$store.dispatch('tagsView/updateVisitedView', this.$route)
}
break;
break
}
}
});
})
},
refreshSelectedTag(view) {
this.$tab.refreshPage(view);
this.$tab.refreshPage(view)
if (this.$route.meta.link) {
this.$store.dispatch("tagsView/delIframeView", this.$route);
this.$store.dispatch('tagsView/delIframeView', this.$route)
}
},
closeSelectedTag(view) {
this.$tab.closePage(view).then(({ visitedViews }) => {
if (this.isActive(view)) {
this.toLastView(visitedViews, view);
this.toLastView(visitedViews, view)
}
});
})
},
closeRightTags() {
this.$tab.closeRightPage(this.selectedTag).then((visitedViews) => {
if (!visitedViews.find((i) => i.fullPath === this.$route.fullPath)) {
this.toLastView(visitedViews);
this.toLastView(visitedViews)
}
});
})
},
closeLeftTags() {
this.$tab.closeLeftPage(this.selectedTag).then((visitedViews) => {
if (!visitedViews.find((i) => i.fullPath === this.$route.fullPath)) {
this.toLastView(visitedViews);
this.toLastView(visitedViews)
}
});
})
},
closeOthersTags() {
this.$router.push(this.selectedTag.fullPath).catch(() => {});
this.$router.push(this.selectedTag.fullPath).catch(() => {})
this.$tab.closeOtherPage(this.selectedTag).then(() => {
this.moveToCurrentTag();
});
this.moveToCurrentTag()
})
},
closeAllTags(view) {
this.$tab.closeAllPage().then(({ visitedViews }) => {
if (this.affixTags.some((tag) => tag.path === this.$route.path)) {
return;
return
}
this.toLastView(visitedViews, view);
});
this.toLastView(visitedViews, view)
})
},
toLastView(visitedViews, view) {
const latestView = visitedViews.slice(-1)[0];
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
this.$router.push(latestView.fullPath);
this.$router.push(latestView.fullPath)
} else {
// now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs.
if (view.name === "Dashboard") {
if (view.name === 'Dashboard') {
// to reload home page
this.$router.replace({ path: "/redirect" + view.fullPath });
this.$router.replace({ path: '/redirect' + view.fullPath })
} else {
this.$router.push("/");
this.$router.push('/')
}
}
},
openMenu(tag, e) {
const menuMinWidth = 105;
const offsetLeft = this.$el.getBoundingClientRect().left; // container margin left
const offsetWidth = this.$el.offsetWidth; // container width
const maxLeft = offsetWidth - menuMinWidth; // left boundary
const left = e.clientX - offsetLeft + 15; // 15: margin right
const menuMinWidth = 105
const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
const offsetWidth = this.$el.offsetWidth // container width
const maxLeft = offsetWidth - menuMinWidth // left boundary
const left = e.clientX - offsetLeft + 15 // 15: margin right
if (left > maxLeft) {
this.left = maxLeft;
this.left = maxLeft
} else {
this.left = left;
this.left = left
}
this.top = e.clientY;
this.visible = true;
this.selectedTag = tag;
this.top = e.clientY
this.visible = true
this.selectedTag = tag
},
closeMenu() {
this.visible = false;
this.visible = false
},
handleScroll() {
this.closeMenu();
this.closeMenu()
},
handleClose() {
//
this.$confirm('请确认是否关闭所有', '', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
this.closeAllTags(this.selectedTag)
})
.catch(() => {})
},
handleDownJump(item) {
//
this.$router.push({
path: item.path,
query: item.query,
})
this.showDropDown = false
},
},
};
}
</script>
<style lang="scss" scoped>
.box-card {
width: 100%;
height: 40px;
background: linear-gradient(90deg, #00d2be 0%, #4eacff 100%);
}
.tags-view-container {
height: 34px;
position: relative;
height: 40px;
width: 100%;
background: #fff;
border-bottom: 1px solid #d8dce5;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
border-radius: 16px 16px 0px 0px;
.tags-view-wrapper {
padding-right: 100px;
border-radius: 16px 16px 0px 0px;
.tags-view-item {
display: inline-block;
position: relative;
cursor: pointer;
height: 26px;
line-height: 26px;
border: 1px solid #d8dce5;
color: #495060;
height: 40px;
line-height: 40px;
color: #808080;
background: #fff;
padding: 0 8px;
font-size: 12px;
margin-left: 5px;
margin-top: 4px;
font-size: 14px;
&:first-of-type {
margin-left: 15px;
}
@ -295,20 +362,38 @@ export default {
margin-right: 15px;
}
&.active {
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;
}
color: #2cbab2;
}
&::after {
content: '';
background: #e6e6e6;
height: 22px;
width: 1px;
display: inline-block;
position: relative;
top: 6px;
margin-left: 8px;
}
}
}
.right-tag-fixed {
position: absolute;
right: 0;
top: 0;
width: 100px;
height: 40px;
display: flex;
align-items: center;
color: #999;
background: #fff;
border-radius: 0px 16px 0px 0px;
padding: 0 12px;
z-index: 9;
.right-icon {
font-size: 16px;
font-weight: bold;
cursor: pointer;
}
}
.contextmenu {
@ -333,6 +418,77 @@ export default {
}
}
}
/* 过渡控制 */
.ant-popover-br-enter-active,
.ant-popover-br-leave-active {
transition: opacity 0.2s cubic-bezier(0.23, 1, 0.32, 1), transform 0.2s cubic-bezier(0.23, 1, 0.32, 1);
transform-origin: right top; //
}
/* 初始进入状态(缩小 + 微微向上) */
.ant-popover-br-enter {
opacity: 0;
transform: scale(0.8) translateY(-8px);
}
/* 进入完成状态 */
.ant-popover-br-enter-to {
opacity: 1;
transform: scale(1) translateY(0);
}
/* 离开开始状态 */
.ant-popover-br-leave {
opacity: 1;
transform: scale(1) translateY(0);
}
/* 离开结束状态(吸回右上角) */
.ant-popover-br-leave-to {
opacity: 0;
transform: scale(0.8) translateY(-8px);
}
.drop-down {
position: absolute;
top: 40px;
right: 45px;
z-index: 9999999;
width: 232px;
min-height: 55px;
max-height: 700px;
overflow-y: auto;
padding: 12px 16px;
color: rgba(0, 0, 0, 0.65);
background: #fff;
background-clip: padding-box;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transform-origin: right top;
.drop-item {
padding: 5px;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
&:hover {
background: rgba(44, 186, 178, 0.1);
}
}
.active-title {
color: #2cbab2;
}
}
.line {
background: #e6e6e6;
height: 22px;
width: 1px;
display: inline-block;
margin: 0 8px;
}
</style>
<style lang="scss">
@ -348,7 +504,7 @@ export default {
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transform-origin: 100% 50%;
&:before {
transform: scale(0.6);
// transform: scale(0.6);
display: inline-block;
vertical-align: -3px;
}

328
src/layout/index-old.vue Normal file
View File

@ -0,0 +1,328 @@
<template>
<div :class="classObj" class="app-wrapper" :style="{'--current-color': theme}">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
<sidebar v-if="!sidebar.hide" class="sidebar-container"/>
<div :class="{hasTagsView:needTagsView,sidebarHide:sidebar.hide}" class="main-container">
<div :class="{'fixed-header':fixedHeader}">
<navbar/>
<tags-view v-if="needTagsView"/>
</div>
<app-main/>
<right-panel>
<settings/>
</right-panel>
</div>
<el-dialog :title="title" :visible.sync="showChangePasswordDialog" width="30%" :close-on-click-modal="false"
:show-close="false"
>
<el-form ref="form" :model="user" :rules="rules" label-width="80px">
<el-form-item label="旧密码" prop="oldPassword">
<el-input v-model="user.oldPassword" placeholder="请输入旧密码" type="password" show-password/>
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input v-model="user.newPassword" placeholder="请输入新密码" type="password" show-password/>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input v-model="user.confirmPassword" placeholder="请确认新密码" type="password" show-password/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submit"> </el-button>
<el-button @click="close"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import RightPanel from '@/components/RightPanel'
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
import ResizeMixin from './mixin/ResizeHandler'
import { mapState } from 'vuex'
import variables from '@/assets/styles/variables.scss'
import { validateNewPassword } from '@/utils/validate'
import { updateUserPwd, checkPasswordStatus } from '@/api/system/user'
import { handleNoWarningLog } from '@/api/system/log'
import {MessageBox} from "element-ui";
export default {
name: 'Layout',
data() {
const equalToPassword = (rule, value, callback) => {
if (this.user.newPassword !== value) {
callback(new Error('两次输入的密码不一致'))
} else {
callback()
}
}
return {
showChangePasswordDialog: false, //
title: '',
user: {
oldPassword: undefined,
newPassword: undefined,
confirmPassword: undefined
},
//
rules: {
oldPassword: [
{ required: true, message: '旧密码不能为空', trigger: 'blur' }
],
newPassword: [
{ required: true, message: '新密码不能为空', trigger: 'blur' },
{ validator: validateNewPassword, trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '确认密码不能为空', trigger: 'blur' },
{ required: true, validator: equalToPassword, trigger: 'blur' }
]
},
socket: null,
wsUrl: this.getWebSocketUrl(),//'ws://localhost:18082/ws', // WebSocket
isConnected: false, //
reconnectInterval: 5000 //
}
},
components: {
AppMain,
Navbar,
RightPanel,
Settings,
Sidebar,
TagsView
},
mixins: [ResizeMixin],
computed: {
...mapState({
theme: state => state.settings.theme,
sideTheme: state => state.settings.sideTheme,
sidebar: state => state.app.sidebar,
device: state => state.app.device,
needTagsView: state => state.settings.tagsView,
fixedHeader: state => state.settings.fixedHeader,
roles: state => state.user.roles,
}),
classObj() {
return {
hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation,
mobile: this.device === 'mobile'
}
},
variables() {
return variables
}
},
created() {
// this.checkPasswordStatus()
// wsUrl
this.wsUrl = this.getWebSocketUrl()
if (this.roles.includes("audit") || this.roles.includes("systemAdmin")) {
this.connectWebSocket();
}
this.handleNoWarningLog()
},
methods: {
// WebSocket URL localStorage null
getWebSocketUrl() {
try {
const systemConfig = JSON.parse(localStorage.getItem('systemConfig'))
return systemConfig && systemConfig.webSocketurl ? systemConfig.webSocketurl : 'ws://localhost:18082/ws'
} catch (error) {
console.error('Failed to get WebSocket URL:', error)
return 'ws://localhost:18082/ws'
}
},
checkPasswordStatus() {
checkPasswordStatus().then(response => {
if (response.code === 200) {
this.showChangePasswordDialog = response.data
this.title = response.msg
}
})
},
handleNoWarningLog(){
handleNoWarningLog().then(response => {
})
},
handleClickOutside() {
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
},
submit() {
this.$refs['form'].validate(valid => {
if (valid) {
updateUserPwd(this.user.oldPassword, this.user.newPassword).then(response => {
this.showChangePasswordDialog = false
this.$modal.msgSuccess('修改成功')
})
}
})
},
close() {
this.$store.dispatch('LogOut').then(() => {
location.href = '/index';
})
},
// WebSocket
connectWebSocket() {
if (this.socket) {
console.log("WebSocket 已连接");
return;
}
console.log("WebSocket URL:{}",this.wsUrl)
this.socket = new WebSocket(this.wsUrl);
// WebSocket
this.socket.onopen = () => {
console.log("WebSocket 连接成功");
this.isConnected = true;
};
//
this.socket.onmessage = (event) => {
console.log("收到消息:", event.data);
const warning = JSON.parse(event.data);
this.handleWarning(warning);
};
//
this.socket.onclose = () => {
console.log("WebSocket 连接已关闭");
this.isConnected = false;
this.socket = null;
//
this.reconnectWebSocket();
};
//
this.socket.onerror = (error) => {
console.error("WebSocket 错误:", error);
this.isConnected = false;
this.socket = null;
//
this.reconnectWebSocket();
};
},
// WebSocket
reconnectWebSocket() {
console.log("尝试重新连接 WebSocket...");
setTimeout(() => {
this.connectWebSocket();
}, this.reconnectInterval);
},
//
handleWarning(warning) {
console.log(warning)
let warningContent = '';
if (warning.operaUserName) {
warningContent += `<p><strong>操作人:</strong>${warning.operaUserName}</p>`;
}
if (warning.warningEvent) {
warningContent += `<p><strong>事件:</strong>${warning.warningEvent}</p>`;
}
if (warning.warningIp) {
warningContent += `<p><strong>IP</strong>${warning.warningIp}</p>`;
}
if (warning.operaTime) {
warningContent += `<p><strong>时间:</strong>${warning.operaTime}</p>`;
}
if (warningContent) {
MessageBox.alert(
warningContent,
'告警通知',
{
dangerouslyUseHTMLString: true,
confirmButtonText: '确认',
customClass: 'custom-message-box',
callback: () => {
this.notifyBackend(warning.warningId);
}
}
);
}
},
//
notifyBackend(warningId) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
const message = {
warningId,
};
this.socket.send(warningId);
console.log(`已通知后端处理告警: ${warningId}`);
}
}
},
beforeDestroy() {
// WebSocket
if (this.socket) {
this.socket.close();
}
}
}
</script>
<style lang="scss" scoped>
@import "~@/assets/styles/mixin.scss";
@import "~@/assets/styles/variables.scss";
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar {
position: fixed;
top: 0;
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.fixed-header {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$base-sidebar-width});
transition: width 0.28s;
}
.hideSidebar .fixed-header {
width: calc(100% - 54px);
}
.sidebarHide .fixed-header {
width: 100%;
}
.mobile .fixed-header {
width: 100%;
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<div :class="classObj" class="app-wrapper" :style="{'--current-color': theme}">
<div class="app-wrapper" :style="{'--current-color': theme}">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
<sidebar v-if="!sidebar.hide" class="sidebar-container"/>
<!-- <sidebar v-if="!sidebar.hide" class="sidebar-container"/> -->
<div :class="{hasTagsView:needTagsView,sidebarHide:sidebar.hide}" class="main-container">
<div :class="{'fixed-header':fixedHeader}">
<navbar/>

View File

@ -116,7 +116,7 @@ const user = {
const user = res.user
sessionStorage.setItem('companyId', res.user.deptId)
sessionStorage.setItem('deptName', res.user.dept.deptName)
const avatar = user.avatar ? user.avatar : require('@/assets/images/profile.jpg')
const avatar = user.avatar ? user.avatar : require('@/assets/images/profile.png')
commit('SET_ROLES', res.roles && res.roles.length > 0 ? res.roles : ['ROLE_DEFAULT'])
commit('SET_PERMISSIONS', res.permissions)
commit('SET_ID', user.userId)