mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-11-16 01:24:24 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1987960fce | |||
| 4c782eb815 | |||
| 1e3b38f427 | |||
| 2166936f57 | |||
| 53e1efcdc1 | |||
| 71d5c36ec3 | |||
| be4b1f5f25 | |||
| cd4238274a |
@@ -43,6 +43,14 @@ http {
|
|||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.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)$ {
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||||
expires 30d;
|
expires 30d;
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 666 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 929 KiB |
@@ -95,11 +95,28 @@ function App() {
|
|||||||
const updateBg = () => {
|
const updateBg = () => {
|
||||||
const customBg = siteConfig.app_bg_image;
|
const customBg = siteConfig.app_bg_image;
|
||||||
if (customBg) {
|
if (customBg) {
|
||||||
document.documentElement.style.setProperty("--custom-bg-image", `url(${customBg})`);
|
if (customBg === "theme") {
|
||||||
document.documentElement.classList.add("has-custom-bg");
|
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 {
|
} else {
|
||||||
document.documentElement.style.removeProperty("--custom-bg-image");
|
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-custom-bg");
|
||||||
|
document.documentElement.classList.remove("has-theme-bg");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
updateBg();
|
updateBg();
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ export const AnimatedPage = ({
|
|||||||
className?: string;
|
className?: string;
|
||||||
}) => (
|
}) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ y: 0 }}
|
||||||
className={className}
|
className={className}
|
||||||
exit={{ opacity: 0, y: -8 }}
|
exit={{ y: -8 }}
|
||||||
initial={{ opacity: 0, y: 16 }}
|
initial={{ y: 16 }}
|
||||||
transition={{ duration: 0.28, ease: [0.25, 0.46, 0.45, 0.94] }}
|
transition={{ duration: 0.28, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -28,9 +28,8 @@ export const AnimatedPage = ({
|
|||||||
* Children should use `staggerItem` as their `variants` prop.
|
* Children should use `staggerItem` as their `variants` prop.
|
||||||
*/
|
*/
|
||||||
export const staggerContainer = {
|
export const staggerContainer = {
|
||||||
hidden: { opacity: 0 },
|
hidden: {},
|
||||||
show: {
|
show: {
|
||||||
opacity: 1,
|
|
||||||
transition: {
|
transition: {
|
||||||
staggerChildren: 0.05,
|
staggerChildren: 0.05,
|
||||||
delayChildren: 0.05,
|
delayChildren: 0.05,
|
||||||
@@ -42,9 +41,8 @@ export const staggerContainer = {
|
|||||||
* Individual stagger item variant — fade-in + slide-up.
|
* Individual stagger item variant — fade-in + slide-up.
|
||||||
*/
|
*/
|
||||||
export const staggerItem = {
|
export const staggerItem = {
|
||||||
hidden: { opacity: 0, y: 12 },
|
hidden: { y: 12 },
|
||||||
show: {
|
show: {
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
y: 0,
|
||||||
transition: { duration: 0.25, ease: [0.25, 0.46, 0.45, 0.94] },
|
transition: { duration: 0.25, ease: [0.25, 0.46, 0.45, 0.94] },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const ThemeSettings: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="border border-gray-200 dark:border-gray-700">
|
<Card className="shadow-md">
|
||||||
<CardBody className="p-6">
|
<CardBody className="p-6">
|
||||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-5">
|
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-5">
|
||||||
主题设置
|
主题设置
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export default function AdminLayout({
|
|||||||
const [isCollapsed, setIsCollapsed] = useState(
|
const [isCollapsed, setIsCollapsed] = useState(
|
||||||
() => localStorage.getItem("sidebar_collapsed") === "true",
|
() => localStorage.getItem("sidebar_collapsed") === "true",
|
||||||
);
|
);
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const [isAdmin, setIsAdmin] = useState(() => getAdminFlag());
|
||||||
const [monitorAllowed, setMonitorAllowed] = useState<boolean | null>(null);
|
const [monitorAllowed, setMonitorAllowed] = useState<boolean | null>(null);
|
||||||
const [monitorAccessReason, setMonitorAccessReason] = useState<string | null>(
|
const [monitorAccessReason, setMonitorAccessReason] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
@@ -373,16 +373,15 @@ export default function AdminLayout({
|
|||||||
{/* 左侧菜单栏 */}
|
{/* 左侧菜单栏 */}
|
||||||
<aside
|
<aside
|
||||||
className={`
|
className={`
|
||||||
${isMobile ? "fixed h-screen top-0 left-0 rounded-r-3xl" : "relative h-full rounded-3xl"}
|
${isMobile ? "fixed h-screen top-0 left-0 rounded-r-3xl" : "relative h-full rounded-3xl"}
|
||||||
${isMobile && !mobileMenuVisible ? "-translate-x-full" : "translate-x-0"}
|
${isMobile && !mobileMenuVisible ? "-translate-x-full" : "translate-x-0"}
|
||||||
${isMobile ? "w-64" : isCollapsed ? "w-20" : "w-[260px]"}
|
${isMobile ? "w-64" : isCollapsed ? "w-20" : "w-[260px]"}
|
||||||
bg-white/60 dark:bg-zinc-900/60 backdrop-blur-3xl
|
bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl
|
||||||
shadow-[0_10px_30px_rgba(0,0,0,0.1)]
|
shadow-[0_10px_30px_rgba(0,0,0,0.1)]
|
||||||
border border-white/80 dark:border-white/10
|
border border-white/80 dark:border-white/10
|
||||||
z-50
|
z-50
|
||||||
transition-all duration-300 ease-in-out
|
transition-all duration-300 ease-in-out
|
||||||
flex flex-col flex-shrink-0
|
flex flex-col flex-shrink-0 `}
|
||||||
`}
|
|
||||||
>
|
>
|
||||||
{/* Logo 区域 */}
|
{/* Logo 区域 */}
|
||||||
<div className="px-6 py-8 flex items-center overflow-hidden whitespace-nowrap box-border">
|
<div className="px-6 py-8 flex items-center overflow-hidden whitespace-nowrap box-border">
|
||||||
@@ -434,7 +433,7 @@ export default function AdminLayout({
|
|||||||
>
|
>
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute inset-0 rounded-2xl bg-white/80 dark:bg-white/10 shadow-[0_2px_8px_rgba(0,0,0,0.04)] border border-white/50 dark:border-white/5"
|
className="absolute inset-0 rounded-2xl bg-white/60 dark:bg-white/10 backdrop-blur-xl shadow-[0_12px_36px_rgba(0,0,0,0.18)] border border-white dark:border-white/20"
|
||||||
layoutId="sidebar-active"
|
layoutId="sidebar-active"
|
||||||
transition={{
|
transition={{
|
||||||
type: "spring",
|
type: "spring",
|
||||||
@@ -531,7 +530,7 @@ export default function AdminLayout({
|
|||||||
{isMobile && (
|
{isMobile && (
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
className="absolute top-4 left-4 z-40 bg-white/60 dark:bg-zinc-900/60 backdrop-blur-md shadow-sm border border-white/80 dark:border-white/10"
|
className="absolute top-4 left-4 z-40 bg-white/20 dark:bg-zinc-900/20 backdrop-blur-md shadow-sm border border-white/80 dark:border-white/10"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
onPress={toggleMobileMenu}
|
onPress={toggleMobileMenu}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default function H5SimpleLayout({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col min-h-screen bg-mesh-gradient">
|
<div className="flex flex-col min-h-screen bg-mesh-gradient">
|
||||||
{/* 顶部导航栏 */}
|
{/* 顶部导航栏 */}
|
||||||
<header className="bg-white/60 dark:bg-zinc-900/60 backdrop-blur-xl shadow-sm border-b border-white/80 dark:border-white/10 h-14 safe-top flex-shrink-0 flex items-center justify-between px-4 relative z-10">
|
<header className="bg-white/20 dark:bg-zinc-900/20 backdrop-blur-xl shadow-sm border-b border-white/80 dark:border-white/10 h-14 safe-top flex-shrink-0 flex items-center justify-between px-4 relative z-10">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button isIconOnly size="sm" variant="light" onPress={handleBack}>
|
<Button isIconOnly size="sm" variant="light" onPress={handleBack}>
|
||||||
<BackIcon className="w-5 h-5" />
|
<BackIcon className="w-5 h-5" />
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ interface TabItem {
|
|||||||
export default function H5Layout({ children }: { children: React.ReactNode }) {
|
export default function H5Layout({ children }: { children: React.ReactNode }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const [isAdmin, setIsAdmin] = useState(() => getAdminFlag());
|
||||||
const [monitorAllowed, setMonitorAllowed] = useState<boolean | null>(null);
|
const [monitorAllowed, setMonitorAllowed] = useState<boolean | null>(null);
|
||||||
const [monitorAccessReason, setMonitorAccessReason] = useState<string | null>(
|
const [monitorAccessReason, setMonitorAccessReason] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
@@ -168,7 +168,7 @@ export default function H5Layout({ children }: { children: React.ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col min-h-screen bg-mesh-gradient">
|
<div className="flex flex-col min-h-screen bg-mesh-gradient">
|
||||||
{/* 顶部导航栏 */}
|
{/* 顶部导航栏 */}
|
||||||
<header className="bg-white/60 dark:bg-zinc-900/60 backdrop-blur-xl shadow-sm border-b border-white/80 dark:border-white/10 h-14 safe-top flex-shrink-0 flex items-center justify-between px-4 relative z-10">
|
<header className="bg-white/20 dark:bg-zinc-900/20 backdrop-blur-xl shadow-sm border-b border-white/80 dark:border-white/10 h-14 safe-top flex-shrink-0 flex items-center justify-between px-4 relative z-10">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<BrandLogo size={20} />
|
<BrandLogo size={20} />
|
||||||
<h1 className="text-sm font-bold text-foreground">
|
<h1 className="text-sm font-bold text-foreground">
|
||||||
@@ -186,7 +186,7 @@ export default function H5Layout({ children }: { children: React.ReactNode }) {
|
|||||||
<div aria-hidden className="h-[calc(4rem+var(--safe-area-bottom))]" />
|
<div aria-hidden className="h-[calc(4rem+var(--safe-area-bottom))]" />
|
||||||
|
|
||||||
{/* 底部Tabbar */}
|
{/* 底部Tabbar */}
|
||||||
<nav className="bg-white/70 dark:bg-zinc-900/70 backdrop-blur-2xl border-t border-white/80 dark:border-white/10 h-[calc(4rem+var(--safe-area-bottom))] flex-shrink-0 flex items-center justify-around px-2 fixed bottom-0 left-0 right-0 z-30">
|
<nav className="bg-white/20 dark:bg-zinc-900/20 backdrop-blur-2xl border-t border-white/80 dark:border-white/10 h-[calc(4rem+var(--safe-area-bottom))] flex-shrink-0 flex items-center justify-around px-2 fixed bottom-0 left-0 right-0 z-30">
|
||||||
{filteredTabItems.map((item) => {
|
{filteredTabItems.map((item) => {
|
||||||
const isActive = location.pathname === item.path;
|
const isActive = location.pathname === item.path;
|
||||||
const isMonitor = item.path === "/monitor";
|
const isMonitor = item.path === "/monitor";
|
||||||
|
|||||||
@@ -1,12 +1,42 @@
|
|||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import { registerSW } from "virtual:pwa-register";
|
import { registerSW } from "virtual:pwa-register";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
import App from "./App.tsx";
|
import App from "./App.tsx";
|
||||||
import { Provider } from "./provider.tsx";
|
import { Provider } from "./provider.tsx";
|
||||||
import "@/styles/globals.css";
|
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(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
|||||||
@@ -621,10 +621,13 @@ export default function ConfigPage() {
|
|||||||
|
|
||||||
const renderBgImageUploader = () => {
|
const renderBgImageUploader = () => {
|
||||||
const bgImage = configs["app_bg_image"] || "";
|
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 (
|
return (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<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
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@@ -638,7 +641,23 @@ export default function ConfigPage() {
|
|||||||
onPress={() => bgImageFileInputRef.current?.click()}
|
onPress={() => bgImageFileInputRef.current?.click()}
|
||||||
isLoading={bgImageUploading}
|
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>
|
</Button>
|
||||||
{bgImage && (
|
{bgImage && (
|
||||||
<Button
|
<Button
|
||||||
@@ -647,11 +666,12 @@ export default function ConfigPage() {
|
|||||||
onPress={() => handleConfigChange("app_bg_image", "")}
|
onPress={() => handleConfigChange("app_bg_image", "")}
|
||||||
isDisabled={bgImageUploading}
|
isDisabled={bgImageUploading}
|
||||||
>
|
>
|
||||||
清除背景
|
恢复默认
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{bgImage && (
|
|
||||||
|
{bgImage && isImage && (
|
||||||
<div className="relative rounded-xl overflow-hidden border border-divider">
|
<div className="relative rounded-xl overflow-hidden border border-divider">
|
||||||
<img
|
<img
|
||||||
src={bgImage}
|
src={bgImage}
|
||||||
@@ -663,6 +683,23 @@ export default function ConfigPage() {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1067,7 +1067,7 @@ export default function DashboardPage() {
|
|||||||
{group.forwards.map((forward) => (
|
{group.forwards.map((forward) => (
|
||||||
<div
|
<div
|
||||||
key={forward.id}
|
key={forward.id}
|
||||||
className="bg-white dark:bg-default-100/50 border border-gray-200 dark:border-default-200 rounded-lg p-3 hover:shadow-md transition-shadow"
|
className="bg-white/10 dark:bg-black/10 backdrop-blur-md border border-white/20 dark:border-white/10 rounded-lg p-3 shadow-sm hover:shadow-md transition-all duration-300"
|
||||||
>
|
>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
@@ -1076,7 +1076,7 @@ export default function DashboardPage() {
|
|||||||
</h4>
|
</h4>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<button
|
<button
|
||||||
className={`block px-2 py-1 bg-default-50/50 dark:bg-default-100/20 backdrop-blur-md rounded-lg border border-default-200/50 dark:border-default-700/50 font-mono text-xs truncate text-foreground transition-colors duration-200 ${hasMultipleIps(forward.inIp) ? "cursor-pointer hover:bg-default-100 dark:hover:bg-default-200/50" : ""}`}
|
className={`block px-2 py-1 bg-white/20 dark:bg-black/20 backdrop-blur-md rounded-lg border border-white/20 dark:border-white/10 font-mono text-xs truncate text-foreground transition-all duration-300 ${hasMultipleIps(forward.inIp) ? "cursor-pointer hover:bg-white/30 dark:hover:bg-white/10" : ""}`}
|
||||||
disabled={!hasMultipleIps(forward.inIp)}
|
disabled={!hasMultipleIps(forward.inIp)}
|
||||||
title={formatInAddress(
|
title={formatInAddress(
|
||||||
forward.inIp,
|
forward.inIp,
|
||||||
@@ -1097,7 +1097,7 @@ export default function DashboardPage() {
|
|||||||
↓
|
↓
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className={`block px-2 py-1 bg-default-50/50 dark:bg-default-100/20 backdrop-blur-md rounded-lg border border-default-200/50 dark:border-default-700/50 font-mono text-xs truncate text-foreground transition-colors duration-200 ${hasMultipleRemoteAddresses(forward.remoteAddr) ? "cursor-pointer hover:bg-default-100 dark:hover:bg-default-200/50" : ""}`}
|
className={`block px-2 py-1 bg-white/20 dark:bg-black/20 backdrop-blur-md rounded-lg border border-white/20 dark:border-white/10 font-mono text-xs truncate text-foreground transition-all duration-300 ${hasMultipleRemoteAddresses(forward.remoteAddr) ? "cursor-pointer hover:bg-white/30 dark:hover:bg-white/10" : ""}`}
|
||||||
disabled={
|
disabled={
|
||||||
!hasMultipleRemoteAddresses(
|
!hasMultipleRemoteAddresses(
|
||||||
forward.remoteAddr,
|
forward.remoteAddr,
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export const FlowChartCard = ({
|
|||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white/70 dark:bg-zinc-900/70 backdrop-blur-xl border border-white/50 rounded-2xl shadow-[0_10px_30px_rgba(0,0,0,0.1)] p-4">
|
<div className="bg-white/20 dark:bg-black/20 backdrop-blur-3xl border border-white/50 rounded-2xl shadow-[0_10px_30px_rgba(0,0,0,0.1)] p-4">
|
||||||
<p className="font-medium text-foreground">{`时间: ${label ?? ""}`}</p>
|
<p className="font-medium text-foreground">{`时间: ${label ?? ""}`}</p>
|
||||||
<p className="text-primary font-semibold mt-1">{`流量: ${formatFlow(flowValue)}`}</p>
|
<p className="text-primary font-semibold mt-1">{`流量: ${formatFlow(flowValue)}`}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3689,7 +3689,7 @@ export default function ForwardPage() {
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={forward.id}
|
key={forward.id}
|
||||||
className="group h-full flex flex-col overflow-hidden"
|
className="group h-full flex flex-col overflow-hidden bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl border border-white/80 dark:border-white/10 shadow-[0_15px_35px_rgba(0,0,0,0.1)]"
|
||||||
>
|
>
|
||||||
<CardHeader className="pb-2 md:pb-2">
|
<CardHeader className="pb-2 md:pb-2">
|
||||||
<div className="flex justify-between items-start w-full">
|
<div className="flex justify-between items-start w-full">
|
||||||
@@ -3746,7 +3746,7 @@ export default function ForwardPage() {
|
|||||||
{/* 地址信息 */}
|
{/* 地址信息 */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<button
|
<button
|
||||||
className={`cursor-pointer px-2 py-1 bg-default-50 dark:bg-default-100/50 rounded border border-default-200 dark:border-default-300 transition-colors duration-200 ${
|
className={`cursor-pointer px-2 py-1 bg-white/20 dark:bg-black/20 backdrop-blur-md rounded border border-default-200 dark:border-default-300 transition-colors duration-200 ${
|
||||||
hasMultipleAddresses(forward.inIp)
|
hasMultipleAddresses(forward.inIp)
|
||||||
? "hover:bg-default-100 dark:hover:bg-default-200/50"
|
? "hover:bg-default-100 dark:hover:bg-default-200/50"
|
||||||
: ""
|
: ""
|
||||||
@@ -3786,7 +3786,7 @@ export default function ForwardPage() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={`cursor-pointer px-2 py-1 bg-default-50 dark:bg-default-100/50 rounded border border-default-200 dark:border-default-300 transition-colors duration-200 ${
|
className={`cursor-pointer px-2 py-1 bg-white/20 dark:bg-black/20 backdrop-blur-md rounded border border-default-200 dark:border-default-300 transition-colors duration-200 ${
|
||||||
hasMultipleAddresses(forward.remoteAddr)
|
hasMultipleAddresses(forward.remoteAddr)
|
||||||
? "hover:bg-default-100 dark:hover:bg-default-200/50"
|
? "hover:bg-default-100 dark:hover:bg-default-200/50"
|
||||||
: ""
|
: ""
|
||||||
@@ -4176,7 +4176,7 @@ export default function ForwardPage() {
|
|||||||
{sortedForwards.length} 条规则
|
{sortedForwards.length} 条规则
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Card className="overflow-hidden rounded-2xl border border-white/80 dark:border-white/10 bg-white/40 dark:bg-zinc-900/40 backdrop-blur-2xl shadow-[0_15px_35px_rgba(0,0,0,0.1)]">
|
<Card 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)]">
|
||||||
<DndContext
|
<DndContext
|
||||||
collisionDetection={pointerWithin}
|
collisionDetection={pointerWithin}
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
@@ -4256,7 +4256,7 @@ export default function ForwardPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Card>
|
<Card className="bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl border border-white/80 dark:border-white/10 shadow-[0_15px_35px_rgba(0,0,0,0.1)]">
|
||||||
<CardBody className="text-center py-20 flex flex-col items-center justify-center min-h-[240px]">
|
<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">
|
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
|
||||||
暂无规则配置
|
暂无规则配置
|
||||||
@@ -4302,7 +4302,7 @@ export default function ForwardPage() {
|
|||||||
</DndContext>
|
</DndContext>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Card>
|
<Card className="bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl border border-white/80 dark:border-white/10 shadow-[0_15px_35px_rgba(0,0,0,0.1)]">
|
||||||
<CardBody className="text-center py-20 flex flex-col items-center justify-center min-h-[240px]">
|
<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">
|
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
|
||||||
暂无规则配置
|
暂无规则配置
|
||||||
@@ -4329,7 +4329,7 @@ export default function ForwardPage() {
|
|||||||
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/20 dark:bg-zinc-900/20 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/20 dark:bg-black/20 backdrop-blur-3xl px-5 py-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm font-semibold text-foreground">
|
<span className="text-sm font-semibold text-foreground">
|
||||||
{group.userName}
|
{group.userName}
|
||||||
@@ -4415,10 +4415,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 border-b border-white/20 dark:border-white/10 bg-white/40 dark:bg-black/40 backdrop-blur-md px-4 py-2.5"
|
headerClassName="flex items-center justify-between border-b border-white/20 dark:border-white/10 bg-white/20 dark:bg-black/20 backdrop-blur-3xl px-4 py-2.5"
|
||||||
titleClassName="truncate text-sm font-semibold text-default-700"
|
titleClassName="truncate text-sm font-semibold text-default-700"
|
||||||
tunnel={tunnel}
|
tunnel={tunnel}
|
||||||
wrapperClassName="overflow-hidden rounded-2xl border border-white/80 dark:border-white/10 bg-white/40 dark:bg-zinc-900/40 backdrop-blur-2xl shadow-[0_15px_35px_rgba(0,0,0,0.1)]"
|
wrapperClassName="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)]"
|
||||||
onToggleCollapsed={() =>
|
onToggleCollapsed={() =>
|
||||||
toggleTunnelGroupCollapsed(
|
toggleTunnelGroupCollapsed(
|
||||||
group.userId,
|
group.userId,
|
||||||
@@ -4562,7 +4562,7 @@ export default function ForwardPage() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Card>
|
<Card className="bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl border border-white/80 dark:border-white/10 shadow-[0_15px_35px_rgba(0,0,0,0.1)]">
|
||||||
<CardBody className="text-center py-20 flex flex-col items-center justify-center min-h-[240px]">
|
<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">
|
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
|
||||||
暂无规则配置
|
暂无规则配置
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
|
import { AnimatedPage } from "@/components/animated-page";
|
||||||
|
import { PageLoadingState } from "@/components/page-state";
|
||||||
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
||||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||||
import { Input } from "@/shadcn-bridge/heroui/input";
|
import { Input } from "@/shadcn-bridge/heroui/input";
|
||||||
@@ -22,7 +24,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/shadcn-bridge/heroui/table";
|
} from "@/shadcn-bridge/heroui/table";
|
||||||
import { Chip } from "@/shadcn-bridge/heroui/chip";
|
import { Chip } from "@/shadcn-bridge/heroui/chip";
|
||||||
import { Spinner } from "@/shadcn-bridge/heroui/spinner";
|
|
||||||
import {
|
import {
|
||||||
assignGroupPermission,
|
assignGroupPermission,
|
||||||
assignTunnelsToGroup,
|
assignTunnelsToGroup,
|
||||||
@@ -469,14 +471,12 @@ export default function GroupPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-3 lg:px-6 py-8 space-y-6">
|
<AnimatedPage className="px-3 lg:px-6 py-8 space-y-6">
|
||||||
{loading && (
|
{loading ? (
|
||||||
<div className="flex justify-center py-10">
|
<PageLoadingState message="正在加载..." />
|
||||||
<Spinner size="lg" />
|
) : (
|
||||||
</div>
|
<>
|
||||||
)}
|
<Card>
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center gap-3 pb-2">
|
<CardHeader className="flex flex-row items-center gap-3 pb-2">
|
||||||
<h3 className="text-lg font-semibold">隧道分组</h3>
|
<h3 className="text-lg font-semibold">隧道分组</h3>
|
||||||
<Button
|
<Button
|
||||||
@@ -922,6 +922,8 @@ export default function GroupPage() {
|
|||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatedPage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export default function IndexPage() {
|
|||||||
initial={{ opacity: 0, y: 24 }}
|
initial={{ opacity: 0, y: 24 }}
|
||||||
transition={{ duration: 0.35, ease: [0.25, 0.46, 0.45, 0.94] }}
|
transition={{ duration: 0.35, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||||||
>
|
>
|
||||||
<Card className="w-full bg-white/60 dark:bg-zinc-900/60 backdrop-blur-3xl shadow-[0_20px_40px_rgba(0,0,0,0.15)] border-white/80 dark:border-white/10 rounded-[32px] p-2 sm:p-4">
|
<Card className="w-full bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl shadow-[0_20px_40px_rgba(0,0,0,0.15)] border-white/80 dark:border-white/10 rounded-[32px] p-2 sm:p-4">
|
||||||
<CardHeader className="pb-0 pt-6 px-6 flex-col items-center">
|
<CardHeader className="pb-0 pt-6 px-6 flex-col items-center">
|
||||||
<div className="w-14 h-14 bg-primary rounded-2xl flex items-center justify-center mb-4 shadow-sm text-white">
|
<div className="w-14 h-14 bg-primary rounded-2xl flex items-center justify-center mb-4 shadow-sm text-white">
|
||||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
||||||
|
|||||||
@@ -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">
|
<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) => (
|
{filteredRules.map((rule) => (
|
||||||
<StaggerItem key={rule.id}>
|
<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">
|
<CardHeader className="pb-2 md:pb-2">
|
||||||
<div className="flex justify-between items-start w-full">
|
<div className="flex justify-between items-start w-full">
|
||||||
<div>
|
<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]">
|
<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">
|
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
|
||||||
暂无限速规则
|
暂无限速规则
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ export default function MonitorPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab Switcher */}
|
{/* Tab Switcher */}
|
||||||
<div className="flex items-center gap-1 p-1 rounded-2xl bg-white/40 dark:bg-black/40 backdrop-blur-lg border border-white/50 dark:border-white/10 w-fit shadow-sm">
|
<div className="flex items-center gap-1 p-1 rounded-2xl bg-white/20 dark:bg-black/20 backdrop-blur-3xl border border-white/50 dark:border-white/10 w-fit shadow-sm">
|
||||||
<button
|
<button
|
||||||
className={`flex items-center gap-1.5 px-5 py-2 rounded-xl text-sm font-semibold transition-all duration-200 ${
|
className={`flex items-center gap-1.5 px-5 py-2 rounded-xl text-sm font-semibold transition-all duration-200 ${
|
||||||
activeTab === "nodes"
|
activeTab === "nodes"
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ const SortableItem = ({
|
|||||||
export default function NodePage() {
|
export default function NodePage() {
|
||||||
const [nodeList, setNodeList] = useState<Node[]>([]);
|
const [nodeList, setNodeList] = useState<Node[]>([]);
|
||||||
const [nodeOrder, setNodeOrder] = useState<number[]>([]);
|
const [nodeOrder, setNodeOrder] = useState<number[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(true);
|
||||||
const [localSearchKeyword, setLocalSearchKeyword] = useLocalStorageState(
|
const [localSearchKeyword, setLocalSearchKeyword] = useLocalStorageState(
|
||||||
"node-search-keyword-local",
|
"node-search-keyword-local",
|
||||||
"",
|
"",
|
||||||
|
|||||||
@@ -1446,7 +1446,7 @@ export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) {
|
|||||||
|
|
||||||
{/* Active monitor info bar */}
|
{/* Active monitor info bar */}
|
||||||
{resolvedActiveMonitor && (
|
{resolvedActiveMonitor && (
|
||||||
<div className="flex items-center justify-between gap-3 p-3 rounded-2xl bg-white/40 dark:bg-black/40 backdrop-blur-md border border-white/50 dark:border-white/10 shadow-[0_4px_12px_rgba(0,0,0,0.05)]">
|
<div className="flex items-center justify-between gap-3 p-3 rounded-2xl bg-white/20 dark:bg-black/20 backdrop-blur-3xl border border-white/50 dark:border-white/10 shadow-[0_4px_12px_rgba(0,0,0,0.05)]">
|
||||||
<div className="flex items-center gap-3 min-w-0 flex-wrap">
|
<div className="flex items-center gap-3 min-w-0 flex-wrap">
|
||||||
<Chip size="sm" color="primary" variant="flat">{resolvedActiveMonitor.type.toUpperCase()}</Chip>
|
<Chip size="sm" color="primary" variant="flat">{resolvedActiveMonitor.type.toUpperCase()}</Chip>
|
||||||
<span className="font-mono text-xs text-default-500">{resolvedActiveMonitor.target}</span>
|
<span className="font-mono text-xs text-default-500">{resolvedActiveMonitor.target}</span>
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default function PanelSharingPage() {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
const [nodes, setNodes] = useState<Node[]>([]);
|
const [nodes, setNodes] = useState<Node[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(true);
|
||||||
const [remoteUsageLoading, setRemoteUsageLoading] = useState(false);
|
const [remoteUsageLoading, setRemoteUsageLoading] = useState(false);
|
||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
|
|||||||
@@ -1578,7 +1578,7 @@ export default function TunnelPage() {
|
|||||||
{/* 隧道卡片网格 */}
|
{/* 隧道卡片网格 */}
|
||||||
{tunnels.length > 0 ? (
|
{tunnels.length > 0 ? (
|
||||||
viewMode === "list" ? (
|
viewMode === "list" ? (
|
||||||
<Card>
|
<Card className="bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl border border-white/80 dark:border-white/10 shadow-[0_15px_35px_rgba(0,0,0,0.1)]">
|
||||||
<Table
|
<Table
|
||||||
aria-label="隧道列表"
|
aria-label="隧道列表"
|
||||||
className="overflow-x-auto min-w-full"
|
className="overflow-x-auto min-w-full"
|
||||||
@@ -1721,7 +1721,7 @@ export default function TunnelPage() {
|
|||||||
{(listeners) => (
|
{(listeners) => (
|
||||||
<Card
|
<Card
|
||||||
key={tunnel.id}
|
key={tunnel.id}
|
||||||
className="group overflow-hidden"
|
className="group overflow-hidden bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl border border-white/80 dark:border-white/10 shadow-[0_15px_35px_rgba(0,0,0,0.1)]"
|
||||||
>
|
>
|
||||||
<CardHeader className="pb-2 md:pb-2">
|
<CardHeader className="pb-2 md:pb-2">
|
||||||
<div className="flex justify-between items-start w-full">
|
<div className="flex justify-between items-start w-full">
|
||||||
@@ -1771,7 +1771,7 @@ export default function TunnelPage() {
|
|||||||
<div className="pt-2 border-t border-divider">
|
<div className="pt-2 border-t border-divider">
|
||||||
<div className="flex items-center justify-center gap-2 text-xs">
|
<div className="flex items-center justify-center gap-2 text-xs">
|
||||||
{/* 入口节点 */}
|
{/* 入口节点 */}
|
||||||
<div className="flex items-center gap-1 px-2 py-1 bg-primary-50 dark:bg-primary-100/20 rounded border border-primary-200 dark:border-primary-300/20">
|
<div className="flex items-center gap-1 px-2 py-1 bg-primary-50/30 dark:bg-primary-100/20 backdrop-blur-md rounded border border-primary-200/50 dark:border-primary-300/20">
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="w-3 h-3 text-primary-600"
|
className="w-3 h-3 text-primary-600"
|
||||||
@@ -1806,7 +1806,7 @@ export default function TunnelPage() {
|
|||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
{/* 转发链 */}
|
{/* 转发链 */}
|
||||||
<div className="flex items-center gap-1 px-2 py-1 bg-secondary-50 dark:bg-secondary-100/20 rounded border border-secondary-200 dark:border-secondary-300/20">
|
<div className="flex items-center gap-1 px-2 py-1 bg-secondary-50/30 dark:bg-secondary-100/20 backdrop-blur-md rounded border border-secondary-200/50 dark:border-secondary-300/20">
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="w-3 h-3 text-secondary-600"
|
className="w-3 h-3 text-secondary-600"
|
||||||
@@ -1844,7 +1844,7 @@ export default function TunnelPage() {
|
|||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
{/* 出口节点 */}
|
{/* 出口节点 */}
|
||||||
<div className="flex items-center gap-1 px-2 py-1 bg-success-50 dark:bg-success-100/20 rounded border border-success-200 dark:border-success-300/20">
|
<div className="flex items-center gap-1 px-2 py-1 bg-success-50/30 dark:bg-success-100/20 backdrop-blur-md rounded border border-success-200/50 dark:border-success-300/20">
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="w-3 h-3 text-success-600"
|
className="w-3 h-3 text-success-600"
|
||||||
@@ -1871,7 +1871,7 @@ export default function TunnelPage() {
|
|||||||
<div
|
<div
|
||||||
className={`grid gap-2 ${tunnel.type === 2 && tunnel.ipPreference ? "grid-cols-3" : "grid-cols-2"}`}
|
className={`grid gap-2 ${tunnel.type === 2 && tunnel.ipPreference ? "grid-cols-3" : "grid-cols-2"}`}
|
||||||
>
|
>
|
||||||
<div className="text-center p-1.5 bg-default-50 dark:bg-default-100/30 rounded">
|
<div className="text-center p-1.5 bg-white/5 dark:bg-black/5 backdrop-blur-3xl rounded-lg border border-divider">
|
||||||
<div className="text-xs text-default-500">
|
<div className="text-xs text-default-500">
|
||||||
流量计算
|
流量计算
|
||||||
</div>
|
</div>
|
||||||
@@ -1879,7 +1879,7 @@ export default function TunnelPage() {
|
|||||||
{getTunnelFlowDisplay(tunnel.flow)}
|
{getTunnelFlowDisplay(tunnel.flow)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center p-1.5 bg-default-50 dark:bg-default-100/30 rounded">
|
<div className="text-center p-1.5 bg-white/5 dark:bg-black/5 backdrop-blur-3xl rounded-lg border border-divider">
|
||||||
<div className="text-xs text-default-500">
|
<div className="text-xs text-default-500">
|
||||||
流量倍率
|
流量倍率
|
||||||
</div>
|
</div>
|
||||||
@@ -1888,7 +1888,7 @@ export default function TunnelPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{tunnel.type === 2 && tunnel.ipPreference && (
|
{tunnel.type === 2 && tunnel.ipPreference && (
|
||||||
<div className="text-center p-1.5 bg-default-50 dark:bg-default-100/30 rounded">
|
<div className="text-center p-1.5 bg-white/5 dark:bg-black/5 backdrop-blur-3xl rounded-lg border border-divider">
|
||||||
<div className="text-xs text-default-500">
|
<div className="text-xs text-default-500">
|
||||||
连接偏好
|
连接偏好
|
||||||
</div>
|
</div>
|
||||||
@@ -1986,7 +1986,7 @@ export default function TunnelPage() {
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
/* 空状态 */
|
/* 空状态 */
|
||||||
<Card>
|
<Card className="bg-white/20 dark:bg-zinc-900/20 backdrop-blur-3xl border border-white/80 dark:border-white/10 shadow-[0_15px_35px_rgba(0,0,0,0.1)]">
|
||||||
<CardBody className="text-center py-20 flex flex-col items-center justify-center min-h-[240px]">
|
<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">
|
<h3 className="text-xl font-medium text-foreground tracking-tight mb-2">
|
||||||
暂无隧道配置
|
暂无隧道配置
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ const normalizeUserTunnelItem = (item: Partial<UserTunnel>): UserTunnel => {
|
|||||||
export default function UserPage() {
|
export default function UserPage() {
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(true);
|
||||||
const [searchKeyword, setSearchKeyword] = useLocalStorageState(
|
const [searchKeyword, setSearchKeyword] = useLocalStorageState(
|
||||||
"user-search-keyword",
|
"user-search-keyword",
|
||||||
"",
|
"",
|
||||||
@@ -466,10 +466,6 @@ export default function UserPage() {
|
|||||||
void loadMonitorPermissions();
|
void loadMonitorPermissions();
|
||||||
}, [loadMonitorPermissions, loadSpeedLimits, loadTunnels, loadUserGroups]);
|
}, [loadMonitorPermissions, loadSpeedLimits, loadTunnels, loadUserGroups]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
void loadUsers();
|
|
||||||
}, [loadUsers]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchDebounceRef.current) {
|
if (searchDebounceRef.current) {
|
||||||
clearTimeout(searchDebounceRef.current);
|
clearTimeout(searchDebounceRef.current);
|
||||||
|
|||||||
@@ -227,24 +227,56 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bg-mesh-gradient {
|
.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;
|
background-color: #f2f2f7;
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-mesh-gradient::before {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
background: url('/images/bg-light.jpg') no-repeat center center;
|
||||||
|
background-size: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .bg-mesh-gradient {
|
.dark .bg-mesh-gradient {
|
||||||
background: radial-gradient(at 0% 0%, #4a2b30 0%, transparent 50%),
|
background-color: #0b1020;
|
||||||
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%);
|
.dark .bg-mesh-gradient::before {
|
||||||
background-color: #1c1c1e;
|
background: url('/images/bg-dark.jpg') 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 {
|
: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-size: cover !important;
|
||||||
|
background-position: center center !important;
|
||||||
|
background-repeat: no-repeat !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default defineConfig({
|
|||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
VitePWA({
|
VitePWA({
|
||||||
registerType: "autoUpdate",
|
registerType: "prompt",
|
||||||
injectRegister: "auto",
|
injectRegister: "auto",
|
||||||
includeAssets: ["favicon.ico", "apple-touch-icon.png"],
|
includeAssets: ["favicon.ico", "apple-touch-icon.png"],
|
||||||
manifest: {
|
manifest: {
|
||||||
|
|||||||
Reference in New Issue
Block a user