style(theme): 更新网站主题色彩方案与字体配置
- 调整主色调从 #1C1C1C 至 #1A1A1A,优化视觉层次 - 更新背景色系为暖白色调 (#FAFAF7, #F5F4F0 等) - 配置中文字体栈,添加 serif 字体支持 - 优化文本颜色梯度,提升可读性 - 调整边框颜色,统一水墨风格 - 添加 Google Search Console 验证码配置项 - 新增桌面应用架构专家代理配置文件 - 重构 E2E 测试等待策略,提升稳定性 - 添加回归测试脚本,增强质量保障
This commit is contained in:
@@ -1,127 +1,166 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
|
||||
interface ListPageHeroV3Props {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
description?: string;
|
||||
stats?: Array<{ value: string; label: string }>;
|
||||
badge?: {
|
||||
text: string;
|
||||
variant?: 'brand' | 'blue' | 'green' | 'neutral';
|
||||
};
|
||||
}
|
||||
|
||||
const BADGE_VARIANTS = {
|
||||
brand: {
|
||||
bg: 'var(--color-brand-primary-bg)',
|
||||
text: 'var(--color-brand-primary)',
|
||||
border: 'rgba(var(--color-brand-primary-rgb), 0.15)',
|
||||
dot: 'var(--color-brand-primary)',
|
||||
},
|
||||
blue: {
|
||||
bg: 'rgba(var(--color-accent-blue-rgb), 0.08)',
|
||||
text: 'var(--color-accent-blue)',
|
||||
border: 'rgba(var(--color-accent-blue-rgb), 0.15)',
|
||||
dot: 'var(--color-accent-blue)',
|
||||
},
|
||||
green: {
|
||||
bg: 'rgba(34, 197, 94, 0.08)',
|
||||
text: '#16a34a',
|
||||
border: 'rgba(34, 197, 94, 0.15)',
|
||||
dot: '#22c55e',
|
||||
},
|
||||
neutral: {
|
||||
bg: 'var(--color-bg-section)',
|
||||
text: 'var(--color-text-secondary)',
|
||||
border: 'var(--color-border-primary)',
|
||||
dot: 'var(--color-text-subtle)',
|
||||
},
|
||||
};
|
||||
|
||||
export function ListPageHeroV3({
|
||||
title,
|
||||
subtitle,
|
||||
description,
|
||||
stats,
|
||||
badge,
|
||||
}: ListPageHeroV3Props) {
|
||||
return (
|
||||
<section className="relative bg-gradient-to-br from-[#f8f6f3] via-[#f3efe8] to-[#f0ebe4] text-stone-900 py-20 lg:py-28 overflow-hidden">
|
||||
<div className="absolute inset-0 opacity-20">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_rgba(196,30,58,0.06),_transparent_50%)]" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,_rgba(30,58,95,0.04),_transparent_50%)]" />
|
||||
</div>
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const badgeStyle = BADGE_VARIANTS[badge?.variant || 'brand'];
|
||||
|
||||
<div className="container-wide relative z-10">
|
||||
return (
|
||||
<section className="relative overflow-hidden">
|
||||
{/* Multi-layer background */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[var(--color-bg-section)] via-[var(--color-bg-secondary)] to-[var(--color-bg-section)]" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_rgba(var(--color-brand-primary-rgb),_0.05)_0%,_transparent_50%)]" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,_rgba(var(--color-primary-rgb),_0.03)_0%,_transparent_50%)]" />
|
||||
|
||||
{/* Subtle grid texture */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.03]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(rgba(var(--color-border-primary-rgb), 0.5) 1px, transparent 1px), linear-gradient(90deg, rgba(var(--color-border-primary-rgb), 0.5) 1px, transparent 1px)',
|
||||
backgroundSize: '60px 60px',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Decorative accent line */}
|
||||
<div className="absolute top-0 left-0 right-0 h-px" style={{ background: 'linear-gradient(90deg, transparent, var(--color-brand-primary), transparent)' }} aria-hidden="true" />
|
||||
|
||||
<div className="container-wide relative z-10 py-20 lg:py-28">
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={{
|
||||
hidden: {},
|
||||
visible: {
|
||||
transition: {
|
||||
staggerChildren: 0.12,
|
||||
delayChildren: 0.2,
|
||||
},
|
||||
},
|
||||
}}
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="max-w-4xl mx-auto text-center"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] as const }}
|
||||
className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-stone-900/5 backdrop-blur-sm text-stone-700 text-sm font-medium mb-6 border border-stone-900/10"
|
||||
>
|
||||
<span className="w-2 h-2 rounded-full bg-green-400 animate-pulse" />
|
||||
企业级解决方案
|
||||
</motion.div>
|
||||
{/* Dynamic Badge */}
|
||||
{badge && badge.text && (
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, scale: 0.95, y: 8 }}
|
||||
whileInView={{ opacity: 1, scale: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.1, duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full text-sm font-medium mb-6 backdrop-blur-sm"
|
||||
style={{ backgroundColor: badgeStyle.bg, color: badgeStyle.text, border: `1px solid ${badgeStyle.border}` }}
|
||||
>
|
||||
<span className="w-2 h-2 rounded-full animate-pulse" style={{ backgroundColor: badgeStyle.dot }} />
|
||||
{badge?.text || ''}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Title */}
|
||||
<motion.h1
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: 30 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.8, ease: [0.22, 1, 0.36, 1] as const },
|
||||
},
|
||||
}}
|
||||
className="text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight mb-6"
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.15, duration: 0.7, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight mb-6 text-[var(--color-text-primary)]"
|
||||
>
|
||||
{title}
|
||||
</motion.h1>
|
||||
|
||||
{/* Subtitle */}
|
||||
<motion.p
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: 25 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { delay: 0.15, duration: 0.7, ease: [0.22, 1, 0.36, 1] as const },
|
||||
},
|
||||
}}
|
||||
className="text-lg md:text-xl text-stone-600 max-w-2xl mx-auto leading-relaxed mb-8"
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.25, duration: 0.65, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="text-lg md:text-xl max-w-2xl mx-auto leading-relaxed mb-4 text-[var(--color-text-secondary)]"
|
||||
>
|
||||
{subtitle}
|
||||
</motion.p>
|
||||
|
||||
{/* Description */}
|
||||
{description && (
|
||||
<motion.p
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { delay: 0.25, duration: 0.65, ease: [0.22, 1, 0.36, 1] as const },
|
||||
},
|
||||
}}
|
||||
className="text-base text-stone-500 max-w-xl mx-auto mb-10"
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 18 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.35, duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="text-base max-w-xl mx-auto mb-10 text-[var(--color-text-muted)]"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
)}
|
||||
|
||||
{/* Stats Row */}
|
||||
{stats && stats.length > 0 && (
|
||||
<motion.div
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: 30 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { delay: 0.35, duration: 0.75, ease: [0.22, 1, 0.36, 1] as const },
|
||||
},
|
||||
}}
|
||||
className="flex flex-wrap justify-center gap-8 mt-12 pt-12 border-t border-stone-900/10"
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.4, duration: 0.7, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="flex flex-wrap justify-center gap-8 md:gap-12 mt-12 pt-12 border-t border-[var(--color-border-primary)]"
|
||||
>
|
||||
{stats.map((stat, index) => (
|
||||
<motion.div
|
||||
key={stat.label}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, scale: 0.95 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.5 + index * 0.08, duration: 0.45 }}
|
||||
className="text-center min-w-[120px]"
|
||||
transition={{ delay: 0.5 + index * 0.08, duration: 0.45, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="text-center min-w-[100px]"
|
||||
>
|
||||
<div className="text-3xl md:text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-stone-900 to-stone-600">
|
||||
<div className="text-3xl md:text-4xl font-bold tracking-tight text-[var(--color-text-primary)]">
|
||||
{stat.value}
|
||||
</div>
|
||||
<div className="text-sm text-stone-500 mt-1">{stat.label}</div>
|
||||
<div className="text-sm mt-1 text-[var(--color-text-subtle)]">{stat.label}</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Bottom fade */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-20 bg-gradient-to-t from-[var(--color-bg-primary)] to-transparent pointer-events-none" aria-hidden="true" />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ export function ProductCardV3({ product }: ProductCardV3Props) {
|
||||
className="group"
|
||||
>
|
||||
<Link href={`/products/${product.id}`} className="block">
|
||||
<div className="relative h-full rounded-2xl bg-white border border-gray-100 overflow-hidden hover:border-[#C41E3A]/30 hover:shadow-xl hover:-translate-y-1 transition-all duration-300">
|
||||
<div className="aspect-video relative overflow-hidden bg-gradient-to-br from-gray-50 to-gray-100">
|
||||
<div className="relative h-full rounded-2xl bg-white border border-[var(--color-border-primary)] overflow-hidden hover:border-[#C41E3A]/30 hover:shadow-xl hover:-translate-y-1 transition-all duration-300">
|
||||
<div className="aspect-video relative overflow-hidden bg-gradient-to-br from-[var(--color-bg-section)] to-[var(--color-bg-tertiary)]">
|
||||
<Image
|
||||
src={product.image}
|
||||
alt={product.title}
|
||||
@@ -54,11 +54,11 @@ export function ProductCardV3({ product }: ProductCardV3Props) {
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-3 group-hover:text-[#C41E3A] transition-colors">
|
||||
<h3 className="text-xl font-bold text-[var(--color-text-primary)] mb-3 group-hover:text-[#C41E3A] transition-colors">
|
||||
{product.title}
|
||||
</h3>
|
||||
|
||||
<p className="text-sm text-gray-600 leading-relaxed mb-5 line-clamp-2">
|
||||
<p className="text-sm text-[var(--color-text-muted)] leading-relaxed mb-5 line-clamp-2">
|
||||
{product.description}
|
||||
</p>
|
||||
|
||||
@@ -67,7 +67,7 @@ export function ProductCardV3({ product }: ProductCardV3Props) {
|
||||
{product.features.slice(0, 3).map((feature) => (
|
||||
<span
|
||||
key={feature}
|
||||
className="px-2.5 py-1 rounded-md text-xs font-medium bg-gray-50 text-gray-600 border border-gray-100"
|
||||
className="px-2.5 py-1 rounded-md text-xs font-medium bg-[var(--color-bg-section)] text-[var(--color-text-muted)] border border-[var(--color-border-primary)]"
|
||||
>
|
||||
{feature}
|
||||
</span>
|
||||
|
||||
@@ -43,8 +43,8 @@ export function SolutionCardV3({ solution }: SolutionCardV3Props) {
|
||||
className="group"
|
||||
>
|
||||
<Link href={`/solutions/${solution.id}`} className="block">
|
||||
<div className="relative h-full rounded-2xl bg-white border border-gray-100 overflow-hidden hover:border-blue-300 hover:shadow-xl hover:-translate-y-1 transition-all duration-300">
|
||||
<div className="aspect-video relative overflow-hidden bg-gradient-to-br from-blue-50 to-indigo-50 flex items-center justify-center">
|
||||
<div className="relative h-full rounded-2xl bg-white border border-[var(--color-border-primary)] overflow-hidden hover:border-[rgba(var(--color-brand-primary-rgb),0.3)] hover:shadow-xl hover:-translate-y-1 transition-all duration-300">
|
||||
<div className="aspect-video relative overflow-hidden bg-gradient-to-br from-[var(--color-brand-primary-bg)] to-[rgba(var(--color-brand-primary-rgb),0.04)] flex items-center justify-center">
|
||||
{solution.image ? (
|
||||
<Image
|
||||
src={solution.image}
|
||||
@@ -53,17 +53,17 @@ export function SolutionCardV3({ solution }: SolutionCardV3Props) {
|
||||
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
/>
|
||||
) : (
|
||||
<Icon className="w-16 h-16 text-blue-200 group-hover:text-blue-400 transition-colors" />
|
||||
<Icon className="w-16 h-16 text-[var(--color-brand-primary)]/20 group-hover:text-[var(--color-brand-primary)]/40 transition-colors" />
|
||||
)}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent" />
|
||||
|
||||
<div className="absolute top-4 left-4 flex gap-2">
|
||||
<span className="px-3 py-1 rounded-full text-xs font-semibold bg-blue-600 text-white shadow-lg inline-flex items-center gap-1.5">
|
||||
<span className="px-3 py-1 rounded-full text-xs font-semibold bg-[var(--color-brand-primary)] text-white shadow-lg inline-flex items-center gap-1.5">
|
||||
<Icon className="w-3 h-3" />
|
||||
{solution.industry}
|
||||
</span>
|
||||
{solution.suiteCombination && solution.suiteCombination.primaryProducts.length > 0 && (
|
||||
<span className="px-3 py-1 rounded-full text-xs font-semibold bg-white/90 text-stone-700 shadow-lg inline-flex items-center gap-1.5 backdrop-blur-sm">
|
||||
<span className="px-3 py-1 rounded-full text-xs font-semibold bg-white/90 text-[var(--color-text-secondary)] shadow-lg inline-flex items-center gap-1.5 backdrop-blur-sm">
|
||||
<Package className="w-3 h-3" />
|
||||
{solution.suiteCombination.primaryProducts.length} 产品组合
|
||||
</span>
|
||||
@@ -72,26 +72,26 @@ export function SolutionCardV3({ solution }: SolutionCardV3Props) {
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-3 group-hover:text-blue-600 transition-colors">
|
||||
<h3 className="text-xl font-bold text-[var(--color-text-primary)] mb-3 group-hover:text-[var(--color-brand-primary)] transition-colors">
|
||||
{solution.title}
|
||||
</h3>
|
||||
|
||||
<p className="text-sm text-gray-600 leading-relaxed mb-5 line-clamp-2">
|
||||
<p className="text-sm text-[var(--color-text-muted)] leading-relaxed mb-5 line-clamp-2">
|
||||
{solution.description}
|
||||
</p>
|
||||
|
||||
{solution.challenges && solution.challenges.length > 0 && (
|
||||
<div className="space-y-2 mb-5">
|
||||
{solution.challenges.slice(0, 2).map((challenge) => (
|
||||
<div key={challenge} className="flex items-start gap-2 text-sm text-gray-600">
|
||||
<span className="w-1 h-1 rounded-full bg-blue-400 mt-1.5 shrink-0" />
|
||||
<div key={challenge} className="flex items-start gap-2 text-sm text-[var(--color-text-muted)]">
|
||||
<span className="w-1 h-1 rounded-full bg-[var(--color-brand-primary)] mt-1.5 shrink-0" />
|
||||
<span className="line-clamp-1">{challenge}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-blue-600 group-hover:gap-3 transition-all">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-[var(--color-brand-primary)] group-hover:gap-3 transition-all">
|
||||
查看方案
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</div>
|
||||
@@ -124,17 +124,17 @@ export function ServiceCardV3({ service }: ServiceCardV3Props) {
|
||||
className="group"
|
||||
>
|
||||
<Link href={`/services/${service.id}`} className="block">
|
||||
<div className="relative h-full rounded-2xl bg-gradient-to-br from-green-50 to-emerald-50/50 border border-green-100 overflow-hidden hover:border-green-300 hover:shadow-xl hover:-translate-y-1 transition-all duration-300">
|
||||
<div className="relative h-full rounded-2xl bg-gradient-to-br from-[rgba(var(--color-accent-blue-rgb),0.06)] to-[rgba(var(--color-accent-blue-rgb),0.02)] border border-[var(--color-border-primary)] overflow-hidden hover:border-[rgba(var(--color-accent-blue-rgb),0.3)] hover:shadow-xl hover:-translate-y-1 transition-all duration-300">
|
||||
<div className="p-8">
|
||||
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-green-500 to-emerald-600 flex items-center justify-center mb-5 shadow-lg shadow-green-500/20 group-hover:scale-110 transition-transform">
|
||||
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[var(--color-accent-blue)] to-[#1d4ed8] flex items-center justify-center mb-5 shadow-lg shadow-[rgba(var(--color-accent-blue-rgb),0.2)] group-hover:scale-110 transition-transform">
|
||||
<Cpu className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-3 group-hover:text-green-700 transition-colors">
|
||||
<h3 className="text-xl font-bold text-[var(--color-text-primary)] mb-3 group-hover:text-[var(--color-accent-blue)] transition-colors">
|
||||
{service.title}
|
||||
</h3>
|
||||
|
||||
<p className="text-sm text-gray-600 leading-relaxed mb-5 line-clamp-3">
|
||||
<p className="text-sm text-[var(--color-text-muted)] leading-relaxed mb-5 line-clamp-3">
|
||||
{service.description}
|
||||
</p>
|
||||
|
||||
@@ -143,7 +143,7 @@ export function ServiceCardV3({ service }: ServiceCardV3Props) {
|
||||
{service.features.slice(0, 3).map((feature) => (
|
||||
<span
|
||||
key={feature}
|
||||
className="px-2.5 py-1 rounded-md text-xs font-medium bg-white/80 text-green-700 border border-green-100"
|
||||
className="px-2.5 py-1 rounded-md text-xs font-medium bg-[var(--color-bg-primary)] text-[var(--color-accent-blue)] border border-[var(--color-border-primary)]"
|
||||
>
|
||||
{feature.split(':')[0]}
|
||||
</span>
|
||||
@@ -151,7 +151,7 @@ export function ServiceCardV3({ service }: ServiceCardV3Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-green-600 group-hover:gap-3 transition-all">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-[var(--color-accent-blue)] group-hover:gap-3 transition-all">
|
||||
了解服务
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</div>
|
||||
|
||||
@@ -5,10 +5,12 @@ import { COMPANY_INFO, NAVIGATION_V2, MEGA_DROPDOWN_DATA } from '@/lib/constants
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="bg-[var(--color-footer-bg)] text-white py-12 md:py-16" data-testid="footer" role="contentinfo">
|
||||
<footer className="bg-[var(--color-footer-bg)] text-[var(--color-footer-text)] py-12 md:py-16 relative" data-testid="footer" role="contentinfo">
|
||||
{/* Ink texture overlay */}
|
||||
<div className="absolute inset-0 bg-paper-texture opacity-30 pointer-events-none" />
|
||||
<div className="container-wide">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-10 lg:gap-8">
|
||||
<div data-testid="card-brand" className="lg:col-span-2">
|
||||
<div data-testid="card-brand" className="lg:col-span-2 relative z-10">
|
||||
<div className="mb-6">
|
||||
<Image
|
||||
src="/logo-light.svg"
|
||||
@@ -37,15 +39,14 @@ export function Footer() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-testid="card-navigation">
|
||||
<h3 className="font-semibold text-base mb-5 text-white">快速导航</h3>
|
||||
<div data-testid="card-navigation" className="relative z-10">
|
||||
<h3 className="font-semibold text-base mb-5 text-[var(--color-footer-text-link)]">快速导航</h3>
|
||||
<ul className="space-y-3">
|
||||
{NAVIGATION_V2.map((item) => (
|
||||
<li key={item.id}>
|
||||
<StaticLink
|
||||
href={item.href}
|
||||
className="text-[var(--color-footer-text)] hover:text-white transition-colors duration-200 text-sm"
|
||||
>
|
||||
className="text-[var(--color-footer-text)] hover:text-[var(--color-footer-text-link)] transition-colors duration-200 text-sm" >
|
||||
{item.label}
|
||||
</StaticLink>
|
||||
</li>
|
||||
@@ -53,15 +54,14 @@ export function Footer() {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div data-testid="card-products">
|
||||
<h3 className="font-semibold text-base mb-5 text-white">产品与方案</h3>
|
||||
<div data-testid="card-products" className="relative z-10">
|
||||
<h3 className="font-semibold text-base mb-5 text-[var(--color-footer-text-link)]">产品与方案</h3>
|
||||
<ul className="space-y-3">
|
||||
{(MEGA_DROPDOWN_DATA.products ?? []).flatMap(group => group.items).filter(item => item.href !== '#').map((item) => (
|
||||
<li key={item.id}>
|
||||
<StaticLink
|
||||
href={item.href}
|
||||
className="text-[var(--color-footer-text)] hover:text-white transition-colors duration-200 text-sm"
|
||||
>
|
||||
className="text-[var(--color-footer-text)] hover:text-[var(--color-footer-text-link)] transition-colors duration-200 text-sm" >
|
||||
{item.title}
|
||||
</StaticLink>
|
||||
</li>
|
||||
@@ -70,8 +70,7 @@ export function Footer() {
|
||||
<li key={item.id}>
|
||||
<StaticLink
|
||||
href={item.href}
|
||||
className="text-[var(--color-footer-text)] hover:text-white transition-colors duration-200 text-sm"
|
||||
>
|
||||
className="text-[var(--color-footer-text)] hover:text-[var(--color-footer-text-link)] transition-colors duration-200 text-sm" >
|
||||
{item.title}
|
||||
</StaticLink>
|
||||
</li>
|
||||
@@ -79,8 +78,8 @@ export function Footer() {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div data-testid="card-contact">
|
||||
<h3 className="font-semibold text-base mb-5 text-white">联系方式</h3>
|
||||
<div data-testid="card-contact" className="relative z-10">
|
||||
<h3 className="font-semibold text-base mb-5 text-[var(--color-footer-text-link)]">联系方式</h3>
|
||||
<ul className="space-y-4">
|
||||
<li className="flex items-start gap-3">
|
||||
<MapPin className="w-4 h-4 text-[var(--color-brand-primary)] mt-0.5 shrink-0" />
|
||||
@@ -107,16 +106,16 @@ export function Footer() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-[var(--color-footer-border)] mt-12 pt-8 pb-24 md:pb-8" style={{ paddingBottom: 'calc(5rem + env(safe-area-inset-bottom, 0px))' }}>
|
||||
<div className="border-t border-[var(--color-footer-border)] mt-12 pt-8 pb-24 md:pb-8 relative z-10" style={{ paddingBottom: 'calc(5rem + env(safe-area-inset-bottom, 0px))' }}>
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<p className="text-[var(--color-footer-text-muted)] text-sm">
|
||||
© {new Date().getFullYear()} {COMPANY_INFO.name}. All rights reserved.
|
||||
</p>
|
||||
<div className="flex gap-6">
|
||||
<StaticLink href="/privacy" className="text-[var(--color-footer-text-muted)] hover:text-white text-sm transition-colors duration-200">
|
||||
<StaticLink href="/privacy" className="text-[var(--color-footer-text-muted)] hover:text-[var(--color-footer-text-link)] text-sm transition-colors duration-200">
|
||||
隐私政策
|
||||
</StaticLink>
|
||||
<StaticLink href="/terms" className="text-[var(--color-footer-text-muted)] hover:text-white text-sm transition-colors duration-200">
|
||||
<StaticLink href="/terms" className="text-[var(--color-footer-text-muted)] hover:text-[var(--color-footer-text-link)] text-sm transition-colors duration-200">
|
||||
服务条款
|
||||
</StaticLink>
|
||||
</div>
|
||||
@@ -128,8 +127,7 @@ export function Footer() {
|
||||
href="https://beian.miit.gov.cn/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[var(--color-footer-text-link)] hover:text-white transition-colors duration-200"
|
||||
>
|
||||
className="text-[var(--color-footer-text-link)] hover:text-white transition-colors duration-200" >
|
||||
{COMPANY_INFO.icp}
|
||||
</a>
|
||||
<span className="hidden sm:inline text-[var(--color-footer-text-dim)]">|</span>
|
||||
|
||||
@@ -51,10 +51,10 @@ function HeaderContent() {
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleNavClick = useCallback((e: React.MouseEvent<HTMLAnchorElement>, item: NavigationItemV2) => {
|
||||
e.preventDefault();
|
||||
window.location.href = item.href;
|
||||
const handleNavClick = useCallback((_e: React.MouseEvent<HTMLAnchorElement>, item: NavigationItemV2) => {
|
||||
setIsOpen(false);
|
||||
// StaticLink 会 e.preventDefault(),需要手动执行导航
|
||||
window.location.href = item.href;
|
||||
}, []);
|
||||
|
||||
const isActive = useCallback((item: NavigationItemV2) => {
|
||||
@@ -80,7 +80,7 @@ function HeaderContent() {
|
||||
fixed top-0 left-0 right-0 z-50
|
||||
transition-all duration-300 ease-out
|
||||
${isScrolled
|
||||
? 'bg-[var(--color-bg-primary)]/95 backdrop-blur-xl border-b border-[var(--color-border-primary)] shadow-sm'
|
||||
? 'bg-[var(--color-bg-primary)]/90 backdrop-blur-xl border-b border-[var(--color-border-primary)] shadow-sm'
|
||||
: 'bg-transparent'
|
||||
}
|
||||
`}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { ChallengeCard } from '@/components/ui/challenge-card';
|
||||
import { GeometricDecoration } from '@/components/ui/brand-visuals';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
|
||||
const CHALLENGES = [
|
||||
@@ -32,7 +33,8 @@ export function ChallengeSection() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section id="challenges" className="relative py-20 md:py-28 bg-[var(--color-bg-section)]">
|
||||
<section id="challenges" className="relative py-20 md:py-28 bg-[var(--color-bg-section)] bg-gradient-subtle-warm">
|
||||
<GeometricDecoration variant="circles" />
|
||||
<div className="container-wide">
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRef, useState } from 'react';
|
||||
import { motion, useMotionValue, useSpring, useTransform } from 'framer-motion';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { BrandStamp } from '@/components/ui/brand-visuals';
|
||||
import { ArrowRight, Sparkles, MessageCircle, Clock, ShieldCheck } from 'lucide-react';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
|
||||
@@ -75,6 +76,15 @@ export function CTASection({
|
||||
'linear-gradient(to top, rgba(196, 30, 58, 0.06) 0%, transparent 40%), linear-gradient(135deg, transparent 40%, rgba(196, 30, 58, 0.03) 100%)',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.03]"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: 'repeat',
|
||||
backgroundSize: '128px 128px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="container-wide relative z-10">
|
||||
@@ -83,8 +93,11 @@ export function CTASection({
|
||||
whileInView={{ opacity: 1, scale: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-80px' }}
|
||||
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="text-center max-w-3xl mx-auto"
|
||||
className="text-center max-w-3xl mx-auto relative"
|
||||
>
|
||||
<div className="absolute -top-8 left-1/2 -translate-x-1/2 hidden md:block">
|
||||
<BrandStamp>值得信赖</BrandStamp>
|
||||
</div>
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: -8 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
|
||||
@@ -93,7 +93,7 @@ export function HeroSectionV2() {
|
||||
|
||||
<motion.div
|
||||
{...fadeUp(0.35)}
|
||||
className="flex flex-col sm:flex-row items-start gap-4"
|
||||
className="flex flex-col sm:flex-row items-start gap-4 mb-10"
|
||||
>
|
||||
<Button size="lg" asChild className="min-h-[52px] px-8 text-base font-semibold shadow-lg shadow-[var(--color-brand-primary)]/20">
|
||||
<StaticLink href="/contact">
|
||||
@@ -105,24 +105,6 @@ export function HeroSectionV2() {
|
||||
<StaticLink href="/products">探索产品</StaticLink>
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
{...fadeUp(0.45)}
|
||||
className="flex flex-wrap gap-2 sm:gap-3 mt-8"
|
||||
>
|
||||
{CAPABILITIES.map((cap) => {
|
||||
const Icon = cap.icon;
|
||||
return (
|
||||
<div
|
||||
key={cap.label}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-[var(--color-bg-section)] border border-[var(--color-border-primary)]"
|
||||
>
|
||||
<Icon className="w-4 h-4 text-[var(--color-brand-primary)]" strokeWidth={2.2} />
|
||||
<span className="text-xs font-medium text-[var(--color-text-primary)]">{cap.label}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
@@ -132,7 +114,7 @@ export function HeroSectionV2() {
|
||||
>
|
||||
<div
|
||||
ref={cardRef}
|
||||
className="relative ink-glow-border rounded-2xl"
|
||||
className="relative ink-glow-border rounded-2xl lg:opacity-95"
|
||||
style={
|
||||
{
|
||||
'--glow-start': 'var(--color-brand-primary)',
|
||||
@@ -163,7 +145,7 @@ export function HeroSectionV2() {
|
||||
四步合作流程,确保每个项目科学推进、高效交付
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 lg:gap-4 lg:space-y-0 lg:block lg:space-y-4">
|
||||
<div className="grid grid-cols-2 gap-3 lg:gap-4 lg:block lg:space-y-4">
|
||||
{JOURNEY_STEPS.map((step, idx) => {
|
||||
const Icon = step.icon;
|
||||
return (
|
||||
@@ -185,6 +167,32 @@ export function HeroSectionV2() {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Integrated capabilities */}
|
||||
<div className="mt-4 pt-4 border-t border-[var(--color-border-primary)]/50">
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{CAPABILITIES.map((cap, idx) => {
|
||||
const Icon = cap.icon;
|
||||
return (
|
||||
<div
|
||||
key={cap.label}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg border transition-colors duration-200"
|
||||
style={{
|
||||
backgroundColor: idx === 0 ? 'var(--color-brand-primary-bg)' : idx === 1 ? 'rgba(var(--color-accent-blue-rgb), 0.06)' : idx === 2 ? 'rgba(var(--color-accent-purple-rgb), 0.06)' : 'rgba(var(--color-accent-cyan-rgb), 0.06)',
|
||||
borderColor: idx === 0 ? 'rgba(var(--color-brand-primary-rgb), 0.15)' : 'var(--color-border-primary)',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
className="w-4 h-4"
|
||||
style={{ color: idx === 0 ? 'var(--color-brand-primary)' : idx === 1 ? 'var(--color-accent-blue)' : idx === 2 ? 'var(--color-accent-purple)' : 'var(--color-accent-cyan)' }}
|
||||
strokeWidth={2.2}
|
||||
/>
|
||||
<span className="text-xs font-medium text-[var(--color-text-primary)]">{cap.label}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useState } from 'react';
|
||||
import { motion, useMotionValue, useSpring, useTransform } from 'framer-motion';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ArrowRight, MessageCircle } from 'lucide-react';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
|
||||
export interface NarrativeCTAProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
primaryLabel?: string;
|
||||
primaryHref?: string;
|
||||
secondaryLabel?: string;
|
||||
secondaryHref?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 第四层:行动号召
|
||||
* 全宽深色背景 + 水墨纹理 + 双CTA按钮
|
||||
*/
|
||||
export function NarrativeCTA({
|
||||
title = '一起聊聊您的数字化需求',
|
||||
description = '无论您处于数字化转型的哪个阶段,我们都愿意坐下来一起想办法。首次咨询免费,无任何销售压力。',
|
||||
primaryLabel = '预约免费咨询',
|
||||
primaryHref = '/contact',
|
||||
secondaryLabel = '了解我们的方法',
|
||||
secondaryHref = '/team',
|
||||
}: NarrativeCTAProps) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const mouseX = useMotionValue(0);
|
||||
const mouseY = useMotionValue(0);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const springX = useSpring(mouseX, { stiffness: 80, damping: 20 });
|
||||
const springY = useSpring(mouseY, { stiffness: 80, damping: 20 });
|
||||
|
||||
const glowX = useTransform(springX, [0, 1], ['-10%', '110%']);
|
||||
const glowY = useTransform(springY, [0, 1], ['-10%', '110%']);
|
||||
|
||||
function handleMouseMove(e: React.MouseEvent<HTMLDivElement>) {
|
||||
if (!containerRef.current || shouldReduceMotion) return;
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
mouseX.set((e.clientX - rect.left) / rect.width);
|
||||
mouseY.set((e.clientY - rect.top) / rect.height);
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
id="cta"
|
||||
ref={containerRef}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className="relative py-24 md:py-32 overflow-hidden"
|
||||
style={{ backgroundColor: 'var(--color-cta-bg)' }}
|
||||
>
|
||||
{/* Ink texture background */}
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
{!shouldReduceMotion && (
|
||||
<motion.div
|
||||
className="absolute inset-[-20%]"
|
||||
style={{
|
||||
background: 'radial-gradient(circle at center, rgba(196, 30, 58, 0.06) 0%, transparent 50%)',
|
||||
x: glowX,
|
||||
y: glowY,
|
||||
opacity: isHovered ? 1 : 0.4,
|
||||
}}
|
||||
transition={{ opacity: { duration: 0.6 } }}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.03]"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: 'repeat',
|
||||
backgroundSize: '200px 200px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="container-wide relative z-10">
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, scale: 0.97, y: 16 }}
|
||||
whileInView={{ opacity: 1, scale: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-80px' }}
|
||||
transition={{ duration: 0.6, ease: [0.25, 1, 0.5, 1] }}
|
||||
className="text-center max-w-3xl mx-auto"
|
||||
>
|
||||
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-semibold text-white mb-6 tracking-tight leading-tight">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="text-lg text-white/60 leading-relaxed mb-10 max-w-2xl mx-auto">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<Button size="lg" asChild className="group relative overflow-hidden">
|
||||
<StaticLink href={primaryHref}>
|
||||
<span className="relative z-10 flex items-center">
|
||||
<MessageCircle className="w-4 h-4 mr-2" />
|
||||
{primaryLabel}
|
||||
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
|
||||
</span>
|
||||
</StaticLink>
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="border-white/20 text-white hover:bg-white/10 hover:border-white/35 backdrop-blur-sm"
|
||||
asChild
|
||||
>
|
||||
<StaticLink href={secondaryHref}>{secondaryLabel}</StaticLink>
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { InkWashBackground } from '@/components/ui/ink-wash';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface NarrativeHeroProps {
|
||||
/** Page badge/tag */
|
||||
badge?: { text: string; variant?: 'brand' | 'blue' | 'green' | 'purple' };
|
||||
/** Main headline */
|
||||
title: string;
|
||||
/** Keyword highlighted in calligraphy font + brand red */
|
||||
highlight?: string;
|
||||
/** Subtitle / one-line value proposition */
|
||||
subtitle: string;
|
||||
/** Description paragraph */
|
||||
description?: string;
|
||||
/** Primary CTA */
|
||||
primaryCta?: { label: string; href: string };
|
||||
/** Secondary CTA */
|
||||
secondaryCta?: { label: string; href: string };
|
||||
/** Right side content (card, image, etc.) */
|
||||
rightContent?: ReactNode;
|
||||
/** Stats row */
|
||||
stats?: { value: string; label: string }[];
|
||||
/** Extra content below description */
|
||||
extraContent?: ReactNode;
|
||||
}
|
||||
|
||||
const EASE = [0.25, 1, 0.5, 1] as const;
|
||||
|
||||
export function NarrativeHero({
|
||||
badge,
|
||||
title,
|
||||
highlight,
|
||||
subtitle,
|
||||
description,
|
||||
primaryCta,
|
||||
secondaryCta,
|
||||
rightContent,
|
||||
stats,
|
||||
extraContent,
|
||||
}: NarrativeHeroProps) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
const fadeUp = (delay: number) => ({
|
||||
initial: shouldReduceMotion ? {} : { opacity: 0, y: 20 },
|
||||
animate: { opacity: 1, y: 0 },
|
||||
transition: { duration: 0.6, delay, ease: EASE },
|
||||
});
|
||||
|
||||
return (
|
||||
<section
|
||||
className="relative min-h-[80vh] flex flex-col justify-center overflow-hidden bg-[var(--color-bg-primary)]"
|
||||
>
|
||||
<InkWashBackground variant="hero" />
|
||||
|
||||
<div className="container-wide py-20 md:py-28 lg:py-36 relative z-10 flex-1 flex items-center">
|
||||
<div className={`grid grid-cols-1 ${rightContent ? 'lg:grid-cols-2 gap-10 lg:gap-16' : 'max-w-4xl mx-auto text-center'} items-center w-full`}>
|
||||
<div className={rightContent ? '' : 'mx-auto'}>
|
||||
{/* Badge */}
|
||||
{badge && (
|
||||
<motion.div {...fadeUp(0)} className="mb-6">
|
||||
<span className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-[var(--color-brand-primary-bg)] text-[var(--color-brand-primary)] text-xs font-medium border border-[var(--color-brand-primary)]/10">
|
||||
{badge.text}
|
||||
</span>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Subtitle above title */}
|
||||
<motion.p
|
||||
{...fadeUp(0.05)}
|
||||
className="text-base sm:text-lg text-[var(--color-text-muted)] mb-3"
|
||||
>
|
||||
{subtitle}
|
||||
</motion.p>
|
||||
|
||||
{/* Title with calligraphy highlight */}
|
||||
<motion.h1
|
||||
{...fadeUp(0.1)}
|
||||
className="text-4xl sm:text-5xl lg:text-6xl xl:text-7xl tracking-tight mb-6 leading-[1.12]"
|
||||
>
|
||||
{highlight ? (
|
||||
<>
|
||||
{title}{' '}
|
||||
<span className="text-[var(--color-brand-primary)] font-calligraphy">{highlight}</span>
|
||||
</>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</motion.h1>
|
||||
|
||||
{/* Description */}
|
||||
{description && (
|
||||
<motion.p
|
||||
{...fadeUp(0.2)}
|
||||
className="text-base sm:text-lg text-[var(--color-text-muted)] max-w-lg leading-relaxed mb-8"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
)}
|
||||
|
||||
{/* CTAs */}
|
||||
{(primaryCta || secondaryCta) && (
|
||||
<motion.div
|
||||
{...fadeUp(0.3)}
|
||||
className={`flex ${rightContent ? 'flex-col sm:flex-row items-start' : 'flex-col sm:flex-row items-center justify-center'} gap-4 mb-10`}
|
||||
>
|
||||
{primaryCta && (
|
||||
<Button size="lg" asChild className="min-h-[52px] px-8 text-base font-semibold shadow-lg shadow-[var(--color-brand-primary)]/15">
|
||||
<StaticLink href={primaryCta.href}>
|
||||
{primaryCta.label}
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</StaticLink>
|
||||
</Button>
|
||||
)}
|
||||
{secondaryCta && (
|
||||
<Button size="lg" variant="ghost" asChild className="text-[var(--color-text-muted)]">
|
||||
<StaticLink href={secondaryCta.href}>{secondaryCta.label}</StaticLink>
|
||||
</Button>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Extra content */}
|
||||
{extraContent}
|
||||
</div>
|
||||
|
||||
{/* Right side content */}
|
||||
{rightContent && (
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 28 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.7, delay: 0.4, ease: EASE }}
|
||||
>
|
||||
{rightContent}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats row */}
|
||||
{stats && stats.length > 0 && (
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.6, ease: EASE }}
|
||||
className="container-wide pb-16 relative z-10"
|
||||
>
|
||||
<div className="ink-divider mb-10" />
|
||||
<div className="flex flex-wrap justify-center gap-8 md:gap-16">
|
||||
{stats.map((stat) => (
|
||||
<div key={stat.label} className="text-center">
|
||||
<div className="text-3xl md:text-4xl font-bold text-[var(--color-text-primary)] leading-none mb-1">
|
||||
{stat.value}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--color-text-subtle)] tracking-wide">{stat.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { InkWashBackground } from '@/components/ui/ink-wash';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface NarrativeTrustProps {
|
||||
/** Section badge */
|
||||
badge?: string;
|
||||
/** Section title */
|
||||
title: string;
|
||||
/** Title highlight */
|
||||
highlight?: string;
|
||||
/** Stat metrics */
|
||||
metrics?: { value: string; label: string; description?: string }[];
|
||||
/** Custom content (e.g., case studies, testimonials) */
|
||||
children?: ReactNode;
|
||||
/** Background variant */
|
||||
bgVariant?: 'default' | 'section';
|
||||
}
|
||||
|
||||
/**
|
||||
* 第三层:信任证明
|
||||
* 数据指标 + 案例摘要 + 资质认证
|
||||
*/
|
||||
export function NarrativeTrust({
|
||||
badge,
|
||||
title,
|
||||
highlight,
|
||||
metrics,
|
||||
children,
|
||||
bgVariant = 'default',
|
||||
}: NarrativeTrustProps) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section className={`relative section-padding overflow-hidden ${bgVariant === 'section' ? 'bg-[var(--color-bg-section)]' : 'bg-[var(--color-bg-primary)]'}`}>
|
||||
<InkWashBackground variant="subtle" />
|
||||
|
||||
<div className="container-wide relative z-10">
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.6, ease: [0.25, 1, 0.5, 1] }}
|
||||
className="mb-14"
|
||||
>
|
||||
{badge && (
|
||||
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-[var(--color-brand-primary-bg)] border border-[var(--color-brand-primary)]/10 mb-4">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[var(--color-brand-primary)]" />
|
||||
<span className="text-xs font-medium tracking-wider text-[var(--color-brand-primary)]">{badge}</span>
|
||||
</div>
|
||||
)}
|
||||
<h2 className="text-3xl sm:text-4xl font-semibold text-[var(--color-text-primary)] mb-4">
|
||||
{title}
|
||||
{highlight && <span className="text-[var(--color-brand-primary)] font-calligraphy ml-1">{highlight}</span>}
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
{/* Metrics strip */}
|
||||
{metrics && metrics.length > 0 && (
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 16 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, ease: [0.25, 1, 0.5, 1] }}
|
||||
className="grid grid-cols-2 md:grid-cols-4 gap-6 md:gap-8 mb-16"
|
||||
>
|
||||
{metrics.map((metric, idx) => (
|
||||
<motion.div
|
||||
key={metric.label}
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 16 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.4, delay: idx * 0.06 }}
|
||||
className="text-center p-6 rounded-xl bg-[var(--color-bg-primary)] border border-[var(--color-border-primary)]"
|
||||
>
|
||||
<div className="text-3xl md:text-4xl font-bold text-[var(--color-brand-primary)] leading-none mb-2">
|
||||
{metric.value}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-[var(--color-text-primary)] mb-1">{metric.label}</div>
|
||||
{metric.description && (
|
||||
<div className="text-xs text-[var(--color-text-subtle)]">{metric.description}</div>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Custom content */}
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { InkCard } from '@/components/ui/ink-wash';
|
||||
import { InkWashBackground } from '@/components/ui/ink-wash';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
|
||||
export interface ValuePoint {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
/** Optional stat number shown in corner */
|
||||
stat?: string;
|
||||
statLabel?: string;
|
||||
}
|
||||
|
||||
export interface NarrativeValueProps {
|
||||
/** Section badge */
|
||||
badge?: string;
|
||||
/** Section title */
|
||||
title: string;
|
||||
/** Title highlight (calligraphy) */
|
||||
highlight?: string;
|
||||
/** Subtitle */
|
||||
subtitle?: string;
|
||||
/** Value points */
|
||||
points: ValuePoint[];
|
||||
/** Grid columns */
|
||||
columns?: 2 | 3 | 4 | 5;
|
||||
/** Background variant */
|
||||
bgVariant?: 'default' | 'section';
|
||||
}
|
||||
|
||||
export function NarrativeValue({
|
||||
badge,
|
||||
title,
|
||||
highlight,
|
||||
subtitle,
|
||||
points,
|
||||
columns = 3,
|
||||
bgVariant = 'section',
|
||||
}: NarrativeValueProps) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
const gridCols = {
|
||||
2: 'md:grid-cols-2',
|
||||
3: 'md:grid-cols-2 lg:grid-cols-3',
|
||||
4: 'md:grid-cols-2 lg:grid-cols-4',
|
||||
5: 'md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5',
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={`relative section-padding overflow-hidden ${bgVariant === 'section' ? 'bg-[var(--color-bg-section)]' : 'bg-[var(--color-bg-primary)]'}`}>
|
||||
<InkWashBackground variant="subtle" />
|
||||
|
||||
<div className="container-wide relative z-10">
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.6, ease: [0.25, 1, 0.5, 1] }}
|
||||
className="mb-14"
|
||||
>
|
||||
{badge && (
|
||||
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-[var(--color-brand-primary-bg)] border border-[var(--color-brand-primary)]/10 mb-4">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[var(--color-brand-primary)]" />
|
||||
<span className="text-xs font-medium tracking-wider text-[var(--color-brand-primary)]">{badge}</span>
|
||||
</div>
|
||||
)}
|
||||
<h2 className="text-3xl sm:text-4xl font-semibold text-[var(--color-text-primary)] mb-4">
|
||||
{title}
|
||||
{highlight && <span className="text-[var(--color-brand-primary)] font-calligraphy ml-1">{highlight}</span>}
|
||||
</h2>
|
||||
{subtitle && (
|
||||
<p className="text-base text-[var(--color-text-muted)] max-w-2xl">{subtitle}</p>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Value cards grid */}
|
||||
<div className={`grid grid-cols-1 ${gridCols[columns]} gap-6 md:gap-8`}>
|
||||
{points.map((point, idx) => {
|
||||
const Icon = point.icon;
|
||||
return (
|
||||
<motion.div
|
||||
key={point.title}
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-60px' }}
|
||||
transition={{ duration: 0.5, delay: idx * 0.08, ease: [0.25, 1, 0.5, 1] }}
|
||||
>
|
||||
<InkCard padding="md" className="h-full">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="w-11 h-11 rounded-xl flex items-center justify-center bg-[var(--color-brand-primary-bg)]">
|
||||
<Icon className="w-5 h-5 text-[var(--color-brand-primary)]" strokeWidth={1.8} />
|
||||
</div>
|
||||
{point.stat && (
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-[var(--color-brand-primary)] leading-none">{point.stat}</div>
|
||||
{point.statLabel && (
|
||||
<div className="text-[10px] text-[var(--color-text-subtle)] mt-0.5">{point.statLabel}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-base font-semibold text-[var(--color-text-primary)] mb-2">{point.title}</h3>
|
||||
<p className="text-sm text-[var(--color-text-muted)] leading-relaxed">{point.description}</p>
|
||||
</InkCard>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export { NarrativeHero } from './NarrativeHero';
|
||||
export type { NarrativeHeroProps } from './NarrativeHero';
|
||||
export { NarrativeValue } from './NarrativeValue';
|
||||
export type { NarrativeValueProps, ValuePoint } from './NarrativeValue';
|
||||
export { NarrativeTrust } from './NarrativeTrust';
|
||||
export type { NarrativeTrustProps } from './NarrativeTrust';
|
||||
export { NarrativeCTA } from './NarrativeCTA';
|
||||
export type { NarrativeCTAProps } from './NarrativeCTA';
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { ProductCard } from '@/components/ui/product-card';
|
||||
import { GeometricDecoration } from '@/components/ui/brand-visuals';
|
||||
import { PRODUCTS } from '@/lib/constants';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
|
||||
@@ -9,8 +10,9 @@ export function ProductMatrixSection() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section id="products" className="relative py-16 md:py-20 bg-[var(--color-bg-primary)]">
|
||||
<div className="container-wide">
|
||||
<section id="products" className="relative py-16 md:py-20 bg-[var(--color-bg-primary)] bg-texture-grid">
|
||||
<GeometricDecoration variant="lines" />
|
||||
<div className="container-wide relative z-10">
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { Shield, Users, Cpu, Handshake } from 'lucide-react';
|
||||
import { Shield, Users, Cpu, Handshake, Quote } from 'lucide-react';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
|
||||
const TRUST_PILLARS = [
|
||||
const METRICS = [
|
||||
{ icon: Users, value: '50+', label: '服务企业', description: '覆盖金融、制造、零售、政务等行业' },
|
||||
{ icon: Cpu, value: '6', label: '自研产品', description: '覆盖企业数字化全场景' },
|
||||
{ icon: Shield, value: '99%', label: '交付满意度', description: '以结果为导向的交付承诺' },
|
||||
{ icon: Handshake, value: '3年+', label: '平均合作时长', description: '长期陪跑,持续为客户创造价值' },
|
||||
];
|
||||
|
||||
const TESTIMONIALS = [
|
||||
{
|
||||
icon: Shield,
|
||||
title: '私有化部署',
|
||||
description: '数据不出企业,满足安全合规与数据主权要求',
|
||||
quote: 'Novalon 团队不仅技术扎实,更重要的是真正理解我们的业务痛点,方案落地性很强。',
|
||||
author: '某金融机构 数字化负责人',
|
||||
role: '数字化转型项目',
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: '资深团队',
|
||||
description: '核心成员来自大型 IT 企业,具备扎实的工程能力与规范化交付经验',
|
||||
},
|
||||
{
|
||||
icon: Cpu,
|
||||
title: '全栈自研',
|
||||
description: '6 款产品自主研发中,覆盖 ERP、CRM、BI 等企业核心场景',
|
||||
},
|
||||
{
|
||||
icon: Handshake,
|
||||
title: '长期陪跑',
|
||||
description: '不做完就跑,从需求理解到产品打磨,持续陪伴客户成长',
|
||||
quote: '从需求沟通到上线交付,全程透明可控,响应速度超出预期。',
|
||||
author: '某制造企业 IT 总监',
|
||||
title: 'ERP 系统建设项目',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -31,43 +28,91 @@ export function SocialProofSection() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section id="social-proof" role="region" aria-labelledby="social-proof-heading" className="py-12 md:py-16 lg:py-20 bg-[var(--color-bg-section)]">
|
||||
<div className="container-wide">
|
||||
<section id="social-proof" role="region" aria-labelledby="social-proof-heading" className="py-16 md:py-24 lg:py-28 relative overflow-hidden">
|
||||
{/* Background accent */}
|
||||
<div className="absolute inset-0 bg-[var(--color-bg-primary)]" aria-hidden="true" />
|
||||
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-[var(--color-brand-primary)]/10 to-transparent" aria-hidden="true" />
|
||||
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-[var(--color-border-primary)] to-transparent" aria-hidden="true" />
|
||||
|
||||
<div className="container-wide relative z-10">
|
||||
{/* Section Header */}
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="text-center max-w-3xl mx-auto mb-14"
|
||||
className="text-center max-w-3xl mx-auto mb-16"
|
||||
>
|
||||
<h2 id="social-proof-heading" className="text-3xl sm:text-4xl font-semibold text-[var(--color-text-primary)] mb-4">
|
||||
值得信赖
|
||||
用数字说话
|
||||
</h2>
|
||||
<p className="text-base text-[var(--color-text-muted)]">
|
||||
用心打磨产品,以专业赢得信赖
|
||||
我们相信长期主义,用实际成果赢得每一位客户的信任
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8">
|
||||
{TRUST_PILLARS.map((pillar, idx) => {
|
||||
const Icon = pillar.icon;
|
||||
{/* Metrics Bar - horizontal layout instead of card grid */}
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-80px' }}
|
||||
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="grid grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8 mb-16"
|
||||
>
|
||||
{METRICS.map((metric, idx) => {
|
||||
const Icon = metric.icon;
|
||||
return (
|
||||
<motion.div
|
||||
key={pillar.title}
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 24, scale: 0.94 }}
|
||||
whileInView={{ opacity: 1, y: 0, scale: 1 }}
|
||||
key={metric.label}
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-60px' }}
|
||||
transition={{ duration: 0.5, delay: idx * 0.1 + 0.05, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="text-center p-6 md:p-8 rounded-xl bg-[var(--color-bg-primary)] border border-[var(--color-border-primary)] hover:border-[rgba(var(--color-brand-primary-rgb),0.3)] transition-colors duration-300"
|
||||
transition={{ duration: 0.5, delay: idx * 0.1, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="text-center group"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-xl bg-[var(--color-brand-primary)]/5 flex items-center justify-center mx-auto mb-4">
|
||||
<Icon className="w-5 h-5 text-[var(--color-brand-primary)]" strokeWidth={1.8} />
|
||||
<div className="w-12 h-12 rounded-full bg-[var(--color-brand-primary)]/5 flex items-center justify-center mx-auto mb-4 group-hover:bg-[var(--color-brand-primary)]/10 transition-colors duration-300">
|
||||
<Icon className="w-5 h-5 text-[var(--color-brand-primary)]" strokeWidth={1.5} />
|
||||
</div>
|
||||
<div className="text-4xl md:text-5xl font-bold text-[var(--color-text-primary)] tracking-tight mb-1">
|
||||
{metric.value}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-[var(--color-text-secondary)] mb-1">
|
||||
{metric.label}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--color-text-subtle)] max-w-[160px] mx-auto">
|
||||
{metric.description}
|
||||
</div>
|
||||
<h3 className="text-base font-semibold text-[var(--color-text-primary)] mb-2">{pillar.title}</h3>
|
||||
<p className="text-sm text-[var(--color-text-muted)] leading-relaxed">{pillar.description}</p>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
|
||||
{/* Testimonials - quote cards instead of icon cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8">
|
||||
{TESTIMONIALS.map((t, idx) => (
|
||||
<motion.div
|
||||
key={idx}
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-60px' }}
|
||||
transition={{ duration: 0.5, delay: 0.2 + idx * 0.1, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="relative p-6 md:p-8 rounded-2xl bg-[var(--color-bg-section)] border border-[var(--color-border-primary)] hover:border-[rgba(var(--color-brand-primary-rgb),0.2)] transition-all duration-300"
|
||||
>
|
||||
<Quote className="w-8 h-8 text-[var(--color-brand-primary)]/15 mb-4" strokeWidth={1.5} />
|
||||
<blockquote className="text-[var(--color-text-secondary)] text-base leading-relaxed mb-6">
|
||||
“{t.quote}”
|
||||
</blockquote>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-[var(--color-brand-primary)]/10 to-[var(--color-brand-primary)]/5 flex items-center justify-center">
|
||||
<span className="text-sm font-semibold text-[var(--color-brand-primary)]">{t.author[0]}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-[var(--color-text-primary)]">{t.author}</div>
|
||||
<div className="text-xs text-[var(--color-text-subtle)]">{('role' in t) ? t.role : t.title}</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -48,24 +48,28 @@ export function WhyUsSection() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section id="why-us" className="relative py-20 md:py-28 bg-[var(--color-bg-section)] overflow-hidden">
|
||||
<div className="container-wide">
|
||||
<section id="why-us" className="relative py-20 md:py-28 bg-[var(--color-bg-section)] bg-texture-dots overflow-hidden">
|
||||
{/* Decorative large numbers background */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
|
||||
<span className="absolute -top-24 -left-12 text-[20rem] font-bold text-[var(--color-brand-primary)]/[0.02] leading-none select-none font-calligraphy">睿</span>
|
||||
<span className="absolute -bottom-32 -right-8 text-[16rem] font-bold text-[var(--color-primary)]/[0.02] leading-none select-none">新</span>
|
||||
</div>
|
||||
<div className="container-wide relative z-10">
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="text-center max-w-3xl mx-auto mb-14"
|
||||
className="mb-14"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-4 mb-4">
|
||||
<div className="h-px flex-1 max-w-[80px] bg-[var(--color-border-primary)]" />
|
||||
<span className="text-xs font-mono tracking-widest text-[var(--color-text-subtle)]">WHY US</span>
|
||||
<div className="h-px flex-1 max-w-[80px] bg-[var(--color-border-primary)]" />
|
||||
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-[var(--color-brand-primary-bg)] border border-[var(--color-brand-primary)]/10 mb-4">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[var(--color-brand-primary)]" />
|
||||
<span className="text-xs font-medium tracking-wider text-[var(--color-brand-primary)]">核心优势</span>
|
||||
</div>
|
||||
<h2 className="text-3xl sm:text-4xl font-semibold text-[var(--color-text-primary)] mb-4">
|
||||
为什么选择<span className="text-[var(--color-brand-primary)] font-calligraphy">我们</span>
|
||||
</h2>
|
||||
<p className="text-base text-[var(--color-text-muted)] max-w-xl mx-auto">
|
||||
<p className="text-base text-[var(--color-text-muted)]">
|
||||
我们不是一家“做完就跑”的外包公司,而是愿意与您一起成长的数字化转型同行者
|
||||
</p>
|
||||
</motion.div>
|
||||
@@ -83,11 +87,39 @@ export function WhyUsSection() {
|
||||
className="relative group p-6 md:p-8 rounded-2xl bg-[var(--color-bg-primary)] border border-[var(--color-border-primary)] hover:border-[rgba(var(--color-brand-primary-rgb),0.25)] transition-all duration-300"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="w-11 h-11 rounded-xl bg-[var(--color-brand-primary-bg)] flex items-center justify-center">
|
||||
<Icon className="w-5 h-5 text-[var(--color-brand-primary)]" strokeWidth={1.8} />
|
||||
<div
|
||||
className="w-11 h-11 rounded-xl flex items-center justify-center"
|
||||
style={{
|
||||
backgroundColor: idx === 0 ? 'var(--color-brand-primary-bg)' :
|
||||
idx === 1 ? 'rgba(var(--color-accent-blue-rgb), 0.08)' :
|
||||
idx === 2 ? 'rgba(var(--color-accent-purple-rgb), 0.08)' :
|
||||
idx === 3 ? 'rgba(var(--color-accent-cyan-rgb), 0.08)' :
|
||||
'var(--color-brand-primary-bg)',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
className="w-5 h-5"
|
||||
style={{
|
||||
color: idx === 0 ? 'var(--color-brand-primary)' :
|
||||
idx === 1 ? 'var(--color-accent-blue)' :
|
||||
idx === 2 ? 'var(--color-accent-purple)' :
|
||||
idx === 3 ? 'var(--color-accent-cyan)' :
|
||||
'var(--color-brand-primary)',
|
||||
}}
|
||||
strokeWidth={1.8}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-[var(--color-brand-primary)] leading-none">{pillar.stat}</div>
|
||||
<div
|
||||
className="text-2xl font-bold leading-none"
|
||||
style={{
|
||||
color: idx === 0 ? 'var(--color-brand-primary)' :
|
||||
idx === 1 ? 'var(--color-accent-blue)' :
|
||||
idx === 2 ? 'var(--color-accent-purple)' :
|
||||
idx === 3 ? 'var(--color-accent-cyan)' :
|
||||
'var(--color-brand-primary)',
|
||||
}}
|
||||
>{pillar.stat}</div>
|
||||
<div className="text-[10px] text-[var(--color-text-subtle)] mt-0.5">{pillar.statLabel}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,6 +145,40 @@ export function WhyUsSection() {
|
||||
</StaticLink>
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
{/* Tech Stack / Partners Strip */}
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 16 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-60px' }}
|
||||
transition={{ duration: 0.5, delay: 0.4, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="mt-16 pt-10 border-t border-[var(--color-border-primary)]"
|
||||
>
|
||||
<p className="text-xs text-center text-[var(--color-text-subtle)] mb-6 tracking-wide uppercase">技术伙伴 / 技术栈</p>
|
||||
<div className="flex flex-wrap items-center justify-center gap-x-10 gap-y-6">
|
||||
{[
|
||||
{ name: 'Next.js', abbr: 'NEXT' },
|
||||
{ name: 'React', abbr: 'REACT' },
|
||||
{ name: 'TypeScript', abbr: 'TS' },
|
||||
{ name: 'Node.js', abbr: 'NODE' },
|
||||
{ name: 'Python', abbr: 'PY' },
|
||||
{ name: 'Cloud Native', abbr: 'K8S' },
|
||||
{ name: 'AWS / Azure', abbr: 'CLOUD' },
|
||||
].map((tech) => (
|
||||
<div
|
||||
key={tech.name}
|
||||
className="group flex flex-col items-center gap-1.5"
|
||||
>
|
||||
<div className="px-4 py-2 rounded-lg bg-[var(--color-bg-section)] border border-[var(--color-border-primary)] group-hover:border-[rgba(var(--color-brand-primary-rgb),0.2)] group-hover:bg-[var(--color-brand-primary-bg)]/30 transition-all duration-300">
|
||||
<span className="text-sm font-mono font-medium text-[var(--color-text-subtle)] group-hover:text-[var(--color-brand-primary)] transition-colors duration-300 tracking-wider">
|
||||
{tech.abbr}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-[var(--color-text-hint)]">{tech.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
|
||||
// ===== 抽象几何装饰 - 用于各 section 背景 =====
|
||||
export function GeometricDecoration({ variant = 'circles' }: { variant?: 'circles' | 'lines' | 'grid' | 'waves' }) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
if (variant === 'circles') {
|
||||
return (
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
|
||||
<svg className="absolute top-[10%] right-[5%] w-64 h-64 opacity-[0.04]" viewBox="0 0 200 200">
|
||||
<circle cx="100" cy="100" r="80" fill="none" stroke="currentColor" className="text-[var(--color-brand-primary)]" strokeWidth="0.5" />
|
||||
<circle cx="100" cy="100" r="60" fill="none" stroke="currentColor" className="text-[var(--color-brand-primary)]" strokeWidth="0.5" />
|
||||
<circle cx="100" cy="100" r="40" fill="none" stroke="currentColor" className="text-[var(--color-brand-primary)]" strokeWidth="0.5" />
|
||||
{!shouldReduceMotion && (
|
||||
<motion.circle
|
||||
cx="100" cy="100" r="20"
|
||||
fill="rgba(var(--color-brand-primary-rgb, 196, 30, 58), 0.1)"
|
||||
animate={{ r: [20, 35, 20], opacity: [0.1, 0.05, 0.1] }}
|
||||
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut" }}
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
<svg className="absolute bottom-[15%] left-[8%] w-48 h-48 opacity-[0.03]" viewBox="0 0 200 200">
|
||||
<rect x="40" y="40" width="120" height="120" rx="20" fill="none" stroke="currentColor" className="text-[var(--color-text-subtle)]" strokeWidth="0.5" transform="rotate(15 100 100)" />
|
||||
<rect x="60" y="60" width="80" height="80" rx="12" fill="none" stroke="currentColor" className="text-[var(--color-text-subtle)]" strokeWidth="0.5" transform="rotate(15 100 100)" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'lines') {
|
||||
return (
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
|
||||
<svg className="absolute top-0 right-0 w-full h-full opacity-[0.02]" viewBox="0 0 1000 1000" preserveAspectRatio="none">
|
||||
<line x1="800" y1="0" x2="1000" y2="400" stroke="currentColor" className="text-[var(--color-brand-primary)]" strokeWidth="1" />
|
||||
<line x1="850" y1="0" x2="1000" y2="300" stroke="currentColor" className="text-[var(--color-brand-primary)]" strokeWidth="0.5" />
|
||||
<line x1="900" y1="0" x2="1000" y2="200" stroke="currentColor" className="text-[var(--color-brand-primary)]" strokeWidth="0.5" />
|
||||
<line x1="0" y1="600" x2="300" y2="1000" stroke="currentColor" className="text-[var(--color-text-subtle)]" strokeWidth="0.5" />
|
||||
<line x1="0" y1="700" x2="200" y2="1000" stroke="currentColor" className="text-[var(--color-text-subtle)]" strokeWidth="0.5" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ===== 数据可视化条 - 用于指标展示区域 =====
|
||||
export function DataBar({ value, max = 100, label, color = 'brand' }: { value: number; max?: number; label: string; color?: 'brand' | 'blue' | 'purple' | 'cyan' }) {
|
||||
const percentage = Math.min((value / max) * 100, 100);
|
||||
const colorMap = {
|
||||
brand: 'var(--color-brand-primary)',
|
||||
blue: 'var(--color-accent-blue)',
|
||||
purple: 'var(--color-accent-purple)',
|
||||
cyan: 'var(--color-accent-cyan)',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex justify-between items-center text-xs">
|
||||
<span className="text-[var(--color-text-muted)]">{label}</span>
|
||||
<span className="font-mono font-medium text-[var(--color-text-secondary)]">{value}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 rounded-full bg-[var(--color-bg-tertiary)] overflow-hidden">
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
whileInView={{ width: `${percentage}%` }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 1, ease: [0.16, 1, 0.3, 1], delay: 0.2 }}
|
||||
className="h-full rounded-full"
|
||||
style={{ backgroundColor: colorMap[color] }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 品牌渐变分隔线 =====
|
||||
export function GradientDivider() {
|
||||
return (
|
||||
<div className="flex items-center gap-4 py-2">
|
||||
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-[var(--color-border-primary)] to-transparent" />
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-[var(--color-brand-primary)]" />
|
||||
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-[var(--color-border-primary)] to-transparent" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 品牌徽章/印章效果 =====
|
||||
export function BrandStamp({ children }: { children: React.ReactNode }) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { scale: 1.3, opacity: 0, rotate: -12 }}
|
||||
whileInView={{ scale: 1, opacity: 1, rotate: -5 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="inline-block px-3 py-1.5 border-2 border-[var(--color-brand-primary)] rounded-sm"
|
||||
style={{
|
||||
transform: 'rotate(-5deg)',
|
||||
background: 'repeating-linear-gradient(45deg, transparent, transparent 2px, rgba(var(--color-brand-primary-rgb), 0.03) 2px, rgba(var(--color-brand-primary-rgb), 0.03) 4px)',
|
||||
}}
|
||||
>
|
||||
<span className="text-xs font-bold tracking-widest text-[var(--color-brand-primary)] font-calligraphy">
|
||||
{children}
|
||||
</span>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
|
||||
import { type ReactNode, useRef } from 'react';
|
||||
import { useMouseGlow } from '@/hooks/use-mouse-glow';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface InkCardProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
/** Enable ink-glow border on hover */
|
||||
glow?: boolean;
|
||||
/** Enable mouse follow glow */
|
||||
mouseFollow?: boolean;
|
||||
/** Card padding variant */
|
||||
padding?: 'none' | 'sm' | 'md' | 'lg';
|
||||
/** Interactive - adds hover elevation */
|
||||
interactive?: boolean;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
const paddingMap = {
|
||||
none: '',
|
||||
sm: 'p-4 sm:p-5',
|
||||
md: 'p-5 sm:p-6 md:p-7',
|
||||
lg: 'p-6 sm:p-8 md:p-10',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 水墨雅致统一卡片组件
|
||||
* 融合 ink-glow-border + mouse-follow-glow + 宣纸底色
|
||||
*/
|
||||
export function InkCard({
|
||||
children,
|
||||
className,
|
||||
glow = true,
|
||||
mouseFollow = true,
|
||||
padding = 'md',
|
||||
interactive = true,
|
||||
onClick,
|
||||
href,
|
||||
}: InkCardProps) {
|
||||
const { glowStyle, handlers } = useMouseGlow({ radius: 350, opacity: 0.05 });
|
||||
const anchorRef = useRef<HTMLAnchorElement>(null);
|
||||
const divRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const cardClasses = cn(
|
||||
'relative rounded-2xl',
|
||||
'bg-[var(--color-bg-primary)]',
|
||||
'border border-[var(--color-border-primary)]',
|
||||
glow && 'ink-glow-border',
|
||||
interactive && 'transition-all duration-300 hover:border-[rgba(var(--color-brand-primary-rgb),0.2)] hover:shadow-lg',
|
||||
paddingMap[padding],
|
||||
className,
|
||||
);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{/* Mouse follow glow overlay */}
|
||||
{mouseFollow && <div style={glowStyle} />}
|
||||
{/* Card content */}
|
||||
<div className="relative z-[1]">{children}</div>
|
||||
</>
|
||||
);
|
||||
|
||||
if (href) {
|
||||
const isExternal = href.startsWith('http://') || href.startsWith('https://');
|
||||
const handleLinkClick = !isExternal
|
||||
? (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
// 阻止 Next.js 客户端路由拦截(output: 'export' 模式下 RSC payload 不存在)
|
||||
e.preventDefault();
|
||||
window.location.href = href;
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<a
|
||||
ref={anchorRef}
|
||||
href={href}
|
||||
className={cardClasses}
|
||||
onClick={handleLinkClick}
|
||||
{...handlers}
|
||||
style={glow ? { '--glow-start': 'var(--color-text-muted)', '--glow-end': 'var(--color-border-primary)' } as React.CSSProperties : undefined}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={divRef}
|
||||
className={cardClasses}
|
||||
onClick={onClick}
|
||||
{...handlers}
|
||||
style={glow ? { '--glow-start': 'var(--color-text-muted)', '--glow-end': 'var(--color-border-primary)' } as React.CSSProperties : undefined}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
'use client';
|
||||
|
||||
interface InkWashBackgroundProps {
|
||||
/** Background variant */
|
||||
variant?: 'hero' | 'section' | 'subtle';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 水墨晕散背景组件
|
||||
* 用于 Hero 和各大 section 的水墨晕散效果
|
||||
*/
|
||||
export function InkWashBackground({ variant = 'section', className }: InkWashBackgroundProps) {
|
||||
|
||||
if (variant === 'hero') {
|
||||
return (
|
||||
<div className={`absolute inset-0 overflow-hidden pointer-events-none ${className ?? ''}`} aria-hidden="true">
|
||||
{/* 主墨晕 */}
|
||||
<div
|
||||
className="absolute top-[10%] left-[15%] w-[600px] h-[600px] rounded-full"
|
||||
style={{
|
||||
background: 'radial-gradient(circle, rgba(var(--color-primary-rgb), 0.04) 0%, transparent 60%)',
|
||||
}}
|
||||
/>
|
||||
{/* 品牌红点缀 */}
|
||||
<div
|
||||
className="absolute bottom-[20%] right-[10%] w-[400px] h-[400px] rounded-full"
|
||||
style={{
|
||||
background: 'radial-gradient(circle, rgba(var(--color-brand-primary-rgb), 0.025) 0%, transparent 55%)',
|
||||
}}
|
||||
/>
|
||||
{/* 宣纸纹理 */}
|
||||
<div className="absolute inset-0 bg-paper-texture opacity-50" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'subtle') {
|
||||
return (
|
||||
<div className={`absolute inset-0 overflow-hidden pointer-events-none ${className ?? ''}`} aria-hidden="true">
|
||||
<div className="absolute inset-0 bg-paper-texture opacity-30" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default: section
|
||||
return (
|
||||
<div className={`absolute inset-0 overflow-hidden pointer-events-none ${className ?? ''}`} aria-hidden="true">
|
||||
<div
|
||||
className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[400px]"
|
||||
style={{
|
||||
background: 'radial-gradient(ellipse at 50% 0%, rgba(var(--color-primary-rgb), 0.025) 0%, transparent 65%)',
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-paper-texture opacity-30" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { InkCard } from './InkCard';
|
||||
export { InkWashBackground } from './InkWashBackground';
|
||||
@@ -12,21 +12,25 @@ interface ProductCardProps {
|
||||
status?: '研发中' | '内测中' | '已发布';
|
||||
}
|
||||
|
||||
interface ProductStyleConfig {
|
||||
icon: LucideIcon;
|
||||
iconBgOpacity: number;
|
||||
}
|
||||
|
||||
const productIcons: LucideIcon[] = [Database, Users, FileText, BarChart3, Truck, Building2];
|
||||
|
||||
const productConfig: ProductStyleConfig[] = productIcons.map((icon, i) => ({
|
||||
const PRODUCT_COLORS = [
|
||||
{ bg: 'rgba(var(--color-brand-primary-rgb), 0.08)', text: 'var(--color-brand-primary)', border: 'rgba(var(--color-brand-primary-rgb), 0.12)' },
|
||||
{ bg: 'rgba(var(--color-accent-blue-rgb), 0.08)', text: 'var(--color-accent-blue)', border: 'rgba(var(--color-accent-blue-rgb), 0.12)' },
|
||||
{ bg: 'rgba(var(--color-accent-purple-rgb), 0.08)', text: 'var(--color-accent-purple)', border: 'rgba(var(--color-accent-purple-rgb), 0.12)' },
|
||||
{ bg: 'rgba(var(--color-accent-cyan-rgb), 0.08)', text: 'var(--color-accent-cyan)', border: 'rgba(var(--color-accent-cyan-rgb), 0.12)' },
|
||||
{ bg: 'rgba(var(--color-brand-primary-rgb), 0.08)', text: 'var(--color-brand-primary)', border: 'rgba(var(--color-brand-primary-rgb), 0.12)' },
|
||||
{ bg: 'rgba(var(--color-accent-blue-rgb), 0.08)', text: 'var(--color-accent-blue)', border: 'rgba(var(--color-accent-blue-rgb), 0.12)' },
|
||||
] as const;
|
||||
|
||||
const productConfig: { icon: LucideIcon; color: { bg: string; text: string; border: string } }[] = productIcons.map((icon, i) => ({
|
||||
icon,
|
||||
iconBgOpacity: 0.04 + i * 0.015,
|
||||
color: PRODUCT_COLORS[i % PRODUCT_COLORS.length] ?? PRODUCT_COLORS[0],
|
||||
}));
|
||||
|
||||
const defaultConfig: ProductStyleConfig = {
|
||||
const defaultConfig = {
|
||||
icon: Database,
|
||||
iconBgOpacity: 0.06,
|
||||
color: PRODUCT_COLORS[0],
|
||||
};
|
||||
|
||||
const statusConfig = {
|
||||
@@ -49,10 +53,11 @@ export function ProductCard({ title, description, href, index, status }: Product
|
||||
<div className="flex items-start justify-between mb-5">
|
||||
<div
|
||||
className="w-11 h-11 rounded-xl flex items-center justify-center"
|
||||
style={{ backgroundColor: `rgba(var(--color-brand-primary-rgb), ${config.iconBgOpacity})` }}
|
||||
style={{ backgroundColor: config.color.bg }}
|
||||
>
|
||||
<IconComponent
|
||||
className="w-5 h-5 text-[var(--color-brand-primary)]"
|
||||
className="w-5 h-5"
|
||||
style={{ color: config.color.text }}
|
||||
strokeWidth={1.8}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, useScroll, useSpring } from 'framer-motion';
|
||||
import { motion, useScroll, useSpring, useTransform } from 'framer-motion';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
|
||||
export function ScrollProgress() {
|
||||
@@ -21,6 +21,8 @@ export function ScrollProgress() {
|
||||
restDelta: 0.001,
|
||||
});
|
||||
|
||||
const glowLeft = useTransform(glowX, (v) => `calc(${v * 100}% - 30px)`);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsVisible(window.scrollY > 80);
|
||||
@@ -55,14 +57,11 @@ export function ScrollProgress() {
|
||||
<motion.div
|
||||
className="absolute top-1/2 -translate-y-1/2 w-[60px] h-[3px] -mt-[0.5px]"
|
||||
style={{
|
||||
left: 'calc(var(--glow-x) * (100% - 60px))',
|
||||
left: glowLeft,
|
||||
background: 'linear-gradient(90deg, transparent, var(--color-brand-primary), transparent)',
|
||||
filter: 'blur(3px)',
|
||||
opacity: 0.6,
|
||||
}}
|
||||
animate={{
|
||||
'--glow-x': glowX,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
Reference in New Issue
Block a user