fix(vibe-stories): finalize laptop viewport sizing

This commit is contained in:
sanbuphy
2026-03-29 13:32:26 +08:00
parent 384638d128
commit a49d0483a3
@@ -72,39 +72,50 @@ const tStories = computed(() => [
} }
]) ])
const defaultScreenViewport = Object.freeze({
left: 19,
top: 1.75,
width: 62.75,
height: 71.5,
radius: 12
})
const currentIndex = ref(0) const currentIndex = ref(0)
let autoplayTimer = null let autoplayTimer = null
const isPaginating = ref(false) const isPaginating = ref(false)
const containerRef = ref(null) const containerRef = ref(null)
const laptopRef = ref(null)
let wheelHandler = null let wheelHandler = null
let resizeObserver = null
const screenRegion = ref({ const LAPTOP_ASPECT_RATIO = 2675 / 4608
centerX: 50, const laptopHeightPx = ref(null)
centerY: 45.75,
width: 80, // Visible image container geometry relative to `.laptop-container`.
height: 90, // Adjust these five values directly to control the screen viewport.
radius: 4 const screenViewport = ref({ ...defaultScreenViewport })
})
const formatPercent = (value) => `${value}%` const formatPercent = (value) => `${value}%`
const formatPixels = (value) => `${value}px` const formatPixels = (value) => `${value}px`
const screenContentStyle = computed(() => ({ // The percentages below are always resolved against `.laptop-container`.
top: formatPercent(screenRegion.value.centerY - screenRegion.value.height / 2), const screenViewportStyle = computed(() => ({
left: formatPercent(screenRegion.value.centerX - screenRegion.value.width / 2), '--screen-left': formatPercent(screenViewport.value.left),
width: formatPercent(screenRegion.value.width), '--screen-top': formatPercent(screenViewport.value.top),
height: formatPercent(screenRegion.value.height), '--screen-width': formatPercent(screenViewport.value.width),
borderRadius: formatPixels(screenRegion.value.radius) '--screen-height': formatPercent(screenViewport.value.height),
'--screen-radius': formatPixels(screenViewport.value.radius)
})) }))
const currentStory = computed(() => tStories.value[currentIndex.value] ?? tStories.value[0]) const currentStory = computed(() => tStories.value[currentIndex.value] ?? tStories.value[0])
// Keep crop settings explicit in source so they survive reload/HMR. const currentImageStyle = computed(() => currentStory.value?.imageStyle || {})
const currentImageStyle = computed(() => ({
objectFit: 'cover', const laptopContainerStyle = computed(() => (
objectPosition: 'center center', laptopHeightPx.value
...(currentStory.value?.imageStyle || {}) ? { height: `${laptopHeightPx.value}px` }
})) : {}
))
const transitionName = ref('slide-left') const transitionName = ref('slide-left')
@@ -149,9 +160,18 @@ const stopAutoplay = () => {
} }
} }
const updateLaptopHeight = () => {
const laptop = laptopRef.value
if (!laptop) return
const nextHeight = laptop.clientWidth * LAPTOP_ASPECT_RATIO
laptopHeightPx.value = nextHeight > 0 ? nextHeight : null
}
onMounted(() => { onMounted(() => {
startAutoplay() startAutoplay()
const container = containerRef.value const container = containerRef.value
const laptop = laptopRef.value
if (!container) return if (!container) return
wheelHandler = (e) => { wheelHandler = (e) => {
@@ -166,6 +186,17 @@ onMounted(() => {
} }
container.addEventListener('wheel', wheelHandler, { passive: false }) container.addEventListener('wheel', wheelHandler, { passive: false })
updateLaptopHeight()
if (typeof ResizeObserver !== 'undefined' && laptop) {
resizeObserver = new ResizeObserver(() => {
updateLaptopHeight()
})
resizeObserver.observe(laptop)
} else if (typeof window !== 'undefined') {
window.addEventListener('resize', updateLaptopHeight)
}
}) })
onUnmounted(() => { onUnmounted(() => {
@@ -174,6 +205,13 @@ onUnmounted(() => {
if (container && wheelHandler) { if (container && wheelHandler) {
container.removeEventListener('wheel', wheelHandler) container.removeEventListener('wheel', wheelHandler)
} }
if (resizeObserver) {
resizeObserver.disconnect()
resizeObserver = null
} else if (typeof window !== 'undefined') {
window.removeEventListener('resize', updateLaptopHeight)
}
}) })
</script> </script>
@@ -185,7 +223,7 @@ onUnmounted(() => {
</div> </div>
<div class="laptop-wrapper" @mouseenter="stopAutoplay" @mouseleave="startAutoplay"> <div class="laptop-wrapper" @mouseenter="stopAutoplay" @mouseleave="startAutoplay">
<div class="laptop-container"> <div ref="laptopRef" class="laptop-container" :style="laptopContainerStyle">
<!-- Navigation Controls --> <!-- Navigation Controls -->
<button class="nav-btn prev" :aria-label="t.stories?.ui?.prevLabel || 'Previous story'" @click="prev"> <button class="nav-btn prev" :aria-label="t.stories?.ui?.prevLabel || 'Previous story'" @click="prev">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6" /></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6" /></svg>
@@ -194,7 +232,7 @@ onUnmounted(() => {
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg>
</button> </button>
<div class="screen-content" :style="screenContentStyle"> <div class="screen-content" :style="screenViewportStyle">
<a :href="withBase(currentStory.link)" class="screen-link"> <a :href="withBase(currentStory.link)" class="screen-link">
<transition :name="transitionName"> <transition :name="transitionName">
<div :key="currentStory.id" class="screen-image-wrapper"> <div :key="currentStory.id" class="screen-image-wrapper">
@@ -304,13 +342,15 @@ onUnmounted(() => {
width: 100%; width: 100%;
max-width: 700px; max-width: 700px;
margin: 0 auto; margin: 0 auto;
aspect-ratio: 4608 / 2675;
} }
.laptop-frame { .laptop-frame {
position: relative; position: relative;
z-index: 10; z-index: 10;
width: 100%; width: 100%;
height: auto; height: 100%;
object-fit: contain;
pointer-events: none; pointer-events: none;
filter: drop-shadow(0 25px 25px rgb(0 0 0 / 0.15)); filter: drop-shadow(0 25px 25px rgb(0 0 0 / 0.15));
} }
@@ -322,6 +362,11 @@ onUnmounted(() => {
.screen-content { .screen-content {
position: absolute; position: absolute;
z-index: 1; z-index: 1;
top: var(--screen-top);
left: var(--screen-left);
width: var(--screen-width);
height: var(--screen-height);
border-radius: var(--screen-radius);
background: #0b0b0f; background: #0b0b0f;
overflow: hidden; overflow: hidden;
perspective: 1000px; perspective: 1000px;