feat(frontend): add customizable favicon and corner logo with live preview (#218)

* feat(frontend): add customizable favicon and corner logo with live preview

- Add app_logo and app_favicon config fields in config page
- Implement BrandLogo component with URL fallback to SVG logo
- Integrate BrandLogo into all layouts (admin, h5, h5-simple) and navbar
- Add real-time preview for favicon and logo in config page with error state
- Support both relative paths and full image URLs for branding assets
- Sync branding changes (app_name/app_logo/app_favicon) to site config on save

* fix(frontend): preserve tunnel node selection order

* fix(select): use useMemo for option label map to improve performance
This commit is contained in:
sagit
2026-02-27 11:50:03 +08:00
committed by GitHub
parent 6684a3426b
commit d2645589da
10 changed files with 345 additions and 146 deletions
+47 -4
View File
@@ -16,10 +16,15 @@ const getInitialConfig = () => {
version: VERSION,
app_version: APP_VERSION,
github_repo: GITHUB_REPO,
app_logo: "",
app_favicon: "",
};
}
const cachedAppName = localStorage.getItem(CACHE_PREFIX + "app_name");
const cachedAppLogo = localStorage.getItem(CACHE_PREFIX + "app_logo") || "";
const cachedAppFavicon =
localStorage.getItem(CACHE_PREFIX + "app_favicon") || "";
if (cachedAppName) {
return {
@@ -27,6 +32,8 @@ const getInitialConfig = () => {
version: VERSION,
app_version: APP_VERSION,
github_repo: GITHUB_REPO,
app_logo: cachedAppLogo,
app_favicon: cachedAppFavicon,
};
}
@@ -34,6 +41,9 @@ const getInitialConfig = () => {
name: "FLVX",
version: VERSION,
app_version: APP_VERSION,
github_repo: GITHUB_REPO,
app_logo: cachedAppLogo,
app_favicon: cachedAppFavicon,
};
};
@@ -99,7 +109,7 @@ export const getCachedConfig = async (key: string): Promise<string | null> => {
// 获取所有配置(优先从缓存)
export const getCachedConfigs = async (): Promise<Record<string, string>> => {
// 尝试从缓存获取所有配置
const configKeys = ["app_name"];
const configKeys = ["app_name", "app_logo", "app_favicon"];
const cachedConfigs: Record<string, string> = {};
let hasCachedData = false;
@@ -136,15 +146,48 @@ export const getCachedConfigs = async (): Promise<Record<string, string>> => {
return {};
};
const updateDocumentFavicon = (faviconUrl: string) => {
if (typeof document === "undefined") {
return;
}
const normalized = faviconUrl.trim() || "/favicon.ico";
const iconLinks = Array.from(
document.head.querySelectorAll<HTMLLinkElement>(
'link[rel="icon"], link[rel="shortcut icon"]',
),
);
if (iconLinks.length === 0) {
const link = document.createElement("link");
link.rel = "icon";
link.href = normalized;
document.head.appendChild(link);
return;
}
iconLinks.forEach((link) => {
link.href = normalized;
});
};
// 动态更新网站配置
export const updateSiteConfig = async () => {
const appName = await getCachedConfig("app_name");
const configMap = await getCachedConfigs();
const appName = (configMap.app_name || "").trim();
const appLogo = (configMap.app_logo || "").trim();
const appFavicon = (configMap.app_favicon || "").trim();
if (appName && appName !== siteConfig.name) {
siteConfig.name = appName;
// 更新页面标题
document.title = appName;
}
siteConfig.app_logo = appLogo;
siteConfig.app_favicon = appFavicon;
document.title = siteConfig.name;
updateDocumentFavicon(siteConfig.app_favicon);
};
// 清除配置缓存的工具函数