Files
test-repo/docs/.vitepress/theme/components/VibeStories.vue
T

490 lines
11 KiB
Vue
Raw Normal View History

<script setup>
import { inject, onMounted, onUnmounted, ref } from 'vue'
import { withBase } from 'vitepress'
import macbookImage from '../../../../assets/macbook.png'
// Try to inject translation context from parent or provide a default fallback
const t = inject('t', {
value: {
stories: {
cat: 'Vibe Stories',
title: '看见每一个<br><span class="highlight">闪光的你。</span>',
sub: '在这里,发现大家如何使用 AI 创造属于自己的作品。'
}
}
})
const tStories = [
{
id: 1,
title: t.value?.stories?.s1?.title || '我的第一个全栈项目',
author: t.value?.stories?.s1?.author || 'Sanbu',
avatar: '👨‍💻',
image: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=2564&auto=format&fit=crop',
link: '/zh-cn/vibe-stories/story-1'
},
{
id: 2,
title: t.value?.stories?.s2?.title || '从产品经理到独立开发者',
author: t.value?.stories?.s2?.author || 'Alice',
avatar: '👩‍🎨',
image: 'https://images.unsplash.com/photo-1550745165-9bc0b252726f?q=80&w=2564&auto=format&fit=crop',
link: '/zh-cn/vibe-stories/story-2'
},
{
id: 3,
title: t.value?.stories?.s3?.title || '用 AI 提效 10 倍',
author: t.value?.stories?.s3?.author || 'Bob',
avatar: '🚀',
image: 'https://images.unsplash.com/photo-1558655146-d09347e92766?q=80&w=2564&auto=format&fit=crop',
link: '/zh-cn/vibe-stories/story-3'
}
]
const currentIndex = ref(0)
let autoplayTimer = null
const isPaginating = ref(false)
const containerRef = ref(null)
let wheelHandler = null
const transitionName = ref('slide-left')
const next = () => {
if (isPaginating.value) return
isPaginating.value = true
transitionName.value = 'slide-left'
currentIndex.value = (currentIndex.value + 1) % tStories.length
setTimeout(() => {
isPaginating.value = false
}, 800)
}
const prev = () => {
if (isPaginating.value) return
isPaginating.value = true
transitionName.value = 'slide-right'
currentIndex.value = (currentIndex.value - 1 + tStories.length) % tStories.length
setTimeout(() => {
isPaginating.value = false
}, 800)
}
const setIndex = (index) => {
if (index === currentIndex.value) return
transitionName.value = index > currentIndex.value ? 'slide-left' : 'slide-right'
currentIndex.value = index
}
const startAutoplay = () => {
autoplayTimer = setInterval(() => {
if (!isPaginating.value) {
transitionName.value = 'slide-left'
currentIndex.value = (currentIndex.value + 1) % tStories.length
}
}, 4000)
}
const stopAutoplay = () => {
if (autoplayTimer) {
clearInterval(autoplayTimer)
}
}
onMounted(() => {
startAutoplay()
const container = containerRef.value
if (!container) return
wheelHandler = (e) => {
if (Math.abs(e.deltaX) > 20 && Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
e.preventDefault()
if (e.deltaX > 0) {
next()
} else {
prev()
}
}
}
container.addEventListener('wheel', wheelHandler, { passive: false })
})
onUnmounted(() => {
stopAutoplay()
const container = containerRef.value
if (container && wheelHandler) {
container.removeEventListener('wheel', wheelHandler)
}
})
</script>
<template>
<div ref="containerRef" class="vibe-stories-container">
<div class="section-header">
<h3 class="section-headline" v-html="t.stories?.title || '看见每一个<br><span class=\'highlight\'>闪光的你。</span>'"></h3>
<p class="section-sub">{{ t.stories?.sub || '在这里,发现大家如何使用 AI 创造属于自己的作品。' }}</p>
</div>
<div class="laptop-wrapper" @mouseenter="stopAutoplay" @mouseleave="startAutoplay">
<div class="laptop-container">
<!-- Navigation Controls -->
<button class="nav-btn prev" aria-label="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>
</button>
<button class="nav-btn next" aria-label="Next story" @click="next">
<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>
<!-- Screen Content -->
<div class="screen-content">
<a :href="withBase(tStories[currentIndex].link)" class="screen-link">
<transition :name="transitionName">
<div :key="currentIndex" class="screen-image-wrapper">
<img
:src="tStories[currentIndex].image"
class="screen-image"
alt="Story screenshot"
/>
</div>
</transition>
</a>
</div>
<!-- Laptop Frame -->
<img :src="macbookImage" class="laptop-frame" alt="MacBook Frame" />
</div>
<!-- Story Info & Avatar -->
<div class="story-info">
<div class="story-avatar">{{ tStories[currentIndex].avatar }}</div>
<div class="story-text">
<a :href="withBase(tStories[currentIndex].link)" class="story-title">
{{ tStories[currentIndex].title }}
</a>
<div class="story-author">by {{ tStories[currentIndex].author }}</div>
</div>
</div>
<!-- Indicators -->
<div class="indicators">
<button
v-for="(_, index) in tStories"
:key="index"
class="indicator-dot"
:class="{ active: index === currentIndex }"
aria-label="Select story"
@click="setIndex(index)"
></button>
</div>
</div>
</div>
</template>
<style scoped>
.vibe-stories-container {
max-width: 1120px;
margin: 0 auto;
padding: 0 20px 28px;
text-align: center;
}
.section-header {
margin-bottom: 24px;
}
.section-headline {
font-size: 60px;
line-height: 1.08;
font-weight: 700;
letter-spacing: -0.034em;
margin-bottom: 10px;
color: #1d1d1f;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'PingFang SC', sans-serif;
}
.dark .section-headline {
color: #f5f5f7;
}
.highlight {
background: linear-gradient(120deg, #0066cc, #3399ff);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.dark .highlight {
background: linear-gradient(120deg, #2997ff, #66b3ff);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.section-sub {
font-size: 19px;
line-height: 1.4;
font-weight: 400;
letter-spacing: -0.01em;
color: #6e6e73;
max-width: 760px;
margin: 0 auto;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'PingFang SC', sans-serif;
}
.dark .section-sub {
color: #a1a1a6;
}
.laptop-wrapper {
position: relative;
width: 100%;
margin-top: 0;
}
.laptop-container {
position: relative;
width: 100%;
max-width: 700px;
margin: 0 auto;
}
.laptop-frame {
position: relative;
z-index: 10;
width: 100%;
height: auto;
pointer-events: none;
filter: drop-shadow(0 25px 25px rgb(0 0 0 / 0.15));
}
.dark .laptop-frame {
filter: drop-shadow(0 25px 25px rgb(255 255 255 / 0.05));
}
.screen-content {
position: absolute;
z-index: 1;
background: transparent;
top: 0;
left: 10%;
width: 80%;
height: 92%;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
overflow: hidden;
perspective: 1000px;
/* Force hardware acceleration and fix Safari overflow clipping bug */
transform: translateZ(0);
-webkit-transform: translateZ(0);
-webkit-mask-image: -webkit-radial-gradient(white, black);
mask-image: radial-gradient(white, black);
isolation: isolate;
}
.screen-link {
display: block;
width: 100%;
height: 100%;
background: transparent;
position: relative;
overflow: hidden;
border-radius: inherit;
/* Fix boundary bleeding in Safari */
transform: translateZ(0);
-webkit-transform: translateZ(0);
mask-image: radial-gradient(white, black);
-webkit-mask-image: -webkit-radial-gradient(white, black);
}
.screen-image-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.screen-image {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
/* Transitions */
.slide-left-enter-active,
.slide-left-leave-active,
.slide-right-enter-active,
.slide-right-leave-active {
transition: transform 0.6s cubic-bezier(0.25, 1, 0.5, 1);
will-change: transform;
}
.slide-left-enter-from {
transform: translateX(100%);
}
.slide-left-leave-to {
transform: translateX(-100%);
}
.slide-right-enter-from {
transform: translateX(-100%);
}
.slide-right-leave-to {
transform: translateX(100%);
}
/* Nav Buttons */
.nav-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 20;
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(8px);
border: none;
border-radius: 50%;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #333;
opacity: 0;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.laptop-wrapper:hover .nav-btn {
opacity: 1;
}
.nav-btn:hover {
background: rgba(255, 255, 255, 0.9);
transform: translateY(-50%) scale(1.1);
}
.nav-btn.prev {
left: 20px;
}
.nav-btn.next {
right: 20px;
}
@media (max-width: 768px) {
.nav-btn {
opacity: 1;
width: 36px;
height: 36px;
}
.nav-btn.prev { left: 10px; }
.nav-btn.next { right: 10px; }
.section-headline { font-size: 42px; }
.section-sub { font-size: 17px; }
.laptop-container { max-width: 100%; }
.story-info {
margin-top: 18px;
gap: 12px;
}
}
/* Story Info */
.story-info {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-top: 22px;
}
.story-avatar {
font-size: 48px;
line-height: 1;
background: #f5f5f7;
border-radius: 50%;
width: 72px;
height: 72px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.dark .story-avatar {
background: #2c2c2e;
}
.story-text {
text-align: left;
}
.story-title {
display: block;
font-size: 20px;
font-weight: 600;
color: #1d1d1f;
text-decoration: none;
margin-bottom: 4px;
transition: color 0.2s;
}
.dark .story-title {
color: #f5f5f7;
}
.story-title:hover {
color: #0066cc;
}
.dark .story-title:hover {
color: #2997ff;
}
.story-author {
font-size: 15px;
color: #86868b;
}
/* Indicators */
.indicators {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 24px;
}
.indicator-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #d2d2d7;
border: none;
padding: 0;
cursor: pointer;
transition: all 0.3s ease;
}
.dark .indicator-dot {
background: #424245;
}
.indicator-dot:hover {
background: #86868b;
}
.indicator-dot.active {
width: 24px;
border-radius: 4px;
background: #1d1d1f;
}
.dark .indicator-dot.active {
background: #f5f5f7;
}
</style>