f4684f694e
✨ 新增功能: - Work 和 News 页面实现筛选功能 - Contact 页面联系表单提交功能(加载状态、成功/失败反馈) - Newsletter 订阅功能(邮箱验证、状态反馈) - 移动端汉堡菜单功能(全屏菜单、动画效果) - 用户登录/注册系统(支持账号/邮箱/手机号三合一) - 用户状态管理(Pinia store + localStorage 持久化) - 登录和注册弹窗组件(表单验证、错误提示) - 用户头像和下拉菜单(桌面端和移动端) 🎨 优化改进: - Footer 显示问题修复(多重备用机制) - 联系信息更新(邮箱改为 chenchun@virtheart.com,手机改为微信) - 完整的国际化支持(中英文双语) - 响应式设计优化 🔧 技术实现: - GSAP 动画系统 - 表单验证和反馈 - 状态持久化 - 自动生成用户头像 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
253 lines
8.2 KiB
Vue
Executable File
253 lines
8.2 KiB
Vue
Executable File
<template>
|
|
<main class="page-container min-h-screen pt-32 pb-20 px-4 md:px-8 mx-auto max-w-[1200px]">
|
|
<div
|
|
ref="headerWrapRef"
|
|
class="flex flex-col items-center justify-center text-center mb-10 md:mb-16"
|
|
style="opacity: 0; visibility: hidden;"
|
|
>
|
|
<h1 ref="titleRef" class="text-3xl md:text-5xl font-bold mb-4 md:mb-6 tracking-tight text-black">{{ t('work.title') }}</h1>
|
|
<p ref="descRef" class="text-base md:text-lg text-gray-600 max-w-2xl">{{ t('work.description') }}</p>
|
|
</div>
|
|
|
|
<div
|
|
ref="filterWrapRef"
|
|
class="flex justify-center gap-3 md:gap-4 mb-10 md:mb-14 flex-wrap"
|
|
style="opacity: 0; visibility: hidden;"
|
|
>
|
|
<button
|
|
v-for="category in categories"
|
|
:key="category"
|
|
@click="activeCategory = category"
|
|
class="px-5 md:px-6 py-2 md:py-2.5 rounded-full text-sm md:text-base font-semibold transition-all duration-300"
|
|
:class="activeCategory === category
|
|
? 'bg-black text-white shadow-lg'
|
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'"
|
|
>
|
|
{{ t(`work.categories.${category}`) }}
|
|
</button>
|
|
</div>
|
|
|
|
<div
|
|
ref="projectsWrapRef"
|
|
class="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8"
|
|
style="opacity: 0; visibility: hidden;"
|
|
>
|
|
<a
|
|
v-for="project in filteredWorkData"
|
|
:key="project.id"
|
|
:href="project.url"
|
|
class="work-card group relative block rounded-2xl md:rounded-3xl overflow-hidden cursor-pointer hover:shadow-2xl transition-all duration-500"
|
|
>
|
|
<div class="relative aspect-[4/3] overflow-hidden bg-gray-100">
|
|
<img
|
|
:src="project.image"
|
|
:alt="project.title"
|
|
class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
|
|
/>
|
|
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
|
</div>
|
|
|
|
<div class="p-6 md:p-8 bg-white">
|
|
<div class="flex items-start justify-between mb-3">
|
|
<h3 class="text-xl md:text-2xl font-bold text-black group-hover:text-gray-700 transition-colors duration-300">{{ project.title }}</h3>
|
|
<div class="w-9 h-9 md:w-10 md:h-10 rounded-full flex items-center justify-center bg-gray-50 text-gray-400 group-hover:bg-black group-hover:text-white transition-all duration-300 flex-shrink-0 ml-3">
|
|
<Icon name="ph:arrow-up-right" class="w-4 h-4 md:w-5 md:h-5" />
|
|
</div>
|
|
</div>
|
|
<p class="text-sm md:text-base text-gray-600 mb-4">{{ project.description }}</p>
|
|
<div class="flex flex-wrap gap-2">
|
|
<span
|
|
v-for="tag in project.tags"
|
|
:key="tag"
|
|
class="px-3 py-1 text-xs md:text-sm bg-gray-50 text-gray-700 rounded-full"
|
|
>
|
|
{{ tag }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
</main>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { nextTick, onBeforeUnmount, onMounted, ref, computed } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useSeoMeta } from '#imports'
|
|
import gsap from 'gsap'
|
|
import { useAppStore } from '~/stores/app'
|
|
|
|
const { t } = useI18n()
|
|
const appStore = useAppStore()
|
|
|
|
const headerWrapRef = ref<HTMLElement | null>(null)
|
|
const titleRef = ref<HTMLElement | null>(null)
|
|
const descRef = ref<HTMLElement | null>(null)
|
|
const filterWrapRef = ref<HTMLElement | null>(null)
|
|
const projectsWrapRef = ref<HTMLElement | null>(null)
|
|
const activeCategory = ref('all')
|
|
let projectsCtx: gsap.Context | null = null
|
|
|
|
const categories = ['all', 'webDesign', 'branding', 'development']
|
|
|
|
// 计算属性:根据选中的分类过滤项目
|
|
const filteredWorkData = computed(() => {
|
|
if (activeCategory.value === 'all') {
|
|
return workData
|
|
}
|
|
return workData.filter(item => item.category === activeCategory.value)
|
|
})
|
|
|
|
useSeoMeta({
|
|
title: () => t('work.title'),
|
|
description: () => t('work.description'),
|
|
})
|
|
|
|
const playPageIntro = async () => {
|
|
await nextTick()
|
|
if (!headerWrapRef.value || !titleRef.value || !descRef.value || !filterWrapRef.value || !projectsWrapRef.value) return
|
|
|
|
projectsCtx?.revert()
|
|
|
|
projectsCtx = gsap.context(() => {
|
|
const filterButtons = gsap.utils.toArray<HTMLElement>('button', filterWrapRef.value!)
|
|
const projectCards = gsap.utils.toArray<HTMLElement>('.work-card', projectsWrapRef.value!)
|
|
|
|
gsap.delayedCall(0, () => {
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
const tl = gsap.timeline({ defaults: { ease: 'power3.out' } })
|
|
|
|
tl.set(headerWrapRef.value!, { autoAlpha: 1 })
|
|
tl.fromTo(titleRef.value!, { autoAlpha: 0, y: 20 }, { autoAlpha: 1, y: 0, duration: 0.85, ease: 'power4.out', clearProps: 'transform' })
|
|
tl.fromTo(descRef.value!, { autoAlpha: 0, y: 16 }, { autoAlpha: 1, y: 0, duration: 0.7, ease: 'power4.out', clearProps: 'transform' }, '-=0.45')
|
|
|
|
tl.set(filterWrapRef.value!, { autoAlpha: 1 }, '-=0.2')
|
|
tl.fromTo(
|
|
filterButtons,
|
|
{ autoAlpha: 0, y: 20 },
|
|
{
|
|
autoAlpha: 1,
|
|
y: 0,
|
|
duration: 0.7,
|
|
ease: 'power4.out',
|
|
stagger: { each: 0.08, from: 'start' },
|
|
clearProps: 'transform',
|
|
},
|
|
'-=0.3'
|
|
)
|
|
|
|
tl.set(projectsWrapRef.value!, { autoAlpha: 1 }, '-=0.05')
|
|
tl.fromTo(
|
|
projectCards,
|
|
{ autoAlpha: 0, y: 34, scale: 0.965 },
|
|
{
|
|
autoAlpha: 1,
|
|
y: 0,
|
|
scale: 1,
|
|
duration: 0.95,
|
|
ease: 'power4.out',
|
|
stagger: { each: 0.13, from: 'start' },
|
|
clearProps: 'transform',
|
|
},
|
|
'+=0.08'
|
|
)
|
|
|
|
tl.set([headerWrapRef.value!, filterWrapRef.value!, projectsWrapRef.value!], { clearProps: 'opacity,visibility' })
|
|
})
|
|
})
|
|
})
|
|
}, headerWrapRef.value)
|
|
}
|
|
|
|
onMounted(() => {
|
|
const handleLoadingDone = () => {
|
|
playPageIntro()
|
|
}
|
|
|
|
if (!appStore.isAppLoading) {
|
|
playPageIntro()
|
|
return
|
|
}
|
|
|
|
window.addEventListener('app-loading-done', handleLoadingDone, { once: true })
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
projectsCtx?.revert()
|
|
})
|
|
|
|
interface WorkItem {
|
|
id: string
|
|
title: string
|
|
description: string
|
|
image: string
|
|
url: string
|
|
tags: string[]
|
|
category: string
|
|
}
|
|
|
|
const workData: WorkItem[] = [
|
|
{
|
|
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/800/600',
|
|
url: '#',
|
|
tags: ['Web Design', 'E-commerce', 'React'],
|
|
category: 'webDesign'
|
|
},
|
|
{
|
|
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/800/600',
|
|
url: '#',
|
|
tags: ['Branding', 'Marketing', 'UI/UX'],
|
|
category: 'branding'
|
|
},
|
|
{
|
|
id: 'nero-vision',
|
|
title: 'Nero Vision',
|
|
description: 'Full-stack web application for creative professionals to manage their portfolios.',
|
|
image: 'https://picsum.photos/id/119/800/600',
|
|
url: '#',
|
|
tags: ['Development', 'Vue.js', 'Node.js'],
|
|
category: 'development'
|
|
},
|
|
{
|
|
id: 'bold-moves',
|
|
title: 'Bold Moves',
|
|
description: 'Interactive website showcasing innovative architectural designs with 3D visualizations.',
|
|
image: 'https://picsum.photos/id/164/800/600',
|
|
url: '#',
|
|
tags: ['Web Design', '3D', 'Three.js'],
|
|
category: 'webDesign'
|
|
},
|
|
{
|
|
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/800/600',
|
|
url: '#',
|
|
tags: ['Branding', 'Visual Identity', 'Corporate'],
|
|
category: 'branding'
|
|
},
|
|
{
|
|
id: 'flux-api',
|
|
title: 'Flux API',
|
|
description: 'Scalable REST API with comprehensive documentation and developer tools.',
|
|
image: 'https://picsum.photos/id/326/800/600',
|
|
url: '#',
|
|
tags: ['Development', 'API', 'Python'],
|
|
category: 'development'
|
|
}
|
|
]
|
|
</script>
|
|
|
|
<style scoped>
|
|
p {
|
|
word-break: break-word;
|
|
}
|
|
</style>
|