Files
nl-blogs/client/src/pages/WorkDetail.vue
2026-07-04 17:58:19 +08:00

253 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<section id="work-detail" class="work-detail-page page-section block">
<button @click="$router.push('/works')" class="fixed top-8 right-8 z-[60] mix-blend-difference text-white hover:scale-110 transition-transform">
<div class="rounded-full border border-white/20 p-4 backdrop-blur-md bg-white/5"><Icon name="x" :size="24" /></div>
</button>
<div class="fixed top-0 left-0 h-1 bg-art-accent z-[60] w-0 transition-all duration-100" id="scroll-progress"></div>
<!-- Loading state -->
<div v-if="loading" class="fixed inset-0 bg-black flex items-center justify-center z-[99]">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-art-accent"></div>
</div>
<!-- Error state -->
<div v-else-if="error" class="fixed inset-0 bg-black flex flex-col items-center justify-center z-[99] p-8">
<p class="text-red-500 text-xl mb-4">{{ error }}</p>
<button @click="fetchWorkDetails" class="px-6 py-3 bg-art-accent text-white rounded hover:bg-opacity-80 transition-colors">
重试
</button>
<button @click="$router.push('/works')" class="mt-4 px-6 py-3 border border-white/30 text-white rounded hover:bg-white/5 transition-colors">
返回作品列表
</button>
</div>
<!-- Work content -->
<div v-else class="relative w-full" id="work-detail-content">
<div class="relative h-[85vh] w-full overflow-hidden group">
<img
id="work-hero-img"
:src="work.heroImg"
class="absolute inset-0 w-full h-full object-cover filter grayscale group-hover:grayscale-0 transition-all duration-1000 scale-105"
:alt="work.title"
>
<div class="absolute inset-0 bg-black/40"></div>
<!-- Play Button Overlay有视频时显示点击弹层播放不覆盖主图 -->
<div
v-if="work.videoUrl"
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-24 h-24 rounded-full border border-white/20 bg-white/5 backdrop-blur-md flex items-center justify-center cursor-pointer hover:scale-110 hover:bg-white/10 transition-all duration-500 group-hover:border-art-accent/50 z-20"
@click="videoModalOpen = true"
>
<Icon name="play" :size="32" class="text-white fill-white ml-1" />
<div class="absolute inset-0 rounded-full border border-white/20 animate-pulse-slow"></div>
</div>
<div class="absolute bottom-0 left-0 p-8 md:p-20 w-full">
<div class="flex items-end justify-between border-t border-white/30 pt-8 animate-slide-down">
<div>
<span id="work-category" class="font-mono text-art-accent text-sm tracking-[0.2em] uppercase mb-2 block">{{ work.category }}</span>
<h1 id="work-title" class="font-serif text-5xl md:text-8xl text-white leading-[0.9] mix-blend-overlay">{{ work.title }}</h1>
</div>
<span id="work-year" class="hidden md:block font-mono text-white/50 text-xl">{{ work.year }}</span>
</div>
</div>
</div>
<div class="flex flex-col md:flex-row max-w-[1920px] mx-auto min-h-screen">
<div class="md:w-1/3 p-8 md:p-16 md:sticky md:top-0 md:h-screen md:max-h-screen overflow-y-auto custom-scrollbar flex flex-col justify-between border-r border-white/5 bg-[#050505]">
<div class="space-y-12">
<div>
<h3 class="font-mono text-xs text-art-muted uppercase tracking-widest mb-4">关于项目</h3>
<div id="work-desc" class="text-white/80 font-light leading-relaxed text-lg font-serif" v-html="work.desc"></div>
</div>
<div>
<h3 class="font-mono text-xs text-art-muted uppercase tracking-widest mb-6">技术全景</h3>
<div id="work-tech-stack" class="space-y-6">
<div v-for="(tech, index) in work.techStack" :key="index">
<div class="tech-category-title">{{ tech.category }}</div>
<div class="tech-grid">
<span v-for="(item, itemIndex) in tech.items" :key="itemIndex" class="tech-item">{{ item }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="mt-12 space-y-4">
<!-- Live Link -->
<a
v-if="work.links.live"
id="work-link-live"
:href="work.links.live"
target="_blank"
class="group flex items-center justify-between w-full py-4 border-t border-white/10 hover:bg-white/5 transition-colors"
>
<span class="font-serif text-xl italic text-white group-hover:text-art-accent transition-colors flex items-center gap-3">
<Icon name="globe" :size="20" />
访问线上项目
</span>
<Icon name="arrow-up-right" :size="20" class="text-white group-hover:rotate-45 transition-transform" />
</a>
<!-- Demo Link -->
<a
v-if="work.links.demo"
id="work-link-demo"
:href="work.links.demo"
target="_blank"
class="group flex items-center justify-between w-full py-4 border-t border-white/10 hover:bg-white/5 transition-colors"
>
<span class="font-serif text-xl italic text-white group-hover:text-art-accent transition-colors flex items-center gap-3">
<Icon name="play-circle" :size="20" />
查看演示
</span>
<Icon name="arrow-up-right" :size="20" class="text-white group-hover:rotate-45 transition-transform" />
</a>
<!-- GitHub Link -->
<a
v-if="work.links.github"
id="work-link-github"
:href="work.links.github"
target="_blank"
class="group flex items-center justify-between w-full py-4 border-t border-white/10 hover:bg-white/5 transition-colors"
>
<span class="font-serif text-xl italic text-white group-hover:text-art-accent transition-colors flex items-center gap-3">
<Icon name="github" :size="20" />
查看源码
</span>
<Icon name="arrow-up-right" :size="20" class="text-white group-hover:rotate-45 transition-transform" />
</a>
</div>
</div>
<div class="md:w-2/3 bg-[#080808] flex flex-col">
<div id="work-gallery" class="flex flex-col flex-grow">
<template v-for="(img, index) in work.gallery" :key="index">
<!-- Alternating layout logic -->
<img
v-if="index % 2 === 0"
:src="img"
:alt="`${work.title} - 图片 ${index + 1}`"
class="gallery-image w-full object-cover aspect-video mb-8 md:mb-16 grayscale hover:grayscale-0 transition-all duration-700"
>
<div v-else class="flex justify-end mb-8 md:mb-16">
<img
:src="img"
:alt="`${work.title} - 图片 ${index + 1}`"
class="gallery-image w-4/5 object-cover aspect-[4/3] grayscale hover:grayscale-0 transition-all duration-700"
>
</div>
</template>
</div>
<div
class="h-[40vh] flex items-center justify-center border-t border-white/5 bg-[#050505] cursor-pointer group hover:bg-white/5 transition-colors"
@click="openNextWork"
>
<div class="text-center">
<p class="font-mono text-xs text-art-muted mb-4 tracking-widest">下一个作品</p>
<h2 class="font-serif text-5xl text-white group-hover:italic transition-all">返回作品列表</h2>
</div>
</div>
<!-- <footer class="py-8 text-center border-t border-white/5 relative z-10 bg-[#050505]"><p class="text-art-muted text-xs font-mono tracking-widest opacity-50">&copy; 2024 年糕崽崽. 保留所有权利.</p></footer>-->
</div>
</div>
<DPlayerModal
:visible="videoModalOpen"
:video-url="work.videoUrl || ''"
:poster="work.heroImg"
:title="work.title"
@close="videoModalOpen = false"
/>
</div>
</section>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { fetchWork, Work } from '../services/api'
import { useScrollAnimation } from '../composables/useScrollAnimation'
import Icon from '../components/Icon.vue'
import DPlayerModal from '../components/DPlayerModal.vue'
const route = useRoute()
const router = useRouter()
const workId = route.params.id as string
const work = ref<Work>({
id: workId,
title: '加载中...',
category: '',
year: '',
heroImg: 'https://via.placeholder.com/1600x900',
desc: '<p>加载中...</p>',
techStack: [],
gallery: [],
links: {},
next: ''
})
const loading = ref(true)
const error = ref('')
const videoModalOpen = ref(false)
const { initObserver } = useScrollAnimation()
const updateScrollProgress = () => {
const scrollTop = window.scrollY
const docHeight = document.documentElement.scrollHeight
const winHeight = window.innerHeight
const scrollPercent = scrollTop / (docHeight - winHeight)
const progressBar = document.getElementById('scroll-progress')
if (progressBar) {
progressBar.style.width = `${scrollPercent * 100}%`
}
}
const openNextWork = () => {
if (work.value.next) {
router.push(`/works/${work.value.next}`)
} else {
router.push('/works')
}
}
const fetchWorkDetails = async () => {
loading.value = true
error.value = ''
try {
const workData = await fetchWork(workId)
if (workData) {
work.value = workData
// 初始化动画 - 在数据加载完成后
initObserver()
} else {
error.value = '未找到该作品'
}
} catch (err) {
error.value = '获取作品详情失败,请稍后重试'
console.error('Error fetching work details:', err)
} finally {
loading.value = false
}
}
// 监听路由变化,重新获取数据
watch(() => route.params.id, (newId) => {
if (newId) {
// 强制刷新当前组件状态
work.value = { ...work.value, id: newId as string, title: '加载中...' }
window.scrollTo(0, 0)
fetchWorkDetails()
}
})
onMounted(() => {
fetchWorkDetails()
// Icons are now handled by Vue components
// 添加滚动事件监听
window.addEventListener('scroll', updateScrollProgress)
})
onBeforeUnmount(() => {
// 移除滚动事件监听
window.removeEventListener('scroll', updateScrollProgress)
})
</script>