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
+117
View File
@@ -0,0 +1,117 @@
import sharp from 'sharp';
import type { GeneratedDerivatives, ImageDerivative } from './types';
export const THUMBNAIL_SIZE = 320;
const IMAGE_MIME_PREFIX = 'image/';
/**
* 判断 MIME 类型是否为图片
*/
export function isImage(mimeType: string): boolean {
return mimeType.startsWith(IMAGE_MIME_PREFIX);
}
/**
* 获取图片宽高元数据
* 非图片或解析失败时返回 null
*/
export async function getImageMetadata(
buffer: Buffer
): Promise<{ width: number; height: number } | null> {
try {
const metadata = await sharp(buffer).metadata();
if (!metadata.width || !metadata.height) return null;
return { width: metadata.width, height: metadata.height };
} catch {
return null;
}
}
function buildDerivativeFileName(originalName: string, suffix: string, ext: string): string {
const baseName = originalName.replace(/\.[^.]+$/, '');
return `${baseName}-${suffix}.${ext}`;
}
function buildDerivative(
originalName: string,
suffix: string,
ext: string,
width: number,
height: number,
buffer: Buffer
): ImageDerivative {
const fileName = buildDerivativeFileName(originalName, suffix, ext);
return {
path: `public/uploads/${fileName}`,
url: `/uploads/${fileName}`,
width,
height,
buffer,
};
}
/**
* 为图片生成缩略图、WebP、AVIF 派生格式
* 非图片返回空对象
*
* 实现说明:
* - 缩略图限制长边为 THUMBNAIL_SIZE,保持比例
* - WebP/AVIF 保持原图尺寸,用于现代浏览器优化
*/
export async function generateDerivatives(
buffer: Buffer,
mimeType: string
): Promise<GeneratedDerivatives> {
if (!isImage(mimeType)) {
return {};
}
const metadata = await getImageMetadata(buffer);
if (!metadata) {
return {};
}
const originalName = `image-${Date.now()}`;
const derivatives: GeneratedDerivatives = {};
// 缩略图
const thumbnailBuffer = await sharp(buffer)
.resize(THUMBNAIL_SIZE, THUMBNAIL_SIZE, { fit: 'inside', withoutEnlargement: true })
.toBuffer();
const thumbnailMeta = await sharp(thumbnailBuffer).metadata();
derivatives.thumbnail = buildDerivative(
originalName,
'thumbnail',
'webp',
thumbnailMeta.width || THUMBNAIL_SIZE,
thumbnailMeta.height || THUMBNAIL_SIZE,
thumbnailBuffer
);
// WebP
const webpBuffer = await sharp(buffer).webp({ quality: 85 }).toBuffer();
const webpMeta = await sharp(webpBuffer).metadata();
derivatives.webp = buildDerivative(
originalName,
'webp',
'webp',
webpMeta.width || metadata.width,
webpMeta.height || metadata.height,
webpBuffer
);
// AVIF
const avifBuffer = await sharp(buffer).avif({ quality: 80 }).toBuffer();
const avifMeta = await sharp(avifBuffer).metadata();
derivatives.avif = buildDerivative(
originalName,
'avif',
'avif',
avifMeta.width || metadata.width,
avifMeta.height || metadata.height,
avifBuffer
);
return derivatives;
}