Compare commits

...

5 Commits

Author SHA1 Message Date
sagit be4b1f5f25 fix(ui): improve background handling and PWA update prompts (#419)
## Changes
- Add adaptive solid color background support for dark/light themes.
- Replace mesh gradient with semi-transparent blurred background images.
- Fix background scrolling issue on mobile browsers.
- Remove borders from specific cards for consistent UI styling.
- Prevent caching of PWA service worker files in Nginx.
- Introduce interactive toast prompt for PWA updates instead of silent
refresh.
2026-04-05 20:55:15 +08:00
sagitchu cd4238274a fix(ui): improve background handling and PWA update prompts
- Add adaptive solid color background support for dark/light themes.
- Replace mesh gradient with semi-transparent blurred background images.
- Fix background scrolling issue on mobile browsers.
- Remove borders from specific cards for consistent UI styling.
- Prevent caching of PWA service worker files in Nginx.
- Introduce interactive toast prompt for PWA updates instead of silent refresh.
2026-04-05 20:53:25 +08:00
sagit 25dc03c894 style: optimize card and table backgrounds for better custom background visibility (#418)
Changed multiple bg-white/40 and bg-white/60 classes to more transparent
variants like bg-white/10 and bg-white/20, or bg-transparent to allow
the custom background image to show through.
2026-04-05 18:47:13 +08:00
sagitchu 4cf9c9174a style: optimize card and table backgrounds for better custom background visibility 2026-04-05 18:45:17 +08:00
sagit a45bd43e73 feat: add global background image setting to config page (#417)
Add app_bg_image setting to /config page to allow global background
customization.
2026-04-05 18:36:31 +08:00
12 changed files with 157 additions and 33 deletions
+8
View File
@@ -43,6 +43,14 @@ http {
root /usr/share/nginx/html;
index index.html;
# PWA 和 Service Worker 不缓存
location ~* (sw\.js|service-worker\.js|workbox-.*\.js|manifest\.webmanifest)$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
try_files $uri =404;
}
# 静态文件缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 30d;
+19 -2
View File
@@ -95,11 +95,28 @@ function App() {
const updateBg = () => {
const customBg = siteConfig.app_bg_image;
if (customBg) {
document.documentElement.style.setProperty("--custom-bg-image", `url(${customBg})`);
document.documentElement.classList.add("has-custom-bg");
if (customBg === "theme") {
document.documentElement.style.removeProperty("--custom-bg-image");
document.documentElement.style.removeProperty("--custom-bg-color");
document.documentElement.classList.add("has-theme-bg");
document.documentElement.classList.remove("has-custom-bg");
} else if (customBg.startsWith("http") || customBg.startsWith("data:") || customBg.startsWith("/") || customBg.startsWith("blob:")) {
document.documentElement.style.setProperty("--custom-bg-image", `url(${customBg})`);
document.documentElement.style.setProperty("--custom-bg-color", "transparent");
document.documentElement.classList.add("has-custom-bg");
document.documentElement.classList.remove("has-theme-bg");
} else {
// Assume solid color like "#ffffff", "white", etc.
document.documentElement.style.setProperty("--custom-bg-image", "none");
document.documentElement.style.setProperty("--custom-bg-color", customBg);
document.documentElement.classList.add("has-custom-bg");
document.documentElement.classList.remove("has-theme-bg");
}
} else {
document.documentElement.style.removeProperty("--custom-bg-image");
document.documentElement.style.removeProperty("--custom-bg-color");
document.documentElement.classList.remove("has-custom-bg");
document.documentElement.classList.remove("has-theme-bg");
}
};
updateBg();
@@ -54,7 +54,7 @@ export const ThemeSettings: React.FC = () => {
};
return (
<Card className="border border-gray-200 dark:border-gray-700">
<Card className="shadow-md">
<CardBody className="p-6">
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-5">
主题设置
+31 -1
View File
@@ -1,12 +1,42 @@
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { registerSW } from "virtual:pwa-register";
import toast from "react-hot-toast";
import App from "./App.tsx";
import { Provider } from "./provider.tsx";
import "@/styles/globals.css";
registerSW({ immediate: true });
const updateSW = registerSW({
immediate: true,
onNeedRefresh() {
toast(
(t) => (
<div className="flex flex-col gap-3">
<span className="text-sm font-medium text-foreground">发现新版本,是否立即刷新以应用更新?</span>
<div className="flex gap-2 justify-end">
<button
className="px-3 py-1.5 text-xs font-medium bg-primary text-primary-foreground hover:bg-primary/90 rounded-md transition-colors"
onClick={() => {
updateSW(true);
toast.dismiss(t.id);
}}
>
刷新
</button>
<button
className="px-3 py-1.5 text-xs font-medium bg-default-200 text-default-700 hover:bg-default-300 rounded-md transition-colors"
onClick={() => toast.dismiss(t.id)}
>
稍后
</button>
</div>
</div>
),
{ duration: Infinity, position: "bottom-right" }
);
},
});
ReactDOM.createRoot(document.getElementById("root")!).render(
<BrowserRouter>
+41 -4
View File
@@ -621,10 +621,13 @@ export default function ConfigPage() {
const renderBgImageUploader = () => {
const bgImage = configs["app_bg_image"] || "";
const isImage = bgImage.startsWith("http") || bgImage.startsWith("data:") || bgImage.startsWith("/") || bgImage.startsWith("blob:");
const isTheme = bgImage === "theme";
const isSolidColor = bgImage && !isImage && !isTheme;
return (
<div className="flex flex-col gap-4 w-full">
<div className="flex items-center gap-4">
<div className="flex flex-wrap items-center gap-4">
<input
type="file"
accept="image/*"
@@ -638,7 +641,23 @@ export default function ConfigPage() {
onPress={() => bgImageFileInputRef.current?.click()}
isLoading={bgImageUploading}
>
选择图片
上传图片
</Button>
<Button
color="secondary"
variant="flat"
onPress={() => handleConfigChange("app_bg_image", "theme")}
isDisabled={bgImageUploading || isTheme}
>
自适应纯色 (跟随深色模式)
</Button>
<Button
color="default"
variant="flat"
onPress={() => handleConfigChange("app_bg_image", "#ffffff")}
isDisabled={bgImageUploading || bgImage === "#ffffff"}
>
白色纯色
</Button>
{bgImage && (
<Button
@@ -647,11 +666,12 @@ export default function ConfigPage() {
onPress={() => handleConfigChange("app_bg_image", "")}
isDisabled={bgImageUploading}
>
清除背景
恢复默认
</Button>
)}
</div>
{bgImage && (
{bgImage && isImage && (
<div className="relative rounded-xl overflow-hidden border border-divider">
<img
src={bgImage}
@@ -663,6 +683,23 @@ export default function ConfigPage() {
</div>
</div>
)}
{bgImage && isTheme && (
<div className="relative rounded-xl border border-divider bg-background h-32 flex items-center justify-center">
<span className="text-foreground text-sm font-medium">当前为自适应纯色背景</span>
</div>
)}
{bgImage && isSolidColor && (
<div
className="relative rounded-xl border border-divider h-32 flex items-center justify-center"
style={{ backgroundColor: bgImage }}
>
<span className="text-gray-500 bg-white/80 dark:bg-black/80 px-2 py-1 rounded text-sm font-medium border border-gray-200 dark:border-gray-800 shadow-sm">
纯色背景 ({bgImage})
</span>
</div>
)}
</div>
);
};
+4 -4
View File
@@ -4192,7 +4192,7 @@ export default function ForwardPage() {
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]",
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>
@@ -4327,7 +4327,7 @@ export default function ForwardPage() {
return (
<div
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 gap-2">
@@ -4638,10 +4638,10 @@ export default function ForwardPage() {
collapsed={collapsed}
countClassName="text-xs text-default-600"
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"
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={() =>
toggleTunnelGroupCollapsed(
group.userId,
+2 -2
View File
@@ -327,7 +327,7 @@ export default function LimitPage() {
<StaggerList className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
{filteredRules.map((rule) => (
<StaggerItem key={rule.id}>
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden h-full">
<Card className="shadow-sm overflow-hidden h-full">
<CardHeader className="pb-2 md:pb-2">
<div className="flex justify-between items-start w-full">
<div>
@@ -413,7 +413,7 @@ export default function LimitPage() {
)
) : (
/* 空状态 */
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
<Card className="shadow-sm bg-default-50/50">
<CardBody className="text-center py-20 flex flex-col items-center justify-center min-h-[240px]">
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
暂无限速规则
+3 -3
View File
@@ -172,7 +172,7 @@ export default function MonitorPage() {
<AnimatedPage className="px-3 lg:px-6 py-8">
{/* 顶部英雄数据指标 (Hero Metrics) */}
<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">
<span className="text-default-600 font-medium text-sm">System Load</span>
</div>
@@ -189,7 +189,7 @@ export default function MonitorPage() {
</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">
<span className="text-default-600 font-medium text-sm">Active Connections</span>
</div>
@@ -206,7 +206,7 @@ export default function MonitorPage() {
</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">
<span className="text-default-600 font-medium text-sm">Bandwidth</span>
</div>
+3 -3
View File
@@ -1770,7 +1770,7 @@ export default function NodePage() {
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]",
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>
@@ -2249,7 +2249,7 @@ export default function NodePage() {
)}
</span>
</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 ? (
<div className="flex flex-wrap gap-1">
{remoteUsage.usedPorts.map((port) => (
@@ -2285,7 +2285,7 @@ export default function NodePage() {
remoteUsage.bindings.map((binding) => (
<div
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">
<span
+1 -1
View File
@@ -1586,7 +1586,7 @@ export default function TunnelPage() {
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]",
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>
+43 -11
View File
@@ -227,24 +227,56 @@ body {
}
.bg-mesh-gradient {
background: radial-gradient(at 0% 0%, #ff9a9e 0%, transparent 50%),
radial-gradient(at 100% 0%, #fecfef 0%, transparent 50%),
radial-gradient(at 100% 100%, #c2e9fb 0%, transparent 50%),
radial-gradient(at 0% 100%, #a1c4fd 0%, transparent 50%);
background-color: #f2f2f7;
position: relative;
z-index: 0;
}
.dark .bg-mesh-gradient {
background: radial-gradient(at 0% 0%, #4a2b30 0%, transparent 50%),
radial-gradient(at 100% 0%, #493b45 0%, transparent 50%),
radial-gradient(at 100% 100%, #354756 0%, transparent 50%),
radial-gradient(at 0% 100%, #2f394f 0%, transparent 50%);
background-color: #1c1c1e;
.bg-mesh-gradient::before {
content: "";
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
background: url('https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=2564&auto=format&fit=crop') no-repeat center center;
background-size: cover;
}
:root.dark .bg-mesh-gradient {
background-color: #0b1020;
}
.dark .bg-mesh-gradient::before {
background: url('https://images.unsplash.com/photo-1557682250-33bd709cbe85?q=80&w=2564&auto=format&fit=crop') no-repeat center center;
background-size: cover;
}
:root.has-theme-bg .bg-mesh-gradient {
background-image: none !important;
background-color: #f6f7fb !important;
}
:root.dark.has-theme-bg .bg-mesh-gradient {
background-color: #0b1020 !important;
}
:root.has-theme-bg .bg-mesh-gradient::before {
background-image: none !important;
}
:root.has-custom-bg .bg-mesh-gradient {
background: var(--custom-bg-image) no-repeat center center fixed !important;
background-image: none !important;
background-color: var(--custom-bg-color, transparent) !important;
}
:root.has-custom-bg .bg-mesh-gradient::before {
background-image: var(--custom-bg-image, none) !important;
background-size: cover !important;
background-position: center center !important;
background-repeat: no-repeat !important;
}
@layer utilities {
+1 -1
View File
@@ -8,7 +8,7 @@ export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: "autoUpdate",
registerType: "prompt",
injectRegister: "auto",
includeAssets: ["favicon.ico", "apple-touch-icon.png"],
manifest: {