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,52 @@
'use client';
import { notFound } from 'next/navigation';
import CaseDetailPage from '@/components/sections/case-detail-page';
import { ScrollProgress } from '@/components/ui/scroll-progress';
import type { ContentItem } from '@/lib/cms/types';
import type { CaseStudyData } from '@/lib/cms/mock-data';
interface CaseDetailClientProps {
item: ContentItem;
}
/**
* 案例详情客户端组件(CMS 化版本)
* 数据源:CMS ContentItem(由 server 端从 mock-data.ts 同步预取)
* 类型系统:使用 CMS 类型 ContentItem,未来切换真实 API 时无需改业务代码
*/
export function CaseDetailClient({ item }: CaseDetailClientProps) {
if (!item) {
notFound();
}
const data = item.data as unknown as CaseStudyData;
const detailData = {
id: item.id,
client: data.client,
industry: data.industry,
title: item.title,
subtitle: data.subtitle,
challenge: data.challenge,
solution: data.solution,
result: data.result,
metrics: data.metrics,
timeline: data.timeline,
services: data.services,
testimonial: data.testimonial
? {
quote: data.testimonial.quote,
author: data.testimonial.author,
role: `${data.testimonial.role},${data.testimonial.company}`,
}
: undefined,
};
return (
<>
<ScrollProgress />
<CaseDetailPage data={detailData} />
</>
);
}
+40
View File
@@ -0,0 +1,40 @@
import { notFound } from 'next/navigation';
import { CaseDetailClient } from './client';
import { getPublishedItemBySlug, getAllPublishedSlugs } from '@/lib/cms/data-server';
import { COMPANY_INFO } from '@/lib/constants/company';
import type { CaseStudyData } from '@/lib/cms/mock-data';
import type { Metadata } from 'next';
interface CaseDetailPageProps {
params: Promise<{ slug: string }>;
}
export async function generateStaticParams() {
const slugs = await getAllPublishedSlugs('case-study');
return slugs.map((s) => ({ slug: s.slug }));
}
export async function generateMetadata({ params }: CaseDetailPageProps): Promise<Metadata> {
const { slug } = await params;
const item = await getPublishedItemBySlug('case-study', slug);
if (!item) {
return { title: `案例未找到 - ${COMPANY_INFO.displayName}` };
}
const data = item.data as unknown as CaseStudyData;
return {
title: `${item.title} - ${data.industry}案例 - ${COMPANY_INFO.displayName}`,
description: `${data.client}数字化转型案例:${data.challenge.substring(0, 80)}... ${data.metrics?.map((m: { value: string; label: string }) => `${m.value} ${m.label}`).join(',')}`,
};
}
export default async function CaseDetailPage({ params }: CaseDetailPageProps) {
const { slug } = await params;
const item = await getPublishedItemBySlug('case-study', slug);
if (!item) { notFound(); }
return <CaseDetailClient item={item} />;
}
@@ -0,0 +1,320 @@
'use client';
import { useState, useMemo } from 'react';
import {
Building2, ArrowRight, TrendingUp, Target, Lightbulb,
Filter,
} from 'lucide-react';
import { ScrollReveal } from '@/components/ui/scroll-reveal';
import { Badge } from '@/components/ui/badge';
import { MetricCard } from '@/components/ui/metric-card';
import {
CASE_INDUSTRIES,
getCasesByIndustry,
type CaseStudy,
} from '@/lib/constants/cases';
import {
GrainOverlay,
FloatingInkParticles,
SectionLabel,
} from '@/components/ui/page-decoration';
import { cn } from '@/lib/utils';
const colorMap: Record<CaseStudy['color'], string> = {
brand: 'bg-brand text-brand',
blue: 'bg-accent-blue text-accent-blue',
teal: 'bg-accent-teal text-accent-teal',
amber: 'bg-accent-amber text-accent-amber',
purple: 'bg-accent-purple text-accent-purple',
};
function CaseCard({ caseItem }: { caseItem: CaseStudy; index?: number }) {
const colorStyle = colorMap[caseItem.color];
const [bgClass, textClass] = colorStyle.split(' ');
return (
<a
href={`/cases/${caseItem.slug}`}
className="group relative block overflow-hidden border border-white/[0.08] hover:border-white/20 bg-ink transition-all duration-500 hover:-translate-y-1 flex flex-col h-full"
>
<div className={cn(
'absolute top-0 left-0 right-0 h-1 scale-x-0 group-hover:scale-x-100 transition-transform duration-700 origin-left',
bgClass
)} />
<div className="p-6 sm:p-7 lg:p-8 flex flex-col flex-1">
<div className="flex items-center gap-3 mb-5">
<Badge variant="outline" className={cn('border-transparent', bgClass, 'text-white')}>
{caseItem.industry}
</Badge>
<span className="text-[10px] tracking-[0.25em] uppercase text-white/30 font-semibold">
{caseItem.companySize}
</span>
</div>
<h3 className="text-lg sm:text-xl font-bold text-white mb-4 leading-tight group-hover:text-brand transition-colors duration-300">
{caseItem.title}
</h3>
<div className="space-y-4 mb-6 flex-1">
<div>
<div className="flex items-center gap-2 mb-2">
<Target className={cn('w-3.5 h-3.5', textClass)} />
<span className={cn('text-[11px] tracking-wide uppercase font-bold', textClass)}>挑战</span>
</div>
<p className="text-sm text-white/50 leading-relaxed line-clamp-2 pl-5.5">
{caseItem.challenge}
</p>
</div>
<div>
<div className="flex items-center gap-2 mb-2">
<Lightbulb className={cn('w-3.5 h-3.5', textClass)} />
<span className={cn('text-[11px] tracking-wide uppercase font-bold', textClass)}>方案</span>
</div>
<p className="text-sm text-white/50 leading-relaxed line-clamp-2 pl-5.5">
{caseItem.solution}
</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 pt-5 border-t border-white/[0.08] mb-5">
{caseItem.metrics.slice(0, 2).map((m, i) => (
<div key={i}>
<div className="text-2xl sm:text-3xl font-bold text-white mb-1 tabular-nums">
{m.value}
</div>
<div className="text-xs text-white/40">{m.label}</div>
</div>
))}
</div>
<div className="flex items-center justify-between pt-4 mt-auto border-t border-white/[0.06]">
<div className="flex items-center gap-2">
<Building2 className="w-4 h-4 text-white/30" />
<span className="text-xs text-white/40">{caseItem.client}</span>
</div>
<div className="flex items-center gap-1.5 text-brand text-sm font-medium">
查看详情
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform duration-300" />
</div>
</div>
</div>
</a>
);
}
const PAGE_METRICS = [
{
icon: <Building2 className="w-5 h-5" />,
label: '服务客户',
value: 500,
suffix: '+',
description: '覆盖六大行业,500+企业客户的共同选择',
trend: { value: '23% YoY', direction: 'up' as const, label: '增长' },
accentColor: 'brand' as const,
},
{
icon: <TrendingUp className="w-5 h-5" />,
label: '成功率',
value: 98,
suffix: '%',
description: '项目一次性上线成功率,持续稳定交付',
trend: { value: '97%续约率', direction: 'up' as const, label: '' },
accentColor: 'teal' as const,
},
{
icon: <Target className="w-5 h-5" />,
label: '平均效率提升',
value: 35,
suffix: '%',
description: '客户核心业务流程效率平均提升幅度',
trend: { value: '基于 200+ 案例', direction: 'neutral' as const, label: '' },
accentColor: 'blue' as const,
},
{
icon: <Lightbulb className="w-5 h-5" />,
label: '行业覆盖',
value: 6,
suffix: '大',
description: '制造、零售、医疗、教育、金融、物流六大行业',
trend: { value: '持续扩展中', direction: 'up' as const, label: '' },
accentColor: 'amber' as const,
},
];
export default function CasesContentV1() {
const [selectedIndustry, setSelectedIndustry] = useState('all');
const filteredCases = useMemo(() => {
return getCasesByIndustry(selectedIndustry);
}, [selectedIndustry]);
return (
<div className="min-h-screen bg-ink text-white overflow-x-hidden" data-theme="dark">
{/* Hero Section */}
<section className="relative min-h-[70vh] flex items-center overflow-hidden bg-ink">
<GrainOverlay />
<FloatingInkParticles />
<div className="absolute inset-0 bg-gradient-to-b from-ink-light/30 via-transparent to-ink pointer-events-none" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-40">
<div className="max-w-4xl">
<ScrollReveal>
<SectionLabel>Case Studies</SectionLabel>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold leading-[0.95] tracking-tighter mb-8 sm:mb-10">
真实案例,
<span className="block mt-3 text-brand">可衡量的成果</span>
</h1>
</ScrollReveal>
<ScrollReveal delay={0.2}>
<p className="text-lg md:text-xl text-white/60 max-w-2xl leading-relaxed mb-10">
每一个案例,都是我们与客户共同创造的价值故事。从挑战到方案,从过程到成果——用数据说话,用结果证明。
</p>
</ScrollReveal>
<ScrollReveal delay={0.3}>
<div className="flex flex-wrap gap-3">
<Badge variant="outline" className="border-brand/30 text-brand px-4 py-1.5 text-xs">
500+ 企业客户
</Badge>
<Badge variant="outline" className="border-white/20 text-white/70 px-4 py-1.5 text-xs">
98% 项目成功率
</Badge>
<Badge variant="outline" className="border-white/20 text-white/70 px-4 py-1.5 text-xs">
6 大行业覆盖
</Badge>
<Badge variant="outline" className="border-white/20 text-white/70 px-4 py-1.5 text-xs">
97% 客户续约率
</Badge>
</div>
</ScrollReveal>
</div>
</div>
</section>
{/* Key Metrics Section */}
<section className="relative py-16 sm:py-20 md:py-24 overflow-hidden bg-ink-light">
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6">
{PAGE_METRICS.map((metric, i) => (
<ScrollReveal key={metric.label} delay={i * 0.08}>
<div className="h-full">
<MetricCard
icon={metric.icon}
label={metric.label}
value={metric.value}
suffix={metric.suffix}
description={metric.description}
trend={metric.trend}
accentColor={metric.accentColor}
theme="dark"
className="h-full"
/>
</div>
</ScrollReveal>
))}
</div>
</div>
</section>
{/* Cases List Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-ink">
<GrainOverlay />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-8 mb-12 sm:mb-16 md:mb-20">
<ScrollReveal className="lg:max-w-2xl">
<SectionLabel>All Cases</SectionLabel>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight mt-4">
全部案例
</h2>
<p className="mt-6 text-lg text-white/60 leading-relaxed">
按行业筛选,找到与您业务最相关的成功故事
</p>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<div className="flex items-center gap-2 flex-wrap">
<Filter className="w-4 h-4 text-white/40" />
{CASE_INDUSTRIES.map((industry) => (
<button
key={industry.id}
onClick={() => setSelectedIndustry(industry.id)}
className={cn(
'px-4 py-2 text-xs font-medium transition-all duration-300',
selectedIndustry === industry.id
? 'bg-brand text-white'
: 'bg-white/[0.05] text-white/60 hover:bg-white/[0.1] hover:text-white'
)}
>
{industry.label}
</button>
))}
</div>
</ScrollReveal>
</div>
{filteredCases.length > 0 ? (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
{filteredCases.map((caseItem, i) => (
<ScrollReveal key={caseItem.id} delay={i * 0.08}>
<div className="h-full">
<CaseCard caseItem={caseItem} index={i} />
</div>
</ScrollReveal>
))}
</div>
) : (
<div className="text-center py-20">
<p className="text-white/50 text-lg">暂无该行业的案例</p>
</div>
)}
</div>
</section>
{/* CTA Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-ink-light">
<GrainOverlay />
<FloatingInkParticles />
<div className="absolute inset-0 bg-gradient-to-b from-ink-light/50 to-ink pointer-events-none" />
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<ScrollReveal className="text-center max-w-3xl mx-auto">
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-tight">
您的成功故事,
<span className="text-brand">从这里开始</span>
</h2>
<p className="mt-6 text-lg text-white/60 leading-relaxed">
无论您处于数字化转型的哪个阶段,我们都有相应的经验和能力帮助您。
让我们聊聊您的挑战,看看能创造什么样的成果。
</p>
<div className="mt-12 flex flex-col sm:flex-row justify-center gap-4 sm:gap-6">
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
>
预约咨询
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
</a>
<a
href="/services"
className="inline-flex items-center justify-center gap-3 px-12 py-6 font-semibold text-base text-white border border-white/20 hover:border-white/40 hover:bg-white/[0.03] transition-all duration-300 min-h-[52px] touch-manipulation"
>
了解服务
</a>
</div>
</ScrollReveal>
</div>
</section>
</div>
);
}
@@ -0,0 +1,316 @@
'use client';
import { useState, useMemo } from 'react';
import { motion } from 'framer-motion';
import { Building2, ArrowUpRight, Target, Lightbulb, Filter } from 'lucide-react';
import {
CASE_INDUSTRIES,
getCasesByIndustry,
type CaseStudy,
} from '@/lib/constants/cases';
import { cn } from '@/lib/utils';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
const PAGE_METRICS = [
{ value: '500+', label: '服务客户', sub: '覆盖六大行业' },
{ value: '98%', label: '项目成功率', sub: '持续稳定交付' },
{ value: '35%', label: '平均效率提升', sub: '核心业务流程' },
{ value: '6大', label: '行业覆盖', sub: '制造零售医疗等' },
];
function CaseCard({ caseItem, index }: { caseItem: CaseStudy; index: number }) {
return (
<motion.a
href={`/cases/${caseItem.slug}`}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{ duration: 0.6, delay: index * 0.06, ease: EASE_OUT }}
className="group relative block overflow-hidden border border-border-primary hover:border-border-secondary bg-white hover:bg-bg-secondary transition-all duration-500 flex flex-col h-full"
>
<div className="absolute top-0 left-0 h-0 w-[2px] group-hover:h-full transition-all duration-500 origin-top bg-brand" />
<div className="p-6 sm:p-7 lg:p-8 flex flex-col flex-1 relative z-10">
<div className="flex items-center gap-3 mb-5">
<span className="inline-block px-3 py-1 text-[11px] font-bold text-brand border border-brand/20 bg-brand/[0.05]">
{caseItem.industry}
</span>
<span className="text-[10px] tracking-[0.25em] uppercase text-text-muted font-semibold">
{caseItem.companySize}
</span>
</div>
<h3 className="text-lg sm:text-xl font-bold text-ink mb-4 leading-tight group-hover:text-brand transition-colors duration-300">
{caseItem.title}
</h3>
<div className="space-y-4 mb-6 flex-1">
<div>
<div className="flex items-center gap-2 mb-2">
<Target className="w-3.5 h-3.5 text-brand" />
<span className="text-[11px] tracking-wide uppercase font-bold text-brand">挑战</span>
</div>
<p className="text-sm text-text-secondary leading-relaxed line-clamp-2 pl-5.5">
{caseItem.challenge}
</p>
</div>
<div>
<div className="flex items-center gap-2 mb-2">
<Lightbulb className="w-3.5 h-3.5 text-text-muted" />
<span className="text-[11px] tracking-wide uppercase font-bold text-text-secondary">方案</span>
</div>
<p className="text-sm text-text-secondary leading-relaxed line-clamp-2 pl-5.5">
{caseItem.solution}
</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 pt-5 border-t border-border-primary mb-5">
{caseItem.metrics.slice(0, 2).map((m, i) => (
<div key={i}>
<div className={`text-2xl sm:text-3xl font-black mb-1 tabular-nums tracking-tight ${
i === 0 ? 'text-brand' : 'text-ink'
}`}>
{m.value}
</div>
<div className="text-xs text-text-muted">{m.label}</div>
</div>
))}
</div>
<div className="flex items-center justify-between pt-4 mt-auto border-t border-border-primary">
<div className="flex items-center gap-2">
<Building2 className="w-4 h-4 text-text-muted" />
<span className="text-xs text-text-muted">{caseItem.client}</span>
</div>
<div className="flex items-center gap-1.5 text-brand text-sm font-semibold">
查看详情
<ArrowUpRight className="w-4 h-4 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-transform duration-300" />
</div>
</div>
</div>
</motion.a>
);
}
export default function CasesContentV2({
cases: casesProp,
industries: industriesProp,
}: {
cases?: CaseStudy[];
industries?: { id: string; label: string }[];
}) {
const [selectedIndustry, setSelectedIndustry] = useState('all');
const industries = industriesProp ?? CASE_INDUSTRIES;
const filteredCases = useMemo(() => {
if (casesProp) return casesProp;
return getCasesByIndustry(selectedIndustry);
}, [selectedIndustry, casesProp]);
return (
<div className="min-h-screen bg-white text-ink overflow-x-hidden">
{/* Hero Section */}
<section className="relative min-h-[70vh] flex items-center overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-40">
<div className="max-w-4xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.1, ease: EASE_OUT }}
className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-8"
>
Case Studies
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.2, ease: EASE_OUT }}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-black text-ink leading-[0.88] tracking-tightest mb-10 sm:mb-12 md:mb-16"
>
<div className="block">真实案例,</div>
<div className="block mt-4 sm:mt-6">
<span className="relative inline-block">
<span className="relative text-brand font-black">
可衡量的成果
<motion.span
className="absolute -bottom-2 left-0 w-full h-[3px] bg-brand"
style={{ transformOrigin: 'left' }}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.8, delay: 1.2, ease: EASE_OUT }}
/>
</span>
</span>
</div>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5, ease: EASE_OUT }}
className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12"
>
每一个案例,都是我们与客户共同创造的价值故事。
<br className="hidden sm:block" />
从挑战到方案,从过程到成果——用数据说话,用结果证明。
</motion.p>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.7, ease: EASE_OUT }}
className="flex flex-wrap gap-3"
>
<span className="px-4 py-2 text-xs font-bold text-brand border border-brand/20 bg-brand/[0.05]">
500+ 企业客户
</span>
<span className="px-4 py-2 text-xs font-medium text-text-secondary border border-border-primary">
98% 项目成功率
</span>
<span className="px-4 py-2 text-xs font-medium text-text-secondary border border-border-primary">
6 大行业覆盖
</span>
<span className="px-4 py-2 text-xs font-medium text-text-secondary border border-border-primary">
97% 客户续约率
</span>
</motion.div>
</div>
</div>
</section>
{/* Metrics Strip */}
<section className="relative py-16 sm:py-20 md:py-24 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 relative z-10">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8 md:gap-12">
{PAGE_METRICS.map((metric, i) => (
<motion.div
key={metric.label}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.6, delay: i * 0.08, ease: EASE_OUT }}
className="text-center lg:text-left"
>
<div className={`text-3xl sm:text-4xl md:text-5xl font-black tracking-tight mb-2 ${
i === 0 ? 'text-brand' : 'text-ink'
}`}>
{metric.value}
</div>
<div className="text-sm font-medium text-text-secondary mb-1">{metric.label}</div>
<div className="text-xs text-text-muted">{metric.sub}</div>
</motion.div>
))}
</div>
</div>
</section>
{/* Cases List Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-8 mb-12 sm:mb-16 md:mb-20">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="lg:max-w-2xl"
>
<div className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-6">
All Cases
</div>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
全部案例
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
按行业筛选,找到与您业务最相关的成功故事
</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, delay: 0.15, ease: EASE_OUT }}
>
<div className="flex items-center gap-2 flex-wrap">
<Filter className="w-4 h-4 text-text-muted" />
{industries.map((industry) => (
<button
key={industry.id}
onClick={() => setSelectedIndustry(industry.id)}
className={cn(
'px-4 py-2 text-xs font-medium transition-all duration-300',
selectedIndustry === industry.id
? 'bg-brand text-white'
: 'bg-bg-secondary text-text-secondary hover:bg-bg-tertiary hover:text-ink'
)}
>
{industry.label}
</button>
))}
</div>
</motion.div>
</div>
{filteredCases.length > 0 ? (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-border-primary">
{filteredCases.map((caseItem, i) => (
<CaseCard key={caseItem.id} caseItem={caseItem} index={i} />
))}
</div>
) : (
<div className="text-center py-20">
<p className="text-text-secondary text-lg">暂无该行业的案例</p>
</div>
)}
</div>
</section>
{/* CTA Section */}
<section className="relative py-20 sm:py-28 md: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="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="max-w-3xl"
>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95] mb-8">
您的成功故事,<br className="sm:hidden" />从这里开始
</h2>
<p className="text-lg text-text-secondary leading-relaxed mb-10">
无论您处于数字化转型的哪个阶段,我们都有相应的经验和能力帮助您。
<br className="hidden sm:block" />
让我们聊聊您的挑战,看看能创造什么样的成果。
</p>
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
>
预约咨询
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</a>
<a
href="/services"
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-border-secondary hover:bg-white transition-all duration-300 min-h-[52px] touch-manipulation"
>
了解服务
</a>
</div>
</motion.div>
</div>
</section>
</div>
);
}
@@ -0,0 +1,315 @@
'use client';
import { useState, useMemo } from 'react';
import { motion } from 'framer-motion';
import { Building2, ArrowUpRight, Target, Lightbulb, Filter } from 'lucide-react';
import {
type CaseStudy,
} from '@/lib/constants/cases';
import { cn } from '@/lib/utils';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
const PAGE_METRICS = [
{ value: '500+', label: '服务客户', sub: '覆盖六大行业' },
{ value: '98%', label: '项目成功率', sub: '持续稳定交付' },
{ value: '35%', label: '平均效率提升', sub: '核心业务流程' },
{ value: '6大', label: '行业覆盖', sub: '制造零售医疗等' },
];
function CaseCard({ caseItem, index }: { caseItem: CaseStudy; index: number }) {
return (
<motion.a
href={`/cases/${caseItem.slug}`}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{ duration: 0.6, delay: index * 0.06, ease: EASE_OUT }}
className="group relative block overflow-hidden border border-border-primary hover:border-border-secondary bg-white hover:bg-bg-secondary transition-all duration-500 flex flex-col h-full"
>
<div className="absolute top-0 left-0 h-0 w-[2px] group-hover:h-full transition-all duration-500 origin-top bg-brand" />
<div className="p-6 sm:p-7 lg:p-8 flex flex-col flex-1 relative z-10">
<div className="flex items-center gap-3 mb-5">
<span className="inline-block px-3 py-1 text-[11px] font-bold text-brand border border-brand/20 bg-brand/[0.05]">
{caseItem.industry}
</span>
<span className="text-[10px] tracking-[0.25em] uppercase text-text-muted font-semibold">
{caseItem.companySize}
</span>
</div>
<h3 className="text-lg sm:text-xl font-bold text-ink mb-4 leading-tight group-hover:text-brand transition-colors duration-300">
{caseItem.title}
</h3>
<div className="space-y-4 mb-6 flex-1">
<div>
<div className="flex items-center gap-2 mb-2">
<Target className="w-3.5 h-3.5 text-brand" />
<span className="text-[11px] tracking-wide uppercase font-bold text-brand">挑战</span>
</div>
<p className="text-sm text-text-secondary leading-relaxed line-clamp-2 pl-5.5">
{caseItem.challenge}
</p>
</div>
<div>
<div className="flex items-center gap-2 mb-2">
<Lightbulb className="w-3.5 h-3.5 text-text-muted" />
<span className="text-[11px] tracking-wide uppercase font-bold text-text-secondary">方案</span>
</div>
<p className="text-sm text-text-secondary leading-relaxed line-clamp-2 pl-5.5">
{caseItem.solution}
</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 pt-5 border-t border-border-primary mb-5">
{caseItem.metrics.slice(0, 2).map((m, i) => (
<div key={i}>
<div className={`text-2xl sm:text-3xl font-black mb-1 tabular-nums tracking-tight ${
i === 0 ? 'text-brand' : 'text-ink'
}`}>
{m.value}
</div>
<div className="text-xs text-text-muted">{m.label}</div>
</div>
))}
</div>
<div className="flex items-center justify-between pt-4 mt-auto border-t border-border-primary">
<div className="flex items-center gap-2">
<Building2 className="w-4 h-4 text-text-muted" />
<span className="text-xs text-text-muted">{caseItem.client}</span>
</div>
<div className="flex items-center gap-1.5 text-brand text-sm font-semibold">
查看详情
<ArrowUpRight className="w-4 h-4 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-transform duration-300" />
</div>
</div>
</div>
</motion.a>
);
}
export default function CasesContentV3({
cases: casesProp,
industries: industriesProp,
}: {
cases?: CaseStudy[];
industries?: { id: string; label: string }[];
}) {
const [selectedIndustry, setSelectedIndustry] = useState('all');
const industries = industriesProp ?? [];
const filteredCases = useMemo(() => {
const allCases = casesProp ?? [];
if (selectedIndustry === 'all') return allCases;
return allCases.filter(c => c.industry === selectedIndustry);
}, [selectedIndustry, casesProp]);
return (
<div className="min-h-screen bg-white text-ink overflow-x-hidden">
{/* Hero Section */}
<section className="relative min-h-[70vh] flex items-center overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-40">
<div className="max-w-4xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.1, ease: EASE_OUT }}
className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-8"
>
Case Studies
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.2, ease: EASE_OUT }}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-black text-ink leading-[0.88] tracking-tightest mb-10 sm:mb-12 md:mb-16"
>
<div className="block">真实案例,</div>
<div className="block mt-4 sm:mt-6">
<span className="relative inline-block">
<span className="relative text-brand font-black">
可衡量的成果
<motion.span
className="absolute -bottom-2 left-0 w-full h-[3px] bg-brand"
style={{ transformOrigin: 'left' }}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.8, delay: 1.2, ease: EASE_OUT }}
/>
</span>
</span>
</div>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5, ease: EASE_OUT }}
className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12"
>
每一个案例,都是我们与客户共同创造的价值故事。
<br className="hidden sm:block" />
从挑战到方案,从过程到成果——用数据说话,用结果证明。
</motion.p>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.7, ease: EASE_OUT }}
className="flex flex-wrap gap-3"
>
<span className="px-4 py-2 text-xs font-bold text-brand border border-brand/20 bg-brand/[0.05]">
500+ 企业客户
</span>
<span className="px-4 py-2 text-xs font-medium text-text-secondary border border-border-primary">
98% 项目成功率
</span>
<span className="px-4 py-2 text-xs font-medium text-text-secondary border border-border-primary">
6 大行业覆盖
</span>
<span className="px-4 py-2 text-xs font-medium text-text-secondary border border-border-primary">
97% 客户续约率
</span>
</motion.div>
</div>
</div>
</section>
{/* Metrics Strip */}
<section className="relative py-16 sm:py-20 md:py-24 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 relative z-10">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8 md:gap-12">
{PAGE_METRICS.map((metric, i) => (
<motion.div
key={metric.label}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.6, delay: i * 0.08, ease: EASE_OUT }}
className="text-center lg:text-left"
>
<div className={`text-3xl sm:text-4xl md:text-5xl font-black tracking-tight mb-2 ${
i === 0 ? 'text-brand' : 'text-ink'
}`}>
{metric.value}
</div>
<div className="text-sm font-medium text-text-secondary mb-1">{metric.label}</div>
<div className="text-xs text-text-muted">{metric.sub}</div>
</motion.div>
))}
</div>
</div>
</section>
{/* Cases List Section */}
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-8 mb-12 sm:mb-16 md:mb-20">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="lg:max-w-2xl"
>
<div className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-6">
All Cases
</div>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
全部案例
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
按行业筛选,找到与您业务最相关的成功故事
</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, delay: 0.15, ease: EASE_OUT }}
>
<div className="flex items-center gap-2 flex-wrap">
<Filter className="w-4 h-4 text-text-muted" />
{industries.map((industry) => (
<button
key={industry.id}
onClick={() => setSelectedIndustry(industry.id)}
className={cn(
'px-4 py-2 text-xs font-medium transition-all duration-300',
selectedIndustry === industry.id
? 'bg-brand text-white'
: 'bg-bg-secondary text-text-secondary hover:bg-bg-tertiary hover:text-ink'
)}
>
{industry.label}
</button>
))}
</div>
</motion.div>
</div>
{filteredCases.length > 0 ? (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-border-primary">
{filteredCases.map((caseItem, i) => (
<CaseCard key={caseItem.id} caseItem={caseItem} index={i} />
))}
</div>
) : (
<div className="text-center py-20">
<p className="text-text-secondary text-lg">暂无该行业的案例</p>
</div>
)}
</div>
</section>
{/* CTA Section */}
<section className="relative py-20 sm:py-28 md: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="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.8, ease: EASE_OUT }}
className="max-w-3xl"
>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95] mb-8">
您的成功故事,<br className="sm:hidden" />从这里开始
</h2>
<p className="text-lg text-text-secondary leading-relaxed mb-10">
无论您处于数字化转型的哪个阶段,我们都有相应的经验和能力帮助您。
<br className="hidden sm:block" />
让我们聊聊您的挑战,看看能创造什么样的成果。
</p>
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
<a
href="/contact"
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
>
预约咨询
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</a>
<a
href="/services"
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-border-secondary hover:bg-white transition-all duration-300 min-h-[52px] touch-manipulation"
>
了解服务
</a>
</div>
</motion.div>
</div>
</section>
</div>
);
}
+8
View File
@@ -0,0 +1,8 @@
'use client';
import type { CaseStudy } from '@/lib/constants/cases';
import CasesContentV3 from './cases-content-v3';
export function CasesClient({ cases }: { cases?: CaseStudy[] }) {
return <CasesContentV3 cases={cases} />;
}
+16
View File
@@ -0,0 +1,16 @@
import { Metadata } from 'next';
import { COMPANY_INFO } from '@/lib/constants';
import { getPublishedItems } from '@/lib/cms/data-server';
import type { CaseStudy } from '@/lib/constants/cases';
import { CasesClient } from './client';
export const metadata: Metadata = {
title: `客户案例 - ${COMPANY_INFO.displayName}`,
description: `500+企业客户的成功案例,覆盖制造、零售、医疗、教育、金融、物流六大行业。真实数据,可衡量的成果——见证数字化转型的价值。`,
};
export default async function CasesPage() {
const items = await getPublishedItems('case-study');
const cases = items.map((item) => ({ ...item.data, title: item.data.title || item.title } as unknown as CaseStudy));
return <CasesClient cases={cases} />;
}