TaskLogsColumnDefs.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  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. 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. default:
  104. return (
  105. <Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
  106. {t('未知')}
  107. </Tag>
  108. );
  109. }
  110. };
  111. const renderPlatform = (platform, t) => {
  112. let option = CHANNEL_OPTIONS.find(opt => String(opt.value) === String(platform));
  113. if (option) {
  114. return (
  115. <Tag color={option.color} shape='circle' prefixIcon={<Video size={14} />}>
  116. {option.label}
  117. </Tag>
  118. );
  119. }
  120. switch (platform) {
  121. case 'suno':
  122. return (
  123. <Tag color='green' shape='circle' prefixIcon={<Music size={14} />}>
  124. Suno
  125. </Tag>
  126. );
  127. default:
  128. return (
  129. <Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
  130. {t('未知')}
  131. </Tag>
  132. );
  133. }
  134. };
  135. const renderStatus = (type, t) => {
  136. switch (type) {
  137. case 'SUCCESS':
  138. return (
  139. <Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
  140. {t('成功')}
  141. </Tag>
  142. );
  143. case 'NOT_START':
  144. return (
  145. <Tag color='grey' shape='circle' prefixIcon={<Pause size={14} />}>
  146. {t('未启动')}
  147. </Tag>
  148. );
  149. case 'SUBMITTED':
  150. return (
  151. <Tag color='yellow' shape='circle' prefixIcon={<Clock size={14} />}>
  152. {t('队列中')}
  153. </Tag>
  154. );
  155. case 'IN_PROGRESS':
  156. return (
  157. <Tag color='blue' shape='circle' prefixIcon={<Play size={14} />}>
  158. {t('执行中')}
  159. </Tag>
  160. );
  161. case 'FAILURE':
  162. return (
  163. <Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
  164. {t('失败')}
  165. </Tag>
  166. );
  167. case 'QUEUED':
  168. return (
  169. <Tag color='orange' shape='circle' prefixIcon={<List size={14} />}>
  170. {t('排队中')}
  171. </Tag>
  172. );
  173. case 'UNKNOWN':
  174. return (
  175. <Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
  176. {t('未知')}
  177. </Tag>
  178. );
  179. case '':
  180. return (
  181. <Tag color='grey' shape='circle' prefixIcon={<Loader size={14} />}>
  182. {t('正在提交')}
  183. </Tag>
  184. );
  185. default:
  186. return (
  187. <Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
  188. {t('未知')}
  189. </Tag>
  190. );
  191. }
  192. };
  193. export const getTaskLogsColumns = ({
  194. t,
  195. COLUMN_KEYS,
  196. copyText,
  197. openContentModal,
  198. isAdminUser,
  199. openVideoModal,
  200. }) => {
  201. return [
  202. {
  203. key: COLUMN_KEYS.SUBMIT_TIME,
  204. title: t('提交时间'),
  205. dataIndex: 'submit_time',
  206. render: (text, record, index) => {
  207. return <div>{text ? renderTimestamp(text) : '-'}</div>;
  208. },
  209. },
  210. {
  211. key: COLUMN_KEYS.FINISH_TIME,
  212. title: t('结束时间'),
  213. dataIndex: 'finish_time',
  214. render: (text, record, index) => {
  215. return <div>{text ? renderTimestamp(text) : '-'}</div>;
  216. },
  217. },
  218. {
  219. key: COLUMN_KEYS.DURATION,
  220. title: t('花费时间'),
  221. dataIndex: 'finish_time',
  222. render: (finish, record) => {
  223. return <>{finish ? renderDuration(record.submit_time, finish) : '-'}</>;
  224. },
  225. },
  226. {
  227. key: COLUMN_KEYS.CHANNEL,
  228. title: t('渠道'),
  229. dataIndex: 'channel_id',
  230. render: (text, record, index) => {
  231. return isAdminUser ? (
  232. <div>
  233. <Tag
  234. color={colors[parseInt(text) % colors.length]}
  235. size='large'
  236. shape='circle'
  237. prefixIcon={<Hash size={14} />}
  238. onClick={() => {
  239. copyText(text);
  240. }}
  241. >
  242. {text}
  243. </Tag>
  244. </div>
  245. ) : (
  246. <></>
  247. );
  248. },
  249. },
  250. {
  251. key: COLUMN_KEYS.PLATFORM,
  252. title: t('平台'),
  253. dataIndex: 'platform',
  254. render: (text, record, index) => {
  255. return <div>{renderPlatform(text, t)}</div>;
  256. },
  257. },
  258. {
  259. key: COLUMN_KEYS.TYPE,
  260. title: t('类型'),
  261. dataIndex: 'action',
  262. render: (text, record, index) => {
  263. return <div>{renderType(text, t)}</div>;
  264. },
  265. },
  266. {
  267. key: COLUMN_KEYS.TASK_ID,
  268. title: t('任务ID'),
  269. dataIndex: 'task_id',
  270. render: (text, record, index) => {
  271. return (
  272. <Typography.Text
  273. ellipsis={{ showTooltip: true }}
  274. onClick={() => {
  275. openContentModal(JSON.stringify(record, null, 2));
  276. }}
  277. >
  278. <div>{text}</div>
  279. </Typography.Text>
  280. );
  281. },
  282. },
  283. {
  284. key: COLUMN_KEYS.TASK_STATUS,
  285. title: t('任务状态'),
  286. dataIndex: 'status',
  287. render: (text, record, index) => {
  288. return <div>{renderStatus(text, t)}</div>;
  289. },
  290. },
  291. {
  292. key: COLUMN_KEYS.PROGRESS,
  293. title: t('进度'),
  294. dataIndex: 'progress',
  295. render: (text, record, index) => {
  296. return (
  297. <div>
  298. {
  299. isNaN(text?.replace('%', '')) ? (
  300. text || '-'
  301. ) : (
  302. <Progress
  303. stroke={
  304. record.status === 'FAILURE'
  305. ? 'var(--semi-color-warning)'
  306. : null
  307. }
  308. percent={text ? parseInt(text.replace('%', '')) : 0}
  309. showInfo={true}
  310. aria-label='task progress'
  311. style={{ minWidth: '160px' }}
  312. />
  313. )
  314. }
  315. </div>
  316. );
  317. },
  318. },
  319. {
  320. key: COLUMN_KEYS.FAIL_REASON,
  321. title: t('详情'),
  322. dataIndex: 'fail_reason',
  323. fixed: 'right',
  324. render: (text, record, index) => {
  325. // 仅当为视频生成任务且成功,且 fail_reason 是 URL 时显示可点击链接
  326. const isVideoTask = record.action === TASK_ACTION_GENERATE || record.action === TASK_ACTION_TEXT_GENERATE;
  327. const isSuccess = record.status === 'SUCCESS';
  328. const isUrl = typeof text === 'string' && /^https?:\/\//.test(text);
  329. if (isSuccess && isVideoTask && isUrl) {
  330. return (
  331. <a
  332. href="#"
  333. onClick={e => {
  334. e.preventDefault();
  335. openVideoModal(text);
  336. }}
  337. >
  338. {t('点击预览视频')}
  339. </a>
  340. );
  341. }
  342. if (!text) {
  343. return t('无');
  344. }
  345. return (
  346. <Typography.Text
  347. ellipsis={{ showTooltip: true }}
  348. style={{ width: 100 }}
  349. onClick={() => {
  350. openContentModal(text);
  351. }}
  352. >
  353. {text}
  354. </Typography.Text>
  355. );
  356. },
  357. },
  358. ];
  359. };