From e057e336013933ad51a024e9668aa220d9ac3c74 Mon Sep 17 00:00:00 2001 From: YiLin <482244139@qq.com> Date: Mon, 3 Aug 2026 09:05:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=B7=A5=E4=BD=9C=E5=8C=BA?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/WorkSpace/WorkSpace.css | 161 +++++++ pages/WorkSpace/WorkSpace.vue | 160 ++++++- unpackage/dist/dev/app-plus/app-service.js | 422 ++++++++++++++++-- .../dist/dev/app-plus/pages/WYXD/WYXD.css | 140 ++++++ .../WorkSpace/TemplateSpace/TemplateSpace.css | 140 ++++++ .../app-plus/pages/WorkSpace/WorkSpace.css | 140 ++++++ utils/downloadManager.js | 284 ++++++++++++ 7 files changed, 1416 insertions(+), 31 deletions(-) create mode 100644 utils/downloadManager.js diff --git a/pages/WorkSpace/WorkSpace.css b/pages/WorkSpace/WorkSpace.css index 20ce011..8fd29d9 100644 --- a/pages/WorkSpace/WorkSpace.css +++ b/pages/WorkSpace/WorkSpace.css @@ -380,3 +380,164 @@ .workspace-footer .iconfont { 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; +} diff --git a/pages/WorkSpace/WorkSpace.vue b/pages/WorkSpace/WorkSpace.vue index a014c6f..f93d98a 100644 --- a/pages/WorkSpace/WorkSpace.vue +++ b/pages/WorkSpace/WorkSpace.vue @@ -66,8 +66,20 @@ 取消 + + + + 工作区 + + + 下载列表 + {{ downloadList.length }} + + - + + + 文件夹为空 @@ -89,6 +101,34 @@ + + + + + + 暂无下载文件 + + + + 已下载 {{ downloadList.length }} 个文件 + 清空 + + + + + + + {{ item.name }} + {{ formatDownloadTime(item.time) }} · {{ formatDownloadSize(item.size) }} + {{ item.displayPath || item.path }} + + + + + + + + @@ -122,11 +162,21 @@ updateWorkspaceFileName } 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 workspaceId = ref('') const navbarBeforeTitle = ref(''); const navbarTitle = ref('工作区'); + const activeTab = ref('workspace') + const downloadList = ref([]) // 选择文件模式 const isSelectFolder = ref(false); @@ -395,6 +445,7 @@ import { openFile as openFileNative } from '@/utils/fileOpener.js' const downloadUrl = fileInfo.url; const fileName = folder.name; + const fileSize = fileInfo.size || 0; uni.showLoading({ title: '下载中...' @@ -407,8 +458,14 @@ import { openFile as openFileNative } from '@/utils/fileOpener.js' uni.hideLoading(); if (downloadRes.statusCode === 200) { 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); } else { uni.showToast({ @@ -443,6 +500,102 @@ import { openFile as openFileNative } from '@/utils/fileOpener.js' }); }; + // ===== 下载列表相关 ===== + const switchTab = (tab) => { + activeTab.value = tab + if (tab === 'downloads') { + loadDownloads() + } + } + + const loadDownloads = () => { + downloadList.value = getDownloadList() + } + + const openDownloadFile = (item) => { + // #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 查看器入口 const openWyxdViewer = (folder) => { const filePath = folder.path.startsWith('./') ? folder.path.slice(2) : folder.path; @@ -666,6 +819,7 @@ import { openFile as openFileNative } from '@/utils/fileOpener.js' onMounted(() => { initCurrentFolderList(); + loadDownloads(); }) diff --git a/unpackage/dist/dev/app-plus/app-service.js b/unpackage/dist/dev/app-plus/app-service.js index 6ab1540..ca45452 100644 --- a/unpackage/dist/dev/app-plus/app-service.js +++ b/unpackage/dist/dev/app-plus/app-service.js @@ -5837,7 +5837,7 @@ This will fail in production.`); const openFile$1 = (url) => { downloadAndOpen$1(url); }; - const formatFileSize = (size) => { + const formatFileSize2 = (size) => { if (!size) return "0KB"; if (size < 1) { @@ -6696,7 +6696,7 @@ This will fail in production.`); onUnload(() => { socketStore.disconnect(); }); - const __returned__ = { friendSocketStore, handleFriendConnect, socketStore, ChatType, wyxdWorkspaceId, wyxdFilePath, wyxdFileName, wyxdUserToken, hasWyxdFile, convertMarkdownTable, renderTable, escapeLoneUnderscores, pareseMarkdown, sanitizeContent, previewImage, openFile: openFile$1, formatFileSize, parseFileInfo, selectNormalChat, showNewChatModal, closeNewChatModal, isChatSidebar, handleChatSidebar, goWorkSpace, goWyxdViewer, goCloudDatabase, logOut, previewFileArray, uploadPhoto, deleteImage, fileList, uploadFile, handleConnect, isThinking, isSelfSent, isUploading, isLoadingMessages, uploadedFiles, streamingMessageId, showMessageModal, detailLinks, handleChatContentClick, extractLinks, extractFileNameFromUrl, showMessageDetail, closeMessageDetail, downloadToast, get downloadToastTimer() { + const __returned__ = { friendSocketStore, handleFriendConnect, socketStore, ChatType, wyxdWorkspaceId, wyxdFilePath, wyxdFileName, wyxdUserToken, hasWyxdFile, convertMarkdownTable, renderTable, escapeLoneUnderscores, pareseMarkdown, sanitizeContent, previewImage, openFile: openFile$1, formatFileSize: formatFileSize2, parseFileInfo, selectNormalChat, showNewChatModal, closeNewChatModal, isChatSidebar, handleChatSidebar, goWorkSpace, goWyxdViewer, goCloudDatabase, logOut, previewFileArray, uploadPhoto, deleteImage, fileList, uploadFile, handleConnect, isThinking, isSelfSent, isUploading, isLoadingMessages, uploadedFiles, streamingMessageId, showMessageModal, detailLinks, handleChatContentClick, extractLinks, extractFileNameFromUrl, showMessageDetail, closeMessageDetail, downloadToast, get downloadToastTimer() { return downloadToastTimer; }, set downloadToastTimer(v) { downloadToastTimer = v; @@ -7451,6 +7451,177 @@ This will fail in production.`); ); } const PagesChatChat = /* @__PURE__ */ _export_sfc(_sfc_main$a, [["render", _sfc_render$9], ["__scopeId", "data-v-5eb7b895"], ["__file", "D:/Projects/uniapp/app-test/test1/pages/Chat/Chat.vue"]]); + const STORAGE_KEY = "localDownloads"; + const SUB_DIR = "AppDownloads"; + function getPublicDownloadDir() { + return new Promise((resolve, reject) => { + plus.io.requestFileSystem( + plus.io.PUBLIC_DOWNLOADS, + (fs) => { + fs.root.getDirectory( + SUB_DIR, + { create: true }, + (dirEntry) => { + resolve(dirEntry.fullPath); + }, + (err) => { + formatAppLog("error", "at utils/downloadManager.js:27", "getDirectory failed:", err); + reject(err); + } + ); + }, + (err) => { + formatAppLog("error", "at utils/downloadManager.js:33", "requestFileSystem failed:", err); + reject(err); + } + ); + }); + } + function getDisplayPath(fullPath) { + const idx = fullPath.indexOf("/Download/"); + if (idx !== -1) { + return "下载/" + fullPath.slice(idx + 10); + } + return fullPath; + } + function getDownloadList() { + try { + return uni.getStorageSync(STORAGE_KEY) || []; + } catch (e2) { + return []; + } + } + function saveDownload(tempFilePath, fileName, fileSize) { + return new Promise((resolve, reject) => { + 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 (e2) { + list.pop(); + uni.setStorageSync(STORAGE_KEY, list); + } + resolve(record); + }, + (err) => { + formatAppLog("error", "at utils/downloadManager.js:114", "copyTo failed:", err); + reject(err); + } + ); + }, + reject + ); + }, + reject + ); + }).catch(reject); + }); + } + 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; + } + 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(); + } + ); + }); + } + function clearAllDownloads() { + return new Promise((resolve) => { + const list = getDownloadList(); + if (list.length === 0) { + resolve(); + return; + } + 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(); + }, 5e3); + }); + } + 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]; + } + function formatTime(timestamp) { + const d = new Date(timestamp); + const pad = (n2) => String(n2).padStart(2, "0"); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; + } const _sfc_main$9 = { __name: "WorkSpace", setup(__props, { expose: __expose }) { @@ -7459,6 +7630,8 @@ This will fail in production.`); const workspaceId = vue.ref(""); const navbarBeforeTitle = vue.ref(""); const navbarTitle = vue.ref("工作区"); + const activeTab = vue.ref("workspace"); + const downloadList = vue.ref([]); const isSelectFolder = vue.ref(false); const selectFileList = vue.ref([]); const handleSelectFolder = () => { @@ -7501,7 +7674,7 @@ This will fail in production.`); await doUploadWorkspaceFiles(files); }, fail: (err) => { - formatAppLog("error", "at pages/WorkSpace/WorkSpace.vue:177", "选择文件失败:", err); + formatAppLog("error", "at pages/WorkSpace/WorkSpace.vue:227", "选择文件失败:", err); } }); }; @@ -7538,7 +7711,7 @@ This will fail in production.`); initCurrentFolderList(); } catch (error) { uni.hideLoading(); - formatAppLog("error", "at pages/WorkSpace/WorkSpace.vue:230", "上传文件失败:", error); + formatAppLog("error", "at pages/WorkSpace/WorkSpace.vue:280", "上传文件失败:", error); uni.showToast({ title: typeof error === "string" ? error : "上传失败,请重试", icon: "error" @@ -7578,7 +7751,7 @@ This will fail in production.`); closeNewFolderModal(); initCurrentFolderList(); } catch (error) { - formatAppLog("error", "at pages/WorkSpace/WorkSpace.vue:276", "创建文件夹失败:", error); + formatAppLog("error", "at pages/WorkSpace/WorkSpace.vue:326", "创建文件夹失败:", error); uni.showToast({ title: "创建失败,请重试", icon: "error" @@ -7649,7 +7822,7 @@ This will fail in production.`); initCurrentFolderList(); } catch (error) { uni.hideLoading(); - formatAppLog("error", "at pages/WorkSpace/WorkSpace.vue:354", "重命名失败:", error); + formatAppLog("error", "at pages/WorkSpace/WorkSpace.vue:404", "重命名失败:", error); uni.showToast({ title: typeof error === "string" ? error : "重命名失败,请重试", icon: "error" @@ -7684,6 +7857,7 @@ This will fail in production.`); } const downloadUrl = fileInfo.url; const fileName = folder.name; + const fileSize = fileInfo.size || 0; uni.showLoading({ title: "下载中..." }); @@ -7694,7 +7868,11 @@ This will fail in production.`); uni.hideLoading(); if (downloadRes.statusCode === 200) { const tempPath = downloadRes.tempFilePath; - formatAppLog("log", "at pages/WorkSpace/WorkSpace.vue:411", "// 下载成功,文件下载路径为:", tempPath); + saveDownload(tempPath, fileName, fileSize).then((record) => { + downloadList.value.unshift(record); + uni.showToast({ title: "下载成功", icon: "success" }); + }).catch(() => { + }); openFile$1(tempPath); } else { uni.showToast({ @@ -7705,7 +7883,7 @@ This will fail in production.`); }, fail: (err) => { uni.hideLoading(); - formatAppLog("error", "at pages/WorkSpace/WorkSpace.vue:422", "下载文件失败:", err); + formatAppLog("error", "at pages/WorkSpace/WorkSpace.vue:479", "下载文件失败:", err); uni.showToast({ title: "下载失败,请重试", icon: "error" @@ -7714,7 +7892,7 @@ This will fail in production.`); }); } catch (error) { uni.hideLoading(); - formatAppLog("error", "at pages/WorkSpace/WorkSpace.vue:431", "获取下载链接失败:", error); + formatAppLog("error", "at pages/WorkSpace/WorkSpace.vue:488", "获取下载链接失败:", error); uni.showToast({ title: "获取下载链接失败,请重试", icon: "error" @@ -7726,12 +7904,67 @@ This will fail in production.`); uni.showToast({ title: "无法打开此文件", icon: "error" }); }); }; + const switchTab = (tab) => { + activeTab.value = tab; + if (tab === "downloads") { + loadDownloads(); + } + }; + const loadDownloads = () => { + downloadList.value = getDownloadList(); + }; + const openDownloadFile = (item) => { + plus.io.resolveLocalFileSystemURL( + item.path, + () => { + openFile$1(item.path); + }, + () => { + uni.showToast({ title: "文件已失效,将移除记录", icon: "none" }); + handleDeleteDownload(item.id); + } + ); + }; + 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 openWyxdViewer = (folder) => { const filePath = folder.path.startsWith("./") ? folder.path.slice(2) : folder.path; - formatAppLog("log", "at pages/WorkSpace/WorkSpace.vue:449", "解析文件路径:", filePath); - formatAppLog("log", "at pages/WorkSpace/WorkSpace.vue:450", "folder", folder); - formatAppLog("log", "at pages/WorkSpace/WorkSpace.vue:451", "encode", encodeURIComponent(filePath)); - formatAppLog("log", "at pages/WorkSpace/WorkSpace.vue:452", "encode", encodeURIComponent(folder.name)); + formatAppLog("log", "at pages/WorkSpace/WorkSpace.vue:602", "解析文件路径:", filePath); + formatAppLog("log", "at pages/WorkSpace/WorkSpace.vue:603", "folder", folder); + formatAppLog("log", "at pages/WorkSpace/WorkSpace.vue:604", "encode", encodeURIComponent(filePath)); + formatAppLog("log", "at pages/WorkSpace/WorkSpace.vue:605", "encode", encodeURIComponent(folder.name)); uni.navigateTo({ url: "/pages/WYXD/WYXD?workspaceId=" + encodeURIComponent(workspaceId.value) + "&filePath=" + encodeURIComponent(filePath) + "&fileName=" + encodeURIComponent(folder.name) + "&userToken=" + encodeURIComponent(UserToken.value) }); @@ -7750,7 +7983,7 @@ This will fail in production.`); if (res.confirm) { try { const filePath = `./${folder.path}`; - formatAppLog("log", "at pages/WorkSpace/WorkSpace.vue:482", "删除的是:", filePath); + formatAppLog("log", "at pages/WorkSpace/WorkSpace.vue:635", "删除的是:", filePath); await daleteWorkspace(UserToken.value, workspaceId.value, filePath); uni.showToast({ title: "删除成功", @@ -7758,7 +7991,7 @@ This will fail in production.`); }); initCurrentFolderList(); } catch (error) { - formatAppLog("error", "at pages/WorkSpace/WorkSpace.vue:490", "删除失败:", error); + formatAppLog("error", "at pages/WorkSpace/WorkSpace.vue:643", "删除失败:", error); uni.showToast({ title: "删除失败,请重试", icon: "error" @@ -7826,7 +8059,7 @@ This will fail in production.`); const currentFolderList = vue.ref([]); const getCurrentFolderList = () => { var _a, _b; - formatAppLog("log", "at pages/WorkSpace/WorkSpace.vue:571", "当前路径为:", JSON.stringify(folderStack.value)); + formatAppLog("log", "at pages/WorkSpace/WorkSpace.vue:724", "当前路径为:", JSON.stringify(folderStack.value)); const currentPath = folderStack.value.length > 0 ? folderStack.value[folderStack.value.length - 1].path : "."; if (currentPath === ".") { return ((_a = allFoldersData.value) == null ? void 0 : _a.children) || []; @@ -7850,7 +8083,7 @@ This will fail in production.`); const initCurrentFolderList = async () => { workspaceId.value = getWorkspaceId(); UserToken.value = getToken(); - formatAppLog("log", "at pages/WorkSpace/WorkSpace.vue:598", "getWorkspaceId:", workspaceId.value); + formatAppLog("log", "at pages/WorkSpace/WorkSpace.vue:751", "getWorkspaceId:", workspaceId.value); allFoldersData.value = await getWorkspaceList(UserToken.value, workspaceId.value); currentFolderList.value = getCurrentFolderList(); }; @@ -7897,7 +8130,7 @@ This will fail in production.`); uni.navigateBack({ delta: 1, fail() { - formatAppLog("log", "at pages/WorkSpace/WorkSpace.vue:658", "返回失败,进入兜底跳转"); + formatAppLog("log", "at pages/WorkSpace/WorkSpace.vue:811", "返回失败,进入兜底跳转"); uni.reLaunch({ url: "/pages/Chat/Chat" }); @@ -7907,8 +8140,9 @@ This will fail in production.`); }; vue.onMounted(() => { initCurrentFolderList(); + loadDownloads(); }); - const __returned__ = { UserToken, workspaceId, navbarBeforeTitle, navbarTitle, isSelectFolder, selectFileList, handleSelectFolder, selectFolder, showNewFolder, isNFNFocused, newFolderName, closeNewFolderModal, newWorkspaceFolder, handleUploadFile, doUploadWorkspaceFiles, currentFolderPath, folderPathPreview, confirmCreateFolder, handleLongPress, handleRename, handleDownload, openFile: openFile$1, openWyxdViewer, handleAiModify, handleDelete, isMenuOpen, handleMenu, searchKeyword, isSearchFocus, handleSearchFocus, getFileIcon, allFoldersData, folderStack, currentFolderList, getCurrentFolderList, initCurrentFolderList, handleFolderClick, handleSelectAll, handleBackOrCheck, onMounted: vue.onMounted, ref: vue.ref, computed: vue.computed, get getWorkspaceId() { + const __returned__ = { UserToken, workspaceId, navbarBeforeTitle, navbarTitle, activeTab, downloadList, isSelectFolder, selectFileList, handleSelectFolder, selectFolder, showNewFolder, isNFNFocused, newFolderName, closeNewFolderModal, newWorkspaceFolder, handleUploadFile, doUploadWorkspaceFiles, currentFolderPath, folderPathPreview, confirmCreateFolder, handleLongPress, handleRename, handleDownload, openFile: openFile$1, switchTab, loadDownloads, openDownloadFile, handleDeleteDownload, handleClearDownloads, formatDownloadSize, formatDownloadTime, openWyxdViewer, handleAiModify, handleDelete, isMenuOpen, handleMenu, searchKeyword, isSearchFocus, handleSearchFocus, getFileIcon, allFoldersData, folderStack, currentFolderList, getCurrentFolderList, initCurrentFolderList, handleFolderClick, handleSelectAll, handleBackOrCheck, onMounted: vue.onMounted, ref: vue.ref, computed: vue.computed, get getWorkspaceId() { return getWorkspaceId; }, get getToken() { return getToken; @@ -7930,6 +8164,18 @@ This will fail in production.`); return updateWorkspaceFileName; }, get openFileNative() { return openFile; + }, get getDownloadList() { + return getDownloadList; + }, get saveDownload() { + return saveDownload; + }, get deleteDownload() { + return deleteDownload; + }, get clearAllDownloads() { + return clearAllDownloads; + }, get formatFileSize() { + return formatFileSize; + }, get formatTime() { + return formatTime; } }; Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true }); return __returned__; @@ -8097,9 +8343,46 @@ This will fail in production.`); class: "cancel-search", onClick: $setup.handleSearchFocus }, "取消")) : vue.createCommentVNode("v-if", true) + ]), + vue.createElementVNode("view", { class: "workspace-tabs" }, [ + vue.createElementVNode( + "view", + { + class: vue.normalizeClass(["tab-item", { active: $setup.activeTab === "workspace" }]), + onClick: _cache[9] || (_cache[9] = ($event) => $setup.switchTab("workspace")) + }, + " 工作区 ", + 2 + /* CLASS */ + ), + vue.createElementVNode( + "view", + { + class: vue.normalizeClass(["tab-item", { active: $setup.activeTab === "downloads" }]), + onClick: _cache[10] || (_cache[10] = ($event) => $setup.switchTab("downloads")) + }, + [ + vue.createTextVNode(" 下载列表 "), + $setup.downloadList.length > 0 ? (vue.openBlock(), vue.createElementBlock( + "text", + { + key: 0, + class: "tab-badge" + }, + vue.toDisplayString($setup.downloadList.length), + 1 + /* TEXT */ + )) : vue.createCommentVNode("v-if", true) + ], + 2 + /* CLASS */ + ) ]) ]), - vue.createElementVNode("view", { class: "workspace-content" }, [ + $setup.activeTab === "workspace" ? (vue.openBlock(), vue.createElementBlock("view", { + key: 0, + class: "workspace-content" + }, [ $setup.currentFolderList.length === 0 ? (vue.openBlock(), vue.createElementBlock("view", { key: 0, class: "folder-null" @@ -8163,11 +8446,94 @@ This will fail in production.`); /* KEYED_FRAGMENT */ )) ])) : vue.createCommentVNode("v-if", true) - ]), + ])) : vue.createCommentVNode("v-if", true), + $setup.activeTab === "downloads" ? (vue.openBlock(), vue.createElementBlock("view", { + key: 1, + class: "download-content" + }, [ + $setup.downloadList.length === 0 ? (vue.openBlock(), vue.createElementBlock("view", { + key: 0, + class: "download-empty" + }, [ + vue.createElementVNode("text", { class: "iconfont icon-xiazai" }), + vue.createElementVNode("text", null, "暂无下载文件") + ])) : (vue.openBlock(), vue.createElementBlock("view", { + key: 1, + class: "download-list" + }, [ + vue.createElementVNode("view", { class: "download-header" }, [ + vue.createElementVNode( + "text", + null, + "已下载 " + vue.toDisplayString($setup.downloadList.length) + " 个文件", + 1 + /* TEXT */ + ), + vue.createElementVNode("text", { + class: "clear-btn", + onClick: $setup.handleClearDownloads + }, "清空") + ]), + (vue.openBlock(true), vue.createElementBlock( + vue.Fragment, + null, + vue.renderList($setup.downloadList, (item) => { + return vue.openBlock(), vue.createElementBlock("view", { + class: "download-item", + key: item.id, + onClick: ($event) => $setup.openDownloadFile(item) + }, [ + vue.createElementVNode("view", { class: "di-icon" }, [ + vue.createElementVNode( + "text", + { + class: vue.normalizeClass(["iconfont", $setup.getFileIcon(item.name)]) + }, + null, + 2 + /* CLASS */ + ) + ]), + vue.createElementVNode("view", { class: "di-info" }, [ + vue.createElementVNode( + "text", + { class: "di-name" }, + vue.toDisplayString(item.name), + 1 + /* TEXT */ + ), + vue.createElementVNode( + "text", + { class: "di-meta" }, + vue.toDisplayString($setup.formatDownloadTime(item.time)) + " · " + vue.toDisplayString($setup.formatDownloadSize(item.size)), + 1 + /* TEXT */ + ), + vue.createElementVNode( + "text", + { class: "di-path" }, + vue.toDisplayString(item.displayPath || item.path), + 1 + /* TEXT */ + ) + ]), + vue.createElementVNode("view", { + class: "di-delete", + onClick: vue.withModifiers(($event) => $setup.handleDeleteDownload(item.id), ["stop"]) + }, [ + vue.createElementVNode("text", { class: "iconfont icon-del" }) + ], 8, ["onClick"]) + ], 8, ["onClick"]); + }), + 128 + /* KEYED_FRAGMENT */ + )) + ])) + ])) : vue.createCommentVNode("v-if", true), $setup.isSelectFolder ? (vue.openBlock(), vue.createElementBlock( "view", { - key: 0, + key: 2, class: vue.normalizeClass(["workspace-footer", { "folder-actions": $setup.selectFileList.length > 0 }]) }, [ @@ -9388,7 +9754,7 @@ This will fail in production.`); __name: "TemplateSpace", setup(__props, { expose: __expose }) { __expose(); - const formatTime = (timestamp) => { + const formatTime2 = (timestamp) => { if (!timestamp) return ""; const seconds = Math.floor(timestamp); @@ -9693,7 +10059,7 @@ This will fail in production.`); }); } }; - const __returned__ = { formatTime, formatSize, handleLikeTemplate, templateName, editTemplate, checkTemplate, currentTemplateList, checkShowTemplate, navbarBeforeTitle, navbarTitle, isSelectFolder, selectFileList, handleSelectFolder, selectFolder, handleLongPress, handleRename, handleDownload, handleDelete, isMenuOpen, handleMenu, searchKeyword, isSearchFocus, handleSearchFocus, allTemplate, handleSelectAll, handleBackOrCheck, onMounted: vue.onMounted, ref: vue.ref }; + const __returned__ = { formatTime: formatTime2, formatSize, handleLikeTemplate, templateName, editTemplate, checkTemplate, currentTemplateList, checkShowTemplate, navbarBeforeTitle, navbarTitle, isSelectFolder, selectFileList, handleSelectFolder, selectFolder, handleLongPress, handleRename, handleDownload, handleDelete, isMenuOpen, handleMenu, searchKeyword, isSearchFocus, handleSearchFocus, allTemplate, handleSelectAll, handleBackOrCheck, onMounted: vue.onMounted, ref: vue.ref }; Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true }); return __returned__; } @@ -10077,7 +10443,7 @@ This will fail in production.`); return "icon-wenjianyasuo"; return "icon-qitawenjian"; }; - const formatFileSize = (bytes) => { + const formatFileSize2 = (bytes) => { if (!bytes) return ""; if (bytes < 1024) @@ -10878,7 +11244,7 @@ This will fail in production.`); await fetchTeamList(); await fetchDbList(); }); - const __returned__ = { userToken, userId, workspaceId, currentCategory, switchCategory, navbarTitle, isMenuOpen, isSelectFolder, selectFileList, handleMenu, handleSelectFolder, parentFolderName, handleBackOrUp, rawPersonalFileData, rawTeamFileData, personalFiles, teamFiles, currentFolderPath, isFolder, getFileExtension, getFileIcon, formatFileSize, currentFolderDisplayPath, personalFileCount, findFolderByPath, loadCurrentFiles, fetchFileList, fetchTeamFileList, handleFileClick, handleFileLongPress, handleDownload, openFile: openFile$1, openFileWithMenu, handleRename, handleDeleteFile, refreshCurrentFileList, showNewFolder, newFolderName, openNewFolderModal, closeNewFolderModal, confirmCreateFolder, showUploadPathInput, uploadPath, handleUploadFile, closeUploadPathInput, confirmUploadPath, chooseAndUploadFile, doUploadFiles, allDatabases, isDbLoading, personalDatabases, teamDatabases, showNewDb, newDbName, fetchDbList, openNewDbModal, closeNewDbModal, confirmCreateDb, showUploadDb, uploadDbFileName, uploadDbFilePath, openUploadDbModal, closeUploadDbModal, handlePickDbFile, confirmUploadDb, handleDeleteDb, handleDbClick, formatDbTime, getAccessLevelLabel, showTeamManage, teamGroups, currentTeamId, currentTeamMembers, inviteKeyword, currentTeamDescription, currentTeamType, currentUserRoleInTeam, isTeamAdmin, fetchTeamList, selectTeam, openTeamManage, closeTeamManage, showEditTeam, editTeamName, editTeamDesc, editTeamType, openEditTeamModal, closeEditTeamModal, confirmEditTeam, toggleMemberRole, removeTeamMemberHandler, refreshTeamMembers, searchResultList, searched, searchTeamMember, addTeamMemberHandler, handleDeleteTeam, onMounted: vue.onMounted, ref: vue.ref, computed: vue.computed, get getToken() { + const __returned__ = { userToken, userId, workspaceId, currentCategory, switchCategory, navbarTitle, isMenuOpen, isSelectFolder, selectFileList, handleMenu, handleSelectFolder, parentFolderName, handleBackOrUp, rawPersonalFileData, rawTeamFileData, personalFiles, teamFiles, currentFolderPath, isFolder, getFileExtension, getFileIcon, formatFileSize: formatFileSize2, currentFolderDisplayPath, personalFileCount, findFolderByPath, loadCurrentFiles, fetchFileList, fetchTeamFileList, handleFileClick, handleFileLongPress, handleDownload, openFile: openFile$1, openFileWithMenu, handleRename, handleDeleteFile, refreshCurrentFileList, showNewFolder, newFolderName, openNewFolderModal, closeNewFolderModal, confirmCreateFolder, showUploadPathInput, uploadPath, handleUploadFile, closeUploadPathInput, confirmUploadPath, chooseAndUploadFile, doUploadFiles, allDatabases, isDbLoading, personalDatabases, teamDatabases, showNewDb, newDbName, fetchDbList, openNewDbModal, closeNewDbModal, confirmCreateDb, showUploadDb, uploadDbFileName, uploadDbFilePath, openUploadDbModal, closeUploadDbModal, handlePickDbFile, confirmUploadDb, handleDeleteDb, handleDbClick, formatDbTime, getAccessLevelLabel, showTeamManage, teamGroups, currentTeamId, currentTeamMembers, inviteKeyword, currentTeamDescription, currentTeamType, currentUserRoleInTeam, isTeamAdmin, fetchTeamList, selectTeam, openTeamManage, closeTeamManage, showEditTeam, editTeamName, editTeamDesc, editTeamType, openEditTeamModal, closeEditTeamModal, confirmEditTeam, toggleMemberRole, removeTeamMemberHandler, refreshTeamMembers, searchResultList, searched, searchTeamMember, addTeamMemberHandler, handleDeleteTeam, onMounted: vue.onMounted, ref: vue.ref, computed: vue.computed, get getToken() { return getToken; }, get chooseFile() { return chooseFile; @@ -13056,7 +13422,7 @@ This will fail in production.`); const openFile$1 = (url) => { downloadAndOpen(url); }; - const formatFileSize = (size) => { + const formatFileSize2 = (size) => { if (!size) return "0KB"; if (size < 1) { @@ -13888,7 +14254,7 @@ This will fail in production.`); onUnload(() => { socketStore.disconnect(); }); - const __returned__ = { friendSocketStore, handleFriendConnect, socketStore, ChatType, convertMarkdownTable, renderTable, escapeLoneUnderscores, pareseMarkdown, sanitizeContent, previewImage, openFile: openFile$1, formatFileSize, parseFileInfo, selectNormalChat, showNewChatModal, closeNewChatModal, isChatSidebar, handleChatSidebar, goWorkSpace, goCloudDatabase, logOut, previewFileArray, uploadPhoto, deleteImage, fileList, uploadFile, handleConnect, isThinking, isSelfSent, isUploading, uploadedFiles, streamingMessageId, showMessageModal, detailLinks, handleChatContentClick, extractLinks, extractFileNameFromUrl, showMessageDetail, closeMessageDetail, downloadToast, get downloadToastTimer() { + const __returned__ = { friendSocketStore, handleFriendConnect, socketStore, ChatType, convertMarkdownTable, renderTable, escapeLoneUnderscores, pareseMarkdown, sanitizeContent, previewImage, openFile: openFile$1, formatFileSize: formatFileSize2, parseFileInfo, selectNormalChat, showNewChatModal, closeNewChatModal, isChatSidebar, handleChatSidebar, goWorkSpace, goCloudDatabase, logOut, previewFileArray, uploadPhoto, deleteImage, fileList, uploadFile, handleConnect, isThinking, isSelfSent, isUploading, uploadedFiles, streamingMessageId, showMessageModal, detailLinks, handleChatContentClick, extractLinks, extractFileNameFromUrl, showMessageDetail, closeMessageDetail, downloadToast, get downloadToastTimer() { return downloadToastTimer; }, set downloadToastTimer(v) { downloadToastTimer = v; diff --git a/unpackage/dist/dev/app-plus/pages/WYXD/WYXD.css b/unpackage/dist/dev/app-plus/pages/WYXD/WYXD.css index 0060a56..f305efa 100644 --- a/unpackage/dist/dev/app-plus/pages/WYXD/WYXD.css +++ b/unpackage/dist/dev/app-plus/pages/WYXD/WYXD.css @@ -333,6 +333,146 @@ .workspace-footer .iconfont[data-v-507487c4] { 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] { display: flex; flex-direction: column; diff --git a/unpackage/dist/dev/app-plus/pages/WorkSpace/TemplateSpace/TemplateSpace.css b/unpackage/dist/dev/app-plus/pages/WorkSpace/TemplateSpace/TemplateSpace.css index b453107..6a2a885 100644 --- a/unpackage/dist/dev/app-plus/pages/WorkSpace/TemplateSpace/TemplateSpace.css +++ b/unpackage/dist/dev/app-plus/pages/WorkSpace/TemplateSpace/TemplateSpace.css @@ -333,6 +333,146 @@ .workspace-footer .iconfont[data-v-769edd25] { 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] { } diff --git a/unpackage/dist/dev/app-plus/pages/WorkSpace/WorkSpace.css b/unpackage/dist/dev/app-plus/pages/WorkSpace/WorkSpace.css index e6ac8df..637b941 100644 --- a/unpackage/dist/dev/app-plus/pages/WorkSpace/WorkSpace.css +++ b/unpackage/dist/dev/app-plus/pages/WorkSpace/WorkSpace.css @@ -865,3 +865,143 @@ .workspace-footer .iconfont[data-v-c1a60359] { 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; +} diff --git a/utils/downloadManager.js b/utils/downloadManager.js new file mode 100644 index 0000000..bb5c342 --- /dev/null +++ b/utils/downloadManager.js @@ -0,0 +1,284 @@ +/** + * 下载管理工具 + * 文件保存到公共下载目录,用户可通过文件管理器访问 + */ + +const STORAGE_KEY = 'localDownloads' +const SUB_DIR = 'AppDownloads' // 公共下载目录下的子文件夹 + +/** + * 获取公共下载目录路径 + * @returns {Promise} 下载目录绝对路径 + */ +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())}` +}