style(theme): 更新网站主题色彩方案与字体配置

- 调整主色调从 #1C1C1C 至 #1A1A1A,优化视觉层次
- 更新背景色系为暖白色调 (#FAFAF7, #F5F4F0 等)
- 配置中文字体栈,添加 serif 字体支持
- 优化文本颜色梯度,提升可读性
- 调整边框颜色,统一水墨风格
- 添加 Google Search Console 验证码配置项
- 新增桌面应用架构专家代理配置文件
- 重构 E2E 测试等待策略,提升稳定性
- 添加回归测试脚本,增强质量保障
This commit is contained in:
张翔
2026-06-17 11:37:25 +08:00
parent a3cc4c9d43
commit 415a103a24
50 changed files with 3651 additions and 1186 deletions
@@ -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 }}
+14 -1
View File
@@ -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 }}
+29 -21
View File
@@ -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">
&ldquo;{t.quote}&rdquo;
</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>
+77 -11
View File
@@ -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)]">
我们不是一家&ldquo;做完就跑&rdquo;的外包公司,而是愿意与您一起成长的数字化转型同行者
</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>
);