使用@univerjs纯前端渲染excel, 显示图片、链接、样式

我的需求是,仅仅需要预览excel,展示图片、链接、样式等基本内容 univerjs区分两种模式,预设模式和插件模式

插件模式

index.tsx

import 复制代码
import { Spin } from 'antd'
import { LocaleType, mergeLocales, Univer, UniverInstanceType } from '@univerjs/core'
import type { IWorkbookData } from '@univerjs/core'
import DesignZhCN from '@univerjs/design/locale/zh-CN'
import { UniverDocsPlugin } from '@univerjs/docs'
import { UniverDocsUIPlugin } from '@univerjs/docs-ui'
import DocsUIZhCN from '@univerjs/docs-ui/locale/zh-CN'
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula'
import { UniverRenderEnginePlugin } from '@univerjs/engine-render'
import { UniverSheetsPlugin } from '@univerjs/sheets'
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'
import SheetsUIZhCN from '@univerjs/sheets-ui/locale/zh-CN'
import SheetsZhCN from '@univerjs/sheets/locale/zh-CN'
import { UniverUIPlugin } from '@univerjs/ui'
import UIZhCN from '@univerjs/ui/locale/zh-CN'
import SheetsHyperLinkUIZhCN from '@univerjs/sheets-hyper-link-ui/locale/zh-CN'
import { UniverSheetsHyperLinkUIPlugin } from '@univerjs/sheets-hyper-link-ui'
import { UniverSheetsDrawingUIPlugin } from '@univerjs/sheets-drawing-ui'
import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula'
import { UniverSheetsFormulaUIPlugin } from '@univerjs/sheets-formula-ui'
import SheetsFormulaUIZhCN from '@univerjs/sheets-formula-ui/locale/zh-CN'
// 样式
import '@univerjs/design/lib/index.css'
import '@univerjs/ui/lib/index.css'
import '@univerjs/docs-ui/lib/index.css'
import '@univerjs/sheets-ui/lib/index.css'
import '@univerjs/sheets-formula-ui/lib/index.css'
import '@univerjs/sheets-hyper-link-ui/lib/index.css'
import '@univerjs/sheets-drawing-ui/lib/index.css'

import { handleXlsx, handleXls, handleCsv } from './utils'

interface IProps {
  requestUrl: string
  height?: string | number
  getPreviewFile: (url: string) => Promise<any>
  fileType: 'xlsx' | 'xls' | 'csv'
}

const ExcelPreview: React.FC<IProps> = ({ requestUrl, getPreviewFile, fileType, height }) => {
  const [loading, setLoading] = useState(false)
  const [excelData, setExcelData] = useState<IWorkbookData>()
  const univerRef = useRef<any | null>(null)
  useEffect(() => {
    setLoading(true)
    getPreviewFile(requestUrl)
      .then(async response => {
        if (!response) {
          setLoading(false)
          return
        }

        let workbookData: IWorkbookData | undefined

        if (fileType === 'xlsx') {
          workbookData = await handleXlsx(await response.arrayBuffer())
        } else if (fileType === 'xls') {
          workbookData = await handleXls(await response.arrayBuffer())
        } else {
          workbookData = await handleCsv(await response.blob())
        }

        setExcelData(workbookData)
        setLoading(false)
      })
      .catch(error => {
        console.error(error)
        setLoading(false)
      })
  }, [getPreviewFile, fileType, requestUrl])

  // 2️⃣ 初始化 Univer 实例
  useEffect(() => {
    const univer = new Univer({
      locale: LocaleType.ZH_CN,
      locales: {
        [LocaleType.ZH_CN]: mergeLocales(
          DesignZhCN,
          UIZhCN,
          DocsUIZhCN,
          SheetsZhCN,
          SheetsUIZhCN,
          SheetsFormulaUIZhCN,
          SheetsHyperLinkUIZhCN,
        ),
      },
    })
    univer.registerPlugin(UniverRenderEnginePlugin)
    univer.registerPlugin(UniverFormulaEnginePlugin, { notExecuteFormula: true })
    univer.registerPlugin(UniverUIPlugin, {
      container: 'excel-preview-container-univer',
      toolbar: false,
      contextMenu: false,
      footer: true,
      header: false,
      // editable: false,
      // selectionMode: 'text',
    })

    // 表格核心与 UI
    univer.registerPlugin(UniverDocsPlugin)
    univer.registerPlugin(UniverDocsUIPlugin)
    univer.registerPlugin(UniverSheetsPlugin, {
      notExecuteFormula: true,
      autoHeightForMergedCells: true,
    })
    univer.registerPlugin(UniverSheetsUIPlugin)
    univer.registerPlugin(UniverSheetsFormulaPlugin, { notExecuteFormula: false })
    univer.registerPlugin(UniverSheetsFormulaUIPlugin)

    // 绘图与超链接功能
    univer.registerPlugin(UniverSheetsDrawingUIPlugin)
    univer.registerPlugin(UniverSheetsHyperLinkUIPlugin)

    univerRef.current = univer

    return () => {
      univerRef.current?.dispose()
      univerRef.current = null
    }
  }, [])

  useEffect(() => {
    if (!excelData || !univerRef.current) return
    univerRef.current.createUnit(UniverInstanceType.UNIVER_SHEET, excelData)
  }, [excelData])

  return (
    <Spin size="large" spinning={loading}>
      <div
        id="excel-preview-container-univer"
        style={{
          width: '100%',
          height: height || '100%',
          userSelect: 'text',
        }}
      />
    </Spin>
  )
}

