refactor(detail): 合并 L3 客户案例/资质认证区块为共享组件
- 新建 CaseStudiesSection / CertificationsSection 共享组件(src/components/detail), 合并 solution/product 逐字重复的本地实现与 service 的异类实现(卡片 motion.a 链 /cases/<slug> 依赖尚不存在数据 → 404,违反零编造) - bg-white → bg-bg-primary token 化(浅色零视觉差异,深色模式正确反色,对齐暗黑 Phase 2) - 数据闸门保留:caseStudies/certifications 空时 return null,由 L3EmptyFallback 接管 - grid/flex 类直接挂 StaggerReveal,修复合并前的单列布局 bug - 动效 0.8s → 0.7s(detail-cta-section + service CTASection),对齐 ≤700ms 约束 - 附 scripts/audit/l3-b2-verify.mjs 运行时冒烟(product 页浅深双版,0 报错,hydration 通过) 验证:type-check 0 错 / lint 0 错(127 既有 warnings) / 单测 129 套件 1628 通过
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
// B-2 运行时冒烟:确认合并后的共享 L3 组件(CaseStudiesSection /
|
||||
// CertificationsSection)接入详情页后不破坏页面、hydration 成功、无控制台报错。
|
||||
//
|
||||
// 关键约束(2026-09-02):当前所有 product/service/solution 的 caseStudies /
|
||||
// certifications 均为空数组(零编造前提,刻意留空),dataProofs 有填充。
|
||||
// 因此 product 页 L3 整段不渲染(L3EmptyFallback 因 dataProofs 非空而休眠,
|
||||
// CaseStudiesSection/CertificationsSection 因 caseStudies/certs 空而 return null)。
|
||||
// 本脚本不验证「填充态」渲染(无真实数据,禁止编造),只验证:
|
||||
// 1) 页面正常加载、hero 渲染(非空白)
|
||||
// 2) hydration 完成(framer-motion 元素 opacity 达到 1)
|
||||
// 3) 无未捕获控制台错误(合并 import 未破坏模块图)
|
||||
// 4) L3 区块按数据闸门正确缺席(无「客户案例」h2 = CaseStudiesSection 休眠)
|
||||
// 浅色 + 深色双版截图留证。
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||
|
||||
const BASE = 'http://localhost:3000';
|
||||
const OUT = 'dogfood-b2-verify';
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
|
||||
const browser = await chromium.launch({ args: ['--no-proxy-server'] });
|
||||
|
||||
async function check(path, theme) {
|
||||
const ctx = await browser.newContext();
|
||||
const page = await ctx.newPage();
|
||||
if (theme === 'dark') {
|
||||
await page.addInitScript(() => {
|
||||
try { localStorage.setItem('novalon-theme', 'dark'); } catch {}
|
||||
});
|
||||
}
|
||||
const errors = [];
|
||||
page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); });
|
||||
page.on('pageerror', (e) => errors.push('PAGEERROR: ' + e.message));
|
||||
|
||||
await page.goto(BASE + path, { waitUntil: 'networkidle', timeout: 30000 });
|
||||
|
||||
// hydration 探测:轮询 framer-motion 元素 opacity === '1'
|
||||
let hydrated = false;
|
||||
try {
|
||||
await page.waitForFunction(() => {
|
||||
const els = Array.from(document.querySelectorAll('div[style*="opacity"]'));
|
||||
return els.some((el) => getComputedStyle(el).opacity === '1');
|
||||
}, { timeout: 10000 });
|
||||
hydrated = true;
|
||||
} catch { hydrated = false; }
|
||||
|
||||
const heroText = await page.locator('h1').first().innerText().catch(() => '(no h1)');
|
||||
const hasCaseStudiesH2 = await page.locator("h2:has-text('客户案例')").count();
|
||||
const l3Signals = await page.locator("#l3-signals-heading").count();
|
||||
|
||||
const file = `${OUT}/erp-${theme}.png`;
|
||||
await page.screenshot({ path: file, fullPage: true });
|
||||
await ctx.close();
|
||||
|
||||
return {
|
||||
theme, path, hydrated, heroText: heroText.slice(0, 40),
|
||||
hasCaseStudiesH2, l3Signals, errors, file,
|
||||
};
|
||||
}
|
||||
|
||||
const results = [];
|
||||
results.push(await check('/products/erp', 'light'));
|
||||
results.push(await check('/products/erp', 'dark'));
|
||||
|
||||
await browser.close();
|
||||
|
||||
const report = { generatedAt: new Date().toISOString(), results };
|
||||
writeFileSync(`${OUT}/report.json`, JSON.stringify(report, null, 2));
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
@@ -4,11 +4,11 @@ import { motion } from 'framer-motion';
|
||||
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { CTAButton } from '@/components/ui/cta-button';
|
||||
import { ArrowRight, Package, Check, Settings, Users, FileText, Cpu, Shield, TrendingUp } from 'lucide-react';
|
||||
import { ArrowRight, Package, Check, Settings, Users, FileText, Cpu, TrendingUp } from 'lucide-react';
|
||||
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
|
||||
import type { Product } from '@/lib/constants/products';
|
||||
import { MethodologyFramework, TechStackShowcase, FAQSection, DataProofSection } from '@/components/content/sections';
|
||||
import { L3EmptyFallback } from '@/components/detail';
|
||||
import { L3EmptyFallback, CaseStudiesSection, CertificationsSection } from '@/components/detail';
|
||||
|
||||
const PRODUCT_ICONS: Record<string, React.ReactNode> = {
|
||||
'睿新ERP管理系统': <Package className="w-8 h-8" />,
|
||||
@@ -249,126 +249,9 @@ function ProcessSection({ process }: ProcessSectionProps) {
|
||||
);
|
||||
}
|
||||
|
||||
interface CaseStudiesSectionProps {
|
||||
caseStudies: Product['caseStudies'];
|
||||
}
|
||||
|
||||
function CaseStudiesSection({ caseStudies }: CaseStudiesSectionProps) {
|
||||
if (!caseStudies || caseStudies.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-primary">
|
||||
<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">
|
||||
客户案例
|
||||
</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 lg:grid-cols-3 gap-px bg-border-primary">
|
||||
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
|
||||
{caseStudies.map((study) => (
|
||||
<div
|
||||
key={study.client}
|
||||
className="group relative block overflow-hidden bg-bg-primary transition-all duration-500 hover:bg-bg-secondary"
|
||||
>
|
||||
<div className="aspect-[4/3] bg-gradient-to-br from-bg-secondary to-bg-tertiary relative overflow-hidden">
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
{/* 客户名首字作占位图形(无真实配图时),纯装饰 → aria-hidden 豁免对比度 */}
|
||||
<span aria-hidden="true" className="text-6xl font-bold text-text-muted/10">
|
||||
{study.client.charAt(0)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="absolute top-5 left-5">
|
||||
<span className="px-3 py-1.5 text-xs font-bold text-brand bg-brand-soft/80 backdrop-blur-sm">
|
||||
{study.industry}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-8">
|
||||
<h3 className="text-xl font-bold text-ink mb-3 group-hover:translate-x-2 transition-transform duration-500">
|
||||
{study.client}
|
||||
</h3>
|
||||
<p className="text-text-secondary text-sm leading-relaxed mb-6">
|
||||
{study.challenge}
|
||||
</p>
|
||||
<div className="pt-6 border-t border-border-primary">
|
||||
<div className="text-2xl font-bold text-brand mb-1">{study.result}</div>
|
||||
<div className="text-xs text-text-muted">核心成果</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</StaggerReveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
interface CertificationsSectionProps {
|
||||
certifications: Product['certifications'];
|
||||
}
|
||||
|
||||
function CertificationsSection({ certifications }: CertificationsSectionProps) {
|
||||
if (!certifications || certifications.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="relative py-16 sm:py-20 md:py-24 lg:py-32 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">
|
||||
<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">
|
||||
资质认证
|
||||
</span>
|
||||
<div className="w-14 h-px bg-brand" />
|
||||
</div>
|
||||
</SectionLabel>
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-ink tracking-tight">
|
||||
资质认证
|
||||
</h2>
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-px bg-border-primary">
|
||||
<StaggerReveal staggerDelay={0.08} delayChildren={0.05}>
|
||||
{certifications.map((cert, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="group flex items-center gap-4 px-8 py-5 bg-bg-primary hover:bg-bg-secondary transition-all duration-500"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-xl bg-brand-soft flex items-center justify-center text-brand">
|
||||
<Shield className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-ink font-bold">{cert.name}</div>
|
||||
<div className="text-sm text-text-muted">{cert.issuer}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</StaggerReveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
// ===== L3 信任层:客户案例 / 资质认证已合并至共享组件(@/components/detail)=====
|
||||
// 原本地 CaseStudiesSection / CertificationsSection 与 solution 页逐字重复 ~95%,
|
||||
// B-2 统一为共享组件(本页 h2 用默认「客户案例」,底色 token 化后深色模式正确反色)。
|
||||
|
||||
interface CTASectionProps {
|
||||
product: Product;
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { CTAButton } from '@/components/ui/cta-button';
|
||||
import { ArrowUpRight, CheckCircle2, Zap, Clock, Users, Award, TrendingUp, Shield } from 'lucide-react';
|
||||
import { CheckCircle2, Zap, Clock, Users, Award, TrendingUp, Shield } from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import type { Service, CaseStudy } from '@/lib/constants/services';
|
||||
import { L3EmptyFallback } from '@/components/detail';
|
||||
import type { Service } from '@/lib/constants/services';
|
||||
import { L3EmptyFallback, CaseStudiesSection } from '@/components/detail';
|
||||
|
||||
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
|
||||
|
||||
@@ -249,65 +249,9 @@ function ProcessSection({ service }: { service: Service }) {
|
||||
);
|
||||
}
|
||||
|
||||
function CaseStudiesSection({ caseStudies = [] }: { caseStudies?: CaseStudy[] }) {
|
||||
if (caseStudies.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id="cases" className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-primary">
|
||||
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-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 mb-16 sm:mb-20 md:mb-24"
|
||||
>
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
|
||||
真实案例,<br className="sm:hidden" />可衡量的成果
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-px bg-border-primary">
|
||||
{caseStudies.slice(0, 4).map((cs, i) => (
|
||||
<motion.a
|
||||
key={i}
|
||||
href={`/cases/${cs.client.toLowerCase().replace(/\s+/g, '-')}`}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-60px' }}
|
||||
transition={{ duration: 0.6, delay: i * 0.08, ease: EASE_OUT }}
|
||||
className="group relative block p-8 sm:p-10 transition-all duration-500 hover:bg-bg-secondary hover:shadow-lg hover:-translate-y-1 bg-bg-primary border border-transparent hover:border-border-primary"
|
||||
>
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
<span className="text-xs font-medium text-brand">
|
||||
{cs.industry}
|
||||
</span>
|
||||
<span className="text-border-primary">•</span>
|
||||
<span className="text-sm text-text-secondary">{cs.client}</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl sm:text-2xl font-bold mb-6 text-ink leading-tight group-hover:text-brand transition-colors duration-300">
|
||||
{cs.challenge.slice(0, 30)}
|
||||
</h3>
|
||||
|
||||
<p className="text-sm leading-relaxed mb-6 text-text-secondary">
|
||||
{cs.solution}
|
||||
</p>
|
||||
|
||||
<div className="inline-flex items-center gap-2 text-sm font-semibold text-text-secondary group-hover:text-brand transition-colors duration-300">
|
||||
<span>查看详情</span>
|
||||
<ArrowUpRight className="w-4 h-4 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-transform duration-300" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
// L3 信任层「客户案例」已合并至共享组件(@/components/detail)。
|
||||
// 原本地实现为异类:卡片 motion.a 链接 /cases/<slug>(依赖尚不存在的案例数据,链向 404
|
||||
// 违反零编造)+ 动效 0.8s 违规。统一后卡片不带链接,id="cases" 保留供 hero CTA 锚点。
|
||||
|
||||
function CTASection() {
|
||||
return (
|
||||
@@ -319,7 +263,7 @@ function CTASection() {
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-80px' }}
|
||||
transition={{ duration: 0.8, ease: EASE_OUT }}
|
||||
transition={{ duration: 0.7, 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">
|
||||
@@ -352,7 +296,7 @@ export default function ServiceDetailContentV4({ service }: { service: Service }
|
||||
<FeaturesSection service={service} />
|
||||
<ProcessSection service={service} />
|
||||
<L3EmptyFallback caseStudies={service.caseStudies} />
|
||||
<CaseStudiesSection caseStudies={service.caseStudies} />
|
||||
<CaseStudiesSection caseStudies={service.caseStudies} id="cases" heading="真实案例,可衡量的成果" />
|
||||
<CTASection />
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CTAButton } from '@/components/ui/cta-button';
|
||||
import { Factory, ShoppingCart, Heart, GraduationCap, Lightbulb, Settings, Users, TrendingUp, Package, BookOpen, MessageSquare, Calendar, Smartphone, Route, Shield, Truck } 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';
|
||||
import { L3EmptyFallback } from '@/components/detail';
|
||||
import { L3EmptyFallback, CaseStudiesSection, CertificationsSection } from '@/components/detail';
|
||||
|
||||
const INDUSTRY_ICONS: Record<string, React.ReactNode> = {
|
||||
制造业: <Factory className="w-8 h-8" />,
|
||||
@@ -338,128 +338,9 @@ interface CTASectionProps {
|
||||
solution: Solution;
|
||||
}
|
||||
|
||||
// ===== L3 信任层:客户案例(有数据才渲染,无数据不显示,不虚构)=====
|
||||
interface CaseStudiesSectionProps {
|
||||
caseStudies: Solution['caseStudies'];
|
||||
}
|
||||
|
||||
function CaseStudiesSection({ caseStudies }: CaseStudiesSectionProps) {
|
||||
if (!caseStudies || caseStudies.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-white">
|
||||
<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">
|
||||
客户案例
|
||||
</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 lg:grid-cols-3 gap-px bg-border-primary">
|
||||
<StaggerReveal staggerDelay={0.1} delayChildren={0.05}>
|
||||
{caseStudies.map((study) => (
|
||||
<div
|
||||
key={study.client}
|
||||
className="group relative block overflow-hidden bg-white transition-all duration-500 hover:bg-bg-secondary"
|
||||
>
|
||||
<div className="aspect-[4/3] bg-gradient-to-br from-bg-secondary to-bg-tertiary relative overflow-hidden">
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
{/* 客户名首字作占位图形(无真实配图时),纯装饰 → aria-hidden 豁免对比度 */}
|
||||
<span aria-hidden="true" className="text-6xl font-bold text-text-muted/10">
|
||||
{study.client.charAt(0)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="absolute top-5 left-5">
|
||||
<span className="px-3 py-1.5 text-xs font-bold text-brand bg-brand-soft/80 backdrop-blur-sm">
|
||||
{study.industry}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-8">
|
||||
<h3 className="text-xl font-bold text-ink mb-3 group-hover:translate-x-2 transition-transform duration-500">
|
||||
{study.client}
|
||||
</h3>
|
||||
<p className="text-text-secondary text-sm leading-relaxed mb-6">
|
||||
{study.challenge}
|
||||
</p>
|
||||
<div className="pt-6 border-t border-border-primary">
|
||||
<div className="text-2xl font-bold text-brand mb-1">{study.result}</div>
|
||||
<div className="text-xs text-text-muted">核心成果</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</StaggerReveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== L3 信任层:资质认证(有数据才渲染,无数据不显示)=====
|
||||
interface CertificationsSectionProps {
|
||||
certifications: Solution['certifications'];
|
||||
}
|
||||
|
||||
function CertificationsSection({ certifications }: CertificationsSectionProps) {
|
||||
if (!certifications || certifications.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="relative py-16 sm:py-20 md:py-24 lg:py-32 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">
|
||||
<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">
|
||||
资质认证
|
||||
</span>
|
||||
<div className="w-14 h-px bg-brand" />
|
||||
</div>
|
||||
</SectionLabel>
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-ink tracking-tight">
|
||||
资质认证
|
||||
</h2>
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-px bg-border-primary">
|
||||
<StaggerReveal staggerDelay={0.08} delayChildren={0.05}>
|
||||
{certifications.map((cert, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="group flex items-center gap-4 px-8 py-5 bg-white hover:bg-bg-secondary transition-all duration-500"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-xl bg-brand-soft flex items-center justify-center text-brand">
|
||||
<Shield className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-ink font-bold">{cert.name}</div>
|
||||
<div className="text-sm text-text-muted">{cert.issuer}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</StaggerReveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
// ===== L3 信任层:客户案例 / 资质认证已合并至共享组件(@/components/detail)=====
|
||||
// 原本地 CaseStudiesSection / CertificationsSection 与 product 页逐字重复 ~95%,
|
||||
// B-2(commit 见 git log)统一为共享组件;heading 由 prop 传入以保留原文案。
|
||||
|
||||
function CTASection({ solution }: CTASectionProps) {
|
||||
return (
|
||||
@@ -516,7 +397,7 @@ export default function SolutionDetailContentV3({ solution, products = [] }: Sol
|
||||
<ValuePropositionSection valueProposition={solution.valueProposition} />
|
||||
<SuiteCombinationSection suiteCombination={solution.suiteCombination} products={products} />
|
||||
<L3EmptyFallback caseStudies={solution.caseStudies} certifications={solution.certifications} />
|
||||
<CaseStudiesSection caseStudies={solution.caseStudies} />
|
||||
<CaseStudiesSection caseStudies={solution.caseStudies} heading="同行业落地案例" />
|
||||
<CertificationsSection certifications={solution.certifications} />
|
||||
<CTASection solution={solution} />
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* CaseStudiesSection — L3 信任层「客户案例」共享区块(B-2 合并自 3 份本地实现)
|
||||
*
|
||||
* 合并来源与差异消解(2026-09-02,修订方案 B' 第二步):
|
||||
* - solution-detail-content-v3 / product-detail-content-v3 的本地实现原本 ~95% 相同,
|
||||
* 仅 section 底色(bg-white vs bg-bg-primary)与 h2 文案不同 → 统一为 token 底色
|
||||
* (bg-bg-primary:浅色与 bg-white 视觉零差异,深色模式正确反色),
|
||||
* h2 文案改由 heading prop 传入(solution 传「同行业落地案例」,product 用默认「客户案例」)。
|
||||
* - service-detail-content-v4 的本地实现是异类(motion.a 链接 /cases/<slug>、无 SectionLabel、
|
||||
* 动效 0.8s 违规)→ 统一到本组件:卡片不带链接(/cases/<slug> 依赖尚不存在的案例数据,
|
||||
* 链向 404 违反零编造);如需锚点(service hero 的 #cases CTA)用 id prop。
|
||||
*
|
||||
* 布局要点:grid 类必须挂在 StaggerReveal 自身——它会给每个子项包一层 motion.div,
|
||||
* 放外层 div 时包装层成为 grid 唯一子项,卡片会堆成单列(commit 3091035 踩坑记录)。
|
||||
*
|
||||
* 数据闸门:caseStudies 为空时 return null(L3EmptyFallback 接管兜底渲染)。
|
||||
*/
|
||||
|
||||
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
|
||||
import { SectionLabel } from '@/components/ui/page-decoration';
|
||||
|
||||
/** 三处数据源(cases.ts / services.ts / products.ts)的结构公共子集 */
|
||||
export interface CaseStudyItem {
|
||||
client: string;
|
||||
industry: string;
|
||||
challenge: string;
|
||||
solution?: string;
|
||||
result: string;
|
||||
}
|
||||
|
||||
interface CaseStudiesSectionProps {
|
||||
caseStudies?: readonly CaseStudyItem[];
|
||||
/** h2 标题文案(solution 页传「同行业落地案例」,其余用默认) */
|
||||
heading?: string;
|
||||
/** 锚点 id(service 页 hero 的 #cases CTA 依赖) */
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export function CaseStudiesSection({
|
||||
caseStudies,
|
||||
heading = '客户案例',
|
||||
id,
|
||||
}: CaseStudiesSectionProps) {
|
||||
if (!caseStudies || caseStudies.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={id} className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-primary">
|
||||
<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">
|
||||
客户案例
|
||||
</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">
|
||||
{heading}
|
||||
</h2>
|
||||
</ScrollReveal>
|
||||
|
||||
{/* grid 直接挂 StaggerReveal(见文件头注释),gap-px 露出容器底色形成发丝分隔线 */}
|
||||
<StaggerReveal
|
||||
staggerDelay={0.1}
|
||||
delayChildren={0.05}
|
||||
className="grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-border-primary"
|
||||
>
|
||||
{caseStudies.map((study) => (
|
||||
<div
|
||||
key={study.client}
|
||||
className="group relative block h-full overflow-hidden bg-bg-primary transition-all duration-300 hover:bg-bg-secondary"
|
||||
>
|
||||
<div className="aspect-[4/3] bg-gradient-to-br from-bg-secondary to-bg-tertiary relative overflow-hidden">
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
{/* 客户名首字作占位图形(无真实配图时),纯装饰 → aria-hidden 豁免对比度 */}
|
||||
<span aria-hidden="true" className="text-6xl font-bold text-text-muted/10">
|
||||
{study.client.charAt(0)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="absolute top-5 left-5">
|
||||
<span className="px-3 py-1.5 text-xs font-bold text-brand bg-brand-soft/80 backdrop-blur-sm">
|
||||
{study.industry}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-8">
|
||||
<h3 className="text-xl font-bold text-ink mb-3 group-hover:translate-x-2 transition-transform duration-300">
|
||||
{study.client}
|
||||
</h3>
|
||||
<p className="text-text-secondary text-sm leading-relaxed mb-6">
|
||||
{study.challenge}
|
||||
</p>
|
||||
<div className="pt-6 border-t border-border-primary">
|
||||
<div className="text-2xl font-bold text-brand mb-1">{study.result}</div>
|
||||
<div className="text-xs text-text-muted">核心成果</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</StaggerReveal>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* CertificationsSection — L3 信任层「资质认证」共享区块(B-2 合并自 2 份本地实现)
|
||||
*
|
||||
* 合并来源(2026-09-02,修订方案 B' 第二步):
|
||||
* - solution-detail-content-v3 / product-detail-content-v3 的本地实现原本逐字相同,
|
||||
* 仅卡片底色(bg-white vs bg-bg-primary)不同 → 统一为 token(bg-bg-primary:
|
||||
* 浅色视觉零差异,深色模式正确反色,符合暗黑 Phase 2 token 化方向)。
|
||||
*
|
||||
* 数据闸门:certifications 为空时 return null(L3EmptyFallback 接管兜底渲染)。
|
||||
*/
|
||||
|
||||
import { Shield } from 'lucide-react';
|
||||
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
|
||||
import { SectionLabel } from '@/components/ui/page-decoration';
|
||||
|
||||
export interface CertificationItem {
|
||||
name: string;
|
||||
issuer: string;
|
||||
}
|
||||
|
||||
interface CertificationsSectionProps {
|
||||
certifications?: readonly CertificationItem[];
|
||||
}
|
||||
|
||||
export function CertificationsSection({ certifications }: CertificationsSectionProps) {
|
||||
if (!certifications || certifications.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="relative py-16 sm:py-20 md:py-24 lg:py-32 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">
|
||||
<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">
|
||||
资质认证
|
||||
</span>
|
||||
<div className="w-14 h-px bg-brand" />
|
||||
</div>
|
||||
</SectionLabel>
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-ink tracking-tight">
|
||||
资质认证
|
||||
</h2>
|
||||
</ScrollReveal>
|
||||
|
||||
{/* flex 直接挂 StaggerReveal(同 CaseStudiesSection 的布局要点) */}
|
||||
<StaggerReveal
|
||||
staggerDelay={0.08}
|
||||
delayChildren={0.05}
|
||||
className="flex flex-wrap justify-center gap-px bg-border-primary"
|
||||
>
|
||||
{certifications.map((cert, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="group flex items-center gap-4 px-8 py-5 bg-bg-primary hover:bg-bg-secondary transition-all duration-300"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-xl bg-brand-soft flex items-center justify-center text-brand">
|
||||
<Shield className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-ink font-bold">{cert.name}</div>
|
||||
<div className="text-sm text-text-muted">{cert.issuer}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</StaggerReveal>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export function DetailCTASection({
|
||||
initial={{ opacity: 0, y: 32 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.8, ease: [0.22, 1, 0.36, 1] }}
|
||||
transition={{ duration: 0.7, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="max-w-3xl"
|
||||
>
|
||||
{/* 标签 */}
|
||||
|
||||
@@ -13,6 +13,10 @@ export { DetailCTASection } from './detail-cta-section';
|
||||
export { CaseStudyCard } from './detail-case-study';
|
||||
export { CertificationList } from './detail-certification';
|
||||
|
||||
// L3 信任层共享区块(B-2 合并自 solution/product/service 详情页的本地重复实现)
|
||||
export { CaseStudiesSection } from './detail-case-studies-section';
|
||||
export { CertificationsSection } from './detail-certifications-section';
|
||||
|
||||
export { SolutionValueSection } from './solution-value';
|
||||
export { ServiceValueSection } from './service-value';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user