From 6c279d48fde781addc85f72e493a7f3d3d6acc45 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 27 May 2026 03:24:48 +0200 Subject: [PATCH] feat(sub): clash row + reorganise SubPage around Subscription info ClientInfoModal: - Add a Clash / Mihomo row to the subscription section, gated on subClashEnable + subClashURI from /panel/setting/defaultSettings. Defaults payload schema is widened to carry subClashURI/subClashEnable. SubPage: - Drop the rectangular QR-codes header that used to sit at the very top of the card. The subscription info table now leads, followed by Divider("Copy URL") + per-protocol link rows (already converted to the compact ClientInfoModal pattern), then a new Divider("Subscription") + compact rows for the SUB / JSON / CLASH URLs with copy + QR-popover actions. The apps dropdown row remains the footer. CSS clean-up: removed the now-unused .qr-row/.qr-col/.qr-box/.qr-code rules; kept .qr-tag and trimmed the info-table top gap. Added a .sub-link-anchor underline-on-hover style for the new URL rows. --- frontend/src/hooks/useClients.ts | 13 +- .../src/pages/clients/ClientInfoModal.tsx | 48 ++- frontend/src/pages/sub/SubPage.css | 33 +-- frontend/src/pages/sub/SubPage.tsx | 276 ++++++++++-------- frontend/src/schemas/defaults.ts | 2 + 5 files changed, 233 insertions(+), 139 deletions(-) diff --git a/frontend/src/hooks/useClients.ts b/frontend/src/hooks/useClients.ts index 93c470f7..dc6f8989 100644 --- a/frontend/src/hooks/useClients.ts +++ b/frontend/src/hooks/useClients.ts @@ -34,6 +34,8 @@ interface SubSettings { subURI: string; subJsonURI: string; subJsonEnable: boolean; + subClashURI: string; + subClashEnable: boolean; } export interface ClientQueryParams { @@ -157,7 +159,16 @@ export function useClients() { subURI: (defaults.subURI as string) || '', subJsonURI: (defaults.subJsonURI as string) || '', subJsonEnable: !!defaults.subJsonEnable, - }), [defaults.subEnable, defaults.subURI, defaults.subJsonURI, defaults.subJsonEnable]); + subClashURI: (defaults.subClashURI as string) || '', + subClashEnable: !!defaults.subClashEnable, + }), [ + defaults.subEnable, + defaults.subURI, + defaults.subJsonURI, + defaults.subJsonEnable, + defaults.subClashURI, + defaults.subClashEnable, + ]); const ipLimitEnable = !!defaults.ipLimitEnable; const tgBotEnable = !!defaults.tgBotEnable; diff --git a/frontend/src/pages/clients/ClientInfoModal.tsx b/frontend/src/pages/clients/ClientInfoModal.tsx index 4a500a28..c7c7989a 100644 --- a/frontend/src/pages/clients/ClientInfoModal.tsx +++ b/frontend/src/pages/clients/ClientInfoModal.tsx @@ -99,6 +99,8 @@ interface SubSettings { subURI: string; subJsonURI: string; subJsonEnable: boolean; + subClashURI: string; + subClashEnable: boolean; } interface ClientInfoModalProps { @@ -115,7 +117,14 @@ interface ApiMsg { obj?: T; } -const DEFAULT_SUB: SubSettings = { enable: false, subURI: '', subJsonURI: '', subJsonEnable: false }; +const DEFAULT_SUB: SubSettings = { + enable: false, + subURI: '', + subJsonURI: '', + subJsonEnable: false, + subClashURI: '', + subClashEnable: false, +}; export default function ClientInfoModal({ open, @@ -176,6 +185,12 @@ export default function ClientInfoModal({ return subSettings.subJsonURI + client.subId; }, [client?.subId, subSettings?.subJsonEnable, subSettings?.subJsonURI]); + const subClashLink = useMemo(() => { + if (!client?.subId) return ''; + if (!subSettings?.subClashEnable || !subSettings?.subClashURI) return ''; + return subSettings.subClashURI + client.subId; + }, [client?.subId, subSettings?.subClashEnable, subSettings?.subClashURI]); + const showSubscription = !!(subSettings?.enable && client?.subId); async function copyValue(text: string) { @@ -459,6 +474,37 @@ export default function ClientInfoModal({ )} + {subClashLink && ( +
+ + CLASH + + + {client.subId} + +
+ +
+
+ )} )} diff --git a/frontend/src/pages/sub/SubPage.css b/frontend/src/pages/sub/SubPage.css index bc1bfd9a..d3765c7f 100644 --- a/frontend/src/pages/sub/SubPage.css +++ b/frontend/src/pages/sub/SubPage.css @@ -28,44 +28,31 @@ margin-top: 8px; } -.qr-row { - margin-bottom: 12px; -} - -.qr-col { - display: flex; - justify-content: center; -} - -.qr-box { - display: inline-flex; - flex-direction: column; - align-items: center; - gap: 4px; - width: 240px; -} - .qr-tag { width: 100%; text-align: center; margin: 0; } -.qr-code { - cursor: pointer; -} - .info-table { - margin-top: 12px; + margin-top: 4px; } .links-section { - margin-top: 16px; display: flex; flex-direction: column; gap: 8px; } +.sub-link-anchor { + color: inherit; + text-decoration: none; +} + +.sub-link-anchor:hover { + text-decoration: underline; +} + .sub-link-row { display: flex; align-items: center; diff --git a/frontend/src/pages/sub/SubPage.tsx b/frontend/src/pages/sub/SubPage.tsx index 8c06e2fb..2cef8cd7 100644 --- a/frontend/src/pages/sub/SubPage.tsx +++ b/frontend/src/pages/sub/SubPage.tsx @@ -6,6 +6,7 @@ import { Col, ConfigProvider, Descriptions, + Divider, Dropdown, Layout, Menu, @@ -15,6 +16,7 @@ import { Row, Space, Tag, + Tooltip, } from 'antd'; import { AndroidOutlined, @@ -333,63 +335,6 @@ export default function SubPage() { - - -
- {t('pages.settings.subSettings')} - copy(subUrl)} - /> -
- - {subJsonUrl && ( - -
- - {t('pages.settings.subSettings')} JSON - - copy(subJsonUrl)} - /> -
- - )} - {subClashUrl && ( - -
- Clash / Mihomo - copy(subClashUrl)} - /> -
- - )} -
- {links.length > 0 && ( -
- {links.map((link, idx) => { - const meta = parseLinkMeta(link, idx); - const rowTitle = trimEmail(meta.remark, linkEmails[idx] || '') || meta.remark; - const canQr = !isPostQuantumLink(link); - return ( -
- + {t('pages.inbounds.copyLink')} +
+ {links.map((link, idx) => { + const meta = parseLinkMeta(link, idx); + const rowTitle = trimEmail(meta.remark, linkEmails[idx] || '') || meta.remark; + const canQr = !isPostQuantumLink(link); + return ( +
+ + {meta.protocol} + + + {rowTitle} + +
+
+ } + > +
+
+ ); + })} +
+ + )} + + {(subUrl || subJsonUrl || subClashUrl) && ( + <> + {t('subscription.title')} +
+ {subUrl && ( +
+ SUB + - {meta.protocol} - - - {rowTitle} - + {sId} +
-
- } - > -
+ } + > +
- ); - })} - + )} + {subJsonUrl && ( +
+ JSON + + {sId} + +
+
+ } + > +
+ + )} + {subClashUrl && ( +
+ + CLASH + + + {sId} + +
+
+ } + > +
+ + )} + + )} diff --git a/frontend/src/schemas/defaults.ts b/frontend/src/schemas/defaults.ts index 6f2fb809..61d79a24 100644 --- a/frontend/src/schemas/defaults.ts +++ b/frontend/src/schemas/defaults.ts @@ -9,6 +9,8 @@ export const DefaultsPayloadSchema = z.object({ subURI: z.string().optional(), subJsonURI: z.string().optional(), subJsonEnable: z.boolean().optional(), + subClashURI: z.string().optional(), + subClashEnable: z.boolean().optional(), pageSize: z.number().optional(), remarkModel: z.string().optional(), datepicker: z.enum(['gregorian', 'jalalian']).optional(),