TaskLogsColumnDefs.jsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  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, 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. const colors = [
  41. 'amber',
  42. 'blue',
  43. 'cyan',
  44. 'green',
  45. 'grey',
  46. 'indigo',
  47. 'light-blue',
  48. 'lime',
  49. 'orange',
  50. 'pink',
  51. 'purple',
  52. 'red',
  53. 'teal',
  54. 'violet',
  55. 'yellow',
  56. ];
  57. // Render functions
  58. const renderTimestamp = (timestampInSeconds) => {
  59. const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒
  60. const year = date.getFullYear(); // 获取年份
  61. const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份,从0开始需要+1,并保证两位数
  62. const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数
  63. const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数
  64. const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数
  65. const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数
  66. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出
  67. };
  68. function renderDuration(submit_time, finishTime) {
  69. if (!submit_time || !finishTime) return 'N/A';
  70. const durationSec = finishTime - submit_time;
  71. const color = durationSec > 60 ? 'red' : 'green';
  72. // 返回带有样式的颜色标签
  73. return (
  74. <Tag color={color} shape='circle' prefixIcon={<Clock size={14} />}>
  75. {durationSec} 秒
  76. </Tag>
  77. );
  78. }
  79. const renderType = (type, t) => {
  80. switch (type) {
  81. case 'MUSIC':
  82. return (
  83. <Tag color='grey' shape='circle' prefixIcon={<Music size={14} />}>
  84. {t('生成音乐')}
  85. </Tag>
  86. );
  87. case 'LYRICS':
  88. return (
  89. <Tag color='pink' shape='circle' prefixIcon={<FileText size={14} />}>
  90. {t('生成歌词')}
  91. </Tag>
  92. );
  93. case TASK_ACTION_GENERATE:
  94. return (
  95. <Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}>
  96. {t('图生视频')}
  97. </Tag>
  98. );
  99. case TASK_ACTION_TEXT_GENERATE:
  100. return (
  101. <Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}>
  102. {t('文生视频')}
  103. </Tag>
  104. );
  105. case TASK_ACTION_FIRST_TAIL_GENERATE:
  106. return (
  107. <Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}>
  108. {t('首尾生视频')}
  109. </Tag>
  110. );
  111. case TASK_ACTION_REFERENCE_GENERATE:
  112. return (
  113. <Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}>
  114. {t('参照生视频')}
  115. </Tag>
  116. );
  117. case TASK_ACTION_REMIX_GENERATE:
  118. return (
  119. <Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}>
  120. {t('视频Remix')}
  121. </Tag>
  122. );
  123. default:
  124. return (
  125. <Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
  126. {t('未知')}
  127. </Tag>
  128. );
  129. }
  130. };
  131. const renderPlatform = (platform, t) => {
  132. let option = CHANNEL_OPTIONS.find(
  133. (opt) => String(opt.value) === String(platform),
  134. );
  135. if (option) {
  136. return (
  137. <Tag color={option.color} shape='circle' prefixIcon={<Video size={14} />}>
  138. {option.label}
  139. </Tag>
  140. );
  141. }
  142. switch (platform) {
  143. case 'suno':
  144. return (
  145. <Tag color='green' shape='circle' prefixIcon={<Music size={14} />}>
  146. Suno
  147. </Tag>
  148. );
  149. default:
  150. return (
  151. <Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
  152. {t('未知')}
  153. </Tag>
  154. );
  155. }
  156. };
  157. const renderStatus = (type, t) => {
  158. switch (type) {
  159. case 'SUCCESS':
  160. return (
  161. <Tag
  162. color='green'
  163. shape='circle'
  164. prefixIcon={<CheckCircle size={14} />}
  165. >
  166. {t('成功')}
  167. </Tag>
  168. );
  169. case 'NOT_START':
  170. return (
  171. <Tag color='grey' shape='circle' prefixIcon={<Pause size={14} />}>
  172. {t('未启动')}
  173. </Tag>
  174. );
  175. case 'SUBMITTED':
  176. return (
  177. <Tag color='yellow' shape='circle' prefixIcon={<Clock size={14} />}>
  178. {t('队列中')}
  179. </Tag>
  180. );
  181. case 'IN_PROGRESS':
  182. return (
  183. <Tag color='blue' shape='circle' prefixIcon={<Play size={14} />}>
  184. {t('执行中')}
  185. </Tag>
  186. );
  187. case 'FAILURE':
  188. return (
  189. <Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
  190. {t('失败')}
  191. </Tag>
  192. );
  193. case 'QUEUED':
  194. return (
  195. <Tag color='orange' shape='circle' prefixIcon={<List size={14} />}>
  196. {t('排队中')}
  197. </Tag>
  198. );
  199. case 'UNKNOWN':
  200. return (
  201. <Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
  202. {t('未知')}
  203. </Tag>
  204. );
  205. case '':
  206. return (
  207. <Tag color='grey' shape='circle' prefixIcon={<Loader size={14} />}>
  208. {t('正在提交')}
  209. </Tag>
  210. );
  211. default:
  212. return (
  213. <Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
  214. {t('未知')}
  215. </Tag>
  216. );
  217. }
  218. };
  219. export const getTaskLogsColumns = ({
  220. t,
  221. COLUMN_KEYS,
  222. copyText,
  223. openContentModal,
  224. isAdminUser,
  225. openVideoModal,
  226. }) => {
  227. return [
  228. {
  229. key: COLUMN_KEYS.SUBMIT_TIME,
  230. title: t('提交时间'),
  231. dataIndex: 'submit_time',
  232. render: (text, record, index) => {
  233. return <div>{text ? renderTimestamp(text) : '-'}</div>;
  234. },
  235. },
  236. {
  237. key: COLUMN_KEYS.FINISH_TIME,
  238. title: t('结束时间'),
  239. dataIndex: 'finish_time',
  240. render: (text, record, index) => {
  241. return <div>{text ? renderTimestamp(text) : '-'}</div>;
  242. },
  243. },
  244. {
  245. key: COLUMN_KEYS.DURATION,
  246. title: t('花费时间'),
  247. dataIndex: 'finish_time',
  248. render: (finish, record) => {
  249. return <>{finish ? renderDuration(record.submit_time, finish) : '-'}</>;
  250. },
  251. },
  252. {
  253. key: COLUMN_KEYS.CHANNEL,
  254. title: t('渠道'),
  255. dataIndex: 'channel_id',
  256. render: (text, record, index) => {
  257. return isAdminUser ? (
  258. <div>
  259. <Tag
  260. color={colors[parseInt(text) % colors.length]}
  261. size='large'
  262. shape='circle'
  263. prefixIcon={<Hash size={14} />}
  264. onClick={() => {
  265. copyText(text);
  266. }}
  267. >
  268. {text}
  269. </Tag>
  270. </div>
  271. ) : (
  272. <></>
  273. );
  274. },
  275. },
  276. {
  277. key: COLUMN_KEYS.PLATFORM,
  278. title: t('平台'),
  279. dataIndex: 'platform',
  280. render: (text, record, index) => {
  281. return <div>{renderPlatform(text, t)}</div>;
  282. },
  283. },
  284. {
  285. key: COLUMN_KEYS.TYPE,
  286. title: t('类型'),
  287. dataIndex: 'action',
  288. render: (text, record, index) => {
  289. return <div>{renderType(text, t)}</div>;
  290. },
  291. },
  292. {
  293. key: COLUMN_KEYS.TASK_ID,
  294. title: t('任务ID'),
  295. dataIndex: 'task_id',
  296. render: (text, record, index) => {
  297. return (
  298. <Typography.Text
  299. ellipsis={{ showTooltip: true }}
  300. onClick={() => {
  301. openContentModal(JSON.stringify(record, null, 2));
  302. }}
  303. >
  304. <div>{text}</div>
  305. </Typography.Text>
  306. );
  307. },
  308. },
  309. {
  310. key: COLUMN_KEYS.TASK_STATUS,
  311. title: t('任务状态'),
  312. dataIndex: 'status',
  313. render: (text, record, index) => {
  314. return <div>{renderStatus(text, t)}</div>;
  315. },
  316. },
  317. {
  318. key: COLUMN_KEYS.PROGRESS,
  319. title: t('进度'),
  320. dataIndex: 'progress',
  321. render: (text, record, index) => {
  322. return (
  323. <div>
  324. {isNaN(text?.replace('%', '')) ? (
  325. text || '-'
  326. ) : (
  327. <Progress
  328. stroke={
  329. record.status === 'FAILURE'
  330. ? 'var(--semi-color-warning)'
  331. : null
  332. }
  333. percent={text ? parseInt(text.replace('%', '')) : 0}
  334. showInfo={true}
  335. aria-label='task progress'
  336. style={{ minWidth: '160px' }}
  337. />
  338. )}
  339. </div>
  340. );
  341. },
  342. },
  343. {
  344. key: COLUMN_KEYS.FAIL_REASON,
  345. title: t('详情'),
  346. dataIndex: 'fail_reason',
  347. fixed: 'right',
  348. render: (text, record, index) => {
  349. // 仅当为视频生成任务且成功,且 fail_reason 是 URL 时显示可点击链接
  350. const isVideoTask =
  351. record.action === TASK_ACTION_GENERATE ||
  352. record.action === TASK_ACTION_TEXT_GENERATE ||
  353. record.action === TASK_ACTION_FIRST_TAIL_GENERATE ||
  354. record.action === TASK_ACTION_REFERENCE_GENERATE ||
  355. record.action === TASK_ACTION_REMIX_GENERATE;
  356. const isSuccess = record.status === 'SUCCESS';
  357. const isUrl = typeof text === 'string' && /^https?:\/\//.test(text);
  358. if (isSuccess && isVideoTask && isUrl) {
  359. return (
  360. <a
  361. href='#'
  362. onClick={(e) => {
  363. e.preventDefault();
  364. openVideoModal(text);
  365. }}
  366. >
  367. {t('点击预览视频')}
  368. </a>
  369. );
  370. }
  371. if (!text) {
  372. return t('无');
  373. }
  374. return (
  375. <Typography.Text
  376. ellipsis={{ showTooltip: true }}
  377. style={{ width: 100 }}
  378. onClick={() => {
  379. openContentModal(text);
  380. }}
  381. >
  382. {text}
  383. </Typography.Text>
  384. );
  385. },
  386. },
  387. ];
  388. };