chore: sync marketing pages, CMS extensions, tests and project docs

同步工作区剩余变更,主要包括:
- 营销页面组件与布局持续优化(about/news/services/solutions/team 等)
- 详情页四层叙事组件、布局组件、UI 组件调整
- CMS 数据模型、API 路由、权限、工作流、站内通知、媒体管理扩展
- 新增/补充单元测试与 E2E 测试(cms-workflow.spec.ts 等)
- ESLint 9 迁移、jest/tsconfig 配置更新、依赖调整
- 新增 ADR、CMS 评估文档、Release Review / Acceptance 报告
- 移除水墨装饰组件与大体积未使用字体文件
This commit is contained in:
张翔
2026-07-25 08:04:01 +08:00
parent e35090b914
commit 10404dbb36
208 changed files with 18107 additions and 8330 deletions
+54
View File
@@ -2,9 +2,17 @@ import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import TermsOfServicePage from './page';
jest.mock('@/lib/cms/data-server', () => ({
getPublishedItems: jest.fn(),
}));
import { getPublishedItems } from '@/lib/cms/data-server';
const mockedGetPublishedItems = getPublishedItems as jest.MockedFunction<typeof getPublishedItems>;
describe('TermsOfServicePage', () => {
beforeEach(() => {
jest.clearAllMocks();
mockedGetPublishedItems.mockResolvedValue([]);
});
describe('Rendering', () => {
@@ -49,4 +57,50 @@ describe('TermsOfServicePage', () => {
expect(h2s.length).toBeGreaterThan(0);
});
});
describe('CMS integration', () => {
it('should render CMS hero title and description when available', async () => {
mockedGetPublishedItems.mockResolvedValue([
{
id: 'legal-terms',
modelId: 'model-legal-page',
modelCode: 'legal-page',
title: '服务条款',
slug: 'terms',
locale: 'zh-CN',
status: 'published',
data: {
pageType: 'terms',
heroTitle: 'CMS 服务条款标题',
heroDescription: 'CMS 服务条款描述',
lastUpdated: '2026-07-17',
content: '<div data-testid="cms-content"><h2>CMS 条款内容</h2></div>',
},
version: 1,
sortOrder: 0,
createdBy: 'system',
updatedBy: 'system',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
] as unknown as Awaited<ReturnType<typeof getPublishedItems>>);
const page = await TermsOfServicePage();
render(page);
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('CMS 服务条款标题');
expect(screen.getByText('CMS 服务条款描述')).toBeInTheDocument();
expect(screen.getByTestId('cms-content')).toBeInTheDocument();
});
it('should fall back to inline content when CMS is unavailable', async () => {
mockedGetPublishedItems.mockRejectedValue(new Error('DB connection failed'));
const page = await TermsOfServicePage();
render(page);
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('服务条款');
expect(screen.getByText('引言')).toBeInTheDocument();
});
});
});
+13 -2
View File
@@ -7,6 +7,9 @@ export const metadata: Metadata = {
description: `${COMPANY_INFO.name}服务条款`,
};
// ISR:生产环境每 1 小时自动重新验证,CMS 发布/更新时通过 /api/cms/revalidate 主动刷新
export const revalidate = 3600;
function TermsContent() {
return (
<div className="prose prose-lg max-w-none">
@@ -169,6 +172,8 @@ function TermsContent() {
export default async function TermsOfServicePage() {
let cmsContent: string | null = null;
let heroTitle = '服务条款';
let heroDescription = '请仔细阅读以下服务条款,使用我们的服务即表示您同意这些条款';
try {
const items = await getPublishedItems('legal-page');
@@ -176,6 +181,12 @@ export default async function TermsOfServicePage() {
if (termsItem && typeof termsItem.data.content === 'string' && termsItem.data.content.length > 0) {
cmsContent = termsItem.data.content as string;
}
if (termsItem && typeof termsItem.data.heroTitle === 'string' && termsItem.data.heroTitle.length > 0) {
heroTitle = termsItem.data.heroTitle as string;
}
if (termsItem && typeof termsItem.data.heroDescription === 'string' && termsItem.data.heroDescription.length > 0) {
heroDescription = termsItem.data.heroDescription as string;
}
} catch {
// CMS not available, fall back to inline content
}
@@ -185,10 +196,10 @@ export default async function TermsOfServicePage() {
<div className="bg-gradient-to-br from-[var(--color-brand)] via-[var(--color-brand)]/80 to-[var(--color-hero-dark-end)] py-20">
<div className="container-wide">
<h1 className="text-4xl md:text-5xl font-bold text-white mb-4">
服务条款
{heroTitle}
</h1>
<p className="text-xl text-white/90 max-w-2xl">
请仔细阅读以下服务条款,使用我们的服务即表示您同意这些条款
{heroDescription}
</p>
</div>
</div>