Files
nl-blogs/client/src/pages/Services.vue
2026-01-16 08:29:57 +08:00

259 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="services" class="page-section block animate-slide-down">
<div class="py-16 text-center">
<h2 class="font-serif text-5xl italic text-white mb-6">共创数字未来</h2>
<p class="text-art-muted text-lg max-w-2xl mx-auto">用代码构建骨架用设计触动灵魂</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 mb-32 px-4" id="services-grid">
<div class="tilt-card group h-96">
<div class="tilt-inner flex flex-col justify-between">
<div class="w-16 h-16 rounded-2xl bg-blue-500/10 flex items-center justify-center text-blue-400 mb-6 border border-blue-500/20 group-hover:scale-110 transition-transform duration-500">
<i data-lucide="layers" class="w-8 h-8"></i>
</div>
<div>
<h3 class="text-2xl font-bold text-white mb-4">前端架构设计</h3>
<p class="text-art-muted text-sm leading-relaxed">为复杂的大型应用提供可扩展的架构方案</p>
</div>
</div>
</div>
<div class="tilt-card group h-96">
<div class="tilt-inner flex flex-col justify-between border-art-accent/20 bg-white/5">
<div class="w-16 h-16 rounded-2xl bg-art-accent/10 flex items-center justify-center text-art-accent mb-6 border border-art-accent/20 group-hover:scale-110 transition-transform duration-500 shadow-[0_0_30px_-10px_rgba(212,179,131,0.3)]">
<i data-lucide="wand-2" class="w-8 h-8"></i>
</div>
<div>
<h3 class="text-2xl font-bold text-white mb-4">创意交互开发</h3>
<p class="text-art-muted text-sm leading-relaxed">利用 WebGL (Three.js) GSAP 打造令人过目难忘的着陆页</p>
</div>
</div>
</div>
<div class="tilt-card group h-96">
<div class="tilt-inner flex flex-col justify-between">
<div class="w-16 h-16 rounded-2xl bg-purple-500/10 flex items-center justify-center text-purple-400 mb-6 border border-purple-500/20 group-hover:scale-110 transition-transform duration-500">
<i data-lucide="smartphone" class="w-8 h-8"></i>
</div>
<div>
<h3 class="text-2xl font-bold text-white mb-4">跨平台应用</h3>
<p class="text-art-muted text-sm leading-relaxed">使用 UniApp React Native 开发高质量的移动端应用</p>
</div>
</div>
</div>
</div>
<!-- Process Accordion -->
<div class="mb-32 px-4">
<h3 class="font-serif text-3xl text-white text-center mb-12 italic">创作流程</h3>
<div class="process-accordion">
<div class="process-step">
<img src="https://images.unsplash.com/photo-1531403009284-440f080d1e12?q=80&w=2670&auto=format&fit=crop" class="step-bg" alt="Discovery">
<div class="step-content">
<div class="step-number">01</div>
<h4 class="text-xl font-bold text-white mb-2">灵感 & 探索</h4>
<p class="step-desc">深入理解业务需求进行竞品分析寻找视觉灵感确定设计方向这是地基</p>
</div>
</div>
<div class="process-step">
<img src="https://images.unsplash.com/photo-1581291518633-83b4ebd1d83e?q=80&w=2670&auto=format&fit=crop" class="step-bg" alt="Design">
<div class="step-content">
<div class="step-number">02</div>
<h4 class="text-xl font-bold text-white mb-2">架构 & 设计</h4>
<p class="step-desc">设计高保真原型规划技术架构确定数据流向将抽象的想法具象化</p>
</div>
</div>
<div class="process-step">
<img src="https://images.unsplash.com/photo-1555099962-4199c345e5dd?q=80&w=2670&auto=format&fit=crop" class="step-bg" alt="Code">
<div class="step-content">
<div class="step-number">03</div>
<h4 class="text-xl font-bold text-white mb-2">编码 & 雕琢</h4>
<p class="step-desc">编写干净可维护的代码添加微交互和动画让页面"活"起来</p>
</div>
</div>
<div class="process-step">
<img src="https://images.unsplash.com/photo-1460925895917-afdab827c52f?q=80&w=2426&auto=format&fit=crop" class="step-bg" alt="Launch">
<div class="step-content">
<div class="step-number">04</div>
<h4 class="text-xl font-bold text-white mb-2">测试 & 交付</h4>
<p class="step-desc">多设备测试性能优化SEO 配置确保最终交付物完美无瑕</p>
</div>
</div>
</div>
</div>
<!-- Danmaku & Partners -->
<div class="py-20 mb-32 overflow-hidden relative">
<div class="absolute left-0 top-0 w-20 h-full bg-gradient-to-r from-art-bg to-transparent z-10"></div>
<div class="absolute right-0 top-0 w-20 h-full bg-gradient-to-l from-art-bg to-transparent z-10"></div>
<h3 class="text-3xl font-serif text-white text-center mb-12 italic">客户原声</h3>
<div v-if="loading" class="flex justify-center items-center h-64">
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-art-accent"></div>
</div>
<div v-else-if="error" class="text-center text-red-500 py-20">
<p class="mb-4">{{ error }}</p>
<button @click="fetchServicesData" class="px-4 py-2 bg-art-accent text-white rounded hover:bg-opacity-80 transition-colors">
重试
</button>
</div>
<div v-else class="flex flex-col gap-6" id="danmaku-container">
<!-- Danmaku items will be rendered here -->
<div class="danmaku-row animate-marquee hover:[animation-play-state:paused]">
<div v-for="(testimonial, index) in testimonials" :key="testimonial.id" class="danmaku-item">
<div class="w-6 h-6 rounded-full bg-blue-500/20"></div>
<span>"{{ testimonial.content }}"</span>
</div>
<!-- 复制一份数据以实现无缝滚动 -->
<div v-for="(testimonial, index) in testimonials" :key="testimonial.id + '-copy'" class="danmaku-item">
<div class="w-6 h-6 rounded-full bg-blue-500/20"></div>
<span>"{{ testimonial.content }}"</span>
</div>
</div>
<div class="danmaku-row animate-marquee-reverse hover:[animation-play-state:paused]">
<div v-for="(testimonial, index) in testimonials" :key="testimonial.id" class="danmaku-item">
<div class="w-6 h-6 rounded-full bg-purple-500/20"></div>
<span>"{{ testimonial.content }}"</span>
</div>
<!-- 复制一份数据以实现无缝滚动 -->
<div v-for="(testimonial, index) in testimonials" :key="testimonial.id + '-copy'" class="danmaku-item">
<div class="w-6 h-6 rounded-full bg-purple-500/20"></div>
<span>"{{ testimonial.content }}"</span>
</div>
</div>
</div>
</div>
<!-- CTA -->
<div class="max-w-4xl mx-auto text-center px-6 mb-20">
<div class="p-12 rounded-3xl bg-gradient-to-b from-white/10 to-transparent border border-white/10 relative overflow-hidden">
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-64 h-64 bg-art-accent/20 blur-[100px] -z-10"></div>
<h2 class="text-4xl md:text-5xl font-serif text-white mb-6">准备好开始了吗</h2>
<p class="text-art-muted mb-8 max-w-xl mx-auto">无论是一个疯狂的想法还是一个具体的业务需求我都乐意倾听</p>
<button @click="openInquiry" class="inline-flex items-center gap-2 px-8 py-4 bg-white text-black rounded-full font-bold hover:scale-105 transition-transform"><span>发起合作咨询</span><i data-lucide="arrow-right" class="w-5 h-5"></i></button>
</div>
</div>
<!-- Partners -->
<div class="max-w-4xl mx-auto px-6 mb-32 border-t border-white/5 pt-12">
<p class="text-center text-xs font-mono text-art-muted tracking-widest uppercase mb-8">Trusted By</p>
<div v-if="loading" class="flex justify-center items-center h-64">
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-art-accent"></div>
</div>
<div v-else-if="error" class="text-center text-red-500 py-20">
<p class="mb-4">{{ error }}</p>
<button @click="fetchServicesData" class="px-4 py-2 bg-art-accent text-white rounded hover:bg-opacity-80 transition-colors">
重试
</button>
</div>
<div v-else class="flex flex-wrap justify-center gap-8 md:gap-16 opacity-50">
<span
v-for="partner in partners"
:key="partner.id"
class="partner-logo text-xl font-bold text-white cursor-pointer"
:class="getPartnerFontClass(partner.name)"
>
{{ partner.name }}
</span>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeMount, onUpdated, nextTick } from 'vue'
import { useScrollAnimation } from '../composables/useScrollAnimation'
import {
fetchTestimonials,
fetchPartners,
type Testimonial,
type Partner
} from '../services/api'
const testimonials = ref<Testimonial[]>([])
const partners = ref<Partner[]>([])
const loading = ref(false)
const error = ref('')
const refreshIcons = () => {
if ((window as any).lucide) {
(window as any).lucide.createIcons()
}
}
// 获取服务数据
const fetchServicesData = async () => {
loading.value = true
error.value = ''
try {
const [testimonialsData, partnersData] = await Promise.all([
fetchTestimonials(),
fetchPartners()
])
testimonials.value = testimonialsData
partners.value = partnersData
} catch (err) {
console.error('Error fetching services data:', err)
error.value = '获取服务数据失败,请稍后重试'
} finally {
loading.value = false
nextTick(refreshIcons)
}
}
// 获取合作伙伴字体类
const getPartnerFontClass = (name: string): string => {
if (name === 'VOGUE' || name === 'Monocle') {
return 'font-serif italic'
} else if (name === 'WIRED') {
return 'font-mono'
} else if (name === 'stripe') {
return 'font-sans'
}
return 'font-bold'
}
const openInquiry = () => {
// 使用全局函数打开咨询模态框
if ((window as any).openInquiry) {
(window as any).openInquiry()
}
}
onBeforeMount(() => {
fetchServicesData()
})
const { initObserver } = useScrollAnimation()
onMounted(() => {
// 启用滚动动画
initObserver()
// 初始化Lucide图标
refreshIcons()
// 添加鼠标倾斜效果
const tiltCards = document.querySelectorAll('.tilt-card')
tiltCards.forEach(card => {
card.addEventListener('mousemove', (e: any) => {
const cardRect = card.getBoundingClientRect()
const x = e.clientX - cardRect.left
const y = e.clientY - cardRect.top
const xPercent = (x / cardRect.width) * 100
const yPercent = (y / cardRect.height) * 100
;(card as HTMLElement).style.setProperty('--mouse-x', `${xPercent}%`)
;(card as HTMLElement).style.setProperty('--mouse-y', `${yPercent}%`)
// 3D Rotation
const rotateX = (y / cardRect.height - 0.5) * 20
const rotateY = (x / cardRect.width - 0.5) * -20
;(card as HTMLElement).style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`
})
card.addEventListener('mouseleave', () => {
;(card as HTMLElement).style.transform = 'perspective(1000px) rotateX(0) rotateY(0)'
})
})
})
onUpdated(() => {
refreshIcons()
})
</script>