From 3d9558508a3afedd38a455628b43f4ead304bdb1 Mon Sep 17 00:00:00 2001 From: "douboer@gmail.com" Date: Sat, 14 Feb 2026 11:16:40 +0800 Subject: [PATCH] update at 2026-02-14 11:16:40 --- miniapp/pages/index/index.js | 19 ++++++++++++++++--- miniapp/utils/sankey.js | 35 +++++++++++++++++++++++++++++++++++ tests/miniapp.spec.ts | 16 ++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/miniapp/pages/index/index.js b/miniapp/pages/index/index.js index 9823ca7..79df4f6 100644 --- a/miniapp/pages/index/index.js +++ b/miniapp/pages/index/index.js @@ -66,6 +66,15 @@ function getFileExtension(fileName) { return lowerName.slice(lastDotIndex + 1); } +/** + * 从路径提取文件名,兼容 Unix/Windows 路径分隔符。 + */ +function getBaseNameFromPath(filePath) { + const normalized = String(filePath || '').replace(/\\/g, '/'); + const segments = normalized.split('/'); + return segments[segments.length - 1] || ''; +} + const FALLBACK_THEME_COLORS = ['#9b6bc2', '#7e95f7', '#4cc9f0', '#f4a261']; /** @@ -859,7 +868,7 @@ Page({ */ readAndApplyFile(filePath, fileName, onReadFailPrefix) { const that = this; - const extension = getFileExtension(fileName); + const extension = getFileExtension(fileName) || getFileExtension(getBaseNameFromPath(filePath)); const isCsvFile = extension === 'csv'; const readOptions = { filePath, @@ -960,8 +969,12 @@ Page({ if (!picked) { return; } - const filePath = picked.path; - const fileName = picked.name || 'unknown.csv'; + const filePath = picked.path || picked.tempFilePath || ''; + const fileName = + picked.name || + getBaseNameFromPath(filePath) || + // 无法识别文件名时,默认按二进制读取,交由解析器做内容识别。 + 'upload.bin'; that.readAndApplyFile(filePath, fileName, '读取文件失败'); }, fail(err) { diff --git a/miniapp/utils/sankey.js b/miniapp/utils/sankey.js index 8687812..608f3f5 100644 --- a/miniapp/utils/sankey.js +++ b/miniapp/utils/sankey.js @@ -270,6 +270,34 @@ function parseXlsxBuffer(buffer) { return toRawTable(rows); } +/** + * 判断二进制是否为 Zip 容器(xlsx)魔数:50 4B。 + */ +function isZipMagic(bufferLike) { + if (!bufferLike || typeof bufferLike.byteLength !== 'number' || bufferLike.byteLength < 2) { + return false; + } + const bytes = new Uint8Array(bufferLike, 0, 2); + return bytes[0] === 0x50 && bytes[1] === 0x4b; +} + +/** + * 判断二进制是否为 OLE 容器(老 xls)魔数:D0 CF 11 E0 A1 B1 1A E1。 + */ +function isOleMagic(bufferLike) { + if (!bufferLike || typeof bufferLike.byteLength !== 'number' || bufferLike.byteLength < 8) { + return false; + } + const bytes = new Uint8Array(bufferLike, 0, 8); + const signature = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]; + for (let i = 0; i < signature.length; i += 1) { + if (bytes[i] !== signature[i]) { + return false; + } + } + return true; +} + /** * 按文件名后缀自动分流解析器。 */ @@ -281,6 +309,13 @@ function parseTableByFileName(fileName, payload) { if (lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls')) { return parseXlsxBuffer(payload); } + + // 兜底:后缀不可用时,通过文件魔数自动识别 Excel。 + if (payload && typeof payload === 'object' && typeof payload.byteLength === 'number') { + if (isZipMagic(payload) || isOleMagic(payload)) { + return parseXlsxBuffer(payload); + } + } throw new Error('仅支持 .csv / .xlsx / .xls 文件'); } diff --git a/tests/miniapp.spec.ts b/tests/miniapp.spec.ts index 6e1151a..e7f9a94 100644 --- a/tests/miniapp.spec.ts +++ b/tests/miniapp.spec.ts @@ -87,4 +87,20 @@ describe('miniapp utils sankey', () => { expect(table.headers).toEqual(['人數']); expect(table.rows).toEqual([['张三']]); }); + + it('文件名后缀缺失时,仍可根据二进制魔数识别 xlsx', () => { + const sheet = XLSX.utils.aoa_to_sheet([ + ['source', 'target', 'value'], + ['A', 'B', 1] + ]); + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, sheet, 'S1'); + const buffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'buffer' }); + const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); + + const table = parseTableByFileName('upload.bin', arrayBuffer); + + expect(table.headers).toEqual(['source', 'target', 'value']); + expect(table.rows).toEqual([['A', 'B', '1']]); + }); });