diff --git a/frontend/src/pages/xray/BasicsTab.vue b/frontend/src/pages/xray/BasicsTab.vue
new file mode 100644
index 00000000..bfd43398
--- /dev/null
+++ b/frontend/src/pages/xray/BasicsTab.vue
@@ -0,0 +1,471 @@
+
+
+
+
+
+
+
+
+
+
+ Freedom outbound strategy
+ How the direct outbound resolves destinations.
+
+
+ {{ s }}
+
+
+
+
+
+ Routing strategy
+ Domain strategy applied at the routing level.
+
+
+ {{ s }}
+
+
+
+
+
+ Outbound test URL
+ HTTP endpoint used for outbound latency tests.
+
+
+
+
+
+
+
+
+ Inbound uplink stats
+
+
+
+ Inbound downlink stats
+
+
+
+ Outbound uplink stats
+
+
+
+ Outbound downlink stats
+
+
+
+
+
+
+
+
+
+
+ Log level
+
+
+ {{ s }}
+
+
+
+
+
+ Access log
+
+
+ Empty
+ {{ s }}
+
+
+
+
+
+ Error log
+
+
+ Empty
+ {{ s }}
+
+
+
+
+
+ Mask address
+ Truncate IPs in logs.
+
+
+ Empty
+ {{ s }}
+
+
+
+
+
+ DNS log
+
+
+
+
+
+
+
+
+
+
+ Block torrent
+
+
+
+
+
+
+
+
+ Block IPs
+
+
+ {{ p.label }}
+
+
+
+
+
+ Block domains
+
+
+ {{ p.label }}
+
+
+
+
+
+
+
+
+
+ Direct IPs
+
+
+ {{ p.label }}
+
+
+
+
+
+ Direct domains
+
+
+ {{ p.label }}
+
+
+
+
+
+
+
+
+
+ IPv4 forced
+
+
+ {{ p.label }}
+
+
+
+
+
+
+
+
+
+ WARP routing
+
+
+ {{ p.label }}
+
+
+
+ Configure WARP
+
+
+
+
+
+ NordVPN routing
+
+
+ {{ p.label }}
+
+
+
+ Configure NordVPN
+
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/xray/XrayPage.vue b/frontend/src/pages/xray/XrayPage.vue
index 140a3166..71ddb73e 100644
--- a/frontend/src/pages/xray/XrayPage.vue
+++ b/frontend/src/pages/xray/XrayPage.vue
@@ -13,7 +13,9 @@ import {
import { theme as themeState } from '@/composables/useTheme.js';
import { useMediaQuery } from '@/composables/useMediaQuery.js';
+import { message } from 'ant-design-vue';
import AppSidebar from '@/components/AppSidebar.vue';
+import BasicsTab from './BasicsTab.vue';
import { useXraySetting } from './useXraySetting.js';
// Phase 6-i: scaffold + advanced JSON tab. Other tabs (Basics, Routing,
@@ -32,11 +34,26 @@ const {
spinning,
saveDisabled,
xraySetting,
+ templateSettings,
outboundTestUrl,
restartResult,
saveAll,
restartXray,
} = useXraySetting();
+
+// `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"
+// button that today just toasts (the modals land in 6-v).
+const warpExist = computed(
+ () => !!templateSettings.value?.outbounds?.find((o) => o?.tag === 'warp'),
+);
+const nordExist = computed(
+ () => !!templateSettings.value?.outbounds?.find((o) => o?.tag?.startsWith?.('nord-')),
+);
+
+function showWarp() { message.info('WARP outbound modal — coming in 6-v'); }
+function showNord() { message.info('NordVPN outbound modal — coming in 6-v'); }
const { isMobile } = useMediaQuery();
const basePath = window.__X_UI_BASE_PATH__ || '';
@@ -103,12 +120,20 @@ function confirmRestart() {
-
+
Basic template
-
+ (outboundTestUrl = v)"
+ @show-warp="showWarp"
+ @show-nord="showNord"
+ />
diff --git a/frontend/src/pages/xray/useXraySetting.js b/frontend/src/pages/xray/useXraySetting.js
index be985397..65ecaf49 100644
--- a/frontend/src/pages/xray/useXraySetting.js
+++ b/frontend/src/pages/xray/useXraySetting.js
@@ -5,11 +5,21 @@
// printed for the textarea; tabs that want a parsed view can JSON.parse
// it themselves.
-import { onMounted, onUnmounted, ref } from 'vue';
+import { onMounted, onUnmounted, ref, watch } from 'vue';
import { HttpUtil, PromiseUtil } from '@/utils';
const DIRTY_POLL_MS = 1000;
+// Hoists the parsed `templateSettings` alongside the JSON string so
+// structured tabs (Basics/Routing/Outbounds/etc.) can mutate fields
+// directly while the Advanced (JSON) tab edits the same data as text.
+// We keep both in sync with two cooperating watches:
+// • mutating templateSettings re-stringifies into xraySetting;
+// • editing the JSON text re-parses into templateSettings (only on
+// valid JSON — invalid edits leave templateSettings untouched
+// so the structured tabs don't blow up while the user types).
+let syncing = false;
+
export function useXraySetting() {
const fetched = ref(false);
const spinning = ref(false);
@@ -18,6 +28,9 @@ export function useXraySetting() {
const xraySetting = ref('');
const oldXraySetting = ref('');
+ // Parsed mirror — null until first successful fetch / parse.
+ const templateSettings = ref(null);
+
const outboundTestUrl = ref('https://www.google.com/generate_204');
const oldOutboundTestUrl = ref('');
@@ -30,8 +43,11 @@ export function useXraySetting() {
if (!msg?.success) return;
const obj = JSON.parse(msg.obj);
const pretty = JSON.stringify(obj.xraySetting, null, 2);
+ syncing = true;
xraySetting.value = pretty;
oldXraySetting.value = pretty;
+ templateSettings.value = obj.xraySetting;
+ syncing = false;
inboundTags.value = obj.inboundTags || [];
clientReverseTags.value = obj.clientReverseTags || [];
outboundTestUrl.value = obj.outboundTestUrl || 'https://www.google.com/generate_204';
@@ -40,6 +56,37 @@ export function useXraySetting() {
saveDisabled.value = true;
}
+ // Structured tabs mutate templateSettings deeply. Re-stringify on
+ // change so the Advanced JSON view + the dirty-poll see the edits.
+ watch(
+ templateSettings,
+ (next) => {
+ if (syncing || !next) return;
+ syncing = true;
+ try {
+ xraySetting.value = JSON.stringify(next, null, 2);
+ } finally {
+ syncing = false;
+ }
+ },
+ { deep: true },
+ );
+
+ // Advanced JSON edits — only refresh templateSettings when the text
+ // parses, so structured tabs stay readable mid-edit.
+ watch(xraySetting, (next) => {
+ if (syncing) return;
+ try {
+ const parsed = JSON.parse(next);
+ syncing = true;
+ try {
+ templateSettings.value = parsed;
+ } finally {
+ syncing = false;
+ }
+ } catch (_e) { /* ignore — wait for user to finish */ }
+ });
+
async function saveAll() {
spinning.value = true;
try {
@@ -98,6 +145,7 @@ export function useXraySetting() {
spinning,
saveDisabled,
xraySetting,
+ templateSettings,
outboundTestUrl,
inboundTags,
clientReverseTags,