新增: 文件操作增强与预览扩展
- 文件剪贴板复制剪切粘贴,重名自动副本 - SFTP下载到本机与外部文件拖入,目录递归 - 传输进度浮动面板,transfer-progress事件链 - Drawio预览,Excel工作线程,冻结看门狗 - cmd与bat自写batch语法高亮,psm1等映射补齐
This commit is contained in:
@@ -1,30 +1,36 @@
|
||||
/**
|
||||
* 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 预览处理器
|
||||
// Excel 预览处理器(解析在 Web Worker 中进行,不阻塞主线程)
|
||||
export async function previewExcel(file, container) {
|
||||
const XLSX = await import('xlsx')
|
||||
// 1. 取二进制并交给 worker 解析(XLSX.read / sheet_to_json 全在 worker 线程)
|
||||
const arrayBuffer = await file.arrayBuffer()
|
||||
const workbook = XLSX.read(arrayBuffer, { type: 'array' })
|
||||
const { wid, sheetNames } = await parseWorkbook(arrayBuffer)
|
||||
// 切到别的文件时 wid 会推进,丢弃本次结果
|
||||
if (!isCurrent(wid)) return { success: true }
|
||||
|
||||
// 渲染标签页
|
||||
const tabs = workbook.SheetNames.map((name, i) =>
|
||||
`<button class="excel-tab ${i === 0 ? 'active' : ''}" data-idx="${i}">${name}</button>`
|
||||
// 2. 渲染骨架(标签页 + 信息栏 + 内容区)
|
||||
const tabs = sheetNames.map((name, i) =>
|
||||
`<button class="excel-tab ${i === 0 ? 'active' : ''}" data-idx="${i}">${escapeHtml(name)}</button>`
|
||||
).join('')
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="excel-preview">
|
||||
<div class="excel-tabs">${tabs}</div>
|
||||
<div class="excel-info"></div>
|
||||
<div class="excel-content"></div>
|
||||
<div class="excel-content"><div class="loading-hint">解析中...</div></div>
|
||||
</div>
|
||||
<style>
|
||||
.excel-preview{display:flex;flex-direction:column;height:100%;overflow:hidden}
|
||||
@@ -41,6 +47,7 @@ export async function previewExcel(file, container) {
|
||||
.excel-content th.row-num{z-index:3;top:0;left:0}
|
||||
.excel-content tr:hover td{background:var(--color-fill-1)}
|
||||
.excel-content tr:hover .row-num{background:var(--color-fill-2)}
|
||||
.loading-hint{padding:24px;text-align:center;color:var(--color-text-3);font-size:13px}
|
||||
</style>
|
||||
`
|
||||
|
||||
@@ -49,19 +56,15 @@ export async function previewExcel(file, container) {
|
||||
const tabsEl = container.querySelector('.excel-tabs')
|
||||
|
||||
// 当前 sheet 状态
|
||||
let currentSheet = { idx: 0, data: null, renderedRows: 0, totalRows: 0 }
|
||||
|
||||
// 获取 sheet 数据
|
||||
const getSheetData = (idx) => {
|
||||
const ws = workbook.Sheets[workbook.SheetNames[idx]]
|
||||
return XLSX.utils.sheet_to_json(ws, { header: 1, defval: '' })
|
||||
}
|
||||
let currentSheet = { idx: -1, data: null, renderedRows: 0, totalRows: 0 }
|
||||
// 渲染令牌:切 sheet 自增,丢弃乱序回包
|
||||
let renderToken = 0
|
||||
|
||||
// 渲染表格(带行号)
|
||||
const renderTable = (data, startRow = 0) => {
|
||||
let html = '<table><thead><tr><th class="row-num">#</th>'
|
||||
if (data[0]) {
|
||||
data[0].forEach((cell, i) => {
|
||||
data[0].forEach((cell) => {
|
||||
html += `<th>${escapeHtml(cell)}</th>`
|
||||
})
|
||||
}
|
||||
@@ -80,7 +83,7 @@ export async function previewExcel(file, container) {
|
||||
return html
|
||||
}
|
||||
|
||||
// 追加行
|
||||
// 追加行(从内存中的 data 追加,不再过 worker)
|
||||
const appendRows = (data, fromRow, toRow) => {
|
||||
const tbody = contentEl.querySelector('tbody')
|
||||
if (!tbody) return
|
||||
@@ -107,17 +110,35 @@ export async function previewExcel(file, container) {
|
||||
}
|
||||
}
|
||||
|
||||
// 切换 sheet
|
||||
const loadSheet = (idx) => {
|
||||
currentSheet = {
|
||||
idx,
|
||||
data: getSheetData(idx),
|
||||
renderedRows: BATCH_ROWS,
|
||||
totalRows: 0
|
||||
}
|
||||
currentSheet.totalRows = currentSheet.data.length
|
||||
// 切换 sheet(worker 异步解析单个 sheet)
|
||||
const loadSheet = async (idx) => {
|
||||
const token = ++renderToken
|
||||
|
||||
const displayData = currentSheet.data.slice(0, BATCH_ROWS + 1)
|
||||
// 标签高亮
|
||||
tabsEl.querySelectorAll('.excel-tab').forEach(t => {
|
||||
t.classList.toggle('active', parseInt(t.dataset.idx, 10) === idx)
|
||||
})
|
||||
|
||||
// 该 sheet 尚未解析过 → 显示解析中
|
||||
if (currentSheet.idx !== idx) {
|
||||
contentEl.innerHTML = '<div class="loading-hint">解析中...</div>'
|
||||
}
|
||||
|
||||
let data
|
||||
try {
|
||||
data = await getSheet(wid, idx)
|
||||
} catch (e) {
|
||||
if (isCurrent(wid) && token === renderToken) {
|
||||
contentEl.innerHTML = `<div class="preview-error"><p>❌ Excel 解析失败</p><p class="error-detail">${escapeHtml(e && e.message ? e.message : String(e))}</p></div>`
|
||||
}
|
||||
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()
|
||||
}
|
||||
@@ -127,7 +148,7 @@ export async function previewExcel(file, container) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = contentEl
|
||||
if (scrollTop + clientHeight >= scrollHeight - LOAD_MORE_THRESHOLD) {
|
||||
const { data, renderedRows, totalRows } = currentSheet
|
||||
if (renderedRows < totalRows - 1) {
|
||||
if (data && renderedRows < totalRows - 1) {
|
||||
const newEnd = Math.min(renderedRows + BATCH_ROWS, totalRows - 1)
|
||||
appendRows(data, renderedRows + 1, newEnd)
|
||||
currentSheet.renderedRows = newEnd
|
||||
@@ -136,17 +157,17 @@ export async function previewExcel(file, container) {
|
||||
}
|
||||
}
|
||||
|
||||
loadSheet(0)
|
||||
|
||||
// 标签页切换
|
||||
tabsEl.onclick = (e) => {
|
||||
const tab = e.target.closest('.excel-tab')
|
||||
if (!tab) return
|
||||
tabsEl.querySelectorAll('.excel-tab').forEach(t => t.classList.remove('active'))
|
||||
tab.classList.add('active')
|
||||
loadSheet(parseInt(tab.dataset.idx, 10))
|
||||
const idx = parseInt(tab.dataset.idx, 10)
|
||||
if (idx === currentSheet.idx) return
|
||||
loadSheet(idx)
|
||||
}
|
||||
|
||||
await loadSheet(0)
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
@@ -233,7 +254,7 @@ export async function previewCsv(file, container) {
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="csv-preview">
|
||||
<div class="csv-info">📋 ${file.name}</div>
|
||||
<div class="csv-info">📋 ${escapeHtml(file.name)}</div>
|
||||
<div class="csv-content"></div>
|
||||
</div>
|
||||
<style>
|
||||
|
||||
Reference in New Issue
Block a user