290 lines
10 KiB
JavaScript
290 lines
10 KiB
JavaScript
import { fileUploadApi } from "@/services/api/file-upload-service.js";
|
|
import { calcMD5Bytes } from "@/utils/md5.js";
|
|
|
|
const createUploadError = (message, code) => {
|
|
const error = new Error(message);
|
|
error.code = code;
|
|
return error;
|
|
};
|
|
|
|
export const isImagePickCancelled = (error) => error?.code === "IMAGE_PICK_CANCELLED";
|
|
export const isVideoPickCancelled = (error) => error?.code === "VIDEO_PICK_CANCELLED";
|
|
|
|
const createUploadId = () => {
|
|
if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
|
|
return `app-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
};
|
|
|
|
const pickNativeImage = () => new Promise((resolve, reject) => {
|
|
if (!globalThis.plus?.gallery?.pick) {
|
|
reject(createUploadError("当前运行环境不支持从相册选择图片", "IMAGE_PICK_UNAVAILABLE"));
|
|
return;
|
|
}
|
|
globalThis.plus.gallery.pick(
|
|
(filePath) => resolve(filePath),
|
|
() => reject(createUploadError("已取消选择图片", "IMAGE_PICK_CANCELLED")),
|
|
{ filter: "image", multiple: false },
|
|
);
|
|
});
|
|
|
|
const pickBrowserImage = () => new Promise((resolve, reject) => {
|
|
if (typeof globalThis.document?.createElement !== "function" || typeof globalThis.FileReader !== "function") {
|
|
reject(createUploadError("当前运行环境不支持选择图片", "IMAGE_PICK_UNAVAILABLE"));
|
|
return;
|
|
}
|
|
const input = globalThis.document.createElement("input");
|
|
input.type = "file";
|
|
input.accept = "image/*";
|
|
input.style.display = "none";
|
|
const cleanup = () => input.remove();
|
|
input.addEventListener("cancel", () => {
|
|
cleanup();
|
|
reject(createUploadError("已取消选择图片", "IMAGE_PICK_CANCELLED"));
|
|
}, { once: true });
|
|
input.addEventListener("change", () => {
|
|
const browserFile = input.files?.[0];
|
|
if (!browserFile) {
|
|
cleanup();
|
|
reject(createUploadError("已取消选择图片", "IMAGE_PICK_CANCELLED"));
|
|
return;
|
|
}
|
|
const reader = new globalThis.FileReader();
|
|
reader.onload = () => {
|
|
const fileData = reader.result;
|
|
cleanup();
|
|
if (!(fileData instanceof ArrayBuffer)) {
|
|
reject(createUploadError("读取图片数据失败", "IMAGE_READ_INVALID"));
|
|
return;
|
|
}
|
|
resolve({
|
|
browserFile,
|
|
fileName: browserFile.name || "image",
|
|
size: browserFile.size,
|
|
contentType: browserFile.type || "image/*",
|
|
data: fileData,
|
|
});
|
|
};
|
|
reader.onerror = () => {
|
|
cleanup();
|
|
reject(createUploadError("读取图片数据失败", "IMAGE_READ_FAILED"));
|
|
};
|
|
reader.readAsArrayBuffer(browserFile);
|
|
}, { once: true });
|
|
globalThis.document.body?.append(input);
|
|
input.click();
|
|
});
|
|
|
|
const pickNativeVideo = () => new Promise((resolve, reject) => {
|
|
if (!globalThis.plus?.gallery?.pick) {
|
|
reject(createUploadError("当前运行环境不支持从相册选择视频", "VIDEO_PICK_UNAVAILABLE"));
|
|
return;
|
|
}
|
|
globalThis.plus.gallery.pick(
|
|
(filePath) => resolve(filePath),
|
|
() => reject(createUploadError("已取消选择视频", "VIDEO_PICK_CANCELLED")),
|
|
{ filter: "video", multiple: false },
|
|
);
|
|
});
|
|
|
|
const pickBrowserVideo = () => new Promise((resolve, reject) => {
|
|
if (typeof globalThis.document?.createElement !== "function" || typeof globalThis.FileReader !== "function") {
|
|
reject(createUploadError("当前运行环境不支持选择视频", "VIDEO_PICK_UNAVAILABLE"));
|
|
return;
|
|
}
|
|
const input = globalThis.document.createElement("input");
|
|
input.type = "file";
|
|
input.accept = "video/*";
|
|
input.style.display = "none";
|
|
const cleanup = () => input.remove();
|
|
input.addEventListener("cancel", () => {
|
|
cleanup();
|
|
reject(createUploadError("已取消选择视频", "VIDEO_PICK_CANCELLED"));
|
|
}, { once: true });
|
|
input.addEventListener("change", () => {
|
|
const browserFile = input.files?.[0];
|
|
if (!browserFile) {
|
|
cleanup();
|
|
reject(createUploadError("已取消选择视频", "VIDEO_PICK_CANCELLED"));
|
|
return;
|
|
}
|
|
const reader = new globalThis.FileReader();
|
|
reader.onload = () => {
|
|
const fileData = reader.result;
|
|
cleanup();
|
|
if (!(fileData instanceof ArrayBuffer)) {
|
|
reject(createUploadError("读取视频数据失败", "VIDEO_READ_INVALID"));
|
|
return;
|
|
}
|
|
resolve({
|
|
browserFile,
|
|
fileName: browserFile.name || "video",
|
|
size: browserFile.size,
|
|
contentType: browserFile.type || "video/*",
|
|
data: fileData,
|
|
});
|
|
};
|
|
reader.onerror = () => {
|
|
cleanup();
|
|
reject(createUploadError("读取视频数据失败", "VIDEO_READ_FAILED"));
|
|
};
|
|
reader.readAsArrayBuffer(browserFile);
|
|
}, { once: true });
|
|
globalThis.document.body?.append(input);
|
|
input.click();
|
|
});
|
|
|
|
const readNativeFile = (filePath, mediaLabel) => new Promise((resolve, reject) => {
|
|
if (typeof globalThis.uni?.getFileInfo === 'function') {
|
|
const normalizedPath = String(filePath || '')
|
|
const fileName = normalizedPath.split(/[\\/]/).pop() || 'file'
|
|
try {
|
|
globalThis.uni.getFileInfo({
|
|
filePath,
|
|
digestAlgorithm: 'md5',
|
|
success: (file) => resolve({
|
|
filePath,
|
|
fileName,
|
|
size: file.size,
|
|
contentType: 'application/octet-stream',
|
|
md5: String(file.digest || '').toLowerCase()
|
|
}),
|
|
fail: () => reject(createUploadError(`读取${mediaLabel}信息失败`, 'MEDIA_READ_FAILED'))
|
|
})
|
|
} catch (error) {
|
|
reject(error)
|
|
}
|
|
return
|
|
}
|
|
if (!globalThis.plus?.io?.resolveLocalFileSystemURL || !globalThis.plus?.io?.FileReader) {
|
|
reject(createUploadError(`当前运行环境不支持读取${mediaLabel}`, "MEDIA_READ_UNAVAILABLE"));
|
|
return;
|
|
}
|
|
globalThis.plus.io.resolveLocalFileSystemURL(
|
|
filePath,
|
|
(entry) => entry.file(
|
|
(file) => {
|
|
const reader = new globalThis.plus.io.FileReader();
|
|
reader.onloadend = (event) => {
|
|
const fileData = event?.target?.result ?? reader.result;
|
|
if (!(fileData instanceof ArrayBuffer)) {
|
|
reject(createUploadError(`读取${mediaLabel}数据失败`, "MEDIA_READ_INVALID"));
|
|
return;
|
|
}
|
|
resolve({
|
|
filePath,
|
|
fileName: file.name || "image",
|
|
size: file.size,
|
|
contentType: file.type || "image/*",
|
|
data: fileData,
|
|
});
|
|
};
|
|
reader.onerror = () => reject(createUploadError(`读取${mediaLabel}数据失败`, "MEDIA_READ_FAILED"));
|
|
reader.readAsArrayBuffer(file);
|
|
},
|
|
() => reject(createUploadError(`读取${mediaLabel}信息失败`, "MEDIA_READ_FAILED")),
|
|
),
|
|
() => reject(createUploadError(`无法读取所选${mediaLabel}`, "MEDIA_READ_FAILED")),
|
|
);
|
|
});
|
|
|
|
const normalizeUploadReceiptOssId = (ossId) => {
|
|
if (typeof ossId !== "string" || !/^[1-9]\d*$/.test(ossId)) {
|
|
throw createUploadError("文件服务返回的文件 ID 无效", "OSS_ID_INVALID");
|
|
}
|
|
return ossId;
|
|
};
|
|
|
|
const toUploadReceipt = ({ ossId, url = "", thumbnailUrl = "", fileName = "" }) => ({
|
|
// complete 的接口合同是字符串;先原样保留,避免把 int64 提前截断。
|
|
ossId: normalizeUploadReceiptOssId(ossId),
|
|
url,
|
|
thumbnailUrl,
|
|
fileName,
|
|
});
|
|
|
|
export const pickAndUploadImage = async ({ requestController = null } = {}) => {
|
|
const file = globalThis.plus?.gallery?.pick
|
|
? await readNativeFile(await pickNativeImage(), "图片")
|
|
: await pickBrowserImage();
|
|
const fileName = String(file.fileName || "");
|
|
if (!String(file.contentType || "").startsWith("image/") && !/\.(jpe?g|png|gif|webp|heic|heif)$/i.test(fileName)) {
|
|
throw createUploadError("请选择图片文件", "IMAGE_TYPE_INVALID");
|
|
}
|
|
if (!Number.isSafeInteger(file.size) || file.size <= 0) {
|
|
throw createUploadError("所选图片大小无效", "IMAGE_SIZE_INVALID");
|
|
}
|
|
const fileMd5 = file.md5 || calcMD5Bytes(file.data);
|
|
const initPayload = {
|
|
uploadId: createUploadId(),
|
|
fileName,
|
|
fileMd5,
|
|
totalSize: file.size,
|
|
totalChunks: 1,
|
|
chunkSize: file.size,
|
|
contentType: file.contentType,
|
|
};
|
|
const initialized = await fileUploadApi.initializeResumableUpload(initPayload, { requestController });
|
|
if (initialized.instant) return toUploadReceipt(initialized);
|
|
const chunkPayload = {
|
|
uploadId: initialized.uploadId,
|
|
chunkIndex: 0,
|
|
chunkMd5: fileMd5,
|
|
};
|
|
if (file.browserFile) {
|
|
await fileUploadApi.uploadBrowserResumableChunk(chunkPayload, file.browserFile, { requestController });
|
|
} else {
|
|
await fileUploadApi.uploadResumableChunk({ ...chunkPayload, filePath: file.filePath }, { requestController });
|
|
}
|
|
const completed = await fileUploadApi.completeResumableUpload({
|
|
uploadId: initialized.uploadId,
|
|
fileName,
|
|
fileMd5,
|
|
totalSize: file.size,
|
|
totalChunks: 1,
|
|
}, { requestController });
|
|
return toUploadReceipt(completed);
|
|
};
|
|
|
|
export const pickAndUploadVideo = async ({ requestController = null } = {}) => {
|
|
const file = globalThis.plus?.gallery?.pick
|
|
? await readNativeFile(await pickNativeVideo(), "视频")
|
|
: await pickBrowserVideo();
|
|
const fileName = String(file.fileName || "");
|
|
if (!String(file.contentType || "").startsWith("video/") && !/\.(mp4|mov|m4v|webm|avi|mkv)$/i.test(fileName)) {
|
|
throw createUploadError("请选择视频文件", "VIDEO_TYPE_INVALID");
|
|
}
|
|
if (!Number.isSafeInteger(file.size) || file.size <= 0) {
|
|
throw createUploadError("所选视频大小无效", "VIDEO_SIZE_INVALID");
|
|
}
|
|
const fileMd5 = file.md5 || calcMD5Bytes(file.data);
|
|
const initPayload = {
|
|
uploadId: createUploadId(),
|
|
fileName,
|
|
fileMd5,
|
|
totalSize: file.size,
|
|
totalChunks: 1,
|
|
chunkSize: file.size,
|
|
contentType: file.contentType || "video/*",
|
|
};
|
|
const initialized = await fileUploadApi.initializeResumableUpload(initPayload, { requestController });
|
|
if (initialized.instant) return toUploadReceipt(initialized);
|
|
const chunkPayload = {
|
|
uploadId: initialized.uploadId,
|
|
chunkIndex: 0,
|
|
chunkMd5: fileMd5,
|
|
};
|
|
if (file.browserFile) {
|
|
await fileUploadApi.uploadBrowserResumableChunk(chunkPayload, file.browserFile, { requestController });
|
|
} else {
|
|
await fileUploadApi.uploadResumableChunk({ ...chunkPayload, filePath: file.filePath }, { requestController });
|
|
}
|
|
const completed = await fileUploadApi.completeResumableUpload({
|
|
uploadId: initialized.uploadId,
|
|
fileName,
|
|
fileMd5,
|
|
totalSize: file.size,
|
|
totalChunks: 1,
|
|
}, { requestController });
|
|
return toUploadReceipt(completed);
|
|
};
|