433 lines
16 KiB
TypeScript
433 lines
16 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import {
|
||
PlusIcon,
|
||
PencilIcon,
|
||
TrashIcon,
|
||
EyeIcon,
|
||
MagnifyingGlassIcon,
|
||
FunnelIcon
|
||
} from '@heroicons/react/24/outline';
|
||
import { invoke } from '@tauri-apps/api/core';
|
||
import { useNotifications } from '../NotificationSystem';
|
||
import { PROJECT_ID } from './const';
|
||
|
||
interface OutfitItem {
|
||
id: string;
|
||
project_id: string;
|
||
analysis_id?: string;
|
||
name: string;
|
||
category: string;
|
||
description?: string;
|
||
color_pattern?: any;
|
||
design_styles?: string[];
|
||
brand?: string;
|
||
size?: string;
|
||
price?: number;
|
||
purchase_date?: string;
|
||
image_path?: string;
|
||
tags?: string[];
|
||
notes?: string;
|
||
created_at: string;
|
||
updated_at: string;
|
||
}
|
||
|
||
interface OutfitItemListProps {
|
||
projectId: string;
|
||
onCreateItem?: () => void;
|
||
onEditItem?: (item: OutfitItem) => void;
|
||
}
|
||
|
||
const OutfitItemList: React.FC<OutfitItemListProps> = ({
|
||
projectId,
|
||
onCreateItem,
|
||
onEditItem
|
||
}) => {
|
||
const [items, setItems] = useState<OutfitItem[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [searchTerm, setSearchTerm] = useState('');
|
||
const [selectedCategory, setSelectedCategory] = useState<string>('');
|
||
const [selectedItem, setSelectedItem] = useState<OutfitItem | null>(null);
|
||
const { addNotification } = useNotifications();
|
||
|
||
// 加载服装单品列表
|
||
const loadItems = async () => {
|
||
try {
|
||
setLoading(true);
|
||
const options = {
|
||
project_id: PROJECT_ID,
|
||
category: selectedCategory || null,
|
||
limit: 100,
|
||
offset: 0
|
||
};
|
||
|
||
const result = await invoke('list_outfit_items', { options });
|
||
setItems(result as OutfitItem[]);
|
||
} catch (error) {
|
||
console.error('加载服装单品失败:', error);
|
||
addNotification({
|
||
type: 'error',
|
||
title: '加载失败',
|
||
message: '无法加载服装单品列表'
|
||
});
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
// 删除服装单品
|
||
const handleDeleteItem = async (itemId: string) => {
|
||
if (!window.confirm('确定要删除这个服装单品吗?')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await invoke('delete_outfit_item', { id: itemId });
|
||
|
||
addNotification({
|
||
type: 'success',
|
||
title: '删除成功',
|
||
message: '服装单品已删除'
|
||
});
|
||
|
||
// 重新加载列表
|
||
loadItems();
|
||
} catch (error) {
|
||
console.error('删除服装单品失败:', error);
|
||
addNotification({
|
||
type: 'error',
|
||
title: '删除失败',
|
||
message: error instanceof Error ? error.message : '删除服装单品失败'
|
||
});
|
||
}
|
||
};
|
||
|
||
// 过滤服装单品
|
||
const filteredItems = items.filter(item => {
|
||
const matchesSearch = !searchTerm ||
|
||
item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
item.description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
item.brand?.toLowerCase().includes(searchTerm.toLowerCase());
|
||
|
||
const matchesCategory = !selectedCategory || item.category === selectedCategory;
|
||
|
||
return matchesSearch && matchesCategory;
|
||
});
|
||
|
||
// 获取所有类别
|
||
const categories = Array.from(new Set(items.map(item => item.category))).filter(Boolean);
|
||
|
||
// 格式化价格
|
||
const formatPrice = (price?: number) => {
|
||
if (!price) return '';
|
||
return `¥${price.toFixed(2)}`;
|
||
};
|
||
|
||
// 格式化时间
|
||
const formatTime = (timeString: string) => {
|
||
return new Date(timeString).toLocaleString('zh-CN');
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (projectId) {
|
||
loadItems();
|
||
}
|
||
}, [projectId, selectedCategory]);
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="flex items-center justify-center py-12">
|
||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
||
<span className="ml-2 text-gray-600">加载中...</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* 工具栏 */}
|
||
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
||
<div className="flex flex-col sm:flex-row gap-4 flex-1">
|
||
{/* 搜索框 */}
|
||
<div className="relative flex-1 max-w-md">
|
||
<MagnifyingGlassIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||
<input
|
||
type="text"
|
||
placeholder="搜索服装单品..."
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
className="pl-10 pr-4 py-2 w-full border border-gray-300 rounded-md focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||
/>
|
||
</div>
|
||
|
||
{/* 类别筛选 */}
|
||
<div className="relative">
|
||
<FunnelIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||
<select
|
||
value={selectedCategory}
|
||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||
className="pl-10 pr-8 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary-500 focus:border-primary-500 appearance-none bg-white"
|
||
>
|
||
<option value="">所有类别</option>
|
||
{categories.map(category => (
|
||
<option key={category} value={category}>
|
||
{category}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 添加按钮 */}
|
||
<button
|
||
onClick={onCreateItem}
|
||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||
>
|
||
<PlusIcon className="h-4 w-4 mr-2" />
|
||
添加单品
|
||
</button>
|
||
</div>
|
||
|
||
{/* 统计信息 */}
|
||
<div className="bg-gray-50 rounded-lg p-4">
|
||
<div className="flex items-center justify-between text-sm text-gray-600">
|
||
<span>共 {filteredItems.length} 个服装单品</span>
|
||
{searchTerm || selectedCategory ? (
|
||
<span>从 {items.length} 个单品中筛选</span>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 服装单品列表 */}
|
||
{filteredItems.length === 0 ? (
|
||
<div className="text-center py-12">
|
||
<div className="mx-auto h-12 w-12 text-gray-400 mb-4">
|
||
<PlusIcon className="h-full w-full" />
|
||
</div>
|
||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||
{searchTerm || selectedCategory ? '没有找到匹配的服装单品' : '暂无服装单品'}
|
||
</h3>
|
||
<p className="text-gray-500 mb-4">
|
||
{searchTerm || selectedCategory ? '请尝试调整搜索条件' : '请先上传图片进行AI分析,或手动添加服装单品'}
|
||
</p>
|
||
{!searchTerm && !selectedCategory && (
|
||
<button
|
||
onClick={onCreateItem}
|
||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||
>
|
||
<PlusIcon className="h-4 w-4 mr-2" />
|
||
添加第一个单品
|
||
</button>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||
{filteredItems.map((item) => (
|
||
<div
|
||
key={item.id}
|
||
className="bg-white rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition-shadow overflow-hidden"
|
||
>
|
||
{/* 图片预览 */}
|
||
<div className="aspect-square bg-gray-100 relative">
|
||
{item.image_path ? (
|
||
<img
|
||
src={`file://${item.image_path}`}
|
||
alt={item.name}
|
||
className="w-full h-full object-cover"
|
||
onError={(e) => {
|
||
(e.target as HTMLImageElement).style.display = 'none';
|
||
}}
|
||
/>
|
||
) : (
|
||
<div className="w-full h-full flex items-center justify-center">
|
||
<div className="text-gray-400 text-center">
|
||
<div className="w-12 h-12 mx-auto mb-2 bg-gray-200 rounded-full flex items-center justify-center">
|
||
<PlusIcon className="w-6 h-6" />
|
||
</div>
|
||
<span className="text-sm">暂无图片</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="absolute top-2 right-2 flex space-x-1">
|
||
<button
|
||
onClick={() => setSelectedItem(item)}
|
||
className="p-1.5 bg-white rounded-full shadow-md hover:shadow-lg transition-shadow"
|
||
>
|
||
<EyeIcon className="w-3 h-3 text-gray-600" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 单品信息 */}
|
||
<div className="p-4">
|
||
<div className="flex items-start justify-between mb-2">
|
||
<h4 className="font-medium text-gray-900 truncate flex-1">
|
||
{item.name}
|
||
</h4>
|
||
<span className="ml-2 px-2 py-1 text-xs font-medium bg-gray-100 text-gray-800 rounded-full">
|
||
{item.category}
|
||
</span>
|
||
</div>
|
||
|
||
{item.brand && (
|
||
<p className="text-sm text-gray-600 mb-1">
|
||
品牌: {item.brand}
|
||
</p>
|
||
)}
|
||
|
||
{item.price && (
|
||
<p className="text-sm font-medium text-green-600 mb-2">
|
||
{formatPrice(item.price)}
|
||
</p>
|
||
)}
|
||
|
||
{item.description && (
|
||
<p className="text-sm text-gray-500 mb-3 line-clamp-2">
|
||
{item.description}
|
||
</p>
|
||
)}
|
||
|
||
<p className="text-xs text-gray-400 mb-3">
|
||
创建时间: {formatTime(item.created_at)}
|
||
</p>
|
||
|
||
{/* 操作按钮 */}
|
||
<div className="flex space-x-2">
|
||
<button
|
||
onClick={() => onEditItem?.(item)}
|
||
className="flex-1 px-3 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
|
||
>
|
||
<PencilIcon className="w-4 h-4 inline mr-1" />
|
||
编辑
|
||
</button>
|
||
<button
|
||
onClick={() => handleDeleteItem(item.id)}
|
||
className="px-3 py-2 text-sm font-medium text-red-700 bg-red-100 rounded-md hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
||
>
|
||
<TrashIcon className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* 详情模态框 */}
|
||
{selectedItem && (
|
||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||
<div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
|
||
|
||
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||
<div className="sm:flex sm:items-start">
|
||
<div className="mt-3 text-center sm:mt-0 sm:text-left w-full">
|
||
<h3 className="text-lg leading-6 font-medium text-gray-900 mb-4">
|
||
服装单品详情
|
||
</h3>
|
||
|
||
<div className="space-y-3">
|
||
<div>
|
||
<span className="font-medium text-gray-700">名称:</span>
|
||
<span className="ml-2 text-gray-600">{selectedItem.name}</span>
|
||
</div>
|
||
|
||
<div>
|
||
<span className="font-medium text-gray-700">类别:</span>
|
||
<span className="ml-2 text-gray-600">{selectedItem.category}</span>
|
||
</div>
|
||
|
||
{selectedItem.brand && (
|
||
<div>
|
||
<span className="font-medium text-gray-700">品牌:</span>
|
||
<span className="ml-2 text-gray-600">{selectedItem.brand}</span>
|
||
</div>
|
||
)}
|
||
|
||
{selectedItem.size && (
|
||
<div>
|
||
<span className="font-medium text-gray-700">尺寸:</span>
|
||
<span className="ml-2 text-gray-600">{selectedItem.size}</span>
|
||
</div>
|
||
)}
|
||
|
||
{selectedItem.price && (
|
||
<div>
|
||
<span className="font-medium text-gray-700">价格:</span>
|
||
<span className="ml-2 text-gray-600">{formatPrice(selectedItem.price)}</span>
|
||
</div>
|
||
)}
|
||
|
||
{selectedItem.description && (
|
||
<div>
|
||
<span className="font-medium text-gray-700">描述:</span>
|
||
<p className="mt-1 text-gray-600">{selectedItem.description}</p>
|
||
</div>
|
||
)}
|
||
|
||
{selectedItem.design_styles && selectedItem.design_styles.length > 0 && (
|
||
<div>
|
||
<span className="font-medium text-gray-700">设计风格:</span>
|
||
<div className="mt-1 flex flex-wrap gap-1">
|
||
{selectedItem.design_styles.map((style, index) => (
|
||
<span
|
||
key={index}
|
||
className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-full"
|
||
>
|
||
{style}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{selectedItem.tags && selectedItem.tags.length > 0 && (
|
||
<div>
|
||
<span className="font-medium text-gray-700">标签:</span>
|
||
<div className="mt-1 flex flex-wrap gap-1">
|
||
{selectedItem.tags.map((tag, index) => (
|
||
<span
|
||
key={index}
|
||
className="px-2 py-1 text-xs bg-gray-100 text-gray-800 rounded-full"
|
||
>
|
||
{tag}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div>
|
||
<span className="font-medium text-gray-700">创建时间:</span>
|
||
<span className="ml-2 text-gray-600">{formatTime(selectedItem.created_at)}</span>
|
||
</div>
|
||
|
||
<div>
|
||
<span className="font-medium text-gray-700">更新时间:</span>
|
||
<span className="ml-2 text-gray-600">{formatTime(selectedItem.updated_at)}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||
<button
|
||
type="button"
|
||
onClick={() => setSelectedItem(null)}
|
||
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:ml-3 sm:w-auto sm:text-sm"
|
||
>
|
||
关闭
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default OutfitItemList;
|