Vue实现选中多张图片一起拖拽功能

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue图片框选拖拽功能</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }

    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
      color: #333;
      min-height: 100vh;
      padding: 20px;
      display: flex;
      justify-content: center;
      align-items: center;
    }

    .container {
      display: flex;
      flex-direction: column;
      width: 100%;
      max-width: 1200px;
      background: rgba(255, 255, 255, 0.92);
      border-radius: 15px;
      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
      overflow: hidden;
    }

    header {
      text-align: center;
      padding: 25px;
      background: linear-gradient(to right, #3494E6, #EC6EAD);
      color: white;
    }

    h1 {
      font-size: 2.5rem;
      margin-bottom: 10px;
      text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
    }

    .subtitle {
      font-size: 1.2rem;
      opacity: 0.9;
    }

    .content {
      display: flex;
      padding: 30px;
      min-height: 600px;
      gap: 30px;
    }

    .panel {
      flex: 1;
      padding: 25px;
      border-radius: 12px;
      background: #f8f9fa;
      box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
      transition: all 0.3s ease;
      display: flex;
      flex-direction: column;
    }

    .panel-header {
      margin-bottom: 20px;
      padding-bottom: 15px;
      border-bottom: 2px solid #e0e0e0;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .panel-title {
      font-size: 1.8rem;
      color: #2c3e50;
      font-weight: 600;
    }

    .counter {
      background: #3498db;
      color: white;
      padding: 5px 12px;
      border-radius: 20px;
      font-weight: bold;
      font-size: 1.1rem;
    }

    .images-container {
      flex: 1;
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
      gap: 20px;
      padding: 10px;
      overflow-y: auto;
      max-height: 450px;
    }

    .image-item {
      position: relative;
      border-radius: 10px;
      overflow: hidden;
      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
      cursor: pointer;
      transition: all 0.3s ease;
      aspect-ratio: 1/1;
    }

    .image-item img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      display: block;
    }

    .image-item.selected {
      transform: scale(0.95);
      box-shadow: 0 0 0 4px #3498db, 0 8px 16px rgba(0, 0, 0, 0.2);
    }

    .image-item.selected::after {
      content: "✓";
      position: absolute;
      top: 10px;
      right: 10px;
      width: 24px;
      height: 24px;
      background: #3498db;
      color: white;
      border-radius: 50%;
      display: flex;
      justify-content: center;
      align-items: center;
      font-weight: bold;
    }

    .instructions {
      margin-top: 15px;
      padding: 15px;
      background: #e3f2fd;
      border-radius: 8px;
      font-size: 0.95rem;
    }

    .instructions h3 {
      margin-bottom: 8px;
      color: #1565c0;
    }

    .instructions ul {
      padding-left: 20px;
    }

    .instructions li {
      margin: 5px 0;
    }

    /* 拖拽效果 */
    .drag-over {
      background: rgba(52, 152, 219, 0.15);
      box-shadow: inset 0 0 0 4px #3498db;
    }

    /* 动画效果 */
    .fade-move {
      transition: transform 0.5s;
    }

    @media (max-width: 768px) {
      .content {
        flex-direction: column;
      }

      .panel {
        min-height: 400px;
      }
    }
  </style>
</head>

