狠狠色丁香婷婷综合尤物/久久精品综合一区二区三区/中国有色金属学报/国产日韩欧美在线观看 - 国产一区二区三区四区五区tv

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

手擼前端 Grid 拖拽布局

freeflydom
2024年8月15日 12:20 本文熱度 1101

最近有個需求需要實現自定義首頁布局,需要將屏幕按照 6 列 4 行進行等分成多個格子,然后將組件可拖拽對應格子進行渲染展示。

示例

對比一些已有的插件,發現想要實現產品的交互效果,沒有現成可用的。本身功能并不是太過復雜,于是決定自己基于 vue 手擼一個簡易的 Grid 拖拽布局。

完整源碼在此,在線體驗

概況#

需要實現 Grid 拖拽布局,主要了解這兩個東西就行

  • 拖放 API,關于拖放 API 介紹文章有很多 ,可以直接看 MDN 里拖放 API介紹,可以說很詳細了。

  • Grid 布局, Grid 布局與 Flex 布局很相似,但是 Grid 像是二維布局,Flex 則為一維布局,Grid 布局遠比 Flex 布局強大。MDN 關于網格布局介紹

需要實現主要包含:

  • 組件物料欄拖拽到布局容器

  • 布局容器 Grid 布局

  • 放置時是否重疊判斷

  • 拖拽時樣式

  • 放置后樣式

  • 容器內二次拖拽

拖放操作實現#

拖拽中主要使用到的事件如下

被拖拽元素事件:

事件觸發時刻
dragstart當用戶開始拖拽一個元素或選中的文本時觸發。
drag當拖拽元素或選中的文本時觸發。
dragend當拖拽操作結束時觸發

放置容器事件:

事件觸發時刻
?dragenter當拖拽元素或選中的文本到一個可釋放目標時觸發。
dragleave當拖拽元素或選中的文本離開一個可釋放目標時觸發。
dragover當元素或選中的文本被拖到一個可釋放目標上時觸發。
drop當元素或選中的文本在可釋放目標上被釋放時觸發。

可拖拽元素#

讓一個元素能夠拖拽只需要給元素設置 draggable="true" 即可拖拽,拖拽事件 API 提供了 DataTransfer 對象,可以用于設置拖拽數據信息,但是僅僅只能 drop 事件中獲取到,但是我們需要在拖拽中就需要獲取到拖拽信息,用來顯示拖拽時樣式,所以需要我們自己存儲起來,以便讀取。

需要處理主要是,在拖拽時將 將當前元素信息設置到 dragStore 中,結束時清空當前信息

<script setup>

  import { dragStore } from "./drag";


  const props = defineProps<{

    data: DragItem;

    groupName?: string;

  }>();


  const onDragstart = (e) => dragStore.set(props.groupName, { ...props.data });

  const onDragend = () => dragStore.remove(props.groupName);

</script>

<template>

  <div draggable="true" @dragstart="onDragstart" @dragend="onDragend"></div>

</template>

封裝一個存儲方法,然后通過配置相同 key ,可以在同時存在多個放置區域時候,區分開來。

class DragStore<T extends DragItemData> {

  moveItem = new Map<string, DragItemData>();


  set(key: string, data: T) {

    this.moveItem.set(key, data);

  }


  remove(key: string) {

    this.moveItem.delete(key);

  }


  get(key: string): undefined | DragItemData {

    return this.moveItem.get(key);

  }

}

可放置區域#

首先時需要告訴瀏覽器當前區域是可以放置的,只需要在元素監聽 dragenterdragleavedragover 事件即可,然后通過 preventDefault 來阻止瀏覽器默認行為。可以在這三個事件中處理判斷當前位置是否可以放置等等。

示例:

<script setup>

  // 進入放置目標

  const onDragenter = (e) => {

    e.preventDefault();

  };


  // 在目標中移動

  const onDragover = (e) => {

    e.preventDefault();

  };


  // 離開目標

  const onDragleave = (e) => {

    e.preventDefault();

  };

</script>

