超多细节—app图标拖动排序实现详解

news2024/11/24 15:46:12

前言:

最近做了个活动需求大致类似于一个拼图游戏,非常接近于咱们日常app拖动排序的场景。所以想着好好梳理一下,改造改造干脆在此基础上来写一篇实现app拖动排序的文章,跟大家分享下这个大家每天都要接触的场景,到底是怎么样的一个实现的过程。

思路梳理:

按照老惯例,做之前先分析下要实现什么功能点,并预先思考下大致如何去实现。 先随便找个参考图分析分析,如下,得出要解决的逻辑点:

  1. 首当其冲,app(后文称之为方格)要能按住、拖动,根据鼠标/触摸位置来变化(低代码基本操作)

  2. 无论何时方块之间应该不留空位置,总是向前铺满

  3. 当一个方块拖动到另一个方块重叠到一定程度才触发排序,重叠程度需要计算

  4. 非拖动方块的移动逻辑是什么样的,何时移动、何时停止?需要总结出规律

  5. 排序不是拖动一开始就触发的,拖动的开始和停止需要做判定

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 / row
    setBlockWidth(blockWidth)
    let idx = 0
    for (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 = marginValue
            initAniStyle(_positionArr[x][y], idx)
            idx++
        }
    }
    setPositionArr(_positionArr)
}

最后将计算出的位置数据存储到之前定义的positionArr变量,这个变量记录了每个块的位置,是相当重要的,后面的逻辑基本都会围绕这个变量来展开。

打印一下更直观:

image.png

