feat(MessageRecord): 优化消息渲染逻辑并增强样式
重构消息内容解析逻辑,增加对多种消息类型的支持,包括文本、图片、视频、表情包、小程序和文件消息。改进错误处理和兼容性,同时优化UI样式和移动端适配。 - 新增消息类型检测工具函数 - 增强JSON解析和错误处理 - 优化小程序消息的XML解析和显示 - 改进文件消息的图标和文件名显示 - 添加移动端样式适配 - 统一错误消息渲染逻辑
This commit is contained in:
@@ -55,7 +55,7 @@
|
|||||||
.messageBubble {
|
.messageBubble {
|
||||||
background: #1890ff;
|
background: #1890ff;
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 18px 4px 18px 18px;
|
border-radius: 4px 18px 18px 18px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,37 +180,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 小程序消息
|
// 小程序消息样式
|
||||||
.miniProgramMessage {
|
.miniProgramMessage {
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||||
.miniProgramCard {
|
.miniProgramCard {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
padding: 12px;
|
padding: 10px 14px;
|
||||||
border: 1px solid #e8e8e8;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
max-width: 280px;
|
|
||||||
|
|
||||||
&:hover {
|
cursor: pointer;
|
||||||
border-color: #1890ff;
|
transition: all 0.2s ease;
|
||||||
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1);
|
min-width: 260px;
|
||||||
}
|
min-height: 56px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.miniProgramThumb {
|
.miniProgramThumb {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: 6px;
|
background: #f0f0f0;
|
||||||
object-fit: cover;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.miniProgramInfo {
|
.miniProgramInfo {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.miniProgramTitle {
|
.miniProgramTitle {
|
||||||
@@ -218,23 +215,31 @@
|
|||||||
color: #262626;
|
color: #262626;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 3px;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 1;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.miniProgramApp {
|
.miniProgramApp {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #8c8c8c;
|
color: #666666;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: "📱";
|
content: "";
|
||||||
font-size: 10px;
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTcgMEM0LjI0IDAgMiAyLjI0IDIgNVY5QzIgMTEuNzYgNC4yNCAxNCA3IDE0UzEyIDExLjc2IDEyIDlWNUMxMiAyLjI0IDkuNzYgMCA3IDBaTTEwIDlDMTAgMTAuNjUgOC42NSAxMiA3IDEyUzQgMTAuNjUgNCA5VjVDNCAzLjM1IDUuMzUgMiA3IDJTMTAgMy4zNSAxMCA1VjlaIiBmaWxsPSIjNjY2NjY2Ii8+Cjwvc3ZnPgo=")
|
||||||
|
no-repeat center;
|
||||||
|
background-size: contain;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,4 +309,37 @@
|
|||||||
max-width: 150px;
|
max-width: 150px;
|
||||||
max-height: 150px;
|
max-height: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 小程序消息移动端适配
|
||||||
|
.miniProgramMessage {
|
||||||
|
min-width: 400px;
|
||||||
|
.miniProgramCard {
|
||||||
|
max-width: 260px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 52px;
|
||||||
|
&:hover {
|
||||||
|
transform: none; // 移动端禁用悬浮动画
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.miniProgramThumb {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.miniProgramTitle {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.miniProgramApp {
|
||||||
|
font-size: 11px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,17 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
state => state.currentGroupMembers,
|
state => state.currentGroupMembers,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 判断是否为表情包URL的工具函数
|
||||||
|
const isEmojiUrl = (content: string): boolean => {
|
||||||
|
return (
|
||||||
|
content.includes("ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com") ||
|
||||||
|
/\.(gif|webp|png|jpg|jpeg)$/i.test(content) ||
|
||||||
|
content.includes("emoji") ||
|
||||||
|
content.includes("sticker") ||
|
||||||
|
content.includes("expression")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoadingData) {
|
if (isLoadingData) {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
@@ -40,48 +51,72 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
return <div className={styles.messageText}>消息内容不可用</div>;
|
return <div className={styles.messageText}>消息内容不可用</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 统一的错误消息渲染函数
|
||||||
|
const renderErrorMessage = (fallbackText: string) => (
|
||||||
|
<div className={styles.messageText}>{fallbackText}</div>
|
||||||
|
);
|
||||||
|
|
||||||
// 根据msgType进行消息类型判断
|
// 根据msgType进行消息类型判断
|
||||||
switch (msgType) {
|
switch (msgType) {
|
||||||
case 1: // 文本消息
|
case 1: // 文本消息
|
||||||
return <div className={styles.messageText}>{content}</div>;
|
return (
|
||||||
|
<div className={styles.messageBubble}>
|
||||||
|
<div className={styles.messageText}>{content}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
case 3: // 图片消息
|
case 3: // 图片消息
|
||||||
|
// 验证是否为有效的图片URL
|
||||||
|
if (typeof content !== "string" || !content.trim()) {
|
||||||
|
return renderErrorMessage("[图片消息 - 无效链接]");
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className={styles.imageMessage}>
|
<div className={styles.messageBubble}>
|
||||||
<img
|
<div className={styles.imageMessage}>
|
||||||
src={content}
|
<img
|
||||||
alt="图片消息"
|
src={content}
|
||||||
style={{
|
alt="图片消息"
|
||||||
maxWidth: "200px",
|
style={{
|
||||||
maxHeight: "200px",
|
maxWidth: "200px",
|
||||||
borderRadius: "8px",
|
maxHeight: "200px",
|
||||||
}}
|
borderRadius: "8px",
|
||||||
onClick={() => window.open(content, "_blank")}
|
}}
|
||||||
onError={e => {
|
onClick={() => window.open(content, "_blank")}
|
||||||
const target = e.target as HTMLImageElement;
|
onError={e => {
|
||||||
target.style.display = "none";
|
const target = e.target as HTMLImageElement;
|
||||||
target.parentElement!.innerHTML = `<div class="${styles.messageText}">图片加载失败</div>`;
|
const parent = target.parentElement;
|
||||||
}}
|
if (parent) {
|
||||||
/>
|
parent.innerHTML = `<div class="${styles.messageText}">[图片加载失败]</div>`;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 43: // 视频消息
|
case 43: // 视频消息
|
||||||
|
if (typeof content !== "string" || !content.trim()) {
|
||||||
|
return renderErrorMessage("[视频消息 - 无效内容]");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (
|
// 更严格的JSON格式验证
|
||||||
typeof content === "string" &&
|
const trimmedContent = content.trim();
|
||||||
content.trim().startsWith("{") &&
|
if (trimmedContent.startsWith("{") && trimmedContent.endsWith("}")) {
|
||||||
content.trim().endsWith("}")
|
const videoData = JSON.parse(trimmedContent);
|
||||||
) {
|
|
||||||
const videoData = JSON.parse(content);
|
// 验证必要的视频数据字段
|
||||||
if (
|
if (
|
||||||
|
videoData &&
|
||||||
|
typeof videoData === "object" &&
|
||||||
videoData.previewImage &&
|
videoData.previewImage &&
|
||||||
(videoData.tencentUrl || videoData.videoUrl)
|
(videoData.tencentUrl || videoData.videoUrl)
|
||||||
) {
|
) {
|
||||||
const previewImageUrl = videoData.previewImage.replace(
|
const previewImageUrl = String(videoData.previewImage).replace(
|
||||||
/[`"']/g,
|
/[`"']/g,
|
||||||
"",
|
"",
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.videoMessage}>
|
<div className={styles.videoMessage}>
|
||||||
<div className={styles.videoContainer}>
|
<div className={styles.videoContainer}>
|
||||||
@@ -90,10 +125,17 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
alt="视频预览"
|
alt="视频预览"
|
||||||
className={styles.videoPreview}
|
className={styles.videoPreview}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (videoData.videoUrl) {
|
const videoUrl =
|
||||||
window.open(videoData.videoUrl, "_blank");
|
videoData.videoUrl || videoData.tencentUrl;
|
||||||
} else if (videoData.tencentUrl) {
|
if (videoUrl) {
|
||||||
window.open(videoData.tencentUrl, "_blank");
|
window.open(videoUrl, "_blank");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onError={e => {
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
const parent = target.parentElement?.parentElement;
|
||||||
|
if (parent) {
|
||||||
|
parent.innerHTML = `<div class="${styles.messageText}">[视频预览加载失败]</div>`;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -112,21 +154,19 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 如果JSON解析失败或格式不正确,显示视频消息提示
|
return renderErrorMessage("[视频消息]");
|
||||||
return <div className={styles.messageText}>[视频消息]</div>;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return <div className={styles.messageText}>[视频消息]</div>;
|
console.warn("视频消息解析失败:", e);
|
||||||
|
return renderErrorMessage("[视频消息 - 解析失败]");
|
||||||
}
|
}
|
||||||
|
|
||||||
case 47: // 动图表情包(gif、其他表情包)
|
case 47: // 动图表情包(gif、其他表情包)
|
||||||
// 检查是否为特定URL的表情包
|
if (typeof content !== "string" || !content.trim()) {
|
||||||
if (
|
return renderErrorMessage("[表情包 - 无效链接]");
|
||||||
typeof content === "string" &&
|
}
|
||||||
(content.includes("ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com") ||
|
|
||||||
content.includes(".gif") ||
|
// 使用工具函数判断表情包URL
|
||||||
content.includes("emoji") ||
|
if (isEmojiUrl(content)) {
|
||||||
content.includes("sticker"))
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.emojiMessage}>
|
<div className={styles.emojiMessage}>
|
||||||
<img
|
<img
|
||||||
@@ -136,25 +176,91 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
onClick={() => window.open(content, "_blank")}
|
onClick={() => window.open(content, "_blank")}
|
||||||
onError={e => {
|
onError={e => {
|
||||||
const target = e.target as HTMLImageElement;
|
const target = e.target as HTMLImageElement;
|
||||||
target.style.display = "none";
|
const parent = target.parentElement;
|
||||||
target.parentElement!.innerHTML = `<div class="${styles.messageText}">[表情包]</div>`;
|
if (parent) {
|
||||||
|
parent.innerHTML = `<div class="${styles.messageText}">[表情包加载失败]</div>`;
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <div className={styles.messageText}>[表情包]</div>;
|
return renderErrorMessage("[表情包]");
|
||||||
|
|
||||||
case 49: // 小程序/其他:图文、文件
|
case 49: // 小程序/其他:图文、文件
|
||||||
|
if (typeof content !== "string" || !content.trim()) {
|
||||||
|
return renderErrorMessage("[小程序/文件消息 - 无效内容]");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const trimmedContent = content.trim();
|
||||||
|
|
||||||
// 尝试解析JSON格式的小程序消息
|
// 尝试解析JSON格式的小程序消息
|
||||||
if (
|
if (trimmedContent.startsWith("{") && trimmedContent.endsWith("}")) {
|
||||||
typeof content === "string" &&
|
const miniProgramData = JSON.parse(trimmedContent);
|
||||||
content.trim().startsWith("{") &&
|
|
||||||
content.trim().endsWith("}")
|
// 处理包含contentXml的小程序消息格式
|
||||||
) {
|
if (
|
||||||
const miniProgramData = JSON.parse(content);
|
miniProgramData.contentXml &&
|
||||||
if (miniProgramData.title || miniProgramData.appName) {
|
miniProgramData.type === "miniprogram"
|
||||||
|
) {
|
||||||
|
const xmlContent = miniProgramData.contentXml;
|
||||||
|
|
||||||
|
// 从XML中提取title
|
||||||
|
const titleMatch = xmlContent.match(/<title>([^<]*)<\/title>/);
|
||||||
|
const title = titleMatch ? titleMatch[1] : "小程序消息";
|
||||||
|
|
||||||
|
// 从XML中提取thumburl或使用previewImage
|
||||||
|
const thumbUrlMatch = xmlContent.match(
|
||||||
|
/<thumburl>\s*([^<]*?)\s*<\/thumburl>/,
|
||||||
|
);
|
||||||
|
let thumbUrl = thumbUrlMatch ? thumbUrlMatch[1].trim() : "";
|
||||||
|
|
||||||
|
// 如果thumburl为空或无效,使用previewImage
|
||||||
|
if (!thumbUrl || thumbUrl === "`" || thumbUrl.includes("`")) {
|
||||||
|
thumbUrl = miniProgramData.previewImage || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理URL中的特殊字符
|
||||||
|
thumbUrl = thumbUrl.replace(/[`"']/g, "").replace(/&/g, "&");
|
||||||
|
|
||||||
|
// 从XML中提取appname或使用默认值
|
||||||
|
const appNameMatch =
|
||||||
|
xmlContent.match(/<appname\s*\/?>([^<]*)<\/appname>/) ||
|
||||||
|
xmlContent.match(
|
||||||
|
/<sourcedisplayname>([^<]*)<\/sourcedisplayname>/,
|
||||||
|
);
|
||||||
|
const appName = appNameMatch ? appNameMatch[1] : "小程序";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.miniProgramMessage}>
|
||||||
|
<div className={styles.miniProgramCard}>
|
||||||
|
{thumbUrl && (
|
||||||
|
<img
|
||||||
|
src={thumbUrl}
|
||||||
|
alt="小程序缩略图"
|
||||||
|
className={styles.miniProgramThumb}
|
||||||
|
onError={e => {
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
target.style.display = "none";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className={styles.miniProgramInfo}>
|
||||||
|
<div className={styles.miniProgramTitle}>{title}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.miniProgramApp}>{appName}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证传统JSON格式的小程序数据结构
|
||||||
|
if (
|
||||||
|
miniProgramData &&
|
||||||
|
typeof miniProgramData === "object" &&
|
||||||
|
(miniProgramData.title || miniProgramData.appName)
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.miniProgramMessage}>
|
<div className={styles.miniProgramMessage}>
|
||||||
<div className={styles.miniProgramCard}>
|
<div className={styles.miniProgramCard}>
|
||||||
@@ -163,6 +269,10 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
src={miniProgramData.thumb}
|
src={miniProgramData.thumb}
|
||||||
alt="小程序缩略图"
|
alt="小程序缩略图"
|
||||||
className={styles.miniProgramThumb}
|
className={styles.miniProgramThumb}
|
||||||
|
onError={e => {
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
target.style.display = "none";
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className={styles.miniProgramInfo}>
|
<div className={styles.miniProgramInfo}>
|
||||||
@@ -180,20 +290,67 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 文件消息处理
|
|
||||||
if (
|
// 增强的文件消息处理
|
||||||
typeof content === "string" &&
|
const isFileUrl =
|
||||||
(content.includes("http") || content.includes("file"))
|
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 (
|
return (
|
||||||
<div className={styles.fileMessage}>
|
<div className={styles.fileMessage}>
|
||||||
<div className={styles.fileCard}>
|
<div className={styles.fileCard}>
|
||||||
<div className={styles.fileIcon}>📄</div>
|
<div className={styles.fileIcon}>{fileIcon}</div>
|
||||||
<div className={styles.fileInfo}>
|
<div className={styles.fileInfo}>
|
||||||
<div className={styles.fileName}>文件消息</div>
|
<div className={styles.fileName}>
|
||||||
|
{fileName.length > 20
|
||||||
|
? fileName.substring(0, 20) + "..."
|
||||||
|
: fileName}
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className={styles.fileAction}
|
className={styles.fileAction}
|
||||||
onClick={() => window.open(content, "_blank")}
|
onClick={() => {
|
||||||
|
try {
|
||||||
|
window.open(content, "_blank");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("文件打开失败:", e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
点击查看
|
点击查看
|
||||||
</div>
|
</div>
|
||||||
@@ -202,41 +359,62 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <div className={styles.messageText}>[小程序/文件消息]</div>;
|
|
||||||
|
return renderErrorMessage("[小程序/文件消息]");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return <div className={styles.messageText}>[小程序/文件消息]</div>;
|
console.warn("小程序/文件消息解析失败:", e);
|
||||||
|
return renderErrorMessage("[小程序/文件消息 - 解析失败]");
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default: {
|
||||||
// 兼容旧版本:如果没有msgType或msgType不在已知范围内,使用原有逻辑
|
// 兼容旧版本和未知消息类型的处理逻辑
|
||||||
// 检查是否为表情包(兼容旧逻辑)
|
if (typeof content !== "string" || !content.trim()) {
|
||||||
if (
|
return renderErrorMessage(
|
||||||
typeof content === "string" &&
|
`[未知消息类型${msgType ? ` - ${msgType}` : ""}]`,
|
||||||
content.includes("ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com") &&
|
);
|
||||||
content.includes("#")
|
}
|
||||||
) {
|
|
||||||
|
// 智能识别消息类型(兼容旧版本数据)
|
||||||
|
const contentStr = content.trim();
|
||||||
|
|
||||||
|
// 1. 检查是否为表情包(兼容旧逻辑)
|
||||||
|
const isLegacyEmoji =
|
||||||
|
contentStr.includes("ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com") ||
|
||||||
|
/\.(gif|webp|png|jpg|jpeg)$/i.test(contentStr) ||
|
||||||
|
contentStr.includes("emoji") ||
|
||||||
|
contentStr.includes("sticker");
|
||||||
|
|
||||||
|
if (isLegacyEmoji) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.emojiMessage}>
|
<div className={styles.emojiMessage}>
|
||||||
<img
|
<img
|
||||||
src={content}
|
src={contentStr}
|
||||||
alt="表情包"
|
alt="表情包"
|
||||||
style={{ maxWidth: "120px", maxHeight: "120px" }}
|
style={{ maxWidth: "120px", maxHeight: "120px" }}
|
||||||
onClick={() => window.open(content, "_blank")}
|
onClick={() => window.open(contentStr, "_blank")}
|
||||||
|
onError={e => {
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
const parent = target.parentElement;
|
||||||
|
if (parent) {
|
||||||
|
parent.innerHTML = `<div class="${styles.messageText}">[表情包加载失败]</div>`;
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否为带预览图的视频消息(兼容旧逻辑)
|
// 2. 检查是否为视频消息(兼容旧逻辑)
|
||||||
try {
|
if (contentStr.startsWith("{") && contentStr.endsWith("}")) {
|
||||||
if (
|
try {
|
||||||
typeof content === "string" &&
|
const videoData = JSON.parse(contentStr);
|
||||||
content.trim().startsWith("{") &&
|
if (
|
||||||
content.trim().endsWith("}")
|
videoData &&
|
||||||
) {
|
typeof videoData === "object" &&
|
||||||
const videoData = JSON.parse(content);
|
videoData.previewImage &&
|
||||||
if (videoData.previewImage && videoData.tencentUrl) {
|
(videoData.tencentUrl || videoData.videoUrl)
|
||||||
const previewImageUrl = videoData.previewImage.replace(
|
) {
|
||||||
|
const previewImageUrl = String(videoData.previewImage).replace(
|
||||||
/[`"']/g,
|
/[`"']/g,
|
||||||
"",
|
"",
|
||||||
);
|
);
|
||||||
@@ -248,10 +426,17 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
alt="视频预览"
|
alt="视频预览"
|
||||||
className={styles.videoPreview}
|
className={styles.videoPreview}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (videoData.videoUrl) {
|
const videoUrl =
|
||||||
window.open(videoData.videoUrl, "_blank");
|
videoData.videoUrl || videoData.tencentUrl;
|
||||||
} else if (videoData.tencentUrl) {
|
if (videoUrl) {
|
||||||
window.open(videoData.tencentUrl, "_blank");
|
window.open(videoUrl, "_blank");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onError={e => {
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
const parent = target.parentElement?.parentElement;
|
||||||
|
if (parent) {
|
||||||
|
parent.innerHTML = `<div class="${styles.messageText}">[视频预览加载失败]</div>`;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -269,13 +454,74 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("兼容模式JSON解析失败:", e);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
// JSON解析失败,按普通文本处理
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 普通文本消息
|
// 3. 检查是否为图片链接
|
||||||
|
const isImageUrl =
|
||||||
|
contentStr.startsWith("http") &&
|
||||||
|
/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(contentStr);
|
||||||
|
|
||||||
|
if (isImageUrl) {
|
||||||
|
return (
|
||||||
|
<div className={styles.imageMessage}>
|
||||||
|
<img
|
||||||
|
src={contentStr}
|
||||||
|
alt="图片消息"
|
||||||
|
style={{
|
||||||
|
maxWidth: "200px",
|
||||||
|
maxHeight: "200px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
onClick={() => window.open(contentStr, "_blank")}
|
||||||
|
onError={e => {
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
const parent = target.parentElement;
|
||||||
|
if (parent) {
|
||||||
|
parent.innerHTML = `<div class="${styles.messageText}">[图片加载失败]</div>`;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 检查是否为文件链接
|
||||||
|
const isFileLink =
|
||||||
|
contentStr.startsWith("http") &&
|
||||||
|
/\.(pdf|doc|docx|xls|xlsx|ppt|pptx|txt|zip|rar|7z)$/i.test(
|
||||||
|
contentStr,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isFileLink) {
|
||||||
|
const fileName = contentStr.split("/").pop()?.split("?")[0] || "文件";
|
||||||
|
return (
|
||||||
|
<div className={styles.fileMessage}>
|
||||||
|
<div className={styles.fileCard}>
|
||||||
|
<div className={styles.fileIcon}>📄</div>
|
||||||
|
<div className={styles.fileInfo}>
|
||||||
|
<div className={styles.fileName}>
|
||||||
|
{fileName.length > 20
|
||||||
|
? fileName.substring(0, 20) + "..."
|
||||||
|
: fileName}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={styles.fileAction}
|
||||||
|
onClick={() => window.open(contentStr, "_blank")}
|
||||||
|
>
|
||||||
|
点击查看
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 默认按文本消息处理
|
||||||
return <div className={styles.messageText}>{content}</div>;
|
return <div className={styles.messageText}>{content}</div>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -331,13 +577,13 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
className={styles.messageAvatar}
|
className={styles.messageAvatar}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.messageBubble}>
|
<div>
|
||||||
{!isOwn && (
|
{!isOwn && (
|
||||||
<div className={styles.messageSender}>
|
<div className={styles.messageSender}>
|
||||||
{contract.nickname}
|
{contract.nickname}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{parseMessageContent(msg?.content, msg?.msgType)}
|
<>{parseMessageContent(msg?.content, msg?.msgType)}</>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -351,25 +597,23 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
className={styles.messageAvatar}
|
className={styles.messageAvatar}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.messageBubble}>
|
<div>
|
||||||
{!isOwn && (
|
{!isOwn && (
|
||||||
<div className={styles.messageSender}>
|
<div className={styles.messageSender}>
|
||||||
{msg?.sender?.nickname}
|
{msg?.sender?.nickname}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{parseMessageContent(
|
<>
|
||||||
clearWechatidInContent(msg?.sender, msg?.content),
|
{parseMessageContent(
|
||||||
msg?.msgType,
|
clearWechatidInContent(msg?.sender, msg?.content),
|
||||||
)}
|
msg?.msgType,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isOwn && (
|
{isOwn && <>{parseMessageContent(msg?.content, msg?.msgType)}</>}
|
||||||
<div className={styles.messageBubble}>
|
|
||||||
{parseMessageContent(msg?.content, msg?.msgType)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user