mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
refactor(frontend): extract InboundInfoModal helpers, types & buildInboundInfo
InboundInfoModal.tsx 1081 -> 836 lines. Moved the pure data helpers (network host/path readers, link-protocol check, copy/download/statsColor/IP formatting) plus all shared types and the buildInboundInfo data builder into info/helpers.ts and info/types.ts. The state-coupled render body is left intact (no React render tests to guard a deeper split). Code moved verbatim; no behavior change. All gates green, 337 tests pass.
This commit is contained in:
parent
a32fe94872
commit
f116b09f7c
3 changed files with 268 additions and 261 deletions
|
|
@ -1,280 +1,30 @@
|
||||||
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Divider, Modal, Space, Tabs, Tag, Tooltip } from 'antd';
|
import { Button, Divider, Modal, Space, Tabs, Tag, Tooltip } from 'antd';
|
||||||
import { getMessage } from '@/utils/messageBus';
|
|
||||||
import { CopyOutlined, SyncOutlined, DeleteOutlined, DownloadOutlined } from '@ant-design/icons';
|
import { CopyOutlined, SyncOutlined, DeleteOutlined, DownloadOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
import {
|
import { HttpUtil, IntlUtil, SizeFormatter, ColorUtils } from '@/utils';
|
||||||
HttpUtil,
|
|
||||||
IntlUtil,
|
|
||||||
SizeFormatter,
|
|
||||||
ColorUtils,
|
|
||||||
ClipboardManager,
|
|
||||||
FileManager,
|
|
||||||
} from '@/utils';
|
|
||||||
import { Protocols } from '@/schemas/primitives';
|
import { Protocols } from '@/schemas/primitives';
|
||||||
import { InfinityIcon } from '@/components/ui';
|
import { InfinityIcon } from '@/components/ui';
|
||||||
import { useDatepicker } from '@/hooks/useDatepicker';
|
import { useDatepicker } from '@/hooks/useDatepicker';
|
||||||
import { coerceInboundJsonField } from '@/models/dbinbound';
|
|
||||||
import {
|
|
||||||
canEnableTlsFlow,
|
|
||||||
isSS2022 as isSS2022Helper,
|
|
||||||
isSSMultiUser as isSSMultiUserHelper,
|
|
||||||
} from '@/lib/xray/protocol-capabilities';
|
|
||||||
import {
|
import {
|
||||||
genAllLinks,
|
genAllLinks,
|
||||||
genWireguardConfigs,
|
genWireguardConfigs,
|
||||||
genWireguardLinks,
|
genWireguardLinks,
|
||||||
} from '@/lib/xray/inbound-link';
|
} from '@/lib/xray/inbound-link';
|
||||||
import { inboundFromDb } from '@/lib/xray/inbound-from-db';
|
import { inboundFromDb } from '@/lib/xray/inbound-from-db';
|
||||||
import type { SubSettings } from '../useInbounds';
|
|
||||||
|
import {
|
||||||
|
buildInboundInfo,
|
||||||
|
copyText,
|
||||||
|
downloadText,
|
||||||
|
formatIpInfo,
|
||||||
|
hasShareLink,
|
||||||
|
statsColor,
|
||||||
|
} from './helpers';
|
||||||
|
import type { ClientSetting, ClientStats, InboundInfo, InboundInfoModalProps } from './types';
|
||||||
import './InboundInfoModal.css';
|
import './InboundInfoModal.css';
|
||||||
|
|
||||||
const LINK_PROTOCOLS: ReadonlySet<string> = new Set([
|
|
||||||
Protocols.VMESS,
|
|
||||||
Protocols.VLESS,
|
|
||||||
Protocols.TROJAN,
|
|
||||||
Protocols.SHADOWSOCKS,
|
|
||||||
Protocols.HYSTERIA,
|
|
||||||
]);
|
|
||||||
|
|
||||||
function hasShareLink(protocol: string): boolean {
|
|
||||||
return LINK_PROTOCOLS.has(protocol);
|
|
||||||
}
|
|
||||||
|
|
||||||
function readHeader(headers: unknown, name: string): string {
|
|
||||||
const needle = name.toLowerCase();
|
|
||||||
if (Array.isArray(headers)) {
|
|
||||||
for (const h of headers) {
|
|
||||||
if (h && typeof h === 'object' && String((h as { name?: string }).name ?? '').toLowerCase() === needle) {
|
|
||||||
return String((h as { value?: unknown }).value ?? '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
if (headers && typeof headers === 'object') {
|
|
||||||
for (const [k, v] of Object.entries(headers as Record<string, unknown>)) {
|
|
||||||
if (k.toLowerCase() === needle) {
|
|
||||||
return Array.isArray(v) ? String(v[0] ?? '') : String(v ?? '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function readNetworkHost(stream: Record<string, unknown>, network: string): string | null {
|
|
||||||
switch (network) {
|
|
||||||
case 'tcp': {
|
|
||||||
const tcp = stream.tcpSettings as { header?: { request?: { headers?: unknown } } } | undefined;
|
|
||||||
return readHeader(tcp?.header?.request?.headers, 'host');
|
|
||||||
}
|
|
||||||
case 'ws': {
|
|
||||||
const ws = stream.wsSettings as { host?: string; headers?: unknown } | undefined;
|
|
||||||
return (ws?.host && ws.host.length > 0) ? ws.host : readHeader(ws?.headers, 'host');
|
|
||||||
}
|
|
||||||
case 'httpupgrade': {
|
|
||||||
const hu = stream.httpupgradeSettings as { host?: string; headers?: unknown } | undefined;
|
|
||||||
return (hu?.host && hu.host.length > 0) ? hu.host : readHeader(hu?.headers, 'host');
|
|
||||||
}
|
|
||||||
case 'xhttp': {
|
|
||||||
const xh = stream.xhttpSettings as { host?: string; headers?: unknown } | undefined;
|
|
||||||
return (xh?.host && xh.host.length > 0) ? xh.host : readHeader(xh?.headers, 'host');
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function readNetworkPath(stream: Record<string, unknown>, network: string): string | null {
|
|
||||||
switch (network) {
|
|
||||||
case 'tcp': {
|
|
||||||
const tcp = stream.tcpSettings as { header?: { request?: { path?: string[] } } } | undefined;
|
|
||||||
return tcp?.header?.request?.path?.[0] ?? null;
|
|
||||||
}
|
|
||||||
case 'ws':
|
|
||||||
return (stream.wsSettings as { path?: string } | undefined)?.path ?? null;
|
|
||||||
case 'httpupgrade':
|
|
||||||
return (stream.httpupgradeSettings as { path?: string } | undefined)?.path ?? null;
|
|
||||||
case 'xhttp':
|
|
||||||
return (stream.xhttpSettings as { path?: string } | undefined)?.path ?? null;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ClientStats {
|
|
||||||
email: string;
|
|
||||||
up: number;
|
|
||||||
down: number;
|
|
||||||
total: number;
|
|
||||||
expiryTime: number;
|
|
||||||
enable?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ClientSetting {
|
|
||||||
email?: string;
|
|
||||||
id?: string;
|
|
||||||
security?: string;
|
|
||||||
password?: string;
|
|
||||||
flow?: string;
|
|
||||||
subId?: string;
|
|
||||||
totalGB?: number;
|
|
||||||
expiryTime?: number;
|
|
||||||
comment?: string;
|
|
||||||
tgId?: string;
|
|
||||||
enable?: boolean;
|
|
||||||
limitIp?: number;
|
|
||||||
created_at?: number;
|
|
||||||
updated_at?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InboundInfo {
|
|
||||||
protocol: string;
|
|
||||||
clients: ClientSetting[];
|
|
||||||
settings: Record<string, unknown>;
|
|
||||||
isTcp: boolean;
|
|
||||||
isWs: boolean;
|
|
||||||
isHttpupgrade: boolean;
|
|
||||||
isXHTTP: boolean;
|
|
||||||
isGrpc: boolean;
|
|
||||||
isSSMultiUser: boolean;
|
|
||||||
isSS2022: boolean;
|
|
||||||
isVlessTlsFlow: boolean;
|
|
||||||
host: string | null;
|
|
||||||
path: string | null;
|
|
||||||
serviceName: string;
|
|
||||||
serverName: string;
|
|
||||||
stream: {
|
|
||||||
network: string;
|
|
||||||
security: string;
|
|
||||||
xhttp?: { mode?: string };
|
|
||||||
grpc?: { multiMode?: boolean };
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DBInboundLike {
|
|
||||||
id: number;
|
|
||||||
address: string;
|
|
||||||
port: number;
|
|
||||||
listen: string;
|
|
||||||
protocol: string;
|
|
||||||
remark: string;
|
|
||||||
enable?: boolean;
|
|
||||||
isVMess?: boolean;
|
|
||||||
isVLess?: boolean;
|
|
||||||
isTrojan?: boolean;
|
|
||||||
isSS?: boolean;
|
|
||||||
isMixed?: boolean;
|
|
||||||
isHTTP?: boolean;
|
|
||||||
isWireguard?: boolean;
|
|
||||||
settings: unknown;
|
|
||||||
streamSettings: unknown;
|
|
||||||
sniffing: unknown;
|
|
||||||
clientStats?: ClientStats[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildInboundInfo(dbInbound: DBInboundLike): InboundInfo {
|
|
||||||
const settings = coerceInboundJsonField(dbInbound.settings) as Record<string, unknown>;
|
|
||||||
const stream = coerceInboundJsonField(dbInbound.streamSettings) as Record<string, unknown>;
|
|
||||||
const network = (stream.network as string | undefined) ?? '';
|
|
||||||
const security = (stream.security as string | undefined) ?? 'none';
|
|
||||||
const clients = Array.isArray(settings.clients) ? (settings.clients as ClientSetting[]) : [];
|
|
||||||
const xhttpSettings = stream.xhttpSettings as { mode?: string } | undefined;
|
|
||||||
const grpcSettings = stream.grpcSettings as { multiMode?: boolean; serviceName?: string } | undefined;
|
|
||||||
let serverName = '';
|
|
||||||
if (security === 'tls') {
|
|
||||||
const tls = stream.tlsSettings as { sni?: string; serverName?: string } | undefined;
|
|
||||||
serverName = tls?.sni ?? tls?.serverName ?? '';
|
|
||||||
} else if (security === 'reality') {
|
|
||||||
const reality = stream.realitySettings as { serverNames?: string[]; serverName?: string } | undefined;
|
|
||||||
if (Array.isArray(reality?.serverNames)) {
|
|
||||||
serverName = reality.serverNames.join(', ');
|
|
||||||
} else if (reality?.serverName) {
|
|
||||||
serverName = reality.serverName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
protocol: dbInbound.protocol,
|
|
||||||
clients,
|
|
||||||
settings,
|
|
||||||
isTcp: network === 'tcp',
|
|
||||||
isWs: network === 'ws',
|
|
||||||
isHttpupgrade: network === 'httpupgrade',
|
|
||||||
isXHTTP: network === 'xhttp',
|
|
||||||
isGrpc: network === 'grpc',
|
|
||||||
isSSMultiUser: isSSMultiUserHelper({
|
|
||||||
protocol: dbInbound.protocol,
|
|
||||||
settings: settings as { method?: string },
|
|
||||||
}),
|
|
||||||
isSS2022: isSS2022Helper({
|
|
||||||
protocol: dbInbound.protocol,
|
|
||||||
settings: settings as { method?: string },
|
|
||||||
}),
|
|
||||||
isVlessTlsFlow: canEnableTlsFlow({
|
|
||||||
protocol: dbInbound.protocol,
|
|
||||||
streamSettings: { network, security },
|
|
||||||
}),
|
|
||||||
host: readNetworkHost(stream, network),
|
|
||||||
path: readNetworkPath(stream, network),
|
|
||||||
serviceName: grpcSettings?.serviceName ?? '',
|
|
||||||
serverName,
|
|
||||||
stream: {
|
|
||||||
network,
|
|
||||||
security,
|
|
||||||
xhttp: xhttpSettings ? { mode: xhttpSettings.mode } : undefined,
|
|
||||||
grpc: grpcSettings ? { multiMode: grpcSettings.multiMode } : undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InboundInfoModalProps {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
dbInbound: DBInboundLike | null;
|
|
||||||
clientIndex?: number;
|
|
||||||
remarkModel?: string;
|
|
||||||
expireDiff?: number;
|
|
||||||
trafficDiff?: number;
|
|
||||||
ipLimitEnable?: boolean;
|
|
||||||
tgBotEnable?: boolean;
|
|
||||||
nodeAddress?: string;
|
|
||||||
subSettings?: SubSettings;
|
|
||||||
lastOnlineMap?: Record<string, number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyText(value: unknown, t: (k: string) => string) {
|
|
||||||
ClipboardManager.copyText(String(value ?? '')).then((ok) => {
|
|
||||||
if (ok) getMessage().success(t('copied'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadText(content: string, filename: string) {
|
|
||||||
FileManager.downloadTextFile(content, filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
function statsColor(stats: ClientStats, trafficDiff: number) {
|
|
||||||
return ColorUtils.usageColor(stats.up + stats.down, trafficDiff, stats.total);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatIpInfo(record: unknown) {
|
|
||||||
if (record == null) return '';
|
|
||||||
if (typeof record === 'string' || typeof record === 'number') return String(record);
|
|
||||||
const r = record as { ip?: string; IP?: string; timestamp?: number | string; Timestamp?: number | string };
|
|
||||||
const ip = r.ip || r.IP || '';
|
|
||||||
const ts = r.timestamp || r.Timestamp || 0;
|
|
||||||
if (!ip) return String(record);
|
|
||||||
if (!ts) return String(ip);
|
|
||||||
const date = new Date(Number(ts) * 1000);
|
|
||||||
const timeStr = date
|
|
||||||
.toLocaleString('en-GB', {
|
|
||||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
||||||
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
|
||||||
hour12: false,
|
|
||||||
})
|
|
||||||
.replace(',', '');
|
|
||||||
return `${ip} (${timeStr})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function InboundInfoModal({
|
export default function InboundInfoModal({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
|
|
|
||||||
170
frontend/src/pages/inbounds/info/helpers.ts
Normal file
170
frontend/src/pages/inbounds/info/helpers.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
import { getMessage } from '@/utils/messageBus';
|
||||||
|
import { ColorUtils, ClipboardManager, FileManager } from '@/utils';
|
||||||
|
import { Protocols } from '@/schemas/primitives';
|
||||||
|
import { coerceInboundJsonField } from '@/models/dbinbound';
|
||||||
|
import {
|
||||||
|
canEnableTlsFlow,
|
||||||
|
isSS2022 as isSS2022Helper,
|
||||||
|
isSSMultiUser as isSSMultiUserHelper,
|
||||||
|
} from '@/lib/xray/protocol-capabilities';
|
||||||
|
|
||||||
|
import type { ClientSetting, ClientStats, DBInboundLike, InboundInfo } from './types';
|
||||||
|
|
||||||
|
const LINK_PROTOCOLS: ReadonlySet<string> = new Set([
|
||||||
|
Protocols.VMESS,
|
||||||
|
Protocols.VLESS,
|
||||||
|
Protocols.TROJAN,
|
||||||
|
Protocols.SHADOWSOCKS,
|
||||||
|
Protocols.HYSTERIA,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function hasShareLink(protocol: string): boolean {
|
||||||
|
return LINK_PROTOCOLS.has(protocol);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readHeader(headers: unknown, name: string): string {
|
||||||
|
const needle = name.toLowerCase();
|
||||||
|
if (Array.isArray(headers)) {
|
||||||
|
for (const h of headers) {
|
||||||
|
if (h && typeof h === 'object' && String((h as { name?: string }).name ?? '').toLowerCase() === needle) {
|
||||||
|
return String((h as { value?: unknown }).value ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (headers && typeof headers === 'object') {
|
||||||
|
for (const [k, v] of Object.entries(headers as Record<string, unknown>)) {
|
||||||
|
if (k.toLowerCase() === needle) {
|
||||||
|
return Array.isArray(v) ? String(v[0] ?? '') : String(v ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function readNetworkHost(stream: Record<string, unknown>, network: string): string | null {
|
||||||
|
switch (network) {
|
||||||
|
case 'tcp': {
|
||||||
|
const tcp = stream.tcpSettings as { header?: { request?: { headers?: unknown } } } | undefined;
|
||||||
|
return readHeader(tcp?.header?.request?.headers, 'host');
|
||||||
|
}
|
||||||
|
case 'ws': {
|
||||||
|
const ws = stream.wsSettings as { host?: string; headers?: unknown } | undefined;
|
||||||
|
return (ws?.host && ws.host.length > 0) ? ws.host : readHeader(ws?.headers, 'host');
|
||||||
|
}
|
||||||
|
case 'httpupgrade': {
|
||||||
|
const hu = stream.httpupgradeSettings as { host?: string; headers?: unknown } | undefined;
|
||||||
|
return (hu?.host && hu.host.length > 0) ? hu.host : readHeader(hu?.headers, 'host');
|
||||||
|
}
|
||||||
|
case 'xhttp': {
|
||||||
|
const xh = stream.xhttpSettings as { host?: string; headers?: unknown } | undefined;
|
||||||
|
return (xh?.host && xh.host.length > 0) ? xh.host : readHeader(xh?.headers, 'host');
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readNetworkPath(stream: Record<string, unknown>, network: string): string | null {
|
||||||
|
switch (network) {
|
||||||
|
case 'tcp': {
|
||||||
|
const tcp = stream.tcpSettings as { header?: { request?: { path?: string[] } } } | undefined;
|
||||||
|
return tcp?.header?.request?.path?.[0] ?? null;
|
||||||
|
}
|
||||||
|
case 'ws':
|
||||||
|
return (stream.wsSettings as { path?: string } | undefined)?.path ?? null;
|
||||||
|
case 'httpupgrade':
|
||||||
|
return (stream.httpupgradeSettings as { path?: string } | undefined)?.path ?? null;
|
||||||
|
case 'xhttp':
|
||||||
|
return (stream.xhttpSettings as { path?: string } | undefined)?.path ?? null;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildInboundInfo(dbInbound: DBInboundLike): InboundInfo {
|
||||||
|
const settings = coerceInboundJsonField(dbInbound.settings) as Record<string, unknown>;
|
||||||
|
const stream = coerceInboundJsonField(dbInbound.streamSettings) as Record<string, unknown>;
|
||||||
|
const network = (stream.network as string | undefined) ?? '';
|
||||||
|
const security = (stream.security as string | undefined) ?? 'none';
|
||||||
|
const clients = Array.isArray(settings.clients) ? (settings.clients as ClientSetting[]) : [];
|
||||||
|
const xhttpSettings = stream.xhttpSettings as { mode?: string } | undefined;
|
||||||
|
const grpcSettings = stream.grpcSettings as { multiMode?: boolean; serviceName?: string } | undefined;
|
||||||
|
let serverName = '';
|
||||||
|
if (security === 'tls') {
|
||||||
|
const tls = stream.tlsSettings as { sni?: string; serverName?: string } | undefined;
|
||||||
|
serverName = tls?.sni ?? tls?.serverName ?? '';
|
||||||
|
} else if (security === 'reality') {
|
||||||
|
const reality = stream.realitySettings as { serverNames?: string[]; serverName?: string } | undefined;
|
||||||
|
if (Array.isArray(reality?.serverNames)) {
|
||||||
|
serverName = reality.serverNames.join(', ');
|
||||||
|
} else if (reality?.serverName) {
|
||||||
|
serverName = reality.serverName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
protocol: dbInbound.protocol,
|
||||||
|
clients,
|
||||||
|
settings,
|
||||||
|
isTcp: network === 'tcp',
|
||||||
|
isWs: network === 'ws',
|
||||||
|
isHttpupgrade: network === 'httpupgrade',
|
||||||
|
isXHTTP: network === 'xhttp',
|
||||||
|
isGrpc: network === 'grpc',
|
||||||
|
isSSMultiUser: isSSMultiUserHelper({
|
||||||
|
protocol: dbInbound.protocol,
|
||||||
|
settings: settings as { method?: string },
|
||||||
|
}),
|
||||||
|
isSS2022: isSS2022Helper({
|
||||||
|
protocol: dbInbound.protocol,
|
||||||
|
settings: settings as { method?: string },
|
||||||
|
}),
|
||||||
|
isVlessTlsFlow: canEnableTlsFlow({
|
||||||
|
protocol: dbInbound.protocol,
|
||||||
|
streamSettings: { network, security },
|
||||||
|
}),
|
||||||
|
host: readNetworkHost(stream, network),
|
||||||
|
path: readNetworkPath(stream, network),
|
||||||
|
serviceName: grpcSettings?.serviceName ?? '',
|
||||||
|
serverName,
|
||||||
|
stream: {
|
||||||
|
network,
|
||||||
|
security,
|
||||||
|
xhttp: xhttpSettings ? { mode: xhttpSettings.mode } : undefined,
|
||||||
|
grpc: grpcSettings ? { multiMode: grpcSettings.multiMode } : undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function copyText(value: unknown, t: (k: string) => string) {
|
||||||
|
ClipboardManager.copyText(String(value ?? '')).then((ok) => {
|
||||||
|
if (ok) getMessage().success(t('copied'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadText(content: string, filename: string) {
|
||||||
|
FileManager.downloadTextFile(content, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function statsColor(stats: ClientStats, trafficDiff: number) {
|
||||||
|
return ColorUtils.usageColor(stats.up + stats.down, trafficDiff, stats.total);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatIpInfo(record: unknown) {
|
||||||
|
if (record == null) return '';
|
||||||
|
if (typeof record === 'string' || typeof record === 'number') return String(record);
|
||||||
|
const r = record as { ip?: string; IP?: string; timestamp?: number | string; Timestamp?: number | string };
|
||||||
|
const ip = r.ip || r.IP || '';
|
||||||
|
const ts = r.timestamp || r.Timestamp || 0;
|
||||||
|
if (!ip) return String(record);
|
||||||
|
if (!ts) return String(ip);
|
||||||
|
const date = new Date(Number(ts) * 1000);
|
||||||
|
const timeStr = date
|
||||||
|
.toLocaleString('en-GB', {
|
||||||
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||||
|
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
})
|
||||||
|
.replace(',', '');
|
||||||
|
return `${ip} (${timeStr})`;
|
||||||
|
}
|
||||||
87
frontend/src/pages/inbounds/info/types.ts
Normal file
87
frontend/src/pages/inbounds/info/types.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
import type { SubSettings } from '../useInbounds';
|
||||||
|
|
||||||
|
export interface ClientStats {
|
||||||
|
email: string;
|
||||||
|
up: number;
|
||||||
|
down: number;
|
||||||
|
total: number;
|
||||||
|
expiryTime: number;
|
||||||
|
enable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClientSetting {
|
||||||
|
email?: string;
|
||||||
|
id?: string;
|
||||||
|
security?: string;
|
||||||
|
password?: string;
|
||||||
|
flow?: string;
|
||||||
|
subId?: string;
|
||||||
|
totalGB?: number;
|
||||||
|
expiryTime?: number;
|
||||||
|
comment?: string;
|
||||||
|
tgId?: string;
|
||||||
|
enable?: boolean;
|
||||||
|
limitIp?: number;
|
||||||
|
created_at?: number;
|
||||||
|
updated_at?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InboundInfo {
|
||||||
|
protocol: string;
|
||||||
|
clients: ClientSetting[];
|
||||||
|
settings: Record<string, unknown>;
|
||||||
|
isTcp: boolean;
|
||||||
|
isWs: boolean;
|
||||||
|
isHttpupgrade: boolean;
|
||||||
|
isXHTTP: boolean;
|
||||||
|
isGrpc: boolean;
|
||||||
|
isSSMultiUser: boolean;
|
||||||
|
isSS2022: boolean;
|
||||||
|
isVlessTlsFlow: boolean;
|
||||||
|
host: string | null;
|
||||||
|
path: string | null;
|
||||||
|
serviceName: string;
|
||||||
|
serverName: string;
|
||||||
|
stream: {
|
||||||
|
network: string;
|
||||||
|
security: string;
|
||||||
|
xhttp?: { mode?: string };
|
||||||
|
grpc?: { multiMode?: boolean };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DBInboundLike {
|
||||||
|
id: number;
|
||||||
|
address: string;
|
||||||
|
port: number;
|
||||||
|
listen: string;
|
||||||
|
protocol: string;
|
||||||
|
remark: string;
|
||||||
|
enable?: boolean;
|
||||||
|
isVMess?: boolean;
|
||||||
|
isVLess?: boolean;
|
||||||
|
isTrojan?: boolean;
|
||||||
|
isSS?: boolean;
|
||||||
|
isMixed?: boolean;
|
||||||
|
isHTTP?: boolean;
|
||||||
|
isWireguard?: boolean;
|
||||||
|
settings: unknown;
|
||||||
|
streamSettings: unknown;
|
||||||
|
sniffing: unknown;
|
||||||
|
clientStats?: ClientStats[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InboundInfoModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
dbInbound: DBInboundLike | null;
|
||||||
|
clientIndex?: number;
|
||||||
|
remarkModel?: string;
|
||||||
|
expireDiff?: number;
|
||||||
|
trafficDiff?: number;
|
||||||
|
ipLimitEnable?: boolean;
|
||||||
|
tgBotEnable?: boolean;
|
||||||
|
nodeAddress?: string;
|
||||||
|
subSettings?: SubSettings;
|
||||||
|
lastOnlineMap?: Record<string, number>;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue