mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
feat(ChatPage): 新增链接卡片消息渲染(支持解析 XML 并展示标题 / 描述 / 图标),采用 flexbox 优化消息气泡布局,添加文本截断、响应式样式及悬浮效果。
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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('<appmsg') || message.rawContent.includes('<appmsg'))) ||
|
||||
(message.parsedContent && (message.parsedContent.includes('<appmsg') || message.parsedContent.includes('<appmsg')))
|
||||
const bubbleClass = isSent ? 'sent' : 'received'
|
||||
|
||||
// 头像逻辑:
|
||||
@@ -1878,6 +1882,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
|
||||
? '我'
|
||||
: getAvatarLetter(isGroupChat ? (senderName || message.senderUsername || '?') : (session.displayName || session.username))
|
||||
|
||||
|
||||
// 是否有引用消息
|
||||
const hasQuote = message.quotedContent && message.quotedContent.length > 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
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 解析引用消息(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 (
|
||||
<div
|
||||
className="link-message"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (url) {
|
||||
// 优先使用 electron 接口打开外部浏览器
|
||||
if (window.electronAPI?.shell?.openExternal) {
|
||||
window.electronAPI.shell.openExternal(url);
|
||||
} else {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="link-header">
|
||||
<div className="link-content">
|
||||
<div className="link-title" title={title}>{title}</div>
|
||||
<div className="link-desc" title={des}>{des}</div>
|
||||
</div>
|
||||
<div className="link-icon">
|
||||
<Link size={24} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse app message', e);
|
||||
}
|
||||
}
|
||||
// 普通消息
|
||||
return <div className="bubble-content">{renderTextWithEmoji(cleanMessageContent(message.parsedContent))}</div>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user