LogsTable.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  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} 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(true);
  189. const [activePage, setActivePage] = useState(1);
  190. const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
  191. const [searchKeyword, setSearchKeyword] = useState('');
  192. const [searching, setSearching] = useState(false);
  193. const [logType, setLogType] = useState(0);
  194. const isAdminUser = isAdmin();
  195. let now = new Date();
  196. // 初始化start_timestamp为前一天
  197. const [inputs, setInputs] = useState({
  198. username: '',
  199. token_name: '',
  200. model_name: '',
  201. start_timestamp: timestamp2string(now.getTime() / 1000 - 86400),
  202. end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
  203. channel: ''
  204. });
  205. const {username, token_name, model_name, start_timestamp, end_timestamp, channel} = inputs;
  206. const [stat, setStat] = useState({
  207. quota: 0,
  208. token: 0
  209. });
  210. const handleInputChange = (value, name) => {
  211. setInputs((inputs) => ({...inputs, [name]: value}));
  212. };
  213. const getLogSelfStat = async () => {
  214. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  215. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  216. 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}`);
  217. const {success, message, data} = res.data;
  218. if (success) {
  219. setStat(data);
  220. } else {
  221. showError(message);
  222. }
  223. };
  224. const getLogStat = async () => {
  225. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  226. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  227. 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}`);
  228. const {success, message, data} = res.data;
  229. if (success) {
  230. setStat(data);
  231. } else {
  232. showError(message);
  233. }
  234. };
  235. const handleEyeClick = async () => {
  236. if (!showStat) {
  237. if (isAdminUser) {
  238. await getLogStat();
  239. } else {
  240. await getLogSelfStat();
  241. }
  242. }
  243. setShowStat(!showStat);
  244. };
  245. const showUserInfo = async (userId) => {
  246. if (!isAdminUser) {
  247. return;
  248. }
  249. const res = await API.get(`/api/user/${userId}`);
  250. const {success, message, data} = res.data;
  251. if (success) {
  252. Modal.info({
  253. title: '用户信息',
  254. content: <div style={{padding: 12}}>
  255. <p>用户名: {data.username}</p>
  256. <p>余额: {renderQuota(data.quota)}</p>
  257. <p>已用额度:{renderQuota(data.used_quota)}</p>
  258. <p>请求次数:{renderNumber(data.request_count)}</p>
  259. </div>,
  260. centered: true,
  261. })
  262. } else {
  263. showError(message);
  264. }
  265. };
  266. const setLogsFormat = (logs) => {
  267. for (let i = 0; i < logs.length; i++) {
  268. logs[i].timestamp2string = timestamp2string(logs[i].created_at);
  269. logs[i].key = '' + logs[i].id;
  270. }
  271. // data.key = '' + data.id
  272. setLogs(logs);
  273. setLogCount(logs.length + ITEMS_PER_PAGE);
  274. console.log(logCount);
  275. }
  276. const loadLogs = async (startIdx) => {
  277. setLoading(true);
  278. let url = '';
  279. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  280. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  281. if (isAdminUser) {
  282. 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}`;
  283. } else {
  284. url = `/api/log/self/?p=${startIdx}&type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
  285. }
  286. const res = await API.get(url);
  287. const {success, message, data} = res.data;
  288. if (success) {
  289. if (startIdx === 0) {
  290. setLogsFormat(data);
  291. } else {
  292. let newLogs = [...logs];
  293. newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
  294. setLogsFormat(newLogs);
  295. }
  296. } else {
  297. showError(message);
  298. }
  299. setLoading(false);
  300. };
  301. const pageData = logs.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE);
  302. const handlePageChange = page => {
  303. setActivePage(page);
  304. if (page === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) {
  305. // In this case we have to load more data and then append them.
  306. loadLogs(page - 1).then(r => {
  307. });
  308. }
  309. };
  310. const refresh = async () => {
  311. // setLoading(true);
  312. setActivePage(1);
  313. await loadLogs(0);
  314. };
  315. const copyText = async (text) => {
  316. if (await copy(text)) {
  317. showSuccess('已复制:' + text);
  318. } else {
  319. // setSearchKeyword(text);
  320. Modal.error({title: '无法复制到剪贴板,请手动复制', content: text});
  321. }
  322. }
  323. useEffect(() => {
  324. refresh().then();
  325. }, [logType]);
  326. const searchLogs = async () => {
  327. if (searchKeyword === '') {
  328. // if keyword is blank, load files instead.
  329. await loadLogs(0);
  330. setActivePage(1);
  331. return;
  332. }
  333. setSearching(true);
  334. const res = await API.get(`/api/log/self/search?keyword=${searchKeyword}`);
  335. const {success, message, data} = res.data;
  336. if (success) {
  337. setLogs(data);
  338. setActivePage(1);
  339. } else {
  340. showError(message);
  341. }
  342. setSearching(false);
  343. };
  344. const handleKeywordChange = async (e, {value}) => {
  345. setSearchKeyword(value.trim());
  346. };
  347. const sortLog = (key) => {
  348. if (logs.length === 0) return;
  349. setLoading(true);
  350. let sortedLogs = [...logs];
  351. if (typeof sortedLogs[0][key] === 'string') {
  352. sortedLogs.sort((a, b) => {
  353. return ('' + a[key]).localeCompare(b[key]);
  354. });
  355. } else {
  356. sortedLogs.sort((a, b) => {
  357. if (a[key] === b[key]) return 0;
  358. if (a[key] > b[key]) return -1;
  359. if (a[key] < b[key]) return 1;
  360. });
  361. }
  362. if (sortedLogs[0].id === logs[0].id) {
  363. sortedLogs.reverse();
  364. }
  365. setLogs(sortedLogs);
  366. setLoading(false);
  367. };
  368. return (
  369. <>
  370. <Layout>
  371. <Header>
  372. <h3>使用明细(总消耗额度:
  373. {showStat && renderQuota(stat.quota)}
  374. {!showStat &&
  375. <span onClick={handleEyeClick} style={{cursor: 'pointer', color: 'gray'}}>点击查看</span>}
  376. </h3>
  377. </Header>
  378. <Form layout='horizontal' style={{marginTop: 10}}>
  379. <>
  380. <Form.Input field="token_name" label='令牌名称' style={{width: 176}} value={token_name}
  381. placeholder={'可选值'} name='token_name'
  382. onChange={value => handleInputChange(value, 'token_name')}/>
  383. <Form.Input field="model_name" label='模型名称' style={{width: 176}} value={model_name}
  384. placeholder='可选值'
  385. name='model_name'
  386. onChange={value => handleInputChange(value, 'model_name')}/>
  387. <Form.DatePicker field="start_timestamp" label='起始时间' style={{width: 272}}
  388. initValue={start_timestamp}
  389. value={start_timestamp} type='dateTime'
  390. name='start_timestamp'
  391. onChange={value => handleInputChange(value, 'start_timestamp')}/>
  392. <Form.DatePicker field="end_timestamp" fluid label='结束时间' style={{width: 272}}
  393. initValue={end_timestamp}
  394. value={end_timestamp} type='dateTime'
  395. name='end_timestamp'
  396. onChange={value => handleInputChange(value, 'end_timestamp')}/>
  397. {
  398. isAdminUser && <>
  399. <Form.Input field="channel" label='渠道 ID' style={{width: 176}} value={channel}
  400. placeholder='可选值' name='channel'
  401. onChange={value => handleInputChange(value, 'channel')}/>
  402. <Form.Input field="username" label='用户名称' style={{width: 176}} value={username}
  403. placeholder={'可选值'} name='username'
  404. onChange={value => handleInputChange(value, 'username')}/>
  405. </>
  406. }
  407. <Form.Section>
  408. <Button label='查询' type="primary" htmlType="submit" className="btn-margin-right"
  409. onClick={refresh}>查询</Button>
  410. </Form.Section>
  411. </>
  412. </Form>
  413. <Table columns={columns} dataSource={pageData} pagination={{
  414. currentPage: activePage,
  415. pageSize: ITEMS_PER_PAGE,
  416. total: logCount,
  417. pageSizeOpts: [10, 20, 50, 100],
  418. onPageChange: handlePageChange,
  419. }} loading={loading}/>
  420. <Select defaultValue="0" style={{width: 120}} onChange={
  421. (value) => {
  422. setLogType(parseInt(value));
  423. }
  424. }>
  425. <Select.Option value="0">全部</Select.Option>
  426. <Select.Option value="1">充值</Select.Option>
  427. <Select.Option value="2">消费</Select.Option>
  428. <Select.Option value="3">管理</Select.Option>
  429. <Select.Option value="4">系统</Select.Option>
  430. </Select>
  431. </Layout>
  432. </>
  433. );
  434. };
  435. export default LogsTable;