Browse Source

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 <[email protected]>
Black 19 hours ago
parent
commit
121b6e0bd0
1 changed files with 119 additions and 27 deletions
  1. 119 27
      frontend/src/pages/inbounds/InboundInfoModal.vue

+ 119 - 27
frontend/src/pages/inbounds/InboundInfoModal.vue

@@ -19,41 +19,20 @@ import { useDatepicker } from '@/composables/useDatepicker.js';
 const { t } = useI18n();
 const { t } = useI18n();
 const { datepicker } = useDatepicker();
 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({
 const props = defineProps({
   open: { type: Boolean, default: false },
   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 },
   dbInbound: { type: Object, default: null },
-  // Index into inbound.clients to focus on for multi-user inbounds.
   clientIndex: { type: Number, default: 0 },
   clientIndex: { type: Number, default: 0 },
-  // Sidecar config the legacy panel keyed off `app.*`.
   remarkModel: { type: String, default: '-ieo' },
   remarkModel: { type: String, default: '-ieo' },
   expireDiff: { type: Number, default: 0 },
   expireDiff: { type: Number, default: 0 },
   trafficDiff: { type: Number, default: 0 },
   trafficDiff: { type: Number, default: 0 },
   ipLimitEnable: { type: Boolean, default: false },
   ipLimitEnable: { type: Boolean, default: false },
   tgBotEnable: { 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: '' },
   nodeAddress: { type: String, default: '' },
   subSettings: {
   subSettings: {
     type: Object,
     type: Object,
     default: () => ({ enable: false, subURI: '', subJsonURI: '', subJsonEnable: false }),
     default: () => ({ enable: false, subURI: '', subJsonURI: '', subJsonEnable: false }),
   },
   },
-  // Email -> ts (last-online unix-ms) map fetched at the page level.
   lastOnlineMap: { type: Object, default: () => ({}) },
   lastOnlineMap: { type: Object, default: () => ({}) },
 });
 });
 
 
@@ -598,7 +577,8 @@ const showSubscriptionTab = computed(
             <div v-if="inbound.settings.gateway?.length" class="info-row">
             <div v-if="inbound.settings.gateway?.length" class="info-row">
               <dt>Gateway</dt>
               <dt>Gateway</dt>
               <dd><a-tag v-for="(ip, j) in inbound.settings.gateway" :key="`tun-i-gw-${j}`" color="green"
               <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>
             <div v-if="inbound.settings.dns?.length" class="info-row">
             <div v-if="inbound.settings.dns?.length" class="info-row">
               <dt>DNS</dt>
               <dt>DNS</dt>
@@ -612,7 +592,8 @@ const showSubscriptionTab = computed(
             <div v-if="inbound.settings.autoSystemRoutingTable?.length" class="info-row">
             <div v-if="inbound.settings.autoSystemRoutingTable?.length" class="info-row">
               <dt>Auto system routes</dt>
               <dt>Auto system routes</dt>
               <dd><a-tag v-for="(cidr, j) in inbound.settings.autoSystemRoutingTable" :key="`tun-i-rt-${j}`"
               <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>
             </div>
           </dl>
           </dl>
 
 
@@ -670,12 +651,101 @@ const showSubscriptionTab = computed(
                   <span class="account-sep">:</span>
                   <span class="account-sep">:</span>
                   <a-tag class="value-tag">{{ account.pass }}</a-tag>
                   <a-tag class="value-tag">{{ account.pass }}</a-tag>
                   <a-tooltip :title="t('copy')">
                   <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-button>
                   </a-tooltip>
                   </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>
                 </dd>
               </div>
               </div>
             </template>
             </template>
@@ -897,6 +967,7 @@ const showSubscriptionTab = computed(
   white-space: normal;
   white-space: normal;
   word-break: break-all;
   word-break: break-all;
   display: inline-block;
   display: inline-block;
+  margin-right: 0;
 }
 }
 
 
 .value-block {
 .value-block {
@@ -927,6 +998,27 @@ const showSubscriptionTab = computed(
   flex-shrink: 0;
   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 {
 .security-line {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;