LogsTable.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. import React, {useEffect, useState} from 'react';
  2. import {Label} from 'semantic-ui-react';
  3. import {API, isAdmin, showError, timestamp2string} from '../helpers';
  4. import {Table, Avatar, Tag, Form, Button, Layout, Select} from '@douyinfe/semi-ui';
  5. import {ITEMS_PER_PAGE} from '../constants';
  6. import {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. {typeof text === 'string' && text.slice(0, 1)}
  85. </Avatar>
  86. {text}
  87. </div>
  88. :
  89. <></>
  90. );
  91. },
  92. },
  93. {
  94. title: '令牌',
  95. dataIndex: 'token_name',
  96. render: (text, record, index) => {
  97. return (
  98. record.type === 0 || record.type === 2 ?
  99. <div>
  100. {<Tag color='grey' size='large'> {text} </Tag>}
  101. </div>
  102. :
  103. <></>
  104. );
  105. },
  106. },
  107. {
  108. title: '类型',
  109. dataIndex: 'type',
  110. render: (text, record, index) => {
  111. return (
  112. <div>
  113. {renderType(text)}
  114. </div>
  115. );
  116. },
  117. },
  118. {
  119. title: '模型',
  120. dataIndex: 'model_name',
  121. render: (text, record, index) => {
  122. return (
  123. record.type === 0 || record.type === 2 ?
  124. <div>
  125. {<Tag color={stringToColor(text)} size='large'> {text} </Tag>}
  126. </div>
  127. :
  128. <></>
  129. );
  130. },
  131. },
  132. {
  133. title: '提示',
  134. dataIndex: 'prompt_tokens',
  135. render: (text, record, index) => {
  136. return (
  137. record.type === 0 || record.type === 2 ?
  138. <div>
  139. {<span> {text} </span>}
  140. </div>
  141. :
  142. <></>
  143. );
  144. },
  145. },
  146. {
  147. title: '补全',
  148. dataIndex: 'completion_tokens',
  149. render: (text, record, index) => {
  150. return (
  151. parseInt(text) > 0 && (record.type === 0 || record.type === 2) ?
  152. <div>
  153. {<span> {text} </span>}
  154. </div>
  155. :
  156. <></>
  157. );
  158. },
  159. },
  160. {
  161. title: '花费',
  162. dataIndex: 'quota',
  163. render: (text, record, index) => {
  164. return (
  165. record.type === 0 || record.type === 2 ?
  166. <div>
  167. {
  168. renderQuota(text, 6)
  169. }
  170. </div>
  171. :
  172. <></>
  173. );
  174. }
  175. },
  176. {
  177. title: '详情',
  178. dataIndex: 'content',
  179. }
  180. ];
  181. const [logs, setLogs] = useState([]);
  182. const [showStat, setShowStat] = useState(false);
  183. const [loading, setLoading] = useState(true);
  184. const [activePage, setActivePage] = useState(1);
  185. const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
  186. const [searchKeyword, setSearchKeyword] = useState('');
  187. const [searching, setSearching] = useState(false);
  188. const [logType, setLogType] = useState(0);
  189. const isAdminUser = isAdmin();
  190. let now = new Date();
  191. const [inputs, setInputs] = useState({
  192. username: '',
  193. token_name: '',
  194. model_name: '',
  195. start_timestamp: timestamp2string(0),
  196. end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
  197. channel: ''
  198. });
  199. const {username, token_name, model_name, start_timestamp, end_timestamp, channel} = inputs;
  200. const [stat, setStat] = useState({
  201. quota: 0,
  202. token: 0
  203. });
  204. const handleInputChange = (value, name) => {
  205. setInputs((inputs) => ({...inputs, [name]: value}));
  206. };
  207. const getLogSelfStat = async () => {
  208. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  209. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  210. 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}`);
  211. const {success, message, data} = res.data;
  212. if (success) {
  213. setStat(data);
  214. } else {
  215. showError(message);
  216. }
  217. };
  218. const getLogStat = async () => {
  219. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  220. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  221. 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}`);
  222. const {success, message, data} = res.data;
  223. if (success) {
  224. setStat(data);
  225. } else {
  226. showError(message);
  227. }
  228. };
  229. const handleEyeClick = async () => {
  230. if (!showStat) {
  231. if (isAdminUser) {
  232. await getLogStat();
  233. } else {
  234. await getLogSelfStat();
  235. }
  236. }
  237. setShowStat(!showStat);
  238. };
  239. const setLogsFormat = (logs) => {
  240. for (let i = 0; i < logs.length; i++) {
  241. logs[i].timestamp2string = timestamp2string(logs[i].created_at);
  242. logs[i].key = '' + logs[i].id;
  243. }
  244. // data.key = '' + data.id
  245. setLogs(logs);
  246. setLogCount(logs.length + ITEMS_PER_PAGE);
  247. console.log(logCount);
  248. }
  249. const loadLogs = async (startIdx) => {
  250. setLoading(true);
  251. let url = '';
  252. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  253. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  254. if (isAdminUser) {
  255. 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}`;
  256. } else {
  257. url = `/api/log/self/?p=${startIdx}&type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
  258. }
  259. const res = await API.get(url);
  260. const {success, message, data} = res.data;
  261. if (success) {
  262. if (startIdx === 0) {
  263. setLogsFormat(data);
  264. } else {
  265. let newLogs = [...logs];
  266. newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
  267. setLogsFormat(newLogs);
  268. }
  269. } else {
  270. showError(message);
  271. }
  272. setLoading(false);
  273. };
  274. const pageData = logs.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE);
  275. const handlePageChange = page => {
  276. setActivePage(page);
  277. if (page === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) {
  278. // In this case we have to load more data and then append them.
  279. loadLogs(page - 1).then(r => {
  280. });
  281. }
  282. };
  283. const refresh = async () => {
  284. // setLoading(true);
  285. setActivePage(1);
  286. await loadLogs(0);
  287. };
  288. useEffect(() => {
  289. refresh().then();
  290. }, [logType]);
  291. const searchLogs = async () => {
  292. if (searchKeyword === '') {
  293. // if keyword is blank, load files instead.
  294. await loadLogs(0);
  295. setActivePage(1);
  296. return;
  297. }
  298. setSearching(true);
  299. const res = await API.get(`/api/log/self/search?keyword=${searchKeyword}`);
  300. const {success, message, data} = res.data;
  301. if (success) {
  302. setLogs(data);
  303. setActivePage(1);
  304. } else {
  305. showError(message);
  306. }
  307. setSearching(false);
  308. };
  309. const handleKeywordChange = async (e, {value}) => {
  310. setSearchKeyword(value.trim());
  311. };
  312. const sortLog = (key) => {
  313. if (logs.length === 0) return;
  314. setLoading(true);
  315. let sortedLogs = [...logs];
  316. if (typeof sortedLogs[0][key] === 'string') {
  317. sortedLogs.sort((a, b) => {
  318. return ('' + a[key]).localeCompare(b[key]);
  319. });
  320. } else {
  321. sortedLogs.sort((a, b) => {
  322. if (a[key] === b[key]) return 0;
  323. if (a[key] > b[key]) return -1;
  324. if (a[key] < b[key]) return 1;
  325. });
  326. }
  327. if (sortedLogs[0].id === logs[0].id) {
  328. sortedLogs.reverse();
  329. }
  330. setLogs(sortedLogs);
  331. setLoading(false);
  332. };
  333. return (
  334. <>
  335. <Layout>
  336. <Header>
  337. <h3>使用明细(总消耗额度:
  338. {showStat && renderQuota(stat.quota)}
  339. {!showStat &&
  340. <span onClick={handleEyeClick} style={{cursor: 'pointer', color: 'gray'}}>点击查看</span>}
  341. </h3>
  342. </Header>
  343. <Form layout='horizontal' style={{marginTop: 10}}>
  344. <>
  345. <Form.Input field="token_name" label='令牌名称' style={{width: 176}} value={token_name}
  346. placeholder={'可选值'} name='token_name'
  347. onChange={value => handleInputChange(value, 'token_name')}/>
  348. <Form.Input field="model_name" label='模型名称' style={{width: 176}} value={model_name}
  349. placeholder='可选值'
  350. name='model_name'
  351. onChange={value => handlePageChange(value, 'model_name')}/>
  352. <Form.DatePicker field="start_timestamp" label='起始时间' style={{width: 272}}
  353. value={start_timestamp} type='dateTime'
  354. name='start_timestamp'
  355. onChange={value => handleInputChange(value, 'start_timestamp')}/>
  356. <Form.DatePicker field="end_timestamp" fluid label='结束时间' style={{width: 272}}
  357. value={end_timestamp} type='dateTime'
  358. name='end_timestamp'
  359. onChange={value => handleInputChange(value, 'end_timestamp')}/>
  360. {/*<Form.Button fluid label='操作' width={2} onClick={refresh}>查询</Form.Button>*/}
  361. {
  362. isAdminUser && <>
  363. <Form.Input field="channel" label='渠道 ID' style={{width: 176}} value={channel}
  364. placeholder='可选值' name='channel'
  365. onChange={value => handleInputChange(value, 'channel')}/>
  366. <Form.Input field="username" label='用户名称' style={{width: 176}} value={username}
  367. placeholder={'可选值'} name='username'
  368. onChange={value => handleInputChange(value, 'username')}/>
  369. </>
  370. }
  371. <Form.Section>
  372. <Button label='查询' type="primary" htmlType="submit" className="btn-margin-right"
  373. onClick={refresh}>查询</Button>
  374. </Form.Section>
  375. </>
  376. </Form>
  377. <Table columns={columns} dataSource={pageData} pagination={{
  378. currentPage: activePage,
  379. pageSize: ITEMS_PER_PAGE,
  380. total: logCount,
  381. pageSizeOpts: [10, 20, 50, 100],
  382. onPageChange: handlePageChange,
  383. }} loading={loading}/>
  384. <Select defaultValue="0" style={{width: 120}} onChange={
  385. (value) => {
  386. setLogType(parseInt(value));
  387. }
  388. }>
  389. <Select.Option value="0">全部</Select.Option>
  390. <Select.Option value="1">充值</Select.Option>
  391. <Select.Option value="2">消费</Select.Option>
  392. <Select.Option value="3">管理</Select.Option>
  393. <Select.Option value="4">系统</Select.Option>
  394. </Select>
  395. </Layout>
  396. </>
  397. );
  398. };
  399. export default LogsTable;