feat: 模板详情弹框UI/UX全面优化

视觉设计优化:
- 重新设计信息架构,按重要性分层展示
- 优化颜色系统,减少视觉噪音
- 统一间距和字体层次,提升可读性
- 使用渐变背景和卡片设计增强视觉层次

 交互体验改进:
- 实现渐进披露,核心信息优先展示
- 添加可折叠区域,减少信息密度
- 优化状态指示器,更清晰的文件状态展示
- 改进匹配规则编辑器的位置和样式

 响应式适配:
- 移动端友好的布局设计
- 自适应网格系统
- 触摸友好的交互元素
- 优化小屏幕下的信息展示

 信息架构重构:
- 概览页:核心信息卡片 + 可折叠技术详情
- 素材页:主要信息 + 可展开详细信息
- 轨道页:轨道概览 + 片段详情 + 可展开属性

遵循frontend-developer规范,提升用户体验
This commit is contained in:
imeepos
2025-07-15 10:33:05 +08:00
parent 73c2187757
commit 9f222b562d

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { X, Calendar, Clock, Monitor, Layers, FileText, Image, Video, Music, Type, Sparkles, CheckCircle, XCircle, AlertCircle, Upload, Cloud } from 'lucide-react';
import { X, Calendar, Clock, Monitor, Layers, FileText, Image, Video, Music, Type, Sparkles, CheckCircle, XCircle, AlertCircle, Upload, Cloud, ChevronDown, ChevronRight, Info } from 'lucide-react';
import { Template, TemplateMaterial, TemplateMaterialType, TrackType } from '../../types/template';
import { SegmentMatchingRuleEditor } from './SegmentMatchingRuleEditor';
@@ -13,6 +13,31 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
onClose,
}) => {
const [activeTab, setActiveTab] = useState<'overview' | 'materials' | 'tracks'>('overview');
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({});
const [expandedMaterials, setExpandedMaterials] = useState<Record<string, boolean>>({});
const [expandedSegments, setExpandedSegments] = useState<Record<string, boolean>>({});
// 切换展开状态
const toggleSection = (sectionId: string) => {
setExpandedSections(prev => ({
...prev,
[sectionId]: !prev[sectionId]
}));
};
const toggleMaterial = (materialId: string) => {
setExpandedMaterials(prev => ({
...prev,
[materialId]: !prev[materialId]
}));
};
const toggleSegment = (segmentId: string) => {
setExpandedSegments(prev => ({
...prev,
[segmentId]: !prev[segmentId]
}));
};
// 格式化时长
const formatDuration = (microseconds: number) => {
@@ -207,40 +232,41 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
}
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl w-full max-w-4xl mx-4 max-h-[90vh] overflow-hidden">
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-2xl w-full max-w-5xl max-h-[95vh] overflow-hidden">
{/* 模态框头部 */}
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<div>
<h2 className="text-xl font-semibold text-gray-900">{template.name}</h2>
<div className="flex items-center justify-between p-4 sm:p-6 border-b border-gray-200 bg-gradient-to-r from-gray-50 to-white">
<div className="flex-1 min-w-0">
<h2 className="text-lg sm:text-xl font-semibold text-gray-900 truncate">{template.name}</h2>
{template.description && (
<p className="text-sm text-gray-600 mt-1">{template.description}</p>
<p className="text-sm text-gray-600 mt-1 line-clamp-2">{template.description}</p>
)}
</div>
<button
onClick={onClose}
className="p-1 rounded-full hover:bg-gray-100 transition-colors"
className="ml-4 p-2 rounded-full hover:bg-gray-100 transition-colors flex-shrink-0"
>
<X className="w-5 h-5 text-gray-500" />
</button>
</div>
{/* 标签页导航 */}
<div className="border-b border-gray-200">
<nav className="flex space-x-8 px-6">
<div className="border-b border-gray-200 bg-white">
<nav className="flex space-x-4 sm:space-x-8 px-4 sm:px-6 overflow-x-auto">
{tabs.map((tab) => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`flex items-center py-4 px-1 border-b-2 font-medium text-sm transition-colors ${activeTab === tab.id
className={`flex items-center py-3 sm:py-4 px-2 sm:px-1 border-b-2 font-medium text-sm transition-colors whitespace-nowrap ${activeTab === tab.id
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<Icon className="w-4 h-4 mr-2" />
{tab.label}
<Icon className="w-4 h-4 mr-2 flex-shrink-0" />
<span className="hidden sm:inline">{tab.label}</span>
<span className="sm:hidden">{tab.label.charAt(0)}</span>
</button>
);
})}
@@ -248,144 +274,224 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
</div>
{/* 标签页内容 */}
<div className="p-6 overflow-y-auto max-h-[60vh]">
<div className="p-4 sm:p-6 overflow-y-auto max-h-[60vh] sm:max-h-[65vh]">
{activeTab === 'overview' && (
<div className="space-y-6">
{/* 基本信息 */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center mb-2">
<Monitor className="w-4 h-4 text-gray-600 mr-2" />
<span className="text-sm font-medium text-gray-700"></span>
</div>
<div className="text-lg font-semibold text-gray-900">
{template.canvas_config.width}×{template.canvas_config.height}
</div>
<div className="text-xs text-gray-500">{template.canvas_config.ratio}</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center mb-2">
<Clock className="w-4 h-4 text-gray-600 mr-2" />
<span className="text-sm font-medium text-gray-700"></span>
</div>
<div className="text-lg font-semibold text-gray-900">
{formatDuration(template.duration)}
</div>
<div className="text-xs text-gray-500">{template.fps} FPS</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center mb-2">
<FileText className="w-4 h-4 text-gray-600 mr-2" />
<span className="text-sm font-medium text-gray-700"></span>
</div>
<div className="text-lg font-semibold text-gray-900">
{template.materials.length}
</div>
<div className="text-xs text-gray-500"></div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center mb-2">
<Layers className="w-4 h-4 text-gray-600 mr-2" />
<span className="text-sm font-medium text-gray-700"></span>
</div>
<div className="text-lg font-semibold text-gray-900">
{template.tracks.length}
</div>
<div className="text-xs text-gray-500"></div>
</div>
</div>
{/* 模板信息 */}
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="text-sm font-medium text-gray-700 mb-3"></h3>
<div className="grid grid-cols-1 gap-3 text-sm">
<div className="flex items-center">
<FileText className="w-4 h-4 text-gray-500 mr-2" />
<span className="text-gray-600">ID</span>
<span className="ml-1 font-mono text-blue-600 text-xs">{template.id}</span>
</div>
{template.source_file_path && (
<div className="flex items-center">
<FileText className="w-4 h-4 text-gray-500 mr-2" />
<span className="text-gray-600"></span>
<span className="ml-1 text-gray-900 break-all">{template.source_file_path}</span>
<div className="space-y-8">
{/* 核心信息卡片 */}
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-100 rounded-xl p-4 sm:p-6">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6">
<div className="text-center">
<div className="inline-flex items-center justify-center w-12 h-12 bg-blue-100 rounded-lg mb-3">
<Monitor className="w-6 h-6 text-blue-600" />
</div>
)}
<div className="text-2xl font-bold text-gray-900">
{template.canvas_config.width}×{template.canvas_config.height}
</div>
<div className="text-sm text-gray-600 mt-1"></div>
<div className="text-xs text-gray-500">{template.canvas_config.ratio}</div>
</div>
<div className="text-center">
<div className="inline-flex items-center justify-center w-12 h-12 bg-green-100 rounded-lg mb-3">
<Clock className="w-6 h-6 text-green-600" />
</div>
<div className="text-2xl font-bold text-gray-900">
{formatDuration(template.duration)}
</div>
<div className="text-sm text-gray-600 mt-1"></div>
<div className="text-xs text-gray-500">{template.fps} FPS</div>
</div>
<div className="text-center">
<div className="inline-flex items-center justify-center w-12 h-12 bg-purple-100 rounded-lg mb-3">
<FileText className="w-6 h-6 text-purple-600" />
</div>
<div className="text-2xl font-bold text-gray-900">
{template.materials.length}
</div>
<div className="text-sm text-gray-600 mt-1"></div>
</div>
<div className="text-center">
<div className="inline-flex items-center justify-center w-12 h-12 bg-orange-100 rounded-lg mb-3">
<Layers className="w-6 h-6 text-orange-600" />
</div>
<div className="text-2xl font-bold text-gray-900">
{template.tracks.length}
</div>
<div className="text-sm text-gray-600 mt-1"></div>
</div>
</div>
</div>
{/* 创建信息 */}
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="text-sm font-medium text-gray-700 mb-3"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div className="flex items-center">
<Calendar className="w-4 h-4 text-gray-500 mr-2" />
<span className="text-gray-600"></span>
<span className="ml-1 text-gray-900">{formatDate(template.created_at)}</span>
{/* 技术详情 - 可折叠 */}
<div className="border border-gray-200 rounded-lg overflow-hidden">
<button
onClick={() => toggleSection('technical')}
className="w-full px-6 py-4 bg-gray-50 hover:bg-gray-100 transition-colors flex items-center justify-between"
>
<div className="flex items-center space-x-3">
<Info className="w-5 h-5 text-gray-600" />
<span className="font-medium text-gray-900"></span>
</div>
<div className="flex items-center">
<Calendar className="w-4 h-4 text-gray-500 mr-2" />
<span className="text-gray-600"></span>
<span className="ml-1 text-gray-900">{formatDate(template.updated_at)}</span>
{expandedSections.technical ? (
<ChevronDown className="w-5 h-5 text-gray-600" />
) : (
<ChevronRight className="w-5 h-5 text-gray-600" />
)}
</button>
{expandedSections.technical && (
<div className="px-6 py-4 bg-white border-t border-gray-200">
<div className="space-y-4">
<div className="flex items-center justify-between py-2">
<span className="text-sm text-gray-600">ID</span>
<code className="text-xs bg-gray-100 px-2 py-1 rounded font-mono text-blue-600">
{template.id}
</code>
</div>
{template.source_file_path && (
<div className="flex items-start justify-between py-2">
<span className="text-sm text-gray-600"></span>
<span className="text-xs text-gray-900 text-right max-w-md break-all">
{template.source_file_path}
</span>
</div>
)}
</div>
</div>
</div>
)}
</div>
{/* 时间信息 - 可折叠 */}
<div className="border border-gray-200 rounded-lg overflow-hidden">
<button
onClick={() => toggleSection('timestamps')}
className="w-full px-6 py-4 bg-gray-50 hover:bg-gray-100 transition-colors flex items-center justify-between"
>
<div className="flex items-center space-x-3">
<Calendar className="w-5 h-5 text-gray-600" />
<span className="font-medium text-gray-900"></span>
</div>
{expandedSections.timestamps ? (
<ChevronDown className="w-5 h-5 text-gray-600" />
) : (
<ChevronRight className="w-5 h-5 text-gray-600" />
)}
</button>
{expandedSections.timestamps && (
<div className="px-6 py-4 bg-white border-t border-gray-200">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="flex items-center justify-between py-2">
<span className="text-sm text-gray-600"></span>
<span className="text-sm text-gray-900">{formatDate(template.created_at)}</span>
</div>
<div className="flex items-center justify-between py-2">
<span className="text-sm text-gray-600"></span>
<span className="text-sm text-gray-900">{formatDate(template.updated_at)}</span>
</div>
</div>
</div>
)}
</div>
</div>
)}
{activeTab === 'materials' && (
<div className="space-y-4">
<div className="space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-medium text-gray-900">
({template.materials.length})
<h3 className="text-xl font-semibold text-gray-900">
</h3>
<span className="text-sm text-gray-500 bg-gray-100 px-3 py-1 rounded-full">
{template.materials.length}
</span>
</div>
<div className="space-y-3">
<div className="grid gap-4">
{template.materials.map((material) => {
const uploadStatus = getUploadStatusInfo(material);
const fileExistence = getFileExistenceInfo(material);
const isExpanded = expandedMaterials[material.id];
return (
<div key={material.id} className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-3">
{getMaterialIcon(material.material_type)}
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-900 truncate">
{getMaterialName(material)}
<div key={material.id} className="bg-white border border-gray-200 rounded-lg overflow-hidden hover:shadow-md transition-shadow">
{/* 主要信息 */}
<div className="p-4">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-3 flex-1">
<div className="flex-shrink-0">
{getMaterialIcon(material.material_type)}
</div>
<div className="text-xs text-gray-500 mt-1 space-y-1">
<div>ID: <span className="font-mono text-blue-600">{material.id}</span></div>
<div>ID: <span className="font-mono text-purple-600">{material.original_id}</span></div>
<div>: {material.material_type}</div>
{/* 文件存在状态 */}
<div className="flex items-center space-x-1">
{fileExistence.icon}
<span className={fileExistence.color}>{fileExistence.text}</span>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-2">
<h4 className="text-base font-medium text-gray-900 truncate">
{getMaterialName(material)}
</h4>
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
{material.material_type}
</span>
</div>
{/* 上传状态 */}
<div className="flex items-center space-x-1">
{uploadStatus.icon}
<span className={uploadStatus.color}>{uploadStatus.text}</span>
{/* 状态指示器 */}
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-1">
{fileExistence.icon}
<span className={`text-xs font-medium ${fileExistence.color}`}>
{fileExistence.text}
</span>
</div>
<div className="flex items-center space-x-1">
{uploadStatus.icon}
<span className={`text-xs font-medium ${uploadStatus.color}`}>
{uploadStatus.text}
</span>
</div>
</div>
</div>
</div>
{/* 文件信息摘要 */}
<div className="flex-shrink-0 text-right text-xs text-gray-500 space-y-1">
{material.duration && (
<div className="font-medium">{formatDuration(material.duration)}</div>
)}
{material.file_size && (
<div>{formatFileSize(material.file_size)}</div>
)}
{material.width && material.height && (
<div>{material.width}×{material.height}</div>
)}
</div>
{/* 展开按钮 */}
<button
onClick={() => toggleMaterial(material.id)}
className="ml-4 p-1 rounded-full hover:bg-gray-100 transition-colors"
>
{isExpanded ? (
<ChevronDown className="w-4 h-4 text-gray-600" />
) : (
<ChevronRight className="w-4 h-4 text-gray-600" />
)}
</button>
</div>
{/* 详细信息 - 可展开 */}
{isExpanded && (
<div className="mt-4 pt-4 border-t border-gray-100 space-y-4">
{/* 文字素材显示文本内容 */}
{material.material_type === 'Text' && material.metadata && (() => {
const textData = parseTextContent(material.metadata);
return textData ? (
<div className="text-xs text-gray-700 bg-gray-100 p-2 rounded mt-1">
<div className="font-medium text-gray-600 mb-1">:</div>
<div className="break-words mb-2">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<div className="text-sm font-medium text-blue-900 mb-2"></div>
<div className="text-sm text-blue-800 break-words mb-3">
{textData.content?.text || '无文本内容'}
</div>
{(textData.font_family || textData.font_size || textData.color) && (
<div className="text-xs text-gray-500 space-y-1">
<div className="text-xs text-blue-700 space-y-1">
{textData.font_family && (
<div>: {textData.font_family}</div>
)}
@@ -399,37 +505,48 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
)}
</div>
) : (
<div className="text-xs text-gray-500 bg-gray-100 p-2 rounded mt-1">
<div className="text-xs text-gray-500 bg-gray-100 p-2 rounded">
</div>
);
})()}
{/* 非文字素材显示文件路径 */}
{material.material_type !== 'Text' && material.original_path && (
<div className="text-xs text-gray-500 break-all mt-1">
: {material.original_path}
{/* 技术详情 */}
<div className="bg-gray-50 rounded-lg p-3">
<div className="text-sm font-medium text-gray-900 mb-2"></div>
<div className="space-y-2 text-xs text-gray-600">
<div className="flex justify-between">
<span>ID</span>
<code className="bg-white px-2 py-1 rounded font-mono text-blue-600">
{material.id}
</code>
</div>
<div className="flex justify-between">
<span>ID</span>
<code className="bg-white px-2 py-1 rounded font-mono text-purple-600">
{material.original_id}
</code>
</div>
{material.original_path && material.material_type !== 'Text' && (
<div className="flex justify-between items-start">
<span></span>
<span className="text-right max-w-xs break-all">
{material.original_path}
</span>
</div>
)}
{material.remote_url && (
<div className="flex justify-between items-start">
<span>URL</span>
<span className="text-right max-w-xs break-all">
{material.remote_url}
</span>
</div>
)}
</div>
)}
{material.remote_url && (
<div className="text-xs text-gray-500 break-all mt-1">
URL: {material.remote_url}
</div>
)}
</div>
</div>
</div>
<div className="text-right text-xs text-gray-500">
{material.duration && (
<div>: {formatDuration(material.duration)}</div>
)}
{material.file_size && (
<div>: {formatFileSize(material.file_size)}</div>
)}
{material.width && material.height && (
<div>: {material.width}×{material.height}</div>
)}
</div>
)}
</div>
</div>
);
@@ -439,128 +556,188 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
)}
{activeTab === 'tracks' && (
<div className="space-y-4">
<div className="space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-medium text-gray-900">
({template.tracks.length})
<h3 className="text-xl font-semibold text-gray-900">
</h3>
<span className="text-sm text-gray-500 bg-gray-100 px-3 py-1 rounded-full">
{template.tracks.length}
</span>
</div>
<div className="space-y-4">
{template.tracks.map((track) => (
<div key={track.id} className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-2">
{getTrackIcon(track.track_type)}
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-900">
{track.name}
</span>
<div className="text-xs text-gray-500">
<span>ID: <span className="font-mono text-green-600">{track.id}</span></span>
<span className="ml-3">: {track.track_type}</span>
<span className="ml-3">: {track.track_index}</span>
<div key={track.id} className="bg-white border border-gray-200 rounded-lg overflow-hidden">
{/* 轨道头部 */}
<div className="bg-gray-50 px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="flex-shrink-0">
{getTrackIcon(track.track_type)}
</div>
<div>
<h4 className="text-base font-medium text-gray-900">
{track.name}
</h4>
<div className="flex items-center space-x-4 text-xs text-gray-500 mt-1">
<span className="inline-flex items-center px-2 py-1 rounded-full bg-gray-100 text-gray-800">
{track.track_type}
</span>
<span> {track.track_index}</span>
</div>
</div>
</div>
<div className="text-right">
<div className="text-sm font-medium text-gray-900">
{track.segments.length}
</div>
<button
onClick={() => toggleSection(`track-${track.id}`)}
className="text-xs text-blue-600 hover:text-blue-800 mt-1"
>
{expandedSections[`track-${track.id}`] ? '收起详情' : '查看详情'}
</button>
</div>
</div>
<span className="text-xs text-gray-500">
{track.segments.length}
</span>
</div>
{/* 轨道技术详情 */}
{expandedSections[`track-${track.id}`] && (
<div className="px-6 py-4 bg-gray-50 border-b border-gray-200">
<div className="text-xs text-gray-600">
<span>ID: </span>
<code className="bg-white px-2 py-1 rounded font-mono text-green-600">
{track.id}
</code>
</div>
</div>
)}
{/* 片段列表 */}
{track.segments.length > 0 && (
<div className="space-y-2">
<div className="divide-y divide-gray-100">
{track.segments.map((segment) => {
const segmentProps = parseSegmentProperties(segment.properties || null);
const isSegmentExpanded = expandedSegments[segment.id];
return (
<div key={segment.id} className="bg-white p-3 rounded border">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-900">{segment.name}</span>
<span className="text-xs text-gray-500">
{formatDurationWithMs(segment.start_time)} - {formatDurationWithMs(segment.end_time)}
</span>
</div>
<div className="text-xs text-gray-500 space-y-1">
<div>
<span>ID: <span className="font-mono text-orange-600">{segment.id}</span></span>
<span className="ml-3">: {segment.segment_index}</span>
</div>
<div className="flex items-center space-x-4">
<span>: {formatDurationWithMs(segment.duration)}</span>
{segmentProps?.speed && segmentProps.speed !== 1.0 && (
<span className={`px-2 py-0.5 rounded text-xs ${
segmentProps.speed > 1.0
? 'bg-red-100 text-red-800'
: 'bg-green-100 text-green-800'
}`}>
: {segmentProps.speed.toFixed(3)}x {segmentProps.speed > 1.0 ? '(加速)' : '(减速)'}
<div key={segment.id} className="p-4">
{/* 片段主要信息 */}
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<div className="flex items-center space-x-3 mb-2">
<h5 className="text-sm font-medium text-gray-900">
{segment.name}
</h5>
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">
#{segment.segment_index}
</span>
)}
{segmentProps?.speed === 1.0 && (
<span className="bg-gray-100 text-gray-600 px-2 py-0.5 rounded text-xs"></span>
)}
</div>
{/* 播放速度不影响片段时长,移除实际播放时长显示 */}
{/* 时间轴位置信息 */}
{segmentProps?.target_timerange && (
<div className="bg-blue-50 p-2 rounded mt-1">
<div className="text-xs font-medium text-blue-700 mb-1">:</div>
<div className="text-xs text-blue-600 space-y-0.5">
<div>: {formatDurationWithMs(segmentProps.target_timerange.start)}</div>
<div>: {formatDurationWithMs(segmentProps.target_timerange.duration)}</div>
<div>: {formatDurationWithMs(segmentProps.target_timerange.start + segmentProps.target_timerange.duration)}</div>
</div>
</div>
)}
{/* 源素材时间范围 */}
{segmentProps?.source_timerange && (
<div className="bg-purple-50 p-2 rounded mt-1">
<div className="text-xs font-medium text-purple-700 mb-1">:</div>
<div className="text-xs text-purple-600 space-y-0.5">
<div>: {formatDurationWithMs(segmentProps.source_timerange.start)}</div>
<div>: {formatDurationWithMs(segmentProps.source_timerange.duration)}</div>
<div>: {formatDurationWithMs(segmentProps.source_timerange.start + segmentProps.source_timerange.duration)}</div>
</div>
<div className="flex items-center space-x-4 text-xs text-gray-600">
<span className="font-medium">
{formatDurationWithMs(segment.start_time)} - {formatDurationWithMs(segment.end_time)}
</span>
<span>: {formatDurationWithMs(segment.duration)}</span>
{segmentProps?.speed && segmentProps.speed !== 1.0 && (
<span className={`px-2 py-1 rounded text-xs font-medium ${
segmentProps.speed > 1.0
? 'bg-red-100 text-red-700'
: 'bg-green-100 text-green-700'
}`}>
{segmentProps.speed.toFixed(2)}x
</span>
)}
</div>
)}
{/* 其他属性信息 */}
{(segmentProps?.visible !== undefined || segmentProps?.volume !== undefined) && (
<div className="bg-gray-50 p-2 rounded mt-1">
<div className="text-xs font-medium text-gray-700 mb-1">:</div>
<div className="text-xs text-gray-600 space-y-0.5">
{segmentProps?.visible !== undefined && (
<div>: {segmentProps.visible ? '可见' : '隐藏'}</div>
)}
{segmentProps?.volume !== undefined && (
<div>: {(segmentProps.volume * 100).toFixed(0)}%</div>
)}
</div>
</div>
)}
{/* 匹配规则编辑器 */}
<div className="bg-yellow-50 p-2 rounded mt-1">
<SegmentMatchingRuleEditor
segmentId={segment.id}
currentRule={segment.matching_rule}
onRuleUpdated={(newRule) => {
// 可以在这里添加更新后的回调逻辑
console.log('片段匹配规则已更新:', segment.id, newRule);
}}
/>
</div>
{segment.template_material_id && (
<div>
使ID: <span className="font-mono text-blue-600">{segment.template_material_id}</span>
</div>
)}
{!segment.template_material_id && (
<div className="text-gray-400"></div>
)}
<button
onClick={() => toggleSegment(segment.id)}
className="ml-4 p-1 rounded-full hover:bg-gray-100 transition-colors"
>
{isSegmentExpanded ? (
<ChevronDown className="w-4 h-4 text-gray-600" />
) : (
<ChevronRight className="w-4 h-4 text-gray-600" />
)}
</button>
</div>
{/* 匹配规则 - 始终显示 */}
<div className="mb-3">
<SegmentMatchingRuleEditor
segmentId={segment.id}
currentRule={segment.matching_rule}
onRuleUpdated={(newRule) => {
console.log('片段匹配规则已更新:', segment.id, newRule);
}}
/>
</div>
{/* 关联素材信息 */}
{segment.template_material_id && (
<div className="text-xs text-gray-600 mb-3">
<span>: </span>
<code className="bg-blue-50 text-blue-700 px-2 py-1 rounded font-mono">
{segment.template_material_id}
</code>
</div>
)}
{/* 详细属性 - 可展开 */}
{isSegmentExpanded && (
<div className="mt-4 pt-4 border-t border-gray-100 space-y-4">
{/* 技术详情 */}
<div className="bg-gray-50 rounded-lg p-3">
<div className="text-sm font-medium text-gray-900 mb-2"></div>
<div className="text-xs text-gray-600">
<span>ID: </span>
<code className="bg-white px-2 py-1 rounded font-mono text-orange-600">
{segment.id}
</code>
</div>
</div>
{/* 时间轴位置信息 */}
{segmentProps?.target_timerange && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<div className="text-sm font-medium text-blue-900 mb-2"></div>
<div className="text-xs text-blue-700 space-y-1">
<div>: {formatDurationWithMs(segmentProps.target_timerange.start)}</div>
<div>: {formatDurationWithMs(segmentProps.target_timerange.duration)}</div>
<div>: {formatDurationWithMs(segmentProps.target_timerange.start + segmentProps.target_timerange.duration)}</div>
</div>
</div>
)}
{/* 源素材时间范围 */}
{segmentProps?.source_timerange && (
<div className="bg-purple-50 border border-purple-200 rounded-lg p-3">
<div className="text-sm font-medium text-purple-900 mb-2"></div>
<div className="text-xs text-purple-700 space-y-1">
<div>: {formatDurationWithMs(segmentProps.source_timerange.start)}</div>
<div>: {formatDurationWithMs(segmentProps.source_timerange.duration)}</div>
<div>: {formatDurationWithMs(segmentProps.source_timerange.start + segmentProps.source_timerange.duration)}</div>
</div>
</div>
)}
{/* 播放属性 */}
{(segmentProps?.visible !== undefined || segmentProps?.volume !== undefined) && (
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
<div className="text-sm font-medium text-green-900 mb-2"></div>
<div className="text-xs text-green-700 space-y-1">
{segmentProps?.visible !== undefined && (
<div>: {segmentProps.visible ? '可见' : '隐藏'}</div>
)}
{segmentProps?.volume !== undefined && (
<div>: {(segmentProps.volume * 100).toFixed(0)}%</div>
)}
</div>
</div>
)}
</div>
)}
</div>
);
})}
@@ -574,10 +751,10 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
</div>
{/* 模态框底部 */}
<div className="flex items-center justify-end p-6 border-t border-gray-200">
<div className="flex items-center justify-end p-4 sm:p-6 border-t border-gray-200 bg-gray-50">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors shadow-sm"
>
</button>