refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens

- Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>)
  and its unscoped global `.ant-statistic-*` CSS overrides; consumers
  (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD
  `<Statistic>` directly.
- Add Statistic component tokens to ConfigProvider so the title (11px)
  and content (17px) font sizes still apply, without `!important`
  global selectors.
- Move dark / ultra-dark card border colours from `body.dark .ant-card`
  + `html[data-theme='ultra-dark'] .ant-card` selectors into Card
  `colorBorderSecondary` tokens; page-cards.css now only carries the
  custom radius/shadow/transition that has no token equivalent.
- Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot
  keyframe and per-state ring-colour overrides; AntD `<Badge
  status="processing" color={…}>` already pulses the ring in the same
  colour, no extra CSS needed.
This commit is contained in:
MHSanaei 2026-05-25 03:47:49 +02:00
parent 6f9fdb154d
commit 7e5f279284
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
11 changed files with 49 additions and 384 deletions

View file

@ -1,52 +0,0 @@
.ant-statistic-content {
font-size: 17px !important;
line-height: 1.4 !important;
font-weight: 600;
}
.ant-statistic-content-value,
.ant-statistic-content-prefix,
.ant-statistic-content-suffix {
font-size: 17px !important;
}
.ant-statistic-content-prefix {
margin-inline-end: 8px !important;
opacity: 0.7;
}
.ant-statistic-content-prefix .anticon {
font-size: 17px !important;
}
.ant-statistic-content-suffix {
font-size: 12px !important;
opacity: 0.55;
margin-inline-start: 4px;
font-weight: 500;
}
.ant-statistic-title {
font-size: 11px !important;
margin-bottom: 6px !important;
letter-spacing: 0.6px;
text-transform: uppercase;
color: rgba(0, 0, 0, 0.55);
font-weight: 500;
}
body.dark .ant-statistic-content {
color: rgba(255, 255, 255, 0.92);
}
body.dark .ant-statistic-title {
color: rgba(255, 255, 255, 0.72);
}
html[data-theme='ultra-dark'] .ant-statistic-content {
color: rgba(255, 255, 255, 0.95);
}
html[data-theme='ultra-dark'] .ant-statistic-title {
color: rgba(255, 255, 255, 0.70);
}

View file

@ -1,14 +0,0 @@
import type { ReactNode } from 'react';
import { Statistic } from 'antd';
import './CustomStatistic.css';
interface CustomStatisticProps {
title?: string;
value?: string | number;
prefix?: ReactNode;
suffix?: ReactNode;
}
export default function CustomStatistic({ title = '', value = '', prefix, suffix }: CustomStatisticProps) {
return <Statistic title={title} value={value} prefix={prefix} suffix={suffix} />;
}

View file

@ -68,10 +68,25 @@ const ULTRA_DARK_MENU_TOKENS = {
darkSubMenuItemBg: '#000', darkSubMenuItemBg: '#000',
darkPopupBg: '#101013', darkPopupBg: '#101013',
}; };
const DARK_CARD_TOKENS = {
colorBorderSecondary: 'rgba(255, 255, 255, 0.06)',
};
const ULTRA_DARK_CARD_TOKENS = {
colorBorderSecondary: 'rgba(255, 255, 255, 0.04)',
};
const STATISTIC_TOKENS = {
contentFontSize: 17,
titleFontSize: 11,
};
export function buildAntdThemeConfig(isDark: boolean, isUltra: boolean): ThemeConfig { export function buildAntdThemeConfig(isDark: boolean, isUltra: boolean): ThemeConfig {
if (!isDark) { if (!isDark) {
return { algorithm: antdTheme.defaultAlgorithm }; return {
algorithm: antdTheme.defaultAlgorithm,
components: {
Statistic: STATISTIC_TOKENS,
},
};
} }
return { return {
algorithm: antdTheme.darkAlgorithm, algorithm: antdTheme.darkAlgorithm,
@ -79,6 +94,8 @@ export function buildAntdThemeConfig(isDark: boolean, isUltra: boolean): ThemeCo
components: { components: {
Layout: isUltra ? ULTRA_DARK_LAYOUT_TOKENS : DARK_LAYOUT_TOKENS, Layout: isUltra ? ULTRA_DARK_LAYOUT_TOKENS : DARK_LAYOUT_TOKENS,
Menu: isUltra ? ULTRA_DARK_MENU_TOKENS : DARK_MENU_TOKENS, Menu: isUltra ? ULTRA_DARK_MENU_TOKENS : DARK_MENU_TOKENS,
Card: isUltra ? ULTRA_DARK_CARD_TOKENS : DARK_CARD_TOKENS,
Statistic: STATISTIC_TOKENS,
}, },
}; };
} }

View file

