fix:
This commit is contained in:
parent
538f223217
commit
07d5463836
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue