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