import { appApi } from "@/utils/api.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"; 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 data = reader.result; cleanup(); if (!(data instanceof ArrayBuffer)) { reject(createUploadError("读取图片数据失败", "IMAGE_READ_INVALID")); return; } resolve({ browserFile, fileName: browserFile.name || "image", size: browserFile.size, contentType: browserFile.type || "image/*", data, }); }; reader.onerror = () => { cleanup(); reject(createUploadError("读取图片数据失败", "IMAGE_READ_FAILED")); }; reader.readAsArrayBuffer(browserFile); }, { once: true }); globalThis.document.body?.append(input); input.click(); }); const readNativeImage = (filePath) => new Promise((resolve, reject) => { if (!globalThis.plus?.io?.resolveLocalFileSystemURL || !globalThis.plus?.io?.FileReader) { reject(createUploadError("当前运行环境不支持读取图片", "IMAGE_READ_UNAVAILABLE")); return; } globalThis.plus.io.resolveLocalFileSystemURL( filePath, (entry) => entry.file( (file) => { const reader = new globalThis.plus.io.FileReader(); reader.onloadend = (event) => { const data = event?.target?.result ?? reader.result; if (!(data instanceof ArrayBuffer)) { reject(createUploadError("读取图片数据失败", "IMAGE_READ_INVALID")); return; } resolve({ filePath, fileName: file.name || "image", size: file.size, contentType: file.type || "image/*", data, }); }; reader.onerror = () => reject(createUploadError("读取图片数据失败", "IMAGE_READ_FAILED")); reader.readAsArrayBuffer(file); }, () => reject(createUploadError("读取图片信息失败", "IMAGE_READ_FAILED")), ), () => reject(createUploadError("无法读取所选图片", "IMAGE_READ_FAILED")), ); }); export const toConsumerOssId = (ossId) => { if (typeof ossId !== "string" || !/^[1-9]\d*$/.test(ossId)) { throw createUploadError("文件服务返回的文件 ID 无效", "OSS_ID_INVALID"); } const numericId = Number(ossId); if (!Number.isSafeInteger(numericId)) { throw createUploadError( "文件已上传,但服务返回的文件 ID 超出 APP 安全整数范围;需由后端统一文件 ID 合同后才能关联业务数据", "OSS_ID_UNSAFE", ); } return numericId; }; 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 readNativeImage(await pickNativeImage()) : await pickBrowserImage(); if (!Number.isSafeInteger(file.size) || file.size <= 0) { throw createUploadError("所选图片大小无效", "IMAGE_SIZE_INVALID"); } const fileMd5 = calcMD5Bytes(file.data); const initPayload = { uploadId: createUploadId(), fileName: file.fileName, fileMd5, totalSize: file.size, totalChunks: 1, chunkSize: file.size, contentType: file.contentType, }; const initialized = await appApi.initializeResumableUpload(initPayload, { requestController }); if (initialized.instant) return toUploadReceipt(initialized); const chunkPayload = { uploadId: initialized.uploadId, chunkIndex: 0, chunkMd5: fileMd5, }; if (file.browserFile) { await appApi.uploadBrowserResumableChunk(chunkPayload, file.browserFile, { requestController }); } else { await appApi.uploadResumableChunk({ ...chunkPayload, filePath: file.filePath }, { requestController }); } const completed = await appApi.completeResumableUpload({ uploadId: initialized.uploadId, fileName: file.fileName, fileMd5, totalSize: file.size, totalChunks: 1, }, { requestController }); return toUploadReceipt(completed); };