mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-13 05:51:58 +00:00
refactor(panel): rename injected globals + collapse QR modal entries
Rename the SPA globals injected by Go to drop the ad-hoc dunder shape and free up the bare `webBasePath` name (still the DB setting key) from colliding with the JS global it used to share: window.__X_UI_BASE_PATH__ -> window.X_UI_BASE_PATH window.__X_UI_CUR_VER__ -> window.X_UI_CUR_VER Also rework the QR-Code modal to fold every QR (subscription + JSON sub URL, share links, WireGuard config/peer links) into a single a-collapse with one panel per QR. Subscription panels are listed first and open by default; everything else stays collapsed so a multi-link inbound no longer scrolls forever.
This commit is contained in:
parent
737300b14b
commit
745e394c74
15 changed files with 97 additions and 82 deletions
|
|
@ -22,7 +22,7 @@ function readMetaToken() {
|
|||
// recurse through this same interceptor.
|
||||
async function fetchCsrfToken() {
|
||||
try {
|
||||
const basePath = window.__X_UI_BASE_PATH__;
|
||||
const basePath = window.X_UI_BASE_PATH;
|
||||
const url = (typeof basePath === 'string' && basePath !== '' && basePath !== '/'
|
||||
? basePath.replace(/\/$/, '') + CSRF_TOKEN_PATH
|
||||
: CSRF_TOKEN_PATH);
|
||||
|
|
@ -59,7 +59,7 @@ export function setupAxios() {
|
|||
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
|
||||
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
|
||||
const basePath = window.__X_UI_BASE_PATH__;
|
||||
const basePath = window.X_UI_BASE_PATH;
|
||||
if (typeof basePath === 'string' && basePath !== '' && basePath !== '/') {
|
||||
axios.defaults.baseURL = basePath;
|
||||
}
|
||||
|
|
@ -98,7 +98,7 @@ export function setupAxios() {
|
|||
// the user right back on the dashboard and the interceptor
|
||||
// would loop. Navigate to the dev login entry instead.
|
||||
if (import.meta.env.DEV) {
|
||||
const basePath = window.__X_UI_BASE_PATH__ || '/';
|
||||
const basePath = window.X_UI_BASE_PATH || '/';
|
||||
window.location.href = `${basePath}login.html`;
|
||||
} else {
|
||||
window.location.reload();
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ export class WebSocketClient {
|
|||
|
||||
#buildUrl() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
// basePath comes from window.__X_UI_BASE_PATH__ which is only injected
|
||||
// basePath comes from window.X_UI_BASE_PATH which is only injected
|
||||
// by the Go binary in production. In dev (Vite serves directly) the
|
||||
// global is missing and basePath would be '' — without the fallback to
|
||||
// '/' we'd build `ws://host:portws` (no separator) and the WebSocket
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ let sharedClient = null;
|
|||
|
||||
function getSharedClient() {
|
||||
if (sharedClient) return sharedClient;
|
||||
const basePath = (typeof window !== 'undefined' && window.__X_UI_BASE_PATH__) || '';
|
||||
const basePath = (typeof window !== 'undefined' && window.X_UI_BASE_PATH) || '';
|
||||
sharedClient = new WebSocketClient(basePath);
|
||||
return sharedClient;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ const { isMobile } = useMediaQuery();
|
|||
// the id→node map for the new "Node" column. Fetched once on mount.
|
||||
const { byId: nodesById } = useNodeList();
|
||||
|
||||
const basePath = window.__X_UI_BASE_PATH__ || '';
|
||||
const basePath = window.X_UI_BASE_PATH || '';
|
||||
const requestUri = window.location.pathname;
|
||||
|
||||
onMounted(async () => {
|
||||
|
|
@ -631,7 +631,7 @@ function onRowAction({ key, dbInbound }) {
|
|||
:ip-limit-enable="ipLimitEnable" :tg-bot-enable="tgBotEnable" :sub-settings="subSettings"
|
||||
:last-online-map="lastOnlineMap" :node-address="infoNodeAddress" />
|
||||
<QrCodeModal v-model:open="qrOpen" :db-inbound="qrDbInbound" :client="qrClient" :remark-model="remarkModel"
|
||||
:node-address="qrNodeAddress" />
|
||||
:node-address="qrNodeAddress" :sub-settings="subSettings" />
|
||||
|
||||
<TextModal v-model:open="textOpen" :title="textTitle" :content="textContent" :file-name="textFileName" />
|
||||
<PromptModal v-model:open="promptOpen" :title="promptTitle" :ok-text="promptOkText" :type="promptType"
|
||||
|
|
|
|||
|
|
@ -1,26 +1,21 @@
|
|||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { Protocols } from '@/models/inbound.js';
|
||||
import QrPanel from './QrPanel.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// Light QR-only modal — used for the "qrcode" row action on
|
||||
// single-user Shadowsocks and WireGuard inbounds. The big info modal
|
||||
// (InboundInfoModal) is too detailed when the user just wants the
|
||||
// share link as a QR.
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
dbInbound: { type: Object, default: null },
|
||||
client: { type: Object, default: null },
|
||||
remarkModel: { type: String, default: '-ieo' },
|
||||
// Address of the node hosting this inbound (empty string for local).
|
||||
// When set, share/QR links use it as the host instead of the panel's
|
||||
// origin — node-managed inbounds proxy from the node, not the panel.
|
||||
nodeAddress: { type: String, default: '' },
|
||||
subSettings: {
|
||||
type: Object,
|
||||
default: () => ({ enable: false, subURI: '', subJsonURI: '', subJsonEnable: false }),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:open']);
|
||||
|
|
@ -28,6 +23,50 @@ const emit = defineEmits(['update:open']);
|
|||
const links = ref([]);
|
||||
const wireguardConfigs = ref([]);
|
||||
const wireguardLinks = ref([]);
|
||||
const subLink = ref('');
|
||||
const subJsonLink = ref('');
|
||||
const activeKeys = ref([]);
|
||||
|
||||
const qrItems = computed(() => {
|
||||
const items = [];
|
||||
if (subLink.value) {
|
||||
items.push({
|
||||
key: 'sub',
|
||||
header: t('subscription.title'),
|
||||
value: subLink.value,
|
||||
});
|
||||
}
|
||||
if (subJsonLink.value) {
|
||||
items.push({
|
||||
key: 'sub-json',
|
||||
header: `${t('subscription.title')} (JSON)`,
|
||||
value: subJsonLink.value,
|
||||
});
|
||||
}
|
||||
links.value.forEach((link, idx) => {
|
||||
items.push({
|
||||
key: `l${idx}`,
|
||||
header: link.remark || `Link ${idx + 1}`,
|
||||
value: link.link,
|
||||
});
|
||||
});
|
||||
wireguardConfigs.value.forEach((cfg, idx) => {
|
||||
items.push({
|
||||
key: `wc${idx}`,
|
||||
header: `Peer ${idx + 1} config`,
|
||||
value: cfg,
|
||||
downloadName: `peer-${idx + 1}.conf`,
|
||||
});
|
||||
if (wireguardLinks.value[idx]) {
|
||||
items.push({
|
||||
key: `wl${idx}`,
|
||||
header: `Peer ${idx + 1} link`,
|
||||
value: wireguardLinks.value[idx],
|
||||
});
|
||||
}
|
||||
});
|
||||
return items;
|
||||
});
|
||||
|
||||
watch(() => props.open, (next) => {
|
||||
if (!next || !props.dbInbound) return;
|
||||
|
|
@ -46,6 +85,21 @@ watch(() => props.open, (next) => {
|
|||
wireguardConfigs.value = [];
|
||||
wireguardLinks.value = [];
|
||||
}
|
||||
|
||||
const subId = props.client?.subId;
|
||||
if (props.subSettings?.enable && subId) {
|
||||
subLink.value = (props.subSettings.subURI || '') + subId;
|
||||
subJsonLink.value = props.subSettings.subJsonEnable
|
||||
? (props.subSettings.subJsonURI || '') + subId
|
||||
: '';
|
||||
} else {
|
||||
subLink.value = '';
|
||||
subJsonLink.value = '';
|
||||
}
|
||||
const open = [];
|
||||
if (subLink.value) open.push('sub');
|
||||
if (subJsonLink.value) open.push('sub-json');
|
||||
activeKeys.value = open;
|
||||
});
|
||||
|
||||
function close() {
|
||||
|
|
@ -56,12 +110,17 @@ function close() {
|
|||
<template>
|
||||
<a-modal :open="open" :title="t('qrCode')" :footer="null" width="420px" @cancel="close">
|
||||
<template v-if="dbInbound">
|
||||
<QrPanel v-for="(link, idx) in links" :key="`l${idx}`" :value="link.link"
|
||||
:remark="link.remark || `Link ${idx + 1}`" />
|
||||
<template v-for="(cfg, idx) in wireguardConfigs" :key="`w${idx}`">
|
||||
<QrPanel :value="cfg" :remark="`Peer ${idx + 1} config`" :download-name="`peer-${idx + 1}.conf`" />
|
||||
<QrPanel v-if="wireguardLinks[idx]" :value="wireguardLinks[idx]" :remark="`Peer ${idx + 1} link`" />
|
||||
</template>
|
||||
<a-collapse v-model:active-key="activeKeys" ghost class="qr-collapse">
|
||||
<a-collapse-panel v-for="item in qrItems" :key="item.key" :header="item.header">
|
||||
<QrPanel :value="item.value" :remark="item.header" :download-name="item.downloadName || ''" />
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.qr-collapse :deep(.ant-collapse-content-box) {
|
||||
padding: 8px 0 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ function exportDb() {
|
|||
// The Go endpoint streams x-ui.db as a download. Setting
|
||||
// window.location triggers a browser download without leaving
|
||||
// the page (the Go side responds with Content-Disposition: attachment).
|
||||
window.location = window.__X_UI_BASE_PATH__+'panel/api/server/getDb';
|
||||
window.location = window.X_UI_BASE_PATH+'panel/api/server/getDb';
|
||||
}
|
||||
|
||||
function importDb() {
|
||||
|
|
|
|||
|
|
@ -53,14 +53,14 @@ onMounted(() => {
|
|||
});
|
||||
});
|
||||
|
||||
const basePath = window.__X_UI_BASE_PATH__ || '';
|
||||
const basePath = window.X_UI_BASE_PATH || '';
|
||||
const requestUri = window.location.pathname;
|
||||
|
||||
// In production, dist.go injects window.__X_UI_CUR_VER__ at serve time.
|
||||
// In production, dist.go injects window.X_UI_CUR_VER at serve time.
|
||||
// In dev, Vite serves the HTML directly so the global is missing — fall
|
||||
// back to currentVersion from the panel-update API once it answers.
|
||||
const displayVersion = computed(
|
||||
() => panelUpdateInfo.value?.currentVersion || window.__X_UI_CUR_VER__ || '?',
|
||||
() => panelUpdateInfo.value?.currentVersion || window.X_UI_CUR_VER || '?',
|
||||
);
|
||||
|
||||
// Hide/reveal the public IPv4/IPv6 — same pattern as legacy.
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const user = reactive({
|
|||
twoFactorCode: '',
|
||||
});
|
||||
|
||||
const basePath = window.__X_UI_BASE_PATH__ || '';
|
||||
const basePath = window.X_UI_BASE_PATH || '';
|
||||
|
||||
onMounted(async () => {
|
||||
const msg = await HttpUtil.post('/getTwoFactorEnable');
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ useWebSocket({ nodes: applyNodesEvent });
|
|||
|
||||
const { isMobile } = useMediaQuery();
|
||||
|
||||
const basePath = window.__X_UI_BASE_PATH__ || '';
|
||||
const basePath = window.X_UI_BASE_PATH || '';
|
||||
const requestUri = window.location.pathname;
|
||||
|
||||
// === Form modal state =================================================
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ async function sendUpdateUser() {
|
|||
if (msg?.success) {
|
||||
// Force re-login at the standard logout path; basePath is handled
|
||||
// by the Go router so a relative redirect is correct here.
|
||||
const basePath = window.__X_UI_BASE_PATH__ || '';
|
||||
const basePath = window.X_UI_BASE_PATH || '';
|
||||
window.location.replace(`${basePath}logout`);
|
||||
}
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const { t } = useI18n();
|
|||
const { fetched, spinning, saveDisabled, allSetting, saveAll } = useAllSetting();
|
||||
const { isMobile } = useMediaQuery();
|
||||
|
||||
const basePath = window.__X_UI_BASE_PATH__ || '';
|
||||
const basePath = window.X_UI_BASE_PATH || '';
|
||||
const requestUri = window.location.pathname;
|
||||
|
||||
// AD-Vue 4's <a-back-top> calls `target()` after mount to find the
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ function onRemoveRoutingRules({ prefix }) {
|
|||
void message;
|
||||
const { isMobile } = useMediaQuery();
|
||||
|
||||
const basePath = window.__X_UI_BASE_PATH__ || '';
|
||||
const basePath = window.X_UI_BASE_PATH || '';
|
||||
const requestUri = window.location.pathname;
|
||||
|
||||
// See SettingsPage scrollTarget — wrap so `document` is in scope.
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ function refreshBasePath() {
|
|||
}
|
||||
|
||||
// `apply: 'serve'` keeps the injection out of `vite build` — dist.go
|
||||
// already injects __X_UI_BASE_PATH__ at runtime in production.
|
||||
// already injects webBasePath at runtime in production.
|
||||
function injectBasePathPlugin() {
|
||||
return {
|
||||
name: 'xui-inject-base-path',
|
||||
|
|
@ -65,7 +65,7 @@ function injectBasePathPlugin() {
|
|||
transformIndexHtml(html) {
|
||||
const basePath = refreshBasePath();
|
||||
const escaped = basePath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
const tag = `<script>window.__X_UI_BASE_PATH__="${escaped}";</script>`;
|
||||
const tag = `<script>window.X_UI_BASE_PATH="${escaped}";</script>`;
|
||||
return html.replace('</head>', `${tag}</head>`);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue