e2efce550f
点击图片全屏遮罩放大,Esc 或点击遮罩关闭,纯自定义实现无额外依赖。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
96 lines
1.7 KiB
Vue
96 lines
1.7 KiB
Vue
<script setup lang="ts">
|
|
const props = defineProps<{
|
|
src?: string
|
|
alt?: string
|
|
}>()
|
|
|
|
const route = useRoute()
|
|
const isZoomed = ref(false)
|
|
|
|
const resolvedSrc = computed(() => {
|
|
if (!props.src) return ''
|
|
if (props.src.startsWith('/') || props.src.startsWith('http')) return props.src
|
|
const base = route.path.replace(/\/$/, '')
|
|
return `${base}/${props.src}`
|
|
})
|
|
|
|
function open() {
|
|
isZoomed.value = true
|
|
}
|
|
|
|
function close() {
|
|
isZoomed.value = false
|
|
}
|
|
|
|
function onKeydown(e: KeyboardEvent) {
|
|
if (e.key === 'Escape') close()
|
|
}
|
|
|
|
onMounted(() => document.addEventListener('keydown', onKeydown))
|
|
onUnmounted(() => document.removeEventListener('keydown', onKeydown))
|
|
</script>
|
|
|
|
<template>
|
|
<img
|
|
:src="resolvedSrc"
|
|
:alt="alt"
|
|
loading="lazy"
|
|
decoding="async"
|
|
class="zoomable-img"
|
|
@click="open"
|
|
/>
|
|
|
|
<ClientOnly>
|
|
<Teleport to="body">
|
|
<Transition name="zoom-fade">
|
|
<div
|
|
v-if="isZoomed"
|
|
class="zoom-overlay"
|
|
@click="close"
|
|
>
|
|
<img
|
|
:src="resolvedSrc"
|
|
:alt="alt"
|
|
class="zoom-img"
|
|
/>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</ClientOnly>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.zoomable-img {
|
|
cursor: zoom-in;
|
|
}
|
|
|
|
.zoom-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 9999;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
cursor: zoom-out;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.zoom-img {
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
object-fit: contain;
|
|
border-radius: 0.5rem;
|
|
}
|
|
|
|
.zoom-fade-enter-active,
|
|
.zoom-fade-leave-active {
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
|
|
.zoom-fade-enter-from,
|
|
.zoom-fade-leave-to {
|
|
opacity: 0;
|
|
}
|
|
</style>
|