TaskLogsColumnDefs.jsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React from 'react';
  16. import { Progress, Tag, Tooltip, Typography } from '@douyinfe/semi-ui';
  17. import {
  18. Music,
  19. FileText,
  20. HelpCircle,
  21. CheckCircle,
  22. Pause,
  23. Clock,
  24. Play,
  25. XCircle,
  26. Loader,
  27. List,
  28. Hash,
  29. Video,
  30. Sparkles,
  31. } from 'lucide-react';
  32. import {
  33. TASK_ACTION_FIRST_TAIL_GENERATE,
  34. TASK_ACTION_GENERATE,
  35. TASK_ACTION_REFERENCE_GENERATE,
  36. TASK_ACTION_TEXT_GENERATE,
  37. TASK_ACTION_REMIX_GENERATE,
  38. } from '../../../constants/common.constant';
  39. import { CHANNEL_OPTIONS } from '../../../constants/channel.constants';
  40. import { stringToColor } from '../../../helpers/render';
  41. import { Avatar, Space } from '@douyinfe/semi-ui';
  42. const colors = [
  43. 'amber',
  44. 'blue',
  45. 'cyan',
  46. 'green',
  47. 'grey',
  48. 'indigo',
  49. 'light-blue',
  50. 'lime',
  51. 'orange',
  52. 'pink',
  53. 'purple',
  54. 'red',
  55. 'teal',
  56. 'violet',
  57. 'yellow',
  58. ];
  59. // Render functions
  60. const renderTimestamp = (timestampInSeconds) => {
  61. const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒
  62. const year = date.getFullYear(); // 获取年份
  63. const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份,从0开始需要+1,并保证两位数
  64. const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数
  65. const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数
  66. const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数
  67. const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数
  68. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出
  69. };
  70. function renderDuration(submit_time, finishTime) {
  71. if (!submit_time || !finishTime) return 'N/A';
  72. const durationSec = finishTime - submit_time;
  73. const color = durationSec > 60 ? 'red' : 'green';
  74. // 返回带有样式的颜色标签
  75. return (
  76. <Tag color={color} shape='circle'>
  77. {durationSec} s
  78. </Tag>
  79. );
  80. }
  81. const renderType = (type, t) => {
  82. switch (type) {
  83. case 'MUSIC':
  84. return (
  85. <Tag color='grey' shape='circle' prefixIcon={<Music size={14} />}>
  86. {t('生成音乐')}
  87. </Tag>
  88. );
  89. case 'LYRICS':
  90. return (
  91. <Tag color='pink' shape='circle' prefixIcon={<FileText size={14} />}>
  92. {t('生成歌词')}
  93. </Tag>
  94. );
  95. case TASK_ACTION_GENERATE:
  96. return (
  97. <Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}>
  98. {t('图生视频')}
  99. </Tag>
  100. );
  101. case TASK_ACTION_TEXT_GENERATE:
  102. return (
  103. <Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}>
  104. {t('文生视频')}
  105. </Tag>
  106. );
  107. case TASK_ACTION_FIRST_TAIL_GENERATE:
  108. return (
  109. <Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}>
  110. {t('首尾生视频')}
  111. </Tag>
  112. );
  113. case TASK_ACTION_REFERENCE_GENERATE:
  114. return (
  115. <Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}>
  116. {t('参照生视频')}
  117. </Tag>
  118. );
  119. case TASK_ACTION_REMIX_GENERATE:
  120. return (
  121. <Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}>
  122. {t('视频Remix')}
  123. </Tag>
  124. );
  125. default:
  126. return (
  127. <Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
  128. {t('未知')}
  129. </Tag>
  130. );
  131. }
  132. };
  133. const renderPlatform = (platform, t) => {
  134. let option = CHANNEL_OPTIONS.find(
  135. (opt) => String(opt.value) === String(platform),
  136. );
  137. if (option) {
  138. return (
  139. <Tag color={option.color} shape='circle'>
  140. {option.label}
  141. </Tag>
  142. );
  143. }
  144. switch (platform) {
  145. case 'suno':
  146. return (
  147. <Tag color='green' shape='circle'>
  148. Suno
  149. </Tag>
  150. );
  151. default:
  152. return (
  153. <Tag color='white' shape='circle'>
  154. {t('未知')}
  155. </Tag>
  156. );
  157. }
  158. };
  159. const renderStatus = (type, t) => {
  160. switch (type) {
  161. case 'SUCCESS':
  162. return (
  163. <Tag
  164. color='green'
  165. shape='circle'
  166. prefixIcon={<CheckCircle size={14} />}
  167. >
  168. {t('成功')}
  169. </Tag>
  170. );
  171. case 'NOT_START':
  172. return (
  173. <Tag color='grey' shape='circle' prefixIcon={<Pause size={14} />}>
  174. {t('未启动')}
  175. </Tag>
  176. );
  177. case 'SUBMITTED':
  178. return (
  179. <Tag color='yellow' shape='circle' prefixIcon={<Clock size={14} />}>
  180. {t('队列中')}
  181. </Tag>
  182. );
  183. case 'IN_PROGRESS':
  184. return (
  185. <Tag color='blue' shape='circle' prefixIcon={<Play size={14} />}>
  186. {t('执行中')}
  187. </Tag>
  188. );
  189. case 'FAILURE':
  190. return (
  191. <Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
  192. {t('失败')}
  193. </Tag>
  194. );
  195. case 'QUEUED':
  196. return (
  197. <Tag color='orange' shape='circle' prefixIcon={<List size={14} />}>
  198. {t('排队中')}
  199. </Tag>
  200. );
  201. case 'UNKNOWN':
  202. return (
  203. <Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
  204. {t('未知')}
  205. </Tag>
  206. );
  207. case '':
  208. return (
  209. <Tag color='grey' shape='circle' prefixIcon={<Loader size={14} />}>
  210. {t('正在提交')}
  211. </Tag>
  212. );
  213. default:
  214. return (
  215. <Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
  216. {t('未知')}
  217. </Tag>
  218. );
  219. }
  220. };
  221. export const getTaskLogsColumns = ({
  222. t,
  223. COLUMN_KEYS,
  224. copyText,
  225. openContentModal,
  226. isAdminUser,
  227. openVideoModal,
  228. }) => {
  229. return [
  230. {
  231. key: COLUMN_KEYS.SUBMIT_TIME,
  232. title: t('提交时间'),
  233. dataIndex: 'submit_time',
  234. render: (text, record, index) => {
  235. return <div>{text ? renderTimestamp(text) : '-'}</div>;
  236. },
  237. },
  238. {
  239. key: COLUMN_KEYS.FINISH_TIME,
  240. title: t('结束时间'),
  241. dataIndex: 'finish_time',
  242. render: (text, record, index) => {
  243. return <div>{text ? renderTimestamp(text) : '-'}</div>;
  244. },
  245. },
  246. {
  247. key: COLUMN_KEYS.DURATION,
  248. title: t('花费时间'),
  249. dataIndex: 'finish_time',
  250. render: (finish, record) => {
  251. return <>{finish ? renderDuration(record.submit_time, finish) : '-'}</>;
  252. },
  253. },
  254. {
  255. key: COLUMN_KEYS.CHANNEL,
  256. title: t('渠道'),
  257. dataIndex: 'channel_id',
  258. render: (text, record, index) => {
  259. return isAdminUser ? (
  260. <div>
  261. <Tag
  262. color={colors[parseInt(text) % colors.length]}
  263. size='large'
  264. shape='circle'
  265. onClick={() => {
  266. copyText(text);
  267. }}
  268. >
  269. {text}
  270. </Tag>
  271. </div>
  272. ) : (
  273. <></>
  274. );
  275. },
  276. },
  277. {
  278. key: COLUMN_KEYS.USERNAME,
  279. title: t('用户'),
  280. dataIndex: 'username',
  281. render: (userId, record, index) => {
  282. if (!isAdminUser) {
  283. return <></>;
  284. }
  285. const displayText = String(record.username || userId || '?');
  286. return (
  287. <Space>
  288. <Avatar
  289. size='extra-small'
  290. color={stringToColor(displayText)}
  291. >
  292. {displayText.slice(0, 1)}
  293. </Avatar>
  294. <Typography.Text>
  295. {displayText}
  296. </Typography.Text>
  297. </Space>
  298. );
  299. },
  300. },
  301. {
  302. key: COLUMN_KEYS.PLATFORM,
  303. title: t('平台'),
  304. dataIndex: 'platform',
  305. render: (text, record, index) => {
  306. return <div>{renderPlatform(text, t)}</div>;
  307. },
  308. },
  309. {
  310. key: COLUMN_KEYS.TYPE,
  311. title: t('类型'),
  312. dataIndex: 'action',
  313. render: (text, record, index) => {
  314. return <div>{renderType(text, t)}</div>;
  315. },
  316. },
  317. {
  318. key: COLUMN_KEYS.TASK_ID,
  319. title: t('任务ID'),
  320. dataIndex: 'task_id',
  321. render: (text, record, index) => {
  322. return (
  323. <Typography.Text
  324. ellipsis={{ showTooltip: true }}
  325. onClick={() => {
  326. openContentModal(JSON.stringify(record, null, 2));
  327. }}
  328. >
  329. <div>{text}</div>
  330. </Typography.Text>
  331. );
  332. },
  333. },
  334. {
  335. key: COLUMN_KEYS.TASK_STATUS,
  336. title: t('任务状态'),
  337. dataIndex: 'status',
  338. render: (text, record, index) => {
  339. return <div>{renderStatus(text, t)}</div>;
  340. },
  341. },
  342. {
  343. key: COLUMN_KEYS.PROGRESS,
  344. title: t('进度'),
  345. dataIndex: 'progress',
  346. render: (text, record, index) => {
  347. return (
  348. <div>
  349. {isNaN(text?.replace('%', '')) ? (
  350. text || '-'
  351. ) : (
  352. <Progress
  353. stroke={
  354. record.status === 'FAILURE'
  355. ? 'var(--semi-color-warning)'
  356. : null
  357. }
  358. percent={text ? parseInt(text.replace('%', '')) : 0}
  359. showInfo={true}
  360. aria-label='task progress'
  361. style={{ minWidth: '160px' }}
  362. />
  363. )}
  364. </div>
  365. );
  366. },
  367. },
  368. {
  369. key: COLUMN_KEYS.FAIL_REASON,
  370. title: t('详情'),
  371. dataIndex: 'fail_reason',
  372. fixed: 'right',
  373. render: (text, record, index) => {
  374. // 视频预览:优先使用 result_url,兼容旧数据 fail_reason 中的 URL
  375. const isVideoTask =
  376. record.action === TASK_ACTION_GENERATE ||
  377. record.action === TASK_ACTION_TEXT_GENERATE ||
  378. record.action === TASK_ACTION_FIRST_TAIL_GENERATE ||
  379. record.action === TASK_ACTION_REFERENCE_GENERATE ||
  380. record.action === TASK_ACTION_REMIX_GENERATE;
  381. const isSuccess = record.status === 'SUCCESS';
  382. const resultUrl = record.result_url;
  383. const hasResultUrl = typeof resultUrl === 'string' && /^https?:\/\//.test(resultUrl);
  384. if (isSuccess && isVideoTask && hasResultUrl) {
  385. return (
  386. <a
  387. href='#'
  388. onClick={(e) => {
  389. e.preventDefault();
  390. openVideoModal(resultUrl);
  391. }}
  392. >
  393. {t('点击预览视频')}
  394. </a>
  395. );
  396. }
  397. if (!text) {
  398. return t('无');
  399. }
  400. return (
  401. <Typography.Text
  402. ellipsis={{ showTooltip: true }}
  403. style={{ width: 100 }}
  404. onClick={() => {
  405. openContentModal(text);
  406. }}
  407. >
  408. {text}
  409. </Typography.Text>
  410. );
  411. },
  412. },
  413. ];
  414. };