From c20dd42d7a772f82a67badd97063616ea0c7f87d Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Fri, 8 May 2026 14:18:21 +0200 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20Phase=206-ii=20=E2=80=94=20xr?= =?UTF-8?q?ay=20Basics=20tab=20structured=20editor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 --- frontend/src/pages/xray/BasicsTab.vue | 471 ++++++++++++++++++++++ frontend/src/pages/xray/XrayPage.vue | 29 +- frontend/src/pages/xray/useXraySetting.js | 50 ++- 3 files changed, 547 insertions(+), 3 deletions(-) create mode 100644 frontend/src/pages/xray/BasicsTab.vue 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 @@ + + + + + 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() { - + - + 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,