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 + + +
+
+ + +