index.js 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362
  1. import React, { useContext, useEffect, useRef, useState, useMemo, useCallback } from 'react';
  2. import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
  3. import { useNavigate } from 'react-router-dom';
  4. import { Wallet, Activity, Zap, Gauge, PieChart, Server, Bell, HelpCircle } from 'lucide-react';
  5. import {
  6. Card,
  7. Form,
  8. Spin,
  9. IconButton,
  10. Modal,
  11. Avatar,
  12. Tabs,
  13. TabPane,
  14. Empty,
  15. Tag,
  16. Timeline,
  17. Collapse,
  18. Progress
  19. } from '@douyinfe/semi-ui';
  20. import {
  21. IconRefresh,
  22. IconSearch,
  23. IconMoneyExchangeStroked,
  24. IconHistogram,
  25. IconRotate,
  26. IconCoinMoneyStroked,
  27. IconTextStroked,
  28. IconPulse,
  29. IconStopwatchStroked,
  30. IconTypograph,
  31. IconPieChart2Stroked,
  32. IconPlus,
  33. IconMinus
  34. } from '@douyinfe/semi-icons';
  35. import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations';
  36. import { VChart } from '@visactor/react-vchart';
  37. import {
  38. API,
  39. isAdmin,
  40. isMobile,
  41. showError,
  42. timestamp2string,
  43. timestamp2string1,
  44. getQuotaWithUnit,
  45. modelColorMap,
  46. renderNumber,
  47. renderQuota,
  48. modelToColor,
  49. copy,
  50. showSuccess,
  51. getRelativeTime
  52. } from '../../helpers';
  53. import { UserContext } from '../../context/User/index.js';
  54. import { StatusContext } from '../../context/Status/index.js';
  55. import { useTranslation } from 'react-i18next';
  56. const Detail = (props) => {
  57. // ========== Hooks - Context ==========
  58. const [userState, userDispatch] = useContext(UserContext);
  59. const [statusState, statusDispatch] = useContext(StatusContext);
  60. // ========== Hooks - Navigation & Translation ==========
  61. const { t } = useTranslation();
  62. const navigate = useNavigate();
  63. // ========== Hooks - Refs ==========
  64. const formRef = useRef();
  65. const initialized = useRef(false);
  66. const apiScrollRef = useRef(null);
  67. // ========== Constants & Shared Configurations ==========
  68. const CHART_CONFIG = { mode: 'desktop-browser' };
  69. const CARD_PROPS = {
  70. shadows: 'always',
  71. bordered: false,
  72. headerLine: true
  73. };
  74. const FORM_FIELD_PROPS = {
  75. className: "w-full mb-2 !rounded-lg",
  76. size: 'large'
  77. };
  78. const ICON_BUTTON_CLASS = "text-white hover:bg-opacity-80 !rounded-full";
  79. const FLEX_CENTER_GAP2 = "flex items-center gap-2";
  80. // ========== Constants ==========
  81. let now = new Date();
  82. const isAdminUser = isAdmin();
  83. // ========== Helper Functions ==========
  84. const getDefaultTime = useCallback(() => {
  85. return localStorage.getItem('data_export_default_time') || 'hour';
  86. }, []);
  87. const getTimeInterval = useCallback((timeType, isSeconds = false) => {
  88. const intervals = {
  89. hour: isSeconds ? 3600 : 60,
  90. day: isSeconds ? 86400 : 1440,
  91. week: isSeconds ? 604800 : 10080
  92. };
  93. return intervals[timeType] || intervals.hour;
  94. }, []);
  95. const getInitialTimestamp = useCallback(() => {
  96. const defaultTime = getDefaultTime();
  97. const now = new Date().getTime() / 1000;
  98. switch (defaultTime) {
  99. case 'hour':
  100. return timestamp2string(now - 86400);
  101. case 'week':
  102. return timestamp2string(now - 86400 * 30);
  103. default:
  104. return timestamp2string(now - 86400 * 7);
  105. }
  106. }, [getDefaultTime]);
  107. const updateMapValue = useCallback((map, key, value) => {
  108. if (!map.has(key)) {
  109. map.set(key, 0);
  110. }
  111. map.set(key, map.get(key) + value);
  112. }, []);
  113. const initializeMaps = useCallback((key, ...maps) => {
  114. maps.forEach(map => {
  115. if (!map.has(key)) {
  116. map.set(key, 0);
  117. }
  118. });
  119. }, []);
  120. const updateChartSpec = useCallback((setterFunc, newData, subtitle, newColors, dataId) => {
  121. setterFunc(prev => ({
  122. ...prev,
  123. data: [{ id: dataId, values: newData }],
  124. title: {
  125. ...prev.title,
  126. subtext: subtitle,
  127. },
  128. color: {
  129. specified: newColors,
  130. },
  131. }));
  132. }, []);
  133. const createSectionTitle = useCallback((Icon, text) => (
  134. <div className={FLEX_CENTER_GAP2}>
  135. <Icon size={16} />
  136. {text}
  137. </div>
  138. ), []);
  139. const createFormField = useCallback((Component, props) => (
  140. <Component {...FORM_FIELD_PROPS} {...props} />
  141. ), []);
  142. // ========== Time Options ==========
  143. const timeOptions = useMemo(() => [
  144. { label: t('小时'), value: 'hour' },
  145. { label: t('天'), value: 'day' },
  146. { label: t('周'), value: 'week' },
  147. ], [t]);
  148. // ========== Hooks - State ==========
  149. const [inputs, setInputs] = useState({
  150. username: '',
  151. token_name: '',
  152. model_name: '',
  153. start_timestamp: getInitialTimestamp(),
  154. end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
  155. channel: '',
  156. data_export_default_time: '',
  157. });
  158. const [dataExportDefaultTime, setDataExportDefaultTime] = useState(getDefaultTime());
  159. const [loading, setLoading] = useState(false);
  160. const [quotaData, setQuotaData] = useState([]);
  161. const [consumeQuota, setConsumeQuota] = useState(0);
  162. const [consumeTokens, setConsumeTokens] = useState(0);
  163. const [times, setTimes] = useState(0);
  164. const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]);
  165. const [lineData, setLineData] = useState([]);
  166. const [modelColors, setModelColors] = useState({});
  167. const [activeChartTab, setActiveChartTab] = useState('1');
  168. const [showApiScrollHint, setShowApiScrollHint] = useState(false);
  169. const [searchModalVisible, setSearchModalVisible] = useState(false);
  170. const [trendData, setTrendData] = useState({
  171. balance: [],
  172. usedQuota: [],
  173. requestCount: [],
  174. times: [],
  175. consumeQuota: [],
  176. tokens: [],
  177. rpm: [],
  178. tpm: []
  179. });
  180. // ========== Additional Refs for new cards ==========
  181. const announcementScrollRef = useRef(null);
  182. const faqScrollRef = useRef(null);
  183. const uptimeScrollRef = useRef(null);
  184. // ========== Additional State for scroll hints ==========
  185. const [showAnnouncementScrollHint, setShowAnnouncementScrollHint] = useState(false);
  186. const [showFaqScrollHint, setShowFaqScrollHint] = useState(false);
  187. const [showUptimeScrollHint, setShowUptimeScrollHint] = useState(false);
  188. // ========== Uptime data ==========
  189. const [uptimeData, setUptimeData] = useState([]);
  190. const [uptimeLoading, setUptimeLoading] = useState(false);
  191. // ========== Props Destructuring ==========
  192. const { username, model_name, start_timestamp, end_timestamp, channel } = inputs;
  193. // ========== Chart Specs State ==========
  194. const [spec_pie, setSpecPie] = useState({
  195. type: 'pie',
  196. data: [
  197. {
  198. id: 'id0',
  199. values: pieData,
  200. },
  201. ],
  202. outerRadius: 0.8,
  203. innerRadius: 0.5,
  204. padAngle: 0.6,
  205. valueField: 'value',
  206. categoryField: 'type',
  207. pie: {
  208. style: {
  209. cornerRadius: 10,
  210. },
  211. state: {
  212. hover: {
  213. outerRadius: 0.85,
  214. stroke: '#000',
  215. lineWidth: 1,
  216. },
  217. selected: {
  218. outerRadius: 0.85,
  219. stroke: '#000',
  220. lineWidth: 1,
  221. },
  222. },
  223. },
  224. title: {
  225. visible: true,
  226. text: t('模型调用次数占比'),
  227. subtext: `${t('总计')}:${renderNumber(times)}`,
  228. },
  229. legends: {
  230. visible: true,
  231. orient: 'left',
  232. },
  233. label: {
  234. visible: true,
  235. },
  236. tooltip: {
  237. mark: {
  238. content: [
  239. {
  240. key: (datum) => datum['type'],
  241. value: (datum) => renderNumber(datum['value']),
  242. },
  243. ],
  244. },
  245. },
  246. color: {
  247. specified: modelColorMap,
  248. },
  249. });
  250. const [spec_line, setSpecLine] = useState({
  251. type: 'bar',
  252. data: [
  253. {
  254. id: 'barData',
  255. values: lineData,
  256. },
  257. ],
  258. xField: 'Time',
  259. yField: 'Usage',
  260. seriesField: 'Model',
  261. stack: true,
  262. legends: {
  263. visible: true,
  264. selectMode: 'single',
  265. },
  266. title: {
  267. visible: true,
  268. text: t('模型消耗分布'),
  269. subtext: `${t('总计')}:${renderQuota(consumeQuota, 2)}`,
  270. },
  271. bar: {
  272. state: {
  273. hover: {
  274. stroke: '#000',
  275. lineWidth: 1,
  276. },
  277. },
  278. },
  279. tooltip: {
  280. mark: {
  281. content: [
  282. {
  283. key: (datum) => datum['Model'],
  284. value: (datum) => renderQuota(datum['rawQuota'] || 0, 4),
  285. },
  286. ],
  287. },
  288. dimension: {
  289. content: [
  290. {
  291. key: (datum) => datum['Model'],
  292. value: (datum) => datum['rawQuota'] || 0,
  293. },
  294. ],
  295. updateContent: (array) => {
  296. array.sort((a, b) => b.value - a.value);
  297. let sum = 0;
  298. for (let i = 0; i < array.length; i++) {
  299. if (array[i].key == '其他') {
  300. continue;
  301. }
  302. let value = parseFloat(array[i].value);
  303. if (isNaN(value)) {
  304. value = 0;
  305. }
  306. if (array[i].datum && array[i].datum.TimeSum) {
  307. sum = array[i].datum.TimeSum;
  308. }
  309. array[i].value = renderQuota(value, 4);
  310. }
  311. array.unshift({
  312. key: t('总计'),
  313. value: renderQuota(sum, 4),
  314. });
  315. return array;
  316. },
  317. },
  318. },
  319. color: {
  320. specified: modelColorMap,
  321. },
  322. });
  323. // ========== Hooks - Memoized Values ==========
  324. const performanceMetrics = useMemo(() => {
  325. const timeDiff = (Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000;
  326. const avgRPM = (times / timeDiff).toFixed(3);
  327. const avgTPM = isNaN(consumeTokens / timeDiff) ? '0' : (consumeTokens / timeDiff).toFixed(3);
  328. return { avgRPM, avgTPM, timeDiff };
  329. }, [times, consumeTokens, end_timestamp, start_timestamp]);
  330. const getGreeting = useMemo(() => {
  331. const hours = new Date().getHours();
  332. let greeting = '';
  333. if (hours >= 5 && hours < 12) {
  334. greeting = t('早上好');
  335. } else if (hours >= 12 && hours < 14) {
  336. greeting = t('中午好');
  337. } else if (hours >= 14 && hours < 18) {
  338. greeting = t('下午好');
  339. } else {
  340. greeting = t('晚上好');
  341. }
  342. const username = userState?.user?.username || '';
  343. return `👋${greeting},${username}`;
  344. }, [t, userState?.user?.username]);
  345. // ========== Hooks - Callbacks ==========
  346. const getTrendSpec = useCallback((data, color) => ({
  347. type: 'line',
  348. data: [{ id: 'trend', values: data.map((val, idx) => ({ x: idx, y: val })) }],
  349. xField: 'x',
  350. yField: 'y',
  351. height: 40,
  352. width: 100,
  353. axes: [
  354. {
  355. orient: 'bottom',
  356. visible: false
  357. },
  358. {
  359. orient: 'left',
  360. visible: false
  361. }
  362. ],
  363. padding: 0,
  364. autoFit: false,
  365. legends: { visible: false },
  366. tooltip: { visible: false },
  367. crosshair: { visible: false },
  368. line: {
  369. style: {
  370. stroke: color,
  371. lineWidth: 2
  372. }
  373. },
  374. point: {
  375. visible: false
  376. },
  377. background: {
  378. fill: 'transparent'
  379. }
  380. }), []);
  381. const groupedStatsData = useMemo(() => [
  382. {
  383. title: createSectionTitle(Wallet, t('账户数据')),
  384. color: 'bg-blue-50',
  385. items: [
  386. {
  387. title: t('当前余额'),
  388. value: renderQuota(userState?.user?.quota),
  389. icon: <IconMoneyExchangeStroked size="large" />,
  390. avatarColor: 'blue',
  391. onClick: () => navigate('/console/topup'),
  392. trendData: [],
  393. trendColor: '#3b82f6'
  394. },
  395. {
  396. title: t('历史消耗'),
  397. value: renderQuota(userState?.user?.used_quota),
  398. icon: <IconHistogram size="large" />,
  399. avatarColor: 'purple',
  400. trendData: [],
  401. trendColor: '#8b5cf6'
  402. }
  403. ]
  404. },
  405. {
  406. title: createSectionTitle(Activity, t('使用统计')),
  407. color: 'bg-green-50',
  408. items: [
  409. {
  410. title: t('请求次数'),
  411. value: userState.user?.request_count,
  412. icon: <IconRotate size="large" />,
  413. avatarColor: 'green',
  414. trendData: [],
  415. trendColor: '#10b981'
  416. },
  417. {
  418. title: t('统计次数'),
  419. value: times,
  420. icon: <IconPulse size="large" />,
  421. avatarColor: 'cyan',
  422. trendData: trendData.times,
  423. trendColor: '#06b6d4'
  424. }
  425. ]
  426. },
  427. {
  428. title: createSectionTitle(Zap, t('资源消耗')),
  429. color: 'bg-yellow-50',
  430. items: [
  431. {
  432. title: t('统计额度'),
  433. value: renderQuota(consumeQuota),
  434. icon: <IconCoinMoneyStroked size="large" />,
  435. avatarColor: 'yellow',
  436. trendData: trendData.consumeQuota,
  437. trendColor: '#f59e0b'
  438. },
  439. {
  440. title: t('统计Tokens'),
  441. value: isNaN(consumeTokens) ? 0 : consumeTokens,
  442. icon: <IconTextStroked size="large" />,
  443. avatarColor: 'pink',
  444. trendData: trendData.tokens,
  445. trendColor: '#ec4899'
  446. }
  447. ]
  448. },
  449. {
  450. title: createSectionTitle(Gauge, t('性能指标')),
  451. color: 'bg-indigo-50',
  452. items: [
  453. {
  454. title: t('平均RPM'),
  455. value: performanceMetrics.avgRPM,
  456. icon: <IconStopwatchStroked size="large" />,
  457. avatarColor: 'indigo',
  458. trendData: trendData.rpm,
  459. trendColor: '#6366f1'
  460. },
  461. {
  462. title: t('平均TPM'),
  463. value: performanceMetrics.avgTPM,
  464. icon: <IconTypograph size="large" />,
  465. avatarColor: 'orange',
  466. trendData: trendData.tpm,
  467. trendColor: '#f97316'
  468. }
  469. ]
  470. }
  471. ], [
  472. createSectionTitle, t, userState?.user?.quota, userState?.user?.used_quota, userState?.user?.request_count,
  473. times, consumeQuota, consumeTokens, trendData, performanceMetrics, navigate
  474. ]);
  475. const handleCopyUrl = useCallback(async (url) => {
  476. if (await copy(url)) {
  477. showSuccess(t('复制成功'));
  478. }
  479. }, [t]);
  480. const handleSpeedTest = useCallback((apiUrl) => {
  481. const encodedUrl = encodeURIComponent(apiUrl);
  482. const speedTestUrl = `https://www.tcptest.cn/http/${encodedUrl}`;
  483. window.open(speedTestUrl, '_blank');
  484. }, []);
  485. const handleInputChange = useCallback((value, name) => {
  486. if (name === 'data_export_default_time') {
  487. setDataExportDefaultTime(value);
  488. return;
  489. }
  490. setInputs((inputs) => ({ ...inputs, [name]: value }));
  491. }, []);
  492. const loadQuotaData = useCallback(async () => {
  493. setLoading(true);
  494. try {
  495. let url = '';
  496. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  497. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  498. if (isAdminUser) {
  499. url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
  500. } else {
  501. url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
  502. }
  503. const res = await API.get(url);
  504. const { success, message, data } = res.data;
  505. if (success) {
  506. setQuotaData(data);
  507. if (data.length === 0) {
  508. data.push({
  509. count: 0,
  510. model_name: '无数据',
  511. quota: 0,
  512. created_at: now.getTime() / 1000,
  513. });
  514. }
  515. data.sort((a, b) => a.created_at - b.created_at);
  516. updateChartData(data);
  517. } else {
  518. showError(message);
  519. }
  520. } finally {
  521. setLoading(false);
  522. }
  523. }, [start_timestamp, end_timestamp, username, dataExportDefaultTime, isAdminUser]);
  524. const loadUptimeData = useCallback(async () => {
  525. setUptimeLoading(true);
  526. try {
  527. const res = await API.get('/api/uptime/status');
  528. const { success, message, data } = res.data;
  529. if (success) {
  530. setUptimeData(data || []);
  531. } else {
  532. showError(message);
  533. }
  534. } catch (err) {
  535. console.error(err);
  536. } finally {
  537. setUptimeLoading(false);
  538. }
  539. }, []);
  540. const refresh = useCallback(async () => {
  541. await Promise.all([loadQuotaData(), loadUptimeData()]);
  542. }, [loadQuotaData, loadUptimeData]);
  543. const handleSearchConfirm = useCallback(() => {
  544. refresh();
  545. setSearchModalVisible(false);
  546. }, [refresh]);
  547. const initChart = useCallback(async () => {
  548. await loadQuotaData();
  549. await loadUptimeData();
  550. }, [loadQuotaData, loadUptimeData]);
  551. const showSearchModal = useCallback(() => {
  552. setSearchModalVisible(true);
  553. }, []);
  554. const handleCloseModal = useCallback(() => {
  555. setSearchModalVisible(false);
  556. }, []);
  557. // ========== Regular Functions ==========
  558. const checkApiScrollable = () => {
  559. if (apiScrollRef.current) {
  560. const element = apiScrollRef.current;
  561. const isScrollable = element.scrollHeight > element.clientHeight;
  562. const isAtBottom = element.scrollTop + element.clientHeight >= element.scrollHeight - 5;
  563. setShowApiScrollHint(isScrollable && !isAtBottom);
  564. }
  565. };
  566. const handleApiScroll = () => {
  567. checkApiScrollable();
  568. };
  569. const checkCardScrollable = (ref, setHintFunction) => {
  570. if (ref.current) {
  571. const element = ref.current;
  572. const isScrollable = element.scrollHeight > element.clientHeight;
  573. const isAtBottom = element.scrollTop + element.clientHeight >= element.scrollHeight - 5;
  574. setHintFunction(isScrollable && !isAtBottom);
  575. }
  576. };
  577. const handleCardScroll = (ref, setHintFunction) => {
  578. checkCardScrollable(ref, setHintFunction);
  579. };
  580. // ========== Effects for scroll detection ==========
  581. useEffect(() => {
  582. const timer = setTimeout(() => {
  583. checkApiScrollable();
  584. checkCardScrollable(announcementScrollRef, setShowAnnouncementScrollHint);
  585. checkCardScrollable(faqScrollRef, setShowFaqScrollHint);
  586. checkCardScrollable(uptimeScrollRef, setShowUptimeScrollHint);
  587. }, 100);
  588. return () => clearTimeout(timer);
  589. }, [uptimeData]);
  590. const getUserData = async () => {
  591. let res = await API.get(`/api/user/self`);
  592. const { success, message, data } = res.data;
  593. if (success) {
  594. userDispatch({ type: 'login', payload: data });
  595. } else {
  596. showError(message);
  597. }
  598. };
  599. // ========== Data Processing Functions ==========
  600. const processRawData = useCallback((data) => {
  601. const result = {
  602. totalQuota: 0,
  603. totalTimes: 0,
  604. totalTokens: 0,
  605. uniqueModels: new Set(),
  606. timePoints: [],
  607. timeQuotaMap: new Map(),
  608. timeTokensMap: new Map(),
  609. timeCountMap: new Map()
  610. };
  611. data.forEach((item) => {
  612. result.uniqueModels.add(item.model_name);
  613. result.totalTokens += item.token_used;
  614. result.totalQuota += item.quota;
  615. result.totalTimes += item.count;
  616. const timeKey = timestamp2string1(item.created_at, dataExportDefaultTime);
  617. if (!result.timePoints.includes(timeKey)) {
  618. result.timePoints.push(timeKey);
  619. }
  620. initializeMaps(timeKey, result.timeQuotaMap, result.timeTokensMap, result.timeCountMap);
  621. updateMapValue(result.timeQuotaMap, timeKey, item.quota);
  622. updateMapValue(result.timeTokensMap, timeKey, item.token_used);
  623. updateMapValue(result.timeCountMap, timeKey, item.count);
  624. });
  625. result.timePoints.sort();
  626. return result;
  627. }, [dataExportDefaultTime, initializeMaps, updateMapValue]);
  628. const calculateTrendData = useCallback((timePoints, timeQuotaMap, timeTokensMap, timeCountMap) => {
  629. const quotaTrend = timePoints.map(time => timeQuotaMap.get(time) || 0);
  630. const tokensTrend = timePoints.map(time => timeTokensMap.get(time) || 0);
  631. const countTrend = timePoints.map(time => timeCountMap.get(time) || 0);
  632. const rpmTrend = [];
  633. const tpmTrend = [];
  634. if (timePoints.length >= 2) {
  635. const interval = getTimeInterval(dataExportDefaultTime);
  636. for (let i = 0; i < timePoints.length; i++) {
  637. rpmTrend.push(timeCountMap.get(timePoints[i]) / interval);
  638. tpmTrend.push(timeTokensMap.get(timePoints[i]) / interval);
  639. }
  640. }
  641. return {
  642. balance: [],
  643. usedQuota: [],
  644. requestCount: [],
  645. times: countTrend,
  646. consumeQuota: quotaTrend,
  647. tokens: tokensTrend,
  648. rpm: rpmTrend,
  649. tpm: tpmTrend
  650. };
  651. }, [dataExportDefaultTime, getTimeInterval]);
  652. const generateModelColors = useCallback((uniqueModels) => {
  653. const newModelColors = {};
  654. Array.from(uniqueModels).forEach((modelName) => {
  655. newModelColors[modelName] =
  656. modelColorMap[modelName] ||
  657. modelColors[modelName] ||
  658. modelToColor(modelName);
  659. });
  660. return newModelColors;
  661. }, [modelColors]);
  662. const aggregateDataByTimeAndModel = useCallback((data) => {
  663. const aggregatedData = new Map();
  664. data.forEach((item) => {
  665. const timeKey = timestamp2string1(item.created_at, dataExportDefaultTime);
  666. const modelKey = item.model_name;
  667. const key = `${timeKey}-${modelKey}`;
  668. if (!aggregatedData.has(key)) {
  669. aggregatedData.set(key, {
  670. time: timeKey,
  671. model: modelKey,
  672. quota: 0,
  673. count: 0,
  674. });
  675. }
  676. const existing = aggregatedData.get(key);
  677. existing.quota += item.quota;
  678. existing.count += item.count;
  679. });
  680. return aggregatedData;
  681. }, [dataExportDefaultTime]);
  682. const generateChartTimePoints = useCallback((aggregatedData, data) => {
  683. let chartTimePoints = Array.from(
  684. new Set([...aggregatedData.values()].map((d) => d.time)),
  685. );
  686. if (chartTimePoints.length < 7) {
  687. const lastTime = Math.max(...data.map((item) => item.created_at));
  688. const interval = getTimeInterval(dataExportDefaultTime, true);
  689. chartTimePoints = Array.from({ length: 7 }, (_, i) =>
  690. timestamp2string1(lastTime - (6 - i) * interval, dataExportDefaultTime),
  691. );
  692. }
  693. return chartTimePoints;
  694. }, [dataExportDefaultTime, getTimeInterval]);
  695. const updateChartData = useCallback((data) => {
  696. const processedData = processRawData(data);
  697. const { totalQuota, totalTimes, totalTokens, uniqueModels, timePoints, timeQuotaMap, timeTokensMap, timeCountMap } = processedData;
  698. const trendDataResult = calculateTrendData(timePoints, timeQuotaMap, timeTokensMap, timeCountMap);
  699. setTrendData(trendDataResult);
  700. const newModelColors = generateModelColors(uniqueModels);
  701. setModelColors(newModelColors);
  702. const aggregatedData = aggregateDataByTimeAndModel(data);
  703. const modelTotals = new Map();
  704. for (let [_, value] of aggregatedData) {
  705. updateMapValue(modelTotals, value.model, value.count);
  706. }
  707. const newPieData = Array.from(modelTotals).map(([model, count]) => ({
  708. type: model,
  709. value: count,
  710. })).sort((a, b) => b.value - a.value);
  711. const chartTimePoints = generateChartTimePoints(aggregatedData, data);
  712. let newLineData = [];
  713. chartTimePoints.forEach((time) => {
  714. let timeData = Array.from(uniqueModels).map((model) => {
  715. const key = `${time}-${model}`;
  716. const aggregated = aggregatedData.get(key);
  717. return {
  718. Time: time,
  719. Model: model,
  720. rawQuota: aggregated?.quota || 0,
  721. Usage: aggregated?.quota ? getQuotaWithUnit(aggregated.quota, 4) : 0,
  722. };
  723. });
  724. const timeSum = timeData.reduce((sum, item) => sum + item.rawQuota, 0);
  725. timeData.sort((a, b) => b.rawQuota - a.rawQuota);
  726. timeData = timeData.map((item) => ({ ...item, TimeSum: timeSum }));
  727. newLineData.push(...timeData);
  728. });
  729. newLineData.sort((a, b) => a.Time.localeCompare(b.Time));
  730. updateChartSpec(
  731. setSpecPie,
  732. newPieData,
  733. `${t('总计')}:${renderNumber(totalTimes)}`,
  734. newModelColors,
  735. 'id0'
  736. );
  737. updateChartSpec(
  738. setSpecLine,
  739. newLineData,
  740. `${t('总计')}:${renderQuota(totalQuota, 2)}`,
  741. newModelColors,
  742. 'barData'
  743. );
  744. setPieData(newPieData);
  745. setLineData(newLineData);
  746. setConsumeQuota(totalQuota);
  747. setTimes(totalTimes);
  748. setConsumeTokens(totalTokens);
  749. }, [
  750. processRawData, calculateTrendData, generateModelColors, aggregateDataByTimeAndModel,
  751. generateChartTimePoints, updateChartSpec, updateMapValue, t
  752. ]);
  753. // ========== Status Data Management ==========
  754. const announcementLegendData = useMemo(() => [
  755. { color: 'grey', label: t('默认'), type: 'default' },
  756. { color: 'blue', label: t('进行中'), type: 'ongoing' },
  757. { color: 'green', label: t('成功'), type: 'success' },
  758. { color: 'orange', label: t('警告'), type: 'warning' },
  759. { color: 'red', label: t('异常'), type: 'error' }
  760. ], [t]);
  761. const uptimeStatusMap = useMemo(() => ({
  762. 1: { color: '#10b981', label: t('正常'), text: t('可用率') }, // UP
  763. 0: { color: '#ef4444', label: t('异常'), text: t('有异常') }, // DOWN
  764. 2: { color: '#f59e0b', label: t('高延迟'), text: t('高延迟') }, // PENDING
  765. 3: { color: '#3b82f6', label: t('维护中'), text: t('维护中') } // MAINTENANCE
  766. }), [t]);
  767. const uptimeLegendData = useMemo(() =>
  768. Object.entries(uptimeStatusMap).map(([status, info]) => ({
  769. status: Number(status),
  770. color: info.color,
  771. label: info.label
  772. })), [uptimeStatusMap]);
  773. const getUptimeStatusColor = useCallback((status) =>
  774. uptimeStatusMap[status]?.color || '#8b9aa7',
  775. [uptimeStatusMap]);
  776. const getUptimeStatusText = useCallback((status) =>
  777. uptimeStatusMap[status]?.text || t('未知'),
  778. [uptimeStatusMap, t]);
  779. const apiInfoData = useMemo(() => {
  780. return statusState?.status?.api_info || [];
  781. }, [statusState?.status?.api_info]);
  782. const announcementData = useMemo(() => {
  783. const announcements = statusState?.status?.announcements || [];
  784. // 处理后台配置的公告数据,自动生成相对时间
  785. return announcements.map(item => ({
  786. ...item,
  787. time: getRelativeTime(item.publishDate)
  788. }));
  789. }, [statusState?.status?.announcements]);
  790. const faqData = useMemo(() => {
  791. return statusState?.status?.faq || [];
  792. }, [statusState?.status?.faq]);
  793. // ========== Hooks - Effects ==========
  794. useEffect(() => {
  795. getUserData();
  796. if (!initialized.current) {
  797. initVChartSemiTheme({
  798. isWatchingThemeSwitch: true,
  799. });
  800. initialized.current = true;
  801. initChart();
  802. }
  803. }, []);
  804. return (
  805. <div className="bg-gray-50 h-full">
  806. <div className="flex items-center justify-between mb-4">
  807. <h2 className="text-2xl font-semibold text-gray-800">{getGreeting}</h2>
  808. <div className="flex gap-3">
  809. <IconButton
  810. icon={<IconSearch />}
  811. onClick={showSearchModal}
  812. className={`bg-green-500 hover:bg-green-600 ${ICON_BUTTON_CLASS}`}
  813. />
  814. <IconButton
  815. icon={<IconRefresh />}
  816. onClick={refresh}
  817. loading={loading}
  818. className={`bg-blue-500 hover:bg-blue-600 ${ICON_BUTTON_CLASS}`}
  819. />
  820. </div>
  821. </div>
  822. {/* 搜索条件Modal */}
  823. <Modal
  824. title={t('搜索条件')}
  825. visible={searchModalVisible}
  826. onOk={handleSearchConfirm}
  827. onCancel={handleCloseModal}
  828. closeOnEsc={true}
  829. size={isMobile() ? 'full-width' : 'small'}
  830. centered
  831. >
  832. <Form ref={formRef} layout='vertical' className="w-full">
  833. {createFormField(Form.DatePicker, {
  834. field: 'start_timestamp',
  835. label: t('起始时间'),
  836. initValue: start_timestamp,
  837. value: start_timestamp,
  838. type: 'dateTime',
  839. name: 'start_timestamp',
  840. onChange: (value) => handleInputChange(value, 'start_timestamp')
  841. })}
  842. {createFormField(Form.DatePicker, {
  843. field: 'end_timestamp',
  844. label: t('结束时间'),
  845. initValue: end_timestamp,
  846. value: end_timestamp,
  847. type: 'dateTime',
  848. name: 'end_timestamp',
  849. onChange: (value) => handleInputChange(value, 'end_timestamp')
  850. })}
  851. {createFormField(Form.Select, {
  852. field: 'data_export_default_time',
  853. label: t('时间粒度'),
  854. initValue: dataExportDefaultTime,
  855. placeholder: t('时间粒度'),
  856. name: 'data_export_default_time',
  857. optionList: timeOptions,
  858. onChange: (value) => handleInputChange(value, 'data_export_default_time')
  859. })}
  860. {isAdminUser && createFormField(Form.Input, {
  861. field: 'username',
  862. label: t('用户名称'),
  863. value: username,
  864. placeholder: t('可选值'),
  865. name: 'username',
  866. onChange: (value) => handleInputChange(value, 'username')
  867. })}
  868. </Form>
  869. </Modal>
  870. <Spin spinning={loading}>
  871. <div className="mb-4">
  872. <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
  873. {groupedStatsData.map((group, idx) => (
  874. <Card
  875. key={idx}
  876. {...CARD_PROPS}
  877. className={`${group.color} border-0 !rounded-2xl w-full`}
  878. title={group.title}
  879. >
  880. <div className="space-y-4">
  881. {group.items.map((item, itemIdx) => (
  882. <div
  883. key={itemIdx}
  884. className="flex items-center justify-between cursor-pointer"
  885. onClick={item.onClick}
  886. >
  887. <div className="flex items-center">
  888. <Avatar
  889. className="mr-3"
  890. size="small"
  891. color={item.avatarColor}
  892. >
  893. {item.icon}
  894. </Avatar>
  895. <div>
  896. <div className="text-xs text-gray-500">{item.title}</div>
  897. <div className="text-lg font-semibold">{item.value}</div>
  898. </div>
  899. </div>
  900. {item.trendData && item.trendData.length > 0 && (
  901. <div className="w-24 h-10">
  902. <VChart
  903. spec={getTrendSpec(item.trendData, item.trendColor)}
  904. option={CHART_CONFIG}
  905. />
  906. </div>
  907. )}
  908. </div>
  909. ))}
  910. </div>
  911. </Card>
  912. ))}
  913. </div>
  914. </div>
  915. <div className="mb-4">
  916. <div className={`grid grid-cols-1 gap-4 ${!statusState?.status?.self_use_mode_enabled ? 'lg:grid-cols-4' : ''}`}>
  917. <Card
  918. {...CARD_PROPS}
  919. className={`shadow-sm !rounded-2xl ${!statusState?.status?.self_use_mode_enabled ? 'lg:col-span-3' : ''}`}
  920. title={
  921. <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between w-full gap-3">
  922. <div className={FLEX_CENTER_GAP2}>
  923. <PieChart size={16} />
  924. {t('模型数据分析')}
  925. </div>
  926. <Tabs
  927. type="button"
  928. activeKey={activeChartTab}
  929. onChange={setActiveChartTab}
  930. >
  931. <TabPane tab={
  932. <span>
  933. <IconHistogram />
  934. {t('消耗分布')}
  935. </span>
  936. } itemKey="1" />
  937. <TabPane tab={
  938. <span>
  939. <IconPieChart2Stroked />
  940. {t('调用次数分布')}
  941. </span>
  942. } itemKey="2" />
  943. </Tabs>
  944. </div>
  945. }
  946. >
  947. <div style={{ height: 400 }}>
  948. {activeChartTab === '1' ? (
  949. <VChart
  950. spec={spec_line}
  951. option={CHART_CONFIG}
  952. />
  953. ) : (
  954. <VChart
  955. spec={spec_pie}
  956. option={CHART_CONFIG}
  957. />
  958. )}
  959. </div>
  960. </Card>
  961. {!statusState?.status?.self_use_mode_enabled && (
  962. <Card
  963. {...CARD_PROPS}
  964. className="bg-gray-50 border-0 !rounded-2xl"
  965. title={
  966. <div className={FLEX_CENTER_GAP2}>
  967. <Server size={16} />
  968. {t('API信息')}
  969. </div>
  970. }
  971. >
  972. <div className="card-content-container">
  973. <div
  974. ref={apiScrollRef}
  975. className="space-y-3 max-h-96 overflow-y-auto card-content-scroll"
  976. onScroll={handleApiScroll}
  977. >
  978. {apiInfoData.length > 0 ? (
  979. apiInfoData.map((api) => (
  980. <div key={api.id} className="flex p-2 hover:bg-white rounded-lg transition-colors cursor-pointer">
  981. <div className="flex-shrink-0 mr-3">
  982. <Avatar
  983. size="extra-small"
  984. color={api.color}
  985. >
  986. {api.route.substring(0, 2)}
  987. </Avatar>
  988. </div>
  989. <div className="flex-1">
  990. <div className="text-sm font-medium text-gray-900 mb-1 !font-bold flex items-center gap-2">
  991. <Tag
  992. prefixIcon={<Gauge size={12} />}
  993. size="small"
  994. color="white"
  995. shape='circle'
  996. onClick={() => handleSpeedTest(api.url)}
  997. className="cursor-pointer hover:opacity-80 text-xs"
  998. >
  999. {t('测速')}
  1000. </Tag>
  1001. {api.route}
  1002. </div>
  1003. <div
  1004. className="!text-semi-color-primary break-all cursor-pointer hover:underline mb-1"
  1005. onClick={() => handleCopyUrl(api.url)}
  1006. >
  1007. {api.url}
  1008. </div>
  1009. <div className="text-gray-500">
  1010. {api.description}
  1011. </div>
  1012. </div>
  1013. </div>
  1014. ))
  1015. ) : (
  1016. <div className="flex justify-center items-center py-8">
  1017. <Empty
  1018. image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
  1019. darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
  1020. title={t('暂无API信息')}
  1021. description={t('请联系管理员在系统设置中配置API信息')}
  1022. style={{ padding: '12px' }}
  1023. />
  1024. </div>
  1025. )}
  1026. </div>
  1027. <div
  1028. className="card-content-fade-indicator"
  1029. style={{ opacity: showApiScrollHint ? 1 : 0 }}
  1030. />
  1031. </div>
  1032. </Card>
  1033. )}
  1034. </div>
  1035. </div>
  1036. {/* 系统公告和常见问答卡片 */}
  1037. {!statusState?.status?.self_use_mode_enabled && (
  1038. <div className="mb-4">
  1039. <div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
  1040. {/* 公告卡片 */}
  1041. <Card
  1042. {...CARD_PROPS}
  1043. className="shadow-sm !rounded-2xl lg:col-span-2"
  1044. title={
  1045. <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2 w-full">
  1046. <div className="flex items-center gap-2">
  1047. <Bell size={16} />
  1048. {t('系统公告')}
  1049. <Tag size="small" color="grey" shape="circle">
  1050. {t('显示最新20条')}
  1051. </Tag>
  1052. </div>
  1053. {/* 图例 */}
  1054. <div className="flex flex-wrap gap-3 text-xs">
  1055. {announcementLegendData.map((legend, index) => (
  1056. <div key={index} className="flex items-center gap-1">
  1057. <div
  1058. className="w-2 h-2 rounded-full"
  1059. style={{
  1060. backgroundColor: legend.color === 'grey' ? '#8b9aa7' :
  1061. legend.color === 'blue' ? '#3b82f6' :
  1062. legend.color === 'green' ? '#10b981' :
  1063. legend.color === 'orange' ? '#f59e0b' :
  1064. legend.color === 'red' ? '#ef4444' : '#8b9aa7'
  1065. }}
  1066. />
  1067. <span className="text-gray-600">{legend.label}</span>
  1068. </div>
  1069. ))}
  1070. </div>
  1071. </div>
  1072. }
  1073. >
  1074. <div className="card-content-container">
  1075. <div
  1076. ref={announcementScrollRef}
  1077. className="p-2 max-h-96 overflow-y-auto card-content-scroll"
  1078. onScroll={() => handleCardScroll(announcementScrollRef, setShowAnnouncementScrollHint)}
  1079. >
  1080. {announcementData.length > 0 ? (
  1081. <Timeline
  1082. mode="alternate"
  1083. dataSource={announcementData}
  1084. />
  1085. ) : (
  1086. <div className="flex justify-center items-center py-8">
  1087. <Empty
  1088. image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
  1089. darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
  1090. title={t('暂无系统公告')}
  1091. description={t('请联系管理员在系统设置中配置公告信息')}
  1092. style={{ padding: '12px' }}
  1093. />
  1094. </div>
  1095. )}
  1096. </div>
  1097. <div
  1098. className="card-content-fade-indicator"
  1099. style={{ opacity: showAnnouncementScrollHint ? 1 : 0 }}
  1100. />
  1101. </div>
  1102. </Card>
  1103. {/* 常见问答卡片 */}
  1104. <Card
  1105. {...CARD_PROPS}
  1106. className="shadow-sm !rounded-2xl lg:col-span-1"
  1107. title={
  1108. <div className={FLEX_CENTER_GAP2}>
  1109. <HelpCircle size={16} />
  1110. {t('常见问答')}
  1111. </div>
  1112. }
  1113. >
  1114. <div className="card-content-container">
  1115. <div
  1116. ref={faqScrollRef}
  1117. className="p-2 max-h-96 overflow-y-auto card-content-scroll"
  1118. onScroll={() => handleCardScroll(faqScrollRef, setShowFaqScrollHint)}
  1119. >
  1120. {faqData.length > 0 ? (
  1121. <Collapse
  1122. accordion
  1123. expandIcon={<IconPlus />}
  1124. collapseIcon={<IconMinus />}
  1125. >
  1126. {faqData.map((item, index) => (
  1127. <Collapse.Panel
  1128. key={index}
  1129. header={item.title}
  1130. itemKey={index.toString()}
  1131. >
  1132. <p>{item.content}</p>
  1133. </Collapse.Panel>
  1134. ))}
  1135. </Collapse>
  1136. ) : (
  1137. <div className="flex justify-center items-center py-8">
  1138. <Empty
  1139. image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
  1140. darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
  1141. title={t('暂无常见问答')}
  1142. description={t('请联系管理员在系统设置中配置常见问答')}
  1143. style={{ padding: '12px' }}
  1144. />
  1145. </div>
  1146. )}
  1147. </div>
  1148. <div
  1149. className="card-content-fade-indicator"
  1150. style={{ opacity: showFaqScrollHint ? 1 : 0 }}
  1151. />
  1152. </div>
  1153. </Card>
  1154. {/* 服务可用性卡片 */}
  1155. <Card
  1156. {...CARD_PROPS}
  1157. className="shadow-sm !rounded-2xl lg:col-span-1"
  1158. title={
  1159. <div className="flex items-center justify-between w-full gap-2">
  1160. <div className="flex items-center gap-2">
  1161. <Gauge size={16} />
  1162. {t('服务可用性')}
  1163. </div>
  1164. <IconButton
  1165. icon={<IconRefresh />}
  1166. onClick={loadUptimeData}
  1167. loading={uptimeLoading}
  1168. size="small"
  1169. theme="borderless"
  1170. className="text-gray-500 hover:text-blue-500 hover:bg-blue-50 !rounded-full"
  1171. />
  1172. </div>
  1173. }
  1174. footer={uptimeData.length > 0 ? (
  1175. <Card
  1176. bordered={false}
  1177. className="!rounded-2xl backdrop-blur !shadow-none"
  1178. >
  1179. <div className="flex flex-wrap gap-3 text-xs justify-center">
  1180. {uptimeLegendData.map((legend, index) => (
  1181. <div key={index} className="flex items-center gap-1">
  1182. <div
  1183. className="w-2 h-2 rounded-full"
  1184. style={{ backgroundColor: legend.color }}
  1185. />
  1186. <span className="text-gray-600">{legend.label}</span>
  1187. </div>
  1188. ))}
  1189. </div>
  1190. </Card>
  1191. ) : null}
  1192. footerStyle={uptimeData.length > 0 ? { padding: '0px' } : undefined}
  1193. >
  1194. <div className="card-content-container">
  1195. <Spin spinning={uptimeLoading}>
  1196. <div
  1197. ref={uptimeScrollRef}
  1198. className="p-2 max-h-80 overflow-y-auto card-content-scroll"
  1199. onScroll={() => handleCardScroll(uptimeScrollRef, setShowUptimeScrollHint)}
  1200. >
  1201. {uptimeData.length > 0 ? (
  1202. uptimeData.map((monitor, idx) => (
  1203. <div key={idx} className="p-2 hover:bg-white rounded-lg transition-colors">
  1204. <div className="flex items-center justify-between mb-1">
  1205. <div className="flex items-center gap-2">
  1206. <div
  1207. className="w-2 h-2 rounded-full flex-shrink-0"
  1208. style={{
  1209. backgroundColor: getUptimeStatusColor(monitor.status)
  1210. }}
  1211. />
  1212. <span className="text-sm font-medium text-gray-900">{monitor.name}</span>
  1213. </div>
  1214. <span className="text-xs text-gray-500">{((monitor.uptime || 0) * 100).toFixed(2)}%</span>
  1215. </div>
  1216. <div className="flex items-center gap-2">
  1217. <span className="text-xs text-gray-500">{getUptimeStatusText(monitor.status)}</span>
  1218. <div className="flex-1">
  1219. <Progress
  1220. percent={(monitor.uptime || 0) * 100}
  1221. showInfo={false}
  1222. aria-label={`${monitor.name} uptime`}
  1223. stroke={getUptimeStatusColor(monitor.status)}
  1224. />
  1225. </div>
  1226. </div>
  1227. </div>
  1228. ))
  1229. ) : (
  1230. <div className="flex justify-center items-center py-8">
  1231. <Empty
  1232. image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
  1233. darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
  1234. title={t('暂无监控数据')}
  1235. description={t('请联系管理员在系统设置中配置Uptime')}
  1236. style={{ padding: '12px' }}
  1237. />
  1238. </div>
  1239. )}
  1240. </div>
  1241. </Spin>
  1242. <div
  1243. className="card-content-fade-indicator"
  1244. style={{ opacity: showUptimeScrollHint ? 1 : 0 }}
  1245. />
  1246. </div>
  1247. </Card>
  1248. </div>
  1249. </div>
  1250. )}
  1251. </Spin>
  1252. </div>
  1253. );
  1254. };
  1255. export default Detail;