IndexPage.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. import { lazy, useCallback, useEffect, useMemo, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import {
  4. Button,
  5. Card,
  6. Col,
  7. ConfigProvider,
  8. Layout,
  9. message,
  10. Modal,
  11. Row,
  12. Space,
  13. Spin,
  14. Statistic,
  15. Tag,
  16. Tooltip,
  17. } from 'antd';
  18. import {
  19. BarsOutlined,
  20. ControlOutlined,
  21. CloudServerOutlined,
  22. CloudDownloadOutlined,
  23. CloudUploadOutlined,
  24. ArrowUpOutlined,
  25. ArrowDownOutlined,
  26. AreaChartOutlined,
  27. GlobalOutlined,
  28. SwapOutlined,
  29. EyeOutlined,
  30. EyeInvisibleOutlined,
  31. ThunderboltOutlined,
  32. DesktopOutlined,
  33. DatabaseOutlined,
  34. ForkOutlined,
  35. CopyOutlined,
  36. } from '@ant-design/icons';
  37. import { HttpUtil, SizeFormatter, TimeFormatter, ClipboardManager, FileManager } from '@/utils';
  38. import { useTheme } from '@/hooks/useTheme';
  39. import { useStatusQuery } from '@/api/queries/useStatusQuery';
  40. import { useMediaQuery } from '@/hooks/useMediaQuery';
  41. import AppSidebar from '@/components/AppSidebar';
  42. import LazyMount from '@/components/LazyMount';
  43. import { setMessageInstance } from '@/utils/messageBus';
  44. import StatusCard from './StatusCard';
  45. import XrayStatusCard from './XrayStatusCard';
  46. import type { PanelUpdateInfo } from './PanelUpdateModal';
  47. const JsonEditor = lazy(() => import('@/components/JsonEditor'));
  48. const PanelUpdateModal = lazy(() => import('./PanelUpdateModal'));
  49. const LogModal = lazy(() => import('./LogModal'));
  50. const BackupModal = lazy(() => import('./BackupModal'));
  51. const SystemHistoryModal = lazy(() => import('./SystemHistoryModal'));
  52. const XrayMetricsModal = lazy(() => import('./XrayMetricsModal'));
  53. const XrayLogModal = lazy(() => import('./XrayLogModal'));
  54. const VersionModal = lazy(() => import('./VersionModal'));
  55. import './IndexPage.css';
  56. export default function IndexPage() {
  57. const { t } = useTranslation();
  58. const { isDark, isUltra, antdThemeConfig } = useTheme();
  59. const { status, fetched, refresh } = useStatusQuery();
  60. const { isMobile } = useMediaQuery();
  61. const [messageApi, messageContextHolder] = message.useMessage();
  62. useEffect(() => { setMessageInstance(messageApi); }, [messageApi]);
  63. const [ipLimitEnable, setIpLimitEnable] = useState(false);
  64. const [panelUpdateInfo, setPanelUpdateInfo] = useState<PanelUpdateInfo>({
  65. currentVersion: '',
  66. latestVersion: '',
  67. updateAvailable: false,
  68. });
  69. const basePath = window.X_UI_BASE_PATH || '';
  70. const [showIp, setShowIp] = useState(false);
  71. const [logsOpen, setLogsOpen] = useState(false);
  72. const [backupOpen, setBackupOpen] = useState(false);
  73. const [panelUpdateOpen, setPanelUpdateOpen] = useState(false);
  74. const [sysHistoryOpen, setSysHistoryOpen] = useState(false);
  75. const [xrayMetricsOpen, setXrayMetricsOpen] = useState(false);
  76. const [xrayLogsOpen, setXrayLogsOpen] = useState(false);
  77. const [versionOpen, setVersionOpen] = useState(false);
  78. const [configTextOpen, setConfigTextOpen] = useState(false);
  79. const [configText, setConfigText] = useState('');
  80. const [loading, setLoading] = useState(false);
  81. const [loadingTip, setLoadingTip] = useState(t('loading'));
  82. useEffect(() => {
  83. HttpUtil.post('/panel/setting/defaultSettings').then((msg) => {
  84. if (msg?.success && msg.obj) setIpLimitEnable(!!msg.obj.ipLimitEnable);
  85. });
  86. HttpUtil.get('/panel/api/server/getPanelUpdateInfo').then((msg) => {
  87. if (msg?.success && msg.obj) setPanelUpdateInfo(msg.obj);
  88. });
  89. }, []);
  90. const displayVersion = useMemo(
  91. () => panelUpdateInfo.currentVersion || window.X_UI_CUR_VER || '?',
  92. [panelUpdateInfo.currentVersion],
  93. );
  94. const setBusy = useCallback(
  95. ({ busy, tip }: { busy: boolean; tip?: string }) => {
  96. setLoading(busy);
  97. if (tip) setLoadingTip(tip);
  98. },
  99. [],
  100. );
  101. const stopXray = useCallback(async () => {
  102. await HttpUtil.post('/panel/api/server/stopXrayService');
  103. await refresh();
  104. }, [refresh]);
  105. const restartXray = useCallback(async () => {
  106. await HttpUtil.post('/panel/api/server/restartXrayService');
  107. await refresh();
  108. }, [refresh]);
  109. function openPanelVersion() {
  110. if (panelUpdateInfo.updateAvailable) {
  111. setPanelUpdateOpen(true);
  112. } else {
  113. window.open('https://github.com/MHSanaei/3x-ui/releases', '_blank', 'noopener,noreferrer');
  114. }
  115. }
  116. function openTelegram() {
  117. window.open('https://t.me/XrayUI', '_blank', 'noopener,noreferrer');
  118. }
  119. async function openConfig() {
  120. setLoading(true);
  121. try {
  122. const msg = await HttpUtil.get('/panel/api/server/getConfigJson');
  123. if (!msg?.success) return;
  124. setConfigText(JSON.stringify(msg.obj, null, 2));
  125. setConfigTextOpen(true);
  126. } finally {
  127. setLoading(false);
  128. }
  129. }
  130. async function copyConfig() {
  131. const ok = await ClipboardManager.copyText(configText || '');
  132. if (ok) messageApi.success('Copied');
  133. }
  134. function downloadConfig() {
  135. FileManager.downloadTextFile(configText, 'config.json');
  136. }
  137. const pageClass = `index-page ${isDark ? 'is-dark' : ''} ${isUltra ? 'is-ultra' : ''}`.trim();
  138. return (
  139. <ConfigProvider theme={antdThemeConfig}>
  140. {messageContextHolder}
  141. <Layout className={pageClass}>
  142. <AppSidebar />
  143. <Layout className="content-shell">
  144. <Layout.Content className="content-area">
  145. <Spin
  146. spinning={loading || !fetched}
  147. delay={200}
  148. description={loading ? loadingTip : t('loading')}
  149. size="large"
  150. >
  151. {!fetched ? (
  152. <div className="loading-spacer" />
  153. ) : (
  154. <Row gutter={[isMobile ? 8 : 16, 12]}>
  155. <Col span={24}>
  156. <StatusCard status={status} isMobile={isMobile} />
  157. </Col>
  158. <Col xs={24} lg={12}>
  159. <XrayStatusCard
  160. status={status}
  161. isMobile={isMobile}
  162. ipLimitEnable={ipLimitEnable}
  163. onStopXray={stopXray}
  164. onRestartXray={restartXray}
  165. onOpenXrayLogs={() => setXrayLogsOpen(true)}
  166. onOpenLogs={() => setLogsOpen(true)}
  167. onOpenVersionSwitch={() => setVersionOpen(true)}
  168. />
  169. </Col>
  170. <Col xs={24} lg={12}>
  171. <Card
  172. title={t('menu.link')}
  173. hoverable
  174. actions={[
  175. <Space className="action" key="logs" onClick={() => setLogsOpen(true)}>
  176. <BarsOutlined />
  177. {!isMobile && <span>{t('pages.index.logs')}</span>}
  178. </Space>,
  179. <Space className="action" key="config" onClick={openConfig}>
  180. <ControlOutlined />
  181. {!isMobile && <span>{t('pages.index.config')}</span>}
  182. </Space>,
  183. <Space className="action" key="backup" onClick={() => setBackupOpen(true)}>
  184. <CloudServerOutlined />
  185. {!isMobile && <span>{t('pages.index.backupTitle')}</span>}
  186. </Space>,
  187. ]}
  188. />
  189. </Col>
  190. <Col xs={24} lg={12}>
  191. <Card
  192. title={
  193. <Space>
  194. <span>3X-UI</span>
  195. {isMobile && displayVersion && (
  196. <Tag color={panelUpdateInfo.updateAvailable ? 'orange' : 'green'}>
  197. {panelUpdateInfo.updateAvailable
  198. ? `v${panelUpdateInfo.latestVersion}`
  199. : `v${displayVersion}`}
  200. </Tag>
  201. )}
  202. </Space>
  203. }
  204. hoverable
  205. actions={[
  206. <Space className="action" key="tg" onClick={openTelegram}>
  207. <svg
  208. viewBox="0 0 24 24"
  209. width="14"
  210. height="14"
  211. fill="currentColor"
  212. className="tg-icon"
  213. aria-hidden="true"
  214. >
  215. <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" />
  216. </svg>
  217. {!isMobile && <span>@XrayUI</span>}
  218. </Space>,
  219. <Space
  220. key="panel-version"
  221. className={`action ${panelUpdateInfo.updateAvailable ? 'action-update' : ''}`}
  222. onClick={openPanelVersion}
  223. >
  224. <CloudDownloadOutlined />
  225. {!isMobile && (
  226. <span>
  227. {panelUpdateInfo.updateAvailable
  228. ? `${t('update')} ${panelUpdateInfo.latestVersion}`
  229. : `v${displayVersion}`}
  230. </span>
  231. )}
  232. </Space>,
  233. ]}
  234. />
  235. </Col>
  236. <Col xs={24} lg={12}>
  237. <Card
  238. title={t('pages.index.charts')}
  239. hoverable
  240. actions={[
  241. <Space
  242. className="action"
  243. key="sys-history"
  244. onClick={() => setSysHistoryOpen(true)}
  245. >
  246. <AreaChartOutlined />
  247. {!isMobile && <span>{t('pages.index.systemHistoryTitle')}</span>}
  248. </Space>,
  249. <Space
  250. className="action"
  251. key="xray-metrics"
  252. onClick={() => setXrayMetricsOpen(true)}
  253. >
  254. <AreaChartOutlined />
  255. {!isMobile && <span>{t('pages.index.xrayMetricsTitle')}</span>}
  256. </Space>,
  257. ]}
  258. />
  259. </Col>
  260. <Col xs={24} lg={12}>
  261. <Card title={t('pages.index.operationHours')} hoverable>
  262. <Row gutter={isMobile ? [8, 8] : 0}>
  263. <Col span={12}>
  264. <Statistic
  265. title="Xray"
  266. value={TimeFormatter.formatSecond(status.appStats.uptime)}
  267. prefix={<ThunderboltOutlined />}
  268. />
  269. </Col>
  270. <Col span={12}>
  271. <Statistic
  272. title="OS"
  273. value={TimeFormatter.formatSecond(status.uptime)}
  274. prefix={<DesktopOutlined />}
  275. />
  276. </Col>
  277. </Row>
  278. </Card>
  279. </Col>
  280. <Col xs={24} lg={12}>
  281. <Card title={t('usage')} hoverable>
  282. <Row gutter={isMobile ? [8, 8] : 0}>
  283. <Col span={12}>
  284. <Statistic
  285. title={t('pages.index.memory')}
  286. value={SizeFormatter.sizeFormat(status.appStats.mem)}
  287. prefix={<DatabaseOutlined />}
  288. />
  289. </Col>
  290. <Col span={12}>
  291. <Statistic
  292. title={t('pages.index.threads')}
  293. value={status.appStats.threads}
  294. prefix={<ForkOutlined />}
  295. />
  296. </Col>
  297. </Row>
  298. </Card>
  299. </Col>
  300. <Col xs={24} lg={12}>
  301. <Card title={t('pages.index.overallSpeed')} hoverable>
  302. <Row gutter={isMobile ? [8, 8] : 0}>
  303. <Col span={12}>
  304. <Statistic
  305. title={t('pages.index.upload')}
  306. value={SizeFormatter.sizeFormat(status.netIO.up)}
  307. prefix={<ArrowUpOutlined />}
  308. suffix="/s"
  309. />
  310. </Col>
  311. <Col span={12}>
  312. <Statistic
  313. title={t('pages.index.download')}
  314. value={SizeFormatter.sizeFormat(status.netIO.down)}
  315. prefix={<ArrowDownOutlined />}
  316. suffix="/s"
  317. />
  318. </Col>
  319. </Row>
  320. </Card>
  321. </Col>
  322. <Col xs={24} lg={12}>
  323. <Card title={t('pages.index.totalData')} hoverable>
  324. <Row gutter={isMobile ? [8, 8] : 0}>
  325. <Col span={12}>
  326. <Statistic
  327. title={t('pages.index.sent')}
  328. value={SizeFormatter.sizeFormat(status.netTraffic.sent)}
  329. prefix={<CloudUploadOutlined />}
  330. />
  331. </Col>
  332. <Col span={12}>
  333. <Statistic
  334. title={t('pages.index.received')}
  335. value={SizeFormatter.sizeFormat(status.netTraffic.recv)}
  336. prefix={<CloudDownloadOutlined />}
  337. />
  338. </Col>
  339. </Row>
  340. </Card>
  341. </Col>
  342. <Col xs={24} lg={12}>
  343. <Card
  344. title={t('pages.index.ipAddresses')}
  345. hoverable
  346. extra={
  347. <Tooltip
  348. title={t('pages.index.toggleIpVisibility')}
  349. placement={isMobile ? 'topRight' : 'top'}
  350. >
  351. {showIp ? (
  352. <EyeOutlined
  353. className="ip-toggle-icon"
  354. onClick={() => setShowIp(false)}
  355. />
  356. ) : (
  357. <EyeInvisibleOutlined
  358. className="ip-toggle-icon"
  359. onClick={() => setShowIp(true)}
  360. />
  361. )}
  362. </Tooltip>
  363. }
  364. >
  365. <Row className={showIp ? 'ip-visible' : 'ip-hidden'} gutter={isMobile ? [8, 8] : 0}>
  366. <Col span={isMobile ? 24 : 12}>
  367. <Statistic
  368. title="IPv4"
  369. value={status.publicIP.ipv4}
  370. prefix={<GlobalOutlined />}
  371. />
  372. </Col>
  373. <Col span={isMobile ? 24 : 12}>
  374. <Statistic
  375. title="IPv6"
  376. value={status.publicIP.ipv6}
  377. prefix={<GlobalOutlined />}
  378. />
  379. </Col>
  380. </Row>
  381. </Card>
  382. </Col>
  383. <Col xs={24} lg={12}>
  384. <Card title={t('pages.index.connectionCount')} hoverable>
  385. <Row gutter={isMobile ? [8, 8] : 0}>
  386. <Col span={12}>
  387. <Statistic
  388. title="TCP"
  389. value={status.tcpCount}
  390. prefix={<SwapOutlined />}
  391. />
  392. </Col>
  393. <Col span={12}>
  394. <Statistic
  395. title="UDP"
  396. value={status.udpCount}
  397. prefix={<SwapOutlined />}
  398. />
  399. </Col>
  400. </Row>
  401. </Card>
  402. </Col>
  403. </Row>
  404. )}
  405. </Spin>
  406. </Layout.Content>
  407. </Layout>
  408. <LazyMount when={panelUpdateOpen}>
  409. <PanelUpdateModal
  410. open={panelUpdateOpen}
  411. info={panelUpdateInfo}
  412. onClose={() => setPanelUpdateOpen(false)}
  413. onBusy={setBusy}
  414. />
  415. </LazyMount>
  416. <LazyMount when={logsOpen}>
  417. <LogModal open={logsOpen} onClose={() => setLogsOpen(false)} />
  418. </LazyMount>
  419. <LazyMount when={backupOpen}>
  420. <BackupModal
  421. open={backupOpen}
  422. basePath={basePath}
  423. onClose={() => setBackupOpen(false)}
  424. onBusy={setBusy}
  425. />
  426. </LazyMount>
  427. <LazyMount when={sysHistoryOpen}>
  428. <SystemHistoryModal
  429. open={sysHistoryOpen}
  430. status={status}
  431. onClose={() => setSysHistoryOpen(false)}
  432. />
  433. </LazyMount>
  434. <LazyMount when={xrayMetricsOpen}>
  435. <XrayMetricsModal open={xrayMetricsOpen} onClose={() => setXrayMetricsOpen(false)} />
  436. </LazyMount>
  437. <LazyMount when={xrayLogsOpen}>
  438. <XrayLogModal open={xrayLogsOpen} onClose={() => setXrayLogsOpen(false)} />
  439. </LazyMount>
  440. <LazyMount when={versionOpen}>
  441. <VersionModal
  442. open={versionOpen}
  443. status={status}
  444. onClose={() => setVersionOpen(false)}
  445. onBusy={setBusy}
  446. />
  447. </LazyMount>
  448. <LazyMount when={configTextOpen}>
  449. <Modal
  450. open={configTextOpen}
  451. title={t('pages.index.config')}
  452. width={isMobile ? '100%' : 900}
  453. style={isMobile ? { top: 20, maxWidth: 'calc(100vw - 16px)' } : undefined}
  454. onCancel={() => setConfigTextOpen(false)}
  455. footer={[
  456. <Button
  457. key="download"
  458. onClick={downloadConfig}
  459. size={isMobile ? 'small' : 'middle'}
  460. icon={<CloudDownloadOutlined />}
  461. >
  462. {isMobile ? 'Download' : 'config.json'}
  463. </Button>,
  464. <Button
  465. key="copy"
  466. type="primary"
  467. onClick={copyConfig}
  468. size={isMobile ? 'small' : 'middle'}
  469. icon={<CopyOutlined />}
  470. >
  471. Copy
  472. </Button>,
  473. ]}
  474. >
  475. <JsonEditor
  476. value={configText}
  477. onChange={setConfigText}
  478. minHeight={isMobile ? '300px' : '420px'}
  479. maxHeight={isMobile ? '500px' : '720px'}
  480. readOnly
  481. />
  482. </Modal>
  483. </LazyMount>
  484. </Layout>
  485. </ConfigProvider>
  486. );
  487. }