export default ExcelPreview

utils.ts

typescript 复制代码
import { IWorkbookData, IWorksheetData, ICellData, LocaleType } from '@univerjs/core'
import * as XLSX from 'xlsx'
import LuckyExcel from '@zwight/luckyexcel'
import { v4 as uuidv4 } from 'uuid'

const DEFAULT_ROW_COUNT = 1000
const DEFAULT_COLUMN_COUNT = 20
const DEFAULT_COLUMN_WIDTH = 73
const DEFAULT_ROW_HEIGHT = 19

/**
 * 生成基础 WorkbookData
 */
function createWorkbookData(sheets: IWorksheetData[]): IWorkbookData {
  const sheetOrder = sheets.map(s => s.id)
  const sheetsMap: { [sheetId: string]: Partial<IWorksheetData> } = {}
  sheets.forEach(s => {
    sheetsMap[s.id] = s
  })

  return {
    id: uuidv4(),
    name: 'Workbook',
    appVersion: '1.0.0',
    locale: LocaleType.ZH_CN,
    styles: {},
    sheetOrder,
    sheets: sheetsMap,
    // 资源(图片/样式等)。此处先空数组,避免渲染期望字段缺失
    resources: [],
    defaultStyle: null,
  }
}
/**
 * 将二维数组转换成 IWorksheetData 的 cellData 结构(行、列均为数字索引)
 */
function arrayToCellData(data: any[][]): { [row: number]: { [col: number]: ICellData } } {
  const cellData: { [row: number]: { [col: number]: ICellData } } = {}

  data.forEach((row, rowIndex) => {
    if (!cellData[rowIndex]) cellData[rowIndex] = {}
    row.forEach((value, colIndex) => {
      // 仅在有值时填充 cell,避免生成庞大的稀疏表
      if (value !== undefined && value !== null && value !== '') {
        const cell: ICellData = { v: value }
        // 简单类型标记(可选)
        if (typeof value === 'number') {
          ;(cell as any).t = 2 // number
        } else if (typeof value === 'boolean') {
          ;(cell as any).t = 3 // boolean
        } else {
          ;(cell as any).t = 1 // string / others
        }
        cellData[rowIndex][colIndex] = cell
      }
    })
  })

  return cellData
}

/**
 * 计算二维数组的行列数(含默认兜底)
 */
function computeDimensions(rows: any[][]): { rowCount: number; columnCount: number } {
  const rowCount = Math.max(DEFAULT_ROW_COUNT, rows.length)
  const columnCount = Math.max(
    DEFAULT_COLUMN_COUNT,
    rows.reduce((max, row) => Math.max(max, row.length), 0),
  )
  return { rowCount, columnCount }
}

