一、上传文件
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>