<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          可視化拖拽組件庫一些技術(shù)要點(diǎn)原理分析

          共 12517字,需瀏覽 26分鐘

           ·

          2021-01-19 08:15

          本文是對《可視化拖拽組件庫一些技術(shù)要點(diǎn)原理分析》[1]的補(bǔ)充。上一篇文章主要講解了以下幾個(gè)功能點(diǎn):

          1.編輯器2.自定義組件3.拖拽4.刪除組件、調(diào)整圖層層級5.放大縮小6.撤消、重做7.組件屬性設(shè)置8.吸附9.預(yù)覽、保存代碼10.綁定事件11.綁定動(dòng)畫12.導(dǎo)入 PSD13.手機(jī)模式

          現(xiàn)在這篇文章會(huì)在此基礎(chǔ)上再補(bǔ)充 4 個(gè)功能點(diǎn),分別是:

          ?拖拽旋轉(zhuǎn)?復(fù)制粘貼剪切?數(shù)據(jù)交互?發(fā)布

          和上篇文章一樣,我已經(jīng)將新功能的代碼更新到了 github:

          ?github 項(xiàng)目地址[2]?在線預(yù)覽[3]

          友善提醒:建議結(jié)合源碼一起閱讀,效果更好(這個(gè) DEMO 使用的是 Vue 技術(shù)棧)。

          14. 拖拽旋轉(zhuǎn)

          在寫上一篇文章時(shí),原來的 DEMO 已經(jīng)可以支持旋轉(zhuǎn)功能了。但是這個(gè)旋轉(zhuǎn)功能還有很多不完善的地方:

          1.不支持拖拽旋轉(zhuǎn)。2.旋轉(zhuǎn)后的放大縮小不正確。3.旋轉(zhuǎn)后的自動(dòng)吸附不正確。4.旋轉(zhuǎn)后八個(gè)可伸縮點(diǎn)的光標(biāo)不正確。

          這一小節(jié),我們將逐一解決這四個(gè)問題。

          拖拽旋轉(zhuǎn)

          拖拽旋轉(zhuǎn)需要使用?Math.atan2()[4]?函數(shù)。

          Math.atan2() 返回從原點(diǎn)(0,0)到(x,y)點(diǎn)的線段與x軸正方向之間的平面角度(弧度值),也就是Math.atan2(y,x)。Math.atan2(y,x)中的y和x都是相對于圓點(diǎn)(0,0)的距離。

          簡單的說就是以組件中心點(diǎn)為原點(diǎn)?(centerX,centerY),用戶按下鼠標(biāo)時(shí)的坐標(biāo)設(shè)為?(startX,startY),鼠標(biāo)移動(dòng)時(shí)的坐標(biāo)設(shè)為?(curX,curY)。旋轉(zhuǎn)角度可以通過?(startX,startY)?和?(curX,curY)?計(jì)算得出。

          那我們?nèi)绾蔚玫綇狞c(diǎn)?(startX,startY)?到點(diǎn)?(curX,curY)?之間的旋轉(zhuǎn)角度呢?

          第一步,鼠標(biāo)點(diǎn)擊時(shí)的坐標(biāo)設(shè)為?(startX,startY)

          const startY = e.clientYconst startX = e.clientX

          第二步,算出組件中心點(diǎn):

          // 獲取組件中心點(diǎn)位置const rect = this.$el.getBoundingClientRect()const centerX = rect.left + rect.width / 2const centerY = rect.top + rect.height / 2

          第三步,按住鼠標(biāo)移動(dòng)時(shí)的坐標(biāo)設(shè)為?(curX,curY)

          const curX = moveEvent.clientXconst curY = moveEvent.clientY

          第四步,分別算出?(startX,startY)?和?(curX,curY)?對應(yīng)的角度,再將它們相減得出旋轉(zhuǎn)的角度。另外,還需要注意的就是?Math.atan2()?方法的返回值是一個(gè)弧度,因此還需要將弧度轉(zhuǎn)化為角度。所以完整的代碼為:

          // 旋轉(zhuǎn)前的角度const rotateDegreeBefore = Math.atan2(startY - centerY, startX - centerX) / (Math.PI / 180)// 旋轉(zhuǎn)后的角度const rotateDegreeAfter = Math.atan2(curY - centerY, curX - centerX) / (Math.PI / 180)// 獲取旋轉(zhuǎn)的角度值, startRotate 為初始角度值pos.rotate = startRotate + rotateDegreeAfter - rotateDegreeBefore

          放大縮小

          組件旋轉(zhuǎn)后的放大縮小會(huì)有 BUG。

          從上圖可以看到,放大縮小時(shí)會(huì)發(fā)生移位。另外伸縮的方向和我們拖動(dòng)的方向也不對。造成這一 BUG 的原因是:當(dāng)初設(shè)計(jì)放大縮小功能沒有考慮到旋轉(zhuǎn)的場景。所以無論旋轉(zhuǎn)多少角度,放大縮小仍然是按沒旋轉(zhuǎn)時(shí)計(jì)算的。

          下面再看一個(gè)具體的示例:

          從上圖可以看出,在沒有旋轉(zhuǎn)時(shí),按住頂點(diǎn)往上拖動(dòng),只需用?y2 - y1?就可以得出拖動(dòng)距離?s。這時(shí)將組件原來的高度加上?s?就能得出新的高度,同時(shí)將組件的?topleft?屬性更新。

          現(xiàn)在旋轉(zhuǎn) 180 度,如果這時(shí)拖住頂點(diǎn)往下拖動(dòng),我們期待的結(jié)果是組件高度增加。但這時(shí)計(jì)算的方式和原來沒旋轉(zhuǎn)時(shí)是一樣的,所以結(jié)果和我們期待的相反,組件的高度將會(huì)變小(如果不理解這個(gè)現(xiàn)象,可以想像一下沒有旋轉(zhuǎn)的那張圖,按住頂點(diǎn)往下拖動(dòng))。

          如何解決這個(gè)問題呢?我從 github 上的一個(gè)項(xiàng)目?snapping-demo[5]?找到了解決方案:將放大縮小和旋轉(zhuǎn)角度關(guān)聯(lián)起來。

          解決方案

          下面是一個(gè)已旋轉(zhuǎn)一定角度的矩形,假設(shè)現(xiàn)在拖動(dòng)它左上方的點(diǎn)進(jìn)行拉伸。

          現(xiàn)在我們將一步步分析如何得出拉伸后的組件的正確大小和位移。

          第一步,按下鼠標(biāo)時(shí)通過組件的坐標(biāo)(無論旋轉(zhuǎn)多少度,組件的?top?left?屬性不變)和大小算出組件中心點(diǎn):

          const center = {    x: style.left + style.width / 2,    y: style.top + style.height / 2,}

          第二步,用當(dāng)前點(diǎn)擊坐標(biāo)和組件中心點(diǎn)算出當(dāng)前點(diǎn)擊坐標(biāo)的對稱點(diǎn)坐標(biāo):

          // 獲取畫布位移信息const editorRectInfo = document.querySelector('#editor').getBoundingClientRect()
          // 當(dāng)前點(diǎn)擊坐標(biāo)const curPoint = { x: e.clientX - editorRectInfo.left, y: e.clientY - editorRectInfo.top,}
          // 獲取對稱點(diǎn)的坐標(biāo)const symmetricPoint = { x: center.x - (curPoint.x - center.x), y: center.y - (curPoint.y - center.y),}

          第三步,摁住組件左上角進(jìn)行拉伸時(shí),通過當(dāng)前鼠標(biāo)實(shí)時(shí)坐標(biāo)和對稱點(diǎn)計(jì)算出新的組件中心點(diǎn):

          const curPositon = {    x: moveEvent.clientX - editorRectInfo.left,    y: moveEvent.clientY - editorRectInfo.top,}
          const newCenterPoint = getCenterPoint(curPositon, symmetricPoint)
          // 求兩點(diǎn)之間的中點(diǎn)坐標(biāo)function getCenterPoint(p1, p2) { return { x: p1.x + ((p2.x - p1.x) / 2), y: p1.y + ((p2.y - p1.y) / 2), }}

          由于組件處于旋轉(zhuǎn)狀態(tài),即使你知道了拉伸時(shí)移動(dòng)的?xy?距離,也不能直接對組件進(jìn)行計(jì)算。否則就會(huì)出現(xiàn) BUG,移位或者放大縮小方向不正確。因此,我們需要在組件未旋轉(zhuǎn)的情況下對其進(jìn)行計(jì)算。

          第四步,根據(jù)已知的旋轉(zhuǎn)角度、新的組件中心點(diǎn)、當(dāng)前鼠標(biāo)實(shí)時(shí)坐標(biāo)可以算出當(dāng)前鼠標(biāo)實(shí)時(shí)坐標(biāo)?currentPosition?在未旋轉(zhuǎn)時(shí)的坐標(biāo)?newTopLeftPoint。同時(shí)也能根據(jù)已知的旋轉(zhuǎn)角度、新的組件中心點(diǎn)、對稱點(diǎn)算出組件對稱點(diǎn)?sPoint?在未旋轉(zhuǎn)時(shí)的坐標(biāo)?newBottomRightPoint

          對應(yīng)的計(jì)算公式如下:

          /** * 計(jì)算根據(jù)圓心旋轉(zhuǎn)后的點(diǎn)的坐標(biāo) * @param   {Object}  point  旋轉(zhuǎn)前的點(diǎn)坐標(biāo) * @param   {Object}  center 旋轉(zhuǎn)中心 * @param   {Number}  rotate 旋轉(zhuǎn)的角度 * @return  {Object}         旋轉(zhuǎn)后的坐標(biāo) * https://www.zhihu.com/question/67425734/answer/252724399 旋轉(zhuǎn)矩陣公式 */export function calculateRotatedPointCoordinate(point, center, rotate) {    /**     * 旋轉(zhuǎn)公式:     *  點(diǎn)a(x, y)     *  旋轉(zhuǎn)中心c(x, y)     *  旋轉(zhuǎn)后點(diǎn)n(x, y)     *  旋轉(zhuǎn)角度θ                tan ??     * nx = cosθ * (ax - cx) - sinθ * (ay - cy) + cx     * ny = sinθ * (ax - cx) + cosθ * (ay - cy) + cy     */
          return { x: (point.x - center.x) * Math.cos(angleToRadian(rotate)) - (point.y - center.y) * Math.sin(angleToRadian(rotate)) + center.x, y: (point.x - center.x) * Math.sin(angleToRadian(rotate)) + (point.y - center.y) * Math.cos(angleToRadian(rotate)) + center.y, }}

          上面的公式涉及到線性代數(shù)中旋轉(zhuǎn)矩陣的知識(shí),對于一個(gè)沒上過大學(xué)的人來說,實(shí)在太難了。還好我從知乎上的一個(gè)回答[6]中找到了這一公式的推理過程,下面是回答的原文:

          通過以上幾個(gè)計(jì)算值,就可以得到組件新的位移值?top?left?以及新的組件大小。對應(yīng)的完整代碼如下:

          function calculateLeftTop(style, curPositon, pointInfo) {    const { symmetricPoint } = pointInfo    const newCenterPoint = getCenterPoint(curPositon, symmetricPoint)    const newTopLeftPoint = calculateRotatedPointCoordinate(curPositon, newCenterPoint, -style.rotate)    const newBottomRightPoint = calculateRotatedPointCoordinate(symmetricPoint, newCenterPoint, -style.rotate)
          const newWidth = newBottomRightPoint.x - newTopLeftPoint.x const newHeight = newBottomRightPoint.y - newTopLeftPoint.y if (newWidth > 0 && newHeight > 0) { style.width = Math.round(newWidth) style.height = Math.round(newHeight) style.left = Math.round(newTopLeftPoint.x) style.top = Math.round(newTopLeftPoint.y) }}

          現(xiàn)在再來看一下旋轉(zhuǎn)后的放大縮小:

          自動(dòng)吸附

          自動(dòng)吸附是根據(jù)組件的四個(gè)屬性?top?left?width?height?計(jì)算的,在將組件進(jìn)行旋轉(zhuǎn)后,這些屬性的值是不會(huì)變的。所以無論組件旋轉(zhuǎn)多少度,吸附時(shí)仍然按未旋轉(zhuǎn)時(shí)計(jì)算。這樣就會(huì)有一個(gè)問題,雖然實(shí)際上組件的?top?left?width?height?屬性沒有變化。但在外觀上卻發(fā)生了變化。下面是兩個(gè)同樣的組件:一個(gè)沒旋轉(zhuǎn),一個(gè)旋轉(zhuǎn)了 45 度。

          可以看出來旋轉(zhuǎn)后按鈕的?height?屬性和我們從外觀上看到的高度是不一樣的,所以在這種情況下就出現(xiàn)了吸附不正確的 BUG。

          解決方案

          如何解決這個(gè)問題?我們需要拿組件旋轉(zhuǎn)后的大小及位移來做吸附對比。也就是說不要拿組件實(shí)際的屬性來對比,而是拿我們看到的大小和位移做對比。

          從上圖可以看出,旋轉(zhuǎn)后的組件在 x 軸上的投射長度為兩條紅線長度之和。這兩條紅線的長度可以通過正弦和余弦算出,左邊的紅線用正弦計(jì)算,右邊的紅線用余弦計(jì)算:

          const newWidth = style.width * cos(style.rotate) + style.height * sin(style.rotate)

          同理,高度也是一樣:

          const newHeight = style.height * cos(style.rotate) + style.width * sin(style.rotate)

          新的寬度和高度有了,再根據(jù)組件原有的?top?left?屬性,可以得出組件旋轉(zhuǎn)后新的?top?left?屬性。下面附上完整代碼:

          translateComponentStyle(style) {    style = { ...style }    if (style.rotate != 0) {        const newWidth = style.width * cos(style.rotate) + style.height * sin(style.rotate)        const diffX = (style.width - newWidth) / 2        style.left += diffX        style.right = style.left + newWidth
          const newHeight = style.height * cos(style.rotate) + style.width * sin(style.rotate) const diffY = (newHeight - style.height) / 2 style.top -= diffY style.bottom = style.top + newHeight
          style.width = newWidth style.height = newHeight } else { style.bottom = style.top + style.height style.right = style.left + style.width }
          return style}

          經(jīng)過修復(fù)后,吸附也可以正常顯示了。

          光標(biāo)

          光標(biāo)和可拖動(dòng)的方向不對,是因?yàn)榘藗€(gè)點(diǎn)的光標(biāo)是固定設(shè)置的,沒有隨著角度變化而變化。

          解決方案

          由于?360 / 8 = 45,所以可以為每一個(gè)方向分配 45 度的范圍,每個(gè)范圍對應(yīng)一個(gè)光標(biāo)。同時(shí)為每個(gè)方向設(shè)置一個(gè)初始角度,也就是未旋轉(zhuǎn)時(shí)組件每個(gè)方向?qū)?yīng)的角度。

          pointList: ['lt', 't', 'rt', 'r', 'rb', 'b', 'lb', 'l'], // 八個(gè)方向initialAngle: { // 每個(gè)點(diǎn)對應(yīng)的初始角度    lt: 0,    t: 45,    rt: 90,    r: 135,    rb: 180,    b: 225,    lb: 270,    l: 315,},angleToCursor: [ // 每個(gè)范圍的角度對應(yīng)的光標(biāo)    { start: 338, end: 23, cursor: 'nw' },    { start: 23, end: 68, cursor: 'n' },    { start: 68, end: 113, cursor: 'ne' },    { start: 113, end: 158, cursor: 'e' },    { start: 158, end: 203, cursor: 'se' },    { start: 203, end: 248, cursor: 's' },    { start: 248, end: 293, cursor: 'sw' },    { start: 293, end: 338, cursor: 'w' },],cursors: {},

          計(jì)算方式也很簡單:

          1.假設(shè)現(xiàn)在組件已旋轉(zhuǎn)了一定的角度 a。2.遍歷八個(gè)方向,用每個(gè)方向的初始角度 + a 得出現(xiàn)在的角度 b。3.遍歷?angleToCursor?數(shù)組,看看 b 在哪一個(gè)范圍中,然后將對應(yīng)的光標(biāo)返回。

          經(jīng)常上面三個(gè)步驟就可以計(jì)算出組件旋轉(zhuǎn)后正確的光標(biāo)方向。具體的代碼如下:

          getCursor() {  const { angleToCursor, initialAngle, pointList, curComponent } = this  const rotate = (curComponent.style.rotate + 360) % 360 // 防止角度有負(fù)數(shù),所以 + 360  const result = {}  let lastMatchIndex = -1 // 從上一個(gè)命中的角度的索引開始匹配下一個(gè),降低時(shí)間復(fù)雜度  pointList.forEach(point => {      const angle = (initialAngle[point] + rotate) % 360      const len = angleToCursor.length      let i = 0      while (i < len) {          lastMatchIndex = (lastMatchIndex + 1) % len          const angleLimit = angleToCursor[lastMatchIndex]          if (angle < 23 || angle >= 338) {              result[point] = 'nw-resize'              break          }
          if (angleLimit.start <= angle && angle < angleLimit.end) { result[point] = angleLimit.cursor + '-resize' break } } })
          return result},

          從上面的動(dòng)圖可以看出來,現(xiàn)在八個(gè)方向上的光標(biāo)是可以正確顯示的。

          15. 復(fù)制粘貼剪切

          相對于拖拽旋轉(zhuǎn)功能,復(fù)制粘貼就比較簡單了。

          const ctrlKey = 17, vKey = 86, cKey = 67, xKey = 88let isCtrlDown = false
          window.onkeydown = (e) => { if (e.keyCode == ctrlKey) { isCtrlDown = true } else if (isCtrlDown && e.keyCode == cKey) { this.$store.commit('copy') } else if (isCtrlDown && e.keyCode == vKey) { this.$store.commit('paste') } else if (isCtrlDown && e.keyCode == xKey) { this.$store.commit('cut') }}
          window.onkeyup = (e) => { if (e.keyCode == ctrlKey) { isCtrlDown = false }}

          監(jiān)聽用戶的按鍵操作,在按下特定按鍵時(shí)觸發(fā)對應(yīng)的操作。

          復(fù)制操作

          在 vuex 中使用?copyData?來表示復(fù)制的數(shù)據(jù)。當(dāng)用戶按下?ctrl + c?時(shí),將當(dāng)前組件數(shù)據(jù)深拷貝到?copyData

          copy(state) {    state.copyData = {        data: deepCopy(state.curComponent),        index: state.curComponentIndex,    }},

          同時(shí)需要將當(dāng)前組件在組件數(shù)據(jù)中的索引記錄起來,在剪切中要用到。

          粘貼操作

          paste(state, isMouse) {    if (!state.copyData) {        toast('請選擇組件')        return    }
          const data = state.copyData.data
          if (isMouse) { data.style.top = state.menuTop data.style.left = state.menuLeft } else { data.style.top += 10 data.style.left += 10 }
          data.id = generateID() store.commit('addComponent', { component: data }) store.commit('recordSnapshot') state.copyData = null},

          粘貼時(shí),如果是按鍵操作?ctrl+v。則將組件的?top?left?屬性加 10,以免和原來的組件重疊在一起。如果是使用鼠標(biāo)右鍵執(zhí)行粘貼操作,則將復(fù)制的組件放到鼠標(biāo)點(diǎn)擊處。

          剪切操作

          cut({ copyData }) {    if (copyData) {        store.commit('addComponent', { component: copyData.data, index: copyData.index })    }
          store.commit('copy') store.commit('deleteComponent')},

          剪切操作本質(zhì)上還是復(fù)制,只不過在執(zhí)行復(fù)制后,需要將當(dāng)前組件刪除。為了避免用戶執(zhí)行剪切操作后,不執(zhí)行粘貼操作,而是繼續(xù)執(zhí)行剪切。這時(shí)就需要將原先剪切的數(shù)據(jù)進(jìn)行恢復(fù)。所以復(fù)制數(shù)據(jù)中記錄的索引就起作用了,可以通過索引將原來的數(shù)據(jù)恢復(fù)到原來的位置中。

          右鍵操作

          右鍵操作和按鍵操作是一樣的,一個(gè)功能兩種觸發(fā)途徑。

          <li @click="copy" v-show="curComponent">復(fù)制li><li @click="paste">粘貼li><li @click="cut" v-show="curComponent">剪切li>
          cut() { this.$store.commit('cut')},
          copy() { this.$store.commit('copy')},
          paste() { this.$store.commit('paste', true)},

          16. 數(shù)據(jù)交互

          方式一

          提前寫好一系列 ajax 請求API,點(diǎn)擊組件時(shí)按需選擇 API,選好 API 再填參數(shù)。例如下面這個(gè)組件,就展示了如何使用 ajax 請求向后臺(tái)交互:

          <template>    <div>{{ propValue.data }}div>template>
          <script>export default { // propValue: { // api: { // request: a, // params, // }, // data: null // } props: { propValue: { type: Object, default: () => {}, }, }, created() { this.propValue.api.request(this.propValue.api.params).then(res => { this.propValue.data = res.data }) },}script>

          方式二

          方式二適合純展示的組件,例如有一個(gè)報(bào)警組件,可以根據(jù)后臺(tái)傳來的數(shù)據(jù)顯示對應(yīng)的顏色。在編輯頁面的時(shí)候,可以通過 ajax 向后臺(tái)請求頁面能夠使用的 websocket 數(shù)據(jù):

          const data = ['status', 'text'...]

          然后再為不同的組件添加上不同的屬性。例如有 a 組件,它綁定的屬性為?status

          // 組件能接收的數(shù)據(jù)props: {    propValue: {        type: String,    },    element: {        type: Object,    },    wsKey: {        type: String,        default: '',    },},

          在組件中通過?wsKey?獲取這個(gè)綁定的屬性。等頁面發(fā)布后或者預(yù)覽時(shí),通過 weboscket 向后臺(tái)請求全局?jǐn)?shù)據(jù)放在 vuex 上。組件就可以通過?wsKey?訪問數(shù)據(jù)了。

          <template>    <div>{{ wsData[wsKey] }}div>template>
          <script>import { mapState } from 'vuex'
          export default { props: { propValue: { type: String, }, element: { type: Object, }, wsKey: { type: String, default: '', }, }, computed: mapState([ 'wsData', ]),script>

          和后臺(tái)交互的方式有很多種,不僅僅包括上面兩種,我在這里僅提供一些思路,以供參考。

          17. 發(fā)布

          頁面發(fā)布有兩種方式:一是將組件數(shù)據(jù)渲染為一個(gè)單獨(dú)的 HTML 頁面;二是從本項(xiàng)目中抽取出一個(gè)最小運(yùn)行時(shí) runtime 作為一個(gè)單獨(dú)的項(xiàng)目。

          這里說一下第二種方式,本項(xiàng)目中的最小運(yùn)行時(shí)其實(shí)就是預(yù)覽頁面加上自定義組件。將這些代碼提取出來作為一個(gè)項(xiàng)目單獨(dú)打包。發(fā)布頁面時(shí)將組件數(shù)據(jù)以 JSON 的格式傳給服務(wù)端,同時(shí)為每個(gè)頁面生成一個(gè)唯一 ID。

          假設(shè)現(xiàn)在有三個(gè)頁面,發(fā)布頁面生成的 ID 為 a、b、c。訪問頁面時(shí)只需要把 ID 帶上,這樣就可以根據(jù) ID 獲取每個(gè)頁面對應(yīng)的組件數(shù)據(jù)。

          www.test.com/?id=awww.test.com/?id=cwww.test.com/?id=b

          按需加載

          如果自定義組件過大,例如有數(shù)十個(gè)甚至上百個(gè)。這時(shí)可以將自定義組件用?import?的方式導(dǎo)入,做到按需加載,減少首屏渲染時(shí)間:

          import Vue from 'vue'
          const components = [ 'Picture', 'VText', 'VButton',]
          components.forEach(key => { Vue.component(key, () => import(`@/custom-component/${key}`))})

          按版本發(fā)布

          自定義組件有可能會(huì)有更新的情況。例如原來的組件使用了大半年,現(xiàn)在有功能變更,為了不影響原來的頁面。建議在發(fā)布時(shí)帶上組件的版本號(hào):

          - v-text  - v1.vue  - v2.vue

          例如?v-text?組件有兩個(gè)版本,在左側(cè)組件列表區(qū)使用時(shí)就可以帶上版本號(hào):

          {  component: 'v-text',  version: 'v1'  ...}

          這樣導(dǎo)入組件時(shí)就可以根據(jù)組件版本號(hào)進(jìn)行導(dǎo)入:

          import Vue from 'vue'import componentList from '@/custom-component/component-list`
          componentList.forEach(component => { Vue.component(component.name, () => import(`@/custom-component/${component.name}/${component.version}`))})

          參考資料

          ?Math[7]?通過Math.atan2 計(jì)算角度[8]?為什么矩陣能用來表示角的旋轉(zhuǎn)?[9]?snapping-demo[10]?vue-next-drag[11]

          References

          [1]?《可視化拖拽組件庫一些技術(shù)要點(diǎn)原理分析》:?https://juejin.cn/post/6908502083075325959
          [2]?github 項(xiàng)目地址:?https://github.com/woai3c/visual-drag-demo
          [3]?在線預(yù)覽:?https://woai3c.github.io/visual-drag-demo
          [4]?Math.atan2():?https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Math/atan2
          [5]?snapping-demo:?https://github.com/shenhudong/snapping-demo/wiki/corner-handle
          [6]?回答:?https://www.zhihu.com/question/67425734/answer/252724399
          [7]?Math:?https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Math
          [8]?通過Math.atan2 計(jì)算角度:?https://www.jianshu.com/p/9817e267925a
          [9]?為什么矩陣能用來表示角的旋轉(zhuǎn)?:?https://www.zhihu.com/question/67425734/answer/252724399
          [10]?snapping-demo:?https://github.com/shenhudong/snapping-demo/wiki/corner-handle
          [11]?vue-next-drag:?https://github.com/lycHub/vue-next-drag


          瀏覽 75
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美成人精品一二三区欧美风情 | 大香蕉伊人在线观看 | www.水蜜桃视频 | 一级黄片学生妹 | 亚洲成人无码网站 |