3x-ui/frontend/src/pages/xray/useXraySetting.js
MHSanaei c20dd42d7a
feat(frontend): Phase 6-ii — xray Basics tab structured editor
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 <noreply@anthropic.com>
2026-05-08 14:18:21 +02:00

157 lines
4.8 KiB
JavaScript

// 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, 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);
const saveDisabled = ref(true);
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('');
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);
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';
oldOutboundTestUrl.value = outboundTestUrl.value;
fetched.value = true;
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 {
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,
templateSettings,
outboundTestUrl,
inboundTags,
clientReverseTags,
restartResult,
fetchAll,
saveAll,
restartXray,
};
}