|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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> |
|
|
); |
|
|
}, |
|
|
}, |
|
|
]; |
|
|
}; |
|
|
|