TaskLogsColumnDefs.jsx 10 KB

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