Uniapp 适配安卓与 iOS 的 PDF、DOC 文件上传

一、上传文件

1、配置权限

首先需要在manifest.json中配置相应的读写本地文件的权限

javascript 复制代码
"app-plus" : {
    "webview" : {
        "allowFileAccess" : true,
        "allowFileAccessFromFileURLs" : true,
        "allowUniversalAccessFromFileURLs" : true,
        "hardwareAccelerated" : true
    },
    /* 权限配置 - 新增 */
    "permissions" : {
        "File" : {},
        "Runtime" : {},
        "Cache" : {},
        "Device" : {},
        "Gallery" : {},
        "NativeUI" : {},
        "NativeObj" : {},
        "IO" : {},
        "fileSystem" : {
            "description" : "访问文件系统"
        }
    },
"modules" : {
    "Camera" : {},
    "Barcode" : {},
    "filemanager" : {}
  },
    "distribute" : {
            /* android打包配置 */
            "android" : {
                "permissions" : [
                    "<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>",
                    "<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
                    "<uses-permission android:name=\"android.permission.MANAGE_EXTERNAL_STORAGE\" android:minSdkVersion=\"30\"/>",
                    "<uses-permission android:name=\"android.permission.ACCESS_MEDIA_LOCATION\"/>",
                    "<uses-permission android:name=\"android.permission.INTERNET\"/>"
                ],
                /* Android 11+ 文件访问适配 - 新增 */
                "android:requestLegacyExternalStorage" : true,
                /* 文件提供者配置 - 新增 */
                "application" : {
                    "meta-data" : [
                        {
                            "name" : "android.support.FILE_PROVIDER_PATHS",
                            "value" : "@xml/file_paths"
                        }
                    ]
                }
            }
        },
        "uniStatistics" : {
            "enable" : false
        }
    },

2、安卓上传

下面是安卓上传文件步骤(这里用pdf举例)

javascript 复制代码
chooseFile() {
	plus.io.chooseFile({
		title: '选择文件',
		filetypes: ['pdf'],
		multiple: false,
	}, async (e) => {
		try {
			const filePath = e.files[0];
			const fileName = filePath.substring(filePath.lastIndexOf('/') + 1);
			const fileExt = fileName.split('.').pop()?.toLowerCase();
			// 校验后缀是否为pdf
			if (fileExt !== 'pdf') {
				uni.showToast({
					title: '仅支持上传PDF格式文件',
					icon: 'none',
					duration: 2000
				});
				return; // 终止后续操作
			}
			plus.io.resolveLocalFileSystemURL(e.files[0], async (
				entry) => {
				const realPath = entry.toURL().startsWith('file://') ?
					entry.toURL() :
					'file://' + entry.toURL();
				const mimeType = entry.name.endsWith('.pdf') ?
					'application/pdf' :
					'application/octet-stream';
				const item = {
					url: realPath,
					filename: entry.name
				}
                console.log(realPath,'本地虚拟文件路径');// 在这里拿到realPath后就可以调用oss直传接口
				
			}, (err) => {
				// 错误处理
				const rawPath = e.files[0];
				const finalPath = rawPath.startsWith('file://') ? rawPath : 'file://'
					rawPath;
				// 可以在这里 fallback 处理
			});
		} catch (error) {
			console.error('异步操作出错:', error);
			uni.showToast({
				title: '操作失败',
				icon: 'none'
			});
		}
	});
},

3、ios上传

ios上传是通过webview加载本地静态html实现

uniapp webview+本地静态html+base 64文件传递混合上传方案

