mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-23 11:27:13 +00:00
feat(clients/inbounds): IP log popups, clearer titles, tag-based inbound labels
Add an IP Log popup (view list + refresh + clear) to the client edit form and the Client Information modal, with IPs stacked vertically. Identify inbounds by their xray tag (not remark/protocol:port) across every picker and chip: attach/detach modals, the attached-inbounds column and field, the filter drawer, and bulk-add. Add the tag field to the InboundOption schema (the backend already returned it). Clarify modal titles/labels: Client Information (was More Information) and Inbound Information (was Inbound's Data); Client Information / QR Code titles now include the client email. i18n: rename keys moreInformation->clientInfo and inboundData->inboundInfo with proper translations in all languages; addTitle->addClient, editTitle->editClient, addToGroupPlaceholder->groupName.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Divider, Modal, Popover, Tag, Tooltip, message } from 'antd';
|
||||
import { CopyOutlined, QrcodeOutlined } from '@ant-design/icons';
|
||||
import { CopyOutlined, EyeOutlined, QrcodeOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
|
||||
import { ClipboardManager, HttpUtil, IntlUtil, SizeFormatter } from '@/utils';
|
||||
import { useDatepicker } from '@/hooks/useDatepicker';
|
||||
@@ -145,10 +145,16 @@ export default function ClientInfoModal({
|
||||
const dateLabel = (ts?: number) => (!ts || ts <= 0 ? '-' : IntlUtil.formatDate(ts, datepicker));
|
||||
const [messageApi, messageContextHolder] = message.useMessage();
|
||||
const [links, setLinks] = useState<string[]>([]);
|
||||
const [clientIps, setClientIps] = useState<string[]>([]);
|
||||
const [ipsLoading, setIpsLoading] = useState(false);
|
||||
const [ipsClearing, setIpsClearing] = useState(false);
|
||||
const [ipsModalOpen, setIpsModalOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setLinks([]);
|
||||
setClientIps([]);
|
||||
setIpsModalOpen(false);
|
||||
return;
|
||||
}
|
||||
if (!client?.subId) return;
|
||||
@@ -197,12 +203,41 @@ export default function ClientInfoModal({
|
||||
if (ok) messageApi.success(t('copied'));
|
||||
}
|
||||
|
||||
async function loadIps() {
|
||||
if (!client?.email) return;
|
||||
setIpsLoading(true);
|
||||
try {
|
||||
const msg = await HttpUtil.post(`/panel/api/clients/ips/${encodeURIComponent(client.email)}`) as ApiMsg<unknown[]>;
|
||||
if (!msg?.success) { setClientIps([]); return; }
|
||||
const arr = Array.isArray(msg.obj) ? msg.obj : [];
|
||||
setClientIps(arr.filter((x): x is string => typeof x === 'string' && x.length > 0));
|
||||
} finally {
|
||||
setIpsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function clearIps() {
|
||||
if (!client?.email) return;
|
||||
setIpsClearing(true);
|
||||
try {
|
||||
const msg = await HttpUtil.post(`/panel/api/clients/clearIps/${encodeURIComponent(client.email)}`) as ApiMsg;
|
||||
if (msg?.success) setClientIps([]);
|
||||
} finally {
|
||||
setIpsClearing(false);
|
||||
}
|
||||
}
|
||||
|
||||
function openIpsModal() {
|
||||
setIpsModalOpen(true);
|
||||
if (clientIps.length === 0) void loadIps();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{messageContextHolder}
|
||||
<Modal
|
||||
open={open}
|
||||
title={client ? client.email : t('info')}
|
||||
title={client ? `${t('pages.clients.clientInfo')} — ${client.email}` : t('pages.clients.clientInfo')}
|
||||
footer={null}
|
||||
width={640}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
@@ -313,6 +348,14 @@ export default function ClientInfoModal({
|
||||
<td>{t('pages.clients.ipLimit')}</td>
|
||||
<td>{!client.limitIp ? <Tag>∞</Tag> : <Tag>{client.limitIp}</Tag>}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t('pages.inbounds.IPLimitlog')}</td>
|
||||
<td>
|
||||
<Button size="small" icon={<EyeOutlined />} loading={ipsLoading} onClick={openIpsModal}>
|
||||
{clientIps.length > 0 ? clientIps.length : ''}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t('pages.inbounds.createdAt')}</td>
|
||||
<td><Tag>{dateLabel(client.createdAt)}</Tag></td>
|
||||
@@ -335,30 +378,27 @@ export default function ClientInfoModal({
|
||||
if (ids.length === 0) return <span className="hint">—</span>;
|
||||
const visible = ids.slice(0, INBOUND_CHIP_LIMIT);
|
||||
const overflow = ids.slice(INBOUND_CHIP_LIMIT);
|
||||
const inboundChip = (id: number, compact: boolean) => {
|
||||
const inboundChip = (id: number) => {
|
||||
const ib = inboundsById[id];
|
||||
const proto = (ib?.protocol || '').toLowerCase();
|
||||
const color = INBOUND_PROTOCOL_COLORS[proto] ?? 'default';
|
||||
const fullLabel = ib
|
||||
? `${ib.remark || `#${id}`} (${ib.protocol}:${ib.port})`
|
||||
: `#${id}`;
|
||||
const compactLabel = ib ? `${ib.protocol}:${ib.port}` : `#${id}`;
|
||||
const label = ib?.tag ?? '';
|
||||
return (
|
||||
<Tooltip key={id} title={fullLabel}>
|
||||
<Tag color={color}>{compact ? compactLabel : fullLabel}</Tag>
|
||||
<Tooltip key={id} title={label}>
|
||||
<Tag color={color}>{label}</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div className="chips">
|
||||
{visible.map((id) => inboundChip(id, true))}
|
||||
{visible.map((id) => inboundChip(id))}
|
||||
{overflow.length > 0 && (
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="bottomRight"
|
||||
content={
|
||||
<div className="chips chips-stack">
|
||||
{overflow.map((id) => inboundChip(id, false))}
|
||||
{overflow.map((id) => inboundChip(id))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -510,6 +550,47 @@ export default function ClientInfoModal({
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={ipsModalOpen}
|
||||
title={`${t('pages.inbounds.IPLimitlog')}${client?.email ? ` — ${client.email}` : ''}`}
|
||||
width={440}
|
||||
onCancel={() => setIpsModalOpen(false)}
|
||||
footer={[
|
||||
<Button key="refresh" icon={<ReloadOutlined />} loading={ipsLoading} onClick={loadIps}>
|
||||
{t('refresh')}
|
||||
</Button>,
|
||||
<Button key="clear" danger loading={ipsClearing} disabled={clientIps.length === 0} onClick={clearIps}>
|
||||
{t('pages.clients.clearAll')}
|
||||
</Button>,
|
||||
<Button key="close" type="primary" onClick={() => setIpsModalOpen(false)}>
|
||||
{t('close')}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{clientIps.length > 0 ? (
|
||||
<div style={{ maxHeight: 360, overflowY: 'auto' }}>
|
||||
{clientIps.map((ip, idx) => (
|
||||
<Tag
|
||||
key={idx}
|
||||
color="blue"
|
||||
style={{
|
||||
display: 'block',
|
||||
width: 'fit-content',
|
||||
maxWidth: '100%',
|
||||
marginBottom: 6,
|
||||
padding: '2px 8px',
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
||||
}}
|
||||
>
|
||||
{ip}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Tag>{t('tgbot.noIpRecord')}</Tag>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user