@ -18,6 +18,7 @@ import {
Select, Select,
Space, Space,
Spin, Spin,
Statistic,
Switch, Switch,
Table, Table,
Tag, Tag,
@ -49,7 +50,6 @@ import { useClients } from '@/hooks/useClients';
import { useDatepicker } from '@/hooks/useDatepicker'; import { useDatepicker } from '@/hooks/useDatepicker';
import type { ClientRecord, InboundOption } from '@/hooks/useClients'; import type { ClientRecord, InboundOption } from '@/hooks/useClients';
import AppSidebar from '@/components/AppSidebar'; import AppSidebar from '@/components/AppSidebar';
import CustomStatistic from '@/components/CustomStatistic';
import { IntlUtil, SizeFormatter } from '@/utils'; import { IntlUtil, SizeFormatter } from '@/utils';
import { setMessageInstance } from '@/utils/messageBus'; import { setMessageInstance } from '@/utils/messageBus';
import LazyMount from '@/components/LazyMount'; import LazyMount from '@/components/LazyMount';
@ -624,7 +624,7 @@ export default function ClientsPage() {
<Card size="small" hoverable className="summary-card"> <Card size="small" hoverable className="summary-card">
<Row gutter={[16, 12]}> <Row gutter={[16, 12]}>
<Col xs={12} sm={8} md={4}> <Col xs={12} sm={8} md={4}>
<CustomStatistic title={t('clients')} value={String(summary.total)} prefix={<TeamOutlined />} /> <Statistic title={t('clients')} value={String(summary.total)} prefix={<TeamOutlined />} />
</Col> </Col>
<Col xs={12} sm={8} md={4}> <Col xs={12} sm={8} md={4}>
<Popover <Popover
@ -632,7 +632,7 @@ export default function ClientsPage() {
open={summary.online.length ? undefined : false} open={summary.online.length ? undefined : false}
content={<div className="client-email-list">{summary.online.map((e) => <div key={e}>{e}</div>)}</div>} content={<div className="client-email-list">{summary.online.map((e) => <div key={e}>{e}</div>)}</div>}
> >
<CustomStatistic title={t('online')} value={String(summary.online.length)} prefix={<span className="dot dot-blue" />} /> <Statistic title={t('online')} value={String(summary.online.length)} prefix={<span className="dot dot-blue" />} />
</Popover> </Popover>
</Col> </Col>
<Col xs={12} sm={8} md={4}> <Col xs={12} sm={8} md={4}>
@ -641,7 +641,7 @@ export default function ClientsPage() {
open={summary.depleted.length ? undefined : false} open={summary.depleted.length ? undefined : false}
content={<div className="client-email-list">{summary.depleted.map((e) => <div key={e}>{e}</div>)}</div>} content={<div className="client-email-list">{summary.depleted.map((e) => <div key={e}>{e}</div>)}</div>}
> >
<CustomStatistic title={t('depleted')} value={String(summary.depleted.length)} prefix={<span className="dot dot-red" />} /> <Statistic title={t('depleted')} value={String(summary.depleted.length)} prefix={<span className="dot dot-red" />} />
</Popover> </Popover>
</Col> </Col>
<Col xs={12} sm={8} md={4}> <Col xs={12} sm={8} md={4}>
@ -650,7 +650,7 @@ export default function ClientsPage() {
open={summary.expiring.length ? undefined : false} open={summary.expiring.length ? undefined : false}
content={<div className="client-email-list">{summary.expiring.map((e) => <div key={e}>{e}</div>)}</div>} content={<div className="client-email-list">{summary.expiring.map((e) => <div key={e}>{e}</div>)}</div>}
> >
<CustomStatistic title={t('depletingSoon')} value={String(summary.expiring.length)} prefix={<span className="dot dot-orange" />} /> <Statistic title={t('depletingSoon')} value={String(summary.expiring.length)} prefix={<span className="dot dot-orange" />} />
</Popover> </Popover>
</Col> </Col>
<Col xs={12} sm={8} md={4}> <Col xs={12} sm={8} md={4}>
@ -659,11 +659,11 @@ export default function ClientsPage() {
open={summary.deactive.length ? undefined : false} open={summary.deactive.length ? undefined : false}
content={<div className="client-email-list">{summary.deactive.map((e) => <div key={e}>{e}</div>)}</div>} content={<div className="client-email-list">{summary.deactive.map((e) => <div key={e}>{e}</div>)}</div>}
> >
<CustomStatistic title={t('disabled')} value={String(summary.deactive.length)} prefix={<span className="dot dot-gray" />} /> <Statistic title={t('disabled')} value={String(summary.deactive.length)} prefix={<span className="dot dot-gray" />} />
</Popover> </Popover>
</Col> </Col>
<Col xs={12} sm={8} md={4}> <Col xs={12} sm={8} md={4}>
<CustomStatistic title={t('subscription.active')} value={String(summary.active)} prefix={<span className="dot dot-green" />} /> <Statistic title={t('subscription.active')} value={String(summary.active)} prefix={<span className="dot dot-green" />} />
</Col> </Col>
</Row> </Row>
</Card> </Card>

View file

@ -8,6 +8,7 @@ import {
Modal, Modal,
Row, Row,
Spin, Spin,
Statistic,
message, message,
} from 'antd'; } from 'antd';
@ -26,7 +27,6 @@ import { useMediaQuery } from '@/hooks/useMediaQuery';
import { useWebSocket } from '@/hooks/useWebSocket'; import { useWebSocket } from '@/hooks/useWebSocket';
import { useNodesQuery } from '@/api/queries/useNodesQuery'; import { useNodesQuery } from '@/api/queries/useNodesQuery';
import AppSidebar from '@/components/AppSidebar'; import AppSidebar from '@/components/AppSidebar';
import CustomStatistic from '@/components/CustomStatistic';
const TextModal = lazy(() => import('@/components/TextModal')); const TextModal = lazy(() => import('@/components/TextModal'));
const PromptModal = lazy(() => import('@/components/PromptModal')); const PromptModal = lazy(() => import('@/components/PromptModal'));
@ -463,21 +463,21 @@ export default function InboundsPage() {
<Card size="small" hoverable className="summary-card"> <Card size="small" hoverable className="summary-card">
<Row gutter={[16, 12]}> <Row gutter={[16, 12]}>
<Col xs={12} sm={12} md={8}> <Col xs={12} sm={12} md={8}>
<CustomStatistic <Statistic
title={t('pages.inbounds.totalDownUp')} title={t('pages.inbounds.totalDownUp')}
value={`${SizeFormatter.sizeFormat(totals.up)} / ${SizeFormatter.sizeFormat(totals.down)}`} value={`${SizeFormatter.sizeFormat(totals.up)} / ${SizeFormatter.sizeFormat(totals.down)}`}
prefix={<SwapOutlined />} prefix={<SwapOutlined />}
/> />
</Col> </Col>
<Col xs={12} sm={12} md={8}> <Col xs={12} sm={12} md={8}>
<CustomStatistic <Statistic
title={t('pages.inbounds.totalUsage')} title={t('pages.inbounds.totalUsage')}
value={SizeFormatter.sizeFormat(totals.up + totals.down)} value={SizeFormatter.sizeFormat(totals.up + totals.down)}
prefix={<PieChartOutlined />} prefix={<PieChartOutlined />}
/> />
</Col> </Col>
<Col xs={24} sm={24} md={8}> <Col xs={24} sm={24} md={8}>
<CustomStatistic <Statistic
title={t('pages.inbounds.inboundCount')} title={t('pages.inbounds.inboundCount')}
value={String(dbInbounds.length)} value={String(dbInbounds.length)}
prefix={<BarsOutlined />} prefix={<BarsOutlined />}

View file

@ -40,152 +40,11 @@
min-height: calc(100vh - 120px); min-height: calc(100vh - 120px);
} }
.index-page .ant-card {
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.06);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
transition: transform 0.2s ease, box-shadow 0.25s ease, border-color 0.2s ease;
}
body.dark .index-page .ant-card {
border-color: rgba(255, 255, 255, 0.06);
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.03);
}
html[data-theme='ultra-dark'] .index-page .ant-card {
border-color: rgba(255, 255, 255, 0.04);
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.6),
inset 0 1px 0 rgba(255, 255, 255, 0.025);
}
.index-page .ant-card.ant-card-hoverable:hover {
transform: translateY(-2px);
border-color: rgba(0, 0, 0, 0.10);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
}
body.dark .index-page .ant-card.ant-card-hoverable:hover {
border-color: rgba(255, 255, 255, 0.12);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
html[data-theme='ultra-dark'] .index-page .ant-card.ant-card-hoverable:hover {
border-color: rgba(255, 255, 255, 0.08);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.75),
inset 0 1px 0 rgba(255, 255, 255, 0.03);
}
.index-page .ant-card .ant-card-head {
min-height: 44px;
padding-inline: 16px;
}
.index-page .ant-card .ant-card-head-title {
font-size: 13px;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
opacity: 0.75;
}
.index-page .ant-card .ant-card-body {
padding: 18px 20px;
}
.index-page .ant-card .ant-card-body > .ant-row > .ant-col {
position: relative;
padding: 4px 6px;
}
@media (min-width: 769px) {
.index-page .ant-card .ant-card-body > .ant-row > .ant-col + .ant-col::before {
content: '';
position: absolute;
left: 0;
top: 10%;
bottom: 10%;
width: 1px;
background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.10), transparent);
pointer-events: none;
}
}
body.dark .index-page .ant-card .ant-card-body > .ant-row > .ant-col + .ant-col::before {
background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.12), transparent);
}
.index-page .ant-card .ant-card-head {
border-bottom-color: rgba(0, 0, 0, 0.06);
}
.index-page .ant-card .ant-card-actions {
border-top-color: rgba(0, 0, 0, 0.06);
background: transparent;
}
.index-page .ant-card .ant-card-actions > li {
border-inline-end-color: rgba(0, 0, 0, 0.06);
}
body.dark .index-page .ant-card .ant-card-head {
border-bottom-color: rgba(255, 255, 255, 0.06);
}
body.dark .index-page .ant-card .ant-card-actions {
border-top-color: rgba(255, 255, 255, 0.06);
}
body.dark .index-page .ant-card .ant-card-actions > li {
border-inline-end-color: rgba(255, 255, 255, 0.06);
}
html[data-theme='ultra-dark'] .index-page .ant-card .ant-card-head {
border-bottom-color: rgba(255, 255, 255, 0.04);
}
html[data-theme='ultra-dark'] .index-page .ant-card .ant-card-actions {
border-top-color: rgba(255, 255, 255, 0.04);
}
html[data-theme='ultra-dark'] .index-page .ant-card .ant-card-actions > li {
border-inline-end-color: rgba(255, 255, 255, 0.04);
}
.index-page .action { .index-page .action {
cursor: pointer; cursor: pointer;
justify-content: center; justify-content: center;
max-width: 100%; max-width: 100%;
padding: 0 8px;
flex-wrap: nowrap; flex-wrap: nowrap;
color: rgba(0, 0, 0, 0.78);
font-weight: 500;
transition: opacity 0.15s ease, transform 0.15s ease, color 0.2s ease;
}
.index-page .action .anticon {
color: rgba(0, 0, 0, 0.72);
}
body.dark .index-page .action {
color: rgba(255, 255, 255, 0.82);
}
body.dark .index-page .action .anticon {
color: rgba(255, 255, 255, 0.75);
}
html[data-theme='ultra-dark'] .index-page .action {
color: rgba(255, 255, 255, 0.86);
}
html[data-theme='ultra-dark'] .index-page .action .anticon {
color: rgba(255, 255, 255, 0.78);
} }
.index-page .action > span:not(.anticon):not(.tg-icon) { .index-page .action > span:not(.anticon):not(.tg-icon) {
@ -195,16 +54,6 @@ html[data-theme='ultra-dark'] .index-page .action .anticon {
min-width: 0; min-width: 0;
} }
.index-page .action:hover {
opacity: 0.75;
transform: translateY(-1px);
}
.index-page .ant-card-actions > li {
margin: 8px 0;
min-width: 0;
}
.index-page .action-update { .index-page .action-update {
color: #fa8c16; color: #fa8c16;
font-weight: 600; font-weight: 600;

View file

@ -11,6 +11,7 @@ import {
Row, Row,
Space, Space,
Spin, Spin,
Statistic,
Tag, Tag,
Tooltip, Tooltip,
} from 'antd'; } from 'antd';
@ -39,7 +40,6 @@ import { useTheme } from '@/hooks/useTheme';
import { useStatusQuery } from '@/api/queries/useStatusQuery'; import { useStatusQuery } from '@/api/queries/useStatusQuery';
import { useMediaQuery } from '@/hooks/useMediaQuery'; import { useMediaQuery } from '@/hooks/useMediaQuery';
import AppSidebar from '@/components/AppSidebar'; import AppSidebar from '@/components/AppSidebar';
import CustomStatistic from '@/components/CustomStatistic';
import LazyMount from '@/components/LazyMount'; import LazyMount from '@/components/LazyMount';
import { setMessageInstance } from '@/utils/messageBus'; import { setMessageInstance } from '@/utils/messageBus';
import StatusCard from './StatusCard'; import StatusCard from './StatusCard';
@ -285,14 +285,14 @@ export default function IndexPage() {
<Card title={t('pages.index.operationHours')} hoverable> <Card title={t('pages.index.operationHours')} hoverable>
<Row gutter={isMobile ? [8, 8] : 0}> <Row gutter={isMobile ? [8, 8] : 0}>
<Col span={12}> <Col span={12}>
<CustomStatistic <Statistic
title="Xray" title="Xray"
value={TimeFormatter.formatSecond(status.appStats.uptime)} value={TimeFormatter.formatSecond(status.appStats.uptime)}
prefix={<ThunderboltOutlined />} prefix={<ThunderboltOutlined />}
/> />
</Col> </Col>
<Col span={12}> <Col span={12}>
<CustomStatistic <Statistic
title="OS" title="OS"
value={TimeFormatter.formatSecond(status.uptime)} value={TimeFormatter.formatSecond(status.uptime)}
prefix={<DesktopOutlined />} prefix={<DesktopOutlined />}
@ -306,14 +306,14 @@ export default function IndexPage() {
<Card title={t('usage')} hoverable> <Card title={t('usage')} hoverable>
<Row gutter={isMobile ? [8, 8] : 0}> <Row gutter={isMobile ? [8, 8] : 0}>
<Col span={12}> <Col span={12}>
<CustomStatistic <Statistic
title={t('pages.index.memory')} title={t('pages.index.memory')}
value={SizeFormatter.sizeFormat(status.appStats.mem)} value={SizeFormatter.sizeFormat(status.appStats.mem)}
prefix={<DatabaseOutlined />} prefix={<DatabaseOutlined />}
/> />
</Col> </Col>
<Col span={12}> <Col span={12}>
<CustomStatistic <Statistic
title={t('pages.index.threads')} title={t('pages.index.threads')}
value={status.appStats.threads} value={status.appStats.threads}
prefix={<ForkOutlined />} prefix={<ForkOutlined />}
@ -327,7 +327,7 @@ export default function IndexPage() {
<Card title={t('pages.index.overallSpeed')} hoverable> <Card title={t('pages.index.overallSpeed')} hoverable>
<Row gutter={isMobile ? [8, 8] : 0}> <Row gutter={isMobile ? [8, 8] : 0}>
<Col span={12}> <Col span={12}>
<CustomStatistic <Statistic
title={t('pages.index.upload')} title={t('pages.index.upload')}
value={SizeFormatter.sizeFormat(status.netIO.up)} value={SizeFormatter.sizeFormat(status.netIO.up)}
prefix={<ArrowUpOutlined />} prefix={<ArrowUpOutlined />}
@ -335,7 +335,7 @@ export default function IndexPage() {
/> />
</Col> </Col>
<Col span={12}> <Col span={12}>
<CustomStatistic <Statistic
title={t('pages.index.download')} title={t('pages.index.download')}
value={SizeFormatter.sizeFormat(status.netIO.down)} value={SizeFormatter.sizeFormat(status.netIO.down)}
prefix={<ArrowDownOutlined />} prefix={<ArrowDownOutlined />}
@ -350,14 +350,14 @@ export default function IndexPage() {
<Card title={t('pages.index.totalData')} hoverable> <Card title={t('pages.index.totalData')} hoverable>
<Row gutter={isMobile ? [8, 8] : 0}> <Row gutter={isMobile ? [8, 8] : 0}>
<Col span={12}> <Col span={12}>
<CustomStatistic <Statistic
title={t('pages.index.sent')} title={t('pages.index.sent')}
value={SizeFormatter.sizeFormat(status.netTraffic.sent)} value={SizeFormatter.sizeFormat(status.netTraffic.sent)}
prefix={<CloudUploadOutlined />} prefix={<CloudUploadOutlined />}
/> />
</Col> </Col>
<Col span={12}> <Col span={12}>
<CustomStatistic <Statistic
title={t('pages.index.received')} title={t('pages.index.received')}
value={SizeFormatter.sizeFormat(status.netTraffic.recv)} value={SizeFormatter.sizeFormat(status.netTraffic.recv)}
prefix={<CloudDownloadOutlined />} prefix={<CloudDownloadOutlined />}
@ -392,14 +392,14 @@ export default function IndexPage() {
> >
<Row className={showIp ? 'ip-visible' : 'ip-hidden'} gutter={isMobile ? [8, 8] : 0}> <Row className={showIp ? 'ip-visible' : 'ip-hidden'} gutter={isMobile ? [8, 8] : 0}>
<Col span={isMobile ? 24 : 12}> <Col span={isMobile ? 24 : 12}>
<CustomStatistic <Statistic
title="IPv4" title="IPv4"
value={status.publicIP.ipv4} value={status.publicIP.ipv4}
prefix={<GlobalOutlined />} prefix={<GlobalOutlined />}
/> />
</Col> </Col>
<Col span={isMobile ? 24 : 12}> <Col span={isMobile ? 24 : 12}>
<CustomStatistic <Statistic
title="IPv6" title="IPv6"
value={status.publicIP.ipv6} value={status.publicIP.ipv6}
prefix={<GlobalOutlined />} prefix={<GlobalOutlined />}
@ -413,14 +413,14 @@ export default function IndexPage() {
<Card title={t('pages.index.connectionCount')} hoverable> <Card title={t('pages.index.connectionCount')} hoverable>
<Row gutter={isMobile ? [8, 8] : 0}> <Row gutter={isMobile ? [8, 8] : 0}>
<Col span={12}> <Col span={12}>
<CustomStatistic <Statistic
title="TCP" title="TCP"
value={status.tcpCount} value={status.tcpCount}
prefix={<SwapOutlined />} prefix={<SwapOutlined />}
/> />
</Col> </Col>
<Col span={12}> <Col span={12}>
<CustomStatistic <Statistic
title="UDP" title="UDP"
value={status.udpCount} value={status.udpCount}
prefix={<SwapOutlined />} prefix={<SwapOutlined />}

View file

@ -12,33 +12,3 @@
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
} }
.xray-processing-animation .ant-badge-status-dot {
animation: xray-pulse 1.2s linear infinite;
}
.xray-running-animation .ant-badge-status-processing::after {
border-color: #1677ff;
}
.xray-stop-animation .ant-badge-status-processing::after {
border-color: #fa8c16;
}
.xray-error-animation .ant-badge-status-processing::after {
border-color: #f5222d;
}
@keyframes xray-pulse {
0%,
50%,
100% {
transform: scale(1);
opacity: 1;
}
10% {
transform: scale(1.5);
opacity: 0.2;
}
}

View file

@ -28,13 +28,6 @@ const XRAY_STATE_KEYS: Record<string, string> = {
error: 'pages.index.xrayStatusError', error: 'pages.index.xrayStatusError',
}; };
function badgeAnimationClass(color: string): string {
if (color === 'green') return 'xray-running-animation';
if (color === 'orange') return 'xray-stop-animation';
if (color === 'red') return 'xray-error-animation';
return 'xray-processing-animation';
}
export default function XrayStatusCard({ export default function XrayStatusCard({
status, status,
isMobile, isMobile,
@ -65,12 +58,7 @@ export default function XrayStatusCard({
const extra = const extra =
status.xray.state !== 'error' ? ( status.xray.state !== 'error' ? (
<Badge <Badge status="processing" text={stateText} color={status.xray.color} />
status="processing"
className={`xray-processing-animation ${badgeAnimationClass(status.xray.color)}`}
text={stateText}
color={status.xray.color}
/>
) : ( ) : (
<Popover <Popover
title={ title={
@ -93,12 +81,7 @@ export default function XrayStatusCard({
</> </>
} }
> >
<Badge <Badge status="processing" text={stateText} color={status.xray.color} />
status="processing"
text={stateText}
color={status.xray.color}
className="xray-processing-animation xray-error-animation"
/>
</Popover> </Popover>
); );

View file

@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Card, Col, ConfigProvider, Layout, Modal, Row, Spin, message } from 'antd'; import { Card, Col, ConfigProvider, Layout, Modal, Row, Spin, Statistic, message } from 'antd';
import { import {
CheckCircleOutlined, CheckCircleOutlined,
CloseCircleOutlined, CloseCircleOutlined,
@ -14,7 +14,6 @@ import { useNodesQuery } from '@/api/queries/useNodesQuery';
import type { NodeRecord } from '@/api/queries/useNodesQuery'; import type { NodeRecord } from '@/api/queries/useNodesQuery';
import { useNodeMutations } from '@/api/queries/useNodeMutations'; import { useNodeMutations } from '@/api/queries/useNodeMutations';
import AppSidebar from '@/components/AppSidebar'; import AppSidebar from '@/components/AppSidebar';
import CustomStatistic from '@/components/CustomStatistic';
import NodeList from './NodeList'; import NodeList from './NodeList';
import NodeFormModal from './NodeFormModal'; import NodeFormModal from './NodeFormModal';
import { setMessageInstance } from '@/utils/messageBus'; import { setMessageInstance } from '@/utils/messageBus';
@ -109,28 +108,28 @@ export default function NodesPage() {
<Card size="small" hoverable className="summary-card"> <Card size="small" hoverable className="summary-card">
<Row gutter={[16, isMobile ? 16 : 12]}> <Row gutter={[16, isMobile ? 16 : 12]}>
<Col xs={12} sm={12} md={6}> <Col xs={12} sm={12} md={6}>
<CustomStatistic <Statistic
title={t('pages.nodes.totalNodes')} title={t('pages.nodes.totalNodes')}
value={String(totals.total)} value={String(totals.total)}
prefix={<CloudServerOutlined />} prefix={<CloudServerOutlined />}
/> />
</Col> </Col>
<Col xs={12} sm={12} md={6}> <Col xs={12} sm={12} md={6}>
<CustomStatistic <Statistic
title={t('pages.nodes.onlineNodes')} title={t('pages.nodes.onlineNodes')}
value={String(totals.online)} value={String(totals.online)}
prefix={<CheckCircleOutlined style={{ color: '#52c41a' }} />} prefix={<CheckCircleOutlined style={{ color: '#52c41a' }} />}
/> />
</Col> </Col>
<Col xs={12} sm={12} md={6}> <Col xs={12} sm={12} md={6}>
<CustomStatistic <Statistic
title={t('pages.nodes.offlineNodes')} title={t('pages.nodes.offlineNodes')}
value={String(totals.offline)} value={String(totals.offline)}
prefix={<CloseCircleOutlined style={{ color: '#ff4d4f' }} />} prefix={<CloseCircleOutlined style={{ color: '#ff4d4f' }} />}
/> />
</Col> </Col>
<Col xs={12} sm={12} md={6}> <Col xs={12} sm={12} md={6}>
<CustomStatistic <Statistic
title={t('pages.nodes.avgLatency')} title={t('pages.nodes.avgLatency')}
value={totals.avgLatency > 0 ? `${totals.avgLatency} ms` : '-'} value={totals.avgLatency > 0 ? `${totals.avgLatency} ms` : '-'}
prefix={<ThunderboltOutlined />} prefix={<ThunderboltOutlined />}

View file

@ -6,7 +6,6 @@
.nodes-page .ant-card, .nodes-page .ant-card,
.api-docs-page .ant-card { .api-docs-page .ant-card {
border-radius: 12px; border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.06);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
transition: transform 0.2s ease, box-shadow 0.25s ease, border-color 0.2s ease; transition: transform 0.2s ease, box-shadow 0.25s ease, border-color 0.2s ease;
} }
@ -18,7 +17,6 @@ body.dark .xray-page .ant-card,
body.dark .settings-page .ant-card, body.dark .settings-page .ant-card,
body.dark .nodes-page .ant-card, body.dark .nodes-page .ant-card,
body.dark .api-docs-page .ant-card { body.dark .api-docs-page .ant-card {
border-color: rgba(255, 255, 255, 0.06);
box-shadow: box-shadow:
0 1px 2px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.03); inset 0 1px 0 rgba(255, 255, 255, 0.03);
@ -31,7 +29,6 @@ html[data-theme='ultra-dark'] .xray-page .ant-card,
html[data-theme='ultra-dark'] .settings-page .ant-card, html[data-theme='ultra-dark'] .settings-page .ant-card,
html[data-theme='ultra-dark'] .nodes-page .ant-card, html[data-theme='ultra-dark'] .nodes-page .ant-card,
html[data-theme='ultra-dark'] .api-docs-page .ant-card { html[data-theme='ultra-dark'] .api-docs-page .ant-card {
border-color: rgba(255, 255, 255, 0.04);
box-shadow: box-shadow:
0 1px 2px rgba(0, 0, 0, 0.6), 0 1px 2px rgba(0, 0, 0, 0.6),
inset 0 1px 0 rgba(255, 255, 255, 0.025); inset 0 1px 0 rgba(255, 255, 255, 0.025);
@ -45,7 +42,6 @@ html[data-theme='ultra-dark'] .api-docs-page .ant-card {
.nodes-page .ant-card.ant-card-hoverable:hover, .nodes-page .ant-card.ant-card-hoverable:hover,
.api-docs-page .ant-card.ant-card-hoverable:hover { .api-docs-page .ant-card.ant-card-hoverable:hover {
transform: translateY(-2px); transform: translateY(-2px);
border-color: rgba(0, 0, 0, 0.10);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
} }
@ -56,7 +52,6 @@ body.dark .xray-page .ant-card.ant-card-hoverable:hover,
body.dark .settings-page .ant-card.ant-card-hoverable:hover, body.dark .settings-page .ant-card.ant-card-hoverable:hover,
body.dark .nodes-page .ant-card.ant-card-hoverable:hover, body.dark .nodes-page .ant-card.ant-card-hoverable:hover,
body.dark .api-docs-page .ant-card.ant-card-hoverable:hover { body.dark .api-docs-page .ant-card.ant-card-hoverable:hover {
border-color: rgba(255, 255, 255, 0.12);
box-shadow: box-shadow:
0 8px 24px rgba(0, 0, 0, 0.5), 0 8px 24px rgba(0, 0, 0, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.04); inset 0 1px 0 rgba(255, 255, 255, 0.04);
@ -69,22 +64,11 @@ html[data-theme='ultra-dark'] .xray-page .ant-card.ant-card-hoverable:hover,
html[data-theme='ultra-dark'] .settings-page .ant-card.ant-card-hoverable:hover, html[data-theme='ultra-dark'] .settings-page .ant-card.ant-card-hoverable:hover,
html[data-theme='ultra-dark'] .nodes-page .ant-card.ant-card-hoverable:hover, html[data-theme='ultra-dark'] .nodes-page .ant-card.ant-card-hoverable:hover,
html[data-theme='ultra-dark'] .api-docs-page .ant-card.ant-card-hoverable:hover { html[data-theme='ultra-dark'] .api-docs-page .ant-card.ant-card-hoverable:hover {
border-color: rgba(255, 255, 255, 0.08);
box-shadow: box-shadow:
0 8px 24px rgba(0, 0, 0, 0.75), 0 8px 24px rgba(0, 0, 0, 0.75),
inset 0 1px 0 rgba(255, 255, 255, 0.03); inset 0 1px 0 rgba(255, 255, 255, 0.03);
} }
.index-page .ant-card .ant-card-head,
.clients-page .ant-card .ant-card-head,
.inbounds-page .ant-card .ant-card-head,
.xray-page .ant-card .ant-card-head,
.settings-page .ant-card .ant-card-head,
.nodes-page .ant-card .ant-card-head,
.api-docs-page .ant-card .ant-card-head {
border-bottom-color: rgba(0, 0, 0, 0.06);
}
.index-page .ant-card .ant-card-actions, .index-page .ant-card .ant-card-actions,
.clients-page .ant-card .ant-card-actions, .clients-page .ant-card .ant-card-actions,
.inbounds-page .ant-card .ant-card-actions, .inbounds-page .ant-card .ant-card-actions,
@ -92,76 +76,5 @@ html[data-theme='ultra-dark'] .api-docs-page .ant-card.ant-card-hoverable:hover
.settings-page .ant-card .ant-card-actions, .settings-page .ant-card .ant-card-actions,
.nodes-page .ant-card .ant-card-actions, .nodes-page .ant-card .ant-card-actions,
.api-docs-page .ant-card .ant-card-actions { .api-docs-page .ant-card .ant-card-actions {
border-top-color: rgba(0, 0, 0, 0.06);
background: transparent; background: transparent;
} }
.index-page .ant-card .ant-card-actions > li,
.clients-page .ant-card .ant-card-actions > li,
.inbounds-page .ant-card .ant-card-actions > li,
.xray-page .ant-card .ant-card-actions > li,
.settings-page .ant-card .ant-card-actions > li,
.nodes-page .ant-card .ant-card-actions > li,
.api-docs-page .ant-card .ant-card-actions > li {
border-inline-end-color: rgba(0, 0, 0, 0.06);
}
body.dark .index-page .ant-card .ant-card-head,
body.dark .clients-page .ant-card .ant-card-head,
body.dark .inbounds-page .ant-card .ant-card-head,
body.dark .xray-page .ant-card .ant-card-head,
body.dark .settings-page .ant-card .ant-card-head,
body.dark .nodes-page .ant-card .ant-card-head,
body.dark .api-docs-page .ant-card .ant-card-head {
border-bottom-color: rgba(255, 255, 255, 0.06);
}
body.dark .index-page .ant-card .ant-card-actions,
body.dark .clients-page .ant-card .ant-card-actions,
body.dark .inbounds-page .ant-card .ant-card-actions,
body.dark .xray-page .ant-card .ant-card-actions,
body.dark .settings-page .ant-card .ant-card-actions,
body.dark .nodes-page .ant-card .ant-card-actions,
body.dark .api-docs-page .ant-card .ant-card-actions {
border-top-color: rgba(255, 255, 255, 0.06);
}
body.dark .index-page .ant-card .ant-card-actions > li,
body.dark .clients-page .ant-card .ant-card-actions > li,
body.dark .inbounds-page .ant-card .ant-card-actions > li,
body.dark .xray-page .ant-card .ant-card-actions > li,
body.dark .settings-page .ant-card .ant-card-actions > li,
body.dark .nodes-page .ant-card .ant-card-actions > li,
body.dark .api-docs-page .ant-card .ant-card-actions > li {
border-inline-end-color: rgba(255, 255, 255, 0.06);
}
html[data-theme='ultra-dark'] .index-page .ant-card .ant-card-head,
html[data-theme='ultra-dark'] .clients-page .ant-card .ant-card-head,
html[data-theme='ultra-dark'] .inbounds-page .ant-card .ant-card-head,
html[data-theme='ultra-dark'] .xray-page .ant-card .ant-card-head,
html[data-theme='ultra-dark'] .settings-page .ant-card .ant-card-head,
html[data-theme='ultra-dark'] .nodes-page .ant-card .ant-card-head,
html[data-theme='ultra-dark'] .api-docs-page .ant-card .ant-card-head {
border-bottom-color: rgba(255, 255, 255, 0.04);
}
html[data-theme='ultra-dark'] .index-page .ant-card .ant-card-actions,
html[data-theme='ultra-dark'] .clients-page .ant-card .ant-card-actions,
html[data-theme='ultra-dark'] .inbounds-page .ant-card .ant-card-actions,
html[data-theme='ultra-dark'] .xray-page .ant-card .ant-card-actions,
html[data-theme='ultra-dark'] .settings-page .ant-card .ant-card-actions,
html[data-theme='ultra-dark'] .nodes-page .ant-card .ant-card-actions,
html[data-theme='ultra-dark'] .api-docs-page .ant-card .ant-card-actions {
border-top-color: rgba(255, 255, 255, 0.04);
}
html[data-theme='ultra-dark'] .index-page .ant-card .ant-card-actions > li,
html[data-theme='ultra-dark'] .clients-page .ant-card .ant-card-actions > li,
html[data-theme='ultra-dark'] .inbounds-page .ant-card .ant-card-actions > li,
html[data-theme='ultra-dark'] .xray-page .ant-card .ant-card-actions > li,
html[data-theme='ultra-dark'] .settings-page .ant-card .ant-card-actions > li,
html[data-theme='ultra-dark'] .nodes-page .ant-card .ant-card-actions > li,
html[data-theme='ultra-dark'] .api-docs-page .ant-card .ant-card-actions > li {
border-inline-end-color: rgba(255, 255, 255, 0.04);
}