效果:
初始的目录:
目录结构
src/
├── router/
│ ├── index.js
├── store/
│ ├── test.js
├── views/
│ ├── model/
│ │ ├── Model.vue
│ ├── Home.vue
│ ├── About.vue
│ └── Settings.vue
├── App.vue
└── main.js
思路:
- 弹窗组件化显示状态由全局showPop 变量控制
- 动态路由与弹窗绑定当路由变化时,确保弹窗能够根据路径同步更新active激活项逻辑处理
// route-index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'home',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
name: 'about',
component: () => import('@/views/About.vue')
},
{
path: '/settings',
name: 'settings',
component: () => import('@/views/Settings.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
// store-test.js
import { defineStore } from 'pinia'
export const testStore = defineStore('test', {
state: () => ({
showPop: false
}),
actions: {
setShowPop(IS) {
this.showPop = IS
}
}
})
// Model.vue
<template>
<div class="no-mask-popup" v-draggable="onDragStop" v-if="popupShow"
:style="{ width: Width, minHeight: Height, top: Top, left: Left }">
<div class="popup-head">
<slot name="title">
</slot>
<div class="popup-head-title">{{ Title }}</div>
<div class="popup-head-colse">
<span class="icon-svg-close" @click.stop="closePopup">
x
</span>
</div>
</div>
<slot></slot>
</div>
</template>
<script setup name="Model">
import { ref, onMounted, watch } from 'vue'
// 接受参数
const props = defineProps({
id: {
type: String,
required: true
},
Title: {
type: String,
default: ''
},
Width: {
type: String,
default: '50%'
},
Height: {
type: String,
default: '42%'
},
Top: {
type: String,
default: '15%'
},
Left: {
type: String,
default: '20%'
},
})
watch(
() => props.Top,
(newVal) => {
Top.value = newVal;
}
);
watch(
() => props.Left,
(newVal) => {
Left.value = newVal;
}
);
// 记录弹窗位置
// 将 Top 和 Left 改为响应式数据
const Top = ref(props.Top);
const Left = ref(props.Left);
onMounted(() => {
const savedTop = localStorage.getItem(`popupTop_${props.id}`);
const savedLeft = localStorage.getItem(`popupLeft_${props.id}`);
if (savedTop) Top.value = savedTop;
if (savedLeft) Left.value = savedLeft;
});
const onDragStop = (newPosition) => {
Top.value = newPosition.top;
Left.value = newPosition.left;
// 通知父组件更新位置
emit('update-position', newPosition);
};
const popupShow = ref(false)
const showPopup = () => {
popupShow.value = true;
};
const emit = defineEmits(['closePopup', 'update-position']);
const closePopup = () => {
popupShow.value = false;
// 保存当前的位置信息
localStorage.setItem(`popupTop_${props.id}`, Top.value);
localStorage.setItem(`popupLeft_${props.id}`, Left.value);
emit('closePopup');
};
defineExpose({ showPopup, closePopup, })
</script>
<style scoped>
.no-mask-popup {
position: absolute;
top: 15%;
left: 20%;
background-color: pink;
padding: 1rem;
.popup-head {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0rem 0rem 24px 0rem;
color: #fff;
font-size: 1.2rem;
font-weight: bold;
cursor: move;
}
.popup-head-title {
color: #fff;
font-size: 20px;
font-weight: 700;
}
.popup-head-colse {
cursor: pointer;
}
.icon-svg-close {
color: #fff;
cursor: pointer;
align-items: center;
display: inline-flex;
width: 1.5rem;
height: 1.5rem;
line-height: 1.5rem;
justify-content: center;
position: relative;
font-size: 20px;
font-weight: 700;
}
}
</style>
//About.vue
<template>
<h1>Welcome to About Page</h1>
</template>
// Home.vue
<template>
<div>
<h1>欢迎来到首页</h1>
</div>
</template>
<script setup>
</script>
// Setting.vue
<template>
<div>
<h1>欢迎来到设置</h1>
</div>
</template>
<script setup>
</script>
// PopupInfo.vue
<template>
<PopupView ref="parentPopup" :id="'parentPopup'" :is="showPopup" @update-position="updatePosition" :Top="sharedTop"
:Left="sharedLeft" :Title="'父级弹窗'" @closePopup="onParentPopupClose">
<p>这是父级弹窗的内容。</p>
<button @click="openChildPopup">打开子级弹窗</button>
</PopupView>
<!-- 子级弹窗 -->
<PopupView ref="childPopup" :id="'childPopup'" :Top="sharedTop" :Left="sharedLeft" @update-position="updatePosition"
:Title="'子级弹窗'" @closePopup="onChildPopupClose">
<p>这是子级弹窗的内容。</p>
<button @click="closeChildPopup">关闭子级弹窗</button>
</PopupView>
</template>
<script setup>
import { ref, computed } from 'vue';
import PopupView from '@/views/model/Model.vue'; // 请根据实际路径调整
import { testStore } from "@/stores/test.js";
const teststore = testStore();
const parentPopup = ref(null);
const childPopup = ref(null);
// 共享的位置信息
const sharedTop = ref('15%');
const sharedLeft = ref('20%');
// 从 localStorage 中恢复位置
const savedTop = localStorage.getItem('popupTop_shared');
const savedLeft = localStorage.getItem('popupLeft_shared');
if (savedTop) sharedTop.value = savedTop;
if (savedLeft) sharedLeft.value = savedLeft;
// 更新位置信息并保存到 localStorage
const updatePosition = (newPosition) => {
sharedTop.value = newPosition.top;
sharedLeft.value = newPosition.left;
localStorage.setItem('popupTop_shared', sharedTop.value);
localStorage.setItem('popupLeft_shared', sharedLeft.value);
};
const showPopup = computed(() => {
if (parentPopup.value && teststore.showPop) {
parentPopup.value.showPopup();
}
});
// 关闭父级弹窗的回调
const onParentPopupClose = () => {
// 父级弹窗关闭后的逻辑
localStorage.removeItem('popup-info')
teststore.setShowPop(false)
};
// 打开子级弹窗
const openChildPopup = () => {
// 关闭父级弹窗(实际上是隐藏)
parentPopup.value.closePopup();
// 打开子级弹窗
childPopup.value.showPopup();
};
// 关闭子级弹窗的回调
const onChildPopupClose = () => {
// 子级弹窗关闭后的逻辑
// 重新打开父级弹窗,实现“返回上一级”功能
parentPopup.value.showPopup();
};
// 关闭子级弹窗
const closeChildPopup = () => {
childPopup.value.closePopup();
};
</script>
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCN from 'element-plus/dist/locale/zh-cn.mjs'
import App from './App.vue'
import router from './router'
import draggableDirective from '@/utils/draggable'
const app = createApp(App)
app.directive('draggable', draggableDirective)
app.use(createPinia())
app.use(router)
app.use(ElementPlus, {
locale: zhCN
})
app.mount('#app')
//utils- draggable.js
const draggableDirective = {
beforeMount(el, binding) {
const header = el.querySelector('.popup-head'); // 获取头部元素
if (!header) return; // 如果没有找到头部元素,则不做任何处理
header.onmousedown = (e) => {
let offsetX = e.clientX - el.getBoundingClientRect().left;
let offsetY = e.clientY - el.getBoundingClientRect().top;
// 添加避免文本选中的样式
document.body.style.userSelect = 'none';
const parent = el.parentElement;
const parentRect = parent.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
document.onmousemove = function (e) {
let newLeft = e.pageX - offsetX;
let newTop = e.pageY - offsetY;
// 限制拖动范围在父节点内
if (newLeft < parentRect.left) newLeft = parentRect.left;
if (newTop < parentRect.top) newTop = parentRect.top;
if (newLeft + elRect.width > parentRect.right) newLeft = parentRect.right - elRect.width;
if (newTop + elRect.height > parentRect.bottom) newTop = parentRect.bottom - elRect.height;
el.style.position = 'absolute';
el.style.left = newLeft - parentRect.left + 'px';
el.style.top = newTop - parentRect.top + 'px';
};
document.onmouseup = function () {
document.onmousemove = null;
document.onmouseup = null;
// 恢复文本选中
document.body.style.userSelect = '';
// 如果绑定的值是函数,则在拖拽结束时调用它
if (typeof binding.value === 'function') {
const newPosition = {
top: el.style.top,
left: el.style.left,
};
binding.value(newPosition);
}
};
};
},
};
export default draggableDirective;
// app.vue
<template>
<div style="width: 100%;height: 100vh;">
<!-- 顶部菜单 -->
<div class="header">
<div class="left-wrapper">我的系统</div>
<div class="center-wrapper">
<el-menu :default-active="activeMenu" mode="horizontal" class="header-menu">
<template v-for="item in menuList" :key="item.name">
<!-- 有子菜单 -->
<el-sub-menu v-if="item.children" :index="item.name">
<template #title>{{ item.title }}</template>
<el-menu-item v-for="subItem in item.children" :key="subItem.name" :index="subItem.name"
@click="handleMenuClick(subItem.name)">
{{ subItem.title }}
</el-menu-item>
</el-sub-menu>
<!-- 无子菜单 -->
<el-menu-item v-else :index="item.name" @click="MenuClick(item.name)">
{{ item.title }}
</el-menu-item>
</template>
</el-menu>
</div>
<div class="right-wrapper">闹着玩</div>
</div>
<router-view />
<PopupInfo></PopupInfo>
</div>
</template>
<script setup>
import { testStore } from "@/stores/test.js";
const test = testStore();
import PopupInfo from "@/views/PopupInfo.vue";
import { nextTick, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
const router = useRouter();
const route = useRoute();
const menuList = [
{ name: "home", title: "首页" },
{ name: "about", title: "关于" },
{
name: "popup",
title: "弹窗",
children: [
{ name: "popup-info", title: "信息弹窗" },
{ name: "popup-alert", title: "警告弹窗" },
],
},
{ name: "settings", title: "系统设置" },
];
// 绑定当前激活菜单
const activeMenu = ref(route.name);
// 路由变化时更新激活菜单项
watch(route, (newRoute) => {
activeMenu.value = newRoute.name;
});
const handleMenuClick = (name) => {
console.log("点击了菜单项:", name);
activeMenu.value = null;
nextTick(() => {
activeMenu.value = route.name;
if (name === 'popup-info') {
localStorage.setItem('popup-info', 'true')
test.setShowPop(true)
return;
}
});
};
const MenuClick = (name) => {
router.push({ name })
}
</script>
<style scoped>
.header {
display: flex;
align-items: center;
justify-content: space-between;
background: #2c3e50;
color: white;
padding: 0 20px;
height: 60px;
}
.left-wrapper {
font-size: 24px;
font-weight: bold;
}
.center-wrapper {
flex: 1;
display: flex;
margin: 0 20px;
}
:deep(.el-menu--horizontal>.el-sub-menu .el-sub-menu__title) {
color: #fff;
}
:deep(.el-menu--horizontal>.el-sub-menu .el-sub-menu__title:hover) {
background: #409eff !important;
color: #ffd700 !important;
}
:deep(.el-menu--horizontal>.el-sub-menu.is-active .el-sub-menu__title) {
border-bottom: 2px solid red;
}
.el-menu--horizontal>.el-menu-item {
color: #fff !important;
}
.right-wrapper {
font-size: 16px;
}
.header-menu {
width: 100%;
background-color: #2c3e50;
color: white;
}
:deep(.el-menu-item.is-active) {
color: red !important;
/* 激活项文字红色 */
font-weight: bold;
border-bottom: 2px solid red;
/* 添加激活项下划线 */
}
:deep(.el-menu-item:hover) {
background: #409eff !important;
color: #ffd700 !important;
/* 悬停文字金色 */
}
</style>
标签:el,vue,const,value,import,全局,弹窗
From: https://blog.csdn.net/m0_72030584/article/details/143918005