/** * 极简 ZIP 打包(store 存储,不压缩)。 * * OOXML 容器(xlsx / docx)本质都是 zip 包,这里只实现写文件所需的最小集合: * 本地文件头 + 中央目录 + 目录结束记录。不压缩可以省掉 deflate 依赖, * 体积对导出场景可以接受,且所有解析器都兼容。 */ const CRC_TABLE = (() => { const table = new Uint32Array(256); for (let i = 0; i < 256; i += 1) { let c = i; for (let k = 0; k < 8; k += 1) { c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; } table[i] = c >>> 0; } return table; })(); const crc32 = (bytes) => { let c = 0xffffffff; for (let i = 0; i < bytes.length; i += 1) { c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8); } return (c ^ 0xffffffff) >>> 0; }; const encoder = new TextEncoder(); export const utf8 = (text) => encoder.encode(text); /** * @param {Array<{name: string, data: Uint8Array}>} files * @param {string} mimeType * @returns {Blob} */ export function createZip(files, mimeType) { const now = new Date(); const dosTime = (now.getHours() << 11) | (now.getMinutes() << 5) | Math.floor(now.getSeconds() / 2); const dosDate = ((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate(); const localParts = []; const centralParts = []; let offset = 0; files.forEach((file) => { const nameBytes = utf8(file.name); const data = file.data; const crc = crc32(data); const local = new Uint8Array(30 + nameBytes.length); const lv = new DataView(local.buffer); lv.setUint32(0, 0x04034b50, true); lv.setUint16(4, 20, true); lv.setUint16(6, 0x0800, true); lv.setUint16(8, 0, true); lv.setUint16(10, dosTime, true); lv.setUint16(12, dosDate, true); lv.setUint32(14, crc, true); lv.setUint32(18, data.length, true); lv.setUint32(22, data.length, true); lv.setUint16(26, nameBytes.length, true); lv.setUint16(28, 0, true); local.set(nameBytes, 30); const central = new Uint8Array(46 + nameBytes.length); const cv = new DataView(central.buffer); cv.setUint32(0, 0x02014b50, true); cv.setUint16(4, 20, true); cv.setUint16(6, 20, true); cv.setUint16(8, 0x0800, true); cv.setUint16(10, 0, true); cv.setUint16(12, dosTime, true); cv.setUint16(14, dosDate, true); cv.setUint32(16, crc, true); cv.setUint32(20, data.length, true); cv.setUint32(24, data.length, true); cv.setUint16(28, nameBytes.length, true); cv.setUint16(30, 0, true); cv.setUint16(32, 0, true); cv.setUint16(34, 0, true); cv.setUint16(36, 0, true); cv.setUint32(38, 0, true); cv.setUint32(42, offset, true); central.set(nameBytes, 46); localParts.push(local, data); centralParts.push(central); offset += local.length + data.length; }); const centralSize = centralParts.reduce((sum, part) => sum + part.length, 0); const end = new Uint8Array(22); const ev = new DataView(end.buffer); ev.setUint32(0, 0x06054b50, true); ev.setUint16(4, 0, true); ev.setUint16(6, 0, true); ev.setUint16(8, files.length, true); ev.setUint16(10, files.length, true); ev.setUint32(12, centralSize, true); ev.setUint32(16, offset, true); ev.setUint16(20, 0, true); return new Blob([...localParts, ...centralParts, end], { type: mimeType }); } export default createZip;