/**
 * 从二维数组构建 IWorksheetData
 */
function buildSheetFrom2DArray(name: string, rows: any[][]): IWorksheetData {
  const { rowCount, columnCount } = computeDimensions(rows)
  return {
    id: uuidv4(),
    name,
    rowCount,
    columnCount,
    defaultColumnWidth: DEFAULT_COLUMN_WIDTH,
    defaultRowHeight: DEFAULT_ROW_HEIGHT,
    zoomRatio: 1,
    scrollTop: 0,
    scrollLeft: 0,
    hidden: 0,
    tabColor: '',
    drawings: {},
    drawingsOrder: [],
    cellData: arrayToCellData(rows),
  } as unknown as IWorksheetData
}

/**
 * 将 XLSX 解析结果转换为 sheets(所有 sheet 使用 header:1 的二维数组)
 */
function buildSheetsFromXlsxWorkbook(xlsxWorkbook: XLSX.WorkBook): IWorksheetData[] {
  return xlsxWorkbook.SheetNames.map(sheetName => {
    const rows = XLSX.utils.sheet_to_json<any[]>(xlsxWorkbook.Sheets[sheetName], { header: 1 })
    return buildSheetFrom2DArray(sheetName, rows)
  })
}

/**
 * 仅使用 xlsx 解析(值级别)并返回完整 IWorkbookData
 */
function parseXlsxArrayBufferToWorkbook(buffer: ArrayBuffer): IWorkbookData {
  const xlsxWorkbook = XLSX.read(buffer, { type: 'array' })
  const sheets: IWorksheetData[] = buildSheetsFromXlsxWorkbook(xlsxWorkbook)
  return createWorkbookData(sheets)
}

/**
 * 处理 XLSX 文件
 */
export const handleXlsx = async (buffer: ArrayBuffer): Promise<IWorkbookData> => {
  function ensureSheetDefaults(sheet: any): IWorksheetData {
    const s: any = { ...sheet }
    // 计算行列数(若缺失)
    if ((s.rowCount == null || s.columnCount == null) && s.cellData) {
      const rowIndices = Object.keys(s.cellData)
        .map(k => Number(k))
        .filter(n => !Number.isNaN(n))
      const maxRow = rowIndices.length > 0 ? Math.max(...rowIndices) : -1
      let maxCol = -1
      rowIndices.forEach(r => {
        const cols = s.cellData[r]
        if (!cols) return
        const colIndices = Object.keys(cols)
          .map(k => Number(k))
          .filter(n => !Number.isNaN(n))
        if (colIndices.length > 0) maxCol = Math.max(maxCol, Math.max(...colIndices))
      })
      if (s.rowCount == null) s.rowCount = Math.max(DEFAULT_ROW_COUNT, maxRow + 1)
      if (s.columnCount == null) s.columnCount = Math.max(DEFAULT_COLUMN_COUNT, maxCol + 1)
    }
    if (s.rowCount == null) s.rowCount = DEFAULT_ROW_COUNT
    if (s.columnCount == null) s.columnCount = DEFAULT_COLUMN_COUNT
    if (s.defaultColumnWidth == null) s.defaultColumnWidth = DEFAULT_COLUMN_WIDTH
    if (s.defaultRowHeight == null) s.defaultRowHeight = DEFAULT_ROW_HEIGHT
    if (s.zoomRatio == null) s.zoomRatio = 1
    if (s.scrollTop == null) s.scrollTop = 0
    if (s.scrollLeft == null) s.scrollLeft = 0
    if (s.hidden == null) s.hidden = 0
    if (s.tabColor == null) s.tabColor = ''
    if (s.drawings == null) s.drawings = {}
    if (s.drawingsOrder == null) s.drawingsOrder = []
    return s as IWorksheetData
  }

  function normalizeExportJsonToWorkbook(exportJson: any): IWorkbookData {
    if (!exportJson) throw new Error('Invalid exportJson')
    const wb: any = { ...exportJson }
    if (!wb.id) wb.id = uuidv4()
    if (!wb.name) wb.name = 'Workbook'
    if (!wb.styles) wb.styles = {}
    if (!wb.resources) wb.resources = []

    if (Array.isArray(wb.sheets)) {
      const arr = wb.sheets
      const sheetOrder: string[] = []
      const map: Record<string, any> = {}
      arr.forEach((s: any) => {
        const id = s.id || uuidv4()
        sheetOrder.push(id)
        map[id] = ensureSheetDefaults({ ...s, id })
      })
      wb.sheets = map
      if (!wb.sheetOrder) wb.sheetOrder = sheetOrder
    } else if (wb.sheets && typeof wb.sheets === 'object') {
      if (!wb.sheetOrder) wb.sheetOrder = Object.keys(wb.sheets)
      Object.keys(wb.sheets).forEach((id: string) => {
        wb.sheets[id] = ensureSheetDefaults({ id, ...wb.sheets[id] })
      })
    } else {
      throw new Error('exportJson.sheets missing')
    }

    return wb as IWorkbookData
  }

  const blob = new Blob([buffer], {
    type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  })
  const file = new File([blob], 'import.xlsx', { type: blob.type })

  return new Promise<IWorkbookData>((resolve, reject) => {
    LuckyExcel.transformExcelToUniver(
      file,
      (exportJson: any) => {
        try {
          const wb = normalizeExportJsonToWorkbook(exportJson)
          resolve(wb)
        } catch (err) {
          // 解析结果异常时,回退到 xlsx 仅值解析
          try {
            resolve(parseXlsxArrayBufferToWorkbook(buffer))
          } catch (fallbackErr) {
            reject(fallbackErr)
          }
        }
      },
      (error: any) => {
        // 插件报错时,回退到 xlsx 仅值解析
        try {
          resolve(parseXlsxArrayBufferToWorkbook(buffer))
        } catch (e) {
          reject(error || e)
        }
      },
    )
  })
}

