| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765 |
- import React, { useState, useEffect, useMemo } from 'react';
- import { Button } from '@/components/ui/button';
- import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
- import { Label } from '@/components/ui/label';
- import { Input } from '@/components/ui/input';
- import { Badge } from '@/components/ui/badge';
- import { Alert, AlertDescription } from '@/components/ui/alert';
- import { Progress } from '@/components/ui/progress';
- import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
- import { ScrollArea } from '@/components/ui/scroll-area';
- import { Separator } from '@/components/ui/separator';
- import {
- User,
- Play,
- Loader2,
- CheckCircle,
- Wand2,
- Sparkles,
- Image as ImageIcon,
- FileText,
- Zap,
- Save,
- Upload,
- Plus,
- Settings,
- Palette,
- Camera,
- Shirt,
- ImageIcon as ImageIcon2
- } from 'lucide-react';
- import { motion, AnimatePresence } from 'framer-motion';
- import { materialLibraryAPI } from '@/api/materials';
- import { textTemplateAPI } from '@/api/textTemplates';
- import { aiSwapAPI } from '@/api/ai_swap';
- import { aiSwapBgAPI } from '@/api/ai_swap_bg';
- import { useAuth } from '@/contexts/AuthContext';
- // 素材选择卡片组件
- const MaterialCard = ({ item, isSelected, onSelect, type = 'image' }) => (
- <motion.div
- whileHover={{ scale: 1.02 }}
- className={`relative aspect-square bg-slate-100 rounded-lg overflow-hidden cursor-pointer transition-all ${
- isSelected ? 'ring-2 ring-blue-500 shadow-lg' : 'hover:ring-2 hover:ring-slate-300'
- }`}
- onClick={() => onSelect(item.id)}
- >
- {item.file_url ? (
- <img
- src={item.file_url}
- alt={item.original_filename}
- className="w-full h-full object-cover"
- />
- ) : (
- <div className="w-full h-full flex flex-col items-center justify-center text-slate-400">
- <ImageIcon className="w-8 h-8 mb-2" />
- <span className="text-xs">暂无图片</span>
- </div>
- )}
- {isSelected && (
- <div className="absolute top-2 right-2 w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center">
- <CheckCircle className="w-4 h-4 text-white" />
- </div>
- )}
- <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-2">
- <p className="text-white text-xs font-medium truncate">
- {item.original_filename}
- </p>
- </div>
- </motion.div>
- );
- // 模板选择组件
- const TemplateItem = ({ template, isSelected, onSelect, type }) => (
- <motion.div
- whileHover={{ scale: 1.01 }}
- className={`p-3 rounded-lg border cursor-pointer transition-all ${
- isSelected ? 'border-blue-500 bg-blue-50' : 'border-slate-200 hover:border-slate-300'
- }`}
- onClick={() => onSelect(template.id)}
- >
- <div className="flex items-center justify-between mb-2">
- <span className="text-sm font-medium">{template.name}</span>
- {isSelected && <CheckCircle className="w-4 h-4 text-blue-500" />}
- </div>
- <Badge variant="outline" className="text-xs">
- {template.tag || type}
- </Badge>
- </motion.div>
- );
- export default function AIGeneration() {
- const { user } = useAuth();
-
- // 状态管理
- const [originalImages, setOriginalImages] = useState([]);
- const [ipCharacters, setIpCharacters] = useState([]);
- const [clothingItems, setClothingItems] = useState([]);
- const [sceneTemplates, setSceneTemplates] = useState([]);
- const [copyTemplates, setCopyTemplates] = useState([]);
- const [selectedOriginalImages, setSelectedOriginalImages] = useState([]);
- const [selectedIpCharacters, setSelectedIpCharacters] = useState([]);
- const [selectedClothingItems, setSelectedClothingItems] = useState([]);
- const [selectedSceneTemplates, setSelectedSceneTemplates] = useState([]);
- const [selectedCopyTemplates, setSelectedCopyTemplates] = useState([]);
- const [taskName, setTaskName] = useState('');
- const [selectedTaskTypes, setSelectedTaskTypes] = useState([]);
- const [quantityPerGroup, setQuantityPerGroup] = useState(1);
- const [isGenerating, setIsGenerating] = useState(false);
- const [generationProgress, setGenerationProgress] = useState(0);
- const [currentTask, setCurrentTask] = useState('');
- const [message, setMessage] = useState(null);
- const [loading, setLoading] = useState(false);
- const [currentTaskId, setCurrentTaskId] = useState(null); // 兼容保留,展示第一个任务ID
- const [currentTaskIds, setCurrentTaskIds] = useState([]); // 多任务ID
- const [taskStatus, setTaskStatus] = useState(null); // 聚合状态摘要
- const [pollingInterval, setPollingInterval] = useState(null);
- useEffect(() => {
- if (user) {
- loadMaterials();
- loadTemplates();
- }
- }, [user]);
- // 清理轮询定时器
- useEffect(() => {
- return () => {
- if (pollingInterval) {
- clearInterval(pollingInterval);
- }
- };
- }, [pollingInterval]);
- const loadMaterials = async () => {
- // 使用当前登录用户的ID
- const userId = user?.id;
- if (!userId) {
- setMessage({ type: 'error', text: '请先登录' });
- return;
- }
-
- setLoading(true);
- try {
- // 获取原始素材(原图)- 对应素材库的"原始素材"标签页
- const originalResponse = await materialLibraryAPI.getMaterials(userId, 'original');
- console.log('原始素材API响应:', originalResponse);
- if (originalResponse.success) {
- const originalData = originalResponse.images.map(item => ({
- ...item,
- file_url: getImageUrl(item)
- }));
- setOriginalImages(originalData);
- console.log('原始素材数据:', originalData);
- }
- // 获取IP素材(人脸)- 对应素材库的"IP素材"标签页
- const faceResponse = await materialLibraryAPI.getMaterials(userId, 'face');
- console.log('IP素材API响应:', faceResponse);
- if (faceResponse.success) {
- const faceData = faceResponse.images.map(item => ({
- ...item,
- file_url: getImageUrl(item)
- }));
- setIpCharacters(faceData);
- console.log('IP素材数据:', faceData);
- }
- // 获取产品素材(服装)- 对应素材库的"产品素材"标签页,但API类型是'cloth'
- const clothResponse = await materialLibraryAPI.getMaterials(userId, 'cloth');
- console.log('产品素材API响应:', clothResponse);
- if (clothResponse.success) {
- const clothData = clothResponse.images.map(item => ({
- ...item,
- file_url: getImageUrl(item)
- }));
- setClothingItems(clothData);
- console.log('产品素材数据:', clothData);
- }
- } catch (error) {
- console.error('加载素材失败:', error);
- setMessage({ type: 'error', text: '加载素材失败,请稍后重试' });
- } finally {
- setLoading(false);
- }
- };
- // 获取图片URL
- const getImageUrl = (item) => {
- if (!item.stored_path) return '';
- const filename = item.stored_path.split(/[\\/]/).pop();
- const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000';
- return `${baseURL}/materials/${filename}`;
- };
- const loadTemplates = async () => {
- const userId = user?.id;
- if (!userId) {
- setMessage({ type: 'error', text: '请先登录' });
- return;
- }
- try {
- const [sceneResp, copyResp] = await Promise.all([
- textTemplateAPI.getTextTemplates(userId, 'prompt', 1, 100),
- textTemplateAPI.getTextTemplates(userId, 'copywrite', 1, 100),
- ]);
- const mapTemplates = (resp) => (resp && resp.success && Array.isArray(resp.templates)
- ? resp.templates.map(t => ({
- id: t.id,
- name: t.text_name,
- tag: t.text_label,
- created_date: t.created_at,
- content: t.text_content,
- }))
- : []);
- setSceneTemplates(mapTemplates(sceneResp));
- setCopyTemplates(mapTemplates(copyResp));
- } catch (error) {
- console.error('加载模板失败:', error);
- setMessage({ type: 'error', text: '加载模板失败,请稍后重试' });
- }
- };
- // 轮询多个任务状态并聚合进度
- const pollAllTasksStatus = async (taskIds) => {
- try {
- if (!taskIds || taskIds.length === 0) return;
- const responses = await Promise.allSettled(taskIds.map(id => aiSwapAPI.getTaskStatus(id)));
- const fulfilled = responses
- .filter(r => r.status === 'fulfilled')
- .map(r => r.value);
- const rejected = responses.filter(r => r.status === 'rejected');
- const total = taskIds.length;
- const completed = fulfilled.filter(r => r.status === 'completed').length;
- const failed = fulfilled.filter(r => r.status === 'failed').length + rejected.length;
- const processing = fulfilled.filter(r => r.status === 'processing').length;
- const pending = total - completed - failed - processing;
- // 聚合进度(平均)
- const avgProgress = fulfilled.length > 0
- ? Math.round(
- fulfilled.reduce((sum, r) => sum + (typeof r.progress === 'number' ? r.progress : 0), 0) / fulfilled.length
- )
- : 0;
- setGenerationProgress(avgProgress);
- setTaskStatus({
- total,
- completed,
- failed,
- processing,
- pending,
- details: fulfilled
- });
- if (completed + failed === total) {
- setCurrentTask('生成完成');
- setMessage({ type: failed === 0 ? 'success' : 'error', text: failed === 0 ? '全部生成完成!' : `部分失败:成功 ${completed},失败 ${failed}` });
- setIsGenerating(false);
- if (pollingInterval) {
- clearInterval(pollingInterval);
- setPollingInterval(null);
- }
- } else if (processing > 0 || pending > 0) {
- setCurrentTask('AI处理中...');
- }
- } catch (error) {
- console.error('轮询任务状态失败:', error);
- setMessage({ type: 'error', text: '获取任务状态失败' });
- }
- };
- const handleSelection = (setter, selectedItems, id) => {
- setter(prev => {
- if (prev.includes(id)) {
- return prev.filter(item => item !== id);
- } else {
- return [...prev, id];
- }
- });
- };
- const handleTaskTypeToggle = (taskType) => {
- setSelectedTaskTypes(prev => {
- if (prev.includes(taskType)) {
- return prev.filter(type => type !== taskType);
- } else {
- return [...prev, taskType];
- }
- });
- };
- // 取消当前任务
- const handleCancelTask = async () => {
- if (!currentTaskIds || currentTaskIds.length === 0) return;
- try {
- await Promise.allSettled(currentTaskIds.map(id => aiSwapAPI.cancelTask(id)));
- setMessage({ type: 'success', text: '已取消全部任务' });
- setIsGenerating(false);
- setCurrentTaskId(null);
- setCurrentTaskIds([]);
- setTaskStatus(null);
- if (pollingInterval) {
- clearInterval(pollingInterval);
- setPollingInterval(null);
- }
- } catch (error) {
- console.error('取消任务失败:', error);
- setMessage({ type: 'error', text: '取消任务失败' });
- }
- };
- const handleGenerate = async () => {
- // 验证用户登录状态
- if (!user?.id) {
- setMessage({ type: 'error', text: '请先登录' });
- return;
- }
- // 验证选择条件
- if (selectedIpCharacters.length === 0) {
- setMessage({ type: 'error', text: '请选择至少一个IP形象' });
- return;
- }
- if (selectedClothingItems.length === 0) {
- setMessage({ type: 'error', text: '请选择至少一件服装' });
- return;
- }
- if (!selectedTaskTypes.includes('换脸') ||
- !selectedTaskTypes.includes('换衣')) {
- setMessage({ type: 'error', text: '请选择换脸和换衣任务类型' });
- return;
- }
- if (selectedSceneTemplates.length === 0) {
- setMessage({ type: 'error', text: '请选择至少一个场景模板' });
- return;
- }
- setIsGenerating(true);
- setGenerationProgress(0);
- setMessage(null);
- setCurrentTaskId(null);
- setTaskStatus(null);
- try {
- // 使用当前登录用户的ID
- const userId = user.id;
-
- // 组合选择,进行笛卡尔积
- const selectedFaces = ipCharacters.filter(item => selectedIpCharacters.includes(item.id));
- const selectedClothes = clothingItems.filter(item => selectedClothingItems.includes(item.id));
- const selectedScenes = sceneTemplates.filter(item => selectedSceneTemplates.includes(item.id));
- const selectedCopies = selectedCopyTemplates.length > 0
- ? copyTemplates.filter(item => selectedCopyTemplates.includes(item.id))
- : [null];
- if (selectedFaces.length === 0 || selectedClothes.length === 0 || selectedScenes.length === 0) {
- throw new Error('选中的素材/模板不存在');
- }
- const submittedTaskIds = [];
- const totalCombos = selectedFaces.length * selectedClothes.length * selectedScenes.length * selectedCopies.length;
- console.log(`即将提交组合任务数量: ${totalCombos}`);
- // 顺序提交,避免瞬时过载(也可改成并发 Promise.all)
- for (const face of selectedFaces) {
- for (const cloth of selectedClothes) {
- for (const scene of selectedScenes) {
- for (const copy of selectedCopies) {
- let prompt = 'AI换脸换装';
- if (scene) {
- prompt += `,场景:${scene.content}`;
- }
- if (copy) {
- prompt += `,文案风格:${copy.name}`;
- }
- if (taskName.trim()) {
- prompt += `,任务:${taskName.trim()}`;
- }
- const swapData = {
- user_id: userId,
- face_image_id: face.id,
- cloth_image_id: cloth.id,
- prompt,
- quantity: quantityPerGroup,
- };
- const resp = await aiSwapAPI.processSwap(swapData);
- if (resp && resp.success === true && resp.task_id) {
- submittedTaskIds.push(resp.task_id);
- } else {
- console.warn('组合任务提交失败:', resp);
- }
- }
- }
- }
- }
- if (submittedTaskIds.length > 0) {
- setCurrentTaskIds(submittedTaskIds);
- setCurrentTaskId(submittedTaskIds[0]);
- setMessage({ type: 'success', text: `已提交 ${submittedTaskIds.length}/${totalCombos} 个任务,处理中...` });
- setCurrentTask('准备素材');
- setGenerationProgress(5);
- // 开始轮询:聚合全部任务进度
- const interval = setInterval(() => {
- pollAllTasksStatus(submittedTaskIds);
- }, 2000);
- setPollingInterval(interval);
- // 立即轮询一次
- pollAllTasksStatus(submittedTaskIds);
- } else {
- setIsGenerating(false);
- setMessage({ type: 'error', text: '未能成功提交任何任务,请稍后重试' });
- }
-
- } catch (error) {
- setIsGenerating(false);
- console.error('换脸换装请求异常:', error);
- const errorMsg = error.message || '处理失败,请稍后重试';
- setMessage({ type: 'error', text: errorMsg });
- }
- };
- const canGenerate = selectedIpCharacters.length > 0 &&
- selectedClothingItems.length > 0 &&
- selectedTaskTypes.includes('换脸') &&
- selectedTaskTypes.includes('换衣') &&
- selectedSceneTemplates.length > 0 &&
- !isGenerating;
- const totalSelected = selectedIpCharacters.length + selectedClothingItems.length;
- return (
- <div className="min-h-screen bg-slate-50">
- {/* 顶部标题栏 */}
- <div className="bg-white border-b border-slate-200 px-6 py-4">
- <div className="max-w-7xl mx-auto">
- <div className="flex items-center justify-between">
- <div>
- <h1 className="text-2xl font-bold text-slate-900">智能生成</h1>
- <p className="text-slate-600 text-sm mt-1">
- 选择素材和模板,使用ComfyUI一键生成营销内容
- </p>
- </div>
- <Button
- onClick={handleGenerate}
- disabled={!canGenerate}
- size="lg"
- className="bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white px-8"
- >
- {isGenerating ? (
- <>
- <Loader2 className="w-5 h-5 mr-2 animate-spin" />
- 生成中...
- </>
- ) : (
- <>
- <Play className="w-5 h-5 mr-2" />
- 开始生成
- </>
- )}
- </Button>
- </div>
- </div>
- </div>
- {/* 消息提示 */}
- {message && (
- <div className="px-6 py-2">
- <Alert className={`${message.type === 'error' ? 'border-red-200 bg-red-50 text-red-700' : 'border-green-200 bg-green-50 text-green-700'}`}>
- <AlertDescription>{message.text}</AlertDescription>
- </Alert>
- </div>
- )}
- {/* 主要内容区域 */}
- <div className="max-w-7xl mx-auto px-6 py-6">
- <div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
-
- {/* 第一栏:选择原图 */}
- <Card className="h-fit">
- <CardHeader className="pb-3">
- <CardTitle className="flex items-center gap-2 text-lg">
- <Camera className="w-5 h-5" />
- 选择原图
- <Badge variant="secondary" className="ml-auto">
- {selectedOriginalImages.length}
- </Badge>
- </CardTitle>
- </CardHeader>
- <CardContent>
- {loading ? (
- <div className="text-center py-8">
- <Loader2 className="w-8 h-8 mx-auto mb-3 animate-spin text-slate-400" />
- <p className="text-sm text-slate-500">加载中...</p>
- </div>
- ) : originalImages.length > 0 ? (
- <div className="grid grid-cols-1 gap-4">
- {originalImages.map((image) => (
- <MaterialCard
- key={image.id}
- item={image}
- isSelected={selectedOriginalImages.includes(image.id)}
- onSelect={(id) => handleSelection(setSelectedOriginalImages, selectedOriginalImages, id)}
- />
- ))}
- </div>
- ) : (
- <div className="text-center py-8 text-slate-500">
- <ImageIcon className="w-12 h-12 mx-auto mb-3 text-slate-300" />
- <p className="text-sm">暂无原图</p>
- <p className="text-xs mt-1">请先到素材库上传</p>
- </div>
- )}
- </CardContent>
- </Card>
- {/* 第二栏:选择IP形象 */}
- <Card className="h-fit">
- <CardHeader className="pb-3">
- <CardTitle className="flex items-center gap-2 text-lg">
- <User className="w-5 h-5" />
- 选择IP形象
- <Badge variant="secondary" className="ml-auto">
- {selectedIpCharacters.length}
- </Badge>
- </CardTitle>
- </CardHeader>
- <CardContent>
- {loading ? (
- <div className="text-center py-8">
- <Loader2 className="w-8 h-8 mx-auto mb-3 animate-spin text-slate-400" />
- <p className="text-sm text-slate-500">加载中...</p>
- </div>
- ) : ipCharacters.length > 0 ? (
- <div className="grid grid-cols-1 gap-4">
- {ipCharacters.map((character) => (
- <MaterialCard
- key={character.id}
- item={character}
- isSelected={selectedIpCharacters.includes(character.id)}
- onSelect={(id) => handleSelection(setSelectedIpCharacters, selectedIpCharacters, id)}
- />
- ))}
- </div>
- ) : (
- <div className="text-center py-8 text-slate-500">
- <User className="w-12 h-12 mx-auto mb-3 text-slate-300" />
- <p className="text-sm">暂无IP形象</p>
- <p className="text-xs mt-1">请先到素材库上传</p>
- </div>
- )}
- </CardContent>
- </Card>
- {/* 第三栏:选择服装 */}
- <Card className="h-fit">
- <CardHeader className="pb-3">
- <CardTitle className="flex items-center gap-2 text-lg">
- <Shirt className="w-5 h-5" />
- 选择服装
- <Badge variant="secondary" className="ml-auto">
- {selectedClothingItems.length}
- </Badge>
- </CardTitle>
- </CardHeader>
- <CardContent>
- {loading ? (
- <div className="text-center py-8">
- <Loader2 className="w-8 h-8 mx-auto mb-3 animate-spin text-slate-400" />
- <p className="text-sm text-slate-500">加载中...</p>
- </div>
- ) : clothingItems.length > 0 ? (
- <div className="grid grid-cols-1 gap-4">
- {clothingItems.map((clothing) => (
- <MaterialCard
- key={clothing.id}
- item={clothing}
- isSelected={selectedClothingItems.includes(clothing.id)}
- onSelect={(id) => handleSelection(setSelectedClothingItems, selectedClothingItems, id)}
- />
- ))}
- </div>
- ) : (
- <div className="text-center py-8 text-slate-500">
- <Shirt className="w-12 h-12 mx-auto mb-3 text-slate-300" />
- <p className="text-sm">暂无服装</p>
- <p className="text-xs mt-1">请先到素材库上传</p>
- </div>
- )}
- </CardContent>
- </Card>
- {/* 第四栏:生成配置 */}
- <Card className="h-fit" style={{color: 'black'}}>
- <CardHeader className="pb-3">
- <CardTitle className="flex items-center gap-2 text-lg">
- <Settings className="w-5 h-5" />
- 生成配置
- </CardTitle>
- </CardHeader>
- <CardContent className="space-y-6">
-
- {/* 任务命名 */}
- <div>
- <Label htmlFor="taskName" className="text-sm font-bold">任务命名</Label>
- <div className="relative mt-2">
- <Input
- id="taskName"
- value={taskName}
- onChange={(e) => setTaskName(e.target.value)}
- placeholder="输入任务名称..."
- className="pr-8"
- />
- <FileText className="w-4 h-4 absolute right-2 top-1/2 transform -translate-y-1/2 text-slate-400" />
- </div>
- </div>
- {/* 任务类型 */}
- <div>
- <Label className="text-sm font-bold">任务类型</Label>
- <div className="mt-2 space-y-2">
- {['换脸', '换衣', '换背景'].map((taskType) => (
- <div key={taskType} className="flex items-center space-x-2">
- <input
- type="checkbox"
- id={taskType}
- checked={selectedTaskTypes.includes(taskType)}
- onChange={() => handleTaskTypeToggle(taskType)}
- className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
- />
- <Label htmlFor={taskType} className="text-sm cursor-pointer">
- {taskType}
- </Label>
- </div>
- ))}
- </div>
- </div>
- {/* 每组生成数量 */}
- <div>
- <Label htmlFor="quantity" className="text-sm font-bold">每组生成数量</Label>
- <Input
- id="quantity"
- type="number"
- min="1"
- max="10"
- value={quantityPerGroup}
- onChange={(e) => setQuantityPerGroup(parseInt(e.target.value) || 1)}
- className="mt-2 w-full"
- />
- </div>
- {/* 场景模板 */}
- <div>
- <div className="flex items-center justify-between mb-2">
- <Label className="text-sm font-bold">场景模板</Label>
- <Badge variant="secondary" className="text-xs">
- {selectedSceneTemplates.length}
- </Badge>
- </div>
- <ScrollArea className="h-32">
- <div className="space-y-2">
- {sceneTemplates.map((template) => (
- <TemplateItem
- key={template.id}
- template={template}
- isSelected={selectedSceneTemplates.includes(template.id)}
- onSelect={(id) => handleSelection(setSelectedSceneTemplates, selectedSceneTemplates, id)}
- type="scene"
- />
- ))}
- </div>
- </ScrollArea>
- </div>
- {/* 文案模板 */}
- <div>
- <div className="flex items-center justify-between mb-2">
- <Label className="text-sm font-bold">文案模板</Label>
- <Badge variant="secondary" className="text-xs">
- {selectedCopyTemplates.length}
- </Badge>
- </div>
- <ScrollArea className="h-32">
- <div className="space-y-2">
- {copyTemplates.map((template) => (
- <TemplateItem
- key={template.id}
- template={template}
- isSelected={selectedCopyTemplates.includes(template.id)}
- onSelect={(id) => handleSelection(setSelectedCopyTemplates, selectedCopyTemplates, id)}
- type="copy"
- />
- ))}
- </div>
- </ScrollArea>
- </div>
- </CardContent>
- </Card>
- </div>
- {/* 生成进度 */}
- {isGenerating && (
- <Card className="mt-6">
- <CardHeader>
- <CardTitle className="flex items-center justify-between">
- <div className="flex items-center gap-2">
- <Loader2 className="w-5 h-5 animate-spin" />
- 生成进度
- </div>
- {currentTaskIds && currentTaskIds.length > 0 && (
- <Button
- variant="outline"
- size="sm"
- onClick={handleCancelTask}
- className="text-red-600 hover:text-red-700 hover:bg-red-50"
- >
- 取消任务
- </Button>
- )}
- </CardTitle>
- </CardHeader>
- <CardContent>
- <div className="space-y-4">
- <div className="flex justify-between items-center">
- <span className="text-sm font-medium">{currentTask}</span>
- <span className="text-sm text-slate-500">{generationProgress}%</span>
- </div>
- <Progress value={generationProgress} className="w-full" />
- {currentTaskIds && currentTaskIds.length > 0 && (
- <div className="text-xs text-slate-500 space-y-1">
- <div>任务数: {currentTaskIds.length}</div>
- <div>示例任务ID: {currentTaskIds[0]}</div>
- {taskStatus && (
- <div className="flex flex-wrap gap-3">
- <span>完成: {taskStatus.completed}</span>
- <span>进行中: {taskStatus.processing}</span>
- <span>等待: {taskStatus.pending}</span>
- <span>失败: {taskStatus.failed}</span>
- </div>
- )}
- </div>
- )}
- </div>
- </CardContent>
- </Card>
- )}
- </div>
- </div>
- );
- }
|