From 59a4a713cd5d30876a67a668081ffc5c077286ae Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Fri, 8 May 2026 14:13:26 +0200 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20Phase=206-i=20=E2=80=94=20xra?= =?UTF-8?q?y=20page=20scaffold=20+=20Advanced=20JSON=20tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 --- frontend/src/pages/xray/XrayPage.vue | 233 ++++++++++++++++++++++ frontend/src/pages/xray/useXraySetting.js | 109 ++++++++++ frontend/src/xray.js | 16 ++ frontend/vite.config.js | 3 + frontend/xray.html | 13 ++ 5 files changed, 374 insertions(+) create mode 100644 frontend/src/pages/xray/XrayPage.vue create mode 100644 frontend/src/pages/xray/useXraySetting.js create mode 100644 frontend/src/xray.js create mode 100644 frontend/xray.html diff --git a/frontend/src/pages/xray/XrayPage.vue b/frontend/src/pages/xray/XrayPage.vue new file mode 100644 index 00000000..140a3166 --- /dev/null +++ b/frontend/src/pages/xray/XrayPage.vue @@ -0,0 +1,233 @@ + + + + + diff --git a/frontend/src/pages/xray/useXraySetting.js b/frontend/src/pages/xray/useXraySetting.js new file mode 100644 index 00000000..be985397 --- /dev/null +++ b/frontend/src/pages/xray/useXraySetting.js @@ -0,0 +1,109 @@ +// Drives the xray page's fetch / dirty / save lifecycle. The Go side +// returns the live xraySetting (the full JSON config), the inboundTags +// list, and a few sidecar values (clientReverseTags, outboundTestUrl) +// the structured tabs need. We keep the JSON as a string here — pretty- +// printed for the textarea; tabs that want a parsed view can JSON.parse +// it themselves. + +import { onMounted, onUnmounted, ref } from 'vue'; +import { HttpUtil, PromiseUtil } from '@/utils'; + +const DIRTY_POLL_MS = 1000; + +export function useXraySetting() { + const fetched = ref(false); + const spinning = ref(false); + const saveDisabled = ref(true); + + const xraySetting = ref(''); + const oldXraySetting = ref(''); + + const outboundTestUrl = ref('https://www.google.com/generate_204'); + const oldOutboundTestUrl = ref(''); + + const inboundTags = ref([]); + const clientReverseTags = ref([]); + const restartResult = ref(''); + + async function fetchAll() { + const msg = await HttpUtil.post('/panel/xray/'); + if (!msg?.success) return; + const obj = JSON.parse(msg.obj); + const pretty = JSON.stringify(obj.xraySetting, null, 2); + xraySetting.value = pretty; + oldXraySetting.value = pretty; + inboundTags.value = obj.inboundTags || []; + clientReverseTags.value = obj.clientReverseTags || []; + outboundTestUrl.value = obj.outboundTestUrl || 'https://www.google.com/generate_204'; + oldOutboundTestUrl.value = outboundTestUrl.value; + fetched.value = true; + saveDisabled.value = true; + } + + async function saveAll() { + spinning.value = true; + try { + const msg = await HttpUtil.post('/panel/xray/update', { + xraySetting: xraySetting.value, + outboundTestUrl: outboundTestUrl.value || 'https://www.google.com/generate_204', + }); + if (msg?.success) await fetchAll(); + } finally { + spinning.value = false; + } + } + + async function restartXray() { + spinning.value = true; + try { + const msg = await HttpUtil.post('/panel/api/server/restartXrayService'); + if (msg?.success) { + // Match legacy: short pause, then poll for the result blob so + // the popover surfaces any startup error from the new process. + await PromiseUtil.sleep(500); + const r = await HttpUtil.get('/panel/xray/getXrayResult'); + if (r?.success) restartResult.value = r.obj || ''; + } + } finally { + spinning.value = false; + } + } + + // Same 1s busy-loop pattern the settings page uses — keep it cheap + // and consistent. Real work (the JSON diff) is just a string compare. + let timer = null; + function startDirtyPoll() { + if (timer != null) return; + timer = setInterval(() => { + saveDisabled.value = + oldXraySetting.value === xraySetting.value + && oldOutboundTestUrl.value === outboundTestUrl.value; + }, DIRTY_POLL_MS); + } + function stopDirtyPoll() { + if (timer != null) { + clearInterval(timer); + timer = null; + } + } + + onMounted(() => { + fetchAll(); + startDirtyPoll(); + }); + onUnmounted(stopDirtyPoll); + + return { + fetched, + spinning, + saveDisabled, + xraySetting, + outboundTestUrl, + inboundTags, + clientReverseTags, + restartResult, + fetchAll, + saveAll, + restartXray, + }; +} diff --git a/frontend/src/xray.js b/frontend/src/xray.js new file mode 100644 index 00000000..679dbd3b --- /dev/null +++ b/frontend/src/xray.js @@ -0,0 +1,16 @@ +import { createApp } from 'vue'; +import Antd, { message } from 'ant-design-vue'; +import 'ant-design-vue/dist/reset.css'; + +import { setupAxios } from '@/api/axios-init.js'; +import '@/composables/useTheme.js'; +import XrayPage from '@/pages/xray/XrayPage.vue'; + +setupAxios(); + +const messageContainer = document.getElementById('message'); +if (messageContainer) { + message.config({ getContainer: () => messageContainer }); +} + +createApp(XrayPage).use(Antd).mount('#app'); diff --git a/frontend/vite.config.js b/frontend/vite.config.js index a4baa643..d5ff27ad 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -19,6 +19,8 @@ const MIGRATED_ROUTES = { '/panel/settings/': '/settings.html', '/panel/inbounds': '/inbounds.html', '/panel/inbounds/': '/inbounds.html', + '/panel/xray': '/xray.html', + '/panel/xray/': '/xray.html', }; // Build a proxy config that suppresses ECONNREFUSED noise when the Go @@ -72,6 +74,7 @@ export default defineConfig({ login: path.resolve(__dirname, 'login.html'), settings: path.resolve(__dirname, 'settings.html'), inbounds: path.resolve(__dirname, 'inbounds.html'), + xray: path.resolve(__dirname, 'xray.html'), }, }, }, diff --git a/frontend/xray.html b/frontend/xray.html new file mode 100644 index 00000000..80915eb7 --- /dev/null +++ b/frontend/xray.html @@ -0,0 +1,13 @@ + + + + + + 3x-ui · Xray + + +
+
+ + +