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
+107 -68
View File
@@ -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>
);
}
+5 -5
View File
@@ -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>
+17 -19
View File
@@ -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>
+4 -4
View File
@@ -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 }}
+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>
);
+113
View File
@@ -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>
);
}
+101
View File
@@ -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>
);
}
+2
View File
@@ -0,0 +1,2 @@
export { InkCard } from './InkCard';
export { InkWashBackground } from './InkWashBackground';
+16 -11
View File
@@ -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>
+4 -5
View File
@@ -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>