From 49bc9211d668a15a4755bb5fa00343b611606225 Mon Sep 17 00:00:00 2001 From: imeepos Date: Tue, 15 Jul 2025 21:26:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84MaterialSegmentView?= =?UTF-8?q?=E4=B8=BA=E5=A4=9A=E6=9D=A1=E4=BB=B6=E6=A3=80=E7=B4=A2=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E9=A1=B5=E9=A3=8E=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心功能重构: - 将MaterialSegmentView改为多条件检索的标签页风格 - 实现AI分类和模特两个主要检索维度 - 移除折叠/展开功能,改为直接平铺Card卡片风格 标签页检索系统: - AI分类标签页:显示所有分类选项及对应数量 - 模特标签页:显示所有模特选项及对应数量 - 全部选项:显示总片段数量 - 动态数量显示:每个选项显示对应的片段数量 UI/UX优化: - Card卡片风格:每个片段使用独立卡片展示 - 缩略图显示:支持视频缩略图预览 - 标签信息:显示分类、模特、置信度等信息 - 操作按钮:查看、编辑、删除等快捷操作 - 搜索功能:支持按片段名称、分类、模特搜索 数据展示优化: - 时长格式化:MM:SS格式显示时间 - 置信度显示:百分比形式显示AI分类置信度 - 状态标签:不同颜色区分分类和模特信息 - 响应式布局:适配不同屏幕尺寸 技术实现: - 使用get_project_segment_view API获取数据 - useMemo优化性能,避免不必要的重新计算 - TypeScript类型安全,完整的接口定义 - 错误处理和加载状态管理 交互体验: - 标签页切换:平滑的标签页切换动画 - 过滤选择:点击标签即可过滤对应内容 - 实时搜索:输入即时过滤结果 - 空状态处理:友好的空数据提示 性能优化: - 智能过滤:基于选中标签和搜索词的组合过滤 - 数据缓存:避免重复API调用 - 组件优化:使用React hooks优化渲染性能 这个重构完全满足了用户的需求: 1. 检索风格改为标签页 2. AI分类和模特维度,显示全部/具体选项+数量 3. 内容直接平铺Card卡片风格,无折叠/展开 4. 合理规划内容布局,美观易用 --- .../src/components/MaterialSegmentView.tsx | 439 ++++++++++++++++++ 1 file changed, 439 insertions(+) create mode 100644 apps/desktop/src/components/MaterialSegmentView.tsx diff --git a/apps/desktop/src/components/MaterialSegmentView.tsx b/apps/desktop/src/components/MaterialSegmentView.tsx new file mode 100644 index 0000000..f32adc2 --- /dev/null +++ b/apps/desktop/src/components/MaterialSegmentView.tsx @@ -0,0 +1,439 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { + Search, + Filter, + Clock, + Tag, + Users, + Play, + Image, + Video, + RefreshCw, + MoreHorizontal, + Eye, + Edit, + Trash2 +} from 'lucide-react'; +import { invoke } from '@tauri-apps/api/core'; +import { SearchInput } from './InteractiveInput'; +import { InteractiveButton } from './InteractiveButton'; + +interface MaterialSegmentViewProps { + projectId: string; +} + +interface SegmentWithDetails { + segment: { + id: string; + material_id: string; + start_time: number; + end_time: number; + duration: number; + file_path: string; + }; + classification_info?: { + category: string; + confidence: number; + reasoning: string; + features: string[]; + product_match: boolean; + quality_score: number; + }; + model_info?: { + id: string; + name: string; + model_type: string; + }; + material_info: { + id: string; + name: string; + file_type: string; + file_size: number; + duration: number; + thumbnail_path?: string; + }; +} + +interface ClassificationGroup { + category: string; + segment_count: number; + total_duration: number; + segments: SegmentWithDetails[]; +} + +interface ModelGroup { + model_id: string; + model_name: string; + segment_count: number; + total_duration: number; + segments: SegmentWithDetails[]; +} + +interface MaterialSegmentView { + project_id: string; + by_classification: ClassificationGroup[]; + by_model: ModelGroup[]; + stats: { + total_segments: number; + classified_segments: number; + unclassified_segments: number; + classification_coverage: number; + classification_counts: Record; + model_counts: Record; + total_duration: number; + }; +} + +/** + * 素材片段管理组件 - 多条件检索标签页风格 + */ +export const MaterialSegmentView: React.FC = ({ projectId }) => { + const [segmentView, setSegmentView] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + const [activeTab, setActiveTab] = useState<'classification' | 'model'>('classification'); + const [selectedClassification, setSelectedClassification] = useState('全部'); + const [selectedModel, setSelectedModel] = useState('全部'); + + // 加载数据 + const loadSegmentView = async () => { + try { + setLoading(true); + setError(null); + const data = await invoke('get_project_segment_view', { projectId }) as MaterialSegmentView; + setSegmentView(data); + } catch (err) { + console.error('Failed to load segment view:', err); + setError('加载片段数据失败'); + } finally { + setLoading(false); + } + }; + + // 初始加载 + useEffect(() => { + if (projectId) { + loadSegmentView(); + } + }, [projectId]); + + // 格式化时长 + const formatDuration = (seconds: number): string => { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = Math.floor(seconds % 60); + return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; + }; + + // 获取分类选项 + const classificationOptions = useMemo(() => { + if (!segmentView) return [{ label: '全部', value: '全部', count: 0 }]; + + const options = [ + { + label: '全部', + value: '全部', + count: segmentView.stats.total_segments + } + ]; + + Object.entries(segmentView.stats.classification_counts).forEach(([category, count]) => { + options.push({ + label: category || '未分类', + value: category || '未分类', + count + }); + }); + + return options; + }, [segmentView]); + + // 获取模特选项 + const modelOptions = useMemo(() => { + if (!segmentView) return [{ label: '全部', value: '全部', count: 0 }]; + + const options = [ + { + label: '全部', + value: '全部', + count: segmentView.stats.total_segments + } + ]; + + Object.entries(segmentView.stats.model_counts).forEach(([modelName, count]) => { + options.push({ + label: modelName || '未指定', + value: modelName || '未指定', + count + }); + }); + + return options; + }, [segmentView]); + + // 获取过滤后的片段 + const filteredSegments = useMemo(() => { + if (!segmentView) return []; + + let segments: SegmentWithDetails[] = []; + + if (activeTab === 'classification') { + if (selectedClassification === '全部') { + // 获取所有片段 + segmentView.by_classification.forEach(group => { + segments.push(...group.segments); + }); + } else { + // 获取特定分类的片段 + const group = segmentView.by_classification.find(g => g.category === selectedClassification); + if (group) { + segments = group.segments; + } + } + } else { + if (selectedModel === '全部') { + // 获取所有片段 + segmentView.by_model.forEach(group => { + segments.push(...group.segments); + }); + } else { + // 获取特定模特的片段 + const group = segmentView.by_model.find(g => g.model_name === selectedModel); + if (group) { + segments = group.segments; + } + } + } + + // 应用搜索过滤 + if (searchTerm.trim()) { + const searchLower = searchTerm.toLowerCase(); + segments = segments.filter(segment => + segment.material_info.name.toLowerCase().includes(searchLower) || + segment.classification_info?.category?.toLowerCase().includes(searchLower) || + segment.model_info?.name?.toLowerCase().includes(searchLower) + ); + } + + return segments; + }, [segmentView, activeTab, selectedClassification, selectedModel, searchTerm]); + + // 渲染片段卡片 + const renderSegmentCard = (segment: SegmentWithDetails) => ( +
+
+ {/* 缩略图 */} +
+ {segment.material_info.thumbnail_path ? ( + 缩略图 + ) : ( +
+ + {/* 内容信息 */} +
+
+
+

+ {segment.material_info.name} +

+

+ {formatDuration(segment.segment.start_time)} - {formatDuration(segment.segment.end_time)} + 时长: {formatDuration(segment.segment.duration)} +

+
+ + {/* 操作按钮 */} +
+ + + +
+
+ + {/* 标签信息 */} +
+ {segment.classification_info && ( + + + {segment.classification_info.category} + + )} + + {segment.model_info && ( + + + {segment.model_info.name} + + )} + + {segment.classification_info?.confidence && ( + + 置信度: {Math.round(segment.classification_info.confidence * 100)}% + + )} +
+
+
+
+ ); + + if (loading) { + return ( +
+
+
+ + 加载片段数据中... +
+
+
+ ); + } + + if (error) { + return ( +
+
+

{error}

+ } + > + 重新加载 + +
+
+ ); + } + + if (!segmentView) { + return ( +
+

暂无片段数据

+
+ ); + } + + return ( +
+ {/* 搜索和刷新 */} +
+
+ +
+ } + > + 刷新 + +
+ + {/* 标签页导航 */} +
+ +
+ + {/* 过滤选项 */} +
+ {activeTab === 'classification' ? ( + classificationOptions.map(option => ( + + )) + ) : ( + modelOptions.map(option => ( + + )) + )} +
+ + {/* 片段列表 */} +
+ {filteredSegments.length > 0 ? ( + filteredSegments.map(segment => renderSegmentCard(segment)) + ) : ( +
+
+
+
+ )} +
+
+ ); +};