/**
 * 处理 XLS 文件
 */
export function handleXls(buffer: ArrayBuffer): IWorkbookData {
  const xlsWorkbook = XLSX.read(buffer, { type: 'array' })
  const sheets: IWorksheetData[] = buildSheetsFromXlsxWorkbook(xlsWorkbook)
  return createWorkbookData(sheets)
}

/**
 * 处理 CSV 文件
 */
export async function handleCsv(blob: Blob): Promise<IWorkbookData> {
  const text = await blob.text()
  const rows = text.split(/\r?\n/).map(r => r.split(','))
  const sheet: IWorksheetData = buildSheetFrom2DArray('Sheet1', rows)
  return createWorkbookData([sheet])
}

预设模式 index.tsx

typescript 复制代码
import React, { useEffect, useState, useRef } from 'react'
import { Spin } from 'antd'
import { UniverSheetsCorePreset } from '@univerjs/preset-sheets-core'
import { UniverSheetsDrawingPreset } from '@univerjs/preset-sheets-drawing'
import sheetsDrawingZhCN from '@univerjs/preset-sheets-drawing/locales/zh-CN'
import { UniverSheetsHyperLinkPreset } from '@univerjs/preset-sheets-hyper-link'
import sheetsHyperLinkZhCN from '@univerjs/preset-sheets-hyper-link/locales/zh-CN'
import sheetsCoreZhCN from '@univerjs/preset-sheets-core/locales/zh-CN'
import { createUniver, LocaleType, mergeLocales } from '@univerjs/presets'
import type { IWorkbookData } from '@univerjs/presets'
import { handleXlsx, handleXls, handleCsv } from './utils'

import '@univerjs/preset-sheets-core/lib/index.css'

