diff --git a/frontend/src/pages/inbounds/list/InboundList.css b/frontend/src/pages/inbounds/list/InboundList.css index 16f2db9..6edc3e8 100644 --- a/frontend/src/pages/inbounds/list/InboundList.css +++ b/frontend/src/pages/inbounds/list/InboundList.css @@ -180,3 +180,11 @@ padding: 4px; } } + +.forwarding-tag { + max-width: 180px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: top; +} diff --git a/frontend/src/pages/inbounds/list/InboundList.tsx b/frontend/src/pages/inbounds/list/InboundList.tsx index 0f34fc8..e9106b7 100644 --- a/frontend/src/pages/inbounds/list/InboundList.tsx +++ b/frontend/src/pages/inbounds/list/InboundList.tsx @@ -137,11 +137,17 @@ export default function InboundList({ if (ok) setSelectedRowKeys([]); }, [onBulkDelete, selectedRowKeys]); + const inboundsById = useMemo( + () => new Map(dbInbounds.map((ib) => [ib.id, ib] as const)), + [dbInbounds], + ); + const columns = useInboundColumns({ hasAnyRemark, hasAnySubSortIndex, hasActiveNode, nodesById, + inboundsById, clientCount, inboundSpeed, subEnable, diff --git a/frontend/src/pages/inbounds/list/helpers.ts b/frontend/src/pages/inbounds/list/helpers.ts index dd35959..7526593 100644 --- a/frontend/src/pages/inbounds/list/helpers.ts +++ b/frontend/src/pages/inbounds/list/helpers.ts @@ -104,3 +104,26 @@ export function showQrCodeMenu(dbInbound: DBInboundRecord): boolean { } return false; } + +// Landing label for the list-row forwarding chip: the managed target's +// remark (falling back to its tag), or the manual endpoint address:port. +// Returns null when the inbound does not forward, so the caller renders +// no chip. The stored spec rides the row as a JSON string. +export function forwardingLandingLabel( + record: DBInboundRecord, + inboundsById: Map, +): string | null { + const spec = coerceInboundJsonField(record.forwarding) as { + mode?: string; + landing?: string; + targetInboundId?: number; + address?: string; + port?: number; + }; + if (spec.mode !== 'forward') return null; + if (spec.landing === 'manual') { + return spec.address ? `${spec.address}:${spec.port ?? 0}` : 'SK5'; + } + const target = spec.targetInboundId ? inboundsById.get(spec.targetInboundId) : undefined; + return target?.remark || target?.tag || 'SK5'; +} diff --git a/frontend/src/pages/inbounds/list/types.ts b/frontend/src/pages/inbounds/list/types.ts index 1ca44c3..ededf6a 100644 --- a/frontend/src/pages/inbounds/list/types.ts +++ b/frontend/src/pages/inbounds/list/types.ts @@ -32,8 +32,10 @@ export interface DBInboundRecord extends ProtocolFlags { expiryTime: number; _expiryTime: { valueOf(): number } | null; nodeId?: number | null; + tag?: string; settings: unknown; streamSettings: unknown; + forwarding?: unknown; } export interface ClientCountEntry { diff --git a/frontend/src/pages/inbounds/list/useInboundColumns.tsx b/frontend/src/pages/inbounds/list/useInboundColumns.tsx index bfefec7..9ed976f 100644 --- a/frontend/src/pages/inbounds/list/useInboundColumns.tsx +++ b/frontend/src/pages/inbounds/list/useInboundColumns.tsx @@ -17,6 +17,7 @@ import { } from '@/components/utility/speedTagStyle'; import { InboundSpeedTag, isActiveSpeed } from './InboundSpeedTag'; import { + forwardingLandingLabel, readStreamHints, networkLabel, networkL4, @@ -31,6 +32,7 @@ interface UseInboundColumnsParams { hasAnySubSortIndex: boolean; hasActiveNode: boolean; nodesById: Map; + inboundsById: Map; clientCount: Record; inboundSpeed: Record; subEnable: boolean; @@ -45,6 +47,7 @@ export function useInboundColumns({ hasAnySubSortIndex, hasActiveNode, nodesById, + inboundsById, clientCount, inboundSpeed, subEnable, @@ -257,6 +260,17 @@ export function useInboundColumns({ , ); } + const fwdLabel = forwardingLandingLabel(record, inboundsById); + if (fwdLabel) { + const text = `${t('pages.inbounds.forwardingTab')} → ${fwdLabel}`; + tags.push( + + + {text} + + , + ); + } return
{tags}
; }, }, @@ -453,6 +467,7 @@ export function useInboundColumns({ hasAnySubSortIndex, hasActiveNode, nodesById, + inboundsById, clientCount, inboundSpeed, subEnable, diff --git a/frontend/src/test/forwarding-chip.test.ts b/frontend/src/test/forwarding-chip.test.ts new file mode 100644 index 0000000..0ec5b11 --- /dev/null +++ b/frontend/src/test/forwarding-chip.test.ts @@ -0,0 +1,64 @@ +/// +import { describe, expect, it } from 'vitest'; + +import { forwardingLandingLabel } from '@/pages/inbounds/list/helpers'; +import type { DBInboundRecord } from '@/pages/inbounds/list/types'; + +function rec(partial: Partial): DBInboundRecord { + return { + id: 1, + enable: true, + remark: '', + subSortIndex: 1, + port: 0, + protocol: 'vless', + up: 0, + down: 0, + total: 0, + expiryTime: 0, + _expiryTime: null, + settings: '', + streamSettings: '', + ...partial, + } as DBInboundRecord; +} + +const TARGET = rec({ id: 42, remark: 'PPF-L3-sk5', tag: 'inbound-47814', protocol: 'mixed' }); +const BY_ID = new Map([[TARGET.id, TARGET]]); + +describe('forwardingLandingLabel (list-row chip)', () => { + it('managed landing resolves the target remark', () => { + const row = rec({ + forwarding: JSON.stringify({ mode: 'forward', landing: 'managed', targetInboundId: 42 }), + }); + expect(forwardingLandingLabel(row, BY_ID)).toBe('PPF-L3-sk5'); + }); + + it('managed landing falls back to SK5 when the target row is gone', () => { + const row = rec({ + forwarding: JSON.stringify({ mode: 'forward', landing: 'managed', targetInboundId: 99 }), + }); + expect(forwardingLandingLabel(row, BY_ID)).toBe('SK5'); + }); + + it('manual landing shows address:port', () => { + const row = rec({ + forwarding: JSON.stringify({ + mode: 'forward', + landing: 'manual', + address: '10.240.64.2', + port: 47814, + }), + }); + expect(forwardingLandingLabel(row, BY_ID)).toBe('10.240.64.2:47814'); + }); + + it('direct / empty / malformed specs render no chip', () => { + expect(forwardingLandingLabel(rec({ forwarding: '' }), BY_ID)).toBeNull(); + expect( + forwardingLandingLabel(rec({ forwarding: JSON.stringify({ mode: 'direct' }) }), BY_ID), + ).toBeNull(); + expect(forwardingLandingLabel(rec({ forwarding: '{oops' }), BY_ID)).toBeNull(); + expect(forwardingLandingLabel(rec({}), BY_ID)).toBeNull(); + }); +});