mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing
Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
2e3b7e29a9
commit
c76b8b4a81
2 changed files with 74 additions and 16 deletions
|
|
@ -59,7 +59,7 @@ const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
|
||||||
const inbound = ref(null);
|
const inbound = ref(null);
|
||||||
const dbForm = ref(null);
|
const dbForm = ref(null);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const advancedJson = ref({ stream: '', sniffing: '' });
|
const advancedJson = ref({ stream: '', sniffing: '', settings: '' });
|
||||||
// Cached default cert/key paths from /panel/setting/defaultSettings —
|
// Cached default cert/key paths from /panel/setting/defaultSettings —
|
||||||
// powers the "Set default cert" button on the TLS form.
|
// powers the "Set default cert" button on the TLS form.
|
||||||
const defaultCert = ref('');
|
const defaultCert = ref('');
|
||||||
|
|
@ -211,8 +211,15 @@ function freshDbForm() {
|
||||||
|
|
||||||
function primeAdvancedJson() {
|
function primeAdvancedJson() {
|
||||||
if (!inbound.value) return;
|
if (!inbound.value) return;
|
||||||
advancedJson.value.stream = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2);
|
try {
|
||||||
advancedJson.value.sniffing = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2);
|
advancedJson.value.stream = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2);
|
||||||
|
} catch (_e) { /* keep prior text */ }
|
||||||
|
try {
|
||||||
|
advancedJson.value.sniffing = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2);
|
||||||
|
} catch (_e) { /* keep prior text */ }
|
||||||
|
try {
|
||||||
|
advancedJson.value.settings = JSON.stringify(JSON.parse(inbound.value.settings.toString()), null, 2);
|
||||||
|
} catch (_e) { /* keep prior text */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.open, (next) => {
|
watch(() => props.open, (next) => {
|
||||||
|
|
@ -431,6 +438,7 @@ async function submit() {
|
||||||
// transports — both go to wire as serialized JSON.
|
// transports — both go to wire as serialized JSON.
|
||||||
let streamSettings;
|
let streamSettings;
|
||||||
let sniffing;
|
let sniffing;
|
||||||
|
let settings;
|
||||||
try {
|
try {
|
||||||
streamSettings = canEnableStream.value
|
streamSettings = canEnableStream.value
|
||||||
? JSON.stringify(JSON.parse(advancedJson.value.stream))
|
? JSON.stringify(JSON.parse(advancedJson.value.stream))
|
||||||
|
|
@ -441,6 +449,9 @@ async function submit() {
|
||||||
try {
|
try {
|
||||||
sniffing = JSON.stringify(JSON.parse(advancedJson.value.sniffing || inbound.value.sniffing.toString()));
|
sniffing = JSON.stringify(JSON.parse(advancedJson.value.sniffing || inbound.value.sniffing.toString()));
|
||||||
} catch (e) { message.error(`Sniffing JSON invalid: ${e.message}`); return; }
|
} catch (e) { message.error(`Sniffing JSON invalid: ${e.message}`); return; }
|
||||||
|
try {
|
||||||
|
settings = JSON.stringify(JSON.parse(advancedJson.value.settings || 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
|
||||||
|
|
@ -459,7 +470,7 @@ async function submit() {
|
||||||
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: inbound.value.settings.toString(),
|
settings: settings,
|
||||||
streamSettings: streamSettings,
|
streamSettings: streamSettings,
|
||||||
sniffing: sniffing,
|
sniffing: sniffing,
|
||||||
};
|
};
|
||||||
|
|
@ -486,17 +497,35 @@ const okText = computed(() =>
|
||||||
props.mode === 'edit' ? t('pages.client.submitEdit') : t('create'),
|
props.mode === 'edit' ? t('pages.client.submitEdit') : t('create'),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Whenever the structured stream form mutates the model, refresh the
|
// Whenever the structured form mutates stream / sniffing / settings,
|
||||||
// Advanced JSON tab so it reflects the latest state. Use a deep watch
|
// refresh the matching slice of the Advanced JSON tab so the user
|
||||||
// on the parsed JSON of the stream.
|
// always sees the live state — flipping a switch in Sniffing or
|
||||||
|
// editing encryption in Protocol now reflects in Advanced.
|
||||||
watch(
|
watch(
|
||||||
() => inbound.value && JSON.stringify(inbound.value.stream?.toJson?.() || {}),
|
() => inbound.value && JSON.stringify(inbound.value.stream?.toJson?.() || {}),
|
||||||
(next) => {
|
() => {
|
||||||
if (next) {
|
if (!inbound.value?.stream) return;
|
||||||
try {
|
try {
|
||||||
advancedJson.value.stream = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2);
|
advancedJson.value.stream = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2);
|
||||||
} catch (_e) { /* leave as is */ }
|
} catch (_e) { /* leave as is */ }
|
||||||
}
|
},
|
||||||
|
);
|
||||||
|
watch(
|
||||||
|
() => inbound.value && JSON.stringify(inbound.value.sniffing?.toJson?.() || {}),
|
||||||
|
() => {
|
||||||
|
if (!inbound.value?.sniffing) return;
|
||||||
|
try {
|
||||||
|
advancedJson.value.sniffing = 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 {
|
||||||
|
advancedJson.value.settings = JSON.stringify(JSON.parse(inbound.value.settings.toString()), null, 2);
|
||||||
|
} catch (_e) { /* leave as is */ }
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -1631,6 +1660,10 @@ watch(
|
||||||
message="Edit raw stream JSON to access advanced fields we don't yet expose through the form."
|
message="Edit raw stream JSON to access advanced fields we don't yet expose through the form."
|
||||||
class="mb-12" />
|
class="mb-12" />
|
||||||
<a-form layout="vertical">
|
<a-form layout="vertical">
|
||||||
|
<a-form-item label="settings (clients, encryption, fallbacks, …)">
|
||||||
|
<a-textarea v-model:value="advancedJson.settings" :auto-size="{ minRows: 10, maxRows: 24 }"
|
||||||
|
spellcheck="false" class="json-editor" />
|
||||||
|
</a-form-item>
|
||||||
<a-form-item label="streamSettings">
|
<a-form-item label="streamSettings">
|
||||||
<a-textarea v-model:value="advancedJson.stream" :auto-size="{ minRows: 10, maxRows: 24 }" spellcheck="false"
|
<a-textarea v-model:value="advancedJson.stream" :auto-size="{ minRows: 10, maxRows: 24 }" spellcheck="false"
|
||||||
class="json-editor" />
|
class="json-editor" />
|
||||||
|
|
|
||||||
|
|
@ -29,17 +29,42 @@ const props = defineProps({
|
||||||
|
|
||||||
const canvas = ref(null);
|
const canvas = ref(null);
|
||||||
|
|
||||||
|
// Byte-mode capacities (level M) for QR versions 1..40 — used to pick
|
||||||
|
// the matrix width up front so we can size the canvas as a multiple
|
||||||
|
// of pixelSize. Without this, QRious renders at floor(size/matrix)
|
||||||
|
// and centers, leaving a white margin whenever size isn't divisible.
|
||||||
|
const QR_M_BYTE_CAPACITY = [
|
||||||
|
14, 26, 42, 62, 84, 106, 122, 152, 180, 213,
|
||||||
|
251, 287, 331, 362, 412, 450, 504, 560, 624, 666,
|
||||||
|
711, 779, 857, 911, 997, 1059, 1125, 1190, 1264, 1370,
|
||||||
|
1452, 1538, 1628, 1722, 1809, 1911, 1989, 2099, 2213, 2331,
|
||||||
|
];
|
||||||
|
|
||||||
|
function pickQrMatrixWidth(value) {
|
||||||
|
const byteLen = new TextEncoder().encode(value).length;
|
||||||
|
for (let i = 0; i < QR_M_BYTE_CAPACITY.length; i++) {
|
||||||
|
if (byteLen <= QR_M_BYTE_CAPACITY[i]) return 17 + 4 * (i + 1);
|
||||||
|
}
|
||||||
|
return 17 + 4 * 40; // version 40 (177 modules)
|
||||||
|
}
|
||||||
|
|
||||||
function paint() {
|
function paint() {
|
||||||
if (!props.showQr || !canvas.value || !props.value) return;
|
if (!props.showQr || !canvas.value || !props.value) return;
|
||||||
|
// Canvas size = matrixWidth × pixelSize, so the QR fills it edge-to-
|
||||||
|
// edge. pixelSize is floored against the requested size so the QR
|
||||||
|
// never grows past the host's expected box.
|
||||||
|
const matrixWidth = pickQrMatrixWidth(props.value);
|
||||||
|
const pixelSize = Math.max(1, Math.floor(props.size / matrixWidth));
|
||||||
|
const exactSize = matrixWidth * pixelSize;
|
||||||
// eslint-disable-next-line no-new
|
// eslint-disable-next-line no-new
|
||||||
new QRious({
|
new QRious({
|
||||||
element: canvas.value,
|
element: canvas.value,
|
||||||
size: props.size,
|
size: exactSize,
|
||||||
value: props.value,
|
value: props.value,
|
||||||
background: 'white',
|
background: 'white',
|
||||||
backgroundAlpha: 1,
|
backgroundAlpha: 1,
|
||||||
foreground: 'black',
|
foreground: 'black',
|
||||||
padding: 2,
|
padding: 0,
|
||||||
level: 'M',
|
level: 'M',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +140,7 @@ function download() {
|
||||||
|
|
||||||
.qr-panel-canvas canvas {
|
.qr-panel-canvas canvas {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: #fff;
|
display: block;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue