<!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>