| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import React from 'react'; |
| import { Progress, Tag, Typography } from '@douyinfe/semi-ui'; |
| import { |
| Music, |
| FileText, |
| HelpCircle, |
| CheckCircle, |
| Pause, |
| Clock, |
| Play, |
| XCircle, |
| Loader, |
| List, |
| Hash, |
| Video, |
| Sparkles, |
| } from 'lucide-react'; |
| import { |
| TASK_ACTION_FIRST_TAIL_GENERATE, |
| TASK_ACTION_GENERATE, |
| TASK_ACTION_REFERENCE_GENERATE, |
| TASK_ACTION_TEXT_GENERATE, |
| } from '../../../constants/common.constant'; |
| import { CHANNEL_OPTIONS } from '../../../constants/channel.constants'; |
|
|
| const colors = [ |
| 'amber', |
| 'blue', |
| 'cyan', |
| 'green', |
| 'grey', |
| 'indigo', |
| 'light-blue', |
| 'lime', |
| 'orange', |
| 'pink', |
| 'purple', |
| 'red', |
| 'teal', |
| 'violet', |
| 'yellow', |
| ]; |
|
|
| |
| const renderTimestamp = (timestampInSeconds) => { |
| const date = new Date(timestampInSeconds * 1000); |
|
|
| const year = date.getFullYear(); |
| const month = ('0' + (date.getMonth() + 1)).slice(-2); |
| const day = ('0' + date.getDate()).slice(-2); |
| const hours = ('0' + date.getHours()).slice(-2); |
| const minutes = ('0' + date.getMinutes()).slice(-2); |
| const seconds = ('0' + date.getSeconds()).slice(-2); |
|
|
| return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; |
| }; |
|
|
| function renderDuration(submit_time, finishTime) { |
| if (!submit_time || !finishTime) return 'N/A'; |
| const durationSec = finishTime - submit_time; |
| const color = durationSec > 60 ? 'red' : 'green'; |
|
|
| |
| return ( |
| <Tag color={color} shape='circle' prefixIcon={<Clock size={14} />}> |
| {durationSec} 秒 |
| </Tag> |
| ); |
| } |
|
|
| const renderType = (type, t) => { |
| switch (type) { |
| case 'MUSIC': |
| return ( |
| <Tag color='grey' shape='circle' prefixIcon={<Music size={14} />}> |
| {t('生成音乐')} |
| </Tag> |
| ); |
| case 'LYRICS': |
| return ( |
| <Tag color='pink' shape='circle' prefixIcon={<FileText size={14} />}> |
| {t('生成歌词')} |
| </Tag> |
| ); |
| case TASK_ACTION_GENERATE: |
| return ( |
| <Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}> |
| {t('图生视频')} |
| </Tag> |
| ); |
| case TASK_ACTION_TEXT_GENERATE: |
| return ( |
| <Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}> |
| {t('文生视频')} |
| </Tag> |
| ); |
| case TASK_ACTION_FIRST_TAIL_GENERATE: |
| return ( |
| <Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}> |
| {t('首尾生视频')} |
| </Tag> |
| ); |
| case TASK_ACTION_REFERENCE_GENERATE: |
| return ( |
| <Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}> |
| {t('参照生视频')} |
| </Tag> |
| ); |
| default: |
| return ( |
| <Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}> |
| {t('未知')} |
| </Tag> |
| ); |
| } |
| }; |
|
|
| const renderPlatform = (platform, t) => { |
| let option = CHANNEL_OPTIONS.find( |
| (opt) => String(opt.value) === String(platform), |
| ); |
| if (option) { |
| return ( |
| <Tag color={option.color} shape='circle' prefixIcon={<Video size={14} />}> |
| {option.label} |
| </Tag> |
| ); |
| } |
| switch (platform) { |
| case 'suno': |
| return ( |
| <Tag color='green' shape='circle' prefixIcon={<Music size={14} />}> |
| Suno |
| </Tag> |
| ); |
| default: |
| return ( |
| <Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}> |
| {t('未知')} |
| </Tag> |
| ); |
| } |
| }; |
|
|
| const renderStatus = (type, t) => { |
| switch (type) { |
| case 'SUCCESS': |
| return ( |
| <Tag |
| color='green' |
| shape='circle' |
| prefixIcon={<CheckCircle size={14} />} |
| > |
| {t('成功')} |
| </Tag> |
| ); |
| case 'NOT_START': |
| return ( |
| <Tag color='grey' shape='circle' prefixIcon={<Pause size={14} />}> |
| {t('未启动')} |
| </Tag> |
| ); |
| case 'SUBMITTED': |
| return ( |
| <Tag color='yellow' shape='circle' prefixIcon={<Clock size={14} />}> |
| {t('队列中')} |
| </Tag> |
| ); |
| case 'IN_PROGRESS': |
| return ( |
| <Tag color='blue' shape='circle' prefixIcon={<Play size={14} />}> |
| {t('执行中')} |
| </Tag> |
| ); |
| case 'FAILURE': |
| return ( |
| <Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}> |
| {t('失败')} |
| </Tag> |
| ); |
| case 'QUEUED': |
| return ( |
| <Tag color='orange' shape='circle' prefixIcon={<List size={14} />}> |
| {t('排队中')} |
| </Tag> |
| ); |
| case 'UNKNOWN': |
| return ( |
| <Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}> |
| {t('未知')} |
| </Tag> |
| ); |
| case '': |
| return ( |
| <Tag color='grey' shape='circle' prefixIcon={<Loader size={14} />}> |
| {t('正在提交')} |
| </Tag> |
| ); |
| default: |
| return ( |
| <Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}> |
| {t('未知')} |
| </Tag> |
| ); |
| } |
| }; |
|
|
| export const getTaskLogsColumns = ({ |
| t, |
| COLUMN_KEYS, |
| copyText, |
| openContentModal, |
| isAdminUser, |
| openVideoModal, |
| }) => { |
| return [ |
| { |
| key: COLUMN_KEYS.SUBMIT_TIME, |
| title: t('提交时间'), |
| dataIndex: 'submit_time', |
| render: (text, record, index) => { |
| return <div>{text ? renderTimestamp(text) : '-'}</div>; |
| }, |
| }, |
| { |
| key: COLUMN_KEYS.FINISH_TIME, |
| title: t('结束时间'), |
| dataIndex: 'finish_time', |
| render: (text, record, index) => { |
| return <div>{text ? renderTimestamp(text) : '-'}</div>; |
| }, |
| }, |
| { |
| key: COLUMN_KEYS.DURATION, |
| title: t('花费时间'), |
| dataIndex: 'finish_time', |
| render: (finish, record) => { |
| return <>{finish ? renderDuration(record.submit_time, finish) : '-'}</>; |
| }, |
| }, |
| { |
| key: COLUMN_KEYS.CHANNEL, |
| title: t('渠道'), |
| dataIndex: 'channel_id', |
| render: (text, record, index) => { |
| return isAdminUser ? ( |
| <div> |
| <Tag |
| color={colors[parseInt(text) % colors.length]} |
| size='large' |
| shape='circle' |
| prefixIcon={<Hash size={14} />} |
| onClick={() => { |
| copyText(text); |
| }} |
| > |
| {text} |
| </Tag> |
| </div> |
| ) : ( |
| <></> |
| ); |
| }, |
| }, |
| { |
| key: COLUMN_KEYS.PLATFORM, |
| title: t('平台'), |
| dataIndex: 'platform', |
| render: (text, record, index) => { |
| return <div>{renderPlatform(text, t)}</div>; |
| }, |
| }, |
| { |
| key: COLUMN_KEYS.TYPE, |
| title: t('类型'), |
| dataIndex: 'action', |
| render: (text, record, index) => { |
| return <div>{renderType(text, t)}</div>; |
| }, |
| }, |
| { |
| key: COLUMN_KEYS.TASK_ID, |
| title: t('任务ID'), |
| dataIndex: 'task_id', |
| render: (text, record, index) => { |
| return ( |
| <Typography.Text |
| ellipsis={{ showTooltip: true }} |
| onClick={() => { |
| openContentModal(JSON.stringify(record, null, 2)); |
| }} |
| > |
| <div>{text}</div> |
| </Typography.Text> |
| ); |
| }, |
| }, |
| { |
| key: COLUMN_KEYS.TASK_STATUS, |
| title: t('任务状态'), |
| dataIndex: 'status', |
| render: (text, record, index) => { |
| return <div>{renderStatus(text, t)}</div>; |
| }, |
| }, |
| { |
| key: COLUMN_KEYS.PROGRESS, |
| title: t('进度'), |
| dataIndex: 'progress', |
| render: (text, record, index) => { |
| return ( |
| <div> |
| {isNaN(text?.replace('%', '')) ? ( |
| text || '-' |
| ) : ( |
| <Progress |
| stroke={ |
| record.status === 'FAILURE' |
| ? 'var(--semi-color-warning)' |
| : null |
| } |
| percent={text ? parseInt(text.replace('%', '')) : 0} |
| showInfo={true} |
| aria-label='task progress' |
| style={{ minWidth: '160px' }} |
| /> |
| )} |
| </div> |
| ); |
| }, |
| }, |
| { |
| key: COLUMN_KEYS.FAIL_REASON, |
| title: t('详情'), |
| dataIndex: 'fail_reason', |
| fixed: 'right', |
| render: (text, record, index) => { |
| |
| const isVideoTask = |
| record.action === TASK_ACTION_GENERATE || |
| record.action === TASK_ACTION_TEXT_GENERATE || |
| record.action === TASK_ACTION_FIRST_TAIL_GENERATE || |
| record.action === TASK_ACTION_REFERENCE_GENERATE; |
| const isSuccess = record.status === 'SUCCESS'; |
| const isUrl = typeof text === 'string' && /^https?:\/\//.test(text); |
| if (isSuccess && isVideoTask && isUrl) { |
| return ( |
| <a |
| href='#' |
| onClick={(e) => { |
| e.preventDefault(); |
| openVideoModal(text); |
| }} |
| > |
| {t('点击预览视频')} |
| </a> |
| ); |
| } |
| if (!text) { |
| return t('无'); |
| } |
| return ( |
| <Typography.Text |
| ellipsis={{ showTooltip: true }} |
| style={{ width: 100 }} |
| onClick={() => { |
| openContentModal(text); |
| }} |
| > |
| {text} |
| </Typography.Text> |
| ); |
| }, |
| }, |
| ]; |
| }; |
|
|