This commit is contained in:
hy
2026-08-03 17:09:31 +08:00
26 changed files with 2546 additions and 370 deletions

View File

@@ -24,7 +24,7 @@
</view> </view>
<view class="new-chat-btn" @click="goContactPages"> <view class="new-chat-btn" @click="goContactPages">
<text>+ 聊天列表</text> <text>+ 更多聊天列表</text>
</view> </view>
</view> </view>
<view class="chat-history"> <view class="chat-history">

View File

@@ -1,5 +1,5 @@
{ {
"name" : "test1", "name" : "宇恒一号",
"appid" : "__UNI__923FD9E", "appid" : "__UNI__923FD9E",
"description" : "", "description" : "",
"versionName" : "1.1.1", "versionName" : "1.1.1",

View File

@@ -105,11 +105,13 @@
font-size: 36rpx; font-size: 36rpx;
font-weight: 700; font-weight: 700;
color: #1a1a2e; color: #1a1a2e;
align-items: center;
} }
.ncd-header uni-icons { .ncd-header uni-icons {
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
align-items: center;
} }
.ncd-options { .ncd-options {
@@ -325,6 +327,62 @@
color: #ff4d4f; color: #ff4d4f;
} }
/* ==========WYXD 文件入口横幅========== */
.wyxd-banner {
display: flex;
align-items: center;
padding: 14rpx 24rpx;
margin: 6rpx 20rpx;
background: linear-gradient(135deg, #e8f4fd, #f0e6ff);
border: 1.5rpx solid rgba(120, 120, 220, 0.25);
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(100, 100, 200, 0.08);
}
.wyxd-banner:active {
background: linear-gradient(135deg, #d8eafb, #e6d8ff);
transform: scale(0.98);
}
.wyxd-banner-icon {
font-size: 32rpx;
color: #6c6ce0;
margin-right: 12rpx;
}
.wyxd-banner-text {
flex: 1;
font-size: 26rpx;
font-weight: 600;
color: #4a4ab8;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wyxd-banner-arrow {
font-size: 24rpx;
color: #9999cc;
}
.wyxd-banner-close {
width: 36rpx;
height: 36rpx;
line-height: 36rpx;
text-align: center;
font-size: 28rpx;
color: #9999cc;
border-radius: 50%;
background: rgba(100, 100, 200, 0.08);
margin-left: 8rpx;
flex-shrink: 0;
}
.wyxd-banner-close:active {
background: rgba(100, 100, 200, 0.18);
color: #6c6ce0;
}
/* ==========聊天主体部分========== */ /* ==========聊天主体部分========== */
.main-chat { .main-chat {
display: flex; display: flex;

View File

@@ -64,6 +64,15 @@
</view> </view>
</view> </view>
<!-- WYXD 文件入口从工作区 AI 修改进入时显示 -->
<view v-if="hasWyxdFile" class="wyxd-banner">
<view class="wyxd-banner-icon iconfont icon-wenjian"></view>
<view class="wyxd-banner-text" @click="goWyxdViewer">查看: {{ wyxdFileName }}</view>
<view class="wyxd-banner-arrow" @click="goWyxdViewer">&rsaquo;</view>
<view class="wyxd-banner-close" @click.stop="hasWyxdFile = false">&times;</view>
</view>
<view class="main-chat"> <view class="main-chat">
<scroll-view class="chat-messages" direction="vertical" scroll-y :scroll-into-view="scrollToView" <scroll-view class="chat-messages" direction="vertical" scroll-y :scroll-into-view="scrollToView"
:upper-threshold="0" :scroll-with-animation="!isThinking" @scroll="onScroll"> :upper-threshold="0" :scroll-with-animation="!isThinking" @scroll="onScroll">
@@ -210,10 +219,6 @@
</view> </view>
</view> </view>
<!-- 底部锚点两个交替切换确保流式时每次都能触发滚动到底部 --> <!-- 底部锚点两个交替切换确保流式时每次都能触发滚动到底部 -->
<!-- <view>
<view id="chat-bottom-a" style="height: 0rpx;width: 1rpx;"></view>
<view id="chat-bottom-b" style="height: 0rpx;width: 1rpx;"></view>
</view> -->
<view id="chat-bottom-a" style="height: 1rpx;"></view> <view id="chat-bottom-a" style="height: 1rpx;"></view>
<view id="chat-bottom-b" style="height: 1rpx;"></view> <view id="chat-bottom-b" style="height: 1rpx;"></view>
</scroll-view> </scroll-view>
@@ -297,6 +302,7 @@
watch watch
} from 'vue'; } from 'vue';
import { import {
onLoad,
onShow, onShow,
onUnload onUnload
} from '@dcloudio/uni-app'; } from '@dcloudio/uni-app';
@@ -340,6 +346,7 @@
chooseFile chooseFile
} from '@/uni_modules/lime-choose-file' } from '@/uni_modules/lime-choose-file'
import socketManager from '../../utils/socket'; import socketManager from '../../utils/socket';
import { openFile as openFileNative, downloadAndOpen } from '@/utils/fileOpener.js'
const friendSocketStore = useFriendSocketStore() const friendSocketStore = useFriendSocketStore()
// 连接实时通讯eriendsocket // 连接实时通讯eriendsocket
@@ -360,6 +367,13 @@
// 聊天类型ai好友群聊 // 聊天类型ai好友群聊
const ChatType = ref(0) const ChatType = ref(0)
// WYXD 关联数据(从 WorkSpace 的「AI修改」进入时携带
const wyxdWorkspaceId = ref('')
const wyxdFilePath = ref('')
const wyxdFileName = ref('')
const wyxdUserToken = ref('')
const hasWyxdFile = ref(false)
// marked.setOptions({ // marked.setOptions({
// breaks: true, // 自动把换行符转成 <br> 换行 // breaks: true, // 自动把换行符转成 <br> 换行
@@ -453,6 +467,35 @@
return text 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 '<span class="chat-inline-link chat-download-btn" data-url="' + href + '">'
+ '<span class="chat-dl-icon">' + fileInfo.emoji + '</span>'
+ '<span class="chat-dl-name">' + name + '</span>'
+ '</span>'
}
// 支持的文件扩展名(用于原始 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节点 // Markdown转HTML节点
const pareseMarkdown = (content) => { const pareseMarkdown = (content) => {
if (!content) return '' if (!content) return ''
@@ -462,14 +505,60 @@
try { try {
// 转义单词中的单下划线,防止 snarkdown 误解析为斜体 // 转义单词中的单下划线,防止 snarkdown 误解析为斜体
content = escapeLoneUnderscores(content) 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 // 先转换表格,再用 snarkdown 处理其他 Markdown
content = convertMarkdownTable(content) content = convertMarkdownTable(content)
let html = snarkdown(content) let html = snarkdown(content)
// 将 <a href="..."> 替换为自定义 span避免 WebView 触发默认下载
// 步骤1暂存 <a> 标签,避免 raw URL 替换误伤 href 内的链接
const linkPlaceholders = []
html = html.replace( html = html.replace(
/<a\s+href="([^"]*)"[^>]*>(.*?)<\/a>/gi, /<a\s+href="([^"]*)"[^>]*>(.*?)<\/a>/gi,
'<span class="chat-inline-link" data-url="$1">$2</span>' (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恢复 <a> 标签并分类处理
html = html.replace(/\x00LINK_(\d+)\x00/g, (_, idx) => {
const { href, innerText } = linkPlaceholders[idx]
if (isDownloadLink(href)) {
const plainText = innerText
.replace(/<[^>]*>/g, ' ')
.replace(/&nbsp;/gi, ' ')
.replace(/\s+/g, ' ')
.trim()
return renderDownloadBtn(href, plainText)
}
// 普通链接保持原有样式
return '<span class="chat-inline-link" data-url="' + href + '">' + innerText + '</span>'
})
return html return html
} catch (e) { } catch (e) {
console.error('解析失败', e) console.error('解析失败', e)
@@ -510,15 +599,7 @@
} }
// 打开文件 // 打开文件
const openFile = (url) => { const openFile = (url) => {
uni.downloadFile({ downloadAndOpen(url);
url: url,
success: (res) => {
uni.openDocument({
filePath: res.tempFilePath,
showMenu: true
})
}
})
} }
// 文件大小格式化 // 文件大小格式化
const formatFileSize = (size) => { const formatFileSize = (size) => {
@@ -625,6 +706,17 @@
}) })
} }
// 跳转到 WYXD 查看器(强制重新下载,不走缓存)
const goWyxdViewer = () => {
uni.navigateTo({
url: '/pages/WYXD/WYXD?workspaceId=' + encodeURIComponent(wyxdWorkspaceId.value)
+ '&filePath=' + encodeURIComponent(wyxdFilePath.value)
+ '&fileName=' + encodeURIComponent(wyxdFileName.value)
+ '&userToken=' + encodeURIComponent(wyxdUserToken.value)
+ '&forceDownload=1'
})
}
// 跳转到云端数据库 // 跳转到云端数据库
const goCloudDatabase = () => { const goCloudDatabase = () => {
// 保存当前会话id确保从工作区返回时能恢复 // 保存当前会话id确保从工作区返回时能恢复
@@ -872,6 +964,50 @@
} }
return urlStr 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) => { const showMessageDetail = (message) => {
detailLinks.value = extractLinks(message.content || '') detailLinks.value = extractLinks(message.content || '')
showMessageModal.value = true showMessageModal.value = true
@@ -932,12 +1068,8 @@
success: (downloadRes) => { success: (downloadRes) => {
if (downloadRes.statusCode === 200) { if (downloadRes.statusCode === 200) {
hideDownloadToast() hideDownloadToast()
uni.openDocument({ openFileNative(downloadRes.tempFilePath, () => {
filePath: downloadRes.tempFilePath, showDownloadToast('error', '无法打开此文件', 2500)
showMenu: true,
fail: () => {
showDownloadToast('error', '无法打开此文件', 2500)
}
}) })
} else { } else {
showDownloadToast('error', '下载失败', 2500) showDownloadToast('error', '下载失败', 2500)
@@ -1185,14 +1317,16 @@
if (reason === 'pc offline') { if (reason === 'pc offline') {
isThinking.value = false isThinking.value = false
socketStore.authFailReason = '' socketStore.authFailReason = ''
nextTick(() => { uni.showToast({
uni.showModal({ title: 'PC端不在线请先登录',
title: '提示', icon: 'none',
content: 'PC端不在线不可聊天', duration: 500
showCancel: false,
confirmText: '知道了'
})
}) })
setTimeout(() => {
uni.reLaunch({
url: '/pages/Login/Login'
})
}, 500)
} }
}) })
@@ -1387,7 +1521,8 @@
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) );
console.log("获取到的所有消息:", allmessages.value);
// currentMessages.value = allmessages.value.slice(-pageInfoNumber); // currentMessages.value = allmessages.value.slice(-pageInfoNumber);
// 数据获取后执行滚动 // 数据获取后执行滚动
scrollToBottom(); scrollToBottom();
@@ -1505,6 +1640,7 @@
if (oldId && oldId !== newId) { if (oldId && oldId !== newId) {
isLoadingMessages.value = true isLoadingMessages.value = true
allmessages.value = [] allmessages.value = []
hasWyxdFile.value = false
} }
switch (ChatType.value) { switch (ChatType.value) {
case 0: case 0:
@@ -1528,6 +1664,17 @@
immediate: true immediate: true
}) })
// 接收 WYXD 参数从工作区「AI修改」进入时由 navigateTo 传入)
onLoad((options) => {
if (options.wyxdFilePath) {
wyxdWorkspaceId.value = options.wyxdWorkspaceId ? decodeURIComponent(options.wyxdWorkspaceId) : ''
wyxdFilePath.value = decodeURIComponent(options.wyxdFilePath)
wyxdFileName.value = options.wyxdFileName ? decodeURIComponent(options.wyxdFileName) : '未命名文档'
wyxdUserToken.value = options.wyxdUserToken ? decodeURIComponent(options.wyxdUserToken) : ''
hasWyxdFile.value = true
}
})
onMounted(() => { onMounted(() => {
// currentSessionId.value = getCurrentSessionId() // currentSessionId.value = getCurrentSessionId()
}) })
@@ -1648,6 +1795,43 @@
text-decoration: underline; text-decoration: underline;
word-break: break-all; 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;
}
}
} }
/* 流式气泡中的加载动画 */ /* 流式气泡中的加载动画 */

