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

发布于:2025-06-24 ⋅ 阅读:(13) ⋅ 点赞:(0)
<!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>

网站公告

今日签到

点亮在社区的每一天
去签到