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