View File

@@ -475,6 +475,7 @@
uploadCloudDbFile, uploadCloudDbFile,
deleteCloudDb deleteCloudDb
} from '@/utils/cloud-api.js'; } from '@/utils/cloud-api.js';
import { openFile as openFileNative } from '@/utils/fileOpener.js'
const userToken = ref(''); const userToken = ref('');
const userId = ref(''); const userId = ref('');
@@ -826,19 +827,10 @@
} }
}; };
// 使用系统默认应用打开 // 打开文件(弹出应用选择菜单)
const openFile = (filePath) => { const openFile = (filePath) => {
uni.openDocument({ openFileNative(filePath, () => {
filePath: filePath, openFileWithMenu(filePath);
success: () => uni.showToast({
title: '打开成功',
icon: 'success'
}),
fail: (err) => {
console.error('打开文件失败:', err);
// 如果默认应用打开失败,尝试弹出选择菜单
openFileWithMenu(filePath);
}
}); });
}; };

View File

@@ -32,9 +32,14 @@
<!-- 幻灯片 --> <!-- 幻灯片 -->
<view v-else class="wyxd-slide-area"> <view v-else class="wyxd-slide-area">
<scroll-view class="wyxd-slide-scroll" scroll-y :show-scrollbar="false"> <swiper class="wyxd-swiper" :current="currentIndex"
<view class="wyxd-slide-card" v-html="currentSlide.html"></view> @change="onSwiperChange" :duration="250" easing-function="easeOutCubic">
</scroll-view> <swiper-item v-for="(slide, idx) in slides" :key="idx">
<scroll-view class="wyxd-slide-scroll" scroll-y :show-scrollbar="false">
<view class="wyxd-slide-card" v-html="slide.html"></view>
</scroll-view>
</swiper-item>
</swiper>
</view> </view>
</view> </view>
@@ -89,10 +94,12 @@ const statusText = computed(() => {
const displayName = computed(() => fileName.value || '未命名文档') const displayName = computed(() => fileName.value || '未命名文档')
const currentSlide = computed(() => slides.value[currentIndex.value] || { html: '' })
const pad = (n) => String(n).padStart(2, '0') const pad = (n) => String(n).padStart(2, '0')
const onSwiperChange = (e) => {
currentIndex.value = e.detail.current
}
const goBack = () => { const goBack = () => {
uni.navigateBack() uni.navigateBack()
} }
@@ -181,7 +188,8 @@ onLoad((options) => {
userToken: userToken.value userToken: userToken.value
}) })
if (isWyxdCached(workspaceId.value, filePath.value)) { // forceDownload 强制重新下载不走缓存
if (options.forceDownload !== '1' && isWyxdCached(workspaceId.value, filePath.value)) {
loadFromCache() loadFromCache()
} else { } else {
startDownload() startDownload()
@@ -247,6 +255,11 @@ onLoad((options) => {
/* padding: 20rpx; */ /* padding: 20rpx; */
} }
.wyxd-swiper {
width: 100%;
height: 100%;
}
.wyxd-slide-scroll { .wyxd-slide-scroll {
height: 100%; height: 100%;
background: #fff; background: #fff;

View File

@@ -380,3 +380,164 @@
.workspace-footer .iconfont { .workspace-footer .iconfont {
font-size: 40rpx !important; font-size: 40rpx !important;
} }
/* ===== 标签切换 ===== */
.workspace-tabs {
display: flex;
width: 100%;
padding: 0 24rpx;
box-sizing: border-box;
gap: 20rpx;
}
.tab-item {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 18rpx 0;
font-size: 28rpx;
font-weight: 500;
color: #999;
border-bottom: 4rpx solid transparent;
transition: all 0.2s ease;
position: relative;
}
.tab-item.active {
color: #6c63ff;
border-bottom-color: #6c63ff;
}
.tab-badge {
margin-left: 8rpx;
background: #ff5e57;
color: #fff;
font-size: 20rpx;
padding: 2rpx 10rpx;
border-radius: 20rpx;
line-height: 1.4;
}
/* ===== 下载列表 ===== */
.download-content {
flex: 1;
overflow: auto;
}
.download-empty {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: 200rpx;
color: #bbb;
font-size: 28rpx;
}
.download-empty .iconfont {
font-size: 140rpx;
color: #ddd;
margin-bottom: 20rpx;
}
.download-list {
padding: 20rpx 24rpx;
}
.download-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12rpx 8rpx 20rpx;
font-size: 24rpx;
color: #999;
}
.clear-btn {
color: #ff5e57;
font-size: 24rpx;
font-weight: 500;
padding: 4rpx 12rpx;
}
.download-item {
display: flex;
align-items: center;
padding: 24rpx 20rpx;
background: rgba(255, 255, 255, 0.85);
border-radius: 20rpx;
margin-bottom: 16rpx;
transition: all 0.15s ease;
}
.download-item:active {
transform: scale(0.98);
background: rgba(255, 255, 255, 0.95);
}
.di-icon {
width: 80rpx;
height: 80rpx;
display: flex;
justify-content: center;
align-items: center;
background: rgba(108, 99, 255, 0.08);
border-radius: 18rpx;
margin-right: 20rpx;
flex-shrink: 0;
}
.di-icon .iconfont {
font-size: 40rpx;
color: #6c63ff;
}
.di-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 6rpx;
min-width: 0;
}
.di-name {
font-size: 28rpx;
font-weight: 500;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.di-meta {
font-size: 22rpx;
color: #999;
}
.di-path {
font-size: 20rpx;
color: #bbb;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.di-delete {
width: 56rpx;
height: 56rpx;
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.di-delete .iconfont {
font-size: 36rpx;
color: #ccc;
transition: color 0.15s ease;
}
.di-delete:active .iconfont {
color: #ff5e57;
}

View File

@@ -66,8 +66,20 @@
<view v-if="isSearchFocus" class="cancel-search" @click="handleSearchFocus">取消</view> <view v-if="isSearchFocus" class="cancel-search" @click="handleSearchFocus">取消</view>
</view> </view>
<!-- 标签切换 -->
<view class="workspace-tabs">
<view class="tab-item" :class="{ active: activeTab === 'workspace' }" @click="switchTab('workspace')">
工作区
</view>
<view class="tab-item" :class="{ active: activeTab === 'downloads' }" @click="switchTab('downloads')">
下载列表
<text v-if="downloadList.length > 0" class="tab-badge">{{ downloadList.length }}</text>
</view>
</view>
</view> </view>
<view class="workspace-content">
<!-- ===== 工作区文件 ===== -->
<view class="workspace-content" v-if="activeTab === 'workspace'">
<view class="folder-null" v-if="currentFolderList.length===0"> <view class="folder-null" v-if="currentFolderList.length===0">
<view class="iconfont icon-wenjianjia"></view> <view class="iconfont icon-wenjianjia"></view>
文件夹为空 文件夹为空
@@ -89,6 +101,34 @@
</view> </view>
</view> </view>
</view> </view>
<!-- ===== 下载列表 ===== -->
<view class="download-content" v-if="activeTab === 'downloads'">
<view v-if="downloadList.length === 0" class="download-empty">
<text class="iconfont icon-xiazai"></text>
<text>暂无下载文件</text>
</view>
<view v-else class="download-list">
<view class="download-header">
<text>已下载 {{ downloadList.length }} 个文件</text>
<text class="clear-btn" @click="handleClearDownloads">清空</text>
</view>
<view class="download-item" v-for="item in downloadList" :key="item.id" @click="openDownloadFile(item)">
<view class="di-icon">
<text class="iconfont" :class="getFileIcon(item.name)"></text>
</view>
<view class="di-info">
<text class="di-name">{{ item.name }}</text>
<text class="di-meta">{{ formatDownloadTime(item.time) }} · {{ formatDownloadSize(item.size) }}</text>
<text class="di-path">{{ item.displayPath || item.path }}</text>
</view>
<view class="di-delete" @click.stop="handleDeleteDownload(item.id)">
<text class="iconfont icon-del"></text>
</view>
</view>
</view>
</view>
<view v-if="isSelectFolder" class="workspace-footer" :class="{'folder-actions':selectFileList.length>0}"> <view v-if="isSelectFolder" class="workspace-footer" :class="{'folder-actions':selectFileList.length>0}">
<view class="iconfont icon-shangchuan" @click="handleUploadFile"></view> <view class="iconfont icon-shangchuan" @click="handleUploadFile"></view>
<!-- <view class="iconfont icon-fuzhi"></view> --> <!-- <view class="iconfont icon-fuzhi"></view> -->
@@ -121,11 +161,22 @@
workspaceUploadFileByURL, workspaceUploadFileByURL,
updateWorkspaceFileName updateWorkspaceFileName
} from '@/utils/cloud-api.js' } from '@/utils/cloud-api.js'
import { openFile as openFileNative } from '@/utils/fileOpener.js'
import {
getDownloadList,
saveDownload,
deleteDownload,
clearAllDownloads,
formatFileSize,
formatTime
} from '@/utils/downloadManager.js'
const UserToken = ref('') const UserToken = ref('')
const workspaceId = ref('') const workspaceId = ref('')
const navbarBeforeTitle = ref(''); const navbarBeforeTitle = ref('');
const navbarTitle = ref('工作区'); const navbarTitle = ref('工作区');
const activeTab = ref('workspace')
const downloadList = ref([])
// 选择文件模式 // 选择文件模式
const isSelectFolder = ref(false); const isSelectFolder = ref(false);
@@ -284,20 +335,35 @@
const handleLongPress = (folder) => { const handleLongPress = (folder) => {
if (isSelectFolder.value) return; if (isSelectFolder.value) return;
const isWyxd = folder.name && folder.name.toLowerCase().endsWith('.wyxd'); const isWyxd = folder.name && folder.name.toLowerCase().endsWith('.wyxd');
const itemList = isWyxd ? ['下载', '删除', 'WYXD查看器'] : ['下载', '删除']; const itemList = isWyxd ? ['下载', '删除', 'WYXD查看器', 'AI修改'] : ['下载', '删除'];
uni.showActionSheet({ uni.showActionSheet({
title: folder.name,
itemList: itemList, itemList: itemList,
success: (res) => { success: (res) => {
switch (res.tapIndex) { if (isWyxd) {
case 0: switch (res.tapIndex) {
handleDownload(folder); case 0:
break; handleDownload(folder);
case 1: break;
handleDelete(folder); case 1:
break; handleDelete(folder);
case 2: break;
if (isWyxd) openWyxdViewer(folder); case 2:
break; openWyxdViewer(folder);
break;
case 3: // 'AI修改',
handleAiModify(folder);
break;
}
} else {
switch (res.tapIndex) {
case 0:
handleDownload(folder);
break;
case 1:
handleDelete(folder);
break;
}
} }
} }
}) })
@@ -379,6 +445,7 @@
const downloadUrl = fileInfo.url; const downloadUrl = fileInfo.url;
const fileName = folder.name; const fileName = folder.name;
const fileSize = fileInfo.size || 0;
uni.showLoading({ uni.showLoading({
title: '下载中...' title: '下载中...'
@@ -391,8 +458,14 @@
uni.hideLoading(); uni.hideLoading();
if (downloadRes.statusCode === 200) { if (downloadRes.statusCode === 200) {
const tempPath = downloadRes.tempFilePath; const tempPath = downloadRes.tempFilePath;
// 下载成功,直接打开文件 // 持久化保存到本地
console.log("// 下载成功,文件下载路径为:",tempPath); saveDownload(tempPath, fileName, fileSize).then((record) => {
downloadList.value.unshift(record);
uni.showToast({ title: '下载成功', icon: 'success' });
}).catch(() => {
// 保存失败不影响打开
});
// 下载成功后打开文件
openFile(tempPath); openFile(tempPath);
} else { } else {
uni.showToast({ uni.showToast({
@@ -420,42 +493,108 @@
} }
}; };
// 使用系统默认应用打开 // 打开文件(弹出应用选择菜单)
const openFile = (filePath) => { const openFile = (filePath) => {
uni.openDocument({ openFileNative(filePath, () => {
filePath: filePath, uni.showToast({ title: '无法打开此文件', icon: 'error' });
success: () => uni.showToast({
title: '打开成功',
icon: 'success'
}),
fail: (err) => {
console.error('打开文件失败:', err);
uni.showToast({
title: '无法打开此文件',
icon: 'error'
});
}
}); });
}; };
// 选择其他应用打开 // ===== 下载列表相关 =====
const openFileWithMenu = (filePath) => { const switchTab = (tab) => {
uni.openDocument({ activeTab.value = tab
filePath: filePath, if (tab === 'downloads') {
showMenu: true, loadDownloads()
success: () => uni.showToast({ }
title: '打开成功', }
icon: 'success'
}), const loadDownloads = () => {
fail: (err) => { downloadList.value = getDownloadList()
console.error('打开文件失败:', err); }
uni.showToast({
title: '无法打开此文件', const openDownloadFile = (item) => {
icon: 'error' // #ifdef APP-PLUS
}); // 检查公共目录下文件是否存在
plus.io.resolveLocalFileSystemURL(
item.path,
() => {
// 文件存在,直接打开
openFile(item.path)
},
() => {
// 文件已被手动删除
uni.showToast({ title: '文件已失效,将移除记录', icon: 'none' })
handleDeleteDownload(item.id)
} }
}); )
}; // #endif
// #ifndef APP-PLUS
uni.getSavedFileList({
success: (res) => {
const found = res.fileList.find(f => f.filePath === item.path)
if (found) {
openFile(item.path)
} else {
uni.showToast({ title: '文件已失效,将移除记录', icon: 'none' })
handleDeleteDownload(item.id)
}
},
fail: () => openFile(item.path)
})
// #endif
}
const handleDeleteDownload = (id) => {
uni.showModal({
title: '删除文件',
content: '确定要删除此下载文件吗?',
success: (res) => {
if (res.confirm) {
deleteDownload(id).then(() => {
downloadList.value = getDownloadList()
uni.showToast({ title: '已删除', icon: 'success' })
}).catch(() => {
uni.showToast({ title: '删除失败', icon: 'error' })
})
}
}
})
}
const handleClearDownloads = () => {
if (downloadList.value.length === 0) return
uni.showModal({
title: '清空下载',
content: '确定要清空所有下载文件吗?此操作不可撤销。',
success: (res) => {
if (res.confirm) {
clearAllDownloads().then(() => {
downloadList.value = []
uni.showToast({ title: '已清空', icon: 'success' })
})
}
}
})
}
const formatDownloadSize = formatFileSize
const formatDownloadTime = formatTime
// const getFileIcon = (fileName) => {
// const ext = fileName.split('.').pop().toLowerCase()
// const iconMap = {
// pdf: 'icon-PDF',
// doc: 'icon-word', docx: 'icon-word',
// xls: 'icon-excel', xlsx: 'icon-excel',
// ppt: 'icon-PPD', pptx: 'icon-PPD',
// zip: 'icon-yasuobao', rar: 'icon-yasuobao', '7z': 'icon-yasuobao',
// jpg: 'icon-tupian', jpeg: 'icon-tupian', png: 'icon-tupian', gif: 'icon-tupian',
// mp3: 'icon-yinle', wav: 'icon-yinle', flac: 'icon-yinle',
// mp4: 'icon-shipin', avi: 'icon-shipin', mov: 'icon-shipin',
// txt: 'icon-txt', md: 'icon-txt',
// }
// return iconMap[ext] || 'icon-weizhiwenjian'
// }
// WYXD 查看器入口 // WYXD 查看器入口
const openWyxdViewer = (folder) => { const openWyxdViewer = (folder) => {
@@ -472,6 +611,17 @@
}); });
}; };
// AI修改入口跳转对话页并传入 WYXD 信息
const handleAiModify = (folder) => {
const filePath = folder.path.startsWith('./') ? folder.path.slice(2) : folder.path;
uni.navigateTo({
url: '/pages/Chat/Chat?wyxdWorkspaceId=' + encodeURIComponent(workspaceId.value)
+ '&wyxdFilePath=' + encodeURIComponent(filePath)
+ '&wyxdFileName=' + encodeURIComponent(folder.name)
+ '&wyxdUserToken=' + encodeURIComponent(UserToken.value)
});
};
// 删除 // 删除
const handleDelete = (folder) => { const handleDelete = (folder) => {
uni.showModal({ uni.showModal({
@@ -669,6 +819,7 @@
onMounted(() => { onMounted(() => {
initCurrentFolderList(); initCurrentFolderList();
loadDownloads();
}) })
</script> </script>

View File

@@ -325,6 +325,7 @@
chooseFile chooseFile
} from '@/uni_modules/lime-choose-file' } from '@/uni_modules/lime-choose-file'
import socketManager from '../../utils/socket'; import socketManager from '../../utils/socket';
import { openFile as openFileNative } from '@/utils/fileOpener.js'
const friendSocketStore = useFriendSocketStore() const friendSocketStore = useFriendSocketStore()
// 连接实时通讯eriendsocket // 连接实时通讯eriendsocket
@@ -490,15 +491,7 @@
} }
// 打开文件 // 打开文件
const openFile = (url) => { const openFile = (url) => {
uni.downloadFile({ downloadAndOpen(url);
url: url,
success: (res) => {
uni.openDocument({
filePath: res.tempFilePath,
showMenu: true
})
}
})
} }
// 文件大小格式化 // 文件大小格式化
const formatFileSize = (size) => { const formatFileSize = (size) => {
@@ -1131,14 +1124,16 @@
if (reason === 'pc offline') { if (reason === 'pc offline') {
isThinking.value = false isThinking.value = false
socketStore.authFailReason = '' socketStore.authFailReason = ''
nextTick(() => { uni.showToast({
uni.showModal({ title: 'PC端不在线',
title: '提示', icon: 'none',
content: 'PC端不在线不可聊天', duration: 1500
showCancel: false,
confirmText: '知道了'
})
}) })
setTimeout(() => {
uni.reLaunch({
url: '/pages/Login/Login'
})
}, 1500)
} }
}) })

View File

@@ -1,8 +1,8 @@
;(function(){ ;(function(){
let u=void 0,isReady=false,onReadyCallbacks=[],isServiceReady=false,onServiceReadyCallbacks=[]; let u=void 0,isReady=false,onReadyCallbacks=[],isServiceReady=false,onServiceReadyCallbacks=[];
const __uniConfig = {"pages":[],"globalStyle":{"backgroundColor":"#F8F8F8","navigationBar":{"backgroundColor":"#F8F8F8","titleText":"uni-app","type":"default","titleColor":"#000000"},"isNVue":false},"nvue":{"compiler":"uni-app","styleCompiler":"uni-app","flex-direction":"column"},"renderer":"auto","appname":"test1","splashscreen":{"alwaysShowBeforeRender":true,"autoclose":true},"compilerVersion":"5.07","entryPagePath":"pages/Login/Login","entryPageQuery":"","realEntryPagePath":"","networkTimeout":{"request":60000,"connectSocket":60000,"uploadFile":60000,"downloadFile":60000},"locales":{},"darkmode":false,"themeConfig":{}}; const __uniConfig = {"pages":[],"globalStyle":{"backgroundColor":"#F8F8F8","navigationBar":{"backgroundColor":"#F8F8F8","titleText":"uni-app","type":"default","titleColor":"#000000"},"isNVue":false},"nvue":{"compiler":"uni-app","styleCompiler":"uni-app","flex-direction":"column"},"renderer":"auto","appname":"宇恒一号","splashscreen":{"alwaysShowBeforeRender":true,"autoclose":true},"compilerVersion":"5.07","entryPagePath":"pages/Login/Login","entryPageQuery":"","realEntryPagePath":"","networkTimeout":{"request":60000,"connectSocket":60000,"uploadFile":60000,"downloadFile":60000},"locales":{},"darkmode":false,"themeConfig":{}};
const __uniRoutes = [{"path":"pages/Login/Login","meta":{"isQuit":true,"isEntry":true,"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"登录页面","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/Chat/Chat","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"聊天页面(主页面)","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/WorkSpace/WorkSpace","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"工作区文件管理","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/UserProfileModal/UserProfileModal","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/ContactPages/ContactPages","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/WorkSpace/TemplateSpace/TemplateSpace","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/CloudDatabase/CloudDatabase","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"云端数据","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/CloudDbDetail/CloudDbDetail","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"数据库详情","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/text/text","meta":{"navigationBar":{"titleText":"","type":"default"},"isNVue":false}},{"path":"pages/text/IntuitiveAipptViewer","meta":{"navigationBar":{"titleText":"","type":"default"},"isNVue":false}}].map(uniRoute=>(uniRoute.meta.route=uniRoute.path,__uniConfig.pages.push(uniRoute.path),uniRoute.path='/'+uniRoute.path,uniRoute)); const __uniRoutes = [{"path":"pages/Login/Login","meta":{"isQuit":true,"isEntry":true,"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"登录页面","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/Chat/Chat","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"聊天页面(主页面)","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/WorkSpace/WorkSpace","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"工作区文件管理","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/UserProfileModal/UserProfileModal","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/ContactPages/ContactPages","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/WorkSpace/TemplateSpace/TemplateSpace","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/CloudDatabase/CloudDatabase","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"云端数据","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/CloudDbDetail/CloudDbDetail","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"数据库详情","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/text/text","meta":{"navigationBar":{"titleText":"","type":"default"},"isNVue":false}},{"path":"pages/text/IntuitiveAipptViewer","meta":{"navigationBar":{"titleText":"","type":"default"},"isNVue":false}},{"path":"pages/WYXD/WYXD","meta":{"titleNView":false,"bounce":"none","navigationBar":{"titleText":"","style":"custom","type":"default"},"isNVue":false}}].map(uniRoute=>(uniRoute.meta.route=uniRoute.path,__uniConfig.pages.push(uniRoute.path),uniRoute.path='/'+uniRoute.path,uniRoute));
__uniConfig.styles=[];//styles __uniConfig.styles=[];//styles
__uniConfig.onReady=function(callback){if(__uniConfig.ready){callback()}else{onReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"ready",{get:function(){return isReady},set:function(val){isReady=val;if(!isReady){return}const callbacks=onReadyCallbacks.slice(0);onReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}}); __uniConfig.onReady=function(callback){if(__uniConfig.ready){callback()}else{onReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"ready",{get:function(){return isReady},set:function(val){isReady=val;if(!isReady){return}const callbacks=onReadyCallbacks.slice(0);onReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}});
__uniConfig.onServiceReady=function(callback){if(__uniConfig.serviceReady){callback()}else{onServiceReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"serviceReady",{get:function(){return isServiceReady},set:function(val){isServiceReady=val;if(!isServiceReady){return}const callbacks=onServiceReadyCallbacks.slice(0);onServiceReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}}); __uniConfig.onServiceReady=function(callback){if(__uniConfig.serviceReady){callback()}else{onServiceReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"serviceReady",{get:function(){return isServiceReady},set:function(val){isServiceReady=val;if(!isServiceReady){return}const callbacks=onServiceReadyCallbacks.slice(0);onServiceReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}});

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@
"iPad" "iPad"
], ],
"id": "__UNI__923FD9E", "id": "__UNI__923FD9E",
"name": "test1", "name": "宇恒一号",
"version": { "version": {
"name": "1.1.1", "name": "1.1.1",
"code": 2 "code": 2

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
;(function(){ ;(function(){
let u=void 0,isReady=false,onReadyCallbacks=[],isServiceReady=false,onServiceReadyCallbacks=[]; let u=void 0,isReady=false,onReadyCallbacks=[],isServiceReady=false,onServiceReadyCallbacks=[];
const __uniConfig = {"pages":[],"globalStyle":{"backgroundColor":"#F8F8F8","navigationBar":{"backgroundColor":"#F8F8F8","titleText":"uni-app","type":"default","titleColor":"#000000"},"isNVue":false},"nvue":{"compiler":"uni-app","styleCompiler":"uni-app","flex-direction":"column"},"renderer":"auto","appname":"test1","splashscreen":{"alwaysShowBeforeRender":true,"autoclose":true},"compilerVersion":"5.07","entryPagePath":"pages/Login/Login","entryPageQuery":"","realEntryPagePath":"","networkTimeout":{"request":60000,"connectSocket":60000,"uploadFile":60000,"downloadFile":60000},"locales":{},"darkmode":false,"themeConfig":{}}; const __uniConfig = {"pages":[],"globalStyle":{"backgroundColor":"#F8F8F8","navigationBar":{"backgroundColor":"#F8F8F8","titleText":"uni-app","type":"default","titleColor":"#000000"},"isNVue":false},"nvue":{"compiler":"uni-app","styleCompiler":"uni-app","flex-direction":"column"},"renderer":"auto","appname":"宇恒一号","splashscreen":{"alwaysShowBeforeRender":true,"autoclose":true},"compilerVersion":"5.07","entryPagePath":"pages/Login/Login","entryPageQuery":"","realEntryPagePath":"","networkTimeout":{"request":60000,"connectSocket":60000,"uploadFile":60000,"downloadFile":60000},"locales":{},"darkmode":false,"themeConfig":{}};
const __uniRoutes = [{"path":"pages/Login/Login","meta":{"isQuit":true,"isEntry":true,"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"登录页面","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/Chat/Chat","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"聊天页面(主页面)","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/WorkSpace/WorkSpace","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"工作区文件管理","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/UserProfileModal/UserProfileModal","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/ContactPages/ContactPages","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/WorkSpace/TemplateSpace/TemplateSpace","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/CloudDatabase/CloudDatabase","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"云端数据","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/CloudDbDetail/CloudDbDetail","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"数据库详情","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/text/text","meta":{"navigationBar":{"titleText":"","type":"default"},"isNVue":false}},{"path":"pages/text/IntuitiveAipptViewer","meta":{"navigationBar":{"titleText":"","type":"default"},"isNVue":false}},{"path":"pages/WYXD/WYXD","meta":{"titleNView":false,"bounce":"none","navigationBar":{"titleText":"","style":"custom","type":"default"},"isNVue":false}}].map(uniRoute=>(uniRoute.meta.route=uniRoute.path,__uniConfig.pages.push(uniRoute.path),uniRoute.path='/'+uniRoute.path,uniRoute)); const __uniRoutes = [{"path":"pages/Login/Login","meta":{"isQuit":true,"isEntry":true,"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"登录页面","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/Chat/Chat","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"聊天页面(主页面)","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/WorkSpace/WorkSpace","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"工作区文件管理","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/UserProfileModal/UserProfileModal","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/ContactPages/ContactPages","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/WorkSpace/TemplateSpace/TemplateSpace","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/CloudDatabase/CloudDatabase","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"云端数据","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/CloudDbDetail/CloudDbDetail","meta":{"titleNView":false,"bounce":"none","softinputMode":"adjustResize","navigationBar":{"titleText":"数据库详情","style":"custom","type":"default"},"isNVue":false}},{"path":"pages/text/text","meta":{"navigationBar":{"titleText":"","type":"default"},"isNVue":false}},{"path":"pages/text/IntuitiveAipptViewer","meta":{"navigationBar":{"titleText":"","type":"default"},"isNVue":false}},{"path":"pages/WYXD/WYXD","meta":{"titleNView":false,"bounce":"none","navigationBar":{"titleText":"","style":"custom","type":"default"},"isNVue":false}}].map(uniRoute=>(uniRoute.meta.route=uniRoute.path,__uniConfig.pages.push(uniRoute.path),uniRoute.path='/'+uniRoute.path,uniRoute));
__uniConfig.styles=[];//styles __uniConfig.styles=[];//styles
__uniConfig.onReady=function(callback){if(__uniConfig.ready){callback()}else{onReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"ready",{get:function(){return isReady},set:function(val){isReady=val;if(!isReady){return}const callbacks=onReadyCallbacks.slice(0);onReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}}); __uniConfig.onReady=function(callback){if(__uniConfig.ready){callback()}else{onReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"ready",{get:function(){return isReady},set:function(val){isReady=val;if(!isReady){return}const callbacks=onReadyCallbacks.slice(0);onReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}});

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"iPad" "iPad"
], ],
"id": "__UNI__923FD9E", "id": "__UNI__923FD9E",
"name": "test1", "name": "宇恒一号",
"version": { "version": {
"name": "1.1.1", "name": "1.1.1",
"code": 2 "code": 2

View File

@@ -1006,10 +1006,12 @@ to { opacity: 1; transform: translateY(0) scale(1);
font-size: 1.125rem; font-size: 1.125rem;
font-weight: 700; font-weight: 700;
color: #1a1a2e; color: #1a1a2e;
align-items: center;
} }
.ncd-header uni-icons[data-v-5eb7b895] { .ncd-header uni-icons[data-v-5eb7b895] {
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
align-items: center;
} }
.ncd-options[data-v-5eb7b895] { .ncd-options[data-v-5eb7b895] {
display: flex; display: flex;
@@ -1192,6 +1194,55 @@ to { opacity: 1; transform: translateY(0) scale(1);
.log-out .iconfont[data-v-5eb7b895] { .log-out .iconfont[data-v-5eb7b895] {
color: #ff4d4f; color: #ff4d4f;
} }
/* ==========WYXD 文件入口横幅========== */
.wyxd-banner[data-v-5eb7b895] {
display: flex;
align-items: center;
padding: 0.4375rem 0.75rem;
margin: 0.1875rem 0.625rem;
background: linear-gradient(135deg, #e8f4fd, #f0e6ff);
border: 0.04688rem solid rgba(120, 120, 220, 0.25);
border-radius: 0.5rem;
box-shadow: 0 0.0625rem 0.25rem rgba(100, 100, 200, 0.08);
}
.wyxd-banner[data-v-5eb7b895]:active {
background: linear-gradient(135deg, #d8eafb, #e6d8ff);
transform: scale(0.98);
}
.wyxd-banner-icon[data-v-5eb7b895] {
font-size: 1rem;
color: #6c6ce0;
margin-right: 0.375rem;
}
.wyxd-banner-text[data-v-5eb7b895] {
flex: 1;
font-size: 0.8125rem;
font-weight: 600;
color: #4a4ab8;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wyxd-banner-arrow[data-v-5eb7b895] {
font-size: 0.75rem;
color: #9999cc;
}
.wyxd-banner-close[data-v-5eb7b895] {
width: 1.125rem;
height: 1.125rem;
line-height: 1.125rem;
text-align: center;
font-size: 0.875rem;
color: #9999cc;
border-radius: 50%;
background: rgba(100, 100, 200, 0.08);
margin-left: 0.25rem;
flex-shrink: 0;
}
.wyxd-banner-close[data-v-5eb7b895]:active {
background: rgba(100, 100, 200, 0.18);
color: #6c6ce0;
}
/* ==========聊天主体部分========== */ /* ==========聊天主体部分========== */
.main-chat[data-v-5eb7b895] { .main-chat[data-v-5eb7b895] {
display: flex; display: flex;
@@ -1665,6 +1716,7 @@ to { opacity: 1; transform: translateY(0);
/* 普通正文 */ /* 普通正文 */
/* 粗体和斜体样式 */ /* 粗体和斜体样式 */
/* 气泡内的内联链接(替代 <a> 标签,点击由 JS 拦截处理) */ /* 气泡内的内联链接(替代 <a> 标签,点击由 JS 拦截处理) */
/* 可下载文件链接 → 按钮卡片样式 */
} }
.chat-content h1 { .chat-content h1 {
font-size: 1.3125rem; font-size: 1.3125rem;
@@ -1710,6 +1762,38 @@ to { opacity: 1; transform: translateY(0);
text-decoration: underline; text-decoration: underline;
word-break: break-all; 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 { .bubble-loading-dots {

View File

@@ -333,6 +333,146 @@
.workspace-footer .iconfont[data-v-507487c4] { .workspace-footer .iconfont[data-v-507487c4] {
font-size: 1.25rem !important; font-size: 1.25rem !important;
} }
/* ===== 标签切换 ===== */
.workspace-tabs[data-v-507487c4] {
display: flex;
width: 100%;
padding: 0 0.75rem;
box-sizing: border-box;
gap: 0.625rem;
}
.tab-item[data-v-507487c4] {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 0.5625rem 0;
font-size: 0.875rem;
font-weight: 500;
color: #999;
border-bottom: 0.125rem solid transparent;
transition: all 0.2s ease;
position: relative;
}
.tab-item.active[data-v-507487c4] {
color: #6c63ff;
border-bottom-color: #6c63ff;
}
.tab-badge[data-v-507487c4] {
margin-left: 0.25rem;
background: #ff5e57;
color: #fff;
font-size: 0.625rem;
padding: 0.0625rem 0.3125rem;
border-radius: 0.625rem;
line-height: 1.4;
}
/* ===== 下载列表 ===== */
.download-content[data-v-507487c4] {
flex: 1;
overflow: auto;
}
.download-empty[data-v-507487c4] {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: 6.25rem;
color: #bbb;
font-size: 0.875rem;
}
.download-empty .iconfont[data-v-507487c4] {
font-size: 4.375rem;
color: #ddd;
margin-bottom: 0.625rem;
}
.download-list[data-v-507487c4] {
padding: 0.625rem 0.75rem;
}
.download-header[data-v-507487c4] {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.375rem 0.25rem 0.625rem;
font-size: 0.75rem;
color: #999;
}
.clear-btn[data-v-507487c4] {
color: #ff5e57;
font-size: 0.75rem;
font-weight: 500;
padding: 0.125rem 0.375rem;
}
.download-item[data-v-507487c4] {
display: flex;
align-items: center;
padding: 0.75rem 0.625rem;
background: rgba(255, 255, 255, 0.85);
border-radius: 0.625rem;
margin-bottom: 0.5rem;
transition: all 0.15s ease;
}
.download-item[data-v-507487c4]:active {
transform: scale(0.98);
background: rgba(255, 255, 255, 0.95);
}
.di-icon[data-v-507487c4] {
width: 2.5rem;
height: 2.5rem;
display: flex;
justify-content: center;
align-items: center;
background: rgba(108, 99, 255, 0.08);
border-radius: 0.5625rem;
margin-right: 0.625rem;
flex-shrink: 0;
}
.di-icon .iconfont[data-v-507487c4] {
font-size: 1.25rem;
color: #6c63ff;
}
.di-info[data-v-507487c4] {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.1875rem;
min-width: 0;
}
.di-name[data-v-507487c4] {
font-size: 0.875rem;
font-weight: 500;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.di-meta[data-v-507487c4] {
font-size: 0.6875rem;
color: #999;
}
.di-path[data-v-507487c4] {
font-size: 0.625rem;
color: #bbb;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.di-delete[data-v-507487c4] {
width: 1.75rem;
height: 1.75rem;
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.di-delete .iconfont[data-v-507487c4] {
font-size: 1.125rem;
color: #ccc;
transition: color 0.15s ease;
}
.di-delete:active .iconfont[data-v-507487c4] {
color: #ff5e57;
}
.wyxd-container[data-v-507487c4] { .wyxd-container[data-v-507487c4] {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -380,6 +520,10 @@
min-height: 0; min-height: 0;
/* padding: 20rpx; */ /* padding: 20rpx; */
} }
.wyxd-swiper[data-v-507487c4] {
width: 100%;
height: 100%;
}
.wyxd-slide-scroll[data-v-507487c4] { .wyxd-slide-scroll[data-v-507487c4] {
height: 100%; height: 100%;
background: #fff; background: #fff;

View File

@@ -333,6 +333,146 @@
.workspace-footer .iconfont[data-v-769edd25] { .workspace-footer .iconfont[data-v-769edd25] {
font-size: 1.25rem !important; font-size: 1.25rem !important;
} }
/* ===== 标签切换 ===== */
.workspace-tabs[data-v-769edd25] {
display: flex;
width: 100%;
padding: 0 0.75rem;
box-sizing: border-box;
gap: 0.625rem;
}
.tab-item[data-v-769edd25] {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 0.5625rem 0;
font-size: 0.875rem;
font-weight: 500;
color: #999;
border-bottom: 0.125rem solid transparent;
transition: all 0.2s ease;
position: relative;
}
.tab-item.active[data-v-769edd25] {
color: #6c63ff;
border-bottom-color: #6c63ff;
}
.tab-badge[data-v-769edd25] {
margin-left: 0.25rem;
background: #ff5e57;
color: #fff;
font-size: 0.625rem;
padding: 0.0625rem 0.3125rem;
border-radius: 0.625rem;
line-height: 1.4;
}
/* ===== 下载列表 ===== */
.download-content[data-v-769edd25] {
flex: 1;
overflow: auto;
}
.download-empty[data-v-769edd25] {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: 6.25rem;
color: #bbb;
font-size: 0.875rem;
}
.download-empty .iconfont[data-v-769edd25] {
font-size: 4.375rem;
color: #ddd;
margin-bottom: 0.625rem;
}
.download-list[data-v-769edd25] {
padding: 0.625rem 0.75rem;
}
.download-header[data-v-769edd25] {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.375rem 0.25rem 0.625rem;
font-size: 0.75rem;
color: #999;
}
.clear-btn[data-v-769edd25] {
color: #ff5e57;
font-size: 0.75rem;
font-weight: 500;
padding: 0.125rem 0.375rem;
}
.download-item[data-v-769edd25] {
display: flex;
align-items: center;
padding: 0.75rem 0.625rem;
background: rgba(255, 255, 255, 0.85);
border-radius: 0.625rem;
margin-bottom: 0.5rem;
transition: all 0.15s ease;
}
.download-item[data-v-769edd25]:active {
transform: scale(0.98);
background: rgba(255, 255, 255, 0.95);
}
.di-icon[data-v-769edd25] {
width: 2.5rem;
height: 2.5rem;
display: flex;
justify-content: center;
align-items: center;
background: rgba(108, 99, 255, 0.08);
border-radius: 0.5625rem;
margin-right: 0.625rem;
flex-shrink: 0;
}
.di-icon .iconfont[data-v-769edd25] {
font-size: 1.25rem;
color: #6c63ff;
}
.di-info[data-v-769edd25] {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.1875rem;
min-width: 0;
}
.di-name[data-v-769edd25] {
font-size: 0.875rem;
font-weight: 500;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.di-meta[data-v-769edd25] {
font-size: 0.6875rem;
color: #999;
}
.di-path[data-v-769edd25] {
font-size: 0.625rem;
color: #bbb;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.di-delete[data-v-769edd25] {
width: 1.75rem;
height: 1.75rem;
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.di-delete .iconfont[data-v-769edd25] {
font-size: 1.125rem;
color: #ccc;
transition: color 0.15s ease;
}
.di-delete:active .iconfont[data-v-769edd25] {
color: #ff5e57;
}
/* 编辑模板弹窗 */ /* 编辑模板弹窗 */
.edit-template-wrapper[data-v-769edd25] { .edit-template-wrapper[data-v-769edd25] {
} }

View File

@@ -865,3 +865,143 @@
.workspace-footer .iconfont[data-v-c1a60359] { .workspace-footer .iconfont[data-v-c1a60359] {
font-size: 1.25rem !important; font-size: 1.25rem !important;
} }
/* ===== 标签切换 ===== */
.workspace-tabs[data-v-c1a60359] {
display: flex;
width: 100%;
padding: 0 0.75rem;
box-sizing: border-box;
gap: 0.625rem;
}
.tab-item[data-v-c1a60359] {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 0.5625rem 0;
font-size: 0.875rem;
font-weight: 500;
color: #999;
border-bottom: 0.125rem solid transparent;
transition: all 0.2s ease;
position: relative;
}
.tab-item.active[data-v-c1a60359] {
color: #6c63ff;
border-bottom-color: #6c63ff;
}
.tab-badge[data-v-c1a60359] {
margin-left: 0.25rem;
background: #ff5e57;
color: #fff;
font-size: 0.625rem;
padding: 0.0625rem 0.3125rem;
border-radius: 0.625rem;
line-height: 1.4;
}
/* ===== 下载列表 ===== */
.download-content[data-v-c1a60359] {
flex: 1;
overflow: auto;
}
.download-empty[data-v-c1a60359] {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: 6.25rem;
color: #bbb;
font-size: 0.875rem;
}
.download-empty .iconfont[data-v-c1a60359] {
font-size: 4.375rem;
color: #ddd;
margin-bottom: 0.625rem;
}
.download-list[data-v-c1a60359] {
padding: 0.625rem 0.75rem;
}
.download-header[data-v-c1a60359] {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.375rem 0.25rem 0.625rem;
font-size: 0.75rem;
color: #999;
}
.clear-btn[data-v-c1a60359] {
color: #ff5e57;
font-size: 0.75rem;
font-weight: 500;
padding: 0.125rem 0.375rem;
}
.download-item[data-v-c1a60359] {
display: flex;
align-items: center;
padding: 0.75rem 0.625rem;
background: rgba(255, 255, 255, 0.85);
border-radius: 0.625rem;
margin-bottom: 0.5rem;
transition: all 0.15s ease;
}
.download-item[data-v-c1a60359]:active {
transform: scale(0.98);
background: rgba(255, 255, 255, 0.95);
}
.di-icon[data-v-c1a60359] {
width: 2.5rem;
height: 2.5rem;
display: flex;
justify-content: center;
align-items: center;
background: rgba(108, 99, 255, 0.08);
border-radius: 0.5625rem;
margin-right: 0.625rem;
flex-shrink: 0;
}
.di-icon .iconfont[data-v-c1a60359] {
font-size: 1.25rem;
color: #6c63ff;
}
.di-info[data-v-c1a60359] {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.1875rem;
min-width: 0;
}
.di-name[data-v-c1a60359] {
font-size: 0.875rem;
font-weight: 500;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.di-meta[data-v-c1a60359] {
font-size: 0.6875rem;
color: #999;
}
.di-path[data-v-c1a60359] {
font-size: 0.625rem;
color: #bbb;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.di-delete[data-v-c1a60359] {
width: 1.75rem;
height: 1.75rem;
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.di-delete .iconfont[data-v-c1a60359] {
font-size: 1.125rem;
color: #ccc;
transition: color 0.15s ease;
}
.di-delete:active .iconfont[data-v-c1a60359] {
color: #ff5e57;
}

View File

@@ -1006,10 +1006,12 @@ to { opacity: 1; transform: translateY(0) scale(1);
font-size: 1.125rem; font-size: 1.125rem;
font-weight: 700; font-weight: 700;
color: #1a1a2e; color: #1a1a2e;
align-items: center;
} }
.ncd-header uni-icons[data-v-fdf84df1] { .ncd-header uni-icons[data-v-fdf84df1] {
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
align-items: center;
} }
.ncd-options[data-v-fdf84df1] { .ncd-options[data-v-fdf84df1] {
display: flex; display: flex;
@@ -1192,6 +1194,55 @@ to { opacity: 1; transform: translateY(0) scale(1);
.log-out .iconfont[data-v-fdf84df1] { .log-out .iconfont[data-v-fdf84df1] {
color: #ff4d4f; color: #ff4d4f;
} }
/* ==========WYXD 文件入口横幅========== */
.wyxd-banner[data-v-fdf84df1] {
display: flex;
align-items: center;
padding: 0.4375rem 0.75rem;
margin: 0.1875rem 0.625rem;
background: linear-gradient(135deg, #e8f4fd, #f0e6ff);
border: 0.04688rem solid rgba(120, 120, 220, 0.25);
border-radius: 0.5rem;
box-shadow: 0 0.0625rem 0.25rem rgba(100, 100, 200, 0.08);
}
.wyxd-banner[data-v-fdf84df1]:active {
background: linear-gradient(135deg, #d8eafb, #e6d8ff);
transform: scale(0.98);
}
.wyxd-banner-icon[data-v-fdf84df1] {
font-size: 1rem;
color: #6c6ce0;
margin-right: 0.375rem;
}
.wyxd-banner-text[data-v-fdf84df1] {
flex: 1;
font-size: 0.8125rem;
font-weight: 600;
color: #4a4ab8;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wyxd-banner-arrow[data-v-fdf84df1] {
font-size: 0.75rem;
color: #9999cc;
}
.wyxd-banner-close[data-v-fdf84df1] {
width: 1.125rem;
height: 1.125rem;
line-height: 1.125rem;
text-align: center;
font-size: 0.875rem;
color: #9999cc;
border-radius: 50%;
background: rgba(100, 100, 200, 0.08);
margin-left: 0.25rem;
flex-shrink: 0;
}
.wyxd-banner-close[data-v-fdf84df1]:active {
background: rgba(100, 100, 200, 0.18);
color: #6c6ce0;
}
/* ==========聊天主体部分========== */ /* ==========聊天主体部分========== */
.main-chat[data-v-fdf84df1] { .main-chat[data-v-fdf84df1] {
display: flex; display: flex;

284
utils/downloadManager.js Normal file
View File

@@ -0,0 +1,284 @@
/**
* 下载管理工具
* 文件保存到公共下载目录,用户可通过文件管理器访问
*/
const STORAGE_KEY = 'localDownloads'
const SUB_DIR = 'AppDownloads' // 公共下载目录下的子文件夹
/**
* 获取公共下载目录路径
* @returns {Promise<string>} 下载目录绝对路径
*/
function getPublicDownloadDir() {
return new Promise((resolve, reject) => {
// #ifdef APP-PLUS
plus.io.requestFileSystem(
plus.io.PUBLIC_DOWNLOADS,
(fs) => {
// PUBLIC_DOWNLOADS 对应 /storage/emulated/0/Download/
fs.root.getDirectory(
SUB_DIR,
{ create: true },
(dirEntry) => {
resolve(dirEntry.fullPath)
},
(err) => {
console.error('getDirectory failed:', err)
reject(err)
}
)
},
(err) => {
console.error('requestFileSystem failed:', err)
reject(err)
}
)
// #endif
// #ifndef APP-PLUS
reject(new Error('仅APP端支持'))
// #endif
})
}
/**
* 从完整路径中提取可读的显示路径
* 如 /storage/emulated/0/Download/AppDownloads/xxx.pdf → 下载/AppDownloads/xxx.pdf
*/
function getDisplayPath(fullPath) {
const idx = fullPath.indexOf('/Download/')
if (idx !== -1) {
return '下载/' + fullPath.slice(idx + 10)
}
return fullPath
}
/**
* 获取所有下载记录
* @returns {Array} 下载记录列表
*/
export function getDownloadList() {
try {
return uni.getStorageSync(STORAGE_KEY) || []
} catch (e) {
return []
}
}
/**
* 保存下载记录(复制到公共下载目录)
* @param {string} tempFilePath 临时文件路径uni.downloadFile 返回的)
* @param {string} fileName 文件名
* @param {number} fileSize 文件大小(字节,可选)
*/
export function saveDownload(tempFilePath, fileName, fileSize) {
return new Promise((resolve, reject) => {
// #ifdef APP-PLUS
getPublicDownloadDir().then((dirPath) => {
const destPathForUrl = plus.io.convertLocalFileSystemURL(dirPath + '/' + fileName)
plus.io.resolveLocalFileSystemURL(
tempFilePath,
(fileEntry) => {
plus.io.resolveLocalFileSystemURL(
dirPath,
(dirEntry) => {
fileEntry.copyTo(
dirEntry,
fileName,
() => {
const record = {
id: Date.now().toString(),
name: fileName,
path: destPathForUrl,
displayPath: getDisplayPath(destPathForUrl),
size: fileSize || 0,
time: Date.now()
}
const list = getDownloadList()
const exists = list.some(item => item.name === fileName)
if (exists) {
const idx = list.findIndex(item => item.name === fileName)
if (idx !== -1) list.splice(idx, 1)
}
list.unshift(record)
try {
uni.setStorageSync(STORAGE_KEY, list)
} catch (e) {
list.pop()
uni.setStorageSync(STORAGE_KEY, list)
}
resolve(record)
},
(err) => {
console.error('copyTo failed:', err)
reject(err)
}
)
},
reject
)
},
reject
)
}).catch(reject)
// #endif
// #ifndef APP-PLUS
// 非 APP 端降级使用 uni.saveFile
uni.saveFile({
tempFilePath: tempFilePath,
success: (saveRes) => {
const record = {
id: Date.now().toString(),
name: fileName,
path: saveRes.savedFilePath,
size: fileSize || 0,
time: Date.now()
}
const list = getDownloadList()
list.unshift(record)
uni.setStorageSync(STORAGE_KEY, list)
resolve(record)
},
fail: reject
})
// #endif
})
}
/**
* 删除单条下载记录及其文件
* @param {string} id 记录 ID
*/
export function deleteDownload(id) {
return new Promise((resolve, reject) => {
const list = getDownloadList()
const record = list.find(item => item.id === id)
if (!record) {
reject(new Error('记录不存在'))
return
}
// #ifdef APP-PLUS
// 删除公共目录下的文件
plus.io.resolveLocalFileSystemURL(
record.path,
(fileEntry) => {
fileEntry.remove(
() => {
const newList = list.filter(item => item.id !== id)
uni.setStorageSync(STORAGE_KEY, newList)
resolve()
},
() => {
// 文件不存在也清除记录
const newList = list.filter(item => item.id !== id)
uni.setStorageSync(STORAGE_KEY, newList)
resolve()
}
)
},
() => {
// 无法解析路径,直接清除记录
const newList = list.filter(item => item.id !== id)
uni.setStorageSync(STORAGE_KEY, newList)
resolve()
}
)
// #endif
// #ifndef APP-PLUS
uni.removeSavedFile({
filePath: record.path,
success: () => {
const newList = list.filter(item => item.id !== id)
uni.setStorageSync(STORAGE_KEY, newList)
resolve()
},
fail: () => {
const newList = list.filter(item => item.id !== id)
uni.setStorageSync(STORAGE_KEY, newList)
resolve()
}
})
// #endif
})
}
/**
* 清空所有下载记录和文件
*/
export function clearAllDownloads() {
return new Promise((resolve) => {
const list = getDownloadList()
if (list.length === 0) {
resolve()
return
}
// #ifdef APP-PLUS
let count = 0
const done = () => {
count++
if (count >= list.length) {
uni.setStorageSync(STORAGE_KEY, [])
resolve()
}
}
list.forEach(record => {
plus.io.resolveLocalFileSystemURL(
record.path,
(fileEntry) => {
fileEntry.remove(done, done)
},
done
)
})
setTimeout(() => {
uni.setStorageSync(STORAGE_KEY, [])
resolve()
}, 5000)
// #endif
// #ifndef APP-PLUS
let count = 0
list.forEach(record => {
uni.removeSavedFile({
filePath: record.path,
success: () => { count++; if (count >= list.length) { uni.setStorageSync(STORAGE_KEY, []); resolve() } },
fail: () => { count++; if (count >= list.length) { uni.setStorageSync(STORAGE_KEY, []); resolve() } }
})
})
if (list.length === 0) resolve()
setTimeout(() => { uni.setStorageSync(STORAGE_KEY, []); resolve() }, 3000)
// #endif
})
}
/**
* 格式化文件大小为可读字符串
* @param {number} bytes 字节数
* @returns {string}
*/
export function formatFileSize(bytes) {
if (!bytes || bytes <= 0) return '未知大小'
const units = ['B', 'KB', 'MB', 'GB']
let i = 0
let size = bytes
while (size >= 1024 && i < units.length - 1) {
size /= 1024
i++
}
return size.toFixed(i === 0 ? 0 : 1) + ' ' + units[i]
}
/**
* 格式化时间戳为可读日期
* @param {number} timestamp
* @returns {string}
*/
export function formatTime(timestamp) {
const d = new Date(timestamp)
const pad = n => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
}

124
utils/fileOpener.js Normal file
View File

@@ -0,0 +1,124 @@
/**
* 文件打开工具 - 强制弹出应用选择器
*
* uni.openDocument 的 showMenu 和 plus.runtime.openFile 在真机上
* 都可能直接跳转默认应用。用 Android 原生 Intent.createChooser
* 强制弹出"选择应用"对话框。
*
* Android 7.0+ 会阻止 file:// URI 暴露给其他应用,需要禁用此限制。
*/
/**
* 根据文件扩展名获取 MIME 类型
*/
function getMimeType(filePath) {
const ext = filePath.substring(filePath.lastIndexOf('.') + 1).toLowerCase()
const mimeMap = {
'pdf': 'application/pdf',
'doc': 'application/msword',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xls': 'application/vnd.ms-excel',
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'ppt': 'application/vnd.ms-powerpoint',
'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'txt': 'text/plain',
'html': 'text/html',
'htm': 'text/html',
'xml': 'text/xml',
'json': 'application/json',
'csv': 'text/csv',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'png': 'image/png',
'gif': 'image/gif',
'bmp': 'image/bmp',
'webp': 'image/webp',
'mp4': 'video/mp4',
'mp3': 'audio/mpeg',
'wav': 'audio/wav',
'zip': 'application/zip',
'rar': 'application/x-rar-compressed',
'7z': 'application/x-7z-compressed',
'apk': 'application/vnd.android.package-archive'
}
return mimeMap[ext] || '*/*'
}
/**
* 打开文件,弹出应用选择器
* @param {string} filePath 本地文件路径
* @param {Function} onFail 打开失败时的回调
*/
export function openFile(filePath, onFail) {
// #ifdef APP-PLUS
try {
const main = plus.android.runtimeMainActivity()
const Intent = plus.android.importClass('android.content.Intent')
const Uri = plus.android.importClass('android.net.Uri')
const File = plus.android.importClass('java.io.File')
// Android 7.0+ 默认禁止 file:// URI禁用此限制
if (plus.os.sdkInt >= 24) {
const StrictMode = plus.android.importClass('android.os.StrictMode')
StrictMode.disableDeathOnFileUriExposure()
}
const file = new File(filePath)
if (!file.exists()) {
throw new Error('文件不存在: ' + filePath)
}
const uri = Uri.fromFile(file)
const mimeType = getMimeType(filePath)
const intent = new Intent(Intent.ACTION_VIEW)
intent.setDataAndType(uri, mimeType)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
// 核心createChooser 每次必弹选择器,无视已设置的默认应用
const chooser = Intent.createChooser(intent, '选择应用打开')
main.startActivity(chooser)
console.log('openFile success - chooser shown')
} catch (e) {
console.error('openFile native failed, fallback:', e)
uni.openDocument({
filePath: filePath,
showMenu: true,
fail: typeof onFail === 'function' ? onFail : () => {}
})
}
// #endif
// #ifndef APP-PLUS
uni.openDocument({
filePath: filePath,
showMenu: true,
fail: typeof onFail === 'function' ? onFail : () => {}
})
// #endif
}
/**
* 下载文件并打开(弹出应用选择器)
* @param {string} url 远程文件 URL
* @param {Function} onFail 失败回调
*/
export function downloadAndOpen(url, onFail) {
uni.downloadFile({
url: url,
success: (res) => {
if (res.statusCode === 200) {
openFile(res.tempFilePath, onFail)
} else {
console.error('下载失败, statusCode:', res.statusCode)
if (typeof onFail === 'function') onFail()
}
},
fail: (err) => {
console.error('downloadFile fail:', err)
if (typeof onFail === 'function') onFail()
}
})
}