Vue3 + Element-Plus + vue-draggable-plus 实现图片拖拽排序和图片上传到阿里云 OSS 父组件实现真正上传(最新保姆级)

Vue3 + Element-Plus + vue-draggable-plus 实现图片拖拽排序和图片上传到阿里云 OSS(最新保姆级)父组件实现真正上传

  • 1、效果展示
  • 2、UploadImage.vue 组件封装
  • 3、相关请求封装
  • 4、SwiperConfig.vue 调用组件
  • 5、后端接口

1、效果展示


如果没有安装插件,请先安装 vue-draggable-plus 插件:

js 复制代码
cnpm install vue-draggable-plus

2、UploadImage.vue 组件封装


js 复制代码
<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 对象添加到 FormData
            const 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 imgList
    const uploadedFile = {
        url: response.url, // Assume the response contains the image URL
        name: 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

js 复制代码
/*
 * @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({
  // 判断环境设置不同的baseURL
  baseURL: 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.token
    if (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?.status
    switch (status) {
      case 401:
        message = 'token 失效,请重新登录'
        break
      case 403:
        message = '拒绝访问'
        break
      case 404:
        message = '请求地址错误'
        break
      case 500:
        message = '服务器故障'
        break
      default:
        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

js 复制代码
// * 请求响应参数(不包含data)
export interface Result {
  code: number
  message: string
  success?: boolean
}

// * 请求响应参数(包含data)
export interface ResultData<T = any> extends Result {
  data: T
}

index.ts

js 复制代码
/*
 * @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 调用组件


c 复制代码
<!--
 * @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

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 {

    @Autowired
    private 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

java 复制代码
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/18
            SimpleDateFormat 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

yml 复制代码
alioss: # 阿里云配置
  endpoint: "https://cn-chengdu.oss.aliyuncs.com"    # Endpoint以西南(成都)为例,其它Region请按实际情况填写。
  bucketName: ""  # 填写Bucket名称,例如examplebucket。
  access_key: ""  # 点击头像->Accesskey管理查看 秘钥
  access_key_secret: "" # 密码
相关推荐
程序员大金21 分钟前
基于python+django+vue的高校成绩管理系统
vue.js·python·django
&白帝&1 小时前
vue3-count-to实现数字动态增长效果
javascript
Jiaberrr1 小时前
微信小程序中 Echarts 的巧妙运用
前端·javascript·vue.js·微信小程序·echarts
GISer_Jing1 小时前
Vue路由管理面试题目VueRouter
前端·vue.js·面试
苹果酱05672 小时前
Springboot中的SPI 机制
java·vue.js·spring boot·mysql·课程设计
努力往上爬de蜗牛2 小时前
el-table ToggleRowSelection实现取消选中没效果(virtual-scroll)
前端·javascript·elementui
Jane - UTS 数据传输系统3 小时前
处理VUE框架中,ElementUI控件u-table空值排序问题
前端·vue.js·elementui·字符串 空值处理
lauo3 小时前
【智体OS】官方上新发布智体机器人:使用rtrobot智体应用远程控制平衡车机器人
前端·javascript·机器人·开源
imluch3 小时前
实现一个复杂度最低的,效率最高的 listToTree 方法
javascript
sanguine__3 小时前
javaScript学习
前端·javascript·学习