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;
|
||||
}
|
||||
}
|
||||
|
||||
.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([]);
|
||||
}, [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,
|
||||
|
||||
@@ -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<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: { valueOf(): number } | null;
|
||||
nodeId?: number | null;
|
||||
tag?: string;
|
||||
settings: unknown;
|
||||
streamSettings: unknown;
|
||||
forwarding?: unknown;
|
||||
}
|
||||
|
||||
export interface ClientCountEntry {
|
||||
|
||||
@@ -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<number, NodeRecord>;
|
||||
inboundsById: Map<number, DBInboundRecord>;
|
||||
clientCount: Record<number, ClientCountEntry>;
|
||||
inboundSpeed: Record<number, InboundSpeedEntry>;
|
||||
subEnable: boolean;
|
||||
@@ -45,6 +47,7 @@ export function useInboundColumns({
|
||||
hasAnySubSortIndex,
|
||||
hasActiveNode,
|
||||
nodesById,
|
||||
inboundsById,
|
||||
clientCount,
|
||||
inboundSpeed,
|
||||
subEnable,
|
||||
@@ -257,6 +260,17 @@ export function useInboundColumns({
|
||||
</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>;
|
||||
},
|
||||
},
|
||||
@@ -453,6 +467,7 @@ export function useInboundColumns({
|
||||
hasAnySubSortIndex,
|
||||
hasActiveNode,
|
||||
nodesById,
|
||||
inboundsById,
|
||||
clientCount,
|
||||
inboundSpeed,
|
||||
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