fix(metrics): 数字口径 basis 结构强制,删除详情页虚构佐证

复评 P0 收尾:把「每个数字带口径」从个别页自觉升级为结构性强制。
- metrics-basis.ts:METRIC_BASES / METRIC_BASIS_NOTES + resolveMetricBasis
  (缺失·非法·大小写错一律回落最弱 target)+ weakestBasis(一组数字取最弱者聚合)
  + FORBIDDEN_PROOF_PHRASES;MetricsBasisNote 收 basis prop
- 类型层 Product.metrics / DataProof / ServiceMetric / Solution.outcomes /
  DataMetric / about keyMetrics 全部加 basis?;9 处调用点改为 basis 驱动
- content-types.ts 新增共享 metricBasisField(select)展开进 9 处字段定义;
  trustSignals 有意跳过(已有自己的 source)。seed 显式声明 55 条 target
- 删除字面虚构佐证:详情页「每一项指标都源自真实客户案例」、「经过实战验证」×3、
  「来自真实客户的使用数据」、「经实际场景验证」;给此前完全裸奔的 3 个共享
  数字渲染器(DataProofSection / detail-trust / detail-product-value)补上角注
- metrics-basis.test 3→16 例:basis 解析正控制、weakestBasis 次序、seed 全量
  显式声明 + 覆盖证明(防空跑假绿)+ 禁任何条目自称 verified + 禁词全源码扫描
- 文档同源:DESIGN.md 立 The Declared-Basis Rule;PRODUCT.md 常驻禁令;
  CONTEXT.md 记决策并警示 admin 字段需重跑 seed 才激活

边界:CMS 字段定义存 ContentModel.fields(DB 内 JSON),basis 下拉须重跑 seed
才会出现在 admin 表单;本次 DB 与 seed 数据值零改动,渲染端对缺 basis 兜底 target。

