mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-13 07:01:25 +00:00
feat(frontend): swap QRious for ant-design-vue's a-qrcode
- Migrate SubPage, QrPanel and TwoFactorModal from a QRious canvas to <a-qrcode type="svg">, which renders the QR matrix as crispEdges SVG rectangles — pixel-perfect at any display size or DPR, no more white scan-line artifacts from non-integer canvas scaling - Drop the now-unused qrious dependency and its manualChunks entry - Default the panel to ultra-dark on first load (existing user preferences in localStorage are preserved) - Let the sub controller read subpage.html from web/dist/ first and fall back to the embedded copy, so Vite rebuilds in dev no longer require a Go recompile to refresh the asset hashes
This commit is contained in:
parent
c1efc48694
commit
04828246fc
8 changed files with 75 additions and 261 deletions
119
frontend/package-lock.json
generated
119
frontend/package-lock.json
generated
|
|
@ -1,20 +1,18 @@
|
|||
{
|
||||
"name": "3x-ui-frontend",
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "3x-ui-frontend",
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"axios": "^1.7.9",
|
||||
"dayjs": "^1.11.20",
|
||||
"moment": "^2.30.1",
|
||||
"otpauth": "^9.5.1",
|
||||
"qrious": "^4.0.2",
|
||||
"qs": "^6.13.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.4",
|
||||
|
|
@ -207,42 +205,6 @@
|
|||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array/node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-helpers": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz",
|
||||
|
|
@ -968,12 +930,33 @@
|
|||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
|
|
@ -1306,42 +1289,6 @@
|
|||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/brace-expansion": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/espree": {
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
|
||||
|
|
@ -2073,6 +2020,21 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
|
|
@ -2318,11 +2280,6 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qrious": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/qrious/-/qrious-4.0.2.tgz",
|
||||
"integrity": "sha512-xWPJIrK1zu5Ypn898fBp8RHkT/9ibquV2Kv24S/JY9VYEhMBMKur1gHVsOiNUh7PHP9uCgejjpZUHUIXXKoU/g=="
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
|
||||
|
|
|
|||
|
|
@ -15,9 +15,7 @@
|
|||
"ant-design-vue": "^4.2.6",
|
||||
"axios": "^1.7.9",
|
||||
"dayjs": "^1.11.20",
|
||||
"moment": "^2.30.1",
|
||||
"otpauth": "^9.5.1",
|
||||
"qrious": "^4.0.2",
|
||||
"qs": "^6.13.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.4",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ function readBool(key, fallback) {
|
|||
}
|
||||
|
||||
const isDark = readBool(STORAGE_DARK, true);
|
||||
const isUltra = readBool(STORAGE_ULTRA, false);
|
||||
const isUltra = readBool(STORAGE_ULTRA, true);
|
||||
|
||||
export const theme = reactive({
|
||||
isDark,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
<script setup>
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import QRious from 'qrious';
|
||||
import { CopyOutlined, DownloadOutlined } from '@ant-design/icons-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
|
|
@ -9,73 +7,14 @@ import { ClipboardManager, FileManager } from '@/utils';
|
|||
|
||||
const { t } = useI18n();
|
||||
|
||||
// Renders a single share-link as a clickable QR code + a copy button
|
||||
// + (optional) a download button. Used per-link inside the inbound
|
||||
// info modal — the canvas is repainted whenever `value` changes.
|
||||
|
||||
const props = defineProps({
|
||||
// The link or config text to encode + display.
|
||||
value: { type: String, required: true },
|
||||
// Header label shown next to the copy button.
|
||||
remark: { type: String, default: '' },
|
||||
// Optional download filename — when set, surfaces a download button.
|
||||
downloadName: { type: String, default: '' },
|
||||
// Final on-screen QR size in CSS pixels. The canvas drawing buffer
|
||||
// is rounded down to a multiple of the QR matrix width (so the QR
|
||||
// fills it edge-to-edge) and CSS then scales the canvas to exactly
|
||||
// this size — so a denser QR (e.g. WireGuard config) and a sparser
|
||||
// one (its link) display at identical dimensions.
|
||||
size: { type: Number, default: 240 },
|
||||
// Toggle the QR rendering off when callers only want the "row of buttons"
|
||||
// styling (used when the legacy panel rendered links without QRs).
|
||||
showQr: { type: Boolean, default: true },
|
||||
});
|
||||
|
||||
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;
|
||||
new QRious({
|
||||
element: canvas.value,
|
||||
size: exactSize,
|
||||
value: props.value,
|
||||
background: 'white',
|
||||
backgroundAlpha: 1,
|
||||
foreground: 'black',
|
||||
padding: 0,
|
||||
level: 'M',
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(paint);
|
||||
watch(() => props.value, paint);
|
||||
watch(() => props.size, paint);
|
||||
|
||||
async function copy() {
|
||||
const ok = await ClipboardManager.copyText(props.value);
|
||||
if (ok) message.success(t('copied'));
|
||||
|
|
@ -107,7 +46,8 @@ function download() {
|
|||
</a-tooltip>
|
||||
</div>
|
||||
<div v-if="showQr" class="qr-panel-canvas">
|
||||
<canvas ref="canvas" :style="{ width: `${size}px`, height: `${size}px` }" @click="copy" />
|
||||
<a-qrcode class="qr-code" :value="value" :size="size" type="svg" :bordered="false"
|
||||
:title="t('copy')" @click="copy" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -140,14 +80,10 @@ function download() {
|
|||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.qr-panel-canvas canvas {
|
||||
.qr-panel-canvas .qr-code {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
padding: 0 !important;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
/* Drawing buffer is matrix-snapped (smaller than display size for
|
||||
* dense QRs); scale up crisply so dense and sparse QRs share the
|
||||
* same on-screen footprint without blurring. */
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,13 @@
|
|||
<script setup>
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { message } from 'ant-design-vue';
|
||||
import * as OTPAuth from 'otpauth';
|
||||
import QRious from 'qrious';
|
||||
|
||||
import { ClipboardManager } from '@/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// Two flavors of this modal:
|
||||
// • type='set' shows a QR code + manual key + a 6-digit verifier
|
||||
// (used when enabling 2FA the first time);
|
||||
// • type='confirm' shows just the 6-digit verifier (used when
|
||||
// toggling 2FA off and when changing the admin user/password).
|
||||
//
|
||||
// Either way the parent supplies a `confirm(success: boolean)`
|
||||
// callback — we run it with `true` only if the entered code matches
|
||||
// the live TOTP value, otherwise `false`.
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
title: { type: String, default: '' },
|
||||
|
|
@ -30,29 +19,10 @@ const props = defineProps({
|
|||
const emit = defineEmits(['update:open', 'confirm']);
|
||||
|
||||
const enteredCode = ref('');
|
||||
const qrCanvas = ref(null);
|
||||
const qrValue = ref('');
|
||||
|
||||
let totp = null;
|
||||
|
||||
// Byte-mode capacities (level L) for QR versions 1..40 — used to pick
|
||||
// the matrix width up front so the canvas size is an exact multiple of
|
||||
// pixelSize. Without this, QRious renders at floor(size/matrix) and
|
||||
// centers, leaving a white margin around the QR.
|
||||
const QR_L_BYTE_CAPACITY = [
|
||||
17, 32, 53, 78, 106, 134, 154, 192, 230, 271,
|
||||
321, 367, 425, 458, 520, 586, 644, 718, 792, 858,
|
||||
929, 1003, 1091, 1171, 1273, 1367, 1465, 1528, 1628, 1732,
|
||||
1840, 1952, 2068, 2188, 2303, 2431, 2563, 2699, 2809, 2953,
|
||||
];
|
||||
|
||||
function pickQrMatrixWidth(value) {
|
||||
const byteLen = new TextEncoder().encode(value).length;
|
||||
for (let i = 0; i < QR_L_BYTE_CAPACITY.length; i++) {
|
||||
if (byteLen <= QR_L_BYTE_CAPACITY[i]) return 17 + 4 * (i + 1);
|
||||
}
|
||||
return 17 + 4 * 40;
|
||||
}
|
||||
|
||||
function buildTotp() {
|
||||
totp = new OTPAuth.TOTP({
|
||||
issuer: '3x-ui',
|
||||
|
|
@ -62,25 +32,7 @@ function buildTotp() {
|
|||
period: 30,
|
||||
secret: props.token,
|
||||
});
|
||||
}
|
||||
|
||||
async function paintQr() {
|
||||
await nextTick();
|
||||
if (!qrCanvas.value || !totp) return;
|
||||
const value = totp.toString();
|
||||
const matrixWidth = pickQrMatrixWidth(value);
|
||||
const pixelSize = Math.max(1, Math.floor(200 / matrixWidth));
|
||||
const exactSize = matrixWidth * pixelSize;
|
||||
new QRious({
|
||||
element: qrCanvas.value,
|
||||
size: exactSize,
|
||||
value,
|
||||
background: 'white',
|
||||
backgroundAlpha: 1,
|
||||
foreground: 'black',
|
||||
padding: 0,
|
||||
level: 'L',
|
||||
});
|
||||
qrValue.value = totp.toString();
|
||||
}
|
||||
|
||||
watch(() => props.open, (next) => {
|
||||
|
|
@ -88,7 +40,6 @@ watch(() => props.open, (next) => {
|
|||
enteredCode.value = '';
|
||||
if (props.token) {
|
||||
buildTotp();
|
||||
if (props.type === 'set') paintQr();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -124,9 +75,8 @@ async function copyToken() {
|
|||
<a-divider />
|
||||
<p>{{ t('pages.settings.security.twoFactorModalFirstStep') }}</p>
|
||||
<div class="qr-wrap">
|
||||
<div class="qr-bg">
|
||||
<canvas ref="qrCanvas" class="qr-cv" @click="copyToken" />
|
||||
</div>
|
||||
<a-qrcode class="qr-code" :value="qrValue" :size="180" type="svg" :bordered="false"
|
||||
error-level="L" :title="t('copy')" @click="copyToken" />
|
||||
<span class="qr-token">{{ token }}</span>
|
||||
</div>
|
||||
<a-divider />
|
||||
|
|
@ -154,22 +104,11 @@ async function copyToken() {
|
|||
gap: 12px;
|
||||
}
|
||||
|
||||
.qr-bg {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
background: #fff;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.qr-cv {
|
||||
.qr-code {
|
||||
cursor: pointer;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
/* Drawing buffer is matrix-snapped (smaller than display size); scale
|
||||
* up crisply so the QR fills the box without blurring. */
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
padding: 0 !important;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.qr-token {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
CopyOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import QRious from 'qrious';
|
||||
|
||||
import { ClipboardManager, IntlUtil, LanguageManager } from '@/utils';
|
||||
import {
|
||||
|
|
@ -71,32 +70,7 @@ function onLangChange(next) {
|
|||
LanguageManager.setLanguage(next);
|
||||
}
|
||||
|
||||
// QR code rendering ===========================================
|
||||
// Each ref points at a canvas element we paint after mount; QRious
|
||||
// sizes itself from the element's `size` attribute.
|
||||
const subQr = ref(null);
|
||||
const subJsonQr = ref(null);
|
||||
const subClashQr = ref(null);
|
||||
|
||||
function paintQr(canvas, value) {
|
||||
if (!canvas || !value) return;
|
||||
new QRious({
|
||||
element: canvas,
|
||||
size: 220,
|
||||
value,
|
||||
background: 'white',
|
||||
backgroundAlpha: 1,
|
||||
foreground: 'black',
|
||||
padding: 4,
|
||||
level: 'M',
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
paintQr(subQr.value, subUrl);
|
||||
paintQr(subJsonQr.value, subJsonUrl);
|
||||
paintQr(subClashQr.value, subClashUrl);
|
||||
});
|
||||
const QR_SIZE = 240;
|
||||
|
||||
// Actions =====================================================
|
||||
async function copy(value) {
|
||||
|
|
@ -184,7 +158,8 @@ const themeClass = computed(() => ({
|
|||
<a-col :xs="24" :sm="subJsonUrl || subClashUrl ? 12 : 24" class="qr-col">
|
||||
<div class="qr-box">
|
||||
<a-tag color="purple" class="qr-tag">{{ t('pages.settings.subSettings') }}</a-tag>
|
||||
<canvas ref="subQr" class="qr-canvas" :title="t('copy')" @click="copy(subUrl)" />
|
||||
<a-qrcode class="qr-code" :value="subUrl" :size="QR_SIZE" type="svg" :bordered="false"
|
||||
:title="t('copy')" @click="copy(subUrl)" />
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col v-if="subJsonUrl" :xs="24" :sm="12" class="qr-col">
|
||||
|
|
@ -192,13 +167,15 @@ const themeClass = computed(() => ({
|
|||
<a-tag color="purple" class="qr-tag">
|
||||
{{ t('pages.settings.subSettings') }} JSON
|
||||
</a-tag>
|
||||
<canvas ref="subJsonQr" class="qr-canvas" :title="t('copy')" @click="copy(subJsonUrl)" />
|
||||
<a-qrcode class="qr-code" :value="subJsonUrl" :size="QR_SIZE" type="svg" :bordered="false"
|
||||
:title="t('copy')" @click="copy(subJsonUrl)" />
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col v-if="subClashUrl" :xs="24" :sm="12" class="qr-col">
|
||||
<div class="qr-box">
|
||||
<a-tag color="purple" class="qr-tag">Clash / Mihomo</a-tag>
|
||||
<canvas ref="subClashQr" class="qr-canvas" :title="t('copy')" @click="copy(subClashUrl)" />
|
||||
<a-qrcode class="qr-code" :value="subClashUrl" :size="QR_SIZE" type="svg" :bordered="false"
|
||||
:title="t('copy')" @click="copy(subClashUrl)" />
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
|
@ -336,7 +313,7 @@ const themeClass = computed(() => ({
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 220px;
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.qr-tag {
|
||||
|
|
@ -345,8 +322,9 @@ const themeClass = computed(() => ({
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.qr-canvas {
|
||||
.qr-code {
|
||||
cursor: pointer;
|
||||
padding: 0 !important;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -163,7 +163,6 @@ export default defineConfig({
|
|||
|| id.includes('/node_modules/@vue/')
|
||||
) return 'vendor-vue';
|
||||
if (id.includes('dayjs')) return 'vendor-dayjs';
|
||||
if (id.includes('qrious')) return 'vendor-qrious';
|
||||
if (id.includes('axios')) return 'vendor-axios';
|
||||
if (
|
||||
id.includes('vue3-persian-datetime-picker')
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
|
@ -154,11 +155,17 @@ func (a *SUBController) subs(c *gin.Context) {
|
|||
// page's static asset references resolve correctly when the panel runs
|
||||
// behind a URL prefix.
|
||||
func (a *SUBController) serveSubPage(c *gin.Context, basePath string, page PageData) {
|
||||
dist := webpkg.EmbeddedDist()
|
||||
body, err := dist.ReadFile("dist/subpage.html")
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "missing embedded subpage")
|
||||
return
|
||||
var body []byte
|
||||
if diskBody, diskErr := os.ReadFile("web/dist/subpage.html"); diskErr == nil {
|
||||
body = diskBody
|
||||
} else {
|
||||
dist := webpkg.EmbeddedDist()
|
||||
readBody, err := dist.ReadFile("dist/subpage.html")
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "missing embedded subpage")
|
||||
return
|
||||
}
|
||||
body = readBody
|
||||
}
|
||||
|
||||
// Vite emits absolute asset URLs (`/assets/...`); when the panel is
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue