diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index 7508435..da75131 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -15,6 +15,7 @@ import { SortableContext, sortableKeyboardCoordinates, rectSortingStrategy, + verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; @@ -143,16 +144,21 @@ export default function ForwardPage() { const savedMode = localStorage.getItem("forward-view-mode"); return (savedMode as "grouped" | "direct") || "direct"; - } catch { + } catch (e) { return "direct"; } }); + // 筛选状态 + const [filterUserId, setFilterUserId] = useState("all"); + const [filterTunnelId, setFilterTunnelId] = useState("all"); + // 拖拽排序相关状态 const [forwardOrder, setForwardOrder] = useState([]); // 模态框状态 const [modalOpen, setModalOpen] = useState(false); + const [isFilterModalOpen, setIsFilterModalOpen] = useState(false); const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [addressModalOpen, setAddressModalOpen] = useState(false); const [diagnosisModalOpen, setDiagnosisModalOpen] = useState(false); @@ -1184,6 +1190,15 @@ export default function ForwardPage() { (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); + } } // 确保过滤后的转发列表有效 @@ -1227,7 +1242,7 @@ export default function ForwardPage() { } return sortedByDb; - }, [forwards, forwardOrder, viewMode, tokenUserId]); + }, [forwards, forwardOrder, viewMode, tokenUserId, filterUserId, filterTunnelId]); const sortableForwardIds = useMemo( () => sortedForwards.map((f) => f.id).filter((id) => id > 0), @@ -1259,6 +1274,170 @@ export default function ForwardPage() { ); }; + // 生成用作筛选项的用户和隧道列表 + const uniqueUsers = useMemo(() => { + const userMap = new Map(); + forwards.forEach((f) => { + const uId = f.userId || 0; + if (!userMap.has(uId)) { + userMap.set(uId, { id: uId, name: f.userName || "未知用户" }); + } + }); + + return Array.from(userMap.values()); + }, [forwards]); + + // 可拖拽的表格行组件 + const SortableTableRow = ({ forward, selectMode, selectedIds, toggleSelect, getStrategyDisplay, formatInAddress, formatRemoteAddress, handleServiceToggle, handleEdit, handleDelete, handleDiagnose, showAddressModal, hasMultipleAddresses }: any) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: forward.id }); + + const style = { + transform: transform ? CSS.Transform.toString(transform) : undefined, + transition: isDragging ? undefined : transition || undefined, + opacity: isDragging ? 0.5 : 1, + backgroundColor: isDragging ? "var(--nextui-default-100)" : undefined, + }; + + const strategyDisplay = getStrategyDisplay(forward.strategy); + + return ( + + {selectMode && ( + + toggleSelect(forward.id)} + /> + + )} + +
+ +
+
+ + + {forward.userName || "未知用户"} + + + + + {forward.tunnelName} + + + + {forward.name} + + + + + + + + + + {strategyDisplay.text} + + + +
+ handleServiceToggle(forward)} + /> +
+
+ +
+ + + +
+
+
+ ); + }; + // 渲染转发卡片 const renderForwardCard = (forward: Forward, listeners?: any) => { const statusDisplay = getStatusDisplay(forward.status); @@ -1524,8 +1703,39 @@ export default function ForwardPage() {
{/* 页面头部 */}
-
+
+ {/* 筛选按钮 (仅在分组视图显示) */} + {viewMode === "grouped" && ( + + )} {/* 显示模式切换按钮 */} + + + + )} + +
); } diff --git a/vite-frontend/src/shadcn-bridge/heroui/table.tsx b/vite-frontend/src/shadcn-bridge/heroui/table.tsx index 800fc23..a26afe1 100644 --- a/vite-frontend/src/shadcn-bridge/heroui/table.tsx +++ b/vite-frontend/src/shadcn-bridge/heroui/table.tsx @@ -72,18 +72,24 @@ interface TableBodyProps loadingContent?: React.ReactNode; } -export function TableBody({ - children, - className, - emptyContent, - isLoading, - items, - loadingContent, - ...props -}: TableBodyProps) { +export const TableBody = React.forwardRef< + HTMLTableSectionElement, + TableBodyProps +>(function TableBody( + { + children, + className, + emptyContent, + isLoading, + items, + loadingContent, + ...props + }: TableBodyProps, + ref: React.Ref, +) { if (isLoading) { return ( - + {loadingContent ?? "加载中..."} @@ -96,7 +102,7 @@ export function TableBody({ if (items && typeof children === "function") { if (items.length === 0) { return ( - + {emptyContent ?? "暂无数据"} @@ -107,7 +113,7 @@ export function TableBody({ } return ( - + {items.map((item) => { const key = typeof item === "object" && item !== null && "id" in item @@ -125,7 +131,7 @@ export function TableBody({ if (staticChildren.length === 0) { return ( - + {emptyContent ?? "暂无数据"} @@ -136,11 +142,11 @@ export function TableBody({ } return ( - + {typeof children === "function" ? null : children} ); -} +}); export function TableColumn({ className, @@ -160,16 +166,21 @@ export function TableColumn({ ); } -export function TableRow({ className, ...props }: React.ComponentProps<"tr">) { +export const TableRow = React.forwardRef< + HTMLTableRowElement, + React.ComponentProps<"tr"> +>(({ className, ...props }, ref) => { const { trClassName } = React.useContext(TableStyleContext); return ( ); -} +}); +TableRow.displayName = "TableRow"; export function TableCell({ className, ...props }: React.ComponentProps<"td">) { const { tdClassName } = React.useContext(TableStyleContext);