252 lines
10 KiB
JavaScript
252 lines
10 KiB
JavaScript
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;
|