mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
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.
This commit is contained in:
parent
87eaa79e5d
commit
6c279d48fd
5 changed files with 233 additions and 139 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -99,6 +99,8 @@ interface SubSettings {
|
|||
subURI: string;
|
||||
subJsonURI: string;
|
||||
subJsonEnable: boolean;
|
||||
subClashURI: string;
|
||||
subClashEnable: boolean;
|
||||
}
|
||||
|
||||
interface ClientInfoModalProps {
|
||||
|
|
@ -115,7 +117,14 @@ interface ApiMsg<T = unknown> {
|
|||
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({
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{subClashLink && (
|
||||
<div className="link-row">
|
||||
<Tooltip title="Clash / Mihomo">
|
||||
<Tag color="gold" className="link-row-tag">CLASH</Tag>
|
||||
</Tooltip>
|
||||
<a
|
||||
href={subClashLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="link-row-title link-row-title-anchor"
|
||||
title={subClashLink}
|
||||
>
|
||||
{client.subId}
|
||||
</a>
|
||||
<div className="link-row-actions">
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button size="small" icon={<CopyOutlined />} onClick={() => copyValue(subClashLink)} />
|
||||
</Tooltip>
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="left"
|
||||
destroyOnHidden
|
||||
content={<QrPanel value={subClashLink} remark={`${client.email} — Clash / Mihomo`} size={220} />}
|
||||
>
|
||||
<Tooltip title={t('pages.clients.qrCode')}>
|
||||
<Button size="small" icon={<QrcodeOutlined />} />
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<Row justify="center">
|
||||
<Col xs={24} sm={22} md={18} lg={14} xl={12}>
|
||||
<Card hoverable className="subscription-card" title={cardTitle} extra={cardExtra}>
|
||||
<Row gutter={[8, 8]} justify="center" className="qr-row">
|
||||
<Col xs={24} sm={subJsonUrl || subClashUrl ? 12 : 24} className="qr-col">
|
||||
<div className="qr-box">
|
||||
<Tag color="purple" className="qr-tag">{t('pages.settings.subSettings')}</Tag>
|
||||
<QRCode
|
||||
className="qr-code"
|
||||
value={subUrl}
|
||||
size={QR_SIZE}
|
||||
type="svg"
|
||||
bordered={false}
|
||||
color="#000000"
|
||||
bgColor="#ffffff"
|
||||
title={t('copy')}
|
||||
onClick={() => copy(subUrl)}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
{subJsonUrl && (
|
||||
<Col xs={24} sm={12} className="qr-col">
|
||||
<div className="qr-box">
|
||||
<Tag color="purple" className="qr-tag">
|
||||
{t('pages.settings.subSettings')} JSON
|
||||
</Tag>
|
||||
<QRCode
|
||||
className="qr-code"
|
||||
value={subJsonUrl}
|
||||
size={QR_SIZE}
|
||||
type="svg"
|
||||
bordered={false}
|
||||
color="#000000"
|
||||
bgColor="#ffffff"
|
||||
title={t('copy')}
|
||||
onClick={() => copy(subJsonUrl)}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
)}
|
||||
{subClashUrl && (
|
||||
<Col xs={24} sm={12} className="qr-col">
|
||||
<div className="qr-box">
|
||||
<Tag color="purple" className="qr-tag">Clash / Mihomo</Tag>
|
||||
<QRCode
|
||||
className="qr-code"
|
||||
value={subClashUrl}
|
||||
size={QR_SIZE}
|
||||
type="svg"
|
||||
bordered={false}
|
||||
color="#000000"
|
||||
bgColor="#ffffff"
|
||||
title={t('copy')}
|
||||
onClick={() => copy(subClashUrl)}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
<Descriptions
|
||||
bordered
|
||||
column={1}
|
||||
|
|
@ -399,6 +344,8 @@ export default function SubPage() {
|
|||
/>
|
||||
|
||||
{links.length > 0 && (
|
||||
<>
|
||||
<Divider>{t('pages.inbounds.copyLink')}</Divider>
|
||||
<div className="links-section">
|
||||
{links.map((link, idx) => {
|
||||
const meta = parseLinkMeta(link, idx);
|
||||
|
|
@ -460,6 +407,107 @@ export default function SubPage() {
|
|||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(subUrl || subJsonUrl || subClashUrl) && (
|
||||
<>
|
||||
<Divider>{t('subscription.title')}</Divider>
|
||||
<div className="links-section">
|
||||
{subUrl && (
|
||||
<div className="sub-link-row">
|
||||
<Tag color="green" className="sub-link-tag">SUB</Tag>
|
||||
<a
|
||||
href={subUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="sub-link-title sub-link-anchor"
|
||||
title={subUrl}
|
||||
>
|
||||
{sId}
|
||||
</a>
|
||||
<div className="sub-link-actions">
|
||||
<Button size="small" icon={<CopyOutlined />} onClick={() => copy(subUrl)} aria-label={t('copy')} title={t('copy')} />
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="left"
|
||||
destroyOnHidden
|
||||
content={
|
||||
<div className="sub-link-qr-popover">
|
||||
<Tag color="green" className="qr-tag">{t('pages.settings.subSettings')}</Tag>
|
||||
<QRCode value={subUrl} size={QR_SIZE} type="svg" bordered={false} color="#000000" bgColor="#ffffff" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button size="small" icon={<QrcodeOutlined />} aria-label="QR" title="QR" />
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{subJsonUrl && (
|
||||
<div className="sub-link-row">
|
||||
<Tag color="purple" className="sub-link-tag">JSON</Tag>
|
||||
<a
|
||||
href={subJsonUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="sub-link-title sub-link-anchor"
|
||||
title={subJsonUrl}
|
||||
>
|
||||
{sId}
|
||||
</a>
|
||||
<div className="sub-link-actions">
|
||||
<Button size="small" icon={<CopyOutlined />} onClick={() => copy(subJsonUrl)} aria-label={t('copy')} title={t('copy')} />
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="left"
|
||||
destroyOnHidden
|
||||
content={
|
||||
<div className="sub-link-qr-popover">
|
||||
<Tag color="purple" className="qr-tag">{t('pages.settings.subSettings')} JSON</Tag>
|
||||
<QRCode value={subJsonUrl} size={QR_SIZE} type="svg" bordered={false} color="#000000" bgColor="#ffffff" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button size="small" icon={<QrcodeOutlined />} aria-label="QR" title="QR" />
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{subClashUrl && (
|
||||
<div className="sub-link-row">
|
||||
<Tooltip title="Clash / Mihomo">
|
||||
<Tag color="gold" className="sub-link-tag">CLASH</Tag>
|
||||
</Tooltip>
|
||||
<a
|
||||
href={subClashUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="sub-link-title sub-link-anchor"
|
||||
title={subClashUrl}
|
||||
>
|
||||
{sId}
|
||||
</a>
|
||||
<div className="sub-link-actions">
|
||||
<Button size="small" icon={<CopyOutlined />} onClick={() => copy(subClashUrl)} aria-label={t('copy')} title={t('copy')} />
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="left"
|
||||
destroyOnHidden
|
||||
content={
|
||||
<div className="sub-link-qr-popover">
|
||||
<Tag color="gold" className="qr-tag">Clash / Mihomo</Tag>
|
||||
<QRCode value={subClashUrl} size={QR_SIZE} type="svg" bordered={false} color="#000000" bgColor="#ffffff" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button size="small" icon={<QrcodeOutlined />} aria-label="QR" title="QR" />
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Row gutter={[8, 8]} justify="center" className="apps-row">
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue