/** * 文件打开工具 - 强制弹出应用选择器 * * 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() } }) }