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的在线表格,功能强大、配置简单、完全开源。https://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 获取数据:
至此,我们可以获取一些基础信息:
- 每次操作都会发送 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 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的协同做一下总结吧:
- 对pako压缩数据进行解析,这是第一个难点;
- 数据存储按照分布式存储会更快;这里是结合着 loadUrl的哈,后端返回保存后的数据进行渲染;
- luckyexcel 进行文件导入;
- exceljs file-saver 实现文件导出;
- 对源码进行二次开发,实现手动关闭 websocket 连接;
- 还有很多细节哈,大家根据需要可以自行定义,有问题欢迎留言讨论。
制作不易,点赞收藏~
相关文章:

Luckysheet 实现excel多人在线协同编辑
前言 前些天看到Luckysheet支持协同编辑Excel,正符合我们协同项目的一部分,故而想进一步完善协同文章,但是遇到了一下困难,特此做声明哈,若侵权,请联系我删除文章! 若侵犯版权、个人隐私&#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 计算机组成:硬件软件2.2 CPU、内存与硬盘2.3 输入设备:键盘输入 三、软件相关介绍…...

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

IDEA版SSM入门到实战(Maven+MyBatis+Spring+SpringMVC) -Maven核心概念
一.Maven的POM POM全称:Project Object Model【项目对象模型】,将项目封装为对象模型,便于使用Maven管理【构建】项目 pom.xml常用标签 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://m…...
Unity的粒子总是丢材质
1)Unity的粒子总是丢材质 2)C#传给C的Byte数组如何释放 3)CommandBuffer.DrawProcedural在手机上为什么不生效 4)游戏加载场景碰撞,会弹出显卡报错,驱动程序超时 这是第359篇UWA技术知识分享的推送…...

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

1. Collection,List, Map, Queue
1. java集合框架体系结构图 2. Collection派生的子接口 其中最重要的子接口是: 1)List 表示有序可重复列表,重要的实现类有:ArrayList, LinkedList ArrayList特点:底层数组实现,随机查找快,增删…...
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语言函数签名和匿名函数
函数签名 函数类型又叫做函数签名,一个函数的类型就是函数定义首行去掉函数名、参数名和{},可以用fmt.Printf的“%T”格式化参数打印函数的类型。 两个函数类型相同的条件是:拥有相同的形参列表和返回值列表,形参名可以不同。 ty…...

Pytest系列(16)- 分布式测试插件之pytest-xdist的详细使用
前言 平常我们功能测试用例非常多时,比如有1千条用例,假设每个用例执行需要1分钟,如果单个测试人员执行需要1000分钟才能跑完当项目非常紧急时,会需要协调多个测试资源来把任务分成两部分,于是执行时间缩短一半&#…...

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

wpf添加Halcon的窗口控件报错:下列控件已成功添加到工具箱中,但未在活动设计器中启用
报错截图如下: 注意一下新建工程的时候选择wpf应用而不是wpf应用程序。 添加成功的控件:...

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…...

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

Linux学习笔记--高级
Shell概述 1,shell概述 是一个c语言编写的脚本语言,是linux和用户的桥梁,用户输入命令交给shell处理。shell,将相应的操作传递给内核(kernel),内核把处理的结果输出给用户 1.1Shell解释器有哪…...

在Java中操作Redis
Redis中如何的去存放一个Java对象? 直接存放Json类型即可,因为我们Json类型最终就是一个String类型。 Redis的Java客户端 Redis的常用命令是我们操作Redis的基础,那么我们在Java程序当中如何来操作Redis呢? 要想基于Java语言…...
【服务器】fiber协程模块
fiber协程模块 以下是从sylar服务器中学的,对其的复习; sylar的fiber协程模块是基于ucontext_t实现非对称协程 函数只有两个行为:调用与返回。一旦函数返回后,它在栈上所拥有的状态将被销毁。协程相比函数多了两个动作…...
SparkML
SparkML SparkML_lr_train :读取py处理后的train表用于训练,将训练模型保存好。 SparkML_lr_predict :读取训练好的模型,读取py处理后的test表用于预测。将预测结果写入normal_data中,根据id修改stream_is_normal的值。…...

iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘
美国西海岸的夏天,再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至,这不仅是开发者的盛宴,更是全球数亿苹果用户翘首以盼的科技春晚。今年,苹果依旧为我们带来了全家桶式的系统更新,包括 iOS 26、iPadOS 26…...

Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动
一、前言说明 在2011版本的gb28181协议中,拉取视频流只要求udp方式,从2016开始要求新增支持tcp被动和tcp主动两种方式,udp理论上会丢包的,所以实际使用过程可能会出现画面花屏的情况,而tcp肯定不丢包,起码…...
大模型多显卡多服务器并行计算方法与实践指南
一、分布式训练概述 大规模语言模型的训练通常需要分布式计算技术,以解决单机资源不足的问题。分布式训练主要分为两种模式: 数据并行:将数据分片到不同设备,每个设备拥有完整的模型副本 模型并行:将模型分割到不同设备,每个设备处理部分模型计算 现代大模型训练通常结合…...
高防服务器能够抵御哪些网络攻击呢?
高防服务器作为一种有着高度防御能力的服务器,可以帮助网站应对分布式拒绝服务攻击,有效识别和清理一些恶意的网络流量,为用户提供安全且稳定的网络环境,那么,高防服务器一般都可以抵御哪些网络攻击呢?下面…...

vue3+vite项目中使用.env文件环境变量方法
vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量,这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...

【开发技术】.Net使用FFmpeg视频特定帧上绘制内容
目录 一、目的 二、解决方案 2.1 什么是FFmpeg 2.2 FFmpeg主要功能 2.3 使用Xabe.FFmpeg调用FFmpeg功能 2.4 使用 FFmpeg 的 drawbox 滤镜来绘制 ROI 三、总结 一、目的 当前市场上有很多目标检测智能识别的相关算法,当前调用一个医疗行业的AI识别算法后返回…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...

以光量子为例,详解量子获取方式
光量子技术获取量子比特可在室温下进行。该方式有望通过与名为硅光子学(silicon photonics)的光波导(optical waveguide)芯片制造技术和光纤等光通信技术相结合来实现量子计算机。量子力学中,光既是波又是粒子。光子本…...
《C++ 模板》
目录 函数模板 类模板 非类型模板参数 模板特化 函数模板特化 类模板的特化 模板,就像一个模具,里面可以将不同类型的材料做成一个形状,其分为函数模板和类模板。 函数模板 函数模板可以简化函数重载的代码。格式:templa…...
【Go语言基础【12】】指针:声明、取地址、解引用
文章目录 零、概述:指针 vs. 引用(类比其他语言)一、指针基础概念二、指针声明与初始化三、指针操作符1. &:取地址(拿到内存地址)2. *:解引用(拿到值) 四、空指针&am…...