This commit is contained in:
root 2025-07-10 23:53:26 +08:00
parent 538f223217
commit 07d5463836
5 changed files with 315 additions and 5 deletions

View File

@ -429,7 +429,7 @@ class TemplateManager:
segment = { segment = {
'id': segment_data.get('id', ''), 'id': segment_data.get('id', ''),
'type': segment_data.get('type', 'video'), 'type': segment_data.get('type', 'video'),
'name': segment_data.get('name', f'Segment {len(track["segments"]) + 1}'), 'name': segment_data.get('name', f'随机'),
'start_time': start_time, 'start_time': start_time,
'end_time': end_time, 'end_time': end_time,
'duration': duration, 'duration': duration,

View File

@ -0,0 +1,105 @@
import React from 'react'
import { Edit, Copy, Trash2, Info } from 'lucide-react'
interface SegmentContextMenuProps {
isOpen: boolean
position: { x: number; y: number }
onClose: () => void
onEdit: () => void
onCopy?: () => void
onDelete?: () => void
onInfo?: () => void
}
export const SegmentContextMenu: React.FC<SegmentContextMenuProps> = ({
isOpen,
position,
onClose,
onEdit,
onCopy,
onDelete,
onInfo
}) => {
React.useEffect(() => {
if (isOpen) {
const handleClickOutside = () => onClose()
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
}
document.addEventListener('click', handleClickOutside)
document.addEventListener('keydown', handleEscape)
return () => {
document.removeEventListener('click', handleClickOutside)
document.removeEventListener('keydown', handleEscape)
}
}
}, [isOpen, onClose])
if (!isOpen) return null
return (
<div
className="fixed bg-white border border-gray-200 rounded-lg shadow-lg py-2 z-50 min-w-[160px]"
style={{
left: position.x,
top: position.y,
}}
onClick={(e) => e.stopPropagation()}
>
<button
onClick={() => {
onEdit()
onClose()
}}
className="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2"
>
<Edit size={14} />
</button>
{onCopy && (
<button
onClick={() => {
onCopy()
onClose()
}}
className="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2"
>
<Copy size={14} />
</button>
)}
{onInfo && (
<button
onClick={() => {
onInfo()
onClose()
}}
className="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2"
>
<Info size={14} />
</button>
)}
{onDelete && (
<>
<hr className="my-1 border-gray-200" />
<button
onClick={() => {
onDelete()
onClose()
}}
className="w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center gap-2"
>
<Trash2 size={14} />
</button>
</>
)}
</div>
)
}

View File

@ -0,0 +1,135 @@
import React from 'react'
interface TrackSegment {
id: string
type: 'video' | 'audio' | 'image' | 'text' | 'effect'
name: string
start_time: number
end_time: number
duration: number
resource_path?: string
properties?: any
effects?: any[]
}
interface SegmentTooltipProps {
segment: TrackSegment | null
position: { x: number; y: number }
isVisible: boolean
}
export const SegmentTooltip: React.FC<SegmentTooltipProps> = ({
segment,
position,
isVisible
}) => {
const formatTime = (seconds: number): string => {
const minutes = Math.floor(seconds / 60)
const secs = (seconds % 60).toFixed(2)
return `${minutes}:${secs.padStart(5, '0')}`
}
const getTypeLabel = (type: string) => {
switch (type) {
case 'video': return '视频'
case 'audio': return '音频'
case 'image': return '图片'
case 'text': return '文本'
case 'effect': return '特效'
default: return '未知'
}
}
const getTypeColor = (type: string) => {
switch (type) {
case 'video': return 'bg-blue-100 text-blue-800'
case 'audio': return 'bg-green-100 text-green-800'
case 'image': return 'bg-yellow-100 text-yellow-800'
case 'text': return 'bg-purple-100 text-purple-800'
case 'effect': return 'bg-gray-100 text-gray-800'
default: return 'bg-gray-100 text-gray-800'
}
}
if (!isVisible || !segment) return null
return (
<div
className="fixed bg-gray-900 text-white rounded-lg shadow-xl p-4 z-50 max-w-sm pointer-events-none"
style={{
left: position.x + 10,
top: position.y - 10,
transform: 'translateY(-100%)'
}}
>
{/* Segment Name */}
<div className="flex items-center gap-2 mb-3">
<span className={`px-2 py-1 rounded text-xs font-medium ${getTypeColor(segment.type)}`}>
{getTypeLabel(segment.type)}
</span>
<h3 className="font-semibold text-white">{segment.name}</h3>
</div>
{/* Time Information */}
<div className="space-y-2 text-sm">
<div className="grid grid-cols-2 gap-4">
<div>
<span className="text-gray-300">:</span>
<div className="font-mono text-white">{formatTime(segment.start_time)}</div>
</div>
<div>
<span className="text-gray-300">:</span>
<div className="font-mono text-white">{formatTime(segment.end_time)}</div>
</div>
</div>
<div>
<span className="text-gray-300">:</span>
<div className="font-mono text-white">{formatTime(segment.duration)}</div>
</div>
{/* Resource Path */}
{segment.resource_path && (
<div>
<span className="text-gray-300">:</span>
<div className="text-white text-xs break-all">
{segment.resource_path.split('/').pop() || segment.resource_path}
</div>
</div>
)}
{/* Effects */}
{segment.effects && segment.effects.length > 0 && (
<div>
<span className="text-gray-300">:</span>
<div className="text-white text-xs">
{segment.effects.length}
</div>
</div>
)}
{/* Properties */}
{segment.properties && Object.keys(segment.properties).length > 0 && (
<div>
<span className="text-gray-300">:</span>
<div className="text-white text-xs">
{Object.keys(segment.properties).length}
</div>
</div>
)}
</div>
{/* Tooltip Arrow */}
<div
className="absolute bottom-0 left-4 transform translate-y-full"
style={{
width: 0,
height: 0,
borderLeft: '6px solid transparent',
borderRight: '6px solid transparent',
borderTop: '6px solid rgb(17, 24, 39)' // gray-900
}}
/>
</div>
)
}

View File

@ -1,4 +1,6 @@
import React from 'react' import React from 'react'
import { SegmentContextMenu } from './SegmentContextMenu'
import { SegmentTooltip } from './SegmentTooltip'
interface TrackSegment { interface TrackSegment {
id: string id: string
@ -40,6 +42,13 @@ export const TrackTimeline: React.FC<TrackTimelineProps> = ({
}) => { }) => {
const [editingSegmentId, setEditingSegmentId] = React.useState<string | null>(null) const [editingSegmentId, setEditingSegmentId] = React.useState<string | null>(null)
const [editingName, setEditingName] = React.useState('') const [editingName, setEditingName] = React.useState('')
const [contextMenu, setContextMenu] = React.useState<{
isOpen: boolean
position: { x: number; y: number }
segment: TrackSegment | null
}>({ isOpen: false, position: { x: 0, y: 0 }, segment: null })
const [hoveredSegment, setHoveredSegment] = React.useState<TrackSegment | null>(null)
const [mousePosition, setMousePosition] = React.useState({ x: 0, y: 0 })
const getSegmentColor = (type: string) => { const getSegmentColor = (type: string) => {
switch (type) { switch (type) {
case 'video': return 'bg-blue-500 hover:bg-blue-600' case 'video': return 'bg-blue-500 hover:bg-blue-600'
@ -84,8 +93,13 @@ export const TrackTimeline: React.FC<TrackTimelineProps> = ({
const handleSegmentRightClick = (e: React.MouseEvent, segment: TrackSegment) => { const handleSegmentRightClick = (e: React.MouseEvent, segment: TrackSegment) => {
e.preventDefault() e.preventDefault()
setEditingSegmentId(segment.id) e.stopPropagation()
setEditingName(segment.name)
setContextMenu({
isOpen: true,
position: { x: e.clientX, y: e.clientY },
segment
})
} }
const handleNameSubmit = (segmentId: string) => { const handleNameSubmit = (segmentId: string) => {
@ -109,6 +123,32 @@ export const TrackTimeline: React.FC<TrackTimelineProps> = ({
} }
} }
const handleSegmentMouseEnter = (e: React.MouseEvent, segment: TrackSegment) => {
setHoveredSegment(segment)
setMousePosition({ x: e.clientX, y: e.clientY })
onSegmentHover?.(segment)
}
const handleSegmentMouseLeave = () => {
setHoveredSegment(null)
onSegmentHover?.(null)
}
const handleSegmentMouseMove = (e: React.MouseEvent) => {
setMousePosition({ x: e.clientX, y: e.clientY })
}
const handleContextMenuClose = () => {
setContextMenu({ isOpen: false, position: { x: 0, y: 0 }, segment: null })
}
const handleContextMenuEdit = () => {
if (contextMenu.segment) {
setEditingSegmentId(contextMenu.segment.id)
setEditingName(contextMenu.segment.name)
}
}
return ( return (
<div className="border border-gray-200 rounded-lg overflow-hidden"> <div className="border border-gray-200 rounded-lg overflow-hidden">
{/* Track Header */} {/* Track Header */}
@ -160,8 +200,9 @@ export const TrackTimeline: React.FC<TrackTimelineProps> = ({
onClick={() => onSegmentClick?.(segment)} onClick={() => onSegmentClick?.(segment)}
onDoubleClick={() => handleSegmentDoubleClick(segment)} onDoubleClick={() => handleSegmentDoubleClick(segment)}
onContextMenu={(e) => handleSegmentRightClick(e, segment)} onContextMenu={(e) => handleSegmentRightClick(e, segment)}
onMouseEnter={() => onSegmentHover?.(segment)} onMouseEnter={(e) => handleSegmentMouseEnter(e, segment)}
onMouseLeave={() => onSegmentHover?.(null)} onMouseLeave={handleSegmentMouseLeave}
onMouseMove={handleSegmentMouseMove}
title={`${segment.name}\n类型: ${segment.type}\n开始: ${formatTime(segment.start_time)}\n结束: ${formatTime(segment.end_time)}\n时长: ${formatTime(segment.duration)}${segment.resource_path ? `\n资源: ${segment.resource_path}` : ''}`} title={`${segment.name}\n类型: ${segment.type}\n开始: ${formatTime(segment.start_time)}\n结束: ${formatTime(segment.end_time)}\n时长: ${formatTime(segment.duration)}${segment.resource_path ? `\n资源: ${segment.resource_path}` : ''}`}
> >
{editingSegmentId === segment.id ? ( {editingSegmentId === segment.id ? (
@ -197,6 +238,33 @@ export const TrackTimeline: React.FC<TrackTimelineProps> = ({
</div> </div>
)} )}
</div> </div>
{/* Context Menu */}
<SegmentContextMenu
isOpen={contextMenu.isOpen}
position={contextMenu.position}
onClose={handleContextMenuClose}
onEdit={handleContextMenuEdit}
onCopy={() => {
// TODO: Implement copy functionality
console.log('Copy segment:', contextMenu.segment?.name)
}}
onDelete={() => {
// TODO: Implement delete functionality
console.log('Delete segment:', contextMenu.segment?.name)
}}
onInfo={() => {
// TODO: Implement info functionality
console.log('Show info for segment:', contextMenu.segment?.name)
}}
/>
{/* Segment Tooltip */}
<SegmentTooltip
segment={hoveredSegment}
position={mousePosition}
isVisible={!!hoveredSegment && !editingSegmentId && !contextMenu.isOpen}
/>
</div> </div>
) )
} }

View File

@ -1,2 +1,4 @@
export { TimelineRuler } from './TimelineRuler' export { TimelineRuler } from './TimelineRuler'
export { TrackTimeline } from './TrackTimeline' export { TrackTimeline } from './TrackTimeline'
export { SegmentContextMenu } from './SegmentContextMenu'
export { SegmentTooltip } from './SegmentTooltip'