fix: 重构项目详情页面

This commit is contained in:
imeepos 2025-07-15 21:20:00 +08:00
parent 05c9694063
commit f67c6357e1
2 changed files with 204 additions and 127 deletions

View File

@ -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<string, number>;
model_counts: Record<string, number>;
total_duration: number;
};
}
/**
* MaterialSegment聚合视图组件
* Tauri
* -
*/
export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ 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<MaterialSegmentView | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [containerHeight, setContainerHeight] = useState(600); // 默认容器高度
const [activeTab, setActiveTab] = useState<'classification' | 'model'>('classification');
const [selectedClassification, setSelectedClassification] = useState<string>('全部');
const [selectedModel, setSelectedModel] = useState<string>('全部');
// 分页状态
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<HTMLDivElement>(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<MaterialSegmentViewProps> = ({ projec
return (
<div className="flex flex-col h-full bg-gray-50">
{/* 头部工具栏 */}
<div className="bg-white border-b border-gray-200 px-6 py-4">
<div className="bg-white border-b border-gray-200 px-6 py-0">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-4">
<h2 className="text-lg font-semibold text-gray-900"></h2>
@ -443,12 +560,6 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
</div>
) : currentView ? (
<div className="h-full overflow-y-auto">
{/* 统计概览 */}
<div className="p-6 bg-white border-b border-gray-200">
<MaterialSegmentStats stats={currentView.stats} viewMode={viewMode} />
</div>
{/* 分组列表 */}
<div className="flex-1">
{getCurrentGroups().length > 0 ? (
<VirtualizedSegmentList

View File

@ -722,18 +722,6 @@ export const ProjectDetails: React.FC = () => {
</div>
</div>
</div>
{/* 片段管理统计 */}
{project && (
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
<MaterialSegmentStats
stats={segmentStats}
viewMode={MaterialSegmentViewMode.ByClassification}
/>
</div>
)}
{/* AI视频分类进度 */}
{project && (
<div>
@ -745,46 +733,24 @@ export const ProjectDetails: React.FC = () => {
/>
</div>
)}
{/* 项目信息 */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
<div className="bg-gray-50 rounded-xl p-4 space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600"></span>
<span className="text-sm text-gray-900 font-mono bg-white px-2 py-1 rounded border">
{project?.path}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600"></span>
<span className="text-sm text-gray-900">
{project?.created_at ? formatTime(project.created_at) : '-'}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600"></span>
<span className="text-sm text-gray-900">
{project?.updated_at ? formatTime(project.updated_at) : '-'}
</span>
</div>
{/* 片段管理统计 */}
{project && (
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
<MaterialSegmentStats
stats={segmentStats}
viewMode={MaterialSegmentViewMode.ByClassification}
/>
</div>
</div>
)}
</div>
)}
{/* 素材管理选项卡 */}
{activeTab === 'materials' && (
<div className="p-4 md:p-6 space-y-6">
{/* AI视频分类进度 */}
{project && (
<VideoClassificationProgress
projectId={project.id}
autoRefresh={true}
refreshInterval={3000}
/>
)}
{/* 素材列表 */}
<div>
<div className="flex items-center justify-between mb-4">