LogsTable.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862
  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,
  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. const { Header } = Layout;
  37. function renderTimestamp(timestamp) {
  38. return <>{timestamp2string(timestamp)}</>;
  39. }
  40. const MODE_OPTIONS = [
  41. { key: 'all', text: 'all', value: 'all' },
  42. { key: 'self', text: 'current user', value: 'self' },
  43. ];
  44. const colors = [
  45. 'amber',
  46. 'blue',
  47. 'cyan',
  48. 'green',
  49. 'grey',
  50. 'indigo',
  51. 'light-blue',
  52. 'lime',
  53. 'orange',
  54. 'pink',
  55. 'purple',
  56. 'red',
  57. 'teal',
  58. 'violet',
  59. 'yellow',
  60. ];
  61. const LogsTable = () => {
  62. const { t } = useTranslation();
  63. function renderType(type) {
  64. switch (type) {
  65. case 1:
  66. return <Tag color='cyan' size='large'>{t('充值')}</Tag>;
  67. case 2:
  68. return <Tag color='lime' size='large'>{t('消费')}</Tag>;
  69. case 3:
  70. return <Tag color='orange' size='large'>{t('管理')}</Tag>;
  71. case 4:
  72. return <Tag color='purple' size='large'>{t('系统')}</Tag>;
  73. default:
  74. return <Tag color='black' size='large'>{t('未知')}</Tag>;
  75. }
  76. }
  77. function renderIsStream(bool) {
  78. if (bool) {
  79. return <Tag color='blue' size='large'>{t('流')}</Tag>;
  80. } else {
  81. return <Tag color='purple' size='large'>{t('非流')}</Tag>;
  82. }
  83. }
  84. function renderUseTime(type) {
  85. const time = parseInt(type);
  86. if (time < 101) {
  87. return (
  88. <Tag color='green' size='large'>
  89. {' '}
  90. {time} s{' '}
  91. </Tag>
  92. );
  93. } else if (time < 300) {
  94. return (
  95. <Tag color='orange' size='large'>
  96. {' '}
  97. {time} s{' '}
  98. </Tag>
  99. );
  100. } else {
  101. return (
  102. <Tag color='red' size='large'>
  103. {' '}
  104. {time} s{' '}
  105. </Tag>
  106. );
  107. }
  108. }
  109. function renderFirstUseTime(type) {
  110. let time = parseFloat(type) / 1000.0;
  111. time = time.toFixed(1);
  112. if (time < 3) {
  113. return (
  114. <Tag color='green' size='large'>
  115. {' '}
  116. {time} s{' '}
  117. </Tag>
  118. );
  119. } else if (time < 10) {
  120. return (
  121. <Tag color='orange' size='large'>
  122. {' '}
  123. {time} s{' '}
  124. </Tag>
  125. );
  126. } else {
  127. return (
  128. <Tag color='red' size='large'>
  129. {' '}
  130. {time} s{' '}
  131. </Tag>
  132. );
  133. }
  134. }
  135. const columns = [
  136. {
  137. title: t('时间'),
  138. dataIndex: 'timestamp2string',
  139. },
  140. {
  141. title: t('渠道'),
  142. dataIndex: 'channel',
  143. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  144. render: (text, record, index) => {
  145. return isAdminUser ? (
  146. record.type === 0 || record.type === 2 ? (
  147. <div>
  148. {
  149. <Tooltip content={record.channel_name || '[未知]'}>
  150. <Tag
  151. color={colors[parseInt(text) % colors.length]}
  152. size='large'
  153. >
  154. {' '}
  155. {text}{' '}
  156. </Tag>
  157. </Tooltip>
  158. }
  159. </div>
  160. ) : (
  161. <></>
  162. )
  163. ) : (
  164. <></>
  165. );
  166. },
  167. },
  168. {
  169. title: t('用户'),
  170. dataIndex: 'username',
  171. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  172. render: (text, record, index) => {
  173. return isAdminUser ? (
  174. <div>
  175. <Avatar
  176. size='small'
  177. color={stringToColor(text)}
  178. style={{ marginRight: 4 }}
  179. onClick={(event) => {
  180. event.stopPropagation();
  181. showUserInfo(record.user_id)
  182. }}
  183. >
  184. {typeof text === 'string' && text.slice(0, 1)}
  185. </Avatar>
  186. {text}
  187. </div>
  188. ) : (
  189. <></>
  190. );
  191. },
  192. },
  193. {
  194. title: t('令牌'),
  195. dataIndex: 'token_name',
  196. render: (text, record, index) => {
  197. return record.type === 0 || record.type === 2 ? (
  198. <div>
  199. <Tag
  200. color='grey'
  201. size='large'
  202. onClick={(event) => {
  203. //cancel the row click event
  204. copyText(event, text);
  205. }}
  206. >
  207. {' '}
  208. {t(text)}{' '}
  209. </Tag>
  210. </div>
  211. ) : (
  212. <></>
  213. );
  214. },
  215. },
  216. {
  217. title: t('分组'),
  218. dataIndex: 'group',
  219. render: (text, record, index) => {
  220. if (record.type === 0 || record.type === 2) {
  221. if (record.group) {
  222. return (
  223. <>
  224. {renderGroup(record.group)}
  225. </>
  226. );
  227. } else {
  228. let other = null;
  229. try {
  230. other = JSON.parse(record.other);
  231. } catch (e) {
  232. console.error(`Failed to parse record.other: "${record.other}".`, e);
  233. }
  234. if (other === null) {
  235. return <></>;
  236. }
  237. if (other.group !== undefined) {
  238. return (
  239. <>
  240. {renderGroup(other.group)}
  241. </>
  242. );
  243. } else {
  244. return <></>;
  245. }
  246. }
  247. } else {
  248. return <></>;
  249. }
  250. },
  251. },
  252. {
  253. title: t('类型'),
  254. dataIndex: 'type',
  255. render: (text, record, index) => {
  256. return <>{renderType(text)}</>;
  257. },
  258. },
  259. {
  260. title: t('模型'),
  261. dataIndex: 'model_name',
  262. render: (text, record, index) => {
  263. return record.type === 0 || record.type === 2 ? (
  264. <>
  265. <Tag
  266. color={stringToColor(text)}
  267. size='large'
  268. onClick={(event) => {
  269. copyText(event, text);
  270. }}
  271. >
  272. {' '}
  273. {text}{' '}
  274. </Tag>
  275. </>
  276. ) : (
  277. <></>
  278. );
  279. },
  280. },
  281. {
  282. title: t('用时/首字'),
  283. dataIndex: 'use_time',
  284. render: (text, record, index) => {
  285. if (record.is_stream) {
  286. let other = getLogOther(record.other);
  287. return (
  288. <>
  289. <Space>
  290. {renderUseTime(text)}
  291. {renderFirstUseTime(other.frt)}
  292. {renderIsStream(record.is_stream)}
  293. </Space>
  294. </>
  295. );
  296. } else {
  297. return (
  298. <>
  299. <Space>
  300. {renderUseTime(text)}
  301. {renderIsStream(record.is_stream)}
  302. </Space>
  303. </>
  304. );
  305. }
  306. },
  307. },
  308. {
  309. title: t('提示'),
  310. dataIndex: 'prompt_tokens',
  311. render: (text, record, index) => {
  312. return record.type === 0 || record.type === 2 ? (
  313. <>{<span> {text} </span>}</>
  314. ) : (
  315. <></>
  316. );
  317. },
  318. },
  319. {
  320. title: t('补全'),
  321. dataIndex: 'completion_tokens',
  322. render: (text, record, index) => {
  323. return parseInt(text) > 0 &&
  324. (record.type === 0 || record.type === 2) ? (
  325. <>{<span> {text} </span>}</>
  326. ) : (
  327. <></>
  328. );
  329. },
  330. },
  331. {
  332. title: t('花费'),
  333. dataIndex: 'quota',
  334. render: (text, record, index) => {
  335. return record.type === 0 || record.type === 2 ? (
  336. <>{renderQuota(text, 6)}</>
  337. ) : (
  338. <></>
  339. );
  340. },
  341. },
  342. {
  343. title: t('重试'),
  344. dataIndex: 'retry',
  345. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  346. render: (text, record, index) => {
  347. let content = t('渠道') + `:${record.channel}`;
  348. if (record.other !== '') {
  349. let other = JSON.parse(record.other);
  350. if (other === null) {
  351. return <></>;
  352. }
  353. if (other.admin_info !== undefined) {
  354. if (
  355. other.admin_info.use_channel !== null &&
  356. other.admin_info.use_channel !== undefined &&
  357. other.admin_info.use_channel !== ''
  358. ) {
  359. // channel id array
  360. let useChannel = other.admin_info.use_channel;
  361. let useChannelStr = useChannel.join('->');
  362. content = t('渠道') + `:${useChannelStr}`;
  363. }
  364. }
  365. }
  366. return isAdminUser ? <div>{content}</div> : <></>;
  367. },
  368. },
  369. {
  370. title: t('详情'),
  371. dataIndex: 'content',
  372. render: (text, record, index) => {
  373. let other = getLogOther(record.other);
  374. if (other == null || record.type !== 2) {
  375. return (
  376. <Paragraph
  377. ellipsis={{
  378. rows: 2,
  379. showTooltip: {
  380. type: 'popover',
  381. opts: { style: { width: 240 } },
  382. },
  383. }}
  384. style={{ maxWidth: 240 }}
  385. >
  386. {text}
  387. </Paragraph>
  388. );
  389. }
  390. let content = renderModelPriceSimple(
  391. other.model_ratio,
  392. other.model_price,
  393. other.group_ratio,
  394. );
  395. return (
  396. <Paragraph
  397. ellipsis={{
  398. rows: 2,
  399. }}
  400. style={{ maxWidth: 240 }}
  401. >
  402. {content}
  403. </Paragraph>
  404. );
  405. },
  406. },
  407. ];
  408. const [styleState, styleDispatch] = useContext(StyleContext);
  409. const [logs, setLogs] = useState([]);
  410. const [expandData, setExpandData] = useState({});
  411. const [showStat, setShowStat] = useState(false);
  412. const [loading, setLoading] = useState(false);
  413. const [loadingStat, setLoadingStat] = useState(false);
  414. const [activePage, setActivePage] = useState(1);
  415. const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
  416. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  417. const [logType, setLogType] = useState(0);
  418. const isAdminUser = isAdmin();
  419. let now = new Date();
  420. // 初始化start_timestamp为今天0点
  421. const [inputs, setInputs] = useState({
  422. username: '',
  423. token_name: '',
  424. model_name: '',
  425. start_timestamp: timestamp2string(getTodayStartTimestamp()),
  426. end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
  427. channel: '',
  428. group: '',
  429. });
  430. const {
  431. username,
  432. token_name,
  433. model_name,
  434. start_timestamp,
  435. end_timestamp,
  436. channel,
  437. group,
  438. } = inputs;
  439. const [stat, setStat] = useState({
  440. quota: 0,
  441. token: 0,
  442. });
  443. const handleInputChange = (value, name) => {
  444. setInputs(inputs => ({ ...inputs, [name]: value }));
  445. };
  446. const getLogSelfStat = async () => {
  447. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  448. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  449. let url = `/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
  450. url = encodeURI(url);
  451. let res = await API.get(url);
  452. const { success, message, data } = res.data;
  453. if (success) {
  454. setStat(data);
  455. } else {
  456. showError(message);
  457. }
  458. };
  459. const getLogStat = async () => {
  460. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  461. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  462. 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}`;
  463. url = encodeURI(url);
  464. let res = await API.get(url);
  465. const { success, message, data } = res.data;
  466. if (success) {
  467. setStat(data);
  468. } else {
  469. showError(message);
  470. }
  471. };
  472. const handleEyeClick = async () => {
  473. if (loadingStat) {
  474. return;
  475. }
  476. setLoadingStat(true);
  477. if (isAdminUser) {
  478. await getLogStat();
  479. } else {
  480. await getLogSelfStat();
  481. }
  482. setShowStat(true);
  483. setLoadingStat(false);
  484. };
  485. const showUserInfo = async (userId) => {
  486. if (!isAdminUser) {
  487. return;
  488. }
  489. const res = await API.get(`/api/user/${userId}`);
  490. const { success, message, data } = res.data;
  491. if (success) {
  492. Modal.info({
  493. title: t('用户信息'),
  494. content: (
  495. <div style={{ padding: 12 }}>
  496. <p>{t('用户名')}: {data.username}</p>
  497. <p>{t('余额')}: {renderQuota(data.quota)}</p>
  498. <p>{t('已用额度')}:{renderQuota(data.used_quota)}</p>
  499. <p>{t('请求次数')}:{renderNumber(data.request_count)}</p>
  500. </div>
  501. ),
  502. centered: true,
  503. });
  504. } else {
  505. showError(message);
  506. }
  507. };
  508. const setLogsFormat = (logs) => {
  509. let expandDatesLocal = {};
  510. for (let i = 0; i < logs.length; i++) {
  511. logs[i].timestamp2string = timestamp2string(logs[i].created_at);
  512. logs[i].key = logs[i].id;
  513. let other = getLogOther(logs[i].other);
  514. let expandDataLocal = [];
  515. if (isAdmin()) {
  516. // let content = '渠道:' + logs[i].channel;
  517. // if (other.admin_info !== undefined) {
  518. // if (
  519. // other.admin_info.use_channel !== null &&
  520. // other.admin_info.use_channel !== undefined &&
  521. // other.admin_info.use_channel !== ''
  522. // ) {
  523. // // channel id array
  524. // let useChannel = other.admin_info.use_channel;
  525. // let useChannelStr = useChannel.join('->');
  526. // content = `渠道:${useChannelStr}`;
  527. // }
  528. // }
  529. // expandDataLocal.push({
  530. // key: '渠道重试',
  531. // value: content,
  532. // })
  533. }
  534. if (isAdminUser && (logs[i].type === 0 || logs[i].type === 2)) {
  535. expandDataLocal.push({
  536. key: t('渠道信息'),
  537. value: `${logs[i].channel} - ${logs[i].channel_name || '[未知]'}`
  538. });
  539. }
  540. if (other?.ws || other?.audio) {
  541. expandDataLocal.push({
  542. key: t('语音输入'),
  543. value: other.audio_input,
  544. });
  545. expandDataLocal.push({
  546. key: t('语音输出'),
  547. value: other.audio_output,
  548. });
  549. expandDataLocal.push({
  550. key: t('文字输入'),
  551. value: other.text_input,
  552. });
  553. expandDataLocal.push({
  554. key: t('文字输出'),
  555. value: other.text_output,
  556. });
  557. }
  558. expandDataLocal.push({
  559. key: t('日志详情'),
  560. value: logs[i].content,
  561. });
  562. if (logs[i].type === 2) {
  563. let content = '';
  564. if (other?.ws || other?.audio) {
  565. content = renderAudioModelPrice(
  566. other.text_input,
  567. other.text_output,
  568. other.model_ratio,
  569. other.model_price,
  570. other.completion_ratio,
  571. other.audio_input,
  572. other.audio_output,
  573. other?.audio_ratio,
  574. other?.audio_completion_ratio,
  575. other.group_ratio,
  576. );
  577. } else {
  578. content = renderModelPrice(
  579. logs[i].prompt_tokens,
  580. logs[i].completion_tokens,
  581. other.model_ratio,
  582. other.model_price,
  583. other.completion_ratio,
  584. other.group_ratio,
  585. );
  586. }
  587. expandDataLocal.push({
  588. key: t('计费过程'),
  589. value: content,
  590. });
  591. }
  592. expandDatesLocal[logs[i].key] = expandDataLocal;
  593. }
  594. setExpandData(expandDatesLocal);
  595. setLogs(logs);
  596. };
  597. const loadLogs = async (startIdx, pageSize, logType = 0) => {
  598. setLoading(true);
  599. let url = '';
  600. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  601. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  602. if (isAdminUser) {
  603. 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}`;
  604. } else {
  605. 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}`;
  606. }
  607. url = encodeURI(url);
  608. const res = await API.get(url);
  609. const { success, message, data } = res.data;
  610. if (success) {
  611. const newPageData = data.items;
  612. setActivePage(data.page);
  613. setPageSize(data.page_size);
  614. setLogCount(data.total);
  615. setLogsFormat(newPageData);
  616. } else {
  617. showError(message);
  618. }
  619. setLoading(false);
  620. };
  621. const handlePageChange = (page) => {
  622. setActivePage(page);
  623. loadLogs(page, pageSize, logType).then((r) => {});
  624. };
  625. const handlePageSizeChange = async (size) => {
  626. localStorage.setItem('page-size', size + '');
  627. setPageSize(size);
  628. setActivePage(1);
  629. loadLogs(activePage, size)
  630. .then()
  631. .catch((reason) => {
  632. showError(reason);
  633. });
  634. };
  635. const refresh = async () => {
  636. setActivePage(1);
  637. handleEyeClick();
  638. await loadLogs(activePage, pageSize, logType);
  639. };
  640. const copyText = async (e, text) => {
  641. e.stopPropagation();
  642. if (await copy(text)) {
  643. showSuccess('已复制:' + text);
  644. } else {
  645. Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
  646. }
  647. };
  648. useEffect(() => {
  649. const localPageSize =
  650. parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
  651. setPageSize(localPageSize);
  652. loadLogs(activePage, localPageSize)
  653. .then()
  654. .catch((reason) => {
  655. showError(reason);
  656. });
  657. handleEyeClick();
  658. }, []);
  659. const expandRowRender = (record, index) => {
  660. return <Descriptions data={expandData[record.key]} />;
  661. };
  662. return (
  663. <>
  664. <Layout>
  665. <Header>
  666. <Spin spinning={loadingStat}>
  667. <Space>
  668. <Tag color='green' size='large' style={{ padding: 15 }}>
  669. {t('总消耗额度')}: {renderQuota(stat.quota)}
  670. </Tag>
  671. <Tag color='blue' size='large' style={{ padding: 15 }}>
  672. RPM: {stat.rpm}
  673. </Tag>
  674. <Tag color='purple' size='large' style={{ padding: 15 }}>
  675. TPM: {stat.tpm}
  676. </Tag>
  677. </Space>
  678. </Spin>
  679. </Header>
  680. <Form layout='horizontal' style={{ marginTop: 10 }}>
  681. <>
  682. <Form.Section>
  683. <div style={{ marginBottom: 10 }}>
  684. {
  685. styleState.isMobile ? (
  686. <div>
  687. <Form.DatePicker
  688. field='start_timestamp'
  689. label={t('起始时间')}
  690. style={{ width: 272 }}
  691. initValue={start_timestamp}
  692. type='dateTime'
  693. onChange={(value) => {
  694. console.log(value);
  695. handleInputChange(value, 'start_timestamp')
  696. }}
  697. />
  698. <Form.DatePicker
  699. field='end_timestamp'
  700. fluid
  701. label={t('结束时间')}
  702. style={{ width: 272 }}
  703. initValue={end_timestamp}
  704. type='dateTime'
  705. onChange={(value) => handleInputChange(value, 'end_timestamp')}
  706. />
  707. </div>
  708. ) : (
  709. <Form.DatePicker
  710. field="range_timestamp"
  711. label={t('时间范围')}
  712. initValue={[start_timestamp, end_timestamp]}
  713. type="dateTimeRange"
  714. name="range_timestamp"
  715. onChange={(value) => {
  716. if (Array.isArray(value) && value.length === 2) {
  717. handleInputChange(value[0], 'start_timestamp');
  718. handleInputChange(value[1], 'end_timestamp');
  719. }
  720. }}
  721. />
  722. )
  723. }
  724. </div>
  725. </Form.Section>
  726. <Form.Input
  727. field='token_name'
  728. label={t('令牌名称')}
  729. value={token_name}
  730. placeholder={t('可选值')}
  731. name='token_name'
  732. onChange={(value) => handleInputChange(value, 'token_name')}
  733. />
  734. <Form.Input
  735. field='model_name'
  736. label={t('模型名称')}
  737. value={model_name}
  738. placeholder={t('可选值')}
  739. name='model_name'
  740. onChange={(value) => handleInputChange(value, 'model_name')}
  741. />
  742. <Form.Input
  743. field='group'
  744. label={t('分组')}
  745. value={group}
  746. placeholder={t('可选值')}
  747. name='group'
  748. onChange={(value) => handleInputChange(value, 'group')}
  749. />
  750. {isAdminUser && (
  751. <>
  752. <Form.Input
  753. field='channel'
  754. label={t('渠道 ID')}
  755. value={channel}
  756. placeholder={t('可选值')}
  757. name='channel'
  758. onChange={(value) => handleInputChange(value, 'channel')}
  759. />
  760. <Form.Input
  761. field='username'
  762. label={t('用户名称')}
  763. value={username}
  764. placeholder={t('可选值')}
  765. name='username'
  766. onChange={(value) => handleInputChange(value, 'username')}
  767. />
  768. </>
  769. )}
  770. <Button
  771. label={t('查询')}
  772. type='primary'
  773. htmlType='submit'
  774. className='btn-margin-right'
  775. onClick={refresh}
  776. loading={loading}
  777. style={{ marginTop: 24 }}
  778. >
  779. {t('查询')}
  780. </Button>
  781. <Form.Section></Form.Section>
  782. </>
  783. </Form>
  784. <div style={{marginTop:10}}>
  785. <Select
  786. defaultValue='0'
  787. style={{ width: 120 }}
  788. onChange={(value) => {
  789. setLogType(parseInt(value));
  790. loadLogs(0, pageSize, parseInt(value));
  791. }}
  792. >
  793. <Select.Option value='0'>{t('全部')}</Select.Option>
  794. <Select.Option value='1'>{t('充值')}</Select.Option>
  795. <Select.Option value='2'>{t('消费')}</Select.Option>
  796. <Select.Option value='3'>{t('管理')}</Select.Option>
  797. <Select.Option value='4'>{t('系统')}</Select.Option>
  798. </Select>
  799. </div>
  800. <Table
  801. style={{ marginTop: 5 }}
  802. columns={columns}
  803. expandedRowRender={expandRowRender}
  804. expandRowByClick={true}
  805. dataSource={logs}
  806. rowKey="key"
  807. pagination={{
  808. formatPageText: (page) =>
  809. t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
  810. start: page.currentStart,
  811. end: page.currentEnd,
  812. total: logs.length
  813. }),
  814. currentPage: activePage,
  815. pageSize: pageSize,
  816. total: logCount,
  817. pageSizeOpts: [10, 20, 50, 100],
  818. showSizeChanger: true,
  819. onPageSizeChange: (size) => {
  820. handlePageSizeChange(size);
  821. },
  822. onPageChange: handlePageChange,
  823. }}
  824. />
  825. </Layout>
  826. </>
  827. );
  828. };
  829. export default LogsTable;