diff --git a/frontend/src/pages/clients/ClientInfoModal.tsx b/frontend/src/pages/clients/ClientInfoModal.tsx index 13441d693..d09c9990c 100644 --- a/frontend/src/pages/clients/ClientInfoModal.tsx +++ b/frontend/src/pages/clients/ClientInfoModal.tsx @@ -1,9 +1,9 @@ import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Divider, Modal, Popover, Tag, Tooltip, message } from 'antd'; -import { CopyOutlined, EyeOutlined, QrcodeOutlined, ReloadOutlined } from '@ant-design/icons'; +import { CopyOutlined, DownloadOutlined, EyeOutlined, QrcodeOutlined, ReloadOutlined } from '@ant-design/icons'; -import { ClipboardManager, HttpUtil, IntlUtil, SizeFormatter } from '@/utils'; +import { ClipboardManager, FileManager, HttpUtil, IntlUtil, SizeFormatter } from '@/utils'; import { formatInboundLabel } from '@/lib/inbounds/label'; import { normalizeClientIps, type ClientIpInfo } from '@/lib/clients/ip-log'; import { useDatepicker } from '@/hooks/useDatepicker'; @@ -64,6 +64,12 @@ const DEFAULT_SUB: SubSettings = { publicHost: '', }; +const SUBSCRIPTION_DOWNLOAD_NAMES = { + standard: 'subscription-standard.txt', + json: 'subscription-json.json', + clash: 'subscription-clash.yaml', +} as const; + export default function ClientInfoModal({ open, client, @@ -89,6 +95,7 @@ export default function ClientInfoModal({ const [ipsLoading, setIpsLoading] = useState(false); const [ipsClearing, setIpsClearing] = useState(false); const [ipsModalOpen, setIpsModalOpen] = useState(false); + const [downloadingFormat, setDownloadingFormat] = useState(null); useEffect(() => { if (!open) { @@ -148,6 +155,21 @@ export default function ClientInfoModal({ if (ok) messageApi.success(t('copied')); } + async function downloadSubscription(url: string, format: keyof typeof SUBSCRIPTION_DOWNLOAD_NAMES) { + if (!url || downloadingFormat) return; + setDownloadingFormat(format); + try { + const response = await fetch(url); + if (!response.ok) throw new Error('Subscription download failed'); + const content = await response.text(); + FileManager.downloadTextFile(content, SUBSCRIPTION_DOWNLOAD_NAMES[format]); + } catch (_) { + messageApi.error(t('somethingWentWrong')); + } finally { + setDownloadingFormat(null); + } + } + async function loadIps() { if (!client?.email) return; setIpsLoading(true); @@ -383,6 +405,9 @@ export default function ClientInfoModal({