@@ -1,9 +1,11 @@
import React , { useEffect , useState } from 'react' ;
import { getAllVideoTasks } from '@/lib/indexeddb' ;
import { getAllVideoTasks , updateVideoTask } from '@/lib/indexeddb' ;
import { Dialog , DialogContent } from '@/components/ui/dialog' ;
import { Button } from '@/components/ui/button' ;
import { api } from '@/lib/api' ;
import { Table , TableHeader , TableBody , TableRow , TableHead , TableCell } from '@/components/ui/table' ;
import { Checkbox } from '@/components/ui/checkbox' ;
import { toast } from 'sonner' ;
interface LocalVideoTask {
id : string ;
@@ -11,6 +13,7 @@ interface LocalVideoTask {
prompt : string ;
create_time : string ;
task_id : string ;
video_url? : string ;
}
interface TaskStatusMap {
@@ -27,6 +30,7 @@ const TryOnVideoTaskPage: React.FC = () => {
const [ dialogOpen , setDialogOpen ] = useState ( false ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ statusLoading , setStatusLoading ] = useState ( false ) ;
const [ selectedIds , setSelectedIds ] = useState < string [ ] > ( [ ] ) ;
// 1. 读取本地任务
useEffect ( ( ) = > {
@@ -39,7 +43,21 @@ const TryOnVideoTaskPage: React.FC = () => {
// 2. 查询后端任务状态
useEffect ( ( ) = > {
if ( ! tasks . length ) return ;
const job_ids = tasks . map ( t = > t . task_id ) . filter ( Boolean ) ;
// 先补全本地已完成的任务到 statusMap
setStatusMap ( prev = > {
const newMap = { . . . prev } ;
tasks . forEach ( t = > {
if ( t . video_url ) {
newMap [ t . task_id ] = { status : 'finished' , video_url : t.video_url } ;
}
} ) ;
return newMap ;
} ) ;
// 只查本地没有 video_url 的任务
const job_ids = tasks
. filter ( t = > ! t . video_url )
. map ( t = > t . task_id )
. filter ( Boolean ) ;
if ( ! job_ids . length ) return ;
setStatusLoading ( true ) ;
api . Service . batchQueryVideoStatusApiJmBatchQueryStatusPost ( { requestBody : { job_ids } } )
@@ -49,6 +67,13 @@ const TryOnVideoTaskPage: React.FC = () => {
if ( res ? . data ) {
( res . data . finished || [ ] ) . forEach ( ( item : any ) = > {
map [ item . job_id ] = { status : 'finished' , video_url : item.video_url } ;
if ( item . video_url ) {
const localTask = tasks . find ( t = > t . task_id === item . job_id ) ;
if ( localTask && ! localTask . video_url ) {
updateVideoTask ( localTask . id , { video_url : item.video_url } ) ;
setTasks ( prev = > prev . map ( t = > ( t . id === localTask . id ? { . . . t , video_url : item.video_url } : t ) ) ) ;
}
}
} ) ;
( res . data . running || [ ] ) . forEach ( ( job_id : string ) = > {
map [ job_id ] = { status : 'running' } ;
@@ -57,7 +82,7 @@ const TryOnVideoTaskPage: React.FC = () => {
map [ job_id ] = { status : 'failed' } ;
} ) ;
}
setStatusMap ( map ) ;
setStatusMap ( prev = > ( { . . . prev , . . . map } ) ) ;
} )
. finally ( ( ) = > setStatusLoading ( false ) ) ;
} , [ tasks ] ) ;
@@ -81,13 +106,62 @@ const TryOnVideoTaskPage: React.FC = () => {
return < span className = 'text-gray-400' > 未 知 < / span > ;
} ;
// 计算可选的任务ID( 已完成且有video_url)
const downloadableIds = tasks . filter ( t = > statusMap [ t . task_id ] ? . status === 'finished' && statusMap [ t . task_id ] ? . video_url ) . map ( t = > t . id ) ;
const isAllSelected = downloadableIds . length > 0 && downloadableIds . every ( id = > selectedIds . includes ( id ) ) ;
const handleSelectAll = ( ) = > {
if ( isAllSelected ) {
setSelectedIds ( [ ] ) ;
} else {
setSelectedIds ( downloadableIds ) ;
}
} ;
const handleSelectOne = ( id : string ) = > {
setSelectedIds ( prev = > ( prev . includes ( id ) ? prev . filter ( i = > i !== id ) : [ . . . prev , id ] ) ) ;
} ;
// 批量下载逻辑
const handleBatchDownload = async ( ) = > {
if ( selectedIds . length === 0 ) {
toast . warning ( '请选择要下载的视频任务' ) ;
return ;
}
const selectedTasks = tasks . filter ( t = > selectedIds . includes ( t . id ) ) ;
let count = 0 ;
for ( const task of selectedTasks ) {
const s = statusMap [ task . task_id ] ;
if ( s ? . status === 'finished' && s . video_url ) {
try {
// 直接用 a 标签下载,避免 CORS
const a = document . createElement ( 'a' ) ;
a . href = s . video_url ;
a . download = ` ${ task . id } .mp4 ` ;
a . target = '_blank' ; // 可选:新标签页
document . body . appendChild ( a ) ;
a . click ( ) ;
a . remove ( ) ;
count ++ ;
} catch ( e ) {
toast . error ( ` 下载失败: ${ task . id } ` ) ;
}
}
}
toast . success ( ` 成功下载 ${ count } 个视频 ` ) ;
} ;
return (
< div className = 'p-6' >
< h2 className = 'text-xl font-bold mb-4' > 视 频 任 务 进 度 < / h2 >
< div className = 'mb-6 flex justify-end ' >
< div className = 'mb-6 flex justify-between items-center ' >
< Button variant = 'outline' onClick = { ( ) = > window . location . reload ( ) } >
刷 新
< / Button >
< Button variant = 'default' onClick = { handleBatchDownload } disabled = { selectedIds . length === 0 } >
批 量 下 载
< / Button >
< / div >
< Dialog open = { dialogOpen } onOpenChange = { setDialogOpen } >
< DialogContent style = { { maxHeight : '80vh' } } className = 'flex items-center justify-center' showCloseButton = { false } >
@@ -103,6 +177,9 @@ const TryOnVideoTaskPage: React.FC = () => {
< Table className = 'min-w-full text-sm' >
< TableHeader >
< TableRow className = 'bg-gray-100' >
< TableHead className = 'p-2 text-center' >
< Checkbox checked = { isAllSelected } onCheckedChange = { handleSelectAll } aria-label = '全选' disabled = { downloadableIds . length === 0 } / >
< / TableHead >
< TableHead className = 'p-2 text-center' > 图 片 < / TableHead >
< TableHead className = 'p-2' > ID < / TableHead >
< TableHead className = 'p-2' > 任 务 ID < / TableHead >
@@ -112,8 +189,19 @@ const TryOnVideoTaskPage: React.FC = () => {
< / TableRow >
< / TableHeader >
< TableBody >
{ tasks . map ( task = > (
{ tasks . map ( task = > {
const s = statusMap [ task . task_id ] ;
const canDownload = s ? . status === 'finished' && s . video_url ;
return (
< TableRow key = { task . id } >
< TableCell className = 'p-2 text-center' >
< Checkbox
checked = { selectedIds . includes ( task . id ) }
onCheckedChange = { ( ) = > handleSelectOne ( task . id ) }
disabled = { ! canDownload }
aria-label = '选择任务'
/ >
< / TableCell >
< TableCell className = 'p-2 text-center' >
{ task . img_url ? (
< img
@@ -135,7 +223,8 @@ const TryOnVideoTaskPage: React.FC = () => {
< TableCell > { task . create_time ? new Date ( task . create_time ) . toLocaleString ( ) : '-' } < / TableCell >
< TableCell > { statusLoading ? '查询中...' : renderStatus ( task ) } < / TableCell >
< / TableRow >
) ) }
) ;
} ) }
< / TableBody >
< / Table >
< / div >