取消<a>原生链接处理,统一在弹窗中点击下载;将消息详情弹窗中链接包装成文件名,并取消原文展示。
This commit is contained in:
@@ -87,7 +87,7 @@
|
||||
</view>
|
||||
<view class="chat-content"
|
||||
:class="{'chat-content-user':message.role==='user', 'chat-content-assistant':message.role==='assistant'}"
|
||||
@click="showMessageDetail(message)"
|
||||
@click="handleChatContentClick($event, message)"
|
||||
v-if="(message.content && String(message.content).trim() !== '') || message._streaming">
|
||||
<!-- 流式加载中:显示跳动的点 -->
|
||||
<view
|
||||
@@ -134,7 +134,8 @@
|
||||
<!-- 没有头像时显示默认图标 -->
|
||||
<view v-else class="iconfont icon-yonghuziliao"></view>
|
||||
</view>
|
||||
<view class="chat-content" :class="{'chat-content-user':message.sender === UserId}">
|
||||
<view class="chat-content" :class="{'chat-content-user':message.sender === UserId}"
|
||||
@click="handleChatContentClick($event, message)">
|
||||
<view v-if="message.content && String(message.content).trim() !== ''"
|
||||
v-html="pareseMarkdown(message.content)"></view>
|
||||
<!-- 2. 图片 + 文件 渲染(核心新增) -->
|
||||
@@ -172,7 +173,8 @@
|
||||
<!-- 没有头像时显示默认图标 -->
|
||||
<view v-else class="iconfont icon-yonghuziliao"></view>
|
||||
</view>
|
||||
<view class="chat-content" :class="{'chat-content-user':message.sender === UserId}">
|
||||
<view class="chat-content" :class="{'chat-content-user':message.sender === UserId}"
|
||||
@click="handleChatContentClick($event, message)">
|
||||
<view v-if="message.message && String(message.message).trim() !== ''"
|
||||
v-html="pareseMarkdown(message.message)"></view>
|
||||
<!-- 2. 图片 + 文件 渲染(核心新增) -->
|
||||
@@ -240,13 +242,16 @@
|
||||
<uni-icons type="closeempty" color="#ff0000" size="24" @click="closeMessageDetail"></uni-icons>
|
||||
</view>
|
||||
<scroll-view class="message-detail-content" scroll-y>
|
||||
<text>{{ detailMessageContent }}</text>
|
||||
<view v-if="detailLinks.length" class="detail-links-section">
|
||||
<view class="detail-links-title">链接 ({{ detailLinks.length }})</view>
|
||||
<view v-for="(link, i) in detailLinks" :key="i" class="detail-link-item" @click="openLink(link)">
|
||||
<text class="link-text">{{ link }}</text>
|
||||
<view class="detail-links-title">文件 ({{ detailLinks.length }})</view>
|
||||
<view v-for="(link, i) in detailLinks" :key="i" class="detail-link-item"
|
||||
@click="openLink(link.url)">
|
||||
<text class="link-text">{{ link.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="detail-empty">
|
||||
<text>无可下载文件</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -439,7 +444,12 @@
|
||||
content = escapeLoneUnderscores(content)
|
||||
// 先转换表格,再用 snarkdown 处理其他 Markdown
|
||||
content = convertMarkdownTable(content)
|
||||
const html = snarkdown(content)
|
||||
let html = snarkdown(content)
|
||||
// 将 <a href="..."> 替换为自定义 span,避免 WebView 触发默认下载
|
||||
html = html.replace(
|
||||
/<a\s+href="([^"]*)"[^>]*>(.*?)<\/a>/gi,
|
||||
'<span class="chat-inline-link" data-url="$1">$2</span>'
|
||||
)
|
||||
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;
|
||||
}
|
||||
|
||||
/* 气泡内的内联链接(替代 <a> 标签,点击由 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;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user