diff --git a/package.json b/package.json
index 17f4887e6c3428cb7b96ed2c3d28439b9100553d..b9059f92319a8f19280a0ac52edbaf0fd09f598b 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/api/system/chunkUpload/index.js b/src/api/system/chunkUpload/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..a61257f93ce90b862859faa235344086429667ca
--- /dev/null
+++ b/src/api/system/chunkUpload/index.js
@@ -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
+ })
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/geek-xd/components/geek-confirm-dialog/geek-confirm-dialog.vue b/src/components/geek-xd/components/geek-confirm-dialog/geek-confirm-dialog.vue
new file mode 100644
index 0000000000000000000000000000000000000000..aafb1058dd82969175a83544101e296e8457b2c1
--- /dev/null
+++ b/src/components/geek-xd/components/geek-confirm-dialog/geek-confirm-dialog.vue
@@ -0,0 +1,123 @@
+
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/geek-xd/components/geek-uploadbox/geek-uploadbox.vue b/src/components/geek-xd/components/geek-uploadbox/geek-uploadbox.vue
new file mode 100644
index 0000000000000000000000000000000000000000..6d4a58b1df88a25d76c65d6ac5bb573b355e27bb
--- /dev/null
+++ b/src/components/geek-xd/components/geek-uploadbox/geek-uploadbox.vue
@@ -0,0 +1,270 @@
+
+
+
+
+
+ {{ uploadText }}
+ {{ displayUploadDesc }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pages.json b/src/pages.json
index 0e977d5bcb2f7d9ad7359a968d96ee366133953b..8248469f89a700b7709b937298ec646e50ec0e08 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -236,6 +236,9 @@
},
{
"path": "code/index"
+ },
+ {
+ "path": "upload/index"
}
]
}
diff --git a/src/pages/template.config.js b/src/pages/template.config.js
index 1e606a437a8f5b973f483f2c206adfe9bbac9d03..2e582d621f244382c522537376c3a324540d8fea 100644
--- a/src/pages/template.config.js
+++ b/src/pages/template.config.js
@@ -14,7 +14,13 @@ export default [
icon: 'wxCenter',
title: '二维码',
title_en: 'index',
- }
+ },
+ {
+ path: '/pages_geek/pages/upload/index',
+ icon: 'wxCenter',
+ title: '分片上传',
+ title_en: 'index',
+ },
]
},
{
diff --git a/src/pages_geek/pages/upload/index.vue b/src/pages_geek/pages/upload/index.vue
new file mode 100644
index 0000000000000000000000000000000000000000..cc468815aaa262796b9fdcf846d044492ca7048f
--- /dev/null
+++ b/src/pages_geek/pages/upload/index.vue
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/types/upload.ts b/src/types/upload.ts
new file mode 100644
index 0000000000000000000000000000000000000000..71e1adc68a1bbb3d96908d0278141ab5e6a3944a
--- /dev/null
+++ b/src/types/upload.ts
@@ -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;
+}
+
+
+
diff --git a/src/utils/ChunkUploaderApp.ts b/src/utils/ChunkUploaderApp.ts
new file mode 100644
index 0000000000000000000000000000000000000000..47a390f3d0eab95d59d9aff0de6c59ed1f479b1d
--- /dev/null
+++ b/src/utils/ChunkUploaderApp.ts
@@ -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;
+
+
+ /**
+ * 构造函数 - 初始化分片上传器
+ * 设置默认分片大小为15MB,并发限制为2个分片
+ */
+ 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 返回上传是否成功
+ */
+ async upload(options: UploadOptions): Promise {
+ 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 上传结果数组
+ */
+ async uploadChunksInBatches(tasks: ChunkTask[], batchSize: number, uploadId: string, filePath: string, localFilePath: string, partETags: PartETag[], progressInfo: any): Promise {
+ 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 上传响应结果
+ */
+ 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 分片任务对象,包含index、start、end等信息
+ * @param uploadId 上传ID
+ * @param filePath 服务器文件路径
+ * @param localFilePath 本地文件路径
+ * @param partETags 分片ETag数组,用于合并分片
+ * @param progressInfo 进度信息对象,包含completedChunks、uploadProgress、chunkCount等
+ * @returns Promise 上传响应结果
+ */
+ 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 复制后的文件完整路径
+ */
+ copyFileToSandbox(srcUrl: string): Promise {
+ 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 删除是否成功
+ */
+ deleteLocalFile(filePath: string): Promise {
+ 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 创建的临时文件完整路径
+ */
+ createAndWriteTempFile(dirPath: number, fileName: String, data: ArrayBuffer | string): Promise {
+ 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 删除是否成功
+ */
+ deleteTempFile(dirPath: number, fileName: string): Promise {
+ 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 Base64编码的分片数据
+ */
+ readAppFileChunk(filePath: string, start: number, length: number): Promise {
+ 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 }
diff --git a/src/utils/ChunkUploaderWx.ts b/src/utils/ChunkUploaderWx.ts
new file mode 100644
index 0000000000000000000000000000000000000000..175432acd2eef3d65b51d2be40e1942acd9ba35d
--- /dev/null
+++ b/src/utils/ChunkUploaderWx.ts
@@ -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 - 上传是否成功
+ */
+ async upload(options: UploadOptions): Promise {
+ 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 - 包含上传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 - 返回所有分片的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 - 操作完成的Promise
+ * @throws {Error} 当文件读取或写入失败时抛出错误
+ */
+ async _processChunk(fileManager: UniApp.FileSystemManager, filePath: string, tempChunkPath: string, start: number, length: number) {
+ const readRes = await new Promise((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 - 合并操作完成的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();
\ No newline at end of file