refactor(inbounds): tighten advanced JSON helpers and fix dark-mode subtitles

Collapsed repeated stream/sniffing/settings handling in InboundFormModal
into shared helpers (stampAdvancedTextFor, parseAdvancedSliceWithLabel,
compactAdvancedJson, withSaving) plus a wrapped-config factory for the
single-key editors. Cuts ~120 lines from the script section with no
behavior change.

The advanced-panel subtitle and editor-meta text used a fixed dark color
that was unreadable on the dark and ultra-dark modal backgrounds.
Switched both to opacity-on-inherit so they pick up AntD's theme-aware
foreground color, the same pattern .section-heading already uses.
This commit is contained in:
MHSanaei
2026-05-15 12:12:47 +02:00
parent 78f1719c6d
commit 5a1019534f

View File

@@ -226,20 +226,7 @@ function freshDbForm() {
function primeAdvancedJson() { function primeAdvancedJson() {
if (!inbound.value) return; if (!inbound.value) return;
// Only set stream text for protocols that support it ['stream', 'sniffing', 'settings'].forEach(stampAdvancedTextFor);
if (canEnableStream.value) {
try {
advancedStreamText.value = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2);
} catch (_e) { /* keep prior text */ }
} else {
advancedStreamText.value = '{}';
}
try {
advancedSniffingText.value = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2);
} catch (_e) { /* keep prior text */ }
try {
advancedSettingsText.value = JSON.stringify(JSON.parse(inbound.value.settings.toString()), null, 2);
} catch (_e) { /* keep prior text */ }
} }
watch(() => props.open, (next) => { watch(() => props.open, (next) => {
@@ -258,34 +245,22 @@ watch(() => props.open, (next) => {
function applyAdvancedJsonToBasic() { function applyAdvancedJsonToBasic() {
if (!inbound.value) return true; if (!inbound.value) return true;
let parsedSettings; let settings; let streamSettings; let sniffing;
let parsedStream;
let parsedSniffing;
try { try {
parsedSettings = advancedSettingsText.value.trim() settings = parseAdvancedSliceWithLabel(advancedSettingsText.value, settingsFallback(), 'Settings');
? JSON.parse(advancedSettingsText.value) streamSettings = parseAdvancedSliceWithLabel(advancedStreamText.value, streamFallback(), 'Stream');
: inbound.value.settings?.toJson?.(); sniffing = parseAdvancedSliceWithLabel(advancedSniffingText.value, sniffingFallback(), 'Sniffing');
} catch (e) { message.error(`Settings JSON invalid: ${e.message}`); return false; } } catch (_e) { return false; }
try {
parsedStream = advancedStreamText.value.trim()
? JSON.parse(advancedStreamText.value)
: inbound.value.stream?.toJson?.();
} catch (e) { message.error(`Stream JSON invalid: ${e.message}`); return false; }
try {
parsedSniffing = advancedSniffingText.value.trim()
? JSON.parse(advancedSniffingText.value)
: inbound.value.sniffing?.toJson?.();
} catch (e) { message.error(`Sniffing JSON invalid: ${e.message}`); return false; }
try { try {
inbound.value = Inbound.fromJson({ inbound.value = Inbound.fromJson({
port: inbound.value.port, port: inbound.value.port,
listen: inbound.value.listen, listen: inbound.value.listen,
protocol: inbound.value.protocol, protocol: inbound.value.protocol,
settings: parsedSettings, settings,
streamSettings: parsedStream, streamSettings,
tag: inbound.value.tag, tag: inbound.value.tag,
sniffing: parsedSniffing, sniffing,
clientStats: inbound.value.clientStats, clientStats: inbound.value.clientStats,
}); });
} catch (e) { } catch (e) {
@@ -350,37 +325,102 @@ function unwrapWrappedObject(parsed, key) {
return parsed; return parsed;
} }
const settingsFallback = () => inbound.value?.settings?.toJson?.() || {};
const sniffingFallback = () => inbound.value?.sniffing?.toJson?.() || {};
const streamFallback = () => inbound.value?.stream?.toJson?.() || {};
const advancedTextRefs = {
stream: advancedStreamText,
sniffing: advancedSniffingText,
settings: advancedSettingsText,
};
function stampAdvancedTextFor(slice) {
const textRef = advancedTextRefs[slice];
if (!textRef) return;
if (slice === 'stream' && !canEnableStream.value) {
textRef.value = '{}';
return;
}
const obj = inbound.value?.[slice];
if (!obj) return;
try {
textRef.value = JSON.stringify(JSON.parse(obj.toString()), null, 2);
} catch (_e) { /* keep prior text */ }
}
function parseAdvancedSliceWithLabel(rawText, fallback, label) {
try {
return parseAdvancedSliceOrFallback(rawText, fallback);
} catch (e) {
message.error(`${label} JSON invalid: ${e.message}`);
throw e;
}
}
function compactAdvancedJson(raw, fallback, label) {
try {
return JSON.stringify(JSON.parse(raw || fallback));
} catch (e) {
message.error(`${label} JSON invalid: ${e.message}`);
throw e;
}
}
async function withSaving(fn) {
saving.value = true;
try { return await fn(); } finally { saving.value = false; }
}
function makeWrappedAdvancedConfig({ key, textRef, getFallback, label }) {
const invalid = `${label} JSON invalid`;
return computed({
get: () => {
if (!inbound.value) return '';
try {
const value = parseAdvancedSliceOrFallback(textRef.value, getFallback());
return JSON.stringify({ [key]: value }, null, 2);
} catch (_e) {
return '';
}
},
set: (next) => {
let parsed;
try {
parsed = JSON.parse(next);
} catch (e) {
message.error(`${invalid}: ${e.message}`);
return;
}
const unwrapped = unwrapWrappedObject(parsed, key);
if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) {
message.error(`${label} JSON must be an object or { ${key}: { ... } }.`);
return;
}
try {
textRef.value = JSON.stringify(unwrapped, null, 2);
} catch (e) {
message.error(`${invalid}: ${e.message}`);
}
},
});
}
const advancedAllConfig = computed({ const advancedAllConfig = computed({
get: () => { get: () => {
if (!inbound.value) return ''; if (!inbound.value) return '';
try { try {
const settings = parseAdvancedSliceOrFallback(
advancedSettingsText.value,
inbound.value.settings?.toJson?.() || {},
);
const streamSettings = parseAdvancedSliceOrFallback(
advancedStreamText.value,
inbound.value.stream?.toJson?.() || {},
);
const sniffing = parseAdvancedSliceOrFallback(
advancedSniffingText.value,
inbound.value.sniffing?.toJson?.() || {},
);
const result = { const result = {
listen: inbound.value.listen, listen: inbound.value.listen,
port: inbound.value.port, port: inbound.value.port,
protocol: inbound.value.protocol, protocol: inbound.value.protocol,
settings, settings: parseAdvancedSliceOrFallback(advancedSettingsText.value, settingsFallback()),
sniffing, sniffing: parseAdvancedSliceOrFallback(advancedSniffingText.value, sniffingFallback()),
tag: inbound.value.tag, tag: inbound.value.tag,
}; };
// Only include streamSettings for protocols that support it
if (canEnableStream.value) { if (canEnableStream.value) {
result.streamSettings = streamSettings; result.streamSettings = parseAdvancedSliceOrFallback(advancedStreamText.value, streamFallback());
} }
return JSON.stringify(result, null, 2); return JSON.stringify(result, null, 2);
} catch (_e) { } catch (_e) {
return ''; return '';
@@ -400,147 +440,47 @@ const advancedAllConfig = computed({
} }
try { try {
if (typeof parsed.listen === 'string') { if (typeof parsed.listen === 'string') inbound.value.listen = parsed.listen;
inbound.value.listen = parsed.listen;
}
if (parsed.port !== undefined) { if (parsed.port !== undefined) {
const parsedPort = Number(parsed.port); const port = Number(parsed.port);
if (!Number.isNaN(parsedPort) && Number.isFinite(parsedPort)) { if (Number.isFinite(port)) inbound.value.port = port;
inbound.value.port = parsedPort;
}
} }
if (typeof parsed.protocol === 'string' && PROTOCOLS.includes(parsed.protocol)) { if (typeof parsed.protocol === 'string' && PROTOCOLS.includes(parsed.protocol)) {
inbound.value.protocol = parsed.protocol; inbound.value.protocol = parsed.protocol;
} }
if (typeof parsed.tag === 'string') { if (typeof parsed.tag === 'string') inbound.value.tag = parsed.tag;
inbound.value.tag = parsed.tag;
}
const existingSettings = parseAdvancedSliceOrFallback( const existingSettings = parseAdvancedSliceOrFallback(advancedSettingsText.value, settingsFallback());
advancedSettingsText.value, advancedSettingsText.value = JSON.stringify(parsed.settings ?? existingSettings, null, 2);
inbound.value?.settings?.toJson?.() || {}, advancedSniffingText.value = JSON.stringify(parsed.sniffing ?? sniffingFallback(), null, 2);
); advancedStreamText.value = canEnableStream.value
const settings = parsed.settings ?? existingSettings; ? JSON.stringify(parsed.streamSettings ?? streamFallback(), null, 2)
const sniffing = parsed.sniffing ?? (inbound.value?.sniffing?.toJson?.() || {}); : '{}';
advancedSettingsText.value = JSON.stringify(settings, null, 2);
advancedSniffingText.value = JSON.stringify(sniffing, null, 2);
// Only update stream settings if protocol supports it
if (canEnableStream.value) {
const streamSettings = parsed.streamSettings ?? (inbound.value?.stream?.toJson?.() || {});
advancedStreamText.value = JSON.stringify(streamSettings, null, 2);
} else {
advancedStreamText.value = '{}';
}
} catch (e) { } catch (e) {
message.error(`All JSON invalid: ${e.message}`); message.error(`All JSON invalid: ${e.message}`);
} }
}, },
}); });
const advancedSettingsConfig = computed({ const advancedSettingsConfig = makeWrappedAdvancedConfig({
get: () => { key: 'settings',
if (!inbound.value) return ''; textRef: advancedSettingsText,
try { getFallback: settingsFallback,
const settings = parseAdvancedSliceOrFallback( label: 'Settings',
advancedSettingsText.value,
inbound.value.settings?.toJson?.() || {},
);
return JSON.stringify({
settings,
}, null, 2);
} catch (_e) {
return '';
}
},
set: (next) => {
let parsed;
try {
parsed = JSON.parse(next);
} catch (e) {
message.error(`Settings JSON invalid: ${e.message}`);
return;
}
const unwrapped = unwrapWrappedObject(parsed, 'settings');
if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) {
message.error('Settings JSON must be an object or { settings: { ... } }.');
return;
}
try {
advancedSettingsText.value = JSON.stringify(unwrapped, null, 2);
} catch (e) {
message.error(`Settings JSON invalid: ${e.message}`);
}
},
}); });
const advancedSniffingConfig = computed({ const advancedSniffingConfig = makeWrappedAdvancedConfig({
get: () => { key: 'sniffing',
if (!inbound.value) return ''; textRef: advancedSniffingText,
try { getFallback: sniffingFallback,
const sniffing = parseAdvancedSliceOrFallback( label: 'Sniffing',
advancedSniffingText.value,
inbound.value.sniffing?.toJson?.() || {},
);
return JSON.stringify({ sniffing }, null, 2);
} catch (_e) {
return '';
}
},
set: (next) => {
let parsed;
try {
parsed = JSON.parse(next);
} catch (e) {
message.error(`Sniffing JSON invalid: ${e.message}`);
return;
}
const unwrapped = unwrapWrappedObject(parsed, 'sniffing');
if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) {
message.error('Sniffing JSON must be an object or { sniffing: { ... } }.');
return;
}
try {
advancedSniffingText.value = JSON.stringify(unwrapped, null, 2);
} catch (e) {
message.error(`Sniffing JSON invalid: ${e.message}`);
}
},
}); });
const advancedStreamConfig = computed({ const advancedStreamConfig = makeWrappedAdvancedConfig({
get: () => { key: 'streamSettings',
if (!inbound.value) return ''; textRef: advancedStreamText,
try { getFallback: streamFallback,
const streamSettings = parseAdvancedSliceOrFallback( label: 'Stream',
advancedStreamText.value,
inbound.value.stream?.toJson?.() || {},
);
return JSON.stringify({ streamSettings }, null, 2);
} catch (_e) {
return '';
}
},
set: (next) => {
let parsed;
try {
parsed = JSON.parse(next);
} catch (e) {
message.error(`Stream JSON invalid: ${e.message}`);
return;
}
const unwrapped = unwrapWrappedObject(parsed, 'streamSettings');
if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) {
message.error('Stream JSON must be an object or { streamSettings: { ... } }.');
return;
}
try {
advancedStreamText.value = JSON.stringify(unwrapped, null, 2);
} catch (e) {
message.error(`Stream JSON invalid: ${e.message}`);
}
},
}); });
// === Random helpers wired to the form's sync icons ================== // === Random helpers wired to the form's sync icons ==================
@@ -575,16 +515,13 @@ function regenInboundWg() {
// === Reality keygen via existing API ================================= // === Reality keygen via existing API =================================
async function genRealityKeypair() { async function genRealityKeypair() {
saving.value = true; await withSaving(async () => {
try {
const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert'); const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
if (msg?.success) { if (msg?.success) {
inbound.value.stream.reality.privateKey = msg.obj.privateKey; inbound.value.stream.reality.privateKey = msg.obj.privateKey;
inbound.value.stream.reality.settings.publicKey = msg.obj.publicKey; inbound.value.stream.reality.settings.publicKey = msg.obj.publicKey;
} }
} finally { });
saving.value = false;
}
} }
function clearRealityKeypair() { function clearRealityKeypair() {
@@ -594,16 +531,13 @@ function clearRealityKeypair() {
} }
async function genMldsa65() { async function genMldsa65() {
saving.value = true; await withSaving(async () => {
try {
const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65'); const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65');
if (msg?.success) { if (msg?.success) {
inbound.value.stream.reality.mldsa65Seed = msg.obj.seed; inbound.value.stream.reality.mldsa65Seed = msg.obj.seed;
inbound.value.stream.reality.settings.mldsa65Verify = msg.obj.verify; inbound.value.stream.reality.settings.mldsa65Verify = msg.obj.verify;
} }
} finally { });
saving.value = false;
}
} }
function clearMldsa65() { function clearMldsa65() {
@@ -627,8 +561,7 @@ function randomizeShortIds() {
// === ECH cert helpers ================================================ // === ECH cert helpers ================================================
async function getNewEchCert() { async function getNewEchCert() {
if (!inbound.value?.stream?.tls) return; if (!inbound.value?.stream?.tls) return;
saving.value = true; await withSaving(async () => {
try {
const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', {
sni: inbound.value.stream.tls.sni, sni: inbound.value.stream.tls.sni,
}); });
@@ -636,9 +569,7 @@ async function getNewEchCert() {
inbound.value.stream.tls.echServerKeys = msg.obj.echServerKeys; inbound.value.stream.tls.echServerKeys = msg.obj.echServerKeys;
inbound.value.stream.tls.settings.echConfigList = msg.obj.echConfigList; inbound.value.stream.tls.settings.echConfigList = msg.obj.echConfigList;
} }
} finally { });
saving.value = false;
}
} }
function clearEchCert() { function clearEchCert() {
@@ -682,17 +613,14 @@ function matchesVlessAuth(block, authId) {
async function getNewVlessEnc(authId) { async function getNewVlessEnc(authId) {
if (!authId || !inbound.value?.settings) return; if (!authId || !inbound.value?.settings) return;
saving.value = true; await withSaving(async () => {
try {
const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc'); const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');
if (!msg?.success) return; if (!msg?.success) return;
const block = (msg.obj?.auths || []).find((a) => matchesVlessAuth(a, authId)); const block = (msg.obj?.auths || []).find((a) => matchesVlessAuth(a, authId));
if (!block) return; if (!block) return;
inbound.value.settings.decryption = block.decryption; inbound.value.settings.decryption = block.decryption;
inbound.value.settings.encryption = block.encryption; inbound.value.settings.encryption = block.encryption;
} finally { });
saving.value = false;
}
} }
function clearVlessEnc() { function clearVlessEnc() {
@@ -737,24 +665,16 @@ async function submit() {
if (!inbound.value || !dbForm.value) return; if (!inbound.value || !dbForm.value) return;
saving.value = true; saving.value = true;
try { try {
// Sniffing tab is structured; stream stays JSON for unsupported let streamSettings; let sniffing; let settings;
// transports — both go to wire as serialized JSON.
let streamSettings;
let sniffing;
let settings;
try { try {
streamSettings = canEnableStream.value streamSettings = canEnableStream.value
? JSON.stringify(JSON.parse(advancedStreamText.value)) ? compactAdvancedJson(advancedStreamText.value, '', 'Stream')
: (inbound.value.stream?.sockopt : (inbound.value.stream?.sockopt
? JSON.stringify({ sockopt: inbound.value.stream.sockopt.toJson() }) ? JSON.stringify({ sockopt: inbound.value.stream.sockopt.toJson() })
: ''); : '');
} catch (e) { message.error(`Stream JSON invalid: ${e.message}`); return; } sniffing = compactAdvancedJson(advancedSniffingText.value, inbound.value.sniffing.toString(), 'Sniffing');
try { settings = compactAdvancedJson(advancedSettingsText.value, inbound.value.settings.toString(), 'Settings');
sniffing = JSON.stringify(JSON.parse(advancedSniffingText.value || inbound.value.sniffing.toString())); } catch (_e) { return; }
} catch (e) { message.error(`Sniffing JSON invalid: ${e.message}`); return; }
try {
settings = JSON.stringify(JSON.parse(advancedSettingsText.value || inbound.value.settings.toString()));
} catch (e) { message.error(`Settings JSON invalid: ${e.message}`); return; }
// The structured form mutates `inbound.stream` directly when the // The structured form mutates `inbound.stream` directly when the
// user edits TCP/WS/gRPC/HTTPUpgrade fields, but if they touched // user edits TCP/WS/gRPC/HTTPUpgrade fields, but if they touched
@@ -810,51 +730,15 @@ const okText = computed(() =>
// Whenever the structured form mutates stream / sniffing / settings, // Whenever the structured form mutates stream / sniffing / settings,
// refresh the matching slice of the Advanced JSON tab so the user // refresh the matching slice of the Advanced JSON tab so the user
// always sees the live state — flipping a switch in Sniffing or // always sees the live state.
// editing encryption in Protocol now reflects in Advanced. ['stream', 'sniffing', 'settings'].forEach((slice) => {
watch( watch(
() => inbound.value && JSON.stringify(inbound.value.stream?.toJson?.() || {}), () => inbound.value && JSON.stringify(inbound.value[slice]?.toJson?.() || {}),
() => { () => stampAdvancedTextFor(slice),
if (!inbound.value?.stream) return; );
// Only update stream text for protocols that support it });
if (!canEnableStream.value) {
advancedStreamText.value = '{}';
return;
}
try {
advancedStreamText.value = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2);
} catch (_e) { /* leave as is */ }
},
);
watch(
() => inbound.value && JSON.stringify(inbound.value.sniffing?.toJson?.() || {}),
() => {
if (!inbound.value?.sniffing) return;
try {
advancedSniffingText.value = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2);
} catch (_e) { /* leave as is */ }
},
);
watch(
() => inbound.value && JSON.stringify(inbound.value.settings?.toJson?.() || {}),
() => {
if (!inbound.value?.settings) return;
try {
advancedSettingsText.value = JSON.stringify(JSON.parse(inbound.value.settings.toString()), null, 2);
} catch (_e) { /* leave as is */ }
},
);
// Watch protocol changes to clear stream settings for protocols that don't support it watch(() => inbound.value?.protocol, () => stampAdvancedTextFor('stream'));
watch(
() => inbound.value?.protocol,
() => {
if (!inbound.value) return;
if (!canEnableStream.value) {
advancedStreamText.value = '{}';
}
},
);
</script> </script>
<template> <template>
@@ -2321,7 +2205,7 @@ watch(
.advanced-panel__subtitle { .advanced-panel__subtitle {
margin-top: 4px; margin-top: 4px;
color: rgba(0, 0, 0, 0.6); opacity: 0.7;
line-height: 1.5; line-height: 1.5;
} }
@@ -2335,7 +2219,7 @@ watch(
.advanced-editor-meta { .advanced-editor-meta {
margin-bottom: 10px; margin-bottom: 10px;
color: rgba(0, 0, 0, 0.65); opacity: 0.75;
line-height: 1.5; line-height: 1.5;
} }
@@ -2350,15 +2234,8 @@ watch(
} }
} }
:global(.dark) .advanced-panel__subtitle, :global(body.dark) .advanced-panel,
:global(.dark) .advanced-editor-meta, :global(html[data-theme='ultra-dark']) .advanced-panel {
:global(.ultra) .advanced-panel__subtitle,
:global(.ultra) .advanced-editor-meta {
color: rgba(255, 255, 255, 0.65);
}
:global(.dark) .advanced-panel,
:global(.ultra) .advanced-panel {
border-color: rgba(255, 255, 255, 0.12); border-color: rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.03); background: rgba(255, 255, 255, 0.03);
} }