baseline: Clover 独立仓库首次基线提交
将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。 当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包), M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
86
frontend/src/app/tasks/[id]/images/page.tsx
Normal file
86
frontend/src/app/tasks/[id]/images/page.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
// 屏4 挑图:0/N异步电影 + 平铺选图 + 单批重试(一期=整体重生) + 能离开
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { ProgressBar } from '@/components/layout/ProgressBar';
|
||||
import { FlywheelBannerFromSse } from '@/components/FlywheelBanner';
|
||||
import { ImageProgressCardSkeleton } from '@/components/tasks/ImageProgressCard';
|
||||
import { ImageProgressHeader } from '@/components/tasks/ImageProgressHeader';
|
||||
import { ImageActionBar } from '@/components/tasks/ImageActionBar';
|
||||
import { SseStatusBar } from '@/components/tasks/SseStatusBar';
|
||||
import { useSse } from '@/hooks/useSse';
|
||||
import { useGenerationStore } from '@/stores/generationStore';
|
||||
import { useTaskStore } from '@/stores/taskStore';
|
||||
import { api } from '@/lib/api';
|
||||
|
||||
export default function ImageSelectionPage() {
|
||||
const params = useParams<{ id: string }>();
|
||||
const taskId = params?.id ? Number(params.id) : null;
|
||||
const router = useRouter();
|
||||
|
||||
const { imageCandidates, imageDone, imageTotal, failedBatches, flywheelInjected } = useGenerationStore();
|
||||
const { selectedImageIds, toggleImageSelection } = useTaskStore();
|
||||
const { connectionStatus, reconnectAttempt } = useSse(taskId);
|
||||
|
||||
const pendingCount = imageTotal - imageDone;
|
||||
const allDone = imageTotal > 0 && pendingCount === 0;
|
||||
|
||||
async function handleRetry(_batchRole: string) {
|
||||
// 一期 regenerate = 整体重生,契约无 batch 字段,不发 body
|
||||
// 二期按批重生时再扩展,_batchRole 目前仅作入参占位(UI 提示用)
|
||||
if (!taskId) return;
|
||||
await api.post(`/api/v1/tasks/${taskId}/regenerate`);
|
||||
}
|
||||
|
||||
async function handleProceedToConfirm() {
|
||||
if (!taskId || selectedImageIds.length === 0) return;
|
||||
for (const candidateId of selectedImageIds) {
|
||||
await api.post(`/api/v1/tasks/${taskId}/image-candidates/select`, { candidate_id: candidateId });
|
||||
}
|
||||
router.push(`/tasks/${taskId}/confirm`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<ProgressBar currentStep={3} />
|
||||
{flywheelInjected && (
|
||||
<FlywheelBannerFromSse
|
||||
recentPreference={flywheelInjected.recentPreference}
|
||||
rejectReasons={flywheelInjected.rejectReasons}
|
||||
/>
|
||||
)}
|
||||
<SseStatusBar status={connectionStatus} attempt={reconnectAttempt} />
|
||||
<div className="p-6 flex-1 overflow-auto">
|
||||
<ImageProgressHeader imageTotal={imageTotal} imageDone={imageDone} pendingCount={pendingCount} />
|
||||
{/* 图片网格(谁好谁先冒) */}
|
||||
<div className="grid grid-cols-3 xl:grid-cols-5 gap-3">
|
||||
{imageCandidates.length === 0
|
||||
? Array.from({ length: 5 }).map((_, i) => <ImageProgressCardSkeleton key={i} />)
|
||||
: imageCandidates.map((c) => (
|
||||
<div key={c.candidate_id}
|
||||
onClick={() => toggleImageSelection(c.candidate_id)}
|
||||
className={`relative rounded-xl overflow-hidden border cursor-pointer transition-all
|
||||
${selectedImageIds.includes(c.candidate_id) ? 'border-brand-orange ring-2 ring-brand-orange' : 'border-border-default hover:border-brand-orange/50'}`}
|
||||
aria-label={`图片 ${c.role},${selectedImageIds.includes(c.candidate_id) ? '已选中' : '未选中'}`}
|
||||
>
|
||||
<img src={c.url} alt={c.role} className="w-full aspect-square object-cover" />
|
||||
<p className="text-xs text-center py-1 text-text-secondary">{c.role}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* 失败批次提示 */}
|
||||
{failedBatches.length > 0 && allDone && (
|
||||
<div role="alert" className="mt-4 bg-red-50 border border-status-error rounded-xl px-4 py-3 text-sm text-status-error">
|
||||
有 {failedBatches.length} 批没出来。
|
||||
{failedBatches.filter((b) => b.retryable).map((b) => (
|
||||
<button key={b.batch} onClick={() => handleRetry(b.batch)}
|
||||
className="ml-2 underline" aria-label={`重试第${b.batch}批`}>⟳ 重试批次{b.batch}</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{imageCandidates.length > 0 && (
|
||||
<ImageActionBar selectedCount={selectedImageIds.length} onProceed={handleProceedToConfirm} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user