Files
nl-blogs/client/src/pages/WorkDetail.vue

173 lines
7.1 KiB
Vue
Raw Normal View History

2026-01-15 13:51:44 +08:00
<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"><i data-lucide="x" class="w-6 h-6"></i></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>
<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">
<a
id="work-link-live"
:href="work.links.live"
target="_blank"
class="group flex items-center justify-between w-full py-6 border-t border-white/10 hover:bg-white/5 transition-colors"
>
<span class="font-serif text-2xl italic text-white group-hover:text-art-accent transition-colors">访问线上项目</span>
<i data-lucide="arrow-up-right" class="w-6 h-6 text-white group-hover:rotate-45 transition-transform"></i>
</a>
</div>
</div>
<div class="md:w-2/3 bg-[#080808] flex flex-col">
<div id="work-gallery" class="flex flex-col flex-grow">
<img
v-for="(img, index) in work.gallery"
:key="index"
:src="img"
:alt="`${work.title} - 图片 ${index + 1}`"
class="gallery-image"
>
</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="$router.push('/works')"
>
<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>
</div>
</section>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { fetchWork, Work } from '../services/api'
import { useScrollAnimation } from '../composables/useScrollAnimation'
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: { live: '#' },
next: ''
})
const loading = ref(true)
const error = ref('')
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 fetchWorkDetails = async () => {
loading.value = true
error.value = ''
try {
const workData = await fetchWork(workId)
if (workData) {
work.value = workData
} else {
error.value = '未找到该作品'
}
} catch (err) {
error.value = '获取作品详情失败,请稍后重试'
console.error('Error fetching work details:', err)
} finally {
loading.value = false
}
}
onMounted(() => {
// 启用滚动动画
useScrollAnimation()
fetchWorkDetails()
// 初始化Lucide图标
if (window.lucide) {
window.lucide.createIcons()
}
// 添加滚动事件监听
window.addEventListener('scroll', updateScrollProgress)
})
onBeforeUnmount(() => {
// 移除滚动事件监听
window.removeEventListener('scroll', updateScrollProgress)
})
</script>