1. LeekCode页面效果

image-llfr.png

2. 效果展示

image-fqbh.png

3. 主要思路

  • 数学思维
  • Pinia持久化存储布局数据结构
  • N叉树的遍历和递归处理
  • 前端拖拽API以及DOM操作
  • Vue组件

4. 核心代码

<!-- @author 葡萄w -->
<!-- 仿力扣答题页面 -->
<!-- 原创 -->
<template>
    <div id="show" class="showDom"/>
    <div id="#flexShadow" style="padding: 2px" class="shadow"/>
</template>

<script setup>
// ===================================== 引入子组件,根据obj生成dom
import {createApp, onMounted} from "vue";
import test from "@/ViewsFront/questionDetail/child/test.vue";
import {flexBox, flexPageStore} from "@/stores/flexPageStore.ts";
import {storeToRefs} from "pinia";
const {flexMemo, childBoxsType} = storeToRefs(flexPageStore());
const obj = flexMemo.value
const flexType = childBoxsType.value
const domMinWidth = 39
const domMinHeight = 39
const navbarHeight = 60
let show;
// ======================================= 绑定 XY 动态布局
function getDomPercentageAndPx(str) {
    try {
        return str.match(/(\d+(\.\d+)?)|(\.\d+)/g).map(match => parseFloat(match))
    }catch (e) {
        return [100,0]
    }
}
const handleResize = (pn) => {
    const nodes = pn.childNodes;
    const n = nodes.length
    const boxCnt = (n + 1) / 2
    const resizeCnt = n - boxCnt
    const pd = (n - 1) * p / (n + 1)
    for (let i = 1; i < n; i+=2) {
        const ln = nodes[i].previousElementSibling
        const nn = nodes[i]
        const rn = nodes[i].nextElementSibling
        if(pn.dataset.childType === '0') {
            ln.style.width = `calc(${getDomPercentageAndPx(ln.style.width)[0]}% - ${pd}px)`
            rn.style.width = `calc(${getDomPercentageAndPx(rn.style.width)[0]}% - ${pd}px)`
            nn.onmousedown = e => {
                const startX = e.clientX;
                const tl = ln.offsetWidth;
                const tr = rn.offsetWidth;
                document.onmousemove = e => {
                    const endX = e.clientX;
                    const resizeSum = resizeCnt * nn.offsetWidth
                    const maxWidth = pn.clientWidth - resizeSum
                    let leftSize = tl + endX - startX
                    let rightSize = tr - (endX - startX)
                    if (leftSize < domMinWidth) {
                        rightSize = leftSize + rightSize - domMinWidth
                        leftSize = domMinWidth;
                    }
                    if (rightSize < domMinWidth) {
                        leftSize = leftSize + rightSize - domMinWidth
                        rightSize = domMinWidth;
                    }
                    ln.style.width =
                        `calc( ${(leftSize / maxWidth) * 100}% - ${pd}px )`;
                    rn.style.width =
                        `calc( ${(rightSize / maxWidth) * 100}% - ${pd}px )`;
                };
                document.onmouseup = () => {
                    document.onmousemove = null;
                    document.onmouseup = null;
                    nn.releaseCapture && nn.releaseCapture();
                    memoDomToObj()
                };
                nn.setCapture && nn.setCapture();
                return false;
            };
        }
        if(pn.dataset.childType === '1') {// 纵向排布

            ln.style.height = `calc(${getDomPercentageAndPx(ln.style.height)[0]}% - ${pd}px)`
            rn.style.height = `calc(${getDomPercentageAndPx(rn.style.height)[0]}% - ${pd}px)`

            nn.onmousedown = e => {
                const startY = e.clientY;
                const tt = ln.offsetHeight;
                const tb = rn.offsetHeight;
                document.onmousemove = e => {
                    const endY = e.clientY;
                    const resizeSum = resizeCnt * nn.offsetHeight
                    const maxHeight = pn.clientHeight - resizeSum
                    let topSize = tt + endY - startY
                    let bomSize = tb - (endY - startY)
                    if (topSize < domMinHeight) {
                        bomSize = topSize + bomSize - domMinHeight
                        topSize = domMinHeight;
                    }
                    if (bomSize < domMinHeight) {
                        topSize = topSize + bomSize - domMinHeight
                        bomSize = domMinHeight;
                    }
                    ln.style.height =
                        `calc( ${(topSize / maxHeight) * 100}% - ${pd}px )`;
                    rn.style.height =
                        `calc( ${(bomSize / maxHeight) * 100}% - ${pd}px )`;
                };
                document.onmouseup = () => {
                    document.onmousemove = null;
                    document.onmouseup = null;
                    nn.releaseCapture && nn.releaseCapture();
                    memoDomToObj()
                };
                nn.setCapture && nn.setCapture();
                return false;
            };
        }
    }
}
// ======================================= 解析box,滚动条,组件,数据结构
const p = 3
function getBoxDom (type, Size) {
    const dom = document.createElement('div');
    if (type === 1) dom.style.height = `calc(${Size}% - 0px)`;
    else dom.style.width = `calc(${Size}% - 0px)`;
    return dom
}
function getResizeDom(type) {
    const dom = document.createElement('div');
    dom.dataset.domType = `${type}`
    dom.classList.add(type === 1 ? "resizeY" : "resizeX");
    return dom
}
function getComponent(titles) {
    const componentId = Math.random()
    const elementApp = createApp(test,
        {'message':'生成组件','number':componentId ,'cardTitles': titles});
    const tempDiv = document.createElement('div');
    elementApp.mount(tempDiv);
    tempDiv.style.width = "calc(100% - 0px)";
    tempDiv.style.height = "calc(100% - 0px)";
    tempDiv.classList.add("innerCard")
    tempDiv.dataset.drop = "true"
    tempDiv.dataset.propsid = `${componentId}`
    return tempDiv
}
function parseObj (parent, obj, childBoxsType) {
    if(obj.content === null){
        childBoxsType = Number(childBoxsType)
        if (childBoxsType === 0)    parent.style.display = 'flex'
        const n = obj.childBoxs.length
        for (let i = 0; i < n; i++) {
            if (i > 0) parent.appendChild(getResizeDom(childBoxsType))
            const box = getBoxDom(childBoxsType, obj.childBoxs[i].size)
            parseObj(box, obj.childBoxs[i], !childBoxsType)
            parent.appendChild(box)
        }
        parent.dataset.childType = `${childBoxsType}`
        handleResize(parent)
    }else{
        parent.dataset.domType = "-1"
        parent.classList.add("box")
        parent.appendChild(getComponent(obj.content));
    }
}
function handlerTree(node) {
    console.log("处理含childType的节点",node)
    handleResize(node)
    const childes = node.childNodes
    const n = childes.length
    for (let i = 0; i < n; i++) {
        if(childes[i].dataset.childType !== undefined)
            handlerTree(childes[i])
    }
}
function memoDomToObj() {
    const res = []
    const node = show.firstChild
    dfs(node, res)
    childBoxsType.value = node.dataset.childType
    flexMemo.value = res[0]
}
function dfs(node, val) {
    if(node.dataset.childType !== undefined) {
        const tmp = new flexBox(null, parseFloat(getDomPercentageAndPx(node.dataset.childType === '0' ? node.style.height : node.style.width)[0]), [])
        val.push(tmp)
        const childes = node.childNodes
        const n = childes.length
        for (let i = 0; i < n; i++) {
            dfs(childes[i], tmp.childBoxs)
        }
    }else if(node.dataset.domType === '-1'){
        const titles = document.getElementById(`card${node.firstChild.dataset.propsid}`).childNodes;
        const n = titles.length
        const tmp = []
        for (let i = 0; i < n; i++) {
            const title = titles[i]
            if(title.dataset?.title) {
                tmp.push(title.dataset.title)
            }
        }
        val.push(new flexBox(tmp, parseFloat(getDomPercentageAndPx(node.parentNode.dataset.childType === '0' ? node.style.width : node.style.height)[0]), []))
    }
}
// ======================================= 定义拖拽api的实现
let x,y;// 鼠标位置
let source = null;// 拿起来的dom
let cloneSource = null// 拖拽渲染的cloneDom
let type = 0// 拖拽类型
let shadow;// 初始化与template中的shadow绑定
// ======================================= node操作获取drop和box以及对应下标
function getDropNode(node) {
    while(node) {
        if(node.dataset?.drop) {
            return node;
        }
        node = node.parentNode
    }
}
function getBoxNode(node) {
    while(node) {
        if(node.dataset?.domType === "-1") {
            return node;
        }
        node = node.parentNode
    }
}
function atDomIndex(dom) {
    return Array.prototype.indexOf.call(dom.parentNode.children, dom);
}
// ================================================ 判断方位的工具函数
function hereLog(dropTo, val) {
    let rect = dropTo.getBoundingClientRect();
    const w = rect.width, h = rect.height;
    const sx = rect.left, sy = rect.top;

    if (type !== val) {
        if(val === 1){
            shadow.style.display = "block"
            shadow.style.left =    `${sx}px`
            shadow.style.top =     `${sy - navbarHeight}px`
            shadow.style.height =  `${h/2}px`
            shadow.style.width =   `${w}px`
        }else if(val === 2){
            shadow.style.display = "block"
            shadow.style.left =    `${sx+w/2}px`
            shadow.style.top =     `${sy - navbarHeight}px`
            shadow.style.height =  `${h}px`
            shadow.style.width =   `${w/2}px`
        }else if(val === 3){
            shadow.style.display = "block"
            shadow.style.left =    `${sx}px`
            shadow.style.top =     `${sy+h/2 - navbarHeight}px`
            shadow.style.height =  `${h/2}px`
            shadow.style.width =   `${w}px`
        }else if(val === 4){
            shadow.style.display = "block"
            shadow.style.left =    `${sx}px`
            shadow.style.top =     `${sy - navbarHeight}px`
            shadow.style.height =  `${h}px`
            shadow.style.width =   `${w/2}px`
        }else if(val === 5){
            shadow.style.display = "block"
            shadow.style.left =    `${sx}px`
            shadow.style.top =     `${sy - navbarHeight}px`
            shadow.style.height =  `${h}px`
            shadow.style.width =   `${w}px`
        }
        type = val
    }
}
function getTitleWhere(onTitle) {
    if (onTitle.id.slice(0,4) !== 'card') {
        let idx = atDomIndex(onTitle) + 1
        const tmp = onTitle.getBoundingClientRect();
        const w = tmp.width, h = tmp.height;
        const sx = tmp.left, sy = tmp.top;
        const cx = tmp.width / 2 + tmp.left;
        idx += (x > cx)
        // 渲染shadow
        if(type !== -idx) {
            if(x < cx) {
                shadow.style.display = "block"
                shadow.style.left =    `${sx - 1}px`
                shadow.style.top =     `${sy + 0.2 * h - navbarHeight}px`
                shadow.style.height =  `${h * 0.6}px`
                shadow.style.width =   `1px`
            }else {
                shadow.style.display = "block"
                shadow.style.left =    `${sx + w - 1}px`
                shadow.style.top =     `${sy + 0.2 * h - navbarHeight}px`
                shadow.style.height =  `${h * 0.6}px`
                shadow.style.width =   `1px`
            }
            type = -idx
        }

    }else {
        const n = onTitle.childNodes.length;
        const tmp = onTitle.childNodes[n-1].getBoundingClientRect();
        const w = tmp.width, h = tmp.height;
        const sx = tmp.left, sy = tmp.top;
        if(type !== -(n+1)) {
            shadow.style.display = "block"
            shadow.style.left =    `${sx + w - 1}px`
            shadow.style.top =     `${sy + 0.2 * h - navbarHeight}px`
            shadow.style.height =  `${h * 0.6}px`
            shadow.style.width =   `1px`
            type = -(n+1)
        }
    }
}
function getCardWhere(dropTo) {
    let rect = dropTo.getBoundingClientRect();
    const w = rect.width, h = rect.height;
    const o1x = rect.left, o1y = rect.top;
    const o5x = rect.left + w / 4, o5y = rect.top + h / 4;
    const o6x = rect.left + w * 3 / 4, o6y = rect.top + h * 3 / 4;
    const cx = o1x + w / 2, cy = o1y + h / 2;
    if(x >= o5x && x <= o6x && y >= o5y && y <= o6y) // 判断5方位
        hereLog(dropTo, 5)
    else if(x >= o5x && x <= o6x) // 判断8 2方位
        hereLog(dropTo, y <= cy ? 1 : 3)
    else if(y >= o5y && y <= o6y) // 判断4 6方位
        hereLog(dropTo, x <= cx ? 4 : 2)
    else if(x < cx && y < cy) // 1
        hereLog(dropTo, y < h * (x - o1x) / w + o1y ? 1 : 4)
    else if(x > cx && y < cy) // 3
        hereLog(dropTo, x < w * (o1y + h - y) / h + o1x ? 1 : 2)
    else if(x < cx && y > cy) // 7
        hereLog(dropTo, x < w * (o1y + h - y) / h + o1x ? 4 : 3)
    else if(x > cx && y > cy) // 9
        hereLog(dropTo, y < h * (x - o1x) / w + o1y ? 2 : 3)
    else
        hereLog(dropTo, 0)
}
// ================================================ 钩子函数
onMounted(() => {
    document.addEventListener("drag", function (event) {
        x = event.clientX;
        y = event.clientY;
    });
    // 初始化所有页面组件
    // initMemo()
    shadow = document.getElementById('#flexShadow')
    // 向展示盒show添入解析完的数据
    show = document.getElementById('show');
    const parent = document.createElement('div');
    parent.dataset.ancestor = "true"
    parent.dataset.childType = `${flexType}`
    parent.style.width = "calc(100% - 0px)";
    parent.style.height = "calc(100% - 0px)";
    parseObj(parent, obj, flexType);
    show.appendChild(parent);


    // 抓起
    show.ondragstart = (e) => {
        source = e.target
        // 防卡边界
        if(cloneSource) document.body.removeChild(cloneSource);
        cloneSource = null
        // 去除默认效果
        cloneSource = source.cloneNode(true)
        cloneSource.draggable = false
        const img = new Image();
        img.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' %3E%3Cpath /%3E%3C/svg%3E";
        e.dataTransfer.setDragImage(img, 0, 0);
        document.body.appendChild(cloneSource);
    }
    // 拖动
    show.ondragover = (e) => {
        // 添加试图效果
        cloneSource.style = 'position:fixed;left:0;top:0;z-index:999;pointer-events:none;transform:translate3d( ' + x + 'px ,' + y + 'px,0);'
        e.preventDefault()
        const dropTo = getDropNode(e.target)

        if (dropTo)  {
            if (dropTo.id.slice(0,4) === "card") {
                getTitleWhere(e.target)
            }else {
                getCardWhere(dropTo)
            }
        }else {
            type = 0
        }
    }
    // 放下
    show.ondrop = (e) => {
        try {
            const comeParent = source.parentNode
            const comeTitle = source.textContent
            const comeParentId = source.parentNode.dataset.propsid
            const comeShow = document.getElementById(`content${comeParentId}`);
            const comeShowTitle = comeShow.childNodes[0].id.slice(14)
            const comeIsShow = comeTitle === comeShowTitle
            const isAllMove = comeParent.childNodes.length === 1

            console.log('所选组件span',source)
            console.log('所选组件', comeTitle)
            console.log('所选目录',comeParent)
            console.log('所选目录ID',comeParentId)
            console.log('所选目录展示盒',comeShow)
            console.log('所选目录展示盒组件',comeShowTitle)
            console.log('所选组件是否被展示',comeIsShow)
            console.log('是否要全部挪走',isAllMove)
            const dom = getDropNode(e.target);
            const dropToId = dom.dataset.propsid
            const selfFun = comeParentId === dropToId
            console.log('自环操作',selfFun)

            let deleteBox = null,
                downDeepBox = getBoxNode(source),
                nextClick = null,
                cutDownDeep = null;
            if(!selfFun && isAllMove) {
                deleteBox = downDeepBox;
            }
            downDeepBox = downDeepBox.parentNode;

            // 获取被移走区域的下一个要显示的组件
            // 必须先获取再执行,否则在自环情况下
            // 会导致source获取的是组件的信息,此处执行click使组件回到memo,source无法冒泡到card上

            if(comeIsShow && !selfFun && !isAllMove && type !== 0) {
                const spans = comeParent.childNodes
                const idx = atDomIndex(source)
                nextClick = spans[(idx - 1 >= 0 ? idx - 1 : idx + 1)]
            }
;            if(type  <  0){ // 插入逻辑
                const dropTitleList = dom.childNodes;
                dom.insertBefore(source, dropTitleList[-type - 1])
                source.click()
            }
            if(type === 1){
                // 先获取父级盒子
                const pn = getBoxNode(e.target).parentNode
                // 获取放下的目标盒子
                const rightDom = dom.parentNode
                // 重点:若父盒子为祖父盒子,情况为仅有一个盒子,此时上部需要竖向排列
                if(pn.dataset.ancestor && pn.childNodes.length === 1) {
                    // 竖向排列
                    pn.dataset.childType = '1'
                    pn.style.display = ''
                    // 仅有一个盒子,竖向排布将长宽调换
                    rightDom.style.height = `calc(${getDomPercentageAndPx(rightDom.style.width)[0]}% - 0px)`
                    rightDom.style.width = ""
                }
                // 得到父盒子的排布规则
                const parentChildType = pn.dataset.childType


                // 横向排布情况:横向盒子的上部操作为将该盒子拉入一个竖向排布的布局,该盒子在上,dropTo盒子在下
                // 此时还未添加盒子,仅仅是将dropTo盒子创建父级
                if(parentChildType === '0') {
                    const tmp = getDomPercentageAndPx(rightDom.style.width)[0]
                    const parentBox = getBoxDom(0, tmp)
                    parentBox.dataset.childType = '1'
                    rightDom.before(parentBox)
                    parentBox.appendChild(rightDom)
                    rightDom.style.width = ""
                    rightDom.style.height = `calc(100% - 0px)`
                    cutDownDeep = parentBox
                }

                // 横向排布已经将dropTo盒子拉入创建好的父级盒子中
                // 竖向排布直接向dropTo盒子上方插入即可

                const tmp = getDomPercentageAndPx(rightDom.style.height)[0]
                // 自己单独一个挪到自己上方的情况,
                if(selfFun && isAllMove) {
                    const otherDom = rightDom?.previousElementSibling?.previousElementSibling
                        ?? rightDom?.nextElementSibling?.nextElementSibling
                    otherDom.style.height = `calc(${getDomPercentageAndPx(otherDom.style.height)[0] + tmp/2}% - 0px)`
                    rightDom.style.height = `calc(${tmp/2}% - 0px)`
                }else{
                    const leftDom = getBoxDom(1, tmp/2)
                    rightDom.style.height = `calc(${tmp/2}% - 0px)`
                    leftDom.dataset.domType = "-1"
                    leftDom.classList.add("box")
                    rightDom.before(leftDom)
                    leftDom.appendChild(getComponent([source.textContent]))
                    rightDom.before(getResizeDom(1))
                    source.remove()
                    document.getElementById(`card${rightDom.firstChild.dataset.propsid}`)?.firstChild?.click()
                }

            }
            if(type === 2){
                console.log('处理右方')

                const pn = getBoxNode(e.target).parentNode
                const leftDom = dom.parentNode
                if(pn.dataset.ancestor && pn.childNodes.length === 1) {
                    // 横向排列
                    pn.dataset.childType = '0'
                    pn.style.display = 'flex'
                    // 仅有一个盒子,横向排布将长宽调换
                    leftDom.style.width = `calc(${getDomPercentageAndPx(leftDom.style.height)[0]}% - 0px)`
                    leftDom.style.height = ""
                }
                // 得到父盒子的排布规则
                const parentChildType = pn.dataset.childType


                // 竖向排布情况:横向盒子的右部操作为将该盒子拉入一个横向排布的布局,该盒子在左,dropTo盒子在右
                // 此时还未添加盒子,仅仅是将dropTo盒子创建父级
                if(parentChildType === '1') {
                    const tmp = getDomPercentageAndPx(leftDom.style.height)[0]
                    const parentBox = getBoxDom(1, tmp)
                    parentBox.dataset.childType = '0'
                    leftDom.before(parentBox)
                    parentBox.appendChild(leftDom)
                    parentBox.style.display = `flex`
                    leftDom.style.height = ""
                    leftDom.style.width = `calc(100% - 0px)`
                    cutDownDeep = parentBox
                }


                const tmp = getDomPercentageAndPx(leftDom.style.width)[0]
                // 自己单独一个挪到自己上方的情况,
                if(selfFun && isAllMove) {
                    const otherDom = leftDom?.previousElementSibling?.previousElementSibling
                        ?? leftDom?.nextElementSibling?.nextElementSibling
                    otherDom.style.width = `calc(${getDomPercentageAndPx(otherDom.style.width)[0] + tmp/2}% - 0px)`
                    leftDom.style.width = `calc(${tmp/2}% - 0px)`
                }else{
                    const rightDom = getBoxDom(0, tmp/2)
                    leftDom.style.width = `calc(${tmp/2}% - 0px)`
                    rightDom.dataset.domType = "-1"
                    rightDom.classList.add("box")
                    leftDom.after(rightDom)
                    rightDom.appendChild(getComponent([source.textContent]))
                    leftDom.after(getResizeDom(0))
                    source.remove()
                    document.getElementById(`card${leftDom.firstChild.dataset.propsid}`)?.firstChild?.click()
                }
            }
            if(type === 3){
                const pn = getBoxNode(e.target).parentNode
                const leftDom = dom.parentNode

                if(pn.dataset.ancestor && pn.childNodes.length === 1) {
                    pn.dataset.childType = '1'
                    pn.style.display = ''
                    leftDom.style.height = `calc(${getDomPercentageAndPx(leftDom.style.width)[0]}% - 0px)`
                    leftDom.style.width = ""
                }
                const parentChildType = pn.dataset.childType



                if(parentChildType === '0') {
                    const tmp = getDomPercentageAndPx(leftDom.style.width)[0]
                    const parentBox = getBoxDom(0, tmp)
                    parentBox.dataset.childType = '1'
                    leftDom.before(parentBox)
                    parentBox.appendChild(leftDom)
                    leftDom.style.width = ""
                    leftDom.style.height = `calc(100% - 0px)`
                    cutDownDeep = parentBox
                }

                // 横向排布已经将dropTo盒子拉入创建好的父级盒子中
                // 竖向排布直接向dropTo盒子上方插入即可

                const tmp = getDomPercentageAndPx(leftDom.style.height)[0]
                if(selfFun && isAllMove) {
                    const otherDom = leftDom?.previousElementSibling?.previousElementSibling
                        ?? leftDom?.nextElementSibling?.nextElementSibling
                    otherDom.style.height = `calc(${getDomPercentageAndPx(otherDom.style.height)[0] + tmp/2}% - 0px)`
                    leftDom.style.height = `calc(${tmp/2}% - 0px)`
                }else{
                    const rightDom = getBoxDom(1, tmp/2)
                    leftDom.style.height = `calc(${tmp/2}% - 0px)`
                    rightDom.dataset.domType = "-1"
                    rightDom.classList.add("box")
                    leftDom.after(rightDom)
                    rightDom.appendChild(getComponent([source.textContent]))
                    leftDom.after(getResizeDom(1))
                    source.remove()
                    document.getElementById(`card${leftDom.firstChild.dataset.propsid}`)?.firstChild?.click()
                }
            }
            if(type === 4){
                console.log('处理左方')
                // 先获取父级盒子
                const pn = getBoxNode(e.target).parentNode
                // 获取放下的目标盒子
                const rightDom = dom.parentNode
                // 重点:若父盒子为祖父盒子,情况为仅有一个盒子,此时需要横向排列
                if(pn.dataset.ancestor && pn.childNodes.length === 1) {
                    // 横向排列
                    pn.dataset.childType = '0'
                    pn.style.display = 'flex'
                    // 仅有一个盒子,横向排布将长宽调换
                    rightDom.style.width = `calc(${getDomPercentageAndPx(rightDom.style.height)[0]}% - 0px)`
                    rightDom.style.height = ""
                }
                // 得到父盒子的排布规则
                const parentChildType = pn.dataset.childType


                // 竖向排布情况:横向盒子的右部操作为将该盒子拉入一个横向排布的布局,该盒子在左,dropTo盒子在右
                // 此时还未添加盒子,仅仅是将dropTo盒子创建父级
                if(parentChildType === '1') {
                    console.log("sfsafasfafaf")
                    const tmp = getDomPercentageAndPx(rightDom.style.height)[0]
                    const parentBox = getBoxDom(1, tmp)
                    parentBox.dataset.childType = '0'
                    rightDom.before(parentBox)
                    parentBox.appendChild(rightDom)
                    parentBox.style.display = `flex`
                    rightDom.style.height = ""
                    rightDom.style.width = `calc(100% - 0px)`
                    cutDownDeep = parentBox
                }


                const tmp = getDomPercentageAndPx(rightDom.style.width)[0]
                // 自己单独一个挪到自己上方的情况,
                if(selfFun && isAllMove) {
                    const otherDom = rightDom?.previousElementSibling?.previousElementSibling
                        ?? rightDom?.nextElementSibling?.nextElementSibling
                    otherDom.style.width = `calc(${getDomPercentageAndPx(otherDom.style.width)[0] + tmp/2}% - 0px)`
                    rightDom.style.width = `calc(${tmp/2}% - 0px)`
                }else{
                    const leftDom = getBoxDom(0, tmp/2)
                    rightDom.style.width = `calc(${tmp/2}% - 0px)`
                    leftDom.dataset.domType = "-1"
                    leftDom.classList.add("box")
                    rightDom.before(leftDom)
                    leftDom.appendChild(getComponent([source.textContent]))
                    rightDom.before(getResizeDom(0))
                    source.remove()
                    document.getElementById(`card${rightDom.firstChild.dataset.propsid}`)?.firstChild?.click()
                }
            }
            if(type === 5 && !selfFun){
                document
                    .getElementById(`card${getBoxNode(e.target).firstChild.dataset.propsid}`)
                    .appendChild(source)
                source.click()
            }
            // 切换显示
            nextClick?.click()
            // ======= 删除空Box逻辑
            if(deleteBox && type !== 0) {
                const boxParent = deleteBox.parentNode
                const childType = boxParent.dataset.childType;
                const nodes = deleteBox.parentNode.childNodes
                const idx = Array.prototype.indexOf.call(boxParent.children, deleteBox)

                const leftDom  = idx - 2 >= 0 ? nodes[idx - 2] : null
                const rightDom = idx + 2 < nodes.length ? nodes[idx + 2] : null

                // 得到%
                const a = getDomPercentageAndPx(childType === "0" ? deleteBox.style.width:deleteBox.style.height)[0]
                // 将剩余组件移动到缓存区
                const component = document.getElementById(`content${comeParentId}`).firstChild;
                if (component) {
                    document.getElementById('componentMemo').appendChild(component);
                }
                // 注意顺序
                boxParent.removeChild(nodes[(idx - 1 >= 0 ? idx - 1 : idx + 1)])
                boxParent.removeChild(deleteBox)

                const n = nodes.length
                const pd = (n-1) * p / (n+1)

                if(leftDom && rightDom) {
                    const ls = getDomPercentageAndPx(childType === "0" ? leftDom.style.width:leftDom.style.height)[0]
                    const rs = getDomPercentageAndPx(childType === "0" ? rightDom.style.width:rightDom.style.height)[0]
                    if(childType === "0"){
                        leftDom.style.width = `calc(${ls + a/2}% - ${pd}px)`;
                        rightDom.style.width = `calc(${rs + a/2}% - ${pd}px)`;
                    }else {
                        leftDom.style.height = `calc(${ls + a/2}% - ${pd}px)`;
                        rightDom.style.height = `calc(${rs + a/2}% - ${pd}px)`;
                    }
                }else if(leftDom) {
                    const ls = getDomPercentageAndPx(childType === "0" ? leftDom.style.width:leftDom.style.height)[0]
                    if(childType === "0"){
                        leftDom.style.width = `calc(${ls + a}% - ${pd}px)`;
                    }else {
                        leftDom.style.height = `calc(${ls + a}% - ${pd}px)`;
                    }

                }else if(rightDom) {
                    const rs = getDomPercentageAndPx(childType === "0" ? rightDom.style.width:rightDom.style.height)[0]
                    if(childType === "0") rightDom.style.width = `calc(${rs + a}% - ${pd}px)`;
                    else rightDom.style.height = `calc(${rs + a}% - ${pd}px)`;
                }
                // handleResize(boxParent)
            }
            // ======= 降级逻辑1(两个元素移除一个导致的降级)
            if(downDeepBox.childNodes.length === 1 && downDeepBox.parentNode.id !== 'show') {
                const fa = downDeepBox
                const ch = downDeepBox.firstChild
                console.log('触发降级', fa)
                // downDeepBox.firstChild.style.height = downDeepBox.height
                // downDeepBox.firstChild.style.width = downDeepBox.width

                // const faLevelType = fa.parentNode.dataset.childType
                ch.style.width = fa.style.width
                ch.style.height = fa.style.height
                // ch.style.display = `${faLevelType === '0' ? '':'flex' }`
                fa.parentNode.insertBefore(ch, fa)
                fa.remove()
                // handleResize(ch.parentNode)
            }
            // ======= 降级逻辑2(分裂时生成的元素与父元素同类型导致的降级)
            if(cutDownDeep && (cutDownDeep.dataset.childType === cutDownDeep.parentNode.dataset.childType)) {
                console.log("降级逻辑2")
                const boxType = cutDownDeep.dataset.childType
                const nodes = cutDownDeep.childNodes
                const n = nodes.length
                const boxSize = getDomPercentageAndPx(boxType === '0'? cutDownDeep.style.width : cutDownDeep.style.height)[0];
                // 降级box 占父级boxSize%
                for (let i=n-1;i>=0;i--) {
                    const child = nodes[i]
                    if(child.dataset.domType === '-1') {
                        const childSize = getDomPercentageAndPx(boxType === '0'? child.style.width : child.style.height)[0];
                        if(boxType === '0') {
                            child.style.width = `calc(${boxSize * childSize / 100}% - 0px)`
                        }else {
                            child.style.height = `calc(${boxSize * childSize / 100}% - 0px)`
                        }
                    }
                    cutDownDeep.after(child)
                }
                cutDownDeep.remove()
            }
        }catch (e) {
            console.log(e)
        }finally {
            type = 0
            shadow.style.display = "none"
            shadow.style.height = `0px`
            shadow.style.width = `0px`
            source = null
            document.body.removeChild(cloneSource);
            cloneSource = null
            handlerTree(show.firstChild)
            memoDomToObj()
        }
    }

})

