| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517 |
- import { lazy, useCallback, useEffect, useMemo, useState } from 'react';
- import { useTranslation } from 'react-i18next';
- import {
- Button,
- Card,
- Col,
- ConfigProvider,
- Layout,
- message,
- Modal,
- Row,
- Space,
- Spin,
- Statistic,
- Tag,
- Tooltip,
- } from 'antd';
- import {
- BarsOutlined,
- ControlOutlined,
- CloudServerOutlined,
- CloudDownloadOutlined,
- CloudUploadOutlined,
- ArrowUpOutlined,
- ArrowDownOutlined,
- AreaChartOutlined,
- GlobalOutlined,
- SwapOutlined,
- EyeOutlined,
- EyeInvisibleOutlined,
- ThunderboltOutlined,
- DesktopOutlined,
- DatabaseOutlined,
- ForkOutlined,
- CopyOutlined,
- } from '@ant-design/icons';
- import { HttpUtil, SizeFormatter, TimeFormatter, ClipboardManager, FileManager } from '@/utils';
- import { useTheme } from '@/hooks/useTheme';
- import { useStatusQuery } from '@/api/queries/useStatusQuery';
- import { useMediaQuery } from '@/hooks/useMediaQuery';
- import AppSidebar from '@/components/AppSidebar';
- import LazyMount from '@/components/LazyMount';
- import { setMessageInstance } from '@/utils/messageBus';
- import StatusCard from './StatusCard';
- import XrayStatusCard from './XrayStatusCard';
- import type { PanelUpdateInfo } from './PanelUpdateModal';
- const JsonEditor = lazy(() => import('@/components/JsonEditor'));
- const PanelUpdateModal = lazy(() => import('./PanelUpdateModal'));
- const LogModal = lazy(() => import('./LogModal'));
- const BackupModal = lazy(() => import('./BackupModal'));
- const SystemHistoryModal = lazy(() => import('./SystemHistoryModal'));
- const XrayMetricsModal = lazy(() => import('./XrayMetricsModal'));
- const XrayLogModal = lazy(() => import('./XrayLogModal'));
- const VersionModal = lazy(() => import('./VersionModal'));
- import './IndexPage.css';
- export default function IndexPage() {
- const { t } = useTranslation();
- const { isDark, isUltra, antdThemeConfig } = useTheme();
- const { status, fetched, refresh } = useStatusQuery();
- const { isMobile } = useMediaQuery();
- const [messageApi, messageContextHolder] = message.useMessage();
- useEffect(() => { setMessageInstance(messageApi); }, [messageApi]);
- const [ipLimitEnable, setIpLimitEnable] = useState(false);
- const [panelUpdateInfo, setPanelUpdateInfo] = useState<PanelUpdateInfo>({
- currentVersion: '',
- latestVersion: '',
- updateAvailable: false,
- });
- const basePath = window.X_UI_BASE_PATH || '';
- const [showIp, setShowIp] = useState(false);
- const [logsOpen, setLogsOpen] = useState(false);
- const [backupOpen, setBackupOpen] = useState(false);
- const [panelUpdateOpen, setPanelUpdateOpen] = useState(false);
- const [sysHistoryOpen, setSysHistoryOpen] = useState(false);
- const [xrayMetricsOpen, setXrayMetricsOpen] = useState(false);
- const [xrayLogsOpen, setXrayLogsOpen] = useState(false);
- const [versionOpen, setVersionOpen] = useState(false);
- const [configTextOpen, setConfigTextOpen] = useState(false);
- const [configText, setConfigText] = useState('');
- const [loading, setLoading] = useState(false);
- const [loadingTip, setLoadingTip] = useState(t('loading'));
- useEffect(() => {
- HttpUtil.post('/panel/setting/defaultSettings').then((msg) => {
- if (msg?.success && msg.obj) setIpLimitEnable(!!msg.obj.ipLimitEnable);
- });
- HttpUtil.get('/panel/api/server/getPanelUpdateInfo').then((msg) => {
- if (msg?.success && msg.obj) setPanelUpdateInfo(msg.obj);
- });
- }, []);
- const displayVersion = useMemo(
- () => panelUpdateInfo.currentVersion || window.X_UI_CUR_VER || '?',
- [panelUpdateInfo.currentVersion],
- );
- const setBusy = useCallback(
- ({ busy, tip }: { busy: boolean; tip?: string }) => {
- setLoading(busy);
- if (tip) setLoadingTip(tip);
- },
- [],
- );
- const stopXray = useCallback(async () => {
- await HttpUtil.post('/panel/api/server/stopXrayService');
- await refresh();
- }, [refresh]);
- const restartXray = useCallback(async () => {
- await HttpUtil.post('/panel/api/server/restartXrayService');
- await refresh();
- }, [refresh]);
- function openPanelVersion() {
- if (panelUpdateInfo.updateAvailable) {
- setPanelUpdateOpen(true);
- } else {
- window.open('https://github.com/MHSanaei/3x-ui/releases', '_blank', 'noopener,noreferrer');
- }
- }
- function openTelegram() {
- window.open('https://t.me/XrayUI', '_blank', 'noopener,noreferrer');
- }
- async function openConfig() {
- setLoading(true);
- try {
- const msg = await HttpUtil.get('/panel/api/server/getConfigJson');
- if (!msg?.success) return;
- setConfigText(JSON.stringify(msg.obj, null, 2));
- setConfigTextOpen(true);
- } finally {
- setLoading(false);
- }
- }
- async function copyConfig() {
- const ok = await ClipboardManager.copyText(configText || '');
- if (ok) messageApi.success('Copied');
- }
- function downloadConfig() {
- FileManager.downloadTextFile(configText, 'config.json');
- }
- const pageClass = `index-page ${isDark ? 'is-dark' : ''} ${isUltra ? 'is-ultra' : ''}`.trim();
- return (
- <ConfigProvider theme={antdThemeConfig}>
- {messageContextHolder}
- <Layout className={pageClass}>
- <AppSidebar />
- <Layout className="content-shell">
- <Layout.Content className="content-area">
- <Spin
- spinning={loading || !fetched}
- delay={200}
- description={loading ? loadingTip : t('loading')}
- size="large"
- >
- {!fetched ? (
- <div className="loading-spacer" />
- ) : (
- <Row gutter={[isMobile ? 8 : 16, 12]}>
- <Col span={24}>
- <StatusCard status={status} isMobile={isMobile} />
- </Col>
- <Col xs={24} lg={12}>
- <XrayStatusCard
- status={status}
- isMobile={isMobile}
- ipLimitEnable={ipLimitEnable}
- onStopXray={stopXray}
- onRestartXray={restartXray}
- onOpenXrayLogs={() => setXrayLogsOpen(true)}
- onOpenLogs={() => setLogsOpen(true)}
- onOpenVersionSwitch={() => setVersionOpen(true)}
- />
- </Col>
- <Col xs={24} lg={12}>
- <Card
- title={t('menu.link')}
- hoverable
- actions={[
- <Space className="action" key="logs" onClick={() => setLogsOpen(true)}>
- <BarsOutlined />
- {!isMobile && <span>{t('pages.index.logs')}</span>}
- </Space>,
- <Space className="action" key="config" onClick={openConfig}>
- <ControlOutlined />
- {!isMobile && <span>{t('pages.index.config')}</span>}
- </Space>,
- <Space className="action" key="backup" onClick={() => setBackupOpen(true)}>
- <CloudServerOutlined />
- {!isMobile && <span>{t('pages.index.backupTitle')}</span>}
- </Space>,
- ]}
- />
- </Col>
- <Col xs={24} lg={12}>
- <Card
- title={
- <Space>
- <span>3X-UI</span>
- {isMobile && displayVersion && (
- <Tag color={panelUpdateInfo.updateAvailable ? 'orange' : 'green'}>
- {panelUpdateInfo.updateAvailable
- ? `v${panelUpdateInfo.latestVersion}`
- : `v${displayVersion}`}
- </Tag>
- )}
- </Space>
- }
- hoverable
- actions={[
- <Space className="action" key="tg" onClick={openTelegram}>
- <svg
- viewBox="0 0 24 24"
- width="14"
- height="14"
- fill="currentColor"
- className="tg-icon"
- aria-hidden="true"
- >
- <path d="M21.93 4.34a1.5 1.5 0 0 0-2.05-1.6L2.97 9.6c-.92.36-.91 1.66.02 1.99l4.32 1.53 1.7 5.23a1 1 0 0 0 1.68.36l2.43-2.43 4.36 3.21a1.5 1.5 0 0 0 2.36-.91l3.09-13.86a1.5 1.5 0 0 0 0-.38ZM9.97 14.66l-.55 3.36-1.36-4.2 9.8-7.05-7.89 7.89Z" />
- </svg>
- {!isMobile && <span>@XrayUI</span>}
- </Space>,
- <Space
- key="panel-version"
- className={`action ${panelUpdateInfo.updateAvailable ? 'action-update' : ''}`}
- onClick={openPanelVersion}
- >
- <CloudDownloadOutlined />
- {!isMobile && (
- <span>
- {panelUpdateInfo.updateAvailable
- ? `${t('update')} ${panelUpdateInfo.latestVersion}`
- : `v${displayVersion}`}
- </span>
- )}
- </Space>,
- ]}
- />
- </Col>
- <Col xs={24} lg={12}>
- <Card
- title={t('pages.index.charts')}
- hoverable
- actions={[
- <Space
- className="action"
- key="sys-history"
- onClick={() => setSysHistoryOpen(true)}
- >
- <AreaChartOutlined />
- {!isMobile && <span>{t('pages.index.systemHistoryTitle')}</span>}
- </Space>,
- <Space
- className="action"
- key="xray-metrics"
- onClick={() => setXrayMetricsOpen(true)}
- >
- <AreaChartOutlined />
- {!isMobile && <span>{t('pages.index.xrayMetricsTitle')}</span>}
- </Space>,
- ]}
- />
- </Col>
- <Col xs={24} lg={12}>
- <Card title={t('pages.index.operationHours')} hoverable>
- <Row gutter={isMobile ? [8, 8] : 0}>
- <Col span={12}>
- <Statistic
- title="Xray"
- value={TimeFormatter.formatSecond(status.appStats.uptime)}
- prefix={<ThunderboltOutlined />}
- />
- </Col>
- <Col span={12}>
- <Statistic
- title="OS"
- value={TimeFormatter.formatSecond(status.uptime)}
- prefix={<DesktopOutlined />}
- />
- </Col>
- </Row>
- </Card>
- </Col>
- <Col xs={24} lg={12}>
- <Card title={t('usage')} hoverable>
- <Row gutter={isMobile ? [8, 8] : 0}>
- <Col span={12}>
- <Statistic
- title={t('pages.index.memory')}
- value={SizeFormatter.sizeFormat(status.appStats.mem)}
- prefix={<DatabaseOutlined />}
- />
- </Col>
- <Col span={12}>
- <Statistic
- title={t('pages.index.threads')}
- value={status.appStats.threads}
- prefix={<ForkOutlined />}
- />
- </Col>
- </Row>
- </Card>
- </Col>
- <Col xs={24} lg={12}>
- <Card title={t('pages.index.overallSpeed')} hoverable>
- <Row gutter={isMobile ? [8, 8] : 0}>
- <Col span={12}>
- <Statistic
- title={t('pages.index.upload')}
- value={SizeFormatter.sizeFormat(status.netIO.up)}
- prefix={<ArrowUpOutlined />}
- suffix="/s"
- />
- </Col>
- <Col span={12}>
- <Statistic
- title={t('pages.index.download')}
- value={SizeFormatter.sizeFormat(status.netIO.down)}
- prefix={<ArrowDownOutlined />}
- suffix="/s"
- />
- </Col>
- </Row>
- </Card>
- </Col>
- <Col xs={24} lg={12}>
- <Card title={t('pages.index.totalData')} hoverable>
- <Row gutter={isMobile ? [8, 8] : 0}>
- <Col span={12}>
- <Statistic
- title={t('pages.index.sent')}
- value={SizeFormatter.sizeFormat(status.netTraffic.sent)}
- prefix={<CloudUploadOutlined />}
- />
- </Col>
- <Col span={12}>
- <Statistic
- title={t('pages.index.received')}
- value={SizeFormatter.sizeFormat(status.netTraffic.recv)}
- prefix={<CloudDownloadOutlined />}
- />
- </Col>
- </Row>
- </Card>
- </Col>
- <Col xs={24} lg={12}>
- <Card
- title={t('pages.index.ipAddresses')}
- hoverable
- extra={
- <Tooltip
- title={t('pages.index.toggleIpVisibility')}
- placement={isMobile ? 'topRight' : 'top'}
- >
- {showIp ? (
- <EyeOutlined
- className="ip-toggle-icon"
- onClick={() => setShowIp(false)}
- />
- ) : (
- <EyeInvisibleOutlined
- className="ip-toggle-icon"
- onClick={() => setShowIp(true)}
- />
- )}
- </Tooltip>
- }
- >
- <Row className={showIp ? 'ip-visible' : 'ip-hidden'} gutter={isMobile ? [8, 8] : 0}>
- <Col span={isMobile ? 24 : 12}>
- <Statistic
- title="IPv4"
- value={status.publicIP.ipv4}
- prefix={<GlobalOutlined />}
- />
- </Col>
- <Col span={isMobile ? 24 : 12}>
- <Statistic
- title="IPv6"
- value={status.publicIP.ipv6}
- prefix={<GlobalOutlined />}
- />
- </Col>
- </Row>
- </Card>
- </Col>
- <Col xs={24} lg={12}>
- <Card title={t('pages.index.connectionCount')} hoverable>
- <Row gutter={isMobile ? [8, 8] : 0}>
- <Col span={12}>
- <Statistic
- title="TCP"
- value={status.tcpCount}
- prefix={<SwapOutlined />}
- />
- </Col>
- <Col span={12}>
- <Statistic
- title="UDP"
- value={status.udpCount}
- prefix={<SwapOutlined />}
- />
- </Col>
- </Row>
- </Card>
- </Col>
- </Row>
- )}
- </Spin>
- </Layout.Content>
- </Layout>
- <LazyMount when={panelUpdateOpen}>
- <PanelUpdateModal
- open={panelUpdateOpen}
- info={panelUpdateInfo}
- onClose={() => setPanelUpdateOpen(false)}
- onBusy={setBusy}
- />
- </LazyMount>
- <LazyMount when={logsOpen}>
- <LogModal open={logsOpen} onClose={() => setLogsOpen(false)} />
- </LazyMount>
- <LazyMount when={backupOpen}>
- <BackupModal
- open={backupOpen}
- basePath={basePath}
- onClose={() => setBackupOpen(false)}
- onBusy={setBusy}
- />
- </LazyMount>
- <LazyMount when={sysHistoryOpen}>
- <SystemHistoryModal
- open={sysHistoryOpen}
- status={status}
- onClose={() => setSysHistoryOpen(false)}
- />
- </LazyMount>
- <LazyMount when={xrayMetricsOpen}>
- <XrayMetricsModal open={xrayMetricsOpen} onClose={() => setXrayMetricsOpen(false)} />
- </LazyMount>
- <LazyMount when={xrayLogsOpen}>
- <XrayLogModal open={xrayLogsOpen} onClose={() => setXrayLogsOpen(false)} />
- </LazyMount>
- <LazyMount when={versionOpen}>
- <VersionModal
- open={versionOpen}
- status={status}
- onClose={() => setVersionOpen(false)}
- onBusy={setBusy}
- />
- </LazyMount>
- <LazyMount when={configTextOpen}>
- <Modal
- open={configTextOpen}
- title={t('pages.index.config')}
- width={isMobile ? '100%' : 900}
- style={isMobile ? { top: 20, maxWidth: 'calc(100vw - 16px)' } : undefined}
- onCancel={() => setConfigTextOpen(false)}
- footer={[
- <Button
- key="download"
- onClick={downloadConfig}
- size={isMobile ? 'small' : 'middle'}
- icon={<CloudDownloadOutlined />}
- >
- {isMobile ? 'Download' : 'config.json'}
- </Button>,
- <Button
- key="copy"
- type="primary"
- onClick={copyConfig}
- size={isMobile ? 'small' : 'middle'}
- icon={<CopyOutlined />}
- >
- Copy
- </Button>,
- ]}
- >
- <JsonEditor
- value={configText}
- onChange={setConfigText}
- minHeight={isMobile ? '300px' : '420px'}
- maxHeight={isMobile ? '500px' : '720px'}
- readOnly
- />
- </Modal>
- </LazyMount>
- </Layout>
- </ConfigProvider>
- );
- }
|