import { useState, useEffect, useRef, ChangeEvent } from "react"; import { useNavigate } from "react-router-dom"; import toast from "react-hot-toast"; import { Input } from "@/shadcn-bridge/heroui/input"; import { Button } from "@/shadcn-bridge/heroui/button"; import { Card, CardBody } from "@/shadcn-bridge/heroui/card"; import { Select, SelectItem } from "@/shadcn-bridge/heroui/select"; import { Switch } from "@/shadcn-bridge/heroui/switch"; import { reinitializeBaseURL } from "@/api/network"; import { getConfigByName, updateConfig } from "@/api"; import { BackIcon } from "@/components/icons"; import { type UpdateReleaseChannel, getUpdateReleaseChannel, setUpdateReleaseChannel, } from "@/utils/version-update"; import { isAdmin } from "@/utils/auth"; import { getPanelAddresses, savePanelAddress, setCurrentPanelAddress, deletePanelAddress, validatePanelAddress, } from "@/utils/panel"; import { siteConfig, updateSiteConfig } from "@/config/site"; interface PanelAddress { name: string; address: string; inx: boolean; } const FORWARD_COMPACT_MODE_CONFIG_KEY = "forward_compact_mode"; const parseBooleanConfig = (value: unknown, defaultValue: boolean) => typeof value === "string" ? value === "true" : defaultValue; export const SettingsPage = () => { const navigate = useNavigate(); const [panelAddresses, setPanelAddresses] = useState([]); const [newName, setNewName] = useState(""); const [newAddress, setNewAddress] = useState(""); const [updateChannel, setUpdateChannel] = useState( getUpdateReleaseChannel(), ); 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); }; useEffect(() => { (window as any).setPanelAddresses = setPanelAddressesFunc; return () => { delete (window as any).setPanelAddresses; }; }, []); // 加载面板地址列表 const loadPanelAddresses = async () => { getPanelAddresses(); }; // 添加新面板地址 const addPanelAddress = async () => { if (!newName.trim() || !newAddress.trim()) { toast.error("请输入名称和地址"); return; } // 验证地址格式 if (!validatePanelAddress(newAddress.trim())) { toast.error( "地址格式不正确,请检查:\n• 必须是完整的URL格式\n• 必须以 http:// 或 https:// 开头\n• 支持域名、IPv4、IPv6 地址\n• 端口号范围:1-65535\n• 示例:http://192.168.1.100:3000", ); return; } savePanelAddress(newName.trim(), newAddress.trim()); setNewName(""); setNewAddress(""); toast.success("添加成功"); }; // 设置当前面板地址 const setCurrentPanel = async (name: string) => { setCurrentPanelAddress(name); reinitializeBaseURL(); }; // 删除面板地址 const handleDeletePanelAddress = async (name: string) => { deletePanelAddress(name); reinitializeBaseURL(); toast.success("删除成功"); }; // 页面加载时获取数据 useEffect(() => { loadPanelAddresses(); loadForwardCompactMode(); }, []); const loadForwardCompactMode = async () => { try { const res = await getConfigByName(FORWARD_COMPACT_MODE_CONFIG_KEY); setForwardCompactMode(parseBooleanConfig(res.data?.value, false)); } catch { setForwardCompactMode(false); } }; const handleForwardCompactModeChange = async (enabled: boolean) => { if (!admin || forwardCompactModeSaving) { return; } const previous = forwardCompactMode; setForwardCompactMode(enabled); setForwardCompactModeSaving(true); try { const response = await updateConfig( FORWARD_COMPACT_MODE_CONFIG_KEY, enabled ? "true" : "false", ); if (response.code === 0) { toast.success(`规则页面精简模式已${enabled ? "开启" : "关闭"}`); window.dispatchEvent( new CustomEvent("forwardCompactModeChanged", { detail: { enabled }, }), ); } else { setForwardCompactMode(previous); toast.error(response.msg || "保存精简模式失败"); } } catch { setForwardCompactMode(previous); toast.error("保存精简模式失败"); } finally { setForwardCompactModeSaving(false); } }; const handleUpdateChannelChange = (channel: UpdateReleaseChannel) => { setUpdateChannel(channel); setUpdateReleaseChannel(channel); toast.success( `更新通道已切换为${channel === "stable" ? "稳定版" : "开发版"}`, ); }; return (
{/* 顶部导航 */}

面板设置

{/* 内容区域 */}

外观设置

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

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

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

)}

更新设置

版本提示会根据该通道检查最新版本。

显示设置

规则页面精简模式

开启后,规则页面列表使用 2.1.6-alpha8 样式。{" "}

{!admin && (

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

)}
{/* 添加新地址 */}

添加新面板地址

setNewName(e.target.value)} /> setNewAddress(e.target.value)} />
{/* 地址列表 */}

已保存的面板地址

{panelAddresses.length === 0 ? (

暂无保存的面板地址

) : (
{panelAddresses.map((panel, index) => (
{panel.name} {panel.inx && ( 当前 )}

{panel.address}

{!panel.inx && ( )}
))}
)}
); };