宁波网站建设最好,物流网站建设图片,常见网站结构,创办一个网站的费用前言 前些天看到Luckysheet支持协同编辑Excel#xff0c;正符合我们协同项目的一部分#xff0c;故而想进一步完善协同文章#xff0c;但是遇到了一下困难#xff0c;特此做声明哈#xff0c;若侵权#xff0c;请联系我删除文章#xff01; 若侵犯版权、个人隐私#x…前言 前些天看到Luckysheet支持协同编辑Excel正符合我们协同项目的一部分故而想进一步完善协同文章但是遇到了一下困难特此做声明哈若侵权请联系我删除文章 若侵犯版权、个人隐私请联系删除哈我可不想踩缝纫机 Luckysheet 一款纯前端类似excel的在线表格功能强大、配置简单、完全开源。当然也原生支持协同下面我们针对协同部分做详细讲解。官网使用的是Java也有协同的Demo我就不说了下面用 Node 实现协同完整的样例如下我们开始吧 Luckysheet 基础使用
引入依赖
CDN
link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/css/pluginsCss.css /
link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/plugins.css /
link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/luckysheet/dist/css/luckysheet.css /
link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/luckysheet/dist/assets/iconfont/iconfont.css /
script srchttps://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/js/plugin.js/script
script srchttps://cdn.jsdelivr.net/npm/luckysheet/dist/luckysheet.umd.js/script本地打包
Luckysheet: Luckysheet 一款纯前端类似excel的在线表格功能强大、配置简单、完全开源。https://gitee.com/mengshukeji/Luckysheet 官网建议我们在上网址下载完整的包这样我们得到的是luckysheet的源码可以进行二次开发。很重要哈最后我们也会这样做。 npm i --s // 执行 npm 命令进行依赖包的下载 npm run build // 执行打包命令二次开发是需要修改源码的 把dist包放到自己的项目中我已经更名了哈 然后index.html 直接引入这个地址的文件就行了二开一定是引这个地址哈。 !-- 引入 luck Sheet 二次开发地址 就是你刚才 build 的那个 dist 包 --link relstylesheet href./luckysheet/dist/plugins/css/pluginsCss.css /link relstylesheet href./luckysheet/dist/plugins/plugins.css /link relstylesheet href./luckysheet/dist/css/luckysheet.css /link relstylesheet href./luckysheet/dist/assets/iconfont/iconfont.css /script src./luckysheet/dist/plugins/js/plugin.js/scriptscript src./luckysheet/dist/luckysheet.umd.js/script 这个方式建议大家都试试二次开发一定是这个方式哈
npm 如果大家觉得不用二开就是用原生的功能 那直接使用 npm 下载就行了。 npm i luckysheet link relstylesheet href./node_modules/luckysheet/dist/plugins/css/pluginsCss.css /link relstylesheet href./node_modules/luckysheet/dist/plugins/plugins.css /link relstylesheet href./node_modules/luckysheet/dist/css/luckysheet.css /link relstylesheet href./node_modules/luckysheet/dist/assets/iconfont/iconfont.css /script src./node_modules/luckysheet/dist/plugins/js/plugin.js/scriptscript src./node_modules/luckysheet/dist/luckysheet.umd.js/script
初始化
指定容器
div idluckysheet stylemargin:0px;padding:0px;position:absolute;width:100%;height:100%;left: 0px;top: 0px;/div创建表格
onMounted(() {// 初始化表格var options {container: luckysheet, //luckysheet为容器id};luckysheet.create(options);
}); 这样就已经是一个完善的表格编辑器了支持函数、图表、填充等多项功能。
协同编辑 因此我们分别配置这几个参数
loadUrl 配置loadUrl接口地址加载所有工作表的配置并包含当前页单元格数据与loadSheetUrl配合使用。参数为gridKey表格主键
$.post(loadurl, {gridKey : server.gridKey}, function (d) {}) 源码写法如上因此我们需要创建一个 post请求的地址 app.use(/excel, excelRouter); // 添加公共前缀 配置 loadUrl加了 baseURL是做了请求代理哈 allowUpdate: true,loadUrl: /baseURL/excel, 接口要求返回以下数据我们直接复制然后返回
[ //status为1的sheet页重点是需要提供初始化的数据celldata{name: Cell,index: sheet_01,order: 0,status: 1,celldata: [{r:0,c:0,v:{v:1,m:1,ct:{fa:General,t:n}}}]},//其他status为0的sheet页无需提供celldata只需要配置项即可{name: Data,index: sheet_02,order: 1,status: 0},{name: Picture,index: sheet_03,order: 2,status: 0}
]本例中只返回一个sheet表初始化 0 0 单元格内容为 ‘默认数据’
router.post(/, (req, res, next) {// console.log(lucySheet);let sheetData [//status为1的sheet页重点是需要提供初始化的数据celldata{name: Cell,index: sheet_01,order: 0,status: 1,celldata: [{r: 0,c: 0,v: { v: 默认数据, m: 111, ct: { fa: General, t: n } },},],},];res.json(JSON.stringify(sheetData));
}); updateUrl 操作表格后实时保存数据的websocket地址此接口也是共享编辑的接口地址。注意发送给后端的数据默认是经过pako压缩过后的。后台拿到数据需要先解压。通过共享编辑功能可以实现Luckysheet实时保存数据和多人同步数据每一次操作都会发送不同的参数到后台
因此我们需要初始化一个 ws 连接
module.exports () {console.log(等待初始化 WS 服务...);// 搭建ws服务器const { WebSocketServer } require(ws);const wss new WebSocketServer({ port: 9000 });console.log( WS 服务初始化成功连接地址ws://localhost:9000);wss.on(connection, (ws, req) {console.log(用户连接);});
};打开控制台可以看到连接成功的提示我们可以一下源码是怎么处理的 除了看到输出语句外我们更应该关注一个 send 事件因为 websocket 是通过send 发送数据的还有的是pako.gzip()压缩。因此服务端监听 message 获取数据 至此我们可以获取一些基础信息 每次操作都会发送 send 事件每次发送的数据都经过 pako.gzip 压缩node 获取的都是 buffer 数据 也就是这样我也不知道如何进行下去了就加了官方的微信就发生了篇头的那张截图。但是革命还在继续。加了官网微信群特此感谢【小李飞刀刀】的指导。 解析Buffer
const pako require(pako);/*** DESC 导出解压方法* param { string } str* returns*/
exports.unzip (str) {let chartData str.toString().split().map((i) i.charCodeAt(0));let binData new Uint8Array(chartData);let data pako.inflate(binData);return decodeURIComponent(String.fromCharCode.apply(null, new Uint16Array(data)));
}; 得到上图就知道该怎么办了吧映射的是用户的所有操作哈。需要添加用户标记 let id Math.random().toString().split(.)[1].slice(0, 3);// 需要添加自定义属性ws.wid id;ws.wname user_ id;
处理用户光标 我们一定要看源码是如何处理的哈官网文档并没有那么详细 因此同步光标的时候我们应该发送type 3 的数据我们封装ws的事件响应中心
// wss.clients 所有的客户端
wss.clients.forEach((conn) {// 不发送给自己if (conn.wid ws.wid) return;// 使得 this 指向当前连接对象wshandle.call(conn, unzip(data));
}); 我们还没做数据同步哈因此数据没有显示不影响先显示用户光标。
同步数据
/*** ws 事件响应中心* 根据不同的事件返回不同的数据* type 1 成功/失败* type 2 更新数据* type 3 用户光标* type 4 批量处理数据*/
function wshandle(data) {// 表示用户移动鼠标 实际是需要根据指令实现不同的响应的但是这里统一做 更新数据this.send(callbackdata.call(this, data, JSON.parse(data).t mv ? 3 : 2));
} 至此协同好像已经实现了但是还没完。
用户退出 源码中需要返回 {message ,id} 两个数据因此直接封装 退出函数
/*** 用户退出*/
function exit() {this.send(JSON.stringify({ message: 用户退出, id: this.wid }));
} 监听ws close 事件 ws.on(close, (ws) {try {// 实现用户退出wss.clients.forEach((conn) {if (conn.wid ws.wid) return;// 使得 this 指向当前连接对象exit.call(conn);});} catch (error) {console.log(error);}}); BUG修复 不知道大家发现没有当多人协作时我们的用户id 是错的原因是我们move时传的参数不对 // 使得 this 指向当前连接对象 ,并且保证操作对象始终是当前用户
wshandle.call(conn, { id: ws.wid, name: ws.wname }, unzip(data));// 表示用户移动鼠标 实际是需要根据指令实现不同的响应的但是这里统一做 更新数据
// 手动传输 user
this.send(callbackdata(user, data, JSON.parse(data).t mv ? 3 : 2));// function callback:return JSON.stringify({createTime: dayjs().format(YYYYMMHH mm:hh:ss),data,id: user.id,returnMessage: success,status: 0,type,username: user.name,});
数据库存储
全量存储 表格操作完成后使用luckysheet.getAllSheets()方法获取到全部的工作表数据全部发送到后台存储。 协同存储 协同存储就是用户的每次操作都会触发 websocket因此我们直接在websocket中调用控制层实现数据的更新举例说明
[{data:[], // 每个工作表参数组成的一维数组name: Cell, //工作表名称color: , //工作表颜色index: 0, //工作表索引status: 1, //激活状态order: 0, //工作表的下标hide: 0,//是否隐藏row: 36, //行数column: 18, //列数defaultRowHeight: 19, //自定义行高defaultColWidth: 73, //自定义列宽celldata: [], //初始化使用的单元格数据config: {merge:{}, //合并单元格rowlen:{}, //表格行高columnlen:{}, //表格列宽rowhidden:{}, //隐藏行colhidden:{}, //隐藏列borderInfo:{}, //边框authority:{}, //工作表保护},scrollLeft: 0, //左右滚动条位置scrollTop: 315, //上下滚动条位置luckysheet_select_save: [], //选中的区域calcChain: [],//公式链isPivotTable:false,//是否数据透视表pivotTable:{},//数据透视表设置filter_select: {},//筛选范围filter: null,//筛选配置luckysheet_alternateformat_save: [], //交替颜色luckysheet_alternateformat_save_modelCustom: [], //自定义交替颜色 luckysheet_conditionformat_save: {},//条件格式frozen: {}, //冻结行列配置chart: [], //图表配置zoomRatio:1, // 缩放比例image:[], //图片showGridLines: 1, //是否显示网格线dataVerification:{} //数据验证配置},// ... 其他 sheet 页数据与上类似
]上是整个sheet的配置项数据库表可以根据这个来构建数据表单独分开、样式表也单独分开还有基础配置表: 这样就不用存储很多无效的数据能实现对某一条数据的精确控制与存储节省数据库存储空间。
文件导入 两种方式实现哈先隐藏默认然后自定定位实现添加按钮或者根据配置项实现配置
/deep/.luckysheet_info_detail_save,
/deep/.luckysheet_info_detail_update {display: none;
} npm i luckyexcel 绑定了一个 input refimportFileRef
const importFileHandle (e) {let { files } e.target;LuckyExcel.transformExcelToLucky(files[0], (exportJson, luckysheetfile) {luckysheet.create({container: luckysheet, // luckysheet is the container iddata: exportJson.sheets,title: exportJson.info.name,userInfo: exportJson.info.name.creator,});// 清空importFileRef.value.value ;});
}; 但是这样会丢失协同性
// 文件导入
const importFileHandle (e) {let { files } e.target;LuckyExcel.transformExcelToLucky(files[0], (exportJson, luckysheetfile) {// 【会丢失协同性】// luckysheet.create({// container: luckysheet, // luckysheet is the container id// data: exportJson.sheets,// title: exportJson.info.name,// userInfo: exportJson.info.name.creator,// });let { info, sheets } exportJson;luckysheet.setWorkbookName(info.name);sheets.forEach((sheet) {// sheet 便是每一个 sheet 页需要根据实际的数量动态创建luckysheet.setSheetAdd({sheetObject: sheet,});});// 清空importFileRef.value.value ;});
}; 文件导出 npm i exceljs file-saver import Excel from exceljs;import FileSaver from file-saver;import { ElMessage } from element-plus;export const exportExcel async (name, luckysheet) {// 获取 bufferlet buffer await getBuffer(luckysheet);download(name, buffer);
};/*** 使用 fileSaver 进行文件保存操作* param {Buffer} buffer*/
function download(name, buffer) {try {const blob new Blob([buffer], {type: application/vnd.ms-excel;charsetutf-8,});FileSaver.saveAs(blob, ${name}.xlsx);ElMessage.success(文件导出成功);} catch (error) {ElMessage.error(文件导出失败);}
}/**** param { Array as luckysheet.getluckysheetfile() } luckysheet* returns*/
async function getBuffer(luckysheet) {// 参数为luckysheet.getluckysheetfile()获取的对象// 1.创建工作簿可以为工作簿添加属性const workbook new Excel.Workbook();// 2.创建表格第二个参数可以配置创建什么样的工作表luckysheet.every(function (table) {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);return true;});// 4.写入 bufferconst buffer await workbook.xlsx.writeBuffer();return buffer;
}var setMerge function (luckyMerge {}, worksheet) {const mergearr Object.values(luckyMerge);mergearr.forEach(function (elem) {// elem格式{r: 0, c: 0, rs: 1, cs: 2}// 按开始行开始列结束行结束列合并相当于 K10:M12worksheet.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;}// console.log(luckyBorderInfo, luckyBorderInfo)luckyBorderInfo.forEach(function (elem) {// 现在只兼容到borderType 为range的情况// console.log(ele, elem)if (elem.rangeType range) {let border borderConvert(elem.borderType, elem.style, elem.color);let rang elem.range[0];// console.log(range, rang)let row rang.row;let column rang.column;for (let i row[0] 1; i row[1] 2; i) {for (let y column[0] 1; y column[1] 2; y) {worksheet.getCell(i, y).border border;}}}if (elem.rangeType cell) {// col_index: 2// row_index: 1// b: {// color: #d0d4e3// style: 1// }const { col_index, row_index } elem.value;const borderData Object.assign({}, elem.value);delete borderData.col_index;delete borderData.row_index;let border addborderToCell(borderData, row_index, col_index);// console.log(bordre, border, borderData)worksheet.getCell(row_index 1, col_index 1).border border;}// console.log(rang.column_focus 1, rang.row_focus 1)// worksheet.getCell(rang.row_focus 1, rang.column_focus 1).border border});
};
var setStyleAndValue function (cellArr, worksheet) {if (!Array.isArray(cellArr)) {return;}cellArr.forEach(function (row, rowid) {// const dbrow worksheet.getRow(rowid1);// //设置单元格行高,默认乘以1.2倍// dbrow.heightluckysheet.getRowHeight([rowid])[rowid]*1.2;row.every(function (cell, columnid) {if (rowid 0) {const dobCol worksheet.getColumn(columnid 1);//设置单元格列宽除以8dobCol.width luckysheet.getColumnWidth([columnid])[columnid] / 8;}if (!cell) {return true;}//设置背景色let bg cell.bg || #FFFFFF; //默认whitebg bg yellow ? FFFF00 : bg.replace(#, );let fill {type: pattern,pattern: solid,fgColor: { argb: 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 ;if (cell.f) {value { formula: cell.f, result: cell.v };} else if (!cell.v cell.ct cell.ct.s) {// xls转为xlsx之后内部存在不同的格式都会进到富文本里即值不存在与cell.v而是存在于cell.ct.s之后// value cell.ct.s[0].vcell.ct.s.forEach((arr) {value arr.v;});} else {value cell.v;}// style 填入到_value中可以实现填充色let letter createCellPos(columnid);let target worksheet.getCell(letter (rowid 1));// console.log(1233, letter (rowid 1))for (const key in fill) {target.fill fill;break;}target.font font;target.alignment alignment;target.value value;return true;});});
};var fontConvert function (ff 0,fc #000000,bl 0,it 0,fs 10,cl 0,ul 0
) {// luckysheetff(样式), 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;},};// 出现Bug导入的时候ff为luckyToExcel的val//设置字体颜色fc fc red ? FFFF0000 : fc.replace(#, );let font {name: typeof ff number ? luckyToExcel[ff] : ff,family: 1,size: fs,color: { argb: fc },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;}// console.log(border, border)return border;
};function addborderToCell(borders, row_index, col_index) {let border {};const luckyExcel {type: {l: left,r: right,b: bottom,t: top,},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,},};// console.log(borders, borders)for (const bor in borders) {// console.log(bor)if (borders[bor].color.indexOf(rgb) -1) {border[luckyExcel.type[bor]] {style: luckyExcel.style[borders[bor].style],color: { argb: borders[bor].color.replace(#, ) },};} else {border[luckyExcel.type[bor]] {style: luckyExcel.style[borders[bor].style],color: { argb: borders[bor].color },};}}return border;
}function createCellPos(n) {let ordA A.charCodeAt(0);let ordZ Z.charCodeAt(0);let len ordZ - ordA 1;let s ;while (n 0) {s String.fromCharCode((n % len) ordA) s;n Math.floor(n / len) - 1;}return s;
}关联文件 在excel协同的时候还需要跟我们quill编辑器类似绑定fileid updateUrl: ws://localhost:9000?fileid router.currentRoute.value.params.fileid, // 实现传参, 二开实现websocket的关闭连接
// 源码中 server.js 添加方法
closeWebSocket: function () {let _this this;if (WebSocket in window) {_this.websocket.close();} else console.error(## closeWebSocket, locale().websocket.support);},global.api(api.js 文件)
/*** 导出 websocket 的关闭方法* luckysheet.wsclose() 进行调用*/
export function wsclose() {console.log(调用自定义方法 server.closeWebSocket())server.closeWebSocket();
} 重新打包在需要的地方进行调用 但是每次关闭连接后都会alert把这个关了 与文件关联后不是同一个文件的不能协同编辑。
总结 到此功能都已经开发完了。还是那句话哈 如果侵权了请联系删除 如果侵权了请联系删除 如果侵权了请联系删除 对luckysheet的协同做一下总结吧
对pako压缩数据进行解析这是第一个难点数据存储按照分布式存储会更快这里是结合着 loadUrl的哈后端返回保存后的数据进行渲染luckyexcel 进行文件导入exceljs file-saver 实现文件导出对源码进行二次开发实现手动关闭 websocket 连接还有很多细节哈大家根据需要可以自行定义有问题欢迎留言讨论。
制作不易点赞收藏~