init : 初始化仓库
This commit is contained in:
78
src/layout/appmain/AppMain.vue
Normal file
78
src/layout/appmain/AppMain.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="app-main-container">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition name="fade-transform" type="transition" appear mode="out-in">
|
||||
<div>
|
||||
<template v-if="!route.meta.noCache">
|
||||
<keep-alive>
|
||||
<suspense>
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
<template #fallback>
|
||||
<div>Loading...</div>
|
||||
</template>
|
||||
</suspense>
|
||||
</keep-alive>
|
||||
</template>
|
||||
<template v-else>
|
||||
<suspense>
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
<template #fallback>
|
||||
<div>Loading...</div>
|
||||
</template>
|
||||
</suspense>
|
||||
</template>
|
||||
</div>
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.app-main-container {
|
||||
height:calc(100vh - 130px);
|
||||
padding:0 15px;
|
||||
max-height: calc(100vh - 96px);
|
||||
overflow: auto;
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.app-main-container::-webkit-scrollbar {
|
||||
width:6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
// 滚动条轨道
|
||||
.app-main-container::-webkit-scrollbar-track {
|
||||
background: rgb(239, 239, 239);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
// 小滑块
|
||||
.app-main-container::-webkit-scrollbar-thumb {
|
||||
background: rgba(80, 81, 82, 0.29);
|
||||
border-radius: 10px;
|
||||
}
|
||||
/* fade-transform */
|
||||
.fade-transform-leave-active,
|
||||
.fade-transform-enter-active {
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
/* 可能为enter失效,拆分为 enter-from和enter-to */
|
||||
.fade-transform-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
.fade-transform-enter-to {
|
||||
opacity: 1;
|
||||
transform: translateX(0px);
|
||||
}
|
||||
|
||||
.fade-transform-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
</style>
|
||||
50
src/layout/index.vue
Normal file
50
src/layout/index.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<el-aside
|
||||
:class="siderbarStore.isCollapse ? 'collapse' : 'expand'"
|
||||
>
|
||||
<SiderBar></SiderBar>
|
||||
</el-aside>
|
||||
<el-main :class="siderbarStore.isCollapse ? 'main-collapse' : ''">
|
||||
<NavBar></NavBar>
|
||||
<TagsView></TagsView>
|
||||
<AppMain></AppMain>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SiderBar from './siderbar/index.vue'
|
||||
import NavBar from './navbar/index.vue'
|
||||
import TagsView from './tagsview/index.vue'
|
||||
import AppMain from './appmain/AppMain.vue';
|
||||
import { useSiderBar } from '../stores/siderbar';
|
||||
const siderbarStore = useSiderBar()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.expand {
|
||||
animation: Expand 0.15s ease forwards;
|
||||
}
|
||||
@keyframes Expand {
|
||||
from {
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
to {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
.collapse {
|
||||
animation: Collapse 0.15s ease forwards;
|
||||
}
|
||||
@keyframes Collapse {
|
||||
from {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
to {
|
||||
width: 64px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
273
src/layout/navbar/BellSocket.vue
Normal file
273
src/layout/navbar/BellSocket.vue
Normal file
@@ -0,0 +1,273 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-popover
|
||||
:width="300"
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
popper-style="box-shadow: rgb(14 18 22 / 35%) 0px 10px 38px -10px, rgb(14 18 22 / 20%) 0px 10px 20px -15px; padding: 10px;"
|
||||
>
|
||||
<template #reference>
|
||||
<el-badge :hidden="total===0" :value="total" class="item">
|
||||
<el-icon size="22px" style="cursor: pointer">
|
||||
<Bell/>
|
||||
</el-icon>
|
||||
</el-badge>
|
||||
</template>
|
||||
<template #default>
|
||||
<div v-if="total===0" style="height: 100px;display: flex;align-items: center;justify-content: center">
|
||||
暂无数据~
|
||||
</div>
|
||||
<ul v-else>
|
||||
<li v-for="(notice,index) in noticeList" :key="index">
|
||||
<span @click="handleToNotifyDetail(notice,index)">{{ notice.noticeTitle }}</span>
|
||||
<span v-if="notice.state==='0'" @click="handleRead(notice)">已读</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="notify-btn">
|
||||
<el-button type="primary" @click="handlePrevious" :disabled="pageInfo.pageNum===1" link>上一页</el-button>
|
||||
<span @click="handleMoreRead">本页已读</span>
|
||||
<el-button type="primary" @click="handleNext" :disabled="pageInfo.pageNum*pageInfo.pageSize>total" link>下一页
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
<el-dialog width="1200px" title="通知公告详情" v-model="visible" @close="visible=false">
|
||||
<el-form :model="viewForm" label-width="100px">
|
||||
<el-row>
|
||||
<el-col :span="24" class="title-block">
|
||||
<el-text class="title">{{ viewForm.noticeTitle }}</el-text>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-text v-if="viewForm.contentType === 'text'">{{ viewForm.noticeContent }}</el-text>
|
||||
<span v-else v-html="viewForm.noticeContent"></span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {getNotifyList, getNotifyDetail, readAllNotify, readSingleNotify} from "@/api/notice/notify";
|
||||
import {getToken} from '@/utils/auth'
|
||||
import {ElMessage} from "element-plus";
|
||||
import {useRouter} from "vue-router";
|
||||
const router = useRouter()
|
||||
const pageInfo = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 8
|
||||
})
|
||||
let send = {
|
||||
type: "ping"
|
||||
}
|
||||
const viewForm = ref();
|
||||
const visible = ref(false);
|
||||
const showNotify = ref(false);
|
||||
const total = ref();
|
||||
const noticeList = ref();
|
||||
//查看详情
|
||||
const handleViewDetails = (noticeId) => {
|
||||
getNotifyDetail(noticeId).then(res => {
|
||||
visible.value = true
|
||||
viewForm.value = res.data
|
||||
})
|
||||
}
|
||||
const setWsUrl=(url)=>{
|
||||
return (window.location.protocol === 'http:' ? "ws://" : "wss://")+window.location.host + import.meta.env.VITE_BASE_URL + url;
|
||||
}
|
||||
const initWebSocket = () => {
|
||||
try {
|
||||
//怎么区分http https /url(全局url) 封装url 只填个url?
|
||||
const wsUrl=setWsUrl('/notice-ws/notice')
|
||||
const socket = new WebSocket(wsUrl)
|
||||
// 2. ws.send()给服务器发送信息
|
||||
//连接发生错误的回调方法
|
||||
socket.onerror = function () {
|
||||
console.log("ws连接发生错误");
|
||||
};
|
||||
//连接成功建立的回调方法
|
||||
socket.onopen = function () {
|
||||
let authInfo = {
|
||||
token: getToken(),
|
||||
type: "auth",
|
||||
cluster: "notice"
|
||||
}
|
||||
socket.send(JSON.stringify(authInfo))
|
||||
console.log("ws连接成功");
|
||||
}
|
||||
//接收到消息的回调方法
|
||||
socket.onmessage = function (event) {
|
||||
let data = JSON.parse(event.data)
|
||||
console.log('测试铃铛',data)
|
||||
if (data.type === 'notice') {
|
||||
noticeList.value.push(data.notice)
|
||||
total.value += 1
|
||||
} else if(!data.type&&data.cluster==="notice"){
|
||||
noticeList.value.push(data)
|
||||
total.value += 1
|
||||
}
|
||||
// console.log("服务器返回的信息: ", JSON.parse(event.data));
|
||||
}
|
||||
//连接关闭的回调方法
|
||||
socket.onclose = function () {
|
||||
// initWebSocket()
|
||||
console.log("ws连接关闭");
|
||||
}
|
||||
setInterval(() => {
|
||||
socket.send(JSON.stringify(send))
|
||||
}, 30000)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
console.log("ws连接失败");
|
||||
}
|
||||
}
|
||||
initWebSocket()
|
||||
|
||||
const searchNotifyList = () => {
|
||||
let params = {
|
||||
cluster: "notice",
|
||||
state: 0,
|
||||
...pageInfo
|
||||
}
|
||||
getNotifyList(params).then(res => {
|
||||
console.log("获取到个人公告", res)
|
||||
if (res.data) {
|
||||
noticeList.value = res.data.rows
|
||||
total.value = res.data.total
|
||||
}
|
||||
// initWebSocket()
|
||||
})
|
||||
}
|
||||
searchNotifyList()
|
||||
|
||||
//点击名字跳转到详情页
|
||||
const handleToNotifyDetail = (notice, index) => {
|
||||
noticeList.value.splice(index, 1)
|
||||
total.value -= 1
|
||||
viewForm.value = {
|
||||
noticeTitle: '',
|
||||
noticeContent: ''
|
||||
}
|
||||
// router.push({path: `/system/notice/inform/index/${notice.noticeId}`})
|
||||
handleViewDetails(notice.noticeId)
|
||||
}
|
||||
|
||||
//单个已读
|
||||
const handleRead = (notice) => {
|
||||
readSingleNotify(notice.noticeId).then(res => {
|
||||
if (res.code === 1000) {
|
||||
ElMessage.success(res.msg)
|
||||
searchNotifyList()
|
||||
} else {
|
||||
ElMessage.error(res.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
//本页已读
|
||||
const handleMoreRead = () => {
|
||||
let notifyIds = []
|
||||
noticeList.value.forEach(item => {
|
||||
notifyIds.push(item.noticeId)
|
||||
})
|
||||
readAllNotify(notifyIds).then(res => {
|
||||
if (res.code === 1000) {
|
||||
ElMessage.success(res.msg)
|
||||
searchNotifyList()
|
||||
} else {
|
||||
ElMessage.error(res.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
//上一页
|
||||
const handlePrevious = () => {
|
||||
if (pageInfo.pageNum !== 1) {
|
||||
pageInfo.pageNum -= 1
|
||||
searchNotifyList()
|
||||
}
|
||||
}
|
||||
|
||||
//下一页
|
||||
const handleNext = () => {
|
||||
if (pageInfo.pageNum * pageInfo.pageSize <= total.value) {
|
||||
pageInfo.pageNum += 1
|
||||
searchNotifyList()
|
||||
}
|
||||
}
|
||||
// onMounted(() => {
|
||||
//
|
||||
// });
|
||||
// 组件被销毁之前,清空 sock 对象
|
||||
// onBeforeUnmount(() => {
|
||||
// // 关闭连接
|
||||
// // 销毁 websocket 实例对象
|
||||
// // socket = null;
|
||||
// });
|
||||
// defineExpose({
|
||||
// searchNotifyList
|
||||
// })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.title-block {
|
||||
text-align: center;
|
||||
padding-bottom: 30px;
|
||||
|
||||
.title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
ul::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
// 滚动条轨道
|
||||
ul::-webkit-scrollbar-track {
|
||||
background: rgb(239, 239, 239);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
// 小滑块
|
||||
ul::-webkit-scrollbar-thumb {
|
||||
background: rgba(80, 81, 82, 0.29);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
ul {
|
||||
height: 100px;
|
||||
overflow-y: auto;
|
||||
padding: 0 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
li {
|
||||
height: 25px;
|
||||
line-height: 25px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
span:first-child:hover {
|
||||
color: #2a99ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
span:last-child {
|
||||
color: #2a99ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notify-btn {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
color: #2a99ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
49
src/layout/navbar/Breadcrumb.vue
Normal file
49
src/layout/navbar/Breadcrumb.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="item.path">
|
||||
<span v-if="item.meta.noRedirect || index === breadcrumbList.length-1" class="no-redirect">
|
||||
{{ item.meta.title }}
|
||||
</span>
|
||||
<router-link v-else :to="item.redirect || item.path">
|
||||
{{ item.meta.title }}
|
||||
</router-link>
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
watch(route, ()=> {
|
||||
getBreadcrumb()
|
||||
})
|
||||
|
||||
//面包屑导航数据
|
||||
const breadcrumbList = ref([])
|
||||
|
||||
//获取面包屑导航数据
|
||||
const getBreadcrumb = () => {
|
||||
let matched = route.matched.filter(item => item.meta && item.meta.title)
|
||||
const first = matched[0]
|
||||
if(!isDashboard(first)) {
|
||||
matched = [{path: '/home', meta: {title: '首页'}}].concat(matched)
|
||||
}
|
||||
breadcrumbList.value.length = 0;
|
||||
const reBreadcrumbList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
|
||||
breadcrumbList.value.push(...reBreadcrumbList)
|
||||
}
|
||||
|
||||
const isDashboard = (meta) => {
|
||||
const name = meta && meta.name
|
||||
if(!name) {
|
||||
return
|
||||
}
|
||||
return name.trim().toLocaleLowerCase() === 'Home'.toLocaleLowerCase()
|
||||
}
|
||||
|
||||
getBreadcrumb()
|
||||
</script>
|
||||
30
src/layout/navbar/Hamburger.vue
Normal file
30
src/layout/navbar/Hamburger.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="toggle" @click="toggleClick">
|
||||
<component :is="siderbarStore.getSiderBarStatus() ? 'Fold' : 'Expand'" class="icon"></component>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useSiderBar} from '@/stores/siderbar.js'
|
||||
const siderbarStore = useSiderBar()
|
||||
const toggleClick = () => {
|
||||
siderbarStore.setSiderBarStatus()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.toggle{
|
||||
line-height: 65px;
|
||||
padding: 0 15px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: #79bbff;
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
vertical-align: middle;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
126
src/layout/navbar/index.vue
Normal file
126
src/layout/navbar/index.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div class="navbar">
|
||||
<Hamburger></Hamburger>
|
||||
<Breadcrumb></Breadcrumb>
|
||||
<div class="right-bar">
|
||||
<bell-socket/>
|
||||
<div class="user-box">
|
||||
<div>
|
||||
<img :src="userInfo.avatar" alt="" @click.stop="handleVisitedP">
|
||||
<span>{{userInfo.userName}}</span>
|
||||
</div>
|
||||
<div class="person" v-if="visitedP">
|
||||
<ul>
|
||||
<li @click="handleToAuth">个人中心</li>
|
||||
<li @click="handleLogout">退出登录</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useRouter} from 'vue-router';
|
||||
import Breadcrumb from './Breadcrumb.vue';
|
||||
import Hamburger from './Hamburger.vue';
|
||||
import {useAuthStore} from '@/stores/userstore.js'
|
||||
import BellSocket from "./BellSocket.vue";
|
||||
import {getUserInfo} from "../../api/login";
|
||||
import {usePermisstionStroe} from '@/stores/permisstion'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const permisstionStore = usePermisstionStroe()
|
||||
const userInfo = ref({})
|
||||
const visitedP = ref(false)
|
||||
const router = useRouter()
|
||||
onMounted(() => {
|
||||
setUserInfo()
|
||||
document.addEventListener('click', nullBlockClick)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', nullBlockClick)
|
||||
})
|
||||
const setUserInfo = () => {
|
||||
getUserInfo().then(res=>{
|
||||
userInfo.value = res.data.user
|
||||
})
|
||||
}
|
||||
const nullBlockClick = () => {
|
||||
visitedP.value = false
|
||||
}
|
||||
const handleVisitedP = () => {
|
||||
visitedP.value = !visitedP.value
|
||||
}
|
||||
|
||||
const handleToAuth = () => {
|
||||
visitedP.value = !visitedP.value
|
||||
router.push('/auth')
|
||||
}
|
||||
const handleLogout = () => {
|
||||
visitedP.value = !visitedP.value
|
||||
authStore.userLogout()
|
||||
permisstionStore.removeMenu()
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
height: 65px;
|
||||
padding: 0 15px 0 0;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
.right-bar {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
.user-box{
|
||||
margin-left: 10px;
|
||||
position: relative;
|
||||
>div:first-child{
|
||||
display:flex;
|
||||
align-items: center;
|
||||
>span{
|
||||
margin-left: 5px;
|
||||
}
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: 1px solid #418DFF;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.person {
|
||||
position: absolute;
|
||||
width: 80px;
|
||||
right: 0;
|
||||
z-index: 300;
|
||||
bottom: -70px;
|
||||
padding: 10px 5px;
|
||||
border-radius: 4px;
|
||||
background-color: #fff;
|
||||
box-shadow: 2px 2px 2px 1px rgb(171, 167, 167);
|
||||
|
||||
li {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #79bbff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
48
src/layout/siderbar/MenuItem.vue
Normal file
48
src/layout/siderbar/MenuItem.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<!-- 有子菜单 -->
|
||||
<template v-for="item in menuItem" :key="item.path">
|
||||
<el-sub-menu v-if="item?.children?.length>0 && !item.hidden" :index="item.path">
|
||||
<template #title>
|
||||
<svg-icon :name="item.icon"/>
|
||||
<span>{{ item.title }}</span>
|
||||
</template>
|
||||
<Item :menu-item="item.children"></Item>
|
||||
</el-sub-menu>
|
||||
<el-menu-item v-else-if="!item.hidden&&!item.meta.isFrame" :index="handleGo(item.path)">
|
||||
<template #title>
|
||||
<div v-if="item.path==='/tool/swagger'">
|
||||
<a class="port-link" :href="link" target="_blank">
|
||||
<svg-icon :name="item.icon"/>
|
||||
{{ item.title }}
|
||||
</a>
|
||||
</div>
|
||||
<div v-else>
|
||||
<svg-icon :name="item.icon"/>
|
||||
<span>{{ item.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SvgIcon from '@/components/svgIcon/index.vue'
|
||||
import Item from './MenuItem.vue'
|
||||
|
||||
const props = defineProps({
|
||||
menuItem: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
const link = ref('http://gateway.feashow.cn/doc.html#/home')
|
||||
const handleGo = (path) => {
|
||||
if (path === "/tool/swagger") {
|
||||
return ''
|
||||
} else {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
52
src/layout/siderbar/index.vue
Normal file
52
src/layout/siderbar/index.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="logo" ref="logo">
|
||||
<span v-if="!siderbarStore.isCollapse">SmartOpsWeb</span>
|
||||
</div>
|
||||
<el-scrollbar :height="`calc(100vh - ${logoHeight}px)`" style="background-color: #ffffff">
|
||||
<el-menu
|
||||
router
|
||||
:default-active="activeMenu"
|
||||
:unique-opened="true"
|
||||
:collapse="siderbarStore.isCollapse"
|
||||
active-text-color="#333"
|
||||
text-color="#333"
|
||||
style="border: none;"
|
||||
:collapse-transition="false"
|
||||
>
|
||||
<MenuItem :menu-item="permisstionStore.menuList"></MenuItem>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MenuItem from './MenuItem.vue';
|
||||
import {useRoute} from 'vue-router'
|
||||
import {useSiderBar} from '@/stores/siderbar.js'
|
||||
import {usePermisstionStroe} from '@/stores/permisstion.js'
|
||||
import {nextTick} from 'vue';
|
||||
|
||||
const siderbarStore = useSiderBar()
|
||||
const permisstionStore = usePermisstionStroe()
|
||||
const route = useRoute()
|
||||
const link = ref('')
|
||||
const title = ref('')
|
||||
const logo = ref(null)
|
||||
const logoHeight = ref()
|
||||
const getLogoH = () => {
|
||||
logoHeight.value = logo.value.offsetHeight
|
||||
}
|
||||
const activeMenu = computed(() => {
|
||||
const {path} = route
|
||||
return path
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
getLogoH()
|
||||
})
|
||||
})
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
getLogoH()
|
||||
})
|
||||
</script>
|
||||
123
src/layout/tagsview/index.vue
Normal file
123
src/layout/tagsview/index.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div class="link-box">
|
||||
<el-scrollbar noresize>
|
||||
<div>
|
||||
<router-link
|
||||
v-for="item in tagsViewStore.visitedViews"
|
||||
:key="item.path" :to="{ path: item.path }" class="tag"
|
||||
:class="isActive(item) ? 'active' : ''"
|
||||
@click.prevent
|
||||
@contextmenu.prevent.native="openMenu(item, $event)">
|
||||
<span>{{ item.meta.title }}</span>
|
||||
<span @click.prevent="closeTagView(item.path)">
|
||||
<svg-icon name="close" :class-name="'close-icon'"/>
|
||||
</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<ul class="contextmenu" :style="{ 'left': left + 'px' }" v-if="visible">
|
||||
<li class="el-dropdown-menu__item" @click="closeCurrentTagView">关闭当前</li>
|
||||
<li class="el-dropdown-menu__item" @click="closeOtherTagView">关闭其他</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useRoute} from 'vue-router';
|
||||
import {useTagsView} from '@/stores/tagsview.js'
|
||||
import SvgIcon from '@/components/svgIcon/index.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const tagsViewStore = useTagsView()
|
||||
const visible = ref(false)
|
||||
const left = ref()
|
||||
const tagPath = ref()
|
||||
|
||||
watch(route, () => {
|
||||
init()
|
||||
})
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', nullBlockClick)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', nullBlockClick)
|
||||
})
|
||||
const nullBlockClick = () => {
|
||||
visible.value = false
|
||||
}
|
||||
const init = () => {
|
||||
tagsViewStore.addVisitedViews(route)
|
||||
}
|
||||
const closeTagView = (path) => {
|
||||
tagsViewStore.delVisitedViews(path)
|
||||
}
|
||||
const isActive = (tag) => {
|
||||
return tag.path === route.path
|
||||
}
|
||||
const openMenu = (tag, e) => {
|
||||
tagPath.value = tag
|
||||
left.value = e.x - 230
|
||||
visible.value = true
|
||||
}
|
||||
// 关闭当前
|
||||
const closeCurrentTagView = () => {
|
||||
tagsViewStore.delVisitedViews(tagPath.value.path)
|
||||
visible.value = false
|
||||
}
|
||||
// 关闭其他
|
||||
const closeOtherTagView = () => {
|
||||
tagsViewStore.delOtherVisitedViews(tagPath.value)
|
||||
visible.value = false
|
||||
}
|
||||
init()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.link-box {
|
||||
position: relative;
|
||||
padding: 12px 0;
|
||||
line-height: 30px;
|
||||
|
||||
.tag {
|
||||
padding: 6px;
|
||||
border: 1px solid darkgray;
|
||||
font-size: 13px;
|
||||
margin-right: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: #E9F4FF;
|
||||
border: 1px solid #418DFF;
|
||||
color: #333333;
|
||||
|
||||
> span:first-child {
|
||||
color: #418DFF;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.contextmenu {
|
||||
padding: 5px;
|
||||
width: 88px;
|
||||
//height: 82px;
|
||||
position: absolute;
|
||||
top: 47px;
|
||||
z-index: 3000;
|
||||
background: #fff;
|
||||
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
|
||||
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 7px 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #eee;
|
||||
color: #EDC49A;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user