feat(pages): 添加联系、首页和作品页面,更新样式和配置文件
This commit is contained in:
61
src/components/AnimatedCounter.tsx
Normal file
61
src/components/AnimatedCounter.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
|
||||
interface AnimatedCounterProps {
|
||||
end: number
|
||||
suffix?: string
|
||||
duration?: number
|
||||
}
|
||||
|
||||
export default function AnimatedCounter({ end, suffix = '', duration = 2000 }: AnimatedCounterProps) {
|
||||
const [count, setCount] = useState(0)
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true)
|
||||
observer.disconnect()
|
||||
}
|
||||
},
|
||||
{ threshold: 0.3 }
|
||||
)
|
||||
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current)
|
||||
}
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible) return
|
||||
|
||||
let startTime: number
|
||||
let animationId: number
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
if (!startTime) startTime = currentTime
|
||||
const elapsed = currentTime - startTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
|
||||
const easeOut = 1 - Math.pow(1 - progress, 3)
|
||||
setCount(Math.floor(easeOut * end))
|
||||
|
||||
if (progress < 1) {
|
||||
animationId = requestAnimationFrame(animate)
|
||||
}
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame(animate)
|
||||
|
||||
return () => cancelAnimationFrame(animationId)
|
||||
}, [isVisible, end, duration])
|
||||
|
||||
return (
|
||||
<div ref={ref} className="text-4xl md:text-5xl font-bold gradient-text">
|
||||
{count}{suffix}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
166
src/components/CaseComparison.tsx
Normal file
166
src/components/CaseComparison.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import { useState } from 'react'
|
||||
import ScrollReveal from './ScrollReveal'
|
||||
|
||||
const comparisons = [
|
||||
{
|
||||
id: 1,
|
||||
title: '品牌宣传片',
|
||||
traditional: {
|
||||
time: '2-4周',
|
||||
cost: '¥50,000-200,000',
|
||||
team: '导演+摄影+后期+演员',
|
||||
process: '脚本撰写→选角→拍摄→后期制作→修改→交付'
|
||||
},
|
||||
ai: {
|
||||
time: '1-3天',
|
||||
cost: '¥5,000-20,000',
|
||||
team: 'AI工程师+创意策划',
|
||||
process: '需求确认→AI生成→人工优化→交付'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '产品展示视频',
|
||||
traditional: {
|
||||
time: '1-2周',
|
||||
cost: '¥20,000-80,000',
|
||||
team: '摄影师+灯光师+后期',
|
||||
process: '产品准备→场景搭建→拍摄→精修→交付'
|
||||
},
|
||||
ai: {
|
||||
time: '4-8小时',
|
||||
cost: '¥2,000-8,000',
|
||||
team: 'AI工程师',
|
||||
process: '产品图片→AI建模→动画生成→交付'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '社交媒体短视频',
|
||||
traditional: {
|
||||
time: '3-7天',
|
||||
cost: '¥10,000-30,000',
|
||||
team: '编导+拍摄+剪辑',
|
||||
process: '策划→拍摄→剪辑→字幕→交付'
|
||||
},
|
||||
ai: {
|
||||
time: '2-4小时',
|
||||
cost: '¥1,000-3,000',
|
||||
team: 'AI工程师',
|
||||
process: '文案输入→AI生成→一键发布'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
export default function CaseComparison() {
|
||||
const [activeCase, setActiveCase] = useState(comparisons[0])
|
||||
|
||||
return (
|
||||
<section className="py-24 px-4 bg-[#0d0d2b]/50">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<ScrollReveal>
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-flex items-center px-4 py-2 rounded-full glass-card mb-6">
|
||||
<span className="text-purple-400 text-sm font-medium">效率对比</span>
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
AI vs 传统 <span className="gradient-text">制作对比</span>
|
||||
</h2>
|
||||
<p className="text-gray-400 text-lg max-w-2xl mx-auto">
|
||||
数据说话,AI制作效率提升10倍,成本降低80%
|
||||
</p>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
{/* Case Tabs */}
|
||||
<ScrollReveal delay={100}>
|
||||
<div className="flex flex-wrap justify-center gap-3 mb-12">
|
||||
{comparisons.map((comp) => (
|
||||
<button
|
||||
key={comp.id}
|
||||
onClick={() => setActiveCase(comp)}
|
||||
className={`px-5 py-2.5 rounded-xl font-medium transition-all duration-300 ${
|
||||
activeCase.id === comp.id
|
||||
? 'gradient-bg-animated text-white'
|
||||
: 'glass-card text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{comp.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
{/* Comparison Cards */}
|
||||
<ScrollReveal delay={200}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{/* Traditional */}
|
||||
<div className="glass-card rounded-3xl p-8 border-red-500/20">
|
||||
<div className="flex items-center gap-3 mb-8">
|
||||
<div className="w-12 h-12 rounded-xl bg-red-500/20 flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-white">传统制作</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between py-4 border-b border-white/5">
|
||||
<span className="text-gray-400">制作周期</span>
|
||||
<span className="text-red-400 font-semibold">{activeCase.traditional.time}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-4 border-b border-white/5">
|
||||
<span className="text-gray-400">制作成本</span>
|
||||
<span className="text-red-400 font-semibold">{activeCase.traditional.cost}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-4 border-b border-white/5">
|
||||
<span className="text-gray-400">团队配置</span>
|
||||
<span className="text-gray-300 text-sm text-right">{activeCase.traditional.team}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400 block mb-3">制作流程</span>
|
||||
<p className="text-gray-300 text-sm leading-relaxed">{activeCase.traditional.process}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI */}
|
||||
<div className="glass-card rounded-3xl p-8 border-green-500/20 relative overflow-hidden">
|
||||
<div className="absolute top-4 right-4 px-3 py-1 rounded-full text-xs font-semibold gradient-bg-animated">
|
||||
推荐
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mb-8">
|
||||
<div className="w-12 h-12 rounded-xl bg-green-500/20 flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-white">AI制作</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between py-4 border-b border-white/5">
|
||||
<span className="text-gray-400">制作周期</span>
|
||||
<span className="text-green-400 font-semibold">{activeCase.ai.time}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-4 border-b border-white/5">
|
||||
<span className="text-gray-400">制作成本</span>
|
||||
<span className="text-green-400 font-semibold">{activeCase.ai.cost}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-4 border-b border-white/5">
|
||||
<span className="text-gray-400">团队配置</span>
|
||||
<span className="text-gray-300 text-sm">{activeCase.ai.team}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400 block mb-3">制作流程</span>
|
||||
<p className="text-gray-300 text-sm leading-relaxed">{activeCase.ai.process}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
88
src/components/FAQ.tsx
Normal file
88
src/components/FAQ.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { useState } from 'react'
|
||||
import ScrollReveal from './ScrollReveal'
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: 'AI生成的视频质量如何?能达到商用标准吗?',
|
||||
answer: '我们的AI视频已经可以达到商用标准。通过多轮优化和人工精修,最终输出的视频在画面质量、流畅度和创意表现上都能满足品牌宣传、社交媒体等商业场景的需求。我们服务过的100+客户中,98%对最终效果表示满意。'
|
||||
},
|
||||
{
|
||||
question: '制作一个AI视频需要多长时间?',
|
||||
answer: '根据视频复杂度不同,制作周期从2小时到3天不等。简单的社交媒体短视频通常4-8小时即可完成,而复杂的品牌宣传片可能需要1-3天。相比传统制作方式,效率提升10倍以上。'
|
||||
},
|
||||
{
|
||||
question: 'AI视频的版权归属如何?',
|
||||
answer: '所有通过我们生成的AI视频,版权完全归属客户所有。我们使用的AI工具均拥有合法授权,生成的内容不存在版权纠纷风险。同时,我们会提供完整的版权证明文件。'
|
||||
},
|
||||
{
|
||||
question: '你们使用哪些AI技术?',
|
||||
answer: '我们综合使用业界领先的AI工具,包括:Midjourney/Stable Diffusion(图像生成)、Runway/Pika Labs(视频生成)、HeyGen/D-ID(数字人)、ElevenLabs(语音合成)等。根据项目需求选择最合适的工具组合。'
|
||||
},
|
||||
{
|
||||
question: '如何保证视频符合品牌调性?',
|
||||
answer: '我们有专业的创意团队会在项目初期深入了解您的品牌调性、目标受众和传播目标。通过品牌指南学习、风格参考收集和多轮沟通确认,确保AI生成的视频精准传达品牌价值。'
|
||||
},
|
||||
{
|
||||
question: '支持哪些视频格式和尺寸?',
|
||||
answer: '我们支持所有主流视频格式和尺寸,包括:横屏16:9(1920x1080/3840x2160)、竖屏9:16(1080x1920)、方形1:1(1080x1080)等。同时支持MP4、MOV、AVI等格式输出,满足不同平台的发布需求。'
|
||||
}
|
||||
]
|
||||
|
||||
export default function FAQ() {
|
||||
const [openIndex, setOpenIndex] = useState<number | null>(0)
|
||||
|
||||
return (
|
||||
<section className="py-24 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<ScrollReveal>
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-flex items-center px-4 py-2 rounded-full glass-card mb-6">
|
||||
<span className="text-purple-400 text-sm font-medium">常见问题</span>
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
<span className="gradient-text">FAQ</span>
|
||||
</h2>
|
||||
<p className="text-gray-400 text-lg">
|
||||
关于AI视频制作,您可能想了解的问题
|
||||
</p>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="space-y-4">
|
||||
{faqs.map((faq, index) => (
|
||||
<ScrollReveal key={index} delay={index * 50}>
|
||||
<div className="glass-card rounded-2xl overflow-hidden">
|
||||
<button
|
||||
onClick={() => setOpenIndex(openIndex === index ? null : index)}
|
||||
className="w-full text-left p-6 flex items-center justify-between gap-4"
|
||||
>
|
||||
<span className="text-white font-medium pr-4">{faq.question}</span>
|
||||
<svg
|
||||
className={`w-5 h-5 text-purple-400 flex-shrink-0 transition-transform duration-300 ${
|
||||
openIndex === index ? 'rotate-180' : ''
|
||||
}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-300 ${
|
||||
openIndex === index ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'
|
||||
}`}
|
||||
>
|
||||
<div className="px-6 pb-6">
|
||||
<div className="h-px bg-white/10 mb-4" />
|
||||
<p className="text-gray-400 leading-relaxed">{faq.answer}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
46
src/components/Footer.tsx
Normal file
46
src/components/Footer.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-[#0d0d2b] border-t border-white/10">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<div className="w-10 h-10 gradient-bg rounded-lg flex items-center justify-center">
|
||||
<span className="text-white font-bold text-xl">C</span>
|
||||
</div>
|
||||
<span className="text-xl font-bold gradient-text">CYH Technology</span>
|
||||
</div>
|
||||
<p className="text-gray-400 text-sm">
|
||||
香港领先的AI短视频制作公司,用科技创意赋能品牌传播。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-white font-semibold mb-4">快速链接</h3>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Link to="/" className="text-gray-400 hover:text-white text-sm transition-colors">首页</Link>
|
||||
<Link to="/about" className="text-gray-400 hover:text-white text-sm transition-colors">关于我们</Link>
|
||||
<Link to="/works" className="text-gray-400 hover:text-white text-sm transition-colors">作品展示</Link>
|
||||
<Link to="/contact" className="text-gray-400 hover:text-white text-sm transition-colors">联系我们</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-white font-semibold mb-4">联系方式</h3>
|
||||
<div className="flex flex-col space-y-2 text-gray-400 text-sm">
|
||||
<p>邮箱: info@cyhtechnology.com</p>
|
||||
<p>电话: +852 XXXX XXXX</p>
|
||||
<p>地址: 香港九龙湾XX大厦</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-white/10 mt-8 pt-8 text-center text-gray-500 text-sm">
|
||||
<p>© 2024 HK CYH Technology. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
22
src/components/MarqueeTicker.tsx
Normal file
22
src/components/MarqueeTicker.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
const partners = [
|
||||
'腾讯', '阿里巴巴', '字节跳动', '华为', '小米',
|
||||
'百度', '京东', '美团', '网易', '快手',
|
||||
'B站', '小红书', '滴滴', '拼多多', '携程'
|
||||
]
|
||||
|
||||
export default function MarqueeTicker() {
|
||||
return (
|
||||
<div className="overflow-hidden py-8">
|
||||
<div className="flex animate-marquee">
|
||||
{[...partners, ...partners].map((partner, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex-shrink-0 mx-8 px-6 py-3 glass-card rounded-full"
|
||||
>
|
||||
<span className="text-gray-400 font-medium whitespace-nowrap">{partner}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
109
src/components/Navbar.tsx
Normal file
109
src/components/Navbar.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
|
||||
const navLinks = [
|
||||
{ path: '/', label: '首页' },
|
||||
{ path: '/about', label: '关于我们' },
|
||||
{ path: '/works', label: '作品展示' },
|
||||
{ path: '/contact', label: '联系我们' },
|
||||
]
|
||||
|
||||
export default function Navbar() {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
const location = useLocation()
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setScrolled(window.scrollY > 50)
|
||||
}
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
return () => window.removeEventListener('scroll', handleScroll)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<nav className={`fixed top-0 left-0 right-0 z-50 transition-all duration-500 ${
|
||||
scrolled
|
||||
? 'bg-[#0a0a1a]/90 backdrop-blur-xl shadow-2xl shadow-purple-500/10'
|
||||
: 'bg-transparent'
|
||||
}`}>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-20">
|
||||
<Link to="/" className="flex items-center space-x-3 group">
|
||||
<div className="relative">
|
||||
<div className="w-12 h-12 gradient-bg-animated rounded-xl flex items-center justify-center transform group-hover:rotate-12 transition-transform duration-300">
|
||||
<span className="text-white font-bold text-2xl">C</span>
|
||||
</div>
|
||||
<div className="absolute inset-0 gradient-bg rounded-xl blur-lg opacity-50 group-hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xl font-bold gradient-text">CYH Technology</span>
|
||||
<div className="h-0.5 w-0 group-hover:w-full gradient-bg transition-all duration-300" />
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className="hidden md:flex items-center space-x-1">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
className="relative px-4 py-2 text-sm font-medium transition-colors duration-300 group"
|
||||
>
|
||||
<span className={
|
||||
location.pathname === link.path
|
||||
? 'text-white'
|
||||
: 'text-gray-400 group-hover:text-white'
|
||||
}>
|
||||
{link.label}
|
||||
</span>
|
||||
<div className={`absolute bottom-0 left-1/2 -translate-x-1/2 h-0.5 gradient-bg rounded-full transition-all duration-300 ${
|
||||
location.pathname === link.path ? 'w-8' : 'w-0 group-hover:w-8'
|
||||
}`} />
|
||||
{location.pathname === link.path && (
|
||||
<div className="absolute inset-0 bg-white/5 rounded-lg" />
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="md:hidden relative w-10 h-10 flex items-center justify-center"
|
||||
>
|
||||
<div className={`w-6 h-0.5 bg-white transition-all duration-300 ${
|
||||
isOpen ? 'rotate-45 translate-y-0' : '-translate-y-1.5'
|
||||
}`} />
|
||||
<div className={`w-6 h-0.5 bg-white absolute transition-all duration-300 ${
|
||||
isOpen ? 'opacity-0' : 'opacity-100'
|
||||
}`} />
|
||||
<div className={`w-6 h-0.5 bg-white transition-all duration-300 ${
|
||||
isOpen ? '-rotate-45 translate-y-0' : 'translate-y-1.5'
|
||||
}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`md:hidden transition-all duration-500 overflow-hidden ${
|
||||
isOpen ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'
|
||||
}`}>
|
||||
<div className="bg-[#0a0a1a]/95 backdrop-blur-xl border-t border-white/10 px-4 py-4 space-y-2">
|
||||
{navLinks.map((link, index) => (
|
||||
<Link
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
onClick={() => setIsOpen(false)}
|
||||
className={`block px-4 py-3 rounded-xl transition-all duration-300 ${
|
||||
location.pathname === link.path
|
||||
? 'text-white bg-gradient-to-r from-purple-500/20 to-transparent'
|
||||
: 'text-gray-400 hover:text-white hover:bg-white/5'
|
||||
}`}
|
||||
style={{ animationDelay: `${index * 100}ms` }}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
126
src/components/ParticleBackground.tsx
Normal file
126
src/components/ParticleBackground.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
interface Particle {
|
||||
x: number
|
||||
y: number
|
||||
size: number
|
||||
speedX: number
|
||||
speedY: number
|
||||
opacity: number
|
||||
}
|
||||
|
||||
export default function ParticleBackground() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
let animationId: number
|
||||
let particles: Particle[] = []
|
||||
let mouseX = 0
|
||||
let mouseY = 0
|
||||
|
||||
const resize = () => {
|
||||
canvas.width = window.innerWidth
|
||||
canvas.height = window.innerHeight
|
||||
}
|
||||
|
||||
const createParticles = () => {
|
||||
particles = []
|
||||
const count = Math.floor((canvas.width * canvas.height) / 15000)
|
||||
for (let i = 0; i < count; i++) {
|
||||
particles.push({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height,
|
||||
size: Math.random() * 2 + 0.5,
|
||||
speedX: (Math.random() - 0.5) * 0.5,
|
||||
speedY: (Math.random() - 0.5) * 0.5,
|
||||
opacity: Math.random() * 0.5 + 0.1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const drawParticles = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
particles.forEach((particle, i) => {
|
||||
particle.x += particle.speedX
|
||||
particle.y += particle.speedY
|
||||
|
||||
if (particle.x < 0) particle.x = canvas.width
|
||||
if (particle.x > canvas.width) particle.x = 0
|
||||
if (particle.y < 0) particle.y = canvas.height
|
||||
if (particle.y > canvas.height) particle.y = 0
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2)
|
||||
ctx.fillStyle = `rgba(102, 126, 234, ${particle.opacity})`
|
||||
ctx.fill()
|
||||
|
||||
particles.forEach((particle2, j) => {
|
||||
if (i === j) return
|
||||
const dx = particle.x - particle2.x
|
||||
const dy = particle.y - particle2.y
|
||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
if (distance < 150) {
|
||||
ctx.beginPath()
|
||||
ctx.strokeStyle = `rgba(102, 126, 234, ${0.15 * (1 - distance / 150)})`
|
||||
ctx.lineWidth = 0.5
|
||||
ctx.moveTo(particle.x, particle.y)
|
||||
ctx.lineTo(particle2.x, particle2.y)
|
||||
ctx.stroke()
|
||||
}
|
||||
})
|
||||
|
||||
const dx = particle.x - mouseX
|
||||
const dy = particle.y - mouseY
|
||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
if (distance < 200) {
|
||||
const force = (200 - distance) / 200
|
||||
particle.speedX += (dx / distance) * force * 0.02
|
||||
particle.speedY += (dy / distance) * force * 0.02
|
||||
}
|
||||
|
||||
particle.speedX *= 0.99
|
||||
particle.speedY *= 0.99
|
||||
})
|
||||
|
||||
animationId = requestAnimationFrame(drawParticles)
|
||||
}
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
mouseX = e.clientX
|
||||
mouseY = e.clientY
|
||||
}
|
||||
|
||||
resize()
|
||||
createParticles()
|
||||
drawParticles()
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
resize()
|
||||
createParticles()
|
||||
})
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animationId)
|
||||
window.removeEventListener('resize', resize)
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
style={{ zIndex: 1 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
159
src/components/ProcessAnimation.tsx
Normal file
159
src/components/ProcessAnimation.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import { useState } from 'react'
|
||||
import ScrollReveal from './ScrollReveal'
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: 1,
|
||||
title: '需求沟通',
|
||||
desc: '深入了解您的品牌、目标受众和传播目标',
|
||||
detail: '我们会安排专属项目经理与您进行1对1沟通,了解视频用途、风格偏好、核心卖点等关键信息,并输出详细的需求文档。',
|
||||
icon: (
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
</svg>
|
||||
),
|
||||
gradient: 'from-purple-500 to-pink-500'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '创意策划',
|
||||
desc: 'AI辅助生成创意方案,快速产出多版脚本',
|
||||
detail: '基于需求文档,我们使用AI快速生成多个创意方案和脚本草稿。您可以从中选择最喜欢的方案,或融合多个方案的优点。',
|
||||
icon: (
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
),
|
||||
gradient: 'from-blue-500 to-cyan-500'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'AI生成',
|
||||
desc: '使用前沿AI技术,高效生成视频素材',
|
||||
detail: '根据确认的脚本,我们使用Midjourney、Runway、Stable Diffusion等AI工具生成视频素材,包括画面、配音、字幕等。',
|
||||
icon: (
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
),
|
||||
gradient: 'from-green-500 to-emerald-500'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '人工精修',
|
||||
desc: '专业团队优化细节,确保品质达标',
|
||||
detail: 'AI生成的初稿会由专业后期团队进行精修,包括画面调色、节奏调整、特效添加等,确保最终效果达到商用标准。',
|
||||
icon: (
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
),
|
||||
gradient: 'from-orange-500 to-yellow-500'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '交付验收',
|
||||
desc: '多轮修改直至满意,提供完整源文件',
|
||||
detail: '我们提供2-3轮免费修改,直至您完全满意。交付内容包括:成品视频、源文件、字体版权证明等,方便您后续二次编辑。',
|
||||
icon: (
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
),
|
||||
gradient: 'from-pink-500 to-rose-500'
|
||||
}
|
||||
]
|
||||
|
||||
export default function ProcessAnimation() {
|
||||
const [activeStep, setActiveStep] = useState(0)
|
||||
|
||||
return (
|
||||
<section className="py-24 px-4 bg-[#0d0d2b]/50">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<ScrollReveal>
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-flex items-center px-4 py-2 rounded-full glass-card mb-6">
|
||||
<span className="text-purple-400 text-sm font-medium">制作流程</span>
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
从创意到成片,<span className="gradient-text">高效交付</span>
|
||||
</h2>
|
||||
<p className="text-gray-400 text-lg max-w-2xl mx-auto">
|
||||
标准化流程,让每一个环节都可控、可追溯
|
||||
</p>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12">
|
||||
{/* Steps Timeline */}
|
||||
<div className="lg:col-span-5">
|
||||
<div className="space-y-4">
|
||||
{steps.map((step, index) => (
|
||||
<button
|
||||
key={step.id}
|
||||
onClick={() => setActiveStep(index)}
|
||||
className={`w-full text-left p-5 rounded-2xl transition-all duration-300 ${
|
||||
activeStep === index
|
||||
? 'glass-card border-purple-500/50'
|
||||
: 'hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`w-12 h-12 rounded-xl bg-gradient-to-br ${step.gradient} flex items-center justify-center text-white flex-shrink-0 transition-transform duration-300 ${
|
||||
activeStep === index ? 'scale-110' : ''
|
||||
}`}>
|
||||
{step.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs text-gray-500">步骤 {step.id}</span>
|
||||
<h3 className={`font-semibold transition-colors ${
|
||||
activeStep === index ? 'text-white' : 'text-gray-400'
|
||||
}`}>
|
||||
{step.title}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">{step.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step Detail */}
|
||||
<div className="lg:col-span-7">
|
||||
<div className="glass-card rounded-3xl p-8 md:p-10 h-full">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className={`w-16 h-16 rounded-2xl bg-gradient-to-br ${steps[activeStep].gradient} flex items-center justify-center text-white`}>
|
||||
{steps[activeStep].icon}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-gray-500">步骤 {steps[activeStep].id}</span>
|
||||
<h3 className="text-2xl font-bold text-white">{steps[activeStep].title}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-400 text-lg leading-relaxed mb-8">
|
||||
{steps[activeStep].detail}
|
||||
</p>
|
||||
|
||||
{/* Visual Indicator */}
|
||||
<div className="relative h-2 bg-white/5 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`absolute inset-y-0 left-0 bg-gradient-to-r ${steps[activeStep].gradient} rounded-full transition-all duration-500`}
|
||||
style={{ width: `${((activeStep + 1) / steps.length) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between mt-3 text-sm text-gray-500">
|
||||
<span>开始</span>
|
||||
<span>{activeStep + 1}/{steps.length}</span>
|
||||
<span>交付</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
69
src/components/ScrollReveal.tsx
Normal file
69
src/components/ScrollReveal.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { useEffect, useRef, ReactNode } from 'react'
|
||||
|
||||
interface ScrollRevealProps {
|
||||
children: ReactNode
|
||||
direction?: 'up' | 'down' | 'left' | 'right'
|
||||
delay?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function ScrollReveal({
|
||||
children,
|
||||
direction = 'up',
|
||||
delay = 0,
|
||||
className = ''
|
||||
}: ScrollRevealProps) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setTimeout(() => {
|
||||
entry.target.classList.add('animate-visible')
|
||||
}, delay)
|
||||
observer.disconnect()
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
)
|
||||
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current)
|
||||
}
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [delay])
|
||||
|
||||
const getInitialTransform = () => {
|
||||
switch (direction) {
|
||||
case 'up': return 'translateY(60px)'
|
||||
case 'down': return 'translateY(-60px)'
|
||||
case 'left': return 'translateX(-60px)'
|
||||
case 'right': return 'translateX(60px)'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={className}
|
||||
style={{
|
||||
opacity: 0,
|
||||
transform: getInitialTransform(),
|
||||
transition: `all 0.8s cubic-bezier(0.4, 0, 0.2, 1) ${delay}ms`
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const style = document.createElement('style')
|
||||
style.textContent = `
|
||||
.animate-visible {
|
||||
opacity: 1 !important;
|
||||
transform: translateY(0) translateX(0) !important;
|
||||
}
|
||||
`
|
||||
document.head.appendChild(style)
|
||||
148
src/components/ServiceScenes.tsx
Normal file
148
src/components/ServiceScenes.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { useState } from 'react'
|
||||
import ScrollReveal from './ScrollReveal'
|
||||
|
||||
const scenes = [
|
||||
{
|
||||
id: 'social',
|
||||
icon: (
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2m-9 0h10m-10 0H5a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2h-2" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 11v6m-3-3h6" />
|
||||
</svg>
|
||||
),
|
||||
title: '社交媒体',
|
||||
desc: '抖音、小红书、Instagram等平台爆款内容',
|
||||
features: ['15秒竖屏视频', '高完播率设计', '爆款文案生成', '多平台适配'],
|
||||
gradient: 'from-pink-500 to-rose-500'
|
||||
},
|
||||
{
|
||||
id: 'ecommerce',
|
||||
icon: (
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
|
||||
</svg>
|
||||
),
|
||||
title: '电商营销',
|
||||
desc: '产品展示、直播切片、详情页视频',
|
||||
features: ['3D产品展示', '卖点可视化', '多角度展示', '批量生成'],
|
||||
gradient: 'from-orange-500 to-yellow-500'
|
||||
},
|
||||
{
|
||||
id: 'education',
|
||||
icon: (
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
),
|
||||
title: '教育培训',
|
||||
desc: '在线课程、知识科普、企业内训',
|
||||
features: ['知识可视化', 'AI讲师生成', '课程片头制作', '互动元素'],
|
||||
gradient: 'from-blue-500 to-cyan-500'
|
||||
},
|
||||
{
|
||||
id: 'brand',
|
||||
icon: (
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
),
|
||||
title: '企业宣传',
|
||||
desc: '品牌故事、企业宣传片、招聘视频',
|
||||
features: ['品牌形象塑造', '企业文化展示', '数字人代言', '多语言版本'],
|
||||
gradient: 'from-purple-500 to-violet-500'
|
||||
}
|
||||
]
|
||||
|
||||
export default function ServiceScenes() {
|
||||
const [activeScene, setActiveScene] = useState(scenes[0])
|
||||
|
||||
return (
|
||||
<section className="py-24 px-4">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<ScrollReveal>
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-flex items-center px-4 py-2 rounded-full glass-card mb-6">
|
||||
<span className="text-purple-400 text-sm font-medium">应用场景</span>
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
覆盖全场景 <span className="gradient-text">AI视频</span> 需求
|
||||
</h2>
|
||||
<p className="text-gray-400 text-lg max-w-2xl mx-auto">
|
||||
无论您是什么行业,我们都能提供专业的AI视频解决方案
|
||||
</p>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
{/* Scene Tabs */}
|
||||
<div className="lg:col-span-4 space-y-3">
|
||||
{scenes.map((scene) => (
|
||||
<button
|
||||
key={scene.id}
|
||||
onClick={() => setActiveScene(scene)}
|
||||
className={`w-full text-left p-5 rounded-2xl transition-all duration-300 ${
|
||||
activeScene.id === scene.id
|
||||
? 'glass-card border-purple-500/50'
|
||||
: 'hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-12 h-12 rounded-xl bg-gradient-to-br ${scene.gradient} flex items-center justify-center text-white flex-shrink-0`}>
|
||||
{scene.icon}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={`font-semibold transition-colors ${
|
||||
activeScene.id === scene.id ? 'text-white' : 'text-gray-400'
|
||||
}`}>
|
||||
{scene.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500">{scene.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Scene Detail */}
|
||||
<div className="lg:col-span-8">
|
||||
<div className="glass-card rounded-3xl p-8 md:p-10">
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<div className={`w-16 h-16 rounded-2xl bg-gradient-to-br ${activeScene.gradient} flex items-center justify-center text-white`}>
|
||||
{activeScene.icon}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold text-white">{activeScene.title}</h3>
|
||||
<p className="text-gray-400">{activeScene.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
|
||||
{activeScene.features.map((feature, index) => (
|
||||
<div key={index} className="flex items-center gap-3 p-4 rounded-xl bg-white/5">
|
||||
<div className={`w-8 h-8 rounded-lg bg-gradient-to-br ${activeScene.gradient} flex items-center justify-center flex-shrink-0`}>
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-gray-300">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="aspect-video rounded-2xl overflow-hidden bg-gradient-to-br from-purple-900/50 to-blue-900/50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-20 h-20 mx-auto mb-4 rounded-full gradient-bg-animated flex items-center justify-center">
|
||||
<svg className="w-10 h-10 text-white ml-1" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-gray-400">点击观看 {activeScene.title} 案例视频</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
37
src/components/TestimonialCard.tsx
Normal file
37
src/components/TestimonialCard.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
interface Testimonial {
|
||||
name: string
|
||||
role: string
|
||||
company: string
|
||||
content: string
|
||||
avatar: string
|
||||
}
|
||||
|
||||
interface TestimonialCardProps {
|
||||
testimonial: Testimonial
|
||||
}
|
||||
|
||||
export default function TestimonialCard({ testimonial }: TestimonialCardProps) {
|
||||
return (
|
||||
<div className="glass-card rounded-2xl p-8 hover-lift">
|
||||
<div className="flex items-center mb-6">
|
||||
<div className="w-14 h-14 rounded-full overflow-hidden mr-4 border-2 border-purple-500/30">
|
||||
<img
|
||||
src={testimonial.avatar}
|
||||
alt={testimonial.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-white font-semibold">{testimonial.name}</h4>
|
||||
<p className="text-gray-400 text-sm">{testimonial.role} · {testimonial.company}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<span key={i} className="text-yellow-400 text-lg">★</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-gray-300 leading-relaxed">"{testimonial.content}"</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
73
src/components/VideoCard.tsx
Normal file
73
src/components/VideoCard.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { Video } from '../data/videos'
|
||||
|
||||
interface VideoCardProps {
|
||||
video: Video
|
||||
onClick: (video: Video) => void
|
||||
}
|
||||
|
||||
export default function VideoCard({ video, onClick }: VideoCardProps) {
|
||||
const [tilt, setTilt] = useState({ x: 0, y: 0 })
|
||||
const cardRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!cardRef.current) return
|
||||
const rect = cardRef.current.getBoundingClientRect()
|
||||
const x = (e.clientX - rect.left) / rect.width
|
||||
const y = (e.clientY - rect.top) / rect.height
|
||||
setTilt({
|
||||
x: (y - 0.5) * 10,
|
||||
y: (x - 0.5) * -10
|
||||
})
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setTilt({ x: 0, y: 0 })
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={cardRef}
|
||||
className="group cursor-pointer rounded-2xl overflow-hidden gradient-border glow-on-hover"
|
||||
onClick={() => onClick(video)}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
style={{
|
||||
transform: `perspective(1000px) rotateX(${tilt.x}deg) rotateY(${tilt.y}deg)`,
|
||||
transition: 'transform 0.3s ease'
|
||||
}}
|
||||
>
|
||||
<div className="relative aspect-video overflow-hidden">
|
||||
<img
|
||||
src={video.thumbnail}
|
||||
alt={video.title}
|
||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-all duration-500" />
|
||||
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-20 h-20 rounded-full gradient-bg-animated flex items-center justify-center opacity-0 group-hover:opacity-100 transform scale-0 group-hover:scale-100 transition-all duration-500 shadow-2xl shadow-purple-500/50">
|
||||
<svg className="w-10 h-10 text-white ml-1" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-4 right-4 px-3 py-1.5 rounded-full text-xs font-semibold gradient-bg-animated shadow-lg">
|
||||
{video.category}
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 h-1 gradient-bg-animated transform scale-x-0 group-hover:scale-x-100 transition-transform duration-500 origin-left" />
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-[#0d0d2b]">
|
||||
<h3 className="text-white font-bold text-lg mb-2 group-hover:text-transparent group-hover:bg-clip-text group-hover:bg-gradient-to-r group-hover:from-purple-400 group-hover:to-pink-400 transition-all duration-300">
|
||||
{video.title}
|
||||
</h3>
|
||||
<p className="text-gray-400 text-sm line-clamp-2 group-hover:text-gray-300 transition-colors">
|
||||
{video.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
70
src/components/VideoModal.tsx
Normal file
70
src/components/VideoModal.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { Video } from '../data/videos'
|
||||
|
||||
interface VideoModalProps {
|
||||
video: Video | null
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export default function VideoModal({ video, onClose }: VideoModalProps) {
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (video && videoRef.current) {
|
||||
videoRef.current.play()
|
||||
}
|
||||
}, [video])
|
||||
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose()
|
||||
}
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
return () => document.removeEventListener('keydown', handleEscape)
|
||||
}, [onClose])
|
||||
|
||||
if (!video) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="relative w-full max-w-4xl bg-[#1a1a2e] rounded-2xl overflow-hidden animate-fade-in-up"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-4 right-4 z-10 w-10 h-10 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center transition-colors"
|
||||
>
|
||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div className="aspect-video">
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={video.url}
|
||||
controls
|
||||
className="w-full h-full"
|
||||
poster={video.thumbnail}
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h2 className="text-2xl font-bold text-white">{video.title}</h2>
|
||||
<span className="px-3 py-1 rounded-full text-sm gradient-bg">
|
||||
{video.category}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-400">{video.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user