<body>
  <div id="app">
    <div class="container">
      <header>
        <h1>Vue图片框选拖拽功能</h1>
        <div class="subtitle">鼠标框选图片后拖拽到目标区域</div>
      </header>

      <div class="content">
        <!-- 左侧图库 -->
        <div class="panel" :class="{ 'drag-over': isDragOverSource }" @dragover.prevent="handleDragOver('source')"
          @dragleave="handleDragLeave('source')" @drop="handleDrop($event, 'source')">
          <div class="panel-header">
            <h2 class="panel-title">图片库</h2>
            <div class="counter">{{ sourceImages.length }} 张图片</div>
          </div>

          <div class="images-container" @mousedown="startSelection" @mousemove="updateSelection" @mouseup="endSelection"
            @dragstart="handleGroupDragStart" draggable="true" ref="sourceContainer">
            <div v-for="(image, index) in sourceImages" :key="'source-' + image.id" class="image-item"
              :class="{ 'selected': selectedImages.includes(image.id) }" @click="toggleSelect($event, image.id)"
              :ref="'source-img-' + image.id">
              <img :src="image.url" :alt="'图片' + image.id">
            </div>
            <div class="selection-box" v-if="isSelecting" :style="selectionBoxStyle"></div>
          </div>

          <div class="instructions">
            <h3>操作指南</h3>
            <ul>
              <li>点击图片进行选择(按住 Ctrl/Command 可多选)</li>
              <li>按住鼠标<strong>拖动框选</strong>多个图片</li>
              <li>拖拽选中图片到右侧收藏夹</li>
            </ul>
          </div>
        </div>

        <!-- 右侧收藏夹 -->
        <div class="panel" :class="{ 'drag-over': isDragOverTarget }" @dragover.prevent="handleDragOver('target')"
          @dragleave="handleDragLeave('target')" @drop="handleDrop($event, 'target')">
          <div class="panel-header">
            <h2 class="panel-title">我的收藏夹</h2>
            <div class="counter">{{ targetImages.length }} 张图片</div>
          </div>

          <div class="images-container" @mousedown="startSelection" @mousemove="updateSelection" @mouseup="endSelection"
            @dragstart="handleGroupDragStart" draggable="true" ref="targetContainer">
            <div v-for="(image, index) in targetImages" :key="'target-' + image.id" class="image-item"
              :class="{ 'selected': selectedImages.includes(image.id) }" @click="toggleSelect($event, image.id)"
              :ref="'target-img-' + image.id">
              <img :src="image.url" :alt="'图片' + image.id">
            </div>
            <div class="selection-box" v-if="isSelecting" :style="selectionBoxStyle"></div>
          </div>

          <div class="instructions">
            <h3>提示</h3>
            <ul>
              <li>已选择 <span class="highlight">{{ selectedImages.length }}</span> 张图片</li>
              <li>从收藏夹拖回图片到图库可移除</li>
              <li>支持跨区域拖拽操作</li>
            </ul>
          </div>
        </div>
      </div>

      <div class="status-bar">
        <div>当前状态: {{ statusMessage }}</div>
        <div>已选择: {{ selectedImages.length }} 张图片</div>
      </div>
    </div>
  </div>

  <script>
    function getImageUrl(id) {
      return `https://picsum.photos/200/200?random=${id}`;
    }

    new Vue({
      el: '#app',
      data: {
        sourceImages: Array.from({ length: 15 }, (_, i) => ({
          id: i + 1,
          url: getImageUrl(i + 1)
        })),
        targetImages: [],
        selectedImages: [],
        isDragOverSource: false,
        isDragOverTarget: false,
        lastSelectedIndex: -1,

        // 框选相关
        isSelecting: false,
        selectionStart: { x: 0, y: 0 },
        selectionEnd: { x: 0, y: 0 },
        currentContainer: null,
        statusMessage: "就绪"
      },
      computed: {
        selectionBoxStyle() {
          const left = Math.min(this.selectionStart.x, this.selectionEnd.x);
          const top = Math.min(this.selectionStart.y, this.selectionEnd.y);
          const width = Math.abs(this.selectionEnd.x - this.selectionStart.x);
          const height = Math.abs(this.selectionEnd.y - this.selectionStart.y);

          return {
            left: `${left}px`,
            top: `${top}px`,
            width: `${width}px`,
            height: `${height}px`,
            display: width > 2 && height > 2 ? 'block' : 'none'
          };
        }
      },
      methods: {
        startSelection(event) {
          if (event.target.classList.contains('image-item') ||
            event.target.parentElement.classList.contains('image-item')) {
            return;
          }

          this.isSelecting = true;
          this.currentContainer = event.currentTarget;
          const rect = this.currentContainer.getBoundingClientRect();

          this.selectionStart = {
            x: event.clientX - rect.left,
            y: event.clientY - rect.top
          };

          this.selectionEnd = { ...this.selectionStart };
          this.statusMessage = "框选操作中...";
        },

        updateSelection(event) {
          if (!this.isSelecting) return;

          const rect = this.currentContainer.getBoundingClientRect();
          this.selectionEnd = {
            x: event.clientX - rect.left,
            y: event.clientY - rect.top
          };

          const left = Math.min(this.selectionStart.x, this.selectionEnd.x);
          const top = Math.min(this.selectionStart.y, this.selectionEnd.y);
          const right = Math.max(this.selectionStart.x, this.selectionEnd.x);
          const bottom = Math.max(this.selectionStart.y, this.selectionEnd.y);

          const containerId = this.currentContainer === this.$refs.sourceContainer ? 'source' : 'target';
          const images = containerId === 'source' ? this.sourceImages : this.targetImages;

          images.forEach(image => {
            const imgRef = this.$refs[`${containerId}-img-${image.id}`][0];
            if (!imgRef) return;

            const imgRect = imgRef.getBoundingClientRect();
            const containerRect = this.currentContainer.getBoundingClientRect();

            const imgLeft = imgRect.left - containerRect.left;
            const imgTop = imgRect.top - containerRect.top;
            const imgRight = imgLeft + imgRect.width;
            const imgBottom = imgTop + imgRect.height;

            const isOverlapping =
              imgLeft < right &&
              imgRight > left &&
              imgTop < bottom &&
              imgBottom > top;

            if (isOverlapping) {
              if (!this.selectedImages.includes(image.id)) {
                this.selectedImages.push(image.id);
              }
            }
          });
        },

        endSelection() {
          if (!this.isSelecting) return;

          this.isSelecting = false;
          this.statusMessage = `已选择 ${this.selectedImages.length} 张图片`;
        },

        toggleSelect(event, imageId) {
          event.stopPropagation();

          if (event.shiftKey && this.lastSelectedIndex !== -1) {
            const currentIndex = this.findImageIndex(imageId);
            const start = Math.min(this.lastSelectedIndex, currentIndex);
            const end = Math.max(this.lastSelectedIndex, currentIndex);

            const allImages = [...this.sourceImages, ...this.targetImages];
            const range = allImages.slice(start, end + 1);

            this.selectedImages = range.map(img => img.id);
          } else if (event.ctrlKey || event.metaKey) {
            const index = this.selectedImages.indexOf(imageId);
            if (index > -1) {
              this.selectedImages.splice(index, 1);
            } else {
              this.selectedImages.push(imageId);
            }
            this.lastSelectedIndex = this.findImageIndex(imageId);
          } else {
            if (this.selectedImages.includes(imageId) && this.selectedImages.length === 1) {
              this.selectedImages = [];
            } else {
              this.selectedImages = [imageId];
            }
            this.lastSelectedIndex = this.findImageIndex(imageId);
          }

          this.statusMessage = `已选择 ${this.selectedImages.length} 张图片`;
        },

        findImageIndex(imageId) {
          const allImages = [...this.sourceImages, ...this.targetImages];
          return allImages.findIndex(img => img.id === imageId);
        },

        handleGroupDragStart(event) {
          if (this.selectedImages.length === 0) {
            event.preventDefault();
            return;
          }

          event.dataTransfer.setData('text/plain', JSON.stringify(this.selectedImages));
          event.dataTransfer.effectAllowed = 'move';
          this.statusMessage = "拖拽操作中...";
        },

        handleDragOver(area) {
          if (area === 'source') {
            this.isDragOverSource = true;
            this.isDragOverTarget = false;
          } else {
            this.isDragOverSource = false;
            this.isDragOverTarget = true;
          }
        },

        handleDragLeave(area) {
          if (area === 'source') {
            this.isDragOverSource = false;
          } else {
            this.isDragOverTarget = false;
          }
        },

        handleDrop(event, targetArea) {
          event.preventDefault();

          this.isDragOverSource = false;
          this.isDragOverTarget = false;

          const imageIdsToMove = JSON.parse(event.dataTransfer.getData('text/plain'));

          if (imageIdsToMove.length === 0) return;

          const sourceArea = this.sourceImages.some(img => imageIdsToMove.includes(img.id)) ? 'source' : 'target';

          if (sourceArea === targetArea) return;

          this.moveImages(imageIdsToMove, sourceArea, targetArea);

          this.selectedImages = [];
          this.statusMessage = `已移动 ${imageIdsToMove.length} 张图片`;
        },

        moveImages(imageIds, sourceArea, targetArea) {
          const sourceArray = sourceArea === 'source' ? this.sourceImages : this.targetImages;
          const targetArray = targetArea === 'source' ? this.sourceImages : this.targetImages;

          const imagesToMove = sourceArray.filter(img => imageIds.includes(img.id));

          if (sourceArea === 'source') {
            this.sourceImages = sourceArray.filter(img => !imageIds.includes(img.id));
          } else {
            this.targetImages = sourceArray.filter(img => !imageIds.includes(img.id));
          }

          if (targetArea === 'source') {
            this.sourceImages = [...this.sourceImages, ...imagesToMove];
          } else {
            this.targetImages = [...this.targetImages, ...imagesToMove];
          }
        }
      }
    });
  </script>
</body>

</html>
相关推荐
剪刀石头布啊5 分钟前
泛洪DFS、BFS
前端
qetfw6 分钟前
Windows Server AD CS:根 CA、Web 证书模板与域内自动注册
前端·windows·windows-server
剪刀石头布啊6 分钟前
为什么React组件很少前缀,而vue的不少组件都有前缀
前端
剪刀石头布啊12 分钟前
gird网格布局
前端
瞬时出道12 分钟前
AI 回答为什么能一个字一个字蹦出来?前端搞懂 SSE 这一篇就够了
vue.js
啃火龙果的兔子13 分钟前
Google Chrome(谷歌浏览器)常用快捷键
前端·chrome
剪刀石头布啊16 分钟前
js真的是单线程实现异步并发么?
前端
剪刀石头布啊20 分钟前
js中改变this指向的操作有哪些
前端
剪刀石头布啊21 分钟前
vue、react 列表中使用 index 作为 key 的话,修改某一个元素后会发生什么
前端
方方洛22 分钟前
glowglow:语言无关的语法高亮器是怎么实现的
前端·前端框架