/** * Office 文件预览处理器 * * Excel 解析已迁移到 Web Worker(见 excel.worker.js + excelWorkerClient.js), * 主线程不再出现 XLSX.read() 同步调用,避免大表解析冻结整个界面。 */ import { escapeHtml } from './fileUtils' import { isExcelFile, isWordFile, isOfficeFile, isCsvFile } from './fileTypeHelpers' import { parseWorkbook, getSheet, isCurrent } from './excelWorkerClient' // 每批加载行数 const BATCH_ROWS = 200 const LOAD_MORE_THRESHOLD = 100 // Excel 预览处理器(解析在 Web Worker 中进行,不阻塞主线程) export async function previewExcel(file, container) { // 1. 取二进制并交给 worker 解析(XLSX.read / sheet_to_json 全在 worker 线程) const arrayBuffer = await file.arrayBuffer() const { wid, sheetNames } = await parseWorkbook(arrayBuffer) // 切到别的文件时 wid 会推进,丢弃本次结果 if (!isCurrent(wid)) return { success: true } // 2. 渲染骨架(标签页 + 信息栏 + 内容区) const tabs = sheetNames.map((name, i) => `` ).join('') container.innerHTML = `
${tabs}
解析中...
` const contentEl = container.querySelector('.excel-content') const infoEl = container.querySelector('.excel-info') const tabsEl = container.querySelector('.excel-tabs') // 当前 sheet 状态 let currentSheet = { idx: -1, data: null, renderedRows: 0, totalRows: 0 } // 渲染令牌:切 sheet 自增,丢弃乱序回包 let renderToken = 0 // 渲染表格(带行号) const renderTable = (data, startRow = 0) => { let html = '' if (data[0]) { data[0].forEach((cell) => { html += `` }) } html += '' for (let i = 1; i < data.length && i <= startRow + BATCH_ROWS; i++) { html += `` if (data[i]) { data[i].forEach(cell => { html += `` }) } html += '' } html += '
#${escapeHtml(cell)}
${i}${escapeHtml(cell)}
' return html } // 追加行(从内存中的 data 追加,不再过 worker) const appendRows = (data, fromRow, toRow) => { const tbody = contentEl.querySelector('tbody') if (!tbody) return for (let i = fromRow; i <= toRow && i < data.length; i++) { let html = `${i}` if (data[i]) { data[i].forEach(cell => { html += `${escapeHtml(cell)}` }) } html += '' tbody.insertAdjacentHTML('beforeend', html) } } // 更新信息栏 const updateInfo = () => { const { renderedRows, totalRows } = currentSheet if (renderedRows >= totalRows) { infoEl.textContent = `共 ${totalRows} 行` } else { infoEl.textContent = `已加载 ${renderedRows}/${totalRows} 行(滚动加载更多)` } } // 切换 sheet(worker 异步解析单个 sheet) const loadSheet = async (idx) => { const token = ++renderToken // 标签高亮 tabsEl.querySelectorAll('.excel-tab').forEach(t => { t.classList.toggle('active', parseInt(t.dataset.idx, 10) === idx) }) // 该 sheet 尚未解析过 → 显示解析中 if (currentSheet.idx !== idx) { contentEl.innerHTML = '
解析中...
' } let data try { data = await getSheet(wid, idx) } catch (e) { if (isCurrent(wid) && token === renderToken) { contentEl.innerHTML = `

❌ Excel 解析失败

${escapeHtml(e && e.message ? e.message : String(e))}

