diff --git a/pages/Chat/Chat.vue b/pages/Chat/Chat.vue index 38f9f55..06fdf82 100644 --- a/pages/Chat/Chat.vue +++ b/pages/Chat/Chat.vue @@ -467,6 +467,35 @@ import { openFile as openFileNative, downloadAndOpen } from '@/utils/fileOpener. return text } + // 判断链接是否为可下载文件 + const isDownloadLink = (url) => { + const supportedExtensions = ['html', 'htm', 'xlsx', 'xls', 'pdf', 'zip', 'rar', '7z', 'md', 'markdown', + 'docx', 'doc', 'pptx', 'ppt', 'txt', 'csv', 'json', 'xml', 'yaml', 'yml', + 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'ico', 'tiff', 'tif', 'dxf' + ] + const extPattern = supportedExtensions.join('|') + const extRegex = new RegExp(`\\.(${extPattern})(?:\\?|$)`, 'i') + return extRegex.test(url) + } + + // 渲染下载按钮 HTML + const renderDownloadBtn = (href, displayName) => { + const ext = ((href.split('.').pop() || '').split('?')[0] || '').toLowerCase() + const fileInfo = getFileTypeInfo(ext) + const name = displayName || extractFileNameFromUrl(href) + return '' + + '' + fileInfo.emoji + '' + + '' + name + '' + + '' + } + + // 支持的文件扩展名(用于原始 URL 检测) + const supportedExtensions = ['html', 'htm', 'xlsx', 'xls', 'pdf', 'zip', 'rar', '7z', 'md', 'markdown', + 'docx', 'doc', 'pptx', 'ppt', 'txt', 'csv', 'json', 'xml', 'yaml', 'yml', + 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'ico', 'tiff', 'tif', 'dxf' + ] + const extPattern = supportedExtensions.join('|') + // Markdown转HTML节点 const pareseMarkdown = (content) => { if (!content) return '' @@ -476,14 +505,60 @@ import { openFile as openFileNative, downloadAndOpen } from '@/utils/fileOpener. try { // 转义单词中的单下划线,防止 snarkdown 误解析为斜体 content = escapeLoneUnderscores(content) + + // 修复1: URL 中的中文括号编码,防止 snarkdown 截断URL + // [text](http://xxx/高二(1)班.xlsx) → [text](http://xxx/高二%281%29班.xlsx) + content = content.replace( + /\[([^\]]+)\]\((https?:\/\/[^\s]+)\)/g, + (m, name, url) => '[' + name + '](' + url.replace(/\(/g, '%28').replace(/\)/g, '%29') + ')' + ) + + // 修复2: 误用括号 [高二](1)班成绩表 → 高二(1)班成绩表 + // 当 (数字/短文本) 后紧跟中文时,说明不是真实URL,还原为纯文本 + content = content.replace( + /\[([^\]]+)\]\((\d{1,2}|[^\s)]{1,3})\)(?=[\u4e00-\u9fff])/g, + '$1($2)' + ) + // 先转换表格,再用 snarkdown 处理其他 Markdown content = convertMarkdownTable(content) let html = snarkdown(content) - // 将 替换为自定义 span,避免 WebView 触发默认下载 + + // 步骤1:暂存 标签,避免 raw URL 替换误伤 href 内的链接 + const linkPlaceholders = [] html = html.replace( /]*>(.*?)<\/a>/gi, - '$2' + (match, href, innerText) => { + const idx = linkPlaceholders.length + linkPlaceholders.push({ href, innerText }) + return '\x00LINK_' + idx + '\x00' + } ) + + // 步骤2:将正文中的原始文件 URL 替换为下载按钮 + const rawUrlRegex = new RegExp( + `https?://[^\\s<>"'']+\\.(${extPattern})(?:[?#][^\\s<>"'']*)?`, + 'gi' + ) + html = html.replace(rawUrlRegex, (url) => { + return renderDownloadBtn(url) + }) + + // 步骤3:恢复 标签并分类处理 + html = html.replace(/\x00LINK_(\d+)\x00/g, (_, idx) => { + const { href, innerText } = linkPlaceholders[idx] + if (isDownloadLink(href)) { + const plainText = innerText + .replace(/<[^>]*>/g, ' ') + .replace(/ /gi, ' ') + .replace(/\s+/g, ' ') + .trim() + return renderDownloadBtn(href, plainText) + } + // 普通链接保持原有样式 + return '' + innerText + '' + }) + return html } catch (e) { console.error('解析失败', e) @@ -839,6 +914,50 @@ import { openFile as openFileNative, downloadAndOpen } from '@/utils/fileOpener. } return urlStr } + + // 文件类型 → 图标和标签映射 + const getFileTypeInfo = (ext) => { + const map = { + // 文档类 + pdf: { emoji: '📕', label: 'PDF', color: '#e74c3c' }, + doc: { emoji: '📘', label: 'Word', color: '#2b579a' }, + docx: { emoji: '📘', label: 'Word', color: '#2b579a' }, + xls: { emoji: '📗', label: 'Excel', color: '#217346' }, + xlsx: { emoji: '📗', label: 'Excel', color: '#217346' }, + ppt: { emoji: '📙', label: 'PPT', color: '#d24726' }, + pptx: { emoji: '📙', label: 'PPT', color: '#d24726' }, + txt: { emoji: '📄', label: '文本', color: '#666' }, + md: { emoji: '📝', label: 'Markdown', color: '#333' }, + markdown: { emoji: '📝', label: 'Markdown', color: '#333' }, + csv: { emoji: '📊', label: 'CSV', color: '#217346' }, + json: { emoji: '📋', label: 'JSON', color: '#f0a500' }, + xml: { emoji: '📋', label: 'XML', color: '#e67e22' }, + yaml: { emoji: '📋', label: 'YAML', color: '#e67e22' }, + yml: { emoji: '📋', label: 'YAML', color: '#e67e22' }, + // 压缩包 + zip: { emoji: '📦', label: 'ZIP', color: '#f39c12' }, + rar: { emoji: '📦', label: 'RAR', color: '#f39c12' }, + '7z': { emoji: '📦', label: '7Z', color: '#f39c12' }, + // 网页 + html: { emoji: '🌐', label: '网页', color: '#e44d26' }, + htm: { emoji: '🌐', label: '网页', color: '#e44d26' }, + // 图片 + jpg: { emoji: '🖼️', label: '图片', color: '#9b59b6' }, + jpeg: { emoji: '🖼️', label: '图片', color: '#9b59b6' }, + png: { emoji: '🖼️', label: '图片', color: '#9b59b6' }, + gif: { emoji: '🖼️', label: 'GIF', color: '#9b59b6' }, + bmp: { emoji: '🖼️', label: '图片', color: '#9b59b6' }, + svg: { emoji: '🖼️', label: 'SVG', color: '#9b59b6' }, + webp: { emoji: '🖼️', label: '图片', color: '#9b59b6' }, + ico: { emoji: '🖼️', label: '图标', color: '#9b59b6' }, + tiff: { emoji: '🖼️', label: '图片', color: '#9b59b6' }, + tif: { emoji: '🖼️', label: '图片', color: '#9b59b6' }, + // CAD/设计类 + dxf: { emoji: '📐', label: 'CAD/DXF', color: '#2c3e50' }, + } + return map[ext] || { emoji: '📎', label: ext.toUpperCase(), color: '#888' } + } + const showMessageDetail = (message) => { detailLinks.value = extractLinks(message.content || '') showMessageModal.value = true @@ -1381,7 +1500,8 @@ import { openFile as openFileNative, downloadAndOpen } from '@/utils/fileOpener. const takeConversationMessages = async () => { try { allmessages.value = await getConversationMessages(userToken.value, currentSessionId.value) || []; - // console.log("获取到的所有消息:", JSON.stringify(allmessages.value) ); + console.log("获取到的所有消息:", JSON.stringify(allmessages.value) ); + console.log("获取到的所有消息:", allmessages.value); // currentMessages.value = allmessages.value.slice(-pageInfoNumber); // 数据获取后执行滚动 scrollToBottom(); @@ -1654,6 +1774,43 @@ import { openFile as openFileNative, downloadAndOpen } from '@/utils/fileOpener. text-decoration: underline; word-break: break-all; } + + /* 可下载文件链接 → 按钮卡片样式 */ + .chat-download-btn { + display: inline-flex !important; + align-items: center; + gap: 8rpx; + padding: 8rpx 14rpx; + margin: 4rpx 0; + background: #f5f7fb; + border: 2rpx solid #e4e8f0; + border-radius: 8rpx; + text-decoration: none !important; + color: #333 !important; + word-break: break-all; + transition: all 0.15s ease; + + &:active { + background: #eef1f7; + border-color: #c8cde0; + transform: scale(0.98); + } + + .chat-dl-icon { + font-size: 26rpx !important; + line-height: 1; + flex-shrink: 0; + } + + .chat-dl-name { + font-size: 16px !important; + font-weight: 500; + color: #333; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } } /* 流式气泡中的加载动画 */ diff --git a/unpackage/dist/dev/app-plus/app-service.js b/unpackage/dist/dev/app-plus/app-service.js index ca45452..d87f692 100644 --- a/unpackage/dist/dev/app-plus/app-service.js +++ b/unpackage/dist/dev/app-plus/app-service.js @@ -5800,21 +5800,127 @@ This will fail in production.`); text = text.replace(/\x00\x00/g, "__"); return text; }; + const isDownloadLink = (url) => { + const supportedExtensions2 = [ + "html", + "htm", + "xlsx", + "xls", + "pdf", + "zip", + "rar", + "7z", + "md", + "markdown", + "docx", + "doc", + "pptx", + "ppt", + "txt", + "csv", + "json", + "xml", + "yaml", + "yml", + "jpg", + "jpeg", + "png", + "gif", + "bmp", + "svg", + "webp", + "ico", + "tiff", + "tif", + "dxf" + ]; + const extPattern2 = supportedExtensions2.join("|"); + const extRegex = new RegExp(`\\.(${extPattern2})(?:\\?|$)`, "i"); + return extRegex.test(url); + }; + const renderDownloadBtn = (href, displayName) => { + const ext = ((href.split(".").pop() || "").split("?")[0] || "").toLowerCase(); + const fileInfo = getFileTypeInfo(ext); + const name2 = displayName || extractFileNameFromUrl(href); + return '' + fileInfo.emoji + '' + name2 + ""; + }; + const supportedExtensions = [ + "html", + "htm", + "xlsx", + "xls", + "pdf", + "zip", + "rar", + "7z", + "md", + "markdown", + "docx", + "doc", + "pptx", + "ppt", + "txt", + "csv", + "json", + "xml", + "yaml", + "yml", + "jpg", + "jpeg", + "png", + "gif", + "bmp", + "svg", + "webp", + "ico", + "tiff", + "tif", + "dxf" + ]; + const extPattern = supportedExtensions.join("|"); const pareseMarkdown = (content) => { if (!content) return ""; content = sanitizeContent(content); try { content = escapeLoneUnderscores(content); + content = content.replace( + /\[([^\]]+)\]\((https?:\/\/[^\s]+)\)/g, + (m, name2, url) => "[" + name2 + "](" + url.replace(/\(/g, "%28").replace(/\)/g, "%29") + ")" + ); + content = content.replace( + /\[([^\]]+)\]\((\d{1,2}|[^\s)]{1,3})\)(?=[\u4e00-\u9fff])/g, + "$1($2)" + ); content = convertMarkdownTable(content); let html = t(content); + const linkPlaceholders = []; html = html.replace( /]*>(.*?)<\/a>/gi, - '$2' + (match, href, innerText) => { + const idx = linkPlaceholders.length; + linkPlaceholders.push({ href, innerText }); + return "\0LINK_" + idx + "\0"; + } ); + const rawUrlRegex = new RegExp( + `https?://[^\\s<>"'']+\\.(${extPattern})(?:[?#][^\\s<>"'']*)?`, + "gi" + ); + html = html.replace(rawUrlRegex, (url) => { + return renderDownloadBtn(url); + }); + html = html.replace(/\x00LINK_(\d+)\x00/g, (_, idx) => { + const { href, innerText } = linkPlaceholders[idx]; + if (isDownloadLink(href)) { + const plainText = innerText.replace(/<[^>]*>/g, " ").replace(/ /gi, " ").replace(/\s+/g, " ").trim(); + return renderDownloadBtn(href, plainText); + } + return '' + innerText + ""; + }); return html; } catch (e2) { - formatAppLog("error", "at pages/Chat/Chat.vue:489", "解析失败", e2); + formatAppLog("error", "at pages/Chat/Chat.vue:564", "解析失败", e2); return content; } }; @@ -5865,7 +5971,7 @@ This will fail in production.`); files }; } catch (e2) { - formatAppLog("error", "at pages/Chat/Chat.vue:558", "解析文件信息失败:", e2); + formatAppLog("error", "at pages/Chat/Chat.vue:633", "解析文件信息失败:", e2); return { textContent: content, files: [] @@ -5903,7 +6009,7 @@ This will fail in production.`); closeNewChatModal(); takeUserConversations(); } catch (error) { - formatAppLog("error", "at pages/Chat/Chat.vue:604", "新建普通会话失败:", error); + formatAppLog("error", "at pages/Chat/Chat.vue:679", "新建普通会话失败:", error); uni.showToast({ title: "创建会话失败,请重试", icon: "none" @@ -5946,7 +6052,7 @@ This will fail in production.`); }; const previewFileArray = vue.ref([]); const uploadPhoto = () => { - formatAppLog("log", "at pages/Chat/Chat.vue:664", "点击了拍照上传"); + formatAppLog("log", "at pages/Chat/Chat.vue:739", "点击了拍照上传"); uni.chooseImage({ count: 1, sourceType: ["camera", "album"], @@ -5969,7 +6075,7 @@ This will fail in production.`); }); }, fail: (err) => { - formatAppLog("error", "at pages/Chat/Chat.vue:682", "选择图片失败", err); + formatAppLog("error", "at pages/Chat/Chat.vue:757", "选择图片失败", err); } }); }; @@ -5978,12 +6084,12 @@ This will fail in production.`); }; const fileList = vue.ref([]); const uploadFile = () => { - formatAppLog("log", "at pages/Chat/Chat.vue:693", "点击了上传文件"); + formatAppLog("log", "at pages/Chat/Chat.vue:768", "点击了上传文件"); chooseFile({ count: 5, type: "all", success: (res) => { - formatAppLog("log", "at pages/Chat/Chat.vue:698", "成功了"); + formatAppLog("log", "at pages/Chat/Chat.vue:773", "成功了"); fileList.value = res.tempFiles; previewFileArray.value.push(...res.tempFiles); uni.showToast({ @@ -5992,7 +6098,7 @@ This will fail in production.`); }); }, fail: (err) => { - formatAppLog("error", "at pages/Chat/Chat.vue:709", "选择失败:", err); + formatAppLog("error", "at pages/Chat/Chat.vue:784", "选择失败:", err); uni.showToast({ title: "选择失败", icon: "error" @@ -6096,6 +6202,47 @@ This will fail in production.`); } return urlStr; }; + const getFileTypeInfo = (ext) => { + const map = { + // 文档类 + pdf: { emoji: "📕", label: "PDF", color: "#e74c3c" }, + doc: { emoji: "📘", label: "Word", color: "#2b579a" }, + docx: { emoji: "📘", label: "Word", color: "#2b579a" }, + xls: { emoji: "📗", label: "Excel", color: "#217346" }, + xlsx: { emoji: "📗", label: "Excel", color: "#217346" }, + ppt: { emoji: "📙", label: "PPT", color: "#d24726" }, + pptx: { emoji: "📙", label: "PPT", color: "#d24726" }, + txt: { emoji: "📄", label: "文本", color: "#666" }, + md: { emoji: "📝", label: "Markdown", color: "#333" }, + markdown: { emoji: "📝", label: "Markdown", color: "#333" }, + csv: { emoji: "📊", label: "CSV", color: "#217346" }, + json: { emoji: "📋", label: "JSON", color: "#f0a500" }, + xml: { emoji: "📋", label: "XML", color: "#e67e22" }, + yaml: { emoji: "📋", label: "YAML", color: "#e67e22" }, + yml: { emoji: "📋", label: "YAML", color: "#e67e22" }, + // 压缩包 + zip: { emoji: "📦", label: "ZIP", color: "#f39c12" }, + rar: { emoji: "📦", label: "RAR", color: "#f39c12" }, + "7z": { emoji: "📦", label: "7Z", color: "#f39c12" }, + // 网页 + html: { emoji: "🌐", label: "网页", color: "#e44d26" }, + htm: { emoji: "🌐", label: "网页", color: "#e44d26" }, + // 图片 + jpg: { emoji: "🖼️", label: "图片", color: "#9b59b6" }, + jpeg: { emoji: "🖼️", label: "图片", color: "#9b59b6" }, + png: { emoji: "🖼️", label: "图片", color: "#9b59b6" }, + gif: { emoji: "🖼️", label: "GIF", color: "#9b59b6" }, + bmp: { emoji: "🖼️", label: "图片", color: "#9b59b6" }, + svg: { emoji: "🖼️", label: "SVG", color: "#9b59b6" }, + webp: { emoji: "🖼️", label: "图片", color: "#9b59b6" }, + ico: { emoji: "🖼️", label: "图标", color: "#9b59b6" }, + tiff: { emoji: "🖼️", label: "图片", color: "#9b59b6" }, + tif: { emoji: "🖼️", label: "图片", color: "#9b59b6" }, + // CAD/设计类 + dxf: { emoji: "📐", label: "CAD/DXF", color: "#2c3e50" } + }; + return map[ext] || { emoji: "📎", label: ext.toUpperCase(), color: "#888" }; + }; const showMessageDetail = (message) => { detailLinks.value = extractLinks(message.content || ""); showMessageModal.value = true; @@ -6190,7 +6337,7 @@ This will fail in production.`); previewFileArray.value = []; } catch (err) { uni.hideLoading(); - formatAppLog("error", "at pages/Chat/Chat.vue:940", "文件上传失败:", err); + formatAppLog("error", "at pages/Chat/Chat.vue:1059", "文件上传失败:", err); uni.showToast({ title: "文件上传失败,请重试", icon: "error" @@ -6324,15 +6471,15 @@ This will fail in production.`); } }; const stopConversation = async () => { - formatAppLog("log", "at pages/Chat/Chat.vue:1110", "中断对话 - 当前会话ID:", currentSessionId.value); + formatAppLog("log", "at pages/Chat/Chat.vue:1229", "中断对话 - 当前会话ID:", currentSessionId.value); try { await socketStore.send({ type: "stop", conversation_id: currentSessionId.value }); - formatAppLog("log", "at pages/Chat/Chat.vue:1116", "中断指令已发送成功"); + formatAppLog("log", "at pages/Chat/Chat.vue:1235", "中断指令已发送成功"); } catch (err) { - formatAppLog("error", "at pages/Chat/Chat.vue:1118", "中断指令发送失败:", err); + formatAppLog("error", "at pages/Chat/Chat.vue:1237", "中断指令发送失败:", err); } if (streamingMessageId.value) { allmessages.value = allmessages.value.filter((m) => m._id !== streamingMessageId.value); @@ -6350,9 +6497,9 @@ This will fail in production.`); }); }; vue.watch(() => socketStore.isThinking, (newVal, oldVal) => { - formatAppLog("log", "at pages/Chat/Chat.vue:1142", "isThinking 变化:", oldVal, "→", newVal, "messageString:", socketStore.messageString); + formatAppLog("log", "at pages/Chat/Chat.vue:1261", "isThinking 变化:", oldVal, "→", newVal, "messageString:", socketStore.messageString); if (!oldVal && newVal) { - formatAppLog("log", "at pages/Chat/Chat.vue:1145", "AI开始回复(跨设备同步),锁定输入框"); + formatAppLog("log", "at pages/Chat/Chat.vue:1264", "AI开始回复(跨设备同步),锁定输入框"); isThinking.value = true; if (!streamingMessageId.value) { streamingMessageId.value = "streaming-" + Date.now(); @@ -6366,7 +6513,7 @@ This will fail in production.`); return; } if (oldVal && !newVal) { - formatAppLog("log", "at pages/Chat/Chat.vue:1161", "AI回复结束(正常/中断),解锁并刷新消息列表"); + formatAppLog("log", "at pages/Chat/Chat.vue:1280", "AI回复结束(正常/中断),解锁并刷新消息列表"); if (streamingMessageId.value) { allmessages.value = allmessages.value.filter((m) => m._id !== streamingMessageId.value); streamingMessageId.value = null; @@ -6420,15 +6567,15 @@ This will fail in production.`); UserData.value = await getUserInfo(userToken.value); UserId.value = UserData.value._id; UserAvatar.value = UserData.value.avatar || ""; - formatAppLog("log", "at pages/Chat/Chat.vue:1227", "用户信息已加载:", UserId.value, UserAvatar.value); + formatAppLog("log", "at pages/Chat/Chat.vue:1346", "用户信息已加载:", UserId.value, UserAvatar.value); } catch (error) { - formatAppLog("error", "at pages/Chat/Chat.vue:1229", "获取用户信息失败:", error); + formatAppLog("error", "at pages/Chat/Chat.vue:1348", "获取用户信息失败:", error); } }; const takeUserConversations = async () => { try { userToken.value = getToken(); - formatAppLog("log", "at pages/Chat/Chat.vue:1237", "token:", userToken.value); + formatAppLog("log", "at pages/Chat/Chat.vue:1356", "token:", userToken.value); UserConversations.value = await getUserConversations(userToken.value) || []; const savedSessionId = getCurrentSessionId(); if (savedSessionId && UserConversations.value.some((c) => c._id === savedSessionId)) { @@ -6438,7 +6585,7 @@ This will fail in production.`); } else { currentSessionId.value = ""; } - formatAppLog("log", "at pages/Chat/Chat.vue:1249", "保存会话id:", currentSessionId.value); + formatAppLog("log", "at pages/Chat/Chat.vue:1368", "保存会话id:", currentSessionId.value); uni.setStorageSync("currentSessionId", currentSessionId.value); } catch (error) { uni.showToast({ @@ -6450,17 +6597,17 @@ This will fail in production.`); const FriendInfoList = vue.ref([]); const takeFriendList = async () => { try { - formatAppLog("log", "at pages/Chat/Chat.vue:1264", "开始获取好友列表"); + formatAppLog("log", "at pages/Chat/Chat.vue:1383", "开始获取好友列表"); const friendList = await getChatFriend(UserId.value); - formatAppLog("log", "at pages/Chat/Chat.vue:1267", "friendList:", friendList); + formatAppLog("log", "at pages/Chat/Chat.vue:1386", "friendList:", friendList); if (friendList && friendList.length) { FriendInfoList.value = await takeUserAvatar(friendList); } else { FriendInfoList.value = []; - formatAppLog("log", "at pages/Chat/Chat.vue:1272", "好友列表为空"); + formatAppLog("log", "at pages/Chat/Chat.vue:1391", "好友列表为空"); } } catch (error) { - formatAppLog("error", "at pages/Chat/Chat.vue:1275", "获取好友列表失败:", error); + formatAppLog("error", "at pages/Chat/Chat.vue:1394", "获取好友列表失败:", error); FriendInfoList.value = []; } finally { UserConversations.value = FriendInfoList.value; @@ -6487,17 +6634,17 @@ This will fail in production.`); }; }); } catch (err) { - formatAppLog("error", "at pages/Chat/Chat.vue:1305", "获取好友头像失败", err); + formatAppLog("error", "at pages/Chat/Chat.vue:1424", "获取好友头像失败", err); return friendList; } }; const GroupList = vue.ref([]); const takeGroupList = async () => { try { - formatAppLog("log", "at pages/Chat/Chat.vue:1315", "开始获取群聊列表"); + formatAppLog("log", "at pages/Chat/Chat.vue:1434", "开始获取群聊列表"); GroupList.value = await getGroup(UserId.value); } catch (error) { - formatAppLog("error", "at pages/Chat/Chat.vue:1320", "获取群聊列表失败:", error); + formatAppLog("error", "at pages/Chat/Chat.vue:1439", "获取群聊列表失败:", error); GroupList.value = []; } finally { UserConversations.value = GroupList.value; @@ -6509,22 +6656,22 @@ This will fail in production.`); return []; try { const memberIds = memberList.map((item) => item.groupContactId); - formatAppLog("log", "at pages/Chat/Chat.vue:1334", "请求头像的ID列表:", memberIds); + formatAppLog("log", "at pages/Chat/Chat.vue:1453", "请求头像的ID列表:", memberIds); const memberAvatarList = await getUserAvatar(userToken.value, memberIds); - formatAppLog("log", "at pages/Chat/Chat.vue:1336", "头像接口返回数据:", memberAvatarList); + formatAppLog("log", "at pages/Chat/Chat.vue:1455", "头像接口返回数据:", memberAvatarList); const userMap = new Map(memberAvatarList.map((user) => [user.user_id, user]) || []); - formatAppLog("log", "at pages/Chat/Chat.vue:1339", "userMap的keys:", Array.from(userMap.keys())); + formatAppLog("log", "at pages/Chat/Chat.vue:1458", "userMap的keys:", Array.from(userMap.keys())); return memberList.map((member) => { const memberId = member.groupContactId; const userInfo = userMap.get(memberId); - formatAppLog("log", "at pages/Chat/Chat.vue:1344", `查找 ${memberId} 的头像:`, userInfo); + formatAppLog("log", "at pages/Chat/Chat.vue:1463", `查找 ${memberId} 的头像:`, userInfo); return { ...member, avatar: (userInfo == null ? void 0 : userInfo.avatar) || null }; }); } catch (err) { - formatAppLog("error", "at pages/Chat/Chat.vue:1351", "获取群成员头像失败", err); + formatAppLog("error", "at pages/Chat/Chat.vue:1470", "获取群成员头像失败", err); return memberList; } }; @@ -6545,6 +6692,8 @@ This will fail in production.`); const takeConversationMessages = async () => { try { allmessages.value = await getConversationMessages(userToken.value, currentSessionId.value) || []; + formatAppLog("log", "at pages/Chat/Chat.vue:1503", "获取到的所有消息:", JSON.stringify(allmessages.value)); + formatAppLog("log", "at pages/Chat/Chat.vue:1504", "获取到的所有消息:", allmessages.value); scrollToBottom(); } catch (error) { uni.showToast({ @@ -6571,12 +6720,12 @@ This will fail in production.`); const takeGroupMessages = async () => { try { allmessages.value = await getGroupMessages(currentSessionId.value) || []; - formatAppLog("log", "at pages/Chat/Chat.vue:1415", "群聊消息:", allmessages.value); + formatAppLog("log", "at pages/Chat/Chat.vue:1535", "群聊消息:", allmessages.value); const memberList = await getGroupMemberList(currentSessionId.value); - formatAppLog("log", "at pages/Chat/Chat.vue:1418", "获取到的群成员列表:", memberList); + formatAppLog("log", "at pages/Chat/Chat.vue:1538", "获取到的群成员列表:", memberList); if (memberList && memberList.length) { groupMemberList.value = await takeGroupMemberAvatar(memberList); - formatAppLog("log", "at pages/Chat/Chat.vue:1421", "群成员列表(带头像):", groupMemberList.value); + formatAppLog("log", "at pages/Chat/Chat.vue:1541", "群成员列表(带头像):", groupMemberList.value); } else { groupMemberList.value = []; } @@ -6600,9 +6749,9 @@ This will fail in production.`); } }; vue.watch(() => friendSocketStore.MessageReceived, (newId) => { - formatAppLog("log", "at pages/Chat/Chat.vue:1468", "收到了好友消息"); + formatAppLog("log", "at pages/Chat/Chat.vue:1588", "收到了好友消息"); if (newId) { - formatAppLog("log", "at pages/Chat/Chat.vue:1470", "ChatType:", ChatType.value); + formatAppLog("log", "at pages/Chat/Chat.vue:1590", "ChatType:", ChatType.value); switch (ChatType.value) { case 0: takeConversationMessages(); @@ -6617,7 +6766,7 @@ This will fail in production.`); friendSocketStore.MessageReceived = false; break; default: - formatAppLog("log", "at pages/Chat/Chat.vue:1486", "default 分支"); + formatAppLog("log", "at pages/Chat/Chat.vue:1606", "default 分支"); friendSocketStore.MessageReceived = false; break; } @@ -6696,7 +6845,7 @@ This will fail in production.`); onUnload(() => { socketStore.disconnect(); }); - const __returned__ = { friendSocketStore, handleFriendConnect, socketStore, ChatType, wyxdWorkspaceId, wyxdFilePath, wyxdFileName, wyxdUserToken, hasWyxdFile, convertMarkdownTable, renderTable, escapeLoneUnderscores, pareseMarkdown, sanitizeContent, previewImage, openFile: openFile$1, formatFileSize: formatFileSize2, parseFileInfo, selectNormalChat, showNewChatModal, closeNewChatModal, isChatSidebar, handleChatSidebar, goWorkSpace, goWyxdViewer, goCloudDatabase, logOut, previewFileArray, uploadPhoto, deleteImage, fileList, uploadFile, handleConnect, isThinking, isSelfSent, isUploading, isLoadingMessages, uploadedFiles, streamingMessageId, showMessageModal, detailLinks, handleChatContentClick, extractLinks, extractFileNameFromUrl, showMessageDetail, closeMessageDetail, downloadToast, get downloadToastTimer() { + const __returned__ = { friendSocketStore, handleFriendConnect, socketStore, ChatType, wyxdWorkspaceId, wyxdFilePath, wyxdFileName, wyxdUserToken, hasWyxdFile, convertMarkdownTable, renderTable, escapeLoneUnderscores, isDownloadLink, renderDownloadBtn, supportedExtensions, extPattern, pareseMarkdown, sanitizeContent, previewImage, openFile: openFile$1, formatFileSize: formatFileSize2, parseFileInfo, selectNormalChat, showNewChatModal, closeNewChatModal, isChatSidebar, handleChatSidebar, goWorkSpace, goWyxdViewer, goCloudDatabase, logOut, previewFileArray, uploadPhoto, deleteImage, fileList, uploadFile, handleConnect, isThinking, isSelfSent, isUploading, isLoadingMessages, uploadedFiles, streamingMessageId, showMessageModal, detailLinks, handleChatContentClick, extractLinks, extractFileNameFromUrl, getFileTypeInfo, showMessageDetail, closeMessageDetail, downloadToast, get downloadToastTimer() { return downloadToastTimer; }, set downloadToastTimer(v) { downloadToastTimer = v; diff --git a/unpackage/dist/dev/app-plus/pages/Chat/Chat.css b/unpackage/dist/dev/app-plus/pages/Chat/Chat.css index 077b726..06bf21d 100644 --- a/unpackage/dist/dev/app-plus/pages/Chat/Chat.css +++ b/unpackage/dist/dev/app-plus/pages/Chat/Chat.css @@ -1716,6 +1716,7 @@ to { opacity: 1; transform: translateY(0); /* 普通正文 */ /* 粗体和斜体样式 */ /* 气泡内的内联链接(替代 标签,点击由 JS 拦截处理) */ + /* 可下载文件链接 → 按钮卡片样式 */ } .chat-content h1 { font-size: 1.3125rem; @@ -1761,6 +1762,38 @@ to { opacity: 1; transform: translateY(0); text-decoration: underline; word-break: break-all; } +.chat-content .chat-download-btn { + display: inline-flex !important; + align-items: center; + gap: 0.25rem; + padding: 0.25rem 0.4375rem; + margin: 0.125rem 0; + background: #f5f7fb; + border: 0.0625rem solid #e4e8f0; + border-radius: 0.25rem; + text-decoration: none !important; + color: #333 !important; + word-break: break-all; + transition: all 0.15s ease; +} +.chat-content .chat-download-btn:active { + background: #eef1f7; + border-color: #c8cde0; + transform: scale(0.98); +} +.chat-content .chat-download-btn .chat-dl-icon { + font-size: 0.8125rem !important; + line-height: 1; + flex-shrink: 0; +} +.chat-content .chat-download-btn .chat-dl-name { + font-size: 16px !important; + font-weight: 500; + color: #333; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} /* 流式气泡中的加载动画 */ .bubble-loading-dots {