mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-26 07:08:01 +00:00
feat(panel): copy connection strings for mixed inbound (#4450)
* feat(panel): copy connection strings for `mixed` inbound * feat(panel): inline share buttons on desktop, dropdown on mobile Replace the credentials-copy dropdown with three labeled share buttons (SOCKS5 / HTTP / Telegram), each with a tooltip preview of the full URL. Reverse the URI auth position so the format becomes `scheme://host:port@user:pass` (matches Hiddify-style sharing). Add a Telegram t.me/socks link with URL-encoded user/pass. On viewports <=600px the inline row collapses into a single Copy dropdown to keep the per-account row from wrapping into clutter. RTL panels are unaffected — the share divider uses inline-* logical props. --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
@@ -19,41 +19,20 @@ import { useDatepicker } from '@/composables/useDatepicker.js';
|
||||
const { t } = useI18n();
|
||||
const { datepicker } = useDatepicker();
|
||||
|
||||
// One modal handles every protocol's info / share view because the
|
||||
// legacy template did the same. The big v-if forks at the top decide
|
||||
// which sub-block of the body renders:
|
||||
// • multi-user inbound (VMess/VLess/Trojan/SS-multi/Hysteria) → per-
|
||||
// client row + share links
|
||||
// • SS single-user → connection details + share link
|
||||
// • WireGuard → secret/peers + per-peer config download
|
||||
// • Mixed/HTTP/Tunnel → connection details only
|
||||
//
|
||||
// We display links via QrPanel — each link gets its own QR + copy +
|
||||
// (for WireGuard configs) download button.
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
// Result of inbounds-page checkFallback() so the link-gen sees the
|
||||
// root inbound's listen/port/security when the dbInbound is a
|
||||
// domain-socket fallback (`@<name>`).
|
||||
dbInbound: { type: Object, default: null },
|
||||
// Index into inbound.clients to focus on for multi-user inbounds.
|
||||
clientIndex: { type: Number, default: 0 },
|
||||
// Sidecar config the legacy panel keyed off `app.*`.
|
||||
remarkModel: { type: String, default: '-ieo' },
|
||||
expireDiff: { type: Number, default: 0 },
|
||||
trafficDiff: { type: Number, default: 0 },
|
||||
ipLimitEnable: { type: Boolean, default: false },
|
||||
tgBotEnable: { type: Boolean, default: false },
|
||||
// Address of the node hosting this inbound; '' for local. Wired
|
||||
// through to share/QR link generation so node-managed inbounds
|
||||
// produce links that connect to the node, not the central panel.
|
||||
nodeAddress: { type: String, default: '' },
|
||||
subSettings: {
|
||||
type: Object,
|
||||
default: () => ({ enable: false, subURI: '', subJsonURI: '', subJsonEnable: false }),
|
||||
},
|
||||
// Email -> ts (last-online unix-ms) map fetched at the page level.
|
||||
lastOnlineMap: { type: Object, default: () => ({}) },
|
||||
});
|
||||
|
||||
@@ -598,7 +577,8 @@ const showSubscriptionTab = computed(
|
||||
<div v-if="inbound.settings.gateway?.length" class="info-row">
|
||||
<dt>Gateway</dt>
|
||||
<dd><a-tag v-for="(ip, j) in inbound.settings.gateway" :key="`tun-i-gw-${j}`" color="green"
|
||||
class="value-tag">{{ ip }}</a-tag></dd>
|
||||
class="value-tag">{{
|
||||
ip }}</a-tag></dd>
|
||||
</div>
|
||||
<div v-if="inbound.settings.dns?.length" class="info-row">
|
||||
<dt>DNS</dt>
|
||||
@@ -612,7 +592,8 @@ const showSubscriptionTab = computed(
|
||||
<div v-if="inbound.settings.autoSystemRoutingTable?.length" class="info-row">
|
||||
<dt>Auto system routes</dt>
|
||||
<dd><a-tag v-for="(cidr, j) in inbound.settings.autoSystemRoutingTable" :key="`tun-i-rt-${j}`"
|
||||
color="green">{{ cidr }}</a-tag></dd>
|
||||
color="green">{{
|
||||
cidr }}</a-tag></dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
@@ -670,12 +651,101 @@ const showSubscriptionTab = computed(
|
||||
<span class="account-sep">:</span>
|
||||
<a-tag class="value-tag">{{ account.pass }}</a-tag>
|
||||
<a-tooltip :title="t('copy')">
|
||||
<a-button size="small" @click="copyText(`${account.user}:${account.pass}`)">
|
||||
<template #icon>
|
||||
<CopyOutlined />
|
||||
</template>
|
||||
<a-button size="small" type="text"
|
||||
@click="copyText(`${account.user}:${account.pass}`)">
|
||||
<template #icon><CopyOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-space :size="4" wrap class="share-buttons share-desktop">
|
||||
<a-tooltip :title="`socks5://${dbInbound.address}:${dbInbound.port}@${account.user}:${account.pass}`">
|
||||
<a-button size="small"
|
||||
@click="copyText(`socks5://${dbInbound.address}:${dbInbound.port}@${account.user}:${account.pass}`)">
|
||||
SOCKS5
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="`http://${dbInbound.address}:${dbInbound.port}@${account.user}:${account.pass}`">
|
||||
<a-button size="small"
|
||||
@click="copyText(`http://${dbInbound.address}:${dbInbound.port}@${account.user}:${account.pass}`)">
|
||||
HTTP
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="https://t.me/socks?server=...&port=...&user=...&pass=...">
|
||||
<a-button size="small"
|
||||
@click="copyText(`https://t.me/socks?server=${encodeURIComponent(dbInbound.address)}&port=${dbInbound.port}&user=${encodeURIComponent(account.user)}&pass=${encodeURIComponent(account.pass)}`)">
|
||||
Telegram
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
<a-dropdown :trigger="['click']" class="share-mobile">
|
||||
<a-button size="small">
|
||||
<template #icon><CopyOutlined /></template>
|
||||
{{ t('copy') }}
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu @click="({ key }) => {
|
||||
const h = dbInbound.address;
|
||||
const port = dbInbound.port;
|
||||
if (key === 'telegram') {
|
||||
copyText(`https://t.me/socks?server=${encodeURIComponent(h)}&port=${port}&user=${encodeURIComponent(account.user)}&pass=${encodeURIComponent(account.pass)}`);
|
||||
} else {
|
||||
copyText(`${key}://${h}:${port}@${account.user}:${account.pass}`);
|
||||
}
|
||||
}">
|
||||
<a-menu-item key="socks5">SOCKS5</a-menu-item>
|
||||
<a-menu-item key="http">HTTP</a-menu-item>
|
||||
<a-menu-item key="telegram">Telegram</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</dd>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="inbound.settings.auth === 'noauth'">
|
||||
<div class="info-row">
|
||||
<dt>{{ t('copy') }}</dt>
|
||||
<dd>
|
||||
<a-space :size="4" wrap class="share-buttons share-desktop">
|
||||
<a-tooltip :title="`socks5://${dbInbound.address}:${dbInbound.port}`">
|
||||
<a-button size="small"
|
||||
@click="copyText(`socks5://${dbInbound.address}:${dbInbound.port}`)">
|
||||
SOCKS5
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="`http://${dbInbound.address}:${dbInbound.port}`">
|
||||
<a-button size="small"
|
||||
@click="copyText(`http://${dbInbound.address}:${dbInbound.port}`)">
|
||||
HTTP
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="https://t.me/socks?server=...&port=...">
|
||||
<a-button size="small"
|
||||
@click="copyText(`https://t.me/socks?server=${encodeURIComponent(dbInbound.address)}&port=${dbInbound.port}`)">
|
||||
Telegram
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
<a-dropdown :trigger="['click']" class="share-mobile">
|
||||
<a-button size="small">
|
||||
<template #icon><CopyOutlined /></template>
|
||||
{{ t('copy') }}
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu @click="({ key }) => {
|
||||
const h = dbInbound.address;
|
||||
const port = dbInbound.port;
|
||||
if (key === 'telegram') {
|
||||
copyText(`https://t.me/socks?server=${encodeURIComponent(h)}&port=${port}`);
|
||||
} else {
|
||||
copyText(`${key}://${h}:${port}`);
|
||||
}
|
||||
}">
|
||||
<a-menu-item key="socks5">SOCKS5</a-menu-item>
|
||||
<a-menu-item key="http">HTTP</a-menu-item>
|
||||
<a-menu-item key="telegram">Telegram</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</dd>
|
||||
</div>
|
||||
</template>
|
||||
@@ -897,6 +967,7 @@ const showSubscriptionTab = computed(
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
display: inline-block;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.value-block {
|
||||
@@ -927,6 +998,27 @@ const showSubscriptionTab = computed(
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.share-buttons,
|
||||
.share-mobile {
|
||||
margin-inline-start: 4px;
|
||||
padding-inline-start: 8px;
|
||||
border-inline-start: 1px solid rgba(128, 128, 128, 0.25);
|
||||
}
|
||||
|
||||
.share-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.share-desktop {
|
||||
display: none !important;
|
||||
}
|
||||
.share-mobile {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.security-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user