mirror of
https://github.com/YFGaia/dify-plus.git
synced 2026-06-04 10:14:00 +08:00
368 lines
12 KiB
TypeScript
368 lines
12 KiB
TypeScript
'use client'
|
|
import type { FC } from 'react'
|
|
import React, { useCallback, useEffect, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import {
|
|
RiCheckLine,
|
|
RiErrorWarningLine,
|
|
RiLoader2Line,
|
|
RiPauseLine,
|
|
RiPlayLargeLine,
|
|
RiRefreshLine,
|
|
RiStopLine,
|
|
} from '@remixicon/react'
|
|
import { fetchProgressApi, resumeBatchApi, retryFailedTasksApi, stopBatchApi } from '@/service/web-extend' // extend: 批量运行工单
|
|
import type { BatchStatus } from '@/utils/batch-progress-manager' // extend: 批量运行工单
|
|
import ActionButton from '@/app/components/base/action-button'
|
|
|
|
import { cn } from '@/utils/classnames'
|
|
|
|
export type BatchProgressProps = {
|
|
batchId: string
|
|
fileName: string
|
|
workflowId?: string
|
|
jobData: {
|
|
id: string
|
|
fileName: string
|
|
createdAt: string
|
|
status: string
|
|
totalRows: number
|
|
processedRows: number
|
|
error?: string
|
|
}
|
|
onDownload: () => void
|
|
onRetrySuccess?: () => void
|
|
onJobUpdate?: (jobData: { status: string, processedRows: number, error?: string }) => void // 新增:任务更新回调
|
|
}
|
|
|
|
const BatchProgress: FC<BatchProgressProps> = ({
|
|
batchId,
|
|
fileName,
|
|
workflowId,
|
|
jobData,
|
|
onDownload,
|
|
onRetrySuccess,
|
|
onJobUpdate,
|
|
}) => {
|
|
const { t } = useTranslation()
|
|
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
// 本地进度状态,用于独立刷新
|
|
const [localProgress, setLocalProgress] = useState({
|
|
status: jobData.status,
|
|
processedRows: jobData.processedRows,
|
|
totalRows: jobData.totalRows,
|
|
error: jobData.error,
|
|
})
|
|
|
|
// 自动刷新单个任务的进度(每 3 秒)
|
|
useEffect(() => {
|
|
// 只在任务进行中时刷新
|
|
if (localProgress.status !== 'pending' && localProgress.status !== 'processing')
|
|
return
|
|
|
|
const refreshInterval = setInterval(async () => {
|
|
try {
|
|
const progress = await fetchProgressApi(batchId)
|
|
if (progress) {
|
|
const newStatus = progress.status as string
|
|
const newProcessedRows = progress.processed_rows as number
|
|
const newError = progress.error as string | undefined
|
|
|
|
// 只有当数据有变化时才更新
|
|
if (
|
|
newStatus !== localProgress.status
|
|
|| newProcessedRows !== localProgress.processedRows
|
|
|| newError !== localProgress.error
|
|
) {
|
|
const updatedProgress = {
|
|
status: newStatus,
|
|
processedRows: newProcessedRows,
|
|
totalRows: progress.total_rows as number,
|
|
error: newError,
|
|
}
|
|
setLocalProgress(updatedProgress)
|
|
// 通知父组件数据已更新(用于列表级别的状态同步)
|
|
onJobUpdate?.({
|
|
status: newStatus,
|
|
processedRows: newProcessedRows,
|
|
error: newError,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
catch (error) {
|
|
console.error('Failed to fetch progress:', error)
|
|
}
|
|
}, 3000)
|
|
|
|
return () => clearInterval(refreshInterval)
|
|
}, [batchId, localProgress.status, localProgress.processedRows, localProgress.error, onJobUpdate])
|
|
|
|
// 停止批量处理
|
|
const handleStop = async () => {
|
|
setIsLoading(true)
|
|
try {
|
|
const success = await stopBatchApi(batchId)
|
|
if (success) {
|
|
// 通知父组件刷新列表
|
|
onRetrySuccess?.()
|
|
}
|
|
}
|
|
catch (error) {
|
|
console.error('Failed to stop batch:', error)
|
|
}
|
|
finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
// 恢复批量处理
|
|
const handleResume = async () => {
|
|
setIsLoading(true)
|
|
try {
|
|
const success = await resumeBatchApi(batchId)
|
|
if (success) {
|
|
// 通知父组件刷新列表
|
|
onRetrySuccess?.()
|
|
}
|
|
}
|
|
catch (error) {
|
|
console.error('Failed to resume batch:', error)
|
|
}
|
|
finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
// 重试失败任务(仅重试失败的任务,保留已完成的任务)
|
|
const handleRetry = async () => {
|
|
setIsLoading(true)
|
|
try {
|
|
const success = await retryFailedTasksApi(batchId)
|
|
if (success) {
|
|
// 通知父组件刷新列表
|
|
onRetrySuccess?.()
|
|
}
|
|
}
|
|
catch (error) {
|
|
console.error('Failed to retry failed tasks:', error)
|
|
}
|
|
finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
const getStatusText = (status: BatchStatus) => {
|
|
switch (status) {
|
|
case 'pending':
|
|
return t('batchWorkflow.pending', { ns: 'extend'})
|
|
case 'processing':
|
|
return t('batchWorkflow.processing', { ns: 'extend'})
|
|
case 'completed':
|
|
return t('batchWorkflow.completed', { ns: 'extend'})
|
|
case 'failed':
|
|
return t('batchWorkflow.failed', { ns: 'extend'})
|
|
case 'stopped':
|
|
return t('batchWorkflow.stopped', { ns: 'extend'})
|
|
default:
|
|
return t('batchWorkflow.pending', { ns: 'extend'})
|
|
}
|
|
}
|
|
|
|
const getStatusColor = (status: BatchStatus) => {
|
|
switch (status) {
|
|
case 'pending':
|
|
return 'text-gray-500' // Extend: 批量运行工单
|
|
case 'processing':
|
|
return 'text-blue-700' // Extend: 批量运行工单
|
|
case 'completed':
|
|
return 'text-green-500'
|
|
case 'failed':
|
|
return 'text-red-500'
|
|
case 'stopped':
|
|
return 'text-yellow-500'
|
|
default:
|
|
return 'text-gray-500' // Extend: 批量运行工单
|
|
}
|
|
}
|
|
|
|
const formatDate = (dateString: string) => {
|
|
if (!dateString) return '-'
|
|
|
|
const date = new Date(dateString)
|
|
// 检查日期是否有效
|
|
if (isNaN(date.getTime()))
|
|
return '-'
|
|
|
|
return date.toLocaleString('zh-CN', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})
|
|
}
|
|
|
|
const currentTime = new Date().toLocaleString('zh-CN', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})
|
|
|
|
// 计算进度
|
|
const progress = localProgress.totalRows > 0 ? (localProgress.processedRows / localProgress.totalRows) * 100 : 0
|
|
const status = localProgress.status as BatchStatus
|
|
const failed_count = 0 // 从列表API没有这个字段,如果需要可以后续添加
|
|
|
|
const getBorderColor = (status: BatchStatus) => {
|
|
switch (status) {
|
|
case 'pending':
|
|
return 'border-gray-300'
|
|
case 'processing':
|
|
return 'border-blue-500'
|
|
case 'completed':
|
|
return 'border-green-500'
|
|
case 'failed':
|
|
return 'border-red-500'
|
|
case 'stopped':
|
|
return 'border-yellow-500'
|
|
default:
|
|
return 'border-gray-300'
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* 统一的批量任务信息框 */}
|
|
<div className={cn('rounded-lg border p-4', getBorderColor(status))}>
|
|
{/* 文件信息 */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<div className="text-sm font-medium text-gray-900">{t('batchWorkflow.uploadedFileName', { ns: 'extend' })}</div>
|
|
<div className="mt-1 text-xs text-gray-500">{t('batchWorkflow.uploadTime', { ns: 'extend' })}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-right">
|
|
<div className="text-sm font-medium text-gray-900">{fileName}</div>
|
|
<div className="mt-1 text-xs text-gray-500">{formatDate(jobData.createdAt)}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 进度条 */}
|
|
<div className="mt-4">
|
|
<div className="mb-2 flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
{status === 'processing' && <RiLoader2Line className="h-4 w-4 animate-spin text-blue-500" />}
|
|
{status === 'completed' && <RiCheckLine className="h-4 w-4 text-green-500" />}
|
|
{status === 'failed' && <RiErrorWarningLine className="h-4 w-4 text-red-500" />}
|
|
{status === 'pending' && <RiLoader2Line className="h-4 w-4 text-gray-500" />}
|
|
{status === 'stopped' && <RiPauseLine className="h-4 w-4 text-yellow-500" />}
|
|
<span className={cn('text-sm font-medium', getStatusColor(status))}>
|
|
{getStatusText(status)}
|
|
</span>
|
|
</div>
|
|
<span className={cn('text-sm font-medium', getStatusColor(status))}>
|
|
{isNaN(progress) ? '0' : Math.round(progress)}%
|
|
</span>
|
|
</div>
|
|
|
|
{/* 进度条可视化 */}
|
|
<div className="h-2 w-full rounded-full bg-gray-200">
|
|
<div
|
|
className={cn(
|
|
'h-2 rounded-full transition-all duration-300',
|
|
status === 'completed' ? 'bg-green-500'
|
|
: status === 'processing' ? 'bg-blue-700'
|
|
: status === 'failed' ? 'bg-red-500'
|
|
: status === 'stopped' ? 'bg-yellow-500' : 'bg-gray-400',
|
|
)}
|
|
style={{ width: `${Math.min(100, Math.max(0, isNaN(progress) ? 0 : progress))}%` }}
|
|
/>
|
|
</div>
|
|
|
|
{/* 详细进度信息 */}
|
|
{localProgress.totalRows > 0 && (
|
|
<div className="mt-2 text-xs text-gray-500">
|
|
{t('batchWorkflow.processed', {
|
|
processed: localProgress.processedRows || 0,
|
|
total: localProgress.totalRows || 0,
|
|
ns: 'extend',
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{/* 错误信息显示 */}
|
|
{localProgress.error && status === 'failed' && (
|
|
<div className="mt-3 rounded-lg border border-red-200 bg-red-50 p-3">
|
|
<div className="flex items-start space-x-2">
|
|
<RiErrorWarningLine className="h-4 w-4 text-red-500 mt-0.5 flex-shrink-0" />
|
|
<div className="flex-1">
|
|
<div className="text-sm font-medium text-red-800 mb-1">
|
|
{t('batchWorkflow.errorOccurred', { ns: 'extend'} )}
|
|
</div>
|
|
<div className="text-xs text-red-700 break-words">
|
|
{localProgress.error}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
|
|
{/* 操作按钮区域 */}
|
|
<div className="mt-4 flex items-center justify-between">
|
|
<div className="flex space-x-2">
|
|
{/* 控制按钮 */}
|
|
{(status === 'processing' || status === 'pending') && (
|
|
<ActionButton onClick={handleStop} disabled={isLoading} size="sm">
|
|
{isLoading ? (
|
|
<RiLoader2Line className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<RiStopLine className="h-4 w-4" />
|
|
)}
|
|
<span className="ml-1">{t('batchWorkflow.stop', { ns: 'extend'})}</span>
|
|
</ActionButton>
|
|
)}
|
|
{status === 'stopped' && (
|
|
<ActionButton onClick={handleResume} disabled={isLoading} size="sm">
|
|
{isLoading ? (
|
|
<RiLoader2Line className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<RiPlayLargeLine className="h-4 w-4" />
|
|
)}
|
|
<span className="ml-1">{t('batchWorkflow.resume', { ns: 'extend'})}</span>
|
|
</ActionButton>
|
|
)}
|
|
{(status === 'failed') && (
|
|
<ActionButton onClick={handleRetry} disabled={isLoading} size="sm">
|
|
{isLoading ? (
|
|
<RiLoader2Line className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<RiRefreshLine className="h-4 w-4" />
|
|
)}
|
|
<span className="ml-1">{t('batchWorkflow.retry', { ns: 'extend'})}</span>
|
|
</ActionButton>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex space-x-2">
|
|
{/* 下载按钮 */}
|
|
{(status === 'failed' || status === 'completed' || (status === 'processing' && progress >= 100)) && (
|
|
<ActionButton onClick={onDownload} size="sm">
|
|
<span>{t('batchWorkflow.download', { ns: 'extend'})}</span>
|
|
</ActionButton>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default React.memo(BatchProgress)
|