feat: 媒体库 blob 去重存储与品牌/注册等前端调整
新增: - blob_store 二进制去重存储 + blob_dedup 测试 其它: - 后端 settings/auth/media_library/upload/operations 服务与 handler 调整 - 前端品牌(BrandLockup/BrandCropModal/BrandSeoPanel)、注册、媒体库、分析详情页、Header/SiteChrome 等更新
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import { X } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { AdminField, AdminSettingsActions, AdminSettingsGroup } from "@/components/admin";
|
||||
import { AdminField, AdminSettingsActions, AdminSettingsGroup, AdminSwitch } from "@/components/admin";
|
||||
import { apiUpdateSiteSettings, apiOperations, type PublicSettings } from "@/lib/api";
|
||||
import { BRAND_LIMITS, runeCount, siteDocumentTitle } from "@/lib/brand";
|
||||
import { toast } from "@/lib/toast";
|
||||
@@ -25,6 +25,7 @@ export default function BasicIdentityPanel({
|
||||
}, []);
|
||||
const [wordmark, setWordmark] = useState(initial.site_wordmark);
|
||||
const [slogan, setSlogan] = useState(initial.site_slogan);
|
||||
const [sloganVisible, setSloganVisible] = useState(initial.brand_slogan_visible);
|
||||
const [desc, setDesc] = useState(initial.site_description);
|
||||
const [keywords, setKeywords] = useState<string[]>(initial.site_keywords);
|
||||
const [kwInput, setKwInput] = useState("");
|
||||
@@ -32,6 +33,7 @@ export default function BasicIdentityPanel({
|
||||
name: initial.site_name,
|
||||
wordmark: initial.site_wordmark,
|
||||
slogan: initial.site_slogan,
|
||||
sloganVisible: initial.brand_slogan_visible,
|
||||
desc: initial.site_description,
|
||||
keywords: initial.site_keywords,
|
||||
});
|
||||
@@ -44,6 +46,7 @@ export default function BasicIdentityPanel({
|
||||
name.trim() !== saved.name ||
|
||||
wordmark.trim() !== saved.wordmark ||
|
||||
slogan.trim() !== saved.slogan ||
|
||||
sloganVisible !== saved.sloganVisible ||
|
||||
desc.trim() !== saved.desc ||
|
||||
JSON.stringify(keywords) !== JSON.stringify(saved.keywords);
|
||||
|
||||
@@ -85,6 +88,7 @@ export default function BasicIdentityPanel({
|
||||
setName(saved.name);
|
||||
setWordmark(saved.wordmark);
|
||||
setSlogan(saved.slogan);
|
||||
setSloganVisible(saved.sloganVisible);
|
||||
setDesc(saved.desc);
|
||||
setKeywords([...saved.keywords]);
|
||||
setKwInput("");
|
||||
@@ -98,6 +102,7 @@ export default function BasicIdentityPanel({
|
||||
name: name.trim(),
|
||||
wordmark: wordmark.trim(),
|
||||
slogan: slogan.trim(),
|
||||
sloganVisible,
|
||||
desc: desc.trim(),
|
||||
keywords: [...keywords],
|
||||
};
|
||||
@@ -115,6 +120,7 @@ export default function BasicIdentityPanel({
|
||||
site_name: snap.name,
|
||||
site_wordmark: snap.wordmark,
|
||||
site_slogan: snap.slogan,
|
||||
brand_slogan_visible: snap.sloganVisible,
|
||||
site_description: snap.desc,
|
||||
site_keywords: snap.keywords,
|
||||
});
|
||||
@@ -123,12 +129,14 @@ export default function BasicIdentityPanel({
|
||||
name: res.site_name,
|
||||
wordmark: res.site_wordmark,
|
||||
slogan: res.site_slogan,
|
||||
sloganVisible: res.brand_slogan_visible,
|
||||
desc: res.site_description,
|
||||
keywords: res.site_keywords,
|
||||
});
|
||||
setName((v) => (v.trim() === snap.name ? res.site_name : v));
|
||||
setWordmark((v) => (v.trim() === snap.wordmark ? res.site_wordmark : v));
|
||||
setSlogan((v) => (v.trim() === snap.slogan ? res.site_slogan : v));
|
||||
setSloganVisible((v) => (v === snap.sloganVisible ? res.brand_slogan_visible : v));
|
||||
setDesc((v) => (v.trim() === snap.desc ? res.site_description : v));
|
||||
setKeywords((prev) =>
|
||||
JSON.stringify(prev) === JSON.stringify(snap.keywords) ? res.site_keywords : prev,
|
||||
@@ -199,18 +207,49 @@ export default function BasicIdentityPanel({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AdminField
|
||||
id="site-slogan"
|
||||
label="标语"
|
||||
className="admin-identity-field"
|
||||
maxLength={BRAND_LIMITS.slogan}
|
||||
value={slogan}
|
||||
onChange={(v) => {
|
||||
setSlogan(v);
|
||||
clearSaveError();
|
||||
}}
|
||||
placeholder="技术分享与讨论"
|
||||
/>
|
||||
<div className="admin-identity-field">
|
||||
<div className="admin-field-head">
|
||||
<label className="field-label" htmlFor="site-slogan">
|
||||
标语
|
||||
</label>
|
||||
<span className="admin-field-count" aria-hidden>
|
||||
{slogan.length}/{BRAND_LIMITS.slogan}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-2 mb-2"
|
||||
title={slogan.trim() ? "在页眉站名下方显示副标题" : "先填写标语后可开启"}
|
||||
>
|
||||
<AdminSwitch
|
||||
checked={sloganVisible}
|
||||
disabled={!slogan.trim()}
|
||||
onChange={() => {
|
||||
setSloganVisible((v) => !v);
|
||||
clearSaveError();
|
||||
}}
|
||||
label="页眉显示标语"
|
||||
/>
|
||||
<span
|
||||
className="text-[12px]"
|
||||
style={{ color: slogan.trim() ? "var(--ink-2)" : "var(--ink-3)" }}
|
||||
>
|
||||
页眉显示标语
|
||||
</span>
|
||||
</div>
|
||||
<div className="admin-field-shell">
|
||||
<input
|
||||
id="site-slogan"
|
||||
className="admin-field-input"
|
||||
maxLength={BRAND_LIMITS.slogan}
|
||||
value={slogan}
|
||||
onChange={(e) => {
|
||||
setSlogan(e.target.value);
|
||||
clearSaveError();
|
||||
}}
|
||||
placeholder="技术分享与讨论"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="admin-identity-title-preview" aria-live="polite">
|
||||
浏览器标题预览 · <strong>{preview}</strong>
|
||||
</p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Check, ImagePlus, Images, Link2, X } from "lucide-react";
|
||||
import { Check, Crop, ImagePlus, Images, Link2, X } from "lucide-react";
|
||||
import { useEffect, useRef, useState, type Dispatch, type SetStateAction } from "react";
|
||||
import {
|
||||
AdminSegmented,
|
||||
@@ -11,8 +11,6 @@ import BrandCropModal from "@/components/BrandCropModal";
|
||||
import BrandLockup from "@/components/BrandLockup";
|
||||
import { apiBrandFromMedia, apiUpdateSiteSettings, apiUploadBrand, type PublicSettings } from "@/lib/api";
|
||||
import {
|
||||
BRAND_LOGO_FITS,
|
||||
BRAND_LOGO_SIZES,
|
||||
brandAccept,
|
||||
brandAliasPath,
|
||||
brandFileOk,
|
||||
@@ -313,6 +311,17 @@ export default function BrandSeoPanel({
|
||||
>
|
||||
<Images size={14} /> 库
|
||||
</button>
|
||||
{preview && !/\.(svg|ico)(\?.*)?$/i.test(preview) ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-line btn-sm"
|
||||
disabled={saving}
|
||||
title="调整画幅、铺法或重新裁剪"
|
||||
onClick={() => openCrop(slot, null, preview)}
|
||||
>
|
||||
<Crop size={14} /> 调整
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="admin-icon-btn"
|
||||
@@ -361,7 +370,7 @@ export default function BrandSeoPanel({
|
||||
frame
|
||||
name={initial.site_name}
|
||||
wordmark={initial.site_wordmark}
|
||||
slogan={initial.site_slogan}
|
||||
slogan={initial.brand_slogan_visible ? initial.site_slogan : ""}
|
||||
logoLight={previewOf(light)}
|
||||
logoDark={previewOf(dark)}
|
||||
/>
|
||||
@@ -388,46 +397,9 @@ export default function BrandSeoPanel({
|
||||
/>
|
||||
</div>
|
||||
{mark !== "text" ? (
|
||||
<>
|
||||
<div className="admin-brand-studio-field">
|
||||
<span className="admin-brand-studio-kicker">比例</span>
|
||||
<div className="admin-brand-size-row" role="group" aria-label="Logo 比例">
|
||||
{BRAND_LOGO_SIZES.map((item) => {
|
||||
const on = logoSize === item.id;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
className={`admin-brand-size-chip${on ? " is-on" : ""}`}
|
||||
onClick={() => {
|
||||
setLogoSize(item.id);
|
||||
if (saveError) setSaveError("");
|
||||
}}
|
||||
>
|
||||
<span className="admin-brand-size-chip-label">{item.label}</span>
|
||||
<span className="admin-brand-size-chip-px">
|
||||
{item.w}×{item.h}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-brand-studio-field">
|
||||
<span className="admin-brand-studio-kicker">铺法</span>
|
||||
<AdminSegmented
|
||||
role="group"
|
||||
size="sm"
|
||||
ariaLabel="适配方式"
|
||||
value={logoFit}
|
||||
onChange={(v) => {
|
||||
setLogoFit(v);
|
||||
if (saveError) setSaveError("");
|
||||
}}
|
||||
options={BRAND_LOGO_FITS.map((item) => ({ key: item.id, label: item.label }))}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
<p className="admin-brand-studio-field text-[11.5px] self-center mb-0.5" style={{ color: "var(--ink-3)" }}>
|
||||
画幅与铺法在上传/调整图片的裁剪弹窗中设置
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -471,6 +443,16 @@ export default function BrandSeoPanel({
|
||||
slot={cropSlot}
|
||||
seedFile={cropSeedFile}
|
||||
seedUrl={cropSeedUrl}
|
||||
logoSize={logoSize}
|
||||
logoFit={logoFit}
|
||||
onSizeChange={(s) => {
|
||||
setLogoSize(s);
|
||||
if (saveError) setSaveError("");
|
||||
}}
|
||||
onFitChange={(f) => {
|
||||
setLogoFit(f);
|
||||
if (saveError) setSaveError("");
|
||||
}}
|
||||
onClose={closeCrop}
|
||||
onSeedConsumed={() => {
|
||||
setCropSeedFile(null);
|
||||
|
||||
Reference in New Issue
Block a user