feat: 添加项目详情/素材管理的MaterialSegment聚合视图功能

- 新增MaterialSegment聚合视图,支持按AI分类和模特聚合展示
- 实现后端MaterialSegmentViewService和相关API命令
- 创建前端React组件:MaterialSegmentView、MaterialSegmentGroup、MaterialSegmentCard等
- 添加MaterialSegment详细信息模态框和批量操作对话框
- 实现搜索、筛选、排序、分页功能
- 集成虚拟滚动和性能优化
- 在ProjectDetails页面添加片段管理选项卡
- 遵循promptx开发规范和UI/UX设计标准
This commit is contained in:
imeepos
2025-07-15 16:49:08 +08:00
parent bab1dfc5fd
commit 590e254fe1
24 changed files with 4340 additions and 3 deletions

View File

@@ -0,0 +1,283 @@
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>
);
};