mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-11-16 01:24:24 +08:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 25dc03c894 | |||
| 4cf9c9174a | |||
| a45bd43e73 | |||
| 364bace87f | |||
| 2caafab937 | |||
| 901b903a70 | |||
| 930cc86614 |
@@ -963,7 +963,7 @@ func (h *Handler) updateSingleConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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("配置值不能为空"))
|
response.WriteJSON(w, response.ErrDefault("配置值不能为空"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="Pragma" content="no-cache" />
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
<meta name="theme-color" content="#2563eb" />
|
<meta name="theme-color" content="#2563eb" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
|
|||||||
@@ -45,10 +45,18 @@ http {
|
|||||||
|
|
||||||
# 静态文件缓存
|
# 静态文件缓存
|
||||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||||
expires 1y;
|
expires 30d;
|
||||||
add_header Cache-Control "public, immutable";
|
add_header Cache-Control "public, immutable";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# HTML 文件不缓存
|
||||||
|
location ~* \.(?:htm|html)$ {
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
|
add_header Pragma "no-cache";
|
||||||
|
add_header Expires "0";
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
# Vue Router的history模式支持
|
# Vue Router的history模式支持
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ interface ConfigItem {
|
|||||||
label: string;
|
label: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
type: "input" | "switch" | "select";
|
type: "input" | "switch" | "select" | "bg_image";
|
||||||
options?: { label: string; value: string; description?: string }[];
|
options?: { label: string; value: string; description?: string }[];
|
||||||
dependsOn?: string; // 依赖的配置项key
|
dependsOn?: string; // 依赖的配置项key
|
||||||
dependsValue?: string; // 依赖的配置项值
|
dependsValue?: string; // 依赖的配置项值
|
||||||
@@ -86,6 +86,12 @@ const toBrandAssetKind = (key: BrandPreviewKey): BrandAssetKind => {
|
|||||||
|
|
||||||
// 网站配置项定义
|
// 网站配置项定义
|
||||||
const CONFIG_ITEMS: ConfigItem[] = [
|
const CONFIG_ITEMS: ConfigItem[] = [
|
||||||
|
{
|
||||||
|
key: "app_bg_image",
|
||||||
|
label: "自定义背景",
|
||||||
|
description: "上传自定义背景图片(建议使用深色/浅色均可看清的图片,或使用半透明模糊效果)",
|
||||||
|
type: "bg_image",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "ip",
|
key: "ip",
|
||||||
label: "面板后端地址",
|
label: "面板后端地址",
|
||||||
@@ -254,6 +260,8 @@ export default function ConfigPage() {
|
|||||||
|
|
||||||
const logoFileInputRef = useRef<HTMLInputElement>(null);
|
const logoFileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const faviconFileInputRef = useRef<HTMLInputElement>(null);
|
const faviconFileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const bgImageFileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [bgImageUploading, setBgImageUploading] = useState(false);
|
||||||
|
|
||||||
const [announcement, setAnnouncement] = useState<AnnouncementData>({
|
const [announcement, setAnnouncement] = useState<AnnouncementData>({
|
||||||
content: "",
|
content: "",
|
||||||
@@ -547,6 +555,118 @@ export default function ConfigPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBgImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
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<string>((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 (
|
||||||
|
<div className="flex flex-col gap-4 w-full">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
ref={bgImageFileInputRef}
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleBgImageUpload}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
onPress={() => bgImageFileInputRef.current?.click()}
|
||||||
|
isLoading={bgImageUploading}
|
||||||
|
>
|
||||||
|
选择图片
|
||||||
|
</Button>
|
||||||
|
{bgImage && (
|
||||||
|
<Button
|
||||||
|
color="danger"
|
||||||
|
variant="flat"
|
||||||
|
onPress={() => handleConfigChange("app_bg_image", "")}
|
||||||
|
isDisabled={bgImageUploading}
|
||||||
|
>
|
||||||
|
清除背景
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{bgImage && (
|
||||||
|
<div className="relative rounded-xl overflow-hidden border border-divider">
|
||||||
|
<img
|
||||||
|
src={bgImage}
|
||||||
|
alt="背景预览"
|
||||||
|
className="w-full max-h-48 object-cover opacity-80"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent flex items-end p-4">
|
||||||
|
<span className="text-white text-sm font-medium">预览效果</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderBrandPreview = (key: BrandPreviewKey) => {
|
const renderBrandPreview = (key: BrandPreviewKey) => {
|
||||||
const previewUrl = (configs[key] || "").trim();
|
const previewUrl = (configs[key] || "").trim();
|
||||||
const appNamePreview = (configs.app_name || "").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";
|
const isCommercialDisabled = ["app_name", "app_logo", "app_favicon", "hide_footer_brand"].includes(item.key) && configs.is_commercial !== "true";
|
||||||
|
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
|
case "bg_image":
|
||||||
|
return renderBgImageUploader();
|
||||||
|
|
||||||
case "input":
|
case "input":
|
||||||
if (isBrandPreviewKey(item.key)) {
|
if (isBrandPreviewKey(item.key)) {
|
||||||
return renderBrandAssetUploader(item.key, isChanged);
|
return renderBrandAssetUploader(item.key, isChanged);
|
||||||
|
|||||||
@@ -4192,7 +4192,7 @@ export default function ForwardPage() {
|
|||||||
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-[24px]",
|
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-[24px]",
|
||||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
|
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
|
||||||
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
|
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
|
||||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors group",
|
tr: "hover:bg-white/10 dark:hover:bg-white/5 transition-colors group",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
@@ -4327,7 +4327,7 @@ export default function ForwardPage() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`grouped-table-${group.userId}-${group.userName}`}
|
key={`grouped-table-${group.userId}-${group.userName}`}
|
||||||
className="overflow-hidden rounded-2xl border border-white/80 dark:border-white/10 bg-white/60 dark:bg-zinc-900/60 backdrop-blur-3xl shadow-[0_15px_35px_rgba(0,0,0,0.1)]"
|
className="overflow-hidden rounded-2xl border border-white/80 dark:border-white/10 bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl shadow-[0_15px_35px_rgba(0,0,0,0.1)]"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between border-b border-white/20 dark:border-white/10 bg-white/40 dark:bg-black/40 backdrop-blur-md px-5 py-4">
|
<div className="flex items-center justify-between border-b border-white/20 dark:border-white/10 bg-white/40 dark:bg-black/40 backdrop-blur-md px-5 py-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -4638,10 +4638,10 @@ export default function ForwardPage() {
|
|||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
countClassName="text-xs text-default-600"
|
countClassName="text-xs text-default-600"
|
||||||
groupUserId={group.userId}
|
groupUserId={group.userId}
|
||||||
headerClassName="flex items-center justify-between rounded-lg bg-white/40 dark:bg-black/40 backdrop-blur-md border border-white/20 dark:border-white/10 px-3 py-2"
|
headerClassName="flex items-center justify-between rounded-lg bg-white/10 dark:bg-black/10 backdrop-blur-md border border-white/20 dark:border-white/10 px-3 py-2"
|
||||||
titleClassName="truncate text-sm font-semibold text-default-700"
|
titleClassName="truncate text-sm font-semibold text-default-700"
|
||||||
tunnel={tunnel}
|
tunnel={tunnel}
|
||||||
wrapperClassName="rounded-xl border border-divider bg-white/20 dark:bg-black/20 space-y-3"
|
wrapperClassName="rounded-xl border border-divider bg-white/5 dark:bg-black/5 space-y-3"
|
||||||
onToggleCollapsed={() =>
|
onToggleCollapsed={() =>
|
||||||
toggleTunnelGroupCollapsed(
|
toggleTunnelGroupCollapsed(
|
||||||
group.userId,
|
group.userId,
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ export default function MonitorPage() {
|
|||||||
<AnimatedPage className="px-3 lg:px-6 py-8">
|
<AnimatedPage className="px-3 lg:px-6 py-8">
|
||||||
{/* 顶部英雄数据指标 (Hero Metrics) */}
|
{/* 顶部英雄数据指标 (Hero Metrics) */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||||
<div className="rounded-3xl border border-white/80 dark:border-white/10 bg-white/60 dark:bg-zinc-900/60 backdrop-blur-3xl shadow-[0_10px_30px_rgba(0,0,0,0.1)] p-6 relative overflow-hidden flex flex-col justify-between h-40">
|
<div className="rounded-3xl border border-white/80 dark:border-white/10 bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl shadow-[0_10px_30px_rgba(0,0,0,0.1)] p-6 relative overflow-hidden flex flex-col justify-between h-40">
|
||||||
<div className="flex justify-between items-center z-10 relative">
|
<div className="flex justify-between items-center z-10 relative">
|
||||||
<span className="text-default-600 font-medium text-sm">System Load</span>
|
<span className="text-default-600 font-medium text-sm">System Load</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -189,7 +189,7 @@ export default function MonitorPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-3xl border border-white/80 dark:border-white/10 bg-white/60 dark:bg-zinc-900/60 backdrop-blur-3xl shadow-[0_10px_30px_rgba(0,0,0,0.1)] p-6 relative overflow-hidden flex flex-col justify-between h-40">
|
<div className="rounded-3xl border border-white/80 dark:border-white/10 bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl shadow-[0_10px_30px_rgba(0,0,0,0.1)] p-6 relative overflow-hidden flex flex-col justify-between h-40">
|
||||||
<div className="flex justify-between items-center z-10 relative">
|
<div className="flex justify-between items-center z-10 relative">
|
||||||
<span className="text-default-600 font-medium text-sm">Active Connections</span>
|
<span className="text-default-600 font-medium text-sm">Active Connections</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,7 +206,7 @@ export default function MonitorPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-3xl border border-white/80 dark:border-white/10 bg-white/60 dark:bg-zinc-900/60 backdrop-blur-3xl shadow-[0_10px_30px_rgba(0,0,0,0.1)] p-6 relative overflow-hidden flex flex-col justify-between h-40">
|
<div className="rounded-3xl border border-white/80 dark:border-white/10 bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl shadow-[0_10px_30px_rgba(0,0,0,0.1)] p-6 relative overflow-hidden flex flex-col justify-between h-40">
|
||||||
<div className="flex justify-between items-center z-10 relative">
|
<div className="flex justify-between items-center z-10 relative">
|
||||||
<span className="text-default-600 font-medium text-sm">Bandwidth</span>
|
<span className="text-default-600 font-medium text-sm">Bandwidth</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1770,7 +1770,7 @@ export default function NodePage() {
|
|||||||
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-2xl",
|
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-2xl",
|
||||||
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
|
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
|
||||||
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
|
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
|
||||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
tr: "hover:bg-white/10 dark:hover:bg-white/5 transition-colors",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
@@ -2249,7 +2249,7 @@ export default function NodePage() {
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-20 overflow-y-auto rounded bg-white/70 dark:bg-black/20 p-1.5 [scrollbar-width:thin] [&::-webkit-scrollbar]:w-1">
|
<div className="max-h-20 overflow-y-auto rounded bg-white/10 dark:bg-black/10 p-1.5 [scrollbar-width:thin] [&::-webkit-scrollbar]:w-1">
|
||||||
{remoteUsage.usedPorts.length > 0 ? (
|
{remoteUsage.usedPorts.length > 0 ? (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{remoteUsage.usedPorts.map((port) => (
|
{remoteUsage.usedPorts.map((port) => (
|
||||||
@@ -2285,7 +2285,7 @@ export default function NodePage() {
|
|||||||
remoteUsage.bindings.map((binding) => (
|
remoteUsage.bindings.map((binding) => (
|
||||||
<div
|
<div
|
||||||
key={binding.bindingId}
|
key={binding.bindingId}
|
||||||
className="rounded border border-default-200 dark:border-default-100/30 bg-white/70 dark:bg-black/20 p-2"
|
className="rounded border border-default-200 dark:border-default-100/30 bg-white/10 dark:bg-black/10 p-2"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useRef, ChangeEvent } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
deletePanelAddress,
|
deletePanelAddress,
|
||||||
validatePanelAddress,
|
validatePanelAddress,
|
||||||
} from "@/utils/panel";
|
} from "@/utils/panel";
|
||||||
import { siteConfig, updateSiteConfig } from "@/config/site";
|
|
||||||
|
|
||||||
interface PanelAddress {
|
interface PanelAddress {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -47,100 +46,9 @@ export const SettingsPage = () => {
|
|||||||
const [forwardCompactMode, setForwardCompactMode] = useState(false);
|
const [forwardCompactMode, setForwardCompactMode] = useState(false);
|
||||||
const [forwardCompactModeSaving, setForwardCompactModeSaving] =
|
const [forwardCompactModeSaving, setForwardCompactModeSaving] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [bgImage, setBgImage] = useState(() => siteConfig.app_bg_image || "");
|
|
||||||
const [bgImageSaving, setBgImageSaving] = useState(false);
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const admin = isAdmin();
|
const admin = isAdmin();
|
||||||
|
|
||||||
const handleBgImageUpload = async (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
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<string>((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[]) => {
|
const setPanelAddressesFunc = (newAddress: PanelAddress[]) => {
|
||||||
setPanelAddresses(newAddress);
|
setPanelAddresses(newAddress);
|
||||||
};
|
};
|
||||||
@@ -275,83 +183,6 @@ export const SettingsPage = () => {
|
|||||||
{/* 内容区域 */}
|
{/* 内容区域 */}
|
||||||
<div className="max-w-4xl mx-auto px-4 py-6">
|
<div className="max-w-4xl mx-auto px-4 py-6">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Card>
|
|
||||||
<CardBody className="p-6">
|
|
||||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
|
||||||
外观设置
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
自定义背景图片
|
|
||||||
</label>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
|
||||||
留空则使用系统默认的网格渐变背景。推荐上传 1920x1080 尺寸的图片。
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
{bgImage ? (
|
|
||||||
<div className="relative w-40 h-24 rounded-md overflow-hidden border border-gray-200 dark:border-gray-700 group">
|
|
||||||
<img src={bgImage} alt="Background Preview" className="w-full h-full object-cover" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-center w-40 h-24 rounded-md border border-dashed border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-400 text-xs">
|
|
||||||
无自定义背景
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
className="hidden"
|
|
||||||
ref={fileInputRef}
|
|
||||||
onChange={handleBgImageUpload}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
color="primary"
|
|
||||||
variant="flat"
|
|
||||||
size="sm"
|
|
||||||
isDisabled={!admin || bgImageSaving}
|
|
||||||
onPress={() => fileInputRef.current?.click()}
|
|
||||||
>
|
|
||||||
选择图片...
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end gap-2 mt-2">
|
|
||||||
{bgImage && (
|
|
||||||
<Button
|
|
||||||
color="danger"
|
|
||||||
variant="flat"
|
|
||||||
isDisabled={!admin || bgImageSaving}
|
|
||||||
onPress={() => {
|
|
||||||
setBgImage("");
|
|
||||||
void handleBgImageSave("");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
恢复默认
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
color="primary"
|
|
||||||
isDisabled={!admin}
|
|
||||||
isLoading={bgImageSaving}
|
|
||||||
onPress={() => handleBgImageSave()}
|
|
||||||
>
|
|
||||||
保存背景
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{!admin && (
|
|
||||||
<p className="mt-2 text-xs text-amber-600 dark:text-amber-400">
|
|
||||||
仅管理员可修改该全局配置。
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardBody className="p-6">
|
<CardBody className="p-6">
|
||||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||||
|
|||||||
@@ -1586,7 +1586,7 @@ export default function TunnelPage() {
|
|||||||
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-[24px]",
|
wrapper: "bg-transparent p-0 shadow-none border-none overflow-hidden rounded-[24px]",
|
||||||
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
|
th: "bg-transparent text-default-600 font-semibold text-sm border-b border-white/20 dark:border-white/10 py-3 uppercase tracking-wider first:rounded-tl-[24px] last:rounded-tr-[24px]",
|
||||||
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
|
td: "py-3 border-b border-divider/50 group-data-[last=true]:border-b-0",
|
||||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
tr: "hover:bg-white/10 dark:hover:bg-white/5 transition-colors",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
|
|||||||
Reference in New Issue
Block a user