<template>

  <div @dragenter="onDragenter($event)" @dragover="onDragover($event)" @dragleave="onDragleave($event)" @drop="onDrop($event)"></div>

</template>

上面的代碼已經可以讓,元素可以拖拽,然后當元素拖到可防止區域時候,可以看到鼠標樣式會變為可放置樣式了。

Grid 布局#

我們是需要進行 Grid 拖拽布局,所以先對上面放置容器進行改造,首先就是需要將容器進行格子劃分區域顯示。

計算 Grid 格子大小#

我這里直接使用了 @vueuse/core 的 useElementSize 的 hooks 去獲取容器元素大小變動,也可以自己通過 ResizeObserver 去監聽元素變動,然后根據設置列數、行數、間隔去計算單個格子大小。

import { useElementSize } from "@vueuse/core";


/**

 * 容器等分尺寸

 * @param {*} target 容器 HTML

 * @param {*} column 列數

 * @param {*} row 行數

 * @param {*} gap 間隔

 * @returns

 */

export const useBoxSize = (target: Ref<HTMLElement | undefined>, column: number, row: number, gap: number) => {

  const { width, height } = useElementSize(target);

  return computed(() => ({

    width: (width.value - (column - 1) * gap) / column,

    height: (height.value - (row - 1) * gap) / row,

  }));

};

設置 Grid 樣式#

根據列數和行數循環生成格子數,rowCountcolumnCount為行數和列數。

<div class="drop-content__drop-container" @dragenter="onDragenter($event)" @dragover="onDragover($event)" @dragleave="onDragleave($event)" @drop="onDrop($event)">

  <template v-for="x in rowCount">

    <div class="bg-column" v-for="y in columnCount" :key="`${x}-${y}`"></div>

  </template>

</div>

設置 Grid 樣式,下面變量中 gap 為格子間隔,repeat 是 Grid 用來重復設置相同值的,grid-template-columns: repeat(2,100px) 等效于 grid-template-columns: 100px 100px。因為我們只需在容器里監聽拖拽放置事件,所以我們還需要將
所有的 bg-column 事件去掉,設置 pointer-events: none 即可。

.drop-content__drop-container {

  display: grid;

  row-gap: v-bind("gap+'px'");

  column-gap: v-bind("gap+'px'");

  grid-template-columns: repeat(v-bind("columnCount"), v-bind("boxSize.width+'px'"));

  grid-template-rows: repeat(v-bind("rowCount"), v-bind("boxSize.height+'px'"));

  .bg-column {

    background-color: #fff;

    border-radius: 6px;

    pointer-events: none;

  }

}

效果如下:

Grid 容器樣式

放置元素#

放置元素時我們需要先計算出元素在 Grid 位置信息等,這樣才知道元素應該放置那哪個地方。

拖拽位置計算#

當元素拖拽進容器中時,我們可以通過 offsetXoffsetY兩個數據獲取當前鼠標距離容器左上角位置距離,我們可以根據這兩個值計算出對應的在 Grid 中做坐標。

計算方式:

// 計算 x 坐標

const getX = (num) => parseInt(num / (boxSizeWidth + gap));

// 計算 y 坐標

const getY = (num) => parseInt(num / (boxSizeHeight + gap));

需要注意的是上面計算坐標是 0,0 開始的,而 Grid 是 1,1 開始的。

獲取拖拽信息#

我們在進入容器時,通過上面封裝 dragData 來獲取當前拖拽元素信息,獲取它尺寸信息等等。

// 拖拽中的元素

const current = reactive({

  show: <boolean>false,

  id: <undefined | number>undefined,

  column: <number>0, // 寬

  row: <number>0, // 高

  x: <number>0, // 列

  y: <number>0, // 行

});


// 進入放置目標

const onDragenter = (e) => {

  e.preventDefault();

  const dragData = dragStore.get(props.groupName);

  if (dragData) {

    current.column = dragData.column;

    current.row = dragData.row;

    current.x = getX(e.offsetX);

    current.y = getY(e.offsetY);

    current.show = true;

  }

};


