feat: 完成所有页面功能和用户认证系统
✨ 新增功能: - Work 和 News 页面实现筛选功能 - Contact 页面联系表单提交功能(加载状态、成功/失败反馈) - Newsletter 订阅功能(邮箱验证、状态反馈) - 移动端汉堡菜单功能(全屏菜单、动画效果) - 用户登录/注册系统(支持账号/邮箱/手机号三合一) - 用户状态管理(Pinia store + localStorage 持久化) - 登录和注册弹窗组件(表单验证、错误提示) - 用户头像和下拉菜单(桌面端和移动端) 🎨 优化改进: - Footer 显示问题修复(多重备用机制) - 联系信息更新(邮箱改为 chenchun@virtheart.com,手机改为微信) - 完整的国际化支持(中英文双语) - 响应式设计优化 🔧 技术实现: - GSAP 动画系统 - 表单验证和反馈 - 状态持久化 - 自动生成用户头像 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+201
-6
@@ -1,13 +1,208 @@
|
||||
<template>
|
||||
<div class="page-container flex min-h-screen items-center justify-center pt-32">
|
||||
<h1 class="text-4xl font-bold">Studio Page</h1>
|
||||
</div>
|
||||
<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-16 md:mb-24"
|
||||
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('studio.title') }}</h1>
|
||||
<p ref="descRef" class="text-base md:text-lg text-gray-600 max-w-2xl">{{ t('studio.description') }}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="missionWrapRef"
|
||||
class="mb-20 md:mb-28"
|
||||
style="opacity: 0; visibility: hidden;"
|
||||
>
|
||||
<div class="studio-section bg-gradient-to-br from-black to-gray-800 rounded-2xl md:rounded-3xl p-8 md:p-12 text-white">
|
||||
<h2 class="text-2xl md:text-4xl font-bold mb-4 md:mb-6">{{ t('studio.mission.title') }}</h2>
|
||||
<p class="text-base md:text-xl leading-relaxed text-gray-100 max-w-3xl">{{ t('studio.mission.content') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="valuesWrapRef"
|
||||
class="mb-20 md:mb-28"
|
||||
style="opacity: 0; visibility: hidden;"
|
||||
>
|
||||
<h2 class="text-2xl md:text-4xl font-bold mb-8 md:mb-12 text-center text-black">{{ t('studio.values.title') }}</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8">
|
||||
<div
|
||||
v-for="value in values"
|
||||
:key="value.key"
|
||||
class="value-card group relative p-6 md:p-8 rounded-2xl bg-white border border-gray-100 hover:shadow-xl transition-all duration-500 hover:-translate-y-2"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 bg-gradient-to-br rounded-2xl"
|
||||
:class="value.color"
|
||||
></div>
|
||||
|
||||
<div class="relative z-10">
|
||||
<div class="w-12 h-12 md:w-14 md:h-14 rounded-2xl flex items-center justify-center bg-gray-50 text-black group-hover:bg-white group-hover:shadow-sm transition-all duration-300 mb-5">
|
||||
<Icon :name="value.icon" class="w-6 h-6 md:w-8 md:h-8" />
|
||||
</div>
|
||||
<h3 class="text-xl md:text-2xl font-bold mb-3 text-black">{{ t(`studio.values.${value.key}`) }}</h3>
|
||||
<p class="text-sm md:text-base text-gray-600 leading-relaxed">{{ t(`studio.values.${value.key}Desc`) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="teamWrapRef"
|
||||
class="text-center"
|
||||
style="opacity: 0; visibility: hidden;"
|
||||
>
|
||||
<div class="studio-section bg-gradient-to-br from-gray-50 to-gray-100 rounded-2xl md:rounded-3xl p-8 md:p-12">
|
||||
<h2 class="text-2xl md:text-4xl font-bold mb-4 md:mb-6 text-black">{{ t('studio.team.title') }}</h2>
|
||||
<p class="text-base md:text-xl text-gray-700 max-w-3xl mx-auto">{{ t('studio.team.description') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Studio 页面
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref } 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 missionWrapRef = ref<HTMLElement | null>(null)
|
||||
const valuesWrapRef = ref<HTMLElement | null>(null)
|
||||
const teamWrapRef = ref<HTMLElement | null>(null)
|
||||
let sectionsCtx: gsap.Context | null = null
|
||||
|
||||
useSeoMeta({
|
||||
title: 'Studio',
|
||||
description: '工作室页面',
|
||||
title: () => t('studio.title'),
|
||||
description: () => t('studio.description'),
|
||||
})
|
||||
|
||||
const playPageIntro = async () => {
|
||||
await nextTick()
|
||||
if (!headerWrapRef.value || !titleRef.value || !descRef.value || !missionWrapRef.value || !valuesWrapRef.value || !teamWrapRef.value) return
|
||||
|
||||
sectionsCtx?.revert()
|
||||
|
||||
sectionsCtx = gsap.context(() => {
|
||||
const valueCards = gsap.utils.toArray<HTMLElement>('.value-card', valuesWrapRef.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(missionWrapRef.value!, { autoAlpha: 1 }, '-=0.2')
|
||||
tl.fromTo(
|
||||
'.studio-section',
|
||||
{ autoAlpha: 0, y: 30, scale: 0.97 },
|
||||
{
|
||||
autoAlpha: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
duration: 0.9,
|
||||
ease: 'power4.out',
|
||||
clearProps: 'transform',
|
||||
},
|
||||
missionWrapRef.value
|
||||
)
|
||||
|
||||
tl.set(valuesWrapRef.value!, { autoAlpha: 1 }, '-=0.1')
|
||||
tl.fromTo(
|
||||
valueCards,
|
||||
{ autoAlpha: 0, y: 34, scale: 0.965 },
|
||||
{
|
||||
autoAlpha: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
duration: 0.95,
|
||||
ease: 'power4.out',
|
||||
stagger: { each: 0.12, from: 'start' },
|
||||
clearProps: 'transform',
|
||||
},
|
||||
'+=0.1'
|
||||
)
|
||||
|
||||
tl.set(teamWrapRef.value!, { autoAlpha: 1 }, '-=0.3')
|
||||
tl.fromTo(
|
||||
teamWrapRef.value!,
|
||||
{ autoAlpha: 0, y: 30 },
|
||||
{
|
||||
autoAlpha: 1,
|
||||
y: 0,
|
||||
duration: 0.85,
|
||||
ease: 'power4.out',
|
||||
clearProps: 'transform',
|
||||
},
|
||||
'+=0.15'
|
||||
)
|
||||
|
||||
tl.set([headerWrapRef.value!, missionWrapRef.value!, valuesWrapRef.value!, teamWrapRef.value!], { clearProps: 'opacity,visibility' })
|
||||
})
|
||||
})
|
||||
})
|
||||
}, headerWrapRef.value)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const handleLoadingDone = () => {
|
||||
playPageIntro()
|
||||
}
|
||||
|
||||
if (!appStore.isAppLoading) {
|
||||
playPageIntro()
|
||||
return
|
||||
}
|
||||
|
||||
window.addEventListener('app-loading-done', handleLoadingDone, { once: true })
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
sectionsCtx?.revert()
|
||||
})
|
||||
|
||||
interface ValueItem {
|
||||
key: string
|
||||
icon: string
|
||||
color: string
|
||||
}
|
||||
|
||||
const values: ValueItem[] = [
|
||||
{
|
||||
key: 'creativity',
|
||||
icon: 'ph:lightbulb-duotone',
|
||||
color: 'from-yellow-50/50 to-orange-100/50'
|
||||
},
|
||||
{
|
||||
key: 'quality',
|
||||
icon: 'ph:medal-duotone',
|
||||
color: 'from-blue-50/50 to-cyan-100/50'
|
||||
},
|
||||
{
|
||||
key: 'collaboration',
|
||||
icon: 'ph:users-three-duotone',
|
||||
color: 'from-purple-50/50 to-pink-100/50'
|
||||
},
|
||||
{
|
||||
key: 'integrity',
|
||||
icon: 'ph:shield-check-duotone',
|
||||
color: 'from-green-50/50 to-emerald-100/50'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
p {
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user