From 3f16b661ac1922c0b8826117c51809c670e6b7d3 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Fri, 8 May 2026 14:27:40 +0200 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20Phase=206-iv=20=E2=80=94=20xr?= =?UTF-8?q?ay=20Outbounds=20tab=20+=20outbound=20modal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 --- frontend/src/pages/xray/OutboundFormModal.vue | 198 +++++++ frontend/src/pages/xray/OutboundsTab.vue | 509 ++++++++++++++++++ frontend/src/pages/xray/XrayPage.vue | 23 +- frontend/src/pages/xray/useXraySetting.js | 49 ++ 4 files changed, 778 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/xray/OutboundFormModal.vue create mode 100644 frontend/src/pages/xray/OutboundsTab.vue diff --git a/frontend/src/pages/xray/OutboundFormModal.vue b/frontend/src/pages/xray/OutboundFormModal.vue new file mode 100644 index 00000000..96766763 --- /dev/null +++ b/frontend/src/pages/xray/OutboundFormModal.vue @@ -0,0 +1,198 @@ + + + + + diff --git a/frontend/src/pages/xray/OutboundsTab.vue b/frontend/src/pages/xray/OutboundsTab.vue new file mode 100644 index 00000000..b488db30 --- /dev/null +++ b/frontend/src/pages/xray/OutboundsTab.vue @@ -0,0 +1,509 @@ + + + + + diff --git a/frontend/src/pages/xray/XrayPage.vue b/frontend/src/pages/xray/XrayPage.vue index e9888e8e..605b18da 100644 --- a/frontend/src/pages/xray/XrayPage.vue +++ b/frontend/src/pages/xray/XrayPage.vue @@ -17,6 +17,7 @@ import { message } from 'ant-design-vue'; import AppSidebar from '@/components/AppSidebar.vue'; import BasicsTab from './BasicsTab.vue'; import RoutingTab from './RoutingTab.vue'; +import OutboundsTab from './OutboundsTab.vue'; import { useXraySetting } from './useXraySetting.js'; // Phase 6-i: scaffold + advanced JSON tab. Other tabs (Basics, Routing, @@ -40,10 +41,20 @@ const { inboundTags, clientReverseTags, restartResult, + outboundsTraffic, + outboundTestStates, + fetchOutboundsTraffic, + resetOutboundsTraffic, + testOutbound, saveAll, restartXray, } = useXraySetting(); +async function onTestOutbound(idx) { + const outbound = templateSettings.value?.outbounds?.[idx]; + if (outbound) await testOutbound(idx, outbound); +} + // `WarpExist` / `NordExist` derive from the parsed templateSettings — // the Basics tab gates its WARP / NordVPN domain selectors on whether // the matching outbound is provisioned, falling back to a "configure" @@ -155,7 +166,17 @@ function confirmRestart() { - + diff --git a/frontend/src/pages/xray/useXraySetting.js b/frontend/src/pages/xray/useXraySetting.js index 65ecaf49..4e7bc74e 100644 --- a/frontend/src/pages/xray/useXraySetting.js +++ b/frontend/src/pages/xray/useXraySetting.js @@ -38,6 +38,13 @@ export function useXraySetting() { const clientReverseTags = ref([]); const restartResult = ref(''); + // Outbounds tab data — traffic stats + per-row test state. Test + // states are keyed by outbound index (sparse object), each entry + // is `{ testing, result }` where result is the wire response from + // /panel/xray/testOutbound or null while the test is in flight. + const outboundsTraffic = ref([]); + const outboundTestStates = ref({}); + async function fetchAll() { const msg = await HttpUtil.post('/panel/xray/'); if (!msg?.success) return; @@ -100,6 +107,42 @@ export function useXraySetting() { } } + async function fetchOutboundsTraffic() { + const msg = await HttpUtil.get('/panel/xray/getOutboundsTraffic'); + if (msg?.success) outboundsTraffic.value = msg.obj || []; + } + + async function resetOutboundsTraffic(tag) { + const msg = await HttpUtil.post('/panel/xray/resetOutboundsTraffic', { tag }); + if (msg?.success) await fetchOutboundsTraffic(); + } + + async function testOutbound(index, outbound) { + if (!outbound) return null; + if (!outboundTestStates.value[index]) outboundTestStates.value[index] = {}; + outboundTestStates.value[index] = { testing: true, result: null }; + try { + const msg = await HttpUtil.post('/panel/xray/testOutbound', { + outbound: JSON.stringify(outbound), + allOutbounds: JSON.stringify(templateSettings.value?.outbounds || []), + }); + if (msg?.success) { + outboundTestStates.value[index] = { testing: false, result: msg.obj }; + return msg.obj; + } + outboundTestStates.value[index] = { + testing: false, + result: { success: false, error: msg?.msg || 'Unknown error' }, + }; + } catch (e) { + outboundTestStates.value[index] = { + testing: false, + result: { success: false, error: String(e) }, + }; + } + return null; + } + async function restartXray() { spinning.value = true; try { @@ -136,6 +179,7 @@ export function useXraySetting() { onMounted(() => { fetchAll(); + fetchOutboundsTraffic(); startDirtyPoll(); }); onUnmounted(stopDirtyPoll); @@ -150,7 +194,12 @@ export function useXraySetting() { inboundTags, clientReverseTags, restartResult, + outboundsTraffic, + outboundTestStates, fetchAll, + fetchOutboundsTraffic, + resetOutboundsTraffic, + testOutbound, saveAll, restartXray, };