feat(cms): 品牌叙事与结构性文案全站 CMS 化并补齐信任证据阶段 0

- 品牌叙事内核(价值主张/定位/承诺/三支柱/语气)下沉 site-config,经 SiteConfigProvider 注入全站
- 新增 page-copy 内容模型承载结构性文案(章节标题/眉标/描述/CTA/空状态),覆盖首页+服务/方案/产品/案例/新闻列表页,「CMS 优先 + 硬编码兜底」不白屏
- 首页「首批客户共创计划」板块(earlyAccessTitle/CtaLabel/Status 驱动)替代单行标签,如实呈现共创/内测/授权公开三档状态
- 关于页资质区数据空时如实展示「建设中」空态
- 零编造:不虚构客户/数据/资质
This commit is contained in:
2026-08-20 09:25:12 +08:00
parent 56be17b0ba
commit 337e1e60c0
34 changed files with 848 additions and 152 deletions
+19 -7
View File
@@ -95,14 +95,23 @@ function CaseCard({ caseItem, index }: { caseItem: CaseStudy; index: number }) {
export default function CasesContentV3({
cases: casesProp,
industries: industriesProp,
pageCopy,
}: {
cases?: CaseStudy[];
industries?: { id: string; label: string }[];
pageCopy?: Record<string, unknown> | null;
}) {
const [selectedIndustry, setSelectedIndustry] = useState('all');
const industries = industriesProp ?? [];
const heroTitle1 = (pageCopy?.casePageHeroTitle1 as string) || '每一个项目,';
const heroTitle2 = (pageCopy?.casePageHeroTitle2 as string) || '都以成果为目标';
const heroDescription = (pageCopy?.casePageHeroDescription as string) || '我们正在与首批客户共同创造价值故事。\n从挑战到方案,从过程到成果——用数据说话,用结果证明。';
const heroDescriptionLines = heroDescription.split('\n');
const filterTitle = (pageCopy?.casePageFilterTitle as string) || '全部案例';
const filterDescription = (pageCopy?.casePageFilterDescription as string) || '按行业筛选,找到与您业务最相关的成功故事';
const filteredCases = useMemo(() => {
const allCases = casesProp ?? [];
if (selectedIndustry === 'all') return allCases;
@@ -130,11 +139,11 @@ export default function CasesContentV3({
transition={{ duration: 1, delay: 0.2, ease: EASE_OUT }}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-black text-ink leading-[0.88] tracking-tightest mb-10 sm:mb-12 md:mb-16"
>
<div className="block">每一个项目,</div>
<div className="block">{heroTitle1}</div>
<div className="block mt-4 sm:mt-6">
<span className="relative inline-block">
<span className="relative text-brand font-black">
都以成果为目标
{heroTitle2}
<motion.span
className="absolute -bottom-2 left-0 w-full h-[3px] bg-brand"
style={{ transformOrigin: 'left' }}
@@ -153,9 +162,12 @@ export default function CasesContentV3({
transition={{ duration: 0.8, delay: 0.5, ease: EASE_OUT }}
className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12"
>
我们正在与首批客户共同创造价值故事。
<br className="hidden sm:block" />
从挑战到方案,从过程到成果——用数据说话,用结果证明。
{heroDescriptionLines.map((line, i) => (
<span key={i}>
{i > 0 && <br className="hidden sm:block" />}
{line}
</span>
))}
</motion.p>
<motion.div
@@ -225,10 +237,10 @@ export default function CasesContentV3({
All Cases
</div>
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
全部案例
{filterTitle}
</h2>
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
按行业筛选,找到与您业务最相关的成功故事
{filterDescription}
</p>
</motion.div>
+3 -2
View File
@@ -6,8 +6,9 @@ import CasesContentV3 from './cases-content-v3';
interface CasesClientProps {
cases?: CaseStudy[];
industries?: { id: string; label: string }[];
pageCopy?: Record<string, unknown> | null;
}
export function CasesClient({ cases, industries }: CasesClientProps) {
return <CasesContentV3 cases={cases} industries={industries} />;
export function CasesClient({ cases, industries, pageCopy }: CasesClientProps) {
return <CasesContentV3 cases={cases} industries={industries} pageCopy={pageCopy} />;
}
+1
View File
@@ -4,6 +4,7 @@ import CasesPage from './page';
jest.mock('@/lib/cms/data-server', () => ({
getPublishedItems: jest.fn(),
getPageCopy: jest.fn().mockResolvedValue(null),
}));
jest.mock('./cases-content-v3', () => ({
+6 -3
View File
@@ -1,7 +1,7 @@
import { Metadata } from 'next';
import { COMPANY_INFO } from '@/lib/constants';
import { CASE_INDUSTRIES } from '@/lib/constants/cases';
import { getPublishedItems } from '@/lib/cms/data-server';
import { getPublishedItems, getPageCopy } from '@/lib/cms/data-server';
import type { CaseStudy } from '@/lib/constants/cases';
import { CasesClient } from './client';
@@ -13,7 +13,10 @@ export const metadata: Metadata = {
export const revalidate = 3600;
export default async function CasesPage() {
const items = await getPublishedItems('case-study');
const [items, pageCopy] = await Promise.all([
getPublishedItems('case-study'),
getPageCopy('cases').catch(() => null),
]);
const cases = items.map((item) => ({ ...item.data, id: item.data.id || item.id, slug: item.data.slug || item.slug, title: item.data.title || item.title } as unknown as CaseStudy));
return <CasesClient cases={cases} industries={CASE_INDUSTRIES} />;
return <CasesClient cases={cases} industries={CASE_INDUSTRIES} pageCopy={pageCopy?.data ?? null} />;
}