feat: 媒体库 blob 去重存储与品牌/注册等前端调整
新增: - blob_store 二进制去重存储 + blob_dedup 测试 其它: - 后端 settings/auth/media_library/upload/operations 服务与 handler 调整 - 前端品牌(BrandLockup/BrandCropModal/BrandSeoPanel)、注册、媒体库、分析详情页、Header/SiteChrome 等更新
This commit is contained in:
@@ -2,6 +2,7 @@ import {
|
||||
normalizeBrandLogoFit,
|
||||
normalizeBrandLogoSize,
|
||||
normalizeBrandMark,
|
||||
normalizeBrandSloganVisible,
|
||||
normalizeBrandURL,
|
||||
normalizeFooterLinks,
|
||||
normalizeKeywords,
|
||||
@@ -1041,6 +1042,8 @@ export interface PublicSettings {
|
||||
brand_logo_size: BrandLogoSize;
|
||||
/** 图槽铺法:cover 填充 / contain 适应 / repeat 平铺 / stretch 拉伸 */
|
||||
brand_logo_fit: BrandLogoFit;
|
||||
/** 标语是否在页眉显示;缺省 true */
|
||||
brand_slogan_visible: boolean;
|
||||
footer_links: FooterLink[];
|
||||
allow_register: boolean;
|
||||
/** 全站是否开放评论;关闭后前台不展示评论能力 */
|
||||
@@ -1144,6 +1147,7 @@ const DEFAULT_PUBLIC_SETTINGS: PublicSettings = {
|
||||
brand_mark: "image_text",
|
||||
brand_logo_size: "sq",
|
||||
brand_logo_fit: "contain",
|
||||
brand_slogan_visible: true,
|
||||
footer_links: [],
|
||||
allow_register: true,
|
||||
allow_comments: true,
|
||||
@@ -1235,6 +1239,7 @@ function normalizePublicSettings(data: Partial<PublicSettings> | null | undefine
|
||||
brand_mark: normalizeBrandMark(data?.brand_mark),
|
||||
brand_logo_size: normalizeBrandLogoSize(data?.brand_logo_size),
|
||||
brand_logo_fit: normalizeBrandLogoFit(data?.brand_logo_fit),
|
||||
brand_slogan_visible: normalizeBrandSloganVisible(data?.brand_slogan_visible),
|
||||
footer_links: normalizeFooterLinks(data?.footer_links),
|
||||
allow_register: data?.allow_register !== false,
|
||||
allow_comments: data?.allow_comments !== false,
|
||||
@@ -3760,6 +3765,7 @@ export type UpdateSiteSettingsBody = {
|
||||
brand_mark?: BrandMark;
|
||||
brand_logo_size?: BrandLogoSize;
|
||||
brand_logo_fit?: BrandLogoFit;
|
||||
brand_slogan_visible?: boolean;
|
||||
footer_links?: FooterLink[];
|
||||
allow_register?: boolean;
|
||||
allow_comments?: boolean;
|
||||
@@ -4825,6 +4831,7 @@ export interface MediaLibraryItem {
|
||||
height: number;
|
||||
uploader?: string;
|
||||
uploaded_at?: string;
|
||||
ref_count?: number; // 引用计数(>1 表示内容相同的重复上传共享此文件)
|
||||
source_url?: string; // 来源回链(帖子/评论锚点/用户主页)
|
||||
source_label?: string; // 来源描述(如:帖子《…》下的评论)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -119,3 +119,27 @@ export async function getCroppedBrandWebp(
|
||||
|
||||
return exportWebp(canvas, BRAND_MAX_BYTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 整图直传(「原图自适应」用):不做任何裁剪,仅按比例缩放到 BRAND_MAX_DIM 内并导出 WebP。
|
||||
* 页眉图框高度固定、宽度按图片原始比例自适应。
|
||||
*/
|
||||
export async function getFullBrandWebp(imageSrc: string): Promise<Blob> {
|
||||
const image = await loadImage(imageSrc);
|
||||
const canvas = document.createElement("canvas");
|
||||
|
||||
const scale = Math.min(1, BRAND_MAX_DIM / Math.max(image.naturalWidth, image.naturalHeight));
|
||||
const outW = Math.max(1, Math.round(image.naturalWidth * scale));
|
||||
const outH = Math.max(1, Math.round(image.naturalHeight * scale));
|
||||
|
||||
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, 0, 0, outW, outH);
|
||||
|
||||
return exportWebp(canvas, BRAND_MAX_BYTES);
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ export interface RtSettingsData {
|
||||
brand_mark?: string;
|
||||
brand_logo_size?: string;
|
||||
brand_logo_fit?: string;
|
||||
brand_slogan_visible?: boolean;
|
||||
footer_links?: { label: string; url: string; new_tab: boolean; align?: string }[];
|
||||
allow_register?: boolean;
|
||||
allow_comments?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user