From ca245738039be90a0ed39973a0674e93933324a4 Mon Sep 17 00:00:00 2001 From: sagitchu Date: Sat, 21 Feb 2026 20:08:29 +0800 Subject: [PATCH 1/3] feat: redesign forward card view completely syncing filters and sorting logic --- vite-frontend/src/pages/forward.tsx | 203 ++++++++-------------------- 1 file changed, 55 insertions(+), 148 deletions(-) diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index 07dc3c1..3032f9f 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -119,18 +119,7 @@ interface ForwardForm { strategy: string; } -// 添加分组接口 -interface UserGroup { - userId: number | null; - userName: string; - tunnelGroups: TunnelGroup[]; -} -interface TunnelGroup { - tunnelId: number; - tunnelName: string; - forwards: Forward[]; -} export default function ForwardPage() { const [loading, setLoading] = useState(true); @@ -233,21 +222,6 @@ export default function ForwardPage() { setViewMode(newMode); try { localStorage.setItem("forward-view-mode", newMode); - - // 切换到直接显示模式时,初始化拖拽排序顺序 - if (newMode === "direct") { - const currentUserId = JwtUtil.getUserIdFromToken(); - const { order, fromDatabase } = buildForwardOrder( - forwards, - currentUserId, - ); - - setForwardOrder(order); - - if (fromDatabase) { - saveOrder(FORWARD_ORDER_KEY, order); - } - } } catch { } }; @@ -270,18 +244,16 @@ export default function ForwardPage() { setForwards(forwardsData); // 初始化拖拽排序顺序 - if (viewMode === "direct") { - const currentUserId = JwtUtil.getUserIdFromToken(); - const { order, fromDatabase } = buildForwardOrder( - forwardsData, - currentUserId, - ); + const currentUserId = JwtUtil.getUserIdFromToken(); + const { order, fromDatabase } = buildForwardOrder( + forwardsData, + currentUserId, + ); - setForwardOrder(order); + setForwardOrder(order); - if (fromDatabase) { - saveOrder(FORWARD_ORDER_KEY, order); - } + if (fromDatabase) { + saveOrder(FORWARD_ORDER_KEY, order); } } else { toast.error(forwardsRes.msg || "获取转发列表失败"); @@ -298,52 +270,7 @@ export default function ForwardPage() { } }; - // 按用户和隧道分组转发数据 - const groupForwardsByUserAndTunnel = (): UserGroup[] => { - const userMap = new Map(); - // 获取排序后的转发列表 - sortedForwards.forEach((forward) => { - const userKey = forward.userId ? forward.userId.toString() : "unknown"; - const userName = forward.userName || "未知用户"; - - if (!userMap.has(userKey)) { - userMap.set(userKey, { - userId: forward.userId || null, - userName, - tunnelGroups: [], - }); - } - - const userGroup = userMap.get(userKey)!; - let tunnelGroup = userGroup.tunnelGroups.find( - (tg) => tg.tunnelId === forward.tunnelId, - ); - - if (!tunnelGroup) { - tunnelGroup = { - tunnelId: forward.tunnelId, - tunnelName: forward.tunnelName, - forwards: [], - }; - userGroup.tunnelGroups.push(tunnelGroup); - } - - tunnelGroup.forwards.push(forward); - }); - - // 排序:先按用户名,再按隧道名 - const result = Array.from(userMap.values()); - - result.sort((a, b) => a.userName.localeCompare(b.userName)); - result.forEach((userGroup) => { - userGroup.tunnelGroups.sort((a, b) => - a.tunnelName.localeCompare(b.tunnelName), - ); - }); - - return result; - }; // 表单验证 const validateForm = (): boolean => { @@ -720,26 +647,10 @@ export default function ForwardPage() { setExportLoading(true); try { - // 根据当前显示模式获取要导出的转发列表 - let forwardsToExport: Forward[] = []; - - if (viewMode === "grouped") { - // 分组模式下,获取指定隧道的转发 - const userGroups = groupForwardsByUserAndTunnel(); - - forwardsToExport = userGroups.flatMap((userGroup) => - userGroup.tunnelGroups - .filter( - (tunnelGroup) => tunnelGroup.tunnelId === selectedTunnelForExport, - ) - .flatMap((tunnelGroup) => tunnelGroup.forwards), - ); - } else { - // 直接显示模式下,过滤指定隧道的转发 - forwardsToExport = sortedForwards.filter( - (forward) => forward.tunnelId === selectedTunnelForExport, - ); - } + // 获取要导出的转发列表 + const forwardsToExport = sortedForwards.filter( + (forward) => forward.tunnelId === selectedTunnelForExport, + ); if (forwardsToExport.length === 0) { toast.error("所选隧道没有转发数据"); @@ -1181,24 +1092,15 @@ export default function ForwardPage() { return []; } - // 在平铺模式下,只显示当前用户的转发 let filteredForwards = forwards; - if (viewMode === "direct") { - if (tokenUserId !== null) { - filteredForwards = forwards.filter( - (forward) => forward.userId === tokenUserId, - ); - } - } else if (viewMode === "grouped") { - if (filterUserId !== "all") { - const targetUserId = parseInt(filterUserId); - filteredForwards = filteredForwards.filter(f => f.userId === targetUserId || (targetUserId === 0 && !f.userId)); - } - if (filterTunnelId !== "all") { - const targetTunnelId = parseInt(filterTunnelId); - filteredForwards = filteredForwards.filter(f => f.tunnelId === targetTunnelId); - } + if (filterUserId !== "all") { + const targetUserId = parseInt(filterUserId); + filteredForwards = filteredForwards.filter(f => f.userId === targetUserId || (targetUserId === 0 && !f.userId)); + } + if (filterTunnelId !== "all") { + const targetTunnelId = parseInt(filterTunnelId); + filteredForwards = filteredForwards.filter(f => f.tunnelId === targetTunnelId); } // 确保过滤后的转发列表有效 @@ -1288,7 +1190,7 @@ export default function ForwardPage() { }, [forwards]); // 可拖拽的表格行组件 - const SortableTableRow = ({ forward, selectMode, selectedIds, toggleSelect, getStrategyDisplay, formatInAddress, formatRemoteAddress, handleServiceToggle, handleEdit, handleDelete, handleDiagnose, showAddressModal, hasMultipleAddresses }: any) => { + const SortableTableRow = ({ forward, selectMode, selectedIds, toggleSelect, getStrategyDisplay, formatInAddress, formatRemoteAddress, handleServiceToggle, handleEdit, handleDelete, handleDiagnose, showAddressModal, hasMultipleAddresses, formatFlow }: any) => { const { attributes, listeners, @@ -1386,6 +1288,11 @@ export default function ForwardPage() { {strategyDisplay.text} + + + {formatFlow((forward.inFlow || 0) + (forward.outFlow || 0))} + +
- {/* 筛选按钮 (仅在分组视图显示) */} - {viewMode === "grouped" && ( - - )} + + + {(filterUserId !== "all" || filterTunnelId !== "all") && ( + + )} + {/* 显示模式切换按钮 */} + ) : ( +
+ setSearchKeyword(e.target.value)} + /> + +
+ )} +
{/* 筛选按钮 */} + ) : ( +
+ setSearchKeyword(e.target.value)} + /> + +
+ )} +
{/* 统一卡片网格 */} - {rules.length > 0 ? ( + {filteredRules.length > 0 ? (
- {rules.map((rule) => ( + {filteredRules.map((rule) => ( ([]); const [nodeOrder, setNodeOrder] = useState([]); const [loading, setLoading] = useState(false); + const [searchKeyword, setSearchKeyword] = useState(""); + const [isSearchVisible, setIsSearchVisible] = useState(false); const [dialogVisible, setDialogVisible] = useState(false); const [dialogTitle, setDialogTitle] = useState(""); const [isEdit, setIsEdit] = useState(false); @@ -821,23 +824,23 @@ export default function NodePage() { prev.map((n) => n.id === form.id ? { - ...n, - name: form.name, - serverIp: - form.serverIpV4?.trim() || - form.serverIpV6?.trim() || - form.serverHost?.trim() || - "", - serverIpV4: form.serverIpV4, - serverIpV6: form.serverIpV6, - port: form.port, - tcpListenAddr: form.tcpListenAddr, - udpListenAddr: form.udpListenAddr, - interfaceName: form.interfaceName, - http: form.http, - tls: form.tls, - socks: form.socks, - } + ...n, + name: form.name, + serverIp: + form.serverIpV4?.trim() || + form.serverIpV6?.trim() || + form.serverHost?.trim() || + "", + serverIpV4: form.serverIpV4, + serverIpV6: form.serverIpV6, + port: form.port, + tcpListenAddr: form.tcpListenAddr, + udpListenAddr: form.udpListenAddr, + interfaceName: form.interfaceName, + http: form.http, + tls: form.tls, + socks: form.socks, + } : n, ), ); @@ -994,7 +997,19 @@ export default function NodePage() { const sortedNodes = useMemo((): Node[] => { if (!nodeList || nodeList.length === 0) return []; - const sortedByDb = [...nodeList].sort((a, b) => { + let filteredNodes = nodeList; + + if (searchKeyword.trim()) { + const lowerKeyword = searchKeyword.toLowerCase(); + filteredNodes = filteredNodes.filter(n => + (n.name && n.name.toLowerCase().includes(lowerKeyword)) || + (n.serverIp && n.serverIp.toLowerCase().includes(lowerKeyword)) || + (n.serverIpV4 && n.serverIpV4.toLowerCase().includes(lowerKeyword)) || + (n.serverIpV6 && n.serverIpV6.toLowerCase().includes(lowerKeyword)) + ); + } + + const sortedByDb = [...filteredNodes].sort((a, b) => { const aInx = a.inx ?? 0; const bInx = b.inx ?? 0; @@ -1007,7 +1022,7 @@ export default function NodePage() { nodeOrder.length > 0 && sortedByDb.every((n) => n.inx === undefined || n.inx === 0) ) { - const nodeMap = new Map(nodeList.map((n) => [n.id, n] as const)); + const nodeMap = new Map(filteredNodes.map((n) => [n.id, n] as const)); const localSorted: Node[] = []; nodeOrder.forEach((id) => { @@ -1016,7 +1031,7 @@ export default function NodePage() { if (node) localSorted.push(node); }); - nodeList.forEach((node) => { + filteredNodes.forEach((node) => { if (!nodeOrder.includes(node.id)) { localSorted.push(node); } @@ -1026,7 +1041,7 @@ export default function NodePage() { } return sortedByDb; - }, [nodeList, nodeOrder]); + }, [nodeList, nodeOrder, searchKeyword]); const sortableNodeIds = useMemo( () => sortedNodes.map((n) => n.id), @@ -1035,9 +1050,64 @@ export default function NodePage() { return (
- {/* 页面头部 */} -
-
+
+
+ {!isSearchVisible ? ( + + ) : ( +
+ setSearchKeyword(e.target.value)} + /> + +
+ )} +
{node.connectionStatus === "online" && - node.systemInfo + node.systemInfo ? formatSpeed( - node.systemInfo.downloadSpeed, - ) + node.systemInfo.downloadSpeed, + ) : "-"}
@@ -1373,10 +1443,10 @@ export default function NodePage() {
{node.connectionStatus === "online" && - node.systemInfo + node.systemInfo ? formatTraffic( - node.systemInfo.uploadTraffic, - ) + node.systemInfo.uploadTraffic, + ) : "-"}
@@ -1386,10 +1456,10 @@ export default function NodePage() {
{node.connectionStatus === "online" && - node.systemInfo + node.systemInfo ? formatTraffic( - node.systemInfo.downloadTraffic, - ) + node.systemInfo.downloadTraffic, + ) : "-"}
diff --git a/vite-frontend/src/pages/tunnel.tsx b/vite-frontend/src/pages/tunnel.tsx index 557bd2c..322b343 100644 --- a/vite-frontend/src/pages/tunnel.tsx +++ b/vite-frontend/src/pages/tunnel.tsx @@ -45,6 +45,7 @@ import { batchDeleteTunnels, batchRedeployTunnels, } from "@/api"; +import { SearchIcon } from "@/components/icons"; import { PageLoadingState } from "@/components/page-state"; import { buildDiagnosisFallbackResult, @@ -111,6 +112,8 @@ export default function TunnelPage() { const [tunnels, setTunnels] = useState([]); const [tunnelOrder, setTunnelOrder] = useState([]); const [nodes, setNodes] = useState([]); + const [searchKeyword, setSearchKeyword] = useState(""); + const [isSearchVisible, setIsSearchVisible] = useState(false); // 模态框状态 const [modalOpen, setModalOpen] = useState(false); @@ -597,7 +600,17 @@ export default function TunnelPage() { const sortedTunnels = useMemo((): Tunnel[] => { if (!tunnels || tunnels.length === 0) return []; - const sortedByDb = [...tunnels].sort((a, b) => { + let filteredTunnels = tunnels; + + if (searchKeyword.trim()) { + const lowerKeyword = searchKeyword.toLowerCase(); + filteredTunnels = filteredTunnels.filter(t => + (t.name && t.name.toLowerCase().includes(lowerKeyword)) || + (t.inIp && t.inIp.toLowerCase().includes(lowerKeyword)) + ); + } + + const sortedByDb = [...filteredTunnels].sort((a, b) => { const aInx = a.inx ?? 0; const bInx = b.inx ?? 0; @@ -610,7 +623,7 @@ export default function TunnelPage() { tunnelOrder.length > 0 && sortedByDb.every((t) => t.inx === undefined || t.inx === 0) ) { - const tunnelMap = new Map(tunnels.map((t) => [t.id, t] as const)); + const tunnelMap = new Map(filteredTunnels.map((t) => [t.id, t] as const)); const localSorted: Tunnel[] = []; tunnelOrder.forEach((id) => { @@ -619,7 +632,7 @@ export default function TunnelPage() { if (tunnel) localSorted.push(tunnel); }); - tunnels.forEach((tunnel) => { + filteredTunnels.forEach((tunnel) => { if (!tunnelOrder.includes(tunnel.id)) { localSorted.push(tunnel); } @@ -629,7 +642,7 @@ export default function TunnelPage() { } return sortedByDb; - }, [tunnels, tunnelOrder]); + }, [tunnels, tunnelOrder, searchKeyword]); const sortableTunnelIds = useMemo( () => sortedTunnels.map((t) => t.id), @@ -672,9 +685,64 @@ export default function TunnelPage() { return (
- {/* 页面头部 */} -
-
+
+
+ {!isSearchVisible ? ( + + ) : ( +
+ setSearchKeyword(e.target.value)} + /> + +
+ )} +
-
- - + ) : ( +
+ setSearchKeyword(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleSearch()} + /> + +
+ )}
+ +
{/* 用户列表 */} From a15be253f544c471067e0c599fc536ab39b80852 Mon Sep 17 00:00:00 2001 From: sagitchu Date: Sat, 21 Feb 2026 21:16:42 +0800 Subject: [PATCH 3/3] feat: add framer-motion animations (page transitions, modals, dropdowns, buttons, search bar) --- .../src/components/animated-page.tsx | 121 +++++++ vite-frontend/src/components/search-bar.tsx | 101 ++++++ vite-frontend/src/components/ui/button.tsx | 2 +- vite-frontend/src/components/ui/checkbox.tsx | 4 +- vite-frontend/src/components/ui/dialog.tsx | 2 +- .../src/components/ui/dropdown-menu.tsx | 4 +- vite-frontend/src/components/ui/select.tsx | 6 +- vite-frontend/src/layouts/admin.tsx | 51 ++- vite-frontend/src/pages/dashboard.tsx | 5 +- vite-frontend/src/pages/forward.tsx | 70 +--- vite-frontend/src/pages/index.tsx | 14 +- vite-frontend/src/pages/limit.tsx | 257 ++++++-------- vite-frontend/src/pages/node.tsx | 70 +--- vite-frontend/src/pages/tunnel.tsx | 70 +--- vite-frontend/src/pages/user.tsx | 316 +++++++++--------- 15 files changed, 583 insertions(+), 510 deletions(-) create mode 100644 vite-frontend/src/components/animated-page.tsx create mode 100644 vite-frontend/src/components/search-bar.tsx diff --git a/vite-frontend/src/components/animated-page.tsx b/vite-frontend/src/components/animated-page.tsx new file mode 100644 index 0000000..ed59695 --- /dev/null +++ b/vite-frontend/src/components/animated-page.tsx @@ -0,0 +1,121 @@ +import { motion } from "framer-motion"; +import React from "react"; + +/** + * Wraps page content with a smooth fade-in + slide-up entrance animation. + * Use this as the outermost wrapper inside each page component. + */ +export const AnimatedPage = ({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) => ( + + {children} + +); + +/** + * Stagger container — apply to the parent of a list/grid of animated items. + * Children should use `staggerItem` as their `variants` prop. + */ +export const staggerContainer = { + hidden: { opacity: 0 }, + show: { + opacity: 1, + transition: { + staggerChildren: 0.05, + delayChildren: 0.05, + }, + }, +}; + +/** + * Individual stagger item variant — fade-in + slide-up. + */ +export const staggerItem = { + hidden: { opacity: 0, y: 12 }, + show: { + opacity: 1, + y: 0, + transition: { duration: 0.25, ease: [0.25, 0.46, 0.45, 0.94] }, + }, +}; + +/** + * Convenience wrapper for a stagger list. + * Renders a `motion.div` (or `motion.ul/motion.tbody`) with stagger behaviour. + */ +export const StaggerList = ({ + children, + className, + as = "div", +}: { + children: React.ReactNode; + className?: string; + as?: "div" | "ul" | "tbody"; +}) => { + const Component = motion[as] as React.ElementType; + + return ( + + {children} + + ); +}; + +/** + * Individual animated item for use inside StaggerList. + */ +export const StaggerItem = ({ + children, + className, + as = "div", +}: { + children: React.ReactNode; + className?: string; + as?: "div" | "li" | "tr"; +}) => { + const Component = motion[as] as React.ElementType; + + return ( + + {children} + + ); +}; + +/** + * Simple fade-in animation for standalone elements (chips, badges, counters, etc.) + */ +export const FadeIn = ({ + children, + delay = 0, + className, +}: { + children: React.ReactNode; + delay?: number; + className?: string; +}) => ( + + {children} + +); diff --git a/vite-frontend/src/components/search-bar.tsx b/vite-frontend/src/components/search-bar.tsx new file mode 100644 index 0000000..6f68fff --- /dev/null +++ b/vite-frontend/src/components/search-bar.tsx @@ -0,0 +1,101 @@ +import { motion, AnimatePresence } from "framer-motion"; + +import { Button } from "@/shadcn-bridge/heroui/button"; +import { Input } from "@/shadcn-bridge/heroui/input"; +import { SearchIcon } from "@/components/icons"; + +interface SearchBarProps { + isVisible: boolean; + value: string; + placeholder?: string; + onOpen: () => void; + onClose: () => void; + onChange: (value: string) => void; +} + +export function SearchBar({ + isVisible, + value, + placeholder = "搜索", + onOpen, + onClose, + onChange, +}: SearchBarProps) { + return ( + // Fixed h-8 so the container never changes height — eliminates the vertical jitter +
+ + {!isVisible ? ( + + + + ) : ( + + onChange(e.target.value)} + /> + + + )} + +
+ ); +} diff --git a/vite-frontend/src/components/ui/button.tsx b/vite-frontend/src/components/ui/button.tsx index 14fe8bb..db8d1a6 100644 --- a/vite-frontend/src/components/ui/button.tsx +++ b/vite-frontend/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors transition-transform duration-100 active:scale-95 disabled:active:scale-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { diff --git a/vite-frontend/src/components/ui/checkbox.tsx b/vite-frontend/src/components/ui/checkbox.tsx index 30748e6..118cbcd 100644 --- a/vite-frontend/src/components/ui/checkbox.tsx +++ b/vite-frontend/src/components/ui/checkbox.tsx @@ -11,14 +11,14 @@ function Checkbox({ return ( diff --git a/vite-frontend/src/components/ui/dialog.tsx b/vite-frontend/src/components/ui/dialog.tsx index 9c086cc..a3f059f 100644 --- a/vite-frontend/src/components/ui/dialog.tsx +++ b/vite-frontend/src/components/ui/dialog.tsx @@ -57,7 +57,7 @@ function DialogContent({ {children} diff --git a/vite-frontend/src/layouts/admin.tsx b/vite-frontend/src/layouts/admin.tsx index c8c6b4e..4b40a70 100644 --- a/vite-frontend/src/layouts/admin.tsx +++ b/vite-frontend/src/layouts/admin.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from "react"; import { useNavigate, useLocation } from "react-router-dom"; import { toast } from "react-hot-toast"; +import { AnimatePresence, motion } from "framer-motion"; import { Button } from "@/shadcn-bridge/heroui/button"; import { @@ -335,21 +336,36 @@ export default function AdminLayout({ return (
  • - + {isActive && ( + + )} + {!isActive && ( + + )} +
    {item.icon}
    + {item.label} +
  • ); })} @@ -476,9 +492,20 @@ export default function AdminLayout({ {/* 主内容 */}
    - {children} + + + {children} + +
    diff --git a/vite-frontend/src/pages/dashboard.tsx b/vite-frontend/src/pages/dashboard.tsx index 652b2f1..4bbc131 100644 --- a/vite-frontend/src/pages/dashboard.tsx +++ b/vite-frontend/src/pages/dashboard.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import toast from "react-hot-toast"; +import { AnimatedPage } from "@/components/animated-page"; import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card"; import { Button } from "@/shadcn-bridge/heroui/button"; @@ -524,7 +525,7 @@ export default function DashboardPage() { } return ( -
    + {announcement && }
    -
    +
    ); } diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index 67bea0e..a1cd0aa 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -1,5 +1,7 @@ import { useState, useEffect, useMemo } from "react"; import toast from "react-hot-toast"; +import { AnimatedPage } from "@/components/animated-page"; +import { SearchBar } from "@/components/search-bar"; import { DndContext, closestCenter, @@ -76,7 +78,6 @@ import { executeForwardBatchToggleService, } from "@/pages/forward/batch-actions"; import { buildForwardOrder, FORWARD_ORDER_KEY } from "@/pages/forward/order"; -import { SearchIcon } from "@/components/icons"; import { PageLoadingState } from "@/components/page-state"; import { useMobileBreakpoint } from "@/hooks/useMobileBreakpoint"; import { saveOrder } from "@/utils/order-storage"; @@ -1619,65 +1620,18 @@ export default function ForwardPage() { } return ( -
    + {/* 页面头部 */}
    - {!isSearchVisible ? ( - - ) : ( -
    - setSearchKeyword(e.target.value)} - /> - -
    - )} + setIsSearchVisible(false)} + onOpen={() => setIsSearchVisible(true)} + />
    {/* 筛选按钮 */} @@ -3160,6 +3114,6 @@ export default function ForwardPage() { )} -
    + ); } diff --git a/vite-frontend/src/pages/index.tsx b/vite-frontend/src/pages/index.tsx index 37756e1..b2a5a52 100644 --- a/vite-frontend/src/pages/index.tsx +++ b/vite-frontend/src/pages/index.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; import toast from "react-hot-toast"; import { Turnstile } from "@marsidev/react-turnstile"; +import { motion } from "framer-motion"; import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card"; import { Input } from "@/shadcn-bridge/heroui/input"; @@ -152,7 +153,12 @@ export default function IndexPage() { return (
    -
    +

    登陆

    @@ -203,7 +209,7 @@ export default function IndexPage() {
    -
    + {/* 版权信息 - 固定在底部,不占据布局空间 */} @@ -253,9 +259,9 @@ export default function IndexPage() { theme: (document.documentElement.classList.contains( "dark", ) || - document.documentElement.getAttribute("data-theme") === + document.documentElement.getAttribute("data-theme") === "dark" || - window.matchMedia("(prefers-color-scheme: dark)").matches + window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light") as "light" | "dark" | "auto", }} diff --git a/vite-frontend/src/pages/limit.tsx b/vite-frontend/src/pages/limit.tsx index 9d9e819..5048b8a 100644 --- a/vite-frontend/src/pages/limit.tsx +++ b/vite-frontend/src/pages/limit.tsx @@ -1,5 +1,7 @@ import { useState, useEffect, useMemo } from "react"; import toast from "react-hot-toast"; +import { AnimatedPage, StaggerList, StaggerItem } from "@/components/animated-page"; +import { SearchBar } from "@/components/search-bar"; import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card"; import { Button } from "@/shadcn-bridge/heroui/button"; @@ -20,7 +22,6 @@ import { deleteSpeedLimit, getTunnelList, } from "@/api"; -import { SearchIcon } from "@/components/icons"; import { PageLoadingState } from "@/components/page-state"; interface SpeedLimitRule { @@ -232,64 +233,17 @@ export default function LimitPage() { } return ( -
    +
    - {!isSearchVisible ? ( - - ) : ( -
    - setSearchKeyword(e.target.value)} - /> - -
    - )} + setIsSearchVisible(false)} + onOpen={() => setIsSearchVisible(true)} + />
    - -
    - - +
    + + +
    + + + ))} -
    + ) : ( /* 空状态 */ @@ -572,6 +525,6 @@ export default function LimitPage() { )} -
    + ); } diff --git a/vite-frontend/src/pages/node.tsx b/vite-frontend/src/pages/node.tsx index e554465..2ff5562 100644 --- a/vite-frontend/src/pages/node.tsx +++ b/vite-frontend/src/pages/node.tsx @@ -1,5 +1,7 @@ import { useState, useEffect, useMemo, useCallback } from "react"; import toast from "react-hot-toast"; +import { AnimatedPage } from "@/components/animated-page"; +import { SearchBar } from "@/components/search-bar"; import { DndContext, KeyboardSensor, @@ -57,7 +59,6 @@ import { getRemoteSyncErrorMessage, } from "@/pages/node/display"; import { tryCopyInstallCommand } from "@/pages/node/install-command"; -import { SearchIcon } from "@/components/icons"; import { buildNodeSystemInfo } from "@/pages/node/system-info"; import { useNodeOfflineTimers } from "@/pages/node/use-node-offline-timers"; import { useNodeRealtime } from "@/pages/node/use-node-realtime"; @@ -1049,64 +1050,17 @@ export default function NodePage() { ); return ( -
    +
    - {!isSearchVisible ? ( - - ) : ( -
    - setSearchKeyword(e.target.value)} - /> - -
    - )} + setIsSearchVisible(false)} + onOpen={() => setIsSearchVisible(true)} + />
    @@ -2168,6 +2122,6 @@ export default function NodePage() { )} -
    + ); } diff --git a/vite-frontend/src/pages/tunnel.tsx b/vite-frontend/src/pages/tunnel.tsx index 322b343..accb43a 100644 --- a/vite-frontend/src/pages/tunnel.tsx +++ b/vite-frontend/src/pages/tunnel.tsx @@ -1,5 +1,7 @@ import { useState, useEffect, useMemo } from "react"; import toast from "react-hot-toast"; +import { AnimatedPage } from "@/components/animated-page"; +import { SearchBar } from "@/components/search-bar"; import { DndContext, KeyboardSensor, @@ -45,7 +47,6 @@ import { batchDeleteTunnels, batchRedeployTunnels, } from "@/api"; -import { SearchIcon } from "@/components/icons"; import { PageLoadingState } from "@/components/page-state"; import { buildDiagnosisFallbackResult, @@ -684,64 +685,17 @@ export default function TunnelPage() { } return ( -
    +
    - {!isSearchVisible ? ( - - ) : ( -
    - setSearchKeyword(e.target.value)} - /> - -
    - )} + setIsSearchVisible(false)} + onOpen={() => setIsSearchVisible(true)} + />
    @@ -2483,6 +2437,6 @@ export default function TunnelPage() { )} -
    + ); } diff --git a/vite-frontend/src/pages/user.tsx b/vite-frontend/src/pages/user.tsx index 5edb9fd..166f66b 100644 --- a/vite-frontend/src/pages/user.tsx +++ b/vite-frontend/src/pages/user.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from "react"; import toast from "react-hot-toast"; +import { AnimatedPage, StaggerList, StaggerItem } from "@/components/animated-page"; import { parseDate } from "@internationalized/date"; import { Button } from "@/shadcn-bridge/heroui/button"; @@ -610,7 +611,7 @@ export default function UserPage() { }; return ( -
    + {/* 页面头部 */}
    @@ -692,7 +693,7 @@ export default function UserPage() { ) : ( -
    + {users.map((user) => { const userStatus = getUserStatus(user); const expStatus = user.expTime @@ -708,173 +709,174 @@ export default function UserPage() { : 0; return ( - - -
    -
    -

    - {user.name || user.user} -

    -

    - @{user.user} -

    + + + +
    +
    +

    + {user.name || user.user} +

    +

    + @{user.user} +

    +
    +
    + + {userStatus.text} + +
    -
    - - {userStatus.text} - -
    -
    - + - -
    - {/* 流量信息 */} -
    -
    - 流量限制 - - {formatFlow(user.flow, "gb")} - -
    -
    - 已使用 - - {formatFlow(usedFlow)} - -
    - 90 - ? "danger" - : flowPercent > 70 - ? "warning" - : "success" - } - size="sm" - value={flowPercent} - /> -
    - - {/* 其他信息 */} -
    -
    - 转发数量 - {user.num} -
    -
    - 重置日期 - - {user.flowResetTime === 0 - ? "不重置" - : `每月${user.flowResetTime}号`} - -
    - {user.expTime && ( + +
    + {/* 流量信息 */} +
    - 过期时间 -
    - {expStatus && expStatus.color === "success" ? ( -
    - {formatDate(user.expTime)} -
    - ) : ( - - {expStatus?.text || "未知状态"} - - )} -
    + 流量限制 + + {formatFlow(user.flow, "gb")} +
    - )} -
    -
    +
    + 已使用 + + {formatFlow(usedFlow)} + +
    + 90 + ? "danger" + : flowPercent > 70 + ? "warning" + : "success" + } + size="sm" + value={flowPercent} + /> +
    -
    - {/* 第一行:编辑和重置 */} -
    - - + {/* 其他信息 */} +
    +
    + 转发数量 + {user.num} +
    +
    + 重置日期 + + {user.flowResetTime === 0 + ? "不重置" + : `每月${user.flowResetTime}号`} + +
    + {user.expTime && ( +
    + 过期时间 +
    + {expStatus && expStatus.color === "success" ? ( +
    + {formatDate(user.expTime)} +
    + ) : ( + + {expStatus?.text || "未知状态"} + + )} +
    +
    + )} +
    - {/* 第二行:权限和删除 */} -
    - - +
    + {/* 第一行:编辑和重置 */} +
    + + +
    + + {/* 第二行:权限和删除 */} +
    + + +
    -
    - - + + + ); })} -
    + )} {/* 用户表单模态框 */} - -
    + ); }