在開發后臺管理系統時,我們經常會用到浮動菜單來快速訪問某些功能。本篇文章將分享一個基于 Vue3 + ElementPlus 實現的浮動菜單組件,支持拖拽移動、邊緣吸附、二級菜單展開、菜單搜索過濾、視頻彈窗等交互效果,極大提升了用戶操作的便捷性與美觀性。
效果預覽
- 懸浮按鈕支持全屏拖拽移動
- 貼邊時自動收縮為小浮標
- 點擊展開二級菜單,支持搜索過濾
- 支持在菜單項上點擊視頻icon預覽操作視頻
- 自帶吸附動畫與滾動提示
父組件(App.vue)
<template><el-config-provider :locale="locale"><router-view /><FloatingMenu :max-items-before-scroll="4" :allowed-menu-ids="[1, 2, 3, 4, 5, 6, 7, 8]" /></el-config-provider>
</template>## 子組件(FloatingMenu.vue)```javascript
<template><div v-if="shouldShowFloatingMenu" class="floating-nav" ref="floatingNav" :style="navStyle"><!-- 主浮標 --><div class="nav-trigger" :class="{ active: isMenuVisible, dragging: isDragging, docked: isDocked }":style="dockStyle" @mousedown="handleMouseDown"><div class="nav-icon" v-if="!isDocked"><svg viewBox="0 0 24 24" v-if="!isMenuVisible"><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" /></svg><svg viewBox="0 0 24 24" v-else><pathd="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" /></svg></div><div v-if="isDocked" class="dock-icon"><svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z" /></svg></div><div class="nav-ripple"></div><div class="nav-pulse"></div></div><!-- 二級菜單面板 --><transition name="menu-slide"><div v-show="isMenuVisible" class="submenu-panel" :class="menuDirection" @click.stop><div class="panel-header"><h3>{{ currentTopMenu?.menu_name }}</h3><div class="search-box" v-if="hasSearch"><div class="search-input-wrapper"><svg class="search-icon" viewBox="0 0 24 24"><pathd="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" /></svg><input v-model="searchQuery" placeholder="搜索菜單..." @click.stop /></div></div></div><div class="menu-scroll-container"><div v-for="item in filteredSubMenus" :key="item.id" class="menu-item" :class="{ active: isActive(item) }"@click="navigateTo(item)"><div class="menu-content"><div class="menu-main"><span class="menu-text">{{ item.menu_name }}</span><div class="menu-icons"><svg class="menu-arrow" viewBox="0 0 24 24"><path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z" /></svg><svg class="demo-icon" viewBox="0 0 24 24" @click.stop="showVideo(item)"><pathd="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z" /></svg></div></div><span class="menu-hint" v-if="item.remark">{{ item.remark }}</span></div></div><div v-if="filteredSubMenus.length === 0" class="empty-state"><svg viewBox="0 0 24 24"><pathd="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" /></svg><span>沒有找到匹配的菜單</span></div></div><div class="panel-footer" v-if="showScrollHint"><div class="scroll-hint"><svg viewBox="0 0 24 24"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z" /></svg><span>滾動查看更多</span></div></div></div></transition></div><OperateVideoDialog v-if="showOperateVisible" ref="videoModal" :videoUrl="videoUrl"@close="closeOperateVideoDialog" />
</template><script setup>
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import OperateVideoDialog from '@/components/popup/OperateVideoDialog.vue'
import { getVideoUrl } from '@/utils/operateVideo';const props = defineProps({maxItemsBeforeScroll: {type: Number,default: 8},allowedMenuIds: {type: Array,default: () => [],validator: value => value.every(id => Number.isInteger(id))}
})const route = useRoute()
const router = useRouter()
const floatingNav = ref(null)
const isMenuVisible = ref(false)
const isDragging = ref(false)
const searchQuery = ref('')
const startPos = ref({ x: 0, y: 0 })
const dragStartTime = ref(0)
const navPos = ref({x: window.innerWidth - 200,y: window.innerHeight / 2 - 100
})const videoModal = ref(null)
const videoUrl = ref("")
const showOperateVisible = ref(false);const isDocked = ref(false)// 監聽路由變化,自動關閉菜單
watch(() => route.path, () => {isMenuVisible.value = falsesearchQuery.value = ''
})// 從 sessionStorage 獲取菜單
const getMenus = () => {try {const menus = JSON.parse(sessionStorage.getItem('menus')) || []return menus} catch (e) {console.error('菜單解析失敗:', e)return []}
}// 處理菜單數據
const allMenus = ref(getMenus())
const topLevelMenus = computed(() => {return allMenus.value.filter(menu => menu.menu_level === 1).map(menu => ({...menu,child: Array.isArray(menu.child) ? menu.child : []}))
})// 當前菜單
const currentTopMenu = computed(() => {const currentPath = route.path.split('?')[0].split('#')[0];// 根據傳入的allowedMenuIds篩選一級菜單const validTopMenus = topLevelMenus.value.filter(menu => {const menuId = parseInt(menu.id);return props.allowedMenuIds.includes(menuId);});// 匹配二級菜單for (const topMenu of validTopMenus) {const matchedSubMenu = (topMenu.child || []).find(subMenu => {const subMenuPath = subMenu.index || subMenu.router;return subMenuPath && currentPath === subMenuPath;});if (matchedSubMenu) {return validTopMenus.find(menu => menu.id === matchedSubMenu.level_pre);}}// 如果沒有匹配的二級菜單,嘗試精確匹配一級菜單return validTopMenus.find(topMenu => {const topMenuPath = topMenu.router || topMenu.index;return topMenuPath && currentPath === topMenuPath;}) || null;
});// 是否顯示浮標
const shouldShowFloatingMenu = computed(() => {try {if (!currentTopMenu.value) return false;const menuId = parseInt(currentTopMenu.value.id);return menuId >= 1 && menuId <= 8;} catch (e) {console.error('浮標顯示判斷出錯:', e);return false;}
});// 當前二級菜單
const currentSubMenus = computed(() => {try {return currentTopMenu.value?.child || []} catch (e) {console.error('獲取子菜單出錯:', e)return []}
})// 搜索過濾
const filteredSubMenus = computed(() => {try {if (!searchQuery.value) return currentSubMenus.valueconst query = searchQuery.value.toLowerCase()return currentSubMenus.value.filter(item =>item.menu_name.toLowerCase().includes(query) ||(item.remark && item.remark.toLowerCase().includes(query)))} catch (e) {console.error('菜單搜索出錯:', e)return currentSubMenus.value}
})// 是否需要顯示搜索框
const hasSearch = computed(() => currentSubMenus.value.length > 10)// 是否需要顯示滾動提示
const showScrollHint = computed(() =>filteredSubMenus.value.length > props.maxItemsBeforeScroll
)const menuDirection = computed(() => {const threshold = window.innerWidth / 2return navPos.value.x < threshold ? 'right' : 'left'
})const dockStyle = computed(() => {if (!isDocked.value) return {}const nearLeft = navPos.value.x <= window.innerWidth / 2return {'border-radius': nearLeft ? '0 32px 32px 0' : '32px 0 0 32px','justify-content': nearLeft ? 'flex-start' : 'flex-end','padding-left': nearLeft ? '4px' : '0','padding-right': nearLeft ? '0' : '4px',}
})// 檢查激活狀態
const isActive = (item) =>item.index && route.path.startsWith(item.index)// 導航功能
const navigateTo = (item) => {try {if (item.index) {router.push(item.index)isMenuVisible.value = falsesearchQuery.value = ''}} catch (e) {console.error('菜單跳轉出錯:', e)isMenuVisible.value = false}
}// 切換菜單
const toggleMenu = () => {isMenuVisible.value = !isMenuVisible.valueif (isMenuVisible.value) {searchQuery.value = ''}
}const showVideo = async (item) => {try {videoUrl.value = await getVideoUrl(item.index || "")showOperateVisible.value = truenextTick(() => {toggleMenu()videoModal.value.open()})} catch (e) {ElMessage.warning(e.message)showOperateVisible.value = false}
}const closeOperateVideoDialog = () => {videoUrl.value = ""showOperateVisible.value = false
}// 處理鼠標按下事件
const handleMouseDown = (e) => {try {e.preventDefault()if (isDocked.value) {// 吸附狀態,點擊恢復為正常浮標,不做拖動isDocked.value = falsenavPos.value.x = navPos.value.x <= window.innerWidth / 2 ? 0 : window.innerWidth - 60return // 不再監聽拖拽事件}isDragging.value = falsedragStartTime.value = Date.now()startPos.value = {x: e.clientX - navPos.value.x,y: e.clientY - navPos.value.y}const onMove = (e) => {// 如果移動距離超過閾值,開始拖拽const deltaX = Math.abs(e.clientX - (startPos.value.x + navPos.value.x))const deltaY = Math.abs(e.clientY - (startPos.value.y + navPos.value.y))if ((deltaX > 5 || deltaY > 5) && !isDragging.value) {isDragging.value = trueisMenuVisible.value = false}if (isDragging.value) {const maxX = window.innerWidth - 60const maxY = window.innerHeight - 60navPos.value = {x: Math.max(0, Math.min(maxX, e.clientX - startPos.value.x)),y: Math.max(0, Math.min(maxY, e.clientY - startPos.value.y))}}}// const onUp = () => {// const clickDuration = Date.now() - dragStartTime.value// // 如果沒有拖拽且點擊時間短,則切換菜單// if (!isDragging.value && clickDuration < 200) {// toggleMenu()// }// if (isDragging.value) {// // 貼邊吸附// // const threshold = window.innerWidth / 2// // navPos.value.x = navPos.value.x < threshold ? 0 : window.innerWidth - 60// sessionStorage.setItem('floatingNavPos', JSON.stringify(navPos.value))// }// isDragging.value = false// document.removeEventListener('mousemove', onMove)// document.removeEventListener('mouseup', onUp)// }const onUp = () => {const clickDuration = Date.now() - dragStartTime.valueif (!isDragging.value && clickDuration < 200) {if (isDocked.value) {isDocked.value = falsenavPos.value.x = navPos.value.x <= window.innerWidth / 2 ? 0 : window.innerWidth - 60} else {toggleMenu()}}if (isDragging.value) {const edgeThreshold = 20const nearLeft = navPos.value.x <= edgeThresholdconst nearRight = navPos.value.x >= window.innerWidth - 60 - edgeThresholdif (nearLeft || nearRight) {isDocked.value = truenavPos.value.x = nearLeft ? 0 : window.innerWidth - 32} else {sessionStorage.setItem('floatingNavPos', JSON.stringify(navPos.value))}}isDragging.value = falsedocument.removeEventListener('mousemove', onMove)document.removeEventListener('mouseup', onUp)}document.addEventListener('mousemove', onMove)document.addEventListener('mouseup', onUp)} catch (e) {console.error('拖拽操作出錯:', e)isDragging.value = false}
}// 樣式計算
const navStyle = computed(() => ({left: `${navPos.value.x}px`,top: `${navPos.value.y}px`,'--active-color': isActiveColor.value,'--active-color-light': isActiveColor.value + '20'
}))// 獲取激活菜單的顏色
const isActiveColor = computed(() => {const activeItem = currentSubMenus.value.find(item => isActive(item))return activeItem ? '#10b981' : '#6366f1'
})// 初始化位置
const initPosition = () => {const savedPos = sessionStorage.getItem('floatingNavPos')if (savedPos) {try {const pos = JSON.parse(savedPos)navPos.value = {x: Math.min(pos.x, window.innerWidth - 60),y: Math.min(pos.y, window.innerHeight - 60)}} catch (e) {console.error('位置解析失敗:', e)}}
}// 窗口大小調整
const handleResize = () => {try {navPos.value = {x: Math.min(navPos.value.x, window.innerWidth - 60),y: Math.min(navPos.value.y, window.innerHeight - 60)}} catch (e) {console.error('窗口調整大小出錯:', e)}
}// 點擊外部關閉菜單
const handleClickOutside = (e) => {if (isMenuVisible.value && !floatingNav.value?.contains(e.target)) {isMenuVisible.value = false}
}onMounted(() => {initPosition()window.addEventListener('resize', handleResize)document.addEventListener('click', handleClickOutside)
})onUnmounted(() => {window.removeEventListener('resize', handleResize)document.removeEventListener('click', handleClickOutside)
})
</script><style scoped>
.floating-nav {position: fixed;z-index: 9999;/** transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); */user-select: none;
}.nav-trigger {position: relative;display: flex;align-items: center;justify-content: center;width: 64px;height: 64px;background: linear-gradient(135deg, var(--active-color), rgba(99, 102, 241, 0.8));color: white;border-radius: 50%;box-shadow:0 8px 32px rgba(0, 0, 0, 0.12),0 4px 16px rgba(99, 102, 241, 0.3),inset 0 1px 0 rgba(255, 255, 255, 0.2);cursor: pointer;transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);overflow: hidden;
}.nav-trigger::before {content: '';position: absolute;top: 0;left: 0;right: 0;bottom: 0;background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), transparent);border-radius: 50%;pointer-events: none;
}.nav-trigger:hover {transform: translateY(-2px) scale(1.05);box-shadow:0 12px 40px rgba(0, 0, 0, 0.16),0 8px 24px rgba(99, 102, 241, 0.4),inset 0 1px 0 rgba(255, 255, 255, 0.3);
}.nav-trigger.active {transform: translateY(-1px) scale(1.02);background: linear-gradient(135deg, #ef4444, #dc2626);box-shadow:0 12px 40px rgba(0, 0, 0, 0.16),0 8px 24px rgba(239, 68, 68, 0.4),inset 0 1px 0 rgba(255, 255, 255, 0.3);
}.nav-trigger.dragging {cursor: grabbing;transform: scale(1.1);box-shadow:0 16px 48px rgba(0, 0, 0, 0.2),0 8px 32px rgba(99, 102, 241, 0.5),inset 0 1px 0 rgba(255, 255, 255, 0.3);
}.nav-icon {width: 24px;height: 24px;display: flex;align-items: center;justify-content: center;transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);z-index: 2;
}.nav-trigger.active .nav-icon {transform: rotate(90deg);
}.nav-icon svg {width: 100%;height: 100%;fill: currentColor;filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2));
}.nav-ripple {position: absolute;top: 50%;left: 50%;width: 0;height: 0;border-radius: 50%;background: rgba(255, 255, 255, 0.3);transform: translate(-50%, -50%);pointer-events: none;transition: all 0.6s ease-out;
}.nav-trigger:active .nav-ripple {width: 120px;height: 120px;opacity: 0;
}.nav-trigger.docked {width: 32px;height: 64px;background: rgba(99, 102, 241, 0.9);box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);transition: all 0.3s ease;display: flex;align-items: center;
}.dock-icon {width: 16px;height: 16px;
}.dock-icon svg {width: 100%;height: 100%;fill: white;transform: rotate(0deg);transition: transform 0.3s;
}/* 自動旋轉箭頭指向 */
.floating-nav[style*="left: 0px"] .dock-icon svg {transform: rotate(0deg);
}.floating-nav[style*="left:"]:not([style*="left: 0px"]) .dock-icon svg {transform: rotate(180deg);
}.nav-pulse {position: absolute;top: -4px;left: -4px;right: -4px;bottom: -4px;border-radius: 50%;background: linear-gradient(135deg, var(--active-color), rgba(99, 102, 241, 0.3));animation: pulse 3s ease-in-out infinite;z-index: -1;
}@keyframes pulse {0% {transform: scale(1);opacity: 1;}50% {transform: scale(1.1);opacity: 0.7;}100% {transform: scale(1);opacity: 1;}
}.submenu-panel {position: absolute;right: 0;bottom: calc(100% + 16px);width: 300px;max-height: 420px;background: rgba(255, 255, 255, 0.95);backdrop-filter: blur(20px);border-radius: 16px;box-shadow:0 20px 64px rgba(0, 0, 0, 0.12),0 8px 32px rgba(0, 0, 0, 0.08),0 0 0 1px rgba(255, 255, 255, 0.5);overflow: hidden;border: 1px solid rgba(229, 231, 235, 0.3);
}.submenu-panel.left {right: calc(100% + 16px);
}.submenu-panel.right {left: calc(100% + 16px);
}.panel-header {padding: 10px;background: linear-gradient(135deg, #f8fafc, #f1f5f9);border-bottom: 1px solid rgba(229, 231, 235, 0.3);
}.panel-header h3 {font-size: 18px;font-weight: 700;color: #1e293b;background: linear-gradient(135deg, #1e293b, #475569);-webkit-background-clip: text;-webkit-text-fill-color: transparent;
}.search-box {margin-top: 16px;
}.search-input-wrapper {position: relative;display: flex;align-items: center;
}.search-icon {position: absolute;left: 14px;width: 16px;height: 16px;fill: #64748b;pointer-events: none;z-index: 1;
}.search-input-wrapper input {width: 100%;padding: 12px 16px 12px 40px;border: 1px solid rgba(209, 213, 219, 0.5);border-radius: 10px;font-size: 14px;background: rgba(255, 255, 255, 0.8);backdrop-filter: blur(8px);transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);outline: none;color: #374151;
}.search-input-wrapper input:focus {border-color: var(--active-color);box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);background: rgba(255, 255, 255, 0.95);
}.menu-scroll-container {max-height: calc(70vh - 160px);overflow-y: auto;padding: 12px 0;
}.menu-item {padding: 0;margin: 6px 16px;cursor: pointer;border-radius: 12px;transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);border: 1px solid transparent;overflow: hidden;position: relative;
}.menu-item::before {content: '';position: absolute;top: 0;left: -100%;width: 100%;height: 100%;background: linear-gradient(90deg, transparent, rgba(99, 102, 241, 0.1), transparent);transition: left 0.5s ease;
}.menu-item:hover::before {left: 100%;
}.menu-item:hover {background: linear-gradient(135deg, #f8fafc, #f1f5f9);border-color: rgba(99, 102, 241, 0.2);transform: translateY(-2px);box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
}.menu-item.active {background: linear-gradient(135deg, var(--active-color-light), rgba(99, 102, 241, 0.1));border-color: var(--active-color);border-left: 4px solid var(--active-color);transform: translateY(-1px);
}.menu-content {padding: 8px 10px;display: flex;flex-direction: column;
}.menu-main {display: flex;align-items: center;justify-content: space-between;margin-bottom: 6px;
}.menu-text {font-size: 15px;font-weight: 600;color: #1e293b;letter-spacing: 0.2px;
}.menu-icons {display: flex;align-items: center;gap: 8px;
}.demo-icon {width: 16px;height: 16px;fill: #9ca3af;cursor: help;transition: all 0.3s ease;
}.demo-icon:hover {fill: var(--active-color);transform: scale(1.1);
}.menu-item:hover .demo-icon {opacity: 1;
}.menu-arrow {width: 18px;height: 18px;fill: #9ca3af;opacity: 0;transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}.menu-item:hover .menu-arrow {opacity: 1;transform: translateX(3px);fill: var(--active-color);
}.menu-item.active .menu-arrow {opacity: 1;fill: var(--active-color);
}.menu-hint {font-size: 12px;color: #64748b;font-weight: 400;line-height: 1.4;opacity: 0.8;
}.empty-state {display: flex;flex-direction: column;align-items: center;padding: 48px 24px;color: #64748b;
}.empty-state svg {width: 56px;height: 56px;fill: #cbd5e1;margin-bottom: 16px;
}.empty-state span {font-size: 14px;font-weight: 500;
}.panel-footer {padding: 12px 20px;background: linear-gradient(135deg, #f8fafc, #f1f5f9);border-top: 1px solid rgba(229, 231, 235, 0.3);
}.scroll-hint {display: flex;align-items: center;justify-content: center;gap: 8px;font-size: 12px;color: #6b7280;font-weight: 500;
}.scroll-hint svg {width: 16px;height: 16px;fill: currentColor;animation: bounce 2s infinite;
}@keyframes bounce {0%,20%,50%,80%,100% {transform: translateY(0);}40% {transform: translateY(-6px);}60% {transform: translateY(-3px);}
}/* 動畫效果 */
.menu-slide-enter-active {transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}.menu-slide-leave-active {transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}.menu-slide-enter-from {opacity: 0;transform: scale(0.8) translateY(30px);
}.menu-slide-leave-to {opacity: 0;transform: scale(0.9) translateY(15px);
}/* 滾動條樣式 */
.menu-scroll-container::-webkit-scrollbar {width: 8px;
}.menu-scroll-container::-webkit-scrollbar-track {background: rgba(0, 0, 0, 0.03);border-radius: 4px;
}.menu-scroll-container::-webkit-scrollbar-thumb {background: linear-gradient(135deg, #cbd5e1, #94a3b8);border-radius: 4px;border: 1px solid rgba(255, 255, 255, 0.2);
}.menu-scroll-container::-webkit-scrollbar-thumb:hover {background: linear-gradient(135deg, #94a3b8, #64748b);
}/* 響應式設計 */
@media (max-width: 768px) {.submenu-panel {width: 280px;max-height: 360px;}.nav-trigger {width: 56px;height: 56px;}.nav-icon {width: 20px;height: 20px;}
}
</style>
OperateVideoDialog.vue(視頻播放)
<template><vxe-modal v-model="isVisible" :title="title" width="800" min-width="600" min-height="400" :show-footer="false" resizeremember transfer @close="close"><div class="video-demo-container"><video ref="videoPlayer" controls class="demo-video" :poster="poster" @play="onVideoPlay"><source :src="videoUrl" type="video/mp4">您的瀏覽器不支持視頻播放</video><div v-if="showTips" class="video-tips"><vxe-icon type="question-circle-fill"></vxe-icon><span>{{ tipsText }}</span></div></div></vxe-modal>
</template><script setup>
import { ref, watch } from 'vue'const props = defineProps({// 視頻地址(必傳)videoUrl: {type: String,required: true},// 彈框標題title: {type: String,default: '操作演示'},// 視頻封面圖poster: {type: String,default: ''},// 是否顯示提示文本showTips: {type: Boolean,default: true},// 提示文本內容tipsText: {type: String,default: '請按照視頻中的步驟進行操作'},// 是否自動播放autoPlay: {type: Boolean,default: false}
})const emit = defineEmits(['play', 'close'])const isVisible = ref(false)
const videoPlayer = ref(null)// 打開彈窗
const open = () => {isVisible.value = true
}// 關閉彈窗
const close = () => {isVisible.value = falseresetVideo()emit('close')
}// 重置視頻
const resetVideo = () => {if (videoPlayer.value) {videoPlayer.value.pause()videoPlayer.value.currentTime = 0}
}// 視頻播放事件
const onVideoPlay = () => {emit('play', props.videoUrl)
}// 自動播放處理
watch(isVisible, (val) => {if (val && props.autoPlay) {nextTick(() => {videoPlayer.value?.play()})}
})// 暴露方法給父組件
defineExpose({open,close
})
</script><style scoped>
.video-demo-container {position: relative;padding: 10px;
}.demo-video {width: 100%;border-radius: 4px;background: #000;aspect-ratio: 16/9;display: block;
}.video-tips {margin-top: 15px;padding: 10px;background-color: #f0f7ff;border-radius: 4px;display: flex;align-items: center;color: #409eff;
}.video-tips .vxe-icon {margin-right: 8px;font-size: 16px;
}
</style>
operateVideo.ts(獲取視頻url)
/*** 根據路由名稱生成視頻URL* @param routeName 路由名稱* @returns 視頻文件的完整URL,如果路由無效則拋出錯誤*/
export const getVideoUrl = async (routeName: any): Promise<string> => {if (!routeName) {throw new Error("該頁面暫無視頻演示");}const cleanRouteName = routeName.toString().trim().replace(/\//g, "").replace(/\*/g, "").replace(/\s+/g, "");if (!cleanRouteName) {throw new Error("該頁面暫無視頻演示");}const url = `https://api.ecom20200909.com/saasFile/video/${cleanRouteName}.mp4`;return url;
};