feat(detail): Layer 3 信任层空数据兜底骨架(L3SignalsSlot + L3EmptyFallback + EarlyAccessNotice)

- 新增 L3SignalsSlot:4 类可验证替代信号卡(方法论/团队/历程/服务承诺),
  服务承诺卡 disabled 占位待业务确认;零编造约束下替代真实案例/认证
- 新增 EarlyAccessNotice:首批客户共创状态条(单一真源 company.ts EARLY_ACCESS)
- 新增 L3EmptyFallback:caseStudies/certifications/dataProofs 三者全空时渲染兜底,
  任一非空 return null 保留本地真实 section 视觉
- 接入 5 处详情页:solution v3 / service v4 / product v3 / erp-upgrade-v3 / standalone/[id]
- 修复 StaggerReveal 网格布局:grid 类须挂在 StaggerReveal 自身(否则卡片堆单列)
- 更新 solution 测试至新行为(空数据→兜底渲染)+ 补 lucide 图标 mock
- 修复 header.test.tsx 双 logo 断言(2593599 遗留)
- 新增 scripts/audit/l3-fallback-verify.mjs 运行时验证(浅/深双版 PASS)
  注:必须用 localhost 而非 127.0.0.1 —— Next dev allowedDevOrigins 默认白名单
  不含 127.0.0.1,chunk 请求带该 Origin 被 403 → hydration 不发生 → 动画全冻结