// 在目標中移動

const onDragover = (e) => {

  e.preventDefault();

  const dragData = dragStore.get(props.groupName);

  if (dragData) {

    current.x = getX(e.offsetX);

    current.y = getY(e.offsetY);

  }

};


const onDragleave = (e) => {

  e.preventDefault();

  current.show = false;

  current.id = undefined;

};

在 drop 事件中,我們將當前拖拽元素存放起來,list 會存放每一次拖拽進來元素信息。

const list = ref([]);


// 放置在目標上

const onDrop = async (e) => {

  e.preventDefault();

  current.show = false;

  const item = dragStore.get(props.groupName);


  list.value.push({

    ...item,

    x: current.x,

    y: current.y,

    id: new Date().getTime(),

  });

};

計算碰撞#

在上面還需要計算當前拖拽的位置是否可以放置,需要處理是否包含在容器內,是否與其他已放置元素存在重疊等等。

計算是否在容器內#

這個是比較好計算的,只需要當前拖拽位置左上角坐標 >= 容器左上角的坐標,然后右下角的坐標 <= 容器的右下角的坐標,就是在容器內的。

代碼實現:

/**

 * 判斷是否在當前四邊形內

 * @param {*} p1 父容器

 * @param {*} p2

 *  對應是 左上角坐標 和 右下角坐標

 *  [0,0,1,1]  => 左上角坐標 0,0  右下角 1,1

 */

export const booleanWithin = (p1: [number, number, number, number], p2: [number, number, number, number]) => {

  return p1[0] <= p2[0] && p1[1] <= p2[1] && p1[2] >= p2[2] && p1[3] >= p2[3];

};

計算是否與現有的相交#

兩個矩形相交情況有很多種,計算比較麻煩,但是我們可以計算他們不相交,然后在取反方式判斷是否相交。

不相交情況只有四種,假設有 p1、p2 連個矩形,它們不相交的情況只有四種:

  • p1 在 p2 左邊

  • p1 在 p2 右邊

  • p1 在 p2 上邊

  • p1 在 p2 下邊

代碼實現:

/**

 * 判斷是兩四邊形是否相交

 * @param {*} p1 父容器

 * @param {*} p2

 *  對應是 左上角坐標 和 右下角坐標

 *  [0,0,1,1]  => 左上角坐標 0,0  右下角 1,1

 */

export const booleanIntersects = (p1: [number, number, number, number], p2: [number, number, number, number]) => {

  return !(p1[2] <= p2[0] || p2[2] <= p1[0] || p1[3] <= p2[1] || p2[3] <= p1[1]);

};

在放置前判斷#

// 是否可以放置

const isPutDown = computed(() => {

  const currentXy = [current.x, current.y, current.x + current.column, current.y + current.row];

  return (

    booleanWithin([0, 0, columnCount.value, rowCount.value], currentXy) && //

    list.value.every((item) => item.id === current.id || !booleanIntersects([item.x, item.y, item.x + item.column, item.y + item.row], currentXy))

  );

});

拖拽時樣式#

上處理了基本拖放數據處理邏輯,為了更好的交互,我們可以在拖拽中顯示元素預占位信息,更加直觀的顯示元素占位大小,類似這樣:

可放置示例

我們可以根據上面 current 中信息去計算大小信息,還可以根據 isPutDown 去判斷當前位置是否可以放置,用來顯示不同交互效果。

不可放置示例

可以直接通過 Grid 的 grid-area 屬性,快速計算出放置位置信息,應為我們上面計算的 x 、y 是從 0 開始的,所以這里需要 +1。

grid-area: `${y + 1} / ${x + 1} / ${y + row + 1}/ ${ x + column + 1 }`

預覽容器#

在元素放置后,我們還需要根據 list 中數據,生成元素占位樣式處理,我們可以拖拽容器上層在放置一個容器,專門用來顯示放置后的樣式,也是可以直接使用 Grid 布局去處理。

預覽樣式#

樣式基本上和 drop-container 樣式抱持一致即可,需要注意的時需要為預覽容器設置 pointer-events: none,避免遮擋了 drop-container 事件監聽。

