185 lines
6.1 KiB
TypeScript
185 lines
6.1 KiB
TypeScript
import React, { useState } from 'react'
|
|
import { Play, Pause, Music, Trash2, BarChart3, Clock, HardDrive, Hash } from 'lucide-react'
|
|
import { AudioFile, AudioService } from '../services/audioService'
|
|
|
|
interface AudioCardProps {
|
|
audio: AudioFile
|
|
onDelete: (audioId: string) => void
|
|
onShowChart: (audio: AudioFile) => void
|
|
}
|
|
|
|
const AudioCard: React.FC<AudioCardProps> = ({
|
|
audio,
|
|
onDelete,
|
|
onShowChart
|
|
}) => {
|
|
const [isPlaying, setIsPlaying] = useState(false)
|
|
const [audioElement, setAudioElement] = useState<HTMLAudioElement | null>(null)
|
|
|
|
const handlePlayPause = () => {
|
|
if (!audioElement) {
|
|
// 创建音频元素
|
|
const audio_el = new Audio(`file://${audio.file_path}`)
|
|
audio_el.addEventListener('ended', () => setIsPlaying(false))
|
|
audio_el.addEventListener('error', (e) => {
|
|
console.error('Audio playback error:', e)
|
|
setIsPlaying(false)
|
|
})
|
|
setAudioElement(audio_el)
|
|
|
|
audio_el.play()
|
|
.then(() => setIsPlaying(true))
|
|
.catch(err => {
|
|
console.error('Failed to play audio:', err)
|
|
setIsPlaying(false)
|
|
})
|
|
} else {
|
|
if (isPlaying) {
|
|
audioElement.pause()
|
|
setIsPlaying(false)
|
|
} else {
|
|
audioElement.play()
|
|
.then(() => setIsPlaying(true))
|
|
.catch(err => {
|
|
console.error('Failed to play audio:', err)
|
|
setIsPlaying(false)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
const handleDelete = () => {
|
|
if (audioElement) {
|
|
audioElement.pause()
|
|
setAudioElement(null)
|
|
setIsPlaying(false)
|
|
}
|
|
onDelete(audio.id)
|
|
}
|
|
|
|
return (
|
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow">
|
|
{/* 音频信息头部 */}
|
|
<div className="p-4 bg-gradient-to-r from-purple-500 to-pink-500 text-white">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-3">
|
|
<div className="p-2 bg-white/20 rounded-lg">
|
|
<Music size={24} />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-medium truncate max-w-48" title={audio.filename}>
|
|
{audio.filename}
|
|
</h3>
|
|
<p className="text-sm text-purple-100">
|
|
{audio.format.toUpperCase()} • {AudioService.formatDuration(audio.duration)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 播放按钮 */}
|
|
<button
|
|
onClick={handlePlayPause}
|
|
className="p-2 bg-white/20 rounded-lg hover:bg-white/30 transition-colors"
|
|
title={isPlaying ? '暂停' : '播放'}
|
|
>
|
|
{isPlaying ? <Pause size={20} /> : <Play size={20} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 音频详细信息 */}
|
|
<div className="p-4 space-y-3">
|
|
{/* 基本信息 */}
|
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
<div className="flex items-center space-x-2">
|
|
<Clock size={14} className="text-gray-400" />
|
|
<span className="text-gray-600">时长:</span>
|
|
<span className="font-medium">{AudioService.formatDuration(audio.duration)}</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<HardDrive size={14} className="text-gray-400" />
|
|
<span className="text-gray-600">大小:</span>
|
|
<span className="font-medium">{AudioService.formatFileSize(audio.file_size)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 音频参数 */}
|
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<span className="text-gray-600">采样率:</span>
|
|
<span className="ml-2 font-medium">{audio.sample_rate} Hz</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-600">声道:</span>
|
|
<span className="ml-2 font-medium">{audio.channels}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 节奏信息 */}
|
|
{audio.tempo && (
|
|
<div className="text-sm">
|
|
<span className="text-gray-600">节拍:</span>
|
|
<span className="ml-2 font-medium">{audio.tempo.toFixed(1)} BPM</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* 频谱特征 */}
|
|
{audio.spectral_centroid && (
|
|
<div className="text-sm space-y-1">
|
|
<div>
|
|
<span className="text-gray-600">频谱重心:</span>
|
|
<span className="ml-2 font-medium">{audio.spectral_centroid.toFixed(0)} Hz</span>
|
|
</div>
|
|
{audio.zero_crossing_rate && (
|
|
<div>
|
|
<span className="text-gray-600">过零率:</span>
|
|
<span className="ml-2 font-medium">{(audio.zero_crossing_rate * 100).toFixed(2)}%</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* MD5哈希 */}
|
|
<div className="text-xs text-gray-500 flex items-center space-x-1">
|
|
<Hash size={12} />
|
|
<span>MD5:</span>
|
|
<code className="bg-gray-100 px-1 rounded font-mono">
|
|
{audio.md5_hash.substring(0, 8)}...
|
|
</code>
|
|
</div>
|
|
|
|
{/* 操作按钮 */}
|
|
<div className="flex items-center justify-between pt-2 border-t border-gray-100">
|
|
<div className="text-xs text-gray-400">
|
|
{new Date(audio.created_at).toLocaleDateString()}
|
|
</div>
|
|
|
|
<div className="flex space-x-2">
|
|
{/* 频率图按钮 */}
|
|
{audio.frequency_chart_path && (
|
|
<button
|
|
onClick={() => onShowChart(audio)}
|
|
className="p-2 text-gray-400 hover:text-blue-600 transition-colors"
|
|
title="查看频率图"
|
|
>
|
|
<BarChart3 size={16} />
|
|
</button>
|
|
)}
|
|
|
|
{/* 删除按钮 */}
|
|
<button
|
|
onClick={handleDelete}
|
|
className="p-2 text-gray-400 hover:text-red-600 transition-colors"
|
|
title="删除"
|
|
>
|
|
<Trash2 size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default AudioCard
|