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:
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user