feat: 将穿搭图片生成功能改为Modal弹框形式
- 创建OutfitImageGenerationModal组件,使用Portal渲染到modal-root容器 - 修改ModelDetail页面,将穿搭生成选项卡改为按钮触发Modal - 更新ModelDetailTabs组件,为穿搭生成选项卡添加特殊处理 - 修复OutfitImageGallery和OutfitImageGenerator的图片路径处理 - 优化UI设计,使用渐变背景和美观的按钮界面 - 支持键盘ESC关闭和背景点击关闭Modal功能
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import {
|
import {
|
||||||
X,
|
X,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
@@ -180,7 +181,12 @@ export const ModelImagePreviewModal: React.FC<ModelImagePreviewModalProps> = ({
|
|||||||
|
|
||||||
const imageUrl = getImageSrc(currentPhoto.file_path);
|
const imageUrl = getImageSrc(currentPhoto.file_path);
|
||||||
|
|
||||||
return (
|
// 获取modal-root容器
|
||||||
|
const modalRoot = document.getElementById('modal-root');
|
||||||
|
if (!modalRoot) return null;
|
||||||
|
|
||||||
|
// 使用Portal渲染到modal-root容器
|
||||||
|
return createPortal(
|
||||||
<div className={`fixed inset-0 z-50 flex items-center justify-center bg-black ${
|
<div className={`fixed inset-0 z-50 flex items-center justify-center bg-black ${
|
||||||
isFullscreen ? 'bg-opacity-100' : 'bg-opacity-75'
|
isFullscreen ? 'bg-opacity-100' : 'bg-opacity-75'
|
||||||
}`}>
|
}`}>
|
||||||
@@ -435,7 +441,8 @@ export const ModelImagePreviewModal: React.FC<ModelImagePreviewModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>,
|
||||||
|
modalRoot
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,14 @@ import {
|
|||||||
List,
|
List,
|
||||||
Search,
|
Search,
|
||||||
Calendar,
|
Calendar,
|
||||||
Sparkles
|
Sparkles,
|
||||||
|
Eye
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { OutfitImageRecord, OutfitImageStatus } from '../types/outfitImage';
|
import { OutfitImageRecord, OutfitImageStatus } from '../types/outfitImage';
|
||||||
import { DeleteConfirmDialog } from './DeleteConfirmDialog';
|
import { DeleteConfirmDialog } from './DeleteConfirmDialog';
|
||||||
|
import { getImageSrc } from '../utils/imagePathUtils';
|
||||||
|
import { ImagePreviewModal } from './ImagePreviewModal';
|
||||||
|
import { GroundingSource } from '../types/ragGrounding';
|
||||||
|
|
||||||
interface OutfitImageGalleryProps {
|
interface OutfitImageGalleryProps {
|
||||||
records: OutfitImageRecord[];
|
records: OutfitImageRecord[];
|
||||||
@@ -40,6 +44,15 @@ export const OutfitImageGallery: React.FC<OutfitImageGalleryProps> = ({
|
|||||||
const [filter, setFilter] = useState<FilterType>('all');
|
const [filter, setFilter] = useState<FilterType>('all');
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
// 图片预览模态框状态
|
||||||
|
const [previewModal, setPreviewModal] = useState<{
|
||||||
|
isOpen: boolean;
|
||||||
|
source: GroundingSource | null;
|
||||||
|
}>({
|
||||||
|
isOpen: false,
|
||||||
|
source: null
|
||||||
|
});
|
||||||
|
|
||||||
// 删除确认对话框状态
|
// 删除确认对话框状态
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState<{
|
const [deleteConfirm, setDeleteConfirm] = useState<{
|
||||||
show: boolean;
|
show: boolean;
|
||||||
@@ -88,6 +101,27 @@ export const OutfitImageGallery: React.FC<OutfitImageGalleryProps> = ({
|
|||||||
setDeleteConfirm({ show: false, record: null, deleting: false });
|
setDeleteConfirm({ show: false, record: null, deleting: false });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 打开图片预览
|
||||||
|
const openImagePreview = useCallback((imageUrl: string, title: string) => {
|
||||||
|
const source: GroundingSource = {
|
||||||
|
uri: imageUrl,
|
||||||
|
title: title,
|
||||||
|
content: { description: '穿搭生成图片' }
|
||||||
|
};
|
||||||
|
setPreviewModal({
|
||||||
|
isOpen: true,
|
||||||
|
source
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 关闭图片预览
|
||||||
|
const closeImagePreview = useCallback(() => {
|
||||||
|
setPreviewModal({
|
||||||
|
isOpen: false,
|
||||||
|
source: null
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const getStatusIcon = (status: OutfitImageStatus) => {
|
const getStatusIcon = (status: OutfitImageStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case OutfitImageStatus.Pending:
|
case OutfitImageStatus.Pending:
|
||||||
@@ -298,12 +332,17 @@ export const OutfitImageGallery: React.FC<OutfitImageGalleryProps> = ({
|
|||||||
{record.status === OutfitImageStatus.Completed && record.outfit_images.length > 0 && (
|
{record.status === OutfitImageStatus.Completed && record.outfit_images.length > 0 && (
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{record.outfit_images.slice(0, 4).map((image, index) => (
|
{record.outfit_images.slice(0, 4).map((image, index) => (
|
||||||
<div key={image.id} className="aspect-square rounded-lg overflow-hidden">
|
<div key={image.id} className="aspect-square rounded-lg overflow-hidden relative group cursor-pointer">
|
||||||
<img
|
<img
|
||||||
src={image.image_url}
|
src={getImageSrc(image.image_url)}
|
||||||
alt={`穿搭图片 ${index + 1}`}
|
alt={`穿搭图片 ${index + 1}`}
|
||||||
className="w-full h-full object-cover hover:scale-105 transition-transform duration-200"
|
className="w-full h-full object-cover hover:scale-105 transition-transform duration-200"
|
||||||
|
onClick={() => openImagePreview(image.image_url, `穿搭图片 ${index + 1}`)}
|
||||||
/>
|
/>
|
||||||
|
{/* 预览按钮覆盖层 */}
|
||||||
|
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-30 transition-all duration-200 flex items-center justify-center">
|
||||||
|
<Eye className="w-6 h-6 text-white opacity-0 group-hover:opacity-100 transition-opacity duration-200" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{record.outfit_images.length > 4 && (
|
{record.outfit_images.length > 4 && (
|
||||||
@@ -381,9 +420,13 @@ export const OutfitImageGallery: React.FC<OutfitImageGalleryProps> = ({
|
|||||||
{record.status === OutfitImageStatus.Completed && record.outfit_images.length > 0 && (
|
{record.status === OutfitImageStatus.Completed && record.outfit_images.length > 0 && (
|
||||||
<div className="flex -space-x-2">
|
<div className="flex -space-x-2">
|
||||||
{record.outfit_images.slice(0, 3).map((image, index) => (
|
{record.outfit_images.slice(0, 3).map((image, index) => (
|
||||||
<div key={image.id} className="w-10 h-10 rounded-lg overflow-hidden border-2 border-white">
|
<div
|
||||||
|
key={image.id}
|
||||||
|
className="w-10 h-10 rounded-lg overflow-hidden border-2 border-white cursor-pointer hover:scale-110 transition-transform duration-200"
|
||||||
|
onClick={() => openImagePreview(image.image_url, `穿搭图片 ${index + 1}`)}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src={image.image_url}
|
src={getImageSrc(image.image_url)}
|
||||||
alt={`穿搭图片 ${index + 1}`}
|
alt={`穿搭图片 ${index + 1}`}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
@@ -416,6 +459,13 @@ export const OutfitImageGallery: React.FC<OutfitImageGalleryProps> = ({
|
|||||||
onCancel={cancelDelete}
|
onCancel={cancelDelete}
|
||||||
deleting={deleteConfirm.deleting}
|
deleting={deleteConfirm.deleting}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 图片预览模态框 */}
|
||||||
|
<ImagePreviewModal
|
||||||
|
isOpen={previewModal.isOpen}
|
||||||
|
source={previewModal.source}
|
||||||
|
onClose={closeImagePreview}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
146
apps/desktop/src/components/OutfitImageGenerationModal.tsx
Normal file
146
apps/desktop/src/components/OutfitImageGenerationModal.tsx
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { X, Sparkles } from 'lucide-react';
|
||||||
|
import { Model } from '../types/model';
|
||||||
|
import { OutfitImageRecord, OutfitImageGenerationRequest } from '../types/outfitImage';
|
||||||
|
import { OutfitImageGenerator } from './OutfitImageGenerator';
|
||||||
|
import { OutfitImageGallery } from './OutfitImageGallery';
|
||||||
|
|
||||||
|
interface OutfitImageGenerationModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
model: Model;
|
||||||
|
outfitRecords: OutfitImageRecord[];
|
||||||
|
onGenerate: (request: OutfitImageGenerationRequest) => Promise<void>;
|
||||||
|
onDeleteRecord: (record: OutfitImageRecord) => Promise<void>;
|
||||||
|
onRefreshRecords: () => Promise<void>;
|
||||||
|
isGenerating?: boolean;
|
||||||
|
recordsLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 穿搭图片生成模态框组件
|
||||||
|
* 使用Portal渲染到modal-root容器,避免复杂容器结构影响
|
||||||
|
*/
|
||||||
|
export const OutfitImageGenerationModal: React.FC<OutfitImageGenerationModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
model,
|
||||||
|
outfitRecords,
|
||||||
|
onGenerate,
|
||||||
|
onDeleteRecord,
|
||||||
|
onRefreshRecords,
|
||||||
|
isGenerating = false,
|
||||||
|
recordsLoading = false
|
||||||
|
}) => {
|
||||||
|
// 键盘事件处理
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
// 防止背景滚动
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
document.body.style.overflow = 'unset';
|
||||||
|
};
|
||||||
|
}, [isOpen, onClose]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
// 获取modal-root容器
|
||||||
|
const modalRoot = document.getElementById('modal-root');
|
||||||
|
if (!modalRoot) return null;
|
||||||
|
|
||||||
|
// 使用Portal渲染到modal-root容器
|
||||||
|
return createPortal(
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
||||||
|
{/* 模态框背景 */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 cursor-pointer"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 模态框内容 */}
|
||||||
|
<div className="relative bg-white rounded-2xl shadow-2xl max-w-7xl max-h-[95vh] w-full mx-4 flex flex-col overflow-hidden">
|
||||||
|
{/* 头部 */}
|
||||||
|
<div className="flex items-center justify-between p-6 border-b border-gray-200 bg-gradient-to-r from-purple-50 to-pink-50">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-pink-500 rounded-xl flex items-center justify-center">
|
||||||
|
<Sparkles className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-gray-900">穿搭图片生成</h2>
|
||||||
|
<p className="text-sm text-gray-600">为 {model.name} 生成AI穿搭效果图</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
title="关闭 (Esc)"
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 主要内容区域 */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-6">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 h-full">
|
||||||
|
{/* 穿搭图片生成器 */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<OutfitImageGenerator
|
||||||
|
modelId={model.id}
|
||||||
|
modelPhotos={model.photos}
|
||||||
|
onGenerate={onGenerate}
|
||||||
|
isGenerating={isGenerating}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 穿搭图片生成记录 */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<OutfitImageGallery
|
||||||
|
records={outfitRecords}
|
||||||
|
onDelete={onDeleteRecord}
|
||||||
|
onRefresh={onRefreshRecords}
|
||||||
|
loading={recordsLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 底部操作栏(可选) */}
|
||||||
|
<div className="flex items-center justify-between p-6 border-t border-gray-200 bg-gray-50">
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
共 {outfitRecords.length} 条生成记录
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<button
|
||||||
|
onClick={onRefreshRecords}
|
||||||
|
className="px-4 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-200 rounded-lg transition-colors text-sm"
|
||||||
|
>
|
||||||
|
刷新记录
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-6 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors text-sm"
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
modalRoot
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OutfitImageGenerationModal;
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { ModelPhoto, PhotoType } from '../types/model';
|
import { ModelPhoto, PhotoType } from '../types/model';
|
||||||
import { OutfitImageGenerationRequest } from '../types/outfitImage';
|
import { OutfitImageGenerationRequest } from '../types/outfitImage';
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
import { getImageSrc } from '../utils/imagePathUtils';
|
||||||
|
|
||||||
interface OutfitImageGeneratorProps {
|
interface OutfitImageGeneratorProps {
|
||||||
modelId: string;
|
modelId: string;
|
||||||
@@ -208,7 +208,7 @@ export const OutfitImageGenerator: React.FC<OutfitImageGeneratorProps> = ({
|
|||||||
onClick={() => setSelectedModelImageId(photo.id)}
|
onClick={() => setSelectedModelImageId(photo.id)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={convertFileSrc(photo.file_path)}
|
src={getImageSrc(photo.file_path)}
|
||||||
alt={photo.file_name}
|
alt={photo.file_name}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
@@ -280,7 +280,7 @@ export const OutfitImageGenerator: React.FC<OutfitImageGeneratorProps> = ({
|
|||||||
<div key={index} className="relative group">
|
<div key={index} className="relative group">
|
||||||
<div className="aspect-square rounded-lg overflow-hidden border border-gray-200">
|
<div className="aspect-square rounded-lg overflow-hidden border border-gray-200">
|
||||||
<img
|
<img
|
||||||
src={imagePath.startsWith('http') ? imagePath : `file://${imagePath}`}
|
src={getImageSrc(imagePath)}
|
||||||
alt={`商品图片 ${index + 1}`}
|
alt={`商品图片 ${index + 1}`}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface ModelDetailTabsProps {
|
|||||||
dynamics: ModelDynamic[];
|
dynamics: ModelDynamic[];
|
||||||
videoTasks: VideoGenerationTask[];
|
videoTasks: VideoGenerationTask[];
|
||||||
outfitRecords: OutfitImageRecord[];
|
outfitRecords: OutfitImageRecord[];
|
||||||
|
onOpenOutfitModal?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,7 +33,8 @@ export const ModelDetailTabs: React.FC<ModelDetailTabsProps> = ({
|
|||||||
model,
|
model,
|
||||||
dynamics,
|
dynamics,
|
||||||
videoTasks,
|
videoTasks,
|
||||||
outfitRecords
|
outfitRecords,
|
||||||
|
onOpenOutfitModal
|
||||||
}) => {
|
}) => {
|
||||||
// 键盘快捷键支持
|
// 键盘快捷键支持
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -120,7 +122,38 @@ export const ModelDetailTabs: React.FC<ModelDetailTabsProps> = ({
|
|||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200/50 mb-6">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200/50 mb-6">
|
||||||
<div className="border-b border-gray-200">
|
<div className="border-b border-gray-200">
|
||||||
<nav className="flex space-x-8 px-6" aria-label="Tabs">
|
<nav className="flex space-x-8 px-6" aria-label="Tabs">
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => {
|
||||||
|
// 穿搭生成选项卡特殊处理 - 显示为按钮形式
|
||||||
|
if (tab.id === 'outfits') {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => {
|
||||||
|
onTabChange(tab.id);
|
||||||
|
onOpenOutfitModal?.();
|
||||||
|
}}
|
||||||
|
title={`${tab.name} (${tab.shortcut})`}
|
||||||
|
className={`${activeTab === tab.id
|
||||||
|
? 'border-purple-500 text-purple-600 bg-purple-50'
|
||||||
|
: 'border-transparent text-gray-500 hover:text-purple-600 hover:border-purple-300 hover:bg-purple-50'
|
||||||
|
} whitespace-nowrap py-4 px-4 border-b-2 font-medium text-sm transition-all duration-200 rounded-t-lg flex items-center gap-2 relative group`}
|
||||||
|
>
|
||||||
|
<span className="text-base">{tab.icon}</span>
|
||||||
|
<span>{tab.name}</span>
|
||||||
|
{tab.badge !== null && tab.badge > 0 && (
|
||||||
|
<span className={`${activeTab === tab.id
|
||||||
|
? 'bg-purple-500 text-white'
|
||||||
|
: 'bg-gray-400 text-white group-hover:bg-purple-500'
|
||||||
|
} text-xs px-2 py-0.5 rounded-full font-semibold transition-colors duration-200`}>
|
||||||
|
{tab.badge}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他选项卡正常处理
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
onClick={() => onTabChange(tab.id)}
|
onClick={() => onTabChange(tab.id)}
|
||||||
@@ -141,7 +174,8 @@ export const ModelDetailTabs: React.FC<ModelDetailTabsProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ import { DeleteConfirmDialog } from '../components/DeleteConfirmDialog';
|
|||||||
import { OutfitImageService } from '../services/outfitImageService';
|
import { OutfitImageService } from '../services/outfitImageService';
|
||||||
import { ModelDashboardStats, OutfitImageRecord, OutfitImageGenerationRequest } from '../types/outfitImage';
|
import { ModelDashboardStats, OutfitImageRecord, OutfitImageGenerationRequest } from '../types/outfitImage';
|
||||||
import { ModelImageGallery } from '../components/ModelImageGallery';
|
import { ModelImageGallery } from '../components/ModelImageGallery';
|
||||||
import { OutfitImageGenerator } from '../components/OutfitImageGenerator';
|
import { OutfitImageGenerationModal } from '../components/OutfitImageGenerationModal';
|
||||||
import { OutfitImageGallery } from '../components/OutfitImageGallery';
|
|
||||||
import { ModelDetailHeader } from '../components/model-detail/ModelDetailHeader';
|
import { ModelDetailHeader } from '../components/model-detail/ModelDetailHeader';
|
||||||
import { ModelDetailTabs, TabId } from '../components/model-detail/ModelDetailTabs';
|
import { ModelDetailTabs, TabId } from '../components/model-detail/ModelDetailTabs';
|
||||||
import { ModelOverviewTab } from '../components/model-detail/ModelOverviewTab';
|
import { ModelOverviewTab } from '../components/model-detail/ModelOverviewTab';
|
||||||
@@ -50,6 +49,9 @@ const ModelDetail: React.FC = () => {
|
|||||||
const [activeTab, setActiveTab] = useState<TabId>('overview');
|
const [activeTab, setActiveTab] = useState<TabId>('overview');
|
||||||
const [dynamics] = useState<ModelDynamic[]>([]);
|
const [dynamics] = useState<ModelDynamic[]>([]);
|
||||||
|
|
||||||
|
// 穿搭图片生成Modal状态
|
||||||
|
const [showOutfitModal, setShowOutfitModal] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
const [deletePhotoConfirm, setDeletePhotoConfirm] = useState<{
|
const [deletePhotoConfirm, setDeletePhotoConfirm] = useState<{
|
||||||
show: boolean;
|
show: boolean;
|
||||||
@@ -451,6 +453,7 @@ const ModelDetail: React.FC = () => {
|
|||||||
dynamics={dynamics}
|
dynamics={dynamics}
|
||||||
videoTasks={videoTasks}
|
videoTasks={videoTasks}
|
||||||
outfitRecords={outfitRecords}
|
outfitRecords={outfitRecords}
|
||||||
|
onOpenOutfitModal={() => setShowOutfitModal(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Tab内容区域 */}
|
{/* Tab内容区域 */}
|
||||||
@@ -503,22 +506,31 @@ const ModelDetail: React.FC = () => {
|
|||||||
{/* 穿搭生成选项卡 */}
|
{/* 穿搭生成选项卡 */}
|
||||||
{activeTab === 'outfits' && (
|
{activeTab === 'outfits' && (
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
|
||||||
{/* 穿搭图片生成 */}
|
<div className="text-center">
|
||||||
<OutfitImageGenerator
|
<div className="w-20 h-20 bg-gradient-to-br from-purple-500 to-pink-500 rounded-2xl flex items-center justify-center mx-auto mb-6">
|
||||||
modelId={model.id}
|
<span className="text-3xl">👗</span>
|
||||||
modelPhotos={model.photos}
|
</div>
|
||||||
onGenerate={handleGenerateOutfitImages}
|
|
||||||
isGenerating={generatingOutfit}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 穿搭图片生成记录 */}
|
<h3 className="text-2xl font-bold text-gray-900 mb-4">AI穿搭图片生成</h3>
|
||||||
<OutfitImageGallery
|
<p className="text-gray-600 mb-8 max-w-md mx-auto">
|
||||||
records={outfitRecords}
|
为 {model.name} 生成专业的AI穿搭效果图,支持多种风格和商品搭配
|
||||||
onDelete={handleDeleteOutfitRecord}
|
</p>
|
||||||
onRefresh={loadOutfitRecords}
|
|
||||||
loading={outfitRecordsLoading}
|
<div className="space-y-4">
|
||||||
/>
|
<button
|
||||||
|
onClick={() => setShowOutfitModal(true)}
|
||||||
|
className="inline-flex items-center px-8 py-4 bg-gradient-to-r from-purple-500 to-pink-500 text-white font-semibold rounded-xl hover:from-purple-600 hover:to-pink-600 transition-all duration-200 shadow-lg hover:shadow-xl transform hover:scale-105"
|
||||||
|
>
|
||||||
|
<span className="text-xl mr-3">✨</span>
|
||||||
|
开始生成穿搭图片
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
已有 {outfitRecords.length} 条生成记录
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -544,6 +556,21 @@ const ModelDetail: React.FC = () => {
|
|||||||
onConfirm={confirmDeletePhoto}
|
onConfirm={confirmDeletePhoto}
|
||||||
onCancel={cancelDeletePhoto}
|
onCancel={cancelDeletePhoto}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 穿搭图片生成Modal */}
|
||||||
|
{model && (
|
||||||
|
<OutfitImageGenerationModal
|
||||||
|
isOpen={showOutfitModal}
|
||||||
|
onClose={() => setShowOutfitModal(false)}
|
||||||
|
model={model}
|
||||||
|
outfitRecords={outfitRecords}
|
||||||
|
onGenerate={handleGenerateOutfitImages}
|
||||||
|
onDeleteRecord={handleDeleteOutfitRecord}
|
||||||
|
onRefreshRecords={loadOutfitRecords}
|
||||||
|
isGenerating={generatingOutfit}
|
||||||
|
recordsLoading={outfitRecordsLoading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user