MjLogsTable.js 15 KB

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