OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)

This commit is contained in:
wangruiguo
2026-09-03 18:49:20 +08:00
commit d647428529
3501 changed files with 1988906 additions and 0 deletions

View File

@@ -0,0 +1,251 @@
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 需要显式引入 CSSpreset 的 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;