feat: 失败的视频重新生成
This commit is contained in:
parent
1db6a1e19c
commit
a3643ee9e5
@ -9,8 +9,8 @@
|
||||
"dev-test": "vite --host --mode=test",
|
||||
"dev-lang-en": "set APP_LANGUAGE=en-US && vite --host --mode=lang-en",
|
||||
"build": "tsc && vite build",
|
||||
"build-zh": "cross-env APP_LANGUAGE=zh-CN tsc && vite build --mode=production",
|
||||
"build-en": "cross-env APP_LANGUAGE=en-US vite build --mode=production",
|
||||
"zh": "cross-env APP_LANGUAGE=zh-CN tsc && vite build --mode=production",
|
||||
"en": "cross-env APP_LANGUAGE=en-US vite build --mode=production",
|
||||
"build-test": "tsc && vite build --mode=test",
|
||||
"build-relative": "tsc && vite build --mode=relative",
|
||||
"build-prod": "tsc && vite build --mode=production",
|
||||
|
@ -160,6 +160,12 @@
|
||||
&.disabled{
|
||||
@apply border-primary-blue bg-[#f4f7fc];
|
||||
}
|
||||
&.status-generating{
|
||||
background: rgba(209, 209, 209, 1);
|
||||
}
|
||||
&.status-generate-failed{
|
||||
background: rgba(255, 0, 0, 0.12);
|
||||
}
|
||||
&.header-row{
|
||||
@apply text-sm;
|
||||
background: none;
|
||||
@ -179,7 +185,7 @@
|
||||
|
||||
&:after {
|
||||
@apply absolute;
|
||||
border-right: solid 1px #e8e8e8;
|
||||
border-right: solid 1px rgba(0,0,0,0.1);
|
||||
content: ' ';
|
||||
top: 2px;
|
||||
bottom: 2px;
|
||||
@ -211,8 +217,8 @@
|
||||
}
|
||||
|
||||
.operation {
|
||||
@apply flex items-center ml-2 gap-4 text-lg text-gray-400 justify-center;
|
||||
width: 120px;
|
||||
@apply flex items-center ml-2 text-lg text-gray-400 justify-center;
|
||||
width: 150px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
@ -168,6 +168,25 @@ export const IconPlaying = ({style, className}: IconProps) => (
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
)
|
||||
export const IconGenerating = ({style, className}: IconProps) => (
|
||||
<svg className={`svg-icon ${className || ''} icon-delete`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em" height="1em" viewBox="0 0 20 20" version="1.1">
|
||||
<path d="M3.463 2.43301C5.27751 0.860592 7.59897 -0.00342947 10 1.02307e-05C15.523 1.02307e-05 20 4.47701 20 10C20 12.136 19.33 14.116 18.19 15.74L15 10H18C18.0001 8.43163 17.5392 6.89781 16.6747 5.58927C15.8101 4.28072 14.5799 3.25517 13.1372 2.64013C11.6944 2.0251 10.1027 1.84771 8.55996 2.13003C7.0172 2.41234 5.59145 3.14191 4.46 4.22801L3.463 2.43301ZM16.537 17.567C14.7225 19.1394 12.401 20.0034 10 20C4.477 20 0 15.523 0 10C0 7.86401 0.67 5.88401 1.81 4.26001L5 10H2C1.99987 11.5684 2.46075 13.1022 3.32534 14.4108C4.18992 15.7193 5.42007 16.7449 6.86282 17.3599C8.30557 17.9749 9.89729 18.1523 11.44 17.87C12.9828 17.5877 14.4085 16.8581 15.54 15.772L16.537 17.567Z" fill="white"/>
|
||||
</svg>
|
||||
)
|
||||
export const IconGenerateFailed = ({style, className}: IconProps) => (
|
||||
<svg className={`svg-icon ${className || ''} icon-delete`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em" height="1em" viewBox="0 0 20 20" version="1.1">
|
||||
<path d="M18 0H2C0.9 0 0.00999999 0.9 0.00999999 2L0 20L4 16H18C19.1 16 20 15.1 20 14V2C20 0.9 19.1 0 18 0ZM11 12H9V10H11V12ZM11 8H9V4H11V8Z" fill="#FFA800"/>
|
||||
</svg>
|
||||
)
|
||||
export const IconRegenerate = ({style, className}: IconProps) => (
|
||||
<svg className={`svg-icon ${className || ''} icon-delete`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em" height="1em" viewBox="0 0 24 24" version="1.1">
|
||||
<path d="M20.4728 3.525C19.3618 2.4074 18.0406 1.52056 16.5851 0.915578C15.1297 0.310592 13.5688 -0.000577199 11.9925 8.03759e-07C5.35835 8.03759e-07 0 5.37 0 12C0 18.63 5.35835 24 11.9925 24C17.591 24 22.2589 20.175 23.5947 15H20.4728C19.8545 16.7543 18.7067 18.2736 17.1878 19.3483C15.6688 20.4229 13.8536 21.0001 11.9925 21C7.02439 21 2.98687 16.965 2.98687 12C2.98687 7.035 7.02439 3 11.9925 3C14.4841 3 16.7054 4.035 18.3265 5.67L13.4934 10.5H24V8.03759e-07L20.4728 3.525Z" fill="currentColor"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
|
||||
export const IconPlay = ({style, className}: IconProps) => (
|
||||
<svg className={`svg-icon ${className || ''} icon-delete`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
|
@ -12,7 +12,7 @@ export function showToast(content: string, type?: 'success' | 'info' | 'warning'
|
||||
}
|
||||
|
||||
export function showErrorToast(e: Error | BizError) {
|
||||
showToast(String(((e instanceof BizError) ? e.data : '') || e.message), 'error')
|
||||
showToast(String(e.message), 'error')
|
||||
}
|
||||
|
||||
|
||||
|
@ -4,13 +4,21 @@ import React, {useEffect} from "react";
|
||||
import {Checkbox, Popconfirm} from "antd";
|
||||
|
||||
import ImageCover from '@/assets/images/cover.png'
|
||||
import {IconDelete, IconEdit, IconPlaying, IconWarningCircle} from "@/components/icons";
|
||||
import {
|
||||
IconDelete,
|
||||
IconEdit,
|
||||
IconGenerateFailed,
|
||||
IconGenerating,
|
||||
IconPlaying, IconRegenerate,
|
||||
IconWarningCircle
|
||||
} from "@/components/icons";
|
||||
import {VideoStatus} from "@/service/api/video.ts";
|
||||
import {formatTime} from "@/util/strings.ts";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
video: VideoInfo | LiveVideoInfo,
|
||||
additionOperation?: React.ReactNode;
|
||||
editable?: boolean;
|
||||
sortable?: boolean;
|
||||
index?: number;
|
||||
@ -20,6 +28,8 @@ type Props = {
|
||||
onCheckedChange?: (checked: boolean) => void;
|
||||
onPlay?: () => void;
|
||||
onEdit?: () => void;
|
||||
onRegenerate?: () => void;
|
||||
hideCheckBox?: boolean;
|
||||
onItemClick?: () => void;
|
||||
onRemove?: () => void;
|
||||
id: number;
|
||||
@ -31,7 +41,8 @@ export const VideoListItem = (
|
||||
{
|
||||
id, video, onRemove, checked,playing,
|
||||
onCheckedChange, onEdit, active, editable,
|
||||
className, sortable, type, index,onItemClick
|
||||
className, sortable, type, index,onItemClick,
|
||||
additionOperation,onRegenerate,hideCheckBox
|
||||
}: Props) => {
|
||||
const {
|
||||
attributes, listeners,
|
||||
@ -44,13 +55,14 @@ export const VideoListItem = (
|
||||
setState({checked})
|
||||
}, [checked])
|
||||
|
||||
const generating = (type == 'create' && video.status == VideoStatus.Generating )
|
||||
const generating = (type == 'create' && video.status == VideoStatus.Generating)
|
||||
const failed = (type == 'create' && (video.status != VideoStatus.Generating && video.status != VideoStatus.Generated) )
|
||||
|
||||
return <div
|
||||
className={`video-item ${className}`}
|
||||
ref={setNodeRef} style={{transform: `translateY(${transform?.y || 0}px)`,}}
|
||||
>
|
||||
<div className={`list-row ${generating ? 'disabled' : ''} ${active?'playing':''}`}>
|
||||
<div className={`list-row ${generating ? 'disabled status-generating' : ''} ${failed ? 'status-generate-failed' : ''} ${active?'playing':''}`}>
|
||||
<div
|
||||
className="col number"
|
||||
{... (sortable && !generating?listeners:{})}
|
||||
@ -60,15 +72,26 @@ export const VideoListItem = (
|
||||
<div className="relative">
|
||||
<img className="w-[100px] h-[56px] object-cover" src={video.cover || ImageCover}/>
|
||||
{generating &&
|
||||
<div className={'absolute inset-0 bg-black/30 text-white flex items-center justify-center'}>
|
||||
<span className="ml-1">{t('video.generating')}</span>
|
||||
<div className={'absolute rounded inset-0 bg-black/40 text-white flex items-center justify-center'}>
|
||||
<div className="text-center">
|
||||
<IconGenerating className="inline-block text-xl" />
|
||||
<div className="text-xs">{t('video.generating')}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{failed &&
|
||||
<div className={'absolute rounded inset-0 bg-black/40 text-white flex items-center justify-center'}>
|
||||
<div className="text-center">
|
||||
<IconGenerateFailed className="inline-block text-xl" />
|
||||
<div className="text-xs">{t('video.generate_failed')}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{/* && active*/}
|
||||
{!generating && playing && <div className={'absolute rounded inset-0 bg-black/30 text-sm text-white flex items-center justify-center'}>
|
||||
{!generating && !failed && playing && <div className={'absolute rounded inset-0 bg-black/40 text-white flex items-center justify-center'}>
|
||||
<div className="text-center">
|
||||
<IconPlaying className="inline-block text-xl" />
|
||||
<div>{t('video.playing')}</div>
|
||||
<div className="text-xs">{t('video.playing')}</div>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
@ -92,7 +115,7 @@ export const VideoListItem = (
|
||||
{/* <button className="hover:text-blue-500 cursor-move">*/}
|
||||
{/* <MenuOutlined/>*/}
|
||||
{/* </button> : <button disabled className="cursor-not-allowed"><MenuOutlined/></button>)}*/}
|
||||
<div className={"flex items-center gap-4"}>
|
||||
<div className={"flex items-center justify-start gap-6"}>
|
||||
{editable && !generating && <>
|
||||
{onEdit &&
|
||||
<button className="hover:text-blue-500" onClick={e=>{
|
||||
@ -102,6 +125,14 @@ export const VideoListItem = (
|
||||
}} style={{fontSize: '1.1em'}}>
|
||||
<IconEdit/>
|
||||
</button>}
|
||||
{onRegenerate &&
|
||||
<button className="text-red-400 hover:text-blue-500" onClick={e=>{
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onRegenerate?.()
|
||||
}} style={{fontSize: '1.1em'}}>
|
||||
<IconRegenerate/>
|
||||
</button>}
|
||||
|
||||
{onRemove && <Popconfirm
|
||||
rootClassName={'popconfirm-main'}
|
||||
@ -112,14 +143,15 @@ export const VideoListItem = (
|
||||
// description={`删除后需从重新${type == 'create' ? '生成' : '推流'}`}
|
||||
onConfirm={onRemove}
|
||||
><button className="hover:text-blue-500"><IconDelete/></button></Popconfirm>}
|
||||
<Checkbox checked={state.checked} onChange={() => {
|
||||
{hideCheckBox ? <span className={"inline-block w-[18px] h-1"}></span> : <Checkbox checked={state.checked} onChange={() => {
|
||||
if (onCheckedChange) {
|
||||
onCheckedChange(!state.checked)
|
||||
} else {
|
||||
setState({checked: !state.checked})
|
||||
}
|
||||
}} />
|
||||
}} />}
|
||||
</>}
|
||||
{additionOperation}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -136,6 +136,7 @@
|
||||
"delete_empty": "Select the video you want to delete",
|
||||
"download": "Download",
|
||||
"generating": "Generating",
|
||||
"generate_failed": "Generate Failed",
|
||||
"playing": "Playing",
|
||||
"push_confirm": "Are you sure you want to streaming these video?",
|
||||
"push_empty": "Select the video you want to streaming",
|
||||
|
@ -136,6 +136,7 @@
|
||||
"delete_empty": "请选择要删除的视频",
|
||||
"download": "下载视频",
|
||||
"generating": "生成中",
|
||||
"generate_failed": "生成失败",
|
||||
"playing": "播放中",
|
||||
"push_confirm": "是否确定一键推流选中新闻视频?",
|
||||
"push_empty": "请选择要推流的新闻视频",
|
||||
|
@ -56,11 +56,11 @@ export default function NewsIndex() {
|
||||
}
|
||||
})
|
||||
|
||||
const handleViewNewsDetail = (id: number) => {
|
||||
const handleViewNewsDetail = (id: number,internal_article_id:number) => {
|
||||
const {update, close} = showLoading(`${t('news.get_detail')}...`)
|
||||
getById(id).then(res => {
|
||||
close()
|
||||
setActiveNews({...res, id})
|
||||
setActiveNews({...res, id,internal_article_id})
|
||||
}).catch(() => {
|
||||
update(t('news.get_detail_error'), 'info')
|
||||
})
|
||||
@ -95,7 +95,7 @@ export default function NewsIndex() {
|
||||
closeIcon={null} open={true} width={1000}
|
||||
footer={null} onCancel={() => setActiveNews(undefined)}
|
||||
>
|
||||
<div className="news-detail pl-16 pr-1 flex pb-5">
|
||||
<div className="news-detail pl-16 pr-1 flex pb-2">
|
||||
<div className="px-4 py-6 bg-white flex-1">
|
||||
<div className="new-title text-2xl">{activeNews?.title}</div>
|
||||
<div className="info mt-2 mb-2 text-sm flex gap-3">
|
||||
@ -111,11 +111,11 @@ export default function NewsIndex() {
|
||||
<CloseOutlined className="text-xl text-gray-400 hover:text-gray-800"
|
||||
onClick={() => setActiveNews(undefined)}/>
|
||||
</div>
|
||||
<div className="whitespace-nowrap text-sm mt-2">
|
||||
<Checkbox
|
||||
<div className="whitespace-nowrap text-sm mt-2 min-w-[58px]">
|
||||
{activeNews.internal_article_id <= 0 && <Checkbox
|
||||
checked={checkedId.includes(activeNews!.id)}
|
||||
onChange={() => handleCheckChange(activeNews!.id)}
|
||||
><span className="ml-[-4px]">{t('select.text')}</span></Checkbox>
|
||||
><span className="ml-[-4px]">{t('select.text')}</span></Checkbox>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -157,7 +157,7 @@ export default function NewsIndex() {
|
||||
<div className="news-content flex-1">
|
||||
<div className="title h-[60px] line-clamp-2 text-lg cursor-pointer hover:text-blue-500"
|
||||
onClick={() => {
|
||||
handleViewNewsDetail(item.id)
|
||||
handleViewNewsDetail(item.id,item.internal_article_id)
|
||||
}}>{item.title}</div>
|
||||
<div className="content flex gap-3 mt-2 mb-3">
|
||||
<div
|
||||
|
@ -6,7 +6,7 @@ import {useSetState} from "ahooks";
|
||||
|
||||
import {VideoListItem} from "@/components/video/video-list-item.tsx";
|
||||
import ArticleEditModal from "@/components/article/edit-modal.tsx";
|
||||
import {deleteFromList, getList, modifyOrder, VideoStatus} from "@/service/api/video.ts";
|
||||
import {deleteFromList, getList, modifyOrder, regenerateById, VideoStatus} from "@/service/api/video.ts";
|
||||
import {formatDuration} from "@/util/strings.ts";
|
||||
import ButtonBatch from "@/components/button-batch.tsx";
|
||||
import {showErrorToast, showToast} from "@/components/message.ts";
|
||||
@ -14,7 +14,7 @@ import {Player, PlayerInstance} from "@/components/video/player.tsx";
|
||||
import ButtonPush2Room from "@/pages/video/components/button-push2room.tsx";
|
||||
import ButtonToTop from "@/components/scoller/button-to-top.tsx";
|
||||
import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx";
|
||||
import {IconDelete} from "@/components/icons";
|
||||
import {IconDelete, IconEdit} from "@/components/icons";
|
||||
import {useLocation} from "react-router-dom";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {find} from "lodash";
|
||||
@ -93,7 +93,7 @@ export default function VideoIndex() {
|
||||
}
|
||||
// 处理全选
|
||||
const handleAllCheckedChange = () => {
|
||||
setCheckedIdArray(state.checkedAll ? [] : videoData.map(v => v.id))
|
||||
setCheckedIdArray(state.checkedAll ? [] : videoData.filter(s=>s.status == VideoStatus.Generated).map(v => v.id))
|
||||
setState({
|
||||
checkedAll: !state.checkedAll
|
||||
})
|
||||
@ -142,6 +142,12 @@ export default function VideoIndex() {
|
||||
loadList()
|
||||
}).catch(showErrorToast)
|
||||
}
|
||||
const processGenerateVideo = async (video: VideoInfo) => {
|
||||
regenerateById(video.article_id).then(() => {
|
||||
//showToast(t('delete_success'), 'success')
|
||||
loadList()
|
||||
}).catch(showErrorToast)
|
||||
}
|
||||
|
||||
return (<div className="container py-5 page-live">
|
||||
<div className="h-[36px]"></div>
|
||||
@ -236,7 +242,7 @@ export default function VideoIndex() {
|
||||
active={checkedIdArray.includes(v.id)}
|
||||
playing={state.playingId == v.id}
|
||||
checked={checkedIdArray.includes(v.id)}
|
||||
className={`list-item-${index} mt-3 mb-2 list-item-state-${v.status}`}
|
||||
className={`list-item-${index} mt-3 mb-2 list-item-state-${v.status} `}
|
||||
onCheckedChange={(checked) => {
|
||||
setCheckedIdArray(idArray => {
|
||||
const newArr = checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id);
|
||||
@ -246,17 +252,21 @@ export default function VideoIndex() {
|
||||
}}
|
||||
onItemClick={() => playVideo(v)}
|
||||
onRemove={() => processDeleteVideo([v.id])}
|
||||
onEdit={v.status == VideoStatus.Generating ? undefined : () => {
|
||||
onEdit={v.status == VideoStatus.Generated ? () => {
|
||||
setEditId(v.article_id)
|
||||
}}
|
||||
}:undefined}
|
||||
onRegenerate={v.status != VideoStatus.Generating && v.status != VideoStatus.Generated?()=>{
|
||||
processGenerateVideo(v)
|
||||
}:undefined}
|
||||
hideCheckBox={v.status != VideoStatus.Generating && v.status != VideoStatus.Generated}
|
||||
editable={v.status != VideoStatus.Generating}
|
||||
sortable={v.status != VideoStatus.Generating}
|
||||
sortable={v.status == VideoStatus.Generated}
|
||||
/>))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
}
|
||||
<div className="h-[100px]"></div>
|
||||
<div className="h-[130px]"></div>
|
||||
</InfiniteScroller>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {get, post} from "@/service/request.ts";
|
||||
import {post} from "@/service/request.ts";
|
||||
import {getById as getArticle} from "./article"
|
||||
|
||||
export function getList() {
|
||||
return post<DataList<VideoInfo>>('/video/list')
|
||||
@ -27,6 +28,11 @@ export function regenerate(title: string, metahuman_text: string, content_group:
|
||||
}
|
||||
})
|
||||
}
|
||||
// 重新生成视频
|
||||
export async function regenerateById(article_id: Id) {
|
||||
const article = await getArticle(article_id);
|
||||
return await regenerate(article.title, article.metahuman_text, article.content_group, article_id)
|
||||
}
|
||||
|
||||
export function getById(id: Id) {
|
||||
return post<VideoInfo>({url: '/video/detail/' + id})
|
||||
|
1
src/types/core.d.ts
vendored
1
src/types/core.d.ts
vendored
@ -37,6 +37,7 @@ declare interface ArticleDetail {
|
||||
|
||||
declare interface NewsInfo {
|
||||
id: number;
|
||||
internal_article_id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
media_name: string;
|
||||
|
Loading…
x
Reference in New Issue
Block a user