mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-04 01:06:36 +08:00
feat(ui): redesign with transparent liquid glass (#414)
* docs: add design spec for iOS 26 Liquid Glass UI redesign * docs: add implementation plan for iOS 26 Liquid Glass UI redesign * feat(ui): add liquid glass CSS variables and update root layouts * feat(ui): update Card and Modal components to iOS 26 Liquid Glass style * feat(ui): redesign dashboard page to Liquid Glass style * feat(ui): redesign node management page cards and charts * feat(ui): redesign monitor page to Liquid Glass style * feat(ui): redesign group and panel-sharing cards to Liquid Glass * feat(ui): redesign settings and user components to Liquid Glass * feat(ui): redesign profile and auth pages to Liquid Glass * fix(ui): revert to LineChart to fix compilation issues * refactor(ui): extract dashboard user header and cleanup admin layout * feat(ui): refine monitor page hero metrics and fix build * fix(ui): real data for monitor hero metrics and card padding adjustments * fix(ui): apply global card padding and fix index layouts * feat(ui): implement liquid-glass-react for all Card components * fix(ui): resolve liquid-glass-react rendering issues causing invisible text and broken layout * fix(ui): native CSS liquid-glass effect replacing NPM package * fix(ui): cleanup duplicate closing tags and complete liquid glass tables * fix(ui): apply perfect rounded corners to list views and table headers in non-compact mode * fix(ui): apply liquid glass styling to grouped rules view in forward page * fix(ui): re-implement liquid-glass natively to prevent text distortion and remove package dependency * fix(ui): remove mouse distortion and preserve proper liquid glass texture * fix(ui): remove svg filter causing text distortion and replace with pure css liquid glass * fix(ui): apply properly scoped border radius to table headers * fix(ui): remove hardcoded bg-default-50/60 from TableHeader * fix(ui): apply global liquid glass styling and transparent overlays to all modals and dialogs * fix(ui): remove absolute/relative class conflicts hiding Dialog modales and implement pure transparent layout * feat(ui): apply global liquid glass styling to cards, buttons, and diagnosis modals
This commit is contained in:
@@ -46,7 +46,7 @@ const ProtectedRoute = ({
|
||||
|
||||
if (!authenticated) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-white dark:bg-black">
|
||||
<div className="flex items-center justify-center min-h-screen bg-mesh-gradient">
|
||||
<div className="text-lg text-gray-700 dark:text-gray-200" />
|
||||
</div>
|
||||
);
|
||||
@@ -78,7 +78,7 @@ const LoginRoute = () => {
|
||||
|
||||
if (authenticated) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-black">
|
||||
<div className="flex items-center justify-center min-h-screen bg-mesh-gradient">
|
||||
<div className="text-lg text-gray-700 dark:text-gray-200" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function Card({ className, style, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-xl border border-default-200 bg-white text-card-foreground shadow-sm dark:bg-default-50/20",
|
||||
className,
|
||||
"relative flex flex-col transition-all duration-300",
|
||||
"rounded-[24px] text-card-foreground shadow-[0_12px_40px_rgba(0,0,0,0.12)] dark:shadow-[0_12px_40px_rgba(0,0,0,0.4)]",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
...style,
|
||||
background: "linear-gradient(135deg, rgba(255,255,255,0.4) 0%, rgba(255,255,255,0.1) 40%, rgba(255,255,255,0.05) 60%, rgba(255,255,255,0.2) 100%)",
|
||||
boxShadow: "inset 0 1px 1px rgba(255,255,255,0.8), inset 0 0 0 1px rgba(255,255,255,0.3), inset 0 -1px 1px rgba(0,0,0,0.1), 0 12px 40px rgba(0,0,0,0.12)",
|
||||
backdropFilter: "blur(24px) saturate(180%)",
|
||||
WebkitBackdropFilter: "blur(24px) saturate(180%)",
|
||||
}}
|
||||
data-slot="card"
|
||||
{...props}
|
||||
/>
|
||||
@@ -57,7 +64,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
className={cn("p-6 pt-0", className)}
|
||||
className={cn("p-6", className)}
|
||||
data-slot="card-content"
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -35,7 +35,7 @@ function DialogOverlay({
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"fixed inset-0 z-50 bg-black/30 backdrop-blur-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
data-slot="dialog-overlay"
|
||||
@@ -57,19 +57,29 @@ function DialogContent({
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-default-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=open]:slide-in-from-bottom-4 data-[state=closed]:slide-out-to-bottom-2 sm:rounded-lg dark:bg-default-50",
|
||||
"fixed left-[50%] top-[50%] z-50 w-full max-w-lg translate-x-[-50%] translate-y-[-50%] duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=open]:slide-in-from-bottom-4 data-[state=closed]:slide-out-to-bottom-2",
|
||||
"flex flex-col transition-all duration-300",
|
||||
"rounded-[24px] text-foreground shadow-[0_20px_60px_rgba(0,0,0,0.2)] dark:shadow-[0_20px_60px_rgba(0,0,0,0.5)] border border-white/80 dark:border-white/10",
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
background: "linear-gradient(135deg, rgba(255,255,255,0.4) 0%, rgba(255,255,255,0.1) 40%, rgba(255,255,255,0.05) 60%, rgba(255,255,255,0.2) 100%)",
|
||||
boxShadow: "inset 0 1px 1px rgba(255,255,255,0.8), inset 0 0 0 1px rgba(255,255,255,0.3), inset 0 -1px 1px rgba(0,0,0,0.1), 0 20px 60px rgba(0,0,0,0.2)",
|
||||
backdropFilter: "blur(24px) saturate(180%)",
|
||||
WebkitBackdropFilter: "blur(24px) saturate(180%)",
|
||||
}}
|
||||
data-slot="dialog-content"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none">
|
||||
<XIcon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
<div className="flex flex-col flex-1 min-h-0 gap-4 p-6 rounded-[inherit] overflow-hidden bg-white/40 dark:bg-zinc-900/40 w-full h-full relative z-10 pointer-events-auto">
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none">
|
||||
<XIcon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
|
||||
@@ -4,12 +4,6 @@ import { toast } from "react-hot-toast";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownTrigger,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
} from "@/shadcn-bridge/heroui/dropdown";
|
||||
import {
|
||||
Modal,
|
||||
ModalContent,
|
||||
@@ -25,7 +19,7 @@ import { getMonitorAccess, updatePassword } from "@/api";
|
||||
import { safeLogout } from "@/utils/logout";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { useMobileBreakpoint } from "@/hooks/useMobileBreakpoint";
|
||||
import { getAdminFlag, getSessionName } from "@/utils/session";
|
||||
import { getAdminFlag } from "@/utils/session";
|
||||
|
||||
interface MenuItem {
|
||||
path: string;
|
||||
@@ -48,13 +42,11 @@ export default function AdminLayout({
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||
|
||||
const { isOpen, onOpenChange } = useDisclosure();
|
||||
const [mobileMenuVisible, setMobileMenuVisible] = useState(false);
|
||||
const [isCollapsed, setIsCollapsed] = useState(
|
||||
() => localStorage.getItem("sidebar_collapsed") === "true",
|
||||
);
|
||||
const [username, setUsername] = useState("");
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [monitorAllowed, setMonitorAllowed] = useState<boolean | null>(null);
|
||||
const [monitorAccessReason, setMonitorAccessReason] = useState<string | null>(
|
||||
@@ -196,10 +188,8 @@ export default function AdminLayout({
|
||||
|
||||
useEffect(() => {
|
||||
// 获取用户信息
|
||||
const name = getSessionName() || "Admin";
|
||||
const adminFlag = getAdminFlag();
|
||||
|
||||
setUsername(name);
|
||||
setIsAdmin(adminFlag);
|
||||
|
||||
// Monitor permission is not strictly role-based; non-admin users may be
|
||||
@@ -368,13 +358,13 @@ export default function AdminLayout({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex ${isMobile ? "min-h-screen" : "h-screen"} bg-gray-100 dark:bg-black`}
|
||||
className={`flex ${isMobile ? "min-h-screen p-0" : "h-screen p-6 gap-6"} bg-mesh-gradient overflow-hidden`}
|
||||
>
|
||||
{/* 移动端遮罩层 */}
|
||||
{isMobile && mobileMenuVisible && (
|
||||
<button
|
||||
aria-label="关闭菜单"
|
||||
className="fixed inset-0 backdrop-blur-sm bg-white/50 dark:bg-black/30 z-40"
|
||||
className="fixed inset-0 bg-black/30 backdrop-blur-sm z-40"
|
||||
type="button"
|
||||
onClick={hideMobileMenu}
|
||||
/>
|
||||
@@ -383,36 +373,34 @@ export default function AdminLayout({
|
||||
{/* 左侧菜单栏 */}
|
||||
<aside
|
||||
className={`
|
||||
${isMobile ? "fixed" : "relative"}
|
||||
${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 ? "w-64" : isCollapsed ? "w-20" : "w-72"}
|
||||
bg-white dark:bg-black
|
||||
shadow-lg
|
||||
border-r border-gray-200 dark:border-gray-600
|
||||
${isMobile ? "w-64" : isCollapsed ? "w-20" : "w-[260px]"}
|
||||
bg-white/60 dark:bg-zinc-900/60 backdrop-blur-3xl
|
||||
shadow-[0_10px_30px_rgba(0,0,0,0.1)]
|
||||
border border-white/80 dark:border-white/10
|
||||
z-50
|
||||
transition-all duration-300 ease-in-out
|
||||
flex flex-col
|
||||
${isMobile ? "h-screen" : "h-full"}
|
||||
${isMobile ? "top-0 left-0" : ""}
|
||||
flex flex-col flex-shrink-0
|
||||
`}
|
||||
>
|
||||
{/* Logo 区域 */}
|
||||
<div className="px-5 h-14 flex items-center overflow-hidden whitespace-nowrap box-border">
|
||||
<div className="flex-shrink-0 flex items-center justify-center w-10">
|
||||
<BrandLogo size={28} />
|
||||
<div className="px-6 py-8 flex items-center overflow-hidden whitespace-nowrap box-border">
|
||||
<div className="flex-shrink-0 flex items-center justify-center w-10 h-10 rounded-xl bg-primary text-white">
|
||||
<BrandLogo size={24} />
|
||||
</div>
|
||||
<div
|
||||
className={`transition-all duration-300 overflow-hidden ${isCollapsed ? "max-w-0 opacity-0 ml-0" : "max-w-[180px] opacity-100 ml-2"}`}
|
||||
className={`transition-all duration-300 overflow-hidden ${isCollapsed ? "max-w-0 opacity-0 ml-0" : "max-w-[180px] opacity-100 ml-3"}`}
|
||||
>
|
||||
<h1 className="text-sm font-bold text-foreground overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
<h1 className="text-xl font-bold text-foreground overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
{siteConfig.name}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 菜单导航 */}
|
||||
<nav className="flex-1 px-3 py-6 overflow-y-auto overflow-x-hidden">
|
||||
<ul className="space-y-1">
|
||||
<nav className="flex-1 px-4 overflow-y-auto overflow-x-hidden [scrollbar-width:none]">
|
||||
<ul className="space-y-2">
|
||||
{filteredMenuItems.map((item) => {
|
||||
const isActive = location.pathname === item.path;
|
||||
const isMonitor = item.path === "/monitor";
|
||||
@@ -422,15 +410,15 @@ export default function AdminLayout({
|
||||
<li key={item.path}>
|
||||
<motion.button
|
||||
className={`
|
||||
w-full flex items-center p-2 rounded-lg text-left
|
||||
relative min-h-[44px] overflow-hidden transition-colors
|
||||
w-full flex items-center p-3 rounded-2xl text-left
|
||||
relative min-h-[48px] overflow-hidden transition-colors
|
||||
${isMonitorBlocked ? "opacity-60" : ""}
|
||||
${
|
||||
isActive
|
||||
? "text-primary-600 dark:text-primary-300"
|
||||
? "text-primary dark:text-primary-400 font-semibold"
|
||||
: isMonitorBlocked
|
||||
? "text-gray-500 dark:text-gray-400"
|
||||
: "text-gray-700 dark:text-gray-200"
|
||||
? "text-gray-500 dark:text-gray-400 font-medium"
|
||||
: "text-gray-600 dark:text-gray-300 font-medium"
|
||||
}
|
||||
`}
|
||||
aria-disabled={isMonitorBlocked}
|
||||
@@ -446,7 +434,7 @@ export default function AdminLayout({
|
||||
>
|
||||
{isActive && (
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-lg bg-primary-100 dark:bg-primary-600/20"
|
||||
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"
|
||||
layoutId="sidebar-active"
|
||||
transition={{
|
||||
type: "spring",
|
||||
@@ -457,18 +445,18 @@ export default function AdminLayout({
|
||||
)}
|
||||
{!isActive && (
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-lg bg-gray-100 dark:bg-gray-900 opacity-0"
|
||||
className="absolute inset-0 rounded-2xl bg-white/40 dark:bg-white/5 opacity-0"
|
||||
transition={{ duration: 0.15 }}
|
||||
whileHover={{ opacity: 1 }}
|
||||
/>
|
||||
)}
|
||||
<div className="flex-shrink-0 w-10 h-10 flex items-center justify-center relative z-10">
|
||||
<div className="flex-shrink-0 w-6 h-6 flex items-center justify-center relative z-10">
|
||||
{item.icon}
|
||||
</div>
|
||||
<div
|
||||
className={`transition-all duration-300 overflow-hidden flex items-center ${isCollapsed ? "max-w-0 opacity-0 ml-0" : "max-w-[200px] opacity-100 ml-2"}`}
|
||||
className={`transition-all duration-300 overflow-hidden flex items-center ${isCollapsed ? "max-w-0 opacity-0 ml-0" : "max-w-[200px] opacity-100 ml-3"}`}
|
||||
>
|
||||
<span className="font-medium text-sm relative z-10 whitespace-nowrap">
|
||||
<span className="relative z-10 whitespace-nowrap">
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
@@ -480,7 +468,7 @@ export default function AdminLayout({
|
||||
</nav>
|
||||
|
||||
{/* 底部版权信息和折叠按钮 */}
|
||||
<div className="px-5 py-2 pb-4 mt-auto flex-shrink-0 flex items-center justify-between overflow-hidden whitespace-nowrap box-border">
|
||||
<div className="px-5 py-6 mt-auto flex-shrink-0 flex flex-col gap-4 overflow-hidden whitespace-nowrap box-border">
|
||||
<div
|
||||
className={`transition-all duration-300 overflow-hidden flex items-center ${isCollapsed ? "max-w-0 opacity-0" : "max-w-[200px] opacity-100"}`}
|
||||
>
|
||||
@@ -496,13 +484,12 @@ export default function AdminLayout({
|
||||
{!isMobile && (
|
||||
<Button
|
||||
isIconOnly
|
||||
className="flex-shrink-0 text-gray-400 hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300 min-w-0 w-10 h-10 rounded-full ml-auto"
|
||||
className="flex-shrink-0 text-gray-400 hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300 min-w-0 w-10 h-10 rounded-full mx-auto"
|
||||
size="sm"
|
||||
variant="light"
|
||||
onPress={toggleCollapse}
|
||||
>
|
||||
{isCollapsed ? (
|
||||
// 向右扩展的提示
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
@@ -517,7 +504,6 @@ export default function AdminLayout({
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
// 向左收起的提示
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
@@ -539,106 +525,34 @@ export default function AdminLayout({
|
||||
|
||||
{/* 主内容区域 */}
|
||||
<div
|
||||
className={`flex flex-col flex-1 ${isMobile ? "min-h-0" : "h-full overflow-hidden"}`}
|
||||
className={`flex flex-col flex-1 ${isMobile ? "min-h-0" : "h-full overflow-hidden"} relative`}
|
||||
>
|
||||
{/* 顶部导航栏 */}
|
||||
<header className="bg-white dark:bg-black shadow-md border-b border-gray-200 dark:border-gray-600 h-14 flex items-center justify-between px-4 lg:px-6 relative z-10">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* 移动端菜单按钮 */}
|
||||
{isMobile && (
|
||||
<Button
|
||||
isIconOnly
|
||||
className="lg:hidden"
|
||||
variant="light"
|
||||
onPress={toggleMobileMenu}
|
||||
>
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 用户菜单 */}
|
||||
<Dropdown placement="bottom-end">
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
className="text-sm font-medium text-foreground"
|
||||
variant="light"
|
||||
>
|
||||
{username}
|
||||
<svg
|
||||
className="w-4 h-4 ml-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="用户菜单">
|
||||
<DropdownItem
|
||||
key="change-password"
|
||||
startContent={
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
onPress={onOpen}
|
||||
>
|
||||
修改密码
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="logout"
|
||||
className="text-danger"
|
||||
color="danger"
|
||||
startContent={
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
onPress={handleLogout}
|
||||
>
|
||||
退出登录
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</header>
|
||||
{/* 移动端菜单按钮 (替代Header) */}
|
||||
{isMobile && (
|
||||
<Button
|
||||
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"
|
||||
variant="flat"
|
||||
onPress={toggleMobileMenu}
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5 text-foreground"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 主内容 */}
|
||||
<main className="flex-1 bg-gray-100 dark:bg-black overflow-y-auto">
|
||||
<main className="flex-1 overflow-y-auto [scrollbar-width:none]">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={location.pathname}
|
||||
|
||||
@@ -6,7 +6,7 @@ export default function DefaultLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative flex flex-col min-h-screen bg-white dark:bg-black">
|
||||
<div className="relative flex flex-col min-h-screen bg-mesh-gradient">
|
||||
<Navbar />
|
||||
<main className="container mx-auto max-w-7xl px-4 sm:px-6 flex-grow pt-4 sm:pt-16">
|
||||
{children}
|
||||
|
||||
@@ -21,9 +21,9 @@ export default function H5SimpleLayout({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-gray-100 dark:bg-black">
|
||||
<div className="flex flex-col min-h-screen bg-mesh-gradient">
|
||||
{/* 顶部导航栏 */}
|
||||
<header className="bg-white dark:bg-black shadow-sm border-b border-gray-200 dark:border-gray-600 h-14 safe-top flex-shrink-0 flex items-center justify-between px-4 relative z-10">
|
||||
<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">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button isIconOnly size="sm" variant="light" onPress={handleBack}>
|
||||
<BackIcon className="w-5 h-5" />
|
||||
@@ -38,7 +38,7 @@ export default function H5SimpleLayout({
|
||||
</header>
|
||||
|
||||
{/* 主内容区域 */}
|
||||
<main className="flex-1 bg-gray-100 dark:bg-black pb-0">{children}</main>
|
||||
<main className="flex-1 pb-0">{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -166,9 +166,9 @@ export default function H5Layout({ children }: { children: React.ReactNode }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-gray-100 dark:bg-black">
|
||||
<div className="flex flex-col min-h-screen bg-mesh-gradient">
|
||||
{/* 顶部导航栏 */}
|
||||
<header className="bg-white dark:bg-black shadow-sm border-b border-gray-200 dark:border-gray-600 h-14 safe-top flex-shrink-0 flex items-center justify-between px-4 relative z-10">
|
||||
<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">
|
||||
<div className="flex items-center gap-2">
|
||||
<BrandLogo size={20} />
|
||||
<h1 className="text-sm font-bold text-foreground">
|
||||
@@ -180,13 +180,13 @@ export default function H5Layout({ children }: { children: React.ReactNode }) {
|
||||
</header>
|
||||
|
||||
{/* 主内容区域 */}
|
||||
<main className="flex-1 bg-gray-100 dark:bg-black">{children}</main>
|
||||
<main className="flex-1">{children}</main>
|
||||
|
||||
{/* 用于给固定 Tabbar 腾出空间的占位元素 */}
|
||||
<div aria-hidden className="h-[calc(4rem+var(--safe-area-bottom))]" />
|
||||
|
||||
{/* 底部Tabbar */}
|
||||
<nav className="bg-white dark:bg-black border-t border-gray-200 dark:border-gray-600 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/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">
|
||||
{filteredTabItems.map((item) => {
|
||||
const isActive = location.pathname === item.path;
|
||||
const isMonitor = item.path === "/monitor";
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { AnimatedPage } from "@/components/animated-page";
|
||||
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownTrigger,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
} from "@/shadcn-bridge/heroui/dropdown";
|
||||
import {
|
||||
Modal,
|
||||
ModalContent,
|
||||
@@ -15,6 +22,8 @@ import { AnnouncementBanner } from "@/pages/dashboard/components/announcement-ba
|
||||
import { AnnouncementModal } from "@/pages/dashboard/components/announcement-modal";
|
||||
import { FlowChartCard } from "@/pages/dashboard/components/flow-chart-card";
|
||||
import { MetricCard } from "@/pages/dashboard/components/metric-card";
|
||||
import { getSessionName } from "@/utils/session";
|
||||
import { safeLogout } from "@/utils/logout";
|
||||
import {
|
||||
formatNodeRenewalTime,
|
||||
getNodeRenewalCycleLabel,
|
||||
@@ -35,6 +44,15 @@ interface AddressItem {
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const navigate = useNavigate();
|
||||
const username = getSessionName() || "User";
|
||||
|
||||
const handleLogout = () => {
|
||||
safeLogout();
|
||||
toast.success("已退出登录");
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
const {
|
||||
loading,
|
||||
userInfo,
|
||||
@@ -630,6 +648,48 @@ export default function DashboardPage() {
|
||||
|
||||
return (
|
||||
<AnimatedPage className="px-3 lg:px-6 py-2 lg:py-4">
|
||||
{/* 顶部个人状态和下拉菜单 */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl lg:text-3xl font-bold text-foreground">Good morning, {username}</h1>
|
||||
<p className="text-sm text-default-500 mt-1">Here's what's happening with your network today.</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Dropdown placement="bottom-end">
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
className="rounded-full w-10 h-10 min-w-0 bg-primary text-white font-bold text-sm shadow-[0_4px_12px_rgba(0,122,255,0.3)]"
|
||||
isIconOnly
|
||||
variant="solid"
|
||||
>
|
||||
{username.slice(0, 2).toUpperCase()}
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="用户菜单" className="bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl border border-white/50 dark:border-white/10 rounded-2xl shadow-[0_10px_30px_rgba(0,0,0,0.1)]">
|
||||
<DropdownItem
|
||||
key="profile"
|
||||
startContent={
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /></svg>
|
||||
}
|
||||
onPress={() => navigate("/profile")}
|
||||
>
|
||||
个人资料
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="logout"
|
||||
className="text-danger"
|
||||
color="danger"
|
||||
startContent={
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z" clipRule="evenodd" /></svg>
|
||||
}
|
||||
onPress={handleLogout}
|
||||
>
|
||||
退出登录
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
{announcement && <AnnouncementBanner announcement={announcement} />}
|
||||
{announcement && (
|
||||
<AnnouncementModal
|
||||
@@ -823,7 +883,7 @@ export default function DashboardPage() {
|
||||
|
||||
{/* 隧道权限 - 管理员不显示 */}
|
||||
{!isAdmin && (
|
||||
<Card className="mb-6 lg:mb-8 border border-gray-200 dark:border-default-200 shadow-md">
|
||||
<Card className="mb-6 lg:mb-8">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
@@ -945,7 +1005,7 @@ export default function DashboardPage() {
|
||||
)}
|
||||
|
||||
{/* 规则配置 */}
|
||||
<Card className="border border-gray-200 dark:border-default-200 shadow-md">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
@@ -1016,7 +1076,7 @@ export default function DashboardPage() {
|
||||
</h4>
|
||||
<div className="space-y-1">
|
||||
<button
|
||||
className={`block px-2 py-1 bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-300 rounded font-mono text-xs truncate ${hasMultipleIps(forward.inIp) ? "cursor-pointer hover:bg-green-200 dark:hover:bg-green-500/30" : ""}`}
|
||||
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" : ""}`}
|
||||
disabled={!hasMultipleIps(forward.inIp)}
|
||||
title={formatInAddress(
|
||||
forward.inIp,
|
||||
@@ -1037,7 +1097,7 @@ export default function DashboardPage() {
|
||||
↓
|
||||
</div>
|
||||
<button
|
||||
className={`block px-2 py-1 bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-300 rounded font-mono text-xs truncate ${hasMultipleRemoteAddresses(forward.remoteAddr) ? "cursor-pointer hover:bg-blue-200 dark:hover:bg-blue-500/30" : ""}`}
|
||||
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" : ""}`}
|
||||
disabled={
|
||||
!hasMultipleRemoteAddresses(
|
||||
forward.remoteAddr,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
CartesianGrid,
|
||||
Line,
|
||||
LineChart,
|
||||
ResponsiveContainer,
|
||||
@@ -28,7 +27,7 @@ export const FlowChartCard = ({
|
||||
formatFlow,
|
||||
}: FlowChartCardProps) => {
|
||||
return (
|
||||
<Card className="mb-6 lg:mb-8 border border-gray-200 dark:border-default-200 shadow-md">
|
||||
<Card className="mb-6 lg:mb-8">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
@@ -51,17 +50,16 @@ export const FlowChartCard = ({
|
||||
) : (
|
||||
<div className="h-64 lg:h-80 w-full">
|
||||
<ResponsiveContainer height="100%" width="100%">
|
||||
<LineChart data={chartData}>
|
||||
<CartesianGrid className="opacity-30" strokeDasharray="3 3" />
|
||||
<LineChart data={chartData} margin={{ top: 20, right: 0, left: -20, bottom: 0 }}>
|
||||
<XAxis
|
||||
axisLine={{ stroke: "#e5e7eb", strokeWidth: 1 }}
|
||||
axisLine={false}
|
||||
dataKey="time"
|
||||
tick={{ fontSize: 12 }}
|
||||
tick={{ fill: "currentColor", opacity: 0.5, fontSize: 12 }}
|
||||
tickLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={{ stroke: "#e5e7eb", strokeWidth: 1 }}
|
||||
tick={{ fontSize: 12 }}
|
||||
axisLine={false}
|
||||
tick={{ fill: "currentColor", opacity: 0.5, fontSize: 12 }}
|
||||
tickFormatter={(value: number | string) => {
|
||||
const v = typeof value === "number" ? value : Number(value);
|
||||
|
||||
@@ -97,27 +95,20 @@ export const FlowChartCard = ({
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-default-100 border border-default-200 rounded-lg shadow-lg p-3">
|
||||
<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">
|
||||
<p className="font-medium text-foreground">{`时间: ${label ?? ""}`}</p>
|
||||
<p className="text-primary">{`流量: ${formatFlow(flowValue)}`}</p>
|
||||
<p className="text-primary font-semibold mt-1">{`流量: ${formatFlow(flowValue)}`}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}}
|
||||
cursor={{ fill: "rgba(0, 122, 255, 0.05)" }}
|
||||
/>
|
||||
<Line
|
||||
activeDot={{
|
||||
r: 4,
|
||||
stroke: "#8b5cf6",
|
||||
strokeWidth: 2,
|
||||
fill: "#fff",
|
||||
}}
|
||||
dataKey="flow"
|
||||
dot={false}
|
||||
stroke="#8b5cf6"
|
||||
strokeWidth={3}
|
||||
stroke="#007aff"
|
||||
type="monotone"
|
||||
/>
|
||||
</LineChart>
|
||||
|
||||
@@ -18,22 +18,24 @@ export const MetricCard = ({
|
||||
bottomContent,
|
||||
}: MetricCardProps) => {
|
||||
return (
|
||||
<Card className="border border-gray-200 dark:border-default-200 shadow-md hover:shadow-lg transition-shadow">
|
||||
<CardBody className="p-3 lg:p-4">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Card className="h-48 flex flex-col justify-between overflow-hidden">
|
||||
<CardBody className="!p-4 md:!p-6 h-full flex flex-col justify-between overflow-visible">
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs lg:text-sm text-default-600 truncate">
|
||||
<p className="text-sm font-medium text-default-600 truncate">
|
||||
{title}
|
||||
</p>
|
||||
<div
|
||||
className={`p-1.5 lg:p-2 rounded-lg flex-shrink-0 ${iconClassName}`}
|
||||
className={`p-2 rounded-xl flex-shrink-0 ${iconClassName}`}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-base lg:text-xl font-bold text-foreground truncate">
|
||||
<p className="text-2xl lg:text-3xl font-bold text-foreground truncate mt-2">
|
||||
{value}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-auto pt-4">
|
||||
{bottomContent}
|
||||
</div>
|
||||
</CardBody>
|
||||
|
||||
@@ -3689,7 +3689,7 @@ export default function ForwardPage() {
|
||||
return (
|
||||
<Card
|
||||
key={forward.id}
|
||||
className="group h-full flex flex-col shadow-sm border border-divider hover:shadow-md transition-shadow duration-200 overflow-hidden"
|
||||
className="group h-full flex flex-col overflow-hidden"
|
||||
>
|
||||
<CardHeader className="pb-2 md:pb-2">
|
||||
<div className="flex justify-between items-start w-full">
|
||||
@@ -3966,7 +3966,7 @@ export default function ForwardPage() {
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between mb-6 gap-3">
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
<Button
|
||||
color={activeFilterCount > 0 ? "primary" : "default"}
|
||||
className="bg-default/10 dark:bg-default/20 backdrop-blur-md text-default-700 dark:text-default-300 font-semibold hover:bg-default/20 dark:hover:bg-default/30"
|
||||
startContent={
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
@@ -3982,7 +3982,6 @@ export default function ForwardPage() {
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
variant={activeFilterCount > 0 ? "flat" : "bordered"}
|
||||
onPress={() => setIsSearchModalOpen(true)}
|
||||
>
|
||||
高级筛选 {activeFilterCount > 0 ? `(${activeFilterCount})` : ""}
|
||||
@@ -4177,7 +4176,7 @@ export default function ForwardPage() {
|
||||
{sortedForwards.length} 条规则
|
||||
</span>
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-xl border border-divider bg-content1 shadow-md">
|
||||
<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)]">
|
||||
<DndContext
|
||||
collisionDetection={pointerWithin}
|
||||
sensors={sensors}
|
||||
@@ -4190,9 +4189,10 @@ export default function ForwardPage() {
|
||||
<Table
|
||||
aria-label="全部规则列表"
|
||||
classNames={{
|
||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
||||
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-default-50/50 transition-colors",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors group",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
@@ -4253,10 +4253,10 @@ export default function ForwardPage() {
|
||||
</Table>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
) : (
|
||||
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
|
||||
<Card>
|
||||
<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">
|
||||
暂无规则配置
|
||||
@@ -4302,7 +4302,7 @@ export default function ForwardPage() {
|
||||
</DndContext>
|
||||
</>
|
||||
) : (
|
||||
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
|
||||
<Card>
|
||||
<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">
|
||||
暂无规则配置
|
||||
@@ -4327,9 +4327,9 @@ export default function ForwardPage() {
|
||||
return (
|
||||
<div
|
||||
key={`grouped-table-${group.userId}-${group.userName}`}
|
||||
className="overflow-hidden rounded-xl border border-divider bg-content1 shadow-md"
|
||||
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)]"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-divider bg-default-100/40 px-4 py-3">
|
||||
<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">
|
||||
<span className="text-sm font-semibold text-foreground">
|
||||
{group.userName}
|
||||
@@ -4415,10 +4415,10 @@ export default function ForwardPage() {
|
||||
collapsed={collapsed}
|
||||
countClassName="text-xs text-default-600"
|
||||
groupUserId={group.userId}
|
||||
headerClassName="flex items-center justify-between border-b border-divider bg-default-100/60 px-4 py-2.5"
|
||||
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"
|
||||
titleClassName="truncate text-sm font-semibold text-default-700"
|
||||
tunnel={tunnel}
|
||||
wrapperClassName="overflow-hidden rounded-lg border border-divider bg-default-50/60"
|
||||
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)]"
|
||||
onToggleCollapsed={() =>
|
||||
toggleTunnelGroupCollapsed(
|
||||
group.userId,
|
||||
@@ -4439,9 +4439,10 @@ export default function ForwardPage() {
|
||||
aria-label={`${group.userName}-${tunnel.tunnelName}规则列表`}
|
||||
className={`table-fixed ${FORWARD_GROUPED_TABLE_MIN_WIDTH_CLASS}`}
|
||||
classNames={{
|
||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
||||
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-default-50/50 transition-colors",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
@@ -4561,7 +4562,7 @@ export default function ForwardPage() {
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
|
||||
<Card>
|
||||
<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">
|
||||
暂无规则配置
|
||||
@@ -4637,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-default-100/60 px-3 py-2"
|
||||
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"
|
||||
titleClassName="truncate text-sm font-semibold text-default-700"
|
||||
tunnel={tunnel}
|
||||
wrapperClassName="rounded-xl border border-divider bg-default-50/60 space-y-3"
|
||||
wrapperClassName="rounded-xl border border-divider bg-white/20 dark:bg-black/20 space-y-3"
|
||||
onToggleCollapsed={() =>
|
||||
toggleTunnelGroupCollapsed(
|
||||
group.userId,
|
||||
@@ -4682,7 +4683,7 @@ export default function ForwardPage() {
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
|
||||
<Card>
|
||||
<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">
|
||||
暂无规则配置
|
||||
@@ -5415,7 +5416,7 @@ export default function ForwardPage() {
|
||||
</div>
|
||||
)}
|
||||
</ModalHeader>
|
||||
<ModalBody className="bg-content1">
|
||||
<ModalBody className="bg-transparent">
|
||||
{diagnosisResult ? (
|
||||
<div className="space-y-4">
|
||||
{diagnosisLoading && (
|
||||
@@ -5939,7 +5940,7 @@ export default function ForwardPage() {
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter className="bg-content1 border-t border-divider">
|
||||
<ModalFooter className="bg-transparent border-t border-white/20 dark:border-white/10">
|
||||
<Button variant="light" onPress={onClose}>
|
||||
关闭
|
||||
</Button>
|
||||
|
||||
@@ -489,7 +489,15 @@ export default function GroupPage() {
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Table aria-label="隧道分组列表">
|
||||
<Table
|
||||
aria-label="隧道分组列表"
|
||||
classNames={{
|
||||
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",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn>名称</TableColumn>
|
||||
<TableColumn>隧道</TableColumn>
|
||||
@@ -561,7 +569,15 @@ export default function GroupPage() {
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Table aria-label="用户分组列表">
|
||||
<Table
|
||||
aria-label="用户分组列表"
|
||||
classNames={{
|
||||
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",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn>名称</TableColumn>
|
||||
<TableColumn>用户</TableColumn>
|
||||
@@ -665,7 +681,15 @@ export default function GroupPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table aria-label="分组权限列表">
|
||||
<Table
|
||||
aria-label="分组权限列表"
|
||||
classNames={{
|
||||
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",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn>ID</TableColumn>
|
||||
<TableColumn>用户分组</TableColumn>
|
||||
|
||||
@@ -8,9 +8,7 @@ import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
||||
import { Input } from "@/shadcn-bridge/heroui/input";
|
||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { title } from "@/components/primitives";
|
||||
import { VersionFooter } from "@/components/version-footer";
|
||||
import DefaultLayout from "@/layouts/default";
|
||||
import { login, LoginData, checkCaptcha, getConfigByName } from "@/api";
|
||||
import { writeLoginSession } from "@/utils/session";
|
||||
import { useWebViewMode } from "@/hooks/useWebViewMode";
|
||||
@@ -152,29 +150,36 @@ export default function IndexPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<section className="flex flex-col items-center justify-center gap-4 py-4 sm:py-8 md:py-10 pb-20 min-h-[calc(100dvh-120px)] sm:min-h-[calc(100dvh-200px)]">
|
||||
<div className="relative flex flex-col min-h-screen bg-mesh-gradient overflow-hidden">
|
||||
<section className="flex flex-col items-center justify-center flex-1 w-full p-4 relative z-10">
|
||||
<motion.div
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="w-full max-w-md px-4 sm:px-0"
|
||||
className="w-full max-w-[420px] px-4 sm:px-0"
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
transition={{ duration: 0.35, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||||
>
|
||||
<Card className="w-full">
|
||||
<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">
|
||||
<CardHeader className="pb-0 pt-6 px-6 flex-col items-center">
|
||||
<h1 className={title({ size: "sm" })}>登陆</h1>
|
||||
<p className="text-small text-default-500 mt-2">
|
||||
请输入您的账号信息
|
||||
<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>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-foreground">Welcome to Flvx</h1>
|
||||
<p className="text-sm text-default-500 mt-2 font-medium">
|
||||
Sign in to manage your networks
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardBody className="px-6 py-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<CardBody className="px-6 py-6 mt-2">
|
||||
<div className="flex flex-col gap-5">
|
||||
<Input
|
||||
classNames={{
|
||||
inputWrapper: "bg-white/50 dark:bg-black/50 backdrop-blur-md border border-white/60 dark:border-white/10 h-12 shadow-sm rounded-xl",
|
||||
input: "text-base font-medium",
|
||||
}}
|
||||
errorMessage={errors.username}
|
||||
isDisabled={loading}
|
||||
isInvalid={!!errors.username}
|
||||
label="用户名"
|
||||
placeholder="请输入用户名"
|
||||
label="Username"
|
||||
placeholder="admin"
|
||||
value={form.username}
|
||||
variant="bordered"
|
||||
onChange={(e) =>
|
||||
@@ -184,10 +189,14 @@ export default function IndexPage() {
|
||||
/>
|
||||
|
||||
<Input
|
||||
classNames={{
|
||||
inputWrapper: "bg-white/50 dark:bg-black/50 backdrop-blur-md border border-white/60 dark:border-white/10 h-12 shadow-sm rounded-xl",
|
||||
input: "text-base font-medium tracking-wider",
|
||||
}}
|
||||
isDisabled={loading}
|
||||
isInvalid={!!errors.password}
|
||||
label="密码"
|
||||
placeholder="请输入密码"
|
||||
label="Password"
|
||||
placeholder="••••••••"
|
||||
type="password"
|
||||
value={form.password}
|
||||
variant="bordered"
|
||||
@@ -198,14 +207,12 @@ export default function IndexPage() {
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="mt-2"
|
||||
color="primary"
|
||||
className="mt-4 h-12 rounded-xl bg-primary text-white font-bold text-base shadow-[0_8px_16px_rgba(0,122,255,0.3)] transition-transform active:scale-[0.98]"
|
||||
disabled={loading}
|
||||
isLoading={loading}
|
||||
size="lg"
|
||||
onPress={handleLogin}
|
||||
>
|
||||
{loading ? (showCaptcha ? "验证中..." : "登录中...") : "登录"}
|
||||
{loading ? (showCaptcha ? "Verifying..." : "Signing in...") : "Sign In"}
|
||||
</Button>
|
||||
</div>
|
||||
</CardBody>
|
||||
@@ -275,6 +282,6 @@ export default function IndexPage() {
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</DefaultLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -267,9 +267,10 @@ export default function LimitPage() {
|
||||
aria-label="限速规则列表"
|
||||
className="overflow-x-auto min-w-full"
|
||||
classNames={{
|
||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
||||
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-default-50/50 transition-colors",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
||||
import { getMonitorNodes } from "@/api";
|
||||
import { MonitorView } from "@/pages/node/monitor-view";
|
||||
import { TunnelMonitorView } from "@/pages/node/tunnel-monitor-view";
|
||||
import { useNodeRealtime } from "@/pages/node/use-node-realtime";
|
||||
|
||||
type MonitorNode = {
|
||||
id: number;
|
||||
@@ -20,6 +21,16 @@ type MonitorNode = {
|
||||
|
||||
type MonitorTab = "nodes" | "tunnels";
|
||||
|
||||
const formatBytesPerSecond = (bytesPerSecond: number): string => {
|
||||
if (!Number.isFinite(bytesPerSecond) || bytesPerSecond <= 0) return "0 B/s";
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ["B/s", "KB/s", "MB/s", "GB/s", "TB/s"];
|
||||
const i = Math.floor(Math.log(bytesPerSecond) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytesPerSecond / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
export default function MonitorPage() {
|
||||
const [nodes, setNodes] = useState<MonitorNodeApiItem[]>([]);
|
||||
const [nodesLoading, setNodesLoading] = useState(false);
|
||||
@@ -27,6 +38,55 @@ export default function MonitorPage() {
|
||||
const [viewMode, setViewMode] = useState<"list" | "grid">("list");
|
||||
const [activeTab, setActiveTab] = useState<MonitorTab>("nodes");
|
||||
|
||||
const [realtimeNodeStatus, setRealtimeNodeStatus] = useState<Record<number, "online" | "offline">>({});
|
||||
const [realtimeNodeMetrics, setRealtimeNodeMetrics] = useState<Record<number, any>>({});
|
||||
|
||||
const handleRealtimeMessage = useCallback((message: any) => {
|
||||
const nodeId = Number(message?.id ?? 0);
|
||||
if (!nodeId || Number.isNaN(nodeId)) return;
|
||||
|
||||
const type = String(message?.type ?? "");
|
||||
const payload = message?.data;
|
||||
|
||||
if (type === "status") {
|
||||
const status = Number(payload);
|
||||
setRealtimeNodeStatus((prev) => ({
|
||||
...prev,
|
||||
[nodeId]: status === 1 ? "online" : "offline",
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "metric") {
|
||||
let raw = payload;
|
||||
if (typeof raw === "string") {
|
||||
try {
|
||||
raw = JSON.parse(raw);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!raw || typeof raw !== "object") return;
|
||||
const metric = raw as any;
|
||||
|
||||
setRealtimeNodeMetrics((prev) => ({
|
||||
...prev,
|
||||
[nodeId]: {
|
||||
cpuUsage: Number(metric.cpuUsage ?? metric.cpu_usage ?? 0),
|
||||
netInSpeed: Number(metric.netInSpeed ?? metric.net_in_speed ?? 0),
|
||||
netOutSpeed: Number(metric.netOutSpeed ?? metric.net_out_speed ?? 0),
|
||||
tcpConns: Number(metric.tcpConns ?? metric.tcp_conns ?? 0),
|
||||
udpConns: Number(metric.udpConns ?? metric.udp_conns ?? 0),
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useNodeRealtime({
|
||||
onMessage: handleRealtimeMessage,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const loadNodes = useCallback(async (options?: { silent?: boolean }) => {
|
||||
const silent = options?.silent ?? false;
|
||||
if (!silent) setNodesLoading(true);
|
||||
@@ -73,22 +133,101 @@ export default function MonitorPage() {
|
||||
.map((n) => ({
|
||||
id: Number(n.id),
|
||||
name: String(n.name ?? ""),
|
||||
connectionStatus: n.status === 1 ? "online" : "offline",
|
||||
connectionStatus: realtimeNodeStatus[Number(n.id)] || (n.status === 1 ? "online" : "offline"),
|
||||
version: n.version,
|
||||
}));
|
||||
|
||||
return new Map<number, MonitorNode>(list.map((n) => [n.id, n]));
|
||||
}, [nodes]);
|
||||
}, [nodes, realtimeNodeStatus]);
|
||||
|
||||
const aggregateMetrics = useMemo(() => {
|
||||
let totalCpu = 0;
|
||||
let totalConns = 0;
|
||||
let totalNetIn = 0;
|
||||
let totalNetOut = 0;
|
||||
let onlineCount = 0;
|
||||
|
||||
nodeMap.forEach((node) => {
|
||||
if (node.connectionStatus === "online") {
|
||||
onlineCount++;
|
||||
const metric = realtimeNodeMetrics[node.id];
|
||||
if (metric) {
|
||||
totalCpu += metric.cpuUsage;
|
||||
totalConns += metric.tcpConns + metric.udpConns;
|
||||
totalNetIn += metric.netInSpeed;
|
||||
totalNetOut += metric.netOutSpeed;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const avgCpu = onlineCount > 0 ? totalCpu / onlineCount : 0;
|
||||
return {
|
||||
avgCpu,
|
||||
totalConns,
|
||||
totalBandwidth: totalNetIn + totalNetOut,
|
||||
};
|
||||
}, [nodeMap, realtimeNodeMetrics]);
|
||||
|
||||
return (
|
||||
<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="flex justify-between items-center z-10 relative">
|
||||
<span className="text-default-600 font-medium text-sm">System Load</span>
|
||||
</div>
|
||||
<div className="z-10 relative">
|
||||
<span className="text-4xl font-bold text-foreground">{aggregateMetrics.avgCpu.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 right-0 h-12 flex items-end gap-1 px-6 pb-4 opacity-50 z-0">
|
||||
<div className="w-full bg-primary/40 h-2 rounded-t-sm" />
|
||||
<div className="w-full bg-primary/40 h-3 rounded-t-sm" />
|
||||
<div className="w-full bg-primary/40 h-1.5 rounded-t-sm" />
|
||||
<div className="w-full bg-primary/40 h-4 rounded-t-sm" />
|
||||
<div className="w-full bg-primary/40 h-2.5 rounded-t-sm" />
|
||||
<div className="w-full bg-primary h-5 rounded-t-sm shadow-[0_0_10px_rgba(0,122,255,0.5)]" />
|
||||
</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="flex justify-between items-center z-10 relative">
|
||||
<span className="text-default-600 font-medium text-sm">Active Connections</span>
|
||||
</div>
|
||||
<div className="z-10 relative">
|
||||
<span className="text-4xl font-bold text-foreground">{aggregateMetrics.totalConns}</span>
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 right-0 h-12 flex items-end gap-1 px-6 pb-4 opacity-50 z-0">
|
||||
<div className="w-full bg-success/40 h-3.5 rounded-t-sm" />
|
||||
<div className="w-full bg-success/40 h-4.5 rounded-t-sm" />
|
||||
<div className="w-full bg-success/40 h-2.5 rounded-t-sm" />
|
||||
<div className="w-full bg-success/40 h-6 rounded-t-sm" />
|
||||
<div className="w-full bg-success/40 h-5.5 rounded-t-sm" />
|
||||
<div className="w-full bg-success h-7 rounded-t-sm shadow-[0_0_10px_rgba(52,199,89,0.5)]" />
|
||||
</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="flex justify-between items-center z-10 relative">
|
||||
<span className="text-default-600 font-medium text-sm">Bandwidth</span>
|
||||
</div>
|
||||
<div className="z-10 relative">
|
||||
<span className="text-4xl font-bold text-foreground">{formatBytesPerSecond(aggregateMetrics.totalBandwidth)}</span>
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 right-0 h-12 flex items-end gap-1 px-6 pb-4 opacity-50 z-0">
|
||||
<div className="w-full bg-secondary/40 h-2 rounded-t-sm" />
|
||||
<div className="w-full bg-secondary/40 h-3 rounded-t-sm" />
|
||||
<div className="w-full bg-secondary/40 h-5 rounded-t-sm" />
|
||||
<div className="w-full bg-secondary/40 h-8 rounded-t-sm" />
|
||||
<div className="w-full bg-secondary/40 h-7 rounded-t-sm" />
|
||||
<div className="w-full bg-secondary h-10 rounded-t-sm shadow-[0_0_10px_rgba(175,82,222,0.5)]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 space-y-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-xl font-semibold truncate">监控</h2>
|
||||
<div className="text-xs text-default-500 truncate">
|
||||
实时节点状态 + 隧道质量检测 + 历史指标图表 + 服务监控(TCP/ICMP)
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold truncate">监控面板</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
@@ -114,11 +253,11 @@ export default function MonitorPage() {
|
||||
</div>
|
||||
|
||||
{/* Tab Switcher */}
|
||||
<div className="flex items-center gap-1 p-1 rounded-xl bg-default-100 dark:bg-default-50/10 w-fit">
|
||||
<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">
|
||||
<button
|
||||
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium 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"
|
||||
? "bg-background shadow-sm text-foreground"
|
||||
? "bg-white dark:bg-zinc-800 shadow-[0_2px_8px_rgba(0,0,0,0.08)] text-foreground"
|
||||
: "text-default-500 hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setActiveTab("nodes")}
|
||||
@@ -127,9 +266,9 @@ export default function MonitorPage() {
|
||||
节点
|
||||
</button>
|
||||
<button
|
||||
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium 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 === "tunnels"
|
||||
? "bg-background shadow-sm text-foreground"
|
||||
? "bg-white dark:bg-zinc-800 shadow-[0_2px_8px_rgba(0,0,0,0.08)] text-foreground"
|
||||
: "text-default-500 hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setActiveTab("tunnels")}
|
||||
|
||||
@@ -1767,9 +1767,10 @@ export default function NodePage() {
|
||||
aria-label="节点列表"
|
||||
className="overflow-x-auto min-w-full"
|
||||
classNames={{
|
||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
||||
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-default-50/50 transition-colors",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
@@ -1949,7 +1950,7 @@ export default function NodePage() {
|
||||
{(listeners) => (
|
||||
<Card
|
||||
key={node.id}
|
||||
className={`group relative overflow-visible shadow-sm border border-divider hover:shadow-md transition-shadow duration-200 h-full flex flex-col ${node.expiryReminderDismissed ? "" : expiryMeta.accentClassName}`}
|
||||
className={`group relative overflow-visible h-full flex flex-col ${node.expiryReminderDismissed ? "" : expiryMeta.accentClassName}`}
|
||||
data-node-card="true"
|
||||
>
|
||||
<CardHeader className="pb-3 md:pb-3">
|
||||
@@ -1979,13 +1980,20 @@ export default function NodePage() {
|
||||
)}
|
||||
<div className="min-w-0 flex-1 pt-0.5">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<h3 className="font-semibold text-foreground truncate text-sm leading-5">
|
||||
<h3 className="font-semibold text-foreground truncate text-lg leading-5">
|
||||
{node.name}
|
||||
</h3>
|
||||
<span
|
||||
className={`h-2.5 w-2.5 rounded-full flex-shrink-0 ${connectionStatusMeta.color === "success" ? "bg-emerald-500" : "bg-rose-500"}`}
|
||||
<div
|
||||
className={`flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium flex-shrink-0
|
||||
${connectionStatusMeta.color === "success" ? "bg-emerald-500/20 text-emerald-600" : "bg-rose-500/20 text-rose-600"}
|
||||
`}
|
||||
title={connectionStatusMeta.text}
|
||||
/>
|
||||
>
|
||||
<span
|
||||
className={`h-1.5 w-1.5 rounded-full ${connectionStatusMeta.color === "success" ? "bg-emerald-500" : "bg-rose-500"}`}
|
||||
/>
|
||||
{connectionStatusMeta.text}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -174,7 +174,7 @@ function ServerCard({ node, metric, onPress }: { node: any; metric: RealtimeNode
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="group relative overflow-hidden shadow-sm border border-divider dark:border-default-100 hover:-translate-y-1 hover:shadow-lg transition-all duration-300 h-full flex flex-col cursor-pointer bg-background"
|
||||
className="group relative hover:-translate-y-1 hover:shadow-[0_20px_40px_rgba(0,0,0,0.15)] transition-all duration-300 h-full flex flex-col cursor-pointer"
|
||||
onClick={onPress}
|
||||
>
|
||||
{/* Dynamic top gradient bar based on status */}
|
||||
@@ -208,8 +208,8 @@ function ServerCard({ node, metric, onPress }: { node: any; metric: RealtimeNode
|
||||
<span className="font-mono font-medium">{isOnline && metric ? `${metric.cpuUsage.toFixed(1)}%` : "-"}</span>
|
||||
</div>
|
||||
<Progress
|
||||
className="h-1"
|
||||
color={getColorByUsage(metric?.cpuUsage)}
|
||||
size="sm"
|
||||
value={isOnline && metric ? metric.cpuUsage : 0}
|
||||
/>
|
||||
</div>
|
||||
@@ -219,6 +219,7 @@ function ServerCard({ node, metric, onPress }: { node: any; metric: RealtimeNode
|
||||
<span className="font-mono font-medium">{isOnline && metric ? `${metric.memoryUsage.toFixed(1)}%` : "-"}</span>
|
||||
</div>
|
||||
<Progress
|
||||
className="h-1"
|
||||
color={getColorByUsage(metric?.memoryUsage)}
|
||||
size="sm"
|
||||
value={isOnline && metric ? metric.memoryUsage : 0}
|
||||
@@ -1157,9 +1158,10 @@ export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) {
|
||||
aria-label="节点列表"
|
||||
className="overflow-x-auto min-w-full"
|
||||
classNames={{
|
||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
||||
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-default-50/50 transition-colors",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
@@ -1321,7 +1323,7 @@ export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) {
|
||||
{ label: "↑ 上行速度", value: formatBytesPerSecond(detailRealtimeMetric.netOutSpeed), color: "primary" as const },
|
||||
{ label: "运行时间", value: formatUptime(detailRealtimeMetric.uptime), color: "default" as const },
|
||||
].map((item) => (
|
||||
<Card key={item.label} className="border border-divider/60 shadow-sm hover:shadow-md transition-shadow bg-gradient-to-br from-background to-default-50/50">
|
||||
<Card key={item.label} className="hover:shadow-[0_15px_40px_rgba(0,0,0,0.15)] transition-shadow">
|
||||
<CardBody className="py-3 px-4 flex flex-col items-center justify-center min-h-[5rem]">
|
||||
<span className="text-[11px] text-default-500 mb-1.5">{item.label}</span>
|
||||
<span className={`text-sm font-semibold font-mono ${item.color === 'danger' ? 'text-danger' : item.color === 'warning' ? 'text-warning' : item.color === 'success' ? 'text-success' : item.color === 'primary' ? 'text-primary' : ''}`}>
|
||||
@@ -1444,7 +1446,7 @@ export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) {
|
||||
|
||||
{/* Active monitor info bar */}
|
||||
{resolvedActiveMonitor && (
|
||||
<div className="flex items-center justify-between gap-3 p-3 rounded-lg bg-default-50 dark:bg-default-100/30 border border-divider">
|
||||
<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 gap-3 min-w-0 flex-wrap">
|
||||
<Chip size="sm" color="primary" variant="flat">{resolvedActiveMonitor.type.toUpperCase()}</Chip>
|
||||
<span className="font-mono text-xs text-default-500">{resolvedActiveMonitor.target}</span>
|
||||
@@ -1584,9 +1586,10 @@ export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) {
|
||||
aria-label="监控记录"
|
||||
className="w-full overflow-x-auto"
|
||||
classNames={{
|
||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
||||
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-default-50/50 transition-colors",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
|
||||
@@ -987,9 +987,10 @@ export function TunnelMonitorView({ viewMode = "grid" }: TunnelMonitorViewProps)
|
||||
aria-label="隧道列表"
|
||||
className="overflow-x-auto min-w-full"
|
||||
classNames={{
|
||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
||||
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-default-50/50 transition-colors",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
|
||||
@@ -459,7 +459,7 @@ export default function PanelSharingPage() {
|
||||
{shares.map((share) => (
|
||||
<Card
|
||||
key={share.id}
|
||||
className="border border-divider shadow-sm"
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<CardHeader className="flex justify-between pb-2 md:pb-2">
|
||||
<h3 className="font-bold">{share.name}</h3>
|
||||
@@ -606,7 +606,7 @@ export default function PanelSharingPage() {
|
||||
{remoteUsageNodes.map((node) => (
|
||||
<Card
|
||||
key={node.nodeId}
|
||||
className="border border-divider shadow-sm"
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<CardHeader className="flex justify-between pb-2 md:pb-2">
|
||||
<h3 className="font-bold">{node.nodeName}</h3>
|
||||
|
||||
@@ -202,7 +202,7 @@ export default function ProfilePage() {
|
||||
<div className="px-3 lg:px-6 py-8 flex flex-col h-full">
|
||||
<div className="space-y-6 flex-1">
|
||||
{/* 用户信息卡片 */}
|
||||
<Card className="border border-gray-200 dark:border-default-200 shadow-md hover:shadow-lg transition-shadow">
|
||||
<Card>
|
||||
<CardBody className="p-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-12 h-12 bg-primary-100 dark:bg-primary-900/30 rounded-full flex items-center justify-center">
|
||||
@@ -242,7 +242,7 @@ export default function ProfilePage() {
|
||||
</Card>
|
||||
|
||||
{/* 功能网格 */}
|
||||
<Card className="border border-gray-200 dark:border-default-200 shadow-md hover:shadow-lg transition-shadow">
|
||||
<Card>
|
||||
<CardBody className="p-4">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{/* 管理员功能 */}
|
||||
|
||||
@@ -183,7 +183,7 @@ export const SettingsPage = () => {
|
||||
{/* 内容区域 */}
|
||||
<div className="max-w-4xl mx-auto px-4 py-6">
|
||||
<div className="space-y-6">
|
||||
<Card className="border border-gray-200 dark:border-gray-700">
|
||||
<Card>
|
||||
<CardBody className="p-6">
|
||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
更新设置
|
||||
@@ -213,7 +213,7 @@ export const SettingsPage = () => {
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card className="border border-gray-200 dark:border-gray-700">
|
||||
<Card>
|
||||
<CardBody className="p-6">
|
||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
显示设置
|
||||
@@ -247,7 +247,7 @@ export const SettingsPage = () => {
|
||||
</Card>
|
||||
|
||||
{/* 添加新地址 */}
|
||||
<Card className="border border-gray-200 dark:border-gray-700">
|
||||
<Card>
|
||||
<CardBody className="p-6">
|
||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
添加新面板地址
|
||||
@@ -275,7 +275,7 @@ export const SettingsPage = () => {
|
||||
</Card>
|
||||
|
||||
{/* 地址列表 */}
|
||||
<Card className="border border-gray-200 dark:border-gray-700">
|
||||
<Card>
|
||||
<CardBody className="p-6">
|
||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
已保存的面板地址
|
||||
|
||||
@@ -1583,9 +1583,10 @@ export default function TunnelPage() {
|
||||
aria-label="隧道列表"
|
||||
className="overflow-x-auto min-w-full"
|
||||
classNames={{
|
||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
||||
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-default-50/50 transition-colors",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
@@ -1720,7 +1721,7 @@ export default function TunnelPage() {
|
||||
{(listeners) => (
|
||||
<Card
|
||||
key={tunnel.id}
|
||||
className="group shadow-sm border border-divider hover:shadow-md transition-shadow duration-200 overflow-hidden"
|
||||
className="group overflow-hidden"
|
||||
>
|
||||
<CardHeader className="pb-2 md:pb-2">
|
||||
<div className="flex justify-between items-start w-full">
|
||||
@@ -1985,7 +1986,7 @@ export default function TunnelPage() {
|
||||
)
|
||||
) : (
|
||||
/* 空状态 */
|
||||
<Card className="shadow-sm border border-gray-200 dark:border-gray-700 bg-default-50/50">
|
||||
<Card>
|
||||
<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">
|
||||
暂无隧道配置
|
||||
@@ -3152,7 +3153,7 @@ export default function TunnelPage() {
|
||||
</div>
|
||||
)}
|
||||
</ModalHeader>
|
||||
<ModalBody className="bg-content1">
|
||||
<ModalBody className="bg-transparent">
|
||||
{diagnosisResult ? (
|
||||
<div className="space-y-4">
|
||||
{diagnosisLoading && (
|
||||
@@ -3674,7 +3675,7 @@ export default function TunnelPage() {
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter className="bg-content1 border-t border-divider">
|
||||
<ModalFooter className="bg-transparent border-t border-white/20 dark:border-white/10">
|
||||
<Button variant="light" onPress={onClose}>
|
||||
关闭
|
||||
</Button>
|
||||
|
||||
@@ -1084,9 +1084,10 @@ export default function UserPage() {
|
||||
aria-label="用户列表"
|
||||
className="overflow-x-auto min-w-full"
|
||||
classNames={{
|
||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
||||
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-default-50/50 transition-colors",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
@@ -1108,14 +1109,11 @@ export default function UserPage() {
|
||||
return (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`shrink-0 w-2 h-2 rounded-full ${
|
||||
userStatus.color === "success"
|
||||
? "bg-success"
|
||||
: "bg-danger"
|
||||
}`}
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center justify-center shrink-0 w-10 h-10 rounded-full bg-primary text-white font-bold text-sm relative">
|
||||
{(user.name || user.user).slice(0, 2).toUpperCase()}
|
||||
<span className={`absolute bottom-0 right-0 w-3 h-3 border-2 border-white dark:border-zinc-900 rounded-full ${userStatus.color === "success" ? "bg-success" : "bg-danger"}`} />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-foreground text-sm">
|
||||
{user.name || user.user}
|
||||
@@ -1252,16 +1250,22 @@ export default function UserPage() {
|
||||
|
||||
return (
|
||||
<StaggerItem key={user.id}>
|
||||
<Card className="shadow-sm border border-divider hover:shadow-md transition-shadow duration-200 overflow-hidden h-full">
|
||||
<Card className="overflow-hidden h-full">
|
||||
<CardHeader className="pb-2 md:pb-2">
|
||||
<div className="flex justify-between items-start w-full">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-foreground truncate text-sm">
|
||||
{user.name || user.user}
|
||||
</h3>
|
||||
<p className="text-xs text-default-500 truncate">
|
||||
@{user.user}
|
||||
</p>
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div className="flex items-center justify-center shrink-0 w-10 h-10 rounded-full bg-primary text-white font-bold text-sm relative">
|
||||
{(user.name || user.user).slice(0, 2).toUpperCase()}
|
||||
<span className={`absolute bottom-0 right-0 w-3 h-3 border-2 border-white dark:border-zinc-900 rounded-full ${userStatus.color === "success" ? "bg-success" : "bg-danger"}`} />
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<h3 className="font-semibold text-foreground truncate text-sm">
|
||||
{user.name || user.user}
|
||||
</h3>
|
||||
<p className="text-xs text-default-500 truncate">
|
||||
@{user.user}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 ml-2">
|
||||
<Chip
|
||||
@@ -1896,9 +1900,9 @@ export default function UserPage() {
|
||||
<Table
|
||||
aria-label="用户隧道权限列表"
|
||||
classNames={{
|
||||
th: "bg-default-100/50 text-default-600 font-semibold text-sm border-b border-divider py-3 uppercase tracking-wider",
|
||||
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-default-50/50 transition-colors",
|
||||
tr: "hover:bg-white/40 dark:hover:bg-white/10 transition-colors",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
|
||||
@@ -84,14 +84,22 @@ function mapSize(
|
||||
|
||||
const solidColorClassMap: Partial<Record<HeroButtonColor, string>> = {
|
||||
default:
|
||||
"bg-default-200 text-default-800 hover:bg-default-300 dark:bg-default-100 dark:text-default-800 dark:hover:bg-default-200",
|
||||
success: "bg-success text-white hover:bg-success-600",
|
||||
warning: "bg-warning text-warning-900 hover:bg-warning-400",
|
||||
"bg-default/10 dark:bg-default/20 backdrop-blur-md text-default-800 dark:text-default-300 hover:bg-default/20 dark:hover:bg-default/30",
|
||||
success:
|
||||
"bg-success/10 dark:bg-success/20 backdrop-blur-md text-success-700 dark:text-success-400 hover:bg-success/20 dark:hover:bg-success/30",
|
||||
warning:
|
||||
"bg-warning/10 dark:bg-warning/20 backdrop-blur-md text-warning-700 dark:text-warning-400 hover:bg-warning/20 dark:hover:bg-warning/30",
|
||||
primary:
|
||||
"bg-primary/10 dark:bg-primary/20 backdrop-blur-md text-primary-700 dark:text-primary-400 hover:bg-primary/20 dark:hover:bg-primary/30",
|
||||
secondary:
|
||||
"bg-secondary/10 dark:bg-secondary/20 backdrop-blur-md text-secondary-700 dark:text-secondary-400 hover:bg-secondary/20 dark:hover:bg-secondary/30",
|
||||
danger:
|
||||
"bg-danger/10 dark:bg-danger/20 backdrop-blur-md text-danger-700 dark:text-danger-400 hover:bg-danger/20 dark:hover:bg-danger/30",
|
||||
};
|
||||
|
||||
const borderedColorClassMap: Record<HeroButtonColor, string> = {
|
||||
default:
|
||||
"border-default-300 text-default-700 hover:bg-default-100 dark:border-default-300 dark:text-default-600 dark:hover:bg-default-200/40",
|
||||
"bg-default/10 dark:bg-default/20 backdrop-blur-md text-default-700 dark:text-default-300 hover:bg-default/20 dark:hover:bg-default/30",
|
||||
primary:
|
||||
"border-primary text-primary hover:bg-primary-50 dark:border-primary-500/60 dark:text-primary-300 dark:hover:bg-primary-900/20",
|
||||
secondary:
|
||||
@@ -121,17 +129,17 @@ const lightColorClassMap: Record<HeroButtonColor, string> = {
|
||||
|
||||
const flatColorClassMap: Record<HeroButtonColor, string> = {
|
||||
default:
|
||||
"bg-default-100 text-default-700 hover:bg-default-200 dark:bg-default-100/45 dark:text-default-700 dark:hover:bg-default-200/60",
|
||||
"bg-default/10 dark:bg-default/20 backdrop-blur-md text-default-700 dark:text-default-300 hover:bg-default/20 dark:hover:bg-default/30",
|
||||
primary:
|
||||
"bg-primary-100 text-primary-700 hover:bg-primary-200 dark:bg-primary-900/30 dark:text-primary-300 dark:hover:bg-primary-900/45",
|
||||
"bg-primary/10 dark:bg-primary/20 backdrop-blur-md text-primary-700 dark:text-primary-400 hover:bg-primary/20 dark:hover:bg-primary/30",
|
||||
secondary:
|
||||
"bg-secondary-100 text-secondary-700 hover:bg-secondary-200 dark:bg-secondary-900/30 dark:text-secondary-300 dark:hover:bg-secondary-900/45",
|
||||
"bg-secondary/10 dark:bg-secondary/20 backdrop-blur-md text-secondary-700 dark:text-secondary-400 hover:bg-secondary/20 dark:hover:bg-secondary/30",
|
||||
success:
|
||||
"bg-success-100 text-success-700 hover:bg-success-200 dark:bg-success-900/30 dark:text-success-300 dark:hover:bg-success-900/45",
|
||||
"bg-success/10 dark:bg-success/20 backdrop-blur-md text-success-700 dark:text-success-400 hover:bg-success/20 dark:hover:bg-success/30",
|
||||
warning:
|
||||
"bg-warning-100 text-warning-700 hover:bg-warning-200 dark:bg-warning-900/30 dark:text-warning-300 dark:hover:bg-warning-900/45",
|
||||
"bg-warning/10 dark:bg-warning/20 backdrop-blur-md text-warning-700 dark:text-warning-400 hover:bg-warning/20 dark:hover:bg-warning/30",
|
||||
danger:
|
||||
"bg-danger-100 text-danger-700 hover:bg-danger-200 dark:bg-danger-900/30 dark:text-danger-300 dark:hover:bg-danger-900/45",
|
||||
"bg-danger/10 dark:bg-danger/20 backdrop-blur-md text-danger-700 dark:text-danger-400 hover:bg-danger/20 dark:hover:bg-danger/30",
|
||||
};
|
||||
|
||||
const shadowColorClassMap: Record<HeroButtonColor, string> = {
|
||||
|
||||
@@ -21,7 +21,7 @@ export function CardHeader({
|
||||
export function CardBody({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<CardContent
|
||||
className={cn("p-4 pt-0 md:p-6 md:pt-0", className)}
|
||||
className={cn("p-4 md:p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -44,13 +44,14 @@ export function DropdownTrigger({ children }: { children: React.ReactNode }) {
|
||||
|
||||
export interface DropdownMenuProps {
|
||||
"aria-label"?: string;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function DropdownMenu({ children }: DropdownMenuProps) {
|
||||
export function DropdownMenu({ children, className }: DropdownMenuProps) {
|
||||
const { align } = React.useContext(DropdownContext);
|
||||
|
||||
return <DropdownMenuContent align={align}>{children}</DropdownMenuContent>;
|
||||
return <DropdownMenuContent align={align} className={className}>{children}</DropdownMenuContent>;
|
||||
}
|
||||
|
||||
export interface DropdownItemProps {
|
||||
|
||||
@@ -149,10 +149,10 @@ export function ModalContent({
|
||||
mapSize(resolvedSize),
|
||||
context?.classNames?.base,
|
||||
resolvedScrollBehavior === "outside"
|
||||
? "max-h-[90vh] overflow-y-auto"
|
||||
? "max-h-[90vh] overflow-y-auto scrollbar-hide"
|
||||
: "",
|
||||
resolvedScrollBehavior === "inside"
|
||||
? "max-h-[90vh] flex flex-col overflow-hidden [&>[data-slot=modal-body]]:min-h-0 [&>[data-slot=modal-body]]:flex-1 [&>[data-slot=modal-body]]:overflow-y-auto"
|
||||
? "max-h-[90vh] flex flex-col overflow-hidden [&_[data-slot=modal-body]]:min-h-0 [&_[data-slot=modal-body]]:flex-1 [&_[data-slot=modal-body]]:overflow-y-auto [&_[data-slot=modal-body]]:scrollbar-hide"
|
||||
: "",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -57,7 +57,7 @@ export function TableHeader({
|
||||
);
|
||||
|
||||
return (
|
||||
<thead className={cn("border-b bg-default-50/60", className)} {...props}>
|
||||
<thead className={cn("border-b", className)} {...props}>
|
||||
{hasRow ? children : <TableRow>{children}</TableRow>}
|
||||
</thead>
|
||||
);
|
||||
|
||||
@@ -211,3 +211,46 @@ body {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Liquid Glass Variables */
|
||||
:root {
|
||||
--glass-bg: rgba(255, 255, 255, 0.6);
|
||||
--glass-border: rgba(255, 255, 255, 0.8);
|
||||
--glass-card: rgba(255, 255, 255, 0.7);
|
||||
--glass-overlay: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--glass-bg: rgba(30, 30, 30, 0.6);
|
||||
--glass-border: rgba(255, 255, 255, 0.15);
|
||||
--glass-card: rgba(40, 40, 40, 0.6);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.scrollbar-hide {
|
||||
/* IE and Edge */
|
||||
-ms-overflow-style: none;
|
||||
/* Firefox */
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user