/**
* 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) =>
`${escapeHtml(name)} `
).join('')
container.innerHTML = `
`
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 += `${escapeHtml(cell)} `
})
}
html += ''
for (let i = 1; i < data.length && i <= startRow + BATCH_ROWS; i++) {
html += `${i} `
if (data[i]) {
data[i].forEach(cell => {
html += `${escapeHtml(cell)} `
})
}
html += ' '
}
html += '
'
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}) ${result.messages.map(m => `${m.message} `).join('')} `
: ''
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 += `${escapeHtml(cell)} ` })
html += ''
}
// 数据行
for (let i = Math.max(1, startRow); i < endRow; i++) {
html += `${i} `
if (rows[i]) {
rows[i].forEach(cell => { html += `${escapeHtml(cell)} ` })
}
html += ' '
}
html += '
'
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) }
}
}