diff --git a/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/index.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/index.tsx index 58d7042..93251c4 100644 --- a/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/index.tsx +++ b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/index.tsx @@ -22,6 +22,7 @@ import { PlayCircleOutlined, SearchOutlined, LinkOutlined, + QuestionCircleOutlined, } from "@ant-design/icons"; import { QuickWordsItem, @@ -76,6 +77,8 @@ const QuickWords: React.FC = ({ onInsert }) => { const [addModalVisible, setAddModalVisible] = useState(false); const [editModalVisible, setEditModalVisible] = useState(false); const [groupModalVisible, setGroupModalVisible] = useState(false); + const [previewModalVisible, setPreviewModalVisible] = useState(false); + const [previewReply, setPreviewReply] = useState(null); const [editingItem, setEditingItem] = useState(null); const [editingGroup, setEditingGroup] = useState(null); const [isAddingGroup, setIsAddingGroup] = useState(false); @@ -147,6 +150,11 @@ const QuickWords: React.FC = ({ onInsert }) => { }; const previewAndConfirmSend = (reply: QuickWordsReply) => { + setPreviewReply(reply); + setPreviewModalVisible(true); + }; + + const renderPreviewContent = (reply: QuickWordsReply) => { let previewNode: React.ReactNode = null; if (reply.msgType === MessageType.IMAGE) { previewNode = ( @@ -198,7 +206,6 @@ const QuickWords: React.FC = ({ onInsert }) => {
= ({ onInsert }) => { lineHeight: 1.4 }} > - {reply.title} + 标题: {reply.title}
{/* 2. 封面图 */} @@ -282,7 +289,7 @@ const QuickWords: React.FC = ({ onInsert }) => { textOverflow: "ellipsis" }} > - {linkData.desc} + 描述: {linkData.desc} )} @@ -310,16 +317,16 @@ const QuickWords: React.FC = ({ onInsert }) => { } } - Modal.confirm({ - title: "确认发送该快捷语?", - content: previewNode, - okText: "发送", - cancelText: "取消", - onOk: () => { - sendQuickReplyNow(reply); - message.success("已发送"); - }, - }); + return previewNode; + }; + + const handleConfirmSend = () => { + if (previewReply) { + sendQuickReplyNow(previewReply); + message.success("已发送"); + setPreviewModalVisible(false); + setPreviewReply(null); + } }; // 获取快捷语数据 @@ -765,6 +772,28 @@ const QuickWords: React.FC = ({ onInsert }) => { setIsAddingGroup(false); }} /> + + {/* 预览确认发送模态窗 */} + + + 确认发送该快捷语? + + } + open={previewModalVisible} + onOk={handleConfirmSend} + onCancel={() => { + setPreviewModalVisible(false); + setPreviewReply(null); + }} + okText="发送" + cancelText="取消" + width={520} + centered + > + {previewReply && renderPreviewContent(previewReply)} + ); }; diff --git a/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx b/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx index 5700111..d037009 100644 --- a/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx +++ b/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx @@ -26,7 +26,7 @@ import { useUserStore } from "@storeModule/user"; import { MessageManager } from "@/utils/dbAction/message"; import { ContactManager } from "@/utils/dbAction/contact"; import { formatWechatTime } from "@/utils/common"; -import { messageFilter } from "@/utils/filter"; +import { formatMessagePreview } from "@/utils/messagePreview"; import { ChatSession } from "@/utils/db"; import { VirtualSessionList } from "@/components/VirtualSessionList"; interface MessageListProps {} @@ -68,7 +68,7 @@ const SessionItem: React.FC = React.memo(
- {messageFilter(session.content)} + {formatMessagePreview(session.content)}
diff --git a/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.virtual.tsx b/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.virtual.tsx index 36c94f9..4a6b32e 100644 --- a/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.virtual.tsx +++ b/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.virtual.tsx @@ -24,7 +24,7 @@ import { useWeChatStore } from "@weChatStore/weChat"; import { useUserStore } from "@storeModule/user"; import { useContactStore } from "@weChatStore/contacts"; import { formatWechatTime } from "@/utils/common"; -import { messageFilter } from "@/utils/filter"; +import { formatMessagePreview } from "@/utils/messagePreview"; import { UserOutlined, TeamOutlined } from "@ant-design/icons"; import { Avatar, Badge } from "antd"; @@ -61,7 +61,7 @@ const SessionItem: React.FC<{
- {messageFilter(session.content)} + {formatMessagePreview(session.content)}
diff --git a/src/utils/messagePreview.ts b/src/utils/messagePreview.ts new file mode 100644 index 0000000..627d65d --- /dev/null +++ b/src/utils/messagePreview.ts @@ -0,0 +1,250 @@ +/** + * 消息预览格式化工具 + * 用于会话列表中显示消息预览,参考 会话列表预览消息规则.md + */ + +// 图片扩展名正则 +const IMAGE_EXT_REGEX = /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i; + +// 视频扩展名正则 +const VIDEO_EXT_REGEX = /\.(mp4|avi|mov|wmv|flv|mkv|webm|m4v)$/i; + +// 音频扩展名正则 +const AUDIO_EXT_REGEX = /\.(mp3|wav|wma|flac|aac|ogg|m4a)$/i; + +// 阿里云 OSS 前缀 +const ALIYUN_OSS_PREFIX = 'https://ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com'; + +/** + * 尝试解析 JSON + */ +const tryParseJson = (content: string): Record | null => { + try { + return JSON.parse(content); + } catch { + return null; + } +}; + +/** + * 从 XML 字符串中提取 title + */ +const extractTitleFromXml = (xmlString: string): string | null => { + try { + // 尝试提取 标签内容 + const titleMatch = xmlString.match( + /<title>([^<]*(?:<!\[CDATA\[[^\]]*\]\]>[^<]*)*)<\/title>/i + ); + if (titleMatch && titleMatch[1]) { + let title = titleMatch[1]; + // 处理 CDATA + title = title.replace(/<!\[CDATA\[(.*?)\]\]>/gi, '$1'); + // 去除首尾空白 + title = title.trim(); + if (title) { + return title; + } + } + } catch { + // 解析失败,返回 null + } + return null; +}; + +/** + * 检查是否为阿里云 OSS 链接,并判断类型 + */ +const checkAliyunOssLink = (url: string): '图片' | '视频' | '音频' | null => { + if (!url.includes(ALIYUN_OSS_PREFIX)) { + return null; + } + + // 根据文件扩展名判断类型 + if (IMAGE_EXT_REGEX.test(url)) { + return '图片'; + } + if (VIDEO_EXT_REGEX.test(url)) { + return '视频'; + } + if (AUDIO_EXT_REGEX.test(url)) { + return '音频'; + } + + return null; +}; + +/** + * 检查是否为小程序消息 + */ +const isMiniProgramMessage = (jsonData: Record<string, any>): boolean => { + // 检查是否有 contentXml 且包含 appid + if (jsonData.contentXml && typeof jsonData.contentXml === 'string') { + const xmlContent = jsonData.contentXml; + // 检查是否包含 <appmsg appid= 或 <appid> + if (xmlContent.includes('<appmsg') && xmlContent.includes('appid')) { + return true; + } + } + + // 检查是否有 type: "miniprogram" + if (jsonData.type === 'miniprogram') { + return true; + } + + // 检查是否有 weappinfo 对象 + if (jsonData.weappinfo || jsonData.weappInfo) { + return true; + } + + return false; +}; + +/** + * 格式化消息预览内容 + * @param content 原始消息内容 + * @returns 格式化后的预览文本 + */ +export function formatMessagePreview( + content: string | null | undefined +): string { + // 处理空值 + if (!content || typeof content !== 'string') { + return '暂无消息'; + } + + const trimmed = content.trim(); + + if (!trimmed) { + return '暂无消息'; + } + + // 1. 检查是否为阿里云 OSS 链接(纯链接字符串) + const aliyunOssType = checkAliyunOssLink(trimmed); + if (aliyunOssType) { + return `[${aliyunOssType}]`; + } + + // 2. 尝试解析 JSON + const jsonData = tryParseJson(trimmed); + + if (jsonData && typeof jsonData === 'object') { + // 2.1 检查是否为小程序消息 + if (isMiniProgramMessage(jsonData)) { + return '[小程序消息]'; + } + + // 2.2 检查 JSON 中是否有阿里云 OSS 链接 + // 遍历 JSON 对象的所有值,查找链接 + const findAliyunOssLink = (obj: any): string | null => { + if (typeof obj === 'string' && obj.includes(ALIYUN_OSS_PREFIX)) { + return obj; + } + if (typeof obj === 'object' && obj !== null) { + for (const value of Object.values(obj)) { + const link = findAliyunOssLink(value); + if (link) { + return link; + } + } + } + return null; + }; + + const ossLink = findAliyunOssLink(jsonData); + if (ossLink) { + const ossType = checkAliyunOssLink(ossLink); + if (ossType) { + return `[${ossType}]`; + } + } + + // 2.3 尝试从 contentXml 中提取 title + if (jsonData.contentXml && typeof jsonData.contentXml === 'string') { + const title = extractTitleFromXml(jsonData.contentXml); + if (title) { + // 限制长度 + const maxLength = 50; + return title.length > maxLength + ? title.substring(0, maxLength) + '...' + : title; + } + } + + // 2.4 检查 JSON 是否过长或被截断 + // 如果 JSON 字符串很长(超过 500 字符),可能被截断 + if (trimmed.length > 500) { + // 尝试提取 title + const title = extractTitleFromXml(trimmed); + if (title) { + const maxLength = 50; + return title.length > maxLength + ? title.substring(0, maxLength) + '...' + : title; + } + return '[文本过长]'; + } + + // 2.5 尝试从 JSON 中提取有意义的信息 + if (jsonData.title) { + const title = String(jsonData.title); + const maxLength = 50; + return title.length > maxLength + ? title.substring(0, maxLength) + '...' + : title; + } + + if (jsonData.content) { + const content = String(jsonData.content); + const maxLength = 50; + return content.length > maxLength + ? content.substring(0, maxLength) + '...' + : content; + } + + // 2.6 无法识别的 JSON,返回通用提示 + return '[消息]'; + } + + // 3. 检查是否为普通 HTTP 链接 + if (/^https?:\/\//i.test(trimmed)) { + // 检查是否为图片链接 + if (IMAGE_EXT_REGEX.test(trimmed)) { + return '[图片]'; + } + // 检查是否为视频链接 + if (VIDEO_EXT_REGEX.test(trimmed)) { + return '[视频]'; + } + // 检查是否为音频链接 + if (AUDIO_EXT_REGEX.test(trimmed)) { + return '[音频]'; + } + // 普通链接 + return '[链接]'; + } + + // 4. 检查是否为 XML 字符串(但没有被 JSON 包裹) + if ( + trimmed.includes('<?xml') || + trimmed.includes('<msg>') || + trimmed.includes('<appmsg') + ) { + const title = extractTitleFromXml(trimmed); + if (title) { + const maxLength = 50; + return title.length > maxLength + ? title.substring(0, maxLength) + '...' + : title; + } + return '[文本过长]'; + } + + // 5. 普通文本消息 + // 限制长度,避免过长文本影响显示 + const maxLength = 50; + if (trimmed.length > maxLength) { + return trimmed.substring(0, maxLength) + '...'; + } + + return trimmed; +}