mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-31 10:14:15 +00:00
503 lines
19 KiB
TypeScript
503 lines
19 KiB
TypeScript
|
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||
|
|
import { useTranslation } from 'react-i18next';
|
||
|
|
import {
|
||
|
|
Button,
|
||
|
|
Card,
|
||
|
|
Col,
|
||
|
|
ConfigProvider,
|
||
|
|
Layout,
|
||
|
|
message,
|
||
|
|
Modal,
|
||
|
|
Row,
|
||
|
|
Space,
|
||
|
|
Spin,
|
||
|
|
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 { useStatus } from '@/hooks/useStatus';
|
||
|
|
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||
|
|
import AppSidebar from '@/components/AppSidebar';
|
||
|
|
import CustomStatistic from '@/components/CustomStatistic';
|
||
|
|
import JsonEditor from '@/components/JsonEditor';
|
||
|
|
import { setMessageInstance } from '@/utils/messageBus';
|
||
|
|
import StatusCard from './StatusCard';
|
||
|
|
import XrayStatusCard from './XrayStatusCard';
|
||
|
|
import PanelUpdateModal from './PanelUpdateModal';
|
||
|
|
import type { PanelUpdateInfo } from './PanelUpdateModal';
|
||
|
|
import LogModal from './LogModal';
|
||
|
|
import BackupModal from './BackupModal';
|
||
|
|
import SystemHistoryModal from './SystemHistoryModal';
|
||
|
|
import XrayMetricsModal from './XrayMetricsModal';
|
||
|
|
import XrayLogModal from './XrayLogModal';
|
||
|
|
import VersionModal from './VersionModal';
|
||
|
|
import '@/styles/page-cards.css';
|
||
|
|
import './IndexPage.css';
|
||
|
|
|
||
|
|
export default function IndexPage() {
|
||
|
|
const { t } = useTranslation();
|
||
|
|
const { isDark, isUltra, antdThemeConfig } = useTheme();
|
||
|
|
const { status, fetched, refresh } = useStatus();
|
||
|
|
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 requestUri = window.location.pathname;
|
||
|
|
|
||
|
|
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 basePath={basePath} requestUri={requestUri} />
|
||
|
|
|
||
|
|
<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}>
|
||
|
|
<CustomStatistic
|
||
|
|
title="Xray"
|
||
|
|
value={TimeFormatter.formatSecond(status.appStats.uptime)}
|
||
|
|
prefix={<ThunderboltOutlined />}
|
||
|
|
/>
|
||
|
|
</Col>
|
||
|
|
<Col span={12}>
|
||
|
|
<CustomStatistic
|
||
|
|
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}>
|
||
|
|
<CustomStatistic
|
||
|
|
title={t('pages.index.memory')}
|
||
|
|
value={SizeFormatter.sizeFormat(status.appStats.mem)}
|
||
|
|
prefix={<DatabaseOutlined />}
|
||
|
|
/>
|
||
|
|
</Col>
|
||
|
|
<Col span={12}>
|
||
|
|
<CustomStatistic
|
||
|
|
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}>
|
||
|
|
<CustomStatistic
|
||
|
|
title={t('pages.index.upload')}
|
||
|
|
value={SizeFormatter.sizeFormat(status.netIO.up)}
|
||
|
|
prefix={<ArrowUpOutlined />}
|
||
|
|
suffix="/s"
|
||
|
|
/>
|
||
|
|
</Col>
|
||
|
|
<Col span={12}>
|
||
|
|
<CustomStatistic
|
||
|
|
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}>
|
||
|
|
<CustomStatistic
|
||
|
|
title={t('pages.index.sent')}
|
||
|
|
value={SizeFormatter.sizeFormat(status.netTraffic.sent)}
|
||
|
|
prefix={<CloudUploadOutlined />}
|
||
|
|
/>
|
||
|
|
</Col>
|
||
|
|
<Col span={12}>
|
||
|
|
<CustomStatistic
|
||
|
|
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}>
|
||
|
|
<CustomStatistic
|
||
|
|
title="IPv4"
|
||
|
|
value={status.publicIP.ipv4}
|
||
|
|
prefix={<GlobalOutlined />}
|
||
|
|
/>
|
||
|
|
</Col>
|
||
|
|
<Col span={isMobile ? 24 : 12}>
|
||
|
|
<CustomStatistic
|
||
|
|
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}>
|
||
|
|
<CustomStatistic
|
||
|
|
title="TCP"
|
||
|
|
value={status.tcpCount}
|
||
|
|
prefix={<SwapOutlined />}
|
||
|
|
/>
|
||
|
|
</Col>
|
||
|
|
<Col span={12}>
|
||
|
|
<CustomStatistic
|
||
|
|
title="UDP"
|
||
|
|
value={status.udpCount}
|
||
|
|
prefix={<SwapOutlined />}
|
||
|
|
/>
|
||
|
|
</Col>
|
||
|
|
</Row>
|
||
|
|
</Card>
|
||
|
|
</Col>
|
||
|
|
</Row>
|
||
|
|
)}
|
||
|
|
</Spin>
|
||
|
|
</Layout.Content>
|
||
|
|
</Layout>
|
||
|
|
|
||
|
|
<PanelUpdateModal
|
||
|
|
open={panelUpdateOpen}
|
||
|
|
info={panelUpdateInfo}
|
||
|
|
onClose={() => setPanelUpdateOpen(false)}
|
||
|
|
onBusy={setBusy}
|
||
|
|
/>
|
||
|
|
<LogModal open={logsOpen} onClose={() => setLogsOpen(false)} />
|
||
|
|
<BackupModal
|
||
|
|
open={backupOpen}
|
||
|
|
basePath={basePath}
|
||
|
|
onClose={() => setBackupOpen(false)}
|
||
|
|
onBusy={setBusy}
|
||
|
|
/>
|
||
|
|
<SystemHistoryModal
|
||
|
|
open={sysHistoryOpen}
|
||
|
|
status={status}
|
||
|
|
onClose={() => setSysHistoryOpen(false)}
|
||
|
|
/>
|
||
|
|
<XrayMetricsModal open={xrayMetricsOpen} onClose={() => setXrayMetricsOpen(false)} />
|
||
|
|
<XrayLogModal open={xrayLogsOpen} onClose={() => setXrayLogsOpen(false)} />
|
||
|
|
<VersionModal
|
||
|
|
open={versionOpen}
|
||
|
|
status={status}
|
||
|
|
onClose={() => setVersionOpen(false)}
|
||
|
|
onBusy={setBusy}
|
||
|
|
/>
|
||
|
|
|
||
|
|
<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>
|
||
|
|
</Layout>
|
||
|
|
</ConfigProvider>
|
||
|
|
);
|
||
|
|
}
|