forked from admin/hair-keeper
189 lines
6.2 KiB
TypeScript
189 lines
6.2 KiB
TypeScript
'use client'
|
||
|
||
import React from 'react'
|
||
import { trpc } from '@/lib/trpc'
|
||
import { Button } from '@/components/ui/button'
|
||
import { FileSearch } from 'lucide-react'
|
||
import { toast } from 'sonner'
|
||
import { TaskDialog, BaseTaskProgress } from '@/components/common/task-dialog'
|
||
import type { AnalyzePackagesProgress } from '@/server/queues'
|
||
|
||
/**
|
||
* 扩展的分析进度类型
|
||
*/
|
||
interface AnalyzeProgress extends BaseTaskProgress, AnalyzePackagesProgress {}
|
||
|
||
interface PackageAnalyzeDialogProps {
|
||
open: boolean
|
||
onOpenChange: (open: boolean) => void
|
||
jobId: string | null
|
||
onAnalyzeCompleted: () => void
|
||
}
|
||
|
||
interface PackageAnalyzeTriggerProps {
|
||
onStartAnalyze: () => void
|
||
isStarting: boolean
|
||
}
|
||
|
||
/**
|
||
* 依赖包分析触发器按钮
|
||
*/
|
||
export function PackageAnalyzeTrigger({
|
||
onStartAnalyze,
|
||
isStarting
|
||
}: PackageAnalyzeTriggerProps) {
|
||
return (
|
||
<Button
|
||
variant="default"
|
||
onClick={onStartAnalyze}
|
||
disabled={isStarting}
|
||
>
|
||
<FileSearch className="mr-2 h-4 w-4" />
|
||
{isStarting ? '启动中...' : '依赖包分析'}
|
||
</Button>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 依赖包分析进度对话框
|
||
*/
|
||
export function PackageAnalyzeDialog({
|
||
open,
|
||
onOpenChange,
|
||
jobId,
|
||
onAnalyzeCompleted
|
||
}: PackageAnalyzeDialogProps) {
|
||
// 停止分析任务 mutation
|
||
const cancelMutation = trpc.devArch!.cancelAnalyzePackagesJob.useMutation({
|
||
onSuccess: () => {
|
||
toast.success('已发送停止请求')
|
||
},
|
||
onError: (error) => {
|
||
toast.error(error.message || '停止任务失败')
|
||
},
|
||
})
|
||
|
||
// 停止任务
|
||
const handleCancelTask = async (taskJobId: string) => {
|
||
await cancelMutation.mutateAsync({ jobId: taskJobId })
|
||
}
|
||
|
||
// 自定义状态消息渲染
|
||
const renderStatusMessage = (progress: AnalyzeProgress) => {
|
||
if (progress.state === 'waiting') {
|
||
return '任务等待中...'
|
||
} else if (progress.state === 'active') {
|
||
if (progress.currentPackage) {
|
||
return `正在分析: ${progress.currentPackage}`
|
||
}
|
||
return '正在分析依赖包...'
|
||
} else if (progress.state === 'completed') {
|
||
const successCount = (progress.analyzedPackages || 0) - (progress.failedPackages || 0)
|
||
const failedCount = progress.failedPackages || 0
|
||
const skippedCount = progress.skippedPackages || 0
|
||
|
||
const parts = [`成功 ${successCount} 个`]
|
||
if (failedCount > 0) {
|
||
parts.push(`失败 ${failedCount} 个`)
|
||
}
|
||
if (skippedCount > 0) {
|
||
parts.push(`跳过 ${skippedCount} 个`)
|
||
}
|
||
parts.push(`共 ${progress.totalPackages || 0} 个依赖包`)
|
||
|
||
return `分析完成!${parts.join(',')}`
|
||
} else if (progress.state === 'failed') {
|
||
return progress.error || '分析失败'
|
||
}
|
||
return ''
|
||
}
|
||
|
||
// 自定义详细信息渲染
|
||
const renderDetails = (progress: AnalyzeProgress) => {
|
||
if (progress.totalPackages === undefined && progress.analyzedPackages === undefined) {
|
||
return null
|
||
}
|
||
|
||
const successCount = (progress.analyzedPackages || 0) - (progress.failedPackages || 0)
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
{/* 进度统计 */}
|
||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||
{progress.totalPackages !== undefined && (
|
||
<div>
|
||
<span className="text-muted-foreground">依赖包总数:</span>
|
||
<span className="ml-1 font-medium">{progress.totalPackages}</span>
|
||
</div>
|
||
)}
|
||
{progress.analyzedPackages !== undefined && (
|
||
<div>
|
||
<span className="text-muted-foreground">已分析:</span>
|
||
<span className="ml-1 font-medium">{progress.analyzedPackages}</span>
|
||
</div>
|
||
)}
|
||
{successCount > 0 && (
|
||
<div>
|
||
<span className="text-muted-foreground">成功:</span>
|
||
<span className="ml-1 font-medium text-green-600">{successCount}</span>
|
||
</div>
|
||
)}
|
||
{progress.failedPackages !== undefined && progress.failedPackages > 0 && (
|
||
<div>
|
||
<span className="text-muted-foreground">失败:</span>
|
||
<span className="ml-1 font-medium text-red-600">{progress.failedPackages}</span>
|
||
</div>
|
||
)}
|
||
{progress.skippedPackages !== undefined && progress.skippedPackages > 0 && (
|
||
<div>
|
||
<span className="text-muted-foreground">跳过:</span>
|
||
<span className="ml-1 font-medium text-blue-600">{progress.skippedPackages}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 当前处理的依赖包 */}
|
||
{progress.currentPackage && progress.state === 'active' && (
|
||
<div className="rounded-md bg-muted p-3 text-sm">
|
||
<div className="text-muted-foreground mb-1">当前依赖包:</div>
|
||
<div className="font-mono text-xs break-all">{progress.currentPackage}</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 最近的错误信息 */}
|
||
{progress.recentErrors && progress.recentErrors.length > 0 && (
|
||
<div className="rounded-md border border-red-200 bg-red-50 p-3 text-sm">
|
||
<div className="text-red-800 font-medium mb-2">最近的错误 (最多显示10条):</div>
|
||
<div className="space-y-2 max-h-40 overflow-y-auto">
|
||
{progress.recentErrors.map((err, index) => (
|
||
<div key={index} className="text-xs">
|
||
<div className="font-mono text-red-700 break-all">{err.packageName}</div>
|
||
<div className="text-red-600 mt-1">{err.error}</div>
|
||
{index < progress.recentErrors!.length - 1 && (
|
||
<div className="border-t border-red-200 mt-2" />
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<TaskDialog<AnalyzeProgress>
|
||
open={open}
|
||
onOpenChange={onOpenChange}
|
||
useSubscription={trpc.jobs.subscribeAnalyzePackagesProgress.useSubscription}
|
||
jobId={jobId}
|
||
title="依赖包分析进度"
|
||
description="正在使用AI分析项目依赖包,请稍候..."
|
||
onCancelTask={handleCancelTask}
|
||
onTaskCompleted={onAnalyzeCompleted}
|
||
isCancelling={cancelMutation.isPending}
|
||
renderStatusMessage={renderStatusMessage}
|
||
renderDetails={renderDetails}
|
||
/>
|
||
)
|
||
} |