添加ts类型

This commit is contained in:
liuBingWei 2025-09-02 20:02:52 +08:00
parent d9d2ab0518
commit adf17d57fc
9 changed files with 822 additions and 788 deletions

View File

@ -87,9 +87,11 @@
"@dcloudio/uni-cli-shared": "3.0.0-4060420250429001",
"@dcloudio/uni-stacktracey": "3.0.0-4060420250429001",
"@dcloudio/vite-plugin-uni": "3.0.0-4060420250429001",
"@types/html5plus": "^1.0.5",
"@vue/runtime-core": "^3.5.12",
"@vue/tsconfig": "^0.5.1",
"less": "^4.2.0",
"miniprogram-api-typings": "^4.1.0",
"sass": "1.78.0",
"sass-loader": "^16.0.1",
"typescript": "^5.6.2",

View File

@ -151,8 +151,7 @@ const buildVideoData = (res) => {
// #ifdef MP-WEIXIN
videoData = {
path: res.tempFilePath,
value: res.tempFilePath,
...res,
size:res.size
}
// #endif

View File

@ -14,7 +14,7 @@
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ref } from 'vue'
import modal from '@/plugins/modal'
import { wxChunkUploader } from '@/utils/ChunkUploaderWx'
import appChunkUploader from '@/utils/ChunkUploaderApp'
@ -53,20 +53,16 @@ const validateParams = () => {
* @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
}
const result = await wxChunkUploader.upload({
file: videoFile.value,
onSuccess: () => {
modal.msgSuccess('上传成功')
},
onError: (error) => {
modal.msg(error)
}
});
return result
}
/**
@ -79,9 +75,6 @@ const handleAppChunkUpload = async () => {
const result = await appChunkUploader.upload({
file: file,
onProgress: (progress) => {
console.log('上传进度:', progress)
},
onSuccess: (result) => {
console.log('上传成功:', result)
},

47
src/types/upload.ts Normal file
View File

@ -0,0 +1,47 @@
export interface UploadOptions {
/**文件 */
file: File
/**成功回调 */
onSuccess?: (result: any) => void;
/**失败回调 */
onError?: (error: any) => void;
}
export interface File {
/**文件路径 */
path: string;
/**文件大小 */
size: number;
}
export interface UploadData {
/**上传编号 */
uploadId: string;
/**文件在云端保存路径 */
saveFilePath: string;
/**上传文件的名称 */
uploadFileName: string;
/**上传文件的大小 */
fileSize: number;
/**分片数量 */
chunkCount: number;
/**上传文件的路径 */
filePath: string;
}
export interface PartETag {
partNumber: number;
ETag: string;
}
export interface ChunkTask {
index: number;
start: number;
end: number;
}

View File

@ -1,288 +0,0 @@
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 }

View File