把下面这个html直接放在static目录下,注意需要引入 uni.webview.js,可以使用cdn或者下载放本地直接引用,放在本地直接引用加载速度更快,推荐下载下来直接引入使用
https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>选择 PDF 文件</title>
		<style>
			body {
				margin: 0;
				padding: 20px;
				font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
			}

			.upload-btn {
				width: 100%;
				padding: 15px;
				background: #007aff;
				color: white;
				border: none;
				border-radius: 8px;
				font-size: 18px;
				cursor: pointer;
				transition: background 0.2s;
			}

			.upload-btn:active {
				background: #0066cc;
			}

			.status {
				margin-top: 20px;
				text-align: center;
				color: #666;
				font-size: 16px;
				line-height: 1.5;
			}
		</style>
		<!-- 引入 uni.webview.js(必须) -->
		<!-- <script src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"></script> -->
		<script src="./js/uni.webview.1.5.2.js"></script>
	</head>
	<body>
		<input type="file" id="fileInput" accept=".pdf,application/pdf" style="display: none;" />
		<button class="upload-btn" onclick="selectPdf()">选择 PDF 文件</button>
		<div class="status" id="status">未选择文件</div>

		<script>
			// 状态管理
			let uniReady = false;
			let pendingFile = null; // 暂存用户选中的文件(用于 uni 未就绪时)
			const fileInput = document.getElementById('fileInput');
			const statusText = document.getElementById('status');

			// 监听 uni 初始化完成
			document.addEventListener('UniAppJSBridgeReady', function() {
				console.log('✅ uni 对象已初始化完成,可安全使用 uni.postMessage');
				uniReady = true;

				// 如果有暂存的文件,立即发送
				if (pendingFile) {
					statusText.textContent = '系统初始化完成,正在发送之前选择的文件...';
					sendByPostMessage(pendingFile.base64Data, pendingFile.file);
					pendingFile = null;
				}
			});

			// 触发文件选择
			function selectPdf() {
				fileInput.click();
			}

			// 文件选择监听
			fileInput.addEventListener('change', function(e) {
				const file = e.target.files[0];
				if (!file) return;

				// 校验是否为 PDF
				if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) {
					statusText.textContent = '❌ 请选择 PDF 格式文件';
					return;
				}

				statusText.textContent = `📄 正在读取文件:${file.name}`;
				const reader = new FileReader();

				reader.onload = function(event) {
					const base64Data = event.target.result;
					statusText.textContent = `✅ 文件读取完成,准备传递给 App...`;

					// 尝试发送(内部会判断 uni 是否 ready)
					sendByPostMessage(base64Data, file);
				};

				reader.onerror = function() {
					statusText.textContent = '❌ 文件读取失败,请重试';
					console.error('WebView 文件读取失败');
				};

				reader.readAsDataURL(file);
			});

			// 安全发送消息(核心逻辑)
			function sendByPostMessage(base64Data, file) {
				if (!uniReady) {
					// uni 还没 ready,暂存文件
					console.warn('⚠️ uni 未就绪,暂存文件待发送');
					pendingFile = {
						base64Data,
						file
					};
					statusText.textContent = '⏳ 系统初始化中,文件已暂存,稍后自动发送...';
					return;
				}

				try {
					const message = {
						type: 'pdfSelected',
						fileName: file.name,
						fileSize: file.size,
						base64: base64Data
					};

					// 使用 uni.postMessage 发送(uni-app WebView 专用)
					uni.postMessage({
						data: {
							message: message
						}
					});
					statusText.textContent = '📤 消息已发送,请等待 App 处理...';

					// 清空 input,避免重复选择相同文件无触发
					fileInput.value = '';
				} catch (error) {
					console.error('❌ uni.postMessage 发送失败:', error);
					statusText.textContent = '💥 消息发送失败,请返回重试';
				}
			}

			// (可选)接收 App 回传状态(如果 App 会 postMessage 回来)
			window.addEventListener('message', function(event) {
				const data = event.data;
				if (data && data.type === 'uploadStatus') {
					statusText.textContent = data.msg;
				}
			});
		</script>
	</body>
</html>