` } return } // 丢弃过期 / 乱序回包 if (!isCurrent(wid) || token !== renderToken) return currentSheet = { idx, data, renderedRows: BATCH_ROWS, totalRows: data.length } const displayData = data.slice(0, BATCH_ROWS + 1) contentEl.innerHTML = renderTable(displayData) updateInfo() } // 滚动加载更多 contentEl.onscroll = () => { const { scrollTop, scrollHeight, clientHeight } = contentEl if (scrollTop + clientHeight >= scrollHeight - LOAD_MORE_THRESHOLD) { const { data, renderedRows, totalRows } = currentSheet if (data && renderedRows < totalRows - 1) { const newEnd = Math.min(renderedRows + BATCH_ROWS, totalRows - 1) appendRows(data, renderedRows + 1, newEnd) currentSheet.renderedRows = newEnd updateInfo() } } } // 标签页切换 tabsEl.onclick = (e) => { const tab = e.target.closest('.excel-tab') if (!tab) return const idx = parseInt(tab.dataset.idx, 10) if (idx === currentSheet.idx) return loadSheet(idx) } await loadSheet(0) return { success: true } } // Word 预览处理器 export async function previewWord(file, container) { const mammoth = await import('mammoth') const arrayBuffer = await file.arrayBuffer() const result = await mammoth.convertToHtml({ arrayBuffer }) const warnings = result.messages.length > 0 ? `
转换警告 (${result.messages.length})
` : '' container.innerHTML = `
${result.value}
${warnings}
` return { success: true } } // 文件类型判断(从 fileTypeHelpers 导入) export { isOfficeFile, isExcelFile, isWordFile, isCsvFile } // CSV/TSV 预览处理器(原生实现,支持滚动加载) export async function previewCsv(file, container) { try { if (!container) { return { success: false, error: '容器不存在' } } const text = await file.text() const lines = text.split(/\r?\n/).filter(line => line.trim()) if (lines.length === 0) { throw new Error('文件为空') } // 解析 CSV 行 const parseLine = (line, delimiter) => { const cells = [] let cell = '' let inQuotes = false for (let i = 0; i < line.length; i++) { const char = line[i] if (char === '"') { if (inQuotes && line[i + 1] === '"') { cell += '"' i++ } else { inQuotes = !inQuotes } } else if (char === delimiter && !inQuotes) { cells.push(cell) cell = '' } else { cell += char } } cells.push(cell) return cells } const delimiter = file.name.endsWith('.tsv') ? '\t' : ',' const rows = lines.map(line => parseLine(line, delimiter)) container.innerHTML = `
📋 ${escapeHtml(file.name)}
` const contentEl = container.querySelector('.csv-content') const infoEl = container.querySelector('.csv-info') // 状态 let renderedRows = 0 const totalRows = rows.length // 渲染表格 const renderTable = (startRow = 0) => { const endRow = Math.min(startRow + BATCH_ROWS, totalRows) let html = '' // 表头(第一行) if (startRow === 0 && rows[0]) { html += '' rows[0].forEach(cell => { html += `` }) html += '' } // 数据行 for (let i = Math.max(1, startRow); i < endRow; i++) { html += `` if (rows[i]) { rows[i].forEach(cell => { html += `` }) } html += '' } html += '
#${escapeHtml(cell)}
${i}${escapeHtml(cell)}
' return html } // 追加行 const appendRows = (fromRow) => { const endRow = Math.min(fromRow + BATCH_ROWS, totalRows) const tbody = contentEl.querySelector('tbody') if (!tbody) return for (let i = fromRow; i < endRow; i++) { let html = `${i}` if (rows[i]) { rows[i].forEach(cell => { html += `${escapeHtml(cell)}` }) } html += '' tbody.insertAdjacentHTML('beforeend', html) } return endRow } // 更新信息 const updateInfo = () => { if (renderedRows >= totalRows) { infoEl.textContent = `📋 ${file.name}(共 ${totalRows - 1} 行)` } else { infoEl.textContent = `📋 ${file.name}(已加载 ${renderedRows}/${totalRows - 1} 行)` } } // 初始渲染 contentEl.innerHTML = renderTable(0) renderedRows = Math.min(BATCH_ROWS, totalRows) updateInfo() // 滚动加载 contentEl.onscroll = () => { const { scrollTop, scrollHeight, clientHeight } = contentEl if (scrollTop + clientHeight >= scrollHeight - LOAD_MORE_THRESHOLD) { if (renderedRows < totalRows) { renderedRows = appendRows(renderedRows) updateInfo() } } } return { success: true } } catch (err) { console.error('[previewCsv] 错误:', err) return { success: false, error: err?.message || String(err) } } }