-
{{ clientSettings.tgId }}
-
-
-
-
-
+
+
+ {{ t('pages.inbounds.copyLink') }}
+
+
+ {{ link.link }}
+
+
+
+
+
+
+
-
-
-
- {{ t('pages.inbounds.copyLink') }}
-
-
-
-
-
-
- {{ t('pages.inbounds.copyLink') }}
-
-
-
-
-
-
-
- | {{ t('pages.inbounds.targetAddress') }} |
- {{ t('pages.inbounds.destinationPort') }} |
- {{ t('pages.inbounds.network') }} |
- FollowRedirect |
-
-
-
-
- | {{ inbound.settings.address }} |
- {{ inbound.settings.port }} |
- {{ inbound.settings.network }} |
- {{ inbound.settings.followRedirect }} |
-
-
-
-
-
-
-
-
- | Auth |
- UDP |
- IP |
-
-
-
-
- | {{ inbound.settings.auth }} |
- {{ inbound.settings.udp }} |
- {{ inbound.settings.ip }} |
-
-
-
- |
- {{ t('username') }} |
- {{ t('password') }} |
-
-
- | {{ idx }} |
- {{ account.user }} |
- {{ account.pass }} |
-
-
-
-
-
-
-
-
-
- |
- {{ t('username') }} |
- {{ t('password') }} |
-
-
-
-
- | {{ idx }} |
- {{ account.user }} |
- {{ account.pass }} |
-
-
-
-
-
-
-
-
- | Secret key |
- {{ inbound.settings.secretKey }} |
-
-
- | Public key |
- {{ inbound.settings.pubKey }} |
-
-
- | MTU |
- {{ inbound.settings.mtu }} |
-
-
- | No-kernel TUN |
- {{ inbound.settings.noKernelTun }} |
-
-
-
- | Peer {{ idx + 1 }} |
-
-
- | Secret key |
- {{ peer.privateKey }} |
-
-
- | Public key |
- {{ peer.publicKey }} |
-
-
- | PSK |
- {{ peer.psk }} |
-
-
- | Allowed IPs |
- {{ (peer.allowedIPs || []).join(',') }} |
-
-
- | Keep alive |
- {{ peer.keepAlive }} |
-
-
- |
-
- |
-
-
- |
-
- |
-
-
-
-
+
+
+
@@ -719,6 +787,65 @@ const serverNameLabel = computed(() => inbound.value?.serverName || '');
display: inline-block;
}
+/* Stacked label/value list — one row per field. Long values wrap
+ * (or fall through to a code block) so they never blow out the modal. */
+.info-list {
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+}
+.info-row {
+ display: grid;
+ grid-template-columns: 140px minmax(0, 1fr);
+ align-items: center;
+ gap: 12px;
+ padding: 6px 0;
+ border-bottom: 1px solid rgba(128, 128, 128, 0.12);
+}
+.info-row:last-child {
+ border-bottom: none;
+}
+.info-row dt {
+ margin: 0;
+ font-size: 13px;
+ opacity: 0.75;
+}
+.info-row dd {
+ margin: 0;
+ min-width: 0;
+}
+.value-tag {
+ max-width: 100%;
+ white-space: normal;
+ word-break: break-all;
+ display: inline-block;
+}
+.value-block {
+ display: flex;
+ align-items: flex-start;
+ gap: 6px;
+ min-width: 0;
+}
+.value-code {
+ flex: 1;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: 12px;
+ word-break: break-all;
+ white-space: pre-wrap;
+ padding: 4px 8px;
+ background: rgba(0, 0, 0, 0.04);
+ border-radius: 4px;
+ user-select: all;
+ min-width: 0;
+}
+:global(body.dark) .value-code {
+ background: rgba(255, 255, 255, 0.05);
+}
+.value-copy {
+ flex-shrink: 0;
+}
+
.security-line {
display: flex;
align-items: center;
@@ -769,4 +896,56 @@ const serverNameLabel = computed(() => inbound.value?.serverName || '');
.wg-table td {
word-break: break-all;
}
+
+/* Reusable copy/link panel that replaces QrPanel for the no-QR design. */
+.link-panel {
+ border: 1px solid rgba(128, 128, 128, 0.2);
+ border-radius: 8px;
+ padding: 10px;
+ margin-bottom: 10px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+.link-panel-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex-wrap: wrap;
+}
+.link-panel-text {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: 11px;
+ word-break: break-all;
+ white-space: pre-wrap;
+ padding: 6px 8px;
+ background: rgba(0, 0, 0, 0.04);
+ border-radius: 4px;
+ user-select: all;
+}
+:global(body.dark) .link-panel-text {
+ background: rgba(255, 255, 255, 0.05);
+}
+.link-panel-anchor {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: 11px;
+ word-break: break-all;
+ padding: 6px 8px;
+ background: rgba(0, 0, 0, 0.04);
+ border-radius: 4px;
+ color: var(--ant-color-primary, #1677ff);
+ text-decoration: underline;
+ text-decoration-color: rgba(22, 119, 255, 0.4);
+ transition: background 120ms ease, text-decoration-color 120ms ease;
+}
+.link-panel-anchor:hover {
+ background: rgba(22, 119, 255, 0.08);
+ text-decoration-color: var(--ant-color-primary, #1677ff);
+}
+:global(body.dark) .link-panel-anchor {
+ background: rgba(255, 255, 255, 0.05);
+}
+:global(body.dark) .link-panel-anchor:hover {
+ background: rgba(22, 119, 255, 0.16);
+}
diff --git a/frontend/src/pages/inbounds/InboundList.vue b/frontend/src/pages/inbounds/InboundList.vue
index fc2364ea..e2b16a7b 100644
--- a/frontend/src/pages/inbounds/InboundList.vue
+++ b/frontend/src/pages/inbounds/InboundList.vue
@@ -152,10 +152,10 @@ const desktopColumns = computed(() => [
{ title: t('pages.inbounds.enable'), key: 'enable', align: 'center', width: 35 },
{ title: t('pages.inbounds.remark'), dataIndex: 'remark', key: 'remark', align: 'center', width: 60 },
{ title: t('pages.inbounds.port'), dataIndex: 'port', key: 'port', align: 'center', width: 40 },
- { title: t('pages.inbounds.protocol'), key: 'protocol', align: 'left', width: 70 },
+ { title: t('pages.inbounds.protocol'), key: 'protocol', align: 'left', width: 130 },
{ title: t('clients'), key: 'clients', align: 'left', width: 50 },
{ title: t('pages.inbounds.traffic'), key: 'traffic', align: 'center', width: 90 },
- { title: t('pages.inbounds.allTimeTraffic'), key: 'allTimeInbound', align: 'center', width: 60 },
+ { title: t('pages.inbounds.allTimeTraffic'), key: 'allTimeInbound', align: 'center', width: 95 },
{ title: t('pages.inbounds.expireDate'), key: 'expiryTime', align: 'center', width: 40 },
]);
const mobileColumns = computed(() => [
diff --git a/frontend/src/pages/inbounds/InboundsPage.vue b/frontend/src/pages/inbounds/InboundsPage.vue
index 59eedd29..e5a063d6 100644
--- a/frontend/src/pages/inbounds/InboundsPage.vue
+++ b/frontend/src/pages/inbounds/InboundsPage.vue
@@ -77,6 +77,7 @@ const infoClientIndex = ref(0);
const qrOpen = ref(false);
const qrDbInbound = ref(null);
+const qrClient = ref(null);
// === Shared text + prompt modal state =================================
const textOpen = ref(false);
@@ -266,11 +267,9 @@ function onEditClient({ dbInbound, client }) {
}
function onQrcodeClient({ dbInbound, client }) {
- // Reuse the inbound info modal focused on the chosen client — that's
- // where per-client share links and the per-link QRs live.
- infoDbInbound.value = checkFallback(dbInbound);
- infoClientIndex.value = findClientIndex(dbInbound, client);
- infoOpen.value = true;
+ qrDbInbound.value = checkFallback(dbInbound);
+ qrClient.value = client || null;
+ qrOpen.value = true;
}
function onInfoClient({ dbInbound, client }) {
@@ -464,6 +463,7 @@ function onRowAction({ key, dbInbound }) {
break;
case 'qrcode':
qrDbInbound.value = checkFallback(dbInbound);
+ qrClient.value = null;
qrOpen.value = true;
break;
case 'export':
@@ -645,6 +645,7 @@ function onRowAction({ key, dbInbound }) {
diff --git a/frontend/src/pages/inbounds/QrCodeModal.vue b/frontend/src/pages/inbounds/QrCodeModal.vue
index 1521e0b5..cb6cd215 100644
--- a/frontend/src/pages/inbounds/QrCodeModal.vue
+++ b/frontend/src/pages/inbounds/QrCodeModal.vue
@@ -15,6 +15,7 @@ const { t } = useI18n();
const props = defineProps({
open: { type: Boolean, default: false },
dbInbound: { type: Object, default: null },
+ client: { type: Object, default: null },
remarkModel: { type: String, default: '-ieo' },
});
@@ -28,13 +29,16 @@ watch(() => props.open, (next) => {
if (!next || !props.dbInbound) return;
const inbound = props.dbInbound.toInbound();
if (inbound.protocol === Protocols.WIREGUARD) {
- wireguardConfigs.value = inbound.genWireguardConfigs(props.dbInbound.remark).split('\r\n');
- wireguardLinks.value = inbound.genWireguardLinks(props.dbInbound.remark).split('\r\n');
+ const peerRemark = props.client?.email
+ ? `${props.dbInbound.remark}-${props.client.email}`
+ : props.dbInbound.remark;
+ wireguardConfigs.value = inbound.genWireguardConfigs(peerRemark).split('\r\n');
+ wireguardLinks.value = inbound.genWireguardLinks(peerRemark).split('\r\n');
links.value = [];
} else {
- // SS single-user — pass null client; genAllLinks falls back to
- // the inbound's settings.
- links.value = inbound.genAllLinks(props.dbInbound.remark, props.remarkModel, null);
+ // When a client is provided we generate per-client share links;
+ // otherwise (single-user SS) fall back to the inbound's settings.
+ links.value = inbound.genAllLinks(props.dbInbound.remark, props.remarkModel, props.client);
wireguardConfigs.value = [];
wireguardLinks.value = [];
}