TaskLogsColumnDefs.js 9.6 KB

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