Files

261 lines
9.4 KiB
JavaScript

import { createUniver, defaultTheme, LocaleType } from '@univerjs/presets'
import { UniverSheetsCorePreset } from '@univerjs/preset-sheets-core'
import { UniverSheetsDrawingPreset } from '@univerjs/preset-sheets-drawing'
import { UniverSheetsFilterPreset } from '@univerjs/preset-sheets-filter'
import { UniverSheetsSortPreset } from '@univerjs/preset-sheets-sort'
import { UniverSheetsDataValidationPreset } from '@univerjs/preset-sheets-data-validation'
import { UniverSheetsConditionalFormattingPreset } from '@univerjs/preset-sheets-conditional-formatting'
import { UniverSheetsFindReplacePreset } from '@univerjs/preset-sheets-find-replace'
import zhCNCore from '@univerjs/preset-sheets-core/locales/zh-CN'
import zhCNDrawing from '@univerjs/preset-sheets-drawing/locales/zh-CN'
import zhCNFilter from '@univerjs/preset-sheets-filter/locales/zh-CN'
import zhCNSort from '@univerjs/preset-sheets-sort/locales/zh-CN'
import zhCNDataValidation from '@univerjs/preset-sheets-data-validation/locales/zh-CN'
import zhCNConditionalFormatting from '@univerjs/preset-sheets-conditional-formatting/locales/zh-CN'
import zhCNFindReplace from '@univerjs/preset-sheets-find-replace/locales/zh-CN'
import '@univerjs/presets/lib/styles/preset-sheets-core.css'
import '@univerjs/presets/lib/styles/preset-sheets-filter.css'
// 引入 UI 样式和主题,新版 Univer 必须引入 UI 包本身才会自动注入 React 组件树
import '@univerjs/design/lib/index.css'
import '@univerjs/ui/lib/index.css'
import '@univerjs/sheets-ui/lib/index.css'
import '@univerjs/sheets-formula-ui/lib/index.css'
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import * as ReactDOMClient from 'react-dom/client'
window.React = React
window.ReactDOM = ReactDOM
window.ReactDOMClient = ReactDOMClient
export const createRuntime = (config) => {
const container = config.container
const isDark = config.theme === 'dark'
// 在这里强制配置 UI,保证 React 的根节点注入逻辑被激活
const { univerAPI } = createUniver({
locale: LocaleType.ZH_CN,
locales: {
[LocaleType.ZH_CN]: {
...zhCNCore,
...zhCNDrawing,
...zhCNFilter,
...zhCNSort,
...zhCNDataValidation,
...zhCNConditionalFormatting,
...zhCNFindReplace
}
},
theme: defaultTheme,
presets: [
UniverSheetsCorePreset({
container,
header: true,
footer: true,
workerURL: '/tools/cloud_sheets/vendor/univer/univer.worker.js'
}),
UniverSheetsDrawingPreset(),
UniverSheetsFilterPreset(),
UniverSheetsSortPreset(),
UniverSheetsDataValidationPreset(),
UniverSheetsConditionalFormattingPreset(),
UniverSheetsFindReplacePreset()
]
})
let activeWorkbook = null
const parseWorkbookJson = raw => {
try {
if (typeof raw === 'string') return JSON.parse(raw || '{}')
if (raw && typeof raw === 'object') return raw
return {}
} catch {
return {}
}
}
const stringifyWorkbook = data => {
try {
return JSON.stringify(data)
} catch (err) {
console.error('[UniverAdapter] stringifyWorkbook error', err)
const safeClone = (obj, ancestors = new WeakSet()) => {
if (obj === null || typeof obj !== 'object') return obj
if (ancestors.has(obj)) return undefined // 忽略真正的循环引用
ancestors.add(obj)
if (Array.isArray(obj)) {
const arr = []
for (let i = 0; i < obj.length; i++) arr[i] = safeClone(obj[i], ancestors)
ancestors.delete(obj)
return arr
}
const res = {}
for (const k in obj) {
if (Object.prototype.hasOwnProperty.call(obj, k)) {
res[k] = safeClone(obj[k], ancestors)
}
}
ancestors.delete(obj)
return res
}
return JSON.stringify(safeClone(data))
}
}
const normalizeWorkbook = wb => {
const defaultData = {
id: 'workbook-main',
sheetOrder: ['sheet-1'],
name: '云表格',
appVersion: '1.0.0',
locale: 'zhCN',
styles: {},
sheets: {
'sheet-1': {
id: 'sheet-1',
name: 'Sheet1',
tabColor: '',
hidden: 0,
rowCount: 1000,
columnCount: 30,
zoomRatio: 1,
defaultColumnWidth: 80,
defaultRowHeight: 24,
cellData: {}
}
}
}
return { ...defaultData, ...wb }
}
const makeDefaultWorkbook = () => normalizeWorkbook({})
const loadWorkbook = workbook => {
const data = normalizeWorkbook(workbook)
// 强制生成一个绝对唯一的 ID,防止 Univer 内部根据 ID 缓存旧的快照或产生冲突
data.id = `workbook-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
if (activeWorkbook && typeof activeWorkbook.getId === 'function') {
try { univerAPI.disposeUnit(activeWorkbook.getId()) } catch {}
}
activeWorkbook = univerAPI.createWorkbook(data)
return activeWorkbook
}
loadWorkbook(makeDefaultWorkbook())
// 对于某些 Univer 版本,必须调用挂载才能将 React 根节点插入到 DOM 中。
try {
if (univerAPI && typeof univerAPI.mount === 'function') {
univerAPI.mount(container)
}
} catch (e) {
console.error('挂载 Univer 失败', e)
}
window.univerAPI = univerAPI
return {
setWorkbookJSON: raw => {
const wb = parseWorkbookJson(raw)
loadWorkbook(wb)
},
commitPendingChanges: async () => {
try {
const active = univerAPI.getActiveWorkbook() || activeWorkbook
if (!active) return { ok: false, reason: 'no_active_workbook' }
// 在新版 Univer 架构中,直接触发表格失焦或退出编辑状态的最有效方式:
// 执行一个空操作,或者利用内部的 command 服务退出编辑。
// 为了稳定,这里使用原生的 DOM focus 转移,配合 Univer 提供的 isCellEditing 方法判断。
let isEditing = false
if (typeof active.isCellEditing === 'function') {
isEditing = active.isCellEditing()
}
// 如果我们没找到现成的 endEditingAsync,也可以尝试派发命令
if (isEditing) {
try {
// 很多版本提供 endEditing 方法
if (typeof active.endEditing === 'function') {
active.endEditing()
} else if (typeof active.endEditingAsync === 'function') {
await active.endEditingAsync(true)
} else {
// 触发原生的回车/失焦事件作为降级方案
const activeEl = document.activeElement
if (activeEl && typeof activeEl.blur === 'function') {
activeEl.blur()
}
// 模拟触发 Enter 键事件让其结束编辑
const enterEvent = new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true })
if (activeEl) activeEl.dispatchEvent(enterEvent)
}
} catch(e) {}
}
return { ok: true, isCellEditingBefore: isEditing }
} catch (e) {
console.error('[UniverAdapter] commitPendingChanges error', e)
return { ok: false, reason: e && e.message ? e.message : String(e || 'unknown_error') }
}
},
getWorkbookJSON: () => {
try {
const active = univerAPI.getActiveWorkbook() || activeWorkbook
if (active && typeof active.save === 'function') {
const workbookModel = typeof active.getWorkbook === 'function' ? active.getWorkbook() : null
let data = null
if (workbookModel && typeof workbookModel.save === 'function') {
data = workbookModel.save()
}
if ((!data || Object.keys(data).length === 0) && typeof active.save === 'function') {
data = active.save()
}
if ((!data || Object.keys(data).length === 0) && workbookModel && typeof workbookModel.getSnapshot === 'function') {
data = workbookModel.getSnapshot()
}
if ((!data || Object.keys(data).length === 0) && workbookModel && typeof workbookModel.getConfig === 'function') {
data = workbookModel.getConfig()
}
if ((!data || Object.keys(data).length === 0) && typeof active.getSnapshot === 'function') {
data = active.getSnapshot()
}
try {
const sheet1 = data.sheets && data.sheets['sheet-1']
const cellData = sheet1 && sheet1.cellData
const sampleCell = cellData && cellData['0'] && cellData['0']['0']
console.log('[UniverAdapter] Sample cell 0,0:', sampleCell)
console.log('[UniverAdapter] Workbook name:', workbookModel && workbookModel.getName ? workbookModel.getName() : active.getName ? active.getName() : '')
} catch(e){}
return stringifyWorkbook(data)
} else {
console.error('[UniverAdapter] active workbook error:', active)
}
} catch (e) {
console.error('[UniverAdapter] getWorkbookJSON error', e)
}
return '{}'
},
dispose: () => {
try {
if (activeWorkbook && typeof activeWorkbook.getId === 'function') {
univerAPI.disposeUnit(activeWorkbook.getId())
}
} catch {}
try {
if (univerAPI && typeof univerAPI.dispose === 'function') {
univerAPI.dispose()
}
} catch {}
}
}
}
window.CloudSheetsUniverAdapter = { create: createRuntime }