Files
陈春 3d68f56a94 feat: 新增作品和动态详情页
 Work 详情页 (/work/[slug]):
- Hero 大图 + 渐变遮罩
- 项目概述、挑战、解决方案
- 项目截图画廊
- 右侧信息卡片(客户、年份、周期、服务)
- 访问项目按钮
- 相关项目推荐
- 返回列表按钮
- 完整中英文内容

 News 详情页 (/news/[slug]):
- Hero 大图 + 渐变遮罩
- 分类标签 + 日期 + 阅读时间 + 作者
- 文章正文(多段落 + 引用 + 图片)
- 分享按钮(Twitter、LinkedIn、复制链接)
- 相关文章推荐
- 返回列表按钮
- 完整中英文内容

🔧 列表页更新:
- work.vue → work/index.vue
- news.vue → news/index.vue
- 卡片链接改为 NuxtLink 到详情页

🌍 国际化:
- 6 个作品详情(中文 + 英文)
- 6 篇文章详情(中文 + 英文)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 20:35:02 +08:00

290 lines
10 KiB
Vue
Raw Permalink 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>
<main class="min-h-screen">
<!-- 返回按钮 -->
<div class="fixed top-28 left-4 md:left-8 z-30">
<NuxtLink
to="/work"
class="inline-flex items-center gap-2 px-4 py-2.5 rounded-full bg-white/90 backdrop-blur-sm shadow-lg hover:shadow-xl transition-all duration-300 text-sm font-semibold text-black hover:-translate-x-1"
>
<Icon name="ph:arrow-left" class="w-5 h-5" />
{{ t('work.backToList') }}
</NuxtLink>
</div>
<!-- Hero 大图 -->
<section class="relative h-[60vh] md:h-[70vh] overflow-hidden">
<img
:src="project.image"
:alt="project.title"
class="w-full h-full object-cover"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent"></div>
<div class="absolute bottom-0 left-0 right-0 p-8 md:p-16">
<div class="max-w-[1200px] mx-auto">
<div class="flex flex-wrap gap-2 mb-4">
<span
v-for="tag in project.tags"
:key="tag"
class="px-3 py-1 text-xs md:text-sm bg-white/20 backdrop-blur-sm text-white rounded-full"
>
{{ tag }}
</span>
</div>
<h1 class="text-4xl md:text-6xl lg:text-7xl font-bold text-white mb-4 tracking-tight">
{{ project.title }}
</h1>
<p class="text-lg md:text-xl text-white/80 max-w-2xl">
{{ project.description }}
</p>
</div>
</div>
</section>
<!-- 项目信息 -->
<section class="max-w-[1200px] mx-auto px-6 md:px-10 py-16 md:py-24">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-12 lg:gap-16">
<!-- 左侧项目详情 -->
<div class="lg:col-span-2 space-y-12">
<div>
<h2 class="text-2xl md:text-3xl font-bold text-black mb-6">{{ t('work.detail.overview') }}</h2>
<p class="text-gray-700 text-base md:text-lg leading-relaxed">
{{ t(`work.detail.items.${slug}.overview`) }}
</p>
</div>
<div>
<h2 class="text-2xl md:text-3xl font-bold text-black mb-6">{{ t('work.detail.challenge') }}</h2>
<p class="text-gray-700 text-base md:text-lg leading-relaxed">
{{ t(`work.detail.items.${slug}.challenge`) }}
</p>
</div>
<div>
<h2 class="text-2xl md:text-3xl font-bold text-black mb-6">{{ t('work.detail.solution') }}</h2>
<p class="text-gray-700 text-base md:text-lg leading-relaxed">
{{ t(`work.detail.items.${slug}.solution`) }}
</p>
</div>
<!-- 项目截图 -->
<div class="space-y-6">
<h2 class="text-2xl md:text-3xl font-bold text-black">{{ t('work.detail.gallery') }}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div
v-for="(img, index) in project.gallery"
:key="index"
class="rounded-2xl overflow-hidden shadow-lg hover:shadow-2xl transition-shadow duration-300"
>
<img :src="img" :alt="`${project.title} screenshot ${index + 1}`" class="w-full h-auto" />
</div>
</div>
</div>
</div>
<!-- 右侧项目信息卡片 -->
<div class="space-y-8">
<div class="bg-gray-50 rounded-2xl p-6 md:p-8 sticky top-32">
<h3 class="text-lg font-bold text-black mb-6">{{ t('work.detail.projectInfo') }}</h3>
<div class="space-y-5">
<div>
<p class="text-sm text-gray-500 mb-1">{{ t('work.detail.client') }}</p>
<p class="text-base font-semibold text-black">{{ t(`work.detail.items.${slug}.client`) }}</p>
</div>
<div>
<p class="text-sm text-gray-500 mb-1">{{ t('work.detail.year') }}</p>
<p class="text-base font-semibold text-black">{{ project.year }}</p>
</div>
<div>
<p class="text-sm text-gray-500 mb-1">{{ t('work.detail.duration') }}</p>
<p class="text-base font-semibold text-black">{{ t(`work.detail.items.${slug}.duration`) }}</p>
</div>
<div>
<p class="text-sm text-gray-500 mb-1">{{ t('work.detail.services') }}</p>
<div class="flex flex-wrap gap-2 mt-2">
<span
v-for="tag in project.tags"
:key="tag"
class="px-3 py-1 text-xs bg-black text-white rounded-full"
>
{{ tag }}
</span>
</div>
</div>
</div>
<!-- 访问项目按钮 -->
<a
v-if="project.url"
:href="project.url"
target="_blank"
rel="noopener noreferrer"
class="mt-8 w-full inline-flex items-center justify-center gap-2 px-6 py-3.5 rounded-full bg-black text-white font-semibold hover:bg-gray-800 transition-colors duration-300"
>
{{ t('work.detail.visitProject') }}
<Icon name="ph:arrow-up-right" class="w-5 h-5" />
</a>
</div>
</div>
</div>
</section>
<!-- 相关项目 -->
<section class="bg-gray-50 py-16 md:py-24">
<div class="max-w-[1200px] mx-auto px-6 md:px-10">
<h2 class="text-2xl md:text-3xl font-bold text-black mb-10 text-center">{{ t('work.detail.relatedProjects') }}</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<NuxtLink
v-for="related in relatedProjects"
:key="related.id"
:to="`/work/${related.id}`"
class="group block rounded-2xl overflow-hidden bg-white shadow-lg hover:shadow-xl transition-all duration-300 hover:-translate-y-1"
>
<div class="relative aspect-[4/3] overflow-hidden">
<img :src="related.image" :alt="related.title" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" />
</div>
<div class="p-5">
<h3 class="text-lg font-bold text-black group-hover:text-gray-600 transition-colors">{{ related.title }}</h3>
<p class="text-sm text-gray-500 mt-1">{{ related.tags[0] }}</p>
</div>
</NuxtLink>
</div>
</div>
</section>
</main>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useSeoMeta, createError } from '#imports'
const { t } = useI18n()
const route = useRoute()
const slug = route.params.slug as string
interface WorkItem {
id: string
title: string
description: string
image: string
tags: string[]
category: string
year: string
url?: string
gallery: string[]
}
const workData: Record<string, WorkItem> = {
'forma-digital': {
id: 'forma-digital',
title: 'Forma Digital',
description: 'A modern e-commerce platform with seamless user experience and dynamic product showcases.',
image: 'https://picsum.photos/id/201/1200/800',
tags: ['Web Design', 'E-commerce', 'React'],
category: 'webDesign',
year: '2026',
url: '#',
gallery: [
'https://picsum.photos/id/201/800/600',
'https://picsum.photos/id/202/800/600',
'https://picsum.photos/id/203/800/600',
'https://picsum.photos/id/204/800/600',
]
},
'one-step': {
id: 'one-step',
title: 'One Step',
description: 'Brand identity and marketing campaign for a fitness app focused on sustainable habits.',
image: 'https://picsum.photos/id/180/1200/800',
tags: ['Branding', 'Marketing', 'UI/UX'],
category: 'branding',
year: '2024',
gallery: [
'https://picsum.photos/id/180/800/600',
'https://picsum.photos/id/181/800/600',
'https://picsum.photos/id/182/800/600',
'https://picsum.photos/id/183/800/600',
]
},
'nero-vision': {
id: 'nero-vision',
title: 'Nero Vision',
description: 'Full-stack web application for creative professionals to manage their portfolios.',
image: 'https://picsum.photos/id/119/1200/800',
tags: ['Development', 'Vue.js', 'Node.js'],
category: 'development',
year: '2025',
gallery: [
'https://picsum.photos/id/119/800/600',
'https://picsum.photos/id/120/800/600',
'https://picsum.photos/id/121/800/600',
'https://picsum.photos/id/122/800/600',
]
},
'bold-moves': {
id: 'bold-moves',
title: 'Bold Moves',
description: 'Interactive website showcasing innovative architectural designs with 3D visualizations.',
image: 'https://picsum.photos/id/164/1200/800',
tags: ['Web Design', '3D', 'Three.js'],
category: 'webDesign',
year: '2024',
gallery: [
'https://picsum.photos/id/164/800/600',
'https://picsum.photos/id/165/800/600',
'https://picsum.photos/id/166/800/600',
'https://picsum.photos/id/167/800/600',
]
},
'echo-labs': {
id: 'echo-labs',
title: 'Echo Labs',
description: 'Corporate branding and visual identity for a tech startup in the AI space.',
image: 'https://picsum.photos/id/101/1200/800',
tags: ['Branding', 'Visual Identity', 'Corporate'],
category: 'branding',
year: '2025',
gallery: [
'https://picsum.photos/id/101/800/600',
'https://picsum.photos/id/102/800/600',
'https://picsum.photos/id/103/800/600',
'https://picsum.photos/id/104/800/600',
]
},
'flux-api': {
id: 'flux-api',
title: 'Flux API',
description: 'Scalable REST API with comprehensive documentation and developer tools.',
image: 'https://picsum.photos/id/326/1200/800',
tags: ['Development', 'API', 'Python'],
category: 'development',
year: '2025',
gallery: [
'https://picsum.photos/id/326/800/600',
'https://picsum.photos/id/327/800/600',
'https://picsum.photos/id/328/800/600',
'https://picsum.photos/id/329/800/600',
]
}
}
const project = workData[slug]
if (!project) {
throw createError({ statusCode: 404, statusMessage: 'Project not found' })
}
const relatedProjects = computed(() => {
return Object.values(workData)
.filter(p => p.id !== slug)
.slice(0, 3)
})
useSeoMeta({
title: () => `${project.title} - ${t('work.title')}`,
description: () => project.description,
})
</script>