MjLogsTable.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  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. // 修改renderDuration函数以包含颜色逻辑
  232. function renderDuration(submit_time, finishTime) {
  233. // 确保startTime和finishTime都是有效的时间戳
  234. if (!submit_time || !finishTime) return 'N/A';
  235. // 将时间戳转换为Date对象
  236. const start = new Date(submit_time);
  237. const finish = new Date(finishTime);
  238. // 计算时间差(毫秒)
  239. const durationMs = finish - start;
  240. // 将时间差转换为秒,并保留一位小数
  241. const durationSec = (durationMs / 1000).toFixed(1);
  242. // 设置颜色:大于60秒则为红色,小于等于60秒则为绿色
  243. const color = durationSec > 60 ? 'red' : 'green';
  244. // 返回带有样式的颜色标签
  245. return (
  246. <Tag color={color} size="large">
  247. {durationSec} 秒
  248. </Tag>
  249. );
  250. }
  251. const LogsTable = () => {
  252. const [isModalOpen, setIsModalOpen] = useState(false);
  253. const [modalContent, setModalContent] = useState('');
  254. const columns = [
  255. {
  256. title: '提交时间',
  257. dataIndex: 'submit_time',
  258. render: (text, record, index) => {
  259. return <div>{renderTimestamp(text / 1000)}</div>;
  260. },
  261. },
  262. {
  263. title: '花费时间',
  264. dataIndex: 'finish_time', // 以finish_time作为dataIndex
  265. key: 'finish_time',
  266. render: (finish, record) => {
  267. // 假设record.start_time是存在的,并且finish是完成时间的时间戳
  268. return renderDuration(record.submit_time, finish);
  269. },
  270. },
  271. {
  272. title: '渠道',
  273. dataIndex: 'channel_id',
  274. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  275. render: (text, record, index) => {
  276. return (
  277. <div>
  278. <Tag
  279. color={colors[parseInt(text) % colors.length]}
  280. size='large'
  281. onClick={() => {
  282. copyText(text); // 假设copyText是用于文本复制的函数
  283. }}
  284. >
  285. {' '}
  286. {text}{' '}
  287. </Tag>
  288. </div>
  289. );
  290. },
  291. },
  292. {
  293. title: '类型',
  294. dataIndex: 'action',
  295. render: (text, record, index) => {
  296. return <div>{renderType(text)}</div>;
  297. },
  298. },
  299. {
  300. title: '任务ID',
  301. dataIndex: 'mj_id',
  302. render: (text, record, index) => {
  303. return <div>{text}</div>;
  304. },
  305. },
  306. {
  307. title: '提交结果',
  308. dataIndex: 'code',
  309. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  310. render: (text, record, index) => {
  311. return <div>{renderCode(text)}</div>;
  312. },
  313. },
  314. {
  315. title: '任务状态',
  316. dataIndex: 'status',
  317. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  318. render: (text, record, index) => {
  319. return <div>{renderStatus(text)}</div>;
  320. },
  321. },
  322. {
  323. title: '进度',
  324. dataIndex: 'progress',
  325. render: (text, record, index) => {
  326. return (
  327. <div>
  328. {
  329. // 转换例如100%为数字100,如果text未定义,返回0
  330. <Progress
  331. stroke={
  332. record.status === 'FAILURE'
  333. ? 'var(--semi-color-warning)'
  334. : null
  335. }
  336. percent={text ? parseInt(text.replace('%', '')) : 0}
  337. showInfo={true}
  338. aria-label='drawing progress'
  339. />
  340. }
  341. </div>
  342. );
  343. },
  344. },
  345. {
  346. title: '结果图片',
  347. dataIndex: 'image_url',
  348. render: (text, record, index) => {
  349. if (!text) {
  350. return '无';
  351. }
  352. return (
  353. <Button
  354. onClick={() => {
  355. setModalImageUrl(text); // 更新图片URL状态
  356. setIsModalOpenurl(true); // 打开模态框
  357. }}
  358. >
  359. 查看图片
  360. </Button>
  361. );
  362. },
  363. },
  364. {
  365. title: 'Prompt',
  366. dataIndex: 'prompt',
  367. render: (text, record, index) => {
  368. // 如果text未定义,返回替代文本,例如空字符串''或其他
  369. if (!text) {
  370. return '无';
  371. }
  372. return (
  373. <Typography.Text
  374. ellipsis={{ showTooltip: true }}
  375. style={{ width: 100 }}
  376. onClick={() => {
  377. setModalContent(text);
  378. setIsModalOpen(true);
  379. }}
  380. >
  381. {text}
  382. </Typography.Text>
  383. );
  384. },
  385. },
  386. {
  387. title: 'PromptEn',
  388. dataIndex: 'prompt_en',
  389. render: (text, record, index) => {
  390. // 如果text未定义,返回替代文本,例如空字符串''或其他
  391. if (!text) {
  392. return '无';
  393. }
  394. return (
  395. <Typography.Text
  396. ellipsis={{ showTooltip: true }}
  397. style={{ width: 100 }}
  398. onClick={() => {
  399. setModalContent(text);
  400. setIsModalOpen(true);
  401. }}
  402. >
  403. {text}
  404. </Typography.Text>
  405. );
  406. },
  407. },
  408. {
  409. title: '失败原因',
  410. dataIndex: 'fail_reason',
  411. render: (text, record, index) => {
  412. // 如果text未定义,返回替代文本,例如空字符串''或其他
  413. if (!text) {
  414. return '无';
  415. }
  416. return (
  417. <Typography.Text
  418. ellipsis={{ showTooltip: true }}
  419. style={{ width: 100 }}
  420. onClick={() => {
  421. setModalContent(text);
  422. setIsModalOpen(true);
  423. }}
  424. >
  425. {text}
  426. </Typography.Text>
  427. );
  428. },
  429. },
  430. ];
  431. const [logs, setLogs] = useState([]);
  432. const [loading, setLoading] = useState(true);
  433. const [activePage, setActivePage] = useState(1);
  434. const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
  435. const [logType, setLogType] = useState(0);
  436. const isAdminUser = isAdmin();
  437. const [isModalOpenurl, setIsModalOpenurl] = useState(false);
  438. const [showBanner, setShowBanner] = useState(false);
  439. // 定义模态框图片URL的状态和更新函数
  440. const [modalImageUrl, setModalImageUrl] = useState('');
  441. let now = new Date();
  442. // 初始化start_timestamp为前一天
  443. const [inputs, setInputs] = useState({
  444. channel_id: '',
  445. mj_id: '',
  446. start_timestamp: timestamp2string(now.getTime() / 1000 - 2592000),
  447. end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
  448. });
  449. const { channel_id, mj_id, start_timestamp, end_timestamp } = inputs;
  450. const [stat, setStat] = useState({
  451. quota: 0,
  452. token: 0,
  453. });
  454. const handleInputChange = (value, name) => {
  455. setInputs((inputs) => ({ ...inputs, [name]: value }));
  456. };
  457. const setLogsFormat = (logs) => {
  458. for (let i = 0; i < logs.length; i++) {
  459. logs[i].timestamp2string = timestamp2string(logs[i].created_at);
  460. logs[i].key = '' + logs[i].id;
  461. }
  462. // data.key = '' + data.id
  463. setLogs(logs);
  464. setLogCount(logs.length + ITEMS_PER_PAGE);
  465. // console.log(logCount);
  466. };
  467. const loadLogs = async (startIdx) => {
  468. setLoading(true);
  469. let url = '';
  470. let localStartTimestamp = Date.parse(start_timestamp);
  471. let localEndTimestamp = Date.parse(end_timestamp);
  472. if (isAdminUser) {
  473. url = `/api/mj/?p=${startIdx}&channel_id=${channel_id}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
  474. } else {
  475. url = `/api/mj/self/?p=${startIdx}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
  476. }
  477. const res = await API.get(url);
  478. const { success, message, data } = res.data;
  479. if (success) {
  480. if (startIdx === 0) {
  481. setLogsFormat(data);
  482. } else {
  483. let newLogs = [...logs];
  484. newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
  485. setLogsFormat(newLogs);
  486. }
  487. } else {
  488. showError(message);
  489. }
  490. setLoading(false);
  491. };
  492. const pageData = logs.slice(
  493. (activePage - 1) * ITEMS_PER_PAGE,
  494. activePage * ITEMS_PER_PAGE,
  495. );
  496. const handlePageChange = (page) => {
  497. setActivePage(page);
  498. if (page === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) {
  499. // In this case we have to load more data and then append them.
  500. loadLogs(page - 1).then((r) => {});
  501. }
  502. };
  503. const refresh = async () => {
  504. // setLoading(true);
  505. setActivePage(1);
  506. await loadLogs(0);
  507. };
  508. const copyText = async (text) => {
  509. if (await copy(text)) {
  510. showSuccess('已复制:' + text);
  511. } else {
  512. // setSearchKeyword(text);
  513. Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
  514. }
  515. };
  516. useEffect(() => {
  517. refresh().then();
  518. }, [logType]);
  519. useEffect(() => {
  520. const mjNotifyEnabled = localStorage.getItem('mj_notify_enabled');
  521. if (mjNotifyEnabled !== 'true') {
  522. setShowBanner(true);
  523. }
  524. }, []);
  525. return (
  526. <>
  527. <Layout>
  528. {isAdminUser && showBanner ? (
  529. <Banner
  530. type='info'
  531. description='当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。'
  532. />
  533. ) : (
  534. <></>
  535. )}
  536. <Form layout='horizontal' style={{ marginTop: 10 }}>
  537. <>
  538. <Form.Input
  539. field='channel_id'
  540. label='渠道 ID'
  541. style={{ width: 176 }}
  542. value={channel_id}
  543. placeholder={'可选值'}
  544. name='channel_id'
  545. onChange={(value) => handleInputChange(value, 'channel_id')}
  546. />
  547. <Form.Input
  548. field='mj_id'
  549. label='任务 ID'
  550. style={{ width: 176 }}
  551. value={mj_id}
  552. placeholder='可选值'
  553. name='mj_id'
  554. onChange={(value) => handleInputChange(value, 'mj_id')}
  555. />
  556. <Form.DatePicker
  557. field='start_timestamp'
  558. label='起始时间'
  559. style={{ width: 272 }}
  560. initValue={start_timestamp}
  561. value={start_timestamp}
  562. type='dateTime'
  563. name='start_timestamp'
  564. onChange={(value) => handleInputChange(value, 'start_timestamp')}
  565. />
  566. <Form.DatePicker
  567. field='end_timestamp'
  568. fluid
  569. label='结束时间'
  570. style={{ width: 272 }}
  571. initValue={end_timestamp}
  572. value={end_timestamp}
  573. type='dateTime'
  574. name='end_timestamp'
  575. onChange={(value) => handleInputChange(value, 'end_timestamp')}
  576. />
  577. <Form.Section>
  578. <Button
  579. label='查询'
  580. type='primary'
  581. htmlType='submit'
  582. className='btn-margin-right'
  583. onClick={refresh}
  584. >
  585. 查询
  586. </Button>
  587. </Form.Section>
  588. </>
  589. </Form>
  590. <Table
  591. style={{ marginTop: 5 }}
  592. columns={columns}
  593. dataSource={pageData}
  594. pagination={{
  595. currentPage: activePage,
  596. pageSize: ITEMS_PER_PAGE,
  597. total: logCount,
  598. pageSizeOpts: [10, 20, 50, 100],
  599. onPageChange: handlePageChange,
  600. }}
  601. loading={loading}
  602. />
  603. <Modal
  604. visible={isModalOpen}
  605. onOk={() => setIsModalOpen(false)}
  606. onCancel={() => setIsModalOpen(false)}
  607. closable={null}
  608. bodyStyle={{ height: '400px', overflow: 'auto' }} // 设置模态框内容区域样式
  609. width={800} // 设置模态框宽度
  610. >
  611. <p style={{ whiteSpace: 'pre-line' }}>{modalContent}</p>
  612. </Modal>
  613. <ImagePreview
  614. src={modalImageUrl}
  615. visible={isModalOpenurl}
  616. onVisibleChange={(visible) => setIsModalOpenurl(visible)}
  617. />
  618. </Layout>
  619. </>
  620. );
  621. };
  622. export default LogsTable;