Compare commits
2 Commits
bf9d5f3f22
...
9ace485cbd
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ace485cbd | |||
| d5d9423599 |
@@ -52,7 +52,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: rgba(30, 30, 50, 0.35);
|
background-color: rgba(30, 30, 50, 0.35);
|
||||||
z-index: 1000;
|
z-index: 9999;
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
-webkit-backdrop-filter: blur(8px);
|
-webkit-backdrop-filter: blur(8px);
|
||||||
animation: fadeIn 0.25s ease;
|
animation: fadeIn 0.25s ease;
|
||||||
@@ -801,13 +801,15 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border-radius: 40rpx;
|
border-radius: 40rpx;
|
||||||
|
padding: 40rpx 36rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-detail-content {
|
.message-detail-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 200rpx;
|
||||||
max-height: 60vh;
|
max-height: 60vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 20rpx 0;
|
padding: 16rpx 0;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
color: #333333;
|
color: #333333;
|
||||||
|
|||||||
@@ -34,26 +34,6 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- ===== 消息详情弹窗 ===== -->
|
|
||||||
<view v-if="showMessageModal" class="ncd-overlay" @click="closeMessageDetail">
|
|
||||||
<view class="ncd-card message-detail-card" @click.stop>
|
|
||||||
<view class="ncd-header">
|
|
||||||
<text class="ncd-title-zh">消息详情</text>
|
|
||||||
<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>
|
|
||||||
</view>
|
|
||||||
</scroll-view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
|
|
||||||
<view class="chat-page page-container">
|
<view class="chat-page page-container">
|
||||||
<!-- 弹窗出来时的笼罩层 -->
|
<!-- 弹窗出来时的笼罩层 -->
|
||||||
<view v-if="isChatSidebar" class="mask" @click="handleChatSidebar"></view>
|
<view v-if="isChatSidebar" class="mask" @click="handleChatSidebar"></view>
|
||||||
@@ -107,7 +87,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="chat-content"
|
<view class="chat-content"
|
||||||
:class="{'chat-content-user':message.role==='user', 'chat-content-assistant':message.role==='assistant'}"
|
: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">
|
v-if="(message.content && String(message.content).trim() !== '') || message._streaming">
|
||||||
<!-- 流式加载中:显示跳动的点 -->
|
<!-- 流式加载中:显示跳动的点 -->
|
||||||
<view
|
<view
|
||||||
@@ -154,7 +134,8 @@
|
|||||||
<!-- 没有头像时显示默认图标 -->
|
<!-- 没有头像时显示默认图标 -->
|
||||||
<view v-else class="iconfont icon-yonghuziliao"></view>
|
<view v-else class="iconfont icon-yonghuziliao"></view>
|
||||||
</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() !== ''"
|
<view v-if="message.content && String(message.content).trim() !== ''"
|
||||||
v-html="pareseMarkdown(message.content)"></view>
|
v-html="pareseMarkdown(message.content)"></view>
|
||||||
<!-- 2. 图片 + 文件 渲染(核心新增) -->
|
<!-- 2. 图片 + 文件 渲染(核心新增) -->
|
||||||
@@ -192,7 +173,8 @@
|
|||||||
<!-- 没有头像时显示默认图标 -->
|
<!-- 没有头像时显示默认图标 -->
|
||||||
<view v-else class="iconfont icon-yonghuziliao"></view>
|
<view v-else class="iconfont icon-yonghuziliao"></view>
|
||||||
</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() !== ''"
|
<view v-if="message.message && String(message.message).trim() !== ''"
|
||||||
v-html="pareseMarkdown(message.message)"></view>
|
v-html="pareseMarkdown(message.message)"></view>
|
||||||
<!-- 2. 图片 + 文件 渲染(核心新增) -->
|
<!-- 2. 图片 + 文件 渲染(核心新增) -->
|
||||||
@@ -251,6 +233,43 @@
|
|||||||
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- ===== 消息详情弹窗 ===== -->
|
||||||
|
<view v-if="showMessageModal" class="ncd-overlay" @click="closeMessageDetail">
|
||||||
|
<view class="ncd-card message-detail-card" @click.stop>
|
||||||
|
<view class="ncd-header">
|
||||||
|
<text class="ncd-title-zh">消息详情</text>
|
||||||
|
<uni-icons type="closeempty" color="#ff0000" size="24" @click="closeMessageDetail"></uni-icons>
|
||||||
|
</view>
|
||||||
|
<scroll-view class="message-detail-content" scroll-y>
|
||||||
|
<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.url)">
|
||||||
|
<text class="link-text">{{ link.name }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="detail-empty">
|
||||||
|
<text>无可下载文件</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ===== 自定义下载提示(层级高于弹窗) ===== -->
|
||||||
|
<view v-if="downloadToast.show" class="download-toast-overlay" :class="downloadToast.show ? '' : ''">
|
||||||
|
<view class="download-toast-card" :class="'download-toast-' + downloadToast.type">
|
||||||
|
<view v-if="downloadToast.type === 'loading'" class="download-toast-icon">
|
||||||
|
<view class="toast-dot"></view>
|
||||||
|
<view class="toast-dot"></view>
|
||||||
|
<view class="toast-dot"></view>
|
||||||
|
</view>
|
||||||
|
<text v-else class="download-toast-icon">{{ downloadToast.type === 'success' ? '✓' : '✕' }}</text>
|
||||||
|
<text class="download-toast-text">{{ downloadToast.message }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -274,7 +293,8 @@
|
|||||||
createWorkspace,
|
createWorkspace,
|
||||||
createConversation,
|
createConversation,
|
||||||
getUserInfo,
|
getUserInfo,
|
||||||
getUserAvatar
|
getUserAvatar,
|
||||||
|
conversationMessageDownload
|
||||||
} from '@/utils/cloud-api.js'
|
} from '@/utils/cloud-api.js'
|
||||||
import {
|
import {
|
||||||
getToken,
|
getToken,
|
||||||
@@ -424,7 +444,12 @@
|
|||||||
content = escapeLoneUnderscores(content)
|
content = escapeLoneUnderscores(content)
|
||||||
// 先转换表格,再用 snarkdown 处理其他 Markdown
|
// 先转换表格,再用 snarkdown 处理其他 Markdown
|
||||||
content = convertMarkdownTable(content)
|
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
|
return html
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('解析失败', e)
|
console.error('解析失败', e)
|
||||||
@@ -693,31 +718,158 @@
|
|||||||
|
|
||||||
// 消息详情弹窗
|
// 消息详情弹窗
|
||||||
const showMessageModal = ref(false)
|
const showMessageModal = ref(false)
|
||||||
const detailMessageContent = ref('')
|
|
||||||
const detailLinks = 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 extractLinks = (text) => {
|
||||||
const regex = /https?:\/\/[^\s<>"{}|\\^`[\]]+/gi
|
const seen = new Set()
|
||||||
const matches = text.match(regex)
|
const result = []
|
||||||
return matches ? [...new Set(matches)] : []
|
// 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) => {
|
const showMessageDetail = (message) => {
|
||||||
detailMessageContent.value = message.content || ''
|
detailLinks.value = extractLinks(message.content || '')
|
||||||
detailLinks.value = extractLinks(detailMessageContent.value)
|
|
||||||
showMessageModal.value = true
|
showMessageModal.value = true
|
||||||
}
|
}
|
||||||
const closeMessageDetail = () => {
|
const closeMessageDetail = () => {
|
||||||
showMessageModal.value = false
|
showMessageModal.value = false
|
||||||
detailMessageContent.value = ''
|
|
||||||
detailLinks.value = []
|
detailLinks.value = []
|
||||||
}
|
}
|
||||||
const openLink = (url) => {
|
const downloadToast = ref({
|
||||||
// #ifdef APP-PLUS
|
show: false,
|
||||||
plus.runtime.openURL(url)
|
type: 'loading',
|
||||||
// #endif
|
message: ''
|
||||||
// #ifdef H5
|
})
|
||||||
window.open(url, '_blank')
|
let downloadToastTimer = null
|
||||||
// #endif
|
|
||||||
|
const showDownloadToast = (type, message, duration) => {
|
||||||
|
if (downloadToastTimer) clearTimeout(downloadToastTimer)
|
||||||
|
downloadToast.value = {
|
||||||
|
show: true,
|
||||||
|
type,
|
||||||
|
message
|
||||||
}
|
}
|
||||||
|
if (duration > 0) {
|
||||||
|
downloadToastTimer = setTimeout(() => {
|
||||||
|
downloadToast.value = {
|
||||||
|
show: false,
|
||||||
|
type: 'loading',
|
||||||
|
message: ''
|
||||||
|
}
|
||||||
|
}, duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const hideDownloadToast = () => {
|
||||||
|
if (downloadToastTimer) clearTimeout(downloadToastTimer)
|
||||||
|
downloadToast.value = {
|
||||||
|
show: false,
|
||||||
|
type: 'loading',
|
||||||
|
message: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openLink = (url) => {
|
||||||
|
downloadAndHandle(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadAndHandle = async (url) => {
|
||||||
|
try {
|
||||||
|
showDownloadToast('loading', '下载中...', 0)
|
||||||
|
const workspaceId = uni.getStorageSync('workspace_id') || ''
|
||||||
|
const result = await conversationMessageDownload(userToken.value, workspaceId, url)
|
||||||
|
const downloadUrl = typeof result === 'string' ? result : (result?.url || result?.download_url || '')
|
||||||
|
if (!downloadUrl) {
|
||||||
|
showDownloadToast('error', '获取下载地址失败', 2500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.downloadFile({
|
||||||
|
url: downloadUrl,
|
||||||
|
success: (downloadRes) => {
|
||||||
|
if (downloadRes.statusCode === 200) {
|
||||||
|
hideDownloadToast()
|
||||||
|
uni.openDocument({
|
||||||
|
filePath: downloadRes.tempFilePath,
|
||||||
|
showMenu: true,
|
||||||
|
fail: () => {
|
||||||
|
showDownloadToast('error', '无法打开此文件', 2500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showDownloadToast('error', '下载失败', 2500)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
showDownloadToast('error', '下载失败', 2500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
showDownloadToast('error', '下载失败: ' + (err.message || err), 2500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const sendMessage = async () => {
|
const sendMessage = async () => {
|
||||||
const message = textMessage.value.trim()
|
const message = textMessage.value.trim()
|
||||||
@@ -1181,7 +1333,7 @@
|
|||||||
const takeConversationMessages = async () => {
|
const takeConversationMessages = async () => {
|
||||||
try {
|
try {
|
||||||
allmessages.value = await getConversationMessages(userToken.value, currentSessionId.value) || [];
|
allmessages.value = await getConversationMessages(userToken.value, currentSessionId.value) || [];
|
||||||
console.log("获取到的所有消息:", JSON.stringify(allmessages.value) );
|
// console.log("获取到的所有消息:", JSON.stringify(allmessages.value) );
|
||||||
// currentMessages.value = allmessages.value.slice(-pageInfoNumber);
|
// currentMessages.value = allmessages.value.slice(-pageInfoNumber);
|
||||||
// 数据获取后执行滚动
|
// 数据获取后执行滚动
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
@@ -1395,6 +1547,13 @@
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
margin: 8rpx 0;
|
margin: 8rpx 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 气泡内的内联链接(替代 <a> 标签,点击由 JS 拦截处理) */
|
||||||
|
.chat-inline-link {
|
||||||
|
color: #3b86ff !important;
|
||||||
|
text-decoration: underline;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 流式气泡中的加载动画 */
|
/* 流式气泡中的加载动画 */
|
||||||
@@ -1462,4 +1621,112 @@
|
|||||||
color: #007aff;
|
color: #007aff;
|
||||||
word-break: break-all;
|
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;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10001;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-toast-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24rpx 40rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.15);
|
||||||
|
animation: toastFadeIn 0.25s ease;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes toastFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-toast-loading {
|
||||||
|
background: rgba(40, 40, 60, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-toast-success {
|
||||||
|
background: rgba(40, 60, 40, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-toast-error {
|
||||||
|
background: rgba(60, 30, 30, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-toast-icon {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #fff;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-toast-card.download-toast-loading .download-toast-icon {
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-dot {
|
||||||
|
width: 12rpx;
|
||||||
|
height: 12rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes toastDotBlink {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: scale(0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-toast-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -799,8 +799,8 @@
|
|||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
if (downloadRes.statusCode === 200) {
|
if (downloadRes.statusCode === 200) {
|
||||||
const tempPath = downloadRes.tempFilePath;
|
const tempPath = downloadRes.tempFilePath;
|
||||||
// 下载成功,保存到 yxd 目录
|
// 下载成功,直接打开文件
|
||||||
saveFileToYxd(file.name, tempPath);
|
openFile(tempPath);
|
||||||
} else {
|
} else {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '下载失败',
|
title: '下载失败',
|
||||||
@@ -826,104 +826,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 将下载的文件保存到外部存储 yxd 目录
|
|
||||||
const saveFileToYxd = (fileName, tempPath) => {
|
|
||||||
// #ifdef APP-PLUS
|
|
||||||
// App 端:复制文件到 /storage/emulated/0/yxd/
|
|
||||||
const YXD_PATH = '/storage/emulated/0/yxd/';
|
|
||||||
|
|
||||||
const ensureYxdDir = (callback) => {
|
|
||||||
plus.io.resolveLocalFileSystemURL(YXD_PATH, (dirEntry) => {
|
|
||||||
// 目录已存在
|
|
||||||
callback(dirEntry);
|
|
||||||
}, () => {
|
|
||||||
// 目录不存在,先创建
|
|
||||||
plus.io.resolveLocalFileSystemURL('/storage/emulated/0/', (rootEntry) => {
|
|
||||||
rootEntry.getDirectory('yxd', { create: true }, (dirEntry) => {
|
|
||||||
callback(dirEntry);
|
|
||||||
}, (err) => {
|
|
||||||
console.error('创建 yxd 目录失败:', JSON.stringify(err));
|
|
||||||
uni.showToast({ title: '创建目录失败', icon: 'error' });
|
|
||||||
openFileConfirm(fileName, tempPath);
|
|
||||||
});
|
|
||||||
}, (err) => {
|
|
||||||
console.error('访问外部存储根目录失败:', JSON.stringify(err));
|
|
||||||
uni.showToast({ title: '无法访问存储目录', icon: 'error' });
|
|
||||||
openFileConfirm(fileName, tempPath);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
ensureYxdDir((yxdDirEntry) => {
|
|
||||||
plus.io.resolveLocalFileSystemURL(tempPath, (fileEntry) => {
|
|
||||||
fileEntry.copyTo(
|
|
||||||
yxdDirEntry,
|
|
||||||
fileName,
|
|
||||||
() => {
|
|
||||||
const targetPath = YXD_PATH + fileName;
|
|
||||||
console.log('文件已保存到:', targetPath);
|
|
||||||
openFileConfirm(fileName, targetPath);
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
console.error('复制文件失败:', JSON.stringify(err));
|
|
||||||
openFileConfirm(fileName, tempPath);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}, (err) => {
|
|
||||||
console.error('读取临时文件失败:', JSON.stringify(err));
|
|
||||||
openFileConfirm(fileName, tempPath);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// #endif
|
|
||||||
// #ifndef APP-PLUS
|
|
||||||
// 小程序/H5 端:使用 uni.saveFile 保存
|
|
||||||
uni.saveFile({
|
|
||||||
tempFilePath: tempPath,
|
|
||||||
success: (saveRes) => {
|
|
||||||
openFileConfirm(fileName, saveRes.savedFilePath);
|
|
||||||
},
|
|
||||||
fail: () => {
|
|
||||||
// saveFile 失败则直接确认打开
|
|
||||||
openFileConfirm(fileName, tempPath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// #endif
|
|
||||||
};
|
|
||||||
|
|
||||||
// 下载成功后确认是否打开文件
|
|
||||||
const openFileConfirm = (fileName, filePath) => {
|
|
||||||
uni.showActionSheet({
|
|
||||||
title: `${fileName} 下载完成`,
|
|
||||||
itemList: ['打开文件', '选择其他应用打开', '仅保存'],
|
|
||||||
success: (res) => {
|
|
||||||
switch (res.tapIndex) {
|
|
||||||
case 0:
|
|
||||||
// 打开文件(系统默认应用)
|
|
||||||
openFile(filePath);
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
// 选择其他应用打开(showMenu 会弹出分享/打开方式菜单)
|
|
||||||
openFileWithMenu(filePath);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
// 仅保存,不打开
|
|
||||||
uni.showToast({
|
|
||||||
title: '文件已保存到 yxd 目录',
|
|
||||||
icon: 'success'
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fail: () => {
|
|
||||||
// 用户取消,默认不打开
|
|
||||||
uni.showToast({
|
|
||||||
title: '文件已保存到 yxd 目录',
|
|
||||||
icon: 'success'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 使用系统默认应用打开
|
// 使用系统默认应用打开
|
||||||
const openFile = (filePath) => {
|
const openFile = (filePath) => {
|
||||||
uni.openDocument({
|
uni.openDocument({
|
||||||
|
|||||||
@@ -392,8 +392,8 @@
|
|||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
if (downloadRes.statusCode === 200) {
|
if (downloadRes.statusCode === 200) {
|
||||||
const tempPath = downloadRes.tempFilePath;
|
const tempPath = downloadRes.tempFilePath;
|
||||||
// 下载成功,保存到 yxd 目录
|
// 下载成功,直接打开文件
|
||||||
saveFileToYxd(fileName, tempPath);
|
openFile(tempPath);
|
||||||
} else {
|
} else {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '下载失败',
|
title: '下载失败',
|
||||||
@@ -420,104 +420,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 将下载的文件保存到外部存储 yxd 目录
|
|
||||||
const saveFileToYxd = (fileName, tempPath) => {
|
|
||||||
// #ifdef APP-PLUS
|
|
||||||
// App 端:复制文件到 /storage/emulated/0/yxd/
|
|
||||||
const YXD_PATH = '/storage/emulated/0/yxd/';
|
|
||||||
|
|
||||||
const ensureYxdDir = (callback) => {
|
|
||||||
plus.io.resolveLocalFileSystemURL(YXD_PATH, (dirEntry) => {
|
|
||||||
callback(dirEntry);
|
|
||||||
}, () => {
|
|
||||||
plus.io.resolveLocalFileSystemURL('/storage/emulated/0/', (rootEntry) => {
|
|
||||||
rootEntry.getDirectory('yxd', {
|
|
||||||
create: true
|
|
||||||
}, (dirEntry) => {
|
|
||||||
callback(dirEntry);
|
|
||||||
}, (err) => {
|
|
||||||
console.error('创建 yxd 目录失败:', JSON.stringify(err));
|
|
||||||
uni.showToast({
|
|
||||||
title: '创建目录失败',
|
|
||||||
icon: 'error'
|
|
||||||
});
|
|
||||||
openFileConfirm(fileName, tempPath);
|
|
||||||
});
|
|
||||||
}, (err) => {
|
|
||||||
console.error('访问外部存储根目录失败:', JSON.stringify(err));
|
|
||||||
uni.showToast({
|
|
||||||
title: '无法访问存储目录',
|
|
||||||
icon: 'error'
|
|
||||||
});
|
|
||||||
openFileConfirm(fileName, tempPath);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
ensureYxdDir((yxdDirEntry) => {
|
|
||||||
plus.io.resolveLocalFileSystemURL(tempPath, (fileEntry) => {
|
|
||||||
fileEntry.copyTo(
|
|
||||||
yxdDirEntry,
|
|
||||||
fileName,
|
|
||||||
() => {
|
|
||||||
const targetPath = YXD_PATH + fileName;
|
|
||||||
console.log('文件已保存到:', targetPath);
|
|
||||||
openFileConfirm(fileName, targetPath);
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
console.error('复制文件失败:', JSON.stringify(err));
|
|
||||||
openFileConfirm(fileName, tempPath);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}, (err) => {
|
|
||||||
console.error('读取临时文件失败:', JSON.stringify(err));
|
|
||||||
openFileConfirm(fileName, tempPath);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// #endif
|
|
||||||
// #ifndef APP-PLUS
|
|
||||||
uni.saveFile({
|
|
||||||
tempFilePath: tempPath,
|
|
||||||
success: (saveRes) => {
|
|
||||||
openFileConfirm(fileName, saveRes.savedFilePath);
|
|
||||||
},
|
|
||||||
fail: () => {
|
|
||||||
openFileConfirm(fileName, tempPath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// #endif
|
|
||||||
};
|
|
||||||
|
|
||||||
// 下载成功后确认是否打开文件
|
|
||||||
const openFileConfirm = (fileName, filePath) => {
|
|
||||||
uni.showActionSheet({
|
|
||||||
title: `${fileName} 下载完成`,
|
|
||||||
itemList: ['打开文件', '选择其他应用打开', '仅保存'],
|
|
||||||
success: (res) => {
|
|
||||||
switch (res.tapIndex) {
|
|
||||||
case 0:
|
|
||||||
openFile(filePath);
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
openFileWithMenu(filePath);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
uni.showToast({
|
|
||||||
title: '文件已保存到 yxd 目录',
|
|
||||||
icon: 'success'
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fail: () => {
|
|
||||||
uni.showToast({
|
|
||||||
title: '文件已保存到 yxd 目录',
|
|
||||||
icon: 'success'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 使用系统默认应用打开
|
// 使用系统默认应用打开
|
||||||
const openFile = (filePath) => {
|
const openFile = (filePath) => {
|
||||||
uni.openDocument({
|
uni.openDocument({
|
||||||
|
|||||||
713
unpackage/dist/dev/app-plus/app-service.js
vendored
713
unpackage/dist/dev/app-plus/app-service.js
vendored
File diff suppressed because it is too large
Load Diff
103
unpackage/dist/dev/app-plus/pages/Chat/Chat.css
vendored
103
unpackage/dist/dev/app-plus/pages/Chat/Chat.css
vendored
@@ -957,7 +957,7 @@ to { opacity: 1;
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: rgba(30, 30, 50, 0.35);
|
background-color: rgba(30, 30, 50, 0.35);
|
||||||
z-index: 1000;
|
z-index: 9999;
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
-webkit-backdrop-filter: blur(8px);
|
-webkit-backdrop-filter: blur(8px);
|
||||||
animation: fadeIn-5eb7b895 0.25s ease;
|
animation: fadeIn-5eb7b895 0.25s ease;
|
||||||
@@ -1616,12 +1616,14 @@ to { opacity: 1; transform: translateY(0);
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border-radius: 1.25rem;
|
border-radius: 1.25rem;
|
||||||
|
padding: 1.25rem 1.125rem;
|
||||||
}
|
}
|
||||||
.message-detail-content[data-v-5eb7b895] {
|
.message-detail-content[data-v-5eb7b895] {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 6.25rem;
|
||||||
max-height: 60vh;
|
max-height: 60vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0.625rem 0;
|
padding: 0.5rem 0;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
color: #333333;
|
color: #333333;
|
||||||
@@ -1655,6 +1657,7 @@ to { opacity: 1; transform: translateY(0);
|
|||||||
/* 文章场景相关 */
|
/* 文章场景相关 */
|
||||||
.chat-content {
|
.chat-content {
|
||||||
/* 普通正文 */
|
/* 普通正文 */
|
||||||
|
/* 气泡内的内联链接(替代 <a> 标签,点击由 JS 拦截处理) */
|
||||||
}
|
}
|
||||||
.chat-content h1 {
|
.chat-content h1 {
|
||||||
font-size: 1.3125rem;
|
font-size: 1.3125rem;
|
||||||
@@ -1685,6 +1688,11 @@ to { opacity: 1; transform: translateY(0);
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
margin: 0.25rem 0;
|
margin: 0.25rem 0;
|
||||||
}
|
}
|
||||||
|
.chat-content .chat-inline-link {
|
||||||
|
color: #3b86ff !important;
|
||||||
|
text-decoration: underline;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
/* 流式气泡中的加载动画 */
|
/* 流式气泡中的加载动画 */
|
||||||
.bubble-loading-dots {
|
.bubble-loading-dots {
|
||||||
@@ -1740,3 +1748,94 @@ to { opacity: 1; transform: translateY(0);
|
|||||||
color: #007aff;
|
color: #007aff;
|
||||||
word-break: break-all;
|
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 {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10001;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.download-toast-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
box-shadow: 0 0.375rem 1.25rem rgba(0, 0, 0, 0.15);
|
||||||
|
animation: toastFadeIn 0.25s ease;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
@keyframes toastFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.85);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.download-toast-loading {
|
||||||
|
background: rgba(40, 40, 60, 0.92);
|
||||||
|
}
|
||||||
|
.download-toast-success {
|
||||||
|
background: rgba(40, 60, 40, 0.92);
|
||||||
|
}
|
||||||
|
.download-toast-error {
|
||||||
|
background: rgba(60, 30, 30, 0.92);
|
||||||
|
}
|
||||||
|
.download-toast-icon {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: #fff;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.download-toast-card.download-toast-loading .download-toast-icon {
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
.toast-dot {
|
||||||
|
width: 0.375rem;
|
||||||
|
height: 0.375rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
@keyframes toastDotBlink {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: scale(0.75);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.download-toast-text {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
@@ -1681,3 +1681,40 @@ export const dbTransferToTeam = async (token, databaseId, teamId) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 会话相关
|
||||||
|
// 下载消息文件
|
||||||
|
export const conversationMessageDownload = (token, workspacesId, localUrl) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.request({
|
||||||
|
url: `${BASE_URL}/cloud_api/phone/conversation/message/download`,
|
||||||
|
method: "POST",
|
||||||
|
header: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
"access_token": token,
|
||||||
|
"workspace_id": workspacesId,
|
||||||
|
"url": localUrl
|
||||||
|
},
|
||||||
|
success: (res) => {
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
const respond = res.data;
|
||||||
|
console.log(respond);
|
||||||
|
if (respond.local_success) {
|
||||||
|
resolve(respond.url || respond.download_url || respond.data || respond);
|
||||||
|
} else {
|
||||||
|
const msg = '下载消息文件出错啦';
|
||||||
|
reject(msg);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(`下载消息文件失败:${res.statusCode}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
const msg = err.errMsg || '网络错误';
|
||||||
|
reject(msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user