LogsTable.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951
  1. import React, { useContext, useEffect, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import {
  4. API,
  5. copy,
  6. getTodayStartTimestamp,
  7. isAdmin,
  8. showError,
  9. showSuccess,
  10. timestamp2string,
  11. } from '../helpers';
  12. import {
  13. Avatar,
  14. Button, Descriptions,
  15. Form,
  16. Layout,
  17. Modal, Popover,
  18. Select,
  19. Space,
  20. Spin,
  21. Table,
  22. Tag,
  23. Tooltip
  24. } from '@douyinfe/semi-ui';
  25. import { ITEMS_PER_PAGE } from '../constants';
  26. import {
  27. renderAudioModelPrice, renderGroup,
  28. renderModelPrice, renderModelPriceSimple,
  29. renderNumber,
  30. renderQuota,
  31. stringToColor
  32. } from '../helpers/render';
  33. import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
  34. import { getLogOther } from '../helpers/other.js';
  35. import { StyleContext } from '../context/Style/index.js';
  36. import { IconInherit, IconRefresh } from '@douyinfe/semi-icons';
  37. const { Header } = Layout;
  38. function renderTimestamp(timestamp) {
  39. return <>{timestamp2string(timestamp)}</>;
  40. }
  41. const MODE_OPTIONS = [
  42. { key: 'all', text: 'all', value: 'all' },
  43. { key: 'self', text: 'current user', value: 'self' },
  44. ];
  45. const colors = [
  46. 'amber',
  47. 'blue',
  48. 'cyan',
  49. 'green',
  50. 'grey',
  51. 'indigo',
  52. 'light-blue',
  53. 'lime',
  54. 'orange',
  55. 'pink',
  56. 'purple',
  57. 'red',
  58. 'teal',
  59. 'violet',
  60. 'yellow',
  61. ];
  62. const LogsTable = () => {
  63. const { t } = useTranslation();
  64. function renderType(type) {
  65. switch (type) {
  66. case 1:
  67. return <Tag color='cyan' size='large'>{t('充值')}</Tag>;
  68. case 2:
  69. return <Tag color='lime' size='large'>{t('消费')}</Tag>;
  70. case 3:
  71. return <Tag color='orange' size='large'>{t('管理')}</Tag>;
  72. case 4:
  73. return <Tag color='purple' size='large'>{t('系统')}</Tag>;
  74. default:
  75. return <Tag color='black' size='large'>{t('未知')}</Tag>;
  76. }
  77. }
  78. function renderIsStream(bool) {
  79. if (bool) {
  80. return <Tag color='blue' size='large'>{t('流')}</Tag>;
  81. } else {
  82. return <Tag color='purple' size='large'>{t('非流')}</Tag>;
  83. }
  84. }
  85. function renderUseTime(type) {
  86. const time = parseInt(type);
  87. if (time < 101) {
  88. return (
  89. <Tag color='green' size='large'>
  90. {' '}
  91. {time} s{' '}
  92. </Tag>
  93. );
  94. } else if (time < 300) {
  95. return (
  96. <Tag color='orange' size='large'>
  97. {' '}
  98. {time} s{' '}
  99. </Tag>
  100. );
  101. } else {
  102. return (
  103. <Tag color='red' size='large'>
  104. {' '}
  105. {time} s{' '}
  106. </Tag>
  107. );
  108. }
  109. }
  110. function renderFirstUseTime(type) {
  111. let time = parseFloat(type) / 1000.0;
  112. time = time.toFixed(1);
  113. if (time < 3) {
  114. return (
  115. <Tag color='green' size='large'>
  116. {' '}
  117. {time} s{' '}
  118. </Tag>
  119. );
  120. } else if (time < 10) {
  121. return (
  122. <Tag color='orange' size='large'>
  123. {' '}
  124. {time} s{' '}
  125. </Tag>
  126. );
  127. } else {
  128. return (
  129. <Tag color='red' size='large'>
  130. {' '}
  131. {time} s{' '}
  132. </Tag>
  133. );
  134. }
  135. }
  136. function renderModelName(record) {
  137. let other = getLogOther(record.other);
  138. let modelMapped = other?.is_model_mapped && other?.upstream_model_name && other?.upstream_model_name !== '';
  139. if (!modelMapped) {
  140. return <Tag
  141. color={stringToColor(record.model_name)}
  142. size='large'
  143. onClick={(event) => {
  144. copyText(event, record.model_name).then(r => {});
  145. }}
  146. >
  147. {' '}{record.model_name}{' '}
  148. </Tag>;
  149. } else {
  150. return (
  151. <>
  152. <Space vertical align={'start'}>
  153. <Popover content={
  154. <div style={{padding: 10}}>
  155. <Space vertical align={'start'}>
  156. <Tag
  157. color={stringToColor(record.model_name)}
  158. size='large'
  159. onClick={(event) => {
  160. copyText(event, record.model_name).then(r => {});
  161. }}
  162. >
  163. {t('请求并计费模型')}{' '}{record.model_name}{' '}
  164. </Tag>
  165. <Tag
  166. color={stringToColor(other.upstream_model_name)}
  167. size='large'
  168. onClick={(event) => {
  169. copyText(event, other.upstream_model_name).then(r => {});
  170. }}
  171. >
  172. {t('实际模型')}{' '}{other.upstream_model_name}{' '}
  173. </Tag>
  174. </Space>
  175. </div>
  176. }>
  177. <Tag
  178. color={stringToColor(record.model_name)}
  179. size='large'
  180. onClick={(event) => {
  181. copyText(event, record.model_name).then(r => {});
  182. }}
  183. suffixIcon={<IconRefresh style={{width: '0.8em', height: '0.8em', opacity: 0.6}} />}
  184. >
  185. {' '}{record.model_name}{' '}
  186. </Tag>
  187. </Popover>
  188. {/*<Tooltip content={t('实际模型')}>*/}
  189. {/* <Tag*/}
  190. {/* color={stringToColor(other.upstream_model_name)}*/}
  191. {/* size='large'*/}
  192. {/* onClick={(event) => {*/}
  193. {/* copyText(event, other.upstream_model_name).then(r => {});*/}
  194. {/* }}*/}
  195. {/* >*/}
  196. {/* {' '}{other.upstream_model_name}{' '}*/}
  197. {/* </Tag>*/}
  198. {/*</Tooltip>*/}
  199. </Space>
  200. </>
  201. );
  202. }
  203. }
  204. const columns = [
  205. {
  206. title: t('时间'),
  207. dataIndex: 'timestamp2string',
  208. },
  209. {
  210. title: t('渠道'),
  211. dataIndex: 'channel',
  212. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  213. render: (text, record, index) => {
  214. return isAdminUser ? (
  215. record.type === 0 || record.type === 2 ? (
  216. <div>
  217. {
  218. <Tooltip content={record.channel_name || '[未知]'}>
  219. <Tag
  220. color={colors[parseInt(text) % colors.length]}
  221. size='large'
  222. >
  223. {' '}
  224. {text}{' '}
  225. </Tag>
  226. </Tooltip>
  227. }
  228. </div>
  229. ) : (
  230. <></>
  231. )
  232. ) : (
  233. <></>
  234. );
  235. },
  236. },
  237. {
  238. title: t('用户'),
  239. dataIndex: 'username',
  240. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  241. render: (text, record, index) => {
  242. return isAdminUser ? (
  243. <div>
  244. <Avatar
  245. size='small'
  246. color={stringToColor(text)}
  247. style={{ marginRight: 4 }}
  248. onClick={(event) => {
  249. event.stopPropagation();
  250. showUserInfo(record.user_id)
  251. }}
  252. >
  253. {typeof text === 'string' && text.slice(0, 1)}
  254. </Avatar>
  255. {text}
  256. </div>
  257. ) : (
  258. <></>
  259. );
  260. },
  261. },
  262. {
  263. title: t('令牌'),
  264. dataIndex: 'token_name',
  265. render: (text, record, index) => {
  266. return record.type === 0 || record.type === 2 ? (
  267. <div>
  268. <Tag
  269. color='grey'
  270. size='large'
  271. onClick={(event) => {
  272. //cancel the row click event
  273. copyText(event, text);
  274. }}
  275. >
  276. {' '}
  277. {t(text)}{' '}
  278. </Tag>
  279. </div>
  280. ) : (
  281. <></>
  282. );
  283. },
  284. },
  285. {
  286. title: t('分组'),
  287. dataIndex: 'group',
  288. render: (text, record, index) => {
  289. if (record.type === 0 || record.type === 2) {
  290. if (record.group) {
  291. return (
  292. <>
  293. {renderGroup(record.group)}
  294. </>
  295. );
  296. } else {
  297. let other = null;
  298. try {
  299. other = JSON.parse(record.other);
  300. } catch (e) {
  301. console.error(`Failed to parse record.other: "${record.other}".`, e);
  302. }
  303. if (other === null) {
  304. return <></>;
  305. }
  306. if (other.group !== undefined) {
  307. return (
  308. <>
  309. {renderGroup(other.group)}
  310. </>
  311. );
  312. } else {
  313. return <></>;
  314. }
  315. }
  316. } else {
  317. return <></>;
  318. }
  319. },
  320. },
  321. {
  322. title: t('类型'),
  323. dataIndex: 'type',
  324. render: (text, record, index) => {
  325. return <>{renderType(text)}</>;
  326. },
  327. },
  328. {
  329. title: t('模型'),
  330. dataIndex: 'model_name',
  331. render: (text, record, index) => {
  332. return record.type === 0 || record.type === 2 ? (
  333. <>{renderModelName(record)}</>
  334. ) : (
  335. <></>
  336. );
  337. },
  338. },
  339. {
  340. title: t('用时/首字'),
  341. dataIndex: 'use_time',
  342. render: (text, record, index) => {
  343. if (record.is_stream) {
  344. let other = getLogOther(record.other);
  345. return (
  346. <>
  347. <Space>
  348. {renderUseTime(text)}
  349. {renderFirstUseTime(other.frt)}
  350. {renderIsStream(record.is_stream)}
  351. </Space>
  352. </>
  353. );
  354. } else {
  355. return (
  356. <>
  357. <Space>
  358. {renderUseTime(text)}
  359. {renderIsStream(record.is_stream)}
  360. </Space>
  361. </>
  362. );
  363. }
  364. },
  365. },
  366. {
  367. title: t('提示'),
  368. dataIndex: 'prompt_tokens',
  369. render: (text, record, index) => {
  370. return record.type === 0 || record.type === 2 ? (
  371. <>{<span> {text} </span>}</>
  372. ) : (
  373. <></>
  374. );
  375. },
  376. },
  377. {
  378. title: t('补全'),
  379. dataIndex: 'completion_tokens',
  380. render: (text, record, index) => {
  381. return parseInt(text) > 0 &&
  382. (record.type === 0 || record.type === 2) ? (
  383. <>{<span> {text} </span>}</>
  384. ) : (
  385. <></>
  386. );
  387. },
  388. },
  389. {
  390. title: t('花费'),
  391. dataIndex: 'quota',
  392. render: (text, record, index) => {
  393. return record.type === 0 || record.type === 2 ? (
  394. <>{renderQuota(text, 6)}</>
  395. ) : (
  396. <></>
  397. );
  398. },
  399. },
  400. {
  401. title: t('重试'),
  402. dataIndex: 'retry',
  403. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  404. render: (text, record, index) => {
  405. let content = t('渠道') + `:${record.channel}`;
  406. if (record.other !== '') {
  407. let other = JSON.parse(record.other);
  408. if (other === null) {
  409. return <></>;
  410. }
  411. if (other.admin_info !== undefined) {
  412. if (
  413. other.admin_info.use_channel !== null &&
  414. other.admin_info.use_channel !== undefined &&
  415. other.admin_info.use_channel !== ''
  416. ) {
  417. // channel id array
  418. let useChannel = other.admin_info.use_channel;
  419. let useChannelStr = useChannel.join('->');
  420. content = t('渠道') + `:${useChannelStr}`;
  421. }
  422. }
  423. }
  424. return isAdminUser ? <div>{content}</div> : <></>;
  425. },
  426. },
  427. {
  428. title: t('详情'),
  429. dataIndex: 'content',
  430. render: (text, record, index) => {
  431. let other = getLogOther(record.other);
  432. if (other == null || record.type !== 2) {
  433. return (
  434. <Paragraph
  435. ellipsis={{
  436. rows: 2,
  437. showTooltip: {
  438. type: 'popover',
  439. opts: { style: { width: 240 } },
  440. },
  441. }}
  442. style={{ maxWidth: 240 }}
  443. >
  444. {text}
  445. </Paragraph>
  446. );
  447. }
  448. let content = renderModelPriceSimple(
  449. other.model_ratio,
  450. other.model_price,
  451. other.group_ratio,
  452. other.cache_tokens || 0,
  453. other.cache_ratio || 1.0,
  454. );
  455. return (
  456. <Paragraph
  457. ellipsis={{
  458. rows: 2,
  459. }}
  460. style={{ maxWidth: 240 }}
  461. >
  462. {content}
  463. </Paragraph>
  464. );
  465. },
  466. },
  467. ];
  468. const [styleState, styleDispatch] = useContext(StyleContext);
  469. const [logs, setLogs] = useState([]);
  470. const [expandData, setExpandData] = useState({});
  471. const [showStat, setShowStat] = useState(false);
  472. const [loading, setLoading] = useState(false);
  473. const [loadingStat, setLoadingStat] = useState(false);
  474. const [activePage, setActivePage] = useState(1);
  475. const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
  476. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  477. const [logType, setLogType] = useState(0);
  478. const isAdminUser = isAdmin();
  479. let now = new Date();
  480. // 初始化start_timestamp为今天0点
  481. const [inputs, setInputs] = useState({
  482. username: '',
  483. token_name: '',
  484. model_name: '',
  485. start_timestamp: timestamp2string(getTodayStartTimestamp()),
  486. end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
  487. channel: '',
  488. group: '',
  489. });
  490. const {
  491. username,
  492. token_name,
  493. model_name,
  494. start_timestamp,
  495. end_timestamp,
  496. channel,
  497. group,
  498. } = inputs;
  499. const [stat, setStat] = useState({
  500. quota: 0,
  501. token: 0,
  502. });
  503. const handleInputChange = (value, name) => {
  504. setInputs(inputs => ({ ...inputs, [name]: value }));
  505. };
  506. const getLogSelfStat = async () => {
  507. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  508. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  509. let url = `/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
  510. url = encodeURI(url);
  511. let res = await API.get(url);
  512. const { success, message, data } = res.data;
  513. if (success) {
  514. setStat(data);
  515. } else {
  516. showError(message);
  517. }
  518. };
  519. const getLogStat = async () => {
  520. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  521. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  522. let url = `/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
  523. url = encodeURI(url);
  524. let res = await API.get(url);
  525. const { success, message, data } = res.data;
  526. if (success) {
  527. setStat(data);
  528. } else {
  529. showError(message);
  530. }
  531. };
  532. const handleEyeClick = async () => {
  533. if (loadingStat) {
  534. return;
  535. }
  536. setLoadingStat(true);
  537. if (isAdminUser) {
  538. await getLogStat();
  539. } else {
  540. await getLogSelfStat();
  541. }
  542. setShowStat(true);
  543. setLoadingStat(false);
  544. };
  545. const showUserInfo = async (userId) => {
  546. if (!isAdminUser) {
  547. return;
  548. }
  549. const res = await API.get(`/api/user/${userId}`);
  550. const { success, message, data } = res.data;
  551. if (success) {
  552. Modal.info({
  553. title: t('用户信息'),
  554. content: (
  555. <div style={{ padding: 12 }}>
  556. <p>{t('用户名')}: {data.username}</p>
  557. <p>{t('余额')}: {renderQuota(data.quota)}</p>
  558. <p>{t('已用额度')}:{renderQuota(data.used_quota)}</p>
  559. <p>{t('请求次数')}:{renderNumber(data.request_count)}</p>
  560. </div>
  561. ),
  562. centered: true,
  563. });
  564. } else {
  565. showError(message);
  566. }
  567. };
  568. const setLogsFormat = (logs) => {
  569. let expandDatesLocal = {};
  570. for (let i = 0; i < logs.length; i++) {
  571. logs[i].timestamp2string = timestamp2string(logs[i].created_at);
  572. logs[i].key = logs[i].id;
  573. let other = getLogOther(logs[i].other);
  574. let expandDataLocal = [];
  575. if (isAdmin()) {
  576. // let content = '渠道:' + logs[i].channel;
  577. // if (other.admin_info !== undefined) {
  578. // if (
  579. // other.admin_info.use_channel !== null &&
  580. // other.admin_info.use_channel !== undefined &&
  581. // other.admin_info.use_channel !== ''
  582. // ) {
  583. // // channel id array
  584. // let useChannel = other.admin_info.use_channel;
  585. // let useChannelStr = useChannel.join('->');
  586. // content = `渠道:${useChannelStr}`;
  587. // }
  588. // }
  589. // expandDataLocal.push({
  590. // key: '渠道重试',
  591. // value: content,
  592. // })
  593. }
  594. if (isAdminUser && (logs[i].type === 0 || logs[i].type === 2)) {
  595. expandDataLocal.push({
  596. key: t('渠道信息'),
  597. value: `${logs[i].channel} - ${logs[i].channel_name || '[未知]'}`
  598. });
  599. }
  600. if (other?.ws || other?.audio) {
  601. expandDataLocal.push({
  602. key: t('语音输入'),
  603. value: other.audio_input,
  604. });
  605. expandDataLocal.push({
  606. key: t('语音输出'),
  607. value: other.audio_output,
  608. });
  609. expandDataLocal.push({
  610. key: t('文字输入'),
  611. value: other.text_input,
  612. });
  613. expandDataLocal.push({
  614. key: t('文字输出'),
  615. value: other.text_output,
  616. });
  617. }
  618. if (other?.cache_tokens > 0) {
  619. expandDataLocal.push({
  620. key: t('缓存 Tokens'),
  621. value: other.cache_tokens,
  622. });
  623. }
  624. expandDataLocal.push({
  625. key: t('日志详情'),
  626. value: logs[i].content,
  627. });
  628. if (logs[i].type === 2) {
  629. let modelMapped = other?.is_model_mapped && other?.upstream_model_name && other?.upstream_model_name !== '';
  630. if (modelMapped) {
  631. expandDataLocal.push({
  632. key: t('请求并计费模型'),
  633. value: logs[i].model_name,
  634. });
  635. expandDataLocal.push({
  636. key: t('实际模型'),
  637. value: other.upstream_model_name,
  638. });
  639. }
  640. let content = '';
  641. if (other?.ws || other?.audio) {
  642. content = renderAudioModelPrice(
  643. other.text_input,
  644. other.text_output,
  645. other.model_ratio,
  646. other.model_price,
  647. other.completion_ratio,
  648. other.audio_input,
  649. other.audio_output,
  650. other?.audio_ratio,
  651. other?.audio_completion_ratio,
  652. other.group_ratio,
  653. other.cache_tokens || 0,
  654. other.cache_ratio || 1.0,
  655. );
  656. } else {
  657. content = renderModelPrice(
  658. logs[i].prompt_tokens,
  659. logs[i].completion_tokens,
  660. other.model_ratio,
  661. other.model_price,
  662. other.completion_ratio,
  663. other.group_ratio,
  664. other.cache_tokens || 0,
  665. other.cache_ratio || 1.0,
  666. );
  667. }
  668. expandDataLocal.push({
  669. key: t('计费过程'),
  670. value: content,
  671. });
  672. if (other?.reasoning_effort) {
  673. expandDataLocal.push({
  674. key: t('Reasoning Effort'),
  675. value: other.reasoning_effort,
  676. });
  677. }
  678. }
  679. expandDatesLocal[logs[i].key] = expandDataLocal;
  680. }
  681. setExpandData(expandDatesLocal);
  682. setLogs(logs);
  683. };
  684. const loadLogs = async (startIdx, pageSize, logType = 0) => {
  685. setLoading(true);
  686. let url = '';
  687. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  688. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  689. if (isAdminUser) {
  690. url = `/api/log/?p=${startIdx}&page_size=${pageSize}&type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
  691. } else {
  692. url = `/api/log/self/?p=${startIdx}&page_size=${pageSize}&type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
  693. }
  694. url = encodeURI(url);
  695. const res = await API.get(url);
  696. const { success, message, data } = res.data;
  697. if (success) {
  698. const newPageData = data.items;
  699. setActivePage(data.page);
  700. setPageSize(data.page_size);
  701. setLogCount(data.total);
  702. setLogsFormat(newPageData);
  703. } else {
  704. showError(message);
  705. }
  706. setLoading(false);
  707. };
  708. const handlePageChange = (page) => {
  709. setActivePage(page);
  710. loadLogs(page, pageSize, logType).then((r) => {});
  711. };
  712. const handlePageSizeChange = async (size) => {
  713. localStorage.setItem('page-size', size + '');
  714. setPageSize(size);
  715. setActivePage(1);
  716. loadLogs(activePage, size)
  717. .then()
  718. .catch((reason) => {
  719. showError(reason);
  720. });
  721. };
  722. const refresh = async () => {
  723. setActivePage(1);
  724. handleEyeClick();
  725. await loadLogs(activePage, pageSize, logType);
  726. };
  727. const copyText = async (e, text) => {
  728. e.stopPropagation();
  729. if (await copy(text)) {
  730. showSuccess('已复制:' + text);
  731. } else {
  732. Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
  733. }
  734. };
  735. useEffect(() => {
  736. const localPageSize =
  737. parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
  738. setPageSize(localPageSize);
  739. loadLogs(activePage, localPageSize)
  740. .then()
  741. .catch((reason) => {
  742. showError(reason);
  743. });
  744. handleEyeClick();
  745. }, []);
  746. const expandRowRender = (record, index) => {
  747. return <Descriptions data={expandData[record.key]} />;
  748. };
  749. return (
  750. <>
  751. <Layout>
  752. <Header>
  753. <Spin spinning={loadingStat}>
  754. <Space>
  755. <Tag color='green' size='large' style={{ padding: 15 }}>
  756. {t('总消耗额度')}: {renderQuota(stat.quota)}
  757. </Tag>
  758. <Tag color='blue' size='large' style={{ padding: 15 }}>
  759. RPM: {stat.rpm}
  760. </Tag>
  761. <Tag color='purple' size='large' style={{ padding: 15 }}>
  762. TPM: {stat.tpm}
  763. </Tag>
  764. </Space>
  765. </Spin>
  766. </Header>
  767. <Form layout='horizontal' style={{ marginTop: 10 }}>
  768. <>
  769. <Form.Section>
  770. <div style={{ marginBottom: 10 }}>
  771. {
  772. styleState.isMobile ? (
  773. <div>
  774. <Form.DatePicker
  775. field='start_timestamp'
  776. label={t('起始时间')}
  777. style={{ width: 272 }}
  778. initValue={start_timestamp}
  779. type='dateTime'
  780. onChange={(value) => {
  781. console.log(value);
  782. handleInputChange(value, 'start_timestamp')
  783. }}
  784. />
  785. <Form.DatePicker
  786. field='end_timestamp'
  787. fluid
  788. label={t('结束时间')}
  789. style={{ width: 272 }}
  790. initValue={end_timestamp}
  791. type='dateTime'
  792. onChange={(value) => handleInputChange(value, 'end_timestamp')}
  793. />
  794. </div>
  795. ) : (
  796. <Form.DatePicker
  797. field="range_timestamp"
  798. label={t('时间范围')}
  799. initValue={[start_timestamp, end_timestamp]}
  800. type="dateTimeRange"
  801. name="range_timestamp"
  802. onChange={(value) => {
  803. if (Array.isArray(value) && value.length === 2) {
  804. handleInputChange(value[0], 'start_timestamp');
  805. handleInputChange(value[1], 'end_timestamp');
  806. }
  807. }}
  808. />
  809. )
  810. }
  811. </div>
  812. </Form.Section>
  813. <Form.Input
  814. field='token_name'
  815. label={t('令牌名称')}
  816. value={token_name}
  817. placeholder={t('可选值')}
  818. name='token_name'
  819. onChange={(value) => handleInputChange(value, 'token_name')}
  820. />
  821. <Form.Input
  822. field='model_name'
  823. label={t('模型名称')}
  824. value={model_name}
  825. placeholder={t('可选值')}
  826. name='model_name'
  827. onChange={(value) => handleInputChange(value, 'model_name')}
  828. />
  829. <Form.Input
  830. field='group'
  831. label={t('分组')}
  832. value={group}
  833. placeholder={t('可选值')}
  834. name='group'
  835. onChange={(value) => handleInputChange(value, 'group')}
  836. />
  837. {isAdminUser && (
  838. <>
  839. <Form.Input
  840. field='channel'
  841. label={t('渠道 ID')}
  842. value={channel}
  843. placeholder={t('可选值')}
  844. name='channel'
  845. onChange={(value) => handleInputChange(value, 'channel')}
  846. />
  847. <Form.Input
  848. field='username'
  849. label={t('用户名称')}
  850. value={username}
  851. placeholder={t('可选值')}
  852. name='username'
  853. onChange={(value) => handleInputChange(value, 'username')}
  854. />
  855. </>
  856. )}
  857. <Button
  858. label={t('查询')}
  859. type='primary'
  860. htmlType='submit'
  861. className='btn-margin-right'
  862. onClick={refresh}
  863. loading={loading}
  864. style={{ marginTop: 24 }}
  865. >
  866. {t('查询')}
  867. </Button>
  868. <Form.Section></Form.Section>
  869. </>
  870. </Form>
  871. <div style={{marginTop:10}}>
  872. <Select
  873. defaultValue='0'
  874. style={{ width: 120 }}
  875. onChange={(value) => {
  876. setLogType(parseInt(value));
  877. loadLogs(0, pageSize, parseInt(value));
  878. }}
  879. >
  880. <Select.Option value='0'>{t('全部')}</Select.Option>
  881. <Select.Option value='1'>{t('充值')}</Select.Option>
  882. <Select.Option value='2'>{t('消费')}</Select.Option>
  883. <Select.Option value='3'>{t('管理')}</Select.Option>
  884. <Select.Option value='4'>{t('系统')}</Select.Option>
  885. </Select>
  886. </div>
  887. <Table
  888. style={{ marginTop: 5 }}
  889. columns={columns}
  890. expandedRowRender={expandRowRender}
  891. expandRowByClick={true}
  892. dataSource={logs}
  893. rowKey="key"
  894. pagination={{
  895. formatPageText: (page) =>
  896. t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
  897. start: page.currentStart,
  898. end: page.currentEnd,
  899. total: logCount
  900. }),
  901. currentPage: activePage,
  902. pageSize: pageSize,
  903. total: logCount,
  904. pageSizeOpts: [10, 20, 50, 100],
  905. showSizeChanger: true,
  906. onPageSizeChange: (size) => {
  907. handlePageSizeChange(size);
  908. },
  909. onPageChange: handlePageChange,
  910. }}
  911. />
  912. </Layout>
  913. </>
  914. );
  915. };
  916. export default LogsTable;