feat(inbounds): forwarding chip on list rows
Rows whose stored ForwardingSpec is enabled get a volcano chip after the protocol tags: '转发 → <target remark>' for managed landings (resolved against the full row set), '转发 → <address>:<port>' for manual endpoints, '转发 → SK5' when the target row is gone. Text is ellipsised at 180px with a tooltip so narrow rows never overflow. Data rides the slim list's existing forwarding field — no API change.
This commit is contained in:
@@ -180,3 +180,11 @@
|
|||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.forwarding-tag {
|
||||||
|
max-width: 180px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|||||||
@@ -137,11 +137,17 @@ export default function InboundList({
|
|||||||
if (ok) setSelectedRowKeys([]);
|
if (ok) setSelectedRowKeys([]);
|
||||||
}, [onBulkDelete, selectedRowKeys]);
|
}, [onBulkDelete, selectedRowKeys]);
|
||||||
|
|
||||||
|
const inboundsById = useMemo(
|
||||||
|
() => new Map(dbInbounds.map((ib) => [ib.id, ib] as const)),
|
||||||
|
[dbInbounds],
|
||||||
|
);
|
||||||
|
|
||||||
const columns = useInboundColumns({
|
const columns = useInboundColumns({
|
||||||
hasAnyRemark,
|
hasAnyRemark,
|
||||||
hasAnySubSortIndex,
|
hasAnySubSortIndex,
|
||||||
hasActiveNode,
|
hasActiveNode,
|
||||||
nodesById,
|
nodesById,
|
||||||
|
inboundsById,
|
||||||
clientCount,
|
clientCount,
|
||||||
inboundSpeed,
|
inboundSpeed,
|
||||||
subEnable,
|
subEnable,
|
||||||
|
|||||||
@@ -104,3 +104,26 @@ export function showQrCodeMenu(dbInbound: DBInboundRecord): boolean {
|
|||||||
}
|
}
|
||||||
return false;
|
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<number, DBInboundRecord>,
|
||||||
|
): 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';
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,8 +32,10 @@ export interface DBInboundRecord extends ProtocolFlags {
|
|||||||
expiryTime: number;
|
expiryTime: number;
|
||||||
_expiryTime: { valueOf(): number } | null;
|
_expiryTime: { valueOf(): number } | null;
|
||||||
nodeId?: number | null;
|
nodeId?: number | null;
|
||||||
|
tag?: string;
|
||||||
settings: unknown;
|
settings: unknown;
|
||||||
streamSettings: unknown;
|
streamSettings: unknown;
|
||||||
|
forwarding?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientCountEntry {
|
export interface ClientCountEntry {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
} from '@/components/utility/speedTagStyle';
|
} from '@/components/utility/speedTagStyle';
|
||||||
import { InboundSpeedTag, isActiveSpeed } from './InboundSpeedTag';
|
import { InboundSpeedTag, isActiveSpeed } from './InboundSpeedTag';
|
||||||
import {
|
import {
|
||||||
|
forwardingLandingLabel,
|
||||||
readStreamHints,
|
readStreamHints,
|
||||||
networkLabel,
|
networkLabel,
|
||||||
networkL4,
|
networkL4,
|
||||||
@@ -31,6 +32,7 @@ interface UseInboundColumnsParams {
|
|||||||
hasAnySubSortIndex: boolean;
|
hasAnySubSortIndex: boolean;
|
||||||
hasActiveNode: boolean;
|
hasActiveNode: boolean;
|
||||||
nodesById: Map<number, NodeRecord>;
|
nodesById: Map<number, NodeRecord>;
|
||||||
|
inboundsById: Map<number, DBInboundRecord>;
|
||||||
clientCount: Record<number, ClientCountEntry>;
|
clientCount: Record<number, ClientCountEntry>;
|
||||||
inboundSpeed: Record<number, InboundSpeedEntry>;
|
inboundSpeed: Record<number, InboundSpeedEntry>;
|
||||||
subEnable: boolean;
|
subEnable: boolean;
|
||||||
@@ -45,6 +47,7 @@ export function useInboundColumns({
|
|||||||
hasAnySubSortIndex,
|
hasAnySubSortIndex,
|
||||||
hasActiveNode,
|
hasActiveNode,
|
||||||
nodesById,
|
nodesById,
|
||||||
|
inboundsById,
|
||||||
clientCount,
|
clientCount,
|
||||||
inboundSpeed,
|
inboundSpeed,
|
||||||
subEnable,
|
subEnable,
|
||||||
@@ -257,6 +260,17 @@ export function useInboundColumns({
|
|||||||
</Tag>,
|
</Tag>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const fwdLabel = forwardingLandingLabel(record, inboundsById);
|
||||||
|
if (fwdLabel) {
|
||||||
|
const text = `${t('pages.inbounds.forwardingTab')} → ${fwdLabel}`;
|
||||||
|
tags.push(
|
||||||
|
<Tooltip key="fwd" title={text}>
|
||||||
|
<Tag color="volcano" className="forwarding-tag">
|
||||||
|
{text}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>,
|
||||||
|
);
|
||||||
|
}
|
||||||
return <div className="protocol-tags">{tags}</div>;
|
return <div className="protocol-tags">{tags}</div>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -453,6 +467,7 @@ export function useInboundColumns({
|
|||||||
hasAnySubSortIndex,
|
hasAnySubSortIndex,
|
||||||
hasActiveNode,
|
hasActiveNode,
|
||||||
nodesById,
|
nodesById,
|
||||||
|
inboundsById,
|
||||||
clientCount,
|
clientCount,
|
||||||
inboundSpeed,
|
inboundSpeed,
|
||||||
subEnable,
|
subEnable,
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
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>): 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user