将文件选择(<input type="file">)和读取逻辑完全放在一个 本地静态 HTML 页面 中,利用浏览器原生能力触发系统文件选择器(尤其在 App 端比直接在 Vue 页面中更可靠)。

这里是写成一个组件,可直接在页面复用

html 复制代码
<template>
	<view class="container">
		<!-- 上传 -->
		<web-view :src="webviewUrl" @message="onWebviewMessage" class="webview"></web-view>
	</view>
</template>

<script>
	export default {
		data() {
			return {
				webviewUrl: plus.io.convertLocalFileSystemURL('/static/upload-pdf.html'),
				originalFileName: '',
			};
		},
		methods: {
			// 监听WebView消息
			onWebviewMessage(e) {
				try {
					let base64 = null;
					let fileName = '';
					if (e.detail && e.detail.data && e.detail.data[0] && e.detail.data[0].message) {
						base64 = e.detail.data[0].message.base64;
						fileName = e.detail.data[0].message.fileName || '';
					}
					if (base64) {
						console.log('开始处理PDF文件,原始文件名:', fileName);
						this.processPdfFile(base64, fileName);
					}
				} catch (err) {
					console.error('消息解析失败:', err);
					uni.hideLoading();
				}
			},
			processPdfFile(base64Data, originalFileName) {
				uni.showLoading({
					title: '处理中...',
					mask: true
				});
				this.originalFileName = originalFileName;
				console.log('使用uni.downloadFile方案处理Base64数据...');
				const base64Url = base64Data;
				uni.downloadFile({
					url: base64Url,
					success: async (res) => {
						if (res.statusCode === 200) {
							console.log('✅ 临时文件下载成功,路径:', res.tempFilePath);
							const isValid = await this.validateTempFile(res.tempFilePath);
							if (isValid) {
								// 在这里获取到虚拟路径就可以直接调用oss直传接口
							} else {
								uni.showToast({
									title: '文件验证失败',
									icon: 'error'
								});
							}
						} else {
							console.error('下载失败,状态码:', res.statusCode);
							uni.showToast({
								title: '文件处理失败',
								icon: 'error'
							});
						}
						uni.hideLoading();
					},
					fail: (err) => {
						console.error('下载失败:', err);
						uni.hideLoading();
						uni.showToast({
							title: '处理失败',
							icon: 'error'
						});
					}
				});
			},
			// 验证临时文件
			validateTempFile(tempFilePath) {
				return new Promise((resolve) => {
					plus.io.resolveLocalFileSystemURL(tempFilePath, (entry) => {
						entry.file((file) => {
							console.log(`📊 文件大小: ${file.size} 字节`);
							if (file.size < 100) {
								console.warn('⚠️ 文件大小异常,可能损坏');
								resolve(false);
								return;
							}
							const reader = new plus.io.FileReader();
							reader.onload = function(e) {
								const firstBytes = e.target.result.substr(0, 20);
								console.log('文件头信息:', firstBytes);
								if (firstBytes.includes('%PDF')) {
									console.log('确认是有效的PDF文件');
									resolve(true);
								} else {
									console.warn('文件头不是PDF格式');
									resolve(true);
								}
							};
							reader.onerror = function() {
								console.warn('无法读取文件头,但文件大小正常,继续上传');
								resolve(true);
							};
							reader.readAsText(file.slice(0, 100));
						}, (err) => {
							console.error('无法读取文件:', err);
							resolve(false);
						});
					}, (err) => {
						console.error('文件路径无效:', err);
						resolve(false);
					});
				});
			},
		}
	};
</script>

<style scoped>
	.container {
		width: 100%;
		height: 100vh;
	}
	.webview {
		width: 100%;
		height: 100%;
	}
</style>
相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
码兄科技4 天前
24小时自助健身房系统软件开发实战指南:功能设计与部署方案
java·spring boot·uni-app
茶底世界之下4 天前
为什么预览、录制、离线导出不能共享同一个背压策略?
ios·swift
AFinalStone5 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui