Files
mixvideo-v2/apps/desktop/src/components/MaterialSegmentStats.tsx
imeepos 590e254fe1 feat: 添加项目详情/素材管理的MaterialSegment聚合视图功能
- 新增MaterialSegment聚合视图,支持按AI分类和模特聚合展示
- 实现后端MaterialSegmentViewService和相关API命令
- 创建前端React组件:MaterialSegmentView、MaterialSegmentGroup、MaterialSegmentCard等
- 添加MaterialSegment详细信息模态框和批量操作对话框
- 实现搜索、筛选、排序、分页功能
- 集成虚拟滚动和性能优化
- 在ProjectDetails页面添加片段管理选项卡
- 遵循promptx开发规范和UI/UX设计标准
2025-07-15 16:49:08 +08:00

284 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react';
import {
BarChart3,
PieChart,
Clock,
Tag,
Users,
CheckCircle,
AlertCircle,
TrendingUp,
FileVideo
} from 'lucide-react';
import { MaterialSegmentStats as IMaterialSegmentStats, MaterialSegmentViewMode } from '../types/materialSegmentView';
interface MaterialSegmentStatsProps {
stats: IMaterialSegmentStats;
viewMode: MaterialSegmentViewMode;
}
/**
* MaterialSegment统计信息组件
* 遵循 Tauri 开发规范的组件设计模式
*/
export const MaterialSegmentStats: React.FC<MaterialSegmentStatsProps> = ({
stats,
viewMode,
}) => {
// 格式化时长
const formatDuration = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.round(seconds % 60);
if (hours > 0) {
return `${hours}h ${minutes}m ${secs}s`;
} else if (minutes > 0) {
return `${minutes}m ${secs}s`;
} else {
return `${secs}s`;
}
};
// 格式化百分比
const formatPercentage = (value: number) => {
return `${Math.round(value * 100)}%`;
};
// 获取分类覆盖率颜色
const getCoverageColor = (coverage: number) => {
if (coverage >= 0.8) return 'text-green-600 bg-green-100';
if (coverage >= 0.6) return 'text-yellow-600 bg-yellow-100';
return 'text-red-600 bg-red-100';
};
// 获取前5个分类/模特
const getTopItems = () => {
const counts = viewMode === MaterialSegmentViewMode.ByClassification
? stats.classification_counts
: stats.model_counts;
return Object.entries(counts)
.sort(([, a], [, b]) => b - a)
.slice(0, 5);
};
const topItems = getTopItems();
return (
<div className="space-y-6">
{/* 总体统计 */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{/* 总片段数 */}
<div className="bg-blue-50 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-blue-600"></p>
<p className="text-2xl font-bold text-blue-900">{stats.total_segments}</p>
</div>
<FileVideo className="w-8 h-8 text-blue-500" />
</div>
</div>
{/* 已分类片段 */}
<div className="bg-green-50 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-green-600"></p>
<p className="text-2xl font-bold text-green-900">{stats.classified_segments}</p>
</div>
<CheckCircle className="w-8 h-8 text-green-500" />
</div>
</div>
{/* 未分类片段 */}
<div className="bg-orange-50 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-orange-600"></p>
<p className="text-2xl font-bold text-orange-900">{stats.unclassified_segments}</p>
</div>
<AlertCircle className="w-8 h-8 text-orange-500" />
</div>
</div>
{/* 总时长 */}
<div className="bg-purple-50 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-purple-600"></p>
<p className="text-lg font-bold text-purple-900">{formatDuration(stats.total_duration)}</p>
</div>
<Clock className="w-8 h-8 text-purple-500" />
</div>
</div>
</div>
{/* 分类覆盖率 */}
<div className="bg-white rounded-lg border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
<TrendingUp className="w-5 h-5 mr-2 text-gray-600" />
</h3>
<div className={`px-3 py-1 rounded-full text-sm font-medium ${getCoverageColor(stats.classification_coverage)}`}>
{formatPercentage(stats.classification_coverage)}
</div>
</div>
{/* 进度条 */}
<div className="mb-4">
<div className="flex items-center justify-between text-sm text-gray-600 mb-2">
<span> {stats.classified_segments} / {stats.total_segments}</span>
<span>{formatPercentage(stats.classification_coverage)}</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className={`h-3 rounded-full transition-all duration-500 ${
stats.classification_coverage >= 0.8
? 'bg-green-500'
: stats.classification_coverage >= 0.6
? 'bg-yellow-500'
: 'bg-red-500'
}`}
style={{ width: `${stats.classification_coverage * 100}%` }}
/>
</div>
</div>
{/* 分类建议 */}
<div className="text-sm text-gray-600">
{stats.classification_coverage >= 0.8 ? (
<p className="text-green-600"> </p>
) : stats.classification_coverage >= 0.6 ? (
<p className="text-yellow-600"> </p>
) : (
<p className="text-red-600"> AI分类处理</p>
)}
</div>
</div>
{/* 分布统计 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 分类/模特分布 */}
<div className="bg-white rounded-lg border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
{viewMode === MaterialSegmentViewMode.ByClassification ? (
<>
<Tag className="w-5 h-5 mr-2 text-blue-500" />
</>
) : (
<>
<Users className="w-5 h-5 mr-2 text-green-500" />
</>
)}
</h3>
<div className="space-y-3">
{topItems.length > 0 ? (
topItems.map(([name, count], _index) => {
const percentage = (count / stats.total_segments) * 100;
const isUnknown = name === '未分类' || name === '未关联模特';
return (
<div key={name} className="flex items-center space-x-3">
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<span className={`text-sm font-medium ${isUnknown ? 'text-orange-600' : 'text-gray-900'}`}>
{name}
{isUnknown && <AlertCircle className="w-4 h-4 inline ml-1" />}
</span>
<span className="text-sm text-gray-600">{count} ({Math.round(percentage)}%)</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
isUnknown
? 'bg-orange-400'
: viewMode === MaterialSegmentViewMode.ByClassification
? 'bg-blue-500'
: 'bg-green-500'
}`}
style={{ width: `${percentage}%` }}
/>
</div>
</div>
</div>
);
})
) : (
<div className="text-center py-8 text-gray-500">
<PieChart className="w-8 h-8 mx-auto mb-2 text-gray-300" />
<p></p>
</div>
)}
</div>
{/* 显示更多链接 */}
{Object.keys(viewMode === MaterialSegmentViewMode.ByClassification ? stats.classification_counts : stats.model_counts).length > 5 && (
<div className="mt-4 pt-4 border-t border-gray-100">
<button className="text-sm text-blue-600 hover:text-blue-800 transition-colors">
{Object.keys(viewMode === MaterialSegmentViewMode.ByClassification ? stats.classification_counts : stats.model_counts).length}
</button>
</div>
)}
</div>
{/* 时长分析 */}
<div className="bg-white rounded-lg border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<BarChart3 className="w-5 h-5 mr-2 text-purple-500" />
</h3>
<div className="space-y-4">
{/* 平均时长 */}
<div className="flex items-center justify-between p-3 bg-purple-50 rounded-lg">
<span className="text-sm font-medium text-purple-700"></span>
<span className="text-lg font-bold text-purple-900">
{formatDuration(stats.total_duration / (stats.total_segments || 1))}
</span>
</div>
{/* 时长分布建议 */}
<div className="space-y-2 text-sm">
<div className="flex items-center justify-between">
<span className="text-gray-600"></span>
<span className="font-medium">{formatDuration(stats.total_duration)}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-600"></span>
<span className="font-medium">{stats.total_segments}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-600"></span>
<span className="font-medium">
{formatDuration(stats.total_duration * stats.classification_coverage)}
</span>
</div>
</div>
{/* 效率指标 */}
<div className="pt-4 border-t border-gray-100">
<div className="text-xs text-gray-500 mb-2"></div>
<div className="flex items-center space-x-2">
<div className="flex-1 bg-gray-200 rounded-full h-2">
<div
className="bg-purple-500 h-2 rounded-full transition-all duration-300"
style={{ width: `${stats.classification_coverage * 100}%` }}
/>
</div>
<span className="text-xs font-medium text-purple-600">
{formatPercentage(stats.classification_coverage)}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
);
};