interface IProps {
  requestUrl: string
  height?: string | number
  getPreviewFile: (url: string) => Promise<any>
  fileType: 'xlsx' | 'xls' | 'csv'
}
const ExcelPreview: React.FC<IProps> = ({ requestUrl, getPreviewFile, fileType, height }) => {
  const [loading, setLoading] = useState(false)
  const [excelData, setExcelData] = useState<IWorkbookData>()
  const instanceRef = useRef<any>(null)

  useEffect(() => {
    setLoading(true)
    getPreviewFile(requestUrl)
      .then(async response => {
        if (!response) {
          setLoading(false)
          return
        }
        if (fileType === 'xlsx') {
          const xlsx = await response.arrayBuffer()
          const workbookData = await handleXlsx(xlsx)
          setExcelData(workbookData)
        } else if (fileType === 'xls') {
          const xls = await response.arrayBuffer()
          const workbookData = await handleXls(xls)
          setExcelData(workbookData)
        } else {
          const csv = await response.blob()
          const workbookData = await handleCsv(csv)
          setExcelData(workbookData)
        }
        setLoading(false)
      })
      .catch(error => {
        // eslint-disable-next-line no-console
        console.log(error)
        setLoading(false)
      })
  }, [getPreviewFile, fileType, requestUrl])

  useEffect(() => {
    if (!excelData) return
    instanceRef.current.createWorkbook(excelData)
  }, [excelData])

  useEffect(() => {
    const { univerAPI } = createUniver({
      locale: LocaleType.ZH_CN,
      locales: {
        [LocaleType.ZH_CN]: mergeLocales(sheetsCoreZhCN, sheetsHyperLinkZhCN, sheetsDrawingZhCN),
      },
      presets: [
        UniverSheetsCorePreset({
          container: 'excel-preview-container-univer',
          toolbar: false,
          contextMenu: false,
          formulaBar: false,
          footer: true,
          editable: false,
          selectionMode: 'text',
        }),
        UniverSheetsDrawingPreset(),
        UniverSheetsHyperLinkPreset(),
      ],
    })
    instanceRef.current = univerAPI

    return () => {
      if (instanceRef.current) {
        instanceRef.current.dispose()
        instanceRef.current = null
      }
    }
  }, [])

  return (
    <>
      <Spin size="large" spinning={loading}>
        <div style={{ width: '100%', height: height || '100%', userSelect: 'text' }} id="excel-preview-container-univer" />
      </Spin>
    </>
  )
}

export default ExcelPreview

utils.ts

typescript 复制代码
import { IWorkbookData, IWorksheetData, ICellData } from '@univerjs/presets'
import * as XLSX from 'xlsx'
import LuckyExcel from '@zwight/luckyexcel'
import { v4 as uuidv4 } from 'uuid'

const DEFAULT_ROW_COUNT = 1000
const DEFAULT_COLUMN_COUNT = 20
const DEFAULT_COLUMN_WIDTH = 73
const DEFAULT_ROW_HEIGHT = 19

/**
 * 生成基础 WorkbookData
 */
function createWorkbookData(sheets: IWorksheetData[]): IWorkbookData {
  const sheetOrder = sheets.map(s => s.id)
  const sheetsMap: { [sheetId: string]: Partial<IWorksheetData> } = {}
  sheets.forEach(s => {
    sheetsMap[s.id] = s
  })

  return {
    id: uuidv4(),
    name: 'Workbook',
    appVersion: '1.0.0',
    // locale: 'en-US',
    styles: {},
    sheetOrder,
    sheets: sheetsMap,
    // 资源(图片/样式等)。此处先空数组,避免渲染期望字段缺失
    resources: [],
    defaultStyle: null,
  }
}
/**
 * 将二维数组转换成 IWorksheetData 的 cellData 结构(行、列均为数字索引)
 */
function arrayToCellData(data: any[][]): { [row: number]: { [col: number]: ICellData } } {
  const cellData: { [row: number]: { [col: number]: ICellData } } = {}

  data.forEach((row, rowIndex) => {
    if (!cellData[rowIndex]) cellData[rowIndex] = {}
    row.forEach((value, colIndex) => {
      // 仅在有值时填充 cell,避免生成庞大的稀疏表
      if (value !== undefined && value !== null && value !== '') {
        const cell: ICellData = { v: value }
        // 简单类型标记(可选)
        if (typeof value === 'number') {
          ;(cell as any).t = 2 // number
        } else if (typeof value === 'boolean') {
          ;(cell as any).t = 3 // boolean
        } else {
          ;(cell as any).t = 1 // string / others
        }
        cellData[rowIndex][colIndex] = cell
      }
    })
  })

  return cellData
}

/**
 * 计算二维数组的行列数(含默认兜底)
 */