Gates: type-check 0 · jest 1594/132 · contrast 7/7 · headings 10/10
lint 0e/128w(与 stash 基线逐行比对,零新增告警)
This commit is contained in:
2026-09-20 10:38:23 +08:00
parent 82accb7cee
commit 3ed3afd819
27 changed files with 332 additions and 97 deletions
+5
View File
@@ -5,6 +5,8 @@ import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { EASE_OUT } from '@/components/ui/page-decoration';
import { CheckCircle2, ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
import { MetricsBasisNote } from '@/components/ui/metrics-basis-note';
import { weakestBasis, type MetricBasis } from '@/lib/constants/metrics-basis';
import { useState } from 'react';
export interface MethodologyLayer {
@@ -318,6 +320,7 @@ export interface DataMetric {
description?: string;
trend?: string;
trendUp?: boolean;
basis?: MetricBasis;
}
export interface DataProofSectionProps {
@@ -328,6 +331,7 @@ export interface DataProofSectionProps {
export function DataProofSection({ title, subtitle, metrics }: DataProofSectionProps) {
if (!metrics || metrics.length === 0) return null;
const basis = weakestBasis(metrics.map((m) => m.basis));
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-primary">
@@ -375,6 +379,7 @@ export function DataProofSection({ title, subtitle, metrics }: DataProofSectionP
))}
</StaggerReveal>
</div>
<MetricsBasisNote basis={basis} className="mt-6" />
</div>
</section>
);
@@ -15,6 +15,8 @@ import {
type LucideIcon,
} from 'lucide-react';
import type { Product } from '@/lib/constants/products';
import { MetricsBasisNote } from '@/components/ui/metrics-basis-note';
import { weakestBasis } from '@/lib/constants/metrics-basis';
interface ProductValueSectionProps {
product: Product;
@@ -250,11 +252,11 @@ export function ProductValueSection({ product }: ProductValueSectionProps) {
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-px bg-brand" />
<span className="text-[13px] tracking-[0.12em] font-semibold text-brand-ink">
效果数据
设计目标
</span>
</div>
<h3 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[0.95]">
用真实效果证明价值
以可量化的目标约束交付
</h3>
</div>
@@ -275,6 +277,10 @@ export function ProductValueSection({ product }: ProductValueSectionProps) {
);
})}
</div>
<MetricsBasisNote
basis={weakestBasis(product.dataProofs.map((proof) => proof.basis))}
className="mt-8"
/>
</motion.div>
)}
</div>
@@ -5,6 +5,8 @@ import { useRef, useEffect, useState } from 'react';
import { CaseStudyCard } from './detail-case-study';
import { CertificationList } from './detail-certification';
import type { CaseStudy, DataProof, Certification } from '@/lib/constants/products';
import { MetricsBasisNote } from '@/components/ui/metrics-basis-note';
import { weakestBasis } from '@/lib/constants/metrics-basis';
interface DetailTrustSectionProps {
caseStudies?: CaseStudy[];
@@ -84,13 +86,13 @@ export function DetailTrustSection({
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-px bg-brand" />
<span className="text-[13px] tracking-[0.12em] font-semibold text-brand-ink">
用真实效果证明价值
目标口径
</span>
</div>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[0.95]">
来自真实客户的使用数据,
以设计目标约束交付,
<br />
让效果可衡量、可验证
在首批共创中转为可核验数据
</h2>
</motion.div>
@@ -114,6 +116,10 @@ export function DetailTrustSection({
</motion.div>
))}
</div>
<MetricsBasisNote
basis={weakestBasis(dataProofs.map((proof) => proof.basis))}
className="mt-8"
/>
</div>
)}
+6 -4
View File
@@ -435,14 +435,14 @@ describe('ProductValueSection', () => {
it('should render data proofs when available', async () => {
const { ProductValueSection } = await import('./detail-product-value');
render(<ProductValueSection product={mockProduct} />);
expect(screen.getByText('用真实效果证明价值')).toBeInTheDocument();
expect(screen.getByText('以可量化的目标约束交付')).toBeInTheDocument();
});
it('should not render data proofs section when empty', async () => {
const { ProductValueSection } = await import('./detail-product-value');
const productWithoutData = { ...mockProduct, dataProofs: [] };
render(<ProductValueSection product={productWithoutData} />);
expect(screen.queryByText('用真实效果证明价值')).not.toBeInTheDocument();
expect(screen.queryByText('以可量化的目标约束交付')).not.toBeInTheDocument();
});
});
@@ -462,8 +462,10 @@ describe('DetailTrustSection', () => {
dataProofs={mockProduct.dataProofs}
/>
);
expect(screen.getByText('用真实效果证明价值')).toBeInTheDocument();
expect(screen.getByText('目标口径')).toBeInTheDocument();
expect(screen.getByText(mockProduct.dataProofs[0]!.metric)).toBeInTheDocument();
// 未声明 basis 的 dataProofs 必须自动落最弱口径角注,不得裸奔
expect(screen.getByTestId('metrics-basis-note')).toHaveTextContent('目标口径,非既成结果');
});
it('should render case studies', async () => {
@@ -500,7 +502,7 @@ describe('DetailTrustSection', () => {
accentColor="#C41E3A"
/>
);
expect(screen.getByText('用真实效果证明价值')).toBeInTheDocument();
expect(screen.getByText('目标口径')).toBeInTheDocument();
expect(screen.getByText('他们已经实现了转型突破')).toBeInTheDocument();
expect(screen.getByText('资质认证')).toBeInTheDocument();
});
@@ -9,7 +9,7 @@
* - 内容源于 `EARLY_ACCESS` 常量(company.ts L119),单一真源。
*
* 与 L3SignalsSlot 关系:EarlyAccessNotice 是 L3EmptyFallback 的次要兜底块,
* 在 signals(4 类信号卡)下方呈现——回答「为什么没有真实客户案例」这个问题。
* 在 signals(4 类信号卡)下方呈现——回答「为什么还没有可公开案例」这个问题。
*/
import Link from 'next/link';
+11 -4
View File
@@ -1,14 +1,21 @@
import { cn } from '@/lib/utils';
import { METRICS_BASIS_NOTE } from '@/lib/constants/metrics-basis';
import { METRIC_BASIS_NOTES, resolveMetricBasis, type MetricBasis } from '@/lib/constants/metrics-basis';
/**
* 结果型数字(提升百分比 / 可用性 / 达标率等)下方的口径角注。
* 全站统一文案,见 METRICS_BASIS_NOTE。供 solutions / services / home / about / products 复用。
* 文案按数字声明的 `basis` 口径解析,未声明一律按最弱的 target 处理,见 METRIC_BASIS_NOTES。
* 供 solutions / services / home / about / products 与详情页 DataProofSection 复用。
*/
export function MetricsBasisNote({ className }: { className?: string }) {
export function MetricsBasisNote({
className,
basis = 'target',
}: {
className?: string;
basis?: MetricBasis | string | null;
}) {
return (
<p className={cn('text-xs text-text-muted', className)} data-testid="metrics-basis-note">
{METRICS_BASIS_NOTE}
{METRIC_BASIS_NOTES[resolveMetricBasis(basis)]}
</p>
);
}