LogsTable.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. import React, { useEffect, useState } from 'react';
  2. import {
  3. API,
  4. copy,
  5. isAdmin,
  6. showError,
  7. showSuccess,
  8. timestamp2string,
  9. } from '../helpers';
  10. import {
  11. Avatar,
  12. Button,
  13. Form,
  14. Layout,
  15. Modal,
  16. Select,
  17. Space,
  18. Spin,
  19. Table,
  20. Tag,
  21. } from '@douyinfe/semi-ui';
  22. import { ITEMS_PER_PAGE } from '../constants';
  23. import { renderNumber, renderQuota, stringToColor } from '../helpers/render';
  24. import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
  25. const { Header } = Layout;
  26. function renderTimestamp(timestamp) {
  27. return <>{timestamp2string(timestamp)}</>;
  28. }
  29. const MODE_OPTIONS = [
  30. { key: 'all', text: '全部用户', value: 'all' },
  31. { key: 'self', text: '当前用户', value: 'self' },
  32. ];
  33. const colors = [
  34. 'amber',
  35. 'blue',
  36. 'cyan',
  37. 'green',
  38. 'grey',
  39. 'indigo',
  40. 'light-blue',
  41. 'lime',
  42. 'orange',
  43. 'pink',
  44. 'purple',
  45. 'red',
  46. 'teal',
  47. 'violet',
  48. 'yellow',
  49. ];
  50. function renderType(type) {
  51. switch (type) {
  52. case 1:
  53. return (
  54. <Tag color='cyan' size='large'>
  55. {' '}
  56. 充值{' '}
  57. </Tag>
  58. );
  59. case 2:
  60. return (
  61. <Tag color='lime' size='large'>
  62. {' '}
  63. 消费{' '}
  64. </Tag>
  65. );
  66. case 3:
  67. return (
  68. <Tag color='orange' size='large'>
  69. {' '}
  70. 管理{' '}
  71. </Tag>
  72. );
  73. case 4:
  74. return (
  75. <Tag color='purple' size='large'>
  76. {' '}
  77. 系统{' '}
  78. </Tag>
  79. );
  80. default:
  81. return (
  82. <Tag color='black' size='large'>
  83. {' '}
  84. 未知{' '}
  85. </Tag>
  86. );
  87. }
  88. }
  89. function renderIsStream(bool) {
  90. if (bool) {
  91. return (
  92. <Tag color='blue' size='large'>
  93. </Tag>
  94. );
  95. } else {
  96. return (
  97. <Tag color='purple' size='large'>
  98. 非流
  99. </Tag>
  100. );
  101. }
  102. }
  103. function renderUseTime(type) {
  104. const time = parseInt(type);
  105. if (time < 101) {
  106. return (
  107. <Tag color='green' size='large'>
  108. {' '}
  109. {time} s{' '}
  110. </Tag>
  111. );
  112. } else if (time < 300) {
  113. return (
  114. <Tag color='orange' size='large'>
  115. {' '}
  116. {time} s{' '}
  117. </Tag>
  118. );
  119. } else {
  120. return (
  121. <Tag color='red' size='large'>
  122. {' '}
  123. {time} s{' '}
  124. </Tag>
  125. );
  126. }
  127. }
  128. const LogsTable = () => {
  129. const columns = [
  130. {
  131. title: '时间',
  132. dataIndex: 'timestamp2string',
  133. },
  134. {
  135. title: '渠道',
  136. dataIndex: 'channel',
  137. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  138. render: (text, record, index) => {
  139. return isAdminUser ? (
  140. record.type === 0 || record.type === 2 ? (
  141. <div>
  142. {
  143. <Tag
  144. color={colors[parseInt(text) % colors.length]}
  145. size='large'
  146. >
  147. {' '}
  148. {text}{' '}
  149. </Tag>
  150. }
  151. </div>
  152. ) : (
  153. <></>
  154. )
  155. ) : (
  156. <></>
  157. );
  158. },
  159. },
  160. {
  161. title: '用户',
  162. dataIndex: 'username',
  163. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  164. render: (text, record, index) => {
  165. return isAdminUser ? (
  166. <div>
  167. <Avatar
  168. size='small'
  169. color={stringToColor(text)}
  170. style={{ marginRight: 4 }}
  171. onClick={() => showUserInfo(record.user_id)}
  172. >
  173. {typeof text === 'string' && text.slice(0, 1)}
  174. </Avatar>
  175. {text}
  176. </div>
  177. ) : (
  178. <></>
  179. );
  180. },
  181. },
  182. {
  183. title: '令牌',
  184. dataIndex: 'token_name',
  185. render: (text, record, index) => {
  186. return record.type === 0 || record.type === 2 ? (
  187. <div>
  188. <Tag
  189. color='grey'
  190. size='large'
  191. onClick={() => {
  192. copyText(text);
  193. }}
  194. >
  195. {' '}
  196. {text}{' '}
  197. </Tag>
  198. </div>
  199. ) : (
  200. <></>
  201. );
  202. },
  203. },
  204. {
  205. title: '类型',
  206. dataIndex: 'type',
  207. render: (text, record, index) => {
  208. return <div>{renderType(text)}</div>;
  209. },
  210. },
  211. {
  212. title: '模型',
  213. dataIndex: 'model_name',
  214. render: (text, record, index) => {
  215. return record.type === 0 || record.type === 2 ? (
  216. <div>
  217. <Tag
  218. color={stringToColor(text)}
  219. size='large'
  220. onClick={() => {
  221. copyText(text);
  222. }}
  223. >
  224. {' '}
  225. {text}{' '}
  226. </Tag>
  227. </div>
  228. ) : (
  229. <></>
  230. );
  231. },
  232. },
  233. {
  234. title: '用时',
  235. dataIndex: 'use_time',
  236. render: (text, record, index) => {
  237. return (
  238. <div>
  239. <Space>
  240. {renderUseTime(text)}
  241. {renderIsStream(record.is_stream)}
  242. </Space>
  243. </div>
  244. );
  245. },
  246. },
  247. {
  248. title: '提示',
  249. dataIndex: 'prompt_tokens',
  250. render: (text, record, index) => {
  251. return record.type === 0 || record.type === 2 ? (
  252. <div>{<span> {text} </span>}</div>
  253. ) : (
  254. <></>
  255. );
  256. },
  257. },
  258. {
  259. title: '补全',
  260. dataIndex: 'completion_tokens',
  261. render: (text, record, index) => {
  262. return parseInt(text) > 0 &&
  263. (record.type === 0 || record.type === 2) ? (
  264. <div>{<span> {text} </span>}</div>
  265. ) : (
  266. <></>
  267. );
  268. },
  269. },
  270. {
  271. title: '花费',
  272. dataIndex: 'quota',
  273. render: (text, record, index) => {
  274. return record.type === 0 || record.type === 2 ? (
  275. <div>{renderQuota(text, 6)}</div>
  276. ) : (
  277. <></>
  278. );
  279. },
  280. },
  281. {
  282. title: '详情',
  283. dataIndex: 'content',
  284. render: (text, record, index) => {
  285. return (
  286. <Paragraph
  287. ellipsis={{
  288. rows: 2,
  289. showTooltip: { type: 'popover', opts: { style: { width: 240 } } },
  290. }}
  291. style={{ maxWidth: 240 }}
  292. >
  293. {text}
  294. </Paragraph>
  295. );
  296. },
  297. },
  298. ];
  299. const [logs, setLogs] = useState([]);
  300. const [showStat, setShowStat] = useState(false);
  301. const [loading, setLoading] = useState(false);
  302. const [loadingStat, setLoadingStat] = useState(false);
  303. const [activePage, setActivePage] = useState(1);
  304. const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
  305. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  306. const [searchKeyword, setSearchKeyword] = useState('');
  307. const [searching, setSearching] = useState(false);
  308. const [logType, setLogType] = useState(0);
  309. const isAdminUser = isAdmin();
  310. let now = new Date();
  311. // 初始化start_timestamp为前一天
  312. const [inputs, setInputs] = useState({
  313. username: '',
  314. token_name: '',
  315. model_name: '',
  316. start_timestamp: timestamp2string(now.getTime() / 1000 - 86400),
  317. end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
  318. channel: '',
  319. });
  320. const {
  321. username,
  322. token_name,
  323. model_name,
  324. start_timestamp,
  325. end_timestamp,
  326. channel,
  327. } = inputs;
  328. const [stat, setStat] = useState({
  329. quota: 0,
  330. token: 0,
  331. });
  332. const handleInputChange = (value, name) => {
  333. setInputs((inputs) => ({ ...inputs, [name]: value }));
  334. };
  335. const getLogSelfStat = async () => {
  336. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  337. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  338. let res = await API.get(
  339. `/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`,
  340. );
  341. const { success, message, data } = res.data;
  342. if (success) {
  343. setStat(data);
  344. } else {
  345. showError(message);
  346. }
  347. };
  348. const getLogStat = async () => {
  349. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  350. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  351. let res = await API.get(
  352. `/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`,
  353. );
  354. const { success, message, data } = res.data;
  355. if (success) {
  356. setStat(data);
  357. } else {
  358. showError(message);
  359. }
  360. };
  361. const handleEyeClick = async () => {
  362. setLoadingStat(true);
  363. if (isAdminUser) {
  364. await getLogStat();
  365. } else {
  366. await getLogSelfStat();
  367. }
  368. setShowStat(true);
  369. setLoadingStat(false);
  370. };
  371. const showUserInfo = async (userId) => {
  372. if (!isAdminUser) {
  373. return;
  374. }
  375. const res = await API.get(`/api/user/${userId}`);
  376. const { success, message, data } = res.data;
  377. if (success) {
  378. Modal.info({
  379. title: '用户信息',
  380. content: (
  381. <div style={{ padding: 12 }}>
  382. <p>用户名: {data.username}</p>
  383. <p>余额: {renderQuota(data.quota)}</p>
  384. <p>已用额度:{renderQuota(data.used_quota)}</p>
  385. <p>请求次数:{renderNumber(data.request_count)}</p>
  386. </div>
  387. ),
  388. centered: true,
  389. });
  390. } else {
  391. showError(message);
  392. }
  393. };
  394. const setLogsFormat = (logs) => {
  395. for (let i = 0; i < logs.length; i++) {
  396. logs[i].timestamp2string = timestamp2string(logs[i].created_at);
  397. logs[i].key = '' + logs[i].id;
  398. }
  399. // data.key = '' + data.id
  400. setLogs(logs);
  401. setLogCount(logs.length + ITEMS_PER_PAGE);
  402. // console.log(logCount);
  403. };
  404. const loadLogs = async (startIdx, pageSize, logType = 0) => {
  405. setLoading(true);
  406. let url = '';
  407. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  408. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  409. if (isAdminUser) {
  410. 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}`;
  411. } else {
  412. 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}`;
  413. }
  414. const res = await API.get(url);
  415. const { success, message, data } = res.data;
  416. if (success) {
  417. if (startIdx === 0) {
  418. setLogsFormat(data);
  419. } else {
  420. let newLogs = [...logs];
  421. newLogs.splice(startIdx * pageSize, data.length, ...data);
  422. setLogsFormat(newLogs);
  423. }
  424. } else {
  425. showError(message);
  426. }
  427. setLoading(false);
  428. };
  429. const pageData = logs.slice(
  430. (activePage - 1) * pageSize,
  431. activePage * pageSize,
  432. );
  433. const handlePageChange = (page) => {
  434. setActivePage(page);
  435. if (page === Math.ceil(logs.length / pageSize) + 1) {
  436. // In this case we have to load more data and then append them.
  437. loadLogs(page - 1, pageSize, logType).then((r) => {});
  438. }
  439. };
  440. const handlePageSizeChange = async (size) => {
  441. localStorage.setItem('page-size', size + '');
  442. setPageSize(size);
  443. setActivePage(1);
  444. loadLogs(0, size)
  445. .then()
  446. .catch((reason) => {
  447. showError(reason);
  448. });
  449. };
  450. const refresh = async (localLogType) => {
  451. // setLoading(true);
  452. setActivePage(1);
  453. await loadLogs(0, pageSize, localLogType);
  454. };
  455. const copyText = async (text) => {
  456. if (await copy(text)) {
  457. showSuccess('已复制:' + text);
  458. } else {
  459. // setSearchKeyword(text);
  460. Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
  461. }
  462. };
  463. useEffect(() => {
  464. // console.log('default effect')
  465. const localPageSize =
  466. parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
  467. setPageSize(localPageSize);
  468. loadLogs(0, localPageSize)
  469. .then()
  470. .catch((reason) => {
  471. showError(reason);
  472. });
  473. }, []);
  474. const searchLogs = async () => {
  475. if (searchKeyword === '') {
  476. // if keyword is blank, load files instead.
  477. await loadLogs(0, pageSize);
  478. setActivePage(1);
  479. return;
  480. }
  481. setSearching(true);
  482. const res = await API.get(`/api/log/self/search?keyword=${searchKeyword}`);
  483. const { success, message, data } = res.data;
  484. if (success) {
  485. setLogs(data);
  486. setActivePage(1);
  487. } else {
  488. showError(message);
  489. }
  490. setSearching(false);
  491. };
  492. return (
  493. <>
  494. <Layout>
  495. <Header>
  496. <Spin spinning={loadingStat}>
  497. <h3>
  498. 使用明细(总消耗额度:
  499. <span
  500. onClick={handleEyeClick}
  501. style={{
  502. cursor: 'pointer',
  503. color: 'gray',
  504. }}
  505. >
  506. {showStat ? renderQuota(stat.quota) : '点击查看'}
  507. </span>
  508. </h3>
  509. </Spin>
  510. </Header>
  511. <Form layout='horizontal' style={{ marginTop: 10 }}>
  512. <>
  513. <Form.Input
  514. field='token_name'
  515. label='令牌名称'
  516. style={{ width: 176 }}
  517. value={token_name}
  518. placeholder={'可选值'}
  519. name='token_name'
  520. onChange={(value) => handleInputChange(value, 'token_name')}
  521. />
  522. <Form.Input
  523. field='model_name'
  524. label='模型名称'
  525. style={{ width: 176 }}
  526. value={model_name}
  527. placeholder='可选值'
  528. name='model_name'
  529. onChange={(value) => handleInputChange(value, 'model_name')}
  530. />
  531. <Form.DatePicker
  532. field='start_timestamp'
  533. label='起始时间'
  534. style={{ width: 272 }}
  535. initValue={start_timestamp}
  536. value={start_timestamp}
  537. type='dateTime'
  538. name='start_timestamp'
  539. onChange={(value) => handleInputChange(value, 'start_timestamp')}
  540. />
  541. <Form.DatePicker
  542. field='end_timestamp'
  543. fluid
  544. label='结束时间'
  545. style={{ width: 272 }}
  546. initValue={end_timestamp}
  547. value={end_timestamp}
  548. type='dateTime'
  549. name='end_timestamp'
  550. onChange={(value) => handleInputChange(value, 'end_timestamp')}
  551. />
  552. {isAdminUser && (
  553. <>
  554. <Form.Input
  555. field='channel'
  556. label='渠道 ID'
  557. style={{ width: 176 }}
  558. value={channel}
  559. placeholder='可选值'
  560. name='channel'
  561. onChange={(value) => handleInputChange(value, 'channel')}
  562. />
  563. <Form.Input
  564. field='username'
  565. label='用户名称'
  566. style={{ width: 176 }}
  567. value={username}
  568. placeholder={'可选值'}
  569. name='username'
  570. onChange={(value) => handleInputChange(value, 'username')}
  571. />
  572. </>
  573. )}
  574. <Form.Section>
  575. <Button
  576. label='查询'
  577. type='primary'
  578. htmlType='submit'
  579. className='btn-margin-right'
  580. onClick={refresh}
  581. loading={loading}
  582. >
  583. 查询
  584. </Button>
  585. </Form.Section>
  586. </>
  587. </Form>
  588. <Table
  589. style={{ marginTop: 5 }}
  590. columns={columns}
  591. dataSource={pageData}
  592. pagination={{
  593. currentPage: activePage,
  594. pageSize: pageSize,
  595. total: logCount,
  596. pageSizeOpts: [10, 20, 50, 100],
  597. showSizeChanger: true,
  598. onPageSizeChange: (size) => {
  599. handlePageSizeChange(size).then();
  600. },
  601. onPageChange: handlePageChange,
  602. }}
  603. />
  604. <Select
  605. defaultValue='0'
  606. style={{ width: 120 }}
  607. onChange={(value) => {
  608. setLogType(parseInt(value));
  609. refresh(parseInt(value)).then();
  610. }}
  611. >
  612. <Select.Option value='0'>全部</Select.Option>
  613. <Select.Option value='1'>充值</Select.Option>
  614. <Select.Option value='2'>消费</Select.Option>
  615. <Select.Option value='3'>管理</Select.Option>
  616. <Select.Option value='4'>系统</Select.Option>
  617. </Select>
  618. </Layout>
  619. </>
  620. );
  621. };
  622. export default LogsTable;