import { createUniver } from '@univerjs/presets'; import { UniverSheetsCorePreset } from '@univerjs/preset-sheets-core'; import { UniverSheetsDrawingPreset } from '@univerjs/preset-sheets-drawing'; import { LocaleType, ImageSourceType } from '@univerjs/core'; import zhCN from '@univerjs/preset-sheets-core/locales/zh-CN'; // Univer 表格 UI 需要显式引入 CSS(preset 的 es 入口只 import 逻辑,不 import 样式) 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-numfmt-ui/lib/index.css'; // drawing(浮动图片/贴图)相关 CSS import '@univerjs/drawing-ui/lib/index.css'; import '@univerjs/sheets-drawing-ui/lib/index.css'; import '@univerjs/preset-sheets-drawing/lib/index.css'; const DEFAULT_COL_WIDTH = 110; /** * 根据列中文标签推断合理列宽,避免所有列一样宽。 */ function guessColWidth(label) { label = (label || '').toString(); if (label.includes('序号')) return 56; if (label.includes('备注') || label.includes('描述')) return 180; if (label.includes('名称') || label.includes('图号') || label.includes('材质')) return 150; if (label.includes('规格') || label.includes('工序') || label.includes('编号')) return 120; if (label.includes('用量') || label.includes('数量') || label.includes('单位')) return 80; return DEFAULT_COL_WIDTH; } /** * OneBot BOM 明细表 —— Univer 表格引擎入口。 * 走 ESM 入口(icons 包的 cjs 构建 require 路径坏掉,esm 构建正常)。 * * 数据布局:第 0 行是表头,第 1..n 行是数据。 * 返回 { univer, univerAPI, sheet, workbook, endEditing, getActiveCell, insertImage, * insertImageAt, fitToContainer },供页面做编辑/保存/附件/自适应等操作。 */ function createOneBotSheet(opts) { const { container, data, // 原始数据行数组(已含 _id 和所有列 key) columns, // 全部列 key 数组(fixed + dynamic) fixedLabels, // { key: 中文标签 } imageColumns, // 图片列 key 数组(可选) fileColumns, // 附件列 key 数组(可选) onCellChange, // (row, col) => void (row 是 0-based 数据行索引,即 sheet 行号-1) onCellClick, // (row, col) => void (点击单元格,row 是 0-based sheet 行号) } = opts; const imageColSet = new Set(imageColumns || []); const fileColSet = new Set(fileColumns || []); // 图片列宽一些,附件列次之 const colIndexByKey = {}; columns.forEach(function (key, c) { colIndexByKey[key] = c; }); const { univer, univerAPI } = createUniver({ locale: LocaleType.ZH_CN, locales: { [LocaleType.ZH_CN]: zhCN }, presets: [ UniverSheetsCorePreset({ container, }), UniverSheetsDrawingPreset(), ], }); // Univer 0.25 不会自动创建 workbook。传空对象 {} 让 Workbook 构造走 getEmptySnapshot() 分支 let workbook = univerAPI.getActiveWorkbook(); if (!workbook) { workbook = univerAPI.createWorkbook({}); } // 空 workbook 没有 sheet,手动插入一个 let sheet = workbook.getActiveSheet(); if (!sheet) { sheet = workbook.insertSheet('Sheet1'); } // 用 setColumnCount/setRowCount 设置行列数;空表也预置若干空数据行,保证可编辑可保存 sheet.setColumnCount(columns.length + 1); sheet.setRowCount(Math.max(data.length, 8) + 2); // 表头(第 0 行) columns.forEach(function (key, c) { const label = fixedLabels[key] || key; sheet.getRange(0, c).setValue(label); }); // 列宽:按列类型分配,图片列更宽 columns.forEach(function (key, c) { let w = guessColWidth(fixedLabels[key] || key); if (imageColSet.has(key)) w = 170; else if (fileColSet.has(key)) w = 140; sheet.setColumnWidth(c, w); }); // 末尾多余列保持默认窄列 sheet.setColumnWidth(columns.length, DEFAULT_COL_WIDTH); // 数据行(第 1 行起) data.forEach(function (rowData, r) { columns.forEach(function (key, c) { const v = rowData[key]; sheet.getRange(r + 1, c).setValue(v === undefined || v === null ? '' : v); }); }); // 监听值变化(排除表头第 0 行)。包 try-catch 防止事件 API 异常中断整个初始化 try { univerAPI.addEvent(univerAPI.Event.SheetValueChanged, function (params) { if (params && params.effectedRanges) { params.effectedRanges.forEach(function (rng) { const row = rng.getRow(); const col = rng.getColumn(); if (row > 0 && typeof onCellChange === 'function') { let val = null; try { val = rng.getValue(); } catch (e) {} // 传 (dataRowIndex, colIndex, newValue) onCellChange(row - 1, col, val); } }); } }); } catch (e) { // 事件监听失败不致命,保存时走全量对比兜底 } // 单元格点击事件(供附件 PDF 弹窗预览/下载等场景使用) if (typeof onCellClick === 'function') { try { univerAPI.addEvent(univerAPI.Event.CellClicked, function (params) { if (params && typeof params.row === 'number' && typeof params.column === 'number') { onCellClick(params.row, params.column); } }); } catch (e) {} } // 读取图片原始尺寸(URL 或 base64),失败返回 null function loadImageSize(source) { return new Promise(function (resolve) { var img = new Image(); img.onload = function () { resolve({ w: img.naturalWidth || 1, h: img.naturalHeight || 1 }); }; img.onerror = function () { resolve(null); }; img.src = source; }); } // 按 URL 源插入浮动图片到指定 cell 位置。尺寸自适应:宽度跟随列宽(默认 max(colW-8, 160)), // 高度按原图比例计算(读不到原图尺寸时按 4:3 兜底)。opts 可覆盖 width/height。 async function insertImageAt(row, col, source, sourceType, offsetX, opts) { opts = opts || {}; offsetX = offsetX || 0; let colW = DEFAULT_COL_WIDTH; try { colW = sheet.getColumnWidth(col) || DEFAULT_COL_WIDTH; } catch (e) {} const width = opts.width || Math.min(Math.max(colW - 8, 60), 160); let height = opts.height || 0; if (!height) { const sz = await loadImageSize(source); height = sz ? Math.round(width * sz.h / sz.w) : Math.round(width * 0.75); } try { sheet.setRowHeight(row, Math.max(sheet.getRowHeight(row), height + 6)); } catch (e) {} const image = await sheet.newOverGridImage() .setSource(source, sourceType || ImageSourceType.URL) .setColumn(col) .setRow(row) .setColumnOffset(4 + offsetX) .setRowOffset(4) .setWidth(width) .setHeight(height) .buildAsync(); sheet.insertImages([image]); return image; } // 渲染图片列:解析 JSON [{url,name,image}] 并插入浮动缩略图(异步,不阻塞主流程) if (imageColumns && imageColumns.length) { (async function renderImages() { for (let c = 0; c < columns.length; c++) { const key = columns[c]; if (!imageColSet.has(key)) continue; for (let r = 0; r < data.length; r++) { const v = data[r][key]; if (typeof v !== 'string' || v === '' || v[0] !== '[') continue; let arr = null; try { arr = JSON.parse(v); } catch (e) { continue; } if (!Array.isArray(arr)) continue; let n = 0; for (const a of arr) { if (a && a.url) { try { await insertImageAt(r + 1, c, a.url, ImageSourceType.URL, n * 48); n++; } catch (e) {} } } } } })(); } const inst = { univer, univerAPI, sheet, workbook, endEditing: function () { return workbook.endEditing(true); }, // 获取当前选中单元格(无选中时返回 null) getActiveCell: function () { return sheet.getActiveCell(); }, // 可靠的行/列数(FWorksheet facade 没有 getRowCount/getColumnCount,只有 getMaxRows/getMaxColumns) getRowCount: function () { try { return sheet.getMaxRows(); } catch (e) { return data.length + 2; } }, getColumnCount: function () { try { return sheet.getMaxColumns(); } catch (e) { return columns.length + 1; } }, // 插入浮动图片(base64 或 URL),定位到指定单元格(向后兼容旧签名) insertImage: function (source, sourceType, row, col) { return insertImageAt(row, col, source, sourceType, 0); }, insertImageAt, // 按容器宽度自适应缩放:让所有列宽之和铺满容器(clamp 到 [0.5, 1.5]) fitToContainer: function () { try { const w = container.clientWidth; if (!w) return; let total = 0; const colCount = inst.getColumnCount(); for (let c = 0; c < colCount; c++) { try { total += sheet.getColumnWidth(c) || DEFAULT_COL_WIDTH; } catch (e) { total += DEFAULT_COL_WIDTH; } } if (total <= 0) return; let zoom = w / total; zoom = Math.max(0.5, Math.min(1.5, zoom)); sheet.zoom(zoom); } catch (e) {} }, // 重置为 100% resetZoom: function () { try { sheet.zoom(1); } catch (e) {} }, }; window.__onebotInst = inst; // 供调试/自动化测试直接调 API // 初次自适应(等容器渲染后) setTimeout(function () { inst.fitToContainer(); }, 60); return inst; } window.createOneBotSheet = createOneBotSheet;