feat(pages): 添加分片上传功能
- 在 pages.json 中新增 upload/index 页面 - 在 template.config.js 中添加分片上传的配置项
This commit is contained in:
parent
2e05794a09
commit
d9d2ab0518
89
src/api/system/chunkUpload/index.js
Normal file
89
src/api/system/chunkUpload/index.js
Normal file
@ -0,0 +1,89 @@
|
||||
import request from '@/utils/request'
|
||||
import config from "@/config";
|
||||
import { getToken } from "@/utils/auth";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**初始化上传 */
|
||||
export function initChunkUpload(fileName, fileSize) {
|
||||
return request({
|
||||
url: '/file/initUpload',
|
||||
method: 'post',
|
||||
params: {
|
||||
fileName,
|
||||
fileSize
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**上传分片视频 */
|
||||
export function uploadChunk(uploadId, filePath, chunkIndex, formattedPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: `${config.baseUrl}/file/uploadChunk`,
|
||||
filePath: formattedPath,
|
||||
name: "chunk",
|
||||
timeout: 60000, // 增加超时时间到60秒
|
||||
header: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
},
|
||||
formData: {
|
||||
uploadId: uploadId,
|
||||
filePath: filePath,
|
||||
chunkIndex: chunkIndex,
|
||||
},
|
||||
success: (res) => {
|
||||
try {
|
||||
const resultData = JSON.parse(res.data);
|
||||
resolve(resultData);
|
||||
} catch (error) {
|
||||
console.error("解析上传结果失败:", error);
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error(`分片${chunkIndex}上传请求失败:`, err);
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**完成分片上传 */
|
||||
export function completeChunkUpload(uploadId, filePath, fileSize, fileName, partETags) {
|
||||
return request({
|
||||
url: '/file/completeUpload',
|
||||
method: 'post',
|
||||
params: {
|
||||
uploadId,
|
||||
filePath,
|
||||
fileSize,
|
||||
fileName,
|
||||
},
|
||||
data: partETags
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<view class="confirm-dialog" v-if="visible">
|
||||
<view class="dialog-mask"></view>
|
||||
<view class="dialog-content">
|
||||
<view class="dialog-header">提示</view>
|
||||
<view class="dialog-title">{{ title }}</view>
|
||||
<view class="dialog-buttons">
|
||||
<button class="button cancel-btn" @click="handleCancel" hover-class="button-hover">{{ cancelText
|
||||
}}</button>
|
||||
<button class="button confirm-btn" @click="handleConfirm" hover-class="button-hover"
|
||||
:style="{ background: color }">{{ confirmText }}</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '提示'
|
||||
},
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: '取消'
|
||||
},
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: '确认'
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#1E88E5'
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['update:visible', 'confirm', 'cancel']);
|
||||
const handleConfirm = () => {
|
||||
emit('update:visible', false);
|
||||
emit('confirm')
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('cancel')
|
||||
emit('update:visible', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.confirm-dialog {
|
||||
.dialog-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 280px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 12px;
|
||||
z-index: 1000;
|
||||
|
||||
.dialog-header {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
padding: 10px 20px 20px 20px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.dialog-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.button {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
border-radius: 8rpx;
|
||||
border: none;
|
||||
|
||||
&.button-hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: #F5F5F5;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<view class="upload-box">
|
||||
<view>
|
||||
<view class="upload-container" v-if="!videoCover" @click="chooseVideo">
|
||||
<uni-icons type="cloud-upload-filled" size="60" color="#ff6634"></uni-icons>
|
||||
<text class="upload-text">{{ uploadText }}</text>
|
||||
<text class="upload-desc">{{ displayUploadDesc }}</text>
|
||||
</view>
|
||||
<view v-else class="video-preview">
|
||||
<view class="cover-container">
|
||||
<view class="icon-cuo" @tap.stop="delectVideo">
|
||||
<uni-icons type="close" size="16" color="#ffffff"></uni-icons>
|
||||
</view>
|
||||
<video :src="videoCover" class="video-cover" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<ConfirmDialog v-model:visible="showModal" title="是否要删除此视频" @confirm="confirmDelete" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { defineProps, defineEmits } from 'vue'
|
||||
import modal from '@/plugins/modal';
|
||||
import ConfirmDialog from '@/components/geek-xd/components/geek-confirm-dialog/geek-confirm-dialog.vue'
|
||||
|
||||
const props = defineProps({
|
||||
uploadText: {
|
||||
type: String,
|
||||
default: '点击上传视频文件'
|
||||
},
|
||||
uploadDesc: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
sourceType: {
|
||||
type: Array,
|
||||
default: () => ['album', 'camera']
|
||||
},
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 500 // 默认500M
|
||||
},
|
||||
supportedTypes: {
|
||||
type: Array,
|
||||
default: () => ['mp4', 'mov', 'avi'] // 支持的视频格式
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['upload', 'delete'])
|
||||
|
||||
const videoCover = ref('')
|
||||
const showModal = ref(false)
|
||||
|
||||
/**
|
||||
* 动态生成上传描述文本
|
||||
* 如果用户提供了自定义描述则使用,否则根据支持的格式和文件大小限制自动生成
|
||||
* @returns {string} 上传描述文本
|
||||
*/
|
||||
const displayUploadDesc = computed(() => {
|
||||
return props.uploadDesc || `支持${props.supportedTypes.join('/')}等格式,文件大小不超过${props.maxSize}M`
|
||||
})
|
||||
|
||||
/**
|
||||
* 删除视频处理函数
|
||||
* 阻止事件冒泡并显示确认删除对话框
|
||||
* @param {Event} event - 点击事件对象
|
||||
*/
|
||||
const delectVideo = (event) => {
|
||||
event.stopPropagation();
|
||||
showModal.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认删除视频
|
||||
* 关闭确认对话框,清空视频封面,并触发删除事件
|
||||
*/
|
||||
const confirmDelete = () => {
|
||||
showModal.value = false
|
||||
videoCover.value = null
|
||||
emit('delete')
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件路径中提取文件扩展名
|
||||
* @param {string} filePath - 文件路径
|
||||
* @returns {string} 文件扩展名(小写),如果没有扩展名则返回空字符串
|
||||
*/
|
||||
const getFileExtension = (filePath) => {
|
||||
if (!filePath) {
|
||||
return '';
|
||||
}
|
||||
const dotIndex = filePath.lastIndexOf('.');
|
||||
if (dotIndex === -1) {
|
||||
return '';
|
||||
}
|
||||
return filePath.substring(dotIndex + 1).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证视频文件类型是否符合支持的格式
|
||||
* 统一的跨平台文件类型验证
|
||||
* @param {string} filePath - 视频文件路径
|
||||
* @returns {boolean} 验证通过返回true,否则返回false
|
||||
*/
|
||||
const validateVideoFileType = (filePath) => {
|
||||
if (!filePath) {
|
||||
modal.msg('无法识别文件类型');
|
||||
return false;
|
||||
}
|
||||
|
||||
const fileExtension = getFileExtension(filePath);
|
||||
console.log('文件类型:', fileExtension);
|
||||
|
||||
if (props.supportedTypes.includes(fileExtension)) return true;
|
||||
|
||||
modal.msg(`请上传正确格式的视频`);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查视频文件大小是否超过限制
|
||||
* @param {number} fileSize - 文件大小(字节)
|
||||
* @returns {boolean} 文件大小符合要求返回true,否则返回false
|
||||
*/
|
||||
const checkVideoFileSize = (fileSize) => {
|
||||
const maxSize = props.maxSize * 1024 * 1024;
|
||||
if (fileSize <= maxSize) return true;
|
||||
modal.msg(`视频大小不能超过${props.maxSize}M`);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装视频数据对象
|
||||
* 根据不同平台返回不同格式的视频数据
|
||||
* @param {Object} res - uni.chooseVideo返回的结果对象
|
||||
* @returns {Object} 组装后的视频数据对象
|
||||
*/
|
||||
const buildVideoData = (res) => {
|
||||
let videoData = {}
|
||||
console.log('选择的视频文件:', res);
|
||||
|
||||
// #ifdef APP-PLUS || H5
|
||||
videoData = {
|
||||
path: res.tempFilePath,
|
||||
size: res.size,
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
videoData = {
|
||||
path: res.tempFilePath,
|
||||
value: res.tempFilePath,
|
||||
...res,
|
||||
}
|
||||
// #endif
|
||||
|
||||
|
||||
console.log('组装后的视频数据:', videoData);
|
||||
|
||||
return videoData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择视频文件
|
||||
* 调用uni-app的chooseVideo API选择视频文件
|
||||
* 包含文件类型验证、大小验证和数据处理
|
||||
*/
|
||||
const chooseVideo = () => {
|
||||
uni.chooseVideo({
|
||||
count: 1,
|
||||
compressed: false,
|
||||
sourceType: props.sourceType,
|
||||
success: (res) => {
|
||||
if (!validateVideoFileType(res.tempFilePath)) return;
|
||||
if (!checkVideoFileSize(res.size)) return;
|
||||
videoCover.value = res.tempFilePath
|
||||
const videoData = buildVideoData(res)
|
||||
setTimeout(() => {
|
||||
modal.msgSuccess('上传成功')
|
||||
emit('upload', videoData)
|
||||
}, 1000)
|
||||
},
|
||||
fail: (err) => {
|
||||
modal.msg('选择视频失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.upload-box {
|
||||
height: 350rpx;
|
||||
margin: 0 20rpx;
|
||||
background: #FFFCFA;
|
||||
border: 2rpx dashed #ff6634;
|
||||
border-radius: 8rpx;
|
||||
padding: 40rpx 0;
|
||||
text-align: center;
|
||||
margin-bottom: 30rpx;
|
||||
margin-top: 20rpx;
|
||||
|
||||
.upload-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #ff6634;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.upload-desc {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.video-preview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.cover-container {
|
||||
position: relative;
|
||||
width: 80%;
|
||||
height: 100%;
|
||||
|
||||
.icon-cuo {
|
||||
position: absolute;
|
||||
top: -20rpx;
|
||||
right: -30rpx;
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
z-index: 10;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
background-color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
.video-cover {
|
||||
width: 100%;
|
||||
height: 220rpx;
|
||||
border-radius: 8rpx;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -236,6 +236,9 @@
|
||||
},
|
||||
{
|
||||
"path": "code/index"
|
||||
},
|
||||
{
|
||||
"path": "upload/index"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -14,7 +14,13 @@ export default [
|
||||
icon: 'wxCenter',
|
||||
title: '二维码',
|
||||
title_en: 'index',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/pages_geek/pages/upload/index',
|
||||
icon: 'wxCenter',
|
||||
title: '分片上传',
|
||||
title_en: 'index',
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
155
src/pages_geek/pages/upload/index.vue
Normal file
155
src/pages_geek/pages/upload/index.vue
Normal file
@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<view class="upload-page">
|
||||
<uni-section class="mb-10" title="视频上传" sub-title="支持大文件分片上传" type="line" />
|
||||
|
||||
<geek-uploadbox @upload="handleUpload" @delete="handleDelete" upload-text="选择视频文件"
|
||||
upload-desc="支持 mp4/mov/avi 格式,最大 500M" />
|
||||
|
||||
<view class="upload-actions">
|
||||
<button class="upload-btn" :disabled="!videoFile" @click="start">
|
||||
{{ videoFile ? '开始上传' : '请先选择文件' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import modal from '@/plugins/modal'
|
||||
import { wxChunkUploader } from '@/utils/ChunkUploaderWx'
|
||||
import appChunkUploader from '@/utils/ChunkUploaderApp'
|
||||
|
||||
const videoFile = ref(null)
|
||||
|
||||
/**
|
||||
* 处理视频上传
|
||||
* @param {Object} data - 上传的视频数据
|
||||
*/
|
||||
const handleUpload = (data) => {
|
||||
videoFile.value = data
|
||||
console.log('上传的视频文件:', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理视频删除
|
||||
*/
|
||||
const handleDelete = () => {
|
||||
videoFile.value = null
|
||||
console.log('视频文件已删除')
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验上传参数
|
||||
* @returns {boolean} 校验结果
|
||||
*/
|
||||
const validateParams = () => {
|
||||
if (videoFile.value) return true
|
||||
modal.msg('请先选择视频文件')
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序分片上传
|
||||
* @returns {Promise<boolean>} 上传结果
|
||||
*/
|
||||
const handleWxChunkUpload = async () => {
|
||||
try {
|
||||
const result = await wxChunkUploader.upload({
|
||||
file: videoFile.value,
|
||||
onSuccess: (result) => {
|
||||
modal.msgSuccess('上传成功')
|
||||
},
|
||||
onError: (error) => {
|
||||
modal.msgError('上传失败')
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('APP上传失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* APP端分片上传
|
||||
* @returns {Promise<boolean>} 上传结果
|
||||
*/
|
||||
const handleAppChunkUpload = async () => {
|
||||
try {
|
||||
const file = videoFile.value
|
||||
|
||||
const result = await appChunkUploader.upload({
|
||||
file: file,
|
||||
onProgress: (progress) => {
|
||||
console.log('上传进度:', progress)
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
console.log('上传成功:', result)
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('上传失败:', error)
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('APP上传失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始上传
|
||||
*/
|
||||
const start = async () => {
|
||||
if (!validateParams()) return
|
||||
|
||||
try {
|
||||
// #ifdef MP-WEIXIN
|
||||
await handleWxChunkUpload()
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
await handleAppChunkUpload()
|
||||
// #endif
|
||||
} catch (error) {
|
||||
console.error('上传过程出错:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.upload-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f6f6f6;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.upload-actions {
|
||||
margin-top: 40rpx;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: linear-gradient(135deg, #1976d2, #42a5f5);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:not(:disabled):active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 4rpx 12rpx rgba(25, 118, 210, 0.3);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #cccccc;
|
||||
color: #999999;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
288
src/utils/ChunkUploaderApp.js
Normal file
288
src/utils/ChunkUploaderApp.js
Normal file
@ -0,0 +1,288 @@
|
||||
import modal from '@/plugins/modal'
|
||||
import {
|
||||
deleteLocalFile,
|
||||
copyFileToSandbox,
|
||||
readAppFileChunk,
|
||||
getFileName,
|
||||
deleteTempFile,
|
||||
createAndWriteTempFile
|
||||
} from "@/utils/fileOper"
|
||||
import { initChunkUpload, uploadChunk, completeChunkUpload } from '@/api/system/chunkUpload'
|
||||
|
||||
/**
|
||||
* APP端分片上传工具类
|
||||
*/
|
||||
class AppChunkUploader {
|
||||
constructor(options = {}) {
|
||||
this.config = {
|
||||
chunkSize: 15 * 1024 * 1024, // 默认分片大小15MB
|
||||
concurrentLimit: 2, // 并发上传的分片数量
|
||||
...options
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取切片end位置
|
||||
* @param {number} start - 开始位置
|
||||
* @param {number} chunkSize - 分片大小
|
||||
* @param {number} fileSize - 文件总大小
|
||||
* @param {number} index - 分片索引
|
||||
* @param {number} totalChunks - 总分片数
|
||||
* @returns {number} end位置
|
||||
*/
|
||||
getSliceEnd(start, chunkSize, fileSize, index, totalChunks) {
|
||||
return index < totalChunks - 1 ? start + chunkSize - 1 : fileSize
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* APP端分片上传单个分片
|
||||
* @param {string} uploadId - 上传ID
|
||||
* @param {string} filePath - 文件路径
|
||||
* @param {number} chunkIndex - 分片索引
|
||||
* @param {ArrayBuffer} chunk - 分片数据
|
||||
* @returns {Promise} 上传结果
|
||||
*/
|
||||
async uploadAppChunk(uploadId, filePath, chunkIndex, chunk) {
|
||||
try {
|
||||
const response = await this.startUploadAppChunk(uploadId, filePath, chunkIndex, chunk)
|
||||
return response
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行APP端分片上传
|
||||
* @param {string} uploadId - 上传ID
|
||||
* @param {string} filePath - 文件路径
|
||||
* @param {number} chunkIndex - 分片索引
|
||||
* @param {ArrayBuffer} chunk - 分片数据
|
||||
* @returns {Promise} 上传结果
|
||||
*/
|
||||
startUploadAppChunk(uploadId, filePath, chunkIndex, chunk) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
// 1. 准备临时文件信息
|
||||
const tempFileName = `temp_chunk/chunk_${uploadId}_${chunkIndex}.bin`
|
||||
const tempDirPath = plus.io.PRIVATE_DOC
|
||||
|
||||
// 2. 创建并写入临时文件
|
||||
const tempFilePath = await createAndWriteTempFile(
|
||||
tempDirPath,
|
||||
tempFileName,
|
||||
chunk
|
||||
)
|
||||
|
||||
//设置文件的全路径
|
||||
let formattedPath = tempFilePath
|
||||
if (tempFilePath && !tempFilePath.startsWith("file://")) {
|
||||
formattedPath = `file://${tempFilePath}`
|
||||
}
|
||||
|
||||
// 3. 上传文件
|
||||
const result = await uploadChunk(uploadId, filePath, chunkIndex, formattedPath)
|
||||
|
||||
// 4. 删除临时文件
|
||||
await deleteTempFile(tempDirPath, tempFileName)
|
||||
|
||||
resolve(result)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 并发上传分片
|
||||
* @param {Array} tasks - 分片任务数组
|
||||
* @param {number} batchSize - 批次大小
|
||||
* @param {string} uploadId - 上传ID
|
||||
* @param {string} filePath - 文件路径
|
||||
* @param {string} localFilePath - 本地文件路径
|
||||
* @param {Array} partETags - 分片ETag数组
|
||||
* @param {Object} progressInfo - 进度信息对象
|
||||
* @returns {Promise<Array>} 上传结果数组
|
||||
*/
|
||||
async uploadChunksInBatches(tasks, batchSize, uploadId, filePath, localFilePath, partETags, progressInfo) {
|
||||
const results = []
|
||||
const { chunkCount } = progressInfo
|
||||
|
||||
for (let i = 0; i < tasks.length; i += batchSize) {
|
||||
const batch = tasks.slice(i, i + batchSize)
|
||||
|
||||
try {
|
||||
const batchResults = await Promise.all(
|
||||
batch.map((task) => this.uploadChunkConcurrently(task, uploadId, filePath, localFilePath, partETags, progressInfo))
|
||||
)
|
||||
results.push(...batchResults)
|
||||
} catch (error) {
|
||||
// 如果批次中有任何分片失败,立即停止上传
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 并发上传单个分片
|
||||
* @param {Object} chunkTask - 分片任务
|
||||
* @param {string} uploadId - 上传ID
|
||||
* @param {string} filePath - 文件路径
|
||||
* @param {string} localFilePath - 本地文件路径
|
||||
* @param {Array} partETags - 分片ETag数组
|
||||
* @param {Object} progressInfo - 进度信息对象
|
||||
* @returns {Promise} 上传结果
|
||||
*/
|
||||
async uploadChunkConcurrently(chunkTask, uploadId, filePath, localFilePath, partETags, progressInfo) {
|
||||
const { index, start, end } = chunkTask
|
||||
const { chunkCount } = progressInfo
|
||||
|
||||
const chunk = await readAppFileChunk(localFilePath, start, end - start)
|
||||
|
||||
const response = await this.uploadAppChunk(uploadId, filePath, index, chunk)
|
||||
|
||||
if (response.data && response.data.etag) {
|
||||
partETags[index] = {
|
||||
partNumber: index + 1,
|
||||
ETag: response.data.etag,
|
||||
}
|
||||
}
|
||||
|
||||
progressInfo.completedChunks++
|
||||
const percent = Math.floor((progressInfo.completedChunks / chunkCount) * 100)
|
||||
const displayPercent = Math.floor(percent / 10) * 10 // 每10%更新一次
|
||||
|
||||
if (displayPercent !== progressInfo.uploadProgress || progressInfo.completedChunks === chunkCount) {
|
||||
modal.closeLoading()
|
||||
modal.loading(`上传中 ${percent}% (请勿离开此页面)`)
|
||||
progressInfo.uploadProgress = displayPercent
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 主要的分片上传方法
|
||||
* @param {Object} options - 上传选项
|
||||
* @param {Object} options.file - 文件对象(包含path和size属性)
|
||||
* @param {string} options.filePath - 文件路径(如果提供file,此参数可选)
|
||||
* @param {string} options.fileName - 文件名称(可选,会自动从路径提取)
|
||||
* @param {number} options.fileSize - 文件大小(如果提供file,此参数可选)
|
||||
* @param {Function} options.onProgress - 进度回调函数(可选)
|
||||
* @param {Function} options.onSuccess - 成功回调函数(可选)
|
||||
* @param {Function} options.onError - 错误回调函数(可选)
|
||||
* @returns {Promise<boolean>} 上传结果
|
||||
*/
|
||||
async upload(options) {
|
||||
const {
|
||||
file,
|
||||
onProgress,
|
||||
onSuccess,
|
||||
onError
|
||||
} = options
|
||||
|
||||
// 优先使用file对象,否则使用单独传入的参数
|
||||
const actualFilePath = file.path
|
||||
const actualFileSize = file.size
|
||||
|
||||
if (!actualFilePath) {
|
||||
throw new Error('必须提供 filePath 或包含 path 属性的 file 对象')
|
||||
}
|
||||
|
||||
if (!actualFileSize) {
|
||||
throw new Error('必须提供 fileSize 或包含 size 属性的 file 对象')
|
||||
}
|
||||
|
||||
try {
|
||||
//初始化文件状态
|
||||
let localFilePath = actualFilePath
|
||||
const actualFileName = getFileName(localFilePath)
|
||||
|
||||
modal.loading("准备上传...")
|
||||
|
||||
// 1.计算分片数量
|
||||
const chunkSize = this.config.chunkSize
|
||||
const chunkCount = Math.ceil(actualFileSize / chunkSize)
|
||||
|
||||
//2.初始化分片上传
|
||||
const initResult = await initChunkUpload(actualFileName, actualFileSize)
|
||||
if (initResult.code !== 200) throw new Error("初始化上传失败")
|
||||
|
||||
const { uploadId, filePath: serverFilePath } = initResult.data
|
||||
const partETags = []
|
||||
|
||||
//3.将文件移动到应用 沙盒 目录
|
||||
localFilePath = await copyFileToSandbox(localFilePath)
|
||||
|
||||
//4.上传所有分片
|
||||
modal.closeLoading()
|
||||
modal.loading("上传中...")
|
||||
|
||||
//5.进度信息对象
|
||||
const progressInfo = {
|
||||
completedChunks: 0,
|
||||
uploadProgress: 0,
|
||||
chunkCount
|
||||
}
|
||||
|
||||
// 创建分片任务队列
|
||||
const chunkTasks = []
|
||||
for (let i = 0; i < chunkCount; i++) {
|
||||
chunkTasks.push({
|
||||
index: i,
|
||||
start: i * chunkSize,
|
||||
end: this.getSliceEnd(i * chunkSize, chunkSize, actualFileSize, i, chunkCount),
|
||||
})
|
||||
}
|
||||
|
||||
//并发上传数据
|
||||
await this.uploadChunksInBatches(
|
||||
chunkTasks,
|
||||
this.config.concurrentLimit,
|
||||
uploadId,
|
||||
serverFilePath,
|
||||
localFilePath,
|
||||
partETags,
|
||||
progressInfo
|
||||
)
|
||||
|
||||
//合并分片
|
||||
modal.msg("正在合并分片...")
|
||||
|
||||
const result = await completeChunkUpload(
|
||||
uploadId, serverFilePath, actualFileSize, actualFileName, partETags
|
||||
)
|
||||
|
||||
await deleteLocalFile(localFilePath) //将临时文件删除,防止占用空间
|
||||
|
||||
modal.msgSuccess("上传成功")
|
||||
|
||||
// 执行成功回调
|
||||
if (onSuccess) {
|
||||
onSuccess(result)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
} catch (error) {
|
||||
modal.closeLoading()
|
||||
const errorMessage = `上传失败: ${error.message || error}`
|
||||
modal.msg(errorMessage)
|
||||
|
||||
// 执行错误回调
|
||||
if (onError) {
|
||||
onError(error)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建默认实例
|
||||
const appChunkUploader = new AppChunkUploader()
|
||||
|
||||
export default appChunkUploader
|
||||
export { AppChunkUploader }
|
||||
202
src/utils/ChunkUploaderWx.js
Normal file
202
src/utils/ChunkUploaderWx.js
Normal file
@ -0,0 +1,202 @@
|
||||
import { initChunkUpload, uploadChunk, completeChunkUpload } from '@/api/system/chunkUpload'
|
||||
import modal from "@/plugins/modal";
|
||||
import { getFileExtension } from "@/utils/fileOper";
|
||||
|
||||
|
||||
/**
|
||||
* 微信小程序分片上传工具类
|
||||
*/
|
||||
export class WxChunkUploader {
|
||||
constructor(config = {}) {
|
||||
this.chunkSize = config.chunkSize || 15 * 1024 * 1024; //默认分片大小,为 15MB
|
||||
this.lastDisplayPercent = 0; //初始化上次显示的上传进度百分比为0
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行分片上传
|
||||
* @param {Object} options - 上传选项
|
||||
* @param {Function} options.onSuccess - 成功回调
|
||||
* @param {Function} options.onError - 错误回调
|
||||
*/
|
||||
async upload(options) {
|
||||
const { file, onSuccess, onError } = options;
|
||||
|
||||
try {
|
||||
// 1. 校验数据
|
||||
this._validateParams(file);
|
||||
|
||||
// 2. 准备上传数据
|
||||
modal.loading("准备上传...");
|
||||
const uploadData = await this._prepareUploadData(file);
|
||||
|
||||
// 3. 执行分片上传
|
||||
modal.closeLoading();
|
||||
modal.loading("上传中...");
|
||||
const partETags = await this._uploadChunks(uploadData);
|
||||
|
||||
// 4. 合并文件
|
||||
modal.closeLoading();
|
||||
modal.loading("合并文件中...");
|
||||
|
||||
//模仿上传的时间,可删除
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
await this._completeUpload(uploadData, partETags);
|
||||
|
||||
setTimeout(() => {
|
||||
onSuccess?.({ success: true });
|
||||
}, 1000);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("分片上传失败:", error);
|
||||
modal.closeLoading();
|
||||
modal.msgError("上传失败");
|
||||
onError?.(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 校验参数
|
||||
*/
|
||||
_validateParams(file) {
|
||||
if (!file.path) throw new Error("文件路径不存在");
|
||||
if (!file.size) throw new Error("文件大小不存在");
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备上传数据
|
||||
*/
|
||||
async _prepareUploadData(file) {
|
||||
const fileSize = file.size;
|
||||
const tempFilePath = file.path;
|
||||
const uploadFileName = `weixin_${Date.now()}.${getFileExtension(tempFilePath)}`;
|
||||
const chunkCount = Math.ceil(fileSize / this.chunkSize);
|
||||
|
||||
console.log("分片数量:", chunkCount);
|
||||
|
||||
// 初始化分片上传
|
||||
const initResult = await initChunkUpload(uploadFileName, fileSize);
|
||||
if (initResult.code !== 200) throw new Error("初始化上传失败");
|
||||
return {
|
||||
uploadId: initResult.data.uploadId,
|
||||
filePath: initResult.data.filePath,
|
||||
uploadFileName: uploadFileName,
|
||||
fileSize: fileSize,
|
||||
chunkCount: chunkCount,
|
||||
tempFilePath: tempFilePath,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 上传所有分片
|
||||
*/
|
||||
// return {
|
||||
// uploadId: initResult.data.uploadId,
|
||||
// filePath: initResult.data.filePath,
|
||||
// fileName:initResult.data.fileName,
|
||||
// fileSize:fileSize,
|
||||
// chunkCount: chunkCount,
|
||||
// };
|
||||
|
||||
async _uploadChunks(uploadData) {
|
||||
const { uploadId, filePath, fileSize, chunkCount, tempFilePath } = uploadData;
|
||||
const fileManager = uni.getFileSystemManager();
|
||||
const partETags = [];
|
||||
|
||||
for (let i = 0; i < chunkCount; i++) {
|
||||
const start = i * this.chunkSize;
|
||||
const end = Math.min(start + this.chunkSize, fileSize);
|
||||
const tempChunkPath = `${wx.env.USER_DATA_PATH}/chunk_${i}.tmp`;
|
||||
|
||||
// 读取并写入分片
|
||||
await this._processChunk(fileManager, tempFilePath, tempChunkPath, start, end - start);
|
||||
|
||||
// 上传分片
|
||||
const response = await uploadChunk(uploadId, filePath, i, tempChunkPath);
|
||||
|
||||
if (response.data?.etag) {
|
||||
partETags.push({
|
||||
partNumber: i + 1,
|
||||
ETag: response.data.etag,
|
||||
});
|
||||
}
|
||||
// 清理临时文件
|
||||
this._cleanupTempFile(fileManager, tempChunkPath);
|
||||
|
||||
// 更新进度 - 确保完全执行完毕
|
||||
this._updateProgress(i, chunkCount);
|
||||
|
||||
}
|
||||
|
||||
return partETags;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个分片
|
||||
*/
|
||||
async _processChunk(fileManager, tempFilePath, tempChunkPath, start, length) {
|
||||
// 读取分片数据
|
||||
const readRes = await new Promise((resolve, reject) => {
|
||||
fileManager.readFile({
|
||||
filePath: tempFilePath,
|
||||
position: start,
|
||||
length: length,
|
||||
success: (res) => resolve(res.data),
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
|
||||
// 写入临时文件
|
||||
await new Promise((resolve, reject) => {
|
||||
fileManager.writeFile({
|
||||
filePath: tempChunkPath,
|
||||
data: readRes,
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 清理临时文件
|
||||
*/
|
||||
_cleanupTempFile(fileManager, tempChunkPath) {
|
||||
try {
|
||||
fileManager.unlinkSync(tempChunkPath);
|
||||
console.log("删除临时文件成功:", tempChunkPath);
|
||||
} catch (e) {
|
||||
console.error("删除临时文件错误:", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新上传进度
|
||||
*/
|
||||
_updateProgress(currentIndex, totalCount) {
|
||||
const percent = Math.floor(((currentIndex + 1) / totalCount) * 100);
|
||||
const displayPercent = Math.floor(percent / 20) * 20;
|
||||
if (displayPercent !== this.lastDisplayPercent || currentIndex === totalCount - 1) {
|
||||
modal.closeLoading();
|
||||
modal.loading(`上传中${displayPercent}%`);
|
||||
this.lastDisplayPercent = displayPercent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成上传
|
||||
*/
|
||||
async _completeUpload(uploadData, partETags) {
|
||||
const { uploadId, filePath, fileSize, uploadFileName } = uploadData;
|
||||
await completeChunkUpload(uploadId, filePath, fileSize, uploadFileName, partETags);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
export const wxChunkUploader = new WxChunkUploader();
|
||||
278
src/utils/fileOper.js
Normal file
278
src/utils/fileOper.js
Normal file
@ -0,0 +1,278 @@
|
||||
|
||||
/**
|
||||
* 将文件复制到应用沙盒目录
|
||||
* @param {*} srcUrl
|
||||
* @returns
|
||||
*/
|
||||
export const copyFileToSandbox = (srcUrl) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const newName = `file_${Date.now()}.${getFileExtension(srcUrl)}`;
|
||||
console.log("文件名称是什么>>>", newName);
|
||||
plus.io.requestFileSystem(
|
||||
plus.io.PRIVATE_DOC,
|
||||
function (dstEntry) {
|
||||
plus.io.resolveLocalFileSystemURL(
|
||||
srcUrl,
|
||||
function (srcEntry) {
|
||||
srcEntry.copyTo(
|
||||
dstEntry.root,
|
||||
newName,
|
||||
function (entry) {
|
||||
console.log("文件复制成功:", entry.fullPath);
|
||||
resolve(entry.fullPath);
|
||||
},
|
||||
function (e) {
|
||||
console.error("复制文件失败:", JSON.stringify(e));
|
||||
reject(e);
|
||||
}
|
||||
);
|
||||
},
|
||||
function (e) {
|
||||
console.error("获取目标目录失败:", JSON.stringify(e));
|
||||
reject(e);
|
||||
}
|
||||
);
|
||||
},
|
||||
function (e) {
|
||||
console.error("获取源文件失败:", JSON.stringify(e));
|
||||
reject(e);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除临时文件
|
||||
* @param {string} dirPath - 目录路径
|
||||
* @param {string} fileName - 文件名
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function deleteTempFile(dirPath, fileName) {
|
||||
return new Promise((resolve) => {
|
||||
plus.io.requestFileSystem(
|
||||
dirPath,
|
||||
(dirEntry) => {
|
||||
console.log("文件目录:", dirPath);
|
||||
console.log("目录存在:", dirEntry);
|
||||
dirEntry.root.getFile(
|
||||
fileName,
|
||||
{ create: false },
|
||||
(fileEntry) => {
|
||||
console.log("临时文件存在:", fileEntry);
|
||||
fileEntry.remove(
|
||||
() => {
|
||||
console.log("删除成功XXXXX");
|
||||
resolve();
|
||||
},
|
||||
(err) => {
|
||||
console.error("删除失败XXXXX:", err);
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
},
|
||||
() => resolve()
|
||||
);
|
||||
},
|
||||
() => resolve()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 删除本地临时文件(临时文件是分片生成的)
|
||||
* @param {*} filePath
|
||||
* @returns
|
||||
*/
|
||||
export const deleteLocalFile = (filePath) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!filePath) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
console.log("准备删除文件:", filePath);
|
||||
plus.io.resolveLocalFileSystemURL(
|
||||
filePath,
|
||||
(entry) => {
|
||||
entry.remove(
|
||||
() => {
|
||||
console.log("文件删除成功:", filePath);
|
||||
resolve(true);
|
||||
},
|
||||
(error) => {
|
||||
console.error("删除文件失败:", JSON.stringify(error));
|
||||
// 失败也视为完成,不中断流程
|
||||
resolve(false);
|
||||
}
|
||||
);
|
||||
},
|
||||
(error) => {
|
||||
console.error("获取文件引用失败:", JSON.stringify(error));
|
||||
// 失败也视为完成,不中断流程
|
||||
resolve(false);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据文件路径获取APP文件信息
|
||||
*/
|
||||
export const getAppFileInfo = (filePath) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
plus.io.resolveLocalFileSystemURL(
|
||||
filePath,
|
||||
(entry) => {
|
||||
entry.file(
|
||||
(file) => {
|
||||
resolve({
|
||||
size: file.size,
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
},
|
||||
(error) => {
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*读取分片的数据 */
|
||||
export const readAppFileChunk = (filePath, start, length) => {
|
||||
console.log("读取分片的路径是什么>>>", filePath);
|
||||
return new Promise((resolve, reject) => {
|
||||
plus.io.resolveLocalFileSystemURL(
|
||||
filePath,
|
||||
(entry) => {
|
||||
entry.file(
|
||||
(file) => {
|
||||
const reader = new plus.io.FileReader();
|
||||
try {
|
||||
const slice = file.slice(start, start + length);
|
||||
reader.readAsDataURL(slice);
|
||||
} catch (sliceError) {
|
||||
reject(sliceError);
|
||||
}
|
||||
reader.onloadend = (e) => {
|
||||
if (e.target.readyState == 2) {
|
||||
try {
|
||||
const base64 = e.target.result.split(",")[1];
|
||||
resolve(base64);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = (err) => {
|
||||
reject(err);
|
||||
};
|
||||
},
|
||||
(error) => {
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
},
|
||||
reject
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 获取文章的扩展名称
|
||||
* @param {*} filePath
|
||||
* @returns
|
||||
*/
|
||||
export const getFileExtension = (filePath) => {
|
||||
if (!filePath) {
|
||||
return "";
|
||||
}
|
||||
// 查找最后一个点号位置
|
||||
const dotIndex = filePath.lastIndexOf(".");
|
||||
if (dotIndex === -1) {
|
||||
return ""; // 没有找到扩展名
|
||||
}
|
||||
// 从点号后面提取扩展名
|
||||
return filePath.substring(dotIndex + 1).toLowerCase();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 获取文件名称
|
||||
* @param {*} filePath
|
||||
* @returns
|
||||
*/
|
||||
|
||||
export const getFileName = (filePath) => {
|
||||
if (!filePath) {
|
||||
return "";
|
||||
}
|
||||
// 查找最后一个斜杠位置
|
||||
const slashIndex = filePath.lastIndexOf("/");
|
||||
if (slashIndex === -1) {
|
||||
return filePath; // 没有斜杠,整个字符串可能就是文件名
|
||||
}
|
||||
// 从最后一个斜杠后面提取文件名
|
||||
return filePath.substring(slashIndex + 1);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 创建临时文件并写入数据
|
||||
* @param {string} dirPath - 目录路径
|
||||
* @param {string} fileName - 文件名
|
||||
* @param {ArrayBuffer} data - 要写入的数据
|
||||
* @returns {Promise<string>} 临时文件的完整路径
|
||||
*/
|
||||
export const createAndWriteTempFile = (dirPath, fileName, data) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
plus.io.requestFileSystem(
|
||||
dirPath,
|
||||
(dirEntry) => {
|
||||
dirEntry.root.getFile(
|
||||
fileName,
|
||||
{ create: true, exclusive: false },
|
||||
(fileEntry) => {
|
||||
fileEntry.createWriter(
|
||||
(writer) => {
|
||||
const filePath = fileEntry.fullPath
|
||||
// 设置写入成功回调
|
||||
writer.onwrite = function () {
|
||||
resolve(filePath)
|
||||
}
|
||||
// 设置写入失败回调
|
||||
writer.onerror = function (e) {
|
||||
reject(e)
|
||||
}
|
||||
// 写入数据
|
||||
try {
|
||||
if (data) {
|
||||
writer.writeAsBinary(data)
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
},
|
||||
(err) => reject(err)
|
||||
)
|
||||
},
|
||||
(err) => reject(err)
|
||||
)
|
||||
},
|
||||
(err) => {
|
||||
reject(err)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user