feat(themes): 添加可编辑的站点外观和皮肤主题(#408)
This commit is contained in:
1 parent
c768fccbc2
commit
5e66cb7c45
70 files changed
+3654
-327
No files matched your search
@@ -1,9 +1,10 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
import type { PublicAppearance } from "@/services/api/appearance";
|
||||
import { applySkinTheme, DEFAULT_CLASSIC_SKIN, normalizeSkinDefinition } from "@/lib/skin-themes";
|
||||
|
||||
export const DEFAULT_PUBLIC_APPEARANCE: PublicAppearance = {
|
||||
schemaVersion: 3,
|
||||
schemaVersion: 6,
|
||||
brandName: "影策",
|
||||
brandSlug: "open-ai-canvas",
|
||||
authHeroTitle: "让一个故事,\n从文字走向银幕。",
|
||||
@@ -14,6 +15,13 @@ export const DEFAULT_PUBLIC_APPEARANCE: PublicAppearance = {
|
||||
authVideoUrl: "https://boss-shjd.biliapi.net/updream/aniforge/video/video_bbcb00bd-650d-4249-9346-5cd21fd2484c_m1hc-u0-1pu13x-3v1s.mp4",
|
||||
authVideoPosterUrl: "https://i0.hdslb.com/bfs/aitool/aniforge/image/02933f26-5f1b-49ff-a811-b7f95ee5e5b8_m1hc-u0-sau.jpg",
|
||||
skinId: "classic",
|
||||
activeSkin: DEFAULT_CLASSIC_SKIN,
|
||||
seoTitle: "影策",
|
||||
seoDescription: "影策,面向 AI 影视与短剧创作的工作台。",
|
||||
seoKeywords: "",
|
||||
footerCopyright: `© ${new Date().getFullYear()} 影策. All rights reserved.`,
|
||||
icpFilingEnabled: false,
|
||||
icpFilingNumber: "",
|
||||
logoConfigured: false,
|
||||
darkLogoConfigured: false,
|
||||
authVideoConfigured: false,
|
||||
@@ -42,11 +50,17 @@ export function normalizePublicAppearance(value?: Partial<PublicAppearance> | nu
|
||||
const customVideo = Boolean(value?.authVideoConfigured);
|
||||
const logoUrl = safeAppearanceURL(value?.logoUrl, DEFAULT_PUBLIC_APPEARANCE.logoUrl);
|
||||
const darkLogoUrl = safeAppearanceURL(value?.darkLogoUrl, logoUrl);
|
||||
const resolvedBrandName = brandName || DEFAULT_PUBLIC_APPEARANCE.brandName;
|
||||
const seoTitle = normalizeAppearanceCopy(value?.seoTitle, resolvedBrandName);
|
||||
const seoDescription = normalizeAppearanceCopy(value?.seoDescription, `${resolvedBrandName},面向 AI 影视与短剧创作的工作台。`, true);
|
||||
const seoKeywords = normalizeAppearanceCopy(value?.seoKeywords, "", true);
|
||||
const footerCopyright = normalizeAppearanceCopy(value?.footerCopyright, `© ${new Date().getFullYear()} ${resolvedBrandName}. All rights reserved.`);
|
||||
const icpFilingNumber = normalizeAppearanceCopy(value?.icpFilingNumber, "", true);
|
||||
return {
|
||||
...DEFAULT_PUBLIC_APPEARANCE,
|
||||
...value,
|
||||
schemaVersion: 3,
|
||||
brandName: brandName || DEFAULT_PUBLIC_APPEARANCE.brandName,
|
||||
schemaVersion: 6,
|
||||
brandName: resolvedBrandName,
|
||||
brandSlug,
|
||||
authHeroTitle,
|
||||
authHeroDescription,
|
||||
@@ -55,7 +69,14 @@ export function normalizePublicAppearance(value?: Partial<PublicAppearance> | nu
|
||||
logoFrameEnabled: value?.logoFrameEnabled !== false,
|
||||
authVideoUrl: safeAppearanceURL(value?.authVideoUrl, DEFAULT_PUBLIC_APPEARANCE.authVideoUrl),
|
||||
authVideoPosterUrl: safeAppearanceURL(value?.authVideoPosterUrl, customVideo ? "" : DEFAULT_PUBLIC_APPEARANCE.authVideoPosterUrl),
|
||||
skinId: value?.skinId === "classic" ? value.skinId : DEFAULT_PUBLIC_APPEARANCE.skinId,
|
||||
skinId: normalizeSkinDefinition(value?.activeSkin).id,
|
||||
activeSkin: normalizeSkinDefinition(value?.activeSkin),
|
||||
seoTitle,
|
||||
seoDescription,
|
||||
seoKeywords,
|
||||
footerCopyright,
|
||||
icpFilingEnabled: Boolean(value?.icpFilingEnabled && icpFilingNumber),
|
||||
icpFilingNumber,
|
||||
logoConfigured: Boolean(value?.logoConfigured),
|
||||
darkLogoConfigured: Boolean(value?.darkLogoConfigured),
|
||||
authVideoConfigured: customVideo,
|
||||
@@ -74,15 +95,25 @@ function normalizeAppearanceCopy(value: unknown, fallback: string, allowEmpty =
|
||||
export function commitPublicAppearance(value?: Partial<PublicAppearance> | null) {
|
||||
const appearance = normalizePublicAppearance(value);
|
||||
useAppearanceStore.getState().setAppearance(appearance);
|
||||
applySkinTheme(appearance.activeSkin, typeof document !== "undefined" && document.documentElement.classList.contains("dark") ? "dark" : "light");
|
||||
applyAppearanceMetadata(appearance);
|
||||
return appearance;
|
||||
}
|
||||
|
||||
export function applyAppearanceMetadata(appearance: PublicAppearance, targetDocument: Document | undefined = typeof document === "undefined" ? undefined : document) {
|
||||
if (!targetDocument) return;
|
||||
targetDocument.title = appearance.brandName;
|
||||
const description = targetDocument.querySelector<HTMLMetaElement>('meta[name="description"]');
|
||||
if (description) description.content = `${appearance.brandName},面向 AI 影视与短剧创作的工作台。`;
|
||||
targetDocument.title = appearance.seoTitle || appearance.brandName;
|
||||
setMeta(targetDocument, "name", "description", appearance.seoDescription);
|
||||
setMeta(targetDocument, "name", "keywords", appearance.seoKeywords);
|
||||
setMeta(targetDocument, "property", "og:title", appearance.seoTitle || appearance.brandName);
|
||||
setMeta(targetDocument, "property", "og:description", appearance.seoDescription);
|
||||
setMeta(targetDocument, "property", "og:site_name", appearance.brandName);
|
||||
setMeta(targetDocument, "property", "og:type", "website");
|
||||
setMeta(targetDocument, "name", "twitter:card", "summary");
|
||||
setMeta(targetDocument, "name", "twitter:title", appearance.seoTitle || appearance.brandName);
|
||||
setMeta(targetDocument, "name", "twitter:description", appearance.seoDescription);
|
||||
const mode = targetDocument.documentElement.classList.contains("dark") ? "dark" : "light";
|
||||
setMeta(targetDocument, "name", "theme-color", appearance.activeSkin.tokens[mode].canvas);
|
||||
let favicon = targetDocument.querySelector<HTMLLinkElement>('link[rel~="icon"]');
|
||||
if (!favicon) {
|
||||
favicon = targetDocument.createElement("link");
|
||||
@@ -90,6 +121,31 @@ export function applyAppearanceMetadata(appearance: PublicAppearance, targetDocu
|
||||
targetDocument.head.appendChild(favicon);
|
||||
}
|
||||
favicon.href = appearanceLogoURL(appearance, targetDocument.documentElement.classList.contains("dark") ? "dark" : "light");
|
||||
|
||||
const location = targetDocument.defaultView?.location;
|
||||
if (location && (location.protocol === "http:" || location.protocol === "https:")) {
|
||||
let canonical = targetDocument.querySelector<HTMLLinkElement>('link[rel="canonical"]');
|
||||
if (!canonical) {
|
||||
canonical = targetDocument.createElement("link");
|
||||
canonical.rel = "canonical";
|
||||
targetDocument.head.appendChild(canonical);
|
||||
}
|
||||
canonical.href = `${location.origin}${location.pathname}`;
|
||||
}
|
||||
}
|
||||
|
||||
function setMeta(targetDocument: Document, attribute: "name" | "property", key: string, content: string) {
|
||||
let element = targetDocument.querySelector<HTMLMetaElement>(`meta[${attribute}="${key}"]`);
|
||||
if (!content) {
|
||||
element?.remove();
|
||||
return;
|
||||
}
|
||||
if (!element) {
|
||||
element = targetDocument.createElement("meta");
|
||||
element.setAttribute(attribute, key);
|
||||
targetDocument.head.appendChild(element);
|
||||
}
|
||||
element.content = content;
|
||||
}
|
||||
|
||||
export function appearanceLogoURL(appearance: PublicAppearance, theme: "light" | "dark") {
|
||||
|
||||
Reference in new issue
Block a user