From 10160fb7f2dd08a2915202f4491c3e217196798b Mon Sep 17 00:00:00 2001 From: zhangxiang Date: Wed, 2 Sep 2026 12:51:03 +0800 Subject: [PATCH] =?UTF-8?q?refactor(detail):=20=E5=90=88=E5=B9=B6=20L3=20?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E6=A1=88=E4=BE=8B/=E8=B5=84=E8=B4=A8?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E5=8C=BA=E5=9D=97=E4=B8=BA=E5=85=B1=E4=BA=AB?= =?UTF-8?q?=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新建 CaseStudiesSection / CertificationsSection 共享组件(src/components/detail), 合并 solution/product 逐字重复的本地实现与 service 的异类实现(卡片 motion.a 链 /cases/ 依赖尚不存在数据 → 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 通过 --- scripts/audit/l3-b2-verify.mjs | 70 ++++++++++ .../products/product-detail-content-v3.tsx | 127 +---------------- .../services/service-detail-content-v4.tsx | 72 ++-------- .../solutions/solution-detail-content-v3.tsx | 129 +----------------- .../detail/detail-case-studies-section.tsx | 112 +++++++++++++++ .../detail/detail-certifications-section.tsx | 75 ++++++++++ src/components/detail/detail-cta-section.tsx | 2 +- src/components/detail/index.ts | 4 + 8 files changed, 280 insertions(+), 311 deletions(-) create mode 100644 scripts/audit/l3-b2-verify.mjs create mode 100644 src/components/detail/detail-case-studies-section.tsx create mode 100644 src/components/detail/detail-certifications-section.tsx diff --git a/scripts/audit/l3-b2-verify.mjs b/scripts/audit/l3-b2-verify.mjs new file mode 100644 index 0000000..f2695c3 --- /dev/null +++ b/scripts/audit/l3-b2-verify.mjs @@ -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)); diff --git a/src/app/(marketing)/products/product-detail-content-v3.tsx b/src/app/(marketing)/products/product-detail-content-v3.tsx index a56338f..52372bf 100644 --- a/src/app/(marketing)/products/product-detail-content-v3.tsx +++ b/src/app/(marketing)/products/product-detail-content-v3.tsx @@ -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 = { '睿新ERP管理系统': , @@ -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 ( -
-
-
- -
- - -
-
- - 客户案例 - -
-
- -

- 客户案例 -

- - -
- - {caseStudies.map((study) => ( -
-
-
- {/* 客户名首字作占位图形(无真实配图时),纯装饰 → aria-hidden 豁免对比度 */} - -
-
- - {study.industry} - -
-
- -
-

- {study.client} -

-

- {study.challenge} -

-
-
{study.result}
-
核心成果
-
-
-
- ))} -
-
-
-
- ); -} - -interface CertificationsSectionProps { - certifications: Product['certifications']; -} - -function CertificationsSection({ certifications }: CertificationsSectionProps) { - if (!certifications || certifications.length === 0) return null; - - return ( -
-
-
- -
- - -
-
- - 资质认证 - -
-
- -

- 资质认证 -

- - -
- - {certifications.map((cert, idx) => ( -
-
- -
-
-
{cert.name}
-
{cert.issuer}
-
-
- ))} -
-
-
-
- ); -} +// ===== L3 信任层:客户案例 / 资质认证已合并至共享组件(@/components/detail)===== +// 原本地 CaseStudiesSection / CertificationsSection 与 solution 页逐字重复 ~95%, +// B-2 统一为共享组件(本页 h2 用默认「客户案例」,底色 token 化后深色模式正确反色)。 interface CTASectionProps { product: Product; diff --git a/src/app/(marketing)/services/service-detail-content-v4.tsx b/src/app/(marketing)/services/service-detail-content-v4.tsx index 604ca8c..938a82b 100644 --- a/src/app/(marketing)/services/service-detail-content-v4.tsx +++ b/src/app/(marketing)/services/service-detail-content-v4.tsx @@ -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 ( -
-
- -

- 真实案例,
可衡量的成果 -

