From b01dbdb6e52f690eec8481b1df4969c2e2865b13 Mon Sep 17 00:00:00 2001 From: sagitchu Date: Sat, 28 Feb 2026 17:20:10 +0800 Subject: [PATCH] feat(frontend): group forwards by user with admin priority sorting - Add ForwardUserGroup interface and helper utilities - Group forwards by user in both grouped table and card views - Sort user groups with admin's own group first, then alphabetically - Restrict drag-and-drop to same user group only - Remove redundant user column from grouped table view - Add user group headers with forward count badges --- vite-frontend/src/pages/forward.tsx | 360 +++++++++++++++++++++------- 1 file changed, 268 insertions(+), 92 deletions(-) diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index 5c77faf..4f6c92d 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -130,6 +130,27 @@ interface ForwardForm { speedId: number | null; } +interface ForwardUserGroup { + userId: number; + userName: string; + items: Forward[]; +} + +const UNKNOWN_FORWARD_USER_NAME = "未知用户"; + +const normalizeForwardUserName = (userName?: string): string => { + const normalized = (userName || UNKNOWN_FORWARD_USER_NAME).trim(); + + return normalized || UNKNOWN_FORWARD_USER_NAME; +}; + +const compareForwardUserNameAsc = (a: string, b: string): number => { + return a.localeCompare(b, "en", { + sensitivity: "base", + numeric: true, + }); +}; + export default function ForwardPage() { const [loading, setLoading] = useState(true); const [forwards, setForwards] = useState([]); @@ -1145,6 +1166,16 @@ export default function ForwardPage() { // 检查 ID 是否有效 if (isNaN(activeId) || isNaN(overId)) return; + const activeForward = forwards.find((forward) => forward.id === activeId); + const overForward = forwards.find((forward) => forward.id === overId); + const activeUserId = activeForward?.userId ?? 0; + const overUserId = overForward?.userId ?? 0; + + // 仅允许在同一用户分组内拖拽,避免不同用户数据混排 + if (activeUserId !== overUserId) { + return; + } + const oldIndex = forwardOrder.indexOf(activeId); const newIndex = forwardOrder.indexOf(overId); @@ -1342,9 +1373,11 @@ export default function ForwardPage() { ); const tokenUserId = JwtUtil.getUserIdFromToken(); + const tokenRoleId = JwtUtil.getRoleIdFromToken(); + const isAdmin = tokenRoleId === 0; // 根据排序顺序获取转发列表 - const sortedForwards = useMemo((): Forward[] => { + const orderedForwards = useMemo((): Forward[] => { // 确保 forwards 数组存在且有效 if (!forwards || forwards.length === 0) { return []; @@ -1426,16 +1459,70 @@ export default function ForwardPage() { }, [ forwards, forwardOrder, - viewMode, - tokenUserId, filterUserId, filterTunnelId, searchKeyword, ]); - const sortableForwardIds = useMemo( - () => sortedForwards.map((f) => f.id).filter((id) => id > 0), - [sortedForwards], + const groupedForwards = useMemo((): ForwardUserGroup[] => { + if (orderedForwards.length === 0) { + return []; + } + + const userGroupMap = new Map(); + + orderedForwards.forEach((forward) => { + const userId = forward.userId ?? 0; + const userName = normalizeForwardUserName(forward.userName); + const existingGroup = userGroupMap.get(userId); + + if (!existingGroup) { + userGroupMap.set(userId, { + userId, + userName, + items: [forward], + }); + + return; + } + + existingGroup.items.push(forward); + + if ( + existingGroup.userName === UNKNOWN_FORWARD_USER_NAME && + userName !== UNKNOWN_FORWARD_USER_NAME + ) { + existingGroup.userName = userName; + } + }); + + const groups = Array.from(userGroupMap.values()); + + groups.sort((a, b) => { + if (isAdmin && tokenUserId !== null) { + const aIsSelf = a.userId === tokenUserId; + const bIsSelf = b.userId === tokenUserId; + + if (aIsSelf !== bIsSelf) { + return aIsSelf ? -1 : 1; + } + } + + const nameCompare = compareForwardUserNameAsc(a.userName, b.userName); + + if (nameCompare !== 0) { + return nameCompare; + } + + return a.userId - b.userId; + }); + + return groups; + }, [orderedForwards, isAdmin, tokenUserId]); + + const sortedForwards = useMemo( + () => groupedForwards.flatMap((group) => group.items), + [groupedForwards], ); // 可拖拽的转发卡片组件 @@ -1474,15 +1561,46 @@ export default function ForwardPage() { const userMap = new Map(); forwards.forEach((f) => { - const uId = f.userId || 0; + const uId = f.userId ?? 0; + const userName = normalizeForwardUserName(f.userName); + const existingUser = userMap.get(uId); - if (!userMap.has(uId)) { - userMap.set(uId, { id: uId, name: f.userName || "未知用户" }); + if (!existingUser) { + userMap.set(uId, { id: uId, name: userName }); + return; + } + + if ( + existingUser.name === UNKNOWN_FORWARD_USER_NAME && + userName !== UNKNOWN_FORWARD_USER_NAME + ) { + existingUser.name = userName; } }); - return Array.from(userMap.values()); - }, [forwards]); + const users = Array.from(userMap.values()); + + users.sort((a, b) => { + if (isAdmin && tokenUserId !== null) { + const aIsSelf = a.id === tokenUserId; + const bIsSelf = b.id === tokenUserId; + + if (aIsSelf !== bIsSelf) { + return aIsSelf ? -1 : 1; + } + } + + const nameCompare = compareForwardUserNameAsc(a.name, b.name); + + if (nameCompare !== 0) { + return nameCompare; + } + + return a.id - b.id; + }); + + return users; + }, [forwards, isAdmin, tokenUserId]); // 可拖拽的表格行组件 const SortableTableRow = ({ @@ -1552,11 +1670,6 @@ export default function ForwardPage() { - - - {forward.userName || "未知用户"} - - {forward.name} @@ -2207,63 +2320,93 @@ export default function ForwardPage() { {/* 根据显示模式渲染不同内容 */} {viewMode === "grouped" ? ( sortedForwards.length > 0 ? ( -
- - - - {selectMode && ( - 选择 - )} - - 用户 - 名称 - 隧道 - 入口 - 目标 - 策略 - 总流量 - 状态 - 操作 - - - {(forward) => ( - + {groupedForwards.map((group) => { + const groupSortableForwardIds = group.items + .map((item) => item.id) + .filter((id) => id > 0); + const isSelfGroup = + isAdmin && tokenUserId !== null && group.userId === tokenUserId; + + return ( +
+
+
+ + {group.userName} + + {isSelfGroup && ( + + 管理员本人 + + )} +
+ + {group.items.length} 条转发 + +
+ + +
- - - )} - -
-
+ + {selectMode && ( + 选择 + )} + + 名称 + 隧道 + 入口 + 目标 + 策略 + 总流量 + 状态 + 操作 + + + {(forward) => ( + + + + )} + + + +
+ ); + })} ) : ( /* 空状态 */ @@ -2279,26 +2422,59 @@ export default function ForwardPage() { ) ) : /* 直接显示模式 */ - forwards.length > 0 ? ( - {}} // 添加空的 onDragStart 处理器 - > - -
- {sortedForwards.map((forward) => - forward && forward.id ? ( - - ) : null, - )} -
-
-
+ sortedForwards.length > 0 ? ( +
+ {groupedForwards.map((group) => { + const groupSortableForwardIds = group.items + .map((item) => item.id) + .filter((id) => id > 0); + const isSelfGroup = + isAdmin && tokenUserId !== null && group.userId === tokenUserId; + + return ( +
+
+
+ + {group.userName} + + {isSelfGroup && ( + + 管理员本人 + + )} +
+ + {group.items.length} 条转发 + +
+ + {}} // 添加空的 onDragStart 处理器 + > + +
+ {group.items.map((forward) => + forward && forward.id ? ( + + ) : null, + )} +
+
+
+
+ ); + })} +
) : ( /* 空状态 */