LogsTable.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. import React, {useEffect, useState} from 'react';
  2. import {Label} from 'semantic-ui-react';
  3. import {API, copy, isAdmin, showError, showSuccess, timestamp2string} from '../helpers';
  4. import {Table, Avatar, Tag, Form, Button, Layout, Select, Popover, Modal, Spin} from '@douyinfe/semi-ui';
  5. import {ITEMS_PER_PAGE} from '../constants';
  6. import {renderNumber, renderQuota, stringToColor} from '../helpers/render';
  7. import {
  8. IconAt,
  9. IconHistogram,
  10. IconGift,
  11. IconKey,
  12. IconUser,
  13. IconLayers,
  14. IconSetting,
  15. IconCreditCard,
  16. IconSemiLogo,
  17. IconHome,
  18. IconMore
  19. } from '@douyinfe/semi-icons';
  20. const {Sider, Content, Header} = Layout;
  21. const {Column} = Table;
  22. function renderTimestamp(timestamp) {
  23. return (
  24. <>
  25. {timestamp2string(timestamp)}
  26. </>
  27. );
  28. }
  29. const MODE_OPTIONS = [
  30. {key: 'all', text: '全部用户', value: 'all'},
  31. {key: 'self', text: '当前用户', value: 'self'}
  32. ];
  33. const colors = ['amber', 'blue', 'cyan', 'green', 'grey', 'indigo',
  34. 'light-blue', 'lime', 'orange', 'pink',
  35. 'purple', 'red', 'teal', 'violet', 'yellow'
  36. ]
  37. function renderType(type) {
  38. switch (type) {
  39. case 1:
  40. return <Tag color='cyan' size='large'> 充值 </Tag>;
  41. case 2:
  42. return <Tag color='lime' size='large'> 消费 </Tag>;
  43. case 3:
  44. return <Tag color='orange' size='large'> 管理 </Tag>;
  45. case 4:
  46. return <Tag color='purple' size='large'> 系统 </Tag>;
  47. default:
  48. return <Tag color='black' size='large'> 未知 </Tag>;
  49. }
  50. }
  51. const LogsTable = () => {
  52. const columns = [
  53. {
  54. title: '时间',
  55. dataIndex: 'timestamp2string',
  56. },
  57. {
  58. title: '渠道',
  59. dataIndex: 'channel',
  60. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  61. render: (text, record, index) => {
  62. return (
  63. isAdminUser ?
  64. record.type === 0 || record.type === 2 ?
  65. <div>
  66. {<Tag color={colors[parseInt(text) % colors.length]} size='large'> {text} </Tag>}
  67. </div>
  68. :
  69. <></>
  70. :
  71. <></>
  72. );
  73. },
  74. },
  75. {
  76. title: '用户',
  77. dataIndex: 'username',
  78. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  79. render: (text, record, index) => {
  80. return (
  81. isAdminUser ?
  82. <div>
  83. <Avatar size="small" color={stringToColor(text)} style={{marginRight: 4}}
  84. onClick={() => showUserInfo(record.user_id)}>
  85. {typeof text === 'string' && text.slice(0, 1)}
  86. </Avatar>
  87. {text}
  88. </div>
  89. :
  90. <></>
  91. );
  92. },
  93. },
  94. {
  95. title: '令牌',
  96. dataIndex: 'token_name',
  97. render: (text, record, index) => {
  98. return (
  99. record.type === 0 || record.type === 2 ?
  100. <div>
  101. <Tag color='grey' size='large' onClick={() => {
  102. copyText(text)
  103. }}> {text} </Tag>
  104. </div>
  105. :
  106. <></>
  107. );
  108. },
  109. },
  110. {
  111. title: '类型',
  112. dataIndex: 'type',
  113. render: (text, record, index) => {
  114. return (
  115. <div>
  116. {renderType(text)}
  117. </div>
  118. );
  119. },
  120. },
  121. {
  122. title: '模型',
  123. dataIndex: 'model_name',
  124. render: (text, record, index) => {
  125. return (
  126. record.type === 0 || record.type === 2 ?
  127. <div>
  128. <Tag color={stringToColor(text)} size='large' onClick={() => {
  129. copyText(text)
  130. }}> {text} </Tag>
  131. </div>
  132. :
  133. <></>
  134. );
  135. },
  136. },
  137. {
  138. title: '提示',
  139. dataIndex: 'prompt_tokens',
  140. render: (text, record, index) => {
  141. return (
  142. record.type === 0 || record.type === 2 ?
  143. <div>
  144. {<span> {text} </span>}
  145. </div>
  146. :
  147. <></>
  148. );
  149. },
  150. },
  151. {
  152. title: '补全',
  153. dataIndex: 'completion_tokens',
  154. render: (text, record, index) => {
  155. return (
  156. parseInt(text) > 0 && (record.type === 0 || record.type === 2) ?
  157. <div>
  158. {<span> {text} </span>}
  159. </div>
  160. :
  161. <></>
  162. );
  163. },
  164. },
  165. {
  166. title: '花费',
  167. dataIndex: 'quota',
  168. render: (text, record, index) => {
  169. return (
  170. record.type === 0 || record.type === 2 ?
  171. <div>
  172. {
  173. renderQuota(text, 6)
  174. }
  175. </div>
  176. :
  177. <></>
  178. );
  179. }
  180. },
  181. {
  182. title: '详情',
  183. dataIndex: 'content',
  184. }
  185. ];
  186. const [logs, setLogs] = useState([]);
  187. const [showStat, setShowStat] = useState(false);
  188. const [loading, setLoading] = useState(false);
  189. const [loadingStat, setLoadingStat] = useState(false);
  190. const [activePage, setActivePage] = useState(1);
  191. const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
  192. const [searchKeyword, setSearchKeyword] = useState('');
  193. const [searching, setSearching] = useState(false);
  194. const [logType, setLogType] = useState(0);
  195. const isAdminUser = isAdmin();
  196. let now = new Date();
  197. // 初始化start_timestamp为前一天
  198. const [inputs, setInputs] = useState({
  199. username: '',
  200. token_name: '',
  201. model_name: '',
  202. start_timestamp: timestamp2string(now.getTime() / 1000 - 86400),
  203. end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
  204. channel: ''
  205. });
  206. const {username, token_name, model_name, start_timestamp, end_timestamp, channel} = inputs;
  207. const [stat, setStat] = useState({
  208. quota: 0,
  209. token: 0
  210. });
  211. const handleInputChange = (value, name) => {
  212. setInputs((inputs) => ({...inputs, [name]: value}));
  213. };
  214. const getLogSelfStat = async () => {
  215. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  216. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  217. let res = await API.get(`/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`);
  218. const {success, message, data} = res.data;
  219. if (success) {
  220. setStat(data);
  221. } else {
  222. showError(message);
  223. }
  224. };
  225. const getLogStat = async () => {
  226. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  227. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  228. let res = await API.get(`/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`);
  229. const {success, message, data} = res.data;
  230. if (success) {
  231. setStat(data);
  232. } else {
  233. showError(message);
  234. }
  235. };
  236. const handleEyeClick = async () => {
  237. setLoadingStat(true);
  238. if (isAdminUser) {
  239. await getLogStat();
  240. } else {
  241. await getLogSelfStat();
  242. }
  243. setShowStat(true);
  244. setLoadingStat(false);
  245. };
  246. const showUserInfo = async (userId) => {
  247. if (!isAdminUser) {
  248. return;
  249. }
  250. const res = await API.get(`/api/user/${userId}`);
  251. const {success, message, data} = res.data;
  252. if (success) {
  253. Modal.info({
  254. title: '用户信息',
  255. content: <div style={{padding: 12}}>
  256. <p>用户名: {data.username}</p>
  257. <p>余额: {renderQuota(data.quota)}</p>
  258. <p>已用额度:{renderQuota(data.used_quota)}</p>
  259. <p>请求次数:{renderNumber(data.request_count)}</p>
  260. </div>,
  261. centered: true,
  262. })
  263. } else {
  264. showError(message);
  265. }
  266. };
  267. const setLogsFormat = (logs) => {
  268. for (let i = 0; i < logs.length; i++) {
  269. logs[i].timestamp2string = timestamp2string(logs[i].created_at);
  270. logs[i].key = '' + logs[i].id;
  271. }
  272. // data.key = '' + data.id
  273. setLogs(logs);
  274. setLogCount(logs.length + ITEMS_PER_PAGE);
  275. // console.log(logCount);
  276. }
  277. const loadLogs = async (startIdx) => {
  278. setLoading(true);
  279. let url = '';
  280. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  281. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  282. if (isAdminUser) {
  283. url = `/api/log/?p=${startIdx}&type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`;
  284. } else {
  285. url = `/api/log/self/?p=${startIdx}&type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
  286. }
  287. const res = await API.get(url);
  288. const {success, message, data} = res.data;
  289. if (success) {
  290. if (startIdx === 0) {
  291. setLogsFormat(data);
  292. } else {
  293. let newLogs = [...logs];
  294. newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
  295. setLogsFormat(newLogs);
  296. }
  297. } else {
  298. showError(message);
  299. }
  300. setLoading(false);
  301. };
  302. const pageData = logs.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE);
  303. const handlePageChange = page => {
  304. setActivePage(page);
  305. if (page === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) {
  306. // In this case we have to load more data and then append them.
  307. loadLogs(page - 1).then(r => {
  308. });
  309. }
  310. };
  311. const refresh = async () => {
  312. // setLoading(true);
  313. setActivePage(1);
  314. await loadLogs(0);
  315. };
  316. const copyText = async (text) => {
  317. if (await copy(text)) {
  318. showSuccess('已复制:' + text);
  319. } else {
  320. // setSearchKeyword(text);
  321. Modal.error({title: '无法复制到剪贴板,请手动复制', content: text});
  322. }
  323. }
  324. useEffect(() => {
  325. refresh().then();
  326. }, [logType]);
  327. const searchLogs = async () => {
  328. if (searchKeyword === '') {
  329. // if keyword is blank, load files instead.
  330. await loadLogs(0);
  331. setActivePage(1);
  332. return;
  333. }
  334. setSearching(true);
  335. const res = await API.get(`/api/log/self/search?keyword=${searchKeyword}`);
  336. const {success, message, data} = res.data;
  337. if (success) {
  338. setLogs(data);
  339. setActivePage(1);
  340. } else {
  341. showError(message);
  342. }
  343. setSearching(false);
  344. };
  345. const handleKeywordChange = async (e, {value}) => {
  346. setSearchKeyword(value.trim());
  347. };
  348. const sortLog = (key) => {
  349. if (logs.length === 0) return;
  350. setLoading(true);
  351. let sortedLogs = [...logs];
  352. if (typeof sortedLogs[0][key] === 'string') {
  353. sortedLogs.sort((a, b) => {
  354. return ('' + a[key]).localeCompare(b[key]);
  355. });
  356. } else {
  357. sortedLogs.sort((a, b) => {
  358. if (a[key] === b[key]) return 0;
  359. if (a[key] > b[key]) return -1;
  360. if (a[key] < b[key]) return 1;
  361. });
  362. }
  363. if (sortedLogs[0].id === logs[0].id) {
  364. sortedLogs.reverse();
  365. }
  366. setLogs(sortedLogs);
  367. setLoading(false);
  368. };
  369. return (
  370. <>
  371. <Layout>
  372. <Header>
  373. <Spin spinning={loadingStat}>
  374. <h3>使用明细(总消耗额度:
  375. <span onClick={handleEyeClick} style={{cursor: 'pointer', color: 'gray'}}>{showStat?renderQuota(stat.quota):"点击查看"}</span>
  376. </h3>
  377. </Spin>
  378. </Header>
  379. <Form layout='horizontal' style={{marginTop: 10}}>
  380. <>
  381. <Form.Input field="token_name" label='令牌名称' style={{width: 176}} value={token_name}
  382. placeholder={'可选值'} name='token_name'
  383. onChange={value => handleInputChange(value, 'token_name')}/>
  384. <Form.Input field="model_name" label='模型名称' style={{width: 176}} value={model_name}
  385. placeholder='可选值'
  386. name='model_name'
  387. onChange={value => handleInputChange(value, 'model_name')}/>
  388. <Form.DatePicker field="start_timestamp" label='起始时间' style={{width: 272}}
  389. initValue={start_timestamp}
  390. value={start_timestamp} type='dateTime'
  391. name='start_timestamp'
  392. onChange={value => handleInputChange(value, 'start_timestamp')}/>
  393. <Form.DatePicker field="end_timestamp" fluid label='结束时间' style={{width: 272}}
  394. initValue={end_timestamp}
  395. value={end_timestamp} type='dateTime'
  396. name='end_timestamp'
  397. onChange={value => handleInputChange(value, 'end_timestamp')}/>
  398. {
  399. isAdminUser && <>
  400. <Form.Input field="channel" label='渠道 ID' style={{width: 176}} value={channel}
  401. placeholder='可选值' name='channel'
  402. onChange={value => handleInputChange(value, 'channel')}/>
  403. <Form.Input field="username" label='用户名称' style={{width: 176}} value={username}
  404. placeholder={'可选值'} name='username'
  405. onChange={value => handleInputChange(value, 'username')}/>
  406. </>
  407. }
  408. <Form.Section>
  409. <Button label='查询' type="primary" htmlType="submit" className="btn-margin-right"
  410. onClick={refresh} loading={loading}>查询</Button>
  411. </Form.Section>
  412. </>
  413. </Form>
  414. <Table style={{marginTop: 5}} columns={columns} dataSource={pageData} pagination={{
  415. currentPage: activePage,
  416. pageSize: ITEMS_PER_PAGE,
  417. total: logCount,
  418. pageSizeOpts: [10, 20, 50, 100],
  419. onPageChange: handlePageChange,
  420. }}/>
  421. <Select defaultValue="0" style={{width: 120}} onChange={
  422. (value) => {
  423. setLogType(parseInt(value));
  424. }
  425. }>
  426. <Select.Option value="0">全部</Select.Option>
  427. <Select.Option value="1">充值</Select.Option>
  428. <Select.Option value="2">消费</Select.Option>
  429. <Select.Option value="3">管理</Select.Option>
  430. <Select.Option value="4">系统</Select.Option>
  431. </Select>
  432. </Layout>
  433. </>
  434. );
  435. };
  436. export default LogsTable;