MjLogsTable.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  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. Banner,
  12. Button,
  13. Form,
  14. ImagePreview,
  15. Layout,
  16. Modal,
  17. Progress,
  18. Table,
  19. Tag,
  20. Typography,
  21. } from '@douyinfe/semi-ui';
  22. import { ITEMS_PER_PAGE } from '../constants';
  23. import { useTranslation } from 'react-i18next';
  24. const colors = [
  25. 'amber',
  26. 'blue',
  27. 'cyan',
  28. 'green',
  29. 'grey',
  30. 'indigo',
  31. 'light-blue',
  32. 'lime',
  33. 'orange',
  34. 'pink',
  35. 'purple',
  36. 'red',
  37. 'teal',
  38. 'violet',
  39. 'yellow',
  40. ];
  41. const LogsTable = () => {
  42. const { t } = useTranslation();
  43. const [isModalOpen, setIsModalOpen] = useState(false);
  44. const [modalContent, setModalContent] = useState('');
  45. function renderType(type) {
  46. switch (type) {
  47. case 'IMAGINE':
  48. return (
  49. <Tag color='blue' size='large'>
  50. {t('绘图')}
  51. </Tag>
  52. );
  53. case 'UPSCALE':
  54. return (
  55. <Tag color='orange' size='large'>
  56. {t('放大')}
  57. </Tag>
  58. );
  59. case 'VARIATION':
  60. return (
  61. <Tag color='purple' size='large'>
  62. {t('变换')}
  63. </Tag>
  64. );
  65. case 'HIGH_VARIATION':
  66. return (
  67. <Tag color='purple' size='large'>
  68. {t('强变换')}
  69. </Tag>
  70. );
  71. case 'LOW_VARIATION':
  72. return (
  73. <Tag color='purple' size='large'>
  74. {t('弱变换')}
  75. </Tag>
  76. );
  77. case 'PAN':
  78. return (
  79. <Tag color='cyan' size='large'>
  80. {t('平移')}
  81. </Tag>
  82. );
  83. case 'DESCRIBE':
  84. return (
  85. <Tag color='yellow' size='large'>
  86. {t('图生文')}
  87. </Tag>
  88. );
  89. case 'BLEND':
  90. return (
  91. <Tag color='lime' size='large'>
  92. {t('图混合')}
  93. </Tag>
  94. );
  95. case 'UPLOAD':
  96. return (
  97. <Tag color='blue' size='large'>
  98. 上传文件
  99. </Tag>
  100. );
  101. case 'SHORTEN':
  102. return (
  103. <Tag color='pink' size='large'>
  104. {t('缩词')}
  105. </Tag>
  106. );
  107. case 'REROLL':
  108. return (
  109. <Tag color='indigo' size='large'>
  110. {t('重绘')}
  111. </Tag>
  112. );
  113. case 'INPAINT':
  114. return (
  115. <Tag color='violet' size='large'>
  116. {t('局部重绘-提交')}
  117. </Tag>
  118. );
  119. case 'ZOOM':
  120. return (
  121. <Tag color='teal' size='large'>
  122. {t('变焦')}
  123. </Tag>
  124. );
  125. case 'CUSTOM_ZOOM':
  126. return (
  127. <Tag color='teal' size='large'>
  128. {t('自定义变焦-提交')}
  129. </Tag>
  130. );
  131. case 'MODAL':
  132. return (
  133. <Tag color='green' size='large'>
  134. {t('窗口处理')}
  135. </Tag>
  136. );
  137. case 'SWAP_FACE':
  138. return (
  139. <Tag color='light-green' size='large'>
  140. {t('换脸')}
  141. </Tag>
  142. );
  143. default:
  144. return (
  145. <Tag color='white' size='large'>
  146. {t('未知')}
  147. </Tag>
  148. );
  149. }
  150. }
  151. function renderCode(code) {
  152. switch (code) {
  153. case 1:
  154. return (
  155. <Tag color='green' size='large'>
  156. {t('已提交')}
  157. </Tag>
  158. );
  159. case 21:
  160. return (
  161. <Tag color='lime' size='large'>
  162. {t('等待中')}
  163. </Tag>
  164. );
  165. case 22:
  166. return (
  167. <Tag color='orange' size='large'>
  168. {t('重复提交')}
  169. </Tag>
  170. );
  171. case 0:
  172. return (
  173. <Tag color='yellow' size='large'>
  174. {t('未提交')}
  175. </Tag>
  176. );
  177. default:
  178. return (
  179. <Tag color='white' size='large'>
  180. {t('未知')}
  181. </Tag>
  182. );
  183. }
  184. }
  185. function renderStatus(type) {
  186. switch (type) {
  187. case 'SUCCESS':
  188. return (
  189. <Tag color='green' size='large'>
  190. {t('成功')}
  191. </Tag>
  192. );
  193. case 'NOT_START':
  194. return (
  195. <Tag color='grey' size='large'>
  196. {t('未启动')}
  197. </Tag>
  198. );
  199. case 'SUBMITTED':
  200. return (
  201. <Tag color='yellow' size='large'>
  202. {t('队列中')}
  203. </Tag>
  204. );
  205. case 'IN_PROGRESS':
  206. return (
  207. <Tag color='blue' size='large'>
  208. {t('执行中')}
  209. </Tag>
  210. );
  211. case 'FAILURE':
  212. return (
  213. <Tag color='red' size='large'>
  214. {t('失败')}
  215. </Tag>
  216. );
  217. case 'MODAL':
  218. return (
  219. <Tag color='yellow' size='large'>
  220. {t('窗口等待')}
  221. </Tag>
  222. );
  223. default:
  224. return (
  225. <Tag color='white' size='large'>
  226. {t('未知')}
  227. </Tag>
  228. );
  229. }
  230. }
  231. const renderTimestamp = (timestampInSeconds) => {
  232. const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒
  233. const year = date.getFullYear(); // 获取年份
  234. const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份,从0开始需要+1,并保证两位数
  235. const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数
  236. const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数
  237. const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数
  238. const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数
  239. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出
  240. };
  241. // 修改renderDuration函数以包含颜色逻辑
  242. function renderDuration(submit_time, finishTime) {
  243. if (!submit_time || !finishTime) return 'N/A';
  244. const start = new Date(submit_time);
  245. const finish = new Date(finishTime);
  246. const durationMs = finish - start;
  247. const durationSec = (durationMs / 1000).toFixed(1);
  248. const color = durationSec > 60 ? 'red' : 'green';
  249. return (
  250. <Tag color={color} size='large'>
  251. {durationSec} {t('秒')}
  252. </Tag>
  253. );
  254. }
  255. const columns = [
  256. {
  257. title: t('提交时间'),
  258. dataIndex: 'submit_time',
  259. render: (text, record, index) => {
  260. return <div>{renderTimestamp(text / 1000)}</div>;
  261. },
  262. },
  263. {
  264. title: t('花费时间'),
  265. dataIndex: 'finish_time', // 以finish_time作为dataIndex
  266. key: 'finish_time',
  267. render: (finish, record) => {
  268. // 假设record.start_time是存在的,并且finish是完成时间的时间戳
  269. return renderDuration(record.submit_time, finish);
  270. },
  271. },
  272. {
  273. title: t('渠道'),
  274. dataIndex: 'channel_id',
  275. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  276. render: (text, record, index) => {
  277. return (
  278. <div>
  279. <Tag
  280. color={colors[parseInt(text) % colors.length]}
  281. size='large'
  282. onClick={() => {
  283. copyText(text); // 假设copyText是用于文本复制的函数
  284. }}
  285. >
  286. {' '}
  287. {text}{' '}
  288. </Tag>
  289. </div>
  290. );
  291. },
  292. },
  293. {
  294. title: t('类型'),
  295. dataIndex: 'action',
  296. render: (text, record, index) => {
  297. return <div>{renderType(text)}</div>;
  298. },
  299. },
  300. {
  301. title: t('任务ID'),
  302. dataIndex: 'mj_id',
  303. render: (text, record, index) => {
  304. return <div>{text}</div>;
  305. },
  306. },
  307. {
  308. title: t('提交结果'),
  309. dataIndex: 'code',
  310. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  311. render: (text, record, index) => {
  312. return <div>{renderCode(text)}</div>;
  313. },
  314. },
  315. {
  316. title: t('任务状态'),
  317. dataIndex: 'status',
  318. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  319. render: (text, record, index) => {
  320. return <div>{renderStatus(text)}</div>;
  321. },
  322. },
  323. {
  324. title: t('进度'),
  325. dataIndex: 'progress',
  326. render: (text, record, index) => {
  327. return (
  328. <div>
  329. {
  330. // 转换例如100%为数字100,如果text未定义,返回0
  331. <Progress
  332. stroke={
  333. record.status === 'FAILURE'
  334. ? 'var(--semi-color-warning)'
  335. : null
  336. }
  337. percent={text ? parseInt(text.replace('%', '')) : 0}
  338. showInfo={true}
  339. aria-label='drawing progress'
  340. />
  341. }
  342. </div>
  343. );
  344. },
  345. },
  346. {
  347. title: t('结果图片'),
  348. dataIndex: 'image_url',
  349. render: (text, record, index) => {
  350. if (!text) {
  351. return t('无');
  352. }
  353. return (
  354. <Button
  355. onClick={() => {
  356. setModalImageUrl(text); // 更新图片URL状态
  357. setIsModalOpenurl(true); // 打开模态框
  358. }}
  359. >
  360. {t('查看图片')}
  361. </Button>
  362. );
  363. },
  364. },
  365. {
  366. title: 'Prompt',
  367. dataIndex: 'prompt',
  368. render: (text, record, index) => {
  369. // 如果text未定义,返回替代文本,例如空字符串''或其他
  370. if (!text) {
  371. return t('无');
  372. }
  373. return (
  374. <Typography.Text
  375. ellipsis={{ showTooltip: true }}
  376. style={{ width: 100 }}
  377. onClick={() => {
  378. setModalContent(text);
  379. setIsModalOpen(true);
  380. }}
  381. >
  382. {text}
  383. </Typography.Text>
  384. );
  385. },
  386. },
  387. {
  388. title: 'PromptEn',
  389. dataIndex: 'prompt_en',
  390. render: (text, record, index) => {
  391. // 如果text未定义,返回替代文本,例如空字符串''或其他
  392. if (!text) {
  393. return t('无');
  394. }
  395. return (
  396. <Typography.Text
  397. ellipsis={{ showTooltip: true }}
  398. style={{ width: 100 }}
  399. onClick={() => {
  400. setModalContent(text);
  401. setIsModalOpen(true);
  402. }}
  403. >
  404. {text}
  405. </Typography.Text>
  406. );
  407. },
  408. },
  409. {
  410. title: t('失败原因'),
  411. dataIndex: 'fail_reason',
  412. render: (text, record, index) => {
  413. // 如果text未定义,返回替代文本,例如空字符串''或其他
  414. if (!text) {
  415. return t('无');
  416. }
  417. return (
  418. <Typography.Text
  419. ellipsis={{ showTooltip: true }}
  420. style={{ width: 100 }}
  421. onClick={() => {
  422. setModalContent(text);
  423. setIsModalOpen(true);
  424. }}
  425. >
  426. {text}
  427. </Typography.Text>
  428. );
  429. },
  430. },
  431. ];
  432. const [logs, setLogs] = useState([]);
  433. const [loading, setLoading] = useState(true);
  434. const [activePage, setActivePage] = useState(1);
  435. const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
  436. const [logType, setLogType] = useState(0);
  437. const isAdminUser = isAdmin();
  438. const [isModalOpenurl, setIsModalOpenurl] = useState(false);
  439. const [showBanner, setShowBanner] = useState(false);
  440. // 定义模态框图片URL的状态和更新函数
  441. const [modalImageUrl, setModalImageUrl] = useState('');
  442. let now = new Date();
  443. // 初始化start_timestamp为前一天
  444. const [inputs, setInputs] = useState({
  445. channel_id: '',
  446. mj_id: '',
  447. start_timestamp: timestamp2string(now.getTime() / 1000 - 2592000),
  448. end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
  449. });
  450. const { channel_id, mj_id, start_timestamp, end_timestamp } = inputs;
  451. const [stat, setStat] = useState({
  452. quota: 0,
  453. token: 0,
  454. });
  455. const handleInputChange = (value, name) => {
  456. setInputs((inputs) => ({ ...inputs, [name]: value }));
  457. };
  458. const setLogsFormat = (logs) => {
  459. for (let i = 0; i < logs.length; i++) {
  460. logs[i].timestamp2string = timestamp2string(logs[i].created_at);
  461. logs[i].key = '' + logs[i].id;
  462. }
  463. // data.key = '' + data.id
  464. setLogs(logs);
  465. setLogCount(logs.length + ITEMS_PER_PAGE);
  466. // console.log(logCount);
  467. };
  468. const loadLogs = async (startIdx) => {
  469. setLoading(true);
  470. let url = '';
  471. let localStartTimestamp = Date.parse(start_timestamp);
  472. let localEndTimestamp = Date.parse(end_timestamp);
  473. if (isAdminUser) {
  474. url = `/api/mj/?p=${startIdx}&channel_id=${channel_id}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
  475. } else {
  476. url = `/api/mj/self/?p=${startIdx}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
  477. }
  478. const res = await API.get(url);
  479. const { success, message, data } = res.data;
  480. if (success) {
  481. if (startIdx === 0) {
  482. setLogsFormat(data);
  483. } else {
  484. let newLogs = [...logs];
  485. newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
  486. setLogsFormat(newLogs);
  487. }
  488. } else {
  489. showError(message);
  490. }
  491. setLoading(false);
  492. };
  493. const pageData = logs.slice(
  494. (activePage - 1) * ITEMS_PER_PAGE,
  495. activePage * ITEMS_PER_PAGE,
  496. );
  497. const handlePageChange = (page) => {
  498. setActivePage(page);
  499. if (page === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) {
  500. // In this case we have to load more data and then append them.
  501. loadLogs(page - 1).then((r) => {});
  502. }
  503. };
  504. const refresh = async () => {
  505. // setLoading(true);
  506. setActivePage(1);
  507. await loadLogs(0);
  508. };
  509. const copyText = async (text) => {
  510. if (await copy(text)) {
  511. showSuccess('已复制:' + text);
  512. } else {
  513. // setSearchKeyword(text);
  514. Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
  515. }
  516. };
  517. useEffect(() => {
  518. refresh().then();
  519. }, [logType]);
  520. useEffect(() => {
  521. const mjNotifyEnabled = localStorage.getItem('mj_notify_enabled');
  522. if (mjNotifyEnabled !== 'true') {
  523. setShowBanner(true);
  524. }
  525. }, []);
  526. return (
  527. <>
  528. <Layout>
  529. {isAdminUser && showBanner ? (
  530. <Banner
  531. type='info'
  532. description={t(
  533. '当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。',
  534. )}
  535. />
  536. ) : (
  537. <></>
  538. )}
  539. <Form layout='horizontal' style={{ marginTop: 10 }}>
  540. <>
  541. <Form.Input
  542. field='channel_id'
  543. label={t('渠道 ID')}
  544. style={{ width: 176 }}
  545. value={channel_id}
  546. placeholder={t('可选值')}
  547. name='channel_id'
  548. onChange={(value) => handleInputChange(value, 'channel_id')}
  549. />
  550. <Form.Input
  551. field='mj_id'
  552. label={t('任务 ID')}
  553. style={{ width: 176 }}
  554. value={mj_id}
  555. placeholder={t('可选值')}
  556. name='mj_id'
  557. onChange={(value) => handleInputChange(value, 'mj_id')}
  558. />
  559. <Form.DatePicker
  560. field='start_timestamp'
  561. label={t('起始时间')}
  562. style={{ width: 272 }}
  563. initValue={start_timestamp}
  564. value={start_timestamp}
  565. type='dateTime'
  566. name='start_timestamp'
  567. onChange={(value) => handleInputChange(value, 'start_timestamp')}
  568. />
  569. <Form.DatePicker
  570. field='end_timestamp'
  571. fluid
  572. label={t('结束时间')}
  573. style={{ width: 272 }}
  574. initValue={end_timestamp}
  575. value={end_timestamp}
  576. type='dateTime'
  577. name='end_timestamp'
  578. onChange={(value) => handleInputChange(value, 'end_timestamp')}
  579. />
  580. <Form.Section>
  581. <Button
  582. label={t('查询')}
  583. type='primary'
  584. htmlType='submit'
  585. className='btn-margin-right'
  586. onClick={refresh}
  587. >
  588. {t('查询')}
  589. </Button>
  590. </Form.Section>
  591. </>
  592. </Form>
  593. <Table
  594. style={{ marginTop: 5 }}
  595. columns={columns}
  596. dataSource={pageData}
  597. pagination={{
  598. currentPage: activePage,
  599. pageSize: ITEMS_PER_PAGE,
  600. total: logCount,
  601. pageSizeOpts: [10, 20, 50, 100],
  602. onPageChange: handlePageChange,
  603. formatPageText: (page) =>
  604. t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
  605. start: page.currentStart,
  606. end: page.currentEnd,
  607. total: logCount,
  608. }),
  609. }}
  610. loading={loading}
  611. />
  612. <Modal
  613. visible={isModalOpen}
  614. onOk={() => setIsModalOpen(false)}
  615. onCancel={() => setIsModalOpen(false)}
  616. closable={null}
  617. bodyStyle={{ height: '400px', overflow: 'auto' }} // 设置模态框内容区域样式
  618. width={800} // 设置模态框宽度
  619. >
  620. <p style={{ whiteSpace: 'pre-line' }}>{modalContent}</p>
  621. </Modal>
  622. <ImagePreview
  623. src={modalImageUrl}
  624. visible={isModalOpenurl}
  625. onVisibleChange={(visible) => setIsModalOpenurl(visible)}
  626. />
  627. </Layout>
  628. </>
  629. );
  630. };
  631. export default LogsTable;