344 lines
15 KiB
TypeScript
344 lines
15 KiB
TypeScript
import React from 'react';
|
||
import {
|
||
X,
|
||
Play,
|
||
Clock,
|
||
Tag,
|
||
Users,
|
||
Star,
|
||
TrendingUp,
|
||
FileVideo,
|
||
MapPin,
|
||
Zap,
|
||
AlertCircle,
|
||
Info
|
||
} from 'lucide-react';
|
||
import { SegmentWithDetails } from '../types/materialSegmentView';
|
||
|
||
interface MaterialSegmentDetailModalProps {
|
||
segmentWithDetails: SegmentWithDetails;
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
onPlay?: () => void;
|
||
}
|
||
|
||
/**
|
||
* MaterialSegment详细信息模态框组件
|
||
* 遵循 Tauri 开发规范的组件设计模式
|
||
*/
|
||
export const MaterialSegmentDetailModal: React.FC<MaterialSegmentDetailModalProps> = ({
|
||
segmentWithDetails,
|
||
isOpen,
|
||
onClose,
|
||
onPlay,
|
||
}) => {
|
||
const { segment, material_name, material_type, classification, model } = segmentWithDetails;
|
||
|
||
if (!isOpen) return null;
|
||
|
||
// 格式化时长
|
||
const formatDuration = (seconds: number) => {
|
||
const minutes = Math.floor(seconds / 60);
|
||
const secs = Math.round(seconds % 60);
|
||
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
||
};
|
||
|
||
// 格式化时间戳
|
||
const formatTimestamp = (seconds: number) => {
|
||
const minutes = Math.floor(seconds / 60);
|
||
const secs = Math.round(seconds % 60);
|
||
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
||
};
|
||
|
||
// 格式化日期
|
||
const formatDate = (dateString: string) => {
|
||
return new Date(dateString).toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: 'short',
|
||
day: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit'
|
||
});
|
||
};
|
||
|
||
// 获取置信度颜色和描述
|
||
const getConfidenceInfo = (confidence: number) => {
|
||
if (confidence >= 0.9) return { color: 'text-green-600 bg-green-100', desc: '非常高' };
|
||
if (confidence >= 0.8) return { color: 'text-green-600 bg-green-100', desc: '高' };
|
||
if (confidence >= 0.6) return { color: 'text-yellow-600 bg-yellow-100', desc: '中等' };
|
||
if (confidence >= 0.4) return { color: 'text-orange-600 bg-orange-100', desc: '较低' };
|
||
return { color: 'text-red-600 bg-red-100', desc: '低' };
|
||
};
|
||
|
||
// 获取质量评分颜色和描述
|
||
const getQualityInfo = (score: number) => {
|
||
if (score >= 0.9) return { color: 'text-green-600', desc: '优秀' };
|
||
if (score >= 0.8) return { color: 'text-green-600', desc: '良好' };
|
||
if (score >= 0.6) return { color: 'text-yellow-600', desc: '一般' };
|
||
if (score >= 0.4) return { color: 'text-orange-600', desc: '较差' };
|
||
return { color: 'text-red-600', desc: '差' };
|
||
};
|
||
|
||
return (
|
||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden">
|
||
{/* 模态框头部 */}
|
||
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||
<div className="flex items-center space-x-3">
|
||
<FileVideo className="w-6 h-6 text-blue-500" />
|
||
<div>
|
||
<h2 className="text-xl font-semibold text-gray-900">{material_name}</h2>
|
||
<p className="text-sm text-gray-600">片段 #{segment.segment_index}</p>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={onClose}
|
||
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
||
>
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* 模态框内容 */}
|
||
<div className="p-6 overflow-y-auto max-h-[calc(90vh-120px)]">
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
{/* 左侧:基本信息 */}
|
||
<div className="space-y-6">
|
||
{/* 片段基本信息 */}
|
||
<div className="bg-gray-50 rounded-lg p-4">
|
||
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
|
||
<Info className="w-5 h-5 mr-2 text-blue-500" />
|
||
基本信息
|
||
</h3>
|
||
<div className="space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm text-gray-600">片段ID</span>
|
||
<span className="text-sm font-mono text-gray-900 bg-gray-200 px-2 py-1 rounded">
|
||
{segment.id.slice(-12)}
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm text-gray-600">素材类型</span>
|
||
<span className="text-sm text-gray-900">{material_type}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm text-gray-600">片段索引</span>
|
||
<span className="text-sm text-gray-900">#{segment.segment_index}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm text-gray-600">创建时间</span>
|
||
<span className="text-sm text-gray-900">{formatDate(segment.created_at)}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 时间信息 */}
|
||
<div className="bg-blue-50 rounded-lg p-4">
|
||
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
|
||
<Clock className="w-5 h-5 mr-2 text-blue-500" />
|
||
时间信息
|
||
</h3>
|
||
<div className="space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm text-gray-600">开始时间</span>
|
||
<span className="text-sm font-mono text-blue-900 bg-blue-200 px-2 py-1 rounded">
|
||
{formatTimestamp(segment.start_time)}
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm text-gray-600">结束时间</span>
|
||
<span className="text-sm font-mono text-blue-900 bg-blue-200 px-2 py-1 rounded">
|
||
{formatTimestamp(segment.end_time)}
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm text-gray-600">片段时长</span>
|
||
<span className="text-lg font-bold text-blue-900">
|
||
{formatDuration(segment.duration)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 文件信息 */}
|
||
<div className="bg-purple-50 rounded-lg p-4">
|
||
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
|
||
<MapPin className="w-5 h-5 mr-2 text-purple-500" />
|
||
文件信息
|
||
</h3>
|
||
<div className="space-y-3">
|
||
<div>
|
||
<span className="text-sm text-gray-600 block mb-1">文件路径</span>
|
||
<span className="text-sm font-mono text-gray-900 bg-gray-200 px-2 py-1 rounded block break-all">
|
||
{segment.file_path}
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm text-gray-600">文件名</span>
|
||
<span className="text-sm text-gray-900 truncate max-w-48">
|
||
{segment.file_path.split('/').pop()}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 播放控制 */}
|
||
<div className="bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg p-4">
|
||
<button
|
||
onClick={onPlay}
|
||
className="w-full flex items-center justify-center space-x-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white py-3 px-4 rounded-lg hover:from-blue-600 hover:to-purple-600 transition-all duration-200 shadow-md hover:shadow-lg"
|
||
>
|
||
<Play className="w-5 h-5" />
|
||
<span className="font-medium">播放片段</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 右侧:AI分析信息 */}
|
||
<div className="space-y-6">
|
||
{/* AI分类信息 */}
|
||
{classification ? (
|
||
<div className="bg-green-50 rounded-lg p-4">
|
||
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
|
||
<Tag className="w-5 h-5 mr-2 text-green-500" />
|
||
AI分类结果
|
||
</h3>
|
||
<div className="space-y-4">
|
||
{/* 分类名称和置信度 */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<span className="text-lg font-semibold text-gray-900">{classification.category}</span>
|
||
{classification.product_match && (
|
||
<span className="ml-2 text-xs bg-green-200 text-green-800 px-2 py-1 rounded-full">
|
||
商品匹配
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div className={`px-3 py-1 rounded-full text-sm font-medium ${getConfidenceInfo(classification.confidence).color}`}>
|
||
{Math.round(classification.confidence * 100)}% ({getConfidenceInfo(classification.confidence).desc})
|
||
</div>
|
||
</div>
|
||
|
||
{/* 质量评分 */}
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm text-gray-600 flex items-center">
|
||
<TrendingUp className="w-4 h-4 mr-1" />
|
||
质量评分
|
||
</span>
|
||
<div className="flex items-center space-x-2">
|
||
<Star className={`w-4 h-4 ${getQualityInfo(classification.quality_score).color}`} />
|
||
<span className={`text-sm font-medium ${getQualityInfo(classification.quality_score).color}`}>
|
||
{Math.round(classification.quality_score * 100)}% ({getQualityInfo(classification.quality_score).desc})
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 分类理由 */}
|
||
<div>
|
||
<span className="text-sm text-gray-600 block mb-2">分类理由</span>
|
||
<p className="text-sm text-gray-900 bg-white p-3 rounded border">
|
||
{classification.reasoning}
|
||
</p>
|
||
</div>
|
||
|
||
{/* 关键特征 */}
|
||
{classification.features.length > 0 && (
|
||
<div>
|
||
<span className="text-sm text-gray-600 block mb-2">关键特征</span>
|
||
<div className="flex flex-wrap gap-2">
|
||
{classification.features.map((feature, index) => (
|
||
<span
|
||
key={index}
|
||
className="text-xs bg-green-200 text-green-800 px-2 py-1 rounded-full"
|
||
>
|
||
{feature}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="bg-orange-50 rounded-lg p-4">
|
||
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
|
||
<AlertCircle className="w-5 h-5 mr-2 text-orange-500" />
|
||
AI分类状态
|
||
</h3>
|
||
<div className="text-center py-6">
|
||
<AlertCircle className="w-12 h-12 text-orange-400 mx-auto mb-3" />
|
||
<p className="text-gray-600 mb-4">该片段尚未进行AI分类</p>
|
||
<button className="inline-flex items-center px-4 py-2 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors">
|
||
<Zap className="w-4 h-4 mr-2" />
|
||
立即分类
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 模特信息 */}
|
||
{model ? (
|
||
<div className="bg-blue-50 rounded-lg p-4">
|
||
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
|
||
<Users className="w-5 h-5 mr-2 text-blue-500" />
|
||
关联模特
|
||
</h3>
|
||
<div className="space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm text-gray-600">模特ID</span>
|
||
<span className="text-sm font-mono text-blue-900 bg-blue-200 px-2 py-1 rounded">
|
||
{model.id.slice(-8)}
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm text-gray-600">模特名称</span>
|
||
<span className="text-lg font-semibold text-blue-900">{model.name}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm text-gray-600">模特类型</span>
|
||
<span className="text-sm text-gray-900">{model.model_type}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="bg-gray-50 rounded-lg p-4">
|
||
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
|
||
<Users className="w-5 h-5 mr-2 text-gray-500" />
|
||
模特关联
|
||
</h3>
|
||
<div className="text-center py-6">
|
||
<Users className="w-12 h-12 text-gray-300 mx-auto mb-3" />
|
||
<p className="text-gray-600 mb-4">该片段尚未关联模特</p>
|
||
<button className="inline-flex items-center px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
|
||
<Users className="w-4 h-4 mr-2" />
|
||
关联模特
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 模态框底部 */}
|
||
<div className="flex items-center justify-end space-x-3 p-6 border-t border-gray-200 bg-gray-50">
|
||
<button
|
||
onClick={onClose}
|
||
className="px-4 py-2 text-gray-600 hover:text-gray-800 transition-colors"
|
||
>
|
||
关闭
|
||
</button>
|
||
<button
|
||
onClick={onPlay}
|
||
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||
>
|
||
<Play className="w-4 h-4 mr-2" />
|
||
播放片段
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|