基于Luckysheet实现的协同编辑在线表格支持在线导入数据库,前端导出,前端导入,后端导出
这两年,在线表格协作工具越来越火,但开源界一直没有相关的实现,被垄断在几个大厂手上,随着Luckysheet的横空出世,开源界终于也有一个漂亮能打的在线表格,而且仔细研究后发现Luckysheet与excel已经特别接近,也实现了协同编辑,故基于Luckysheet,本项目实现了一个协同编辑的demo,以上是前作者说的,于是我就开始给他填坑提示以下是本篇文章正文内容,下面案例可供参考。......
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
提示:这里可以添加本文要记录的大概内容:
这两年,在线表格协作工具越来越火,但开源界一直没有相关的实现,被垄断在几个大厂手上,随着Luckysheet 的横空出世,开源界终于也有一个漂亮能打的在线表格,而且仔细研究后发现Luckysheet与excel已经特别接近,也实现了协同编辑,故基于Luckysheet,本项目实现了一个协同编辑的demo,以上是前作者说的,于是我就开始给他填坑
提示:以下是本篇文章正文内容,下面案例可供参考
一、lucksheet是什么?
在线excel编辑
友情链接:
.Luckysheet
🚀Luckysheet is an online spreadsheet like excel that is powerful, simple to configure, and completely open source
二、 ecsheet 基于MongoDB存储
1.为啥选择这个开源,这个项目选择原因单单是因为相比于别的项目多一个首页也就是展示excel的目录。
2.不足:没有导入导出的协同
三、话不多说上开源Cy_lucksheetProfect
1.原先demo 没有导入导出,开源案例中都是前端导入前端导出,没办法配合协同存到数据库,于是自己分析代码写出来了
2.解释为啥前端导入导出没办法协同:
这是:协同lucksheet初始化代码:
container: "${wb.option.container}", // 设定DOM容器的id
title: "${wb.option.title}", // 设定表格名称
lang: "${wb.option.lang}",
allowUpdate: true,
loadUrl: window.location.protocol + localurl + "/load/${wb.id}",
loadSheetUrl: window.location.protocol + localurl + "/loadSheet/${wb.id}",
updateUrl: "ws://"+localurl + "/ws/" + Math.round(Math.random() * 100) + "/${wb.id}",
functionButton : '<button id="" class="btn btn-primary btn-danger" style=" padding:3px 6px; font-size: 12px; margin-right: 5px;" onclick="exportHandle()">导出</button><button id="" class="btn btn-primary btn-danger" style=" padding:3px 6px; font-size: 12px; margin-right: 85px;" onclick="exportHandles()">在线导出</button>',
这是:普通:
container: 'luckysheet', //luckysheet is the container id
showinfobar:false,
lang: 'zh', // 设定表格语言
allowEdit: true,//作用:是否允许前台编辑
// allowUpdate: true,
allowCopy: true, //是否允许拷贝
showtoolbar: true, //是否第二列显示工具栏
showinfobar: true, //是否显示顶部名称栏
showsheetbar: true, //是否显示底部表格名称区域
showstatisticBar: true, //是否显示底部计数栏
pointEdit: false, //是否是编辑器插入表格模式
pointEditUpdate: null, //编辑器表格更新函数
data:exportJson.sheets,
title:exportJson.info.name,
userInfo:exportJson.info.name.creator,
functionButton : '<button id="" class="btn btn-primary btn-danger" style=" padding:3px 6px; font-size: 12px; margin-right: 5px;" onclick="exportHandle()">导出</button><button id="" class="btn btn-primary btn-danger" style=" padding:3px 6px; font-size: 12px; margin-right: 85px;" onclick="exportHandles()">在线导出</button>',
});
在我的使用中:data 属性和loadUrl 不能并存 loadUrl就是从数据库获得data 这就是协同的区别
四、解决办法
1.前端导入
var input = document.querySelector('input');
input.addEventListener('change', importExcelsss);
/**
* 获取excel数据加載到頁面上
* @param event
*/
function importExcelsss(event) {
alert("导入过程中请勿关闭窗口,请稍后...\n如果遇到大文件导出未响应请点击等待");
var file = event.target.files[0];
var fileName = file.name;
fileName = fileName + "";
//將文件加載到頁面上
LuckyExcel.transformExcelToLucky(file, function(exportJson, luckysheetfile){
if(exportJson.sheets==null || exportJson.sheets.length==0){
alert("Failed to read the content of the excel file, currently does not support xls files!");
return;
}
console.log(exportJson, luckysheetfile);
window.luckysheet.destroy();
window.luckysheet.create({
container: 'luckysheet', //luckysheet is the container id
showinfobar:false,
lang: 'zh', // 设定表格语言
allowEdit: true,//作用:是否允许前台编辑
// allowUpdate: true,
allowCopy: true, //是否允许拷贝
showtoolbar: true, //是否第二列显示工具栏
showinfobar: true, //是否显示顶部名称栏
showsheetbar: true, //是否显示底部表格名称区域
showstatisticBar: true, //是否显示底部计数栏
pointEdit: false, //是否是编辑器插入表格模式
pointEditUpdate: null, //编辑器表格更新函数
data:exportJson.sheets,
title:exportJson.info.name,
userInfo:exportJson.info.name.creator,
functionButton : '<button id="" class="btn btn-primary btn-danger" style=" padding:3px 6px; font-size: 12px; margin-right: 5px;" onclick="exportHandle()">导出</button><button id="" class="btn btn-primary btn-danger" style=" padding:3px 6px; font-size: 12px; margin-right: 85px;" onclick="exportHandles()">在线导出</button>',
});
});
}
可以看到借助 lucksheet 官网的 luckyexcel.umd.js 实现前端导出(bug:切记 lucksheet 里面使用了jquery 和jsscript,再次引用这两个会导致lucksheet 编辑导出异常 )
很显然,前端的导出没办法将放在协同上做(已解决往下看)
2.前端导出三种办法(两种已经实现,还有一种想象的到)
1.借助excel.js 实现这是个vue用的比较多的,但我们可以用htm调用:
function exportHandles(){
var jsdata = new Array();
var sheets = luckysheet.getAllSheets();
exportExcel(sheets);
}
var exportExcel = async function (luckysheet) { // 参数为luckysheet.getluckysheetfile()获取的对象
// 1.创建工作簿,可以为工作簿添加属性
const workbook = new ExcelJS.Workbook()
// 2.创建表格,第二个参数可以配置创建什么样的工作表
luckysheet.every(function (table) {
console.log(JSON.stringify(table.data))
if (table.data.length === 0) return true
const worksheet = workbook.addWorksheet(table.name)
// 3.设置单元格合并,设置单元格边框,设置单元格样式,设置值
setStyleAndValue(table.data, worksheet)
setMerge(table.config.merge, worksheet)
setBorder(table.config.borderInfo, worksheet)
//setwidthcol(table.data, worksheet);
return true
})
// 4.写入 buffer
const buf = await workbook.xlsx.writeBuffer();
// 下载 excel
workbook.xlsx.writeBuffer().then((buf) => {
let blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8' });
const downloadElement = document.createElement('a')
let href = window.URL.createObjectURL(blob)
downloadElement.href = href
downloadElement.download = document.getElementById("luckysheet_info_detail_input").value+".xlsx"; // 文件名字
document.body.appendChild(downloadElement)
downloadElement.click()
document.body.removeChild(downloadElement) // 下载完成移除元素
window.URL.revokeObjectURL(href) // 释放掉blob对象
});
}
var setMerge = function (luckyMerge = {}, worksheet) {
const mergearr = Object.values(luckyMerge)
mergearr.forEach(function (elem) { // elem格式:{r: 0, c: 0, rs: 1, cs: 2}
// 按开始行,开始列,结束行,结束列合并(相当于 K10:M12)
worksheet.mergeCells(elem.r + 1, elem.c + 1, elem.r + elem.rs, elem.c + elem.cs)
})
}
var setBorder = function (luckyBorderInfo, worksheet) {
if (!Array.isArray(luckyBorderInfo)) return
luckyBorderInfo.forEach(function (elem) {
var val=elem.value;
let border = {}
const luckyToExcel = {
type: {
'border-all': 'all',
'border-top': 'top',
'border-right': 'right',
'border-bottom': 'bottom',
'border-left': 'left'
},
style: {
0: 'none',
1: 'thin',
2: 'hair',
3: 'dotted',
4: 'dashDot', // 'Dashed',
5: 'dashDot',
6: 'dashDotDot',
7: 'double',
8: 'medium',
9: 'mediumDashed',
10: 'mediumDashDot',
11: 'mediumDashDotDot',
12: 'slantDashDot',
13: 'thick'
}
}
if(val.t!=undefined){
border['top'] = {style:luckyToExcel.style[val.t.style] , color: val.t.color}
}
if(val.r!=undefined){
border['right'] = {style:luckyToExcel.style[val.r.style] , color: val.r.color}
}
if(val.b!=undefined){
border['bottom'] = {style:luckyToExcel.style[val.b.style] , color: val.b.color}
}
if(val.l!=undefined){
border['left'] = {style:luckyToExcel.style[val.l.style] , color: val.l.color}
}
worksheet.getCell(val.row_index + 1, val.col_index + 1).border = border
})
}
var setStyleAndValue = function (cellArr, worksheet) {
if (!Array.isArray(cellArr)) return
cellArr.forEach(function (row, rowid) {
const dbrow = worksheet.getRow(rowid+1);
dbrow.height=luckysheet.getRowHeight([rowid])[rowid]/1.5;
row.every(function (cell, columnid) {
if (!cell) return true
if(rowid==0){
const dobCol = worksheet.getColumn(columnid+1);
dobCol.width=luckysheet.getColumnWidth([columnid])[columnid]/8;
}
let fill = fillConvert(cell.bg)
let font = fontConvert(cell.ff, cell.fc, cell.bl, cell.it, cell.fs, cell.cl, cell.ul)
let alignment = alignmentConvert(cell.vt, cell.ht, cell.tb, cell.tr)
let value
console.log(JSON.stringify(cell));
var v='';
if(cell.ct.t=='inlineStr'){
var s=cell.ct.s;
s.forEach(function(val,num){
v+=val.v;
})
}else{
v=cell.v;
}
if (cell.f) {
value = { formula: cell.f, result: v }
} else {
value = v
}
let target = worksheet.getCell(rowid + 1, columnid + 1)
target.fill = fill
target.font = font
target.alignment = alignment
target.value = value
return true
})
})
}
var fillConvert = function (bg) {
if (!bg) {
return {
type: 'pattern',
pattern: 'solid',
fgColor:{argb:'#ffffff'.replace('#','')}
}
}
let fill = {
type: 'pattern',
pattern: 'solid',
fgColor: {argb: bg.replace('#', '')}
}
return fill
}
var fontConvert = function (ff = 0, fc = '#000000', bl = 0, it = 0, fs = 10, cl = 0, ul = 0) { // luckysheet:ff(样式), fc(颜色), bl(粗体), it(斜体), fs(大小), cl(删除线), ul(下划线)
const luckyToExcel = {
0: '微软雅黑',
1: '宋体(Song)',
2: '黑体(ST Heiti)',
3: '楷体(ST Kaiti)',
4: '仿宋(ST FangSong)',
5: '新宋体(ST Song)',
6: '华文新魏',
7: '华文行楷',
8: '华文隶书',
9: 'Arial',
10: 'Times New Roman ',
11: 'Tahoma ',
12: 'Verdana',
num2bl: function (num) {
return num === 0 ? false : true
}
}
let font = {
name:ff,
family: 1,
size: fs,
color: {argb: fc.replace('#', '')},
bold: luckyToExcel.num2bl(bl),
italic: luckyToExcel.num2bl(it),
underline: luckyToExcel.num2bl(ul),
strike: luckyToExcel.num2bl(cl)
}
return font
}
var alignmentConvert = function (vt = 'default', ht = 'default', tb = 'default', tr = 'default') { // luckysheet:vt(垂直), ht(水平), tb(换行), tr(旋转)
const luckyToExcel = {
vertical: {
0: 'middle',
1: 'top',
2: 'bottom',
default: 'top'
},
horizontal: {
0: 'center',
1: 'left',
2: 'right',
default: 'left'
},
wrapText: {
0: false,
1: false,
2: true,
default: false
},
textRotation: {
0: 0,
1: 45,
2: -45,
3: 'vertical',
4: 90,
5: -90,
default: 0
}
}
let alignment = {
vertical: luckyToExcel.vertical[vt],
horizontal: luckyToExcel.horizontal[ht],
wrapText: luckyToExcel.wrapText[tb],
textRotation: luckyToExcel.textRotation[tr]
}
return alignment
}
var borderConvert = function (borderType, style = 1, color = '#000') { // 对应luckysheet的config中borderinfo的的参数
if (!borderType) {
return {}
}
const luckyToExcel = {
type: {
'border-all': 'all',
'border-top': 'top',
'border-right': 'right',
'border-bottom': 'bottom',
'border-left': 'left'
},
style: {
0: 'none',
1: 'thin',
2: 'hair',
3: 'dotted',
4: 'dashDot', // 'Dashed',
5: 'dashDot',
6: 'dashDotDot',
7: 'double',
8: 'medium',
9: 'mediumDashed',
10: 'mediumDashDot',
11: 'mediumDashDotDot',
12: 'slantDashDot',
13: 'thick'
}
}
let template = {style: luckyToExcel.style[style], color: {argb: color.replace('#', '')}}
let border = {}
if (luckyToExcel.type[borderType] === 'all') {
border['top'] = template
border['right'] = template
border['bottom'] = template
border['left'] = template
} else {
border[luckyToExcel.type[borderType]] = template
}
return border
}
这样前端导出就实现了,这个协同也可以用,缺点导出有缺失,性能很快,
2.借助javapoi 解析传会前端
function importHandler(){
let upload = document.getElementById("Luckyexcel-demo-file");
// let selectADemo = document.getElementById("Luckyexcel-select-demo");
// let downlodDemo = document.getElementById("Luckyexcel-downlod-file");
// let mask = document.getElementById("lucky-mask-demo");
if(upload){
window.onload = () => {
upload.addEventListener("change", function(evt){
var files = evt.target.files;
if(files==null || files.length==0){
alert("No files wait for import");
return;
}
let name = files[0].name;
let suffixArr = name.split("."), suffix = suffixArr[suffixArr.length-1];
if(suffix!="xlsx"){
alert("Currently only supports the import of xlsx files");
return;
}
LuckyExcel.transformExcelToLucky(files[0], function(exportJson, luckysheetfile){
if(exportJson.sheets==null || exportJson.sheets.length==0){
alert("Failed to read the content of the excel file, currently does not support xls files!");
return;
}
console.log(exportJson, luckysheetfile)
console.log("exportJson");
//ajax 请求 接口 将数据传递给后台 并返回一个id
$.ajax({
type: "POST",
url: "/excel/importFile",
data: {
"exceldata": JSON.stringify(exportJson),
},
dataType: "json",
success: function(data){
console.log(data);
if(data.code==200){
console.log(data);
window.location.href = "/index/"+data.data;
}else{
alert(data.msg);
}
},
error: function(data){
console.log(data.toString());
alert("Failed to import the excel file");
}
});
});
});
}
}
}
importHandler();
调用 /excel/importFile 接口
public class ExportExcelController {
@GetMapping("/export")
public String exportExcel() {
return "export";
}
@PostMapping("/excel/exportFile")
public void downExcelFile(@RequestParam(value = "exceldata") String exceldata, HttpServletRequest request,
HttpServletResponse response) {
// 去除luckysheet中 
的换行
exceldata = exceldata.replace("
", "\\r\\n");
ExcelUtils.exportLuckySheetXlsx(exceldata, request, response);
}
}
借助ExcelUtils.exportLuckySheetXlsx() 导出
3.模板导入模板导出
设想,应该没问题 ,大家可以看这个博客:
https://blog.csdn.net/u014632228/article/details/109738221
4.其实结合以上除了导入需要写到数据库写没啥 ,完全可以用导出来改:
还一个是导入,导出都需要优化我个人觉得 lucksheet 这个json完全可以写一个优化版的全部上传和下载(导出前端可以不考虑)
注:post 请求超过2MB 需要配置下 server: port: 9999 tomcat: # tomcat的URI编码 uri-encoding: UTF-8 # tomcatpost max-http-form-post-size: -1
5、在线导入核心,将前端导入 数据传给后端 解析 创建一个新的excel 将全部数据加载到loadURL 数据源(MongoB)
1.上代码:
@PostMapping("/excel/importFile")
public AjaxResult importExcelFile(@RequestParam(value = "exceldata") String exceldata, HttpServletRequest request,
HttpServletResponse response) throws IOException {
// 去除luckysheet中 
的换行
exceldata = exceldata.replace("
", "\\r\\n");
//实例化一个Gson对象
cn.hutool.json.JSONObject jsonObject = JSONUtil.parseObj(exceldata);
// cn.hutool.json.JSONArray jsonArray = JSONUtil.parseArray(exceldata);
cn.hutool.json.JSONObject info= JSONUtil.parseObj(jsonObject.get("info"));
cn.hutool.json.JSONArray sheetJsonarry= JSONUtil.parseArray(jsonObject.get("sheets"));
WorkBookEntity wb = new WorkBookEntity();
wb.setName(info.get("name").toString().replace(".xlsx", ""));
wb.setOption(SheetUtil.getDefautOption(info));
WorkBookEntity saveWb = workBookRepository.save(wb);
sheetJsonarry.forEach(sheet -> {
WorkSheetEntity ws = new WorkSheetEntity();
ws.setWbId(saveWb.getId());
cn.hutool.json.JSONObject jsonObjects = JSONUtil.parseObj(sheet);
ws.setData(jsonObjects);
ws.setDeleteStatus(0);
workSheetRepository.save(ws);
});
// System.out.println(jsonObject);
//
JSONObject jsonObj = JSONObject.fromObject(exceldata);
//转为相应的实体对象
// JsonRootBean jsonRootBean = gson.fromJson(exceldata, JsonRootBean.class);
//字符串去除 .xlsx
// 将json数据转为jsonArray
//转换为json格式
// WorkBookEntity wb = new WorkBookEntity();
// wb.setName(jsonRootBean.getInfo().getName().replace(".xlsx", ""));
// wb.setOption(SheetUtil.getDefautOption(jsonRootBean));
// WorkBookEntity saveWb = workBookRepository.save(wb);
// //生成sheet数据
// jsonRootBean.getSheets().forEach(sheet -> {
// WorkSheetEntity ws = new WorkSheetEntity();
// ws.setWbId(saveWb.getId());
// cn.hutool.json.JSONObject jsonObject = JSONUtil.parseObj(sheet);
// ws.setData(jsonObject);
// ws.setDeleteStatus(0);
// workSheetRepository.save(ws);
// });
// return new AjaxResult(200, "success", saveWb.getId().toString());
return new AjaxResult(200, "success", saveWb.getId().toString());
}
// @PostMapping("/excel/importFile")
// public JSONObject importExcelFile(@RequestParam(value = "exceldata") String exceldata) {
// Gson gson = new Gson();
// JSONObject jsonObj = JSONObject.fromObject(exceldata);
转为相应的实体对象
//
// JsonRootBean jsonRootBean = gson.fromJson(jsonObj.toString(), JsonRootBean.class);
// //字符串去除 .xlsx
//
//
// // 将json数据转为jsonArray
//
//
// //转换为json格式
// WorkBookEntity wb = new WorkBookEntity();
// wb.setName(jsonRootBean.getInfo().getName().replace(".xlsx", ""));
// wb.setOption(SheetUtil.getDefautOption(jsonRootBean));
// WorkBookEntity saveWb = workBookRepository.save(wb);
// //生成sheet数据
// jsonRootBean.getSheets().forEach(sheet -> {
// WorkSheetEntity ws = new WorkSheetEntity();
// ws.setWbId(saveWb.getId());
// cn.hutool.json.JSONObject jsonObject = JSONUtil.parseObj(sheet);
// ws.setData(jsonObject);
// ws.setDeleteStatus(0);
// workSheetRepository.save(ws);
// });
//
// //写一个json字符串,返回给前端
// JSONObject jsonObject = new JSONObject();
// jsonObject.put("id", saveWb.getId());
//
// return jsonObject;
//
//
//
//
//
注:这里不可以用json实体 实体不全报错还缺少数据
2.代码解析
想象一下导入无非就是创建一个新的模板然后给他附上数据,如果在线的话无非就是修改该空白模板id所对应的data
public void create(HttpServletRequest request, HttpServletResponse response) throws IOException {
WorkBookEntity wb = new WorkBookEntity();
wb.setName("default");
wb.setOption(SheetUtil.getDefautOption());
WorkBookEntity saveWb = workBookRepository.save(wb);
//生成sheet数据
generateSheet(saveWb.getId());
response.sendRedirect("/index/" + saveWb.getId());
}
这是create 方法就是我用的开源库人写的方法 谢谢了啊!
看下好像 其实没啥咋赋值其实 可以先看下这个 getDefautOption()的方法
getDefautOption() {
JSONObject jsonObject = new JSONObject();
jsonObject.put("container", "ecsheet");
jsonObject.put("title", "ecsheet demo");
jsonObject.put("lang", "zh");
jsonObject.put("allowUpdate", true);
jsonObject.put("loadUrl", "");
jsonObject.put("loadSheetUrl", "");
jsonObject.put("updateUrl", "");
return jsonObject;
}
看到没 其实没啥都是默认的,看下他们有数据的数据库 ,
nvacat 看了下数据库两个表 ,workbook 很显然 是个目录表 就是 excel 的id
那么worksheet 就是excel数据
让我们看下 excel的json格式(前端传给接口的exceldata)在这里插入图片描述
可以看出两个 对象 info 和 sheets
那么就把方法改下
public static JSONObject getDefautOption(cn.hutool.json.JSONObject info) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("container", "ecsheet");
jsonObject.put("title", info.get("name").toString().replace(".xlsx", ""));
jsonObject.put("lang", "zh");
jsonObject.put("allowUpdate", true);
jsonObject.put("loadUrl", "");
jsonObject.put("loadSheetUrl", "");
jsonObject.put("updateUrl", "");
return jsonObject;
}
这一步就是创建个空模板然后得到个id
此时给这个id 附上对应数据就行
看下空白模板是生成的死数据
private void generateSheet(String wbId) {
SheetUtil.getDefaultSheetData().forEach(jsonObject -> {
WorkSheetEntity ws = new WorkSheetEntity();
ws.setWbId(wbId);
ws.setData(jsonObject);
ws.setDeleteStatus(0);
workSheetRepository.save(ws);
});
public static List<JSONObject> getDefaultSheetData() {
List<JSONObject> list = new ArrayList<>();
for (int i = 1; i < 4; i++) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("row", 84);
jsonObject.put("column", 60);
jsonObject.put("name", "sheet" + i);
Integer index = i - 1;
jsonObject.put("index", IdUtil.simpleUUID());
jsonObject.put("order", i - 1);
if (i == 1) {
jsonObject.put("status", 1);
} else {
jsonObject.put("status", 0);
}
jsonObject.put("celldata", new ArrayList<JSONObject>() {
});
list.add(jsonObject);
}
return list;
}
很显然这个最后返回的list数据 就是我们的sheets 数据,改写下这不easy解决
```java
sheetJsonarry.forEach(sheet -> {
WorkSheetEntity ws = new WorkSheetEntity();
ws.setWbId(saveWb.getId());
cn.hutool.json.JSONObject jsonObjects = JSONUtil.parseObj(sheet);
ws.setData(jsonObjects);
ws.setDeleteStatus(0);
workSheetRepository.save(ws);
});
# 总结
BUG挺多导入按钮完全可以移到index页 js/sript.js 阻止了lucksheet导成json 目前没有解决办法可以解决 但是 样式丢失index
图表不能用
那个大牛给我加下 谢谢
保存恢复历史解决下
免责声明:本开源项目仅为技术交流此一目的,严禁用于其他任何商业、违法犯罪、恶意攻击等行为;
若第三者用此项目侵犯相关网站权益,一切责任自负;
若本项目侵犯相关网站、个人,组织机构权益,请及时联系作者;
其实大文件上传可以优化一个sheet页上传一个sheet页导入,数据页一页导入然后 合并 在一起 这样使用的人不会有问题 但是正在渲染 怎么解决为动态渲染 这样就没问题了
捐赠:
大家有空多给我的项目改改加点新功能 改好加我qq(1253799421) 我来同步
更多推荐
所有评论(0)