新增: - blob_store 二进制去重存储 + blob_dedup 测试 其它: - 后端 settings/auth/media_library/upload/operations 服务与 handler 调整 - 前端品牌(BrandLockup/BrandCropModal/BrandSeoPanel)、注册、媒体库、分析详情页、Header/SiteChrome 等更新
139 lines
4.7 KiB
TypeScript
139 lines
4.7 KiB
TypeScript
import { brandLogoBox, type BrandLogoFit, type BrandLogoSize, type BrandMark } from "@/lib/brand";
|
||
import { densitySrcSet, isOwnImage, pickVariantWidth, variantURL } from "@/lib/responsiveImage";
|
||
|
||
/** 页眉品牌锁:图加文字 / 纯图 / 纯文字。图槽按画幅占位,铺法决定图怎么填进框。 */
|
||
export default function BrandLockup({
|
||
mode,
|
||
size = "sq",
|
||
fit = "contain",
|
||
name,
|
||
wordmark = "",
|
||
slogan = "",
|
||
logoLight = "",
|
||
logoDark = "",
|
||
frame = false,
|
||
}: {
|
||
mode: BrandMark;
|
||
size?: BrandLogoSize;
|
||
fit?: BrandLogoFit;
|
||
name: string;
|
||
wordmark?: string;
|
||
slogan?: string;
|
||
logoLight?: string;
|
||
logoDark?: string;
|
||
/** 后台预览画出画幅框,方便看非正方形占位 */
|
||
frame?: boolean;
|
||
}) {
|
||
const label = wordmark.trim() || name;
|
||
const line = slogan.trim();
|
||
const showImage = mode !== "text";
|
||
const showText = mode !== "image";
|
||
const box = brandLogoBox(size);
|
||
// auto 图槽宽度随图(可达最宽画幅量级),按 176 取密度变体保证清晰
|
||
const cssPx = size === "auto" ? 176 : Math.max(box.w, box.h);
|
||
const src = logoLight || logoDark;
|
||
const both = !!(logoLight && logoDark && logoLight !== logoDark);
|
||
const useBox = showImage && (!!src || frame);
|
||
|
||
return (
|
||
<span className="j13-brand-lockup inline-flex items-center gap-2.5 min-w-0">
|
||
{showImage ? (
|
||
useBox ? (
|
||
<span
|
||
className="j13-brand-slot"
|
||
data-frame={frame ? "1" : undefined}
|
||
data-fit={src ? fit : undefined}
|
||
data-size={size}
|
||
style={{ width: box.w || undefined, height: box.h }}
|
||
>
|
||
{src ? (
|
||
both ? (
|
||
<>
|
||
<Logo src={logoLight} fit={fit} alt={label} decorative={showText} extra="j13-brand-logo-light" cssPx={cssPx} />
|
||
<Logo src={logoDark} fit={fit} alt={label} decorative={showText} extra="j13-brand-logo-dark" cssPx={cssPx} />
|
||
</>
|
||
) : (
|
||
<Logo src={src} fit={fit} alt={label} decorative={showText} cssPx={cssPx} />
|
||
)
|
||
) : (
|
||
<Bubble h={box.h} label={showText ? undefined : label} />
|
||
)}
|
||
</span>
|
||
) : (
|
||
<Bubble h={36} label={showText ? undefined : label} />
|
||
)
|
||
) : null}
|
||
{showText ? (
|
||
<span className="leading-none min-w-0">
|
||
<span className="block font-extrabold text-[17px] tracking-tight truncate" style={{ color: "var(--ink)" }}>
|
||
{label}
|
||
</span>
|
||
{line ? (
|
||
<span className="block text-[10.5px] mt-0.5 truncate" style={{ color: "var(--ink-3)" }}>
|
||
{line}
|
||
</span>
|
||
) : null}
|
||
</span>
|
||
) : null}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
function Logo({
|
||
src,
|
||
fit,
|
||
alt,
|
||
decorative = false,
|
||
extra = "",
|
||
cssPx,
|
||
}: {
|
||
src: string;
|
||
fit: BrandLogoFit;
|
||
alt: string;
|
||
/** 旁边已有站名时,读屏跳过,HTML 仍保留 alt 供检索 */
|
||
decorative?: boolean;
|
||
extra?: string;
|
||
/** 图槽最大边 CSS px,用于密度变体 */
|
||
cssPx: number;
|
||
}) {
|
||
if (fit === "repeat") {
|
||
return (
|
||
<span
|
||
role={decorative ? undefined : "img"}
|
||
aria-hidden={decorative || undefined}
|
||
aria-label={decorative ? undefined : alt}
|
||
className={`j13-brand-logo j13-brand-logo-tile ${extra}`}
|
||
style={{ backgroundImage: `url("${src.replace(/["\\\n\r]/g, "")}")` }}
|
||
/>
|
||
);
|
||
}
|
||
// 本站光栅图走 1x/2x 变体(SVG/ICO 后端透传);外链图保持原地址
|
||
const own = isOwnImage(src);
|
||
return (
|
||
/* eslint-disable-next-line @next/next/no-img-element */
|
||
<img
|
||
src={own ? variantURL(src, pickVariantWidth(cssPx)) : src}
|
||
srcSet={own ? densitySrcSet(src, cssPx) : undefined}
|
||
alt={alt}
|
||
aria-hidden={decorative || undefined}
|
||
className={`j13-brand-logo ${extra}`}
|
||
/>
|
||
);
|
||
}
|
||
|
||
function Bubble({ h, label }: { h: number; label?: string }) {
|
||
return (
|
||
<span
|
||
role={label ? "img" : undefined}
|
||
aria-label={label}
|
||
aria-hidden={label ? undefined : true}
|
||
className="j13-brand-bubble rounded-2xl flex items-center justify-center shrink-0 transition-transform duration-300 group-hover:rotate-[-6deg]"
|
||
style={{ width: h, height: h, background: "var(--accent)", color: "var(--accent-on)" }}
|
||
>
|
||
<svg aria-hidden="true" focusable="false" width="50%" height="50%" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
|
||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
|
||
</svg>
|
||
</span>
|
||
);
|
||
}
|