- 新增MaterialSegment聚合视图,支持按AI分类和模特聚合展示 - 实现后端MaterialSegmentViewService和相关API命令 - 创建前端React组件:MaterialSegmentView、MaterialSegmentGroup、MaterialSegmentCard等 - 添加MaterialSegment详细信息模态框和批量操作对话框 - 实现搜索、筛选、排序、分页功能 - 集成虚拟滚动和性能优化 - 在ProjectDetails页面添加片段管理选项卡 - 遵循promptx开发规范和UI/UX设计标准
284 lines
11 KiB
TypeScript
284 lines
11 KiB
TypeScript
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>
|
||
);
|
||
};
|