MjLogsTable.js 16 KB

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