342 lines
8.8 KiB
JavaScript
342 lines
8.8 KiB
JavaScript
/**
|
||
* COS 上传服务
|
||
* 实现腾讯云 COS 直传功能
|
||
*/
|
||
|
||
import { get, post } from "./request";
|
||
import { getBaseUrl } from "./server";
|
||
|
||
/**
|
||
* 获取预签名 URL
|
||
* @param {Object} params - 请求参数
|
||
* @param {Date} params.recordTime - 记录时间
|
||
* @param {string} params.deptName - 部门名称
|
||
* @param {string} params.content - 工作内容
|
||
* @param {Array<string>} params.workers - 工作人员列表
|
||
* @param {string} params.fileExt - 文件扩展名,默认 .jpg
|
||
* @param {number} params.imageCount - 图片数量
|
||
* @returns {Promise<Object>} - 返回预签名 URL 信息
|
||
*/
|
||
export const getUploadUrls = async (params) => {
|
||
const baseUrl = getBaseUrl();
|
||
const url = baseUrl + "api/cos/getUploadUrls";
|
||
|
||
const requestData = {
|
||
RecordTime: params.recordTime instanceof Date
|
||
? params.recordTime.toISOString()
|
||
: params.recordTime,
|
||
DeptName: params.deptName || "",
|
||
Content: params.content || "",
|
||
Workers: params.workers || [],
|
||
FileExt: params.fileExt || ".jpg",
|
||
ImageCount: params.imageCount || 1
|
||
};
|
||
|
||
console.log("获取预签名 URL 请求:", url, requestData);
|
||
const res = await post(url, requestData);
|
||
console.log("获取预签名 URL 响应:", res);
|
||
|
||
if (res.code !== 200) {
|
||
throw new Error(res.msg || "获取预签名 URL 失败");
|
||
}
|
||
|
||
return res.data;
|
||
};
|
||
|
||
/**
|
||
* 直传图片到 COS(使用 PUT 方法)
|
||
* @param {string} uploadUrl - 预签名上传 URL
|
||
* @param {string} filePath - 本地文件路径
|
||
* @param {number} retryCount - 重试次数,默认 3
|
||
* @returns {Promise<boolean>} - 上传是否成功
|
||
*/
|
||
export const uploadToCos = async (uploadUrl, filePath, retryCount = 3) => {
|
||
let lastError = null;
|
||
|
||
for (let i = 0; i < retryCount; i++) {
|
||
try {
|
||
console.log(`COS 上传尝试 ${i + 1}/${retryCount}:`, uploadUrl);
|
||
|
||
let result = false;
|
||
|
||
// #ifdef H5
|
||
// H5 环境使用 fetch API
|
||
result = await uploadToCosH5(uploadUrl, filePath);
|
||
// #endif
|
||
|
||
// #ifndef H5
|
||
// App/小程序环境使用 uni.uploadFile
|
||
result = await uploadToCosApp(uploadUrl, filePath);
|
||
// #endif
|
||
|
||
if (result) {
|
||
console.log("COS 上传成功");
|
||
return true;
|
||
}
|
||
} catch (error) {
|
||
console.error(`COS 上传失败 (尝试 ${i + 1}):`, error);
|
||
lastError = error;
|
||
}
|
||
}
|
||
|
||
throw lastError || new Error("COS 上传失败,已重试 " + retryCount + " 次");
|
||
};
|
||
|
||
/**
|
||
* H5 环境上传到 COS
|
||
* @param {string} uploadUrl - 预签名上传 URL
|
||
* @param {string} filePath - 本地文件路径(blob URL 或 base64)
|
||
* @returns {Promise<boolean>}
|
||
*/
|
||
const uploadToCosH5 = async (uploadUrl, filePath) => {
|
||
// 获取文件 Blob
|
||
let blob;
|
||
|
||
if (filePath.startsWith("blob:")) {
|
||
// blob URL
|
||
const response = await fetch(filePath);
|
||
blob = await response.blob();
|
||
} else if (filePath.startsWith("data:")) {
|
||
// base64 数据
|
||
const response = await fetch(filePath);
|
||
blob = await response.blob();
|
||
} else {
|
||
// 普通 URL
|
||
const response = await fetch(filePath);
|
||
blob = await response.blob();
|
||
}
|
||
|
||
// 使用 PUT 方法上传
|
||
const response = await fetch(uploadUrl, {
|
||
method: "PUT",
|
||
body: blob,
|
||
headers: {
|
||
"Content-Type": blob.type || "image/jpeg"
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||
}
|
||
|
||
return true;
|
||
};
|
||
|
||
/**
|
||
* App/小程序环境上传到 COS
|
||
* @param {string} uploadUrl - 预签名上传 URL
|
||
* @param {string} filePath - 本地文件路径
|
||
* @returns {Promise<boolean>}
|
||
*/
|
||
const uploadToCosApp = (uploadUrl, filePath) => {
|
||
return new Promise((resolve, reject) => {
|
||
// 先读取文件内容
|
||
uni.getFileSystemManager().readFile({
|
||
filePath: filePath,
|
||
success: (fileRes) => {
|
||
// 使用 uni.request 发送 PUT 请求
|
||
uni.request({
|
||
url: uploadUrl,
|
||
method: "PUT",
|
||
data: fileRes.data,
|
||
header: {
|
||
"Content-Type": "image/jpeg"
|
||
},
|
||
success: (res) => {
|
||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||
resolve(true);
|
||
} else {
|
||
reject(new Error(`HTTP ${res.statusCode}`));
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
reject(err);
|
||
}
|
||
});
|
||
},
|
||
fail: (err) => {
|
||
// 如果 getFileSystemManager 不可用,尝试使用 uploadFile
|
||
uni.uploadFile({
|
||
url: uploadUrl,
|
||
filePath: filePath,
|
||
name: "file",
|
||
header: {
|
||
"Content-Type": "image/jpeg"
|
||
},
|
||
success: (res) => {
|
||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||
resolve(true);
|
||
} else {
|
||
reject(new Error(`HTTP ${res.statusCode}`));
|
||
}
|
||
},
|
||
fail: (uploadErr) => {
|
||
reject(uploadErr);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 并发上传多张图片到 COS
|
||
* @param {Array<Object>} uploadTasks - 上传任务列表
|
||
* @param {string} uploadTasks[].filePath - 本地文件路径
|
||
* @param {Object} uploadTasks[].uploadUrls - 预签名 URL 对象
|
||
* @param {number} concurrency - 最大并发数,默认 3
|
||
* @param {Function} onProgress - 进度回调 (completed, total)
|
||
* @returns {Promise<Array<Object>>} - 返回上传结果列表
|
||
*/
|
||
export const uploadImagesToCos = async (uploadTasks, concurrency = 3, onProgress = null) => {
|
||
const results = [];
|
||
let completed = 0;
|
||
const total = uploadTasks.length;
|
||
|
||
// 创建任务队列
|
||
const queue = [...uploadTasks];
|
||
const executing = [];
|
||
|
||
const executeTask = async (task, index) => {
|
||
try {
|
||
// 上传到 Daily 目录
|
||
if (task.uploadUrls.daily) {
|
||
await uploadToCos(task.uploadUrls.daily, task.filePath);
|
||
}
|
||
|
||
// 上传到 Workers 目录(每个工人一个目录)
|
||
if (task.uploadUrls.workers) {
|
||
for (const workerName in task.uploadUrls.workers) {
|
||
const workerUrl = task.uploadUrls.workers[workerName];
|
||
await uploadToCos(workerUrl, task.filePath);
|
||
}
|
||
}
|
||
|
||
// 上传到 Content 目录
|
||
if (task.uploadUrls.content) {
|
||
await uploadToCos(task.uploadUrls.content, task.filePath);
|
||
}
|
||
|
||
// 上传到 Dept 目录
|
||
if (task.uploadUrls.dept) {
|
||
await uploadToCos(task.uploadUrls.dept, task.filePath);
|
||
}
|
||
|
||
results[index] = {
|
||
success: true,
|
||
accessUrl: task.accessUrl
|
||
};
|
||
} catch (error) {
|
||
console.error(`图片 ${index + 1} 上传失败:`, error);
|
||
results[index] = {
|
||
success: false,
|
||
error: error.message
|
||
};
|
||
} finally {
|
||
completed++;
|
||
if (onProgress) {
|
||
onProgress(completed, total);
|
||
}
|
||
}
|
||
};
|
||
|
||
// 并发控制
|
||
for (let i = 0; i < queue.length; i++) {
|
||
const task = queue[i];
|
||
const promise = executeTask(task, i);
|
||
executing.push(promise);
|
||
|
||
if (executing.length >= concurrency) {
|
||
await Promise.race(executing);
|
||
// 移除已完成的 Promise
|
||
for (let j = executing.length - 1; j >= 0; j--) {
|
||
// Promise.race 不会告诉我们哪个完成了,所以我们需要检查
|
||
// 这里简化处理,等待所有当前执行的任务完成
|
||
}
|
||
}
|
||
}
|
||
|
||
// 等待所有任务完成
|
||
await Promise.all(executing);
|
||
|
||
return results;
|
||
};
|
||
|
||
/**
|
||
* 批量上传图片到 COS(完整流程)
|
||
* @param {Array<string>} filePaths - 本地文件路径列表
|
||
* @param {Object} recordInfo - 记录信息
|
||
* @param {Date} recordInfo.recordTime - 记录时间
|
||
* @param {string} recordInfo.deptName - 部门名称
|
||
* @param {string} recordInfo.content - 工作内容
|
||
* @param {Array<string>} recordInfo.workers - 工作人员列表
|
||
* @param {Function} onProgress - 进度回调 (stage, completed, total)
|
||
* @returns {Promise<Array<string>>} - 返回上传成功的 COS URL 列表
|
||
*/
|
||
export const batchUploadToCos = async (filePaths, recordInfo, onProgress = null) => {
|
||
if (!filePaths || filePaths.length === 0) {
|
||
throw new Error("没有要上传的图片");
|
||
}
|
||
|
||
// 1. 获取预签名 URL
|
||
if (onProgress) {
|
||
onProgress("getting_urls", 0, filePaths.length);
|
||
}
|
||
|
||
const urlsResponse = await getUploadUrls({
|
||
recordTime: recordInfo.recordTime,
|
||
deptName: recordInfo.deptName,
|
||
content: recordInfo.content,
|
||
workers: recordInfo.workers,
|
||
fileExt: ".jpg",
|
||
imageCount: filePaths.length
|
||
});
|
||
|
||
if (!urlsResponse || !urlsResponse.images || urlsResponse.images.length === 0) {
|
||
throw new Error("获取预签名 URL 失败");
|
||
}
|
||
|
||
// 2. 构建上传任务
|
||
const uploadTasks = filePaths.map((filePath, index) => {
|
||
const imageInfo = urlsResponse.images[index];
|
||
return {
|
||
filePath: filePath,
|
||
uploadUrls: imageInfo.uploadUrls,
|
||
accessUrl: imageInfo.accessUrl
|
||
};
|
||
});
|
||
|
||
// 3. 并发上传
|
||
const uploadResults = await uploadImagesToCos(
|
||
uploadTasks,
|
||
3, // 最大并发数
|
||
(completed, total) => {
|
||
if (onProgress) {
|
||
onProgress("uploading", completed, total);
|
||
}
|
||
}
|
||
);
|
||
|
||
// 4. 收集成功上传的 URL
|
||
const successUrls = [];
|
||
const failedIndexes = [];
|
||
|
||
uploadResults.forEach((result, index) => {
|
||
if (result.success) {
|
||
successUrls.push(result.accessUrl);
|
||
} else {
|
||
failedIndexes.push(index);
|
||
}
|
||
});
|
||
|
||
if (failedIndexes.length > 0) {
|
||
console.warn(`${failedIndexes.length} 张图片上传失败:`, failedIndexes);
|
||
}
|
||
|
||
if (successUrls.length === 0) {
|
||
throw new Error("所有图片上传失败");
|
||
}
|
||
|
||
return successUrls;
|
||
};
|