SOURCE

console 命令行工具 X clear

                    
>
console
class MarkdownParser {
    compile(nodes) {
        let matchArr
        let html = ''
        for (let i = 0, len = nodes.length; i < len; i++) {
            let text = ''
            let index = 0
            const node = nodes[i]
            if (node.nodeType === Node.ELEMENT_NODE) {
                if (node.innerHTML === '<br>') {
                    // 多个空行只算一个
                    html += `<div></div>`
                    while (nodes[i + 1].nodeType === Node.ELEMENT_NODE && nodes[i + 1].innerHTML === '<br>') {
                        i++
                    }

                    continue
                }

                index = node.dataset.index
                text = node.textContent.trim()
            } else {
                text = node.nodeValue.trim()
            }

            matchArr = text.match(/^#\s/)
                        || text.match(/^##\s/)
                        || text.match(/^###\s/)
                        || text.match(/^####\s/)
                        || text.match(/^#####\s/)
                        || text.match(/^######\s/)
                        || text.match(/^\*{3,}/)
                        || text.match(/^>/)
                        || text.match(/^\*\s/)
                        || text.match(/^\d*\.\s/)
                        || text.match(/^```/)
                        || text.match(/^\|.*\|/)

            if (matchArr) {
                let temp = ''
                const re1 = /^>/
                const re2 = /^\*\s/
                const re3 = /^\d*\.\s/
                const re4 = /^```/
                const re5 = /^\|.*\|/
                switch(matchArr[0]) {
                    case '# ':
                        html += `<h1 data-index="${index}">` + this.format(text.substring(2)) + '</h1>'
                        break
                    case '## ':
                        html += `<h2 data-index="${index}">` + this.format(text.substring(3)) + '</h2>'
                        break
                    case '### ':
                        html += `<h3 data-index="${index}">` + this.format(text.substring(4)) + '</h3>'
                        break
                    case '#### ':
                        html += `<h4 data-index="${index}">` + this.format(text.substring(5)) + '</h4>'
                        break
                    case '##### ':
                        html += `<h5 data-index="${index}">` + this.format(text.substring(6)) + '</h5>'
                        break
                    case '###### ':
                        html += `<h6 data-index="${index}">` + this.format(text.substring(7)) + '</h6>'
                        break
                    case text.match(/^\*{3,}/) && text.match(/^\*{3,}/)[0]:
                        html += text.replace(/^\*{3,}/g, '<hr>')
                        break
                    case '>':
                        while (i < len && nodes[i].textContent.match(re1)) {
                            const str = nodes[i].textContent
                            temp += '<div>' + this.format(str.slice(1)) + '</div>'
                            i++
                        }

                        i--
                        html += `<blockquote data-index="${index}">` + temp + '</blockquote>'
                        break
                    case '* ':
                        while (i < len && nodes[i].textContent?.match(re2)) {
                            const str = nodes[i].textContent
                            temp += `<li data-index="${nodes[i]?.dataset?.index}">` + this.format(str.slice(2)) + '</li>'
                            i++
                        }

                        i--
                        html += `<ul data-index="${index}">` + temp + '</ul>'
                        break
                    case text.match(/^\d*\.\s/) && text.match(/^\d*\.\s/)[0]:
                        while (i < len && nodes[i].textContent?.match(re3)) {
                            const str = nodes[i].textContent
                            temp += `<li data-index="${nodes[i]?.dataset?.index}">` + this.format(str.replace(/^\d*\.\s/, '')) + '</li>'
                            i++
                        } 

                        i--
                        html += `<ol data-index="${index}">` + temp + '</ol>'
                        break
                    case '```':
                        i++
                        while (i < len && !re4.test(nodes[i].textContent)) {
                            temp += `<div data-index="${nodes[i]?.dataset?.index}">` + escapeHTML(nodes[i].textContent) + '</div>'
                            i++
                        }
                        
                        html += `<pre data-index="${index}"><code>` + temp + '</code></pre>'
                        break
                    case text.match(/^\|.*\|/) && text.match(/^\|.*\|/)[0]:
                        let thRe = /^\[th\]/
                        let arr, j, jlen
                        while (i < len && re5.test(nodes[i].textContent)) {
                            arr = nodes[i].textContent.split('|')
                            temp += `<tr data-index="${nodes[i]?.dataset?.index}">`
                            for (j = 1, jlen = arr.length - 1; j < jlen; j++) {
                                if (thRe.test(arr[1])) {
                                    temp += '<th>' + arr[j] + '</th>'
                                } else {
                                    temp += '<td>' + arr[j] + '</td>'
                                }
                            }
                            temp += '</tr>'
                            temp = temp.replace('[th]', '')
                            i++
                        }

                        html += '<table>' + temp + '</table>'
                        break
                }
            } else if (text) {
                html += `<div data-index="${index}">` + this.format(text) + '</div>'
            }

        }

        return html
    }

    format(str) { 
        str = str.replace(/\s/g, '&nbsp') 

        const bold = str.match(/\*{2}[^*].*?\*{2}/g) // 惰性匹配
        if (bold) {
            for (let i = 0, len = bold.length; i < len; i++) {
                str = str.replace(bold[i], '<b>' + bold[i].substring(2, bold[i].length - 2) + '</b>')
            }
        }

        const italic = str.match(/\*[^*].*?\*/g)  
        if (italic) {
            for (let i = 0, len = italic.length; i < len; i++) {
                str = str.replace(italic[i], '<i>' + italic[i].substring(1, italic[i].length - 1) + '</i>')
            }
        }

        const code = str.match(/`[^`]*`/g)
        if (code) {
            for (let i = 0, len = code.length; i < len; i++) {
                str = str.replace(code[i], '<code>' + code[i].substring(1, code[i].length - 1) + '</code>')
            }
        }

        const img = str.match(/!\[.*\]\(.*\)/g)
        const re1 = /\(.*\)/
        const re2 = /\[.*\]/
        if (img) {
            for (let i = 0, len = img.length; i < len; i++) {
                const url = img[i].match(re1)[0]
                const title = img[i].match(re2)[0]
                str = str.replace(img[i], '<img src=' + url.substring(1, url.length - 1) + ' alt=' + title.substring(1, title.length -1) + '>')
            }
        }

        const a = str.match(/\[.*?\]\(.*?\)/g)
        if (a) {
            for (let i = 0, len = a.length; i < len; i++) {
                const url = a[i].match(re1)[0]
                const title = a[i].match(re2)[0]
                str = str.replace(a[i], '<a href=' + url.substring(1, url.length - 1)  + '>' + title.substring(1, title.length -1) + '</a>')
            }
        }

        return escapeHTML2(str)
    }
}

function escapeHTML(html) {
    return html.replace(/</g, '&lt;').replace(/>/g, '&gt;')
}

function escapeHTML2(html) {
    return html.replace(/<(\/)?script>/g, '&lt;$1script&gt;')
}

function $(selector) {
    return document.querySelector(selector)
}

// 修改光标位置
function changeCursorPosition(node) {
    const selection = window.getSelection()
    // 清除所有选区 如果是 Caret 类型,清除选区后变为 Range,如果不是 Range 类型,后面的 addRange() 就不起作用
    selection.removeAllRanges()
    const range = document.createRange()
    // 选中节点的内容
    range.selectNode(node)
    selection.addRange(range)
    // 取消选中并将光标移至选区最后
    selection.collapseToEnd()
}

// 清除复制后的内容样式
function clearTextStyle(e) {
    e.preventDefault()

    let text
    const clp = (e.originalEvent || e).clipboardData
    if (clp === undefined || clp === null) {
        text = window.clipboardData.getData('text') || ''
        if (text !== '') {
            if (window.getSelection) {
                var newNode = document.createElement('span')
                newNode.innerHTML = text
                window.getSelection().getRangeAt(0).insertNode(newNode)
            } else {
                document.selection.createRange().pasteHTML(text)
            }
        }
    } else {
        text = clp.getData('text/plain') || ''
        if (text !== '') {
            document.execCommand('insertText', false, text)
        }
    }
}

function throttle(delay) {
    let waitForCallFunc
    let canCall = true
    return function helper(callback, ...args) {
        if (!canCall) {
            if (callback) waitForCallFunc = callback
            return
        }

        callback(...args)
        canCall = false
        setTimeout(() => {
            canCall = true
            if (waitForCallFunc) {
                helper(waitForCallFunc, ...args)
                waitForCallFunc = null
            }
        }, delay)
    }
}

function debounce(delay) {
    let timer
    return function(callback, ...args) {
        clearTimeout(timer)
        timer = setTimeout(() => callback.call(null, ...args), delay)
    }
}

/**
 * 计算 dom 到容器顶部的距离
 * @param {HTMLElement} dom 需要计算的容器
 * @param {HTMLElement} topContainer 终止条件
 * @returns 
 */
 function getHeightToTop(dom) {
    let height = dom.offsetTop
    let parent = dom.offsetParent

    while (parent) {
        height += parent.offsetTop
        parent = parent.offsetParent
    }

    return height
}

// dom 是否在屏幕内
function isInScreen(dom) {
    const { top, bottom } = dom.getBoundingClientRect()
    return bottom >= 0 && top < window.innerHeight
}

// dom 在当前屏幕展示内容的百分比
function percentOfdomInScreen(dom) {
    const { height, bottom } = dom.getBoundingClientRect()
    if (bottom <= 0) return 0
    if (bottom >= height) return 1
    return bottom / height
}

function canNodeCalculate(node) {
    return (
        node.innerHTML 
        && node.innerHTML !== '<br>' 
        && !node.textContent.startsWith('```') 
        && isInScreen(node) 
        && percentOfdomInScreen(node) >= 0
    )
}

const editor = $('#editor')
const showDom = $('#show-content')
const markdown = new MarkdownParser()
showDom.innerHTML = markdown.compile(editor.children)

function onInput() {
    // 为每个元素加上索引,通过索引找到 markdown 渲染后的元素
    let index = 0
    const data = Array.from(editor.children)
    data.forEach(item => {
        delete item.dataset.index
        // 忽略 br 换行符和空文本字节
        if (item.tagName !== 'BR' && item.innerText.trim() !== '') {
            if (!item.children.length || (item.children.length === 1 && item.children[0].tagName === 'BR')) {
                item.dataset.index = index++
                return
            }

            // 这里主要是针对复制过来的有嵌套节点的内容
            const frag = document.createDocumentFragment()
            Array.from(item.childNodes).forEach(e => {
                if (e.nodeType === Node.TEXT_NODE) {
                    const div = document.createElement('div')
                    div.textContent = e.nodeValue
                    item.replaceChild(div, e)
                    div.dataset.index = index++
                    frag.appendChild(div)
                } else if (item.tagName !== 'BR') {
                    e.dataset?.index && delete e.dataset.index
                    e.dataset.index = index++
                    frag.appendChild(e)
                }
            })
            
            editor.replaceChild(frag, item)
            
            // 需要修改光标位置,不然光标会在复制内容的前面,修改后会在复制内容的后面
            changeCursorPosition(editor.querySelector(`[data-index="${index - 1}"]`))
        }
    })

    showDom.innerHTML = markdown.compile(editor.childNodes)
}

const debounceFn = debounce(100) // 防抖
editor.oninput = () => {
    debounceFn(onInput)
}

editor.onpaste = (e) => {
    clearTextStyle(e)
}

// 是否允许滚动
const canScroll = {
    editor: true,
    showDom: true,
}

const debounceFn2 = debounce(100) // 防抖
const throttleFn = throttle(50) // 节流
editor.onscroll = () => {
    if (!canScroll.editor) return

    canScroll.showDom = false
    throttleFn(onScroll, editor, showDom)
    debounceFn2(resumeScroll)
}

showDom.onscroll = () => {
    if (!canScroll.showDom) return

    canScroll.editor = false
    throttleFn(onScroll, showDom, editor)
    debounceFn(resumeScroll)
}

// 恢复滚动
function resumeScroll() {
    canScroll.editor = true
    canScroll.showDom = true
}

/**
 * 
 * @param {HTMLElement} scrollContainer 正在滚动的容器
 * @param {HTMLElement} ShowContainer 需要同步滚动的容器
 * @returns 
 */
function onScroll(scrollContainer, ShowContainer) {
    const scrollHeight = ShowContainer.scrollHeight
    // 滚动到底部
    if (scrollContainer.offsetHeight + scrollContainer.scrollTop >= scrollContainer.scrollHeight) {
        ShowContainer.scrollTo({ top: scrollHeight - ShowContainer.clientHeight })
        return
    }

    // 滚动到顶部
    if (scrollContainer.scrollTop === 0) {
        ShowContainer.scrollTo({ top: 0 })
        return
    }

    const nodes = Array.from(scrollContainer.children)
    for (const node of nodes) {
        // 从上往下遍历,找到第一个在屏幕内的元素
        if (canNodeCalculate(node)) {
            // 如果当前滚动的元素是 <pre> <table>
            if (node.tagName === 'PRE' || node.tagName === 'TABLE') {
                // 如果 pre 里面的子元素同步滚动了,则直接返回
                if (hasPreElementInScrollContainerScroll(node, ShowContainer)) return
                // 否则直接从下一个元素开始计算
                continue
            }

            const index = node.dataset.index
            const dom = ShowContainer.querySelector(`[data-index="${index}"]`)
            if (!dom) continue

            const percent = percentOfdomInScreen(node)
            const heightToTop = getHeightToTop(dom)
            const domNeedHideHeight = dom.offsetHeight * (1 - percent)
            ShowContainer.scrollTo({ top: heightToTop + domNeedHideHeight })
            break
        }
    }
}

function hasPreElementInScrollContainerScroll(preElement, ShowContainer) {
    for (const node of preElement.children[0].children) {
        // 从上往下遍历,找到第一个在屏幕内的元素
        if (isInScreen(node) && percentOfdomInScreen(node) >= 0) {
            const index = node.dataset.index
            const dom = ShowContainer.querySelector(`[data-index="${index}"]`)
            if (!dom) continue

            const percent = percentOfdomInScreen(node)
            const heightToTop = getHeightToTop(dom)
            const domNeedHideHeight = dom.offsetHeight * (1 - percent)
            ShowContainer.scrollTo({ top: heightToTop + domNeedHideHeight })
            return true
        }
    }

    return false
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>markdown 编辑器双屏同步滚动 demo4</title>
    <link rel="stylesheet" href="index.css">
</head>
<body>
    <div id="editor" contenteditable class="text-container">
        <div><br></div><div data-index="0">本文是对[《可视化拖拽组件库一些技术要点原理分析》](https://juejin.cn/post/6908502083075325959)的补充。上一篇文章主要讲解了以下几个功能点:</div><div data-index="1">1. 编辑器</div><div data-index="2">2. 自定义组件</div><div data-index="3">3. 拖拽</div><div data-index="4">4. 删除组件、调整图层层级</div><div data-index="5">5. 放大缩小</div><div data-index="6">6. 撤消、重做</div><div data-index="7">7. 组件属性设置</div><div data-index="8">8. 吸附</div><div data-index="9">9. 预览、保存代码</div><div data-index="10">10. 绑定事件</div><div data-index="11">11. 绑定动画</div><div data-index="12">12. 导入 PSD</div><div data-index="13">13. 手机模式</div><div><br></div><div data-index="14">现在这篇文章会在此基础上再补充 4 个功能点,分别是:</div><div data-index="15">* 拖拽旋转</div><div data-index="16">* 复制粘贴剪切</div><div data-index="17">* 数据交互</div><div data-index="18">* 发布</div><div><br></div><div data-index="19">和上篇文章一样,我已经将新功能的代码更新到了 github:</div><div data-index="20">* [github 项目地址](https://github.com/woai3c/visual-drag-demo)</div><div data-index="21">* [在线预览](https://woai3c.github.io/visual-drag-demo)</div><div><br></div><div data-index="22">**友善提醒**:建议结合源码一起阅读,效果更好(这个 DEMO 使用的是 Vue 技术栈)。</div><div><br></div><div data-index="23">## 14. 拖拽旋转</div><div data-index="24">在写上一篇文章时,原来的 DEMO 已经可以支持旋转功能了。但是这个旋转功能还有很多不完善的地方:</div><div data-index="25">1. 不支持拖拽旋转。</div><div data-index="26">2. 旋转后的放大缩小不正确。</div><div data-index="27">3. 旋转后的自动吸附不正确。</div><div data-index="28">4. 旋转后八个可伸缩点的光标不正确。</div><div><br></div><div data-index="29">这一小节,我们将逐一解决这四个问题。</div><div><br></div><div data-index="30">### 拖拽旋转</div><div data-index="31">拖拽旋转需要使用 [Math.atan2()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Math/atan2) 函数。</div><div><br></div><div data-index="32">&gt;Math.atan2() 返回从原点(0,0)到(x,y)点的线段与x轴正方向之间的平面角度(弧度值),也就是Math.atan2(y,x)。Math.atan2(y,x)中的y和x都是相对于圆点(0,0)的距离。</div><div><br></div><div data-index="33">简单的说就是以组件中心点为原点 `(centerX,centerY)`,用户按下鼠标时的坐标设为 `(startX,startY)`,鼠标移动时的坐标设为 `(curX,curY)`。旋转角度可以通过 `(startX,startY)` 和 `(curX,curY)` 计算得出。</div><div><br></div><div data-index="34">![](https://img-blog.csdnimg.cn/img_convert/c2de0e4cd42f3c2a2a74b2fa5d38f8e6.png)</div><div><br></div><div data-index="35">那我们如何得到从点 `(startX,startY)` 到点 `(curX,curY)` 之间的旋转角度呢?</div><div><br></div><div data-index="36">**第一步**,鼠标点击时的坐标设为 `(startX,startY)`:</div><div data-index="37">```js</div><div data-index="38">const startY = e.clientY</div><div data-index="39">const startX = e.clientX</div><div data-index="40">```</div><div data-index="41">**第二步**,算出组件中心点:</div><div data-index="42">```js</div><div data-index="43">// 获取组件中心点位置</div><div data-index="44">const rect = this.$el.getBoundingClientRect()</div><div data-index="45">const centerX = rect.left + rect.width / 2</div><div data-index="46">const centerY = rect.top + rect.height / 2</div><div data-index="47">```</div><div data-index="48">**第三步**,按住鼠标移动时的坐标设为 `(curX,curY)`:</div><div data-index="49">```js</div><div data-index="50">const curX = moveEvent.clientX</div><div data-index="51">const curY = moveEvent.clientY</div><div data-index="52">```</div><div data-index="53">**第四步**,分别算出 `(startX,startY)` 和 `(curX,curY)` 对应的角度,再将它们相减得出旋转的角度。另外,还需要注意的就是 `Math.atan2()` 方法的返回值是一个弧度,因此还需要将弧度转化为角度。所以完整的代码为:</div><div data-index="54">```js</div><div data-index="55">// 旋转前的角度</div><div data-index="56">const rotateDegreeBefore = Math.atan2(startY - centerY, startX - centerX) / (Math.PI / 180)</div><div data-index="57">// 旋转后的角度</div><div data-index="58">const rotateDegreeAfter = Math.atan2(curY - centerY, curX - centerX) / (Math.PI / 180)</div><div data-index="59">// 获取旋转的角度值, startRotate 为初始角度值</div><div data-index="60">pos.rotate = startRotate + rotateDegreeAfter - rotateDegreeBefore</div><div data-index="61">```</div><div data-index="62">![](https://img-blog.csdnimg.cn/img_convert/f72edf99eeacbce08c5a954ec908350c.gif)</div><div><br></div><div data-index="63">### 放大缩小</div><div data-index="64">组件旋转后的放大缩小会有 BUG。</div><div><br></div><div data-index="65">![](https://img-blog.csdnimg.cn/img_convert/d7788970d2e1567c15ad0fc7985445cc.gif)</div><div><br></div><div data-index="66">从上图可以看到,放大缩小时会发生移位。另外伸缩的方向和我们拖动的方向也不对。造成这一 BUG 的原因是:当初设计放大缩小功能没有考虑到旋转的场景。所以无论旋转多少角度,放大缩小仍然是按没旋转时计算的。</div><div><br></div><div data-index="67">下面再看一个具体的示例:</div><div><br></div><div data-index="68">![](https://img-blog.csdnimg.cn/img_convert/3e815a59f74b2a51bb1141f716c17e12.png)</div><div><br></div><div data-index="69">从上图可以看出,在没有旋转时,按住顶点往上拖动,只需用 `y2 - y1` 就可以得出拖动距离 `s`。这时将组件原来的高度加上 `s` 就能得出新的高度,同时将组件的 `top`、`left` 属性更新。</div><div><br></div><div data-index="70">![](https://img-blog.csdnimg.cn/img_convert/adeedf9ff0fbc9da68259b8021a3ea69.png)</div><div><br></div><div data-index="71">现在旋转 180 度,如果这时拖住顶点往下拖动,我们期待的结果是组件高度增加。但这时计算的方式和原来没旋转时是一样的,所以结果和我们期待的相反,组件的高度将会变小(如果不理解这个现象,可以想像一下没有旋转的那张图,按住顶点往下拖动)。</div><div><br></div><div data-index="72">![](https://img-blog.csdnimg.cn/img_convert/f013c01874f77de619a910ed614d864f.gif)</div><div><br></div><div data-index="73">如何解决这个问题呢?我从 github 上的一个项目 [snapping-demo](https://github.com/shenhudong/snapping-demo/wiki/corner-handle) 找到了解决方案:将放大缩小和旋转角度关联起来。</div><div><br></div><div data-index="74">#### 解决方案</div><div><br></div><div data-index="75">下面是一个已旋转一定角度的矩形,假设现在拖动它左上方的点进行拉伸。</div><div><br></div><div data-index="76">![](https://img-blog.csdnimg.cn/img_convert/8999e5e5e143fe902a59d1969151bb54.png)</div><div><br></div><div data-index="77">现在我们将一步步分析如何得出拉伸后的组件的正确大小和位移。</div><div><br></div><div data-index="78">**第一步**,按下鼠标时通过组件的坐标(无论旋转多少度,组件的 `top` `left` 属性不变)和大小算出组件中心点:</div><div data-index="79">```js</div><div data-index="80">const center = {</div><div data-index="81">&nbsp; &nbsp; x: style.left + style.width / 2,</div><div data-index="82">&nbsp; &nbsp; y: style.top + style.height / 2,</div><div data-index="83">}</div><div data-index="84">```</div><div data-index="85">**第二步**,用**当前点击坐标**和组件中心点算出**当前点击坐标**的对称点坐标:</div><div data-index="86">```js</div><div data-index="87">// 获取画布位移信息</div><div data-index="88">const editorRectInfo = document.querySelector('#editor').getBoundingClientRect()</div><div><br></div><div data-index="89">// 当前点击坐标</div><div data-index="90">const curPoint = {</div><div data-index="91">&nbsp; &nbsp; x: e.clientX - editorRectInfo.left,</div><div data-index="92">&nbsp; &nbsp; y: e.clientY - editorRectInfo.top,</div><div data-index="93">}</div><div><br></div><div data-index="94">// 获取对称点的坐标</div><div data-index="95">const symmetricPoint = {</div><div data-index="96">&nbsp; &nbsp; x: center.x - (curPoint.x - center.x),</div><div data-index="97">&nbsp; &nbsp; y: center.y - (curPoint.y - center.y),</div><div data-index="98">}</div><div data-index="99">```</div><div data-index="100">**第三步**,摁住组件左上角进行拉伸时,通过当前鼠标实时坐标和对称点计算出新的组件中心点:</div><div data-index="101">```js</div><div data-index="102">const curPositon = {</div><div data-index="103">&nbsp; &nbsp; x: moveEvent.clientX - editorRectInfo.left,</div><div data-index="104">&nbsp; &nbsp; y: moveEvent.clientY - editorRectInfo.top,</div><div data-index="105">}</div><div><br></div><div data-index="106">const newCenterPoint = getCenterPoint(curPositon, symmetricPoint)</div><div><br></div><div data-index="107">// 求两点之间的中点坐标</div><div data-index="108">function getCenterPoint(p1, p2) {</div><div data-index="109">&nbsp; &nbsp; return {</div><div data-index="110">&nbsp; &nbsp; &nbsp; &nbsp; x: p1.x + ((p2.x - p1.x) / 2),</div><div data-index="111">&nbsp; &nbsp; &nbsp; &nbsp; y: p1.y + ((p2.y - p1.y) / 2),</div><div data-index="112">&nbsp; &nbsp; }</div><div data-index="113">}</div><div data-index="114">```</div><div data-index="115">由于组件处于旋转状态,即使你知道了拉伸时移动的 `xy` 距离,也不能直接对组件进行计算。否则就会出现 BUG,移位或者放大缩小方向不正确。因此,我们需要在组件未旋转的情况下对其进行计算。</div><div><br></div><div data-index="116">![](https://img-blog.csdnimg.cn/img_convert/da9d36c423db82bd7130c296d66a27d5.png)</div><div><br></div><div data-index="117">**第四步**,根据已知的旋转角度、新的组件中心点、当前鼠标实时坐标可以算出**当前鼠标实时坐标** `currentPosition` 在未旋转时的坐标 `newTopLeftPoint`。同时也能根据已知的旋转角度、新的组件中心点、对称点算出**组件对称点** `sPoint` 在未旋转时的坐标 `newBottomRightPoint`。</div><div><br></div><div data-index="118">对应的计算公式如下:</div><div data-index="119">```js</div><div data-index="120">/**</div><div data-index="121">&nbsp;* 计算根据圆心旋转后的点的坐标</div><div data-index="122">&nbsp;* @param   {Object}  point  旋转前的点坐标</div><div data-index="123">&nbsp;* @param   {Object}  center 旋转中心</div><div data-index="124">&nbsp;* @param   {Number}  rotate 旋转的角度</div><div data-index="125">&nbsp;* @return  {Object}         旋转后的坐标</div><div data-index="126">&nbsp;* https://www.zhihu.com/question/67425734/answer/252724399 旋转矩阵公式</div><div data-index="127">&nbsp;*/</div><div data-index="128">export function calculateRotatedPointCoordinate(point, center, rotate) {</div><div data-index="129">&nbsp; &nbsp; /**</div><div data-index="130">&nbsp; &nbsp; &nbsp;* 旋转公式:</div><div data-index="131">&nbsp; &nbsp; &nbsp;*  点a(x, y)</div><div data-index="132">&nbsp; &nbsp; &nbsp;*  旋转中心c(x, y)</div><div data-index="133">&nbsp; &nbsp; &nbsp;*  旋转后点n(x, y)</div><div data-index="134">&nbsp; &nbsp; &nbsp;*  旋转角度θ                tan ??</div><div data-index="135">&nbsp; &nbsp; &nbsp;* nx = cosθ * (ax - cx) - sinθ * (ay - cy) + cx</div><div data-index="136">&nbsp; &nbsp; &nbsp;* ny = sinθ * (ax - cx) + cosθ * (ay - cy) + cy</div><div data-index="137">&nbsp; &nbsp; &nbsp;*/</div><div><br></div><div data-index="138">&nbsp; &nbsp; return {</div><div data-index="139">&nbsp; &nbsp; &nbsp; &nbsp; x: (point.x - center.x) * Math.cos(angleToRadian(rotate)) - (point.y - center.y) * Math.sin(angleToRadian(rotate)) + center.x,</div><div data-index="140">&nbsp; &nbsp; &nbsp; &nbsp; y: (point.x - center.x) * Math.sin(angleToRadian(rotate)) + (point.y - center.y) * Math.cos(angleToRadian(rotate)) + center.y,</div><div data-index="141">&nbsp; &nbsp; }</div><div data-index="142">}</div><div data-index="143">```</div><div data-index="144">上面的公式涉及到线性代数中旋转矩阵的知识,对于一个没上过大学的人来说,实在太难了。还好我从知乎上的一个[回答](https://www.zhihu.com/question/67425734/answer/252724399)中找到了这一公式的推理过程,下面是回答的原文:</div><div><br></div><div data-index="145">![](https://img-blog.csdnimg.cn/img_convert/8d6388e18f987779b5e1cc3c2e0f3684.png)</div><div><br></div><div data-index="146">![](https://img-blog.csdnimg.cn/img_convert/be69feb4415274e8dc9ddb75f402d82a.png)</div><div><br></div><div data-index="147">通过以上几个计算值,就可以得到组件新的位移值 `top` `left` 以及新的组件大小。对应的完整代码如下:</div><div data-index="148">```js</div><div data-index="149">function calculateLeftTop(style, curPositon, pointInfo) {</div><div data-index="150">&nbsp; &nbsp; const { symmetricPoint } = pointInfo</div><div data-index="151">&nbsp; &nbsp; const newCenterPoint = getCenterPoint(curPositon, symmetricPoint)</div><div data-index="152">&nbsp; &nbsp; const newTopLeftPoint = calculateRotatedPointCoordinate(curPositon, newCenterPoint, -style.rotate)</div><div data-index="153">&nbsp; &nbsp; const newBottomRightPoint = calculateRotatedPointCoordinate(symmetricPoint, newCenterPoint, -style.rotate)</div><div>&nbsp;&nbsp;</div><div data-index="154">&nbsp; &nbsp; const newWidth = newBottomRightPoint.x - newTopLeftPoint.x</div><div data-index="155">&nbsp; &nbsp; const newHeight = newBottomRightPoint.y - newTopLeftPoint.y</div><div data-index="156">&nbsp; &nbsp; if (newWidth &gt; 0 &amp;&amp; newHeight &gt; 0) {</div><div data-index="157">&nbsp; &nbsp; &nbsp; &nbsp; style.width = Math.round(newWidth)</div><div data-index="158">&nbsp; &nbsp; &nbsp; &nbsp; style.height = Math.round(newHeight)</div><div data-index="159">&nbsp; &nbsp; &nbsp; &nbsp; style.left = Math.round(newTopLeftPoint.x)</div><div data-index="160">&nbsp; &nbsp; &nbsp; &nbsp; style.top = Math.round(newTopLeftPoint.y)</div><div data-index="161">&nbsp; &nbsp; }</div><div data-index="162">}</div><div data-index="163">```</div><div data-index="164">现在再来看一下旋转后的放大缩小:</div><div><br></div><div data-index="165">![](https://img-blog.csdnimg.cn/img_convert/179256b334e7bf8851cdddbbc001f8a7.gif)</div><div><br></div><div data-index="166">### 自动吸附</div><div data-index="167">自动吸附是根据组件的四个属性 `top` `left` `width` `height` 计算的,在将组件进行旋转后,这些属性的值是不会变的。所以无论组件旋转多少度,吸附时仍然按未旋转时计算。这样就会有一个问题,虽然实际上组件的 `top` `left` `width` `height` 属性没有变化。但在外观上却发生了变化。下面是两个同样的组件:一个没旋转,一个旋转了 45 度。</div><div><br></div><div data-index="168">![](https://img-blog.csdnimg.cn/img_convert/691534c36a12f885a492800be361ba78.png)</div><div><br></div><div data-index="169">可以看出来旋转后按钮的 `height` 属性和我们从外观上看到的高度是不一样的,所以在这种情况下就出现了吸附不正确的 BUG。</div><div><br></div><div data-index="170">![](https://img-blog.csdnimg.cn/img_convert/f31472ccab5dad26a873d29520360335.gif)</div><div><br></div><div data-index="171">#### 解决方案</div><div data-index="172">如何解决这个问题?我们需要拿组件旋转后的大小及位移来做吸附对比。也就是说不要拿组件实际的属性来对比,而是拿我们看到的大小和位移做对比。</div><div><br></div><div data-index="173">![](https://img-blog.csdnimg.cn/img_convert/c998d115f6b3ba179d996ec1785feebb.png)</div><div><br></div><div data-index="174">从上图可以看出,旋转后的组件在 x 轴上的投射长度为两条红线长度之和。这两条红线的长度可以通过正弦和余弦算出,左边的红线用正弦计算,右边的红线用余弦计算:</div><div data-index="175">```js</div><div data-index="176">const newWidth = style.width * cos(style.rotate) + style.height * sin(style.rotate)</div><div data-index="177">```</div><div data-index="178">同理,高度也是一样:</div><div data-index="179">```js</div><div data-index="180">const newHeight = style.height * cos(style.rotate) + style.width * sin(style.rotate)</div><div data-index="181">```</div><div data-index="182">新的宽度和高度有了,再根据组件原有的 `top` `left` 属性,可以得出组件旋转后新的 `top` `left` 属性。下面附上完整代码:</div><div data-index="183">```js</div><div data-index="184">translateComponentStyle(style) {</div><div data-index="185">&nbsp; &nbsp; style = { ...style }</div><div data-index="186">&nbsp; &nbsp; if (style.rotate != 0) {</div><div data-index="187">&nbsp; &nbsp; &nbsp; &nbsp; const newWidth = style.width * cos(style.rotate) + style.height * sin(style.rotate)</div><div data-index="188">&nbsp; &nbsp; &nbsp; &nbsp; const diffX = (style.width - newWidth) / 2</div><div data-index="189">&nbsp; &nbsp; &nbsp; &nbsp; style.left += diffX</div><div data-index="190">&nbsp; &nbsp; &nbsp; &nbsp; style.right = style.left + newWidth</div><div><br></div><div data-index="191">&nbsp; &nbsp; &nbsp; &nbsp; const newHeight = style.height * cos(style.rotate) + style.width * sin(style.rotate)</div><div data-index="192">&nbsp; &nbsp; &nbsp; &nbsp; const diffY = (newHeight - style.height) / 2</div><div data-index="193">&nbsp; &nbsp; &nbsp; &nbsp; style.top -= diffY</div><div data-index="194">&nbsp; &nbsp; &nbsp; &nbsp; style.bottom = style.top + newHeight</div><div><br></div><div data-index="195">&nbsp; &nbsp; &nbsp; &nbsp; style.width = newWidth</div><div data-index="196">&nbsp; &nbsp; &nbsp; &nbsp; style.height = newHeight</div><div data-index="197">&nbsp; &nbsp; } else {</div><div data-index="198">&nbsp; &nbsp; &nbsp; &nbsp; style.bottom = style.top + style.height</div><div data-index="199">&nbsp; &nbsp; &nbsp; &nbsp; style.right = style.left + style.width</div><div data-index="200">&nbsp; &nbsp; }</div><div><br></div><div data-index="201">&nbsp; &nbsp; return style</div><div data-index="202">}</div><div data-index="203">```</div><div data-index="204">经过修复后,吸附也可以正常显示了。</div><div><br></div><div data-index="205">![](https://img-blog.csdnimg.cn/img_convert/bb59ba649d6f71c49618177fccff5129.gif)</div><div><br></div><div><br></div><div data-index="206">### 光标</div><div data-index="207">光标和可拖动的方向不对,是因为八个点的光标是固定设置的,没有随着角度变化而变化。</div><div><br></div><div data-index="208">![](https://img-blog.csdnimg.cn/img_convert/c66d1cdbc8b3dae2697c1f3c21321df7.gif)</div><div><br></div><div data-index="209">#### 解决方案</div><div data-index="210">由于 `360 / 8 = 45`,所以可以为每一个方向分配 45 度的范围,每个范围对应一个光标。同时为每个方向设置一个初始角度,也就是未旋转时组件每个方向对应的角度。</div><div><br></div><div data-index="211">![](https://img-blog.csdnimg.cn/img_convert/bdd8c41ba9c9411a65c539ff555d163b.png)</div><div><br></div><div data-index="212">```js</div><div data-index="213">pointList: ['lt', 't', 'rt', 'r', 'rb', 'b', 'lb', 'l'], // 八个方向</div><div data-index="214">initialAngle: { // 每个点对应的初始角度</div><div data-index="215">&nbsp; &nbsp; lt: 0,</div><div data-index="216">&nbsp; &nbsp; t: 45,</div><div data-index="217">&nbsp; &nbsp; rt: 90,</div><div data-index="218">&nbsp; &nbsp; r: 135,</div><div data-index="219">&nbsp; &nbsp; rb: 180,</div><div data-index="220">&nbsp; &nbsp; b: 225,</div><div data-index="221">&nbsp; &nbsp; lb: 270,</div><div data-index="222">&nbsp; &nbsp; l: 315,</div><div data-index="223">},</div><div data-index="224">angleToCursor: [ // 每个范围的角度对应的光标</div><div data-index="225">&nbsp; &nbsp; { start: 338, end: 23, cursor: 'nw' },</div><div data-index="226">&nbsp; &nbsp; { start: 23, end: 68, cursor: 'n' },</div><div data-index="227">&nbsp; &nbsp; { start: 68, end: 113, cursor: 'ne' },</div><div data-index="228">&nbsp; &nbsp; { start: 113, end: 158, cursor: 'e' },</div><div data-index="229">&nbsp; &nbsp; { start: 158, end: 203, cursor: 'se' },</div><div data-index="230">&nbsp; &nbsp; { start: 203, end: 248, cursor: 's' },</div><div data-index="231">&nbsp; &nbsp; { start: 248, end: 293, cursor: 'sw' },</div><div data-index="232">&nbsp; &nbsp; { start: 293, end: 338, cursor: 'w' },</div><div data-index="233">],</div><div data-index="234">cursors: {},</div><div data-index="235">```</div><div data-index="236">计算方式也很简单:</div><div data-index="237">1. 假设现在组件已旋转了一定的角度 a。</div><div data-index="238">2. 遍历八个方向,用每个方向的初始角度 + a 得出现在的角度 b。</div><div data-index="239">3. 遍历 `angleToCursor` 数组,看看 b 在哪一个范围中,然后将对应的光标返回。</div><div><br></div><div data-index="240">经常上面三个步骤就可以计算出组件旋转后正确的光标方向。具体的代码如下:</div><div data-index="241">```js</div><div data-index="242">getCursor() {</div><div data-index="243">&nbsp; &nbsp; const { angleToCursor, initialAngle, pointList, curComponent } = this</div><div data-index="244">&nbsp; &nbsp; const rotate = (curComponent.style.rotate + 360) % 360 // 防止角度有负数,所以 + 360</div><div data-index="245">&nbsp; &nbsp; const result = {}</div><div data-index="246">&nbsp; &nbsp; let lastMatchIndex = -1 // 从上一个命中的角度的索引开始匹配下一个,降低时间复杂度</div><div data-index="247">&nbsp; &nbsp; pointList.forEach(point =&gt; {</div><div data-index="248">&nbsp; &nbsp; &nbsp; &nbsp; const angle = (initialAngle[point] + rotate) % 360</div><div data-index="249">&nbsp; &nbsp; &nbsp; &nbsp; const len = angleToCursor.length</div><div data-index="250">&nbsp; &nbsp; &nbsp; &nbsp; while (true) {</div><div data-index="251">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; lastMatchIndex = (lastMatchIndex + 1) % len</div><div data-index="252">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const angleLimit = angleToCursor[lastMatchIndex]</div><div data-index="253">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (angle &lt; 23 || angle &gt;= 338) {</div><div data-index="254">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; result[point] = 'nw-resize'</div><div data-index="255">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return</div><div data-index="256">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }</div><div><br></div><div data-index="257">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (angleLimit.start &lt;= angle &amp;&amp; angle &lt; angleLimit.end) {</div><div data-index="258">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; result[point] = angleLimit.cursor + '-resize'</div><div data-index="259">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return</div><div data-index="260">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }</div><div data-index="261">&nbsp; &nbsp; &nbsp; &nbsp; }</div><div data-index="262">&nbsp; &nbsp; })</div><div><br></div><div data-index="263">&nbsp; &nbsp; return result</div><div data-index="264">},</div><div data-index="265">```</div><div data-index="266">![](https://img-blog.csdnimg.cn/img_convert/6e5bb1c62178c24bbbae485831ab7304.gif)</div><div><br></div><div data-index="267">从上面的动图可以看出来,现在八个方向上的光标是可以正确显示的。</div><div data-index="268">## 15. 复制粘贴剪切</div><div data-index="269">相对于拖拽旋转功能,复制粘贴就比较简单了。</div><div data-index="270">```js</div><div data-index="271">const ctrlKey = 17, vKey = 86, cKey = 67, xKey = 88</div><div data-index="272">let isCtrlDown = false</div><div><br></div><div data-index="273">window.onkeydown = (e) =&gt; {</div><div data-index="274">&nbsp; &nbsp; if (e.keyCode == ctrlKey) {</div><div data-index="275">&nbsp; &nbsp; &nbsp; &nbsp; isCtrlDown = true</div><div data-index="276">&nbsp; &nbsp; } else if (isCtrlDown &amp;&amp; e.keyCode == cKey) {</div><div data-index="277">&nbsp; &nbsp; &nbsp; &nbsp; this.$store.commit('copy')</div><div data-index="278">&nbsp; &nbsp; } else if (isCtrlDown &amp;&amp; e.keyCode == vKey) {</div><div data-index="279">&nbsp; &nbsp; &nbsp; &nbsp; this.$store.commit('paste')</div><div data-index="280">&nbsp; &nbsp; } else if (isCtrlDown &amp;&amp; e.keyCode == xKey) {</div><div data-index="281">&nbsp; &nbsp; &nbsp; &nbsp; this.$store.commit('cut')</div><div data-index="282">&nbsp; &nbsp; }</div><div data-index="283">}</div><div><br></div><div data-index="284">window.onkeyup = (e) =&gt; {</div><div data-index="285">&nbsp; &nbsp; if (e.keyCode == ctrlKey) {</div><div data-index="286">&nbsp; &nbsp; &nbsp; &nbsp; isCtrlDown = false</div><div data-index="287">&nbsp; &nbsp; }</div><div data-index="288">}</div><div data-index="289">```</div><div data-index="290">监听用户的按键操作,在按下特定按键时触发对应的操作。</div><div><br></div><div data-index="291">### 复制操作</div><div data-index="292">在 vuex 中使用 `copyData` 来表示复制的数据。当用户按下 `ctrl + c` 时,将当前组件数据深拷贝到 `copyData`。</div><div data-index="293">```js</div><div data-index="294">copy(state) {</div><div data-index="295">&nbsp; &nbsp; state.copyData = {</div><div data-index="296">&nbsp; &nbsp; &nbsp; &nbsp; data: deepCopy(state.curComponent),</div><div data-index="297">&nbsp; &nbsp; &nbsp; &nbsp; index: state.curComponentIndex,</div><div data-index="298">&nbsp; &nbsp; }</div><div data-index="299">},</div><div data-index="300">```</div><div data-index="301">同时需要将当前组件在组件数据中的索引记录起来,在剪切中要用到。</div><div data-index="302">### 粘贴操作</div><div data-index="303">```js</div><div data-index="304">paste(state, isMouse) {</div><div data-index="305">&nbsp; &nbsp; if (!state.copyData) {</div><div data-index="306">&nbsp; &nbsp; &nbsp; &nbsp; toast('请选择组件')</div><div data-index="307">&nbsp; &nbsp; &nbsp; &nbsp; return</div><div data-index="308">&nbsp; &nbsp; }</div><div><br></div><div data-index="309">&nbsp; &nbsp; const data = state.copyData.data</div><div><br></div><div data-index="310">&nbsp; &nbsp; if (isMouse) {</div><div data-index="311">&nbsp; &nbsp; &nbsp; &nbsp; data.style.top = state.menuTop</div><div data-index="312">&nbsp; &nbsp; &nbsp; &nbsp; data.style.left = state.menuLeft</div><div data-index="313">&nbsp; &nbsp; } else {</div><div data-index="314">&nbsp; &nbsp; &nbsp; &nbsp; data.style.top += 10</div><div data-index="315">&nbsp; &nbsp; &nbsp; &nbsp; data.style.left += 10</div><div data-index="316">&nbsp; &nbsp; }</div><div><br></div><div data-index="317">&nbsp; &nbsp; data.id = generateID()</div><div data-index="318">&nbsp; &nbsp; store.commit('addComponent', { component: data })</div><div data-index="319">&nbsp; &nbsp; store.commit('recordSnapshot')</div><div data-index="320">&nbsp; &nbsp; state.copyData = null</div><div data-index="321">},</div><div data-index="322">```</div><div data-index="323">粘贴时,如果是按键操作 `ctrl+v`。则将组件的 `top` `left` 属性加 10,以免和原来的组件重叠在一起。如果是使用鼠标右键执行粘贴操作,则将复制的组件放到鼠标点击处。&nbsp;</div><div><br></div><div data-index="324">### 剪切操作</div><div data-index="325">```js</div><div data-index="326">cut(state) {</div><div data-index="327">&nbsp; &nbsp; if (!state.curComponent) {</div><div data-index="328">&nbsp; &nbsp; &nbsp; &nbsp; toast('请选择组件')</div><div data-index="329">&nbsp; &nbsp; &nbsp; &nbsp; return</div><div data-index="330">&nbsp; &nbsp; }</div><div><br></div><div data-index="331">&nbsp; &nbsp; if (state.copyData) {</div><div data-index="332">&nbsp; &nbsp; &nbsp; &nbsp; store.commit('addComponent', { component: state.copyData.data, index: state.copyData.index })</div><div data-index="333">&nbsp; &nbsp; &nbsp; &nbsp; if (state.curComponentIndex &gt;= state.copyData.index) {</div><div data-index="334">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 如果当前组件索引大于等于插入索引,需要加一,因为当前组件往后移了一位</div><div data-index="335">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; state.curComponentIndex++</div><div data-index="336">&nbsp; &nbsp; &nbsp; &nbsp; }</div><div data-index="337">&nbsp; &nbsp; }</div><div><br></div><div data-index="338">&nbsp; &nbsp; store.commit('copy')</div><div data-index="339">&nbsp; &nbsp; store.commit('deleteComponent')</div><div data-index="340">},</div><div data-index="341">```</div><div data-index="342">剪切操作本质上还是复制,只不过在执行复制后,需要将当前组件删除。为了避免用户执行剪切操作后,不执行粘贴操作,而是继续执行剪切。这时就需要将原先剪切的数据进行恢复。所以复制数据中记录的索引就起作用了,可以通过索引将原来的数据恢复到原来的位置中。</div><div><br></div><div data-index="343">### 右键操作</div><div data-index="344">右键操作和按键操作是一样的,一个功能两种触发途径。</div><div data-index="345">```html</div><div data-index="346">&lt;li @click="copy" v-show="curComponent"&gt;复制&lt;/li&gt;</div><div data-index="347">&lt;li @click="paste"&gt;粘贴&lt;/li&gt;</div><div data-index="348">&lt;li @click="cut" v-show="curComponent"&gt;剪切&lt;/li&gt;</div><div><br></div><div data-index="349">cut() {</div><div data-index="350">&nbsp; &nbsp; this.$store.commit('cut')</div><div data-index="351">},</div><div><br></div><div data-index="352">copy() {</div><div data-index="353">&nbsp; &nbsp; this.$store.commit('copy')</div><div data-index="354">},</div><div><br></div><div data-index="355">paste() {</div><div data-index="356">&nbsp; &nbsp; this.$store.commit('paste', true)</div><div data-index="357">},</div><div data-index="358">```</div><div data-index="359">## 16. 数据交互</div><div data-index="360">### 方式一</div><div data-index="361">提前写好一系列 ajax 请求API,点击组件时按需选择 API,选好 API 再填参数。例如下面这个组件,就展示了如何使用 ajax 请求向后台交互:</div><div data-index="362">```html</div><div data-index="363">&lt;template&gt;</div><div data-index="364">&nbsp; &nbsp; &lt;div&gt;{{ propValue.data }}&lt;/div&gt;</div><div data-index="365">&lt;/template&gt;</div><div><br></div><div data-index="366">&lt;script&gt;</div><div data-index="367">export default {</div><div data-index="368">&nbsp; &nbsp; // propValue: {</div><div data-index="369">&nbsp; &nbsp; //     api: {</div><div data-index="370">&nbsp; &nbsp; //             request: a,</div><div data-index="371">&nbsp; &nbsp; //             params,</div><div data-index="372">&nbsp; &nbsp; //      },</div><div data-index="373">&nbsp; &nbsp; //     data: null</div><div data-index="374">&nbsp; &nbsp; // }</div><div data-index="375">&nbsp; &nbsp; props: {</div><div data-index="376">&nbsp; &nbsp; &nbsp; &nbsp; propValue: {</div><div data-index="377">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; type: Object,</div><div data-index="378">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; default: () =&gt; {},</div><div data-index="379">&nbsp; &nbsp; &nbsp; &nbsp; },</div><div data-index="380">&nbsp; &nbsp; },</div><div data-index="381">&nbsp; &nbsp; created() {</div><div data-index="382">&nbsp; &nbsp; &nbsp; &nbsp; this.propValue.api.request(this.propValue.api.params).then(res =&gt; {</div><div data-index="383">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.propValue.data = res.data</div><div data-index="384">&nbsp; &nbsp; &nbsp; &nbsp; })</div><div data-index="385">&nbsp; &nbsp; },</div><div data-index="386">}</div><div data-index="387">&lt;/script&gt;</div><div data-index="388">```</div><div data-index="389">### 方式二</div><div data-index="390">方式二适合纯展示的组件,例如有一个报警组件,可以根据后台传来的数据显示对应的颜色。在编辑页面的时候,可以通过 ajax 向后台请求页面能够使用的 websocket 数据:</div><div data-index="391">```js</div><div data-index="392">const data = ['status', 'text'...]</div><div data-index="393">```</div><div data-index="394">然后再为不同的组件添加上不同的属性。例如有 a 组件,它绑定的属性为 `status`。</div><div data-index="395">```</div><div data-index="396">// 组件能接收的数据</div><div data-index="397">props: {</div><div data-index="398">&nbsp; &nbsp; propValue: {</div><div data-index="399">&nbsp; &nbsp; &nbsp; &nbsp; type: String,</div><div data-index="400">&nbsp; &nbsp; },</div><div data-index="401">&nbsp; &nbsp; element: {</div><div data-index="402">&nbsp; &nbsp; &nbsp; &nbsp; type: Object,</div><div data-index="403">&nbsp; &nbsp; },</div><div data-index="404">&nbsp; &nbsp; wsKey: {</div><div data-index="405">&nbsp; &nbsp; &nbsp; &nbsp; type: String,</div><div data-index="406">&nbsp; &nbsp; &nbsp; &nbsp; default: '',</div><div data-index="407">&nbsp; &nbsp; },</div><div data-index="408">},</div><div data-index="409">```</div><div data-index="410">在组件中通过 `wsKey` 获取这个绑定的属性。等页面发布后或者预览时,通过 weboscket 向后台请求全局数据放在 vuex 上。组件就可以通过 `wsKey` 访问数据了。</div><div data-index="411">```html</div><div data-index="412">&lt;template&gt;</div><div data-index="413">&nbsp; &nbsp; &lt;div&gt;{{ wsData[wsKey] }}&lt;/div&gt;</div><div data-index="414">&lt;/template&gt;</div><div><br></div><div data-index="415">&lt;script&gt;</div><div data-index="416">import { mapState } from 'vuex'</div><div><br></div><div data-index="417">export default {</div><div data-index="418">&nbsp; &nbsp; props: {</div><div data-index="419">&nbsp; &nbsp; &nbsp; &nbsp; propValue: {</div><div data-index="420">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; type: String,</div><div data-index="421">&nbsp; &nbsp; &nbsp; &nbsp; },</div><div data-index="422">&nbsp; &nbsp; &nbsp; &nbsp; element: {</div><div data-index="423">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; type: Object,</div><div data-index="424">&nbsp; &nbsp; &nbsp; &nbsp; },</div><div data-index="425">&nbsp; &nbsp; &nbsp; &nbsp; wsKey: {</div><div data-index="426">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; type: String,</div><div data-index="427">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; default: '',</div><div data-index="428">&nbsp; &nbsp; &nbsp; &nbsp; },</div><div data-index="429">&nbsp; &nbsp; },</div><div data-index="430">&nbsp; &nbsp; computed: mapState([</div><div data-index="431">&nbsp; &nbsp; &nbsp; &nbsp; 'wsData',</div><div data-index="432">&nbsp; &nbsp; ]),</div><div data-index="433">&lt;/script&gt;</div><div data-index="434">```</div><div><br></div><div data-index="435">和后台交互的方式有很多种,不仅仅包括上面两种,我在这里仅提供一些思路,以供参考。</div><div data-index="436">## 17. 发布</div><div data-index="437">页面发布有两种方式:一是将组件数据渲染为一个单独的 HTML 页面;二是从本项目中抽取出一个最小运行时 runtime 作为一个单独的项目。</div><div><br></div><div data-index="438">这里说一下第二种方式,本项目中的最小运行时其实就是预览页面加上自定义组件。将这些代码提取出来作为一个项目单独打包。发布页面时将组件数据以 JSON 的格式传给服务端,同时为每个页面生成一个唯一 ID。</div><div><br></div><div data-index="439">假设现在有三个页面,发布页面生成的 ID 为 a、b、c。访问页面时只需要把 ID 带上,这样就可以根据 ID 获取每个页面对应的组件数据。</div><div data-index="440">```js</div><div data-index="441">www.test.com/?id=a</div><div data-index="442">www.test.com/?id=c</div><div data-index="443">www.test.com/?id=b</div><div data-index="444">```</div><div data-index="445">### 按需加载</div><div data-index="446">如果自定义组件过大,例如有数十个甚至上百个。这时可以将自定义组件用 `import` 的方式导入,做到按需加载,减少首屏渲染时间:</div><div data-index="447">```js</div><div data-index="448">import Vue from 'vue'</div><div><br></div><div data-index="449">const components = [</div><div data-index="450">&nbsp; &nbsp; 'Picture',</div><div data-index="451">&nbsp; &nbsp; 'VText',</div><div data-index="452">&nbsp; &nbsp; 'VButton',</div><div data-index="453">]</div><div><br></div><div data-index="454">components.forEach(key =&gt; {</div><div data-index="455">&nbsp; &nbsp; Vue.component(key, () =&gt; import(`@/custom-component/${key}`))</div><div data-index="456">})</div><div data-index="457">```</div><div data-index="458">### 按版本发布</div><div data-index="459">自定义组件有可能会有更新的情况。例如原来的组件使用了大半年,现在有功能变更,为了不影响原来的页面。建议在发布时带上组件的版本号:</div><div data-index="460">```</div><div data-index="461">- v-text</div><div data-index="462">&nbsp; - v1.vue</div><div data-index="463">&nbsp; - v2.vue</div><div data-index="464">```</div><div data-index="465">例如 `v-text` 组件有两个版本,在左侧组件列表区使用时就可以带上版本号:</div><div data-index="466">```js</div><div data-index="467">{</div><div data-index="468">&nbsp; component: 'v-text',</div><div data-index="469">&nbsp; version: 'v1'</div><div data-index="470">&nbsp; ...</div><div data-index="471">}</div><div data-index="472">```</div><div data-index="473">这样导入组件时就可以根据组件版本号进行导入:</div><div data-index="474">```js</div><div data-index="475">import Vue from 'vue'</div><div data-index="476">import componentList from '@/custom-component/component-list`</div><div><br></div><div data-index="477">componentList.forEach(component =&gt; {</div><div data-index="478">&nbsp; &nbsp; Vue.component(component.name, () =&gt; import(`@/custom-component/${component.name}/${component.version}`))</div><div data-index="479">})</div><div data-index="480">```</div><div><br></div><div data-index="481">## 参考资料</div><div data-index="482">* [Math](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Math)</div><div data-index="483">* [通过Math.atan2 计算角度](https://www.jianshu.com/p/9817e267925a)</div><div data-index="484">* [为什么矩阵能用来表示角的旋转?](https://www.zhihu.com/question/67425734/answer/252724399)</div><div data-index="485">* [snapping-demo](https://github.com/shenhudong/snapping-demo/wiki/corner-handle)</div><div data-index="486">* [vue-next-drag](https://github.com/lycHub/vue-next-drag)</div>
    </div>
    <div id="show-content" class="text-container markdown"></div>
</body>
</html>
html,
body {
    height: 100%;
    font-size: 0;
}

* {
    margin: 0;
    padding: 0;
    font-family: 'Microsoft yahei';
}

body {
    display: flex;
}

table {
    border-collapse: collapse;
}

.text-container {
    box-sizing: border-box;
    width: 50%;
    border: 1px solid #e6e6e6;
    height: 100%;
    font-size: 14px;
    background: #f5f5f5;
    padding: 10px;
    line-height: 20px;
    outline: none;
    overflow: auto;
}
.text-container > div {
    min-height: 20px;
}

/* 定义滚动条高宽及背景
 高宽分别对应横竖滚动条的尺寸 */
 ::-webkit-scrollbar {
    width: 5px;
    height: 5px;
}

/* 定义滚动条轨道
  内阴影+圆角 */
::-webkit-scrollbar-track {
    background-color: transparent;
}

/* 定义滑块
  内阴影+圆角 */
::-webkit-scrollbar-thumb {
    border-radius: 4px;
    background-color: #ddd;
}

/* markdown style */
.markdown {
    font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;
    font-size: 16px;
    line-height: 1.5;
    word-wrap: break-word;
    background-color: #fff;
    color: #24292f;
}
.markdown img {
    max-width: 100%;
    margin: 10px 0;
}
.markdown>:first-child {
    margin-top: 0!important;
}
.markdown h1,
.markdown h2 {
    border-bottom: 1px solid #eaecef;
    padding-bottom: .3em;
}
.markdown h1,
.markdown h2, 
.markdown h3, 
.markdown h4, 
.markdown h5, 
.markdown h6 {
    font-weight: 600;
    line-height: 1.25;
    margin-bottom: 16px;
    margin-top: 24px;
}
.markdown div {
    min-height: 20px;
}
code, 
pre, 
tt {
    font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;
    font-size: 14px;
}
.markdown pre {
    background-color: #f6f8fa;
    border-radius: 3px;
    font-size: 85%;
    line-height: 1.45;
    overflow: auto;
    padding: 16px;
    color: #24292e;
    font-size: 14px;
    font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;
    margin: 0;
    margin-bottom: 1em;
}
.markdown code {
    background-color: #f6f8fa;
    border: 0;
    display: inline;
    line-height: inherit;
    margin: 0;
    max-width: auto;
    overflow: visible;
    padding: 0;
    word-wrap: normal;
    word-break: break-all;
}
.markdown blockquote {
    background-color: #f6f8fa;
    padding: 10px;
}
.markdown table {
    display: block;
    overflow: auto;
    width: 100%;
    margin-bottom: 15px;
}
.markdown table tr {
    background-color: #fff;
}
.markdown table td,
.markdown table th {
    border: 1px solid #dfe2e5;
    padding: 6px 13px;
}
.markdown p {
    margin-bottom: 1em;
}
.markdown li {
    word-wrap: break-all;
}
.markdown ul li {
    list-style: disc;
}
.markdown ol,
.markdown ul {
    padding-left: 2em;
}
.markdown blockquote, 
.markdown details, 
.markdown dl, 
.markdown ol, 
.markdown p, 
.markdown pre, 
.markdown table, 
.markdown ul {
    margin-bottom: 16px;
    margin-top: 0;
}
.markdown a {
    color: #0366d6;
    text-decoration: none;
}