| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| |
|
| | 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,
|
| | TASK_ACTION_REMIX_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>
|
| | );
|
| | case TASK_ACTION_REMIX_GENERATE:
|
| | return (
|
| | <Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}>
|
| | {t('视频Remix')}
|
| | </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 ||
|
| | record.action === TASK_ACTION_REMIX_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>
|
| | );
|
| | },
|
| | },
|
| | ];
|
| | };
|
| |
|