(function (root, factory) { if (typeof module === 'object' && module.exports) { module.exports = factory(root); return; } root.UploadPages = factory(root); if (root.document) { root.document.addEventListener('DOMContentLoaded', function () { root.UploadPages.init(); }); } })(typeof globalThis !== 'undefined' ? globalThis : window, function (root) { 'use strict'; var documentRef = root.document; var DEFAULT_CHUNK_SIZE = 4 * 1024 * 1024; var activeUploads = {}; function normalizeUploadResult(data) { var source = data || {}; var ossId = source.ossId; return { // int64 OSS ID 不能经由 Number 转换,直接保留服务端原始文本。 ossId: ossId === undefined || ossId === null ? '' : String(ossId), fileName: source.fileName || source.originalName || '', url: source.url || '' }; } function buildUploadStatus(fileName, ossId) { return (fileName || '文件') + (ossId ? ' 上传完成' : ' 上传失败'); } function getUploadMode() { // 当前 PC 目录只定义统一分片上传流程。 return 'resumable'; } function getApi() { return root.GenealogyApi && root.GenealogyApi.defaultClient; } function shouldRedirectToLogin(api, error) { var status = error && (error.status || error.code); return !api || !api.getToken || !api.getToken() || Number(status) === 401; } function redirectUnauthorized(api, error) { if (!shouldRedirectToLogin(api, error)) return false; if (api && api.clearToken) api.clearToken(); if (!root.location) return true; if (typeof root.location.replace === 'function') { root.location.replace('login.html'); return true; } root.location.href = 'login.html'; return true; } function isForbidden(error) { return Number(error && (error.status || error.code)) === 403; } function query(selector, rootNode) { return (rootNode || documentRef).querySelector(selector); } function queryAll(selector, rootNode) { return Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector)); } function setUploadStatus(status, type, message) { if (!status) return; status.setAttribute('role', 'status'); status.setAttribute('aria-live', 'polite'); if (type && root.ProfileUI && root.ProfileUI.setApiState) { root.ProfileUI.setApiState(status, type, message); return; } status.textContent = message || ''; } function showMessage(message) { if (root.layui && root.layui.layer) { root.layui.layer.msg(message); return; } if (root.alert) root.alert(message); } function createUploadId() { if (root.crypto && typeof root.crypto.randomUUID === 'function') { return 'pc-' + root.crypto.randomUUID(); } return 'pc-' + Date.now() + '-' + Math.random().toString(16).slice(2); } function arrayBufferToBinary(buffer) { var bytes = new Uint8Array(buffer); var parts = []; var offset; for (offset = 0; offset < bytes.length; offset += 8192) { parts.push(String.fromCharCode.apply(null, bytes.subarray(offset, offset + 8192))); } return parts.join(''); } async function hashBlob(blob) { var buffer; if (!blob || typeof blob.arrayBuffer !== 'function') throw new Error('当前浏览器无法读取文件'); if (typeof root.hex_md5 !== 'function') throw new Error('缺少文件 MD5 组件'); buffer = await blob.arrayBuffer(); return root.hex_md5(arrayBufferToBinary(buffer)); } async function retry(operation, maxRetries) { var attempt = 0; while (true) { try { return await operation(); } catch (error) { if (attempt >= maxRetries) throw error; attempt += 1; } } } async function performResumableUpload(api, file, options) { var settings = options || {}; var chunkSize = settings.chunkSize || DEFAULT_CHUNK_SIZE; var totalChunks = Math.ceil(file.size / chunkSize); var calculateHash = settings.hashBlob || hashBlob; var fileMd5 = await calculateHash(file); var initBody = { uploadId: settings.uploadId || createUploadId(), fileName: file.name, fileMd5: fileMd5, totalSize: file.size, totalChunks: totalChunks, chunkSize: chunkSize, contentType: file.type || 'application/octet-stream' }; var initResult = await api.initResumableUpload(initBody); var uploadedChunks = Array.isArray(initResult && initResult.uploadedChunks) ? initResult.uploadedChunks.slice() : []; var uploadedLookup = {}; var serverUploadId; var chunkIndex; if (initResult && initResult.instant) return normalizeUploadResult(initResult); serverUploadId = initResult && initResult.uploadId || initBody.uploadId; uploadedChunks.forEach(function (index) { uploadedLookup[Number(index)] = true; }); if (settings.onProgress) { settings.onProgress({ uploadedChunks: uploadedChunks.length, totalChunks: totalChunks }); } for (chunkIndex = 0; chunkIndex < totalChunks; chunkIndex += 1) { if (uploadedLookup[chunkIndex]) continue; await (async function (index) { var start = index * chunkSize; var chunk = file.slice(start, Math.min(start + chunkSize, file.size)); var chunkMd5 = await calculateHash(chunk); await retry(function () { return api.uploadResumableChunk({ uploadId: serverUploadId, chunkIndex: index, chunkMd5: chunkMd5, file: chunk }); }, settings.maxRetries === undefined ? 2 : settings.maxRetries); uploadedChunks.push(index); if (settings.onProgress) { settings.onProgress({ uploadedChunks: uploadedChunks.length, totalChunks: totalChunks }); } })(chunkIndex); } return normalizeUploadResult(await api.completeResumableUpload({ uploadId: serverUploadId, fileName: file.name, fileMd5: fileMd5, totalSize: file.size, totalChunks: totalChunks })); } function uploadFileForPage(api, file, options) { var key; var promise; if (!api || !file || !file.name || !Number(file.size)) return Promise.reject(new Error('请选择文件')); key = [file.name, file.size, file.lastModified || 0].join(':'); if (activeUploads[key]) return activeUploads[key]; promise = performResumableUpload(api, file, options).finally(function () { delete activeUploads[key]; }); activeUploads[key] = promise; return promise; } function mergeUploadTargetValue(currentValue, ossId, multiple) { var current = String(currentValue || '').trim(); var next = String(ossId || '').trim(); if (!multiple || !current) return next; return current.split(',').concat(next).filter(Boolean).join(','); } async function uploadFromInput(input) { var api = getApi(); var files = Array.prototype.slice.call(input.files || []); var target = query(input.getAttribute('data-upload-target')); var status = query(input.getAttribute('data-upload-status')); var multiple = input.getAttribute('data-upload-multiple') === 'true'; var result; var index; if (!files.length || !target || redirectUnauthorized(api)) return; input.disabled = true; try { for (index = 0; index < files.length; index += 1) { setUploadStatus(status, 'loading', files[index].name + ' 正在上传…'); result = normalizeUploadResult(await uploadFileForPage(api, files[index], { onProgress: function (progress) { setUploadStatus(status, 'loading', '正在上传 ' + progress.uploadedChunks + '/' + progress.totalChunks + ' 分片'); } })); if (!result.ossId) throw new Error('上传响应缺少 ossId'); target.value = mergeUploadTargetValue(target.value, result.ossId, multiple); setUploadStatus(status, '', buildUploadStatus(result.fileName || files[index].name, result.ossId)); } } catch (error) { if (redirectUnauthorized(api, error)) return; setUploadStatus(status, isForbidden(error) ? 'forbidden' : 'error', isForbidden(error) ? '当前账号无权上传文件。' : (error.message || '上传失败')); showMessage(isForbidden(error) ? '当前账号无权上传文件。' : (error.message || '上传失败')); } finally { input.disabled = false; input.value = ''; } } function bindActions() { if (!documentRef) return; documentRef.addEventListener('change', function (event) { var input = event.target.closest('[data-upload-target]'); if (!input || input.type !== 'file') return; uploadFromInput(input); }); } function init() { queryAll('[data-upload-status]').forEach(function (status) { status.setAttribute('role', 'status'); status.setAttribute('aria-live', 'polite'); }); bindActions(); } return { normalizeUploadResult: normalizeUploadResult, buildUploadStatus: buildUploadStatus, getUploadMode: getUploadMode, hashBlob: hashBlob, uploadFileForPage: uploadFileForPage, mergeUploadTargetValue: mergeUploadTargetValue, shouldRedirectToLogin: shouldRedirectToLogin, isForbidden: isForbidden, init: init }; });