feat: 媒体库 blob 去重存储与品牌/注册等前端调整

新增:
- blob_store 二进制去重存储 + blob_dedup 测试

其它:
- 后端 settings/auth/media_library/upload/operations 服务与 handler 调整
- 前端品牌(BrandLockup/BrandCropModal/BrandSeoPanel)、注册、媒体库、分析详情页、Header/SiteChrome 等更新
This commit is contained in:
2026-10-03 02:38:11 +08:00
parent 8cfdfcbbf0
commit 46e0cdc0b3
32 changed files with 1242 additions and 228 deletions

View File

@@ -28,8 +28,8 @@ export type BrandSlot = "logo_light" | "logo_dark" | "favicon";
/** 页眉品牌形式 */
export type BrandMark = "image_text" | "image" | "text";
/** 图槽画幅。方标是默认;其余是页眉里常见的横版比例 */
export type BrandLogoSize = "sq" | "2x1" | "3x1" | "4x1" | "16x9";
/** 图槽画幅。方标是默认;其余是页眉里常见的横版比例;auto=原图比例自适应宽度 */
export type BrandLogoSize = "sq" | "2x1" | "3x1" | "4x1" | "16x9" | "auto";
export const BRAND_LOGO_SIZES: { id: BrandLogoSize; label: string; w: number; h: number }[] = [
{ id: "sq", label: "方标", w: 36, h: 36 },
@@ -37,6 +37,7 @@ export const BRAND_LOGO_SIZES: { id: BrandLogoSize; label: string; w: number; h:
{ id: "3x1", label: "3:1", w: 120, h: 40 },
{ id: "4x1", label: "4:1", w: 176, h: 44 },
{ id: "16x9", label: "16:9", w: 96, h: 54 },
{ id: "auto", label: "原图", w: 0, h: 40 },
];
export function normalizeBrandMark(raw: unknown): BrandMark {
@@ -47,10 +48,38 @@ export function normalizeBrandMark(raw: unknown): BrandMark {
export function normalizeBrandLogoSize(raw: unknown): BrandLogoSize {
const s = String(raw ?? "").trim();
if (s === "sq" || s === "2x1" || s === "3x1" || s === "4x1" || s === "16x9") return s;
if (s === "sq" || s === "2x1" || s === "3x1" || s === "4x1" || s === "16x9" || s === "auto") return s;
return "sq";
}
/** 各固定画幅的宽高比(auto 不在此表,代表自由比例) */
export const BRAND_LOGO_SIZE_ASPECT: Partial<Record<BrandLogoSize, number>> = {
sq: 1,
"2x1": 2,
"3x1": 3,
"4x1": 4,
"16x9": 16 / 9,
};
/**
* 根据图片原始比例推荐画幅:
* 与某个固定画幅差距 ≤15% 时推荐该画幅,否则推荐 auto(原图自适应)。
*/
export function suggestLogoSize(naturalW: number, naturalH: number): BrandLogoSize {
if (!naturalW || !naturalH) return "auto";
const ratio = naturalW / naturalH;
let best: BrandLogoSize = "sq";
let bestDelta = Infinity;
for (const [id, a] of Object.entries(BRAND_LOGO_SIZE_ASPECT) as [BrandLogoSize, number][]) {
const delta = Math.abs(ratio - a) / a;
if (delta < bestDelta) {
bestDelta = delta;
best = id;
}
}
return bestDelta <= 0.15 ? best : "auto";
}
export function brandLogoBox(size: BrandLogoSize): { w: number; h: number } {
const found = BRAND_LOGO_SIZES.find((item) => item.id === size);
return found ? { w: found.w, h: found.h } : { w: 36, h: 36 };
@@ -72,6 +101,13 @@ export function normalizeBrandLogoFit(raw: unknown): BrandLogoFit {
return "contain";
}
/** 标语是否在页眉显示;缺省 true(与后端缺省一致) */
export function normalizeBrandSloganVisible(raw: unknown): boolean {
if (typeof raw === "boolean") return raw;
if (typeof raw === "string") return raw.trim().toLowerCase() !== "false";
return true;
}
export const BRAND_LIMITS = {
wordmark: 16,
slogan: 32,