feat: 品牌裁剪弹窗/Markdown 行内短代码与详情视频块等
新增: - BrandCropModal 品牌图裁剪弹窗 + cropImage 工具 - Markdown 行内短代码(inlineShortcodes + remark 插件) - MdDetails 折叠块、MdVideo 视频块组件 - directiveBlocks/inlineShortcodes 测试 其它: - 聊天后端(handler/service chat)调整 - Markdown 渲染链路、编辑器、全局样式与品牌相关面板更新
This commit is contained in:
@@ -5,6 +5,11 @@ export const AVATAR_OUTPUT_SIZE = 320;
|
||||
// 裁剪后 WebP 文件大小上限(与后端 service.AvatarMaxBytes 保持一致)
|
||||
export const AVATAR_MAX_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
// 品牌 Logo 输出高度上限(与后端 brandMaxDim 对齐)
|
||||
export const BRAND_MAX_DIM = 2048;
|
||||
// 裁剪后品牌图大小上限(与后端 BrandLogoMaxBytes 对齐)
|
||||
export const BRAND_MAX_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
function loadImage(src: string): Promise<HTMLImageElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
@@ -18,6 +23,25 @@ function canvasToWebp(canvas: HTMLCanvasElement, quality: number): Promise<Blob
|
||||
return new Promise((resolve) => canvas.toBlob(resolve, "image/webp", quality));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按裁剪区域导出 WebP。
|
||||
* 自动按 0.92 → 0.65 降质量,直到满足大小上限;仍超限则抛出错误。
|
||||
*/
|
||||
async function exportWebp(
|
||||
canvas: HTMLCanvasElement,
|
||||
maxBytes: number
|
||||
): Promise<Blob> {
|
||||
let last: Blob | null = null;
|
||||
for (const quality of [0.92, 0.85, 0.75, 0.65]) {
|
||||
const blob = await canvasToWebp(canvas, quality);
|
||||
if (!blob) throw new Error("当前浏览器不支持 WebP 导出,请使用最新版 Chrome/Edge/Safari");
|
||||
last = blob;
|
||||
if (blob.size <= maxBytes) return blob;
|
||||
}
|
||||
if (last && last.size <= maxBytes) return last;
|
||||
throw new Error("裁剪后的图片仍超过大小限制,请缩小图片后再试");
|
||||
}
|
||||
|
||||
/**
|
||||
* 按裁剪区域导出正方形 WebP。
|
||||
* 自动按 0.92 → 0.65 降质量,直到满足大小上限;仍超限则抛出错误。
|
||||
@@ -47,13 +71,51 @@ export async function getCroppedAvatarWebp(
|
||||
AVATAR_OUTPUT_SIZE
|
||||
);
|
||||
|
||||
let last: Blob | null = null;
|
||||
for (const quality of [0.92, 0.85, 0.75, 0.65]) {
|
||||
const blob = await canvasToWebp(canvas, quality);
|
||||
if (!blob) throw new Error("当前浏览器不支持 WebP 导出,请使用最新版 Chrome/Edge/Safari");
|
||||
last = blob;
|
||||
if (blob.size <= AVATAR_MAX_BYTES) return blob;
|
||||
}
|
||||
if (last && last.size <= AVATAR_MAX_BYTES) return last;
|
||||
throw new Error("裁剪后的图片仍超过 2MB,请缩小图片后再试");
|
||||
return exportWebp(canvas, AVATAR_MAX_BYTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按裁剪区域导出任意比例 WebP(品牌 Logo 用)。
|
||||
* 输出高度限制在 BRAND_MAX_DIM 以内,宽度按比例自适应。
|
||||
*/
|
||||
export async function getCroppedBrandWebp(
|
||||
imageSrc: string,
|
||||
pixelCrop: Area,
|
||||
aspect: number
|
||||
): Promise<Blob> {
|
||||
const image = await loadImage(imageSrc);
|
||||
const canvas = document.createElement("canvas");
|
||||
|
||||
// 以高度为基准,宽度按纵横比计算;若超宽则按宽度反推高度
|
||||
let outH = pixelCrop.height;
|
||||
let outW = Math.round(outH * aspect);
|
||||
if (outW > BRAND_MAX_DIM) {
|
||||
outW = BRAND_MAX_DIM;
|
||||
outH = Math.round(outW / aspect);
|
||||
}
|
||||
if (outH > BRAND_MAX_DIM) {
|
||||
outH = BRAND_MAX_DIM;
|
||||
outW = Math.round(outH * aspect);
|
||||
}
|
||||
|
||||
canvas.width = outW;
|
||||
canvas.height = outH;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) throw new Error("当前浏览器不支持图片裁剪");
|
||||
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = "high";
|
||||
ctx.drawImage(
|
||||
image,
|
||||
pixelCrop.x,
|
||||
pixelCrop.y,
|
||||
pixelCrop.width,
|
||||
pixelCrop.height,
|
||||
0,
|
||||
0,
|
||||
outW,
|
||||
outH
|
||||
);
|
||||
|
||||
return exportWebp(canvas, BRAND_MAX_BYTES);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user