From 4e8c728457153d76d039906dbf4c22a841b6ff73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Mon, 8 Sep 2025 15:19:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=B6=88=E6=81=AF=E8=AE=B0=E5=BD=95):=20?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=B0=8F=E7=A8=8B=E5=BA=8F=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?2=E5=92=8C=E6=96=87=E7=AB=A0=E9=93=BE=E6=8E=A5=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E6=A0=B7=E5=BC=8F=E5=8F=8A=E5=A4=84=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加对小程序类型2(垂直布局)的支持,包括样式和渲染逻辑 新增文章类型消息的样式和解析处理 增加链接类型消息的样式和解析处理 优化现有小程序消息的样式和布局 --- .../MessageRecord/MessageRecord.module.scss | 204 ++++++++++++++-- .../components/MessageRecord/index.tsx | 219 ++++++++++++++---- 2 files changed, 362 insertions(+), 61 deletions(-) diff --git a/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/components/MessageRecord/MessageRecord.module.scss b/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/components/MessageRecord/MessageRecord.module.scss index d74bea3a..3a85b50a 100644 --- a/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/components/MessageRecord/MessageRecord.module.scss +++ b/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/components/MessageRecord/MessageRecord.module.scss @@ -180,14 +180,15 @@ } } -// 小程序消息样式 +// 小程序消息基础样式 .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-bottom: 1px solid #e1e8ed; background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%); @@ -198,6 +199,7 @@ overflow: hidden; } + // 通用小程序元素样式 .miniProgramThumb { width: 50px; height: 50px; @@ -222,6 +224,7 @@ } .miniProgramTitle { + padding-left: 16px; font-weight: 600; color: #1a1a1a; font-size: 14px; @@ -244,6 +247,189 @@ } } +// 类型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; + + &: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; + } + } + } + } +} + +// 链接类型消息样式 +.linkMessage { + .linkCard { + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + } + } + + .linkDescription { + font-size: 12px; + color: #666; + line-height: 1.4; + margin: 4px 0; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + } +} + +// 文章类型消息样式 +.articleMessage { + .articleCard { + flex-direction: column; + align-items: stretch; + padding: 16px; + min-height: auto; + max-width: 320px; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + } + } + + .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: 80px; + height: 80px; + overflow: hidden; + border-radius: 8px; + background: #f8f9fa; + } + + .articleImage { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; + + &:hover { + transform: scale(1.05); + } + } +} + // 文件消息样式 .fileMessage { .fileCard { @@ -315,22 +501,8 @@ .miniProgramCard { max-width: 260px; padding: 10px 14px; - gap: 10px; min-height: 56px; border-radius: 10px; - - &:hover { - transform: none; // 移动端禁用悬浮动画 - background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%); - border-color: #e1e8ed; - box-shadow: - 0 2px 8px rgba(0, 0, 0, 0.04), - 0 1px 3px rgba(0, 0, 0, 0.06); - - &::before { - opacity: 0; - } - } } .miniProgramThumb { diff --git a/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/components/MessageRecord/index.tsx b/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/components/MessageRecord/index.tsx index ba4e40c6..7a9c5046 100644 --- a/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/components/MessageRecord/index.tsx +++ b/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/components/MessageRecord/index.tsx @@ -9,7 +9,6 @@ import { useWeChatStore } from "@/store/module/weChat/weChat"; interface MessageRecordProps { contract: ContractData | weChatGroup; } - const MessageRecord: React.FC = ({ contract }) => { const messagesEndRef = useRef(null); const currentMessages = useWeChatStore(state => state.currentMessages); @@ -187,29 +186,80 @@ const MessageRecord: React.FC = ({ contract }) => { } return renderErrorMessage("[表情包]"); - case 49: // 小程序/其他:图文、文件 + case 49: // 小程序/文章/其他:图文、文件 if (typeof content !== "string" || !content.trim()) { - return renderErrorMessage("[小程序/文件消息 - 无效内容]"); + return renderErrorMessage("[小程序/文章/文件消息 - 无效内容]"); } try { const trimmedContent = content.trim(); - // 尝试解析JSON格式的小程序消息 + // 尝试解析JSON格式的消息 if (trimmedContent.startsWith("{") && trimmedContent.endsWith("}")) { - const miniProgramData = JSON.parse(trimmedContent); + 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"; + }} + /> +
+ )} +
+
+
文章
+
+ ); + } // 处理包含contentXml的小程序消息格式 - if ( - miniProgramData.contentXml && - miniProgramData.type === "miniprogram" - ) { - const xmlContent = miniProgramData.contentXml; + if (messageData.contentXml && messageData.type === "miniprogram") { + const xmlContent = messageData.contentXml; // 从XML中提取title const titleMatch = xmlContent.match(/([^<]*)<\/title>/); const title = titleMatch ? titleMatch[1] : "小程序消息"; + // 从XML中提取type字段 + const typeMatch = xmlContent.match( + /<weappinfo>[\s\S]*?<type>(\d+)<\/type>[\s\S]*?<\/weappinfo>/, + ); + const miniProgramType = typeMatch ? parseInt(typeMatch[1]) : 1; + // 从XML中提取thumburl或使用previewImage const thumbUrlMatch = xmlContent.match( /<thumburl>\s*([^<]*?)\s*<\/thumburl>/, @@ -218,7 +268,7 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => { // 如果thumburl为空或无效,使用previewImage if (!thumbUrl || thumbUrl === "`" || thumbUrl.includes("`")) { - thumbUrl = miniProgramData.previewImage || ""; + thumbUrl = messageData.previewImage || ""; } // 清理URL中的特殊字符 @@ -232,41 +282,83 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => { ); 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}> + // 根据type类型渲染不同布局 + if (miniProgramType === 2) { + // 类型2:图片区域布局(小程序昵称、图片、标题、小程序标识) + return ( + <div + className={`${styles.miniProgramMessage} ${styles.miniProgramType2}`} + > + <div + className={`${styles.miniProgramCard} ${styles.miniProgramCardType2}`} + > + {/* 小程序昵称 */} + <div className={styles.miniProgramAppTop}>{appName}</div> + {/* 标题 */} <div className={styles.miniProgramTitle}>{title}</div> + {/* 图片 */} + {thumbUrl && ( + <div className={styles.miniProgramImageArea}> + <img + src={thumbUrl} + alt="小程序图片" + className={styles.miniProgramImage} + onError={e => { + const target = e.target as HTMLImageElement; + target.style.display = "none"; + }} + /> + </div> + )} + <div className={styles.miniProgramContent}> + {/* 小程序标识 */} + <div className={styles.miniProgramIdentifier}> + 小程序 + </div> + </div> </div> </div> - <div className={styles.miniProgramApp}>{appName}</div> - </div> - ); + ); + } else { + // 默认类型:横向布局 + return ( + <div + className={`${styles.miniProgramMessage} ${styles.miniProgramType1}`} + > + <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) + messageData && + typeof messageData === "object" && + (messageData.title || messageData.appName) ) { return ( <div className={styles.miniProgramMessage}> <div className={styles.miniProgramCard}> - {miniProgramData.thumb && ( + {messageData.thumb && ( <img - src={miniProgramData.thumb} + src={messageData.thumb} alt="小程序缩略图" className={styles.miniProgramThumb} onError={e => { @@ -277,11 +369,11 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => { )} <div className={styles.miniProgramInfo}> <div className={styles.miniProgramTitle}> - {miniProgramData.title || "小程序消息"} + {messageData.title || "小程序消息"} </div> - {miniProgramData.appName && ( + {messageData.appName && ( <div className={styles.miniProgramApp}> - {miniProgramData.appName} + {messageData.appName} </div> )} </div> @@ -404,17 +496,54 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => { ); } - // 2. 检查是否为视频消息(兼容旧逻辑) + // 2. 检查是否为JSON格式消息(包括视频、链接等) if (contentStr.startsWith("{") && contentStr.endsWith("}")) { try { - const videoData = JSON.parse(contentStr); + const jsonData = JSON.parse(contentStr); + + // 检查是否为链接类型消息 + if (jsonData.type === "link" && jsonData.title && jsonData.url) { + const { title, desc, thumbPath, url } = jsonData; + + return ( + <div + className={`${styles.miniProgramMessage} ${styles.miniProgramType1}`} + > + <div + className={`${styles.miniProgramCard} ${styles.linkCard}`} + onClick={() => window.open(url, "_blank")} + > + {thumbPath && ( + <img + src={thumbPath} + 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> + {desc && ( + <div className={styles.linkDescription}>{desc}</div> + )} + </div> + </div> + <div className={styles.miniProgramApp}>链接</div> + </div> + ); + } + + // 检查是否为视频消息(兼容旧逻辑) if ( - videoData && - typeof videoData === "object" && - videoData.previewImage && - (videoData.tencentUrl || videoData.videoUrl) + jsonData && + typeof jsonData === "object" && + jsonData.previewImage && + (jsonData.tencentUrl || jsonData.videoUrl) ) { - const previewImageUrl = String(videoData.previewImage).replace( + const previewImageUrl = String(jsonData.previewImage).replace( /[`"']/g, "", ); @@ -427,7 +556,7 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => { className={styles.videoPreview} onClick={() => { const videoUrl = - videoData.videoUrl || videoData.tencentUrl; + jsonData.videoUrl || jsonData.tencentUrl; if (videoUrl) { window.open(videoUrl, "_blank"); }