超多细节—app图标拖动排序实现详解
前言:
最近做了个活动需求大致类似于一个拼图游戏,非常接近于咱们日常app拖动排序的场景。所以想着好好梳理一下,改造改造干脆在此基础上来写一篇实现app拖动排序的文章,跟大家分享下这个大家每天都要接触的场景,到底是怎么样的一个实现的过程。
思路梳理:
按照老惯例,做之前先分析下要实现什么功能点,并预先思考下大致如何去实现。 先随便找个参考图分析分析,如下,得出要解决的逻辑点:
-
首当其冲,app(后文称之为方格)要能按住、拖动,根据鼠标/触摸位置来变化(低代码基本操作)
-
无论何时方块之间应该不留空位置,总是向前铺满
-
当一个方块拖动到另一个方块重叠到一定程度才触发排序,重叠程度需要计算
-
非拖动方块的移动逻辑是什么样的,何时移动、何时停止?需要总结出规律
-
排序不是拖动一开始就触发的,拖动的开始和停止需要做判定
PS:在做之前就想到了细节可能会很多,其中的包含了不少有意思的逻辑,实际远不止上述的几点,且看后续展开。
实现
一、创建模拟App宫格布局
首先创建一下类似于桌面图标的n*n宫格布局基本结构,这里我将宫格数量设置为了一个变量,便于代码更加灵活通用以支持不同的图标数;
另外也创建了些后面会用到的state,详细见注释。
const defaultNum = 16 // 默认格子数
const marginValue = 20 // 格子边距
// 移动方向,前进、静止、后退
const moveDirectionMap = {forward:'forward',static:'static',backwards:'backwards'
}
export function UseDragSort() {const containerRef = useRef(null) //格子的父容器const [row] = useState(Math.sqrt(defaultNum)) // 行列数const [imgArr] = useState(new Array(defaultNum).fill(demoImg))const [positionArr, setPositionArr] = useState([]) // 每个块的位置数据const [draggingStop, setDraggingStop] = useState(false) //是否停止拖动动作const [currNode,setCurrNode] = useState(Object) //当前被拖动的元素const [dragStartPosition,setDragStartPosition] = useState([])const [blockWidth,setBlockWidth] = useState(0) //单块宽度const [aimPosition,setAimPosition] = useState([]) //目标落地点const [onMouseUp,setOnMouseUp] = useState(false) //鼠标是否落下useEffect(() => {countPosition()}, []);}
1.1现在思考一下,如何来生成有n*n个格子的宫格?
- 先算算每个格子的尺寸,假设父级容器的宽高是x,容易想到那么每格的「理想尺寸」就应该是x/n,可以生成一个二维矩阵的数据结构来表达宫格的结构关系。
- 根据每个格子的x、y轴比较容易就可以计算出每个宫格的绝对定位位置。但实际上格子之间还有边距,这个也之前也定义了变量marginValue来存储。在计算的时候扣除即可,详细可见countPosition()实现:
/*** 根据宫格数量生成每个宫格的绝对定位位置* @returns {[{top: number, left: number}]} 位置坐标*/
function countPosition() {// 生成一个2维矩阵let _positionArr = Array(row).fill(Array(row).fill({top: 0, left: 0, width: 0}))_positionArr = JSON.parse(JSON.stringify(_positionArr))// 获取容器尺寸const containerSize = containerRef.current.clientWidth// 单格宽度const blockWidth = containerSize / rowsetBlockWidth(blockWidth)let idx = 0for (let x = 0; x < _positionArr.length; x++) { //横坐标for (let y = 0; y < _positionArr[x].length; y++) { // 纵坐标// 根据位置计算每个位置的top和left_positionArr[x][y].top = blockWidth * x_positionArr[x][y].left = blockWidth * y// 宽度扣除margin的值保证刚好填满格子_positionArr[x][y].width = blockWidth - marginValue * 2_positionArr[x][y].margin = marginValueinitAniStyle(_positionArr[x][y], idx)idx++}}setPositionArr(_positionArr)
}
最后将计算出的位置数据存储到之前定义的positionArr变量,这个变量记录了每个块的位置,是相当重要的,后面的逻辑基本都会围绕这个变量来展开。
打印一下更直观:
现在有了位置信息,就可以根据位置信息来生成格子的具体位置并渲染到页面了。 监听到位置信息生成完毕,开始初始化宫格:
useEffect(() => {if (positionArr.length) {initBlockSort().then()}
}, [positionArr])
/*** 初始化每个方块的位置*/
async function initBlockSort() {// log("初始化")// 全部方块节点const childNodes = containerRef.current.childNodes// 开始根据数据,初始化方块位置和尺寸const formatChildNodes = Array(row).fill(Array(row).fill({top: 0, left: 0}))let idx = 0for (let x = 0; x < positionArr.length; x++) {for (let y = 0; y < positionArr[x].length; y++) {// 设置每个方块的初始位置childNodes[idx].style.width = positionArr[x][y].width + 'px'childNodes[idx].style.height = positionArr[x][y].width + 'px'await executeInitAni(childNodes[idx], idx, positionArr[x][y].width)childNodes[idx].style.left = positionArr[x][y].left + 'px'childNodes[idx].style.top = positionArr[x][y].top + 'px'childNodes[idx].style.margin = positionArr[x][y].margin + 'px'// 给每个方块加上鼠标按下事件监听childNodes[idx].addEventListener('mousedown', clickDown.bind(null, childNodes[idx]), false)// 这里顺便把节点转为跟位置数据一致的n维矩阵形式,用于处理后续的拖动排序操作formatChildNodes[x][y] = childNodes[idx]idx++// 给最后一个格子添加动画执行完成监听if (idx === defaultNum - 1) {childNodes[idx].addEventListener('webkitAnimationEnd', () => {// 动画完成后清除掉animation类,否则会导致拖动的坐标设置失效for (const node of childNodes) {node.style.animation = ''}})}}}}
注意这里要给最后一个格子添加动画执行完成监听,用于清除设置的动画属性,防止后续的拖动设置坐标与动画自带的坐标移动产生冲突
渲染:
return (<div className={"drag-box"}><h1>拖动排序</h1><div ref={containerRef} className={'block-box'}>{imgArr.map((item, index) => {return (<div className={'block-img'} id={`${index}`} key={index}>{index+1}</div>)})}</div></div>
)
最后,再简单添加一些css,就得到了一个带位置标记的n宫格,来模拟app桌面。
1.2初始化小动画
可以注意到在之前生成位置信息时,countPosition()顺便触发了一个initAniStyle(_positionArr[x][y], idx)函数,并传入了格子的横、纵坐标和索引。并且给最后一个格子添加了动画执行完成监听。
这是为初始化添加一个小动画,类似于发牌的效果.(至于为什么要加,只能说之前需求写了就顺便讲一讲~~~)
/*** 生成初始化动画,根据每个方块生成一个动画keyframes,* 其实也可以动态修改同一个动画再赋值,没必要影响不大,* 都是从(0,0)起始移动到指定位置* @param nodePosition 位置数据* @param index 索引,用于绑定动画*/
const initAniStyle = (nodePosition, index) => {document.styleSheets[0].insertRule(`@-webkit-keyframes ani${index}{ from{ left:0px;top:0px } to { left:${nodePosition.left}px;top:${nodePosition.top}px; }}`, 0)
}
在上面的initBlockSort()中我们又同步调用了下方的executeInitAni函数对动画进行了执行,该函数返回了一个promise,10ms后调用resolve,以实现了每间隔10ms执行一个块的动画。
这里的场景也是很常见的一个面试题,如何使for循环慢下来?答案之一就是promise啦。
/*** 执行单个块动画* @param targetNode 块节点* @param index 块序号* @param width 宽度* @returns {Promise<unknown>}*/
const executeInitAni = async (targetNode, index, width) => {return new Promise((resolve) => {const sizeStyle = `width:${width}px;height:${width}px`const animStyle = `ani${index} 0.8s ease-in-out forwards`targetNode.setAttribute('style', `animation:${animStyle};-webkit-animation:${animStyle};${sizeStyle}`)setTimeout(() => {// 意味着动画之间的间隔resolve()}, 10)})}
现在,我们得到了一个简单而流畅的初始化动画
顺便一提,由于本文代码主要是用于演示讲解,为了方便理解、最大程度展现逻辑,并没有对例如动画、style等进一步封装。
二、实现元素的拖拽
经过之前的步骤,只算是初步完成了准备,现在进入正题。
在之前初始化的函数中,我们还同时给每个方块添加了鼠标按下的监听事件,现在派上用场了,我们将通过这个事件,来实现拖拽的核心逻辑。
通过对鼠标移动位置的获取,来设置元素的绝对位置,即可实现元素的拖拽效果,另外也需要处理下元素被“松开”之后的逻辑,不然元素会一直黏在光标上,完整函数如下:
let timer = null
let movePixel = [-999, -999]
const clickDown = (targetNode, e) => {setCurrNode(targetNode)// 记录被拖拽元素的起始位置const _left = Number(targetNode.style.left.replace('px',''))const _top = Number(targetNode.style.top.replace('px',''))const _margin = Number(targetNode.style.margin.replace('px',''))const dragPositionLeft = _left+_marginconst dragPositionTop = _top+_marginsetDragStartPosition([dragPositionLeft,dragPositionTop])// 写个定时器判断拖动是否停止if (!timer) {timer = setInterval(() => {if (movePixel[0] === targetNode.style.left && movePixel[1] === targetNode.style.top) {// 一定时间内拖动间隔不再更新就判定停止setDraggingStop(true)} else {// 拖动中就一直更新坐标,并且更新拖动味停止状态[movePixel[0], movePixel[1]] = [targetNode.style.left, targetNode.style.top]setDraggingStop(false)}}, 200)}targetNode.style.cursor = 'pointer';let offsetX = parseInt(targetNode.style.left) // 获取当前的x轴距离let offsetY = parseInt(targetNode.style.top) // 获取当前的y轴距离let innerX = e.clientX - offsetX // 获取鼠标在方块内的x轴距let innerY = e.clientY - offsetY // 获取鼠标在方块内的y轴距targetNode.style.zIndex = '700'// 根据鼠标的移动轨迹修改目标节点的位置document.onmousemove = (e) => {targetNode.style.left = e.clientX - innerX + 'px'targetNode.style.top = e.clientY - innerY + 'px'// 出界判断if (parseInt(targetNode.style.left) <= 0) {targetNode.style.left = '0px'}// 为了避免篇幅过长这里我省略了部分边界的判定,参照上面即可·························}
松开清除事件逻辑:
// 鼠标抬起时后清除一系列事件document.onmouseup = () => {// log('鼠标抬起,清除事件')clearInterval(timer)timer = null// 如果不悬停直接松开鼠标,要判定停止拖动// 如果已经被悬停计时器判定了未松开鼠标的拖动停止(会触发拖动停止的监听事件),再松开鼠标的时候就应该不再认为是拖动停止,所以取反setDraggingStop(prevState => !prevState)document.onmousemove = nulldocument.onmouseup = nulltargetNode.style.zIndex = '2'setOnMouseUp(true)}
}
在这个函数中,我们还记录了这些信息,在后面的覆盖检测、非拖动元素排序会使用到:
1.当前是哪个元素被拖动;
2.当前元素的起始坐标信息
3.通过定时器判定拖动是否停下来
现在可以看看拖动的效果了
三、元素自动排序
好了,到这一步被拖动的目标元素可以自由移动,接下来就解决其他元素该如何找到自己的位置呢?还有就是目标元素如何知道自己该落在哪里?
首先分析下相关动作执行的时机:
- 在元素拖动的过程中,没有必要做出排序行为,而是等拖动停止一定时间后,再开始排序
- 在排序的过程中不应该再触发排序
- 即使鼠标被按住还没松开,也应该预览排序,而不是松开鼠标后再统一排序(这样简单但不够好)
- 排序只针对没有拖动的元素,否则目标元素会从没有松开的光标'溜走',体验很奇怪
- 等到鼠标松开释放目标元素后,再执行目标元素的最后落位
现在开始正式思考排序的核心逻辑
拖动一个元素到一个位置的本质是什么?交换位置?
实际上要分情况:
- 如果元素往前拖动,那就是目标位置——元素位置之间的元素都往后移动一个单位;
- 如果元素往后拖动,那就是目标位置——元素位置之间的元素都往前移动一个单位。
由此引申出一个关键点,如何判定一个元素应该占据一个元素的位置了?
因为如果a元素只是有一个1px的角碰到了b元素,很明显此时a元素是不应该占据b元素的位置的。那么定义元素重叠了多少应该占据位置呢?三分之一?二分之一?这种思路实际不好计算而且繁琐,因为a元素很可能是从斜上方或者各种四面八方来覆盖b元素的。
为了解决这个问题后来我琢磨出了一个了中心点检测的思路。
即当a元素的中心点出现在了b元素之上的时候,就表明a应该占据b的位置了,后来也证明这种思路非常有效。
思路明确,编码开始:
// 监听拖拽开始
useEffect(() => {if (draggingStop) {log('拖拽起始:'+dragStartPosition)// 拖拽暂时停止了,检测目标元素归属coverCheck()}
}, [draggingStop])
// 模拟绘制中心点
function mockDrawCenterDot(centerDot){const newDiv = document.createElement("div")// 要注意中心点本身的宽高,不然会绘制偏差const width = 10newDiv.style.width = width+'px'newDiv.style.height = width+'px'newDiv.style.position = 'absolute'newDiv.style.left = centerDot.left-width/2+'px'newDiv.style.top = centerDot.top-width/2+'px'newDiv.style.zIndex = '700'newDiv.style.backgroundColor = '#00c175'containerRef.current.appendChild(newDiv)
}
为了更直观看效果,我把中心点简单绘制出来了,如图每一次拖动就会标注出中心点:
中心点检测函数,注意对无效落点(比如拖动到两个元素中间)的处理:
// 中心点检测:当被拖动的元素A的中心点位于另一个元素B之上的时候,就判定A应该占据B的位置了
const coverCheck = async ()=>{// 计算当前拖动元素的中心点:元素的宽高的一半再加上顶部和左边的距离就是中心点坐标const width = Number(currNode.style.width.replace('px','')/2)const margin = Number(currNode.style.margin.replace('px',''))//中心点坐标const centerDot = {left:Number(currNode.style.left.replace('px',''))+width+margin,top:Number(currNode.style.top.replace('px',''))+width+margin,}mockDrawCenterDot(centerDot)// mockBorder()// 计算每个块的覆盖坐标区间,例如第一个块{left:[20,85],top:[20,85]},中心点坐标左边距在20-85px,顶部距离在20-85内即判定进入该块区间// const coverRate = []// 是向前移动还是向后移动let moveTo = ''let validArea = false // 是否落到有效位置for(const v of positionArr){const row = [] // 一行的数据for(const child of v){// 左边起点,左边终点,顶部起点,顶部终点const leftBegin = child.left+child.marginconst leftEnd = child.left+child.margin+child.widthconst topBegin = child.top+child.marginconst topEnd = child.top+child.margin+child.width// 根据上面四个起点就可以当前单个块的覆盖范围const currRate = {left:[leftBegin,leftEnd],top:[topBegin,topEnd]}row.push(currRate)// 判定中心点坐标是否落入当前方块覆盖区间if(centerDot.left>=leftBegin && centerDot.left<=leftEnd && centerDot.top>=topBegin && centerDot.top <= topEnd){validArea = true// 存储落地点setAimPosition([currRate.left[0]-marginValue,currRate.top[0]-marginValue])log('有效落点-坐标区间:'+JSON.stringify(currRate))// 根据落点区间和初始拖动元素的位置关系来判断moveTo,原地、前进、后退if(leftBegin === dragStartPosition[0] && topBegin === dragStartPosition[1]){// 原块区间moveTo = moveDirectionMap.static}else if(topBegin > dragStartPosition[1] || (topBegin === dragStartPosition[1] && leftBegin > dragStartPosition[0])){// 落点区间在原位置下面,或者同一高度但比原位置距离左边更远,一定是前进moveTo = moveDirectionMap.backwards}else{// 后退moveTo = moveDirectionMap.forward}// 重排开始moveBlockSort(dragStartPosition,[currRate.left[0],currRate.top[0]],moveTo,currNode).then()}}// coverRate.push(row)}if(!validArea){log('无效落点-归位')// 无效位置的落地点就是起始点setAimPosition([dragStartPosition[0]-marginValue,dragStartPosition[1]-marginValue])// await moveBlockSort(dragStartPosition,dragStartPosition,'static',currNode)}log(moveTo)
}
执行重排
经历上一步,已经确定了哪个元素被占据,哪个元素被拖动,接下来就可以对其他【应当移动的】元素进行移动操作,即上一步的moveBlockSort().
/**** @param beginPosition 起始位置* @param aimPosition 目标落地位置* @param moveDirection 移动方向* @param node 当前节点*/
// 移动逻辑,循环每一个节点,获取它的坐标,如果这个坐标属于被移动的范围,就给这个节点加上移动动画函数让它动起来
// 如何确定是否属于被移动的范围,根据移动块和被占据块的左右关系来判定,计算出大于某个坐标值的块都需要被移动
// 具体怎么动?每一个块只会移动一格,而且要么是向前要么是向后,比较简单(即使换行,对于positionArr来说也是前后一个坐标的含义)
async function moveBlockSort(beginPosition,aimPosition,moveDirection,node){// 先将位置的二维数组扁平化,格子的布局都是固定的,便于获取前后的位置const sortMap = positionArr.flat()// 全部节点const nodes = new Array(...containerRef.current.childNodes).filter(v=>{return Boolean(v.id)})// 根据节点位置计算一个节点的绝对排序,即属于n个节点中的第几个const nodeIndex = (_node)=>{for(let i=0;i<sortMap.length;i++){if(sortMap[i].left+'px' === _node.style.left && sortMap[i].top+'px' === _node.style.top){return i}}return -1}// 需要被移动的元素和它的物理顺序位置const moveIndexArr = []const isForward = moveDirection === moveDirectionMap.forwardif(moveDirection === moveDirectionMap.static){// 原地移动,将被拖动的元素放回起始点即可onceAniBind(node,beginPosition[0]-marginValue,beginPosition[1]-marginValue).then()}else{for(let i=0;i<nodes.length;i++){// 排除当前节点if(nodes[i].id === node.id)continue// 循环所有节点const margin = Number(currNode.style.margin.replace('px',''))const nodeLeft = Number(nodes[i].style.left.replace('px',''))+marginconst nodeTop = Number(nodes[i].style.top.replace('px',''))+margin// 基于起始位置向前移动,那么确定需要移动的块(称为活动块):起始点(不包括)之前到落地点(包括)之间的所有块;向后移动一格if(isForward){// 当前节点是否位于起始点之前const isBeforeBegin = nodeTop < beginPosition[1] || (nodeTop === beginPosition[1] && nodeLeft < beginPosition[0])// 当前节点是否位于目标点之后或者处于目标点const isAimAfter = nodeTop > aimPosition[1] || (nodeTop === aimPosition[1] && nodeLeft >= aimPosition[0])if(isBeforeBegin && isAimAfter){// 这是一个活动块,获取他的顺序位置const currNodeIndex = nodeIndex(nodes[i])// 它应该去的位置就是后退一格moveIndexArr.push([sortMap[currNodeIndex+1],nodes[i]])}}else{// 基于起始位置向后移动,那么确定需要移动的块(称为活动块):起始点(不包括)之前到落地点(包括)之间的所有块;向前移动一格// 当前节点是否位于起始点之后const isAfterBegin = nodeTop > beginPosition[1] || (nodeTop === beginPosition[1] && nodeLeft > beginPosition[0])// 当前节点是否位于目标点之前或者处于目标点const isAimBefore = nodeTop < aimPosition[1] || (nodeTop === aimPosition[1] && nodeLeft <= aimPosition[0])if(isAfterBegin && isAimBefore){// 这是一个活动块,获取他的顺序位置const currNodeIndex = nodeIndex(nodes[i])// 它应该去的位置就是前进一格moveIndexArr.push([sortMap[currNodeIndex-1],nodes[i]])}}}}// 根据moveIndexArr数据,依次对需要移动的元素绑定移动动画for(const v of moveIndexArr){onceAniBind(v[1],v[0].left,v[0].top).then()}
}
为了元素的排序更优雅,简单封装了一个一次性动画绑定函数,来移动每一个元素,即上面最后的onceAniBind()函数。
/*** 为一个元素绑定并执行一个一次性移动动画* @param el 元素* @param left 位置* @param top*/
const onceAniBind = async (el, left, top) => {// 创造个30位左右的随机数当类名const timeStampSign = String(Math.random()).slice(2,20)+String(Math.random()).slice(2,20)const aniLen = 0.5 //动画时长s// 以随机戳为标识创建一个动画帧document.styleSheets[0].insertRule(`@-webkit-keyframes ani${timeStampSign}{ from{ left:${el.style.left};top:${el.style.top} } to { left:${left}px;top:${top}px; }}`, 0)// 为目标元素绑定创建的动画,使用promise可以方便兼容需要依次执行动画的场景return new Promise((aniEnd) => {const animStyle = `ani${timeStampSign} ${aniLen}s ease-in-out forwards`el.setAttribute('style', `animation:${animStyle};-webkit-animation:${animStyle};`)el.addEventListener('webkitAnimationEnd', () => {// 动画完成后清除掉animation类,否则会导致拖动的坐标设置失效el.style.animation = ''// 固定动画终点位置el.style.left = left + 'px'el.style.top = top + 'px'el.style.width = blockWidth-marginValue*2 + 'px'el.style.height = blockWidth-marginValue*2 + 'px'el.style.margin = marginValue + 'px'aniEnd()})})
}
复制代码
最后根据鼠标松开监听,完成对拖拽元素本身的最终落位
useEffect( () => {async function setLastBlock(){if(onMouseUp && aimPosition.length){log(currNode.id)// 将当前拖拽块落地await onceAniBind(currNode,aimPosition[0],aimPosition[1]).then()setOnMouseUp(false)setAimPosition([])}}setLastBlock().then()
}, [onMouseUp,aimPosition]);
最终效果:
至此,所有元素都可以正常自动排序了,不知不觉写了很多代码,希望能让大家有所收获~~~
源码github地址:github.com/bokhuang/ap…
附送250套精选项目源码
源码截图
源码获取:关注公众号「码农园区」,回复 【源码】,即可获取全套源码下载链接
相关文章:

超多细节—app图标拖动排序实现详解
前言: 最近做了个活动需求大致类似于一个拼图游戏,非常接近于咱们日常app拖动排序的场景。所以想着好好梳理一下,改造改造干脆在此基础上来写一篇实现app拖动排序的文章,跟大家分享下这个大家每天都要接触的场景,到底…...
基于深度学习的文字识别
基于深度学习的文字识别 基于深度学习的文字识别(Optical Character Recognition, OCR)是指利用深度神经网络模型自动识别和提取图像中的文字内容。这一技术在文档数字化、自动化办公、车牌识别、手写识别等多个领域有着广泛的应用。 深度学习OCR的基本…...

Pikachu靶场--文件包含
参考借鉴 Pikachu靶场之文件包含漏洞详解_pikachu文件包含-CSDN博客 文件包含(CTF教程,Web安全渗透入门)__bilibili File Inclusion(local) 查找废弃隐藏文件 随机选一个然后提交查询 URL中出现filenamefile2.php filename是file2.php&…...
get put post delete 区别以及幂等
GET 介绍:GET请求用于从服务器获取资源,通常用于获取数据。它的参数会附加在URL的末尾,可以通过URL参数传递数据。GET请求是幂等的,即多次请求同一个URL得到的结果应该是一样的,不会对服务器端产生影响。 特点…...
ultralytics版本及对应的更新
Ultralytics Ultralytics 是一家专注于计算机视觉和深度学习工具的公司,尤以其开源的 YOLO (You Only Look Once) 系列深受欢迎。目前,Ultralytics 主要管理和开发 YOLOv5 和 YOLOv8。以下是各个版本的概述及其主要更新: YOLOv5 YOLOv5 是…...
在现代编程环境中,Perl 如何与其他流行语言(如 Python、Java 等)进行集成和协作?
在现代编程环境中,Perl 可以与其他流行语言(如 Python、Java 等)进行集成和协作。以下是一些常见的方法: 调用外部程序:Perl 可以使用系统调用来执行其他语言编写的可执行文件。这意味着可以从 Perl 中调用 Python、Ja…...

BEV 中 multi-frame fusion 多侦融合(一)
文章目录 参数设置align_dynamic_thing:为了将动态物体的点云数据从上一帧对齐到当前帧流程旋转函数平移公式filter_points_in_ego:筛选出属于特定实例的点get_intermediate_frame_info: 函数用于获取中间帧的信息,包括点云数据、传感器校准信息、自车姿态、边界框及其对应…...
“Docker操作案例实践“
目录 1. 下载nginx 2. Portainer可视化 1. 下载nginx 步骤: 搜索nginx:docker search nginx;下载镜像:docker pull nginx ;查看镜像:docker images ;后台运行 :docker run -d -na…...

Redis 管道
Redis的消息交互 当我们使用客户端对Redis进行一次操作时,如下图所示,客户端将请求传送给服务器,服务器处理完毕后,再将响应回复给客户端,这要花费一个网络数据包来回的时间。 如果连续执行多条指令,那就会…...
ubuntu20.04安装配置openMVG+openMVS
安装 主要跟着官方教程逐步安装 openMVG https://github.com/openMVG/openMVG/blob/master/BUILD.md openMVS https://github.com/cdcseacave/openMVS/wiki/Building 注意事项 1. 库版本要求 使用版本: openMVS 2.2.0 openMVG Eigen 3.4.0 OpenCV 4.6.0 Ce…...

使用CSS常见问题解答卡片
常见问题解答卡片 效果展示 CSS 知识点 CSS 选择器的使用background 渐变背景色运用CSS 综合知识运用 页面整体布局 <div class"container"><h1>经常问的问题</h1><!-- 这里只是展示一个项目 --><div class"tab"><in…...

Kong AI Gateway 正式 GA !
Kong Gateway 3.7 版本已经重磅上线,我们给 AI Gateway 带来了一系列升级,下面是 AI Gateway 的更新亮点一览。 AI Gateway 正式 GA 在 Kong Gateway 的最新版本 3.7 中,我们正式宣布 Kong AI Gateway 达到了通用可用性(GA&…...

HTML5有哪些新特性?
目录 1.语义化标签:2.多媒体支持:3.增强型表单:4.绘图与图形:5.地理定位:6.离线应用与存储:7.性能与集成:8.语义化属性:9.改进的 DOM 操作:10.跨文档通信:11.…...

SQL Server入门-SSMS简单使用(2008R2版)-2
环境: win10,SQL Server 2008 R2 参考: SQL Server 管理套件(SSMS)_w3cschool https://www.w3cschool.cn/sqlserver/sqlserver-oe8928ks.html SQL Server存储过程_w3cschool https://www.w3cschool.cn/sqlserver/sql…...
php实现modbus CRC校验
一:计算CRC校验函数 function calculateCRC16Modbus($string) {$crcBytes [];for ($i 0; $i < strlen($string); $i 2) {$crcBytes[] hexdec(substr($string, $i, 2));}$crc 0xFFFF;$polynomial 0xA001; // This is the polynomial x^16 x^15 x^2 1fo…...

2025年计算机毕业设计题目参考
今年最新计算机毕业设计题目参考 以下可以参考 springboot洗衣店订单管理系统 springboot美发门店管理系统 springboot课程答疑系统 springboot师生共评的作业管理系统 springboot平台的医疗病历交互系统 springboot购物推荐网站的设计与实现 springboot知识管理系统 springbo…...
ERP、CRM、SRM、PLM、HRM、OA……都是啥意思?
经常会听说一些奇怪的系统或平台名称,例如ERP、CRM、SRM、PLM、HRM、OA等。 这些系统,都是干啥用的? █ ERP(企业资源计划) 英文全称:Enterprise Resource Planning 定义:由美国Gartner Gro…...

Jmeter分布式、测试报告、并发数计算、插件添加方式、常用图表
Jmeter分布式 应用场景 当单个测试机无法模拟用户要求的业务场景时,可以使用多台测试机进行模拟,就是Jmeter的分布 式测试。 Jmeter分布式执行原理 Jmeter分布测试时,选择其中一台作为控制机(Controller),…...

3D三维模型展示上传VR全景创建H5开源版开发
3D三维模型展示上传VR全景创建H5开源版开发 新增三级分类(项目分类、项目、默认场景) 新增热点 前台创建项目、场景 场景跳转、提示信息 新增热点图标选择 新增预览场景是显示关联场景 新增3D模型展示功能 当然可以!以下是一个关于3D三维模…...

js中!emailPattern.test(email) 的test是什么意思
test 是 JavaScript 正则表达式(RegExp)对象的方法之一,用于测试一个字符串是否与正则表达式匹配。正则表达式是一种用于匹配字符串的模式,通常用于验证输入数据、查找和替换文本等。 使用 test 方法 test 方法语法如下…...

利用最小二乘法找圆心和半径
#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …...

龙虎榜——20250610
上证指数放量收阴线,个股多数下跌,盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型,指数短线有调整的需求,大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的:御银股份、雄帝科技 驱动…...

工业安全零事故的智能守护者:一体化AI智能安防平台
前言: 通过AI视觉技术,为船厂提供全面的安全监控解决方案,涵盖交通违规检测、起重机轨道安全、非法入侵检测、盗窃防范、安全规范执行监控等多个方面,能够实现对应负责人反馈机制,并最终实现数据的统计报表。提升船厂…...

Python:操作 Excel 折叠
💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...
AI编程--插件对比分析:CodeRider、GitHub Copilot及其他
AI编程插件对比分析:CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展,AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者,分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...

Mac下Android Studio扫描根目录卡死问题记录
环境信息 操作系统: macOS 15.5 (Apple M2芯片)Android Studio版本: Meerkat Feature Drop | 2024.3.2 Patch 1 (Build #AI-243.26053.27.2432.13536105, 2025年5月22日构建) 问题现象 在项目开发过程中,提示一个依赖外部头文件的cpp源文件需要同步,点…...

如何在网页里填写 PDF 表格?
有时候,你可能希望用户能在你的网站上填写 PDF 表单。然而,这件事并不简单,因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件,但原生并不支持编辑或填写它们。更糟的是,如果你想收集表单数据ÿ…...

安宝特方案丨船舶智造的“AR+AI+作业标准化管理解决方案”(装配)
船舶制造装配管理现状:装配工作依赖人工经验,装配工人凭借长期实践积累的操作技巧完成零部件组装。企业通常制定了装配作业指导书,但在实际执行中,工人对指导书的理解和遵循程度参差不齐。 船舶装配过程中的挑战与需求 挑战 (1…...

短视频矩阵系统文案创作功能开发实践,定制化开发
在短视频行业迅猛发展的当下,企业和个人创作者为了扩大影响力、提升传播效果,纷纷采用短视频矩阵运营策略,同时管理多个平台、多个账号的内容发布。然而,频繁的文案创作需求让运营者疲于应对,如何高效产出高质量文案成…...
Java编程之桥接模式
定义 桥接模式(Bridge Pattern)属于结构型设计模式,它的核心意图是将抽象部分与实现部分分离,使它们可以独立地变化。这种模式通过组合关系来替代继承关系,从而降低了抽象和实现这两个可变维度之间的耦合度。 用例子…...