diff --git a/frontend/src/pages/xray/outbounds/OutboundsTab.tsx b/frontend/src/pages/xray/outbounds/OutboundsTab.tsx index 03adb5173..662b9187b 100644 --- a/frontend/src/pages/xray/outbounds/OutboundsTab.tsx +++ b/frontend/src/pages/xray/outbounds/OutboundsTab.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, @@ -50,6 +50,7 @@ import type { XraySettingsValue, SetTemplate, OutboundTestMode, OutboundTestStat import './OutboundsTab.css'; import type { OutboundRow } from './outbounds-tab-types'; +import { originalOutboundIndex } from './outbounds-tab-helpers'; import { useOutboundColumns } from './useOutboundColumns'; import OutboundCardList from './OutboundCardList'; import SubscriptionOutbounds from './SubscriptionOutbounds'; @@ -150,6 +151,8 @@ export default function OutboundsTab({ .filter((o) => !isBalancerLoopbackTag(o.tag || '')), [outbounds], ); + const rowsRef = useRef([]); + rowsRef.current = rows; const dialerProxyTags = useMemo(() => { const tags = new Set(); @@ -188,11 +191,12 @@ export default function OutboundsTab({ loadSubs(); } function openEdit(idx: number) { - setEditingOutbound((templateSettings?.outbounds || [])[idx] as Record); - setEditingIndex(idx); + const target = originalOutboundIndex(rowsRef.current, idx); + setEditingOutbound((templateSettings?.outbounds || [])[target] as Record); + setEditingIndex(target); setExistingTags( (templateSettings?.outbounds || []) - .filter((_, i) => i !== idx) + .filter((_, i) => i !== target) .map((o) => o?.tag) .filter((tg): tg is string => !!tg), ); @@ -217,8 +221,9 @@ export default function OutboundsTab({ } function confirmDelete(idx: number) { + const target = originalOutboundIndex(rowsRef.current, idx); const impact = templateSettings - ? planOutboundDeletion(templateSettings, idx) + ? planOutboundDeletion(templateSettings, target) : { rules: [], balancers: [], observatory: false, burst: false }; modal.confirm({ title: `${t('delete')} ${t('pages.xray.Outbounds')} #${idx + 1}?`, @@ -226,27 +231,33 @@ export default function OutboundsTab({ okText: t('delete'), okType: 'danger', cancelText: t('cancel'), - onOk: () => mutate((tt) => applyOutboundDeletion(tt, idx)), + onOk: () => mutate((tt) => applyOutboundDeletion(tt, target)), }); } function setFirst(idx: number) { + const target = originalOutboundIndex(rowsRef.current, idx); mutate((tt) => { if (!tt.outbounds) return; - const [moved] = tt.outbounds.splice(idx, 1); + const [moved] = tt.outbounds.splice(target, 1); tt.outbounds.unshift(moved); }); } function moveUp(idx: number) { if (idx <= 0) return; + const target = originalOutboundIndex(rowsRef.current, idx); + const prev = originalOutboundIndex(rowsRef.current, idx - 1); mutate((tt) => { if (!tt.outbounds) return; - [tt.outbounds[idx - 1], tt.outbounds[idx]] = [tt.outbounds[idx], tt.outbounds[idx - 1]]; + [tt.outbounds[prev], tt.outbounds[target]] = [tt.outbounds[target], tt.outbounds[prev]]; }); } function moveDown(idx: number) { + if (idx >= rowsRef.current.length - 1) return; + const target = originalOutboundIndex(rowsRef.current, idx); + const next = originalOutboundIndex(rowsRef.current, idx + 1); mutate((tt) => { - if (!tt.outbounds || idx >= tt.outbounds.length - 1) return; - [tt.outbounds[idx + 1], tt.outbounds[idx]] = [tt.outbounds[idx], tt.outbounds[idx + 1]]; + if (!tt.outbounds) return; + [tt.outbounds[next], tt.outbounds[target]] = [tt.outbounds[target], tt.outbounds[next]]; }); } diff --git a/frontend/src/pages/xray/outbounds/outbounds-tab-helpers.ts b/frontend/src/pages/xray/outbounds/outbounds-tab-helpers.ts index f915e464d..254aa2479 100644 --- a/frontend/src/pages/xray/outbounds/outbounds-tab-helpers.ts +++ b/frontend/src/pages/xray/outbounds/outbounds-tab-helpers.ts @@ -6,6 +6,19 @@ import type { OutboundTestMode, OutboundTestState, OutboundTrafficRow } from '@/ import type { OutboundRow } from './outbounds-tab-types'; +/** + * Translate a table row's positional index into that outbound's index in the + * full, unfiltered outbounds array. The table hides balancer-loopback outbounds + * but keeps each visible row's original index in `key`, so any handler that + * mutates the outbounds array (or probes an outbound by index) must map the + * positional index back through `key` or it operates on the wrong outbound once + * a hidden loopback precedes it. + */ +export function originalOutboundIndex(rows: OutboundRow[], positionalIndex: number): number { + const row = rows[positionalIndex]; + return row ? row.key : positionalIndex; +} + export function outboundAddresses(o: OutboundRow): string[] { const settings = o.settings as Record | undefined; switch (o.protocol) { diff --git a/frontend/src/pages/xray/outbounds/useOutboundColumns.tsx b/frontend/src/pages/xray/outbounds/useOutboundColumns.tsx index ab6f4e071..a4d874584 100644 --- a/frontend/src/pages/xray/outbounds/useOutboundColumns.tsx +++ b/frontend/src/pages/xray/outbounds/useOutboundColumns.tsx @@ -158,8 +158,8 @@ export function useOutboundColumns({ key: 'egress', align: 'left', width: 210, - render: (_v, _record, index) => { - const egress = testResult(outboundTestStates, index)?.egress; + render: (_v, record) => { + const egress = testResult(outboundTestStates, record.key)?.egress; const addresses = [ egress?.ipv4 ? { label: 'v4', value: egress.ipv4 } : null, egress?.ipv6 ? { label: 'v6', value: egress.ipv6 } : null, @@ -190,8 +190,8 @@ export function useOutboundColumns({ key: 'egressCountry', align: 'left', width: 160, - render: (_v, _record, index) => { - const egress = testResult(outboundTestStates, index)?.egress; + render: (_v, record) => { + const egress = testResult(outboundTestStates, record.key)?.egress; if (!egress?.country) { return ( @@ -229,9 +229,9 @@ export function useOutboundColumns({ key: 'testResult', align: 'left', width: 140, - render: (_v, _record, index) => { - const r = testResult(outboundTestStates, index); - if (!r) return isTesting(outboundTestStates, index) ? : ; + render: (_v, record) => { + const r = testResult(outboundTestStates, record.key); + if (!r) return isTesting(outboundTestStates, record.key) ? : ; return ; }, }, @@ -240,16 +240,16 @@ export function useOutboundColumns({ key: 'test', align: 'center', width: 80, - render: (_v, record, index) => ( + render: (_v, record) => (