当前位置: 首页 > news >正文

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

前言

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

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

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

Luckysheet 基础使用

引入依赖

CDN

<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/css/pluginsCss.css' />
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/plugins.css' />
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/css/luckysheet.css' />
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/assets/iconfont/iconfont.css' />
<script src="https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/js/plugin.js"></script>
<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 直接引入这个地址的文件就行了(二开一定是引这个地址哈)。 

     <!-- 引入 luck Sheet 二次开发地址  就是你刚才 build 的那个 dist 包 --><link rel='stylesheet' href='./luckysheet/dist/plugins/css/pluginsCss.css' /><link rel='stylesheet' href='./luckysheet/dist/plugins/plugins.css' /><link rel='stylesheet' href='./luckysheet/dist/css/luckysheet.css' /><link rel='stylesheet' href='./luckysheet/dist/assets/iconfont/iconfont.css' /><script src="./luckysheet/dist/plugins/js/plugin.js"></script><script src="./luckysheet/dist/luckysheet.umd.js"></script>

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

npm

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

npm i luckysheet

    <link rel='stylesheet' href='./node_modules/luckysheet/dist/plugins/css/pluginsCss.css' /><link rel='stylesheet' href='./node_modules/luckysheet/dist/plugins/plugins.css' /><link rel='stylesheet' href='./node_modules/luckysheet/dist/css/luckysheet.css' /><link rel='stylesheet' href='./node_modules/luckysheet/dist/assets/iconfont/iconfont.css' /><script src="./node_modules/luckysheet/dist/plugins/js/plugin.js"></script><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>

