diff --git a/src/pages/ChatPage.scss b/src/pages/ChatPage.scss index 45aca76..02ead49 100644 --- a/src/pages/ChatPage.scss +++ b/src/pages/ChatPage.scss @@ -370,9 +370,23 @@ } .message-bubble { - max-width: 65%; + display: flex; + gap: 12px; + max-width: 80%; + margin-bottom: 4px; + align-items: flex-start; + + .bubble-body { + display: flex; + flex-direction: column; + max-width: 100%; + min-width: 0; // 允许收缩 + width: fit-content; // 让气泡宽度由内容决定 + } &.sent { + flex-direction: row-reverse; + .bubble-content { background: var(--primary-gradient); color: #fff; @@ -436,6 +450,11 @@ font-size: 12px; color: var(--text-secondary); margin-bottom: 4px; + // 防止名字撑开气泡宽度 + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .quoted-message { @@ -798,6 +817,99 @@ } // 右侧消息区域 +// ... (previous content) ... + +// 链接卡片消息样式 +.link-message { + cursor: pointer; + background: var(--card-bg); + border-radius: 8px; + overflow: hidden; + border: 1px solid var(--border-color); + transition: all 0.2s ease; + max-width: 300px; + margin-top: 4px; + + &:hover { + background: var(--bg-hover); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + } + + .link-header { + display: flex; + align-items: flex-start; + padding: 12px; + gap: 12px; + } + + .link-content { + flex: 1; + min-width: 0; + } + + .link-title { + font-size: 14px; + font-weight: 500; + color: var(--text-primary); + margin-bottom: 4px; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + line-height: 1.4; + } + + .link-desc { + font-size: 12px; + color: var(--text-secondary); + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + line-height: 1.4; + opacity: 0.8; + } + + .link-icon { + flex-shrink: 0; + width: 40px; + height: 40px; + background: var(--bg-tertiary); + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-secondary); + + svg { + opacity: 0.8; + } + } +} + +// 适配发送出去的消息中的链接卡片 +.message-bubble.sent .link-message { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.2); + + .link-title, + .link-desc { + color: #fff; + } + + .link-icon { + background: rgba(255, 255, 255, 0.2); + color: #fff; + } + + &:hover { + background: rgba(255, 255, 255, 0.2); + } +} + .message-area { flex: 1 1 70%; display: flex; @@ -1493,6 +1605,11 @@ font-size: 12px; color: var(--text-tertiary); margin-bottom: 4px; + // 防止名字撑开气泡宽度 + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } // 引用消息样式 @@ -1541,7 +1658,11 @@ display: flex; flex-direction: column; max-width: 100%; + min-width: 0; // 允许收缩 -webkit-app-region: no-drag; + + // 让气泡宽度由内容决定,而不是被父容器撑开 + width: fit-content; } .bubble-content { @@ -1956,4 +2077,92 @@ width: 14px; height: 14px; } +} + +/* 链接消息样式 (Link Card/App Message) */ +.link-message { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 12px; + width: 280px; + cursor: pointer; + transition: all 0.2s ease; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border-color: var(--primary-light); + } + + .link-header { + display: flex; + gap: 12px; + align-items: flex-start; + } + + .link-content { + flex: 1; + min-width: 0; + } + + .link-title { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 4px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + line-clamp: 2; + overflow: hidden; + line-height: 1.4; + } + + .link-desc { + font-size: 12px; + color: var(--text-secondary); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + line-clamp: 2; + overflow: hidden; + line-height: 1.4; + } + + .link-icon { + width: 40px; + height: 40px; + background: var(--bg-tertiary); + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-tertiary); + flex-shrink: 0; + + svg { + opacity: 0.6; + } + } +} + +/* 适配发送方的背景,使其从气泡颜色中脱离出来,看起来像个独立的卡片 */ +.message-bubble.sent .link-message { + background: #fff; + border: 1px solid rgba(0, 0, 0, 0.1); + + .link-title { + color: #333; + } + + .link-desc { + color: #666; + } + + .link-icon { + background: #f5f5f5; + } } \ No newline at end of file diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 1e11914..379bc0f 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react' -import { Search, MessageSquare, AlertCircle, Loader2, RefreshCw, X, ChevronDown, Info, Calendar, Database, Hash, Play, Pause, Image as ImageIcon } from 'lucide-react' +import { Search, MessageSquare, AlertCircle, Loader2, RefreshCw, X, ChevronDown, Info, Calendar, Database, Hash, Play, Pause, Image as ImageIcon, Link } from 'lucide-react' import { createPortal } from 'react-dom' import { useChatStore } from '../stores/chatStore' import type { ChatSession, Message } from '../types/models' @@ -1865,6 +1865,10 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o ) } + // 检测是否为链接卡片消息 + const isLinkMessage = String(message.localType) === '21474836529' || + (message.rawContent && (message.rawContent.includes(' 0 @@ -2166,6 +2171,10 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o /> ) } + + // 解析引用消息(Links / App Messages) + // localType: 21474836529 corresponds to AppMessage which often contains links + // 带引用的消息 if (hasQuote) { return ( @@ -2178,6 +2187,68 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o ) } + + // 解析引用消息(Links / App Messages) + // localType: 21474836529 corresponds to AppMessage which often contains links + if (isLinkMessage) { + try { + // 清理内容:移除可能的 wxid 前缀,找到 XML 起始位置 + let contentToParse = message.rawContent || message.parsedContent || ''; + const xmlStartIndex = contentToParse.indexOf('<'); + if (xmlStartIndex >= 0) { + contentToParse = contentToParse.substring(xmlStartIndex); + } + + // 处理 HTML 转义字符 + if (contentToParse.includes('<')) { + contentToParse = contentToParse + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, "'"); + } + + const parser = new DOMParser(); + const doc = parser.parseFromString(contentToParse, "text/xml"); + const appMsg = doc.querySelector('appmsg'); + + if (appMsg) { + const title = doc.querySelector('title')?.textContent || '未命名链接'; + const des = doc.querySelector('des')?.textContent || '无描述'; + const url = doc.querySelector('url')?.textContent || ''; + + return ( +
{ + e.stopPropagation(); + if (url) { + // 优先使用 electron 接口打开外部浏览器 + if (window.electronAPI?.shell?.openExternal) { + window.electronAPI.shell.openExternal(url); + } else { + window.open(url, '_blank'); + } + } + }} + > +
+
+
{title}
+
{des}
+
+
+ +
+
+
+ ); + } + } catch (e) { + console.error('Failed to parse app message', e); + } + } // 普通消息 return
{renderTextWithEmoji(cleanMessageContent(message.parsedContent))}
} diff --git a/src/types/models.ts b/src/types/models.ts index 09f6e41..f557caa 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -33,6 +33,7 @@ export interface Message { isSend: number | null senderUsername: string | null parsedContent: string + rawContent?: string imageMd5?: string imageDatName?: string emojiCdnUrl?: string