</script>

<style lang="less">
.showDom {
    width: 100vw;
    height: calc(100vh - var(--navbar-height));
    padding: 0.5vh 0.5vw;
    overflow: hidden;
}

.box {
    padding: 1px;
    overflow: hidden;
}

.innerCard {
    border-radius: 0.5vh;
    border: 1px solid oklch(var(--bc) / 0.3);
    overflow: hidden;

}


.resizeX {
    position: relative;
    width: 3px;
    cursor: col-resize;
    &:hover {
        background-color: #45a3ff;
    }
}
.resizeY {
    position: relative;
    height: 3px;
    cursor: row-resize;
    &:hover {
        background-color: #45a3ff;
    }
}

.shadow {
    position: absolute;
    pointer-events: none;

    top: 50vh;
    left: 50vw;
    width: 0;
    height: 0;
    display: none;
    border-radius: 10px;
    background-color: rgba(46, 227, 158, 0.2);
    border: 1px solid rgb(0, 122, 255);
    z-index: 1000;
    transition: top 0.3s,left 0.3s, width 0.3s, height 0.3s;
}

.flexTitleList{
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.flexTitleListLimit{
    flex-direction: column;
    height: auto;
}


</style>


// function traverseDOM(element) {
//     if (!element ) return;
//     const nodes = Array.from(element.childNodes).filter(node => {
//         return node.dataset?.domtype !== undefined
//     });
//     if (nodes.length === 0) return;
//     const parentDom = element
//     let idx = 0;
//     while(idx < nodes.length) {
//         while(idx < nodes.length && nodes[idx].dataset.domtype !== '0' && nodes[idx].dataset.domtype !== '1') idx += 1;
//         if (idx < nodes.length) {
//             if (nodes[idx].dataset.domtype === '0')
//                 handleResizeX(parentDom,nodes[idx-1],nodes[idx],nodes[idx+1])
//             if (nodes[idx].dataset.domtype === '1')
//                 handleResizeY(parentDom,nodes[idx-1],nodes[idx],nodes[idx+1])
//         }
//         idx+=1;
//     }
//     nodes.forEach(function(child) {
//         traverseDOM(child);
//     });
// }

5. 父级组件

<template>
    <question-detail-flex/>
    <div id="componentMemo" style="display: none">
        <div id="moveComponent-题目正文" class="questionComponentLayout">
            <question-content v-if="res" :body="res"/>
        </div>
        <div id="moveComponent-评论" class="questionComponentLayout">
            <common/>
        </div>
        <div id="moveComponent-代码编辑器" class="questionComponentLayout" style="overflow: hidden;">
            <code-editor
                :value="value"
                :language="coder"
                :handle-change="value_change"
                :language-change="language_change"
            />
        </div>
        <div id="moveComponent-提交记录" class="questionComponentLayout">
            <commit-list v-if="res" :question-id="res.id"/>
        </div>
        <div id="moveComponent-样例测试" class="questionComponentLayout">
            <question-test
                v-if="res"
                :question-id="res.id"
                :eg="res.eg === null ? [] : res.eg"
                :execute="questionJudge"
                v-model:language="coder.value"
                v-model:code="codeStr"
            />
        </div>
    </div>
</template>

6. 持久化仓库

import {ref} from 'vue'
import {defineStore} from 'pinia'
import {MessageType, openView} from "@/tools/notyfTool";

class flexBox {
    content: Array<string> | null;
    size: number;
    childBoxs: Array<flexBox>;

    constructor(content: any, size: number, childBoxs: Array<flexBox>) {
        this.content = content;
        this.size = size;
        this.childBoxs = childBoxs
    }
}

export {flexBox}

export const flexPageStore = defineStore('flexPage', () => {

    const defaultFlex =
        new flexBox(
            null, 100,
            [
                new flexBox(['题目正文', '提交记录'], 35, []),
                new flexBox(null, 65, [new flexBox(['代码编辑器'], 60, []), new flexBox(['样例测试','评论'], 40, [])]),
            ]
        )


    const flexMemo = ref(defaultFlex)
    const childBoxsType = ref(0)

    function reset() {
        flexMemo.value = defaultFlex
        openView(MessageType.Info, "重置布局成功")
    }


    return {flexMemo, childBoxsType, reset}
}, {
    persist: true
})