Files
novalon-website/src/app/(marketing)/solutions/solutions-content-v1.tsx
T
张翔 b5245f9aa2 feat(cms): 添加 CMS 内容管理系统与 Admin 管理后台
- 新增 Prisma + SQLite 数据库模型 (Category, Content, Media, User 等)
- 新增 Admin 管理后台 (认证、内容管理、媒体管理)
- 新增 CMS API 路由 (CRUD, 草稿/发布, 重新验证)
- 新增 CMS 内容版本的历史归档页面
- 新增 components/cms 内容渲染组件
- 新增 components/admin 管理后台 UI 组件
- 更新 Contact API 路由
2026-07-07 06:53:58 +08:00

315 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useRef } from 'react';
import { motion } from 'framer-motion';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { Button } from '@/components/ui/button';
import { ArrowRight, Factory, ShoppingCart, Heart, GraduationCap } from 'lucide-react';
import { cn } from '@/lib/utils';
import { SOLUTIONS, type Solution } from '@/lib/constants/solutions';
import { PRODUCTS } from '@/lib/constants/products';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
const INDUSTRY_ICONS: Record<string, React.ReactNode> = {
: <Factory className="w-7 h-7" />,
: <ShoppingCart className="w-7 h-7" />,
: <Heart className="w-7 h-7" />,
: <GraduationCap className="w-7 h-7" />,
};
function GrainOverlay() {
return (
<div
className="absolute inset-0 pointer-events-none opacity-[0.03] mix-blend-overlay"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E")`,
backgroundRepeat: 'repeat',
backgroundSize: '300px 300px',
}}
/>
);
}
function SectionLabel({ children, className = '' }: { children: React.ReactNode; className?: string }) {
return (
<div className={cn('flex items-center gap-5 mb-7', className)}>
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
{children}
</span>
</div>
);
}
function HeroSection() {
const containerRef = useRef<HTMLDivElement>(null);
return (
<section
ref={containerRef}
className="relative min-h-[85vh] flex items-center overflow-hidden bg-ink"
>
<GrainOverlay />
<div className="absolute inset-0 pointer-events-none">
<div
className="absolute top-0 right-0 w-[42%] h-full"
style={{
background: 'linear-gradient(135deg, transparent 25%, rgba(196, 30, 58, 0.094) 100%)',
clipPath: 'polygon(35% 0, 100% 0, 100% 100%, 0% 100%)',
}}
/>
<div className="absolute bottom-0 left-0 w-full h-2/5 bg-gradient-to-t from-ink to-transparent" />
<div className="absolute top-24 right-[22%] w-[350px] h-[350px] rounded-full opacity-15 blur-3xl bg-brand" />
</div>
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10 w-full py-32 lg:py-40">
<div className="max-w-4xl">
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 1.1, ease: EASE_OUT }}
className="mb-10"
>
<SectionLabel>Industry Solutions</SectionLabel>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 70 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1.2, delay: 0.15, ease: EASE_OUT }}
className="text-5xl sm:text-6xl md:text-7xl lg:text-8xl font-bold text-white leading-[0.85] tracking-tighter mb-12"
>
<div className="block"></div>
<div className="block mt-4">
<span className="relative">
<span className="text-brand"></span>
<motion.span
className="absolute -bottom-2 left-0 w-full h-1 bg-brand"
style={{ transformOrigin: 'left' }}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.8, delay: 1.2, ease: EASE_OUT }}
/>
</span>
</div>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.45, ease: EASE_OUT }}
className="text-xl lg:text-2xl text-dark-text-secondary max-w-2xl leading-relaxed mb-12"
>
+
</motion.p>
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.6, ease: EASE_OUT }}
className="flex flex-wrap gap-6"
>
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/products"></a>
</Button>
</motion.div>
</div>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.9, ease: EASE_OUT }}
className="grid grid-cols-3 gap-12 mt-24 pt-16 border-t border-white/10 max-w-2xl"
>
<div>
<div className="text-5xl font-bold text-white mb-3">4<span className="text-brand">+</span></div>
<div className="text-sm text-dark-text-muted"></div>
</div>
<div>
<div className="text-5xl font-bold text-white mb-3"><span className="text-brand"></span></div>
<div className="text-sm text-dark-text-muted"></div>
</div>
<div>
<div className="text-5xl font-bold text-white mb-3"><span className="text-brand"></span></div>
<div className="text-sm text-dark-text-muted"></div>
</div>
</motion.div>
</div>
</section>
);
}
function SolutionsGridSection() {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="max-w-container mx-auto px-6 lg:px-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-12 mb-28">
<ScrollReveal className="lg:max-w-3xl">
<SectionLabel>Industry Expertise</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white tracking-tight leading-tight">
</h2>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<p className="text-dark-text-secondary max-w-md leading-relaxed text-lg">
</p>
</ScrollReveal>
</div>
<StaggerReveal
className="grid md:grid-cols-2 gap-8"
staggerDelay={0.1}
delayChildren={0.05}
>
{SOLUTIONS.map((solution) => (
<SolutionCard key={solution.id} solution={solution} />
))}
</StaggerReveal>
</div>
</section>
);
}
function SolutionCard({ solution }: { solution: Solution }) {
const industryIcon = INDUSTRY_ICONS[solution.industry] || <Factory className="w-7 h-7" />;
return (
<a
href={`/solutions/${solution.id}`}
className="group relative block border border-white/10 hover:border-brand/40 bg-ink transition-all duration-600 hover:-translate-y-2 overflow-hidden"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left" />
<div className="p-10 lg:p-12 relative z-10">
<div className="flex items-start justify-between mb-8">
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand group-hover:scale-110 transition-transform duration-500">
{industryIcon}
</div>
<span className="px-4 py-2 text-xs font-bold text-brand bg-brand-soft/50 backdrop-blur-sm">
{solution.industry}
</span>
</div>
<div className="mb-6">
<div className="text-dark-text-muted text-sm tracking-widest uppercase mb-2">
{solution.subtitle}
</div>
<h3 className="text-2xl lg:text-3xl font-bold text-white group-hover:translate-x-2 transition-transform duration-500">
{solution.title}
</h3>
</div>
<p className="text-dark-text-secondary leading-relaxed mb-10">
{solution.description}
</p>
<div className="pt-8 border-t border-white/10">
<p className="text-[11px] text-dark-text-muted mb-4 uppercase tracking-widest font-bold">
</p>
<div className="flex flex-wrap gap-2 mb-6">
{solution.suiteCombination.primaryProducts.map(pid => {
const prod = PRODUCTS.find(p => p.id === pid);
return prod ? (
<span
key={pid}
className="px-3 py-1.5 text-xs font-medium text-white bg-ink-light border border-white/10 group-hover:border-white/20 transition-colors"
>
{prod.title.replace('睿新', '').replace('睿视 ', '')}
</span>
) : null;
})}
<span className="px-3 py-1.5 text-xs font-bold text-brand bg-brand-soft">
+
</span>
</div>
<p className="text-sm text-dark-text-muted leading-relaxed">
{solution.suiteCombination.rationale}
</p>
</div>
<motion.div
className="inline-flex items-center gap-3 text-sm font-bold text-brand mt-10"
whileHover={{ x: 8 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
>
<span></span>
<ArrowRight className="w-4 h-4" />
</motion.div>
</div>
</a>
);
}
function CTASection() {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-ink">
<GrainOverlay />
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] rounded-full opacity-[0.08] blur-3xl bg-brand" />
</div>
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="max-w-3xl mx-auto text-center">
<SectionLabel className="justify-center">
<div className="flex items-center gap-5">
<div className="w-14 h-px bg-brand" />
<span className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand">
Get Started
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-6xl font-bold text-white tracking-tight leading-tight mb-8">
</h2>
<p className="text-xl text-dark-text-secondary leading-relaxed mb-12 max-w-2xl mx-auto">
</p>
<div className="flex flex-wrap gap-6 justify-center">
<Button size="xl" asChild>
<a href="/contact">
<ArrowRight className="w-5 h-5 ml-2" />
</a>
</Button>
<Button size="xl" variant="outline" asChild>
<a href="/products"></a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
);
}
export default function SolutionsContentV1() {
return (
<main>
<HeroSection />
<SolutionsGridSection />
<CTASection />
</main>
);
}