创建表格

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 获取数据:

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

  1.  每次操作都会发送 send 事件;
  2. 每次发送的数据都经过 pako.gzip 压缩
  3. 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 ref='importFileRef'

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;charset=utf-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: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;}// 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(rowid+1);// //设置单元格行高,默认乘以1.2倍// dbrow.height=luckysheet.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
) {// 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;},};// 出现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的协同做一下总结吧:

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

制作不易,点赞收藏~

相关文章:

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

前言 前些天看到Luckysheet支持协同编辑Excel&#xff0c;正符合我们协同项目的一部分&#xff0c;故而想进一步完善协同文章&#xff0c;但是遇到了一下困难&#xff0c;特此做声明哈&#xff0c;若侵权&#xff0c;请联系我删除文章&#xff01; 若侵犯版权、个人隐私&#x…...

C++线程库的基本使用(初级)

#include<iostream> #include<thread> #include<string> void printHelloWorld(std::string msg) {std::cout << msg<< std::endl;return; } int main() {std::thread threadl(printHelloWorld,"Hello Thread");//第一个参数是函数名&…...

2023最新版JavaSE教程——第1天:Java语言概述

目录 一、抽丝剥茧话Java1.1 当前大学生就业形势1.2 IT互联网是否依旧靠谱1.3 IT行业岗位分析1.4 软件开发之Java开发1.5 到底多少人在用Java 二、计算机的硬件与软件2.1 计算机组成&#xff1a;硬件软件2.2 CPU、内存与硬盘2.3 输入设备&#xff1a;键盘输入 三、软件相关介绍…...

PTL货位指引标签为仓储管理打开新思路

PTL货位指引标签是一种新型的仓储管理技术&#xff0c;它通过LED灯光指引和数字显示&#xff0c;为仓库管理带来了全新的管理思路和效率提升&#xff0c;成为现代物流仓库管理中的重要工具。 首先&#xff0c;PTL货位指引标签为仓储管理业务带来了管理新思路。传统的仓库管理中…...

IDEA版SSM入门到实战(Maven+MyBatis+Spring+SpringMVC) -Maven核心概念

一.Maven的POM POM全称&#xff1a;Project Object Model【项目对象模型】&#xff0c;将项目封装为对象模型&#xff0c;便于使用Maven管理【构建】项目 pom.xml常用标签 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://m…...

Unity的粒子总是丢材质

1&#xff09;Unity的粒子总是丢材质 2&#xff09;C#传给C的Byte数组如何释放 3&#xff09;CommandBuffer.DrawProcedural在手机上为什么不生效 4&#xff09;游戏加载场景碰撞&#xff0c;会弹出显卡报错&#xff0c;驱动程序超时 这是第359篇UWA技术知识分享的推送&#xf…...

P5906 【模板】回滚莫队不删除莫队

这一题&#xff0c;虽说在洛谷标的是模板题&#xff0c;但可能没有“历史研究”那一题更加模板。 这一题相对于回滚莫队的模板题&#xff0c;可能在回滚的处理上稍微复杂了一点。对于回滚莫队就不多解释了&#xff0c;可以看一下 回滚莫队模板题 这一篇博客&#xff0c;稍微简单…...

1. Collection,List, Map, Queue

1. java集合框架体系结构图 2. Collection派生的子接口 其中最重要的子接口是&#xff1a; 1&#xff09;List 表示有序可重复列表&#xff0c;重要的实现类有&#xff1a;ArrayList, LinkedList ArrayList特点&#xff1a;底层数组实现&#xff0c;随机查找快&#xff0c;增删…...

rabbitmq 交换机相关实例代码

1.扇形交换机 定义扇形交换机和队列 package com.macro.mall.portal.config;import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.FanoutExchange; import org.springframework.amqp.…...

第四章IDEA操作Maven

文章目录 创建父工程开启自动导入配置Maven信息创建Java模块工程创建 Web 模块工程 在IDEA中执行Maven命令直接执行手动输入 在IDEA中查看某个模块的依赖信息工程导入来自版本控制系统来自工程目录 模块导入情景重现导入 Java 类型模块 导入 Web 类型模块 创建父工程 开启自动导…...

Go语言函数签名和匿名函数

函数签名 函数类型又叫做函数签名&#xff0c;一个函数的类型就是函数定义首行去掉函数名、参数名和{}&#xff0c;可以用fmt.Printf的“%T”格式化参数打印函数的类型。 两个函数类型相同的条件是&#xff1a;拥有相同的形参列表和返回值列表&#xff0c;形参名可以不同。 ty…...

Pytest系列(16)- 分布式测试插件之pytest-xdist的详细使用

前言 平常我们功能测试用例非常多时&#xff0c;比如有1千条用例&#xff0c;假设每个用例执行需要1分钟&#xff0c;如果单个测试人员执行需要1000分钟才能跑完当项目非常紧急时&#xff0c;会需要协调多个测试资源来把任务分成两部分&#xff0c;于是执行时间缩短一半&#…...

基于JavaWeb的网上销售系统设计与实现

项目描述 临近学期结束&#xff0c;还是毕业设计&#xff0c;你还在做java程序网络编程&#xff0c;期末作业&#xff0c;老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据疫情当下&#xff0c;你想解决的问…...

wpf添加Halcon的窗口控件报错:下列控件已成功添加到工具箱中,但未在活动设计器中启用

报错截图如下&#xff1a; 注意一下新建工程的时候选择wpf应用而不是wpf应用程序。 添加成功的控件&#xff1a;...

antv/x6 自定义html节点并且支持动态更新节点内容

antv/x6 自定义html节点 效果图定义一个连接桩公共方法注册图形节点创建html节点动态更新节点内容 效果图 定义一个连接桩公共方法 const ports {groups: {top: {position: top,attrs: {circle: {r: 4,magnet: true,stroke: #cf1322,strokeWidth: 1,fill: #fff,style: {visib…...

设计模式之命令模式

阅读建议 嗨&#xff0c;伙计&#xff01;刷到这篇文章咱们就是有缘人&#xff0c;在阅读这篇文章前我有一些建议&#xff1a; 本篇文章大概4000多字&#xff0c;阅读时间长可能需要4-5分钟&#xff0c;请结合示例耐心读完&#xff0c;绝对有收获。设计模式属于程序的设计思…...

Linux学习笔记--高级

Shell概述 1&#xff0c;shell概述 是一个c语言编写的脚本语言&#xff0c;是linux和用户的桥梁&#xff0c;用户输入命令交给shell处理。shell&#xff0c;将相应的操作传递给内核&#xff08;kernel&#xff09;&#xff0c;内核把处理的结果输出给用户 1.1Shell解释器有哪…...

在Java中操作Redis

Redis中如何的去存放一个Java对象&#xff1f; 直接存放Json类型即可&#xff0c;因为我们Json类型最终就是一个String类型。 Redis的Java客户端 Redis的常用命令是我们操作Redis的基础&#xff0c;那么我们在Java程序当中如何来操作Redis呢&#xff1f; 要想基于Java语言…...

【服务器】fiber协程模块

fiber协程模块 以下是从sylar服务器中学的&#xff0c;对其的复习&#xff1b; sylar的fiber协程模块是基于ucontext_t实现非对称协程 函数只有两个行为&#xff1a;调用与返回。一旦函数返回后&#xff0c;它在栈上所拥有的状态将被销毁。协程相比函数多了两个动作&#xf…...

SparkML

SparkML SparkML_lr_train &#xff1a;读取py处理后的train表用于训练&#xff0c;将训练模型保存好。 SparkML_lr_predict &#xff1a;读取训练好的模型&#xff0c;读取py处理后的test表用于预测。将预测结果写入normal_data中&#xff0c;根据id修改stream_is_normal的值。…...

实时定位与路径优化:跑腿App系统开发中的地理信息技术

本文将介绍如何使用地理信息技术实现实时定位和路径优化功能&#xff0c;以提高跑腿服务的效率。 实时定位 用户位置获取 # 示例&#xff1a;获取用户的实时位置 def get_user_location(user_id):# 使用GPS或网络定位技术获取用户的地理坐标# 返回经度和纬度信息return lon…...

Tomcat的HTTP Connector

https://tomcat.apache.org/tomcat-10.1-doc/config/http.html 一个Connector代表一个接收请求、返回响应的端点&#xff08;endpoint&#xff09;。 HTTP Connector 元素代表一个支持HTTP/1.1的Connector组件。一个这样的组件在服务端一个指定的TCP端口上监听连接。一个Serv…...

将Pytorch搭建的ViT模型转为onnx模型

本文尝试将pytorch搭建的ViT模型转为onnx模型。 首先将博主上一篇文章中搭建的模型ViT Vision Transformer超详细解析&#xff0c;网络构建&#xff0c;可视化&#xff0c;数据预处理&#xff0c;全流程实例教程-CSDN博客转存为.pth torch.save(model, my_vit_model.pth) 然…...

图神经网络(GNN)性能优化方案汇总,附37个配套算法模型和代码

图神经网络的表达能力对其性能和应用范围有着重要的影响&#xff0c;是GNN研究的核心问题和发展方向。增强表达能力是扩展GNN应用范围、提高性能的关键所在。 目前GNN的表达能力受特征表示和拓扑结构这两个因素的影响&#xff0c;其中GNN在学习和保持图拓扑方面的缺陷是限制表…...

国科大移动互联网考试资料(2023+2020+2018真题+答案)

老师王文杰。真题附加2022部分...

ModStart系统安全规范建议

1 不要使用弱密码 很多人为了系统管理方便&#xff08;或者是懒&#xff09;&#xff0c;经常会设置类似 123456、admin 这样的管理密码&#xff0c;这样的密码很容易被暴力软件扫描出来。 2 不要使用默认配置 默认的软件系统设置、默认的系统端口、默认的网站设置在发生漏洞…...

【漏洞复现】Django_debug page_XSS漏洞(CVE-2017-12794)

感谢互联网提供分享知识与智慧&#xff0c;在法治的社会里&#xff0c;请遵守有关法律法规 文章目录 1.1、漏洞描述1.2、漏洞等级1.3、影响版本1.4、漏洞复现1、基础环境2、漏洞分析3、漏洞验证 说明内容漏洞编号CVE-2017-12794漏洞名称Django_debug page_XSS漏洞漏洞评级影响范…...

Redis性能调优:深度剖析与示例解析

标题&#xff1a;Redis性能调优&#xff1a;深度剖析与示例解析 引言 Redis是一款强大的开源内存数据库&#xff0c;广泛应用于高性能系统。然而&#xff0c;为了充分发挥Redis的性能&#xff0c;需要进行合理的性能调优。本博客将深入介绍Redis性能调优的策略和示例&#xf…...

oracle查询前几条数据的方法

在Oralce中实现select top N&#xff1a;由于Oracle不支持select top 语句&#xff0c;所以在oracle中经常是用order by 跟rownum的组合来实现select top n的查询。 方法1&#xff1a; SELECT * FROM (SELECT * FROM EMP ORDER BY SAL DESC) WHERE ROWNUM < 5 --抽取处记录…...

c#弹性和瞬态故障处理库Polly

1. 重试&#xff08;Retry&#xff09; Policy .Handle<Exception>() //指定需要重试的异常类型 .Retry(2,(ex,count,context)> { //指定发生异常重试的次数Console.WriteLine($ "重试次数{count},异常{ex.Message}" ); }) …...