diff --git a/pages/Chat/Chat.vue b/pages/Chat/Chat.vue index 18372f0..d0b3969 100644 --- a/pages/Chat/Chat.vue +++ b/pages/Chat/Chat.vue @@ -87,7 +87,7 @@ - + @@ -172,7 +173,8 @@ - + @@ -240,13 +242,16 @@ - {{ detailMessageContent }} - 链接 ({{ detailLinks.length }}) - - {{ link }} + 文件 ({{ detailLinks.length }}) + + {{ link.name }} + + 无可下载文件 + @@ -439,7 +444,12 @@ content = escapeLoneUnderscores(content) // 先转换表格,再用 snarkdown 处理其他 Markdown content = convertMarkdownTable(content) - const html = snarkdown(content) + let html = snarkdown(content) + // 将 替换为自定义 span,避免 WebView 触发默认下载 + html = html.replace( + /]*>(.*?)<\/a>/gi, + '$2' + ) return html } catch (e) { console.error('解析失败', e) @@ -708,38 +718,117 @@ // 消息详情弹窗 const showMessageModal = ref(false) - const detailMessageContent = ref('') const detailLinks = ref([]) + // 点击气泡内容:拦截链接点击,否则 AI 对话弹出详情 + const handleChatContentClick = (e, message) => { + // 在事件路径中向上查找 .chat-inline-link 元素 + let el = e.target + while (el && el.classList) { + if (el.classList.contains('chat-inline-link')) { + const url = el.getAttribute('data-url') || (el.dataset && el.dataset.url) + if (url) { + openLink(url) + } + return + } + el = el.parentElement + } + // 非链接点击:仅 AI 对话弹出详情窗 + if (message.role) { + showMessageDetail(message) + } + } + const extractLinks = (text) => { - const regex = /https?:\/\/[^\s<>"{}|\\^`\[\]()]+/gi - const matches = text.match(regex) - return matches ? [...new Set(matches)] : [] + const seen = new Set() + const result = [] + // 1. 匹配 Markdown 链接 [文件名](url) + const mdLinkRegex = /\[([^\]]*)\]\((https?:\/\/[^\s<>"{}|\\^`\[\]()]+)\)/gi + const mdLinks = [...text.matchAll(mdLinkRegex)] + mdLinks.forEach(m => { + const name = m[1].trim() + const url = m[2] + if (!seen.has(url)) { + seen.add(url) + result.push({ + url, + name: name || url + }) + } + }) + // 2. 去除已匹配的 [xxx](url) 后,再搜裸 URL + let cleaned = text.replace(mdLinkRegex, '') + const bareRegex = /https?:\/\/[^\s<>"{}|\\^`\[\]()]+/gi + const bareMatches = cleaned.match(bareRegex) + if (bareMatches) { + bareMatches.forEach(url => { + if (!seen.has(url)) { + seen.add(url) + result.push({ + url, + name: extractFileNameFromUrl(url) + }) + } + }) + } + return result + } + const extractFileNameFromUrl = (urlStr) => { + try { + const segments = new URL(urlStr).pathname.split('/').filter(s => s) + if (segments.length > 0) { + return decodeURIComponent(segments[segments.length - 1]) + } + } catch (e) { + /* fallback */ + } + const parts = urlStr.split('/').filter(s => s) + if (parts.length > 0) { + const last = parts[parts.length - 1] + const qIdx = last.indexOf('?') + return qIdx > -1 ? last.substring(0, qIdx) : last + } + return urlStr } const showMessageDetail = (message) => { - detailMessageContent.value = message.content || '' - detailLinks.value = extractLinks(detailMessageContent.value) + detailLinks.value = extractLinks(message.content || '') showMessageModal.value = true } const closeMessageDetail = () => { showMessageModal.value = false - detailMessageContent.value = '' detailLinks.value = [] } - const downloadToast = ref({ show: false, type: 'loading', message: '' }) + const downloadToast = ref({ + show: false, + type: 'loading', + message: '' + }) let downloadToastTimer = null const showDownloadToast = (type, message, duration) => { if (downloadToastTimer) clearTimeout(downloadToastTimer) - downloadToast.value = { show: true, type, message } + downloadToast.value = { + show: true, + type, + message + } if (duration > 0) { downloadToastTimer = setTimeout(() => { - downloadToast.value = { show: false, type: 'loading', message: '' } + downloadToast.value = { + show: false, + type: 'loading', + message: '' + } }, duration) } } const hideDownloadToast = () => { if (downloadToastTimer) clearTimeout(downloadToastTimer) - downloadToast.value = { show: false, type: 'loading', message: '' } + downloadToast.value = { + show: false, + type: 'loading', + message: '' + } } const openLink = (url) => { @@ -1458,6 +1547,13 @@ line-height: 1.6; margin: 8rpx 0; } + + /* 气泡内的内联链接(替代 标签,点击由 JS 拦截处理) */ + .chat-inline-link { + color: #3b86ff !important; + text-decoration: underline; + word-break: break-all; + } } /* 流式气泡中的加载动画 */ @@ -1526,6 +1622,15 @@ word-break: break-all; } + .detail-empty { + display: flex; + align-items: center; + justify-content: center; + padding: 40rpx 0; + font-size: 28rpx; + color: #999; + } + /* ========== 自定义下载 Toast(层级高于 ncd-overlay 的 9999)========== */ .download-toast-overlay { position: fixed; @@ -1554,8 +1659,15 @@ } @keyframes toastFadeIn { - from { opacity: 0; transform: scale(0.85); } - to { opacity: 1; transform: scale(1); } + from { + opacity: 0; + transform: scale(0.85); + } + + to { + opacity: 1; + transform: scale(1); + } } .download-toast-loading { @@ -1590,12 +1702,26 @@ animation: toastDotBlink 1.2s infinite ease-in-out; } - .toast-dot:nth-child(2) { animation-delay: 0.3s; } - .toast-dot:nth-child(3) { animation-delay: 0.6s; } + .toast-dot:nth-child(2) { + animation-delay: 0.3s; + } + + .toast-dot:nth-child(3) { + animation-delay: 0.6s; + } @keyframes toastDotBlink { - 0%, 100% { opacity: 0.3; transform: scale(0.75); } - 50% { opacity: 1; transform: scale(1.2); } + + 0%, + 100% { + opacity: 0.3; + transform: scale(0.75); + } + + 50% { + opacity: 1; + transform: scale(1.2); + } } .download-toast-text { @@ -1603,6 +1729,4 @@ color: #fff; font-weight: 500; } - - \ No newline at end of file diff --git a/unpackage/dist/dev/app-plus/app-service.js b/unpackage/dist/dev/app-plus/app-service.js index 8aaebee..b7ba86e 100644 --- a/unpackage/dist/dev/app-plus/app-service.js +++ b/unpackage/dist/dev/app-plus/app-service.js @@ -5631,7 +5631,7 @@ This will fail in production.`); if (friendSocketStore.isConnected) return; if (!userToken.value || !UserId.value) { - formatAppLog("warn", "at pages/Chat/Chat.vue:329", "Token或UserId未准备好"); + formatAppLog("warn", "at pages/Chat/Chat.vue:334", "Token或UserId未准备好"); return; } friendSocketStore.connect({ @@ -5707,10 +5707,14 @@ This will fail in production.`); try { content = escapeLoneUnderscores(content); content = convertMarkdownTable(content); - const html = t(content); + let html = t(content); + html = html.replace( + /]*>(.*?)<\/a>/gi, + '$2' + ); return html; } catch (e2) { - formatAppLog("error", "at pages/Chat/Chat.vue:445", "解析失败", e2); + formatAppLog("error", "at pages/Chat/Chat.vue:455", "解析失败", e2); return content; } }; @@ -5769,7 +5773,7 @@ This will fail in production.`); files }; } catch (e2) { - formatAppLog("error", "at pages/Chat/Chat.vue:522", "解析文件信息失败:", e2); + formatAppLog("error", "at pages/Chat/Chat.vue:532", "解析文件信息失败:", e2); return { textContent: content, files: [] @@ -5806,7 +5810,7 @@ This will fail in production.`); } takeUserConversations(); } catch (error) { - formatAppLog("error", "at pages/Chat/Chat.vue:567", "新建普通会话失败:", error); + formatAppLog("error", "at pages/Chat/Chat.vue:577", "新建普通会话失败:", error); uni.showToast({ title: "创建会话失败,请重试", icon: "none" @@ -5844,7 +5848,7 @@ This will fail in production.`); }; const previewFileArray = vue.ref([]); const uploadPhoto = () => { - formatAppLog("log", "at pages/Chat/Chat.vue:616", "点击了拍照上传"); + formatAppLog("log", "at pages/Chat/Chat.vue:626", "点击了拍照上传"); uni.chooseImage({ count: 1, sourceType: ["camera", "album"], @@ -5867,7 +5871,7 @@ This will fail in production.`); }); }, fail: (err) => { - formatAppLog("error", "at pages/Chat/Chat.vue:634", "选择图片失败", err); + formatAppLog("error", "at pages/Chat/Chat.vue:644", "选择图片失败", err); } }); }; @@ -5876,12 +5880,12 @@ This will fail in production.`); }; const fileList = vue.ref([]); const uploadFile = () => { - formatAppLog("log", "at pages/Chat/Chat.vue:645", "点击了上传文件"); + formatAppLog("log", "at pages/Chat/Chat.vue:655", "点击了上传文件"); chooseFile({ count: 5, type: "all", success: (res) => { - formatAppLog("log", "at pages/Chat/Chat.vue:650", "成功了"); + formatAppLog("log", "at pages/Chat/Chat.vue:660", "成功了"); fileList.value = res.tempFiles; previewFileArray.value.push(...res.tempFiles); uni.showToast({ @@ -5890,7 +5894,7 @@ This will fail in production.`); }); }, fail: (err) => { - formatAppLog("error", "at pages/Chat/Chat.vue:661", "选择失败:", err); + formatAppLog("error", "at pages/Chat/Chat.vue:671", "选择失败:", err); uni.showToast({ title: "选择失败", icon: "error" @@ -5927,39 +5931,111 @@ This will fail in production.`); const uploadedFiles = vue.ref([]); const streamingMessageId = vue.ref(null); const showMessageModal = vue.ref(false); - const detailMessageContent = vue.ref(""); const detailLinks = vue.ref([]); + const handleChatContentClick = (e2, message) => { + let el = e2.target; + while (el && el.classList) { + if (el.classList.contains("chat-inline-link")) { + const url = el.getAttribute("data-url") || el.dataset && el.dataset.url; + if (url) { + openLink(url); + } + return; + } + el = el.parentElement; + } + if (message.role) { + showMessageDetail(message); + } + }; const extractLinks = (text) => { - const regex = /https?:\/\/[^\s<>"{}|\\^`\[\]()]+/gi; - const matches = text.match(regex); - return matches ? [...new Set(matches)] : []; + const seen = /* @__PURE__ */ new Set(); + const result = []; + const mdLinkRegex = /\[([^\]]*)\]\((https?:\/\/[^\s<>"{}|\\^`\[\]()]+)\)/gi; + const mdLinks = [...text.matchAll(mdLinkRegex)]; + mdLinks.forEach((m) => { + const name2 = m[1].trim(); + const url = m[2]; + if (!seen.has(url)) { + seen.add(url); + result.push({ + url, + name: name2 || url + }); + } + }); + let cleaned = text.replace(mdLinkRegex, ""); + const bareRegex = /https?:\/\/[^\s<>"{}|\\^`\[\]()]+/gi; + const bareMatches = cleaned.match(bareRegex); + if (bareMatches) { + bareMatches.forEach((url) => { + if (!seen.has(url)) { + seen.add(url); + result.push({ + url, + name: extractFileNameFromUrl(url) + }); + } + }); + } + return result; + }; + const extractFileNameFromUrl = (urlStr) => { + try { + const segments = new URL(urlStr).pathname.split("/").filter((s) => s); + if (segments.length > 0) { + return decodeURIComponent(segments[segments.length - 1]); + } + } catch (e2) { + } + const parts = urlStr.split("/").filter((s) => s); + if (parts.length > 0) { + const last = parts[parts.length - 1]; + const qIdx = last.indexOf("?"); + return qIdx > -1 ? last.substring(0, qIdx) : last; + } + return urlStr; }; const showMessageDetail = (message) => { - detailMessageContent.value = message.content || ""; - detailLinks.value = extractLinks(detailMessageContent.value); + detailLinks.value = extractLinks(message.content || ""); showMessageModal.value = true; }; const closeMessageDetail = () => { showMessageModal.value = false; - detailMessageContent.value = ""; detailLinks.value = []; }; - const downloadToast = vue.ref({ show: false, type: "loading", message: "" }); + const downloadToast = vue.ref({ + show: false, + type: "loading", + message: "" + }); let downloadToastTimer = null; const showDownloadToast = (type, message, duration) => { if (downloadToastTimer) clearTimeout(downloadToastTimer); - downloadToast.value = { show: true, type, message }; + downloadToast.value = { + show: true, + type, + message + }; if (duration > 0) { downloadToastTimer = setTimeout(() => { - downloadToast.value = { show: false, type: "loading", message: "" }; + downloadToast.value = { + show: false, + type: "loading", + message: "" + }; }, duration); } }; const hideDownloadToast = () => { if (downloadToastTimer) clearTimeout(downloadToastTimer); - downloadToast.value = { show: false, type: "loading", message: "" }; + downloadToast.value = { + show: false, + type: "loading", + message: "" + }; }; const openLink = (url) => { downloadAndHandle(url); @@ -6018,7 +6094,7 @@ This will fail in production.`); previewFileArray.value = []; } catch (err) { uni.hideLoading(); - formatAppLog("error", "at pages/Chat/Chat.vue:806", "文件上传失败:", err); + formatAppLog("error", "at pages/Chat/Chat.vue:895", "文件上传失败:", err); uni.showToast({ title: "文件上传失败,请重试", icon: "error" @@ -6152,15 +6228,15 @@ This will fail in production.`); } }; const stopConversation = async () => { - formatAppLog("log", "at pages/Chat/Chat.vue:975", "中断对话 - 当前会话ID:", currentSessionId.value); + formatAppLog("log", "at pages/Chat/Chat.vue:1064", "中断对话 - 当前会话ID:", currentSessionId.value); try { await socketStore.send({ type: "stop", conversation_id: currentSessionId.value }); - formatAppLog("log", "at pages/Chat/Chat.vue:981", "中断指令已发送成功"); + formatAppLog("log", "at pages/Chat/Chat.vue:1070", "中断指令已发送成功"); } catch (err) { - formatAppLog("error", "at pages/Chat/Chat.vue:983", "中断指令发送失败:", err); + formatAppLog("error", "at pages/Chat/Chat.vue:1072", "中断指令发送失败:", err); } if (streamingMessageId.value) { allmessages.value = allmessages.value.filter((m) => m._id !== streamingMessageId.value); @@ -6178,9 +6254,9 @@ This will fail in production.`); }); }; vue.watch(() => socketStore.isThinking, (newVal, oldVal) => { - formatAppLog("log", "at pages/Chat/Chat.vue:1007", "isThinking 变化:", oldVal, "→", newVal, "messageString:", socketStore.messageString); + formatAppLog("log", "at pages/Chat/Chat.vue:1096", "isThinking 变化:", oldVal, "→", newVal, "messageString:", socketStore.messageString); if (!oldVal && newVal) { - formatAppLog("log", "at pages/Chat/Chat.vue:1010", "AI开始回复(跨设备同步),锁定输入框"); + formatAppLog("log", "at pages/Chat/Chat.vue:1099", "AI开始回复(跨设备同步),锁定输入框"); isThinking.value = true; if (!streamingMessageId.value) { streamingMessageId.value = "streaming-" + Date.now(); @@ -6194,7 +6270,7 @@ This will fail in production.`); return; } if (oldVal && !newVal) { - formatAppLog("log", "at pages/Chat/Chat.vue:1026", "AI回复结束(正常/中断),解锁并刷新消息列表"); + formatAppLog("log", "at pages/Chat/Chat.vue:1115", "AI回复结束(正常/中断),解锁并刷新消息列表"); if (streamingMessageId.value) { allmessages.value = allmessages.value.filter((m) => m._id !== streamingMessageId.value); streamingMessageId.value = null; @@ -6246,15 +6322,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:1090", "用户信息已加载:", UserId.value, UserAvatar.value); + formatAppLog("log", "at pages/Chat/Chat.vue:1179", "用户信息已加载:", UserId.value, UserAvatar.value); } catch (error) { - formatAppLog("error", "at pages/Chat/Chat.vue:1092", "获取用户信息失败:", error); + formatAppLog("error", "at pages/Chat/Chat.vue:1181", "获取用户信息失败:", error); } }; const takeUserConversations = async () => { try { userToken.value = getToken(); - formatAppLog("log", "at pages/Chat/Chat.vue:1100", "token:", userToken.value); + formatAppLog("log", "at pages/Chat/Chat.vue:1189", "token:", userToken.value); UserConversations.value = await getUserConversations(userToken.value) || []; const savedSessionId = getCurrentSessionId(); if (savedSessionId && UserConversations.value.some((c) => c._id === savedSessionId)) { @@ -6264,7 +6340,7 @@ This will fail in production.`); } else { currentSessionId.value = ""; } - formatAppLog("log", "at pages/Chat/Chat.vue:1112", "保存会话id:", currentSessionId.value); + formatAppLog("log", "at pages/Chat/Chat.vue:1201", "保存会话id:", currentSessionId.value); uni.setStorageSync("currentSessionId", currentSessionId.value); } catch (error) { uni.showToast({ @@ -6276,17 +6352,17 @@ This will fail in production.`); const FriendInfoList = vue.ref([]); const takeFriendList = async () => { try { - formatAppLog("log", "at pages/Chat/Chat.vue:1127", "开始获取好友列表"); + formatAppLog("log", "at pages/Chat/Chat.vue:1216", "开始获取好友列表"); const friendList = await getChatFriend(UserId.value); - formatAppLog("log", "at pages/Chat/Chat.vue:1130", "friendList:", friendList); + formatAppLog("log", "at pages/Chat/Chat.vue:1219", "friendList:", friendList); if (friendList && friendList.length) { FriendInfoList.value = await takeUserAvatar(friendList); } else { FriendInfoList.value = []; - formatAppLog("log", "at pages/Chat/Chat.vue:1135", "好友列表为空"); + formatAppLog("log", "at pages/Chat/Chat.vue:1224", "好友列表为空"); } } catch (error) { - formatAppLog("error", "at pages/Chat/Chat.vue:1138", "获取好友列表失败:", error); + formatAppLog("error", "at pages/Chat/Chat.vue:1227", "获取好友列表失败:", error); FriendInfoList.value = []; } finally { UserConversations.value = FriendInfoList.value; @@ -6313,17 +6389,17 @@ This will fail in production.`); }; }); } catch (err) { - formatAppLog("error", "at pages/Chat/Chat.vue:1168", "获取好友头像失败", err); + formatAppLog("error", "at pages/Chat/Chat.vue:1257", "获取好友头像失败", err); return friendList; } }; const GroupList = vue.ref([]); const takeGroupList = async () => { try { - formatAppLog("log", "at pages/Chat/Chat.vue:1178", "开始获取群聊列表"); + formatAppLog("log", "at pages/Chat/Chat.vue:1267", "开始获取群聊列表"); GroupList.value = await getGroup(UserId.value); } catch (error) { - formatAppLog("error", "at pages/Chat/Chat.vue:1183", "获取群聊列表失败:", error); + formatAppLog("error", "at pages/Chat/Chat.vue:1272", "获取群聊列表失败:", error); GroupList.value = []; } finally { UserConversations.value = GroupList.value; @@ -6335,22 +6411,22 @@ This will fail in production.`); return []; try { const memberIds = memberList.map((item) => item.groupContactId); - formatAppLog("log", "at pages/Chat/Chat.vue:1197", "请求头像的ID列表:", memberIds); + formatAppLog("log", "at pages/Chat/Chat.vue:1286", "请求头像的ID列表:", memberIds); const memberAvatarList = await getUserAvatar(userToken.value, memberIds); - formatAppLog("log", "at pages/Chat/Chat.vue:1199", "头像接口返回数据:", memberAvatarList); + formatAppLog("log", "at pages/Chat/Chat.vue:1288", "头像接口返回数据:", memberAvatarList); const userMap = new Map(memberAvatarList.map((user) => [user.user_id, user]) || []); - formatAppLog("log", "at pages/Chat/Chat.vue:1202", "userMap的keys:", Array.from(userMap.keys())); + formatAppLog("log", "at pages/Chat/Chat.vue:1291", "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:1207", `查找 ${memberId} 的头像:`, userInfo); + formatAppLog("log", "at pages/Chat/Chat.vue:1296", `查找 ${memberId} 的头像:`, userInfo); return { ...member, avatar: (userInfo == null ? void 0 : userInfo.avatar) || null }; }); } catch (err) { - formatAppLog("error", "at pages/Chat/Chat.vue:1214", "获取群成员头像失败", err); + formatAppLog("error", "at pages/Chat/Chat.vue:1303", "获取群成员头像失败", err); return memberList; } }; @@ -6393,12 +6469,12 @@ This will fail in production.`); const takeGroupMessages = async () => { try { allmessages.value = await getGroupMessages(currentSessionId.value) || []; - formatAppLog("log", "at pages/Chat/Chat.vue:1274", "群聊消息:", allmessages.value); + formatAppLog("log", "at pages/Chat/Chat.vue:1363", "群聊消息:", allmessages.value); const memberList = await getGroupMemberList(currentSessionId.value); - formatAppLog("log", "at pages/Chat/Chat.vue:1277", "获取到的群成员列表:", memberList); + formatAppLog("log", "at pages/Chat/Chat.vue:1366", "获取到的群成员列表:", memberList); if (memberList && memberList.length) { groupMemberList.value = await takeGroupMemberAvatar(memberList); - formatAppLog("log", "at pages/Chat/Chat.vue:1280", "群成员列表(带头像):", groupMemberList.value); + formatAppLog("log", "at pages/Chat/Chat.vue:1369", "群成员列表(带头像):", groupMemberList.value); } else { groupMemberList.value = []; } @@ -6420,9 +6496,9 @@ This will fail in production.`); } }; vue.watch(() => friendSocketStore.MessageReceived, (newId) => { - formatAppLog("log", "at pages/Chat/Chat.vue:1325", "收到了好友消息"); + formatAppLog("log", "at pages/Chat/Chat.vue:1414", "收到了好友消息"); if (newId) { - formatAppLog("log", "at pages/Chat/Chat.vue:1327", "ChatType:", ChatType.value); + formatAppLog("log", "at pages/Chat/Chat.vue:1416", "ChatType:", ChatType.value); switch (ChatType.value) { case 0: takeConversationMessages(); @@ -6437,7 +6513,7 @@ This will fail in production.`); friendSocketStore.MessageReceived = false; break; default: - formatAppLog("log", "at pages/Chat/Chat.vue:1343", "default 分支"); + formatAppLog("log", "at pages/Chat/Chat.vue:1432", "default 分支"); friendSocketStore.MessageReceived = false; break; } @@ -6499,7 +6575,7 @@ This will fail in production.`); onUnload(() => { socketStore.disconnect(); }); - const __returned__ = { friendSocketStore, handleFriendConnect, socketStore, ChatType, convertMarkdownTable, renderTable, escapeLoneUnderscores, pareseMarkdown, sanitizeContent, previewImage, openFile, formatFileSize, parseFileInfo, selectNormalChat, showNewChatModal, closeNewChatModal, isChatSidebar, handleChatSidebar, goWorkSpace, goCloudDatabase, logOut, previewFileArray, uploadPhoto, deleteImage, fileList, uploadFile, handleConnect, isThinking, isSelfSent, isUploading, uploadedFiles, streamingMessageId, showMessageModal, detailMessageContent, detailLinks, extractLinks, showMessageDetail, closeMessageDetail, downloadToast, get downloadToastTimer() { + const __returned__ = { friendSocketStore, handleFriendConnect, socketStore, ChatType, convertMarkdownTable, renderTable, escapeLoneUnderscores, pareseMarkdown, sanitizeContent, previewImage, openFile, formatFileSize, parseFileInfo, selectNormalChat, showNewChatModal, closeNewChatModal, isChatSidebar, handleChatSidebar, goWorkSpace, goCloudDatabase, logOut, previewFileArray, uploadPhoto, deleteImage, fileList, uploadFile, handleConnect, isThinking, isSelfSent, isUploading, uploadedFiles, streamingMessageId, showMessageModal, detailLinks, handleChatContentClick, extractLinks, extractFileNameFromUrl, showMessageDetail, closeMessageDetail, downloadToast, get downloadToastTimer() { return downloadToastTimer; }, set downloadToastTimer(v) { downloadToastTimer = v; @@ -6733,7 +6809,7 @@ This will fail in production.`); message.content && String(message.content).trim() !== "" || message._streaming ? (vue.openBlock(), vue.createElementBlock("view", { key: 1, class: vue.normalizeClass(["chat-content", { "chat-content-user": message.role === "user", "chat-content-assistant": message.role === "assistant" }]), - onClick: ($event) => $setup.showMessageDetail(message) + onClick: ($event) => $setup.handleChatContentClick($event, message) }, [ message._streaming && (!message.content || String(message.content).trim() === "") ? (vue.openBlock(), vue.createElementBlock("view", { key: 0, @@ -6836,65 +6912,60 @@ This will fail in production.`); 2 /* CLASS */ ), - vue.createElementVNode( - "view", - { - class: vue.normalizeClass(["chat-content", { "chat-content-user": message.sender === $setup.UserId }]) - }, - [ - message.content && String(message.content).trim() !== "" ? (vue.openBlock(), vue.createElementBlock("view", { - key: 0, - innerHTML: $setup.pareseMarkdown(message.content) - }, null, 8, ["innerHTML"])) : vue.createCommentVNode("v-if", true), - message.contentJson ? (vue.openBlock(), vue.createElementBlock("view", { - key: 1, - class: "message-file-list" - }, [ - (vue.openBlock(true), vue.createElementBlock( - vue.Fragment, - null, - vue.renderList(JSON.parse(message.contentJson), (file, idx) => { - return vue.openBlock(), vue.createElementBlock("view", { - key: idx, - class: "file-item" + vue.createElementVNode("view", { + class: vue.normalizeClass(["chat-content", { "chat-content-user": message.sender === $setup.UserId }]), + onClick: ($event) => $setup.handleChatContentClick($event, message) + }, [ + message.content && String(message.content).trim() !== "" ? (vue.openBlock(), vue.createElementBlock("view", { + key: 0, + innerHTML: $setup.pareseMarkdown(message.content) + }, null, 8, ["innerHTML"])) : vue.createCommentVNode("v-if", true), + message.contentJson ? (vue.openBlock(), vue.createElementBlock("view", { + key: 1, + class: "message-file-list" + }, [ + (vue.openBlock(true), vue.createElementBlock( + vue.Fragment, + null, + vue.renderList(JSON.parse(message.contentJson), (file, idx) => { + return vue.openBlock(), vue.createElementBlock("view", { + key: idx, + class: "file-item" + }, [ + ["jpg", "jpeg", "png", "gif", "webp", "bmp"].includes(file.extendName.toLowerCase()) ? (vue.openBlock(), vue.createElementBlock("image", { + key: 0, + src: file.url, + mode: "widthFix", + class: "message-image", + onClick: ($event) => $setup.previewImage(file.url) + }, null, 8, ["src", "onClick"])) : (vue.openBlock(), vue.createElementBlock("view", { + key: 1, + class: "message-file", + onClick: ($event) => $setup.openFile(file.url) }, [ - ["jpg", "jpeg", "png", "gif", "webp", "bmp"].includes(file.extendName.toLowerCase()) ? (vue.openBlock(), vue.createElementBlock("image", { - key: 0, - src: file.url, - mode: "widthFix", - class: "message-image", - onClick: ($event) => $setup.previewImage(file.url) - }, null, 8, ["src", "onClick"])) : (vue.openBlock(), vue.createElementBlock("view", { - key: 1, - class: "message-file", - onClick: ($event) => $setup.openFile(file.url) - }, [ - vue.createElementVNode("view", { class: "file-icon" }, "📄"), - vue.createElementVNode( - "view", - { class: "file-name" }, - vue.toDisplayString(file.name), - 1 - /* TEXT */ - ), - vue.createElementVNode( - "view", - { class: "file-size" }, - vue.toDisplayString($setup.formatFileSize(file.fileSize)), - 1 - /* TEXT */ - ) - ], 8, ["onClick"])) - ]); - }), - 128 - /* KEYED_FRAGMENT */ - )) - ])) : vue.createCommentVNode("v-if", true) - ], - 2 - /* CLASS */ - ) + vue.createElementVNode("view", { class: "file-icon" }, "📄"), + vue.createElementVNode( + "view", + { class: "file-name" }, + vue.toDisplayString(file.name), + 1 + /* TEXT */ + ), + vue.createElementVNode( + "view", + { class: "file-size" }, + vue.toDisplayString($setup.formatFileSize(file.fileSize)), + 1 + /* TEXT */ + ) + ], 8, ["onClick"])) + ]); + }), + 128 + /* KEYED_FRAGMENT */ + )) + ])) : vue.createCommentVNode("v-if", true) + ], 10, ["onClick"]) ], 10, ["id"]); }), 128 @@ -6934,65 +7005,60 @@ This will fail in production.`); 2 /* CLASS */ ), - vue.createElementVNode( - "view", - { - class: vue.normalizeClass(["chat-content", { "chat-content-user": message.sender === $setup.UserId }]) - }, - [ - message.message && String(message.message).trim() !== "" ? (vue.openBlock(), vue.createElementBlock("view", { - key: 0, - innerHTML: $setup.pareseMarkdown(message.message) - }, null, 8, ["innerHTML"])) : vue.createCommentVNode("v-if", true), - message.contentJson ? (vue.openBlock(), vue.createElementBlock("view", { - key: 1, - class: "message-file-list" - }, [ - (vue.openBlock(true), vue.createElementBlock( - vue.Fragment, - null, - vue.renderList(JSON.parse(message.contentJson), (file, idx) => { - return vue.openBlock(), vue.createElementBlock("view", { - key: idx, - class: "file-item" + vue.createElementVNode("view", { + class: vue.normalizeClass(["chat-content", { "chat-content-user": message.sender === $setup.UserId }]), + onClick: ($event) => $setup.handleChatContentClick($event, message) + }, [ + message.message && String(message.message).trim() !== "" ? (vue.openBlock(), vue.createElementBlock("view", { + key: 0, + innerHTML: $setup.pareseMarkdown(message.message) + }, null, 8, ["innerHTML"])) : vue.createCommentVNode("v-if", true), + message.contentJson ? (vue.openBlock(), vue.createElementBlock("view", { + key: 1, + class: "message-file-list" + }, [ + (vue.openBlock(true), vue.createElementBlock( + vue.Fragment, + null, + vue.renderList(JSON.parse(message.contentJson), (file, idx) => { + return vue.openBlock(), vue.createElementBlock("view", { + key: idx, + class: "file-item" + }, [ + ["jpg", "jpeg", "png", "gif", "webp", "bmp"].includes(file.extendName.toLowerCase()) ? (vue.openBlock(), vue.createElementBlock("image", { + key: 0, + src: file.url, + mode: "widthFix", + class: "message-image", + onClick: ($event) => $setup.previewImage(file.url) + }, null, 8, ["src", "onClick"])) : (vue.openBlock(), vue.createElementBlock("view", { + key: 1, + class: "message-file", + onClick: ($event) => $setup.openFile(file.url) }, [ - ["jpg", "jpeg", "png", "gif", "webp", "bmp"].includes(file.extendName.toLowerCase()) ? (vue.openBlock(), vue.createElementBlock("image", { - key: 0, - src: file.url, - mode: "widthFix", - class: "message-image", - onClick: ($event) => $setup.previewImage(file.url) - }, null, 8, ["src", "onClick"])) : (vue.openBlock(), vue.createElementBlock("view", { - key: 1, - class: "message-file", - onClick: ($event) => $setup.openFile(file.url) - }, [ - vue.createElementVNode("view", { class: "file-icon" }, "📄"), - vue.createElementVNode( - "view", - { class: "file-name" }, - vue.toDisplayString(file.name), - 1 - /* TEXT */ - ), - vue.createElementVNode( - "view", - { class: "file-size" }, - vue.toDisplayString($setup.formatFileSize(file.fileSize)), - 1 - /* TEXT */ - ) - ], 8, ["onClick"])) - ]); - }), - 128 - /* KEYED_FRAGMENT */ - )) - ])) : vue.createCommentVNode("v-if", true) - ], - 2 - /* CLASS */ - ) + vue.createElementVNode("view", { class: "file-icon" }, "📄"), + vue.createElementVNode( + "view", + { class: "file-name" }, + vue.toDisplayString(file.name), + 1 + /* TEXT */ + ), + vue.createElementVNode( + "view", + { class: "file-size" }, + vue.toDisplayString($setup.formatFileSize(file.fileSize)), + 1 + /* TEXT */ + ) + ], 8, ["onClick"])) + ]); + }), + 128 + /* KEYED_FRAGMENT */ + )) + ])) : vue.createCommentVNode("v-if", true) + ], 10, ["onClick"]) ], 10, ["id"]); }), 128 @@ -7128,13 +7194,6 @@ This will fail in production.`); class: "message-detail-content", "scroll-y": "" }, [ - vue.createElementVNode( - "text", - null, - vue.toDisplayString($setup.detailMessageContent), - 1 - /* TEXT */ - ), $setup.detailLinks.length ? (vue.openBlock(), vue.createElementBlock("view", { key: 0, class: "detail-links-section" @@ -7142,7 +7201,7 @@ This will fail in production.`); vue.createElementVNode( "view", { class: "detail-links-title" }, - "链接 (" + vue.toDisplayString($setup.detailLinks.length) + ")", + "文件 (" + vue.toDisplayString($setup.detailLinks.length) + ")", 1 /* TEXT */ ), @@ -7153,12 +7212,12 @@ This will fail in production.`); return vue.openBlock(), vue.createElementBlock("view", { key: i, class: "detail-link-item", - onClick: ($event) => $setup.openLink(link) + onClick: ($event) => $setup.openLink(link.url) }, [ vue.createElementVNode( "text", { class: "link-text" }, - vue.toDisplayString(link), + vue.toDisplayString(link.name), 1 /* TEXT */ ) @@ -7167,7 +7226,12 @@ This will fail in production.`); 128 /* KEYED_FRAGMENT */ )) - ])) : vue.createCommentVNode("v-if", true) + ])) : (vue.openBlock(), vue.createElementBlock("view", { + key: 1, + class: "detail-empty" + }, [ + vue.createElementVNode("text", null, "无可下载文件") + ])) ]) ]) ])) : vue.createCommentVNode("v-if", true), diff --git a/unpackage/dist/dev/app-plus/pages/Chat/Chat.css b/unpackage/dist/dev/app-plus/pages/Chat/Chat.css index 4fc6b4f..2547e0d 100644 --- a/unpackage/dist/dev/app-plus/pages/Chat/Chat.css +++ b/unpackage/dist/dev/app-plus/pages/Chat/Chat.css @@ -1657,6 +1657,7 @@ to { opacity: 1; transform: translateY(0); /* 文章场景相关 */ .chat-content { /* 普通正文 */ + /* 气泡内的内联链接(替代 标签,点击由 JS 拦截处理) */ } .chat-content h1 { font-size: 1.3125rem; @@ -1687,6 +1688,11 @@ to { opacity: 1; transform: translateY(0); line-height: 1.6; margin: 0.25rem 0; } +.chat-content .chat-inline-link { + color: #3b86ff !important; + text-decoration: underline; + word-break: break-all; +} /* 流式气泡中的加载动画 */ .bubble-loading-dots { @@ -1742,6 +1748,14 @@ to { opacity: 1; transform: translateY(0); color: #007aff; word-break: break-all; } +.detail-empty { + display: flex; + align-items: center; + justify-content: center; + padding: 1.25rem 0; + font-size: 0.875rem; + color: #999; +} /* ========== 自定义下载 Toast(层级高于 ncd-overlay 的 9999)========== */ .download-toast-overlay {