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 }