diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index 3032f9f..67bea0e 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -76,6 +76,7 @@ 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"; @@ -126,6 +127,8 @@ export default function ForwardPage() { const [forwards, setForwards] = useState([]); const [tunnels, setTunnels] = useState([]); const isMobile = useMobileBreakpoint(); + const [searchKeyword, setSearchKeyword] = useState(""); + const [isSearchVisible, setIsSearchVisible] = useState(false); // 显示模式状态 - 从localStorage读取,默认为平铺显示 const [viewMode, setViewMode] = useState<"grouped" | "direct">(() => { @@ -1103,6 +1106,15 @@ export default function ForwardPage() { filteredForwards = filteredForwards.filter(f => f.tunnelId === targetTunnelId); } + if (searchKeyword.trim()) { + const lowerKeyword = searchKeyword.toLowerCase(); + filteredForwards = filteredForwards.filter(f => + (f.name && f.name.toLowerCase().includes(lowerKeyword)) || + (f.remoteAddr && f.remoteAddr.toLowerCase().includes(lowerKeyword)) || + (f.userName && f.userName.toLowerCase().includes(lowerKeyword)) + ); + } + // 确保过滤后的转发列表有效 if (!filteredForwards || filteredForwards.length === 0) { return []; @@ -1144,7 +1156,7 @@ export default function ForwardPage() { } return sortedByDb; - }, [forwards, forwardOrder, viewMode, tokenUserId, filterUserId, filterTunnelId]); + }, [forwards, forwardOrder, viewMode, tokenUserId, filterUserId, filterTunnelId, searchKeyword]); const sortableForwardIds = useMemo( () => sortedForwards.map((f) => f.id).filter((id) => id > 0), @@ -1609,8 +1621,64 @@ export default function ForwardPage() { return (
{/* 页面头部 */} -
-
+
+
+ {!isSearchVisible ? ( + + ) : ( +
+ 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()} + /> + +
+ )}
+ +
{/* 用户列表 */}