feat(cms): 添加 CMS 内容管理系统与 Admin 管理后台

- 新增 Prisma + SQLite 数据库模型 (Category, Content, Media, User 等)
- 新增 Admin 管理后台 (认证、内容管理、媒体管理)
- 新增 CMS API 路由 (CRUD, 草稿/发布, 重新验证)
- 新增 CMS 内容版本的历史归档页面
- 新增 components/cms 内容渲染组件
- 新增 components/admin 管理后台 UI 组件
- 更新 Contact API 路由
This commit is contained in:
张翔
2026-07-07 06:53:58 +08:00
parent 829d83522c
commit b5245f9aa2
87 changed files with 25659 additions and 0 deletions
@@ -0,0 +1,402 @@
'use client';
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, Lightbulb, Settings, Users, TrendingUp, Package, BarChart3, BookOpen, MessageSquare, Calendar, Smartphone } from 'lucide-react';
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
import type { Solution } from '@/lib/constants/solutions';
import type { Product } from '@/lib/constants/products';
const INDUSTRY_ICONS: Record<string, React.ReactNode> = {
制造业: <Factory className="w-8 h-8" />,
零售业: <ShoppingCart className="w-8 h-8" />,
医疗: <Heart className="w-8 h-8" />,
教育: <GraduationCap className="w-8 h-8" />,
};
const VALUE_ICONS: Record<string, React.ReactNode> = {
Factory: <Factory className="w-6 h-6" />,
Package: <Package className="w-6 h-6" />,
BarChart3: <BarChart3 className="w-6 h-6" />,
Users: <Users className="w-6 h-6" />,
TrendingUp: <TrendingUp className="w-6 h-6" />,
ShoppingCart: <ShoppingCart className="w-6 h-6" />,
GraduationCap: <GraduationCap className="w-6 h-6" />,
BookOpen: <BookOpen className="w-6 h-6" />,
MessageSquare: <MessageSquare className="w-6 h-6" />,
Heart: <Heart className="w-6 h-6" />,
Calendar: <Calendar className="w-6 h-6" />,
Smartphone: <Smartphone className="w-6 h-6" />,
};
interface DetailHeroProps {
solution: Solution;
}
function DetailHero({ solution }: DetailHeroProps) {
const industryIcon = INDUSTRY_ICONS[solution.industry] || <Factory className="w-8 h-8" />;
return (
<section className="relative min-h-[80vh] flex items-center overflow-hidden bg-white pt-24">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-20 sm:py-24 md:py-28 lg:py-32">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="flex items-center gap-4 mb-8"
>
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand">
{industryIcon}
</div>
<div>
<span className="px-3 py-1.5 text-xs font-bold text-brand bg-brand-soft/50">
{solution.industry}
</span>
<div className="text-text-muted text-sm mt-2">{solution.subtitle}</div>
</div>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.15, ease: EASE_OUT }}
className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-ink leading-tight tracking-tighter mb-6 sm:mb-8 max-w-4xl"
>
{solution.title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.3, ease: EASE_OUT }}
className="text-xl lg:text-2xl text-text-secondary max-w-3xl leading-relaxed mb-12"
>
{solution.description}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.45, 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>
</section>
);
}
interface ChallengesSectionProps {
challenges: string[];
}
function ChallengesSection({ challenges }: ChallengesSectionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-secondary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<div className="grid lg:grid-cols-2 gap-20 items-start">
<ScrollReveal>
<SectionLabel>Challenges</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight mb-8">
行业痛点
</h2>
<p className="text-text-secondary text-lg leading-relaxed">
我们深入理解行业面临的核心挑战,提供针对性的解决方案
</p>
</ScrollReveal>
<div className="space-y-px bg-border-primary">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{challenges.map((challenge, idx) => (
<div
key={idx}
className="group relative p-8 bg-white hover:bg-bg-secondary transition-all duration-500"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-brand scale-y-0 group-hover:scale-y-100 transition-transform duration-500 origin-top" />
<div className="flex items-start gap-6">
<div className="w-10 h-10 rounded-xl bg-brand-soft/50 flex items-center justify-center text-brand shrink-0 mt-0.5">
<span className="text-lg font-bold">{idx + 1}</span>
</div>
<p className="text-lg text-ink leading-relaxed">{challenge}</p>
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</div>
</section>
);
}
interface SolutionsSectionProps {
solutions: string[];
}
function SolutionsSection({ solutions }: SolutionsSectionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20 md:mb-20">
<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">
Solutions
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
解决方案
</h2>
</ScrollReveal>
<div className="grid md:grid-cols-2 gap-px bg-border-primary">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{solutions.map((solution, idx) => (
<div
key={idx}
className="group relative p-10 bg-white hover:bg-bg-secondary transition-all duration-500"
>
<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="flex items-start gap-6">
<div className="w-14 h-14 rounded-2xl bg-brand-soft flex items-center justify-center text-brand shrink-0">
<Lightbulb className="w-7 h-7" />
</div>
<div>
<div className="text-xs font-bold text-brand mb-3 tracking-widest uppercase">
方案 {idx + 1}
</div>
<p className="text-xl text-ink font-semibold leading-relaxed">{solution}</p>
</div>
</div>
</div>
))}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface ValuePropositionProps {
valueProposition: Solution['valueProposition'];
}
function ValuePropositionSection({ valueProposition }: ValuePropositionProps) {
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-secondary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20 md:mb-20">
<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">
Value Proposition
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
{valueProposition.headline}
</h2>
</ScrollReveal>
<div className="grid md:grid-cols-3 gap-px bg-border-primary">
<StaggerReveal staggerDelay={0.12} delayChildren={0.05}>
{valueProposition.points.map((point, idx) => {
const icon = VALUE_ICONS[point.icon] || <Settings className="w-6 h-6" />;
return (
<div
key={idx}
className="group relative p-10 text-center bg-white hover:bg-bg-secondary transition-all duration-500"
>
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-1/3 h-1 bg-brand scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-center" />
<div className="w-20 h-20 rounded-3xl bg-brand-soft flex items-center justify-center text-brand mx-auto mb-8">
{icon}
</div>
<h3 className="text-2xl font-bold text-ink mb-4">{point.title}</h3>
<p className="text-text-secondary leading-relaxed">{point.description}</p>
</div>
);
})}
</StaggerReveal>
</div>
</div>
</section>
);
}
interface SuiteCombinationProps {
suiteCombination: Solution['suiteCombination'];
products: Product[];
}
function SuiteCombinationSection({ suiteCombination, products }: SuiteCombinationProps) {
const primaryProducts = suiteCombination.primaryProducts
.map(pid => products.find(p => p.id === pid))
.filter(Boolean);
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-16 sm:mb-20 md:mb-20">
<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">
Suite Combination
</span>
<div className="w-14 h-px bg-brand" />
</div>
</SectionLabel>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight">
推荐产品组合
</h2>
</ScrollReveal>
<div className="grid lg:grid-cols-5 gap-px bg-border-primary mb-px">
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
{primaryProducts.map((product) => product && (
<div
key={product.id}
className="group relative p-8 bg-white hover:bg-bg-secondary transition-all duration-500 text-center lg:col-span-2"
>
<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="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand mx-auto mb-6">
<Package className="w-8 h-8" />
</div>
<div className="text-xs font-bold text-brand mb-2 tracking-widest uppercase">
核心产品
</div>
<h3 className="text-xl font-bold text-ink mb-3">{product.title}</h3>
<p className="text-sm text-text-muted leading-relaxed">{product.description}</p>
</div>
))}
</StaggerReveal>
<ScrollReveal delay={0.2} className="flex items-center justify-center lg:col-span-1 bg-white">
<div className="w-20 h-20 rounded-full bg-brand-soft/30 border-2 border-brand/30 flex items-center justify-center text-brand font-bold text-xl">
+
</div>
</ScrollReveal>
<ScrollReveal delay={0.3} className="lg:col-span-2">
<div className="group relative p-8 bg-brand-soft/10 hover:bg-bg-secondary transition-all duration-500 text-center h-full flex flex-col justify-center">
<div className="w-16 h-16 rounded-2xl bg-brand-soft flex items-center justify-center text-brand mx-auto mb-6">
<Users className="w-8 h-8" />
</div>
<div className="text-xs font-bold text-brand mb-2 tracking-widest uppercase">
配套服务
</div>
<h3 className="text-xl font-bold text-ink mb-3">专业实施服务</h3>
<p className="text-sm text-text-secondary leading-relaxed">
{suiteCombination.complementaryServices.length}项服务配套,确保方案落地见效
</p>
</div>
</ScrollReveal>
</div>
<ScrollReveal>
<div className="p-10 bg-bg-secondary text-center border border-border-primary">
<div className="text-[11px] tracking-[0.4em] uppercase font-bold text-brand mb-4">
方案价值
</div>
<p className="text-xl lg:text-2xl text-ink leading-relaxed max-w-4xl mx-auto font-medium">
{suiteCombination.rationale}
</p>
</div>
</ScrollReveal>
</div>
</section>
);
}
interface CTASectionProps {
solution: Solution;
}
function CTASection({ solution }: CTASectionProps) {
return (
<section className="relative py-36 lg:py-44 overflow-hidden bg-bg-secondary">
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-border-primary" />
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto">
<div className="flex justify-center mb-7">
<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>
</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-bold text-ink tracking-tight leading-tight mb-8">
开启您的<br />
<span className="text-brand">数字化转型之旅</span>
</h2>
<p className="text-xl text-text-secondary mb-12 max-w-2xl mx-auto leading-relaxed">
联系我们,获取{solution.title}的详细方案和专属报价
</p>
<div className="flex flex-wrap justify-center 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="/solutions">查看更多方案</a>
</Button>
</div>
</ScrollReveal>
</div>
</section>
);
}
interface SolutionDetailContentV3Props {
solution: Solution;
products?: Product[];
}
export default function SolutionDetailContentV3({ solution, products = [] }: SolutionDetailContentV3Props) {
return (
<main>
<DetailHero solution={solution} />
<ChallengesSection challenges={solution.challenges} />
<SolutionsSection solutions={solution.solutions} />
<ValuePropositionSection valueProposition={solution.valueProposition} />
<SuiteCombinationSection suiteCombination={solution.suiteCombination} products={products} />
<CTASection solution={solution} />
</main>
);
}