refactor: 优化网站页面结构和数据展示

- 增强服务数据模型,添加 challenges 和 outcomes 字段
- 简化统计数据配置,改为静态定义
- 重构多个页面组件,优化代码结构
- 新增产品、服务、解决方案相关的布局和组件
- 更新样式和动画配置
- 优化测试用例和类型定义
- 修复 ESLint 错误:移除不必要的 useEffect 和未使用的导入
This commit is contained in:
张翔
2026-04-25 08:44:23 +08:00
parent 9650e56dcf
commit 40384ec024
77 changed files with 3751 additions and 1226 deletions
+4 -2
View File
@@ -5,7 +5,9 @@ import { COMPANY_INFO, NAVIGATION } from '@/lib/constants';
export function Footer() {
return (
<footer className="bg-[#F5F5F5] border-t border-[#E5E5E5] py-12" data-testid="footer" role="contentinfo">
<footer className="bg-[#F5F5F5] py-12" data-testid="footer" role="contentinfo">
{/* 顶部渐变装饰线 */}
<div className="h-[2px] bg-gradient-to-r from-transparent via-[#C41E3A]/50 to-transparent" />
<div className="container-wide">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
<div className="bg-white rounded-xl p-6 border border-[#E5E5E5] shadow-sm hover:shadow-md hover:-translate-y-1 transition-all duration-300" data-testid="card-brand">
@@ -114,7 +116,7 @@ export function Footer() {
<div className="border-t border-[#E5E5E5] mt-12 pt-8">
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
<p className="text-[#5C5C5C] text-sm">
© {new Date().getFullYear()} {COMPANY_INFO.name}. All rights reserved.
© {new Date().getFullYear()} {COMPANY_INFO.name}
</p>
<div className="flex gap-6">
<StaticLink href="/privacy" className="text-[#5C5C5C] hover:text-[#C41E3A] text-sm transition-colors duration-200">
+8 -4
View File
@@ -15,17 +15,21 @@ jest.mock('next/navigation', () => ({
}));
jest.mock('next/link', () => {
return ({ children, href, onClick, ...props }: any) => (
const MockLink = ({ children, href, onClick, ...props }: any) => (
<a href={href} onClick={onClick} {...props}>
{children}
</a>
);
MockLink.displayName = 'MockLink';
return MockLink;
});
jest.mock('next/image', () => {
return ({ src, alt, width, height, className, ...props }: any) => (
const MockImage = ({ src, alt, width, height, className, ...props }: any) => (
<img src={src} alt={alt} width={width} height={height} className={className} {...props} />
);
MockImage.displayName = 'MockImage';
return MockImage;
});
jest.mock('framer-motion', () => ({
@@ -45,7 +49,7 @@ jest.mock('lucide-react', () => ({
}));
jest.mock('@/components/ui/button', () => ({
Button: ({ children, className, asChild, ...props }: any) => (
Button: ({ children, className, ...props }: any) => (
<button className={className} {...props}>
{children}
</button>
@@ -55,7 +59,7 @@ jest.mock('@/components/ui/button', () => ({
jest.mock('@/lib/constants', () => ({
COMPANY_INFO: {
name: '四川睿新致远科技有限公司',
shortName: '睿新致',
shortName: '睿新致',
},
NAVIGATION: [
{ id: 'home', label: '首页', href: '/' },
+43 -8
View File
@@ -13,9 +13,28 @@ import { useFocusTrap } from '@/hooks/use-focus-trap';
function HeaderContent() {
const [isOpen, setIsOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
const [headerTheme, setHeaderTheme] = useState<'light' | 'dark'>('light');
const pathname = usePathname();
const focusTrapRef = useFocusTrap<HTMLDivElement>(isOpen);
// 监听 data-header-theme 属性变化(产品落地页控制导航栏颜色)
useEffect(() => {
const handleThemeChange = () => {
const theme = document.documentElement.getAttribute('data-header-theme');
setHeaderTheme(theme === 'dark' ? 'dark' : 'light');
};
handleThemeChange();
const observer = new MutationObserver(handleThemeChange);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-header-theme'],
});
return () => observer.disconnect();
}, []);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 20);
@@ -72,9 +91,13 @@ function HeaderContent() {
className={`
fixed top-0 left-0 right-0 z-50
transition-all duration-300 ease-out
${isScrolled
? 'bg-white/95 backdrop-blur-xl border-b border-[#E2E8F0] shadow-sm'
: 'bg-transparent'
${headerTheme === 'dark'
? isScrolled
? 'bg-[#0A0A0A]/90 backdrop-blur-xl border-b border-[#1A1A1A]'
: 'bg-transparent'
: isScrolled
? 'bg-white/95 backdrop-blur-xl border-b border-[#E2E8F0] shadow-sm'
: 'bg-transparent'
}
`}
>
@@ -86,7 +109,7 @@ function HeaderContent() {
aria-label="返回首页"
>
<Image
src="/logo.svg"
src={headerTheme === 'dark' ? '/logo-white.svg' : '/logo.svg'}
alt={COMPANY_INFO.name}
width={128}
height={32}
@@ -105,9 +128,13 @@ function HeaderContent() {
className={`
relative px-3 py-1.5 text-sm font-medium
transition-all duration-300
${isActive(item)
? 'text-[#1C1C1C]'
: 'text-[#3D3D3D] hover:text-[#1C1C1C]'
${headerTheme === 'dark'
? isActive(item)
? 'text-white'
: 'text-[#B0B0B0] hover:text-white'
: isActive(item)
? 'text-[#1C1C1C]'
: 'text-[#3D3D3D] hover:text-[#1C1C1C]'
}
`}
aria-current={isActive(item) ? 'page' : undefined}
@@ -130,6 +157,8 @@ function HeaderContent() {
<div className="hidden md:flex items-center gap-3">
<Button
size="sm"
variant={headerTheme === 'dark' ? 'outline' : 'default'}
className={headerTheme === 'dark' ? 'border-white/30 text-white hover:bg-white/10 hover:text-white' : ''}
asChild
>
<StaticLink href="/contact" data-testid="consult-button"></StaticLink>
@@ -137,7 +166,13 @@ function HeaderContent() {
</div>
<button
className="md:hidden p-3 -mr-3 text-[#3D3D3D] hover:text-[#1C1C1C] hover:bg-[#F5F5F5] rounded-lg transition-all duration-200 active:scale-95"
className={`
md:hidden p-3 -mr-3 rounded-lg transition-all duration-200 active:scale-95
${headerTheme === 'dark'
? 'text-white hover:bg-white/10'
: 'text-[#3D3D3D] hover:text-[#1C1C1C] hover:bg-[#F5F5F5]'
}
`}
onClick={() => setIsOpen(!isOpen)}
onKeyDown={handleKeyDown}
aria-expanded={isOpen}
+104
View File
@@ -0,0 +1,104 @@
'use client';
import { StaticLink } from '@/components/ui/static-link';
import { Mail } from 'lucide-react';
import { COMPANY_INFO } from '@/lib/constants';
import { FloatingElement, RippleButton } from '@/lib/animations';
/**
* 产品站专属 Footer
*
* 与主站 Footer 的区别:
* - 浅色背景(与产品页浅色主题统一)
* - 产品 CTA 为主(而非公司信息展示)
* - 不显示公众号/企业微信二维码
* - 不显示 ICP/公安备案号(强化"独立产品站"感知)
* - 简洁的两栏布局:产品链接 + 联系方式
*/
const productLinks = [
{ label: '产品功能', href: '#features' },
{ label: '核心优势', href: '#benefits' },
{ label: '实施流程', href: '#process' },
{ label: '技术规格', href: '#specs' },
{ label: '定价方案', href: '#pricing' },
];
export function ProductFooter() {
return (
<footer className="bg-[#F8F8F8]" role="contentinfo">
{/* 顶部装饰线 */}
<div className="h-[2px] bg-gradient-to-r from-transparent via-[#C41E3A]/50 to-transparent" />
{/* CTA 区域 */}
<div className="container-wide py-16">
<div className="text-center max-w-2xl mx-auto">
<h3 className="text-2xl md:text-3xl font-bold text-[#1C1C1C] mb-4">
</h3>
<p className="text-[#5C5C5C] mb-8 text-lg">
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<StaticLink href="/contact">
<RippleButton
rippleColor="rgba(196, 30, 58, 0.3)"
className="px-8 py-3 bg-[#C41E3A] text-white rounded-lg font-medium text-base"
>
</RippleButton>
</StaticLink>
<StaticLink href="/contact">
<RippleButton
rippleColor="rgba(196, 30, 58, 0.3)"
className="px-8 py-3 border border-[#E5E5E5] text-[#5C5C5C] hover:text-[#1C1C1C] hover:bg-white rounded-lg font-medium text-base"
>
</RippleButton>
</StaticLink>
</div>
</div>
</div>
{/* 底部信息 */}
<div className="border-t border-[#E5E5E5]">
<div className="container-wide py-8">
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
{/* 左侧:Logo + 版权 */}
<div className="flex flex-col items-center md:items-start gap-2">
<span className="text-[#999999] text-sm">
&copy; {new Date().getFullYear()} {COMPANY_INFO.shortName}. All rights reserved.
</span>
</div>
{/* 中间:产品链接 */}
<nav className="flex items-center gap-6" aria-label="产品导航">
{productLinks.map((link) => (
<StaticLink
key={link.href}
href={link.href}
className="text-[#999999] hover:text-[#C41E3A] text-sm transition-colors duration-200"
>
{link.label}
</StaticLink>
))}
</nav>
{/* 右侧:联系方式 */}
<div className="flex items-center gap-4">
<a
href={`mailto:${COMPANY_INFO.email}`}
className="flex items-center gap-2 text-[#999999] hover:text-[#C41E3A] text-sm transition-colors duration-200"
>
<FloatingElement amplitude={3} duration={3}>
<Mail className="w-4 h-4" />
</FloatingElement>
<span className="hidden sm:inline">{COMPANY_INFO.email}</span>
</a>
</div>
</div>
</div>
</div>
</footer>
);
}
+96
View File
@@ -0,0 +1,96 @@
'use client';
import { useState, useEffect } from 'react';
import { StaticLink } from '@/components/ui/static-link';
import Image from 'next/image';
import { ArrowLeft, Phone } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Button } from '@/components/ui/button';
import { RippleButton } from '@/lib/animations';
/**
* 产品站专属 Header
*
* 与主站 Header 的区别:
* - 无导航菜单(产品列表、关于我们等)
* - 只保留 Logo + 返回主站按钮
* - 始终浅色毛玻璃风格(适配产品页浅色主题)
* - 更简洁的视觉层次,强化"独立产品站"感知
*/
function ProductHeaderContent() {
const [isScrolled, setIsScrolled] = useState(false);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 20);
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<motion.header
className="fixed top-0 left-0 right-0 z-50 transition-all duration-300"
initial={{ y: -100 }}
animate={{ y: 0 }}
transition={{ duration: 0.5, ease: [0.25, 0.46, 0.45, 0.94] }}
>
<div
className={`transition-all duration-300 ${
isScrolled
? 'bg-white/90 backdrop-blur-xl border-b border-[#E5E5E5] shadow-sm'
: 'bg-transparent'
}`}
>
<div className="container-wide">
<div className="flex items-center justify-between h-16">
<div className="flex items-center gap-3">
{/* Logo */}
<StaticLink href="/" className="flex items-center gap-3 group">
<Image
src="/logo.svg"
alt="Novalon"
width={120}
height={32}
className="h-8 w-auto"
priority
/>
</StaticLink>
{/* 立即咨询 CTA */}
<RippleButton
href="/contact"
className="bg-[#C41E3A] hover:bg-[#A01830] text-white px-5 py-2 rounded-lg text-sm font-semibold inline-flex items-center gap-2"
rippleColor="rgba(255, 255, 255, 0.3)"
>
<Phone className="w-4 h-4" />
<span className="hidden md:inline"></span>
</RippleButton>
</div>
{/* 返回主站按钮 */}
<StaticLink href="/">
<Button
variant="outline"
size="sm"
className="border-[#E5E5E5] text-[#5C5C5C] hover:text-[#1C1C1C] hover:bg-[#F5F5F5] hover:border-[#C41E3A]/30 transition-all duration-200 text-sm"
>
<ArrowLeft className="w-4 h-4 mr-2" />
</Button>
</StaticLink>
</div>
</div>
</div>
</motion.header>
);
}
export function ProductHeader() {
return (
<AnimatePresence mode="wait">
<ProductHeaderContent />
</AnimatePresence>
);
}
+98
View File
@@ -0,0 +1,98 @@
'use client';
import { StaticLink } from '@/components/ui/static-link';
import { Mail } from 'lucide-react';
import { COMPANY_INFO } from '@/lib/constants';
import { FloatingElement, RippleButton } from '@/lib/animations';
/**
* 服务站专属 Footer
*
* 与主站 Footer 的区别:
* - 浅色背景(与服务页浅色主题统一)
* - 服务 CTA 为主
* - 不显示公众号/企业微信二维码
* - 不显示 ICP/公安备案号
* - 简洁的两栏布局:服务链接 + 联系方式
*/
const serviceLinks = [
{ label: '服务概览', href: '#features' },
{ label: '面临挑战', href: '#challenges' },
{ label: '服务流程', href: '#process' },
{ label: '预期成果', href: '#outcomes' },
];
export function ServiceFooter() {
return (
<footer className="bg-[#F8F8F8]" role="contentinfo">
{/* 顶部装饰线 */}
<div className="h-[2px] bg-gradient-to-r from-transparent via-[#C41E3A]/50 to-transparent" />
{/* CTA 区域 */}
<div className="container-wide py-16">
<div className="text-center max-w-2xl mx-auto">
<h3 className="text-2xl md:text-3xl font-bold text-[#1C1C1C] mb-4">
</h3>
<p className="text-[#5C5C5C] mb-8 text-lg">
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<StaticLink href="/contact">
<RippleButton
rippleColor="rgba(196, 30, 58, 0.3)"
className="px-8 py-3 bg-[#C41E3A] text-white rounded-lg font-medium text-base"
>
</RippleButton>
</StaticLink>
<StaticLink href="/services">
<RippleButton
rippleColor="rgba(196, 30, 58, 0.3)"
className="px-8 py-3 border border-[#E5E5E5] text-[#5C5C5C] hover:text-[#1C1C1C] hover:bg-white rounded-lg font-medium text-base"
>
</RippleButton>
</StaticLink>
</div>
</div>
</div>
{/* 底部信息 */}
<div className="border-t border-[#E5E5E5]">
<div className="container-wide py-8">
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
<div className="flex flex-col items-center md:items-start gap-2">
<span className="text-[#999999] text-sm">
&copy; {new Date().getFullYear()} {COMPANY_INFO.shortName}. All rights reserved.
</span>
</div>
<nav className="flex items-center gap-6" aria-label="服务导航">
{serviceLinks.map((link) => (
<StaticLink
key={link.href}
href={link.href}
className="text-[#999999] hover:text-[#C41E3A] text-sm transition-colors duration-200"
>
{link.label}
</StaticLink>
))}
</nav>
<div className="flex items-center gap-4">
<a
href={`mailto:${COMPANY_INFO.email}`}
className="flex items-center gap-2 text-[#999999] hover:text-[#C41E3A] text-sm transition-colors duration-200"
>
<FloatingElement amplitude={3} duration={3}>
<Mail className="w-4 h-4" />
</FloatingElement>
<span className="hidden sm:inline">{COMPANY_INFO.email}</span>
</a>
</div>
</div>
</div>
</div>
</footer>
);
}
+90
View File
@@ -0,0 +1,90 @@
'use client';
import { useState, useEffect } from 'react';
import { StaticLink } from '@/components/ui/static-link';
import Image from 'next/image';
import { ArrowLeft, Phone } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Button } from '@/components/ui/button';
import { RippleButton } from '@/lib/animations';
/**
* 服务站专属 Header
*
* 与主站 Header 的区别:
* - 无导航菜单
* - 只保留 Logo + 返回主站按钮
* - 始终浅色毛玻璃风格
* - 强化"独立服务站"感知
*/
function ServiceHeaderContent() {
const [isScrolled, setIsScrolled] = useState(false);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 20);
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<motion.header
className="fixed top-0 left-0 right-0 z-50 transition-all duration-300"
initial={{ y: -100 }}
animate={{ y: 0 }}
transition={{ duration: 0.5, ease: [0.25, 0.46, 0.45, 0.94] }}
>
<div
className={`transition-all duration-300 ${
isScrolled
? 'bg-white/90 backdrop-blur-xl border-b border-[#E5E5E5] shadow-sm'
: 'bg-transparent'
}`}
>
<div className="container-wide">
<div className="flex items-center justify-between h-16">
<div className="flex items-center gap-3">
<StaticLink href="/" className="flex items-center gap-3 group">
<Image
src="/logo.svg"
alt="Novalon"
width={120}
height={32}
className="h-8 w-auto"
priority
/>
</StaticLink>
<RippleButton
href="/contact"
className="bg-[#C41E3A] hover:bg-[#A01830] text-white px-5 py-2 rounded-lg text-sm font-semibold inline-flex items-center gap-2"
rippleColor="rgba(255, 255, 255, 0.3)"
>
<Phone className="w-4 h-4" />
<span className="hidden md:inline"></span>
</RippleButton>
</div>
<StaticLink href="/">
<Button
variant="outline"
size="sm"
className="border-[#E5E5E5] text-[#5C5C5C] hover:text-[#1C1C1C] hover:bg-[#F5F5F5] hover:border-[#C41E3A]/30 transition-all duration-200 text-sm"
>
<ArrowLeft className="w-4 h-4 mr-2" />
</Button>
</StaticLink>
</div>
</div>
</div>
</motion.header>
);
}
export function ServiceHeader() {
return (
<AnimatePresence mode="wait">
<ServiceHeaderContent />
</AnimatePresence>
);
}
+8
View File
@@ -0,0 +1,8 @@
export { ProductHeroSection } from './product-hero-section';
export { ProductOverviewSection } from './product-overview-section';
export { ProductFeaturesSection } from './product-features-section';
export { ProductBenefitsSection } from './product-benefits-section';
export { ProductProcessSection } from './product-process-section';
export { ProductSpecsSection } from './product-specs-section';
export { ProductPricingSection } from './product-pricing-section';
export { ProductCTASection } from './product-cta-section';
@@ -0,0 +1,68 @@
'use client';
import { useRef } from 'react';
import { ScrollReveal, slideInLeftVariants } from '@/components/ui/scroll-animations';
import { StaggerContainer, StaggerItem, InkCard, CountUp } from '@/lib/animations';
import type { Product } from '@/lib/constants/products';
interface ProductBenefitsSectionProps {
product: Product;
}
function extractNumber(text: string): { number: number; suffix: string } | null {
const match = text.match(/(\d+)%/);
if (match) {
return { number: parseInt(match[1]!, 10), suffix: '%' };
}
return null;
}
function BenefitCard({ benefit }: { benefit: string }) {
const numberInfo = extractNumber(benefit);
return (
<StaggerItem>
<InkCard
className="p-6 md:p-8 bg-white rounded-2xl border border-[#E5E5E5] hover:border-[#C41E3A]/30 transition-colors"
hoverScale={1.02}
hoverShadow="0 20px 40px rgba(196, 30, 58, 0.08)"
>
{numberInfo && (
<div className="mb-4">
<span className="text-4xl md:text-5xl font-bold bg-gradient-to-r from-[#C41E3A] to-[#E85D75] bg-clip-text text-transparent">
<CountUp
end={numberInfo.number}
suffix={numberInfo.suffix}
duration={2000}
/>
</span>
</div>
)}
<p className="text-lg text-[#1C1C1C] leading-relaxed">{benefit}</p>
</InkCard>
</StaggerItem>
);
}
export function ProductBenefitsSection({ product }: ProductBenefitsSectionProps) {
const ref = useRef<HTMLElement>(null);
return (
<section id="benefits" ref={ref} className="relative py-20 md:py-28 bg-white overflow-hidden">
<div className="container-wide">
<ScrollReveal variants={slideInLeftVariants} className="mb-12">
<h2 className="text-3xl md:text-4xl font-bold text-[#1C1C1C] text-left">
</h2>
</ScrollReveal>
<StaggerContainer className="grid md:grid-cols-2 gap-6 max-w-5xl">
{product.benefits.map((benefit, index) => (
<BenefitCard key={index} benefit={benefit} />
))}
</StaggerContainer>
</div>
</section>
);
}
@@ -0,0 +1,57 @@
'use client';
import { Phone } from 'lucide-react';
import { InkReveal, FadeUp, FloatingElement, RippleButton } from '@/lib/animations';
export function ProductCTASection() {
return (
<section className="relative py-24 md:py-32 bg-gradient-to-r from-[#C41E3A] to-[#E85D75] overflow-hidden">
{/* 右上角装饰圆形 */}
<FloatingElement amplitude={8} duration={5} delay={0.5} className="absolute -top-20 -right-20 pointer-events-none">
<div className="w-[280px] h-[280px] bg-white/10 rounded-full" />
</FloatingElement>
{/* 左下角装饰圆形 */}
<FloatingElement amplitude={6} duration={4} delay={1} className="absolute -bottom-16 -left-16 pointer-events-none">
<div className="w-[220px] h-[220px] bg-white/10 rounded-full" />
</FloatingElement>
<div className="container-wide">
<div className="max-w-3xl mx-auto text-center">
<InkReveal delay={0}>
<h2 className="text-3xl md:text-4xl font-bold text-white mb-6">
</h2>
</InkReveal>
<FadeUp delay={0.15}>
<p className="text-lg text-white/90 mb-10">
</p>
</FadeUp>
<FadeUp delay={0.3}>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<RippleButton
href="/contact"
rippleColor="rgba(196, 30, 58, 0.3)"
className="bg-white text-[#C41E3A] px-8 py-4 rounded-lg text-lg font-semibold inline-flex items-center justify-center w-full sm:w-auto"
>
</RippleButton>
<RippleButton
href="tel:+8613800138000"
rippleColor="rgba(255, 255, 255, 0.2)"
className="bg-transparent border-2 border-white text-white px-8 py-4 rounded-lg text-lg font-semibold inline-flex items-center justify-center w-full sm:w-auto"
>
<Phone className="w-4 h-4 mr-2" />
</RippleButton>
</div>
</FadeUp>
</div>
</div>
</section>
);
}
@@ -0,0 +1,137 @@
'use client';
import { useRef, Fragment } from 'react';
import { InkReveal, FadeUp, InkCard, PulseElement } from '@/lib/animations';
import { RippleButton } from '@/lib/animations';
import { ScrollReveal, inkRevealVariants, slideInLeftVariants } from '@/components/ui/scroll-animations';
import type { Product } from '@/lib/constants/products';
interface ProductFeaturesSectionProps {
product: Product;
}
function FeatureItem({
feature,
index,
}: {
feature: string;
index: number;
}) {
// 解析功能标题和描述(中文冒号分隔)
const colonIndex = feature.indexOf('');
const title = colonIndex > -1 ? feature.substring(0, colonIndex) : feature;
const description = colonIndex > -1 ? feature.substring(colonIndex + 1) : '';
// 编号格式化
const number = String(index + 1).padStart(2, '0');
return (
<div className="min-h-[50vh] flex items-center py-16">
<div className="container-wide">
<div className="max-w-6xl mx-auto grid md:grid-cols-2 gap-12 items-center">
{/* 左侧:编号和文字 */}
<div className="order-2 md:order-1">
{/* 编号 - InkReveal 模糊揭示 */}
<InkReveal delay={0}>
<span className="block text-7xl md:text-8xl font-mono text-[#C41E3A]/10 mb-4">
{number}
</span>
</InkReveal>
{/* 功能标题 - ScrollReveal + slideInLeft */}
<ScrollReveal variants={slideInLeftVariants} delay={0.1}>
<h3 className="text-2xl md:text-3xl font-bold text-[#1C1C1C] mb-4">
{title}
</h3>
</ScrollReveal>
{/* 功能描述 - FadeUp */}
{description && (
<FadeUp delay={0.2}>
<p className="text-lg text-[#5C5C5C] leading-relaxed">
{description}
</p>
</FadeUp>
)}
</div>
{/* 右侧:InkCard 弹簧物理悬浮 + PulseElement 脉冲同心圆 */}
<div className="order-1 md:order-2">
<InkCard
className="aspect-square rounded-2xl bg-white border border-[#E5E5E5] shadow-lg flex items-center justify-center"
hoverScale={1.03}
hoverShadow="0 25px 50px rgba(196, 30, 58, 0.15)"
>
<PulseElement scale={1.08} duration={2.5}>
<div className="w-24 h-24 rounded-full bg-[#C41E3A]/10 flex items-center justify-center">
<div className="w-12 h-12 rounded-full bg-[#C41E3A]/20" />
</div>
</PulseElement>
</InkCard>
</div>
</div>
</div>
</div>
);
}
export function ProductFeaturesSection({ product }: ProductFeaturesSectionProps) {
const sectionRef = useRef<HTMLElement>(null);
return (
<section id="features" ref={sectionRef} className="relative bg-[#F8F8F8] overflow-hidden">
{/* 标题 - ScrollReveal + inkRevealVariants 模糊揭示 */}
<div className="pt-32 md:pt-40 pb-16">
<div className="container-wide">
<ScrollReveal variants={inkRevealVariants} delay={0}>
<h2 className="text-3xl md:text-4xl font-bold text-[#1C1C1C] text-center mb-4">
</h2>
</ScrollReveal>
<ScrollReveal variants={inkRevealVariants} delay={0.1}>
<p className="text-lg text-[#5C5C5C] text-center max-w-2xl mx-auto">
</p>
</ScrollReveal>
</div>
</div>
{/* 功能列表 */}
{product.features.map((feature, index) => (
<Fragment key={index}>
<FeatureItem feature={feature} index={index} />
{(index === 1 || index === 3) && (
<div className="py-8">
<div className="container-wide">
<div className="max-w-2xl mx-auto bg-white rounded-2xl border border-[#E5E5E5] p-6 md:p-8 text-center shadow-sm">
<p className="text-[#1C1C1C] font-semibold text-lg mb-4">
</p>
<p className="text-[#5C5C5C] mb-6">
30 线
</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<RippleButton
href="/contact"
rippleColor="rgba(196, 30, 58, 0.3)"
className="bg-[#C41E3A] hover:bg-[#A01830] text-white px-6 py-3 rounded-lg font-semibold inline-flex items-center justify-center"
>
</RippleButton>
<RippleButton
href="/contact"
rippleColor="rgba(196, 30, 58, 0.2)"
className="border border-[#E5E5E5] text-[#5C5C5C] hover:text-[#C41E3A] hover:border-[#C41E3A]/30 px-6 py-3 rounded-lg font-semibold inline-flex items-center justify-center"
>
</RippleButton>
</div>
</div>
</div>
</div>
)}
</Fragment>
))}
</section>
);
}
@@ -0,0 +1,116 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { motion } from 'framer-motion';
import dynamic from 'next/dynamic';
import { ChevronDown } from 'lucide-react';
import { InkReveal, SealStamp, FloatingElement } from '@/lib/animations';
import type { Product } from '@/lib/constants/products';
const InkBackground = dynamic(
() => import('@/components/ui/ink-decoration').then(mod => ({ default: mod.InkBackground })),
{ ssr: false }
);
const DataParticleFlow = dynamic(
() => import('@/components/effects/data-particle-flow').then(mod => ({ default: mod.DataParticleFlow })),
{ ssr: false }
);
interface ProductHeroSectionProps {
product: Product;
}
export function ProductHeroSection({ product }: ProductHeroSectionProps) {
const [isVisible, setIsVisible] = useState(false);
const sectionRef = useRef<HTMLElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting) {
setIsVisible(true);
}
},
{ threshold: 0.1 }
);
if (sectionRef.current) {
observer.observe(sectionRef.current);
}
return () => observer.disconnect();
}, []);
return (
<section
ref={sectionRef}
className="relative min-h-screen flex items-center justify-center overflow-hidden bg-gradient-to-b from-white to-[#F8F8F8]"
>
{/* 背景特效 */}
<InkBackground />
<DataParticleFlow
particleCount={80}
color="#C41E3A"
intensity="subtle"
shape="square"
effect="pulse"
/>
{/* 内容 */}
<div className="container-wide relative z-10 py-32 md:py-40">
<div className="max-w-4xl mx-auto text-center">
{/* 分类标签 - 印章按压效果 */}
<SealStamp
delay={0.1}
className="inline-block px-4 py-2 bg-[#C41E3A]/20 rounded-full text-[#C41E3A] text-sm mb-6"
>
</SealStamp>
{/* 产品名称 - 模糊揭示入场 */}
<InkReveal delay={0.2}>
<h1 className="text-4xl md:text-6xl lg:text-7xl font-bold text-[#1C1C1C] mb-6">
{product.title}
</h1>
</InkReveal>
{/* 价值主张 - 模糊揭示入场 */}
<InkReveal delay={0.4}>
<p className="text-lg md:text-xl text-[#5C5C5C] leading-relaxed mb-10 max-w-2xl mx-auto">
{product.description}
</p>
</InkReveal>
{/* CTA 按钮 */}
<InkReveal delay={0.6}>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<span className="border-2 border-[#D9D9D9] text-[#999999] px-8 py-4 rounded-lg text-lg font-semibold inline-flex items-center justify-center cursor-not-allowed select-none">
</span>
</div>
</InkReveal>
</div>
</div>
{/* 滚动指示器 - 浮动元素包裹 */}
<FloatingElement
amplitude={10}
duration={1.5}
delay={1}
className="absolute bottom-8 left-1/2 -translate-x-1/2"
>
<motion.div
initial={{ opacity: 0 }}
animate={isVisible ? { opacity: 1 } : {}}
transition={{ duration: 0.6, delay: 1 }}
>
<div className="text-[#999999]">
<ChevronDown className="w-8 h-8" />
</div>
</motion.div>
</FloatingElement>
</section>
);
}
@@ -0,0 +1,38 @@
'use client';
import { InkReveal } from '@/lib/animations';
import { ScrollReveal, slideInLeftVariants } from '@/components/ui/scroll-animations';
import type { Product } from '@/lib/constants/products';
interface ProductOverviewSectionProps {
product: Product;
}
export function ProductOverviewSection({ product }: ProductOverviewSectionProps) {
return (
<section id="overview" className="relative py-16 md:py-20 bg-white overflow-hidden">
<div className="container-wide">
<div className="max-w-3xl">
{/* 标题 - 左对齐,slideInLeft 入场 */}
<ScrollReveal variants={slideInLeftVariants} delay={0}>
<h2 className="text-3xl md:text-4xl font-bold text-[#1C1C1C] mb-4">
</h2>
</ScrollReveal>
{/* 朱砂红装饰线 - InkReveal 入场 */}
<InkReveal delay={0.2}>
<div className="w-16 h-1 bg-[#C41E3A] rounded-full mb-8" />
</InkReveal>
{/* 概述文字 - InkReveal 包裹整段,替代 TextReveal */}
<InkReveal delay={0.3}>
<p className="text-lg md:text-xl text-[#5C5C5C] leading-relaxed">
{product.overview}
</p>
</InkReveal>
</div>
</div>
</section>
);
}
@@ -0,0 +1,140 @@
'use client';
import { Check } from 'lucide-react';
import { InkCard } from '@/components/ui/animated-card';
import { ScrollReveal, inkRevealVariants } from '@/components/ui/scroll-animations';
import { FloatingElement, PulseElement, RippleButton } from '@/lib/animations';
import type { Product } from '@/lib/constants/products';
interface ProductPricingSectionProps {
product: Product;
}
const pricingFeatures = {
base: ['核心功能模块', '私有化部署', '标准技术支持', '系统培训'],
standard: ['全部功能模块', '私有化部署', '优先技术支持', '系统培训', '数据迁移服务', '定制化配置'],
enterprise: ['全部功能模块', '私有化部署', '专属技术支持', '系统培训', '数据迁移服务', '定制化开发', 'SLA服务保障', '驻场支持'],
};
function PricingCard({
name,
description,
features,
isRecommended = false,
ctaText = '获取方案',
}: {
name: string;
description: string;
features: string[];
isRecommended?: boolean;
ctaText?: string;
}) {
const cardContent = (
<div
className={`
relative p-6 md:p-8 rounded-2xl
${isRecommended
? 'bg-white border-2 border-[#C41E3A] text-[#1C1C1C]'
: 'bg-white border border-[#E5E5E5]'
}
`}
>
{isRecommended && (
<PulseElement scale={1.08} duration={2} className="absolute -top-3 left-1/2 -translate-x-1/2 z-10">
<div className="bg-[#C41E3A] text-white px-4 py-1 rounded-full text-sm font-semibold whitespace-nowrap">
</div>
</PulseElement>
)}
<h3 className="text-xl font-semibold mb-2 text-[#1C1C1C]">{name}</h3>
<p className={`text-sm mb-6 text-[#5C5C5C]`}>
{description}
</p>
<ul className="space-y-3 mb-8">
{features.map((feature, index) => (
<li key={index} className="flex items-center gap-2">
<Check className="w-5 h-5 text-[#C41E3A]" />
<span className="text-[#5C5C5C]">
{feature}
</span>
</li>
))}
</ul>
<RippleButton
href="/contact"
rippleColor="rgba(196, 30, 58, 0.3)"
className={`
block w-full py-3 rounded-lg font-semibold text-center
${isRecommended
? 'bg-[#C41E3A] text-white'
: 'border border-[#E5E5E5] text-[#5C5C5C] hover:text-[#C41E3A] hover:border-[#C41E3A]/30 bg-white'
}
`}
>
{ctaText}
</RippleButton>
</div>
);
if (isRecommended) {
return (
<FloatingElement amplitude={5} duration={4} className="my-0 md:-my-4">
<InkCard hoverScale={1.03} className="h-full md:scale-105 md:z-10">
{cardContent}
</InkCard>
</FloatingElement>
);
}
return (
<InkCard hoverScale={1.02} className="h-full">
{cardContent}
</InkCard>
);
}
export function ProductPricingSection({ product }: ProductPricingSectionProps) {
return (
<section id="pricing" className="relative py-28 md:py-36 bg-[#F5F5F5] overflow-hidden">
<div className="container-wide">
<ScrollReveal variants={inkRevealVariants} className="mb-4">
<h2 className="text-3xl md:text-4xl font-bold text-[#1C1C1C] text-center">
</h2>
</ScrollReveal>
<ScrollReveal variants={inkRevealVariants} className="mb-12">
<p className="text-center text-[#5C5C5C] text-lg">
</p>
</ScrollReveal>
<div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
<PricingCard
name={product.pricing.base}
description="适合中小企业基础需求"
features={pricingFeatures.base}
ctaText="了解标准版"
/>
<PricingCard
name={product.pricing.standard}
description="适合中型企业深度应用"
features={pricingFeatures.standard}
isRecommended
ctaText="立即获取方案"
/>
<PricingCard
name={product.pricing.enterprise}
description="适合大型企业定制化需求"
features={pricingFeatures.enterprise}
ctaText="联系定制"
/>
</div>
</div>
</section>
);
}
@@ -0,0 +1,75 @@
'use client';
import { useRef } from 'react';
import { ScrollReveal, inkRevealVariants } from '@/components/ui/scroll-animations';
import { StaggerContainer, StaggerItem, SealStamp, FadeUp } from '@/lib/animations';
import type { Product } from '@/lib/constants/products';
interface ProductProcessSectionProps {
product: Product;
}
function ProcessStep({
step,
index,
total,
}: {
step: string;
index: number;
total: number;
}) {
const colonIndex = step.indexOf('');
const title = colonIndex > -1 ? step.substring(0, colonIndex) : step;
const description = colonIndex > -1 ? step.substring(colonIndex + 1) : '';
return (
<StaggerItem className="flex items-start gap-6">
<div className="flex-shrink-0">
<SealStamp delay={index * 0.15}>
<div className="w-12 h-12 rounded-full bg-[#C41E3A] flex items-center justify-center text-white font-bold text-lg">
{index + 1}
</div>
</SealStamp>
{index < total - 1 && (
<div className="w-0.5 h-16 bg-gradient-to-b from-[#C41E3A]/40 to-[#C41E3A]/10 ml-6 mt-2" />
)}
</div>
<FadeUp delay={index * 0.1} className="flex-1 pb-8">
<h3 className="text-xl font-semibold text-[#1C1C1C] mb-2">{title}</h3>
{description && (
<p className="text-[#5C5C5C] leading-relaxed">{description}</p>
)}
</FadeUp>
</StaggerItem>
);
}
export function ProductProcessSection({ product }: ProductProcessSectionProps) {
const ref = useRef<HTMLElement>(null);
return (
<section id="process" ref={ref} className="relative py-24 md:py-32 bg-white overflow-hidden">
<div className="container-wide">
<div className="max-w-3xl mx-auto">
<ScrollReveal variants={inkRevealVariants} className="mb-12 text-center">
<h2 className="text-3xl md:text-4xl font-bold text-[#1C1C1C]">
</h2>
</ScrollReveal>
<StaggerContainer className="mt-8" staggerDelay={0.15}>
{product.process.map((step, index) => (
<ProcessStep
key={index}
step={step}
index={index}
total={product.process.length}
/>
))}
</StaggerContainer>
</div>
</div>
</section>
);
}
@@ -0,0 +1,41 @@
'use client';
import { useRef } from 'react';
import { ScrollReveal, slideInLeftVariants } from '@/components/ui/scroll-animations';
import { StaggerContainer, StaggerItem, InkCard } from '@/lib/animations';
import type { Product } from '@/lib/constants/products';
interface ProductSpecsSectionProps {
product: Product;
}
export function ProductSpecsSection({ product }: ProductSpecsSectionProps) {
const ref = useRef<HTMLElement>(null);
return (
<section id="specs" ref={ref} className="relative py-20 md:py-28 bg-[#FAFAFA] overflow-hidden">
<div className="container-wide">
<ScrollReveal variants={slideInLeftVariants} className="mb-12 text-center">
<h2 className="text-3xl md:text-4xl font-bold text-[#1C1C1C]">
</h2>
</ScrollReveal>
<StaggerContainer className="grid md:grid-cols-2 gap-4 max-w-3xl mx-auto">
{product.specs.map((spec, index) => (
<StaggerItem key={index}>
<InkCard
className="flex items-center gap-4 p-4 bg-white rounded-lg border border-[#E5E5E5] hover:border-[#C41E3A]/30 transition-colors"
hoverScale={1.02}
hoverShadow="0 12px 24px rgba(196, 30, 58, 0.06)"
>
<div className="w-1 h-8 bg-[#C41E3A] rounded-full flex-shrink-0" />
<span className="text-[#1C1C1C]">{spec}</span>
</InkCard>
</StaggerItem>
))}
</StaggerContainer>
</div>
</section>
);
}
+48 -28
View File
@@ -4,12 +4,17 @@ import { motion } from 'framer-motion';
import { useInView } from 'framer-motion';
import { useRef } from 'react';
import { StaticLink } from '@/components/ui/static-link';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { COMPANY_INFO, STATS } from '@/lib/constants';
import { ArrowRight } from 'lucide-react';
import { RippleButton } from '@/lib/animations';
import { COMPANY_INFO } from '@/lib/constants';
import { ArrowRight, CheckCircle2 } from 'lucide-react';
import { useReducedMotion } from '@/hooks/use-reduced-motion';
const VALUES = [
{ title: '务实', description: '不追逐风口,只做真正为客户创造价值的事。' },
{ title: '陪伴', description: '交付只是开始,长期陪跑才是我们的承诺。' },
{ title: '专业', description: '用扎实的工程能力和行业经验赢得信任。' },
];
export function AboutSection() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: '-100px' });
@@ -17,14 +22,17 @@ export function AboutSection() {
return (
<section id="about" role="region" aria-labelledby="about-heading" className="py-24 bg-[#FAFAFA] relative" ref={ref}>
{/* 网格背景 */}
<div className="absolute inset-0 bg-[linear-gradient(rgba(28,28,28,0.02)_1px,transparent_1px),linear-gradient(90deg,rgba(28,28,28,0.02)_1px,transparent_1px)] bg-size-[40px_40px]" />
<div className="container-wide relative z-10">
<motion.div
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6 }}
className="max-w-4xl mx-auto"
>
{/* 标题 */}
<div className="text-center mb-12">
<h2 id="about-heading" className="text-4xl md:text-5xl font-bold text-[#1C1C1C] mb-6">
<span className="tracking-tight font-brand text-[#C41E3A]" style={{ fontWeight: 'normal', WebkitFontSmoothing: 'antialiased', MozOsxFontSmoothing: 'grayscale', textRendering: 'optimizeLegibility' }}>{COMPANY_INFO.shortName}</span>
@@ -34,43 +42,55 @@ export function AboutSection() {
</p>
</div>
<div className="bg-white rounded-2xl p-8 mb-12 border border-[#E5E5E5]">
{/* 品牌理念引用 */}
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.1 }}
className="bg-white rounded-2xl p-8 mb-12 border border-[#E5E5E5]"
>
<p className="text-lg text-[#5C5C5C] leading-relaxed text-center mb-6">
&ldquo;&lsquo;&rsquo;&lsquo;&rsquo;&rdquo;
</p>
<p className="text-[#1C1C1C] font-medium text-center">
</p>
</div>
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.2 }}
className="grid grid-cols-2 md:grid-cols-4 gap-6 mb-12"
>
{STATS.map((stat, idx) => (
<Card key={idx} className="text-center border-[#E5E5E5]">
<CardContent className="pt-6">
<div className="text-3xl sm:text-4xl font-bold text-[#C41E3A] mb-2">{stat.value}</div>
<div className="text-sm text-[#5C5C5C]">{stat.label}</div>
</CardContent>
</Card>
))}
</motion.div>
{/* 核心价值观 */}
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.3 }}
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.15 }}
className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-16"
>
{VALUES.map((value) => (
<div
key={value.title}
className="bg-white rounded-xl p-6 border border-[#E5E5E5] text-center"
>
<div className="w-10 h-10 bg-[#C41E3A]/10 rounded-full flex items-center justify-center mx-auto mb-3">
<CheckCircle2 className="w-5 h-5 text-[#C41E3A]" />
</div>
<h3 className="text-lg font-bold text-[#1C1C1C] mb-2">{value.title}</h3>
<p className="text-sm text-[#5C5C5C] leading-relaxed">{value.description}</p>
</div>
))}
</motion.div>
{/* CTA */}
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.5 }}
className="text-center"
>
<Button size="lg" variant="outline" className="group" asChild>
<StaticLink href="/about">
<StaticLink href="/about">
<RippleButton className="inline-flex items-center gap-2 px-6 py-2.5 border border-[#E5E5E5] rounded-lg text-sm font-medium text-[#1C1C1C] hover:border-[#C41E3A] hover:text-[#C41E3A] transition-colors">
<ArrowRight className="ml-2 w-4 h-4 transition-transform group-hover:translate-x-1" />
</StaticLink>
</Button>
<ArrowRight className="w-4 h-4" />
</RippleButton>
</StaticLink>
</motion.div>
</motion.div>
</div>
+86 -41
View File
@@ -9,17 +9,34 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { TouchSwipe } from '@/components/ui/touch-swipe';
import { CASES } from '@/lib/constants';
import { ArrowRight, Building2 } from 'lucide-react';
import { ArrowRight, Building2, Hotel, Factory, Landmark, Sprout, TrendingUp } from 'lucide-react';
// 行业图标映射,为不同行业提供差异化视觉
const industryIconMap: Record<string, React.ComponentType<{ className?: string }>> = {
'酒店管理': Hotel,
'制造业': Factory,
'政务服务': Landmark,
'智慧农业': Sprout,
};
const industryColorMap: Record<string, { bg: string; icon: string; badge: string }> = {
'酒店管理': { bg: 'from-amber-50 to-orange-50', icon: 'text-amber-600', badge: 'bg-amber-50 text-amber-700' },
'制造业': { bg: 'from-blue-50 to-indigo-50', icon: 'text-blue-600', badge: 'bg-blue-50 text-blue-700' },
'政务服务': { bg: 'from-emerald-50 to-teal-50', icon: 'text-emerald-600', badge: 'bg-emerald-50 text-emerald-700' },
'智慧农业': { bg: 'from-green-50 to-lime-50', icon: 'text-green-600', badge: 'bg-green-50 text-green-700' },
};
const defaultColors = { bg: 'from-[#F5F5F5] to-[#EDEDED]', icon: 'text-[#C41E3A]/30', badge: 'bg-white/90 text-[#1C1C1C]' };
export function CasesSection() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: '-100px' });
return (
<section id="cases" role="region" aria-labelledby="cases-heading" className="py-24 bg-white relative overflow-hidden" ref={ref}>
<section id="cases" role="region" aria-labelledby="cases-heading" className="py-24 bg-[#F8F8F8] relative overflow-hidden" ref={ref}>
<div className="absolute top-1/3 left-0 w-[400px] h-[400px] bg-[rgba(196,30,58,0.03)] rounded-full blur-3xl" />
<div className="absolute top-1/3 right-0 w-[300px] h-[300px] bg-[rgba(196,30,58,0.02)] rounded-full blur-3xl" />
<div className="container-wide relative z-10">
<motion.div
initial={{ opacity: 0, y: 20 }}
@@ -36,53 +53,81 @@ export function CasesSection() {
</motion.div>
<TouchSwipe
onSwipeLeft={() => {
}}
onSwipeRight={() => {
}}
onSwipeLeft={() => {}}
onSwipeRight={() => {}}
className="md:hidden"
>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{CASES.map((caseItem, index) => (
<motion.div
key={caseItem.id}
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.1 + index * 0.1 }}
>
<StaticLink href={`/cases/${caseItem.id}`}>
<Card className="h-full group cursor-pointer border-[#E5E5E5] hover:border-[#C41E3A] transition-colors overflow-hidden">
<div className="relative h-40 bg-gradient-to-br from-[#F5F5F5] to-[#E5E5E5] flex items-center justify-center">
<Building2 className="w-16 h-16 text-[#C41E3A]/20 group-hover:scale-110 transition-transform duration-300" />
<div className="absolute top-4 right-4">
<Badge className="bg-white/90 text-[#1C1C1C] hover:bg-white">
{caseItem.industry}
</Badge>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{CASES.map((caseItem, index) => {
const IndustryIcon = industryIconMap[caseItem.industry] || Building2;
const colors = industryColorMap[caseItem.industry] || defaultColors;
return (
<motion.div
key={caseItem.id}
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.1 + index * 0.1 }}
>
<StaticLink href={`/cases/${caseItem.id}`}>
<Card className="h-full group cursor-pointer border-[#E5E5E5] hover:border-[#C41E3A]/40 hover:shadow-lg hover:-translate-y-1 transition-all duration-300 overflow-hidden">
{/* 行业图标区域 - 使用差异化配色 */}
<div className={`relative h-44 bg-gradient-to-br ${colors.bg} flex items-center justify-center overflow-hidden`}>
{/* 装饰性几何元素 */}
<div className="absolute top-3 right-3 w-16 h-16 rounded-full border border-current opacity-10" />
<div className="absolute bottom-3 left-3 w-10 h-10 rounded-lg border border-current opacity-10 rotate-12" />
<IndustryIcon className={`w-16 h-16 ${colors.icon} opacity-40 group-hover:scale-110 group-hover:opacity-60 transition-all duration-500`} />
<div className="absolute top-4 right-4">
<Badge className={`${colors.badge} hover:opacity-90 text-xs font-medium`}>
{caseItem.industry}
</Badge>
</div>
{/* 成果数据标签 */}
{caseItem.results && caseItem.results.length > 0 && (
<div className="absolute bottom-3 left-3 right-3 flex gap-1.5">
{caseItem.results.slice(0, 2).map((result, i) => (
<span
key={i}
className="inline-flex items-center gap-1 text-[10px] px-2 py-0.5 bg-white/80 backdrop-blur-sm rounded-full text-[#1C1C1C] font-medium"
>
<TrendingUp className="w-2.5 h-2.5 text-[#C41E3A]" />
{result.value}
</span>
))}
</div>
)}
</div>
</div>
<CardContent className="p-6">
<div className="flex items-center gap-2 mb-3">
<Building2 className="w-4 h-4 text-[#C41E3A]" />
<span className="text-sm text-[#5C5C5C]">{caseItem.client}</span>
</div>
<h3 className="text-lg font-semibold text-[#1C1C1C] mb-3 group-hover:text-[#C41E3A] transition-colors">
{caseItem.title}
</h3>
<p className="text-[#5C5C5C] text-sm line-clamp-2 mb-4">
{caseItem.description}
</p>
</CardContent>
</Card>
</StaticLink>
</motion.div>
))}
<CardContent className="p-5">
<div className="flex items-center gap-2 mb-2">
<div className="w-1.5 h-1.5 bg-[#C41E3A] rounded-full shrink-0" />
<span className="text-xs text-[#5C5C5C] truncate">{caseItem.client}</span>
</div>
<h3 className="text-base font-semibold text-[#1C1C1C] mb-2 group-hover:text-[#C41E3A] transition-colors line-clamp-2">
{caseItem.title}
</h3>
<p className="text-[#5C5C5C] text-sm line-clamp-2 mb-3">
{caseItem.description}
</p>
<div className="flex items-center text-[#C41E3A] text-xs font-medium opacity-0 group-hover:opacity-100 transition-opacity">
<ArrowRight className="ml-1 w-3 h-3" />
</div>
</CardContent>
</Card>
</StaticLink>
</motion.div>
);
})}
</div>
</TouchSwipe>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.4 }}
transition={{ duration: 0.6, delay: 0.5 }}
className="text-center mt-12"
>
<Button variant="outline" size="lg" className="group" asChild>
+23 -23
View File
@@ -5,7 +5,7 @@ import { motion } from 'framer-motion';
import { StaticLink } from '@/components/ui/static-link';
import { RippleButton } from '@/components/ui/ripple-button';
import { Button } from '@/components/ui/button';
import { MagneticButton, BlurReveal, CounterWithEffect } from '@/lib/animations';
import { MagneticButton, BlurReveal, CountUp, InkReveal } from '@/lib/animations';
import { COMPANY_INFO, STATS } from '@/lib/constants';
import { ArrowRight, Shield, Zap, Award } from 'lucide-react';
import { useReducedMotion } from '@/hooks/use-reduced-motion';
@@ -28,7 +28,7 @@ function scrollTo(id: string) {
}
}
function handleKeyDown(event: React.KeyboardEvent<HTMLButtonElement>, id: string) {
function handleKeyDown(event: React.KeyboardEvent<HTMLButtonElement | HTMLAnchorElement>, id: string) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
scrollTo(id);
@@ -52,25 +52,25 @@ export function HeroContent({ isVisible }: HeroContentProps) {
);
}
export function HeroTitle({ isVisible }: HeroContentProps) {
export function HeroTitle(_props: HeroContentProps) {
const shouldReduceMotion = useReducedMotion();
return (
<motion.h1
id="hero-heading"
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={isVisible ? { opacity: 1, y: 0 } : {}}
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.1 }}
className="text-5xl sm:text-6xl lg:text-7xl tracking-tight mb-6 font-brand"
style={{
fontWeight: 'normal',
WebkitFontSmoothing: 'antialiased',
MozOsxFontSmoothing: 'grayscale',
textRendering: 'optimizeLegibility'
}}
>
{COMPANY_INFO.shortName}
</motion.h1>
<InkReveal delay={0.1}>
<h1
id="hero-heading"
className="text-5xl sm:text-6xl lg:text-7xl tracking-tight mb-6 font-brand"
style={{
fontWeight: 'normal',
WebkitFontSmoothing: 'antialiased',
MozOsxFontSmoothing: 'grayscale',
textRendering: 'optimizeLegibility',
...(shouldReduceMotion ? { opacity: 1, filter: 'none', transform: 'none' } : {}),
}}
>
{COMPANY_INFO.shortName}
</h1>
</InkReveal>
);
}
@@ -221,11 +221,11 @@ function HeroStatItem({ stat, index, shouldAnimate, shouldReduceMotion }: {
>
<div className="text-4xl sm:text-5xl font-bold text-[#C41E3A] mb-3">
{shouldAnimate ? (
<CounterWithEffect
end={numericValue}
suffix={suffix}
effect="bounce"
<CountUp
end={numericValue}
suffix={suffix}
duration={2000}
className="text-4xl sm:text-5xl font-bold text-[#C41E3A]"
/>
) : (
<span className="text-[#CBD5E0]">0{suffix}</span>
@@ -85,7 +85,7 @@ jest.mock('@/lib/animations', () => ({
jest.mock('@/lib/constants', () => ({
COMPANY_INFO: {
name: '四川睿新致远科技有限公司',
shortName: '睿新致',
shortName: '睿新致',
description: '以智慧连接数字趋势,以伙伴身份陪您成长',
},
STATS: [
@@ -98,7 +98,7 @@ jest.mock('@/lib/constants', () => ({
jest.mock('./hero-section-atoms', () => ({
HeroContent: () => <div></div>,
HeroTitle: () => <h1></h1>,
HeroTitle: () => <h1></h1>,
HeroDescription: () => <p></p>,
HeroButtons: () => <div><button></button><button></button></div>,
HeroFeatures: () => <div><span></span><span>便</span><span></span></div>,
@@ -129,7 +129,7 @@ describe('HeroSection', () => {
it('should render company name', () => {
render(<HeroSection heroStats={<HeroStats />} />);
expect(screen.getByText('睿新致')).toBeInTheDocument();
expect(screen.getByText('睿新致')).toBeInTheDocument();
});
it('should render features', () => {
+1 -1
View File
@@ -2,7 +2,7 @@ import { STATS } from '@/lib/constants';
export function HeroStatsSSR() {
return (
<div className="pt-16 border-t border-[#E2E8F0]">
<div id="stats-section" className="pt-16 border-t border-[#E2E8F0]">
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-12">
{STATS.map((stat) => (
<div
@@ -37,7 +37,6 @@ export function HomeSolutionsSection() {
return (
<section id="solutions" role="region" aria-labelledby="solutions-heading" className="py-24 bg-[#F5F7FA] relative overflow-hidden" ref={ref}>
<div className="absolute top-1/2 right-0 w-[400px] h-[400px] bg-[rgba(196,30,58,0.02)] rounded-full blur-3xl" />
<div className="container-wide relative z-10">
<motion.div
initial={{ opacity: 0, y: 20 }}
+17 -20
View File
@@ -5,7 +5,8 @@ import { useInView } from 'framer-motion';
import { useRef } from 'react';
import { StaticLink } from '@/components/ui/static-link';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { RippleButton } from '@/lib/animations';
import { InkCard } from '@/lib/animations';
import { Badge } from '@/components/ui/badge';
import { ArrowRight, Check, TrendingUp } from 'lucide-react';
import { PRODUCTS } from '@/lib/constants';
@@ -23,8 +24,9 @@ export function ProductsSection() {
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="text-center max-w-3xl mx-auto mb-16"
className="text-left max-w-3xl mx-auto mb-16"
>
<div className="w-16 h-1 bg-[#C41E3A] rounded-full mb-6" />
<h2 id="products-heading" className="text-4xl md:text-5xl font-bold text-[#1C1C1C] mb-6">
<span className="text-[#C41E3A] font-calligraphy"></span>
</h2>
@@ -35,15 +37,13 @@ export function ProductsSection() {
{PRODUCTS.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{PRODUCTS.map((product, idx) => (
<motion.div
{PRODUCTS.map((product) => (
<InkCard
key={product.id}
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.1 + idx * 0.1 }}
className="group cursor-pointer rounded-xl border border-[#E5E5E5] bg-white p-0 overflow-hidden hover:border-[#C41E3A] transition-colors"
>
<StaticLink href={`/products/${product.id}`}>
<Card className="h-full flex flex-col group cursor-pointer border-[#E5E5E5] hover:border-[#1C1C1C] transition-colors">
<Card className="h-full flex flex-col border-0 shadow-none bg-transparent">
<CardHeader>
<Badge variant="secondary" className="w-fit mb-3">
{product.category}
@@ -85,14 +85,14 @@ export function ProductsSection() {
</ul>
</div>
<Button variant="outline" className="w-full mt-auto group-hover:bg-[#A01830] group-hover:text-white group-hover:border-[#A01830] transition-colors">
<div className="w-full mt-auto px-4 py-2 text-center text-sm font-medium border border-[#E5E5E5] rounded-md group-hover:bg-[#A01830] group-hover:text-white group-hover:border-[#A01830] transition-colors">
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
<ArrowRight className="ml-2 w-4 h-4 inline" />
</div>
</CardContent>
</Card>
</StaticLink>
</motion.div>
</InkCard>
))}
</div>
) : (
@@ -119,15 +119,12 @@ export function ProductsSection() {
<p className="text-[#718096] mb-8 max-w-2xl mx-auto">
</p>
<Button
size="lg"
asChild
>
<StaticLink href="/contact">
<StaticLink href="/contact">
<RippleButton className="inline-flex items-center gap-2 px-6 py-2.5 bg-[#C41E3A] hover:bg-[#A01830] text-white rounded-lg text-sm font-medium transition-colors">
<ArrowRight className="ml-2 w-4 h-4" />
</StaticLink>
</Button>
<ArrowRight className="w-4 h-4" />
</RippleButton>
</StaticLink>
</div>
</div>
</motion.div>
+15 -16
View File
@@ -6,7 +6,8 @@ import { useRef } from 'react';
import { StaticLink } from '@/components/ui/static-link';
import { Code, BarChart3, Lightbulb, Puzzle, ArrowRight } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { RippleButton } from '@/lib/animations';
import { InkCard } from '@/lib/animations';
import { SERVICES } from '@/lib/constants';
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
@@ -30,30 +31,28 @@ export function ServicesSection() {
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="text-center max-w-3xl mx-auto mb-16"
className="text-left max-w-3xl mx-auto mb-16"
>
<div className="w-16 h-1 bg-[#C41E3A] rounded-full mb-6" />
<h2 id="services-heading" className="text-4xl md:text-5xl font-bold text-[#1C1C1C] mb-4">
<span className="text-[#C41E3A] font-calligraphy"></span>
</h2>
<p className="text-lg text-[#5C5C5C] max-w-2xl mx-auto">
<p className="text-lg text-[#5C5C5C] max-w-2xl">
</p>
</motion.div>
{SERVICES.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{SERVICES.map((service, index) => {
{SERVICES.map((service) => {
const Icon = iconMap[service.icon];
return (
<motion.div
<InkCard
key={service.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className="rounded-xl border border-[#E5E5E5] bg-white p-6 hover:border-[#C41E3A] transition-colors"
>
<StaticLink href={`/services/${service.id}`}>
<Card className="p-6 h-full group cursor-pointer border-[#E5E5E5] hover:border-[#C41E3A] transition-colors">
<Card className="p-0 h-full border-0 shadow-none bg-transparent group cursor-pointer">
<CardContent className="p-0">
<div className="w-12 h-12 rounded-xl bg-[#F5F5F5] flex items-center justify-center mb-4 group-hover:bg-[#C41E3A] transition-all duration-300">
{Icon && <Icon className="w-6 h-6 text-[#1C1C1C] group-hover:text-white transition-colors" />}
@@ -67,7 +66,7 @@ export function ServicesSection() {
</CardContent>
</Card>
</StaticLink>
</motion.div>
</InkCard>
);
})}
</div>
@@ -83,12 +82,12 @@ export function ServicesSection() {
transition={{ duration: 0.6, delay: 0.4 }}
className="text-center mt-12"
>
<Button variant="outline" size="lg" className="group" asChild>
<StaticLink href="/services">
<StaticLink href="/services">
<RippleButton className="inline-flex items-center gap-2 px-6 py-2.5 border border-[#E5E5E5] rounded-lg text-sm font-medium text-[#1C1C1C] hover:border-[#C41E3A] hover:text-[#C41E3A] transition-colors">
<ArrowRight className="ml-2 w-4 h-4 transition-transform group-hover:translate-x-1" />
</StaticLink>
</Button>
<ArrowRight className="w-4 h-4" />
</RippleButton>
</StaticLink>
</motion.div>
</div>
</section>
+99 -33
View File
@@ -4,34 +4,62 @@ import { motion } from 'framer-motion';
import { useInView } from 'framer-motion';
import { useRef } from 'react';
import { StaticLink } from '@/components/ui/static-link';
import { Button } from '@/components/ui/button';
import { ArrowRight, Shield, Building2, Users } from 'lucide-react';
import { RippleButton } from '@/lib/animations';
import { ArrowRight, Briefcase, GraduationCap, Target, Users } from 'lucide-react';
const TEAM_HIGHLIGHTS = [
const TEAM_MEMBERS = [
{
icon: Shield,
title: '12年+ 行业深耕',
description: '核心团队长期从事技术咨询、企业数字化等领域,积累了丰富的行业经验和最佳实践。',
name: '创始人兼CEO',
initials: 'CEO',
specialties: ['企业战略', '数字化转型', '组织管理'],
bio: '15年+企业服务经验,深耕数字化转型领域,擅长从战略高度为企业规划数字化路径。',
icon: Target,
accentColor: 'from-[#C41E3A] to-[#E85D75]',
},
{
icon: Building2,
title: '大型 IT 企业背景',
description: '开发团队成员来自多个大型传统 IT 企业,具备扎实的工程能力和规范化交付经验。',
name: '联合创始人兼CTO',
initials: 'CTO',
specialties: ['系统架构', '云原生', '微服务'],
bio: '12年+技术架构经验,主导过多个大型企业级系统的设计与交付,精通分布式系统。',
icon: GraduationCap,
accentColor: 'from-[#C41E3A] to-[#D94466]',
},
{
name: '技术总监',
initials: 'TD',
specialties: ['全栈开发', '数据工程', 'DevOps'],
bio: '10年+全栈开发经验,专注于高质量软件交付和工程效能提升,推动敏捷实践落地。',
icon: Briefcase,
accentColor: 'from-[#C41E3A] to-[#C41E3A]',
},
{
name: '咨询总监',
initials: 'CD',
specialties: ['业务咨询', '流程优化', '项目管理'],
bio: '10年+管理咨询经验,擅长客户需求深度分析和解决方案设计,确保项目精准落地。',
icon: Users,
title: '复合型技术团队',
description: '既懂技术又懂业务,能深入理解客户场景,提供真正落地的解决方案。',
accentColor: 'from-[#C41E3A] to-[#A01830]',
},
];
const TEAM_STATS = [
{ value: '12+', label: '年团队经验' },
{ value: '80%', label: '本科及以上学历' },
{ value: '4', label: '核心服务' },
{ value: '5+', label: '行业覆盖' },
];
export function TeamSection() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: '-100px' });
return (
<section id="team" role="region" aria-labelledby="team-heading" className="py-24 bg-[#FAFAFA] relative overflow-hidden" ref={ref}>
{/* 背景装饰 */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[600px] h-[600px] bg-[rgba(196,30,58,0.02)] rounded-full blur-3xl" />
<div className="container-wide relative z-10">
{/* 标题区 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
@@ -42,48 +70,86 @@ export function TeamSection() {
<span className="text-[#C41E3A] font-calligraphy"></span>
</h2>
<p className="text-lg text-[#5C5C5C] leading-relaxed">
12 + IT
IT企业的核心团队
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto mb-12">
{TEAM_HIGHLIGHTS.map((item, idx) => {
const Icon = item.icon;
{/* 团队数据概览 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.1 }}
className="grid grid-cols-2 md:grid-cols-4 gap-4 max-w-4xl mx-auto mb-16"
>
{TEAM_STATS.map((stat) => (
<div
key={stat.label}
className="text-center py-4 px-3 bg-white rounded-xl border border-[#E5E5E5]"
>
<div className="text-2xl sm:text-3xl font-bold bg-gradient-to-r from-[#C41E3A] to-[#E85D75] bg-clip-text text-transparent mb-1">
{stat.value}
</div>
<div className="text-xs sm:text-sm text-[#5C5C5C]">{stat.label}</div>
</div>
))}
</motion.div>
{/* 团队成员卡片 */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 max-w-6xl mx-auto mb-12">
{TEAM_MEMBERS.map((member, idx) => {
const Icon = member.icon;
return (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 20 }}
key={member.name}
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.1 + idx * 0.15 }}
transition={{ duration: 0.5, delay: 0.2 + idx * 0.12 }}
>
<div className="bg-white rounded-2xl p-8 border border-[#E5E5E5] hover:border-[#C41E3A]/30 hover:shadow-lg transition-all duration-300 h-full text-center">
<div className="w-14 h-14 bg-[#C41E3A] rounded-2xl flex items-center justify-center mb-6 mx-auto">
<Icon className="w-7 h-7 text-white" />
<div className="bg-white rounded-2xl p-6 border border-[#E5E5E5] hover:border-[#C41E3A]/30 hover:shadow-lg transition-all duration-300 h-full flex flex-col group">
{/* 头像区 */}
<div className="flex items-center gap-4 mb-4">
<div className={`w-14 h-14 rounded-2xl bg-gradient-to-br ${member.accentColor} flex items-center justify-center shrink-0 group-hover:scale-105 transition-transform duration-300`}>
<Icon className="w-7 h-7 text-white" />
</div>
<div className="min-w-0">
<h3 className="text-base font-bold text-[#1C1C1C] truncate">{member.name}</h3>
<span className="text-xs text-[#C41E3A] font-medium">{member.initials}</span>
</div>
</div>
{/* 简介 */}
<p className="text-sm text-[#5C5C5C] leading-relaxed mb-4 flex-1">{member.bio}</p>
{/* 专业领域标签 */}
<div className="flex flex-wrap gap-1.5">
{member.specialties.map((spec) => (
<span
key={spec}
className="inline-flex items-center text-xs px-2.5 py-1 bg-[#FAFAFA] text-[#3D3D3D] rounded-full border border-[#E5E5E5] group-hover:border-[#C41E3A]/20 transition-colors"
>
{spec}
</span>
))}
</div>
<h3 className="text-lg font-bold text-[#1C1C1C] mb-3">{item.title}</h3>
<p className="text-sm text-[#5C5C5C] leading-relaxed">{item.description}</p>
</div>
</motion.div>
);
})}
</div>
{/* CTA */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.5 }}
transition={{ duration: 0.6, delay: 0.6 }}
className="text-center"
>
<Button
variant="outline"
size="lg"
asChild
>
<StaticLink href="/team">
<StaticLink href="/team">
<RippleButton className="inline-flex items-center gap-2 px-6 py-2.5 border border-[#E5E5E5] rounded-lg text-sm font-medium text-[#1C1C1C] hover:border-[#C41E3A] hover:text-[#C41E3A] transition-colors">
<ArrowRight className="ml-2 w-4 h-4" />
</StaticLink>
</Button>
<ArrowRight className="w-4 h-4" />
</RippleButton>
</StaticLink>
</motion.div>
</div>
</section>
+3 -10
View File
@@ -13,11 +13,9 @@ export function OrganizationSchema() {
"addressCountry": "CN",
"addressLocality": "成都",
"addressRegion": "四川省",
"streetAddress": "成都市高新区"
"streetAddress": "成都市龙泉驿区幸福路12号"
},
"sameAs": [
"https://www.novalon.cn"
]
"sameAs": []
};
return (
@@ -33,12 +31,7 @@ export function WebsiteSchema() {
"@context": "https://schema.org",
"@type": "WebSite",
"name": "四川睿新致远科技有限公司",
"url": "https://www.novalon.cn",
"potentialAction": {
"@type": "SearchAction",
"target": "https://www.novalon.cn/search?q={search_term_string}",
"query-input": "required name=search_term_string"
}
"url": "https://www.novalon.cn"
};
return (
@@ -0,0 +1,70 @@
'use client';
/**
* ServiceCasesSection - 相关案例区域
*
* 展示与当前服务相关的案例卡片,使用 InkCard 实现悬浮效果,
* StaticLink 链接到案例详情页。取前 4 个案例展示。
*/
import { useRef } from 'react';
import { Badge } from '@/components/ui/badge';
import { StaticLink } from '@/components/ui/static-link';
import { StaggerContainer, StaggerItem, InkCard } from '@/lib/animations';
import { ScrollReveal, slideInLeftVariants } from '@/components/ui/scroll-animations';
import { CASES } from '@/lib/constants';
import type { Service } from '@/lib/constants/services';
const SERVICE_CASE_MAP: Record<string, string[]> = {
software: ['case-2', 'case-3'],
data: ['case-2'],
consulting: ['case-3', 'case-1'],
solutions: ['case-1', 'case-4'],
};
export function ServiceCasesSection({ service }: { service: Service }) {
const sectionRef = useRef<HTMLElement>(null);
const relatedCaseIds = SERVICE_CASE_MAP[service.id] || [];
const displayCases = relatedCaseIds
.map(id => CASES.find(c => c.id === id))
.filter((c): c is NonNullable<typeof c> => Boolean(c));
return (
<section id="cases" ref={sectionRef} className="relative py-20 md:py-28 bg-[#F8F8F8] overflow-hidden">
<div className="container-wide">
{/* 标题区 */}
<ScrollReveal variants={slideInLeftVariants} className="mb-12">
<h2 className="text-3xl md:text-4xl font-bold text-[#1C1C1C]">
</h2>
</ScrollReveal>
{/* 案例卡片网格 */}
<StaggerContainer className="grid md:grid-cols-2 gap-6">
{displayCases.map((caseItem) => (
<StaggerItem key={caseItem.id}>
<StaticLink href={`/cases/${caseItem.id}`}>
<InkCard
className="p-6 bg-white rounded-2xl border border-[#E5E5E5] hover:border-[#C41E3A]/30 transition-colors h-full"
hoverScale={1.02}
>
{/* 行业标签 */}
<Badge variant="secondary" className="mb-3">
{caseItem.industry}
</Badge>
{/* 案例标题 */}
<h3 className="text-lg font-semibold text-[#1C1C1C] mb-2">
{caseItem.title}
</h3>
{/* 案例描述 - 限制两行 */}
<p className="text-[#5C5C5C] text-sm leading-relaxed line-clamp-2">
{caseItem.description}
</p>
</InkCard>
</StaticLink>
</StaggerItem>
))}
</StaggerContainer>
</div>
</section>
);
}
@@ -0,0 +1,64 @@
'use client';
/**
* ServiceChallengesSection - 客户面临的挑战区域
*
* 以双列卡片网格展示客户在该业务领域可能遇到的痛点。
* 每张卡片使用 InkCard 实现弹簧物理悬浮效果,
* StaggerContainer + StaggerItem 实现交错入场动画。
*/
import { useRef } from 'react';
import { InkReveal, StaggerContainer, StaggerItem, InkCard } from '@/lib/animations';
import { ScrollReveal, slideInLeftVariants } from '@/components/ui/scroll-animations';
import type { Service } from '@/lib/constants/services';
interface ServiceChallengesSectionProps {
service: Service;
}
export function ServiceChallengesSection({ service }: ServiceChallengesSectionProps) {
const sectionRef = useRef<HTMLElement>(null);
return (
<section id="challenges" ref={sectionRef} className="relative py-20 md:py-28 bg-[#F8F8F8] overflow-hidden">
<div className="container-wide">
{/* 标题区 */}
<ScrollReveal variants={slideInLeftVariants} className="mb-4">
<h2 className="text-3xl md:text-4xl font-bold text-[#1C1C1C]">
</h2>
</ScrollReveal>
{/* 朱砂红装饰线 */}
<div className="w-16 h-1 bg-[#C41E3A] mb-6" />
{/* 副标题描述 */}
<InkReveal delay={0.2}>
<p className="text-lg text-[#5C5C5C] max-w-2xl mb-12">
</p>
</InkReveal>
{/* 挑战卡片网格 */}
<StaggerContainer className="grid md:grid-cols-2 gap-6">
{service.challenges.map((challenge, index) => (
<StaggerItem key={index}>
<InkCard
className="p-6 md:p-8 bg-white rounded-2xl border border-[#E5E5E5] hover:border-[#C41E3A]/30 transition-colors"
hoverScale={1.02}
hoverShadow="0 20px 40px rgba(196, 30, 58, 0.08)"
>
<h3 className="text-xl font-semibold text-[#1C1C1C] mb-3">
{challenge.title}
</h3>
<p className="text-[#5C5C5C] leading-relaxed">
{challenge.description}
</p>
</InkCard>
</StaggerItem>
))}
</StaggerContainer>
</div>
</section>
);
}
@@ -0,0 +1,75 @@
'use client';
/**
* ServiceCTASection - 行动号召区域
*
* 朱砂红渐变背景,配合 FloatingElement 装饰圆形,
* InkReveal 包裹标题,FadeUp 包裹描述和按钮组。
* 主按钮链接到联系页,次按钮链接到服务列表页。
*/
import { Phone } from 'lucide-react';
import { InkReveal, FadeUp, FloatingElement, RippleButton } from '@/lib/animations';
export function ServiceCTASection() {
return (
<section className="relative py-24 md:py-32 bg-gradient-to-r from-[#C41E3A] to-[#E85D75] overflow-hidden">
{/* 右上角装饰圆形 */}
<FloatingElement amplitude={8} duration={5} delay={0.5} className="absolute -top-20 -right-20 pointer-events-none">
<div className="w-[280px] h-[280px] bg-white/10 rounded-full" />
</FloatingElement>
{/* 左下角装饰圆形 */}
<FloatingElement amplitude={6} duration={4} delay={1} className="absolute -bottom-16 -left-16 pointer-events-none">
<div className="w-[220px] h-[220px] bg-white/10 rounded-full" />
</FloatingElement>
<div className="container-wide">
<div className="max-w-3xl mx-auto text-center">
{/* 标题 */}
<InkReveal delay={0}>
<h2 className="text-3xl md:text-4xl font-bold text-white mb-6">
</h2>
</InkReveal>
{/* 描述文字 */}
<FadeUp delay={0.15}>
<p className="text-lg text-white/90 mb-10">
</p>
</FadeUp>
{/* 按钮组 */}
<FadeUp delay={0.3}>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<RippleButton
href="/contact"
rippleColor="rgba(196, 30, 58, 0.3)"
className="bg-white text-[#C41E3A] px-8 py-4 rounded-lg text-lg font-semibold inline-flex items-center justify-center w-full sm:w-auto"
>
</RippleButton>
<RippleButton
href="/contact"
rippleColor="rgba(255, 255, 255, 0.2)"
className="bg-transparent border-2 border-white text-white px-8 py-4 rounded-lg text-lg font-semibold inline-flex items-center justify-center w-full sm:w-auto"
>
</RippleButton>
<RippleButton
href="tel:+8613800138000"
rippleColor="rgba(255, 255, 255, 0.2)"
className="bg-transparent border-2 border-white text-white px-8 py-4 rounded-lg text-lg font-semibold inline-flex items-center justify-center w-full sm:w-auto"
>
<Phone className="w-4 h-4 mr-2" />
</RippleButton>
</div>
</FadeUp>
</div>
</div>
</section>
);
}
@@ -0,0 +1,55 @@
'use client';
/**
* ServiceFeaturesSection - 服务功能特性区域
*
* 居中展示服务概览和功能列表,每项功能使用 CheckCircle2 图标
* 配合 FadeUp 入场动画,StaggerContainer 实现交错出现效果。
*/
import { useRef } from 'react';
import { CheckCircle2 } from 'lucide-react';
import { InkReveal, FadeUp, StaggerContainer, StaggerItem } from '@/lib/animations';
import { ScrollReveal, inkRevealVariants } from '@/components/ui/scroll-animations';
import type { Service } from '@/lib/constants/services';
interface ServiceFeaturesSectionProps {
service: Service;
}
export function ServiceFeaturesSection({ service }: ServiceFeaturesSectionProps) {
const sectionRef = useRef<HTMLElement>(null);
return (
<section id="features" ref={sectionRef} className="relative py-20 md:py-28 bg-white overflow-hidden">
<div className="container-wide">
{/* 标题区 - 居中 */}
<ScrollReveal variants={inkRevealVariants} className="mb-4 text-center">
<h2 className="text-3xl md:text-4xl font-bold text-[#1C1C1C]">
</h2>
</ScrollReveal>
{/* 服务概览 */}
<InkReveal delay={0.2}>
<p className="text-lg text-[#5C5C5C] text-center max-w-2xl mx-auto mb-12">
{service.overview}
</p>
</InkReveal>
{/* 功能列表 */}
<StaggerContainer className="max-w-3xl mx-auto space-y-4">
{service.features.map((feature, index) => (
<StaggerItem key={index}>
<div className="flex items-start gap-3">
<CheckCircle2 className="w-5 h-5 text-[#C41E3A] mt-0.5 flex-shrink-0" />
<FadeUp delay={index * 0.05}>
<span className="text-[#1C1C1C] leading-relaxed">{feature}</span>
</FadeUp>
</div>
</StaggerItem>
))}
</StaggerContainer>
</div>
</section>
);
}
@@ -0,0 +1,143 @@
'use client';
/**
* ServiceHeroSection - 核心业务详情页首屏区域
*
* 展示服务标题、描述和 CTA 按钮,配合水墨背景和粒子特效。
* 使用 InkReveal 实现模糊揭示入场动画,SealStamp 包裹分类标签,
* FloatingElement 驱动滚动指示器浮动效果。
*/
import { useEffect, useRef, useState } from 'react';
import { motion } from 'framer-motion';
import dynamic from 'next/dynamic';
import { ChevronDown } from 'lucide-react';
import { InkReveal, SealStamp, RippleButton, FloatingElement } from '@/lib/animations';
import type { Service } from '@/lib/constants/services';
/* 背景特效组件 - 必须禁用 SSR,避免 Canvas/WebGL 在服务端报错 */
const InkBackground = dynamic(
() => import('@/components/ui/ink-decoration').then(mod => ({ default: mod.InkBackground })),
{ ssr: false }
);
const DataParticleFlow = dynamic(
() => import('@/components/effects/data-particle-flow').then(mod => ({ default: mod.DataParticleFlow })),
{ ssr: false }
);
interface ServiceHeroSectionProps {
service: Service;
}
export function ServiceHeroSection({ service }: ServiceHeroSectionProps) {
const [isVisible, setIsVisible] = useState(false);
const sectionRef = useRef<HTMLElement>(null);
/* 使用 IntersectionObserver 控制滚动指示器可见性 */
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting) {
setIsVisible(true);
}
},
{ threshold: 0.1 }
);
if (sectionRef.current) {
observer.observe(sectionRef.current);
}
return () => observer.disconnect();
}, []);
return (
<section
ref={sectionRef}
className="relative min-h-screen flex items-center justify-center overflow-hidden bg-gradient-to-b from-white to-[#F8F8F8]"
>
{/* 背景特效层 */}
<InkBackground />
<DataParticleFlow
particleCount={60}
color="#C41E3A"
intensity="subtle"
shape="square"
effect="pulse"
/>
{/* 主内容区 */}
<div className="container-wide relative z-10 py-32 md:py-40">
<div className="max-w-4xl mx-auto text-center">
{/* 分类标签 - 印章按压效果 */}
<SealStamp
delay={0.1}
className="inline-block px-4 py-2 bg-[#C41E3A]/20 rounded-full text-[#C41E3A] text-sm mb-6"
>
</SealStamp>
{/* 服务标题 - 模糊揭示入场 */}
<InkReveal delay={0.2}>
<h1 className="text-4xl md:text-6xl lg:text-7xl font-bold text-[#1C1C1C] mb-6">
{service.title}
</h1>
</InkReveal>
{/* 服务描述 - 模糊揭示入场 */}
<InkReveal delay={0.4}>
<p className="text-lg md:text-xl text-[#5C5C5C] leading-relaxed mb-10 max-w-2xl mx-auto">
{service.description}
</p>
</InkReveal>
{/* CTA 按钮组 - 涟漪效果 */}
<InkReveal delay={0.6}>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<RippleButton
href="/contact"
className="border-2 border-[#C41E3A] text-[#C41E3A] hover:bg-[#C41E3A] hover:text-white px-8 py-4 rounded-lg text-lg font-semibold inline-flex items-center justify-center"
rippleColor="rgba(196, 30, 58, 0.2)"
>
</RippleButton>
<RippleButton
href="/contact"
className="bg-[#C41E3A] hover:bg-[#A01830] text-white px-8 py-4 rounded-lg text-lg font-semibold inline-flex items-center justify-center"
rippleColor="rgba(255, 255, 255, 0.3)"
>
</RippleButton>
<RippleButton
href="#challenges"
className="border-2 border-[#C41E3A] text-[#C41E3A] hover:bg-[#C41E3A]/5 px-8 py-4 rounded-lg text-lg font-semibold inline-flex items-center justify-center"
rippleColor="rgba(196, 30, 58, 0.2)"
>
</RippleButton>
</div>
</InkReveal>
</div>
</div>
{/* 滚动指示器 - 浮动元素包裹 */}
<FloatingElement
amplitude={10}
duration={1.5}
delay={1}
className="absolute bottom-8 left-1/2 -translate-x-1/2"
>
<motion.div
initial={{ opacity: 0 }}
animate={isVisible ? { opacity: 1 } : {}}
transition={{ duration: 0.6, delay: 1 }}
>
<div className="text-[#999999]">
<ChevronDown className="w-8 h-8" />
</div>
</motion.div>
</FloatingElement>
</section>
);
}
@@ -0,0 +1,93 @@
'use client';
/**
* ServiceOutcomesSection - 服务成果区域
*
* 以三列卡片网格展示量化成果数据,使用 CountUp 数字动画
* 和渐变色文字突出关键指标。底部汇总 benefits 文本。
*/
import { useRef } from 'react';
import { ScrollReveal, slideInLeftVariants } from '@/components/ui/scroll-animations';
import { StaggerContainer, StaggerItem, InkCard, CountUp, InkReveal } from '@/lib/animations';
import type { Service } from '@/lib/constants/services';
interface ServiceOutcomesSectionProps {
service: Service;
}
/** 从文本中提取数字部分,用于 CountUp 动画 */
function extractNumber(text: string): number | null {
const match = text.match(/(\d+)/);
if (match) {
return parseInt(match[1]!, 10);
}
return null;
}
/** 成果卡片子组件 */
function OutcomeCard({ outcome }: { outcome: { value: string; label: string } }) {
const numberValue = extractNumber(outcome.value);
return (
<StaggerItem>
<InkCard
className="p-6 md:p-8 bg-white rounded-2xl border border-[#E5E5E5] hover:border-[#C41E3A]/30 transition-colors text-center"
hoverScale={1.02}
>
{/* 数字动画区域 */}
<div className="mb-4">
{numberValue !== null ? (
<span className="text-4xl md:text-5xl font-bold bg-gradient-to-r from-[#C41E3A] to-[#E85D75] bg-clip-text text-transparent">
<CountUp
end={numberValue}
duration={2000}
/>
{/* 保留非数字后缀(如 %、+ 等) */}
{outcome.value.replace(/(\d+)/, '')}
</span>
) : (
<span className="text-4xl md:text-5xl font-bold bg-gradient-to-r from-[#C41E3A] to-[#E85D75] bg-clip-text text-transparent">
{outcome.value}
</span>
)}
</div>
{/* 标签文字 */}
<p className="text-[#5C5C5C]">{outcome.label}</p>
</InkCard>
</StaggerItem>
);
}
export function ServiceOutcomesSection({ service }: ServiceOutcomesSectionProps) {
const sectionRef = useRef<HTMLElement>(null);
return (
<section id="outcomes" ref={sectionRef} className="relative py-20 md:py-28 bg-white overflow-hidden">
<div className="container-wide">
{/* 标题区 */}
<ScrollReveal variants={slideInLeftVariants} className="mb-12">
<h2 className="text-3xl md:text-4xl font-bold text-[#1C1C1C]">
</h2>
</ScrollReveal>
{/* 成果卡片网格 */}
<StaggerContainer className="grid sm:grid-cols-3 gap-6">
{service.outcomes.map((outcome, index) => (
<OutcomeCard key={index} outcome={outcome} />
))}
</StaggerContainer>
{/* benefits 汇总文本 */}
<InkReveal delay={0.3}>
<div className="mt-10 p-6 bg-[#F8F8F8] rounded-2xl border border-[#E5E5E5]">
<p className="text-[#5C5C5C] leading-relaxed">
{service.benefits.join('')}
</p>
</div>
</InkReveal>
</div>
</section>
);
}
@@ -0,0 +1,89 @@
'use client';
/**
* ServiceProcessSection - 服务流程区域
*
* 以时间轴形式展示服务实施步骤,左侧使用 SealStamp 编号圆形
* 和渐变连接线,右侧使用 FadeUp 包裹步骤标题和描述。
* 用中文冒号分隔步骤标题和描述。
*/
import { useRef } from 'react';
import { ScrollReveal, inkRevealVariants } from '@/components/ui/scroll-animations';
import { StaggerContainer, StaggerItem, SealStamp, FadeUp } from '@/lib/animations';
import type { Service } from '@/lib/constants/services';
interface ServiceProcessSectionProps {
service: Service;
}
/** 流程步骤子组件 - 解析中文冒号分隔的标题和描述 */
function ProcessStep({
step,
index,
total,
}: {
step: string;
index: number;
total: number;
}) {
/* 用中文冒号分隔步骤标题和描述 */
const colonIndex = step.indexOf('');
const title = colonIndex > -1 ? step.substring(0, colonIndex) : step;
const description = colonIndex > -1 ? step.substring(colonIndex + 1) : '';
return (
<StaggerItem className="flex items-start gap-6">
{/* 左侧:编号圆形 + 渐变连接线 */}
<div className="flex-shrink-0">
<SealStamp delay={index * 0.15}>
<div className="w-12 h-12 rounded-full bg-[#C41E3A] flex items-center justify-center text-white font-bold text-lg">
{index + 1}
</div>
</SealStamp>
{/* 最后一步不显示连接线 */}
{index < total - 1 && (
<div className="w-0.5 h-16 bg-gradient-to-b from-[#C41E3A]/40 to-[#C41E3A]/10 ml-6 mt-2" />
)}
</div>
{/* 右侧:步骤标题和描述 */}
<FadeUp delay={index * 0.1} className="flex-1 pb-8">
<h3 className="text-xl font-semibold text-[#1C1C1C] mb-2">{title}</h3>
{description && (
<p className="text-[#5C5C5C] leading-relaxed">{description}</p>
)}
</FadeUp>
</StaggerItem>
);
}
export function ServiceProcessSection({ service }: ServiceProcessSectionProps) {
const sectionRef = useRef<HTMLElement>(null);
return (
<section id="process" ref={sectionRef} className="relative py-20 md:py-28 bg-[#F8F8F8] overflow-hidden">
<div className="container-wide">
<div className="max-w-3xl mx-auto">
{/* 标题区 - 居中 */}
<ScrollReveal variants={inkRevealVariants} className="mb-12 text-center">
<h2 className="text-3xl md:text-4xl font-bold text-[#1C1C1C]">
</h2>
</ScrollReveal>
{/* 步骤列表 */}
<StaggerContainer className="mt-8" staggerDelay={0.15}>
{service.process.map((step, index) => (
<ProcessStep
key={index}
step={step}
index={index}
total={service.process.length}
/>
))}
</StaggerContainer>
</div>
</div>
</section>
);
}
@@ -0,0 +1,109 @@
'use client';
/**
* AccompanySection - 长期陪跑服务 · 同行伙伴
*
* 解决方案页面的第三个模块,展示长期陪跑服务。
* 使用水墨+朱砂红东方美学设计体系,配合滚动触发动画。
* 内容硬编码在组件内部,无外部 Props。
* delay 基础值较第一个模块增加 0.4,避免同时触发。
*/
import { motion } from 'framer-motion';
import { Users, CheckCircle2, ArrowRight } from 'lucide-react';
import { StaticLink } from '@/components/ui/static-link';
import { Button } from '@/components/ui/button';
import { InkReveal, FadeUp, StaggerContainer, StaggerItem } from '@/lib/animations';
import { ScrollReveal, slideInLeftVariants } from '@/components/ui/scroll-animations';
/** 核心价值点数据 */
const valuePoints = [
'专属客户成功经理',
'季度业务复盘会',
'7×24小时响应通道',
];
export function AccompanySection() {
return (
<motion.section
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.4 }}
className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-12 border border-[#C41E3A]/20"
>
{/* 标题区域:图标 + 标题 + 副标题 */}
<div className="flex items-start gap-6 mb-8">
<div className="w-16 h-16 bg-[#C41E3A] rounded-2xl flex items-center justify-center flex-shrink-0">
<Users className="w-8 h-8 text-white" />
</div>
<div>
<InkReveal delay={0.4}>
<h2 className="text-3xl font-bold text-[#1C1C1C] mb-2">
·
</h2>
</InkReveal>
<FadeUp delay={0.5}>
<p className="text-lg text-[#5C5C5C]">
</p>
</FadeUp>
</div>
</div>
{/* 描述段落 */}
<div className="space-y-6 mb-8">
<FadeUp delay={0.55}>
<p className="text-lg text-[#1C1C1C] leading-relaxed">
线
</p>
</FadeUp>
<FadeUp delay={0.6}>
<p className="text-lg text-[#1C1C1C] leading-relaxed">
访
</p>
</FadeUp>
<FadeUp delay={0.65}>
<p className="text-lg text-[#1C1C1C] leading-relaxed">
</p>
</FadeUp>
</div>
{/* 核心价值点 */}
<div className="mb-8">
<ScrollReveal variants={slideInLeftVariants}>
<h3 className="text-xl font-semibold text-[#1C1C1C] mb-4 flex items-center gap-2">
<CheckCircle2 className="w-6 h-6 text-[#C41E3A]" />
</h3>
</ScrollReveal>
<StaggerContainer className="grid md:grid-cols-3 gap-4">
{valuePoints.map((point) => (
<StaggerItem key={point}>
<div className="flex items-start gap-3 p-4 bg-white rounded-lg border border-[#E5E5E5]">
<div className="w-2 h-2 bg-[#C41E3A] rounded-full mt-2" />
<span className="text-[#1C1C1C]">{point}</span>
</div>
</StaggerItem>
))}
</StaggerContainer>
</div>
{/* CTA 按钮 */}
<FadeUp delay={0.8}>
<div className="flex justify-center">
<Button
size="lg"
className="bg-[#C41E3A] hover:bg-[#A01830] text-white"
asChild
>
<StaticLink href="/contact">
<ArrowRight className="ml-2 w-4 h-4" />
</StaticLink>
</Button>
</div>
</FadeUp>
</motion.section>
);
}
@@ -0,0 +1,108 @@
'use client';
/**
* ConsultingSection - 数字化转型咨询 · 参谋伙伴
*
* 解决方案页面的第一个模块,展示数字化转型咨询服务。
* 使用水墨+朱砂红东方美学设计体系,配合滚动触发动画。
* 内容硬编码在组件内部,无外部 Props。
*/
import { motion } from 'framer-motion';
import { Lightbulb, CheckCircle2, ArrowRight } from 'lucide-react';
import { StaticLink } from '@/components/ui/static-link';
import { Button } from '@/components/ui/button';
import { InkReveal, FadeUp, StaggerContainer, StaggerItem } from '@/lib/animations';
import { ScrollReveal, slideInLeftVariants } from '@/components/ui/scroll-animations';
/** 核心价值点数据 */
const valuePoints = [
'行业趋势洞察报告',
'数字化转型成熟度评估',
'个性化实施路径规划',
];
export function ConsultingSection() {
return (
<motion.section
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-12 border border-[#C41E3A]/20"
>
{/* 标题区域:图标 + 标题 + 副标题 */}
<div className="flex items-start gap-6 mb-8">
<div className="w-16 h-16 bg-[#C41E3A] rounded-2xl flex items-center justify-center flex-shrink-0">
<Lightbulb className="w-8 h-8 text-white" />
</div>
<div>
<InkReveal delay={0}>
<h2 className="text-3xl font-bold text-[#1C1C1C] mb-2">
·
</h2>
</InkReveal>
<FadeUp delay={0.1}>
<p className="text-lg text-[#5C5C5C]">
</p>
</FadeUp>
</div>
</div>
{/* 描述段落 */}
<div className="space-y-6 mb-8">
<FadeUp delay={0.15}>
<p className="text-lg text-[#1C1C1C] leading-relaxed">
</p>
</FadeUp>
<FadeUp delay={0.2}>
<p className="text-lg text-[#1C1C1C] leading-relaxed">
</p>
</FadeUp>
<FadeUp delay={0.25}>
<p className="text-lg text-[#1C1C1C] leading-relaxed">
</p>
</FadeUp>
</div>
{/* 核心价值点 */}
<div className="mb-8">
<ScrollReveal variants={slideInLeftVariants}>
<h3 className="text-xl font-semibold text-[#1C1C1C] mb-4 flex items-center gap-2">
<CheckCircle2 className="w-6 h-6 text-[#C41E3A]" />
</h3>
</ScrollReveal>
<StaggerContainer className="grid md:grid-cols-3 gap-4">
{valuePoints.map((point) => (
<StaggerItem key={point}>
<div className="flex items-start gap-3 p-4 bg-white rounded-lg border border-[#E5E5E5]">
<div className="w-2 h-2 bg-[#C41E3A] rounded-full mt-2" />
<span className="text-[#1C1C1C]">{point}</span>
</div>
</StaggerItem>
))}
</StaggerContainer>
</div>
{/* CTA 按钮 */}
<FadeUp delay={0.4}>
<div className="flex justify-center">
<Button
size="lg"
className="bg-[#C41E3A] hover:bg-[#A01830] text-white"
asChild
>
<StaticLink href="/contact">
<ArrowRight className="ml-2 w-4 h-4" />
</StaticLink>
</Button>
</div>
</FadeUp>
</motion.section>
);
}
@@ -0,0 +1,110 @@
'use client';
/**
* TechSolutionSection - 信息技术解决方案 · 技术伙伴
*
* 解决方案页面的第二个模块,展示信息技术解决方案服务。
* 使用水墨+朱砂红东方美学设计体系,配合滚动触发动画。
* 内容硬编码在组件内部,无外部 Props。
* delay 基础值较第一个模块增加 0.2,避免同时触发。
*/
import { motion } from 'framer-motion';
import { Cpu, CheckCircle2, ArrowRight } from 'lucide-react';
import { StaticLink } from '@/components/ui/static-link';
import { Button } from '@/components/ui/button';
import { InkReveal, FadeUp, StaggerContainer, StaggerItem } from '@/lib/animations';
import { ScrollReveal, slideInLeftVariants } from '@/components/ui/scroll-animations';
/** 核心价值点数据 */
const valuePoints = [
'业务场景深度调研',
'技术方案定制开发',
'敏捷交付快速迭代',
];
export function TechSolutionSection() {
return (
<motion.section
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}
className="bg-gradient-to-br from-[#FFFBF5] to-white rounded-2xl p-12 border border-[#C41E3A]/20"
>
{/* 标题区域:图标 + 标题 + 副标题 */}
<div className="flex items-start gap-6 mb-8">
<div className="w-16 h-16 bg-[#C41E3A] rounded-2xl flex items-center justify-center flex-shrink-0">
<Cpu className="w-8 h-8 text-white" />
</div>
<div>
<InkReveal delay={0.2}>
<h2 className="text-3xl font-bold text-[#1C1C1C] mb-2">
·
</h2>
</InkReveal>
<FadeUp delay={0.3}>
<p className="text-lg text-[#5C5C5C]">
</p>
</FadeUp>
</div>
</div>
{/* 描述段落 */}
<div className="space-y-6 mb-8">
<FadeUp delay={0.35}>
<p className="text-lg text-[#1C1C1C] leading-relaxed">
&ldquo;&rdquo;&ldquo;&rdquo;
</p>
</FadeUp>
<FadeUp delay={0.4}>
<p className="text-lg text-[#1C1C1C] leading-relaxed">
沿
</p>
</FadeUp>
<FadeUp delay={0.45}>
<p className="text-lg text-[#1C1C1C] leading-relaxed">
</p>
</FadeUp>
</div>
{/* 核心价值点 */}
<div className="mb-8">
<ScrollReveal variants={slideInLeftVariants}>
<h3 className="text-xl font-semibold text-[#1C1C1C] mb-4 flex items-center gap-2">
<CheckCircle2 className="w-6 h-6 text-[#C41E3A]" />
</h3>
</ScrollReveal>
<StaggerContainer className="grid md:grid-cols-3 gap-4">
{valuePoints.map((point) => (
<StaggerItem key={point}>
<div className="flex items-start gap-3 p-4 bg-white rounded-lg border border-[#E5E5E5]">
<div className="w-2 h-2 bg-[#C41E3A] rounded-full mt-2" />
<span className="text-[#1C1C1C]">{point}</span>
</div>
</StaggerItem>
))}
</StaggerContainer>
</div>
{/* CTA 按钮(outline 样式) */}
<FadeUp delay={0.6}>
<div className="flex justify-center">
<Button
size="lg"
variant="outline"
className="border-[#C41E3A] text-[#C41E3A] hover:bg-[#C41E3A] hover:text-white"
asChild
>
<StaticLink href="/cases">
<ArrowRight className="ml-2 w-4 h-4" />
</StaticLink>
</Button>
</div>
</FadeUp>
</motion.section>
);
}
+69
View File
@@ -0,0 +1,69 @@
'use client';
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { StaticLink } from '@/components/ui/static-link';
import { MessageCircle, X } from 'lucide-react';
import { useReducedMotion } from '@/hooks/use-reduced-motion';
export function FloatingCTA() {
const [isVisible, setIsVisible] = useState(false);
const [isDismissed, setIsDismissed] = useState(false);
const shouldReduceMotion = useReducedMotion();
useEffect(() => {
const handleScroll = () => {
// 滚动超过首屏(100vh)后显示
if (window.scrollY > window.innerHeight * 0.8) {
setIsVisible(true);
} else {
setIsVisible(false);
// 回到顶部时重置关闭状态
setIsDismissed(false);
}
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
if (isDismissed) {return null;}
return (
<AnimatePresence>
{isVisible && (
<motion.div
initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, scale: 0.8, y: 20 }}
animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1, y: 0 }}
exit={shouldReduceMotion ? { opacity: 0 } : { opacity: 0, scale: 0.8, y: 20 }}
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
className="fixed bottom-6 right-6 z-40 flex items-end gap-3"
>
{/* 关闭按钮 */}
<button
onClick={() => setIsDismissed(true)}
className="w-8 h-8 rounded-full bg-white border border-[#E5E5E5] flex items-center justify-center text-[#5C5C5C] hover:text-[#1C1C1C] hover:border-[#1C1C1C] transition-colors shadow-sm"
aria-label="关闭咨询按钮"
>
<X className="w-3.5 h-3.5" />
</button>
{/* 主 CTA 按钮 */}
<StaticLink href="/contact">
<motion.button
whileHover={shouldReduceMotion ? {} : { scale: 1.05, y: -2 }}
whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}
className="group relative flex items-center gap-2 px-5 py-3 rounded-full bg-[#C41E3A] text-white shadow-[0_4px_20px_rgba(196,30,58,0.35)] hover:shadow-[0_6px_28px_rgba(196,30,58,0.45)] transition-shadow"
aria-label="立即咨询"
>
{/* 脉冲光晕 */}
<span className="absolute inset-0 rounded-full bg-[#C41E3A] animate-ping opacity-20" />
<MessageCircle className="w-5 h-5 relative z-10" />
<span className="text-sm font-medium relative z-10 hidden sm:inline"></span>
</motion.button>
</StaticLink>
</motion.div>
)}
</AnimatePresence>
);
}
+14 -9
View File
@@ -50,19 +50,20 @@ export interface RippleButtonProps
rippleColor?: string;
rippleDuration?: number;
children?: React.ReactNode;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLButtonElement>) => void;
onClick?: (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLButtonElement | HTMLAnchorElement>) => void;
className?: string;
disabled?: boolean;
href?: string;
}
const RippleButton = React.forwardRef<HTMLButtonElement, RippleButtonProps>(
({ className, variant, size, rippleColor, rippleDuration = 800, onClick, children, disabled, ...props }, ref) => {
const RippleButton = React.forwardRef<HTMLButtonElement | HTMLAnchorElement, RippleButtonProps>(
({ className, variant, size, rippleColor, rippleDuration = 800, onClick, children, disabled, href, ...props }, ref) => {
const [ripples, setRipples] = React.useState<Ripple[]>([]);
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
const handleClick = (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
if (disabled) {return;}
const button = e.currentTarget;
const rect = button.getBoundingClientRect();
const x = e.clientX - rect.left;
@@ -86,15 +87,19 @@ const RippleButton = React.forwardRef<HTMLButtonElement, RippleButtonProps>(
return 'rgba(255, 255, 255, 0.4)';
};
const Component = href ? motion.a : motion.button;
const linkProps = href ? { href } : {};
return (
<motion.button
ref={ref}
<Component
ref={ref as React.Ref<never>}
whileHover={disabled ? {} : { scale: 1.03, y: -3 }}
whileTap={disabled ? {} : { scale: 0.97 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
className={cn(rippleButtonVariants({ variant, size, className }))}
onClick={handleClick}
disabled={disabled}
{...linkProps}
{...props}
>
{children}
@@ -115,7 +120,7 @@ const RippleButton = React.forwardRef<HTMLButtonElement, RippleButtonProps>(
/>
))}
</AnimatePresence>
</motion.button>
</Component>
);
}
);