.drop-content__preview,

.drop-content__drop-container {

  // ...

}

每個元素位置信息計算方式,基本和拖拽時樣式計算方式一致,直接通過 grid-area 去布局就可以了。

grid-area: `${y + 1} / ${x + 1} / ${y + row + 1}/ ${ x + column + 1 }`

示例

二次拖拽#

當元素拖拽進來后,我們還需要對放置的元素支持繼續拖拽。因為上面我們將預覽事件通過 pointer-events 去除了,所以我們需要給每個子元素都加上去。然后給子元素添加 draggable="true",然后處理拖拽事件,基本上和上面處理方式一樣,在 dragstartdragend 處理拖拽元素信息。

然后我們還需在 onDrop 進行一番修改,如果是二次拖拽時只需要修改坐標信息,修改原 onDrop 處理方式:

if (item.id) {

  item.x = current.x;

  item.y = current.y;

} else {

  list.value.push({

    ...item,

    x: current.x,

    y: current.y,

    id: new Date().getTime(),

  });

}

位置偏移優化#

當你對元素二次拖拽時,會發現元素會存在偏移問。比如你放置了一個 1x2 元素后,當你從下面拖拽,你會發現拖拽中的占位樣式和你拖拽元素位置存在偏差。

效果如下圖

示例

出現這情況應為上面我們時根據鼠標位置為左上角進行計算的,所以會存在這種偏差問題,我們可在拖拽前計算出偏移量來校正位置。

我們可以在二次拖拽時,獲取到鼠標在當前元素內位置信息、

const onDragstart = (e) => {

  const data = props.data;

  data.offsetX = e.offsetX;

  data.offsetY = e.offsetY;

  dragStore.set(props.groupName, data);

};

在 drop-container 內計算 x、y 值時候減去偏移量,對 onDragenteronDragover 進行如下調整修改

current.x = getX(e.offsetX) - getX(dragData?.offsetX ?? 0);

current.y = getY(e.offsetY) - getY(dragData?.offsetY ?? 0);

拖拽元素優化#

因為上面我們將預覽元素添加了 pointer-events: all,所以在我們拖拽到現有元素上時,會擋住 drop-container 事件的觸發,在二次拖拽時,比如將一個 2x2 元素我們需要往下移動一格時,會發現也會被自己擋住。

預覽元素遮擋問題,可以在拖拽時將其他元素都設置為 none,二次拖拽時要做自己設置為 all 否則會無法拖拽

:style="{ pointerEvents: current.show && item.id !== current.id ? 'none' : 'all' }"`

二次拖拽時自己位置遮擋問題
我們可以在拖拽時增加標識,將自己通過 transform 移除到多拽容器外去

moveing.value

  ? {

      opacity: 0,

      transform: `translate(-999999999px, -9999999999px)`,

    }

  : {};

結語#

到目前為止基本上的 Grid 拖拽布局大致實現了,已經滿足基本業務需求了,當然有需要朋友還可以在上面增加支持拖拉調整大小、碰撞后自動調整位置等等。

完整源碼在此,在線體驗


轉自https://www.cnblogs.com/nextl/p/17871913.html


該文章在 2024/8/15 16:02:53 編輯過
關鍵字查詢
相關文章
正在查詢...
點晴ERP是一款針對中小制造業的專業生產管理軟件系統,系統成熟度和易用性得到了國內大量中小企業的青睞。
點晴PMS碼頭管理系統主要針對港口碼頭集裝箱與散貨日常運作、調度、堆場、車隊、財務費用、相關報表等業務管理,結合碼頭的業務特點,圍繞調度、堆場作業而開發的。集技術的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業的高效ERP管理信息系統。
點晴WMS倉儲管理系統提供了貨物產品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質期管理,貨位管理,庫位管理,生產管理,WMS管理系統,標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務都免費,不限功能、不限時間、不限用戶的免費OA協同辦公管理系統。
Copyright 2010-2025 ClickSun All Rights Reserved