fix(frontend): xray Advanced tab parity + finalmask gating

Advanced tab was a single textarea bound to the full xraySetting blob.
Restore the legacy 4-way view: a radio group toggles between All /
Inbounds / Outbounds / Routing Rules, and the textarea reads/writes
the matching slice through templateSettings. Added the legacy header
("Advanced Xray Configuration Template" + description) so the page
introduces itself like main.

Outbound finalmask leaked into protocols that don't have a stream
(Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the
v-if only checked outbound.stream. Gate the whole FinalMaskForm on
outbound.canEnableStream() to match main.

Drop the leading divider inside FinalMaskForm — its parent already
provides separation, so the rule above "TCP Masks" was redundant.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MHSanaei 2026-05-08 21:53:20 +02:00
parent 18434bdbbd
commit 2e3b7e29a9
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
3 changed files with 83 additions and 13 deletions

View file

@ -72,8 +72,6 @@ function newNoiseItem() {
:label-col="{ md: { span: 8 } }"
:wrapper-col="{ md: { span: 14 } }"
>
<a-divider :style="{ margin: '5px 0 0' }" />
<!-- ============================== TCP MASKS ============================== -->
<template v-if="showTcp">
<a-form-item label="TCP Masks">

View file

@ -930,7 +930,14 @@ function regenerateWgKeys() {
</a-form>
<!-- ============== FinalMask (TCP/UDP masks + QUIC params) ============== -->
<FinalMaskForm v-if="outbound.stream" :stream="outbound.stream" :protocol="proto" />
<!-- Gated by canEnableStream() so TCP masks don't leak into
Freedom / Blackhole / DNS / Socks / HTTP / Wireguard outbounds
(they don't have a stream config at all). Matches legacy. -->
<FinalMaskForm
v-if="outbound.stream && outbound.canEnableStream()"
:stream="outbound.stream"
:protocol="proto"
/>
</a-tab-pane>
<!-- ============================== JSON ============================== -->

View file

@ -53,6 +53,60 @@ async function onTestOutbound(idx) {
if (outbound) await testOutbound(idx, outbound);
}
// === Advanced tab radio-driven view ==============================
// Mirrors the legacy advanced page: a 4-way radio toggles which slice
// of the xray config the textarea edits the full config, just the
// inbounds, just the outbounds, or just the routing rules. Each slice
// reads/writes through templateSettings so edits propagate to the
// dirty-poll and structured tabs.
const advSettings = ref('xraySetting');
const advancedText = computed({
get: () => {
if (advSettings.value === 'xraySetting') return xraySetting.value;
const t = templateSettings.value;
if (!t) return '';
try {
switch (advSettings.value) {
case 'inboundSettings':
return JSON.stringify(t.inbounds || [], null, 2);
case 'outboundSettings':
return JSON.stringify(t.outbounds || [], null, 2);
case 'routingRuleSettings':
return JSON.stringify(t.routing?.rules || [], null, 2);
default:
return '';
}
} catch (_e) {
return '';
}
},
set: (next) => {
if (advSettings.value === 'xraySetting') {
xraySetting.value = next;
return;
}
// Slice edits: parse-then-merge into templateSettings so the
// structured tabs and the dirty-poll re-stringify it cleanly.
let parsed;
try { parsed = JSON.parse(next); } catch (_e) { return; }
const t = templateSettings.value;
if (!t) return;
switch (advSettings.value) {
case 'inboundSettings':
t.inbounds = parsed;
break;
case 'outboundSettings':
t.outbounds = parsed;
break;
case 'routingRuleSettings':
if (!t.routing) t.routing = {};
t.routing.rules = parsed;
break;
}
},
});
// `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"
@ -263,16 +317,27 @@ function confirmRestart() {
<template #tab>
<CodeOutlined /> <span>{{ t('pages.xray.advancedTemplate') }}</span>
</template>
<a-form layout="vertical">
<a-form-item label="xraySetting (full JSON)">
<a-textarea
v-model:value="xraySetting"
:auto-size="{ minRows: 18, maxRows: 40 }"
spellcheck="false"
class="json-editor"
/>
</a-form-item>
</a-form>
<a-list-item-meta
:title="t('pages.xray.Template')"
:description="t('pages.xray.TemplateDesc')"
/>
<a-radio-group
v-model:value="advSettings"
button-style="solid"
:size="isMobile ? 'small' : 'middle'"
:style="{ margin: '12px 0' }"
>
<a-radio-button value="xraySetting">{{ t('pages.xray.completeTemplate') }}</a-radio-button>
<a-radio-button value="inboundSettings">{{ t('pages.xray.Inbounds') }}</a-radio-button>
<a-radio-button value="outboundSettings">{{ t('pages.xray.Outbounds') }}</a-radio-button>
<a-radio-button value="routingRuleSettings">{{ t('pages.xray.Routings') }}</a-radio-button>
</a-radio-group>
<a-textarea
v-model:value="advancedText"
:auto-size="{ minRows: 18, maxRows: 40 }"
spellcheck="false"
class="json-editor"
/>
</a-tab-pane>
</a-tabs>
</a-col>