-
- -
- {caseStudies.slice(0, 4).map((cs, i) => ( - - -
-
- - {cs.industry} - - - {cs.client} -
- -

- {cs.challenge.slice(0, 30)} -

- -

- {cs.solution} -

- -
- 查看详情 - -
-
-
- ))} -
-
-
- ); -} +// L3 信任层「客户案例」已合并至共享组件(@/components/detail)。 +// 原本地实现为异类:卡片 motion.a 链接 /cases/(依赖尚不存在的案例数据,链向 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" >

@@ -352,7 +296,7 @@ export default function ServiceDetailContentV4({ service }: { service: Service } - + ); diff --git a/src/app/(marketing)/solutions/solution-detail-content-v3.tsx b/src/app/(marketing)/solutions/solution-detail-content-v3.tsx index 95e55b3..64dcf7d 100644 --- a/src/app/(marketing)/solutions/solution-detail-content-v3.tsx +++ b/src/app/(marketing)/solutions/solution-detail-content-v3.tsx @@ -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 = { 制造业: , @@ -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 ( -
-
-
- -
- - -
-
- - 客户案例 - -
-
- -

- 同行业落地案例 -

- - -
- - {caseStudies.map((study) => ( -
-
-
- {/* 客户名首字作占位图形(无真实配图时),纯装饰 → aria-hidden 豁免对比度 */} - -
-
- - {study.industry} - -
-
- -
-

- {study.client} -

-

- {study.challenge} -

-
-
{study.result}
-
核心成果
-
-
-
- ))} -
-
-
-
- ); -} - -// ===== L3 信任层:资质认证(有数据才渲染,无数据不显示)===== -interface CertificationsSectionProps { - certifications: Solution['certifications']; -} - -function CertificationsSection({ certifications }: CertificationsSectionProps) { - if (!certifications || certifications.length === 0) return null; - - return ( -
-
-
- -
- - -
-
- - 资质认证 - -
-
- -

- 资质认证 -

- - -
- - {certifications.map((cert, idx) => ( -
-
- -
-
-
{cert.name}
-
{cert.issuer}
-
-
- ))} -
-
-
-
- ); -} +// ===== 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 - + diff --git a/src/components/detail/detail-case-studies-section.tsx b/src/components/detail/detail-case-studies-section.tsx new file mode 100644 index 0000000..d64dd05 --- /dev/null +++ b/src/components/detail/detail-case-studies-section.tsx @@ -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/、无 SectionLabel、 + * 动效 0.8s 违规)→ 统一到本组件:卡片不带链接(/cases/ 依赖尚不存在的案例数据, + * 链向 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 ( +
+
+
+ +
+ + +
+
+ + 客户案例 + +
+
+ +

+ {heading} +

+ + + {/* grid 直接挂 StaggerReveal(见文件头注释),gap-px 露出容器底色形成发丝分隔线 */} + + {caseStudies.map((study) => ( +
+
+
+ {/* 客户名首字作占位图形(无真实配图时),纯装饰 → aria-hidden 豁免对比度 */} + +
+
+ + {study.industry} + +
+
+ +
+

+ {study.client} +

+

+ {study.challenge} +

+
+
{study.result}
+
核心成果
+
+
+
+ ))} +
+
+
+ ); +} diff --git a/src/components/detail/detail-certifications-section.tsx b/src/components/detail/detail-certifications-section.tsx new file mode 100644 index 0000000..d9dddf2 --- /dev/null +++ b/src/components/detail/detail-certifications-section.tsx @@ -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 ( +
+
+
+ +
+ + +
+
+ + 资质认证 + +
+
+ +

+ 资质认证 +

+ + + {/* flex 直接挂 StaggerReveal(同 CaseStudiesSection 的布局要点) */} + + {certifications.map((cert, idx) => ( +
+
+ +
+
+
{cert.name}
+
{cert.issuer}
+
+
+ ))} +
+
+
+ ); +} diff --git a/src/components/detail/detail-cta-section.tsx b/src/components/detail/detail-cta-section.tsx index b0412e8..df1aa56 100644 --- a/src/components/detail/detail-cta-section.tsx +++ b/src/components/detail/detail-cta-section.tsx @@ -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" > {/* 标签 */} diff --git a/src/components/detail/index.ts b/src/components/detail/index.ts index ca71ed5..973c6f4 100644 --- a/src/components/detail/index.ts +++ b/src/components/detail/index.ts @@ -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';