feat(音频消息): 增强音频消息组件功能并改进错误处理

- 支持JSON格式音频URL解析,可获取时长和文本信息
- 添加音频URL可用性检查机制
- 实现更完善的错误处理和重试逻辑
- 增加音频文本显示和错误提示样式
- 优化音频播放控制逻辑,确保跨域访问兼容性
- 将msgId类型改为string以避免潜在类型问题
This commit is contained in:
超级老白兔
2025-09-09 18:29:57 +08:00
parent 03056186c6
commit 98a9aa7701
3 changed files with 246 additions and 43 deletions

View File

@@ -1,11 +1,22 @@
// 消息气泡样式
.messageBubble {
word-wrap: break-word;
padding: 8px 12px;
border-radius: 8px;
background-color: #f0f0f0;
}
// 音频错误提示样式
.audioError {
color: #ff4d4f;
font-size: 12px;
margin-bottom: 8px;
padding: 4px 8px;
background: #fff2f0;
border: 1px solid #ffccc7;
border-radius: 4px;
text-align: center;
}
// 语音消息容器
.audioMessage {
min-width: 200px;
@@ -88,6 +99,17 @@
white-space: nowrap;
}
// 音频文本显示
.audioText {
font-size: 11px;
color: #666;
margin-top: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: help;
}
// 进度条容器
.audioProgress {
margin-top: 8px;

View File

@@ -4,27 +4,119 @@ import styles from "./AudioMessage.module.scss";
interface AudioMessageProps {
audioUrl: string;
msgId: number;
msgId: string;
}
interface AudioData {
durationMs?: number;
url: string;
text?: string;
}
// 解析音频URL支持两种格式纯URL字符串和JSON字符串
const parseAudioUrl = (audioUrl: string): AudioData => {
try {
// 尝试解析为JSON
const parsed = JSON.parse(audioUrl);
if (parsed.url) {
return {
durationMs: parsed.durationMs,
url: parsed.url,
text: parsed.text,
};
}
} catch (error) {
// 如果解析失败说明是纯URL字符串
console.log("音频URL为纯字符串格式");
}
// 返回纯URL格式
return {
url: audioUrl,
};
};
// 测试音频URL是否可访问
const testAudioUrl = async (url: string): Promise<boolean> => {
try {
const response = await fetch(url, { method: "HEAD" });
console.log("音频URL测试结果:", response.status, response.statusText);
return response.ok;
} catch (error) {
console.error("音频URL测试失败:", error);
return false;
}
};
const AudioMessage: React.FC<AudioMessageProps> = ({ audioUrl, msgId }) => {
const [playingAudioId, setPlayingAudioId] = useState<string | null>(null);
const [audioProgress, setAudioProgress] = useState<Record<string, number>>(
{},
);
const [audioError, setAudioError] = useState<string | null>(null);
const audioRefs = useRef<Record<string, HTMLAudioElement>>({});
// 解析音频数据
const audioData = parseAudioUrl(audioUrl);
const actualAudioUrl = audioData.url;
const audioDuration = audioData.durationMs;
const audioText = audioData.text;
const audioId = `audio_${msgId}_${Date.now()}`;
const isPlaying = playingAudioId === audioId;
const progress = audioProgress[audioId] || 0;
// 格式化时长显示
const formatDuration = (ms?: number): string => {
if (!ms) return "语音";
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
};
// 播放/暂停音频
const handleAudioToggle = () => {
const handleAudioToggle = async () => {
const audio = audioRefs.current[audioId];
if (!audio) {
const newAudio = new Audio(audioUrl);
console.log("创建新音频实例:", actualAudioUrl);
console.log("音频数据:", audioData);
// 先测试URL是否可访问
const isUrlAccessible = await testAudioUrl(actualAudioUrl);
if (!isUrlAccessible) {
console.error("音频URL不可访问无法播放");
setAudioError("音频文件无法访问,请检查网络连接");
return;
}
// 清除之前的错误状态
setAudioError(null);
const newAudio = new Audio();
// 设置跨域属性
newAudio.crossOrigin = "anonymous";
newAudio.preload = "metadata";
audioRefs.current[audioId] = newAudio;
newAudio.addEventListener("loadstart", () => {
console.log("音频开始加载");
});
newAudio.addEventListener("loadedmetadata", () => {
console.log("音频元数据加载完成,时长:", newAudio.duration);
});
newAudio.addEventListener("canplay", () => {
console.log("音频可以播放");
});
newAudio.addEventListener("canplaythrough", () => {
console.log("音频可以完整播放");
});
newAudio.addEventListener("timeupdate", () => {
const currentProgress =
(newAudio.currentTime / newAudio.duration) * 100;
@@ -35,17 +127,74 @@ const AudioMessage: React.FC<AudioMessageProps> = ({ audioUrl, msgId }) => {
});
newAudio.addEventListener("ended", () => {
console.log("音频播放结束");
setPlayingAudioId(null);
setAudioProgress(prev => ({ ...prev, [audioId]: 0 }));
});
newAudio.addEventListener("error", () => {
console.error("音频播放失败");
newAudio.addEventListener("error", e => {
console.error("音频播放失败:", e);
console.error("音频URL:", actualAudioUrl);
console.error("错误详情:", newAudio.error);
if (newAudio.error) {
console.error("错误代码:", newAudio.error.code);
console.error("错误消息:", newAudio.error.message);
}
setPlayingAudioId(null);
setAudioError("音频播放失败,请稍后重试");
});
newAudio.play();
setPlayingAudioId(audioId);
// 设置音频源并尝试播放
newAudio.src = actualAudioUrl;
console.log("设置音频源:", actualAudioUrl);
try {
console.log("尝试播放音频...");
await newAudio.play();
console.log("音频播放成功");
setPlayingAudioId(audioId);
} catch (error) {
console.error("播放失败:", error);
console.error("错误名称:", error.name);
console.error("错误消息:", error.message);
// 尝试备用方案不设置crossOrigin
console.log("尝试备用播放方案...");
try {
const fallbackAudio = new Audio(actualAudioUrl);
audioRefs.current[audioId] = fallbackAudio;
// 重新绑定事件监听器
fallbackAudio.addEventListener("timeupdate", () => {
const currentProgress =
(fallbackAudio.currentTime / fallbackAudio.duration) * 100;
setAudioProgress(prev => ({
...prev,
[audioId]: currentProgress,
}));
});
fallbackAudio.addEventListener("ended", () => {
console.log("备用音频播放结束");
setPlayingAudioId(null);
setAudioProgress(prev => ({ ...prev, [audioId]: 0 }));
});
fallbackAudio.addEventListener("error", e => {
console.error("备用音频播放也失败:", e);
setPlayingAudioId(null);
setAudioError("音频播放失败,请稍后重试");
});
await fallbackAudio.play();
console.log("备用方案播放成功");
setPlayingAudioId(audioId);
} catch (fallbackError) {
console.error("备用方案也失败:", fallbackError);
setPlayingAudioId(null);
setAudioError("音频播放失败,请检查音频格式或网络连接");
}
}
} else {
if (isPlaying) {
audio.pause();
@@ -55,53 +204,85 @@ const AudioMessage: React.FC<AudioMessageProps> = ({ audioUrl, msgId }) => {
Object.values(audioRefs.current).forEach(a => a.pause());
setPlayingAudioId(null);
audio.play();
setPlayingAudioId(audioId);
try {
await audio.play();
setPlayingAudioId(audioId);
} catch (error) {
console.error("重新播放失败:", error);
setPlayingAudioId(null);
setAudioError("音频播放失败,请稍后重试");
}
}
}
};
return (
<div className={styles.messageBubble}>
<div className={styles.audioMessage}>
<div className={styles.audioContainer} onClick={handleAudioToggle}>
<div className={styles.audioIcon}>
{isPlaying ? (
<PauseCircleFilled
style={{ fontSize: "20px", color: "#1890ff" }}
/>
) : (
<SoundOutlined style={{ fontSize: "20px", color: "#666" }} />
)}
<>
<div className={styles.messageBubble}>
{audioError && (
<div
className={styles.audioError}
onClick={() => {
setAudioError(null);
handleAudioToggle();
}}
style={{ cursor: "pointer" }}
title="点击重试"
>
{audioError} ()
</div>
<div className={styles.audioContent}>
<div className={styles.audioWaveform}>
{/* 音频波形效果 */}
{Array.from({ length: 20 }, (_, i) => (
<div
key={i}
className={`${styles.waveBar} ${isPlaying ? styles.playing : ""}`}
style={{
height: `${Math.random() * 20 + 10}px`,
animationDelay: `${i * 0.1}s`,
backgroundColor: progress > i * 5 ? "#1890ff" : "#d9d9d9",
}}
)}
<div className={styles.audioMessage}>
<div className={styles.audioContainer} onClick={handleAudioToggle}>
<div className={styles.audioIcon}>
{isPlaying ? (
<PauseCircleFilled
style={{ fontSize: "20px", color: "#1890ff" }}
/>
))}
) : (
<SoundOutlined style={{ fontSize: "20px", color: "#666" }} />
)}
</div>
<div className={styles.audioContent}>
<div className={styles.audioWaveform}>
{/* 音频波形效果 */}
{Array.from({ length: 20 }, (_, i) => (
<div
key={i}
className={`${styles.waveBar} ${isPlaying ? styles.playing : ""}`}
style={{
height: `${Math.random() * 20 + 10}px`,
animationDelay: `${i * 0.1}s`,
backgroundColor: progress > i * 5 ? "#1890ff" : "#d9d9d9",
}}
/>
))}
</div>
<div className={styles.audioDuration}>
{formatDuration(audioDuration)}
</div>
</div>
<div className={styles.audioDuration}></div>
</div>
{progress > 0 && (
<div className={styles.audioProgress}>
<div
className={styles.audioProgressBar}
style={{ width: `${progress}%` }}
/>
</div>
)}
</div>
{progress > 0 && (
<div className={styles.audioProgress}>
<div
className={styles.audioProgressBar}
style={{ width: `${progress}%` }}
/>
</div>
<div>
{audioText && (
<div className={styles.audioText} title={audioText}>
{audioText.length > 10
? `${audioText.substring(0, 10)}...`
: audioText}
</div>
)}
</div>
</div>
</>
);
};

View File

@@ -391,7 +391,7 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
}
// content直接是音频URL字符串
return <AudioMessage audioUrl={content} msgId={msg.id} />;
return <AudioMessage audioUrl={content} msgId={String(msg.id)} />;
case 49: // 小程序/文章/其他:图文、文件
if (typeof content !== "string" || !content.trim()) {