LogsTable.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939
  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. );
  453. return (
  454. <Paragraph
  455. ellipsis={{
  456. rows: 2,
  457. }}
  458. style={{ maxWidth: 240 }}
  459. >
  460. {content}
  461. </Paragraph>
  462. );
  463. },
  464. },
  465. ];
  466. const [styleState, styleDispatch] = useContext(StyleContext);
  467. const [logs, setLogs] = useState([]);
  468. const [expandData, setExpandData] = useState({});
  469. const [showStat, setShowStat] = useState(false);
  470. const [loading, setLoading] = useState(false);
  471. const [loadingStat, setLoadingStat] = useState(false);
  472. const [activePage, setActivePage] = useState(1);
  473. const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
  474. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  475. const [logType, setLogType] = useState(0);
  476. const isAdminUser = isAdmin();
  477. let now = new Date();
  478. // 初始化start_timestamp为今天0点
  479. const [inputs, setInputs] = useState({
  480. username: '',
  481. token_name: '',
  482. model_name: '',
  483. start_timestamp: timestamp2string(getTodayStartTimestamp()),
  484. end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
  485. channel: '',
  486. group: '',
  487. });
  488. const {
  489. username,
  490. token_name,
  491. model_name,
  492. start_timestamp,
  493. end_timestamp,
  494. channel,
  495. group,
  496. } = inputs;
  497. const [stat, setStat] = useState({
  498. quota: 0,
  499. token: 0,
  500. });
  501. const handleInputChange = (value, name) => {
  502. setInputs(inputs => ({ ...inputs, [name]: value }));
  503. };
  504. const getLogSelfStat = async () => {
  505. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  506. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  507. let url = `/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
  508. url = encodeURI(url);
  509. let res = await API.get(url);
  510. const { success, message, data } = res.data;
  511. if (success) {
  512. setStat(data);
  513. } else {
  514. showError(message);
  515. }
  516. };
  517. const getLogStat = async () => {
  518. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  519. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  520. 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}`;
  521. url = encodeURI(url);
  522. let res = await API.get(url);
  523. const { success, message, data } = res.data;
  524. if (success) {
  525. setStat(data);
  526. } else {
  527. showError(message);
  528. }
  529. };
  530. const handleEyeClick = async () => {
  531. if (loadingStat) {
  532. return;
  533. }
  534. setLoadingStat(true);
  535. if (isAdminUser) {
  536. await getLogStat();
  537. } else {
  538. await getLogSelfStat();
  539. }
  540. setShowStat(true);
  541. setLoadingStat(false);
  542. };
  543. const showUserInfo = async (userId) => {
  544. if (!isAdminUser) {
  545. return;
  546. }
  547. const res = await API.get(`/api/user/${userId}`);
  548. const { success, message, data } = res.data;
  549. if (success) {
  550. Modal.info({
  551. title: t('用户信息'),
  552. content: (
  553. <div style={{ padding: 12 }}>
  554. <p>{t('用户名')}: {data.username}</p>
  555. <p>{t('余额')}: {renderQuota(data.quota)}</p>
  556. <p>{t('已用额度')}:{renderQuota(data.used_quota)}</p>
  557. <p>{t('请求次数')}:{renderNumber(data.request_count)}</p>
  558. </div>
  559. ),
  560. centered: true,
  561. });
  562. } else {
  563. showError(message);
  564. }
  565. };
  566. const setLogsFormat = (logs) => {
  567. let expandDatesLocal = {};
  568. for (let i = 0; i < logs.length; i++) {
  569. logs[i].timestamp2string = timestamp2string(logs[i].created_at);
  570. logs[i].key = logs[i].id;
  571. let other = getLogOther(logs[i].other);
  572. let expandDataLocal = [];
  573. if (isAdmin()) {
  574. // let content = '渠道:' + logs[i].channel;
  575. // if (other.admin_info !== undefined) {
  576. // if (
  577. // other.admin_info.use_channel !== null &&
  578. // other.admin_info.use_channel !== undefined &&
  579. // other.admin_info.use_channel !== ''
  580. // ) {
  581. // // channel id array
  582. // let useChannel = other.admin_info.use_channel;
  583. // let useChannelStr = useChannel.join('->');
  584. // content = `渠道:${useChannelStr}`;
  585. // }
  586. // }
  587. // expandDataLocal.push({
  588. // key: '渠道重试',
  589. // value: content,
  590. // })
  591. }
  592. if (isAdminUser && (logs[i].type === 0 || logs[i].type === 2)) {
  593. expandDataLocal.push({
  594. key: t('渠道信息'),
  595. value: `${logs[i].channel} - ${logs[i].channel_name || '[未知]'}`
  596. });
  597. }
  598. if (other?.ws || other?.audio) {
  599. expandDataLocal.push({
  600. key: t('语音输入'),
  601. value: other.audio_input,
  602. });
  603. expandDataLocal.push({
  604. key: t('语音输出'),
  605. value: other.audio_output,
  606. });
  607. expandDataLocal.push({
  608. key: t('文字输入'),
  609. value: other.text_input,
  610. });
  611. expandDataLocal.push({
  612. key: t('文字输出'),
  613. value: other.text_output,
  614. });
  615. }
  616. expandDataLocal.push({
  617. key: t('日志详情'),
  618. value: logs[i].content,
  619. });
  620. if (logs[i].type === 2) {
  621. let modelMapped = other?.is_model_mapped && other?.upstream_model_name && other?.upstream_model_name !== '';
  622. if (modelMapped) {
  623. expandDataLocal.push({
  624. key: t('请求并计费模型'),
  625. value: logs[i].model_name,
  626. });
  627. expandDataLocal.push({
  628. key: t('实际模型'),
  629. value: other.upstream_model_name,
  630. });
  631. }
  632. let content = '';
  633. if (other?.ws || other?.audio) {
  634. content = renderAudioModelPrice(
  635. other.text_input,
  636. other.text_output,
  637. other.model_ratio,
  638. other.model_price,
  639. other.completion_ratio,
  640. other.audio_input,
  641. other.audio_output,
  642. other?.audio_ratio,
  643. other?.audio_completion_ratio,
  644. other.group_ratio,
  645. );
  646. } else {
  647. content = renderModelPrice(
  648. logs[i].prompt_tokens,
  649. logs[i].completion_tokens,
  650. other.model_ratio,
  651. other.model_price,
  652. other.completion_ratio,
  653. other.group_ratio,
  654. );
  655. }
  656. expandDataLocal.push({
  657. key: t('计费过程'),
  658. value: content,
  659. });
  660. if (other?.reasoning_effort) {
  661. expandDataLocal.push({
  662. key: t('Reasoning Effort'),
  663. value: other.reasoning_effort,
  664. });
  665. }
  666. }
  667. expandDatesLocal[logs[i].key] = expandDataLocal;
  668. }
  669. setExpandData(expandDatesLocal);
  670. setLogs(logs);
  671. };
  672. const loadLogs = async (startIdx, pageSize, logType = 0) => {
  673. setLoading(true);
  674. let url = '';
  675. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  676. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  677. if (isAdminUser) {
  678. 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}`;
  679. } else {
  680. 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}`;
  681. }
  682. url = encodeURI(url);
  683. const res = await API.get(url);
  684. const { success, message, data } = res.data;
  685. if (success) {
  686. const newPageData = data.items;
  687. setActivePage(data.page);
  688. setPageSize(data.page_size);
  689. setLogCount(data.total);
  690. setLogsFormat(newPageData);
  691. } else {
  692. showError(message);
  693. }
  694. setLoading(false);
  695. };
  696. const handlePageChange = (page) => {
  697. setActivePage(page);
  698. loadLogs(page, pageSize, logType).then((r) => {});
  699. };
  700. const handlePageSizeChange = async (size) => {
  701. localStorage.setItem('page-size', size + '');
  702. setPageSize(size);
  703. setActivePage(1);
  704. loadLogs(activePage, size)
  705. .then()
  706. .catch((reason) => {
  707. showError(reason);
  708. });
  709. };
  710. const refresh = async () => {
  711. setActivePage(1);
  712. handleEyeClick();
  713. await loadLogs(activePage, pageSize, logType);
  714. };
  715. const copyText = async (e, text) => {
  716. e.stopPropagation();
  717. if (await copy(text)) {
  718. showSuccess('已复制:' + text);
  719. } else {
  720. Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
  721. }
  722. };
  723. useEffect(() => {
  724. const localPageSize =
  725. parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
  726. setPageSize(localPageSize);
  727. loadLogs(activePage, localPageSize)
  728. .then()
  729. .catch((reason) => {
  730. showError(reason);
  731. });
  732. handleEyeClick();
  733. }, []);
  734. const expandRowRender = (record, index) => {
  735. return <Descriptions data={expandData[record.key]} />;
  736. };
  737. return (
  738. <>
  739. <Layout>
  740. <Header>
  741. <Spin spinning={loadingStat}>
  742. <Space>
  743. <Tag color='green' size='large' style={{ padding: 15 }}>
  744. {t('总消耗额度')}: {renderQuota(stat.quota)}
  745. </Tag>
  746. <Tag color='blue' size='large' style={{ padding: 15 }}>
  747. RPM: {stat.rpm}
  748. </Tag>
  749. <Tag color='purple' size='large' style={{ padding: 15 }}>
  750. TPM: {stat.tpm}
  751. </Tag>
  752. </Space>
  753. </Spin>
  754. </Header>
  755. <Form layout='horizontal' style={{ marginTop: 10 }}>
  756. <>
  757. <Form.Section>
  758. <div style={{ marginBottom: 10 }}>
  759. {
  760. styleState.isMobile ? (
  761. <div>
  762. <Form.DatePicker
  763. field='start_timestamp'
  764. label={t('起始时间')}
  765. style={{ width: 272 }}
  766. initValue={start_timestamp}
  767. type='dateTime'
  768. onChange={(value) => {
  769. console.log(value);
  770. handleInputChange(value, 'start_timestamp')
  771. }}
  772. />
  773. <Form.DatePicker
  774. field='end_timestamp'
  775. fluid
  776. label={t('结束时间')}
  777. style={{ width: 272 }}
  778. initValue={end_timestamp}
  779. type='dateTime'
  780. onChange={(value) => handleInputChange(value, 'end_timestamp')}
  781. />
  782. </div>
  783. ) : (
  784. <Form.DatePicker
  785. field="range_timestamp"
  786. label={t('时间范围')}
  787. initValue={[start_timestamp, end_timestamp]}
  788. type="dateTimeRange"
  789. name="range_timestamp"
  790. onChange={(value) => {
  791. if (Array.isArray(value) && value.length === 2) {
  792. handleInputChange(value[0], 'start_timestamp');
  793. handleInputChange(value[1], 'end_timestamp');
  794. }
  795. }}
  796. />
  797. )
  798. }
  799. </div>
  800. </Form.Section>
  801. <Form.Input
  802. field='token_name'
  803. label={t('令牌名称')}
  804. value={token_name}
  805. placeholder={t('可选值')}
  806. name='token_name'
  807. onChange={(value) => handleInputChange(value, 'token_name')}
  808. />
  809. <Form.Input
  810. field='model_name'
  811. label={t('模型名称')}
  812. value={model_name}
  813. placeholder={t('可选值')}
  814. name='model_name'
  815. onChange={(value) => handleInputChange(value, 'model_name')}
  816. />
  817. <Form.Input
  818. field='group'
  819. label={t('分组')}
  820. value={group}
  821. placeholder={t('可选值')}
  822. name='group'
  823. onChange={(value) => handleInputChange(value, 'group')}
  824. />
  825. {isAdminUser && (
  826. <>
  827. <Form.Input
  828. field='channel'
  829. label={t('渠道 ID')}
  830. value={channel}
  831. placeholder={t('可选值')}
  832. name='channel'
  833. onChange={(value) => handleInputChange(value, 'channel')}
  834. />
  835. <Form.Input
  836. field='username'
  837. label={t('用户名称')}
  838. value={username}
  839. placeholder={t('可选值')}
  840. name='username'
  841. onChange={(value) => handleInputChange(value, 'username')}
  842. />
  843. </>
  844. )}
  845. <Button
  846. label={t('查询')}
  847. type='primary'
  848. htmlType='submit'
  849. className='btn-margin-right'
  850. onClick={refresh}
  851. loading={loading}
  852. style={{ marginTop: 24 }}
  853. >
  854. {t('查询')}
  855. </Button>
  856. <Form.Section></Form.Section>
  857. </>
  858. </Form>
  859. <div style={{marginTop:10}}>
  860. <Select
  861. defaultValue='0'
  862. style={{ width: 120 }}
  863. onChange={(value) => {
  864. setLogType(parseInt(value));
  865. loadLogs(0, pageSize, parseInt(value));
  866. }}
  867. >
  868. <Select.Option value='0'>{t('全部')}</Select.Option>
  869. <Select.Option value='1'>{t('充值')}</Select.Option>
  870. <Select.Option value='2'>{t('消费')}</Select.Option>
  871. <Select.Option value='3'>{t('管理')}</Select.Option>
  872. <Select.Option value='4'>{t('系统')}</Select.Option>
  873. </Select>
  874. </div>
  875. <Table
  876. style={{ marginTop: 5 }}
  877. columns={columns}
  878. expandedRowRender={expandRowRender}
  879. expandRowByClick={true}
  880. dataSource={logs}
  881. rowKey="key"
  882. pagination={{
  883. formatPageText: (page) =>
  884. t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
  885. start: page.currentStart,
  886. end: page.currentEnd,
  887. total: logCount
  888. }),
  889. currentPage: activePage,
  890. pageSize: pageSize,
  891. total: logCount,
  892. pageSizeOpts: [10, 20, 50, 100],
  893. showSizeChanger: true,
  894. onPageSizeChange: (size) => {
  895. handlePageSizeChange(size);
  896. },
  897. onPageChange: handlePageChange,
  898. }}
  899. />
  900. </Layout>
  901. </>
  902. );
  903. };
  904. export default LogsTable;