Vue3 + Element-Plus + vue-draggable-plus 实现图片拖拽排序和图片上传到阿里云 OSS(最新保姆级)父组件实现真正上传
- 1、效果展示
- 2、UploadImage.vue 组件封装
- 3、相关请求封装
- 4、SwiperConfig.vue 调用组件
- 5、后端接口
1、效果展示
如果没有安装插件,请先安装 vue-draggable-plus 插件:
cnpm install vue-draggable-plus
2、UploadImage.vue 组件封装
<template><div class="draggable_image_upload"><VueDraggable class="box-uploader" ref="draggableRef" v-model="curList" :animation="600" easing="ease-out"ghostClass="ghost" draggable="ul" @start="onStart" @update="onUpdate"><!-- 使用element-ui el-upload自带样式 --><ul v-for="(item, index) in curList" :key="index" class="el-upload-list el-upload-list--picture-card"><li class="el-upload-list__item is-success animated":style="{ 'height': props.height + ' !important', 'width': props.width + ' !important', 'margin-right': props.space + ' !important' }"><el-image class="originalImg" :src="item.url" :preview-src-list="[item.url]":style="{ 'height': props.height + ' !important', 'width': props.width + ' !important' }" /><label class="el-upload-list__item-status-label"><el-icon class="el-icon--upload-success el-icon--check"><Check /></el-icon></label><span class="el-upload-list__item-actions"><!-- 预览 --><span class="el-upload-list__item-preview" @click="handleImgPreview('originalImg', index)"><el-icon><ZoomIn /></el-icon></span><!-- 删除 --><span class="el-upload-list__item-delete" @click="onImageRemove(item.url)"><el-icon><Delete /></el-icon></span></span></li></ul><!-- 上传组件 --><el-upload v-model:file-list="curList" :action="UPLOAD_IMG_URL" :multiple="multiple"list-type="picture-card" :accept="accept" :show-file-list="false" :before-upload="beforeImgUpload":on-success="handleImgSuccess" ref="uploadRef" :auto-upload="autoUpload" drag><el-icon :style="{ 'height': props.height + ' !important', 'width': props.width + ' !important' }"><Plus /></el-icon></el-upload></VueDraggable></div>
</template><script name="DraggableImageUpload" setup>
import { UPLOAD_IMG_URL, uploadImageApi } from "@/api/upload/index"; //请求url
import { Check, Delete, Download, Plus, ZoomIn } from '@element-plus/icons-vue';
import { ElMessage } from "element-plus";
import { getCurrentInstance, onMounted, reactive, ref, toRefs, computed } from 'vue';
import { VueDraggable } from 'vue-draggable-plus';const draggableRef = ref(null);const { proxy } = getCurrentInstance();const props = defineProps({accept: {type: String,default: ".jpg,.jpeg,.png"},limit: {type: Number,default: 9999},multiple: {type: Boolean,default: true},autoUpload: {type: Boolean,default: false},height: {type: String,default: "100px"},width: {type: String,default: "100px"},space: {type: String,default: "20px"},modelValue: {type: Array,default: () => []}
});
const {modelValue
} = toRefs(props);const data = reactive({maxImgsLen: 0,imgList: []
});const { maxImgsLen, imgList } = toRefs(data);const emit = defineEmits(["update:modelValue", "upload-failure", "upload-success"]);
const curList = computed({get() {return props.modelValue;},set(newValue) {emit("update:modelValue", newValue);}
});const onStart = () => {console.log('start');
};const onUpdate = () => {console.log(curList.value, 'update++++++++++++++');
};const handleImgPreview = (domClass, index) => {const dom = document.getElementsByClassName(domClass);dom[index].firstElementChild.click(); //调用 el-image 的预览方法
};const onImageRemove = (url) => {const list = [...curList.value];list.forEach((item, index) => {if (url === item.url) {list.splice(index, 1);}});curList.value = list;
};const beforeImgUpload = (rawFile) => {const types = ["image/jpeg", "image/jpg", "image/png"];const size = rawFile.size;const isImage = types.includes(rawFile.type);const isLt5M = size / 1024 / 1024 < 5;if (!isImage) {ElMessage("请上传jpeg、jpg、png类型的图片", {type: "error"});return false;} else if (!isLt5M) {ElMessage("上传图片大小不能超过5MB", {type: "error"});return false;}return true;
};const uploadImage = async (file) => {let res = await uploadImageApi(file);return res.data;
}/*** 提供给外部的上传触发方法*/
const triggerUpload = async () => {for (let i = 0; i < modelValue.value.length; i++) {const file = modelValue.value[i];if (file.status === 'ready') {modelValue.value[i] = {...file,status: 'uploading',message: '上传中',};const formData = new FormData();formData.append('file', file.raw); // 将 File 对象添加到 FormDataconst uploadedUrl = await uploadImage(formData);modelValue.value[i] = {name: file.name,size: file.size,uid: file.uid,status: 'success',url: uploadedUrl,};}emit('upload-success');}
};/*** 提供给外部的函数*/
defineExpose({triggerUpload,
});function handleImgSuccess() {ElMessage.success("图片上传成功");// This can be extended based on the response structure from the server// For example, you might want to add the uploaded image to your imgListconst uploadedFile = {url: response.url, // Assume the response contains the image URLname: file.name,size: file.size};curList.value.push(uploadedFile); // Add the uploaded image to curList
}
</script><style lang="scss" scoped>
::v-deep(.el-upload-dragger) {padding: 0;display: flex;justify-content: center;align-items: center;
}.draggable_image_upload {.box-uploader {display: flex;flex-wrap: wrap;vertical-align: middle;:deep(.el-upload) {border-radius: 4px;.circle-plus {width: 24px;height: 24px;}&.el-upload-list {&.el-upload__item {width: 100px;height: 100px;margin: 0 17px 17px 0;border-color: #e7e7e7;padding: 3px;}}&.el-upload--picture-card {border-style: dashed;margin-right: 17px;}}.el-upload-list__item {margin: 0 17px 17px 0;border-color: #e7e7e7;padding: 3px;.originalImg {:deep(.el-image__preview) {-o-object-fit: contain;object-fit: contain;}}}ul:nth-child(6n+6) {li {margin-right: 0;}}}
}
</style>
3、相关请求封装
http/index.ts
/** @Date: 2024-08-17 14:18:14* @LastEditors: zhong* @LastEditTime: 2024-09-17 19:22:36* @FilePath: \rentHouseAdmin\src\api\upload\index.ts*/
import axios from 'axios'
import type {AxiosInstance,AxiosError,AxiosRequestConfig,AxiosResponse,
} from 'axios'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/store/modules/user'
import { ResultEnum } from '@/enums/httpEnums'
import { ResultData } from './type'
import { LOGIN_URL } from '@/config/config'
import { RESEETSTORE } from '../reset'
import router from '@/router'export const service: AxiosInstance = axios.create({// 判断环境设置不同的baseURLbaseURL: import.meta.env.PROD ? import.meta.env.VITE_APP_BASE_URL : '/',timeout: ResultEnum.TIMEOUT as number,
})
/*** @description: 请求拦截器* @returns {*}*/
service.interceptors.request.use((config) => {const userStore = useUserStore()const token = userStore.tokenif (token) {config.headers['access-token'] = token}return config},(error: AxiosError) => {ElMessage.error(error.message)return Promise.reject(error)},
)
/*** @description: 响应拦截器* @returns {*}*/
service.interceptors.response.use((response: AxiosResponse) => {const { data } = response// * 登陆失效if (ResultEnum.EXPIRE.includes(data.code)) {RESEETSTORE()ElMessage.error(data.message || ResultEnum.ERRMESSAGE)router.replace(LOGIN_URL)return Promise.reject(data)}if (data.code && data.code !== ResultEnum.SUCCESS) {ElMessage.error(data.message || ResultEnum.ERRMESSAGE)return Promise.reject(data)}return data},(error: AxiosError) => {// 处理 HTTP 网络错误let message = ''// HTTP 状态码const status = error.response?.statusswitch (status) {case 401:message = 'token 失效,请重新登录'breakcase 403:message = '拒绝访问'breakcase 404:message = '请求地址错误'breakcase 500:message = '服务器故障'breakdefault:message = '网络连接故障'}ElMessage.error(message)return Promise.reject(error)},
)/*** @description: 导出封装的请求方法* @returns {*}*/
const http = {get<T>(url: string,params?: object,config?: AxiosRequestConfig,): Promise<ResultData<T>> {return service.get(url, { params, ...config })},post<T>(url: string,data?: object,config?: AxiosRequestConfig,): Promise<ResultData<T>> {return service.post(url, data, config)},put<T>(url: string,data?: object,config?: AxiosRequestConfig,): Promise<ResultData<T>> {return service.put(url, data, config)},delete<T>(url: string,data?: object,config?: AxiosRequestConfig,): Promise<ResultData<T>> {return service.delete(url, { data, ...config })},
}export default http
http/type.ts
// * 请求响应参数(不包含data)
export interface Result {code: numbermessage: stringsuccess?: boolean
}// * 请求响应参数(包含data)
export interface ResultData<T = any> extends Result {data: T
}
index.ts
/** @Date: 2024-08-17 14:18:14* @LastEditors: zhong* @LastEditTime: 2024-09-17 19:22:36* @FilePath: \rentHouseAdmin\src\api\upload\index.ts*/
// 上传基础路径
export const BASE_URL = import.meta.env.PROD? import.meta.env.VITE_APP_BASE_URL: ''
// 图片上传地址
export const UPLOAD_IMG_URL = `/admin/file/upload`
import http from '@/utils/http'/*** @description 上传图片接口* @param params*/
export function uploadImageApi(file: FormData) {return http.post<any[]>(UPLOAD_IMG_URL,file)
}
4、SwiperConfig.vue 调用组件
<!--* @Date: 2024-12-14 15:08:55* @LastEditors: zhong* @LastEditTime: 2024-12-15 20:51:31* @FilePath: \admin\src\views\small\smallSetting\components\SwiperConfig.vue
-->
<template><div class="card main"><div class="left"><div style="text-align: center;margin-bottom: 20px;"><text style="font-size: 20px;">云尚社区</text></div><div class="mt-4"><el-input style="border-radius: 10px;" size="small" placeholder="请输入关键字" :prefix-icon="Search" /></div><div style="margin-top: 10px;"><el-carousel :interval="4000" type="card" class="card" height="100px" indicator-position="none"style="padding: 6px 0;border-radius: 10px;"><el-carousel-item v-for="(item, index) in data" :key="index"><el-image :src="item.settingValue" style="height: 100px;border-radius: 6px;" /></el-carousel-item></el-carousel></div><div class="card menu"><div v-for="(item, index) in menuList" :key="index" class="menu-item"><div style="display: flex; flex-direction: column;"><el-image :src="item.url" style="height: 60px;width: 70px;;border-radius: 6px;" /><el-text style="margin-top: 8px;">{{ item.name }}</el-text></div></div></div></div><el-divider direction="vertical" style="height: 620px;margin-left: 50px;border-width: 2px;">可选配置</el-divider><div class="right"><el-tag type="" size="large" effect="dark">轮播图配置</el-tag><div style="display: flex;margin-top: 20px;"><div v-for="(item, index) in data" :key="index" style="padding-right: 20px;"><el-image :src="item.settingValue" style="height: 80px;width: 160px;;border-radius: 6px;" /></div></div><div style="margin-top: 20px;"><DraggableImageUpload @upload-success="onUploadSuccess" v-model="uploadImageArray" ref="imageUploadRef"height="80px" width="160px" space="20px" /><el-button type="" @click="handleImageUpload" style="margin-top: 20px;">替换</el-button></div></div></div>
</template><script setup>
import DraggableImageUpload from '@/components/UploadImage/UploadImage.vue'
import { getSystemSettingsConfigApi } from '@/api/system';
import { onMounted, ref } from 'vue';
import { Search } from '@element-plus/icons-vue';
const data = ref([]);
const uploadImageArray = ref([]);
const getSystemSettingsConfig = async () => {let res = await getSystemSettingsConfigApi();data.value = res.data.settingMap.one;console.log(data.value);
}onMounted(() => {getSystemSettingsConfig();
})// 菜单信息
const menuList = ref([]);const imageUploadRef = ref(null); // 创建 ref 引用
const handleImageUpload = () => {imageUploadRef.value.triggerUpload();
}// 上传成功回调
const onUploadSuccess = () => {console.log(uploadImageArray.value);
};
</script><style lang="scss" scoped>
::v-deep(li.el-upload-list__item.is-success) {margin-right: 20px;height: 80px;width: 160px;
}::v-deep(.el-upload.el-upload--picture-card) {height: 80px;width: 160px;
}.main {display: flex;justify-content: space-between;padding: 20px;
}.menu {display: flex;align-items: center;justify-content: center;flex-wrap: wrap;/* 自动换行 */gap: 20px;row-gap: 15px;/* 元素间距 */max-height: calc(40px * 2 + 20px * 2 + 50px);/* 限制两排的总高度 */overflow: hidden;/* 超出隐藏 */margin-top: 10px;border-radius: 10px;
}.item {width: calc(25% - 10px);/* 每排最多显示 4 个 */box-sizing: border-box;
}.content {display: flex;flex-direction: column;align-items: center;
}::v-deep(.el-input__wrapper) {height: 26px;border-radius: 20px;
}.left {padding: 10px;height: 600px;background-color: #f8f8f8;width: 400px;
}.right {margin-left: 50px;flex: 1;height: 600px;
}
</style>
5、后端接口
FileUploadController.java
package com.zhong.controller.apartment;import com.zhong.result.Result;
import com.zhong.utils.AliOssUtil;
import io.minio.errors.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.UUID;@Tag(name = "文件管理")
@RequestMapping("/admin/file")
@RestController
public class FileUploadController {@Autowiredprivate AliOssUtil ossUtil;@Operation(summary = "上传文件")@PostMapping("/upload")public Result<String> upload(@RequestParam MultipartFile file) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {// 获取文件原名String originalFilename = file.getOriginalFilename();// 防止重复上传文件名重复String fileName = null;if (originalFilename != null) {fileName = UUID.randomUUID() + originalFilename.substring(originalFilename.indexOf("."));}// 把文件储存到本地磁盘
// file.transferTo(new File("E:\\SpringBootBase\\ProjectOne\\big-event\\src\\main\\resources\\flies\\" + fileName));String url = ossUtil.uploadFile(fileName, file.getInputStream());System.out.println();return Result.ok(url);}
}
AliOssUtil.class
package com.zhong.utils;import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import com.zhong.result.Result;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;/*** @ClassName : AliOssUtil* @Description : 阿里云上传服务* @Author : zhx* @Date: 2024-03-1 20:29*/
@Component
@Service
public class AliOssUtil {@Value("${alioss.endpoint}")private String ENDPOINT;@Value("${alioss.bucketName}")private String BUCKETNAME;@Value("${alioss.access_key}")private String ACCESS_KEY;@Value("${alioss.access_key_secret}")private String ACCESS_KEY_SECRET;public String uploadFile(String objectName, InputStream inputStream) {String url = "";// 创建OSSClient实例。System.out.println(ACCESS_KEY);OSS ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY, ACCESS_KEY_SECRET);try {// 创建PutObjectRequest对象。// 生成日期文件夹路径,例如:2022/04/18SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd");String dateStr = dateFormat.format(new Date());PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKETNAME, dateStr + "/" + objectName, inputStream);// 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。// ObjectMetadata metadata = new ObjectMetadata();// metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());// metadata.setObjectAcl(CannedAccessControlList.Private);// putObjectRequest.setMetadata(metadata);// 上传文件。PutObjectResult result = ossClient.putObject(putObjectRequest);url = "https://" + BUCKETNAME + "." + ENDPOINT.substring(ENDPOINT.lastIndexOf("/") + 1) + "/" + dateStr + "/" + objectName;} catch (OSSException oe) {System.out.println("Caught an OSSException, which means your request made it to OSS, "+ "but was rejected with an error response for some reason.");System.out.println("Error Message:" + oe.getErrorMessage());System.out.println("Error Code:" + oe.getErrorCode());System.out.println("Request ID:" + oe.getRequestId());System.out.println("Host ID:" + oe.getHostId());} catch (ClientException ce) {System.out.println("Caught an ClientException, which means the client encountered "+ "a serious internal problem while trying to communicate with OSS, "+ "such as not being able to access the network.");System.out.println("Error Message:" + ce.getMessage());} finally {if (ossClient != null) {ossClient.shutdown();}}return url;}public Result deleteFile(String objectName) {System.out.println(objectName);// 创建OSSClient实例。OSS ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY, ACCESS_KEY_SECRET);try {// 删除文件。System.out.println(objectName);System.out.println(objectName.replace(",", "/"));ossClient.deleteObject(BUCKETNAME, objectName.replace(",", "/"));return Result.ok("删除成功!");} catch (OSSException oe) {System.out.println("Caught an OSSException, which means your request made it to OSS, "+ "but was rejected with an error response for some reason.");System.out.println("Error Message:" + oe.getErrorMessage());System.out.println("Error Code:" + oe.getErrorCode());System.out.println("Request ID:" + oe.getRequestId());System.out.println("Host ID:" + oe.getHostId());} catch (ClientException ce) {System.out.println("Caught an ClientException, which means the client encountered "+ "a serious internal problem while trying to communicate with OSS, "+ "such as not being able to access the network.");System.out.println("Error Message:" + ce.getMessage());} finally {if (ossClient != null) {ossClient.shutdown();}}return Result.fail(555,"上传失败!");}
}
application.yml
alioss: # 阿里云配置endpoint: "https://cn-chengdu.oss.aliyuncs.com" # Endpoint以西南(成都)为例,其它Region请按实际情况填写。bucketName: "" # 填写Bucket名称,例如examplebucket。access_key: "" # 点击头像->Accesskey管理查看 秘钥access_key_secret: "" # 密码