function computeDimensions(rows: any[][]): { rowCount: number; columnCount: number } {
  const rowCount = Math.max(DEFAULT_ROW_COUNT, rows.length)
  const columnCount = Math.max(
    DEFAULT_COLUMN_COUNT,
    rows.reduce((max, row) => Math.max(max, row.length), 0),
  )
  return { rowCount, columnCount }
}

/**
 * 从二维数组构建 IWorksheetData
 */
function buildSheetFrom2DArray(name: string, rows: any[][]): IWorksheetData {
  const { rowCount, columnCount } = computeDimensions(rows)
  return {
    id: uuidv4(),
    name,
    rowCount,
    columnCount,
    defaultColumnWidth: DEFAULT_COLUMN_WIDTH,
    defaultRowHeight: DEFAULT_ROW_HEIGHT,
    zoomRatio: 1,
    scrollTop: 0,
    scrollLeft: 0,
    hidden: 0,
    tabColor: '',
    drawings: {},
    drawingsOrder: [],
    cellData: arrayToCellData(rows),
  } as unknown as IWorksheetData
}

/**
 * 将 XLSX 解析结果转换为 sheets(所有 sheet 使用 header:1 的二维数组)
 */
function buildSheetsFromXlsxWorkbook(xlsxWorkbook: XLSX.WorkBook): IWorksheetData[] {
  return xlsxWorkbook.SheetNames.map(sheetName => {
    const rows = XLSX.utils.sheet_to_json<any[]>(xlsxWorkbook.Sheets[sheetName], { header: 1 })
    return buildSheetFrom2DArray(sheetName, rows)
  })
}

/**
 * 仅使用 xlsx 解析(值级别)并返回完整 IWorkbookData
 */
function parseXlsxArrayBufferToWorkbook(buffer: ArrayBuffer): IWorkbookData {
  const xlsxWorkbook = XLSX.read(buffer, { type: 'array' })
  const sheets: IWorksheetData[] = buildSheetsFromXlsxWorkbook(xlsxWorkbook)
  return createWorkbookData(sheets)
}

/**
 * 处理 XLSX 文件
 */
