From 2caafab937de1f306d839b9a823b29f975b8c3c4 Mon Sep 17 00:00:00 2001 From: sagitchu Date: Sun, 5 Apr 2026 18:31:59 +0800 Subject: [PATCH 1/2] feat: add global background image setting to config page --- go-backend/internal/http/handler/handler.go | 2 +- vite-frontend/src/pages/config.tsx | 125 +++++++++++++++++++- vite-frontend/src/pages/settings.tsx | 91 -------------- 3 files changed, 125 insertions(+), 93 deletions(-) diff --git a/go-backend/internal/http/handler/handler.go b/go-backend/internal/http/handler/handler.go index 815bd58..e755e7a 100644 --- a/go-backend/internal/http/handler/handler.go +++ b/go-backend/internal/http/handler/handler.go @@ -963,7 +963,7 @@ func (h *Handler) updateSingleConfig(w http.ResponseWriter, r *http.Request) { return } - if value == "" && name != "app_logo" && name != "app_favicon" { + if value == "" && name != "app_logo" && name != "app_favicon" && name != "app_bg_image" { response.WriteJSON(w, response.ErrDefault("配置值不能为空")) return } diff --git a/vite-frontend/src/pages/config.tsx b/vite-frontend/src/pages/config.tsx index 283d712..d93678c 100644 --- a/vite-frontend/src/pages/config.tsx +++ b/vite-frontend/src/pages/config.tsx @@ -65,7 +65,7 @@ interface ConfigItem { label: string; placeholder?: string; description?: string; - type: "input" | "switch" | "select"; + type: "input" | "switch" | "select" | "bg_image"; options?: { label: string; value: string; description?: string }[]; dependsOn?: string; // 依赖的配置项key dependsValue?: string; // 依赖的配置项值 @@ -86,6 +86,12 @@ const toBrandAssetKind = (key: BrandPreviewKey): BrandAssetKind => { // 网站配置项定义 const CONFIG_ITEMS: ConfigItem[] = [ + { + key: "app_bg_image", + label: "自定义背景", + description: "上传自定义背景图片(建议使用深色/浅色均可看清的图片,或使用半透明模糊效果)", + type: "bg_image", + }, { key: "ip", label: "面板后端地址", @@ -254,6 +260,8 @@ export default function ConfigPage() { const logoFileInputRef = useRef(null); const faviconFileInputRef = useRef(null); + const bgImageFileInputRef = useRef(null); + const [bgImageUploading, setBgImageUploading] = useState(false); const [announcement, setAnnouncement] = useState({ content: "", @@ -547,6 +555,118 @@ export default function ConfigPage() { } }; + const handleBgImageUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + if (!file.type.startsWith("image/")) { + toast.error("只能上传图片文件"); + return; + } + + setBgImageUploading(true); + try { + const compressedImage = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (event) => { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement("canvas"); + let width = img.width; + let height = img.height; + const MAX_WIDTH = 1920; + const MAX_HEIGHT = 1080; + + if (width > height) { + if (width > MAX_WIDTH) { + height = Math.round((height * MAX_WIDTH) / width); + width = MAX_WIDTH; + } + } else { + if (height > MAX_HEIGHT) { + width = Math.round((width * MAX_HEIGHT) / height); + height = MAX_HEIGHT; + } + } + + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext("2d"); + if (!ctx) { + reject(new Error("Canvas context is null")); + return; + } + ctx.drawImage(img, 0, 0, width, height); + + // Output as webp for better compression + const dataUrl = canvas.toDataURL("image/webp", 0.8); + resolve(dataUrl); + }; + img.onerror = () => reject(new Error("图片加载失败")); + img.src = event.target?.result as string; + }; + reader.onerror = () => reject(new Error("文件读取失败")); + reader.readAsDataURL(file); + }); + + handleConfigChange("app_bg_image", compressedImage); + toast.success("自定义背景上传成功"); + } catch (error) { + toast.error(error instanceof Error ? error.message : "图片处理失败"); + } finally { + setBgImageUploading(false); + e.target.value = ""; + } + }; + + const renderBgImageUploader = () => { + const bgImage = configs["app_bg_image"] || ""; + + return ( +
+
+ + + {bgImage && ( + + )} +
+ {bgImage && ( +
+ 背景预览 +
+ 预览效果 +
+
+ )} +
+ ); + }; + const renderBrandPreview = (key: BrandPreviewKey) => { const previewUrl = (configs[key] || "").trim(); const appNamePreview = (configs.app_name || "").trim() || "应用名称"; @@ -699,6 +819,9 @@ export default function ConfigPage() { const isCommercialDisabled = ["app_name", "app_logo", "app_favicon", "hide_footer_brand"].includes(item.key) && configs.is_commercial !== "true"; switch (item.type) { + case "bg_image": + return renderBgImageUploader(); + case "input": if (isBrandPreviewKey(item.key)) { return renderBrandAssetUploader(item.key, isChanged); diff --git a/vite-frontend/src/pages/settings.tsx b/vite-frontend/src/pages/settings.tsx index 48dae5e..1216373 100644 --- a/vite-frontend/src/pages/settings.tsx +++ b/vite-frontend/src/pages/settings.tsx @@ -47,100 +47,9 @@ export const SettingsPage = () => { const [forwardCompactMode, setForwardCompactMode] = useState(false); const [forwardCompactModeSaving, setForwardCompactModeSaving] = useState(false); - const [bgImage, setBgImage] = useState(() => siteConfig.app_bg_image || ""); - const [bgImageSaving, setBgImageSaving] = useState(false); - const fileInputRef = useRef(null); const admin = isAdmin(); - const handleBgImageUpload = async (e: ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; - - if (!file.type.startsWith("image/")) { - toast.error("只能上传图片文件"); - return; - } - - setBgImageSaving(true); - try { - const compressedImage = await new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = (event) => { - const img = new Image(); - img.onload = () => { - const canvas = document.createElement("canvas"); - let width = img.width; - let height = img.height; - const MAX_WIDTH = 1920; - const MAX_HEIGHT = 1080; - - if (width > height) { - if (width > MAX_WIDTH) { - height = Math.round((height * MAX_WIDTH) / width); - width = MAX_WIDTH; - } - } else { - if (height > MAX_HEIGHT) { - width = Math.round((width * MAX_HEIGHT) / height); - height = MAX_HEIGHT; - } - } - - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext("2d"); - if (!ctx) { - reject(new Error("Canvas context is null")); - return; - } - ctx.drawImage(img, 0, 0, width, height); - - // Output as webp for better compression - const dataUrl = canvas.toDataURL("image/webp", 0.8); - resolve(dataUrl); - }; - img.onerror = () => reject(new Error("图片加载失败")); - img.src = event.target?.result as string; - }; - reader.onerror = () => reject(new Error("文件读取失败")); - reader.readAsDataURL(file); - }); - - setBgImage(compressedImage); - // Optional: automatically save after processing - // void handleBgImageSave(compressedImage); - } catch (error) { - toast.error(error instanceof Error ? error.message : "图片处理失败"); - } finally { - setBgImageSaving(false); - // Reset input value so the same file can be selected again if needed - e.target.value = ""; - } - }; - - const handleBgImageSave = async (valueToSave?: string) => { - if (!admin || bgImageSaving) return; - setBgImageSaving(true); - try { - const finalValue = typeof valueToSave === "string" ? valueToSave : bgImage; - const response = await updateConfig("app_bg_image", finalValue); - if (response.code === 0) { - siteConfig.app_bg_image = finalValue; - // 触发配置更新,刷新相关页面或样式 - await updateSiteConfig(); - window.dispatchEvent(new Event("site-config-updated")); - toast.success("背景图片设置已保存"); - } else { - toast.error(response.msg || "保存背景失败"); - } - } catch { - toast.error("保存背景失败"); - } finally { - setBgImageSaving(false); - } - }; - const setPanelAddressesFunc = (newAddress: PanelAddress[]) => { setPanelAddresses(newAddress); }; From 364bace87f60f5ae06490d8f9a8defcd86a7944e Mon Sep 17 00:00:00 2001 From: sagitchu Date: Sun, 5 Apr 2026 18:35:07 +0800 Subject: [PATCH 2/2] fix: remove unused imports and leftover jsx in settings --- vite-frontend/src/pages/settings.tsx | 80 +--------------------------- 1 file changed, 1 insertion(+), 79 deletions(-) diff --git a/vite-frontend/src/pages/settings.tsx b/vite-frontend/src/pages/settings.tsx index 1216373..ef6bebc 100644 --- a/vite-frontend/src/pages/settings.tsx +++ b/vite-frontend/src/pages/settings.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, ChangeEvent } from "react"; +import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import toast from "react-hot-toast"; @@ -23,7 +23,6 @@ import { deletePanelAddress, validatePanelAddress, } from "@/utils/panel"; -import { siteConfig, updateSiteConfig } from "@/config/site"; interface PanelAddress { name: string; @@ -184,83 +183,6 @@ export const SettingsPage = () => { {/* 内容区域 */}
- - -

- 外观设置 -

-
-
-
- -

- 留空则使用系统默认的网格渐变背景。推荐上传 1920x1080 尺寸的图片。 -

-
- {bgImage ? ( -
- Background Preview -
- ) : ( -
- 无自定义背景 -
- )} -
- - -
-
-
-
- {bgImage && ( - - )} - -
- {!admin && ( -

- 仅管理员可修改该全局配置。 -

- )} -
-
-
-
-