chore(frontend): adopt antd v6 api updates

Sweep deprecated props across the React tree:
- Modal: destroyOnClose -> destroyOnHidden, maskClosable -> mask.closable
- Space: direction -> orientation (or removed when redundant)
- Input.Group compact -> Space.Compact block
- Drawer: width -> size
- Spin: tip -> description
- Progress: trailColor -> railColor
- Alert: message -> title
- Popover: overlayClassName -> rootClassName
- BackTop -> FloatButton.BackTop

Also refresh dashboard theming for v6: rename dark/ultra Layout and Menu
tokens (siderBg, darkItemBg, darkSubMenuItemBg, darkPopupBg), tweak gauge
size/stroke, add font-size overrides for Statistic and Progress so the
overview numbers stay legible under v6 defaults.
This commit is contained in:
MHSanaei 2026-05-22 00:42:20 +02:00
parent d6f42b3395
commit 7a4317086b
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
45 changed files with 163 additions and 136 deletions

View file

@ -199,7 +199,7 @@ export default function AppSidebar({ basePath = '', requestUri = '' }: AppSideba
closable={false}
open={drawerOpen}
rootClassName={currentTheme}
width="min(82vw, 320px)"
size="min(82vw, 320px)"
styles={{
wrapper: { padding: 0 },
body: { padding: 0, display: 'flex', flexDirection: 'column', height: '100%' },

View file

@ -1,5 +1,25 @@
.ant-statistic-content {
font-size: 16px;
font-size: 15px !important;
line-height: 1.4 !important;
}
.ant-statistic-content-value,
.ant-statistic-content-prefix,
.ant-statistic-content-suffix {
font-size: 15px !important;
}
.ant-statistic-content-prefix {
margin-inline-end: 6px !important;
}
.ant-statistic-content-prefix .anticon {
font-size: 16px !important;
}
.ant-statistic-title {
font-size: 12px !important;
margin-bottom: 4px !important;
}
body.dark .ant-statistic-content {

View file

@ -55,11 +55,11 @@ export default function PromptModal({
title={title}
okText={okText}
cancelText="Cancel"
maskClosable={false}
mask={{ closable: false }}
confirmLoading={loading}
onOk={() => onConfirm(value)}
onCancel={onClose}
destroyOnClose
destroyOnHidden
>
{type === 'textarea' ? (
<Input.TextArea

View file

@ -30,8 +30,7 @@ export default function TextModal({ open, onClose, title, content, fileName = ''
open={open}
title={title}
onCancel={onClose}
closable
destroyOnClose
destroyOnHidden
footer={(
<>
{fileName && (

View file

@ -42,24 +42,32 @@ const ULTRA_DARK_TOKENS = {
colorBgElevated: '#141414',
};
const DARK_LAYOUT_TOKENS = {
colorBgHeader: '#252526',
colorBgTrigger: '#333333',
colorBgBody: '#1e1e1e',
bodyBg: '#1e1e1e',
headerBg: '#252526',
headerColor: '#ffffff',
footerBg: '#1e1e1e',
siderBg: '#252526',
triggerBg: '#333333',
triggerColor: '#ffffff',
};
const ULTRA_DARK_LAYOUT_TOKENS = {
colorBgHeader: '#0a0a0a',
colorBgTrigger: '#141414',
colorBgBody: '#000',
bodyBg: '#000',
headerBg: '#0a0a0a',
headerColor: '#ffffff',
footerBg: '#000',
siderBg: '#0a0a0a',
triggerBg: '#141414',
triggerColor: '#ffffff',
};
const DARK_MENU_TOKENS = {
colorItemBg: '#252526',
colorSubItemBg: '#1e1e1e',
menuSubMenuBg: '#252526',
darkItemBg: '#252526',
darkSubMenuItemBg: '#1e1e1e',
darkPopupBg: '#252526',
};
const ULTRA_DARK_MENU_TOKENS = {
colorItemBg: '#0a0a0a',
colorSubItemBg: '#000',
menuSubMenuBg: '#0a0a0a',
darkItemBg: '#0a0a0a',
darkSubMenuItemBg: '#000',
darkPopupBg: '#0a0a0a',
};
export function buildAntdThemeConfig(isDark: boolean, isUltra: boolean): ThemeConfig {

View file

@ -210,7 +210,7 @@ export default function ClientBulkAddModal({
okText={t('create')}
cancelText={t('close')}
confirmLoading={saving}
maskClosable={false}
mask={{ closable: false }}
width={640}
onOk={submit}
onCancel={() => onOpenChange(false)}

View file

@ -327,7 +327,7 @@ export default function ClientFormModal({
<Modal
open={open}
title={isEdit ? t('pages.clients.editTitle') : t('pages.clients.addTitle')}
destroyOnClose
destroyOnHidden
okText={isEdit ? t('save') : t('create')}
cancelText={t('cancel')}
okButtonProps={{ loading: submitting }}

View file

@ -626,7 +626,7 @@ export default function ClientsPage() {
<Layout className="content-shell">
<Layout.Content id="content-layout" className="content-area">
<Spin spinning={!fetched} delay={200} tip={t('loading')} size="large">
<Spin spinning={!fetched} delay={200} description={t('loading')} size="large">
{!fetched ? (
<div className="loading-spacer" />
) : (

View file

@ -648,14 +648,14 @@ export default function InboundFormModal({
const sniffingFallback = () => inboundRef.current?.sniffing?.toJson?.() || {};
const streamFallback = () => inboundRef.current?.stream?.toJson?.() || {};
const parseAdvancedSliceWithLabel = (rawText: string, fallback: unknown, label: string) => {
const parseAdvancedSliceWithLabel = useCallback((rawText: string, fallback: unknown, label: string) => {
try {
return parseAdvancedSliceOrFallback(rawText, fallback);
} catch (e) {
message.error(`${label} JSON invalid: ${(e as Error).message}`);
throw e;
}
};
}, []);
const compactAdvancedJson = (raw: string, fallback: string, label: string) => {
try {
@ -696,7 +696,7 @@ export default function InboundFormModal({
return false;
}
return true;
}, [t, refresh]);
}, [t, refresh, parseAdvancedSliceWithLabel]);
const handleTabChange = (next: string) => {
if (activeTabKey === 'advanced' && next !== 'advanced') {
@ -886,16 +886,15 @@ export default function InboundFormModal({
}
}, [canEnableStream, t, mode, dbInbound, isFallbackHost, saveFallbacks, onSaved, onClose]);
const protocolSnapshot = inboundRef.current?.protocol;
const streamSnapshot = JSON.stringify(inboundRef.current?.stream?.toJson?.() || {});
const sniffingSnapshot = JSON.stringify(inboundRef.current?.sniffing?.toJson?.() || {});
const settingsSnapshot = JSON.stringify(inboundRef.current?.settings?.toJson?.() || {});
useEffect(() => {
if (!inboundRef.current) return;
(['stream', 'sniffing', 'settings'] as const).forEach(stampAdvancedTextFor);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
inboundRef.current?.protocol,
JSON.stringify(inboundRef.current?.stream?.toJson?.() || {}),
JSON.stringify(inboundRef.current?.sniffing?.toJson?.() || {}),
JSON.stringify(inboundRef.current?.settings?.toJson?.() || {}),
]);
}, [protocolSnapshot, streamSnapshot, sniffingSnapshot, settingsSnapshot, stampAdvancedTextFor]);
const title = mode === 'edit' ? t('pages.inbounds.modifyInbound') : t('pages.inbounds.addInbound');
const okText = mode === 'edit' ? t('pages.clients.submitEdit') : t('create');
@ -998,7 +997,7 @@ export default function InboundFormModal({
<div key={record.rowKey} style={{ border: '1px solid var(--app-border-tertiary)', borderRadius: 6, padding: '10px 12px', marginBottom: 8 }}>
<Row gutter={8} align="middle" wrap={false}>
<Col flex="none">
<Space direction="vertical" size={2}>
<Space orientation="vertical" size={2}>
<Button size="small" type="text" disabled={index === 0} onClick={() => moveFallback(index, -1)}>
<CaretUpOutlined />
</Button>
@ -1146,7 +1145,7 @@ export default function InboundFormModal({
</Form.Item>
<Form.Item wrapperCol={{ span: 24 }}>
{(ib.settings.accounts || []).map((account: any, idx: number) => (
<Input.Group key={idx} compact className="mb-8">
<Space.Compact key={idx} className="mb-8" block>
<Input style={{ width: '45%' }} value={account.user}
addonBefore={String(idx + 1)} placeholder="Username"
onChange={(e) => { account.user = e.target.value; refresh(); }} />
@ -1155,7 +1154,7 @@ export default function InboundFormModal({
<Button onClick={() => { ib.settings.delAccount(idx); refresh(); }}>
<MinusOutlined />
</Button>
</Input.Group>
</Space.Compact>
))}
</Form.Item>
{ib.protocol === Protocols.HTTP && (
@ -1208,7 +1207,7 @@ export default function InboundFormModal({
{(ib.settings.portMap || []).length > 0 && (
<Form.Item wrapperCol={{ span: 24 }}>
{(ib.settings.portMap as { name: string; value: string }[]).map((pm, idx) => (
<Input.Group key={`pm-${idx}`} compact className="mb-8">
<Space.Compact key={`pm-${idx}`} className="mb-8" block>
<Input style={{ width: '30%' }} value={pm.name} placeholder="5555" addonBefore={String(idx + 1)}
onChange={(e) => { pm.name = e.target.value; refresh(); }} />
<Input style={{ width: '60%' }} value={pm.value} placeholder="1.1.1.1:7777"
@ -1216,7 +1215,7 @@ export default function InboundFormModal({
<Button onClick={() => { ib.settings.removePortMap(idx); refresh(); }}>
<MinusOutlined />
</Button>
</Input.Group>
</Space.Compact>
))}
</Form.Item>
)}
@ -1405,7 +1404,7 @@ export default function InboundFormModal({
{(ib.stream.tcp.request.headers || []).length > 0 && (
<Form.Item wrapperCol={{ span: 24 }}>
{(ib.stream.tcp.request.headers as { name: string; value: string }[]).map((h, idx) => (
<Input.Group key={`tcp-rh-${idx}`} compact className="mb-8">
<Space.Compact key={`tcp-rh-${idx}`} className="mb-8" block>
<Input style={{ width: '45%' }} value={h.name} addonBefore={String(idx + 1)}
placeholder={t('pages.inbounds.stream.general.name')}
onChange={(e) => { h.name = e.target.value; refresh(); }} />
@ -1415,7 +1414,7 @@ export default function InboundFormModal({
<Button onClick={() => { ib.stream.tcp.request.removeHeader(idx); refresh(); }}>
<MinusOutlined />
</Button>
</Input.Group>
</Space.Compact>
))}
</Form.Item>
)}
@ -1440,7 +1439,7 @@ export default function InboundFormModal({
{(ib.stream.tcp.response.headers || []).length > 0 && (
<Form.Item wrapperCol={{ span: 24 }}>
{(ib.stream.tcp.response.headers as { name: string; value: string }[]).map((h, idx) => (
<Input.Group key={`tcp-rsh-${idx}`} compact className="mb-8">
<Space.Compact key={`tcp-rsh-${idx}`} className="mb-8" block>
<Input style={{ width: '45%' }} value={h.name} addonBefore={String(idx + 1)}
placeholder={t('pages.inbounds.stream.general.name')}
onChange={(e) => { h.name = e.target.value; refresh(); }} />
@ -1450,7 +1449,7 @@ export default function InboundFormModal({
<Button onClick={() => { ib.stream.tcp.response.removeHeader(idx); refresh(); }}>
<MinusOutlined />
</Button>
</Input.Group>
</Space.Compact>
))}
</Form.Item>
)}
@ -1482,7 +1481,7 @@ export default function InboundFormModal({
{(ib.stream.ws.headers || []).length > 0 && (
<Form.Item wrapperCol={{ span: 24 }}>
{(ib.stream.ws.headers as { name: string; value: string }[]).map((h, idx) => (
<Input.Group key={`ws-h-${idx}`} compact className="mb-8">
<Space.Compact key={`ws-h-${idx}`} className="mb-8" block>
<Input style={{ width: '45%' }} value={h.name} addonBefore={String(idx + 1)}
placeholder={t('pages.inbounds.stream.general.name')}
onChange={(e) => { h.name = e.target.value; refresh(); }} />
@ -1492,7 +1491,7 @@ export default function InboundFormModal({
<Button onClick={() => { ib.stream.ws.removeHeader(idx); refresh(); }}>
<MinusOutlined />
</Button>
</Input.Group>
</Space.Compact>
))}
</Form.Item>
)}
@ -1518,7 +1517,7 @@ export default function InboundFormModal({
{(ib.stream.httpupgrade.headers || []).length > 0 && (
<Form.Item wrapperCol={{ span: 24 }}>
{(ib.stream.httpupgrade.headers as { name: string; value: string }[]).map((h, idx) => (
<Input.Group key={`hu-h-${idx}`} compact className="mb-8">
<Space.Compact key={`hu-h-${idx}`} className="mb-8" block>
<Input style={{ width: '45%' }} value={h.name} addonBefore={String(idx + 1)}
placeholder={t('pages.inbounds.stream.general.name')}
onChange={(e) => { h.name = e.target.value; refresh(); }} />
@ -1528,7 +1527,7 @@ export default function InboundFormModal({
<Button onClick={() => { ib.stream.httpupgrade.removeHeader(idx); refresh(); }}>
<MinusOutlined />
</Button>
</Input.Group>
</Space.Compact>
))}
</Form.Item>
)}
@ -1545,7 +1544,7 @@ export default function InboundFormModal({
{(ib.stream.xhttp.headers || []).length > 0 && (
<Form.Item wrapperCol={{ span: 24 }}>
{(ib.stream.xhttp.headers as { name: string; value: string }[]).map((h, idx) => (
<Input.Group key={`xh-h-${idx}`} compact className="mb-8">
<Space.Compact key={`xh-h-${idx}`} className="mb-8" block>
<Input style={{ width: '45%' }} value={h.name} addonBefore={String(idx + 1)}
placeholder={t('pages.inbounds.stream.general.name')}
onChange={(e) => { h.name = e.target.value; refresh(); }} />
@ -1555,7 +1554,7 @@ export default function InboundFormModal({
<Button onClick={() => { ib.stream.xhttp.removeHeader(idx); refresh(); }}>
<MinusOutlined />
</Button>
</Input.Group>
</Space.Compact>
))}
</Form.Item>
)}
@ -1652,7 +1651,7 @@ export default function InboundFormModal({
{externalProxyOn && (
<Form.Item wrapperCol={{ span: 24 }}>
{(ib.stream.externalProxy as { forceTls: string; dest: string; port: number; remark: string }[]).map((row, idx) => (
<Input.Group key={`ep-${idx}`} compact style={{ margin: '8px 0' }}>
<Space.Compact key={`ep-${idx}`} style={{ margin: '8px 0' }} block>
<Tooltip title="Force TLS">
<Select value={row.forceTls} style={{ width: '20%' }} onChange={(v) => { row.forceTls = v; refresh(); }}>
<Select.Option value="same">{t('pages.inbounds.same')}</Select.Option>
@ -1669,7 +1668,7 @@ export default function InboundFormModal({
<Input style={{ width: '35%' }} value={row.remark} placeholder={t('pages.inbounds.remark')}
onChange={(e) => { row.remark = e.target.value; refresh(); }}
addonAfter={<MinusOutlined onClick={() => { ib.stream.externalProxy.splice(idx, 1); refresh(); }} />} />
</Input.Group>
</Space.Compact>
))}
</Form.Item>
)}
@ -1762,7 +1761,7 @@ export default function InboundFormModal({
{(ib.stream.hysteria.masquerade.headers || []).length > 0 && (
<Form.Item wrapperCol={{ span: 24 }}>
{(ib.stream.hysteria.masquerade.headers as { name: string; value: string }[]).map((h, idx) => (
<Input.Group key={`mh-${idx}`} compact className="mb-8">
<Space.Compact key={`mh-${idx}`} className="mb-8" block>
<Input style={{ width: '45%' }} value={h.name} addonBefore={String(idx + 1)} placeholder="Name"
onChange={(e) => { h.name = e.target.value; refresh(); }} />
<Input style={{ width: '45%' }} value={h.value} placeholder="Value"
@ -1770,7 +1769,7 @@ export default function InboundFormModal({
<Button onClick={() => { ib.stream.hysteria.masquerade.removeHeader(idx); refresh(); }}>
<MinusOutlined />
</Button>
</Input.Group>
</Space.Compact>
))}
</Form.Item>
)}
@ -1808,14 +1807,14 @@ export default function InboundFormModal({
</Select>
</Form.Item>
<Form.Item label="Min/Max Version">
<Input.Group compact>
<Space.Compact block>
<Select value={ib.stream.tls.minVersion} style={{ width: '50%' }} onChange={(v) => { ib.stream.tls.minVersion = v; refresh(); }}>
{TLS_VERSIONS.map((v) => <Select.Option key={v} value={v}>{v}</Select.Option>)}
</Select>
<Select value={ib.stream.tls.maxVersion} style={{ width: '50%' }} onChange={(v) => { ib.stream.tls.maxVersion = v; refresh(); }}>
{TLS_VERSIONS.map((v) => <Select.Option key={v} value={v}>{v}</Select.Option>)}
</Select>
</Input.Group>
</Space.Compact>
</Form.Item>
<Form.Item label="uTLS">
<Select value={ib.stream.tls.settings.fingerprint} style={{ width: '100%' }} onChange={(v) => { ib.stream.tls.settings.fingerprint = v; refresh(); }}>
@ -2085,11 +2084,11 @@ export default function InboundFormModal({
okText={okText}
cancelText={t('close')}
confirmLoading={saving}
maskClosable={false}
mask={{ closable: false }}
width={780}
onOk={submit}
onCancel={onClose}
destroyOnClose
destroyOnHidden
>
<Tabs activeKey={activeTabKey} onChange={handleTabChange} items={tabItems} />
</Modal>

View file

@ -882,7 +882,7 @@ export default function InboundInfoModal({
tabItems.push({ key: 'inbound', label: t('pages.xray.rules.inbound'), children: inboundTab });
return (
<Modal open={open} onCancel={onClose} title={t('pages.inbounds.inboundData')} footer={null} width={640} destroyOnClose>
<Modal open={open} onCancel={onClose} title={t('pages.inbounds.inboundData')} footer={null} width={640} destroyOnHidden>
<Tabs activeKey={activeTab} onChange={setActiveTab} items={tabItems} />
</Modal>
);

View file

@ -245,12 +245,12 @@ export default function InboundList({
[dbInbounds],
);
const sorterFor = (key: SortKey) => ({
const sorterFor = useCallback((key: SortKey) => ({
sorter: true as const,
showSorterTooltip: false,
sortOrder: sortKey === key ? sortOrder : null,
sortDirections: ['ascend' as const, 'descend' as const],
});
}), [sortKey, sortOrder]);
const columns: TableColumnType<DBInboundRecord>[] = useMemo(() => {
const cols: TableColumnType<DBInboundRecord>[] = [
@ -474,7 +474,7 @@ export default function InboundList({
);
return cols;
}, [t, hasAnyRemark, hasActiveNode, nodesById, clientCount, subEnable, expireDiff, trafficDiff, datepicker, onRowAction, onSwitchEnable]);
}, [t, hasAnyRemark, hasActiveNode, nodesById, clientCount, subEnable, expireDiff, trafficDiff, datepicker, onRowAction, onSwitchEnable, sorterFor]);
const paginationFor = (rows: DBInboundRecord[]) => {
const size = pageSize > 0 ? pageSize : rows.length || 1;
@ -497,7 +497,7 @@ export default function InboundList({
<Card
hoverable
title={(
<Space direction="horizontal">
<Space>
<Button type="primary" onClick={onAddInbound} icon={<PlusOutlined />}>
{!isMobile && t('pages.inbounds.addInbound')}
</Button>
@ -509,7 +509,7 @@ export default function InboundList({
</Space>
)}
>
<Space direction="vertical" style={{ width: '100%' }}>
<Space orientation="vertical" style={{ width: '100%' }}>
{isMobile ? (
<div className="inbound-cards">
{sortedInbounds.length === 0 ? (
@ -571,7 +571,7 @@ export default function InboundList({
centered
title={statsRecord ? `#${statsRecord.id} ${statsRecord.remark || ''}`.trim() : ''}
onCancel={() => setStatsRecord(null)}
destroyOnClose
destroyOnHidden
>
{statsRecord && (
<div className="card-stats">

View file

@ -435,7 +435,7 @@ export default function InboundsPage() {
<Layout className="content-shell">
<Layout.Content id="content-layout" className="content-area">
<Spin spinning={!fetched} delay={200} tip="Loading…" size="large">
<Spin spinning={!fetched} delay={200} description="Loading…" size="large">
{!fetched ? (
<div className="loading-spacer" />
) : (

View file

@ -125,7 +125,7 @@ export default function QrCodeModal({
}));
return (
<Modal open={open} onCancel={onClose} title={t('qrCode')} footer={null} width={420} destroyOnClose>
<Modal open={open} onCancel={onClose} title={t('qrCode')} footer={null} width={420} destroyOnHidden>
{dbInbound && (
<Collapse
ghost

View file

@ -62,7 +62,6 @@ export default function BackupModal({ open, basePath: _basePath, onClose, onBusy
<Modal
open={open}
title={t('pages.index.backupTitle')}
closable
footer={null}
onCancel={onClose}
>

View file

@ -234,7 +234,7 @@ export default function CustomGeoSection({ active }: CustomGeoSectionProps) {
type="info"
showIcon
className="mb-10"
message={t('pages.index.customGeoRoutingHint')}
title={t('pages.index.customGeoRoutingHint')}
/>
<div className="toolbar">

View file

@ -158,7 +158,7 @@ export default function IndexPage() {
<Spin
spinning={loading || !fetched}
delay={200}
tip={loading ? loadingTip : t('loading')}
description={loading ? loadingTip : t('loading')}
size="large"
>
{!fetched ? (
@ -450,7 +450,6 @@ export default function IndexPage() {
title={t('pages.index.config')}
width={isMobile ? '100%' : 900}
style={isMobile ? { top: 20, maxWidth: 'calc(100vw - 16px)' } : undefined}
closable
onCancel={() => setConfigTextOpen(false)}
footer={[
<Button

View file

@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Checkbox, Form, Input, Modal, Select } from 'antd';
import { Button, Checkbox, Form, Modal, Select, Space } from 'antd';
import { DownloadOutlined, SyncOutlined } from '@ant-design/icons';
import { HttpUtil, FileManager, PromiseUtil } from '@/utils';
@ -107,7 +107,6 @@ export default function LogModal({ open, onClose }: LogModalProps) {
return (
<Modal
open={open}
closable
footer={null}
width={isMobile ? '100vw' : 800}
className={isMobile ? 'logmodal-mobile' : undefined}
@ -116,7 +115,7 @@ export default function LogModal({ open, onClose }: LogModalProps) {
>
<Form layout="inline" className="log-toolbar">
<Form.Item>
<Input.Group compact>
<Space.Compact>
<Select value={rows} size="small" style={{ width: 70 }} onChange={setRows}>
<Select.Option value="10">10</Select.Option>
<Select.Option value="20">20</Select.Option>
@ -131,7 +130,7 @@ export default function LogModal({ open, onClose }: LogModalProps) {
<Select.Option value="warning">Warning</Select.Option>
<Select.Option value="err">Error</Select.Option>
</Select>
</Input.Group>
</Space.Compact>
</Form.Item>
<Form.Item>
<Checkbox checked={syslog} onChange={(e) => setSyslog(e.target.checked)}>

View file

@ -72,7 +72,6 @@ export default function PanelUpdateModal({ open, info, onClose, onBusy }: PanelU
<Modal
open={open}
title={t('pages.index.updatePanel')}
closable
footer={null}
onCancel={onClose}
>
@ -80,7 +79,7 @@ export default function PanelUpdateModal({ open, info, onClose, onBusy }: PanelU
<Alert
type="warning"
className="mb-12"
message={t('pages.index.panelUpdateDesc')}
title={t('pages.index.panelUpdateDesc')}
showIcon
/>
)}

View file

@ -2,7 +2,8 @@
text-align: center;
}
.status-card .ant-progress-text {
font-size: 14px !important;
.status-card .ant-progress-text,
.status-card .ant-progress-indicator {
font-size: 12px !important;
font-weight: 500;
}

View file

@ -3,6 +3,7 @@ import { Card, Col, Progress, Row, Tooltip } from 'antd';
import { AreaChartOutlined } from '@ant-design/icons';
import { CPUFormatter, SizeFormatter } from '@/utils';
import { useTheme } from '@/hooks/useTheme';
import type { Status } from '@/models/status';
import './StatusCard.css';
@ -11,11 +12,14 @@ interface StatusCardProps {
isMobile: boolean;
}
const TRAIL_COLOR = 'rgba(128, 128, 128, 0.25)';
export default function StatusCard({ status, isMobile }: StatusCardProps) {
const { t } = useTranslation();
const gaugeSize = isMobile ? 60 : 70;
const { isDark, isUltra } = useTheme();
const gaugeSize = isMobile ? 60 : 90;
const strokeWidth = isMobile ? 7 : 5;
const railColor = isDark
? isUltra ? 'rgba(255, 255, 255, 0.1)' : 'rgba(255, 255, 255, 0.16)'
: 'rgba(0, 0, 0, 0.08)';
return (
<Card hoverable className="status-card">
@ -27,7 +31,8 @@ export default function StatusCard({ status, isMobile }: StatusCardProps) {
type="dashboard"
status="normal"
strokeColor={status.cpu.color}
trailColor={TRAIL_COLOR}
railColor={railColor}
strokeWidth={strokeWidth}
percent={status.cpu.percent}
size={gaugeSize}
/>
@ -56,7 +61,8 @@ export default function StatusCard({ status, isMobile }: StatusCardProps) {
type="dashboard"
status="normal"
strokeColor={status.mem.color}
trailColor={TRAIL_COLOR}
railColor={railColor}
strokeWidth={strokeWidth}
percent={status.mem.percent}
size={gaugeSize}
/>
@ -75,7 +81,8 @@ export default function StatusCard({ status, isMobile }: StatusCardProps) {
type="dashboard"
status="normal"
strokeColor={status.swap.color}
trailColor={TRAIL_COLOR}
railColor={railColor}
strokeWidth={strokeWidth}
percent={status.swap.percent}
size={gaugeSize}
/>
@ -90,7 +97,8 @@ export default function StatusCard({ status, isMobile }: StatusCardProps) {
type="dashboard"
status="normal"
strokeColor={status.disk.color}
trailColor={TRAIL_COLOR}
railColor={railColor}
strokeWidth={strokeWidth}
percent={status.disk.percent}
size={gaugeSize}
/>

View file

@ -106,7 +106,6 @@ export default function SystemHistoryModal({ open, status, onClose }: SystemHist
return (
<Modal
open={open}
closable
footer={null}
width={isMobile ? '95vw' : 900}
onCancel={onClose}

View file

@ -98,7 +98,6 @@ export default function VersionModal({ open, status, onClose, onBusy }: VersionM
<Modal
open={open}
title={t('pages.index.xrayUpdates')}
closable
footer={null}
onCancel={onClose}
>
@ -117,7 +116,7 @@ export default function VersionModal({ open, status, onClose, onBusy }: VersionM
<Alert
type="warning"
className="mb-12"
message={t('pages.index.xraySwitchClickDesk')}
title={t('pages.index.xraySwitchClickDesk')}
showIcon
/>
<List bordered className="version-list">

View file

@ -110,7 +110,6 @@ export default function XrayLogModal({ open, onClose }: XrayLogModalProps) {
return (
<Modal
open={open}
closable
footer={null}
width={isMobile ? '100vw' : '80vw'}
className={isMobile ? 'xraylog-modal-mobile' : undefined}

View file

@ -220,7 +220,6 @@ export default function XrayMetricsModal({ open, onClose }: XrayMetricsModalProp
return (
<Modal
open={open}
closable
footer={null}
width={isMobile ? '95vw' : 900}
onCancel={onClose}
@ -249,7 +248,7 @@ export default function XrayMetricsModal({ open, onClose }: XrayMetricsModalProp
type="warning"
showIcon
className="metrics-alert"
message={t('pages.index.xrayMetricsDisabled')}
title={t('pages.index.xrayMetricsDisabled')}
description={state.reason || t('pages.index.xrayMetricsHint')}
/>
)}
@ -269,7 +268,7 @@ export default function XrayMetricsModal({ open, onClose }: XrayMetricsModalProp
type="info"
showIcon
className="metrics-alert"
message={t('pages.index.xrayObservatoryEmpty')}
title={t('pages.index.xrayObservatoryEmpty')}
description={t('pages.index.xrayObservatoryHint')}
/>
) : (

View file

@ -50,7 +50,7 @@ export default function XrayStatusCard({
const stateText = t(XRAY_STATE_KEYS[status.xray.state] ?? 'pages.index.xrayStatusUnknown');
const title = (
<Space direction="horizontal">
<Space>
<span>{t('pages.index.xrayStatus')}</span>
{isMobile && status.xray.version && status.xray.version !== 'Unknown' && (
<Tag color="green">v{status.xray.version}</Tag>
@ -105,21 +105,21 @@ export default function XrayStatusCard({
const actions = [
...(ipLimitEnable
? [
<Space direction="horizontal" className="action" key="xraylogs" onClick={onOpenXrayLogs}>
<Space className="action" key="xraylogs" onClick={onOpenXrayLogs}>
<BarsOutlined />
{!isMobile && <span>{t('pages.index.logs')}</span>}
</Space>,
]
: []),
<Space direction="horizontal" className="action" key="stop" onClick={onStopXray}>
<Space className="action" key="stop" onClick={onStopXray}>
<PoweroffOutlined />
{!isMobile && <span>{t('pages.index.stopXray')}</span>}
</Space>,
<Space direction="horizontal" className="action" key="restart" onClick={onRestartXray}>
<Space className="action" key="restart" onClick={onRestartXray}>
<ReloadOutlined />
{!isMobile && <span>{t('pages.index.restartXray')}</span>}
</Space>,
<Space direction="horizontal" className="action" key="switch" onClick={onOpenVersionSwitch}>
<Space className="action" key="switch" onClick={onOpenVersionSwitch}>
<ToolOutlined />
{!isMobile && (
<span>

View file

@ -145,12 +145,12 @@ export default function LoginPage() {
{themeIcon}
</button>
<Popover
overlayClassName={isDark ? 'dark' : 'light'}
rootClassName={isDark ? 'dark' : 'light'}
title={t('pages.settings.language')}
placement="bottomRight"
trigger="click"
content={
<Space direction="vertical" size={10} className="settings-popover">
<Space orientation="vertical" size={10} className="settings-popover">
<Select
className="lang-select"
value={lang}
@ -240,7 +240,7 @@ export default function LoginPage() {
size="large"
block
>
{submitting ? '' : t('login')}
{t('login')}
</Button>
</Form.Item>
</Form>

View file

@ -174,7 +174,7 @@ export default function NodeFormModal({
confirmLoading={submitting}
okText={t('save')}
cancelText={t('cancel')}
maskClosable={false}
mask={{ closable: false }}
width="640px"
onOk={onSave}
onCancel={close}
@ -276,14 +276,14 @@ export default function NodeFormModal({
<Alert
type="success"
showIcon
message={t('pages.nodes.connectionOk', { ms: testResult.latencyMs })}
title={t('pages.nodes.connectionOk', { ms: testResult.latencyMs })}
description={testResult.xrayVersion ? `Xray ${testResult.xrayVersion}` : undefined}
/>
) : (
<Alert
type="error"
showIcon
message={t('pages.nodes.connectionFailed')}
title={t('pages.nodes.connectionFailed')}
description={testResult.error}
/>
)}

View file

@ -111,7 +111,7 @@ export default function NodesPage() {
<Layout className="content-shell">
<Layout.Content id="content-layout" className="content-area">
<Spin spinning={!fetched} delay={200} tip="Loading…" size="large">
<Spin spinning={!fetched} delay={200} description="Loading…" size="large">
{!fetched ? (
<div className="loading-spacer" />
) : (

View file

@ -279,7 +279,7 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
onChange={(e) => updateUserField('newPassword', e.target.value)} />
</SettingListItem>
<List.Item>
<Space direction="horizontal" style={{ padding: '0 20px' }}>
<Space style={{ padding: '0 20px' }}>
<Button type="primary" loading={updating} onClick={onUpdateUserClick}>
{t('confirm')}
</Button>

View file

@ -2,11 +2,11 @@ import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Alert,
BackTop,
Button,
Card,
Col,
ConfigProvider,
FloatButton,
Layout,
Modal,
Row,
@ -265,7 +265,7 @@ export default function SettingsPage() {
<Layout className="content-shell">
<Layout.Content id="content-layout" className="content-area">
<Spin spinning={spinning || !fetched} delay={200} tip="Loading…" size="large">
<Spin spinning={spinning || !fetched} delay={200} description="Loading…" size="large">
{!fetched ? (
<div className="loading-spacer" />
) : (
@ -277,7 +277,7 @@ export default function SettingsPage() {
closable
className="conf-alert"
onClose={() => setAlertVisible(false)}
message="Security warnings"
title="Security warnings"
description={(
<>
<b>Your panel may be exposed:</b>
@ -294,7 +294,7 @@ export default function SettingsPage() {
<Card hoverable>
<Row className="header-row">
<Col xs={24} sm={10} className="header-actions">
<Space direction="horizontal">
<Space>
<Button type="primary" disabled={saveDisabled} onClick={saveAll}>
{t('pages.settings.save')}
</Button>
@ -304,8 +304,8 @@ export default function SettingsPage() {
</Space>
</Col>
<Col xs={24} sm={14} className="header-info">
<BackTop target={scrollTarget} visibilityHeight={200} />
<Alert type="warning" showIcon message={t('pages.settings.infoDesc')} />
<FloatButton.BackTop target={scrollTarget} visibilityHeight={200} />
<Alert type="warning" showIcon title={t('pages.settings.infoDesc')} />
</Col>
</Row>
</Card>

View file

@ -329,7 +329,7 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su
options={['ip', 'ipv4', 'ipv6'].map((p) => ({ value: p, label: p }))}
/>
</SettingListItem>
<Space direction="horizontal" style={{ padding: '10px 20px' }}>
<Space style={{ padding: '10px 20px' }}>
{noisesArray.length > 1 && (
<Button type="primary" danger onClick={() => removeNoise(index)}>
{t('delete')}

View file

@ -256,7 +256,7 @@ export default function SubPage() {
placement="bottomRight"
trigger="click"
content={
<Space direction="vertical" size={10} className="settings-popover">
<Space orientation="vertical" size={10} className="settings-popover">
<Select
className="lang-select"
value={lang}

View file

@ -97,7 +97,7 @@ export default function BalancerFormModal({
okText={okText}
cancelText={t('close')}
okButtonProps={{ disabled: !isValid }}
maskClosable={false}
mask={{ closable: false }}
onOk={submit}
onCancel={onClose}
>

View file

@ -306,7 +306,7 @@ export default function BalancersTab({
return (
<>
{modalContextHolder}
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
{rows.length === 0 ? (
<Empty description={t('emptyBalancersDesc')}>
<Button type="primary" icon={<PlusOutlined />} onClick={openAdd}>

View file

@ -206,7 +206,7 @@ export default function BasicsTab({
<Alert
type="warning"
className="mb-12 hint-alert"
message={t('pages.xray.generalConfigsDesc')}
title={t('pages.xray.generalConfigsDesc')}
icon={<ExclamationCircleFilled style={{ color: '#FFA031' }} />}
/>
<SettingListItem
@ -300,7 +300,7 @@ export default function BasicsTab({
<Alert
type="warning"
className="mb-12 hint-alert"
message={t('pages.xray.logConfigsDesc')}
title={t('pages.xray.logConfigsDesc')}
icon={<ExclamationCircleFilled style={{ color: '#FFA031' }} />}
/>
<SettingListItem
@ -377,7 +377,7 @@ export default function BasicsTab({
<Alert
type="warning"
className="mb-12 hint-alert"
message={t('pages.xray.blockConnectionsConfigsDesc')}
title={t('pages.xray.blockConnectionsConfigsDesc')}
icon={<ExclamationCircleFilled style={{ color: '#FFA031' }} />}
/>
@ -428,7 +428,7 @@ export default function BasicsTab({
<Alert
type="warning"
className="mb-12 hint-alert"
message={t('pages.xray.directConnectionsConfigsDesc')}
title={t('pages.xray.directConnectionsConfigsDesc')}
icon={<ExclamationCircleFilled style={{ color: '#FFA031' }} />}
/>
@ -532,7 +532,7 @@ export default function BasicsTab({
key: 'reset',
label: t('pages.settings.resetDefaultConfig'),
children: (
<Space direction="horizontal" style={{ padding: '0 20px' }}>
<Space style={{ padding: '0 20px' }}>
<Button danger onClick={confirmResetDefault}>
{t('pages.settings.resetDefaultConfig')}
</Button>

View file

@ -44,7 +44,7 @@ export default function DnsPresetsModal({ open, onClose, onInstall }: DnsPresets
open={open}
title={t('pages.xray.dns.dnsPresetTitle')}
footer={null}
maskClosable={false}
mask={{ closable: false }}
onCancel={onClose}
>
<List bordered>

View file

@ -156,7 +156,7 @@ export default function DnsServerModal({
title={title}
okText={t('confirm')}
cancelText={t('close')}
maskClosable={false}
mask={{ closable: false }}
onOk={submit}
onCancel={onClose}
>

View file

@ -427,7 +427,7 @@ export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTab
</Button>
</Empty>
) : (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
<Button type="primary" icon={<PlusOutlined />} onClick={() => syncHosts([...hostsList, { domain: '', values: [] }])}>
{t('pages.xray.dns.hostsAdd')}
</Button>
@ -475,7 +475,7 @@ export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTab
</Space>
</Empty>
) : (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
<Space wrap>
<Button type="primary" icon={<PlusOutlined />} onClick={openAddServer}>
{t('pages.xray.dns.add')}
@ -509,7 +509,7 @@ export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTab
</Button>
</Empty>
) : (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
<Button type="primary" icon={<PlusOutlined />} onClick={addFakedns}>
{t('pages.xray.fakedns.add')}
</Button>

View file

@ -235,7 +235,7 @@ export default function NordModal({
}
return (
<Modal open={open} title="NordVPN NordLynx" footer={null} closable maskClosable onCancel={onClose}>
<Modal open={open} title="NordVPN NordLynx" footer={null} onCancel={onClose}>
{nordData == null ? (
<Tabs
defaultActiveKey="token"

View file

@ -266,7 +266,7 @@ export default function OutboundFormModal({
title={title}
okText={okText}
cancelText={t('close')}
maskClosable={false}
mask={{ closable: false }}
width={780}
onOk={onOk}
onCancel={onClose}
@ -434,7 +434,7 @@ export default function OutboundFormModal({
key: '2',
label: 'JSON',
children: (
<Space direction="vertical" size={10} style={{ width: '100%', marginTop: 10 }}>
<Space orientation="vertical" size={10} style={{ width: '100%', marginTop: 10 }}>
<Input.Search
value={linkInput}
placeholder="vmess:// vless:// trojan:// ss:// hysteria2://"

View file

@ -326,7 +326,7 @@ export default function OutboundsTab({
return (
<Popover
placement="topLeft"
overlayClassName="outbound-test-popover"
rootClassName="outbound-test-popover"
content={
<div className="timing-breakdown">
<div className={`td-head ${r.success ? 'ok' : 'fail'}`}>
@ -386,7 +386,7 @@ export default function OutboundsTab({
return (
<>
{modalContextHolder}
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
<Row gutter={[12, 12]} align="middle" justify="space-between">
<Col xs={24} sm={12}>
<Space size="small" wrap>

View file

@ -402,7 +402,7 @@ export default function RoutingTab({
return (
<>
{modalContextHolder}
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
<Button type="primary" icon={<PlusOutlined />} onClick={openAdd}>
{t('pages.xray.Routings')}
</Button>

View file

@ -149,7 +149,7 @@ export default function RuleFormModal({
title={title}
okText={okText}
cancelText={t('close')}
maskClosable={false}
mask={{ closable: false }}
width={640}
onOk={submit}
onCancel={onClose}

View file

@ -207,7 +207,7 @@ export default function WarpModal({
const hasConfig = !ObjectUtil.isEmpty(warpConfig);
return (
<Modal open={open} title="Cloudflare WARP" footer={null} closable maskClosable onCancel={onClose}>
<Modal open={open} title="Cloudflare WARP" footer={null} onCancel={onClose}>
{!hasWarp ? (
<Button type="primary" loading={loading} icon={<ApiOutlined />} onClick={register}>
Create WARP account
@ -268,7 +268,7 @@ export default function WarpModal({
Update
</Button>
{licenseError && (
<Alert message={licenseError} type="error" showIcon className="license-error" />
<Alert title={licenseError} type="error" showIcon className="license-error" />
)}
</div>
</Form.Item>

View file

@ -1,12 +1,12 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
BackTop,
Alert,
Button,
Card,
Col,
ConfigProvider,
FloatButton,
Layout,
message,
Modal,
@ -258,7 +258,7 @@ export default function XrayPage() {
<Layout className="content-shell">
<Layout.Content id="content-layout" className="content-area">
<Spin spinning={spinning || !fetched} delay={200} tip="Loading…" size="large">
<Spin spinning={spinning || !fetched} delay={200} description="Loading…" size="large">
{!fetched ? (
<div className="loading-spacer" />
) : fetchError ? (
@ -274,7 +274,7 @@ export default function XrayPage() {
<Card hoverable>
<Row className="header-row">
<Col xs={24} sm={14} className="header-actions">
<Space direction="horizontal">
<Space>
<Button type="primary" disabled={saveDisabled} onClick={onSaveAll}>
{t('pages.xray.save')}
</Button>
@ -293,8 +293,8 @@ export default function XrayPage() {
</Space>
</Col>
<Col xs={24} sm={10} className="header-info">
<BackTop target={scrollTarget} visibilityHeight={200} />
<Alert type="warning" showIcon message={t('pages.settings.infoDesc')} />
<FloatButton.BackTop target={scrollTarget} visibilityHeight={200} />
<Alert type="warning" showIcon title={t('pages.settings.infoDesc')} />
</Col>
</Row>
</Card>