TaskLogsTable.js 14 KB

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