Files
super_video/src/components/Guide/SpotlightOverlay.vue
T

91 lines
2.0 KiB
Vue

<template>
<div v-if="visible" class="spotlight-overlay">
<div
class="spotlight-hole"
:style="{
top: rect.top + 'px',
left: rect.left + 'px',
width: rect.width + 'px',
height: rect.height + 'px',
borderRadius: borderRadius + 'px',
boxShadow: `0 0 0 9999px rgba(0,0,0,0.6)`
}"
></div>
</div>
</template>
<script setup lang="ts">
import { reactive, watch, onMounted, onBeforeUnmount } from 'vue'
interface Props {
/** 是否可见 */
visible?: boolean
/** 目标元素选择器 */
targetSelector?: string | null
/** 额外内边距 */
padding?: number
/** 圆角 */
borderRadius?: number
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
targetSelector: null,
padding: 8,
borderRadius: 16
})
const rect = reactive({ top: 0, left: 0, width: 0, height: 0 })
const measure = () => {
if (!props.visible || !props.targetSelector) return
const el = document.querySelector(props.targetSelector) as HTMLElement | null
if (!el) return
const r = el.getBoundingClientRect()
const pad = Math.max(0, props.padding)
// 使用视口坐标(fixed层),不叠加滚动偏移
rect.top = r.top - pad
rect.left = r.left - pad
rect.width = r.width + pad * 2
rect.height = r.height + pad * 2
}
watch(
() => [props.visible, props.targetSelector, props.padding],
() => {
requestAnimationFrame(measure)
},
{ immediate: true }
)
const onResize = () => measure()
const onScroll = () => measure()
onMounted(() => {
window.addEventListener('resize', onResize)
window.addEventListener('scroll', onScroll, { passive: true })
measure()
})
onBeforeUnmount(() => {
window.removeEventListener('resize', onResize)
window.removeEventListener('scroll', onScroll)
})
const borderRadius = props.borderRadius
</script>
<style scoped>
.spotlight-overlay {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 100000;
}
.spotlight-hole {
position: absolute;
pointer-events: none;
}
</style>