267 lines
6.6 KiB
Markdown
267 lines
6.6 KiB
Markdown
# 🔄 SelectFile组件拖拽调整顺序功能演示
|
||
|
||
## 🎯 功能概述
|
||
|
||
我已经成功为SelectFile组件添加了拖拽调整顺序的功能,让用户可以通过拖拽来重新排列文件的顺序。
|
||
|
||
### ✨ 核心功能
|
||
|
||
1. **🔄 拖拽排序**
|
||
- 支持鼠标拖拽调整文件顺序
|
||
- 实时视觉反馈和拖拽指示器
|
||
- 顺序指示器显示当前位置
|
||
|
||
2. **🎯 智能交互**
|
||
- 拖拽时显示拖拽指示器
|
||
- 悬停时显示拖拽提示
|
||
- 拖拽完成后自动更新数据
|
||
|
||
3. **📍 视觉反馈**
|
||
- 顺序指示器显示文件位置
|
||
- 拖拽时的视觉效果
|
||
- 悬停时的交互提示
|
||
|
||
### 🔧 技术实现
|
||
|
||
#### 1. 组件模板更新
|
||
|
||
```vue
|
||
<template>
|
||
<a-image-preview-group>
|
||
<div class="select-file-container">
|
||
<!-- 🔄 可拖拽的文件列表 -->
|
||
<div
|
||
class="draggable-file-list"
|
||
@dragover.prevent
|
||
@drop="onDrop"
|
||
>
|
||
<template v-for="(item, index) in localData" :key="item.id || index">
|
||
<div
|
||
class="image-upload-item draggable-item"
|
||
:class="{ 'dragging': dragIndex === index }"
|
||
draggable="true"
|
||
@dragstart="onDragStart(index, $event)"
|
||
@dragend="onDragEnd"
|
||
@dragenter="onDragEnter(index)"
|
||
@dragleave="onDragLeave"
|
||
v-if="isImage(item.url)"
|
||
>
|
||
<!-- 🎯 拖拽指示器 -->
|
||
<div class="drag-indicator">
|
||
<HolderOutlined />
|
||
</div>
|
||
|
||
<a-image :src="item.url" />
|
||
|
||
<!-- 📍 顺序指示器 -->
|
||
<div class="order-indicator">{{ index + 1 }}</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</a-image-preview-group>
|
||
</template>
|
||
```
|
||
|
||
#### 2. 拖拽逻辑实现
|
||
|
||
```typescript
|
||
// 🔄 拖拽相关状态
|
||
const dragIndex = ref<number | null>(null);
|
||
const dragOverIndex = ref<number | null>(null);
|
||
|
||
// 📝 本地数据副本,用于拖拽操作
|
||
const localData = ref<any[]>([]);
|
||
|
||
// 🔄 监听props.data变化,同步到localData
|
||
watch(
|
||
() => props.data,
|
||
(newData) => {
|
||
if (newData) {
|
||
localData.value = [...newData];
|
||
}
|
||
},
|
||
{ immediate: true, deep: true }
|
||
);
|
||
|
||
// 🔄 拖拽开始
|
||
const onDragStart = (index: number, event: DragEvent) => {
|
||
dragIndex.value = index;
|
||
if (event.dataTransfer) {
|
||
event.dataTransfer.effectAllowed = 'move';
|
||
event.dataTransfer.setData('text/html', index.toString());
|
||
}
|
||
};
|
||
|
||
// 🔄 拖拽结束
|
||
const onDragEnd = () => {
|
||
dragIndex.value = null;
|
||
dragOverIndex.value = null;
|
||
};
|
||
|
||
// 🔄 拖拽进入
|
||
const onDragEnter = (index: number) => {
|
||
dragOverIndex.value = index;
|
||
};
|
||
|
||
// 🔄 拖拽放置
|
||
const onDrop = (event: DragEvent) => {
|
||
event.preventDefault();
|
||
|
||
if (dragIndex.value !== null && dragOverIndex.value !== null && dragIndex.value !== dragOverIndex.value) {
|
||
const newData = [...localData.value];
|
||
const draggedItem = newData[dragIndex.value];
|
||
|
||
// 移除拖拽的项目
|
||
newData.splice(dragIndex.value, 1);
|
||
|
||
// 在新位置插入项目
|
||
const insertIndex = dragIndex.value < dragOverIndex.value ? dragOverIndex.value - 1 : dragOverIndex.value;
|
||
newData.splice(insertIndex, 0, draggedItem);
|
||
|
||
// 更新本地数据
|
||
localData.value = newData;
|
||
|
||
// 触发重新排序事件
|
||
emit('reorder', newData);
|
||
}
|
||
|
||
dragIndex.value = null;
|
||
dragOverIndex.value = null;
|
||
};
|
||
```
|
||
|
||
#### 3. 事件定义更新
|
||
|
||
```typescript
|
||
const emit = defineEmits<{
|
||
(e: 'done', data: FileRecord): void;
|
||
(e: 'del', index: number): void;
|
||
(e: 'clear'): void;
|
||
(e: 'reorder', data: any[]): void; // 新增重新排序事件
|
||
}>();
|
||
```
|
||
|
||
#### 4. 样式设计
|
||
|
||
```less
|
||
// 🔄 可拖拽项目样式
|
||
.draggable-item {
|
||
position: relative;
|
||
cursor: move;
|
||
transition: all 0.3s ease;
|
||
border-radius: 8px;
|
||
|
||
&:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
|
||
.drag-indicator {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
&.dragging {
|
||
opacity: 0.5;
|
||
transform: rotate(5deg) scale(0.95);
|
||
z-index: 1000;
|
||
}
|
||
}
|
||
|
||
// 🎯 拖拽指示器
|
||
.drag-indicator {
|
||
position: absolute;
|
||
top: -8px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: rgba(0, 0, 0, 0.8);
|
||
color: white;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
opacity: 0;
|
||
transition: opacity 0.2s ease;
|
||
z-index: 10;
|
||
pointer-events: none;
|
||
}
|
||
|
||
// 📍 顺序指示器
|
||
.order-indicator {
|
||
position: absolute;
|
||
top: -6px;
|
||
right: -6px;
|
||
background: #1890ff;
|
||
color: white;
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: bold;
|
||
z-index: 5;
|
||
border: 2px solid white;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||
}
|
||
```
|
||
|
||
### 🔗 父组件集成
|
||
|
||
在articleEdit.vue中添加对reorder事件的处理:
|
||
|
||
```vue
|
||
<SelectFile
|
||
:placeholder="`请选择图片`"
|
||
:limit="6"
|
||
:data="files"
|
||
@done="chooseFile"
|
||
@del="onDeleteFile"
|
||
@reorder="onReorderFiles"
|
||
/>
|
||
```
|
||
|
||
```typescript
|
||
// 🔄 处理文件重新排序
|
||
const onReorderFiles = (newData: any[]) => {
|
||
files.value = newData;
|
||
form.files = JSON.stringify(files.value.map((d) => d.url));
|
||
message.success('文件顺序已更新');
|
||
};
|
||
```
|
||
|
||
### 🎨 用户体验
|
||
|
||
1. **直观操作**:用户可以直接拖拽文件来调整顺序
|
||
2. **视觉反馈**:拖拽时有清晰的视觉指示
|
||
3. **顺序显示**:每个文件都有序号显示当前位置
|
||
4. **即时更新**:拖拽完成后立即更新数据和UI
|
||
|
||
### 📊 功能特点
|
||
|
||
| 特性 | 描述 | 状态 |
|
||
|------|------|------|
|
||
| 拖拽排序 | 支持鼠标拖拽调整顺序 | ✅ |
|
||
| 视觉反馈 | 拖拽时的动画和指示器 | ✅ |
|
||
| 顺序指示 | 显示文件的当前位置 | ✅ |
|
||
| 数据同步 | 拖拽后自动更新数据 | ✅ |
|
||
| 事件通知 | 触发reorder事件通知父组件 | ✅ |
|
||
| 响应式设计 | 适配不同屏幕尺寸 | ✅ |
|
||
|
||
### 🚀 使用场景
|
||
|
||
1. **文章封面图排序**:调整封面图的显示顺序,第一张作为主封面
|
||
2. **图片轮播排序**:调整轮播图片的播放顺序
|
||
3. **文件优先级**:根据重要性调整文件的排列顺序
|
||
4. **展示顺序**:调整图片在前端的展示顺序
|
||
|
||
### 💡 技术亮点
|
||
|
||
- **原生HTML5拖拽API**:使用标准的拖拽事件
|
||
- **Vue3响应式**:利用Vue3的响应式系统
|
||
- **数据双向绑定**:保持组件内外数据同步
|
||
- **优雅的动画效果**:提供流畅的用户体验
|
||
- **类型安全**:完整的TypeScript类型定义
|
||
|
||
这个功能让用户可以更直观地管理文件顺序,特别是在处理多个封面图时,可以轻松调整哪张图片作为主封面显示!🎉
|