现在有了位置信息,就可以根据位置信息来生成格子的具体位置并渲染到页面了。 监听到位置信息生成完毕,开始初始化宫格:

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 = 0
    for (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桌面。

image.png

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+_margin
    const dragPositionTop = _top+_margin
    setDragStartPosition([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 = null
        document.onmouseup = null
        targetNode.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 = 10
    newDiv.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.margin
            const leftEnd = child.left+child.margin+child.width
            const topBegin = child.top+child.margin
            const 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.forward
    if(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',''))+margin
            const 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套精选项目源码

源码截图

 源码获取:关注公众号「码农园区」,回复 【源码】,即可获取全套源码下载链接

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1834395.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

经纬恒润国内首个物理区域控制器量产

当前&#xff0c;智能化汽车的电子电气架构正在从传统的功能域架构向新一代的中央计算加区域控制的架构演进中&#xff0c;国内新能源汽车厂商都在竞相基于新一代架构理念推出新平台车型。物理区域控制器可以实现车辆区域智能传感器及执行器配电、网关路由、信号采集以及执行器…

OpenSearch 与 Elasticsearch主要差异

1. 什么是 Elasticsearch&#xff1f; Elasticsearch 是一个基于 Apache Lucene 构建的开源、RESTful、分布式搜索和分析引擎。它旨在处理大量数据&#xff0c;使其成为日志和事件数据管理的流行选择。 Elasticsearch 还以其实时功能而闻名&#xff0c;允许用户在数据模式发生…

解决linux下载github项目下载不下来,下载失败, 连接失败的问题

第一步&#xff1a;打开/etc/hosts文件 linux vim /etc/hosts 第二步&#xff1a;文件拉到最下面&#xff0c;输入以下内容 linux #GitHub Start 140.82.113.3 github.com 140.82.114.20 gist.github.com 151.101.184.133 assets-cdn.github.com 151.101.184.133 raw.githubus…

中国最厉害的改名大师颜廷利:食物的真正人生意义是识悟

在探索人生意义的深邃征途中&#xff0c;我们本应以“识悟”为航标&#xff0c;不断扬帆远航&#xff0c;以实现自我的升华。然而&#xff0c;当回望人世繁华&#xff0c;古往今来&#xff0c;无论男女老少&#xff0c;似乎都在“食物”的陪伴下&#xff0c;徘徊往复&#xff0…

Ubuntu下安装和配置MariaDB

Ubuntu下安装和配置MariaDB 简介 MariaDB 是一个流行的开源关系型数据库管理系统,是 MySQL 的一个分支,由 MySQL 的创始人开发和维护。MariaDB 完全兼容 MySQL,并且提供了许多增强功能和性能改进。MariaDB 以其稳定性和高性能受到广泛使用,适用于各种规模的应用。本文将详…

深度神经网络——什么是NLP(自然语言处理)?

自然语言处理&#xff08;NLP&#xff09; 是对使计算机能够处理、分析、解释和推理人类语言的技术和工具的研究和应用。 NLP 是一个跨学科领域&#xff0c;它结合了语言学和计算机科学等领域已建立的技术。 这些技术与人工智能结合使用来创建聊天机器人和数字助理&#xff0c;…

企业数字化转型好帮手蚓链,超多创新亮点等你来!

家人们&#xff0c;今天必须给大家分享一下蚓链这个超棒的数字化转型好帮手呀&#xff01; 在理念创新上&#xff0c;它做到了以用户为中心&#xff0c;给大家带来精准化、个性化的营销体验呢。 组织创新也超厉害&#xff0c;搭建了开放式创新平台&#xff0c;吸引外部合作伙伴…

SyntaxError: EOL while scanning string literal

背景&#xff1a; 在对字符串使用in关系运算符时&#xff0c;报错SyntaxError: EOL while scanning string literal 原因&#xff1a; 这是因为${var}中有换行符\n导致的&#xff0c;通过Log ${var}可以看出换行符确实导致的字符串hello的引号位于两行&#xff0c;从而导致…

计算机SCI期刊,IF=13.3+,期刊质量非常高,声誉佳

一、期刊名称 INTERNATIONAL JOURNAL OF COMPUTER VISION 二、期刊简介概况 期刊类型&#xff1a;SCI 学科领域&#xff1a;计算机科学 影响因子&#xff1a;13.369 中科院分区&#xff1a;2区 三、期刊征稿范围 《国际计算机视觉杂志》详细介绍了这一快速发展的领域的科…

在AMD GPUs上构建解码器Transformer模型

Building a decoder transformer model on AMD GPU(s) — ROCm Blogs 在这篇博客中&#xff0c;我们展示了如何使用PyTorch 2.0和ROCm在单个节点上的单个和多个AMD GPU上运行Andrej Karpathy精美的PyTorch重新实现的GPT。我们使用莎士比亚的作品来训练我们的模型&#xff0c;然…

数据和埋点的通俗解释

举一个生活的例子&#xff0c;让大家理解一下数据和埋点 从前&#xff0c;小镇里新开了一家游乐园&#xff0c;游乐园里有各种各样的游乐设施&#xff0c;过山车、激流勇进、大摆锤、主题餐厅。大家非常喜欢&#xff0c;刚开业不久就收获了很多游客的青睐。 运营了一段时间之…

GD32调试篇:STLINK驱动下载安装

本文章基于兆易创新GD32 MCU所提供的2.2.4版本库函数开发 向上代码兼容GD32F450ZGT6中使用 后续项目主要在下面该专栏中发布&#xff1a; https://blog.csdn.net/qq_62316532/category_12608431.html?spm1001.2014.3001.5482 感兴趣的点个关注收藏一下吧! 电机驱动开发可以跳转…

stablediffusion的controlnet线稿只能用1.5的底模吗,有XL能用的线稿模型吗?

推荐一个超级好用的 SDXL-ControlNet 模型&#xff1a;Anytest&#xff0c;不止是线稿转绘&#xff0c;还能帮你补全线稿以及二创哦&#xff01; 而且操作很简单&#xff0c;不需要预处理器&#xff0c;直接使用即可。 基础的功能包括根据线稿生成图像&#xff0c;对图片进行…

揭秘Kafka:大数据和流计算领域的高可用利器

我是小米,一个喜欢分享技术的29岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货! 哈喽,大家好,我是小米,一个积极活泼、热爱技术分享的大哥哥!今天我们来聊聊在大数据和流计算领域备受推崇的消息系统——Kafka。Kafka以其高效、可伸…

APaaS:智能制造助手

资金不足、IT基础架构薄弱...... 车间业务需求不断地在增加...... 都在说数字化&#xff0c;都在说转型...... 随着企业竞争的日益激烈和市场环境的快速变化&#xff0c;企业需要一个灵活、高效、快速响应市场变化的新平台。在这样的背景下&#xff0c;APaaS应运而生&#x…

从0开始C++(一)

目录 c的基本介绍 C语言和C 的区别 面向过程和面向对象的区别 引用 引用使用的注意事项 赋值 终端输入 cin getline string字符串类 遍历方式 字符串和数字转换 函数 内联函数 函数重载overload 小练习&#xff1a; 参考代码 c的基本介绍 C是一种通用的高级编…

银行卡归属地查询-银行卡归属地接口-银行卡归属地API

接口简介&#xff1a;通过银行卡号查询国内外银行名称、银行卡卡种、卡品牌以及银行卡发卡省份和城市&#xff0c;支持借记卡和部分贷记卡的发卡省市查询。 若银行卡是农村信用社&#xff0c;归属地无法区分到城市&#xff0c;只能到省份 接口地址&#xff1a;https://www.wapi…

高考分数线一分一段统计汇总(熟练SQL窗口函数)

高考分数线一分一段统计汇总(使用SQL窗口函数) select 总分数&#xff0c; 一分一段人数&#xff0c; sum(一分一段人数) over( order by 总分数 desc) as 累计排名 from( select 总分数&#xff0c; count(考生号) as 一分一段人数 from &#xff08; select 考生号&…

【保姆级教程】Linux 基于 Docker 部署 MySQL 和 Nacos 并配置两者连接

一、Linux 部署 Docker 1.1 卸载旧版本&#xff08;如有&#xff09; sudo yum remove docker \docker-client \docker-client-latest \docker-common \docker-latest \docker-latest-logrotate \docker-logrotate \docker-engine1.2 安装 yum-utils 包 sudo yum install -y…

深入理解并打败C语言难关之一————指针(3)

前言&#xff1a; 昨天把指针最为基础的内容讲完了&#xff0c;并且详细说明了传值调用和传址调用的区别&#xff08;这次我也是做到了每日一更&#xff0c;感觉有好多想写的但是没有写完&#xff09;&#xff0c;下面不多废话&#xff0c;下面进入本文想要说的内容 目录&#…