• Luckysheet 实现excel多人在线协同编辑


    前言

            前些天看到Luckysheet支持协同编辑Excel,正符合我们协同项目的一部分,故而想进一步完善协同文章,但是遇到了一下困难,特此做声明哈,若侵权,请联系我删除文章!

            若侵犯版权、个人隐私,请联系删除哈!!!(我可不想踩缝纫机)

            Luckysheet ,一款纯前端类似excel的在线表格,功能强大、配置简单、完全开源。当然,也原生支持协同,下面,我们针对协同部分做详细讲解。官网使用的是Java,也有协同的Demo,我就不说了,下面用 Node 实现协同,完整的样例如下,我们开始吧

    Luckysheet 基础使用

    引入依赖

    CDN

    1. <link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/css/pluginsCss.css' />
    2. <link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/plugins.css' />
    3. <link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/css/luckysheet.css' />
    4. <link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/assets/iconfont/iconfont.css' />
    5. <script src="https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/js/plugin.js">script>
    6. <script src="https://cdn.jsdelivr.net/npm/luckysheet/dist/luckysheet.umd.js">script>

    本地打包

    Luckysheet: 🚀Luckysheet ,一款纯前端类似excel的在线表格,功能强大、配置简单、完全开源。icon-default.png?t=N7T8https://gitee.com/mengshukeji/Luckysheet        官网建议我们在上网址下载完整的包,这样,我们得到的是luckysheet的源码,可以进行二次开发。很重要哈,最后我们也会这样做。

    npm i --s  // 执行 npm 命令,进行依赖包的下载

    npm run build  // 执行打包命令(二次开发是需要修改源码的)

             把dist包放到自己的项目中,我已经更名了哈:

            然后,index.html 直接引入这个地址的文件就行了(二开一定是引这个地址哈)。 

    1. <link rel='stylesheet' href='./luckysheet/dist/plugins/css/pluginsCss.css' />
    2. <link rel='stylesheet' href='./luckysheet/dist/plugins/plugins.css' />
    3. <link rel='stylesheet' href='./luckysheet/dist/css/luckysheet.css' />
    4. <link rel='stylesheet' href='./luckysheet/dist/assets/iconfont/iconfont.css' />
    5. <script src="./luckysheet/dist/plugins/js/plugin.js">script>
    6. <script src="./luckysheet/dist/luckysheet.umd.js">script>

            这个方式建议大家都试试,二次开发一定是这个方式哈!

    npm

            如果大家觉得不用二开,就是用原生的功能 ,那直接使用 npm 下载就行了。

    npm i luckysheet

    1. <link rel='stylesheet' href='./node_modules/luckysheet/dist/plugins/css/pluginsCss.css' />
    2. <link rel='stylesheet' href='./node_modules/luckysheet/dist/plugins/plugins.css' />
    3. <link rel='stylesheet' href='./node_modules/luckysheet/dist/css/luckysheet.css' />
    4. <link rel='stylesheet' href='./node_modules/luckysheet/dist/assets/iconfont/iconfont.css' />
    5. <script src="./node_modules/luckysheet/dist/plugins/js/plugin.js">script>
    6. <script src="./node_modules/luckysheet/dist/luckysheet.umd.js">script>

    初始化

    指定容器

    <div id="luckysheet" style="margin:0px;padding:0px;position:absolute;width:100%;height:100%;left: 0px;top: 0px;">div>
    

    创建表格

    1. onMounted(() => {
    2. // 初始化表格
    3. var options = {
    4. container: "luckysheet", //luckysheet为容器id
    5. };
    6. luckysheet.create(options);
    7. });

             这样就已经是一个完善的表格编辑器了,支持函数、图表、填充等多项功能。

    协同编辑

            因此,我们分别配置这几个参数:

    loadUrl

            配置loadUrl接口地址,加载所有工作表的配置,并包含当前页单元格数据,与loadSheetUrl配合使用。参数为gridKey(表格主键)

    $.post(loadurl, {"gridKey" : server.gridKey}, function (d) {})

            源码写法如上,因此,我们需要创建一个 post请求的地址:

     app.use("/excel", excelRouter); // 添加公共前缀

             配置 loadUrl,加了 baseURL是做了请求代理哈

    1. allowUpdate: true,
    2. loadUrl: "/baseURL/excel",

            接口要求返回以下数据,我们直接复制,然后返回:

    1. "[
    2. //status为1的sheet页,重点是需要提供初始化的数据celldata
    3. {
    4. "name": "Cell",
    5. "index": "sheet_01",
    6. "order": 0,
    7. "status": 1,
    8. "celldata": [{"r":0,"c":0,"v":{"v":1,"m":"1","ct":{"fa":"General","t":"n"}}}]
    9. },
    10. //其他status为0的sheet页,无需提供celldata,只需要配置项即可
    11. {
    12. "name": "Data",
    13. "index": "sheet_02",
    14. "order": 1,
    15. "status": 0
    16. },
    17. {
    18. "name": "Picture",
    19. "index": "sheet_03",
    20. "order": 2,
    21. "status": 0
    22. }
    23. ]"

             本例中,只返回一个sheet表,初始化 0 0 单元格内容为 ‘默认数据’

    1. router.post("/", (req, res, next) => {
    2. // console.log("lucySheet");
    3. let sheetData = [
    4. //status为1的sheet页,重点是需要提供初始化的数据celldata
    5. {
    6. name: "Cell",
    7. index: "sheet_01",
    8. order: 0,
    9. status: 1,
    10. celldata: [
    11. {
    12. r: 0,
    13. c: 0,
    14. v: { v: "默认数据", m: "111", ct: { fa: "General", t: "n" } },
    15. },
    16. ],
    17. },
    18. ];
    19. res.json(JSON.stringify(sheetData));
    20. });

    updateUrl

            操作表格后,实时保存数据的websocket地址,此接口也是共享编辑的接口地址。注意,发送给后端的数据默认是经过pako压缩过后的。后台拿到数据需要先解压。通过共享编辑功能,可以实现Luckysheet实时保存数据和多人同步数据,每一次操作都会发送不同的参数到后台

    因此,我们需要初始化一个 ws 连接:

    1. module.exports = () => {
    2. console.log("等待初始化 WS 服务...");
    3. // 搭建ws服务器
    4. const { WebSocketServer } = require("ws");
    5. const wss = new WebSocketServer({ port: 9000 });
    6. console.log(" WS 服务初始化成功,连接地址:ws://localhost:9000");
    7. wss.on("connection", (ws, req) => {
    8. console.log("用户连接");
    9. });
    10. };

            打开控制台,可以看到连接成功的提示,我们可以一下源码是怎么处理的:

            除了看到输出语句外,我们更应该关注一个 send 事件,因为 websocket 是通过send 发送数据的,还有的是pako.gzip()压缩。因此,服务端监听 message 获取数据:

     至此,我们可以获取一些基础信息:

    1.  每次操作都会发送 send 事件;
    2. 每次发送的数据都经过 pako.gzip 压缩
    3. node 获取的都是 buffer 数据

            也就是这样,我也不知道如何进行下去了,就加了官方的微信,就发生了篇头的那张截图。但是革命还在继续。加了官网微信群,特此感谢【小李飞刀刀】的指导。

    解析Buffer

    1. const pako = require("pako");
    2. /**
    3. * @DESC 导出解压方法
    4. * @param { string } str
    5. * @returns
    6. */
    7. exports.unzip = (str) => {
    8. let chartData = str
    9. .toString()
    10. .split("")
    11. .map((i) => i.charCodeAt(0));
    12. let binData = new Uint8Array(chartData);
    13. let data = pako.inflate(binData);
    14. return decodeURIComponent(
    15. String.fromCharCode.apply(null, new Uint16Array(data))
    16. );
    17. };

            得到上图,就知道该怎么办了吧,映射的是用户的所有操作哈。需要添加用户标记

    1. let id = Math.random().toString().split(".")[1].slice(0, 3);
    2. // 需要添加自定义属性
    3. ws.wid = id;
    4. ws.wname = "user_" + id;

    处理用户光标

            我们一定要看源码是如何处理的哈,官网文档并没有那么详细:

            因此,同步光标的时候,我们应该发送type =3 的数据,我们封装ws的事件响应中心:

    1. // wss.clients 所有的客户端
    2. wss.clients.forEach((conn) => {
    3. // 不发送给自己
    4. if (conn.wid === ws.wid) return;
    5. // 使得 this 指向当前连接对象
    6. wshandle.call(conn, unzip(data));
    7. });

            我们还没做数据同步哈,因此数据没有显示,不影响,先显示用户光标。

    同步数据

    1. /**
    2. * ws 事件响应中心
    3. * 根据不同的事件,返回不同的数据
    4. * type 1 成功/失败
    5. * type 2 更新数据
    6. * type 3 用户光标
    7. * type 4 批量处理数据
    8. */
    9. function wshandle(data) {
    10. // 表示用户移动鼠标 实际是需要根据指令实现不同的响应的,但是这里统一做 更新数据
    11. this.send(callbackdata.call(this, data, JSON.parse(data).t === "mv" ? 3 : 2));
    12. }

            至此,协同好像已经实现了,但是还没完。

    用户退出

            源码中需要返回 {message ,id} 两个数据,因此直接封装 退出函数:

    1. /**
    2. * 用户退出
    3. */
    4. function exit() {
    5. this.send(JSON.stringify({ message: "用户退出", id: this.wid }));
    6. }

            监听ws close 事件:

    1. ws.on("close", (ws) => {
    2. try {
    3. // 实现用户退出
    4. wss.clients.forEach((conn) => {
    5. if (conn.wid === ws.wid) return;
    6. // 使得 this 指向当前连接对象
    7. exit.call(conn);
    8. });
    9. } catch (error) {
    10. console.log(error);
    11. }
    12. });

    BUG修复

            不知道大家发现没有,当多人协作时,我们的用户id 是错的,原因是我们move时,传的参数不对:

    1. // 使得 this 指向当前连接对象 ,并且保证,操作对象始终是当前用户
    2. wshandle.call(conn, { id: ws.wid, name: ws.wname }, unzip(data));
    3. // 表示用户移动鼠标 实际是需要根据指令实现不同的响应的,但是这里统一做 更新数据
    4. // 手动传输 user
    5. this.send(callbackdata(user, data, JSON.parse(data).t === "mv" ? 3 : 2));
    6. // function callback:
    7. return JSON.stringify({
    8. createTime: dayjs().format("YYYYMMHH mm:hh:ss"),
    9. data,
    10. id: user.id,
    11. returnMessage: "success",
    12. status: 0,
    13. type,
    14. username: user.name,
    15. });

    数据库存储

    全量存储

            表格操作完成后,使用luckysheet.getAllSheets()方法获取到全部的工作表数据,全部发送到后台存储。

    协同存储

            协同存储就是用户的每次操作,都会触发 websocket,因此,我们直接在websocket中调用控制层,实现数据的更新,举例说明:

    1. ## Excel 协同数据存储 文件基础信息表 workbooks
    2. - gridKey 唯一key 【主键】
    3. - lang 语言
    4. - column 空表格默认的列数量
    5. - row 空表格默认的行数据量
    6. - fileid 关联的文件 ==> 通过关联获取 luckysheet title 字段
    7. - ... 更多字段,根据项目实际情况添加
    8. ## sheet 信息表 worksheets
    9. - index 工作表索引 【表主键】
    10. - gridKey 【外键】
    11. - name 工作表名称
    12. - status 激活状态
    13. - order 工作表的下标
    14. - hide 是否隐藏
    15. - row 行数
    16. - column 列数
    17. - defaultRowHeight 自定义行高
    18. - defaultColWidth 自定义列宽
    19. ## celldata 数据表 celldatas
    20. - cdid 表主键
    21. - index 【表外键 => 实现关联 worksheets】
    22. - r 行
    23. - c 列
    24. - ctfa 格式名称为自动格式 (要构建成对象)
    25. - ctt 格式类型为数字类型 (要构建成对象)
    26. - v 内容的原始值为
    27. - m 内容的显示值为
    28. - bg 背景为
    29. - ff 字体为
    30. - fc 字体颜色为
    31. - bl 字体加粗
    32. - it 字体斜体
    33. - fs 字体大小为
    34. - cl 启用删除线
    35. - ht 水平居中
    36. - vt 垂直居中
    37. - tr 文字旋转
    38. - tb 文本自动换行
    39. - f 单元格是一个求和公式
    40. - lasteditor 最后编辑者 可以实现单元格的编辑历史追踪

            有了表的基础数据后,我们初始化的时候就可以查询数据库实现表格渲染:

    1. /** 关键函数: fileid 获取初始化 luckysheet方法 */
    2. exports.initLuckysheet = async (req, res, next) => {
    3. let { fileid } = req.query;
    4. if (!fileid) return httpCode(res); // 参数缺失,直接返回
    5. /**
    6. * 根据id查找数据库,组装数据返回
    7. * 刚建的文件,有可能没有,因此需要默认返回数据,并且将这么默认数据作为一条记录
    8. */
    9. // 1. 通过 fileid 查询 gridKey
    10. let bookInfo = await univerImpl.getWorkBookImpl(fileid);
    11. if (!bookInfo.length) return httpCode(res);
    12. let { gridKey } = bookInfo[0];
    13. // 2. 通过 gidKey 查询 sheet 数据 这里有可能有空值
    14. let sheetInfo = await univerImpl.getSheetDataByGridKeyImpl(gridKey);
    15. if (!sheetInfo.length) {
    16. // 如果没有,则创建,并返回空表格 【注意:此处只返回一个 sheet 页,不然浪费空间】
    17. // 创建记录供用户保存
    18. let index = await getNanoid();
    19. await univerImpl.createWorkSheetImpl({
    20. gridKey,
    21. name: "sheet 1",
    22. index, // 工作表索引 【唯一ID】
    23. status: 0, // 激活状态
    24. order: 0, // 工作表的下标
    25. });
    26. return res.json(JSON.stringify([{ name: "sheet 1", celldata: [] }]));
    27. }
    28. // 3. 通过 index 查询 celldata 数据
    29. let { index } = sheetInfo[0];
    30. let cellInfo = await univerImpl.getCellDataByIndexImpl(index);
    31. /** 数据可能是空,也可能是多个,但是注意 ctfa 、ctt 是需要构建成对象返回的,数据库做了扁平化存储 */
    32. if (!cellInfo || !cellInfo.length)
    33. return res.json(JSON.stringify([{ ...sheetInfo[0], celldata: [] }]));
    34. // 4. 组装数据
    35. let celldata = [];
    36. // 注意luckysheet 需要的数据格式
    37. cellInfo.forEach((i) =>
    38. celldata.push({ r: i.r, c: i.c, v: { ...i, ct: { fa: i.ctfa, t: i.ctt } } })
    39. );
    40. // 5. 返回数据
    41. return res.json(JSON.stringify([{ ...sheetInfo[0], celldata }]));
    42. };

             用户每次更新luckysheet,都会触发 ws 事件,实现数据协同存储:

    1. // * v 单个单元格刷新
    2. async function v() {
    3. // 获取cdid
    4. let cdid = await getNanoid();
    5. // 1. 先获取行列信息 当前sheet的index值
    6. let findRes = await univerImpl.findCellDataByRCImpl(this);
    7. // 2. 判断当前行是否已经有数据
    8. findRes.length
    9. ? await univerImpl.updateCellDataImpl(this)
    10. : // 3. 有则更新、没有则新增
    11. await univerImpl.insertCellDataImpl(this, cdid);
    12. }

           以上只是列举思路,完成的代码可以到 git上查看哈,整体的逻辑还是挺多的。 

    文件导入

            两种方式实现哈,先隐藏默认,然后自定定位实现添加按钮,或者根据配置项实现配置

    1. /deep/.luckysheet_info_detail_save,
    2. /deep/.luckysheet_info_detail_update {
    3. display: none;
    4. }

    npm i luckyexcel

             绑定了一个 input ref='importFileRef'

    1. const importFileHandle = (e) => {
    2. let { files } = e.target;
    3. LuckyExcel.transformExcelToLucky(files[0], (exportJson, luckysheetfile) => {
    4. luckysheet.create({
    5. container: "luckysheet", // luckysheet is the container id
    6. data: exportJson.sheets,
    7. title: exportJson.info.name,
    8. userInfo: exportJson.info.name.creator,
    9. });
    10. // 清空
    11. importFileRef.value.value = "";
    12. });
    13. };

             但是这样会丢失协同性:

    1. // 文件导入
    2. const importFileHandle = (e) => {
    3. let { files } = e.target;
    4. LuckyExcel.transformExcelToLucky(files[0], (exportJson, luckysheetfile) => {
    5. // 【会丢失协同性】
    6. // luckysheet.create({
    7. // container: "luckysheet", // luckysheet is the container id
    8. // data: exportJson.sheets,
    9. // title: exportJson.info.name,
    10. // userInfo: exportJson.info.name.creator,
    11. // });
    12. let { info, sheets } = exportJson;
    13. luckysheet.setWorkbookName(info.name);
    14. sheets.forEach((sheet) => {
    15. // sheet 便是每一个 sheet 页,需要根据实际的数量动态创建
    16. luckysheet.setSheetAdd({
    17. sheetObject: sheet,
    18. });
    19. });
    20. // 清空
    21. importFileRef.value.value = "";
    22. });
    23. };

    文件导出

    npm i exceljs file-saver

    1. import Excel from "exceljs";
    2. import FileSaver from "file-saver";
    3. import { ElMessage } from "element-plus";
    4. export const exportExcel = async (name, luckysheet) => {
    5. // 获取 buffer
    6. let buffer = await getBuffer(luckysheet);
    7. download(name, buffer);
    8. };
    9. /**
    10. * 使用 fileSaver 进行文件保存操作
    11. * @param {Buffer} buffer
    12. */
    13. function download(name, buffer) {
    14. try {
    15. const blob = new Blob([buffer], {
    16. type: "application/vnd.ms-excel;charset=utf-8",
    17. });
    18. FileSaver.saveAs(blob, `${name}.xlsx`);
    19. ElMessage.success("文件导出成功");
    20. } catch (error) {
    21. ElMessage.error("文件导出失败");
    22. }
    23. }
    24. /**
    25. *
    26. * @param { Array as luckysheet.getluckysheetfile() } luckysheet
    27. * @returns
    28. */
    29. async function getBuffer(luckysheet) {
    30. // 参数为luckysheet.getluckysheetfile()获取的对象
    31. // 1.创建工作簿,可以为工作簿添加属性
    32. const workbook = new Excel.Workbook();
    33. // 2.创建表格,第二个参数可以配置创建什么样的工作表
    34. luckysheet.every(function (table) {
    35. if (table.data.length === 0) return true;
    36. const worksheet = workbook.addWorksheet(table.name);
    37. // 3.设置单元格合并,设置单元格边框,设置单元格样式,设置值
    38. setStyleAndValue(table.data, worksheet);
    39. setMerge(table.config.merge, worksheet);
    40. setBorder(table.config.borderInfo, worksheet);
    41. return true;
    42. });
    43. // 4.写入 buffer
    44. const buffer = await workbook.xlsx.writeBuffer();
    45. return buffer;
    46. }
    47. var setMerge = function (luckyMerge = {}, worksheet) {
    48. const mergearr = Object.values(luckyMerge);
    49. mergearr.forEach(function (elem) {
    50. // elem格式:{r: 0, c: 0, rs: 1, cs: 2}
    51. // 按开始行,开始列,结束行,结束列合并(相当于 K10:M12)
    52. worksheet.mergeCells(
    53. elem.r + 1,
    54. elem.c + 1,
    55. elem.r + elem.rs,
    56. elem.c + elem.cs
    57. );
    58. });
    59. };
    60. var setBorder = function (luckyBorderInfo, worksheet) {
    61. if (!Array.isArray(luckyBorderInfo)) {
    62. return;
    63. }
    64. // console.log('luckyBorderInfo', luckyBorderInfo)
    65. luckyBorderInfo.forEach(function (elem) {
    66. // 现在只兼容到borderType 为range的情况
    67. // console.log('ele', elem)
    68. if (elem.rangeType === "range") {
    69. let border = borderConvert(elem.borderType, elem.style, elem.color);
    70. let rang = elem.range[0];
    71. // console.log('range', rang)
    72. let row = rang.row;
    73. let column = rang.column;
    74. for (let i = row[0] + 1; i < row[1] + 2; i++) {
    75. for (let y = column[0] + 1; y < column[1] + 2; y++) {
    76. worksheet.getCell(i, y).border = border;
    77. }
    78. }
    79. }
    80. if (elem.rangeType === "cell") {
    81. // col_index: 2
    82. // row_index: 1
    83. // b: {
    84. // color: '#d0d4e3'
    85. // style: 1
    86. // }
    87. const { col_index, row_index } = elem.value;
    88. const borderData = Object.assign({}, elem.value);
    89. delete borderData.col_index;
    90. delete borderData.row_index;
    91. let border = addborderToCell(borderData, row_index, col_index);
    92. // console.log('bordre', border, borderData)
    93. worksheet.getCell(row_index + 1, col_index + 1).border = border;
    94. }
    95. // console.log(rang.column_focus + 1, rang.row_focus + 1)
    96. // worksheet.getCell(rang.row_focus + 1, rang.column_focus + 1).border = border
    97. });
    98. };
    99. var setStyleAndValue = function (cellArr, worksheet) {
    100. if (!Array.isArray(cellArr)) {
    101. return;
    102. }
    103. cellArr.forEach(function (row, rowid) {
    104. // const dbrow = worksheet.getRow(rowid+1);
    105. // //设置单元格行高,默认乘以1.2倍
    106. // dbrow.height=luckysheet.getRowHeight([rowid])[rowid]*1.2;
    107. row.every(function (cell, columnid) {
    108. if (rowid == 0) {
    109. const dobCol = worksheet.getColumn(columnid + 1);
    110. //设置单元格列宽除以8
    111. dobCol.width = luckysheet.getColumnWidth([columnid])[columnid] / 8;
    112. }
    113. if (!cell) {
    114. return true;
    115. }
    116. //设置背景色
    117. let bg = cell.bg || "#FFFFFF"; //默认white
    118. bg = bg === "yellow" ? "FFFF00" : bg.replace("#", "");
    119. let fill = {
    120. type: "pattern",
    121. pattern: "solid",
    122. fgColor: { argb: bg },
    123. };
    124. let font = fontConvert(
    125. cell.ff,
    126. cell.fc,
    127. cell.bl,
    128. cell.it,
    129. cell.fs,
    130. cell.cl,
    131. cell.ul
    132. );
    133. let alignment = alignmentConvert(cell.vt, cell.ht, cell.tb, cell.tr);
    134. let value = "";
    135. if (cell.f) {
    136. value = { formula: cell.f, result: cell.v };
    137. } else if (!cell.v && cell.ct && cell.ct.s) {
    138. // xls转为xlsx之后,内部存在不同的格式,都会进到富文本里,即值不存在与cell.v,而是存在于cell.ct.s之后
    139. // value = cell.ct.s[0].v
    140. cell.ct.s.forEach((arr) => {
    141. value += arr.v;
    142. });
    143. } else {
    144. value = cell.v;
    145. }
    146. // style 填入到_value中可以实现填充色
    147. let letter = createCellPos(columnid);
    148. let target = worksheet.getCell(letter + (rowid + 1));
    149. // console.log('1233', letter + (rowid + 1))
    150. for (const key in fill) {
    151. target.fill = fill;
    152. break;
    153. }
    154. target.font = font;
    155. target.alignment = alignment;
    156. target.value = value;
    157. return true;
    158. });
    159. });
    160. };
    161. var fontConvert = function (
    162. ff = 0,
    163. fc = "#000000",
    164. bl = 0,
    165. it = 0,
    166. fs = 10,
    167. cl = 0,
    168. ul = 0
    169. ) {
    170. // luckysheet:ff(样式), fc(颜色), bl(粗体), it(斜体), fs(大小), cl(删除线), ul(下划线)
    171. const luckyToExcel = {
    172. 0: "微软雅黑",
    173. 1: "宋体(Song)",
    174. 2: "黑体(ST Heiti)",
    175. 3: "楷体(ST Kaiti)",
    176. 4: "仿宋(ST FangSong)",
    177. 5: "新宋体(ST Song)",
    178. 6: "华文新魏",
    179. 7: "华文行楷",
    180. 8: "华文隶书",
    181. 9: "Arial",
    182. 10: "Times New Roman ",
    183. 11: "Tahoma ",
    184. 12: "Verdana",
    185. num2bl: function (num) {
    186. return num === 0 ? false : true;
    187. },
    188. };
    189. // 出现Bug,导入的时候ff为luckyToExcel的val
    190. //设置字体颜色
    191. fc = fc === "red" ? "FFFF0000" : fc.replace("#", "");
    192. let font = {
    193. name: typeof ff === "number" ? luckyToExcel[ff] : ff,
    194. family: 1,
    195. size: fs,
    196. color: { argb: fc },
    197. bold: luckyToExcel.num2bl(bl),
    198. italic: luckyToExcel.num2bl(it),
    199. underline: luckyToExcel.num2bl(ul),
    200. strike: luckyToExcel.num2bl(cl),
    201. };
    202. return font;
    203. };
    204. var alignmentConvert = function (
    205. vt = "default",
    206. ht = "default",
    207. tb = "default",
    208. tr = "default"
    209. ) {
    210. // luckysheet:vt(垂直), ht(水平), tb(换行), tr(旋转)
    211. const luckyToExcel = {
    212. vertical: {
    213. 0: "middle",
    214. 1: "top",
    215. 2: "bottom",
    216. default: "top",
    217. },
    218. horizontal: {
    219. 0: "center",
    220. 1: "left",
    221. 2: "right",
    222. default: "left",
    223. },
    224. wrapText: {
    225. 0: false,
    226. 1: false,
    227. 2: true,
    228. default: false,
    229. },
    230. textRotation: {
    231. 0: 0,
    232. 1: 45,
    233. 2: -45,
    234. 3: "vertical",
    235. 4: 90,
    236. 5: -90,
    237. default: 0,
    238. },
    239. };
    240. let alignment = {
    241. vertical: luckyToExcel.vertical[vt],
    242. horizontal: luckyToExcel.horizontal[ht],
    243. wrapText: luckyToExcel.wrapText[tb],
    244. textRotation: luckyToExcel.textRotation[tr],
    245. };
    246. return alignment;
    247. };
    248. var borderConvert = function (borderType, style = 1, color = "#000") {
    249. // 对应luckysheet的config中borderinfo的的参数
    250. if (!borderType) {
    251. return {};
    252. }
    253. const luckyToExcel = {
    254. type: {
    255. "border-all": "all",
    256. "border-top": "top",
    257. "border-right": "right",
    258. "border-bottom": "bottom",
    259. "border-left": "left",
    260. },
    261. style: {
    262. 0: "none",
    263. 1: "thin",
    264. 2: "hair",
    265. 3: "dotted",
    266. 4: "dashDot", // 'Dashed',
    267. 5: "dashDot",
    268. 6: "dashDotDot",
    269. 7: "double",
    270. 8: "medium",
    271. 9: "mediumDashed",
    272. 10: "mediumDashDot",
    273. 11: "mediumDashDotDot",
    274. 12: "slantDashDot",
    275. 13: "thick",
    276. },
    277. };
    278. let template = {
    279. style: luckyToExcel.style[style],
    280. color: { argb: color.replace("#", "") },
    281. };
    282. let border = {};
    283. if (luckyToExcel.type[borderType] === "all") {
    284. border["top"] = template;
    285. border["right"] = template;
    286. border["bottom"] = template;
    287. border["left"] = template;
    288. } else {
    289. border[luckyToExcel.type[borderType]] = template;
    290. }
    291. // console.log('border', border)
    292. return border;
    293. };
    294. function addborderToCell(borders, row_index, col_index) {
    295. let border = {};
    296. const luckyExcel = {
    297. type: {
    298. l: "left",
    299. r: "right",
    300. b: "bottom",
    301. t: "top",
    302. },
    303. style: {
    304. 0: "none",
    305. 1: "thin",
    306. 2: "hair",
    307. 3: "dotted",
    308. 4: "dashDot", // 'Dashed',
    309. 5: "dashDot",
    310. 6: "dashDotDot",
    311. 7: "double",
    312. 8: "medium",
    313. 9: "mediumDashed",
    314. 10: "mediumDashDot",
    315. 11: "mediumDashDotDot",
    316. 12: "slantDashDot",
    317. 13: "thick",
    318. },
    319. };
    320. // console.log('borders', borders)
    321. for (const bor in borders) {
    322. // console.log(bor)
    323. if (borders[bor].color.indexOf("rgb") === -1) {
    324. border[luckyExcel.type[bor]] = {
    325. style: luckyExcel.style[borders[bor].style],
    326. color: { argb: borders[bor].color.replace("#", "") },
    327. };
    328. } else {
    329. border[luckyExcel.type[bor]] = {
    330. style: luckyExcel.style[borders[bor].style],
    331. color: { argb: borders[bor].color },
    332. };
    333. }
    334. }
    335. return border;
    336. }
    337. function createCellPos(n) {
    338. let ordA = "A".charCodeAt(0);
    339. let ordZ = "Z".charCodeAt(0);
    340. let len = ordZ - ordA + 1;
    341. let s = "";
    342. while (n >= 0) {
    343. s = String.fromCharCode((n % len) + ordA) + s;
    344. n = Math.floor(n / len) - 1;
    345. }
    346. return s;
    347. }

    关联文件

           在excel协同的时候,还需要跟我们quill编辑器类似,绑定fileid:

    updateUrl:

          "ws://localhost:9000?fileid=" + router.currentRoute.value.params.fileid, // 实现传参,

            二开实现websocket的关闭连接:

    1. // 源码中 server.js 添加方法
    2. closeWebSocket: function () {
    3. let _this = this;
    4. if ("WebSocket" in window) {
    5. _this.websocket.close();
    6. } else console.error("## closeWebSocket", locale().websocket.support);
    7. },
    8. global.api(api.js 文件)
    9. /**
    10. * 导出 websocket 的关闭方法:
    11. * luckysheet.wsclose() 进行调用
    12. */
    13. export function wsclose() {
    14. console.log('调用自定义方法 server.closeWebSocket()')
    15. server.closeWebSocket();
    16. }

            重新打包,在需要的地方进行调用:

    但是每次关闭连接后,都会alert,把这个关了:

            与文件关联后,不是同一个文件的不能协同编辑。

    总结

            到此,功能都已经开发完了。还是那句话哈:

            如果侵权了,请联系删除!

            如果侵权了,请联系删除!

            如果侵权了,请联系删除!

            对luckysheet的协同做一下总结吧:

    1. 对pako压缩数据进行解析,这是第一个难点;
    2. 数据存储按照分布式存储会更快;这里是结合着 loadUrl的哈,后端返回保存后的数据进行渲染;
    3. luckyexcel 进行文件导入;
    4. exceljs file-saver 实现文件导出;
    5. 对源码进行二次开发,实现手动关闭 websocket 连接;
    6. 还有很多细节哈,大家根据需要可以自行定义,有问题欢迎留言讨论。

    制作不易,点赞收藏~

  • 相关阅读:
    JAVA中小型医院信息管理系统源码 医院系统源码
    农产品果蔬商城交易系统(Java+Web+MySQL)
    处理csv、bmp等常用数据分析操作--python
    纯钧(ChunJun,原名FlinkX)框架学习
    13.1.资源清单的管理工具-helm
    Java---数据库---MyBatisPlus
    【ECMAScript6】代理与反射
    最新ChatGPT商业运营系统源码+支持GPT4/支持ai绘画+支持Midjourney绘画
    JavaWeb开发之——MySQL数据模型(04)
    计算机二级备考:Word 部分_1文件操作
  • 原文地址:https://blog.csdn.net/weixin_47746452/article/details/134240905