mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 13:14:11 +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 dbForm = ref(null);
|
||||
const saving = ref(false);
|
||||
const advancedJson = ref({ stream: '', sniffing: '' });
|
||||
const advancedJson = ref({ stream: '', sniffing: '', settings: '' });
|
||||
// Cached default cert/key paths from /panel/setting/defaultSettings —
|
||||
// powers the "Set default cert" button on the TLS form.
|
||||
const defaultCert = ref('');
|
||||
|
|
@ -211,8 +211,15 @@ function freshDbForm() {
|
|||
|
||||
function primeAdvancedJson() {
|
||||
if (!inbound.value) return;
|
||||
advancedJson.value.stream = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2);
|
||||
advancedJson.value.sniffing = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2);
|
||||
try {
|
||||
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) => {
|
||||
|
|
@ -431,6 +438,7 @@ async function submit() {
|
|||
// transports — both go to wire as serialized JSON.
|
||||
let streamSettings;
|
||||
let sniffing;
|
||||
let settings;
|
||||
try {
|
||||
streamSettings = canEnableStream.value
|
||||
? JSON.stringify(JSON.parse(advancedJson.value.stream))
|
||||
|
|
@ -441,6 +449,9 @@ async function submit() {
|
|||
try {
|
||||
sniffing = JSON.stringify(JSON.parse(advancedJson.value.sniffing || inbound.value.sniffing.toString()));
|
||||
} 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
|
||||
// user edits TCP/WS/gRPC/HTTPUpgrade fields, but if they touched
|
||||
|
|
@ -459,7 +470,7 @@ async function submit() {
|
|||
listen: inbound.value.listen,
|
||||
port: inbound.value.port,
|
||||
protocol: inbound.value.protocol,
|
||||
settings: inbound.value.settings.toString(),
|
||||
settings: settings,
|
||||
streamSettings: streamSettings,
|
||||
sniffing: sniffing,
|
||||
};
|
||||
|
|
@ -486,17 +497,35 @@ const okText = computed(() =>
|
|||
props.mode === 'edit' ? t('pages.client.submitEdit') : t('create'),
|
||||
);
|
||||
|
||||
// Whenever the structured stream form mutates the model, refresh the
|
||||
// Advanced JSON tab so it reflects the latest state. Use a deep watch
|
||||
// on the parsed JSON of the stream.
|
||||
// Whenever the structured form mutates stream / sniffing / settings,
|
||||
// refresh the matching slice of the Advanced JSON tab so the user
|
||||
// always sees the live state — flipping a switch in Sniffing or
|
||||
// editing encryption in Protocol now reflects in Advanced.
|
||||
watch(
|
||||
() => inbound.value && JSON.stringify(inbound.value.stream?.toJson?.() || {}),
|
||||
(next) => {
|
||||
if (next) {
|
||||
try {
|
||||
advancedJson.value.stream = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2);
|
||||
} catch (_e) { /* leave as is */ }
|
||||
}
|
||||
() => {
|
||||
if (!inbound.value?.stream) return;
|
||||
try {
|
||||
advancedJson.value.stream = 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 {
|
||||
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>
|
||||
|
|
@ -1631,6 +1660,10 @@ watch(
|
|||
message="Edit raw stream JSON to access advanced fields we don't yet expose through the form."
|
||||
class="mb-12" />
|
||||
<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-textarea v-model:value="advancedJson.stream" :auto-size="{ minRows: 10, maxRows: 24 }" spellcheck="false"
|
||||
class="json-editor" />
|
||||
|
|
|
|||
|
|
@ -29,17 +29,42 @@ const props = defineProps({
|
|||
|
||||
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() {
|
||||
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
|
||||
new QRious({
|
||||
element: canvas.value,
|
||||
size: props.size,
|
||||
size: exactSize,
|
||||
value: props.value,
|
||||
background: 'white',
|
||||
backgroundAlpha: 1,
|
||||
foreground: 'black',
|
||||
padding: 2,
|
||||
padding: 0,
|
||||
level: 'M',
|
||||
});
|
||||
}
|
||||
|
|
@ -115,7 +140,7 @@ function download() {
|
|||
|
||||
.qr-panel-canvas canvas {
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue