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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ p }}
+
+
+
+
+
+
+
+ {{ s }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+ Add outbound
+
+
+
+ WARP
+
+
+
+ NordVPN
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ —
+
+
+
+
{{ index + 1 }}
+
+ {{ record.tag }}
+
+
{{ record.protocol }}
+
+ {{ record.streamSettings?.network }}
+
+ {{ record.streamSettings.security }}
+
+
+
+
+
+
+
+
+
+
+ Move to top
+
+
+ Edit
+
+
+ Reset traffic
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ index + 1 }}
+
+
+
+
+
+
+
+ Move to top
+
+
+ Edit
+
+
+ Move up
+
+
+ Move down
+
+
+ Reset traffic
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
+ {{ record.tag }}
+
+
+
{{ record.protocol }}
+
+ {{ record.streamSettings?.network }}
+
+ {{ record.streamSettings.security }}
+
+
+
+
+
+
+
+
+
+
+
+ ↑ {{ SizeFormatter.sizeFormat(trafficFor(record).up) }}
+
+ ↓ {{ SizeFormatter.sizeFormat(trafficFor(record).down) }}
+
+
+
+
+
+
+ {{ testResult(index).delay }} ms
+
+ failed
+
+
+
+ —
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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() {
Outbounds
-
+
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,
};