60%
This commit is contained in:
+1738
-107
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,14 @@ const toWords = (value) => {
|
||||
return words
|
||||
}
|
||||
|
||||
const toWordsFromBytes = (bytes) => {
|
||||
const words = []
|
||||
for (let index = 0; index < bytes.length; index += 1) {
|
||||
words[index >> 2] |= bytes[index] << ((index % 4) * 8)
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
const toHex = (words) => {
|
||||
const alphabet = '0123456789abcdef'
|
||||
let output = ''
|
||||
@@ -70,3 +78,16 @@ export const calcMD5 = (value) => {
|
||||
const utf8Value = unescape(encodeURIComponent(String(value)))
|
||||
return toHex(coreMd5(toWords(utf8Value), utf8Value.length * 8))
|
||||
}
|
||||
|
||||
export const calcMD5Bytes = (value) => {
|
||||
const bytes = value instanceof ArrayBuffer
|
||||
? new Uint8Array(value)
|
||||
: ArrayBuffer.isView(value)
|
||||
? new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
|
||||
: null
|
||||
if (!bytes) throw new TypeError('MD5 字节输入必须是 ArrayBuffer 或 TypedArray')
|
||||
if (bytes.length > 0x1fffffff) {
|
||||
throw new RangeError('当前 MD5 实现不支持超过 512MB 的单文件')
|
||||
}
|
||||
return toHex(coreMd5(toWordsFromBytes(bytes), bytes.length * 8))
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ export const ROUTES = Object.freeze({
|
||||
kind: "root",
|
||||
parent: null,
|
||||
optionalParams: ["genealogyId"],
|
||||
resultOperations: ["genealogy-created"],
|
||||
}),
|
||||
G03: defineRoute({
|
||||
path: "/pages/genealogy/g03-create-genealogy",
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
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 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 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")),
|
||||
);
|
||||
});
|
||||
|
||||
const createUploadId = () => {
|
||||
if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
|
||||
return `app-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
};
|
||||
|
||||
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 filePath = await pickNativeImage();
|
||||
const file = await readNativeImage(filePath);
|
||||
if (!Number.isSafeInteger(file.size) || file.size <= 0) {
|
||||
throw createUploadError("所选图片大小无效", "IMAGE_SIZE_INVALID");
|
||||
}
|
||||
const fileMd5 = calcMD5Bytes(file.data);
|
||||
const uploadId = createUploadId();
|
||||
const initPayload = {
|
||||
uploadId,
|
||||
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);
|
||||
await appApi.uploadResumableChunk({
|
||||
uploadId: initialized.uploadId,
|
||||
chunkIndex: 0,
|
||||
chunkMd5: fileMd5,
|
||||
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);
|
||||
};
|
||||
Reference in New Issue
Block a user