From f67c6357e1453051011251babce8561b813c7dcb Mon Sep 17 00:00:00 2001 From: imeepos Date: Tue, 15 Jul 2025 21:20:00 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=87=8D=E6=9E=84=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/MaterialSegmentView.tsx | 275 ++++++++++++------ apps/desktop/src/pages/ProjectDetails.tsx | 56 +--- 2 files changed, 204 insertions(+), 127 deletions(-) diff --git a/apps/desktop/src/components/MaterialSegmentView.tsx b/apps/desktop/src/components/MaterialSegmentView.tsx index 867e894..ad753f9 100644 --- a/apps/desktop/src/components/MaterialSegmentView.tsx +++ b/apps/desktop/src/components/MaterialSegmentView.tsx @@ -1,105 +1,222 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { - Grid, - List, Search, Filter, - ChevronDown, - ChevronRight, - BarChart3, - Users, Clock, Tag, + Users, + Play, + Image, + Video, RefreshCw, - Trash2, - RotateCcw + MoreHorizontal, + Eye, + Edit, + Trash2 } from 'lucide-react'; -import { useMaterialSegmentViewStore } from '../store/materialSegmentViewStore'; -import { MaterialSegmentViewMode, SegmentSortField, SortDirection, SegmentWithDetails } from '../types/materialSegmentView'; -import { MaterialSegmentStats } from './MaterialSegmentStats'; -import { MaterialSegmentFilters } from './MaterialSegmentFilters'; -import { MaterialSegmentDeleteDialog } from './MaterialSegmentDeleteDialog'; -import { MaterialSegmentReclassifyDialog } from './MaterialSegmentReclassifyDialog'; -import { MaterialSegmentModelDialog } from './MaterialSegmentModelDialog'; -import { VirtualizedSegmentList } from './VirtualizedSegmentList'; -import { MaterialSegmentPagination } from './MaterialSegmentPagination'; -import { LoadingSpinner } from './LoadingSpinner'; -import { ErrorMessage } from './ErrorMessage'; +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; + }; +} + /** - * MaterialSegment聚合视图组件 - * 遵循 Tauri 开发规范的组件设计模式 + * 素材片段管理组件 - 多条件检索标签页风格 */ export const MaterialSegmentView: React.FC = ({ projectId }) => { - const { - currentView, - viewMode, - isLoading, - error, - currentQuery, - selectedSegmentIds, - expandedGroups, - loadProjectSegmentView, - setViewMode, - updateQuery, - clearQuery, - clearSelection, - selectSegment, - deselectSegment, - expandAllGroups, - collapseAllGroups, - toggleGroup, - refreshCurrentView, - clearError, - } = useMaterialSegmentViewStore(); - - const [showFilters, setShowFilters] = useState(false); + const [segmentView, setSegmentView] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); const [searchTerm, setSearchTerm] = useState(''); - const [containerHeight, setContainerHeight] = useState(600); // 默认容器高度 + const [activeTab, setActiveTab] = useState<'classification' | 'model'>('classification'); + const [selectedClassification, setSelectedClassification] = useState('全部'); + const [selectedModel, setSelectedModel] = useState('全部'); - // 分页状态 - const [currentPage, setCurrentPage] = useState(1); - const [pageSize, setPageSize] = useState(20); + // 加载数据 + 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); + } + }; - // 对话框状态 - const [showDeleteDialog, setShowDeleteDialog] = useState(false); - const [showReclassifyDialog, setShowReclassifyDialog] = useState(false); - const [showModelDialog, setShowModelDialog] = useState(false); - const [dialogLoading, setDialogLoading] = useState(false); + // 初始加载 + useEffect(() => { + if (projectId) { + loadSegmentView(); + } + }, [projectId]); - // 容器引用 - const containerRef = React.useRef(null); + // 格式化时长 + 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 getSelectedSegments = (): SegmentWithDetails[] => { - if (!currentView) return []; + // 获取分类选项 + const classificationOptions = useMemo(() => { + if (!segmentView) return [{ label: '全部', value: '全部', count: 0 }]; - const allSegments: SegmentWithDetails[] = []; + const options = [ + { + label: '全部', + value: '全部', + count: segmentView.stats.total_segments + } + ]; - if (viewMode === MaterialSegmentViewMode.ByClassification) { - currentView.by_classification.forEach(group => { - group.segments.forEach(segment => { - if (selectedSegmentIds.has(segment.segment.id)) { - allSegments.push(segment); - } - }); + 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 { - currentView.by_model.forEach(group => { - group.segments.forEach(segment => { - if (selectedSegmentIds.has(segment.segment.id)) { - allSegments.push(segment); - } + 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; + } + } } - return allSegments; - }; + // 应用搜索过滤 + 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]); // 初始加载数据 useEffect(() => { @@ -272,7 +389,7 @@ export const MaterialSegmentView: React.FC = ({ projec return (
{/* 头部工具栏 */} -
+

素材片段管理

@@ -443,12 +560,6 @@ export const MaterialSegmentView: React.FC = ({ projec
) : currentView ? (
- {/* 统计概览 */} -
- -
- - {/* 分组列表 */}
{getCurrentGroups().length > 0 ? ( {
- - {/* 片段管理统计 */} - {project && ( -
-

片段统计

- -
- )} - {/* AI视频分类进度 */} {project && (
@@ -745,46 +733,24 @@ export const ProjectDetails: React.FC = () => { />
)} - - {/* 项目信息 */} -
-

项目信息

-
-
- 项目路径 - - {project?.path} - -
-
- 创建时间 - - {project?.created_at ? formatTime(project.created_at) : '-'} - -
-
- 最后更新 - - {project?.updated_at ? formatTime(project.updated_at) : '-'} - -
+ {/* 片段管理统计 */} + {project && ( +
+

片段统计

+
-
+ )} + +
)} {/* 素材管理选项卡 */} {activeTab === 'materials' && (
- {/* AI视频分类进度 */} - {project && ( - - )} - {/* 素材列表 */}