feat: Implement toggleable search input and data filtering across tunnel, user, and forward pages.

This commit is contained in:
sagitchu
2026-02-21 20:43:32 +08:00
parent ca24573803
commit 572d1c16a6
5 changed files with 390 additions and 86 deletions
+71 -3
View File
@@ -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<Forward[]>([]);
const [tunnels, setTunnels] = useState<Tunnel[]>([]);
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 (
<div className="px-3 lg:px-6 py-8">
{/* 页面头部 */}
<div className="flex items-center justify-between mb-6 gap-2">
<div className="flex-1"></div>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between mb-6 gap-3">
<div className="flex-1 max-w-sm flex items-center gap-2">
{!isSearchVisible ? (
<Button
isIconOnly
aria-label="搜索"
className="text-default-600"
color="default"
size="sm"
variant="flat"
onPress={() => setIsSearchVisible(true)}
>
<SearchIcon className="w-4 h-4" />
</Button>
) : (
<div className="flex w-full items-center gap-2 animate-appearance-in">
<Input
autoFocus
classNames={{
base: "bg-default-100",
input: "bg-transparent",
inputWrapper:
"bg-default-100 border-2 border-default-200 hover:border-default-300 data-[hover=true]:border-default-300",
}}
placeholder="搜索转发名称、地址或用户名"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
/>
<Button
isIconOnly
aria-label="关闭搜索"
className="text-default-600 shrink-0"
color="default"
size="sm"
variant="light"
onPress={() => {
setIsSearchVisible(false);
setSearchKeyword("");
}}
>
<svg
aria-hidden="true"
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M6 18L18 6M6 6l12 12"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
/>
</svg>
</Button>
</div>
)}
</div>
<div className="flex flex-wrap items-center justify-end gap-2 sm:gap-3">
{/* 筛选按钮 */}
<Button