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
+130
View File
@@ -0,0 +1,130 @@
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
import { NextRequest } from 'next/server';
// ─── Mock @/lib/db ────────────────────────────────────────────────────────
const mockContentModelFindMany = jest.fn<(args: unknown) => Promise<unknown[]>>();
jest.mock('@/lib/db', () => ({
prisma: {
contentModel: {
findMany: mockContentModelFindMany,
},
},
}));
// ─── Mock permissions ─────────────────────────────────────────────────────
const mockRequirePermission = jest.fn<
(request: NextRequest, modelCode: string, action: string) => Promise<unknown>
>();
jest.mock('@/lib/permissions', () => ({
requirePermission: mockRequirePermission,
}));
jest.unmock('./route');
import { GET } from './route';
function createMockRequest(): NextRequest {
return {
url: 'http://localhost/api/admin/models',
} as unknown as NextRequest;
}
function mockAuthorized() {
mockRequirePermission.mockResolvedValue({
user: { userId: 'user-1', username: 'admin', role: 'content_admin' },
});
}
function mockUnauthorized() {
mockRequirePermission.mockResolvedValue({
response: new Response(JSON.stringify({ error: '未授权' }), { status: 401 }),
});
}
function mockForbidden() {
mockRequirePermission.mockResolvedValue({
response: new Response(JSON.stringify({ error: '无权限' }), { status: 403 }),
});
}
const mockModels = [
{
id: 'model-1',
code: 'news',
name: '新闻资讯',
description: '新闻内容模型',
fields: JSON.stringify([
{ code: 'title', name: '标题', type: 'text', required: true },
{ code: 'body', name: '正文', type: 'richtext', required: false },
]),
sortOrder: 1,
createdAt: new Date('2026-01-01'),
updatedAt: new Date('2026-01-01'),
},
{
id: 'model-2',
code: 'product',
name: '产品管理',
description: '产品内容模型',
fields: JSON.stringify([{ code: 'name', name: '产品名称', type: 'text', required: true }]),
sortOrder: 2,
createdAt: new Date('2026-01-02'),
updatedAt: new Date('2026-01-02'),
},
];
beforeEach(() => {
jest.clearAllMocks();
mockContentModelFindMany.mockResolvedValue(mockModels);
});
describe('GET /api/admin/models', () => {
it('returns 401 when not authenticated', async () => {
mockUnauthorized();
const response = await GET(createMockRequest());
expect(response.status).toBe(401);
});
it('returns 403 when authenticated but unauthorized', async () => {
mockForbidden();
const response = await GET(createMockRequest());
expect(response.status).toBe(403);
});
it('returns parsed model list ordered by sortOrder', async () => {
mockAuthorized();
const response = await GET(createMockRequest());
const body = await response.json();
expect(response.status).toBe(200);
expect(body).toHaveLength(2);
expect(body[0]).toMatchObject({
code: 'news',
name: '新闻资讯',
fields: [
{ code: 'title', name: '标题', type: 'text', required: true },
{ code: 'body', name: '正文', type: 'richtext', required: false },
],
});
expect(body[1]).toMatchObject({
code: 'product',
name: '产品管理',
});
expect(mockContentModelFindMany).toHaveBeenCalledWith(
expect.objectContaining({
orderBy: { sortOrder: 'asc' },
})
);
});
it('returns 500 on database error', async () => {
mockAuthorized();
mockContentModelFindMany.mockRejectedValue(new Error('db error'));
const response = await GET(createMockRequest());
expect(response.status).toBe(500);
});
});
+4 -4
View File
@@ -1,12 +1,12 @@
import { NextRequest } from 'next/server';
import { prisma } from '@/lib/db';
import { authenticateRequest } from '@/lib/auth';
import { success, unauthorized, internalError } from '@/lib/api-response';
import { requirePermission } from '@/lib/permissions';
import { success, internalError } from '@/lib/api-response';
// GET /api/admin/models - 获取所有内容模型
export async function GET(request: NextRequest) {
const payload = authenticateRequest(request);
if (!payload) return unauthorized();
const permission = await requirePermission(request, 'content-model', 'read');
if ('response' in permission) return permission.response;
try {
const models = await prisma.contentModel.findMany({