export const handleXlsx = async (buffer: ArrayBuffer): Promise<IWorkbookData> => {
  function ensureSheetDefaults(sheet: any): IWorksheetData {
    const s: any = { ...sheet }
    // 计算行列数(若缺失)
    if ((s.rowCount == null || s.columnCount == null) && s.cellData) {
      const rowIndices = Object.keys(s.cellData)
        .map(k => Number(k))
        .filter(n => !Number.isNaN(n))
      const maxRow = rowIndices.length > 0 ? Math.max(...rowIndices) : -1
      let maxCol = -1
      rowIndices.forEach(r => {
        const cols = s.cellData[r]
        if (!cols) return
        const colIndices = Object.keys(cols)
          .map(k => Number(k))
          .filter(n => !Number.isNaN(n))
        if (colIndices.length > 0) maxCol = Math.max(maxCol, Math.max(...colIndices))
      })
      if (s.rowCount == null) s.rowCount = Math.max(DEFAULT_ROW_COUNT, maxRow + 1)
      if (s.columnCount == null) s.columnCount = Math.max(DEFAULT_COLUMN_COUNT, maxCol + 1)
    }
    if (s.rowCount == null) s.rowCount = DEFAULT_ROW_COUNT
    if (s.columnCount == null) s.columnCount = DEFAULT_COLUMN_COUNT
    if (s.defaultColumnWidth == null) s.defaultColumnWidth = DEFAULT_COLUMN_WIDTH
    if (s.defaultRowHeight == null) s.defaultRowHeight = DEFAULT_ROW_HEIGHT
    if (s.zoomRatio == null) s.zoomRatio = 1
    if (s.scrollTop == null) s.scrollTop = 0
    if (s.scrollLeft == null) s.scrollLeft = 0
    if (s.hidden == null) s.hidden = 0
    if (s.tabColor == null) s.tabColor = ''
    if (s.drawings == null) s.drawings = {}
    if (s.drawingsOrder == null) s.drawingsOrder = []
    return s as IWorksheetData
  }

  function normalizeExportJsonToWorkbook(exportJson: any): IWorkbookData {
    if (!exportJson) throw new Error('Invalid exportJson')
    const wb: any = { ...exportJson }
    if (!wb.id) wb.id = uuidv4()
    if (!wb.name) wb.name = 'Workbook'
    if (!wb.styles) wb.styles = {}
    if (!wb.resources) wb.resources = []

    if (Array.isArray(wb.sheets)) {
      const arr = wb.sheets
      const sheetOrder: string[] = []
      const map: Record<string, any> = {}
      arr.forEach((s: any) => {
        const id = s.id || uuidv4()
        sheetOrder.push(id)
        map[id] = ensureSheetDefaults({ ...s, id })
      })
      wb.sheets = map
      if (!wb.sheetOrder) wb.sheetOrder = sheetOrder
    } else if (wb.sheets && typeof wb.sheets === 'object') {
      if (!wb.sheetOrder) wb.sheetOrder = Object.keys(wb.sheets)
      Object.keys(wb.sheets).forEach((id: string) => {
        wb.sheets[id] = ensureSheetDefaults({ id, ...wb.sheets[id] })
      })
    } else {
      throw new Error('exportJson.sheets missing')
    }

    return wb as IWorkbookData
  }

  const blob = new Blob([buffer], {
    type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  })
  const file = new File([blob], 'import.xlsx', { type: blob.type })

  return new Promise<IWorkbookData>((resolve, reject) => {
    LuckyExcel.transformExcelToUniver(
      file,
      (exportJson: any) => {
        try {
          const wb = normalizeExportJsonToWorkbook(exportJson)
          resolve(wb)
        } catch (err) {
          // 解析结果异常时,回退到 xlsx 仅值解析
          try {
            resolve(parseXlsxArrayBufferToWorkbook(buffer))
          } catch (fallbackErr) {
            reject(fallbackErr)
          }
        }
      },
      (error: any) => {
        // 插件报错时,回退到 xlsx 仅值解析
        try {
          resolve(parseXlsxArrayBufferToWorkbook(buffer))
        } catch (e) {
          reject(error || e)
        }
      },
    )
  })
}

/**
 * 处理 XLS 文件
 */
export function handleXls(buffer: ArrayBuffer): IWorkbookData {
  const xlsWorkbook = XLSX.read(buffer, { type: 'array' })
  const sheets: IWorksheetData[] = buildSheetsFromXlsxWorkbook(xlsWorkbook)
  return createWorkbookData(sheets)
}

/**
 * 处理 CSV 文件
 */
export async function handleCsv(blob: Blob): Promise<IWorkbookData> {
  const text = await blob.text()
  const rows = text.split(/\r?\n/).map(r => r.split(','))
  const sheet: IWorksheetData = buildSheetFrom2DArray('Sheet1', rows)
  return createWorkbookData([sheet])
}
相关推荐
努力往上爬de蜗牛20 小时前
react native打包后发生的问题
react.js
c***979820 小时前
Vue性能优化实战
前端·javascript·vue.js
哈哈O哈哈哈20 小时前
ECMAScript 2025 正式发布:10 个让你眼前一亮的 JavaScript 新特性!
前端·javascript
哈哈O哈哈哈20 小时前
2025 年值得关注的 CSS 新属性与功能
前端·css
我叫张小白。20 小时前
TypeScript泛型进阶:掌握类型系统的强大工具
前端·javascript·typescript
麦麦在写代码20 小时前
前端学习4
前端·学习
你听得到1120 小时前
Web前端们!我用三年亲身经历,说说从 uniapp 到 Flutter怎么转型的,这条路我爬过,坑我踩过
前端·flutter·uni-app
葡萄城技术团队20 小时前
在数据录入、指标补录、表单填报场景中,SpreadJS 具备哪些优势和价值
前端
孟陬21 小时前
AI 每日心得——AI 是效率杠杆,而非培养对象
前端
打小就很皮...21 小时前
React 项目开发指南:脚手架搭建、Axios 封装与 Gitee 远程仓库配置
react.js·gitee·axios