TaskLogsTable.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. import React, { useEffect, useState } from 'react';
  2. import { Label } from 'semantic-ui-react';
  3. import {
  4. API,
  5. copy,
  6. isAdmin,
  7. showError,
  8. showSuccess,
  9. timestamp2string,
  10. } from '../helpers';
  11. import {
  12. Table,
  13. Tag,
  14. Form,
  15. Button,
  16. Layout,
  17. Modal,
  18. Typography,
  19. Progress,
  20. Card,
  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. const renderTimestamp = (timestampInSeconds) => {
  41. const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒
  42. const year = date.getFullYear(); // 获取年份
  43. const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份,从0开始需要+1,并保证两位数
  44. const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数
  45. const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数
  46. const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数
  47. const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数
  48. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出
  49. };
  50. function renderDuration(submit_time, finishTime) {
  51. // 确保startTime和finishTime都是有效的时间戳
  52. if (!submit_time || !finishTime) return 'N/A';
  53. // 将时间戳转换为Date对象
  54. const start = new Date(submit_time);
  55. const finish = new Date(finishTime);
  56. // 计算时间差(毫秒)
  57. const durationMs = finish - start;
  58. // 将时间差转换为秒,并保留一位小数
  59. const durationSec = (durationMs / 1000).toFixed(1);
  60. // 设置颜色:大于60秒则为红色,小于等于60秒则为绿色
  61. const color = durationSec > 60 ? 'red' : 'green';
  62. // 返回带有样式的颜色标签
  63. return (
  64. <Tag color={color} size='large'>
  65. {durationSec} 秒
  66. </Tag>
  67. );
  68. }
  69. const LogsTable = () => {
  70. const [isModalOpen, setIsModalOpen] = useState(false);
  71. const [modalContent, setModalContent] = useState('');
  72. const isAdminUser = isAdmin();
  73. const columns = [
  74. {
  75. title: '提交时间',
  76. dataIndex: 'submit_time',
  77. render: (text, record, index) => {
  78. return <div>{text ? renderTimestamp(text) : '-'}</div>;
  79. },
  80. },
  81. {
  82. title: '结束时间',
  83. dataIndex: 'finish_time',
  84. render: (text, record, index) => {
  85. return <div>{text ? renderTimestamp(text) : '-'}</div>;
  86. },
  87. },
  88. {
  89. title: '进度',
  90. dataIndex: 'progress',
  91. width: 50,
  92. render: (text, record, index) => {
  93. return (
  94. <div>
  95. {
  96. // 转换例如100%为数字100,如果text未定义,返回0
  97. isNaN(text.replace('%', '')) ? (
  98. text
  99. ) : (
  100. <Progress
  101. width={42}
  102. type='circle'
  103. showInfo={true}
  104. percent={Number(text.replace('%', '') || 0)}
  105. aria-label='drawing progress'
  106. />
  107. )
  108. }
  109. </div>
  110. );
  111. },
  112. },
  113. {
  114. title: '花费时间',
  115. dataIndex: 'finish_time', // 以finish_time作为dataIndex
  116. key: 'finish_time',
  117. render: (finish, record) => {
  118. // 假设record.start_time是存在的,并且finish是完成时间的时间戳
  119. return <>{finish ? renderDuration(record.submit_time, finish) : '-'}</>;
  120. },
  121. },
  122. {
  123. title: '渠道',
  124. dataIndex: 'channel_id',
  125. className: isAdminUser ? 'tableShow' : 'tableHiddle',
  126. render: (text, record, index) => {
  127. return (
  128. <div>
  129. <Tag
  130. color={colors[parseInt(text) % colors.length]}
  131. size='large'
  132. onClick={() => {
  133. copyText(text); // 假设copyText是用于文本复制的函数
  134. }}
  135. >
  136. {' '}
  137. {text}{' '}
  138. </Tag>
  139. </div>
  140. );
  141. },
  142. },
  143. {
  144. title: '平台',
  145. dataIndex: 'platform',
  146. render: (text, record, index) => {
  147. return <div>{renderPlatform(text)}</div>;
  148. },
  149. },
  150. {
  151. title: '类型',
  152. dataIndex: 'action',
  153. render: (text, record, index) => {
  154. return <div>{renderType(text)}</div>;
  155. },
  156. },
  157. {
  158. title: '任务ID(点击查看详情)',
  159. dataIndex: 'task_id',
  160. render: (text, record, index) => {
  161. return (
  162. <Typography.Text
  163. ellipsis={{ showTooltip: true }}
  164. //style={{width: 100}}
  165. onClick={() => {
  166. setModalContent(JSON.stringify(record, null, 2));
  167. setIsModalOpen(true);
  168. }}
  169. >
  170. <div>{text}</div>
  171. </Typography.Text>
  172. );
  173. },
  174. },
  175. {
  176. title: '任务状态',
  177. dataIndex: 'status',
  178. render: (text, record, index) => {
  179. return <div>{renderStatus(text)}</div>;
  180. },
  181. },
  182. {
  183. title: '失败原因',
  184. dataIndex: 'fail_reason',
  185. render: (text, record, index) => {
  186. // 如果text未定义,返回替代文本,例如空字符串''或其他
  187. if (!text) {
  188. return '无';
  189. }
  190. return (
  191. <Typography.Text
  192. ellipsis={{ showTooltip: true }}
  193. style={{ width: 100 }}
  194. onClick={() => {
  195. setModalContent(text);
  196. setIsModalOpen(true);
  197. }}
  198. >
  199. {text}
  200. </Typography.Text>
  201. );
  202. },
  203. },
  204. ];
  205. const [logs, setLogs] = useState([]);
  206. const [loading, setLoading] = useState(true);
  207. const [activePage, setActivePage] = useState(1);
  208. const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
  209. const [logType] = useState(0);
  210. let now = new Date();
  211. // 初始化start_timestamp为前一天
  212. let zeroNow = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  213. const [inputs, setInputs] = useState({
  214. channel_id: '',
  215. task_id: '',
  216. start_timestamp: timestamp2string(zeroNow.getTime() / 1000),
  217. end_timestamp: '',
  218. });
  219. const { channel_id, task_id, start_timestamp, end_timestamp } = inputs;
  220. const handleInputChange = (value, name) => {
  221. setInputs((inputs) => ({ ...inputs, [name]: value }));
  222. };
  223. const setLogsFormat = (logs) => {
  224. for (let i = 0; i < logs.length; i++) {
  225. logs[i].timestamp2string = timestamp2string(logs[i].created_at);
  226. logs[i].key = '' + logs[i].id;
  227. }
  228. // data.key = '' + data.id
  229. setLogs(logs);
  230. setLogCount(logs.length + ITEMS_PER_PAGE);
  231. // console.log(logCount);
  232. };
  233. const loadLogs = async (startIdx) => {
  234. setLoading(true);
  235. let url = '';
  236. let localStartTimestamp = parseInt(Date.parse(start_timestamp) / 1000);
  237. let localEndTimestamp = parseInt(Date.parse(end_timestamp) / 1000);
  238. if (isAdminUser) {
  239. url = `/api/task/?p=${startIdx}&channel_id=${channel_id}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
  240. } else {
  241. url = `/api/task/self?p=${startIdx}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
  242. }
  243. const res = await API.get(url);
  244. let { success, message, data } = res.data;
  245. if (success) {
  246. if (startIdx === 0) {
  247. setLogsFormat(data);
  248. } else {
  249. let newLogs = [...logs];
  250. newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
  251. setLogsFormat(newLogs);
  252. }
  253. } else {
  254. showError(message);
  255. }
  256. setLoading(false);
  257. };
  258. const pageData = logs.slice(
  259. (activePage - 1) * ITEMS_PER_PAGE,
  260. activePage * ITEMS_PER_PAGE,
  261. );
  262. const handlePageChange = (page) => {
  263. setActivePage(page);
  264. if (page === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) {
  265. // In this case we have to load more data and then append them.
  266. loadLogs(page - 1).then((r) => {});
  267. }
  268. };
  269. const refresh = async () => {
  270. // setLoading(true);
  271. setActivePage(1);
  272. await loadLogs(0);
  273. };
  274. const copyText = async (text) => {
  275. if (await copy(text)) {
  276. showSuccess('已复制:' + text);
  277. } else {
  278. // setSearchKeyword(text);
  279. Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
  280. }
  281. };
  282. useEffect(() => {
  283. refresh().then();
  284. }, [logType]);
  285. const renderType = (type) => {
  286. switch (type) {
  287. case 'MUSIC':
  288. return (
  289. <Label basic color='grey'>
  290. {' '}
  291. 生成音乐{' '}
  292. </Label>
  293. );
  294. case 'LYRICS':
  295. return (
  296. <Label basic color='pink'>
  297. {' '}
  298. 生成歌词{' '}
  299. </Label>
  300. );
  301. default:
  302. return (
  303. <Label basic color='black'>
  304. {' '}
  305. 未知{' '}
  306. </Label>
  307. );
  308. }
  309. };
  310. const renderPlatform = (type) => {
  311. switch (type) {
  312. case 'suno':
  313. return (
  314. <Label basic color='green'>
  315. {' '}
  316. Suno{' '}
  317. </Label>
  318. );
  319. default:
  320. return (
  321. <Label basic color='black'>
  322. {' '}
  323. 未知{' '}
  324. </Label>
  325. );
  326. }
  327. };
  328. const renderStatus = (type) => {
  329. switch (type) {
  330. case 'SUCCESS':
  331. return (
  332. <Label basic color='green'>
  333. {' '}
  334. 成功{' '}
  335. </Label>
  336. );
  337. case 'NOT_START':
  338. return (
  339. <Label basic color='black'>
  340. {' '}
  341. 未启动{' '}
  342. </Label>
  343. );
  344. case 'SUBMITTED':
  345. return (
  346. <Label basic color='yellow'>
  347. {' '}
  348. 队列中{' '}
  349. </Label>
  350. );
  351. case 'IN_PROGRESS':
  352. return (
  353. <Label basic color='blue'>
  354. {' '}
  355. 执行中{' '}
  356. </Label>
  357. );
  358. case 'FAILURE':
  359. return (
  360. <Label basic color='red'>
  361. {' '}
  362. 失败{' '}
  363. </Label>
  364. );
  365. case 'QUEUED':
  366. return (
  367. <Label basic color='red'>
  368. {' '}
  369. 排队中{' '}
  370. </Label>
  371. );
  372. case 'UNKNOWN':
  373. return (
  374. <Label basic color='red'>
  375. {' '}
  376. 未知{' '}
  377. </Label>
  378. );
  379. case '':
  380. return (
  381. <Label basic color='black'>
  382. {' '}
  383. 正在提交{' '}
  384. </Label>
  385. );
  386. default:
  387. return (
  388. <Label basic color='black'>
  389. {' '}
  390. 未知{' '}
  391. </Label>
  392. );
  393. }
  394. };
  395. return (
  396. <>
  397. <Layout>
  398. <Form layout='horizontal' labelPosition='inset'>
  399. <>
  400. {isAdminUser && (
  401. <Form.Input
  402. field='channel_id'
  403. label='渠道 ID'
  404. style={{ width: '236px', marginBottom: '10px' }}
  405. value={channel_id}
  406. placeholder={'可选值'}
  407. name='channel_id'
  408. onChange={(value) => handleInputChange(value, 'channel_id')}
  409. />
  410. )}
  411. <Form.Input
  412. field='task_id'
  413. label={'任务 ID'}
  414. style={{ width: '236px', marginBottom: '10px' }}
  415. value={task_id}
  416. placeholder={'可选值'}
  417. name='task_id'
  418. onChange={(value) => handleInputChange(value, 'task_id')}
  419. />
  420. <Form.DatePicker
  421. field='start_timestamp'
  422. label={'起始时间'}
  423. style={{ width: '236px', marginBottom: '10px' }}
  424. initValue={start_timestamp}
  425. value={start_timestamp}
  426. type='dateTime'
  427. name='start_timestamp'
  428. onChange={(value) => handleInputChange(value, 'start_timestamp')}
  429. />
  430. <Form.DatePicker
  431. field='end_timestamp'
  432. fluid
  433. label={'结束时间'}
  434. style={{ width: '236px', marginBottom: '10px' }}
  435. initValue={end_timestamp}
  436. value={end_timestamp}
  437. type='dateTime'
  438. name='end_timestamp'
  439. onChange={(value) => handleInputChange(value, 'end_timestamp')}
  440. />
  441. <Button
  442. label={'查询'}
  443. type='primary'
  444. htmlType='submit'
  445. className='btn-margin-right'
  446. onClick={refresh}
  447. >
  448. 查询
  449. </Button>
  450. </>
  451. </Form>
  452. <Card>
  453. <Table
  454. columns={columns}
  455. dataSource={pageData}
  456. pagination={{
  457. currentPage: activePage,
  458. pageSize: ITEMS_PER_PAGE,
  459. total: logCount,
  460. pageSizeOpts: [10, 20, 50, 100],
  461. onPageChange: handlePageChange,
  462. }}
  463. loading={loading}
  464. />
  465. </Card>
  466. <Modal
  467. visible={isModalOpen}
  468. onOk={() => setIsModalOpen(false)}
  469. onCancel={() => setIsModalOpen(false)}
  470. closable={null}
  471. bodyStyle={{ height: '400px', overflow: 'auto' }} // 设置模态框内容区域样式
  472. width={800} // 设置模态框宽度
  473. >
  474. <p style={{ whiteSpace: 'pre-line' }}>{modalContent}</p>
  475. </Modal>
  476. </Layout>
  477. </>
  478. );
  479. };
  480. export default LogsTable;