diff --git a/Cunkebao/dist/.vite/manifest.json b/Cunkebao/dist/.vite/manifest.json
index 86ef2f1e..fabab32f 100644
--- a/Cunkebao/dist/.vite/manifest.json
+++ b/Cunkebao/dist/.vite/manifest.json
@@ -1,18 +1,14 @@
{
- "_charts-CM0JFsjx.js": {
- "file": "assets/charts-CM0JFsjx.js",
+ "_charts-BjEBSMrK.js": {
+ "file": "assets/charts-BjEBSMrK.js",
"name": "charts",
"imports": [
- "_ui-Dkyp_L4f.js",
+ "_ui-CiJ_pikt.js",
"_vendor-BPPoWDlG.js"
]
},
- "_ui-D0C0OGrH.css": {
- "file": "assets/ui-D0C0OGrH.css",
- "src": "_ui-D0C0OGrH.css"
- },
- "_ui-Dkyp_L4f.js": {
- "file": "assets/ui-Dkyp_L4f.js",
+ "_ui-CiJ_pikt.js": {
+ "file": "assets/ui-CiJ_pikt.js",
"name": "ui",
"imports": [
"_vendor-BPPoWDlG.js"
@@ -21,6 +17,10 @@
"assets/ui-D0C0OGrH.css"
]
},
+ "_ui-D0C0OGrH.css": {
+ "file": "assets/ui-D0C0OGrH.css",
+ "src": "_ui-D0C0OGrH.css"
+ },
"_utils-DiZV3oaL.js": {
"file": "assets/utils-DiZV3oaL.js",
"name": "utils",
@@ -33,18 +33,18 @@
"name": "vendor"
},
"index.html": {
- "file": "assets/index-DYycL-yo.js",
+ "file": "assets/index-BesOjMPu.js",
"name": "index",
"src": "index.html",
"isEntry": true,
"imports": [
"_vendor-BPPoWDlG.js",
"_utils-DiZV3oaL.js",
- "_ui-Dkyp_L4f.js",
- "_charts-CM0JFsjx.js"
+ "_ui-CiJ_pikt.js",
+ "_charts-BjEBSMrK.js"
],
"css": [
- "assets/index-CHPV8625.css"
+ "assets/index-677RgwmW.css"
]
}
}
\ No newline at end of file
diff --git a/Cunkebao/dist/index.html b/Cunkebao/dist/index.html
index bedc8ebb..16e6d604 100644
--- a/Cunkebao/dist/index.html
+++ b/Cunkebao/dist/index.html
@@ -11,13 +11,13 @@
-
+
-
-
+
+
-
+
diff --git a/Cunkebao/src/components/Upload/AudioRecorder/index.tsx b/Cunkebao/src/components/Upload/AudioRecorder/index.tsx
new file mode 100644
index 00000000..1bcc705e
--- /dev/null
+++ b/Cunkebao/src/components/Upload/AudioRecorder/index.tsx
@@ -0,0 +1,411 @@
+import React, { useState, useRef, useCallback } from "react";
+import { Button, message, Modal } from "antd";
+import {
+ AudioOutlined,
+ PlayCircleOutlined,
+ PauseCircleOutlined,
+ SendOutlined,
+ DeleteOutlined,
+} from "@ant-design/icons";
+import { uploadFile } from "@/api/common";
+
+interface AudioRecorderProps {
+ onAudioUploaded: (audioData: { url: string; durationMs: number }) => void;
+ className?: string;
+ disabled?: boolean;
+ maxDuration?: number; // 最大录音时长(秒)
+}
+
+type RecordingState =
+ | "idle"
+ | "recording"
+ | "recorded"
+ | "playing"
+ | "uploading";
+
+const AudioRecorder: React.FC = ({
+ onAudioUploaded,
+ className,
+ disabled = false,
+ maxDuration = 60,
+}) => {
+ const [visible, setVisible] = useState(false);
+ const [state, setState] = useState("idle");
+ const [recordingTime, setRecordingTime] = useState(0);
+ const [audioBlob, setAudioBlob] = useState(null);
+ const [audioUrl, setAudioUrl] = useState("");
+
+ const mediaRecorderRef = useRef(null);
+ const audioRef = useRef(null);
+ const timerRef = useRef(null);
+ const chunksRef = useRef([]);
+
+ // 打开弹窗
+ const openRecorder = () => {
+ setVisible(true);
+ };
+
+ // 关闭弹窗并重置状态
+ const closeRecorder = () => {
+ if (state === "recording") {
+ stopRecording();
+ }
+ if (state === "playing") {
+ pauseAudio();
+ }
+ deleteRecording();
+ setVisible(false);
+ };
+
+ // 开始录音
+ const startRecording = useCallback(async () => {
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+
+ // 尝试使用MP3格式,如果不支持则回退到WebM
+ const mp3Types = [
+ "audio/mpeg",
+ "audio/mp3",
+ "audio/mpeg; codecs=mp3",
+ "audio/mp4",
+ "audio/mp4; codecs=mp4a.40.2",
+ ];
+
+ let mimeType = "audio/webm"; // 默认回退格式
+
+ // 检测并选择支持的MP3格式
+ for (const type of mp3Types) {
+ if (MediaRecorder.isTypeSupported(type)) {
+ mimeType = type;
+ console.log(`使用音频格式: ${type}`);
+ break;
+ }
+ }
+
+ if (mimeType === "audio/webm") {
+ console.log("浏览器不支持MP3格式,使用WebM格式");
+ }
+
+ const mediaRecorder = new MediaRecorder(stream, { mimeType });
+ mediaRecorderRef.current = mediaRecorder;
+ chunksRef.current = [];
+
+ mediaRecorder.ondataavailable = event => {
+ if (event.data.size > 0) {
+ chunksRef.current.push(event.data);
+ }
+ };
+
+ mediaRecorder.onstop = () => {
+ const blob = new Blob(chunksRef.current, { type: mimeType });
+ setAudioBlob(blob);
+ const url = URL.createObjectURL(blob);
+ setAudioUrl(url);
+ setState("recorded");
+
+ // 停止所有音频轨道
+ stream.getTracks().forEach(track => track.stop());
+ };
+
+ mediaRecorder.start();
+ setState("recording");
+ setRecordingTime(0);
+
+ // 开始计时
+ timerRef.current = setInterval(() => {
+ setRecordingTime(prev => {
+ const newTime = prev + 1;
+ if (newTime >= maxDuration) {
+ stopRecording();
+ }
+ return newTime;
+ });
+ }, 1000);
+ } catch (error) {
+ console.error("录音失败:", error);
+ message.error("无法访问麦克风,请检查权限设置");
+ }
+ }, [maxDuration]);
+
+ // 停止录音
+ const stopRecording = useCallback(() => {
+ if (
+ mediaRecorderRef.current &&
+ mediaRecorderRef.current.state === "recording"
+ ) {
+ mediaRecorderRef.current.stop();
+ }
+ if (timerRef.current) {
+ clearInterval(timerRef.current);
+ timerRef.current = null;
+ }
+ }, []);
+
+ // 播放录音
+ const playAudio = useCallback(() => {
+ if (audioRef.current && audioUrl) {
+ audioRef.current.play();
+ setState("playing");
+ }
+ }, [audioUrl]);
+
+ // 暂停播放
+ const pauseAudio = useCallback(() => {
+ if (audioRef.current) {
+ audioRef.current.pause();
+ setState("recorded");
+ }
+ }, []);
+
+ // 删除录音
+ const deleteRecording = useCallback(() => {
+ if (audioUrl) {
+ URL.revokeObjectURL(audioUrl);
+ }
+ setAudioBlob(null);
+ setAudioUrl("");
+ setRecordingTime(0);
+ setState("idle");
+ }, [audioUrl]);
+
+ // 发送录音
+ const sendAudio = useCallback(async () => {
+ if (!audioBlob) return;
+
+ try {
+ setState("uploading");
+
+ // 创建文件对象
+ const timestamp = Date.now();
+ const fileExtension =
+ audioBlob.type.includes("mp3") ||
+ audioBlob.type.includes("mpeg") ||
+ audioBlob.type.includes("mp4")
+ ? "mp3"
+ : "webm";
+ const audioFile = new File(
+ [audioBlob],
+ `audio_${timestamp}.${fileExtension}`,
+ {
+ type: audioBlob.type,
+ },
+ );
+
+ // 打印文件格式信息
+ console.log("音频文件信息:", {
+ fileName: audioFile.name,
+ fileType: audioFile.type,
+ fileSize: audioFile.size,
+ fileExtension: fileExtension,
+ blobType: audioBlob.type,
+ });
+
+ // 上传文件
+ const filePath = await uploadFile(audioFile);
+
+ // 调用回调函数,传递音频URL和时长(毫秒)
+ onAudioUploaded({
+ url: filePath,
+ durationMs: recordingTime * 1000, // 将秒转换为毫秒
+ });
+
+ // 重置状态并关闭弹窗
+ deleteRecording();
+ setVisible(false);
+ message.success("语音发送成功");
+ } catch (error) {
+ console.error("语音上传失败:", error);
+ message.error("语音发送失败,请重试");
+ setState("recorded");
+ }
+ }, [audioBlob, onAudioUploaded, deleteRecording]);
+
+ // 格式化时间显示
+ const formatTime = (seconds: number) => {
+ const mins = Math.floor(seconds / 60);
+ const secs = seconds % 60;
+ return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
+ };
+
+ // 渲染弹窗内容
+ const renderModalContent = () => {
+ switch (state) {
+ case "idle":
+ return (
+
+
+ 点击下方按钮开始录音
+
+
}
+ onClick={startRecording}
+ style={{
+ borderRadius: "50%",
+ width: "80px",
+ height: "80px",
+ fontSize: "24px",
+ }}
+ />
+
+ );
+
+ case "recording":
+ return (
+
+
+
+ {formatTime(recordingTime)}
+
+
+ 正在录音中...
+
+
+
+
+ );
+
+ case "recorded":
+ case "playing":
+ return (
+
+
+
+ 录音时长: {formatTime(recordingTime)}
+
+
+ {state === "playing"
+ ? "正在播放..."
+ : "录音完成,可以试听或发送"}
+
+
+
+
+
+ ) : (
+
+ )
+ }
+ onClick={state === "playing" ? pauseAudio : playAudio}
+ title={state === "playing" ? "暂停播放" : "播放预览"}
+ />
+
}
+ onClick={deleteRecording}
+ title="删除重录"
+ danger
+ />
+
+
+
+ }
+ onClick={sendAudio}
+ loading={state === ("uploading" as RecordingState)}
+ style={{ minWidth: "120px" }}
+ >
+ 发送录音
+
+
+
+ );
+
+ case "uploading":
+ return (
+
+
+
+ );
+
+ default:
+ return null;
+ }
+ };
+
+ return (
+ <>
+ }
+ onClick={openRecorder}
+ className={className}
+ disabled={disabled}
+ title="点击录音"
+ />
+
+
+ {renderModalContent()}
+ {audioUrl && (
+
+ >
+ );
+};
+
+export default AudioRecorder;
diff --git a/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/ChatWindow.module.scss b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/ChatWindow.module.scss
index d60ef945..ceeff66e 100644
--- a/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/ChatWindow.module.scss
+++ b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/ChatWindow.module.scss
@@ -326,18 +326,6 @@
}
}
-.messageItem {
- margin-bottom: 16px;
- position: relative;
-
- .messageContent {
- display: flex;
- align-items: flex-start;
- gap: 8px;
- max-width: 70%;
- }
-}
-
.messageTime {
text-align: center;
padding: 4px 0;
@@ -346,252 +334,6 @@
margin: 20px 0;
}
-.messageItem {
- .messageContent {
- .messageAvatar {
- flex-shrink: 0;
- }
-
- .messageBubble {
- max-width: 100%;
- .messageSender {
- font-size: 12px;
- color: #8c8c8c;
- margin-bottom: 4px;
- }
-
- .messageText {
- color: #262626;
- line-height: 1.5;
- word-break: break-word;
- background: #fff;
- padding: 8px 12px;
- border-radius: 8px;
-
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
- }
-
- .emojiMessage {
- img {
- max-width: 120px;
- max-height: 120px;
- border-radius: 4px;
- display: block;
- cursor: pointer;
- }
- }
-
- .imageMessage {
- img {
- max-width: 100%;
- border-radius: 8px;
- display: block;
- cursor: pointer;
- }
- }
-
- .videoMessage {
- position: relative;
- display: flex;
- flex-direction: column;
-
- video {
- max-width: 100%;
- border-radius: 8px;
- display: block;
- }
-
- .videoContainer {
- position: relative;
- cursor: pointer;
- display: flex;
- justify-content: center;
- align-items: center;
- overflow: hidden;
- border-radius: 8px;
-
- &:hover .videoPlayIcon {
- transform: scale(1.1);
- }
-
- .videoThumbnail {
- width: 100%;
- display: block;
- border-radius: 8px;
- }
-
- .videoPlayIcon {
- position: absolute;
- display: flex;
- align-items: center;
- justify-content: center;
- background-color: rgba(0, 0, 0, 0.5);
- border-radius: 50%;
- width: 60px;
- height: 60px;
- transition: transform 0.2s ease;
-
- .loadingSpinner {
- width: 32px;
- height: 32px;
- border: 3px solid rgba(255, 255, 255, 0.3);
- border-radius: 50%;
- border-top-color: #fff;
- animation: spin 1s linear infinite;
- }
- }
-
- @keyframes spin {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(360deg);
- }
- }
- }
-
- .downloadButton {
- position: absolute;
- top: 8px;
- right: 8px;
- display: flex;
- align-items: center;
- justify-content: center;
- color: #fff;
- font-size: 18px;
- width: 32px;
- height: 32px;
- border-radius: 50%;
- transition: all 0.2s;
-
- &:hover {
- color: #40a9ff;
- }
- }
- }
-
- .audioMessage {
- position: relative;
- display: flex;
- align-items: center;
- background: #f5f5f5;
- border-radius: 8px;
- padding: 8px;
-
- audio {
- flex: 1;
- min-width: 200px;
- }
-
- .downloadButton {
- display: flex;
- align-items: center;
- justify-content: center;
- color: #1890ff;
- font-size: 18px;
- width: 32px;
- height: 32px;
- border-radius: 50%;
- margin-left: 8px;
- transition: all 0.2s;
-
- &:hover {
- color: #40a9ff;
- }
- }
- }
-
- .fileMessage {
- background: #f5f5f5;
- border-radius: 8px;
- padding: 8px;
- display: flex;
- align-items: center;
- position: relative;
- transition: background-color 0.2s;
- width: 240px;
-
- &:hover {
- background: #e6f7ff;
- }
-
- .fileInfo {
- flex: 1;
- margin-right: 8px;
- cursor: pointer;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .downloadButton {
- display: flex;
- align-items: center;
- justify-content: center;
- color: #1890ff;
- font-size: 18px;
- width: 32px;
- height: 32px;
- border-radius: 50%;
- transition: all 0.2s;
-
- &:hover {
- color: #40a9ff;
- }
- }
- }
-
- .locationMessage {
- background: #f5f5f5;
- border-radius: 8px;
- padding: 8px;
- display: flex;
- align-items: center;
- cursor: pointer;
- transition: background-color 0.2s;
-
- &:hover {
- background: #fff2e8;
- }
- }
-
- .messageTime {
- display: none;
- }
- }
- }
-}
-
-.ownMessage {
- .messageContent {
- flex-direction: row-reverse;
- margin-left: auto;
-
- .messageBubble {
- color: #262626;
- line-height: 1.5;
- word-break: break-word;
- background: #fff;
- border-radius: 8px;
- max-width: 100%;
- .messageText {
- color: #333;
- }
-
- .messageTime {
- color: rgba(255, 255, 255, 0.7);
- }
- }
- }
-}
-
-.otherMessage {
- .messageContent {
- margin-right: auto;
- }
-}
-
// 响应式设计
@media (max-width: 1200px) {
.profileSider {
@@ -631,12 +373,6 @@
}
}
- .messageItem {
- .messageContent {
- max-width: 85%;
- }
- }
-
.profileContent {
padding: 12px;
diff --git a/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx
index 5c4f055d..af8b7ec4 100644
--- a/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx
+++ b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx
@@ -13,6 +13,7 @@ import { useWebSocketStore } from "@/store/module/websocket/websocket";
import { EmojiPicker } from "@/components/EmojiSeclection";
import { EmojiInfo } from "@/components/EmojiSeclection/wechatEmoji";
import SimpleFileUpload from "@/components/Upload/SimpleFileUpload";
+import AudioRecorder from "@/components/Upload/AudioRecorder";
import styles from "./MessageEnter.module.scss";
const { Footer } = Layout;
@@ -94,10 +95,26 @@ const MessageEnter: React.FC = ({ contract }) => {
// 其他格式默认为文件
return 49; // 文件
};
-
- const handleFileUploaded = (filePath: string) => {
+ const FileType = {
+ TEXT: 1,
+ IMAGE: 2,
+ VIDEO: 3,
+ AUDIO: 4,
+ FILE: 5,
+ };
+ const handleFileUploaded = (
+ filePath: string | { url: string; durationMs: number },
+ fileType: number,
+ ) => {
// msgType(1:文本 3:图片 43:视频 47:动图表情包(gif、其他表情包) 49:小程序/其他:图文、文件)
- const msgType = getMsgTypeByFileFormat(filePath);
+ let msgType = 1;
+ if ([FileType.TEXT].includes(fileType)) {
+ msgType = getMsgTypeByFileFormat(filePath as string);
+ } else if ([FileType.IMAGE].includes(fileType)) {
+ msgType = 3;
+ } else if ([FileType.AUDIO].includes(fileType)) {
+ msgType = 34;
+ }
const params = {
wechatAccountId: contract.wechatAccountId,
@@ -105,7 +122,9 @@ const MessageEnter: React.FC = ({ contract }) => {
wechatFriendId: contract?.chatroomId ? 0 : contract.id,
msgSubType: 0,
msgType,
- content: filePath,
+ content: [FileType.AUDIO].includes(fileType)
+ ? JSON.stringify(filePath)
+ : (filePath as { url: string; durationMs: number }).url,
};
sendCommand("CmdSendMessage", params);
};
@@ -119,7 +138,9 @@ const MessageEnter: React.FC = ({ contract }) => {
+ handleFileUploaded(filePath, FileType.FILE)
+ }
maxSize={1}
type={4}
slot={
@@ -131,7 +152,9 @@ const MessageEnter: React.FC = ({ contract }) => {
}
/>
+ handleFileUploaded(filePath, FileType.IMAGE)
+ }
maxSize={1}
type={1}
slot={
@@ -143,13 +166,12 @@ const MessageEnter: React.FC = ({ contract }) => {
}
/>
-
- }
- className={styles.toolbarButton}
- />
-
+
+ handleFileUploaded(audioData, FileType.AUDIO)
+ }
+ className={styles.toolbarButton}
+ />
diff --git a/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/MessageRecord.module.scss b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/MessageRecord.module.scss
index 6a4ede88..4d772c27 100644
--- a/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/MessageRecord.module.scss
+++ b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/MessageRecord.module.scss
@@ -76,7 +76,6 @@
display: flex;
align-items: flex-start;
gap: 8px;
- max-width: 70%;
}
// 头像
@@ -533,10 +532,6 @@
// 响应式设计
@media (max-width: 768px) {
- .messageContent {
- max-width: 85%;
- }
-
.messageBubble {
padding: 6px 10px;
}
diff --git a/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/AudioMessage/AudioMessage.module.scss b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/AudioMessage/AudioMessage.module.scss
new file mode 100644
index 00000000..394058b6
--- /dev/null
+++ b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/AudioMessage/AudioMessage.module.scss
@@ -0,0 +1,139 @@
+// 消息气泡样式
+.messageBubble {
+ word-wrap: break-word;
+ 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;
+ max-width: 100%;
+ width: 100%;
+ box-sizing: border-box;
+ overflow: hidden;
+}
+
+// 音频控制容器
+.audioContainer {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ cursor: pointer;
+ padding: 8px;
+ border-radius: 6px;
+ transition: background-color 0.2s;
+ width: 100%;
+ box-sizing: border-box;
+ overflow: hidden;
+
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.05);
+ }
+}
+
+// 播放图标
+.audioIcon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ background-color: #fff;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ transition: all 0.2s;
+
+ &:hover {
+ transform: scale(1.05);
+ }
+}
+
+// 音频内容区域
+.audioContent {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+ overflow: hidden;
+}
+
+// 波形动画容器
+.audioWaveform {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ height: 30px; // 固定高度防止抖动
+}
+
+// 波形条
+.waveBar {
+ width: 3px;
+ background-color: #d9d9d9;
+ border-radius: 1.5px;
+ transition: all 0.3s ease;
+ transform-origin: center; // 设置变换原点为中心
+
+ &.playing {
+ animation: waveAnimation 1.5s ease-in-out infinite;
+ }
+}
+
+// 音频时长显示
+.audioDuration {
+ font-size: 12px;
+ color: #666;
+ 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;
+ height: 2px;
+ background-color: #e0e0e0;
+ border-radius: 1px;
+ overflow: hidden;
+}
+
+// 进度条
+.audioProgressBar {
+ height: 100%;
+ background-color: #1890ff;
+ border-radius: 1px;
+ transition: width 0.1s ease;
+}
+
+// 波形动画
+@keyframes waveAnimation {
+ 0%,
+ 100% {
+ transform: scaleY(0.5);
+ }
+ 50% {
+ transform: scaleY(1.2);
+ }
+}
diff --git a/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/AudioMessage/AudioMessage.tsx b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/AudioMessage/AudioMessage.tsx
new file mode 100644
index 00000000..0d2ba39b
--- /dev/null
+++ b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/AudioMessage/AudioMessage.tsx
@@ -0,0 +1,245 @@
+import React, { useState, useRef } from "react";
+import { PauseCircleFilled, SoundOutlined } from "@ant-design/icons";
+import styles from "./AudioMessage.module.scss";
+
+interface AudioMessageProps {
+ audioUrl: string;
+ 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字符串
+ }
+
+ // 返回纯URL格式
+ return {
+ url: audioUrl,
+ };
+};
+
+// 测试音频URL是否可访问
+const testAudioUrl = async (url: string): Promise
=> {
+ try {
+ const response = await fetch(url, { method: "HEAD" });
+ return response.ok;
+ } catch (error) {
+ return false;
+ }
+};
+
+const AudioMessage: React.FC = ({ audioUrl, msgId }) => {
+ const [playingAudioId, setPlayingAudioId] = useState(null);
+ const [audioProgress, setAudioProgress] = useState>(
+ {},
+ );
+ const [audioError, setAudioError] = useState(null);
+ const audioRefs = useRef>({});
+
+ // 解析音频数据
+ 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 = async () => {
+ const audio = audioRefs.current[audioId];
+ if (!audio) {
+ // 先测试URL是否可访问
+ const isUrlAccessible = await testAudioUrl(actualAudioUrl);
+ if (!isUrlAccessible) {
+ setAudioError("音频文件无法访问,请检查网络连接");
+ return;
+ }
+
+ // 清除之前的错误状态
+ setAudioError(null);
+
+ const newAudio = new Audio();
+
+ // 设置跨域属性
+ newAudio.crossOrigin = "anonymous";
+ newAudio.preload = "metadata";
+
+ audioRefs.current[audioId] = newAudio;
+
+ newAudio.addEventListener("timeupdate", () => {
+ const currentProgress =
+ (newAudio.currentTime / newAudio.duration) * 100;
+ setAudioProgress(prev => ({
+ ...prev,
+ [audioId]: currentProgress,
+ }));
+ });
+
+ newAudio.addEventListener("ended", () => {
+ setPlayingAudioId(null);
+ setAudioProgress(prev => ({ ...prev, [audioId]: 0 }));
+ });
+
+ newAudio.addEventListener("error", e => {
+ setPlayingAudioId(null);
+ setAudioError("音频播放失败,请稍后重试");
+ });
+
+ // 设置音频源并尝试播放
+ newAudio.src = actualAudioUrl;
+
+ try {
+ await newAudio.play();
+ setPlayingAudioId(audioId);
+ } catch (error) {
+ // 尝试备用方案:不设置crossOrigin
+ 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", () => {
+ setPlayingAudioId(null);
+ setAudioProgress(prev => ({ ...prev, [audioId]: 0 }));
+ });
+
+ fallbackAudio.addEventListener("error", e => {
+ setPlayingAudioId(null);
+ setAudioError("音频播放失败,请稍后重试");
+ });
+
+ await fallbackAudio.play();
+ setPlayingAudioId(audioId);
+ } catch (fallbackError) {
+ setPlayingAudioId(null);
+ setAudioError("音频播放失败,请检查音频格式或网络连接");
+ }
+ }
+ } else {
+ if (isPlaying) {
+ audio.pause();
+ setPlayingAudioId(null);
+ } else {
+ // 停止其他正在播放的音频
+ Object.values(audioRefs.current).forEach(a => a.pause());
+ setPlayingAudioId(null);
+
+ try {
+ await audio.play();
+ setPlayingAudioId(audioId);
+ } catch (error) {
+ setPlayingAudioId(null);
+ setAudioError("音频播放失败,请稍后重试");
+ }
+ }
+ }
+ };
+
+ return (
+ <>
+
+ {audioError && (
+
{
+ setAudioError(null);
+ handleAudioToggle();
+ }}
+ style={{ cursor: "pointer" }}
+ title="点击重试"
+ >
+ {audioError} (点击重试)
+
+ )}
+
+
+
+ {isPlaying ? (
+
+ ) : (
+
+ )}
+
+
+
+ {/* 音频波形效果 */}
+ {Array.from({ length: 20 }, (_, i) => (
+
i * 5 ? "#1890ff" : "#d9d9d9",
+ }}
+ />
+ ))}
+
+
+ {formatDuration(audioDuration)}
+
+
+
+ {progress > 0 && (
+
+ )}
+
+
+
+ {audioText && (
+
+ {audioText.length > 10
+ ? `${audioText.substring(0, 10)}...`
+ : audioText}
+
+ )}
+
+ >
+ );
+};
+
+export default AudioMessage;
diff --git a/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SmallProgramMessage/SmallProgramMessage.module.scss b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SmallProgramMessage/SmallProgramMessage.module.scss
new file mode 100644
index 00000000..40124966
--- /dev/null
+++ b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SmallProgramMessage/SmallProgramMessage.module.scss
@@ -0,0 +1,315 @@
+// 通用消息文本样式
+.messageText {
+ line-height: 1.4;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+// 小程序消息基础样式
+.miniProgramMessage {
+ background: #ffffff;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
+
+ // 通用小程序卡片基础样式
+ .miniProgramCard {
+ display: flex;
+ align-items: center;
+ padding: 12px;
+ border-bottom: 1px solid #e1e8ed;
+ background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
+ cursor: pointer;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ width: 280px;
+ min-height: 64px;
+ overflow: hidden;
+ }
+
+ // 通用小程序元素样式
+ .miniProgramThumb {
+ width: 50px;
+ height: 50px;
+ object-fit: cover;
+ background: linear-gradient(135deg, #f0f2f5 0%, #e6f7ff 100%);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
+ transition: transform 0.2s ease;
+ &:hover {
+ transform: scale(1.05);
+ }
+ }
+
+ .miniProgramInfo {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .miniProgramTitle {
+ padding-left: 16px;
+ font-weight: 600;
+ color: #1a1a1a;
+ font-size: 14px;
+ line-height: 1.4;
+ margin-bottom: 4px;
+ display: -webkit-box;
+ -webkit-line-clamp: 1;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ letter-spacing: -0.01em;
+ }
+
+ .miniProgramApp {
+ font-size: 12px;
+ color: #8c8c8c;
+ line-height: 1.2;
+ font-weight: 500;
+ padding: 6px 12px;
+ }
+}
+
+// 类型1小程序样式(默认横向布局)
+.miniProgramType1 {
+ // 继承基础样式,无需额外定义
+}
+
+// 类型2小程序样式(垂直图片布局)
+.miniProgramType2 {
+ .miniProgramCardType2 {
+ flex-direction: column;
+ align-items: stretch;
+ padding: 0;
+ min-height: 220px;
+ max-width: 280px;
+
+ .miniProgramAppTop {
+ padding: 12px 16px 8px;
+ font-size: 13px;
+ font-weight: 500;
+ color: #495057;
+ background: #f8f9fa;
+ display: flex;
+ align-items: center;
+
+ &::before {
+ content: "📱";
+ font-size: 12px;
+ }
+ }
+
+ .miniProgramImageArea {
+ width: calc(100% - 32px);
+ height: 0;
+ padding-bottom: 75%; // 4:3 宽高比
+ margin: 0px 16px;
+ overflow: hidden;
+ position: relative;
+ background: #f8f9fa;
+
+ .miniProgramImage {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transition: transform 0.3s ease;
+ border: 0.5px solid #e1e8ed;
+ &:hover {
+ transform: scale(1.05);
+ }
+ }
+ }
+
+ .miniProgramContent {
+ padding: 12px 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ .miniProgramTitle {
+ font-size: 14px;
+ font-weight: 600;
+ color: #212529;
+ line-height: 1.4;
+ margin: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ }
+
+ .miniProgramIdentifier {
+ font-size: 11px;
+ color: #6c757d;
+ border-radius: 8px;
+ display: inline-flex;
+ align-items: center;
+ align-self: flex-start;
+ gap: 3px;
+
+ &::before {
+ content: "🏷️";
+ font-size: 9px;
+ }
+ }
+ }
+ }
+}
+
+// 文章类型消息样式
+.articleMessage {
+ .articleCard {
+ flex-direction: column;
+ align-items: stretch;
+ padding: 16px;
+ min-height: auto;
+ max-width: 320px;
+ }
+
+ .articleTitle {
+ font-size: 16px;
+ font-weight: 600;
+ color: #1a1a1a;
+ line-height: 1.4;
+ margin-bottom: 12px;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .articleContent {
+ display: flex;
+ gap: 12px;
+ align-items: flex-start;
+ }
+
+ .articleTextArea {
+ flex: 1;
+ min-width: 0;
+ }
+
+ .articleDescription {
+ font-size: 13px;
+ color: #666;
+ line-height: 1.5;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .articleImageArea {
+ flex-shrink: 0;
+ width: 60px;
+ height: 60px;
+ overflow: hidden;
+ border-radius: 8px;
+ background: #f8f9fa;
+ }
+
+ .articleImage {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transition: transform 0.3s ease;
+ }
+}
+
+// 文件消息样式
+.fileMessage {
+ .fileCard {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px;
+ border: 1px solid #d9d9d9;
+ border-radius: 8px;
+ background: #fafafa;
+ cursor: pointer;
+ transition: all 0.2s;
+ max-width: 250px;
+
+ &:hover {
+ background: #f0f0f0;
+ border-color: #1890ff;
+ }
+ }
+
+ .fileIcon {
+ font-size: 24px;
+ color: #1890ff;
+ flex-shrink: 0;
+ }
+
+ .fileInfo {
+ flex: 1;
+ min-width: 0;
+ }
+
+ .fileName {
+ font-weight: 500;
+ color: #262626;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-bottom: 4px;
+ }
+
+ .fileAction {
+ font-size: 12px;
+ color: #1890ff;
+ cursor: pointer;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+}
+
+// 响应式设计
+@media (max-width: 768px) {
+ // 小程序消息移动端适配
+ .miniProgramMessage {
+ .miniProgramCard {
+ max-width: 260px;
+ padding: 10px 14px;
+ min-height: 56px;
+ border-radius: 10px;
+ }
+
+ .miniProgramThumb {
+ width: 36px;
+ height: 36px;
+ border-radius: 6px;
+
+ &:hover {
+ transform: none;
+ }
+ }
+
+ .miniProgramTitle {
+ font-size: 13px;
+ line-height: 1.3;
+ font-weight: 500;
+ }
+
+ .miniProgramApp {
+ font-size: 11px;
+ padding: 1px 4px;
+
+ &::before {
+ width: 12px;
+ height: 12px;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SmallProgramMessage/index.tsx b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SmallProgramMessage/index.tsx
new file mode 100644
index 00000000..44787540
--- /dev/null
+++ b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SmallProgramMessage/index.tsx
@@ -0,0 +1,259 @@
+import React from "react";
+import { parseWeappMsgStr } from "@/utils/common";
+import styles from "./SmallProgramMessage.module.scss";
+
+interface SmallProgramMessageProps {
+ content: string;
+}
+
+const SmallProgramMessage: React.FC
= ({
+ content,
+}) => {
+ // 统一的错误消息渲染函数
+ const renderErrorMessage = (fallbackText: string) => (
+ {fallbackText}
+ );
+
+ if (typeof content !== "string" || !content.trim()) {
+ return renderErrorMessage("[小程序/文章/文件消息 - 无效内容]");
+ }
+
+ try {
+ const trimmedContent = content.trim();
+
+ // 尝试解析JSON格式的消息
+ if (trimmedContent.startsWith("{") && trimmedContent.endsWith("}")) {
+ const messageData = JSON.parse(trimmedContent);
+
+ // 处理文章类型消息
+ if (messageData.type === "link" && messageData.title && messageData.url) {
+ const { title, desc, thumbPath, url } = messageData;
+
+ return (
+
+
window.open(url, "_blank")}
+ >
+ {/* 标题在第一行 */}
+
{title}
+
+ {/* 下方:文字在左,图片在右 */}
+
+
+ {desc && (
+
{desc}
+ )}
+
+ {thumbPath && (
+
+

{
+ const target = e.target as HTMLImageElement;
+ target.style.display = "none";
+ }}
+ />
+
+ )}
+
+
+
文章
+
+ );
+ }
+
+ // 处理小程序消息 - 统一使用parseWeappMsgStr解析
+ if (messageData.type === "miniprogram" && messageData.contentXml) {
+ try {
+ const parsedData = parseWeappMsgStr(trimmedContent);
+
+ if (parsedData.appmsg) {
+ const { appmsg } = parsedData;
+ const title = appmsg.title || "小程序消息";
+ const appName =
+ appmsg.sourcedisplayname || appmsg.appname || "小程序";
+
+ // 获取小程序类型
+ const miniProgramType =
+ appmsg.weappinfo && appmsg.weappinfo.type
+ ? parseInt(appmsg.weappinfo.type)
+ : 1;
+
+ // 根据type类型渲染不同布局
+ if (miniProgramType === 2) {
+ // 类型2:图片区域布局
+ return (
+
+
+
{appName}
+
{title}
+
+

{
+ const target = e.target as HTMLImageElement;
+ target.style.display = "none";
+ }}
+ />
+
+
+
+
+ );
+ } else {
+ // 默认类型:横向布局
+ return (
+
+
+

{
+ const target = e.target as HTMLImageElement;
+ target.style.display = "none";
+ }}
+ />
+
+
+
{appName}
+
+ );
+ }
+ }
+ } catch (parseError) {
+ console.error("parseWeappMsgStr解析失败:", parseError);
+ return renderErrorMessage("[小程序消息 - 解析失败]");
+ }
+ }
+
+ // 验证传统JSON格式的小程序数据结构
+ if (
+ messageData &&
+ typeof messageData === "object" &&
+ (messageData.title || messageData.appName)
+ ) {
+ return (
+
+
+ {messageData.thumb && (
+

{
+ const target = e.target as HTMLImageElement;
+ target.style.display = "none";
+ }}
+ />
+ )}
+
+
+ {messageData.title || "小程序消息"}
+
+ {messageData.appName && (
+
+ {messageData.appName}
+
+ )}
+
+
+
+ );
+ }
+ }
+
+ // 增强的文件消息处理
+ const isFileUrl =
+ content.startsWith("http") ||
+ content.startsWith("https") ||
+ content.startsWith("file://") ||
+ /\.(pdf|doc|docx|xls|xlsx|ppt|pptx|txt|zip|rar|7z)$/i.test(content);
+
+ if (isFileUrl) {
+ // 尝试从URL中提取文件名
+ const fileName = content.split("/").pop()?.split("?")[0] || "文件";
+ const fileExtension = fileName.split(".").pop()?.toLowerCase();
+
+ // 根据文件类型选择图标
+ let fileIcon = "📄";
+ if (fileExtension) {
+ const iconMap: { [key: string]: string } = {
+ pdf: "📕",
+ doc: "📘",
+ docx: "📘",
+ xls: "📗",
+ xlsx: "📗",
+ ppt: "📙",
+ pptx: "📙",
+ txt: "📝",
+ zip: "🗜️",
+ rar: "🗜️",
+ "7z": "🗜️",
+ jpg: "🖼️",
+ jpeg: "🖼️",
+ png: "🖼️",
+ gif: "🖼️",
+ mp4: "🎬",
+ avi: "🎬",
+ mov: "🎬",
+ mp3: "🎵",
+ wav: "🎵",
+ flac: "🎵",
+ };
+ fileIcon = iconMap[fileExtension] || "📄";
+ }
+
+ return (
+
+
+
{fileIcon}
+
+
+ {fileName.length > 20
+ ? fileName.substring(0, 20) + "..."
+ : fileName}
+
+
{
+ try {
+ window.open(content, "_blank");
+ } catch (e) {
+ console.error("文件打开失败:", e);
+ }
+ }}
+ >
+ 点击查看
+
+
+
+
+ );
+ }
+
+ return renderErrorMessage("[小程序/文件消息]");
+ } catch (e) {
+ console.warn("小程序/文件消息解析失败:", e);
+ return renderErrorMessage("[小程序/文件消息 - 解析失败]");
+ }
+};
+
+export default SmallProgramMessage;
diff --git a/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VideoMessage/VideoMessage.module.scss b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VideoMessage/VideoMessage.module.scss
new file mode 100644
index 00000000..a90b90a8
--- /dev/null
+++ b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VideoMessage/VideoMessage.module.scss
@@ -0,0 +1,153 @@
+// 通用消息文本样式
+.messageText {
+ color: #666;
+ font-style: italic;
+ padding: 8px 12px;
+ background: #f5f5f5;
+ border-radius: 8px;
+ border: 1px solid #e0e0e0;
+}
+
+// 消息气泡样式
+.messageBubble {
+ display: inline-block;
+ max-width: 70%;
+ padding: 8px 12px;
+ border-radius: 12px;
+ word-wrap: break-word;
+ position: relative;
+}
+
+// 视频消息样式
+.videoMessage {
+ position: relative;
+ display: inline-block;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ background: #000;
+
+ .videoContainer {
+ position: relative;
+ display: inline-block;
+ cursor: pointer;
+
+ video {
+ display: block;
+ max-width: 300px;
+ max-height: 400px;
+ border-radius: 8px;
+ }
+ }
+
+ .videoThumbnail {
+ display: block;
+ max-width: 300px;
+ max-height: 400px;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: opacity 0.3s ease;
+ }
+
+ .videoPlayIcon {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ pointer-events: none;
+ z-index: 2;
+
+ .loadingSpinner {
+ width: 48px;
+ height: 48px;
+ border: 4px solid rgba(255, 255, 255, 0.3);
+ border-top: 4px solid #fff;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ }
+ }
+
+ .downloadButton {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ width: 32px;
+ height: 32px;
+ background: rgba(0, 0, 0, 0.6);
+ border-radius: 50%;
+ color: white;
+ border: none;
+ cursor: pointer;
+ transition: background 0.2s;
+ text-decoration: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 3;
+
+ &:hover {
+ background: rgba(0, 0, 0, 0.8);
+ color: white;
+ }
+ }
+
+ .playButton {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 48px;
+ height: 48px;
+ background: rgba(0, 0, 0, 0.6);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.2s;
+
+ svg {
+ margin-left: 2px; // 视觉居中调整
+ }
+ }
+}
+
+@keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+// 响应式设计
+@media (max-width: 768px) {
+ .messageBubble {
+ padding: 6px 10px;
+ }
+
+ .videoMessage .videoThumbnail,
+ .videoMessage .videoContainer video {
+ max-width: 200px;
+ max-height: 250px;
+ }
+
+ .videoMessage .videoPlayIcon {
+ .loadingSpinner {
+ width: 36px;
+ height: 36px;
+ border-width: 3px;
+ }
+ }
+
+ .videoMessage .downloadButton {
+ width: 28px;
+ height: 28px;
+ top: 6px;
+ right: 6px;
+
+ svg {
+ font-size: 14px !important;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VideoMessage/index.tsx b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VideoMessage/index.tsx
new file mode 100644
index 00000000..1edbcd30
--- /dev/null
+++ b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VideoMessage/index.tsx
@@ -0,0 +1,182 @@
+import React from "react";
+import { DownloadOutlined, PlayCircleFilled } from "@ant-design/icons";
+import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
+import { useWeChatStore } from "@/store/module/weChat/weChat";
+import { useWebSocketStore } from "@/store/module/websocket/websocket";
+import styles from "./VideoMessage.module.scss";
+
+interface VideoMessageProps {
+ content: string;
+ msg: ChatRecord;
+ contract: ContractData | weChatGroup;
+}
+
+const VideoMessage: React.FC = ({
+ content,
+ msg,
+ contract,
+}) => {
+ // 检测是否为直接视频链接的函数
+ const isDirectVideoLink = (content: string): boolean => {
+ const trimmedContent = content.trim();
+ return (
+ trimmedContent.startsWith("http") &&
+ (trimmedContent.includes(".mp4") ||
+ trimmedContent.includes(".mov") ||
+ trimmedContent.includes(".avi") ||
+ trimmedContent.includes("video"))
+ );
+ };
+
+ // 处理视频播放请求,发送socket请求获取真实视频地址
+ const handleVideoPlayRequest = (tencentUrl: string, messageId: number) => {
+ console.log("发送视频下载请求:", { messageId, tencentUrl });
+
+ // 先设置加载状态
+ useWeChatStore.getState().setVideoLoading(messageId, true);
+
+ // 构建socket请求数据
+ useWebSocketStore.getState().sendCommand("CmdDownloadVideo", {
+ chatroomMessageId: contract.chatroomId ? messageId : 0,
+ friendMessageId: contract.chatroomId ? 0 : messageId,
+ seq: `${+new Date()}`, // 使用时间戳作为请求序列号
+ tencentUrl: tencentUrl,
+ wechatAccountId: contract.wechatAccountId,
+ });
+ };
+
+ // 渲染错误消息
+ const renderErrorMessage = (message: string) => (
+ {message}
+ );
+
+ if (typeof content !== "string" || !content.trim()) {
+ return renderErrorMessage("[视频消息 - 无效内容]");
+ }
+
+ // 如果content是直接的视频链接(已预览过或下载好的视频)
+ if (isDirectVideoLink(content)) {
+ return (
+
+ );
+ }
+
+ try {
+ // 尝试解析JSON格式的视频数据
+ if (content.startsWith("{") && content.endsWith("}")) {
+ const videoData = JSON.parse(content);
+
+ // 验证必要的视频数据字段
+ if (
+ videoData &&
+ typeof videoData === "object" &&
+ videoData.previewImage &&
+ videoData.tencentUrl
+ ) {
+ const previewImageUrl = String(videoData.previewImage).replace(
+ /[`"']/g,
+ "",
+ );
+
+ // 创建点击处理函数
+ const handlePlayClick = (e: React.MouseEvent, msg: ChatRecord) => {
+ e.stopPropagation();
+ // 如果没有视频URL且不在加载中,则发起下载请求
+ if (!videoData.videoUrl && !videoData.isLoading) {
+ handleVideoPlayRequest(videoData.tencentUrl, msg.id);
+ }
+ };
+
+ // 如果已有视频URL,显示视频播放器
+ if (videoData.videoUrl) {
+ return (
+
+ );
+ }
+
+ // 显示预览图,根据加载状态显示不同的图标
+ return (
+
+
+
handlePlayClick(e, msg)}
+ >
+

{
+ const target = e.target as HTMLImageElement;
+ const parent = target.parentElement?.parentElement;
+ if (parent) {
+ parent.innerHTML = `
[视频预览加载失败]
`;
+ }
+ }}
+ />
+
+ {videoData.isLoading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+ }
+ }
+ return renderErrorMessage("[视频消息]");
+ } catch (e) {
+ console.warn("视频消息解析失败:", e);
+ return renderErrorMessage("[视频消息 - 解析失败]");
+ }
+};
+
+export default VideoMessage;
diff --git a/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx
index c90e216e..001c0954 100644
--- a/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx
+++ b/Cunkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx
@@ -1,23 +1,21 @@
import React, { useEffect, useRef } from "react";
import { Avatar, Divider } from "antd";
-import {
- UserOutlined,
- LoadingOutlined,
- DownloadOutlined,
- PlayCircleFilled,
-} from "@ant-design/icons";
+import { UserOutlined, LoadingOutlined } from "@ant-design/icons";
+import AudioMessage from "./components/AudioMessage/AudioMessage";
+import SmallProgramMessage from "./components/SmallProgramMessage";
+import VideoMessage from "./components/VideoMessage";
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
-import { formatWechatTime, parseWeappMsgStr } from "@/utils/common";
+import { formatWechatTime } from "@/utils/common";
import { getEmojiPath } from "@/components/EmojiSeclection/wechatEmoji";
import styles from "./MessageRecord.module.scss";
import { useWeChatStore } from "@/store/module/weChat/weChat";
-import { useWebSocketStore } from "@/store/module/websocket/websocket";
interface MessageRecordProps {
contract: ContractData | weChatGroup;
}
const MessageRecord: React.FC = ({ contract }) => {
const messagesEndRef = useRef(null);
+
const currentMessages = useWeChatStore(state => state.currentMessages);
const loadChatMessages = useWeChatStore(state => state.loadChatMessages);
const messagesLoading = useWeChatStore(state => state.messagesLoading);
@@ -27,18 +25,6 @@ const MessageRecord: React.FC = ({ contract }) => {
);
const prevMessagesRef = useRef(currentMessages);
- // 检测是否为直接视频链接的函数
- const isDirectVideoLink = (content: string): boolean => {
- const trimmedContent = content.trim();
- return (
- trimmedContent.startsWith("http") &&
- (trimmedContent.includes(".mp4") ||
- trimmedContent.includes(".mov") ||
- trimmedContent.includes(".avi") ||
- trimmedContent.includes("video"))
- );
- };
-
// 判断是否为表情包URL的工具函数
const isEmojiUrl = (content: string): boolean => {
return (
@@ -153,23 +139,6 @@ const MessageRecord: React.FC = ({ contract }) => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
- // 处理视频播放请求,发送socket请求获取真实视频地址
- const handleVideoPlayRequest = (tencentUrl: string, messageId: number) => {
- console.log("发送视频下载请求:", { messageId, tencentUrl });
-
- // 先设置加载状态
- useWeChatStore.getState().setVideoLoading(messageId, true);
-
- // 构建socket请求数据
- useWebSocketStore.getState().sendCommand("CmdDownloadVideo", {
- chatroomMessageId: contract.chatroomId ? messageId : 0,
- friendMessageId: contract.chatroomId ? 0 : messageId,
- seq: `${+new Date()}`, // 使用时间戳作为请求序列号
- tencentUrl: tencentUrl,
- wechatAccountId: contract.wechatAccountId,
- });
- };
-
// 解析消息内容,根据msgType判断消息类型并返回对应的渲染内容
const parseMessageContent = (
content: string | null | undefined,
@@ -225,136 +194,9 @@ const MessageRecord: React.FC = ({ contract }) => {
);
case 43: // 视频消息
- if (typeof content !== "string" || !content.trim()) {
- return renderErrorMessage("[视频消息 - 无效内容]");
- }
-
- // 如果content是直接的视频链接(已预览过或下载好的视频)
- if (isDirectVideoLink(content)) {
- return (
-
- );
- }
-
- try {
- // 尝试解析JSON格式的视频数据
- if (content.startsWith("{") && content.endsWith("}")) {
- const videoData = JSON.parse(content);
-
- // 验证必要的视频数据字段
- if (
- videoData &&
- typeof videoData === "object" &&
- videoData.previewImage &&
- videoData.tencentUrl
- ) {
- const previewImageUrl = String(videoData.previewImage).replace(
- /[`"']/g,
- "",
- );
-
- // 创建点击处理函数
- const handlePlayClick = (
- e: React.MouseEvent,
- msg: ChatRecord,
- ) => {
- e.stopPropagation();
- // 如果没有视频URL且不在加载中,则发起下载请求
- if (!videoData.videoUrl && !videoData.isLoading) {
- handleVideoPlayRequest(videoData.tencentUrl, msg.id);
- }
- };
-
- // 如果已有视频URL,显示视频播放器
- if (videoData.videoUrl) {
- return (
-
- );
- }
-
- // 显示预览图,根据加载状态显示不同的图标
- return (
-
-
-
handlePlayClick(e, msg)}
- >
-

{
- const target = e.target as HTMLImageElement;
- const parent = target.parentElement?.parentElement;
- if (parent) {
- parent.innerHTML = `
[视频预览加载失败]
`;
- }
- }}
- />
-
- {videoData.isLoading ? (
-
- ) : (
-
- )}
-
-
-
-
- );
- }
- }
- return renderErrorMessage("[视频消息]");
- } catch (e) {
- console.warn("视频消息解析失败:", e);
- return renderErrorMessage("[视频消息 - 解析失败]");
- }
+ return (
+
+ );
case 47: // 动图表情包(gif、其他表情包)
if (typeof content !== "string" || !content.trim()) {
@@ -383,259 +225,16 @@ const MessageRecord: React.FC = ({ contract }) => {
}
return renderErrorMessage("[表情包]");
- case 49: // 小程序/文章/其他:图文、文件
+ case 34: // 语音消息
if (typeof content !== "string" || !content.trim()) {
- return renderErrorMessage("[小程序/文章/文件消息 - 无效内容]");
+ return renderErrorMessage("[语音消息 - 无效内容]");
}
- try {
- const trimmedContent = content.trim();
+ // content直接是音频URL字符串
+ return ;
- // 尝试解析JSON格式的消息
- if (trimmedContent.startsWith("{") && trimmedContent.endsWith("}")) {
- const messageData = JSON.parse(trimmedContent);
-
- // 处理文章类型消息
- if (
- messageData.type === "link" &&
- messageData.title &&
- messageData.url
- ) {
- const { title, desc, thumbPath, url } = messageData;
-
- return (
-
-
window.open(url, "_blank")}
- >
- {/* 标题在第一行 */}
-
{title}
-
- {/* 下方:文字在左,图片在右 */}
-
-
- {desc && (
-
- {desc}
-
- )}
-
- {thumbPath && (
-
-

{
- const target = e.target as HTMLImageElement;
- target.style.display = "none";
- }}
- />
-
- )}
-
-
-
文章
-
- );
- }
-
- // 处理小程序消息 - 统一使用parseWeappMsgStr解析
- if (messageData.type === "miniprogram" && messageData.contentXml) {
- try {
- const parsedData = parseWeappMsgStr(trimmedContent);
-
- if (parsedData.appmsg) {
- const { appmsg } = parsedData;
- const title = appmsg.title || "小程序消息";
- const appName =
- appmsg.sourcedisplayname || appmsg.appname || "小程序";
-
- // 获取小程序类型
- const miniProgramType =
- appmsg.weappinfo && appmsg.weappinfo.type
- ? parseInt(appmsg.weappinfo.type)
- : 1;
-
- // 根据type类型渲染不同布局
- if (miniProgramType === 2) {
- // 类型2:图片区域布局
- return (
-
-
-
- {appName}
-
-
{title}
-
-

{
- const target = e.target as HTMLImageElement;
- target.style.display = "none";
- }}
- />
-
-
-
-
- );
- } else {
- // 默认类型:横向布局
- return (
-
-
-

{
- const target = e.target as HTMLImageElement;
- target.style.display = "none";
- }}
- />
-
-
-
{appName}
-
- );
- }
- }
- } catch (parseError) {
- console.error("parseWeappMsgStr解析失败:", parseError);
- return renderErrorMessage("[小程序消息 - 解析失败]");
- }
- }
-
- // 验证传统JSON格式的小程序数据结构
- if (
- messageData &&
- typeof messageData === "object" &&
- (messageData.title || messageData.appName)
- ) {
- return (
-
-
- {messageData.thumb && (
-

{
- const target = e.target as HTMLImageElement;
- target.style.display = "none";
- }}
- />
- )}
-
-
- {messageData.title || "小程序消息"}
-
- {messageData.appName && (
-
- {messageData.appName}
-
- )}
-
-
-
- );
- }
- }
-
- // 增强的文件消息处理
- const isFileUrl =
- content.startsWith("http") ||
- content.startsWith("https") ||
- content.startsWith("file://") ||
- /\.(pdf|doc|docx|xls|xlsx|ppt|pptx|txt|zip|rar|7z)$/i.test(content);
-
- if (isFileUrl) {
- // 尝试从URL中提取文件名
- const fileName = content.split("/").pop()?.split("?")[0] || "文件";
- const fileExtension = fileName.split(".").pop()?.toLowerCase();
-
- // 根据文件类型选择图标
- let fileIcon = "📄";
- if (fileExtension) {
- const iconMap: { [key: string]: string } = {
- pdf: "📕",
- doc: "📘",
- docx: "📘",
- xls: "📗",
- xlsx: "📗",
- ppt: "📙",
- pptx: "📙",
- txt: "📝",
- zip: "🗜️",
- rar: "🗜️",
- "7z": "🗜️",
- jpg: "🖼️",
- jpeg: "🖼️",
- png: "🖼️",
- gif: "🖼️",
- mp4: "🎬",
- avi: "🎬",
- mov: "🎬",
- mp3: "🎵",
- wav: "🎵",
- flac: "🎵",
- };
- fileIcon = iconMap[fileExtension] || "📄";
- }
-
- return (
-
-
-
{fileIcon}
-
-
- {fileName.length > 20
- ? fileName.substring(0, 20) + "..."
- : fileName}
-
-
{
- try {
- window.open(content, "_blank");
- } catch (e) {
- console.error("文件打开失败:", e);
- }
- }}
- >
- 点击查看
-
-
-
-
- );
- }
-
- return renderErrorMessage("[小程序/文件消息]");
- } catch (e) {
- console.warn("小程序/文件消息解析失败:", e);
- return renderErrorMessage("[小程序/文件消息 - 解析失败]");
- }
+ case 49: // 小程序/文章/其他:图文、文件
+ return ;
default: {
// 兼容旧版本和未知消息类型的处理逻辑