diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index d2de647..8966ea8 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -63,6 +63,7 @@ import { resumeForwardService, diagnoseForward, updateForwardOrder, + getConfigByName, } from "@/api"; import { type ForwardAddressItem, @@ -145,6 +146,8 @@ interface ForwardTunnelGroup { const UNKNOWN_FORWARD_USER_NAME = "未知用户"; const UNCATEGORIZED_FORWARD_TUNNEL_NAME = "未分类"; +const FORWARD_COMPACT_MODE_CONFIG_KEY = "forward_compact_mode"; +const FORWARD_COMPACT_MODE_EVENT = "forwardCompactModeChanged"; const FORWARD_GROUPED_TABLE_MIN_WIDTH_CLASS = "min-w-[1320px]"; const FORWARD_GROUPED_TABLE_COLUMN_CLASS = { select: "w-14", @@ -205,6 +208,7 @@ export default function ForwardPage() { "", ); const [isSearchVisible, setIsSearchVisible] = useState(false); + const [compactMode, setCompactMode] = useState(false); // 显示模式状态 - 从localStorage读取,默认为平铺显示 const [viewMode, setViewMode] = useState<"grouped" | "direct">(() => { @@ -308,6 +312,43 @@ export default function ForwardPage() { }; }, []); + useEffect(() => { + const loadForwardCompactMode = async () => { + try { + const response = await getConfigByName(FORWARD_COMPACT_MODE_CONFIG_KEY); + const enabled = + response.code === 0 && + typeof response.data?.value === "string" && + response.data.value === "true"; + + setCompactMode(enabled); + } catch { + setCompactMode(false); + } + }; + + const handleCompactModeChanged = (event: Event) => { + const customEvent = event as CustomEvent<{ enabled?: boolean }>; + + if (typeof customEvent.detail?.enabled === "boolean") { + setCompactMode(customEvent.detail.enabled); + } + }; + + loadForwardCompactMode(); + window.addEventListener( + FORWARD_COMPACT_MODE_EVENT, + handleCompactModeChanged, + ); + + return () => { + window.removeEventListener( + FORWARD_COMPACT_MODE_EVENT, + handleCompactModeChanged, + ); + }; + }, []); + const parseShareIdFromTunnelName = (tunnelName: string): number | null => { const normalized = (tunnelName || "").trim(); @@ -1372,9 +1413,14 @@ export default function ForwardPage() { ); const overTunnelGroupKey = buildForwardTunnelGroupKey(overForward?.tunnelName); - // 仅允许在同一用户+隧道分组内拖拽,避免混排 - if (activeUserId !== overUserId || activeTunnelGroupKey !== overTunnelGroupKey) { - return; + // 非精简模式仅允许在同一用户+隧道分组内拖拽,避免混排 + if (!compactMode) { + if ( + activeUserId !== overUserId || + activeTunnelGroupKey !== overTunnelGroupKey + ) { + return; + } } const oldIndex = forwardOrder.indexOf(activeId); @@ -1773,12 +1819,19 @@ export default function ForwardPage() { return groups; }, [orderedForwards, isAdmin, tokenUserId]); - const sortedForwards = useMemo( - () => - groupedForwards.flatMap((group) => - group.tunnels.flatMap((tunnel) => tunnel.items), - ), - [groupedForwards], + const sortedForwards = useMemo(() => { + if (compactMode) { + return orderedForwards; + } + + return groupedForwards.flatMap((group) => + group.tunnels.flatMap((tunnel) => tunnel.items), + ); + }, [compactMode, orderedForwards, groupedForwards]); + + const sortableForwardIds = useMemo( + () => sortedForwards.map((f) => f.id).filter((id) => id > 0), + [sortedForwards], ); // 可拖拽的转发卡片组件 @@ -2066,6 +2119,221 @@ export default function ForwardPage() { ); }; + const SortableCompactTableRow = ({ + forward, + selectMode, + selectedIds, + toggleSelect, + getStrategyDisplay, + formatInAddress, + formatRemoteAddress, + handleServiceToggle, + handleEdit, + handleDelete, + handleDiagnose, + showAddressModal, + hasMultipleAddresses, + formatFlow, + }: any) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: forward.id }); + + const style = { + transform: transform + ? CSS.Transform.toString({ + ...transform, + x: Math.round(transform.x), + y: Math.round(transform.y), + }) + : 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.name} + + + + + {forward.tunnelName} + + + + + + + + + + + + {strategyDisplay.text} + + + + + {formatFlow(getForwardDisplayFlow(forward))} + + + +
+ handleServiceToggle(forward)} + /> +
+
+ +
+ + + +
+
+
+ ); + }; + // 渲染转发卡片 const renderForwardCard = (forward: Forward, listeners?: any) => { const statusDisplay = getStatusDisplay(forward.status); @@ -2571,7 +2839,107 @@ export default function ForwardPage() { {/* 根据显示模式渲染不同内容 */} - {viewMode === "grouped" ? ( + {compactMode ? ( + viewMode === "grouped" ? ( + sortedForwards.length > 0 ? ( +
+ + + + {selectMode && 选择} + + 用户 + 名称 + 隧道 + 入口 + 目标 + 策略 + 总流量 + 状态 + 操作 + + + {(forward) => ( + + + + )} + +
+
+
+ ) : ( + + +

+ 暂无转发配置 +

+

+ 还没有创建任何转发配置,点击上方按钮开始创建 +

+
+
+ ) + ) : sortedForwards.length > 0 ? ( + {}} + > + +
+ {sortedForwards.map((forward) => + forward && forward.id ? ( + + ) : null, + )} +
+
+
+ ) : ( + + +

+ 暂无转发配置 +

+

+ 还没有创建任何转发配置,点击上方按钮开始创建 +

+
+
+ ) + ) : viewMode === "grouped" ? ( sortedForwards.length > 0 ? (
{groupedForwards.map((group) => { @@ -2734,7 +3102,6 @@ export default function ForwardPage() { })}
) : ( - /* 空状态 */

@@ -2746,8 +3113,7 @@ export default function ForwardPage() { ) - ) : /* 直接显示模式 */ - sortedForwards.length > 0 ? ( + ) : sortedForwards.length > 0 ? (
{groupedForwards.map((group) => { const isSelfGroup = @@ -2771,7 +3137,7 @@ export default function ForwardPage() { 管理员本人 - )} + )}
{groupForwardCount} 条转发 @@ -2802,7 +3168,7 @@ export default function ForwardPage() { collisionDetection={closestCenter} sensors={sensors} onDragEnd={handleDragEnd} - onDragStart={() => {}} // 添加空的 onDragStart 处理器 + onDragStart={() => {}} > ) : ( - /* 空状态 */

diff --git a/vite-frontend/src/pages/settings.tsx b/vite-frontend/src/pages/settings.tsx index 185702e..fb565d7 100644 --- a/vite-frontend/src/pages/settings.tsx +++ b/vite-frontend/src/pages/settings.tsx @@ -6,12 +6,15 @@ import { Input } from "@/shadcn-bridge/heroui/input"; import { Button } from "@/shadcn-bridge/heroui/button"; import { Card, CardBody } from "@/shadcn-bridge/heroui/card"; import { Select, SelectItem } from "@/shadcn-bridge/heroui/select"; +import { Switch } from "@/shadcn-bridge/heroui/switch"; import { reinitializeBaseURL } from "@/api/network"; +import { getConfigByName, updateConfig } from "@/api"; import { type UpdateReleaseChannel, getUpdateReleaseChannel, setUpdateReleaseChannel, } from "@/utils/version-update"; +import { isAdmin } from "@/utils/auth"; import { getPanelAddresses, savePanelAddress, @@ -26,6 +29,8 @@ interface PanelAddress { inx: boolean; } +const FORWARD_COMPACT_MODE_CONFIG_KEY = "forward_compact_mode"; + export const SettingsPage = () => { const navigate = useNavigate(); const [panelAddresses, setPanelAddresses] = useState([]); @@ -34,6 +39,11 @@ export const SettingsPage = () => { const [updateChannel, setUpdateChannel] = useState( getUpdateReleaseChannel(), ); + const [forwardCompactMode, setForwardCompactMode] = useState(false); + const [forwardCompactModeSaving, setForwardCompactModeSaving] = + useState(false); + + const admin = isAdmin(); const setPanelAddressesFunc = (newAddress: PanelAddress[]) => { setPanelAddresses(newAddress); @@ -86,8 +96,57 @@ export const SettingsPage = () => { // 页面加载时获取数据 useEffect(() => { loadPanelAddresses(); + loadForwardCompactMode(); }, []); + const loadForwardCompactMode = async () => { + try { + const res = await getConfigByName(FORWARD_COMPACT_MODE_CONFIG_KEY); + const enabled = + res.code === 0 && + typeof res.data?.value === "string" && + res.data.value === "true"; + + setForwardCompactMode(enabled); + } catch { + setForwardCompactMode(false); + } + }; + + const handleForwardCompactModeChange = async (enabled: boolean) => { + if (!admin || forwardCompactModeSaving) { + return; + } + + const previous = forwardCompactMode; + + setForwardCompactMode(enabled); + setForwardCompactModeSaving(true); + try { + const response = await updateConfig( + FORWARD_COMPACT_MODE_CONFIG_KEY, + enabled ? "true" : "false", + ); + + if (response.code === 0) { + toast.success(`转发页面精简模式已${enabled ? "开启" : "关闭"}`); + window.dispatchEvent( + new CustomEvent("forwardCompactModeChanged", { + detail: { enabled }, + }), + ); + } else { + setForwardCompactMode(previous); + toast.error(response.msg || "保存精简模式失败"); + } + } catch { + setForwardCompactMode(previous); + toast.error("保存精简模式失败"); + } finally { + setForwardCompactModeSaving(false); + } + }; + const handleUpdateChannelChange = (channel: UpdateReleaseChannel) => { setUpdateChannel(channel); setUpdateReleaseChannel(channel); @@ -164,6 +223,37 @@ export const SettingsPage = () => { + + +

+ 显示设置 +

+
+
+
+

+ 转发页面精简模式 +

+

+ 开启后,转发页面列表使用 2.1.6-alpha8 样式。 +

+
+ +
+ {!admin && ( +

+ 仅管理员可修改该全局配置。 +

+ )} +
+
+
+ {/* 添加新地址 */}