28 changed files with 2801 additions and 2084 deletions
@ -0,0 +1,266 @@ |
|||
# 🔄 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类型定义 |
|||
|
|||
这个功能让用户可以更直观地管理文件顺序,特别是在处理多个封面图时,可以轻松调整哪张图片作为主封面显示!🎉 |
File diff suppressed because it is too large
@ -0,0 +1,102 @@ |
|||
<template> |
|||
<a-card :bordered="false"> |
|||
<a-form |
|||
ref="formRef" |
|||
:model="form" |
|||
:label-col="styleResponsive ? { md: 3, sm: 5, xs: 24 } : { flex: '90px' }" |
|||
:wrapper-col="styleResponsive ? { md: 9, sm: 19, xs: 24 } : { flex: '1' }" |
|||
> |
|||
<a-form-item label="允许被搜索" name="name" extra="关闭后,用户无法通过名称搜索到此网站"> |
|||
<a-switch v-model:checked="form.searched" checked-children="允许" un-checked-children="不允许" @change="save" /> |
|||
</a-form-item> |
|||
<a-form-item label="文章发布审核" name="articleReview" extra="开启需要审核后发布,关闭则直接发布"> |
|||
<a-switch v-model:checked="form.articleReview" checked-children="需要" un-checked-children="不需要" @change="save" /> |
|||
</a-form-item> |
|||
<a-form-item label="开发者模式" name="plugin" extra="开启开发者模式"> |
|||
<a-switch v-model:checked="form.plugin" checked-children="启用" un-checked-children="禁用" @change="save" /> |
|||
</a-form-item> |
|||
<a-form-item label="隐藏底部版权信息" name="showAdminCopyright"> |
|||
<a-switch v-model:checked="form.showAdminCopyright" checked-children="显示" un-checked-children="隐藏" @change="save" /> |
|||
</a-form-item> |
|||
</a-form> |
|||
</a-card> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
import {ref, watch} from 'vue'; |
|||
import {message} from 'ant-design-vue'; |
|||
import {useThemeStore} from '@/store/modules/theme'; |
|||
import {storeToRefs} from 'pinia'; |
|||
import {FormInstance} from 'ant-design-vue/es/form'; |
|||
import useFormData from '@/utils/use-form-data'; |
|||
import {addSetting, updateSettingByKey} from "@/api/system/setting"; |
|||
import {useWebsiteSettingStore} from "@/store/modules/setting"; |
|||
|
|||
const props = defineProps<{ |
|||
value?: string; |
|||
// 修改回显的数据 |
|||
data?: any | null; |
|||
}>(); |
|||
|
|||
// 是否开启响应式布局 |
|||
const themeStore = useThemeStore(); |
|||
const settingStore = useWebsiteSettingStore(); |
|||
const {styleResponsive} = storeToRefs(themeStore); |
|||
// 提交状态 |
|||
const loading = ref(false); |
|||
// 是否是修改 |
|||
const isUpdate = ref(false); |
|||
// |
|||
const formRef = ref<FormInstance | null>(null); |
|||
// 表单数据 |
|||
const {form, resetFields, assignFields} = useFormData<any>({ |
|||
settingId: undefined, |
|||
settingKey: 'privacy', |
|||
searched: undefined, |
|||
showAdminCopyright: '' |
|||
}); |
|||
|
|||
/* 保存编辑 */ |
|||
const save = () => { |
|||
if (!formRef.value) { |
|||
return; |
|||
} |
|||
formRef.value |
|||
.validate() |
|||
.then(() => { |
|||
loading.value = true; |
|||
// 更新状态 |
|||
settingStore.setSetting(form) |
|||
const appForm = { |
|||
...form, |
|||
content: JSON.stringify(form) |
|||
}; |
|||
const saveOrUpdate = isUpdate.value ? updateSettingByKey : addSetting; |
|||
saveOrUpdate(appForm) |
|||
.then(() => { |
|||
message.success('保存成功'); |
|||
}) |
|||
.catch((e) => { |
|||
message.error(e.message); |
|||
}); |
|||
}) |
|||
.catch(() => { |
|||
}); |
|||
}; |
|||
|
|||
watch( |
|||
() => props.data, |
|||
(data) => { |
|||
console.log(data,'propss') |
|||
if (data?.settingKey) { |
|||
isUpdate.value = true |
|||
// 表单赋值 |
|||
assignFields(data); |
|||
} else { |
|||
// 新增 |
|||
isUpdate.value = false |
|||
resetFields(); |
|||
} |
|||
} |
|||
); |
|||
</script> |
@ -0,0 +1,187 @@ |
|||
<template> |
|||
<a-card :bordered="false"> |
|||
<a-form |
|||
ref="formRef" |
|||
:model="form" |
|||
:rules="rules" |
|||
:label-col="styleResponsive ? { md: 3, sm: 5, xs: 24 } : { flex: '90px' }" |
|||
:wrapper-col="styleResponsive ? { md: 9, sm: 19, xs: 24 } : { flex: '1' }" |
|||
> |
|||
<a-form-item label="悬浮工具栏" name="floatTool" extra="显示网站悬浮客服工具栏"> |
|||
<a-switch v-model:checked="form.floatTool" checked-children="显示" un-checked-children="隐藏" @change="save" /> |
|||
</a-form-item> |
|||
<a-form-item label="显示站内搜索" name="searchBtn"> |
|||
<a-switch v-model:checked="form.searchBtn" checked-children="显示" un-checked-children="隐藏" @change="save" /> |
|||
</a-form-item> |
|||
<a-form-item label="启用登录注册" name="loginBtn"> |
|||
<a-switch v-model:checked="form.loginBtn" checked-children="启用" un-checked-children="不启用" @change="save" /> |
|||
</a-form-item> |
|||
<a-form-item label="默认编辑器" name="editor" extra="设置默认编辑器"> |
|||
<a-select v-model:value="form.editor" placeholder="请选择编辑器" class="max-w-xs" @change="save"> |
|||
<a-select-option :value="1">富文本编辑器</a-select-option> |
|||
<a-select-option :value="2">Markdown编辑器</a-select-option> |
|||
</a-select> |
|||
</a-form-item> |
|||
</a-form> |
|||
</a-card> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
import {reactive, ref, watch} from 'vue'; |
|||
import {message} from 'ant-design-vue'; |
|||
import {Setting} from '@/api/system/setting/model'; |
|||
import {useThemeStore} from '@/store/modules/theme'; |
|||
import {storeToRefs} from 'pinia'; |
|||
import {FormInstance} from 'ant-design-vue/es/form'; |
|||
import useFormData from '@/utils/use-form-data'; |
|||
import {addSetting, updateSetting} from "@/api/system/setting"; |
|||
import {ItemType} from "ele-admin-pro/es/ele-image-upload/types"; |
|||
import {uploadFile} from "@/api/system/file"; |
|||
|
|||
const props = defineProps<{ |
|||
value?: string; |
|||
// 修改回显的数据 |
|||
data?: Setting | null; |
|||
}>(); |
|||
|
|||
// 保存字段信息(设定好key和描述,content里的字段是随意加的会自动转为json保存到数据库) |
|||
const settingKey = ref('website'); |
|||
|
|||
// 是否开启响应式布局 |
|||
const themeStore = useThemeStore(); |
|||
const {styleResponsive} = storeToRefs(themeStore); |
|||
// 提交状态 |
|||
const loading = ref(false); |
|||
// 是否是修改 |
|||
const isUpdate = ref(false); |
|||
// |
|||
const formRef = ref<FormInstance | null>(null); |
|||
// 表单数据 |
|||
const {form, resetFields, assignFields} = useFormData<Setting>({ |
|||
settingKey: 'website', |
|||
type: 1, |
|||
roleId: undefined, |
|||
openWxAuth: 1, |
|||
openWxBindPhone: 1, |
|||
openWxofficialAuth: 1, |
|||
openWxofficialBindPhone: 1, |
|||
tokenExpireTime: 86400, |
|||
comments: '', |
|||
tenantId: localStorage.getItem('TenantId') |
|||
}); |
|||
|
|||
// 表单验证规则 |
|||
const rules = reactive({ |
|||
type: [ |
|||
{ |
|||
required: true, |
|||
message: '请选择默认注册方式', |
|||
type: 'number', |
|||
trigger: 'blur' |
|||
} |
|||
], |
|||
roleId: [ |
|||
{ |
|||
required: true, |
|||
message: '请选择默认角色', |
|||
type: 'number', |
|||
trigger: 'blur' |
|||
} |
|||
], |
|||
openWxAuth: [ |
|||
{ |
|||
required: true, |
|||
message: '请输入系统名称', |
|||
type: 'number', |
|||
trigger: 'blur' |
|||
} |
|||
], |
|||
openWxBindPhone: [ |
|||
{ |
|||
required: true, |
|||
message: '请输入系统名称', |
|||
type: 'number', |
|||
trigger: 'blur' |
|||
} |
|||
], |
|||
openWxofficialAuth: [ |
|||
{ |
|||
required: true, |
|||
message: '请输入系统名称', |
|||
type: 'number', |
|||
trigger: 'blur' |
|||
} |
|||
], |
|||
openWxofficialBindPhone: [ |
|||
{ |
|||
required: true, |
|||
message: '请输入系统名称', |
|||
type: 'number', |
|||
trigger: 'blur' |
|||
} |
|||
] |
|||
}); |
|||
|
|||
const onUpload = (d: ItemType) => { |
|||
uploadFile(<File>d.file) |
|||
.then((result) => { |
|||
form.logo = result.path; |
|||
message.success('上传成功'); |
|||
}) |
|||
.catch((e) => { |
|||
message.error(e.message); |
|||
}); |
|||
}; |
|||
|
|||
const onClose = () => { |
|||
form.logo = undefined |
|||
} |
|||
|
|||
/* 保存编辑 */ |
|||
const save = () => { |
|||
if (!formRef.value) { |
|||
return; |
|||
} |
|||
formRef.value |
|||
.validate() |
|||
.then(() => { |
|||
loading.value = true; |
|||
const appForm = { |
|||
...form, |
|||
content: JSON.stringify(form) |
|||
}; |
|||
const saveOrUpdate = isUpdate.value ? updateSetting : addSetting; |
|||
saveOrUpdate(appForm) |
|||
.then((msg) => { |
|||
message.success('保存成功'); |
|||
}) |
|||
.catch((e) => { |
|||
message.error(e.message); |
|||
}); |
|||
}) |
|||
.catch(() => { |
|||
}); |
|||
}; |
|||
|
|||
watch( |
|||
() => props.data, |
|||
(data) => { |
|||
if (data?.settingId) { |
|||
isUpdate.value = true |
|||
// 表单赋值 |
|||
if (data.content) { |
|||
const jsonData = JSON.parse(data.content); |
|||
assignFields(jsonData); |
|||
} |
|||
// 其他必要参数 |
|||
form.settingId = data.settingId |
|||
form.settingKey = data.settingKey |
|||
} else { |
|||
// 新增 |
|||
isUpdate.value = false |
|||
resetFields(); |
|||
form.settingKey = settingKey.value |
|||
} |
|||
} |
|||
); |
|||
</script> |
Loading…
Reference in new issue