fix: 重构项目详情页面
This commit is contained in:
parent
05c9694063
commit
f67c6357e1
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in New Issue