From 57ceae97a1a95b5985139dcae618712283a0097d 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 11:08:00 +0800 Subject: [PATCH 01/10] =?UTF-8?q?feat(MessageRecord):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E7=B1=BB=E5=9E=8B=E6=A0=B7=E5=BC=8F=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=B6=88=E6=81=AF=E8=A7=A3=E6=9E=90=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增图片、小程序和文件消息的样式组件 - 根据msgType字段重构消息解析逻辑,支持多种消息类型 - 添加错误处理和兼容旧版本消息格式 --- .../MessageRecord/MessageRecord.module.scss | 131 ++++++-- .../components/MessageRecord/index.tsx | 298 ++++++++++++++---- 2 files changed, 346 insertions(+), 83 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 f35eb61c..3cc95355 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 @@ -166,43 +166,126 @@ } } -// 文件消息样式(如果需要) -.fileMessage { - display: flex; - align-items: center; - gap: 8px; - padding: 8px; - border: 1px solid #d9d9d9; - border-radius: 6px; - background: #fafafa; - cursor: pointer; - transition: background 0.2s; +// 图片消息 +.imageMessage { + img { + border-radius: 8px; + cursor: pointer; + transition: transform 0.2s; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - &:hover { - background: #f0f0f0; + &:hover { + transform: scale(1.02); + } + } +} + +// 小程序消息 +.miniProgramMessage { + .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; + + &:hover { + border-color: #1890ff; + box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1); + } + } + + .miniProgramThumb { + width: 40px; + height: 40px; + border-radius: 6px; + object-fit: cover; + flex-shrink: 0; + } + + .miniProgramInfo { + flex: 1; + min-width: 0; + } + + .miniProgramTitle { + font-weight: 500; + color: #262626; + font-size: 14px; + line-height: 1.4; + margin-bottom: 4px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .miniProgramApp { + font-size: 12px; + color: #8c8c8c; + display: flex; + align-items: center; + gap: 4px; + + &::before { + content: "📱"; + font-size: 10px; + } + } +} + +// 文件消息样式 +.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; - } + .fileName { + font-weight: 500; + color: #262626; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 4px; + } - .fileSize { - font-size: 12px; - color: #8c8c8c; - margin-top: 2px; + .fileAction { + font-size: 12px; + color: #1890ff; + cursor: pointer; + + &:hover { + text-decoration: underline; } } } 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 662aa53c..f57195e2 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 @@ -30,74 +30,253 @@ const MessageRecord: React.FC = ({ contract }) => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; - // 解析消息内容,判断消息类型并返回对应的渲染内容 - const parseMessageContent = (content: string | null | undefined) => { + // 解析消息内容,根据msgType判断消息类型并返回对应的渲染内容 + const parseMessageContent = ( + content: string | null | undefined, + msgType?: number, + ) => { // 处理null或undefined的内容 if (content === null || content === undefined) { return
消息内容不可用
; } - // 检查是否为表情包 - if ( - typeof content === "string" && - content.includes("ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com") && - content.includes("#") - ) { - return ( -
- 表情包 window.open(content, "_blank")} - /> -
- ); - } - // 检查是否为带预览图的视频消息 - try { - if ( - typeof content === "string" && - content.trim().startsWith("{") && - content.trim().endsWith("}") - ) { - const videoData = JSON.parse(content); - // 处理视频消息格式 {"previewImage":"https://...", "tencentUrl":"...", "videoUrl":"...", "isLoading":true} - if (videoData.previewImage && videoData.tencentUrl) { - // 提取预览图URL,去掉可能的引号 - const previewImageUrl = videoData.previewImage.replace(/[`"']/g, ""); + // 根据msgType进行消息类型判断 + switch (msgType) { + case 1: // 文本消息 + return
{content}
; - return ( -
-
- 视频预览 { - if (videoData.videoUrl) { - window.open(videoData.videoUrl, "_blank"); - } else if (videoData.tencentUrl) { - window.open(videoData.tencentUrl, "_blank"); - } - }} - /> -
- - - + case 3: // 图片消息 + return ( +
+ 图片消息 window.open(content, "_blank")} + onError={e => { + const target = e.target as HTMLImageElement; + target.style.display = "none"; + target.parentElement!.innerHTML = `
图片加载失败
`; + }} + /> +
+ ); + + case 43: // 视频消息 + try { + if ( + typeof content === "string" && + content.trim().startsWith("{") && + content.trim().endsWith("}") + ) { + const videoData = JSON.parse(content); + if ( + videoData.previewImage && + (videoData.tencentUrl || videoData.videoUrl) + ) { + const previewImageUrl = videoData.previewImage.replace( + /[`"']/g, + "", + ); + return ( +
+
+ 视频预览 { + if (videoData.videoUrl) { + window.open(videoData.videoUrl, "_blank"); + } else if (videoData.tencentUrl) { + window.open(videoData.tencentUrl, "_blank"); + } + }} + /> +
+ + + +
+
-
+ ); + } + } + // 如果JSON解析失败或格式不正确,显示视频消息提示 + return
[视频消息]
; + } catch (e) { + return
[视频消息]
; + } + + 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")) + ) { + return ( +
+ 表情包 window.open(content, "_blank")} + onError={e => { + const target = e.target as HTMLImageElement; + target.style.display = "none"; + target.parentElement!.innerHTML = `
[表情包]
`; + }} + />
); } - } - } catch (e) { - // JSON解析失败,按普通文本处理 - } + return
[表情包]
; - // 普通文本消息 - return
{content}
; + case 49: // 小程序/其他:图文、文件 + try { + // 尝试解析JSON格式的小程序消息 + if ( + typeof content === "string" && + content.trim().startsWith("{") && + content.trim().endsWith("}") + ) { + const miniProgramData = JSON.parse(content); + if (miniProgramData.title || miniProgramData.appName) { + return ( +
+
+ {miniProgramData.thumb && ( + 小程序缩略图 + )} +
+
+ {miniProgramData.title || "小程序消息"} +
+ {miniProgramData.appName && ( +
+ {miniProgramData.appName} +
+ )} +
+
+
+ ); + } + } + // 文件消息处理 + if ( + typeof content === "string" && + (content.includes("http") || content.includes("file")) + ) { + return ( +
+
+
📄
+
+
文件消息
+
window.open(content, "_blank")} + > + 点击查看 +
+
+
+
+ ); + } + return
[小程序/文件消息]
; + } catch (e) { + return
[小程序/文件消息]
; + } + + default: + // 兼容旧版本:如果没有msgType或msgType不在已知范围内,使用原有逻辑 + // 检查是否为表情包(兼容旧逻辑) + if ( + typeof content === "string" && + content.includes("ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com") && + content.includes("#") + ) { + return ( +
+ 表情包 window.open(content, "_blank")} + /> +
+ ); + } + + // 检查是否为带预览图的视频消息(兼容旧逻辑) + 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( + /[`"']/g, + "", + ); + return ( +
+
+ 视频预览 { + if (videoData.videoUrl) { + window.open(videoData.videoUrl, "_blank"); + } else if (videoData.tencentUrl) { + window.open(videoData.tencentUrl, "_blank"); + } + }} + /> +
+ + + +
+
+
+ ); + } + } + } catch (e) { + // JSON解析失败,按普通文本处理 + } + + // 普通文本消息 + return
{content}
; + } }; // 获取群成员头像 @@ -158,7 +337,7 @@ const MessageRecord: React.FC = ({ contract }) => { {contract.nickname}
)} - {parseMessageContent(msg?.content)} + {parseMessageContent(msg?.content, msg?.msgType)}
)} @@ -180,6 +359,7 @@ const MessageRecord: React.FC = ({ contract }) => { )} {parseMessageContent( clearWechatidInContent(msg?.sender, msg?.content), + msg?.msgType, )} @@ -187,7 +367,7 @@ const MessageRecord: React.FC = ({ contract }) => { {isOwn && (
- {parseMessageContent(msg?.content)} + {parseMessageContent(msg?.content, msg?.msgType)}
)} From bd6f1064fb8a81e0cf59399acda7ae850ea34ea0 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 12:13:38 +0800 Subject: [PATCH 02/10] =?UTF-8?q?feat(MessageRecord):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E6=B8=B2=E6=9F=93=E9=80=BB=E8=BE=91=E5=B9=B6?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构消息内容解析逻辑,增加对多种消息类型的支持,包括文本、图片、视频、表情包、小程序和文件消息。改进错误处理和兼容性,同时优化UI样式和移动端适配。 - 新增消息类型检测工具函数 - 增强JSON解析和错误处理 - 优化小程序消息的XML解析和显示 - 改进文件消息的图标和文件名显示 - 添加移动端样式适配 - 统一错误消息渲染逻辑 --- .../MessageRecord/MessageRecord.module.scss | 82 +++- .../components/MessageRecord/index.tsx | 440 ++++++++++++++---- 2 files changed, 402 insertions(+), 120 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 3cc95355..07fb13f5 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 @@ -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; + } + } + } } 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 f57195e2..ba4e40c6 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 @@ -20,6 +20,17 @@ const MessageRecord: React.FC = ({ 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 = ({ contract }) => { return
消息内容不可用
; } + // 统一的错误消息渲染函数 + const renderErrorMessage = (fallbackText: string) => ( +
{fallbackText}
+ ); + // 根据msgType进行消息类型判断 switch (msgType) { case 1: // 文本消息 - return
{content}
; + return ( +
+
{content}
+
+ ); case 3: // 图片消息 + // 验证是否为有效的图片URL + if (typeof content !== "string" || !content.trim()) { + return renderErrorMessage("[图片消息 - 无效链接]"); + } return ( -
- 图片消息 window.open(content, "_blank")} - onError={e => { - const target = e.target as HTMLImageElement; - target.style.display = "none"; - target.parentElement!.innerHTML = `
图片加载失败
`; - }} - /> +
+
+ 图片消息 window.open(content, "_blank")} + onError={e => { + const target = e.target as HTMLImageElement; + const parent = target.parentElement; + if (parent) { + parent.innerHTML = `
[图片加载失败]
`; + } + }} + /> +
); 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 (
@@ -90,10 +125,17 @@ const MessageRecord: React.FC = ({ 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 = `
[视频预览加载失败]
`; } }} /> @@ -112,21 +154,19 @@ const MessageRecord: React.FC = ({ contract }) => { ); } } - // 如果JSON解析失败或格式不正确,显示视频消息提示 - return
[视频消息]
; + return renderErrorMessage("[视频消息]"); } catch (e) { - return
[视频消息]
; + 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 (
= ({ contract }) => { onClick={() => window.open(content, "_blank")} onError={e => { const target = e.target as HTMLImageElement; - target.style.display = "none"; - target.parentElement!.innerHTML = `
[表情包]
`; + const parent = target.parentElement; + if (parent) { + parent.innerHTML = `
[表情包加载失败]
`; + } }} />
); } - return
[表情包]
; + 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>/); + 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> ); From 79c1a539bb53317486ce9cb94e097c51cd57f68e 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?= <fsmecx@gmail.com> Date: Mon, 8 Sep 2025 14:21:52 +0800 Subject: [PATCH 03/10] =?UTF-8?q?style(MessageRecord):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=B0=8F=E7=A8=8B=E5=BA=8F=E5=8D=A1=E7=89=87=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F=E5=92=8C=E4=BA=A4=E4=BA=92=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 调整卡片尺寸、间距和背景渐变效果 - 添加悬停动画和阴影效果 - 优化移动端适配样式 - 移除冗余的SVG图标代码 --- .../MessageRecord/MessageRecord.module.scss | 79 +++++++++++-------- 1 file changed, 48 insertions(+), 31 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 07fb13f5..d74bea3a 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 @@ -187,19 +187,27 @@ .miniProgramCard { display: flex; align-items: center; - gap: 10px; - padding: 10px 14px; - + gap: 12px; + padding: 12px; + border-bottom: 1px solid #e1e8ed; + background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%); cursor: pointer; - transition: all 0.2s ease; - min-width: 260px; - min-height: 56px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + width: 280px; + min-height: 64px; + overflow: hidden; } .miniProgramThumb { - width: 40px; - height: 40px; - background: #f0f0f0; + 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 { @@ -208,39 +216,31 @@ display: flex; flex-direction: column; justify-content: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .miniProgramTitle { - font-weight: 500; - color: #262626; + font-weight: 600; + color: #1a1a1a; font-size: 14px; line-height: 1.4; - margin-bottom: 3px; + 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: #666666; - display: flex; - align-items: center; - gap: 4px; + color: #8c8c8c; line-height: 1.2; - font-weight: 400; - - &::before { - content: ""; - width: 14px; - height: 14px; - background: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTcgMEM0LjI0IDAgMiAyLjI0IDIgNVY5QzIgMTEuNzYgNC4yNCAxNCA3IDE0UzEyIDExLjc2IDEyIDlWNUMxMiAyLjI0IDkuNzYgMCA3IDBaTTEwIDlDMTAgMTAuNjUgOC42NSAxMiA3IDEyUzQgMTAuNjUgNCA5VjVDNCAzLjM1IDUuMzUgMiA3IDJTMTAgMy4zNSAxMCA1VjlaIiBmaWxsPSIjNjY2NjY2Ii8+Cjwvc3ZnPgo=") - no-repeat center; - background-size: contain; - flex-shrink: 0; - } + font-weight: 500; + padding: 6px 12px; } } @@ -312,29 +312,46 @@ // 小程序消息移动端适配 .miniProgramMessage { - min-width: 400px; .miniProgramCard { max-width: 260px; - padding: 8px 12px; - gap: 8px; - min-height: 52px; + 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 { 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; 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?= <fsmecx@gmail.com> Date: Mon, 8 Sep 2025 15:19:29 +0800 Subject: [PATCH 04/10] =?UTF-8?q?feat(=E6=B6=88=E6=81=AF=E8=AE=B0=E5=BD=95?= =?UTF-8?q?):=20=E5=A2=9E=E5=8A=A0=E5=B0=8F=E7=A8=8B=E5=BA=8F=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B2=E5=92=8C=E6=96=87=E7=AB=A0=E9=93=BE=E6=8E=A5?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E6=A0=B7=E5=BC=8F=E5=8F=8A=E5=A4=84=E7=90=86?= =?UTF-8?q?=E9=80=BB=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<MessageRecordProps> = ({ contract }) => { const messagesEndRef = useRef<HTMLDivElement>(null); const currentMessages = useWeChatStore(state => state.currentMessages); @@ -187,29 +186,80 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ 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 ( + <div + className={`${styles.miniProgramMessage} ${styles.articleMessage}`} + > + <div + className={`${styles.miniProgramCard} ${styles.articleCard}`} + onClick={() => window.open(url, "_blank")} + > + {/* 标题在第一行 */} + <div className={styles.articleTitle}>{title}</div> + + {/* 下方:文字在左,图片在右 */} + <div className={styles.articleContent}> + <div className={styles.articleTextArea}> + {desc && ( + <div className={styles.articleDescription}> + {desc} + </div> + )} + </div> + {thumbPath && ( + <div className={styles.articleImageArea}> + <img + src={thumbPath} + alt="文章缩略图" + className={styles.articleImage} + onError={e => { + const target = e.target as HTMLImageElement; + target.style.display = "none"; + }} + /> + </div> + )} + </div> + </div> + <div className={styles.miniProgramApp}>文章</div> + </div> + ); + } // 处理包含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>([^<]*)<\/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"); } From f13d623d14a9b906bb55c2e707d83a5216c164a4 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?= <fsmecx@gmail.com> Date: Mon, 8 Sep 2025 15:22:09 +0800 Subject: [PATCH 05/10] =?UTF-8?q?style(MessageRecord):=20=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E6=B6=88=E6=81=AF=E8=AE=B0=E5=BD=95=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E5=9B=BE=E7=89=87=E8=BE=B9=E6=A1=86?= =?UTF-8?q?=E5=92=8C=E6=82=AC=E5=81=9C=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除卡片悬停动画效果,调整图片尺寸并添加边框 --- .../MessageRecord/MessageRecord.module.scss | 15 +++------------ 1 file changed, 3 insertions(+), 12 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 3a85b50a..b1f0a5ba 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 @@ -293,7 +293,7 @@ height: 100%; object-fit: cover; transition: transform 0.3s ease; - + border: 0.5px solid #e1e8ed; &:hover { transform: scale(1.05); } @@ -367,11 +367,6 @@ padding: 16px; min-height: auto; max-width: 320px; - - &:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - } } .articleTitle { @@ -411,8 +406,8 @@ .articleImageArea { flex-shrink: 0; - width: 80px; - height: 80px; + width: 60px; + height: 60px; overflow: hidden; border-radius: 8px; background: #f8f9fa; @@ -423,10 +418,6 @@ height: 100%; object-fit: cover; transition: transform 0.3s ease; - - &:hover { - transform: scale(1.05); - } } } From 97cb2b072b5e986bc469d02887bd28e386bf5a1c 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?= <fsmecx@gmail.com> Date: Mon, 8 Sep 2025 15:27:57 +0800 Subject: [PATCH 06/10] =?UTF-8?q?style(MessageRecord):=20=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E6=B6=88=E6=81=AF=E6=B0=94=E6=B3=A1=E7=9A=84=E5=9C=86?= =?UTF-8?q?=E8=A7=92=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将消息气泡的圆角从不对称样式(4px 18px 18px 18px)改为统一的10px圆角,提升视觉一致性 --- .../components/MessageRecord/MessageRecord.module.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 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 b1f0a5ba..1b922238 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 @@ -55,7 +55,7 @@ .messageBubble { background: #1890ff; color: white; - border-radius: 4px 18px 18px 18px; + border-radius: 10px; } } @@ -65,7 +65,7 @@ .messageBubble { background: white; color: #262626; - border-radius: 4px 18px 18px 18px; + border-radius: 10px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } } From dba6ae164cad54a2821ee8fbd31c08005f5153e8 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?= <fsmecx@gmail.com> Date: Mon, 8 Sep 2025 15:53:37 +0800 Subject: [PATCH 07/10] =?UTF-8?q?feat(MessageRecord):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E6=B6=88=E6=81=AF=E9=A2=84=E8=A7=88=E5=92=8C?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增视频消息的预览图显示、加载动画和下载按钮功能。优化视频消息的交互体验,包括点击预览图触发下载请求,显示加载状态,以及视频播放时的下载选项。同时添加了相关样式和动画效果。 --- .../MessageRecord/MessageRecord.module.scss | 59 ++ .../components/MessageRecord/index.tsx | 224 +++++- .../pc/ckbox/components/ChatWindow/demo.tsx | 669 ++++++++++++++++++ 3 files changed, 913 insertions(+), 39 deletions(-) create mode 100644 Cunkebao/src/pages/pc/ckbox/components/ChatWindow/demo.tsx 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 1b922238..6a4ede88 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 @@ -146,6 +146,56 @@ display: block; } + .videoThumbnail { + max-width: 200px; + max-height: 200px; + display: block; + cursor: pointer; + transition: opacity 0.2s; + } + + .videoPlayIcon { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; + } + + .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; + background: rgba(0, 0, 0, 0.6); + color: white; + border: none; + border-radius: 4px; + padding: 6px; + cursor: pointer; + transition: background 0.2s; + text-decoration: none; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + background: rgba(0, 0, 0, 0.8); + color: white; + } + } + .playButton { position: absolute; top: 50%; @@ -166,6 +216,15 @@ } } +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + // 图片消息 .imageMessage { img { 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 7a9c5046..4d12191e 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 @@ -1,10 +1,16 @@ import React, { useEffect, useRef } from "react"; import { Avatar, Divider } from "antd"; -import { UserOutlined, LoadingOutlined } from "@ant-design/icons"; +import { + UserOutlined, + LoadingOutlined, + DownloadOutlined, + PlayCircleFilled, +} from "@ant-design/icons"; import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; import { formatWechatTime } from "@/utils/common"; 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; @@ -18,6 +24,19 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => { const currentGroupMembers = useWeChatStore( state => state.currentGroupMembers, ); + 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 => { @@ -31,18 +50,78 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => { }; useEffect(() => { - if (isLoadingData) { + const prevMessages = prevMessagesRef.current; + + const hasVideoStateChange = currentMessages.some((msg, index) => { + // 首先检查消息对象本身是否为null或undefined + if (!msg || !msg.content) return false; + + const prevMsg = prevMessages[index]; + if (!prevMsg || !prevMsg.content || prevMsg.id !== msg.id) return false; + + try { + const currentContent = + typeof msg.content === "string" + ? JSON.parse(msg.content) + : msg.content; + const prevContent = + typeof prevMsg.content === "string" + ? JSON.parse(prevMsg.content) + : prevMsg.content; + + // 检查视频状态是否发生变化(开始加载、完成加载、获得URL) + const currentHasVideo = + currentContent.previewImage && currentContent.tencentUrl; + const prevHasVideo = prevContent.previewImage && prevContent.tencentUrl; + + if (currentHasVideo && prevHasVideo) { + // 检查加载状态变化或视频URL变化 + return ( + currentContent.isLoading !== prevContent.isLoading || + currentContent.videoUrl !== prevContent.videoUrl + ); + } + + return false; + } catch (e) { + return false; + } + }); + + // 只有在没有视频状态变化时才自动滚动到底部 + if (!hasVideoStateChange && isLoadingData) { scrollToBottom(); } + + // 更新上一次的消息状态 + prevMessagesRef.current = currentMessages; }, [currentMessages, isLoadingData]); const scrollToBottom = () => { 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, + msg: ChatRecord, msgType?: number, ) => { // 处理null或undefined的内容 @@ -98,55 +177,121 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => { return renderErrorMessage("[视频消息 - 无效内容]"); } + // 如果content是直接的视频链接(已预览过或下载好的视频) + if (isDirectVideoLink(content)) { + return ( + <div className={styles.messageBubble}> + <div className={styles.videoMessage}> + <div className={styles.videoContainer}> + <video + controls + src={content} + style={{ maxWidth: "100%", borderRadius: "8px" }} + /> + <a + href={content} + download + className={styles.downloadButton} + style={{ display: "flex" }} + onClick={e => e.stopPropagation()} + > + <DownloadOutlined style={{ fontSize: "18px" }} /> + </a> + </div> + </div> + </div> + ); + } + try { - // 更严格的JSON格式验证 - const trimmedContent = content.trim(); - if (trimmedContent.startsWith("{") && trimmedContent.endsWith("}")) { - const videoData = JSON.parse(trimmedContent); + // 尝试解析JSON格式的视频数据 + if (content.startsWith("{") && content.endsWith("}")) { + const videoData = JSON.parse(content); // 验证必要的视频数据字段 if ( videoData && typeof videoData === "object" && videoData.previewImage && - (videoData.tencentUrl || videoData.videoUrl) + 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 ( + <div className={styles.messageBubble}> + <div className={styles.videoMessage}> + <div className={styles.videoContainer}> + <video + controls + src={videoData.videoUrl} + style={{ maxWidth: "100%", borderRadius: "8px" }} + /> + <a + href={videoData.videoUrl} + download + className={styles.downloadButton} + style={{ display: "flex" }} + onClick={e => e.stopPropagation()} + > + <DownloadOutlined style={{ fontSize: "18px" }} /> + </a> + </div> + </div> + </div> + ); + } + + // 显示预览图,根据加载状态显示不同的图标 return ( - <div className={styles.videoMessage}> - <div className={styles.videoContainer}> - <img - src={previewImageUrl} - alt="视频预览" - className={styles.videoPreview} - onClick={() => { - 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>`; - } - }} - /> - <div className={styles.playButton}> - <svg - width="24" - height="24" - viewBox="0 0 24 24" - fill="white" - > - <path d="M8 5v14l11-7z" /> - </svg> + <div className={styles.messageBubble}> + <div className={styles.videoMessage}> + <div + className={styles.videoContainer} + onClick={e => handlePlayClick(e, msg)} + > + <img + src={previewImageUrl} + alt="视频预览" + className={styles.videoThumbnail} + style={{ + maxWidth: "100%", + borderRadius: "8px", + opacity: videoData.isLoading ? "0.7" : "1", + }} + onError={e => { + const target = e.target as HTMLImageElement; + const parent = target.parentElement?.parentElement; + if (parent) { + parent.innerHTML = `<div class="${styles.messageText}">[视频预览加载失败]</div>`; + } + }} + /> + <div className={styles.videoPlayIcon}> + {videoData.isLoading ? ( + <div className={styles.loadingSpinner}></div> + ) : ( + <PlayCircleFilled + style={{ fontSize: "48px", color: "#fff" }} + /> + )} + </div> </div> </div> </div> @@ -712,7 +857,7 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => { {contract.nickname} </div> )} - <>{parseMessageContent(msg?.content, msg?.msgType)}</> + <>{parseMessageContent(msg?.content, msg, msg?.msgType)}</> </div> </> )} @@ -735,6 +880,7 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => { <> {parseMessageContent( clearWechatidInContent(msg?.sender, msg?.content), + msg, msg?.msgType, )} </> @@ -742,7 +888,7 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => { </> )} - {isOwn && <>{parseMessageContent(msg?.content, msg?.msgType)}</>} + {isOwn && <>{parseMessageContent(msg?.content, msg, msg?.msgType)}</>} </div> </div> ); diff --git a/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/demo.tsx b/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/demo.tsx new file mode 100644 index 00000000..e1bfb7ca --- /dev/null +++ b/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/demo.tsx @@ -0,0 +1,669 @@ +import React, { useEffect, useRef } from "react"; +import { Layout, Button, Avatar, Space, Dropdown, Menu, Tooltip } from "antd"; +import { + PhoneOutlined, + VideoCameraOutlined, + MoreOutlined, + UserOutlined, + DownloadOutlined, + FileOutlined, + FilePdfOutlined, + FileWordOutlined, + FileExcelOutlined, + FilePptOutlined, + PlayCircleFilled, + TeamOutlined, + FolderOutlined, + EnvironmentOutlined, +} from "@ant-design/icons"; +import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; +import styles from "./ChatWindow.module.scss"; +import { useWebSocketStore } from "@/store/module/websocket/websocket"; +import { formatWechatTime } from "@/utils/common"; +import ProfileCard from "./components/ProfileCard"; +import MessageEnter from "./components/MessageEnter"; +import { useWeChatStore } from "@/store/module/weChat/weChat"; +const { Header, Content } = Layout; + +interface ChatWindowProps { + contract: ContractData | weChatGroup; + showProfile?: boolean; + onToggleProfile?: () => void; +} + +const ChatWindow: React.FC<ChatWindowProps> = ({ + contract, + showProfile = true, + onToggleProfile, +}) => { + const messagesEndRef = useRef<HTMLDivElement>(null); + const currentMessages = useWeChatStore(state => state.currentMessages); + const prevMessagesRef = useRef(currentMessages); + + useEffect(() => { + const prevMessages = prevMessagesRef.current; + + const hasVideoStateChange = currentMessages.some((msg, index) => { + // 首先检查消息对象本身是否为null或undefined + if (!msg || !msg.content) return false; + + const prevMsg = prevMessages[index]; + if (!prevMsg || !prevMsg.content || prevMsg.id !== msg.id) return false; + + try { + const currentContent = + typeof msg.content === "string" + ? JSON.parse(msg.content) + : msg.content; + const prevContent = + typeof prevMsg.content === "string" + ? JSON.parse(prevMsg.content) + : prevMsg.content; + + // 检查视频状态是否发生变化(开始加载、完成加载、获得URL) + const currentHasVideo = + currentContent.previewImage && currentContent.tencentUrl; + const prevHasVideo = prevContent.previewImage && prevContent.tencentUrl; + + if (currentHasVideo && prevHasVideo) { + // 检查加载状态变化或视频URL变化 + return ( + currentContent.isLoading !== prevContent.isLoading || + currentContent.videoUrl !== prevContent.videoUrl + ); + } + + return false; + } catch (e) { + return false; + } + }); + + // 只有在没有视频状态变化时才自动滚动到底部 + if (!hasVideoStateChange) { + scrollToBottom(); + } + + // 更新上一次的消息状态 + prevMessagesRef.current = currentMessages; + }, [currentMessages]); + + const scrollToBottom = () => { + 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, + }); + }; + + // 解析消息内容,判断消息类型并返回对应的渲染内容 + const parseMessageContent = ( + content: string | null | undefined, + msg: ChatRecord, + ) => { + // 处理null或undefined的内容 + if (content === null || content === undefined) { + return <div className={styles.messageText}>消息内容不可用</div>; + } + // 检查是否为表情包 + if ( + typeof content === "string" && + content.includes("ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com") && + content.includes("#") + ) { + return ( + <div className={styles.emojiMessage}> + <img + src={content} + alt="表情包" + style={{ maxWidth: "120px", maxHeight: "120px" }} + onClick={() => window.open(content, "_blank")} + /> + </div> + ); + } + + // 检查是否为带预览图的视频消息 + try { + if ( + typeof content === "string" && + content.trim().startsWith("{") && + content.trim().endsWith("}") + ) { + const videoData = JSON.parse(content); + // 处理视频消息格式 {"previewImage":"https://...", "tencentUrl":"...", "videoUrl":"...", "isLoading":true} + if (videoData.previewImage && videoData.tencentUrl) { + // 提取预览图URL,去掉可能的引号 + const previewImageUrl = videoData.previewImage.replace(/[`"']/g, ""); + + // 创建点击处理函数 + const handlePlayClick = (e: React.MouseEvent) => { + e.stopPropagation(); + // 如果没有视频URL且不在加载中,则发起下载请求 + if (!videoData.videoUrl && !videoData.isLoading) { + handleVideoPlayRequest(videoData.tencentUrl, msg.id); + } + }; + + // 如果已有视频URL,显示视频播放器 + if (videoData.videoUrl) { + return ( + <div className={styles.videoMessage}> + <div className={styles.videoContainer}> + <video + controls + src={videoData.videoUrl} + style={{ maxWidth: "100%", borderRadius: "8px" }} + /> + <a + href={videoData.videoUrl} + download + className={styles.downloadButton} + style={{ display: "flex" }} + onClick={e => e.stopPropagation()} + > + <DownloadOutlined style={{ fontSize: "18px" }} /> + </a> + </div> + </div> + ); + } + + // 显示预览图,根据加载状态显示不同的图标 + return ( + <div className={styles.videoMessage}> + <div className={styles.videoContainer} onClick={handlePlayClick}> + <img + src={previewImageUrl} + alt="视频预览" + className={styles.videoThumbnail} + style={{ + maxWidth: "100%", + borderRadius: "8px", + opacity: videoData.isLoading ? "0.7" : "1", + }} + /> + <div className={styles.videoPlayIcon}> + {videoData.isLoading ? ( + <div className={styles.loadingSpinner}></div> + ) : ( + <PlayCircleFilled + style={{ fontSize: "48px", color: "#fff" }} + /> + )} + </div> + </div> + </div> + ); + } + // 保留原有的视频处理逻辑 + else if ( + videoData.type === "video" && + videoData.url && + videoData.thumb + ) { + return ( + <div className={styles.videoMessage}> + <div + className={styles.videoContainer} + onClick={() => window.open(videoData.url, "_blank")} + > + <img + src={videoData.thumb} + alt="视频预览" + className={styles.videoThumbnail} + /> + <div className={styles.videoPlayIcon}> + <VideoCameraOutlined + style={{ fontSize: "32px", color: "#fff" }} + /> + </div> + </div> + <a + href={videoData.url} + download + className={styles.downloadButton} + style={{ display: "flex" }} + onClick={e => e.stopPropagation()} + > + <DownloadOutlined style={{ fontSize: "18px" }} /> + </a> + </div> + ); + } + } + } catch (e) { + // 解析JSON失败,不是视频消息 + console.log("解析视频消息失败:", e); + } + + // 检查是否为图片链接 + if ( + typeof content === "string" && + (content.match(/\.(jpg|jpeg|png|gif)$/i) || + (content.includes("oss-cn-shenzhen.aliyuncs.com") && + content.includes(".jpg"))) + ) { + return ( + <div className={styles.imageMessage}> + <img + src={content} + alt="图片消息" + onClick={() => window.open(content, "_blank")} + /> + </div> + ); + } + + // 检查是否为视频链接 + if ( + typeof content === "string" && + (content.match(/\.(mp4|avi|mov|wmv|flv)$/i) || + (content.includes("oss-cn-shenzhen.aliyuncs.com") && + content.includes(".mp4"))) + ) { + return ( + <div className={styles.videoMessage}> + <video + controls + src={content} + style={{ maxWidth: "100%", borderRadius: "8px" }} + /> + <a + href={content} + download + className={styles.downloadButton} + style={{ display: "flex" }} + onClick={e => e.stopPropagation()} + > + <DownloadOutlined style={{ fontSize: "18px" }} /> + </a> + </div> + ); + } + + // 检查是否为音频链接 + if ( + typeof content === "string" && + (content.match(/\.(mp3|wav|ogg|m4a)$/i) || + (content.includes("oss-cn-shenzhen.aliyuncs.com") && + content.includes(".mp3"))) + ) { + return ( + <div className={styles.audioMessage}> + <audio controls src={content} style={{ maxWidth: "100%" }} /> + <a + href={content} + download + className={styles.downloadButton} + style={{ display: "flex" }} + onClick={e => e.stopPropagation()} + > + <DownloadOutlined style={{ fontSize: "18px" }} /> + </a> + </div> + ); + } + + // 检查是否为Office文件链接 + if ( + typeof content === "string" && + content.match(/\.(doc|docx|xls|xlsx|ppt|pptx|pdf)$/i) + ) { + const fileName = content.split("/").pop() || "文件"; + const fileExt = fileName.split(".").pop()?.toLowerCase(); + + // 根据文件类型选择不同的图标 + let fileIcon = ( + <FileOutlined + style={{ fontSize: "24px", marginRight: "8px", color: "#1890ff" }} + /> + ); + + if (fileExt === "pdf") { + fileIcon = ( + <FilePdfOutlined + style={{ fontSize: "24px", marginRight: "8px", color: "#ff4d4f" }} + /> + ); + } else if (fileExt === "doc" || fileExt === "docx") { + fileIcon = ( + <FileWordOutlined + style={{ fontSize: "24px", marginRight: "8px", color: "#2f54eb" }} + /> + ); + } else if (fileExt === "xls" || fileExt === "xlsx") { + fileIcon = ( + <FileExcelOutlined + style={{ fontSize: "24px", marginRight: "8px", color: "#52c41a" }} + /> + ); + } else if (fileExt === "ppt" || fileExt === "pptx") { + fileIcon = ( + <FilePptOutlined + style={{ fontSize: "24px", marginRight: "8px", color: "#fa8c16" }} + /> + ); + } + + return ( + <div className={styles.fileMessage}> + {fileIcon} + <div className={styles.fileInfo}> + <div + style={{ + fontWeight: "bold", + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + }} + > + {fileName} + </div> + </div> + <a + href={content} + download={fileExt !== "pdf" ? fileName : undefined} + target={fileExt === "pdf" ? "_blank" : undefined} + className={styles.downloadButton} + onClick={e => e.stopPropagation()} + style={{ display: "flex" }} + rel="noreferrer" + > + <DownloadOutlined style={{ fontSize: "18px" }} /> + </a> + </div> + ); + } + + // 检查是否为文件消息(JSON格式) + try { + if ( + typeof content === "string" && + content.trim().startsWith("{") && + content.trim().endsWith("}") + ) { + const fileData = JSON.parse(content); + if (fileData.type === "file" && fileData.title) { + // 检查是否为Office文件 + const fileExt = fileData.title.split(".").pop()?.toLowerCase(); + let fileIcon = ( + <FolderOutlined + style={{ fontSize: "24px", marginRight: "8px", color: "#1890ff" }} + /> + ); + + if (fileExt === "pdf") { + fileIcon = ( + <FilePdfOutlined + style={{ + fontSize: "24px", + marginRight: "8px", + color: "#ff4d4f", + }} + /> + ); + } else if (fileExt === "doc" || fileExt === "docx") { + fileIcon = ( + <FileWordOutlined + style={{ + fontSize: "24px", + marginRight: "8px", + color: "#2f54eb", + }} + /> + ); + } else if (fileExt === "xls" || fileExt === "xlsx") { + fileIcon = ( + <FileExcelOutlined + style={{ + fontSize: "24px", + marginRight: "8px", + color: "#52c41a", + }} + /> + ); + } else if (fileExt === "ppt" || fileExt === "pptx") { + fileIcon = ( + <FilePptOutlined + style={{ + fontSize: "24px", + marginRight: "8px", + color: "#fa8c16", + }} + /> + ); + } + + return ( + <div className={styles.fileMessage}> + {fileIcon} + <div className={styles.fileInfo}> + <div + style={{ + fontWeight: "bold", + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + }} + > + {fileData.title} + </div> + {fileData.totalLen && ( + <div style={{ fontSize: "12px", color: "#8c8c8c" }}> + {Math.round(fileData.totalLen / 1024)} KB + </div> + )} + </div> + <a + href={fileData.url || "#"} + download={fileExt !== "pdf" ? fileData.title : undefined} + target={fileExt === "pdf" ? "_blank" : undefined} + className={styles.downloadButton} + style={{ display: "flex" }} + onClick={e => { + e.stopPropagation(); + if (!fileData.url) { + console.log("文件URL不存在"); + } + }} + rel="noreferrer" + > + <DownloadOutlined style={{ fontSize: "18px" }} /> + </a> + </div> + ); + } + } + } catch (e) { + // 解析JSON失败,不是文件消息 + } + + // 检查是否为位置信息 + if ( + typeof content === "string" && + (content.includes("<location") || content.includes("<msg><location")) + ) { + // 提取位置信息 + const labelMatch = content.match(/label="([^"]*)"/i); + const poiNameMatch = content.match(/poiname="([^"]*)"/i); + const xMatch = content.match(/x="([^"]*)"/i); + const yMatch = content.match(/y="([^"]*)"/i); + + const label = labelMatch + ? labelMatch[1] + : poiNameMatch + ? poiNameMatch[1] + : "位置信息"; + const coordinates = xMatch && yMatch ? `${yMatch[1]}, ${xMatch[1]}` : ""; + + return ( + <div className={styles.locationMessage}> + <EnvironmentOutlined + style={{ fontSize: "24px", marginRight: "8px", color: "#ff4d4f" }} + /> + <div> + <div style={{ fontWeight: "bold" }}>{label}</div> + {coordinates && ( + <div style={{ fontSize: "12px", color: "#8c8c8c" }}> + {coordinates} + </div> + )} + </div> + </div> + ); + } + + // 默认为文本消息 + return <div className={styles.messageText}>{content}</div>; + }; + + // 用于分组消息并添加时间戳的辅助函数 + const groupMessagesByTime = (messages: ChatRecord[]) => { + return messages + .filter(msg => msg !== null && msg !== undefined) // 过滤掉null和undefined的消息 + .map(msg => ({ + time: formatWechatTime(msg?.wechatTime), + messages: [msg], + })); + }; + + const renderMessage = (msg: ChatRecord) => { + // 添加null检查,防止访问null对象的属性 + if (!msg) return null; + + const isOwn = msg?.isSend; + return ( + <div + key={msg.id || `msg-${Date.now()}`} + className={`${styles.messageItem} ${ + isOwn ? styles.ownMessage : styles.otherMessage + }`} + > + <div className={styles.messageContent}> + {!isOwn && ( + <Avatar + size={32} + src={contract.avatar} + icon={<UserOutlined />} + className={styles.messageAvatar} + /> + )} + <div className={styles.messageBubble}> + {!isOwn && ( + <div className={styles.messageSender}>{msg?.senderName}</div> + )} + {parseMessageContent(msg?.content, msg)} + </div> + </div> + </div> + ); + }; + + const chatMenu = ( + <Menu> + <Menu.Item key="profile" icon={<UserOutlined />}> + 查看资料 + </Menu.Item> + <Menu.Item key="call" icon={<PhoneOutlined />}> + 语音通话 + </Menu.Item> + <Menu.Item key="video" icon={<VideoCameraOutlined />}> + 视频通话 + </Menu.Item> + <Menu.Divider /> + <Menu.Item key="pin">置顶聊天</Menu.Item> + <Menu.Item key="mute">消息免打扰</Menu.Item> + <Menu.Divider /> + <Menu.Item key="clear" danger> + 清空聊天记录 + </Menu.Item> + </Menu> + ); + + return ( + <Layout className={styles.chatWindow}> + {/* 聊天主体区域 */} + <Layout className={styles.chatMain}> + {/* 聊天头部 */} + <Header className={styles.chatHeader}> + <div className={styles.chatHeaderInfo}> + <Avatar + size={40} + src={contract.avatar || contract.chatroomAvatar} + icon={ + contract.type === "group" ? <TeamOutlined /> : <UserOutlined /> + } + /> + <div className={styles.chatHeaderDetails}> + <div className={styles.chatHeaderName}> + {contract.nickname || contract.name} + </div> + </div> + </div> + <Space> + <Tooltip title="语音通话"> + <Button + type="text" + icon={<PhoneOutlined />} + className={styles.headerButton} + /> + </Tooltip> + <Tooltip title="视频通话"> + <Button + type="text" + icon={<VideoCameraOutlined />} + className={styles.headerButton} + /> + </Tooltip> + <Dropdown overlay={chatMenu} trigger={["click"]}> + <Button + type="text" + icon={<MoreOutlined />} + className={styles.headerButton} + /> + </Dropdown> + </Space> + </Header> + + {/* 聊天内容 */} + <Content className={styles.chatContent}> + <div className={styles.messagesContainer}> + {groupMessagesByTime(currentMessages).map((group, groupIndex) => ( + <React.Fragment key={`group-${groupIndex}`}> + <div className={styles.messageTime}>{group.time}</div> + {group.messages.map(renderMessage)} + </React.Fragment> + ))} + <div ref={messagesEndRef} /> + </div> + </Content> + + {/* 消息输入组件 */} + <MessageEnter contract={contract} /> + </Layout> + + {/* 右侧个人资料卡片 */} + <ProfileCard + contract={contract} + showProfile={showProfile} + onToggleProfile={onToggleProfile} + /> + </Layout> + ); +}; + +export default ChatWindow; From 527402425f1ef142607e8ba5d294a3150f4bbcc3 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?= <fsmecx@gmail.com> Date: Mon, 8 Sep 2025 16:41:16 +0800 Subject: [PATCH 08/10] =?UTF-8?q?feat(=E6=9C=8B=E5=8F=8B=E5=9C=88=E5=90=8C?= =?UTF-8?q?=E6=AD=A5):=20=E6=96=B0=E5=A2=9E=E6=9C=8B=E5=8F=8B=E5=9C=88?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E8=AE=B0=E5=BD=95=E9=A1=B5=E9=9D=A2=E5=8F=8A?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构朋友圈同步模块结构,将原MomentsSync组件拆分为list和record两个子模块 添加朋友圈同步记录页面,包含搜索、分页和记录展示功能 优化样式文件组织,删除重复代码 新增api接口和数据模型定义 --- Cunkebao/dist/.vite/manifest.json | 18 +- Cunkebao/dist/index.html | 8 +- .../workspace/auto-like/record/index.tsx | 3 - .../auto-like/record/record.module.scss | 2 +- .../moments-sync/{ => list}/index.module.scss | 0 .../{MomentsSync.tsx => list/index.tsx} | 2 +- .../workspace/moments-sync/record/api.ts | 63 ++++ .../workspace/moments-sync/record/data.ts | 119 +++++++ .../workspace/moments-sync/record/index.tsx | 306 ++++++++++++++++++ .../moments-sync/record/record.module.scss | 268 +++++++++++++++ Cunkebao/src/router/module/workspace.tsx | 10 +- 11 files changed, 779 insertions(+), 20 deletions(-) rename Cunkebao/src/pages/mobile/workspace/moments-sync/{ => list}/index.module.scss (100%) rename Cunkebao/src/pages/mobile/workspace/moments-sync/{MomentsSync.tsx => list/index.tsx} (99%) create mode 100644 Cunkebao/src/pages/mobile/workspace/moments-sync/record/api.ts create mode 100644 Cunkebao/src/pages/mobile/workspace/moments-sync/record/data.ts create mode 100644 Cunkebao/src/pages/mobile/workspace/moments-sync/record/index.tsx create mode 100644 Cunkebao/src/pages/mobile/workspace/moments-sync/record/record.module.scss diff --git a/Cunkebao/dist/.vite/manifest.json b/Cunkebao/dist/.vite/manifest.json index e78197a2..435e8e43 100644 --- a/Cunkebao/dist/.vite/manifest.json +++ b/Cunkebao/dist/.vite/manifest.json @@ -1,14 +1,14 @@ { - "_charts-B9_ggjgM.js": { - "file": "assets/charts-B9_ggjgM.js", + "_charts-DrYFgxF3.js": { + "file": "assets/charts-DrYFgxF3.js", "name": "charts", "imports": [ - "_ui-CdpU1706.js", + "_ui-BCezweA9.js", "_vendor-2vc8h_ct.js" ] }, - "_ui-CdpU1706.js": { - "file": "assets/ui-CdpU1706.js", + "_ui-BCezweA9.js": { + "file": "assets/ui-BCezweA9.js", "name": "ui", "imports": [ "_vendor-2vc8h_ct.js" @@ -33,18 +33,18 @@ "name": "vendor" }, "index.html": { - "file": "assets/index-BZQSHOtN.js", + "file": "assets/index-B7uWDiaN.js", "name": "index", "src": "index.html", "isEntry": true, "imports": [ "_vendor-2vc8h_ct.js", "_utils-6WF66_dS.js", - "_ui-CdpU1706.js", - "_charts-B9_ggjgM.js" + "_ui-BCezweA9.js", + "_charts-DrYFgxF3.js" ], "css": [ - "assets/index-CciB7EKw.css" + "assets/index-DkU7m7k6.css" ] } } \ No newline at end of file diff --git a/Cunkebao/dist/index.html b/Cunkebao/dist/index.html index 6f790150..4ae4c1fd 100644 --- a/Cunkebao/dist/index.html +++ b/Cunkebao/dist/index.html @@ -11,13 +11,13 @@ </style> <!-- 引入 uni-app web-view SDK(必须) --> <script type="text/javascript" src="/websdk.js"></script> - <script type="module" crossorigin src="/assets/index-BZQSHOtN.js"></script> + <script type="module" crossorigin src="/assets/index-B7uWDiaN.js"></script> <link rel="modulepreload" crossorigin href="/assets/vendor-2vc8h_ct.js"> <link rel="modulepreload" crossorigin href="/assets/utils-6WF66_dS.js"> - <link rel="modulepreload" crossorigin href="/assets/ui-CdpU1706.js"> - <link rel="modulepreload" crossorigin href="/assets/charts-B9_ggjgM.js"> + <link rel="modulepreload" crossorigin href="/assets/ui-BCezweA9.js"> + <link rel="modulepreload" crossorigin href="/assets/charts-DrYFgxF3.js"> <link rel="stylesheet" crossorigin href="/assets/ui-D0C0OGrH.css"> - <link rel="stylesheet" crossorigin href="/assets/index-CciB7EKw.css"> + <link rel="stylesheet" crossorigin href="/assets/index-DkU7m7k6.css"> </head> <body> <div id="root"></div> diff --git a/Cunkebao/src/pages/mobile/workspace/auto-like/record/index.tsx b/Cunkebao/src/pages/mobile/workspace/auto-like/record/index.tsx index fe08157e..1915fa48 100644 --- a/Cunkebao/src/pages/mobile/workspace/auto-like/record/index.tsx +++ b/Cunkebao/src/pages/mobile/workspace/auto-like/record/index.tsx @@ -137,9 +137,6 @@ export default function AutoLikeRecord() { onChange={handlePageChange} showSizeChanger={false} showQuickJumper - showTotal={(total, range) => - `第 ${range[0]}-${range[1]} 条,共 ${total} 条` - } size="default" className={styles.pagination} /> diff --git a/Cunkebao/src/pages/mobile/workspace/auto-like/record/record.module.scss b/Cunkebao/src/pages/mobile/workspace/auto-like/record/record.module.scss index a6df2c00..b63be1f0 100644 --- a/Cunkebao/src/pages/mobile/workspace/auto-like/record/record.module.scss +++ b/Cunkebao/src/pages/mobile/workspace/auto-like/record/record.module.scss @@ -3,7 +3,7 @@ display: flex; align-items: center; gap: 8px; - padding: 0 16px; + padding: 16px; } .headerSearchInputWrap { position: relative; diff --git a/Cunkebao/src/pages/mobile/workspace/moments-sync/index.module.scss b/Cunkebao/src/pages/mobile/workspace/moments-sync/list/index.module.scss similarity index 100% rename from Cunkebao/src/pages/mobile/workspace/moments-sync/index.module.scss rename to Cunkebao/src/pages/mobile/workspace/moments-sync/list/index.module.scss diff --git a/Cunkebao/src/pages/mobile/workspace/moments-sync/MomentsSync.tsx b/Cunkebao/src/pages/mobile/workspace/moments-sync/list/index.tsx similarity index 99% rename from Cunkebao/src/pages/mobile/workspace/moments-sync/MomentsSync.tsx rename to Cunkebao/src/pages/mobile/workspace/moments-sync/list/index.tsx index 3e81136d..1bd6d1d4 100644 --- a/Cunkebao/src/pages/mobile/workspace/moments-sync/MomentsSync.tsx +++ b/Cunkebao/src/pages/mobile/workspace/moments-sync/list/index.tsx @@ -124,7 +124,7 @@ const MomentsSync: React.FC = () => { <Menu.Item key="view" icon={<EyeOutlined />} - onClick={() => navigate(`/workspace/moments-sync/${task.id}`)} + onClick={() => navigate(`/workspace/moments-sync/record/${task.id}`)} > 查看 </Menu.Item> diff --git a/Cunkebao/src/pages/mobile/workspace/moments-sync/record/api.ts b/Cunkebao/src/pages/mobile/workspace/moments-sync/record/api.ts new file mode 100644 index 00000000..6142fde8 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/moments-sync/record/api.ts @@ -0,0 +1,63 @@ +import request from "@/api/request"; +import { + LikeTask, + CreateLikeTaskData, + UpdateLikeTaskData, + LikeRecord, + PaginatedResponse, +} from "@/pages/workspace/auto-like/record/data"; + +// 获取自动点赞任务列表 +export function fetchAutoLikeTasks( + params = { type: 1, page: 1, limit: 100 }, +): Promise<LikeTask[]> { + return request("/v1/workbench/list", params, "GET"); +} + +// 获取单个任务详情 +export function fetchAutoLikeTaskDetail(id: string): Promise<LikeTask | null> { + return request("/v1/workbench/detail", { id }, "GET"); +} + +// 创建自动点赞任务 +export function createAutoLikeTask(data: CreateLikeTaskData): Promise<any> { + return request("/v1/workbench/create", { ...data, type: 1 }, "POST"); +} + +// 更新自动点赞任务 +export function updateAutoLikeTask(data: UpdateLikeTaskData): Promise<any> { + return request("/v1/workbench/update", { ...data, type: 1 }, "POST"); +} + +// 删除自动点赞任务 +export function deleteAutoLikeTask(id: string): Promise<any> { + return request("/v1/workbench/delete", { id }, "DELETE"); +} + +// 切换任务状态 +export function toggleAutoLikeTask(id: string, status: string): Promise<any> { + return request("/v1/workbench/update-status", { id, status }, "POST"); +} + +// 复制自动点赞任务 +export function copyAutoLikeTask(id: string): Promise<any> { + return request("/v1/workbench/copy", { id }, "POST"); +} + +// 获取点赞记录 +export function fetchLikeRecords( + workbenchId: string, + page: number = 1, + limit: number = 20, + keyword?: string, +): Promise<PaginatedResponse<LikeRecord>> { + const params: any = { + workbenchId, + page: page.toString(), + limit: limit.toString(), + }; + if (keyword) { + params.keyword = keyword; + } + return request("/v1/workbench/like-records", params, "GET"); +} diff --git a/Cunkebao/src/pages/mobile/workspace/moments-sync/record/data.ts b/Cunkebao/src/pages/mobile/workspace/moments-sync/record/data.ts new file mode 100644 index 00000000..de39bd28 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/moments-sync/record/data.ts @@ -0,0 +1,119 @@ +// 自动点赞任务状态 +export type LikeTaskStatus = 1 | 2; // 1: 开启, 2: 关闭 + +// 内容类型 +export type ContentType = "text" | "image" | "video" | "link"; + +// 设备信息 +export interface Device { + id: string; + name: string; + status: "online" | "offline"; + lastActive: string; +} + +// 好友信息 +export interface Friend { + id: string; + nickname: string; + wechatId: string; + avatar: string; + tags: string[]; + region: string; + source: string; +} + +// 点赞记录 +export interface LikeRecord { + id: string; + workbenchId: string; + momentsId: string; + snsId: string; + wechatAccountId: string; + wechatFriendId: string; + likeTime: string; + content: string; + resUrls: string[]; + momentTime: string; + userName: string; + operatorName: string; + operatorAvatar: string; + friendName: string; + friendAvatar: string; +} + +// 自动点赞任务 +export interface LikeTask { + id: string; + name: string; + status: LikeTaskStatus; + deviceCount: number; + targetGroup: string; + likeCount: number; + lastLikeTime: string; + createTime: string; + creator: string; + likeInterval: number; + maxLikesPerDay: number; + timeRange: { start: string; end: string }; + contentTypes: ContentType[]; + targetTags: string[]; + devices: string[]; + friends: string[]; + friendMaxLikes: number; + friendTags: string; + enableFriendTags: boolean; + todayLikeCount: number; + totalLikeCount: number; + updateTime: string; +} + +// 创建任务数据 +export interface CreateLikeTaskData { + name: string; + interval: number; + maxLikes: number; + startTime: string; + endTime: string; + contentTypes: ContentType[]; + devices: string[]; + friends?: string[]; + friendMaxLikes: number; + friendTags?: string; + enableFriendTags: boolean; + targetTags: string[]; +} + +// 更新任务数据 +export interface UpdateLikeTaskData extends CreateLikeTaskData { + id: string; +} + +// 任务配置 +export interface TaskConfig { + interval: number; + maxLikes: number; + startTime: string; + endTime: string; + contentTypes: ContentType[]; + devices: string[]; + friends: string[]; + friendMaxLikes: number; + friendTags: string; + enableFriendTags: boolean; +} + +// API响应类型 +export interface ApiResponse<T = any> { + code: number; + msg: string; + data: T; +} + +// 分页响应类型 +export interface PaginatedResponse<T> { + list: T[]; + total: number; + page: number; + limit: number; +} diff --git a/Cunkebao/src/pages/mobile/workspace/moments-sync/record/index.tsx b/Cunkebao/src/pages/mobile/workspace/moments-sync/record/index.tsx new file mode 100644 index 00000000..1915fa48 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/moments-sync/record/index.tsx @@ -0,0 +1,306 @@ +import React, { useState, useEffect } from "react"; +import { useParams } from "react-router-dom"; +import { + Button, + Input, + Card, + Badge, + Avatar, + Skeleton, + message, + Spin, + Divider, + Pagination, +} from "antd"; +import { + LikeOutlined, + ReloadOutlined, + SearchOutlined, + UserOutlined, +} from "@ant-design/icons"; +import styles from "./record.module.scss"; +import NavCommon from "@/components/NavCommon"; +import { fetchLikeRecords } from "./api"; +import Layout from "@/components/Layout/Layout"; + +// 格式化日期 +const formatDate = (dateString: string) => { + try { + const date = new Date(dateString); + return date.toLocaleString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); + } catch (error) { + return dateString; + } +}; + +export default function AutoLikeRecord() { + const { id } = useParams<{ id: string }>(); + const [records, setRecords] = useState<any[]>([]); + const [recordsLoading, setRecordsLoading] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [total, setTotal] = useState(0); + const pageSize = 10; + + useEffect(() => { + if (!id) return; + setRecordsLoading(true); + fetchLikeRecords(id, 1, pageSize) + .then((response: any) => { + setRecords(response.list || []); + setTotal(response.total || 0); + setCurrentPage(1); + }) + .catch(() => { + message.error("获取点赞记录失败,请稍后重试"); + }) + .finally(() => setRecordsLoading(false)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [id]); + + const handleSearch = () => { + setCurrentPage(1); + fetchLikeRecords(id!, 1, pageSize, searchTerm) + .then((response: any) => { + setRecords(response.list || []); + setTotal(response.total || 0); + setCurrentPage(1); + }) + .catch(() => { + message.error("获取点赞记录失败,请稍后重试"); + }); + }; + + const handleRefresh = () => { + fetchLikeRecords(id!, currentPage, pageSize, searchTerm) + .then((response: any) => { + setRecords(response.list || []); + setTotal(response.total || 0); + }) + .catch(() => { + message.error("获取点赞记录失败,请稍后重试"); + }); + }; + + const handlePageChange = (newPage: number) => { + fetchLikeRecords(id!, newPage, pageSize, searchTerm) + .then((response: any) => { + setRecords(response.list || []); + setTotal(response.total || 0); + setCurrentPage(newPage); + }) + .catch(() => { + message.error("获取点赞记录失败,请稍后重试"); + }); + }; + + return ( + <Layout + header={ + <> + <NavCommon title="点赞记录" /> + <div className={styles.headerSearchBar}> + <div className={styles.headerSearchInputWrap}> + <Input + prefix={<SearchOutlined className={styles.headerSearchIcon} />} + placeholder="搜索好友昵称或内容" + className={styles.headerSearchInput} + value={searchTerm} + onChange={e => setSearchTerm(e.target.value)} + onPressEnter={handleSearch} + allowClear + /> + </div> + <Button + icon={<ReloadOutlined spin={recordsLoading} />} + onClick={handleRefresh} + loading={recordsLoading} + type="default" + shape="circle" + /> + </div> + </> + } + footer={ + <> + <div className={styles.footerPagination}> + <Pagination + current={currentPage} + total={total} + pageSize={pageSize} + onChange={handlePageChange} + showSizeChanger={false} + showQuickJumper + size="default" + className={styles.pagination} + /> + </div> + </> + } + > + <div className={styles.bgWrap}> + <div className={styles.contentWrap}> + {recordsLoading ? ( + <div className={styles.skeletonWrap}> + {Array.from({ length: 3 }).map((_, index) => ( + <div key={index} className={styles.skeletonCard}> + <div className={styles.skeletonCardHeader}> + <Skeleton.Avatar + active + size={40} + className={styles.skeletonAvatar} + /> + <div className={styles.skeletonNameWrap}> + <Skeleton.Input + active + size="small" + className={styles.skeletonName} + style={{ width: 96 }} + /> + <Skeleton.Input + active + size="small" + className={styles.skeletonSub} + style={{ width: 64 }} + /> + </div> + </div> + <Divider className={styles.skeletonSep} /> + <div className={styles.skeletonContentWrap}> + <Skeleton.Input + active + size="small" + className={styles.skeletonContent1} + style={{ width: "100%" }} + /> + <Skeleton.Input + active + size="small" + className={styles.skeletonContent2} + style={{ width: "75%" }} + /> + <div className={styles.skeletonImgWrap}> + <Skeleton.Image + active + className={styles.skeletonImg} + style={{ width: 80, height: 80 }} + /> + <Skeleton.Image + active + className={styles.skeletonImg} + style={{ width: 80, height: 80 }} + /> + </div> + </div> + </div> + ))} + </div> + ) : records.length === 0 ? ( + <div className={styles.emptyWrap}> + <LikeOutlined className={styles.emptyIcon} /> + <p className={styles.emptyText}>暂无点赞记录</p> + </div> + ) : ( + <> + {records.map(record => ( + <div key={record.id} className={styles.recordCard}> + <div className={styles.recordCardHeader}> + <div className={styles.recordCardHeaderLeft}> + <Avatar + src={record.friendAvatar || undefined} + icon={<UserOutlined />} + size={40} + className={styles.avatarImg} + /> + <div className={styles.friendInfo}> + <div + className={styles.friendName} + title={record.friendName} + > + {record.friendName} + </div> + <div className={styles.friendSub}>内容发布者</div> + </div> + </div> + <Badge + className={styles.timeBadge} + count={formatDate(record.momentTime || record.likeTime)} + style={{ + background: "#e8f0fe", + color: "#333", + fontWeight: 400, + }} + /> + </div> + <Divider className={styles.cardSep} /> + <div className={styles.cardContent}> + {record.content && ( + <p className={styles.contentText}>{record.content}</p> + )} + {Array.isArray(record.resUrls) && + record.resUrls.length > 0 && ( + <div + className={ + `${styles.imgGrid} ` + + (record.resUrls.length === 1 + ? styles.grid1 + : record.resUrls.length === 2 + ? styles.grid2 + : record.resUrls.length <= 3 + ? styles.grid3 + : record.resUrls.length <= 6 + ? styles.grid6 + : styles.grid9) + } + > + {record.resUrls + .slice(0, 9) + .map((image: string, idx: number) => ( + <div key={idx} className={styles.imgItem}> + <img + src={image} + alt={`内容图片 ${idx + 1}`} + className={styles.img} + /> + </div> + ))} + </div> + )} + </div> + <div className={styles.operatorWrap}> + <Avatar + src={record.operatorAvatar || undefined} + icon={<UserOutlined />} + size={32} + className={styles.operatorAvatar} + /> + <div className={styles.operatorInfo}> + <span + className={styles.operatorName} + title={record.operatorName} + > + {record.operatorName} + </span> + <span className={styles.operatorAction}> + <LikeOutlined + style={{ color: "red", marginRight: 4 }} + /> + 已赞 + </span> + </div> + </div> + </div> + ))} + </> + )} + </div> + </div> + </Layout> + ); +} diff --git a/Cunkebao/src/pages/mobile/workspace/moments-sync/record/record.module.scss b/Cunkebao/src/pages/mobile/workspace/moments-sync/record/record.module.scss new file mode 100644 index 00000000..b63be1f0 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/moments-sync/record/record.module.scss @@ -0,0 +1,268 @@ +// 搜索栏 +.headerSearchBar { + display: flex; + align-items: center; + gap: 8px; + padding: 16px; +} +.headerSearchInputWrap { + position: relative; + flex: 1; +} +.headerSearchIcon { + position: absolute; + left: 12px; + top: 10px; + width: 16px; + height: 16px; + color: #a3a3a3; +} +.headerSearchInput { + padding-left: 32px !important; +} +.spin { + animation: spin 1s linear infinite; +} +@keyframes spin { + 100% { + transform: rotate(360deg); + } +} + +// 分页 +.footerPagination { + display: flex; + justify-content: center; + align-items: center; + padding: 12px 0; + background: #fff; +} +.pagination { + :global(.ant-pagination-item) { + border-radius: 6px; + } + :global(.ant-pagination-item-active) { + background: #1890ff; + border-color: #1890ff; + } + :global(.ant-pagination-prev), + :global(.ant-pagination-next) { + border-radius: 6px; + } + :global(.ant-pagination-jump-prev), + :global(.ant-pagination-jump-next) { + border-radius: 6px; + } +} + +// 背景和内容 +.bgWrap { + background: #f7f7fa; + min-height: 100vh; + padding-bottom: 80px; +} +.contentWrap { + padding: 0 16px; + display: flex; + flex-direction: column; + gap: 16px; +} + +// 骨架屏 +.skeletonWrap { + display: flex; + flex-direction: column; + gap: 16px; +} +.skeletonCard { + padding: 0px; +} +.skeletonCardHeader { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} +.skeletonAvatar { + width: 40px; + height: 40px; + border-radius: 50%; +} +.skeletonNameWrap { + display: flex; + flex-direction: column; + gap: 8px; +} +.skeletonName { + width: 96px; + height: 16px; +} +.skeletonSub { + width: 64px; + height: 12px; +} +.skeletonSep { + margin: 12px 0; +} +.skeletonContentWrap { + display: flex; + flex-direction: column; + gap: 8px; +} +.skeletonContent1 { + width: 100%; + height: 16px; +} +.skeletonContent2 { + width: 75%; + height: 16px; +} +.skeletonImgWrap { + display: flex; + gap: 8px; + margin-top: 12px; +} +.skeletonImg { + width: 80px; + height: 80px; + border-radius: 8px; +} + +// 空状态 +.emptyWrap { + text-align: center; + padding: 48px 0; +} +.emptyIcon { + width: 48px; + height: 48px; + color: #e5e7eb; + margin: 0 auto 12px auto; +} +.emptyText { + color: #888; + font-size: 16px; +} + +// 记录卡片 +.recordCard { + background: #fff; + border-radius: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + padding: 16px; +} +.recordCardHeader { + display: flex; + align-items: flex-start; + justify-content: space-between; +} +.recordCardHeaderLeft { + display: flex; + align-items: center; + gap: 12px; + max-width: 65%; +} +.avatarImg { + width: 40px; + height: 40px; + border-radius: 50%; +} +.friendInfo { + min-width: 0; +} +.friendName { + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.friendSub { + font-size: 13px; + color: #888; +} +.timeBadge { + background: #e8f0fe; + white-space: nowrap; + flex-shrink: 0; +} +.cardSep { + margin: 12px 0; +} +.cardContent { + margin-bottom: 12px; +} +.contentText { + color: #444; + margin-bottom: 12px; + white-space: pre-line; +} +.imgGrid { + display: grid; + gap: 8px; +} +.grid1 { + grid-template-columns: 1fr; +} +.grid2 { + grid-template-columns: 1fr 1fr; +} +.grid3 { + grid-template-columns: 1fr 1fr 1fr; +} +.grid6 { + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr; +} +.grid9 { + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr 1fr; +} +.imgItem { + position: relative; + aspect-ratio: 1/1; + border-radius: 8px; + overflow: hidden; +} +.img { + width: 100%; + height: 100%; + object-fit: cover; +} + +// 操作人 +.operatorWrap { + display: flex; + align-items: center; + margin-top: 16px; + padding: 8px; + background: #f3f4f6; + border-radius: 8px; +} +.operatorAvatar { + width: 32px !important; + height: 32px !important; + margin-right: 8px; + flex-shrink: 0; +} +.operatorInfo { + font-size: 14px; + position: relative; + flex: 1; + position: relative; +} +.operatorName { + font-weight: 500; + max-width: 100%; + display: inline-block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.operatorAction { + color: #888; + margin-left: 8px; + font-size: 12px; + position: absolute; + right: 0; + top: 2px; +} diff --git a/Cunkebao/src/router/module/workspace.tsx b/Cunkebao/src/router/module/workspace.tsx index 5c010122..c4b365e6 100644 --- a/Cunkebao/src/router/module/workspace.tsx +++ b/Cunkebao/src/router/module/workspace.tsx @@ -8,9 +8,10 @@ import AutoGroupForm from "@/pages/mobile/workspace/auto-group/form"; import GroupPush from "@/pages/mobile/workspace/group-push/list"; import FormGroupPush from "@/pages/mobile/workspace/group-push/form"; import DetailGroupPush from "@/pages/mobile/workspace/group-push/detail"; -import MomentsSync from "@/pages/mobile/workspace/moments-sync/MomentsSync"; -import MomentsSyncDetail from "@/pages/mobile/workspace/moments-sync/Detail"; +import MomentsSync from "@/pages/mobile/workspace/moments-sync/list"; import NewMomentsSync from "@/pages/mobile/workspace/moments-sync/new/index"; +import MomentsSyncDetail from "@/pages/mobile/workspace/moments-sync/Detail"; +import MomentsSyncRecord from "@/pages/mobile/workspace/moments-sync/record"; import AIAssistant from "@/pages/mobile/workspace/ai-assistant/AIAssistant"; import TrafficDistribution from "@/pages/mobile/workspace/traffic-distribution/list/index"; import TrafficDistributionDetail from "@/pages/mobile/workspace/traffic-distribution/detail/index"; @@ -103,6 +104,11 @@ const workspaceRoutes = [ element: <MomentsSyncDetail />, auth: true, }, + { + path: "/workspace/moments-sync/record/:id", + element: <MomentsSyncRecord />, + auth: true, + }, { path: "/workspace/moments-sync/edit/:id", element: <NewMomentsSync />, From d98b056b57a37f2fb5073311a0d47f66fd9e93e1 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?= <fsmecx@gmail.com> Date: Mon, 8 Sep 2025 17:05:30 +0800 Subject: [PATCH 09/10] =?UTF-8?q?feat(=E8=AE=B0=E5=BD=95=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?):=20=E9=87=8D=E6=9E=84=E6=9C=8B=E5=8F=8B=E5=9C=88=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E8=AE=B0=E5=BD=95=E9=A1=B5=E9=9D=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改API端点从点赞记录变更为发表记录 - 更新日期格式化函数处理时间戳 - 移除记录卡片中不必要的操作者信息 - 调整样式移除最大宽度限制 - 更新页面标题和空状态提示文本 --- .../workspace/moments-sync/record/api.ts | 2 +- .../workspace/moments-sync/record/index.tsx | 58 +++++-------------- .../moments-sync/record/record.module.scss | 1 - 3 files changed, 16 insertions(+), 45 deletions(-) diff --git a/Cunkebao/src/pages/mobile/workspace/moments-sync/record/api.ts b/Cunkebao/src/pages/mobile/workspace/moments-sync/record/api.ts index 6142fde8..1a131cbc 100644 --- a/Cunkebao/src/pages/mobile/workspace/moments-sync/record/api.ts +++ b/Cunkebao/src/pages/mobile/workspace/moments-sync/record/api.ts @@ -59,5 +59,5 @@ export function fetchLikeRecords( if (keyword) { params.keyword = keyword; } - return request("/v1/workbench/like-records", params, "GET"); + return request("/v1/workbench/moments-records", params, "GET"); } diff --git a/Cunkebao/src/pages/mobile/workspace/moments-sync/record/index.tsx b/Cunkebao/src/pages/mobile/workspace/moments-sync/record/index.tsx index 1915fa48..b844e809 100644 --- a/Cunkebao/src/pages/mobile/workspace/moments-sync/record/index.tsx +++ b/Cunkebao/src/pages/mobile/workspace/moments-sync/record/index.tsx @@ -24,9 +24,10 @@ import { fetchLikeRecords } from "./api"; import Layout from "@/components/Layout/Layout"; // 格式化日期 -const formatDate = (dateString: string) => { +const formatDate = (timestamp: number) => { + timestamp = timestamp * 1000; try { - const date = new Date(dateString); + const date = new Date(timestamp); return date.toLocaleString("zh-CN", { year: "numeric", month: "2-digit", @@ -35,7 +36,7 @@ const formatDate = (dateString: string) => { minute: "2-digit", }); } catch (error) { - return dateString; + return timestamp; } }; @@ -58,7 +59,7 @@ export default function AutoLikeRecord() { setCurrentPage(1); }) .catch(() => { - message.error("获取点赞记录失败,请稍后重试"); + message.error("获取发表记录失败,请稍后重试"); }) .finally(() => setRecordsLoading(false)); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -73,7 +74,7 @@ export default function AutoLikeRecord() { setCurrentPage(1); }) .catch(() => { - message.error("获取点赞记录失败,请稍后重试"); + message.error("获取发表记录失败,请稍后重试"); }); }; @@ -84,7 +85,7 @@ export default function AutoLikeRecord() { setTotal(response.total || 0); }) .catch(() => { - message.error("获取点赞记录失败,请稍后重试"); + message.error("获取发表记录失败,请稍后重试"); }); }; @@ -96,7 +97,7 @@ export default function AutoLikeRecord() { setCurrentPage(newPage); }) .catch(() => { - message.error("获取点赞记录失败,请稍后重试"); + message.error("获取发表记录失败,请稍后重试"); }); }; @@ -104,7 +105,7 @@ export default function AutoLikeRecord() { <Layout header={ <> - <NavCommon title="点赞记录" /> + <NavCommon title="发表记录" /> <div className={styles.headerSearchBar}> <div className={styles.headerSearchInputWrap}> <Input @@ -204,7 +205,7 @@ export default function AutoLikeRecord() { ) : records.length === 0 ? ( <div className={styles.emptyWrap}> <LikeOutlined className={styles.emptyIcon} /> - <p className={styles.emptyText}>暂无点赞记录</p> + <p className={styles.emptyText}>暂无发表记录</p> </div> ) : ( <> @@ -213,7 +214,7 @@ export default function AutoLikeRecord() { <div className={styles.recordCardHeader}> <div className={styles.recordCardHeaderLeft}> <Avatar - src={record.friendAvatar || undefined} + src={record.operatorAvatar || undefined} icon={<UserOutlined />} size={40} className={styles.avatarImg} @@ -223,20 +224,13 @@ export default function AutoLikeRecord() { className={styles.friendName} title={record.friendName} > - {record.friendName} + {record.operatorName} + </div> + <div className={styles.friendSub}> + {formatDate(record.publishTime)} </div> - <div className={styles.friendSub}>内容发布者</div> </div> </div> - <Badge - className={styles.timeBadge} - count={formatDate(record.momentTime || record.likeTime)} - style={{ - background: "#e8f0fe", - color: "#333", - fontWeight: 400, - }} - /> </div> <Divider className={styles.cardSep} /> <div className={styles.cardContent}> @@ -273,28 +267,6 @@ export default function AutoLikeRecord() { </div> )} </div> - <div className={styles.operatorWrap}> - <Avatar - src={record.operatorAvatar || undefined} - icon={<UserOutlined />} - size={32} - className={styles.operatorAvatar} - /> - <div className={styles.operatorInfo}> - <span - className={styles.operatorName} - title={record.operatorName} - > - {record.operatorName} - </span> - <span className={styles.operatorAction}> - <LikeOutlined - style={{ color: "red", marginRight: 4 }} - /> - 已赞 - </span> - </div> - </div> </div> ))} </> diff --git a/Cunkebao/src/pages/mobile/workspace/moments-sync/record/record.module.scss b/Cunkebao/src/pages/mobile/workspace/moments-sync/record/record.module.scss index b63be1f0..22172d79 100644 --- a/Cunkebao/src/pages/mobile/workspace/moments-sync/record/record.module.scss +++ b/Cunkebao/src/pages/mobile/workspace/moments-sync/record/record.module.scss @@ -160,7 +160,6 @@ display: flex; align-items: center; gap: 12px; - max-width: 65%; } .avatarImg { width: 40px; From 6c8becb2013e20cce8731180adbdbb3aeb51705e 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?= <fsmecx@gmail.com> Date: Mon, 8 Sep 2025 17:25:38 +0800 Subject: [PATCH 10/10] =?UTF-8?q?refactor(workspace):=20=E5=B0=86=E6=9C=8B?= =?UTF-8?q?=E5=8F=8B=E5=9C=88=E5=90=8C=E6=AD=A5=E4=BB=BB=E5=8A=A1=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E9=A1=B5=E7=A7=BB=E5=8A=A8=E5=88=B0=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E4=B8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构朋友圈同步任务详情页的文件位置,从独立目录移动到列表目录下,保持相关功能文件的组织一致性 --- .../mobile/workspace/moments-sync/{ => list}/Detail.tsx | 0 Cunkebao/src/router/module/workspace.tsx | 7 +------ 2 files changed, 1 insertion(+), 6 deletions(-) rename Cunkebao/src/pages/mobile/workspace/moments-sync/{ => list}/Detail.tsx (100%) diff --git a/Cunkebao/src/pages/mobile/workspace/moments-sync/Detail.tsx b/Cunkebao/src/pages/mobile/workspace/moments-sync/list/Detail.tsx similarity index 100% rename from Cunkebao/src/pages/mobile/workspace/moments-sync/Detail.tsx rename to Cunkebao/src/pages/mobile/workspace/moments-sync/list/Detail.tsx diff --git a/Cunkebao/src/router/module/workspace.tsx b/Cunkebao/src/router/module/workspace.tsx index c4b365e6..79878bf3 100644 --- a/Cunkebao/src/router/module/workspace.tsx +++ b/Cunkebao/src/router/module/workspace.tsx @@ -10,7 +10,6 @@ import FormGroupPush from "@/pages/mobile/workspace/group-push/form"; import DetailGroupPush from "@/pages/mobile/workspace/group-push/detail"; import MomentsSync from "@/pages/mobile/workspace/moments-sync/list"; import NewMomentsSync from "@/pages/mobile/workspace/moments-sync/new/index"; -import MomentsSyncDetail from "@/pages/mobile/workspace/moments-sync/Detail"; import MomentsSyncRecord from "@/pages/mobile/workspace/moments-sync/record"; import AIAssistant from "@/pages/mobile/workspace/ai-assistant/AIAssistant"; import TrafficDistribution from "@/pages/mobile/workspace/traffic-distribution/list/index"; @@ -99,11 +98,7 @@ const workspaceRoutes = [ element: <NewMomentsSync />, auth: true, }, - { - path: "/workspace/moments-sync/:id", - element: <MomentsSyncDetail />, - auth: true, - }, + { path: "/workspace/moments-sync/record/:id", element: <MomentsSyncRecord />,