/** * 下载管理工具 * 文件保存到公共下载目录,用户可通过文件管理器访问 */ 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())}` }