验证:type-check 0 错 · lint 0 错(127 既有 warnings) · 单测 129 套件/1628 通过 ·
L3 兜底浅深双版渲染截图 PASS
This commit is contained in:
2026-09-02 12:26:07 +08:00
parent 2a8b81f042
commit 3091035c56
12 changed files with 446 additions and 3 deletions
@@ -0,0 +1,53 @@
'use client';
/**
* EarlyAccessNotice — L3 信任层「首批客户共创」透明状态条
*
* 设计意图:
* - 数据真实性优先:客户案例 / 资质认证需获得授权后才能公开;
* 与其让 L3 整段 `return null` 制造「信任真空」,不如显式标注公司当前阶段。
* - 内容源于 `EARLY_ACCESS` 常量(company.ts L119),单一真源。
*
* 与 L3SignalsSlot 关系:EarlyAccessNotice 是 L3EmptyFallback 的次要兜底块,
* 在 signals(4 类信号卡)下方呈现——回答「为什么没有真实客户案例」这个问题。
*/
import Link from 'next/link';
import { ScrollReveal } from '@/components/ui/scroll-reveal';
import { EARLY_ACCESS } from '@/lib/constants/company';
export function EarlyAccessNotice() {
return (
<section
aria-labelledby="early-access-notice-heading"
className="py-12 sm:py-16 bg-bg-secondary"
>
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal>
<aside
role="note"
aria-label="首批客户共创状态"
className="p-5 sm:p-6 rounded-lg border border-dashed border-brand/40 bg-brand-soft/30"
>
<p
id="early-access-notice-heading"
className="text-sm font-semibold text-brand mb-1"
>
{EARLY_ACCESS.label}
</p>
<p className="text-sm text-text-secondary leading-relaxed mb-3">
{EARLY_ACCESS.desc}
</p>
<Link
href="/contact"
className="inline-flex items-center gap-1 text-sm font-medium text-brand underline-offset-4 hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 focus-visible:ring-offset-bg-secondary rounded"
>
了解共创合作
<span aria-hidden="true">→</span>
</Link>
</aside>
</ScrollReveal>
</div>
</section>
);
}
+4
View File
@@ -4,6 +4,10 @@ export { ProductValueSection } from './detail-product-value';
export { DetailTrustSection } from './detail-trust-section';
export { L3SignalsSlot } from './l3-signals-slot';
export { EarlyAccessNotice } from './early-access-notice';
export { L3EmptyFallback } from './l3-empty-fallback';
export { DetailCTASection } from './detail-cta-section';
export { CaseStudyCard } from './detail-case-study';
@@ -0,0 +1,47 @@
'use client';
/**
* L3EmptyFallback — L3 信任层「数据空」兜底组件
*
* 设计意图:
* - 详情页 L3 由 3 类内容构成:caseStudies / certifications / dataProofs。
* 任一非空时由对应本地 section 渲染真实内容;**三者全空时整段 L3 消失** =
* 用户感受「这个详情页没有信任层」。
* - 零编造约束下,**禁止**伪造案例/认证/数据。本组件提供合规替代:
* 全空时渲染 L3SignalsSlot(4 类可验证信号卡)+ EarlyAccessNotice(共创占位),
* 既保持 L3 结构完整,又不违背「零虚构客户案例」原则。
*
* 与本地 CaseStudiesSection / CertificationsSection 的协作:
* - 三者全空 → 本组件渲染 signals + notice
* - 任一非空 → 本组件 return null,让真实 section 渲染(保留本地视觉细节)
*
* @see /Users/zhangxiang/Codes/Novalon/novalon-website/.workbuddy/memory/MEMORY.md
* "内容真实性约束" + "Layer 3 替代信号"
*/
import { L3SignalsSlot } from './l3-signals-slot';
import { EarlyAccessNotice } from './early-access-notice';
export function L3EmptyFallback({
caseStudies,
certifications,
dataProofs,
}: {
caseStudies?: readonly unknown[];
certifications?: readonly unknown[];
dataProofs?: readonly unknown[];
}) {
const hasContent =
(caseStudies?.length ?? 0) > 0 ||
(certifications?.length ?? 0) > 0 ||
(dataProofs?.length ?? 0) > 0;
if (hasContent) return null;
return (
<>
<L3SignalsSlot />
<EarlyAccessNotice />
</>
);
}
+183
View File
@@ -0,0 +1,183 @@
'use client';
/**
* L3SignalsSlot — L3 信任层「替代信号」槽
*
* 设计意图:
* - 真实数据(客户案例 / 资质认证 / 数据佐证 / 证言)需获得客户授权后才能公开;
* 在那之前,详情页 L3 必须有可验证的兜底内容(避免 L3 整段消失 = 信任真空)。
* - 四类替代信号(方法论透明度 / 团队资历 / 里程碑 / 服务承诺)均为站点内
* 可访问的真实路由(/methodology · /team · /about/brand · /contact),
* 不存在编造。
*
* 接入位置:详情页 L3 区块(案例 / 认证 / 数据)之前,由 L3EmptyFallback 在三者
* 全空时统一包裹渲染;任一非空时本组件不渲染,让真实 section 自行展示。
*
* 设计约束(CLAUDE.md / CONTEXT.md):
* - 品牌红 ≥3 处触达点(eyebrow 短线 + Icon 容器 + 卡片 hover 边)
* - 动效 ≤700ms ease-ink(ScrollReveal/StaggerReveal 默认 400ms,符合"动效四原则 fast")
* - WCAG AA:text-secondary 在 bg-primary 4.5:1+;hover 状态焦点环保留
*
* @see /Users/zhangxiang/Codes/Novalon/novalon-website/.workbuddy/memory/MEMORY.md
* "内容真实性约束"(零编造前提下的 L3 替代信号)
*/
import Link from 'next/link';
import {
ArrowUpRight,
Compass,
Milestone,
ShieldCheck,
Users,
} from 'lucide-react';
import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { SectionLabel } from '@/components/ui/page-decoration';
import { cn } from '@/lib/utils';
interface L3Signal {
href: string;
eyebrow: string;
title: string;
desc: string;
Icon: React.ComponentType<{ className?: string }>;
/**
* 服务承诺卡:文案散落 5 处但无可复用 L3 section,
* 待业务确认后才有具体承诺条款。当前显示「待业务确认」占位(无 href 跳转)。
*/
disabled?: boolean;
}
const SIGNALS: readonly L3Signal[] = [
{
href: '/methodology',
eyebrow: '方法论',
title: '交付方法论',
desc: '从诊断评估到持续优化,四阶段可量化路径;不依赖案例也能证明过程可控。',
Icon: Compass,
},
{
href: '/team',
eyebrow: '团队',
title: '团队资历',
desc: '创始团队与核心成员的背景、承诺宣言与协作方式。',
Icon: Users,
},
{
href: '/about/brand',
eyebrow: '历程',
title: '发展历程',
desc: '2026 年 1 月成立至今的关键节点与里程碑。',
Icon: Milestone,
},
{
href: '/contact',
eyebrow: '承诺',
title: '服务承诺',
desc: '具体承诺条款待业务确认后公开;当前阶段以共创协议为准,欢迎垂询。',
Icon: ShieldCheck,
disabled: true,
},
] as const;
export function L3SignalsSlot({
heading = '在数据之外,我们用透明度建立信任',
description = '客户案例与资质认证需获得授权后才能公开;在那之前,以下四个锚点是我们可验证的承诺。',
}: {
heading?: string;
description?: string;
}) {
return (
<section
aria-labelledby="l3-signals-heading"
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">
<ScrollReveal className="text-center max-w-3xl mx-auto mb-12">
<SectionLabel align="center">信任锚点</SectionLabel>
<h2
id="l3-signals-heading"
className="text-2xl sm:text-3xl md:text-4xl font-bold text-ink tracking-tight leading-tight"
>
{heading}
</h2>
<p className="mt-4 text-base text-text-secondary leading-relaxed">{description}</p>
</ScrollReveal>
{/* grid 类直接挂在 StaggerReveal 上:StaggerReveal 会为每个子项包一层 motion.div,
若把 grid 放在外层容器,包装层会成为 grid 的唯一子项,卡片将堆成单列 */}
<StaggerReveal
staggerDelay={0.06}
delayChildren={0.05}
className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6"
>
{SIGNALS.map((s) => (
<SignalCard key={s.eyebrow} signal={s} />
))}
</StaggerReveal>
</div>
</section>
);
}
function SignalCard({ signal }: { signal: L3Signal }) {
const { href, eyebrow, title, desc, Icon, disabled } = signal;
const cardBody = (
<div
className={cn(
'group relative h-full p-6 sm:p-8 rounded-lg border border-border-primary bg-bg-primary',
'transition-all duration-300 ease-out',
disabled
? 'opacity-70 cursor-not-allowed'
: 'hover:border-brand/40 hover:shadow-md hover:-translate-y-1'
)}
>
<span className="inline-block text-[11px] tracking-[0.15em] uppercase font-bold text-brand mb-4">
{eyebrow}
</span>
<div
className="w-10 h-10 mb-4 flex items-center justify-center rounded-md bg-brand-soft text-brand"
aria-hidden="true"
>
<Icon className="w-5 h-5" />
</div>
<h3 className="text-base font-bold text-ink mb-2 flex items-center gap-1.5">
{title}
{!disabled && (
<ArrowUpRight
className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5"
aria-hidden="true"
/>
)}
</h3>
<p className="text-sm text-text-secondary leading-relaxed">{desc}</p>
{disabled && (
<p className="mt-4 text-xs text-text-muted italic">待业务确认</p>
)}
</div>
);
if (disabled) {
return (
<div aria-disabled="true" className="block rounded-lg">
{cardBody}
</div>
);
}
return (
<Link
href={href}
className="block rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 focus-visible:ring-offset-bg-secondary"
>
{cardBody}
</Link>
);
}