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>
|
||||
|
||||
Reference in New Issue
Block a user