@ -0,0 +1,479 @@
import modal from '@/plugins/modal'
import { initChunkUpload, uploadChunk, completeChunkUpload } from '@/api/system/chunkUpload'
import { UploadOptions, PartETag, ChunkTask } from '@/types/upload'
/**
* APP端分片上传工具类
*
*/
class AppChunkUploader {
/**
*
*/
private chunkSize: number;
/**
*
*/
private concurrentLimit: number;
/**
* -
* 15MB2
*/
constructor() {
this.chunkSize = 15 * 1024 * 1024; // 默认分片大小15MB
this.concurrentLimit = 2; // 并发上传的分片数量
}
/**
*
* @param options
* @param options.file path和size属性
* @param options.onSuccess
* @param options.onError
* @returns Promise<boolean>
*/
async upload(options: UploadOptions): Promise<boolean> {
const { file, onSuccess, onError } = options
try {
const actualFilePath = file.path
const actualFileSize = file.size
if (!actualFilePath) throw new Error('文件路径不存在')
if (!actualFileSize) throw new Error('文件大小不存在')
//初始化文件状态
let localFilePath = actualFilePath
const actualFileName = this.getFileName(localFilePath)
modal.loading("准备上传...")
// 1.计算分片数量
const chunkSize = this.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: PartETag[] = [];
//3.将文件移动到应用 沙盒 目录
localFilePath = await this.copyFileToSandbox(localFilePath)
//4.上传所有分片
modal.closeLoading()
modal.loading("上传中...")
//5.进度信息对象
const progressInfo = {
completedChunks: 0,
uploadProgress: 0,
chunkCount
}
// 创建分片任务队列
const chunkTasks: ChunkTask[] = []
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.concurrentLimit,
uploadId,
serverFilePath,
localFilePath,
partETags,
progressInfo
)
//合并分片
modal.closeLoading();
modal.loading("正在合并分片...")
//完成分片上传
await completeChunkUpload(
uploadId, serverFilePath, actualFileSize, actualFileName, partETags
)
//将临时文件删除,防止占用空间
await this.deleteLocalFile(localFilePath)
modal.closeLoading()
// 执行成功回调
onSuccess?.({ success: true })
return true
} catch (error) {
modal.closeLoading()
const errorMessage = error instanceof Error ? error.message : `上传失败`
onError?.(errorMessage)
return false
}
}
/**
* end位置
* @param start
* @param chunkSize
* @param fileSize
* @param index
* @param totalChunks
* @returns number
*/
getSliceEnd(start: number, chunkSize: number, fileSize: number, index: number, totalChunks: number) {
return index < totalChunks - 1 ? start + chunkSize - 1 : fileSize
}
/**
*
* @param tasks
* @param batchSize
* @param uploadId ID
* @param filePath
* @param localFilePath
* @param partETags ETag数组
* @param progressInfo
* @returns Promise<any[]>
*/
async uploadChunksInBatches(tasks: ChunkTask[], batchSize: number, uploadId: string, filePath: string, localFilePath: string, partETags: PartETag[], progressInfo: any): Promise<any[]> {
const results = []
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) {
const errorMessage = error instanceof Error ? error.message : '并发上传失败'
throw new Error(errorMessage)
}
}
return results
}
/**
* APP端分片上传单个分片
* @param uploadId ID
* @param filePath
* @param chunkIndex
* @param chunk ArrayBuffer或字符串
* @returns Promise<any>
*/
async uploadAppChunk(uploadId: string, filePath: string, chunkIndex: number, chunk: ArrayBuffer | string) {
try {
const response = await this.startUploadAppChunk(uploadId, filePath, chunkIndex, chunk)
return response
} catch (error) {
throw new Error('分片上传失败')
}
}
/**
* APP端分片上传
* @param uploadId ID
* @param filePath
* @param chunkIndex
* @param chunk ArrayBuffer或字符串
* @returns Promise Promise
*/
startUploadAppChunk(uploadId: string, filePath: string, chunkIndex: number, chunk: ArrayBuffer | string) {
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 this.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 this.deleteTempFile(tempDirPath, tempFileName)
resolve(result)
} catch (error) {
reject(error)
}
})
}
/**
*
* @param chunkTask indexstartend等信息
* @param uploadId ID
* @param filePath
* @param localFilePath
* @param partETags ETag数组
* @param progressInfo completedChunksuploadProgresschunkCount等
* @returns Promise<any>
*/
async uploadChunkConcurrently(chunkTask: any, uploadId: string, filePath: string, localFilePath: string, partETags: PartETag[], progressInfo: any) {
const { index, start, end } = chunkTask
const { chunkCount } = progressInfo
const chunk = await this.readAppFileChunk(localFilePath, start, end - start)
const response = await this.uploadAppChunk(uploadId, filePath, index, chunk) as any
if (response.data && response.data.etag) {
partETags.push({
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 filePath
* @returns string
*/
getFileName(filePath: string): string {
if (!filePath) {
return "";
}
// 查找最后一个斜杠位置
const slashIndex = filePath.lastIndexOf("/");
if (slashIndex === -1) {
return filePath; // 没有斜杠,整个字符串可能就是文件名
}
// 从最后一个斜杠后面提取文件名
return filePath.substring(slashIndex + 1);
};
/**
*
* @param srcUrl URL路径
* @returns Promise<string>
*/
copyFileToSandbox(srcUrl: string): Promise<string> {
return new Promise((resolve, reject) => {
const newName = `file_${Date.now()}.${this.getFileExtension(srcUrl)}`;
plus.io.requestFileSystem(
plus.io.PRIVATE_DOC,
(dstEntry) => {
plus.io.resolveLocalFileSystemURL(
srcUrl,
(srcEntry) => {
srcEntry.copyTo(
dstEntry.root,
newName,
(entry) => {
if (entry.fullPath) {
resolve(entry.fullPath);
} else {
reject(new Error('File path is undefined'));
}
},
(e) => reject(e)
);
},
(e) => reject(e)
);
},
(e) => reject(e)
);
});
};
/**
*
* @param filePath
* @returns string
*/
getFileExtension(filePath: string): string {
if (!filePath) {
return "";
}
// 查找最后一个点号位置
const dotIndex = filePath.lastIndexOf(".");
if (dotIndex === -1) {
return ""; // 没有找到扩展名
}
// 从点号后面提取扩展名
return filePath.substring(dotIndex + 1).toLowerCase();
};
/**
*
* @param filePath
* @returns Promise<boolean>
*/
deleteLocalFile(filePath: string): Promise<boolean> {
return new Promise((resolve, reject) => {
if (!filePath) {
resolve(false);
return;
}
plus.io.resolveLocalFileSystemURL(
filePath,
(entry) => {
entry.remove(
() => { resolve(true); },
(error) => { resolve(false); }
);
},
(error) => { resolve(false); }
);
});
};
/**
*
* @param dirPath plus.io.PRIVATE_DOC等
* @param fileName
* @param data ArrayBuffer或字符串
* @returns Promise<string>
*/
createAndWriteTempFile(dirPath: number, fileName: String, data: ArrayBuffer | string): Promise<string> {
return new Promise((resolve, reject) => {
plus.io.requestFileSystem(
dirPath,
(dirEntry: any) => {
dirEntry.root.getFile(
fileName,
{ create: true, exclusive: false },
(fileEntry: any) => {
fileEntry.createWriter(
(writer: any) => {
const filePath = fileEntry.fullPath
writer.onwrite = function () { resolve(filePath) }
writer.onerror = function (e: any) { reject(e) }
try {
if (data) writer.writeAsBinary(data)
} catch (e) { reject(e) }
},
(err: any) => reject(err)
)
},
(err: any) => reject(err)
)
},
(err) => { reject(err) }
)
})
}
/**
*
* @param dirPath plus.io.PRIVATE_DOC等
* @param fileName
* @returns Promise<boolean>
*/
deleteTempFile(dirPath: number, fileName: string): Promise<boolean> {
return new Promise((resolve, reject) => {
plus.io.requestFileSystem(
dirPath,
(dirEntry) => {
if (!dirEntry || !dirEntry.root) {
reject(new Error('Directory entry or root is undefined'));
return;
}
dirEntry.root.getFile(
fileName,
{ create: false },
(fileEntry) => {
fileEntry.remove(
() => { resolve(true); },
(err) => { resolve(true); }
);
},
() => resolve(true)
);
},
() => resolve(true)
);
});
}
/**
* APP端文件分片的数据
* @param filePath
* @param start
* @param length
* @returns Promise<string> Base64编码的分片数据
*/
readAppFileChunk(filePath: string, start: number, length: number): Promise<string> {
return new Promise((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(
filePath,
(entry: any) => {
entry.file(
(file: any) => {
const reader = new plus.io.FileReader();
try {
const slice = file.slice(start, start + length);
reader.readAsDataURL(slice);
} catch (sliceError) {
reject(sliceError);
}
reader.onloadend = (e: any) => {
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: any) => { reject(error); }
);
},
(error) => { reject(error); }
);
});
};
}
export default new AppChunkUploader()
export { AppChunkUploader }

View File

@ -1,202 +0,0 @@
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();

View File

@ -0,0 +1,282 @@
import { initChunkUpload, uploadChunk, completeChunkUpload } from '@/api/system/chunkUpload'
import modal from "@/plugins/modal";
import { UploadOptions, File, UploadData, PartETag } from "@/types/upload";
// 声明微信小程序全局对象
declare const wx: any;
/**
*
*
*
*
*/
export class WxChunkUploader {
/** 分片大小,单位字节 */
private chunkSize: number;
/** 上次显示的上传进度百分比 */
private lastDisplayPercent: number;
/**
*
* @param config
* @param config.chunkSize 15MB
*/
constructor() {
this.chunkSize = 15 * 1024 * 1024; //默认分片大小,为 15MB
this.lastDisplayPercent = 0; //初始化上次显示的上传进度百分比为0
}
/**
*
*
* @param options -
* @param options.file -
* @param options.onSuccess -
* @param options.onError -
* @returns Promise<boolean> -
*/
async upload(options: UploadOptions): Promise<boolean> {
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(() => {
modal.closeLoading();
onSuccess?.({ success: true });
}, 1000);
return true;
} catch (error) {
modal.closeLoading();
const errorMessage = error instanceof Error ? error.message : '上传失败';
onError?.(errorMessage);
return false;
}
}
/**
*
*
* @param file -
* @throws {Error}
* @throws {Error}
*/
_validateParams(file: File) {
if (!file.path) throw new Error("文件路径不存在");
if (!file.size) throw new Error("文件大小不存在");
}
/**
*
*
* @param file -
* @returns Promise<UploadData> - ID
* @throws {Error}
*/
async _prepareUploadData(file: File) {
try {
const fileSize = file.size;
const filePath = file.path;
const uploadFileName = `weixin_${Date.now()}.${this.getFileExtension(filePath)}`;
const chunkCount = Math.ceil(fileSize / this.chunkSize);
// 初始化分片上传
const initResult = await initChunkUpload(uploadFileName, fileSize);
if (initResult.code !== 200) throw new Error("初始化上传失败");
return {
uploadId: initResult.data.uploadId,
saveFilePath: initResult.data.filePath,
uploadFileName: uploadFileName,
fileSize: fileSize,
chunkCount: chunkCount,
filePath: filePath,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '准备上传数据失败';
throw new Error(`${errorMessage}`);
}
}
/**
*
*
* @param uploadData - ID
* @returns Promise<PartETag[]> - ETag信息数组
* @throws {Error}
*/
async _uploadChunks(uploadData: UploadData) {
try {
const { uploadId, saveFilePath, fileSize, chunkCount, filePath } = uploadData;
const fileManager = uni.getFileSystemManager();
const partETags: PartETag[] = [];
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, filePath, tempChunkPath, start, end - start);
// 上传分片
const response = await uploadChunk(uploadId, saveFilePath, 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;
} catch (e) {
const errorMessage = e instanceof Error ? e.message : '上传分片失败';
throw new Error(errorMessage);
}
}
/**
*
*
* @param fileManager - uni-app文件系统管理器实例
* @param filePath -
* @param tempChunkPath -
* @param start -
* @param length -
* @returns Promise<void> - Promise
* @throws {Error}
*/
async _processChunk(fileManager: UniApp.FileSystemManager, filePath: string, tempChunkPath: string, start: number, length: number) {
const readRes = await new Promise<ArrayBuffer | string>((resolve, reject) => {
fileManager.readFile({
filePath: filePath,
position: start,
length: length,
success: (res: any) => resolve(res.data as ArrayBuffer | string),
fail: reject,
});
});
// 写入临时文件
await new Promise((resolve, reject) => {
fileManager.writeFile({
filePath: tempChunkPath,
data: readRes,
success: resolve,
fail: reject,
});
});
}
/**
*
*
* @param fileManager - uni-app文件系统管理器实例
* @param tempChunkPath -
* @throws {Error}
*/
_cleanupTempFile(fileManager: UniApp.FileSystemManager, tempChunkPath: string) {
try {
fileManager.unlinkSync(tempChunkPath);
} catch (e) {
throw new Error("删除临时文件错误");
}
}
/**
*
*
* @param currentIndex - 0
* @param totalCount -
*/
_updateProgress(currentIndex: number, totalCount: number) {
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;
}
}
/**
*
*
* @param uploadData - ID等关键信息
* @param partETags - ETag信息数组
* @returns Promise<void> - Promise
* @throws {Error}
*/
async _completeUpload(uploadData: UploadData, partETags: PartETag[]) {
try {
const { uploadId, saveFilePath, fileSize, uploadFileName } = uploadData;
await completeChunkUpload(uploadId, saveFilePath, fileSize, uploadFileName, partETags);
} catch (e) {
const errorMessage = e instanceof Error ? e.message : '上传失败';
throw new Error(errorMessage);
}
}
/**
*
*
* @param filePath -
* @returns string - 'jpg', 'mp4', 'pdf'
* @example
* getFileExtension('/path/to/video.mp4') // 返回 'mp4'
* getFileExtension('/path/to/image.JPG') // 返回 'jpg'
* getFileExtension('/path/to/file') // 返回 ''
*/
getFileExtension(filePath: string): string {
if (!filePath) {
return "";
}
// 查找最后一个点号位置
const dotIndex = filePath.lastIndexOf(".");
if (dotIndex === -1) {
return ""; // 没有找到扩展名
}
// 从点号后面提取扩展名
return filePath.substring(dotIndex + 1).toLowerCase();
};
}
export const wxChunkUploader = new WxChunkUploader();

View File

@ -1,278 +0,0 @@
/**
* 将文件复制到应用沙盒目录
* @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)
}
)
})
}