feat: 封装多图上传组件
Former-commit-id: d36f4fb6fb9e2c3da42518f6689a78ab2370af57
This commit is contained in:
@@ -1,11 +1,13 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
|
import { AxiosPromise } from 'axios';
|
||||||
|
import { FileInfo } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传文件
|
* 上传文件
|
||||||
*
|
*
|
||||||
* @param file
|
* @param file
|
||||||
*/
|
*/
|
||||||
export function uploadFile(file: File) {
|
export function uploadFileApi(file: File): AxiosPromise<FileInfo> {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
return request({
|
return request({
|
||||||
@@ -21,12 +23,12 @@ export function uploadFile(file: File) {
|
|||||||
/**
|
/**
|
||||||
* 删除文件
|
* 删除文件
|
||||||
*
|
*
|
||||||
* @param path
|
* @param fileName 文件名
|
||||||
*/
|
*/
|
||||||
export function deleteFile(path?: string) {
|
export function deleteFileApi(fileName?: string) {
|
||||||
return request({
|
return request({
|
||||||
url: '/api/v1/files',
|
url: '/api/v1/files',
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
params: { path: path }
|
params: { fileName: fileName }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
7
src/api/file/types.ts
Normal file
7
src/api/file/types.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* 文件API类型声明
|
||||||
|
*/
|
||||||
|
export interface FileInfo {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
154
src/components/Upload/MultiUpload.vue
Normal file
154
src/components/Upload/MultiUpload.vue
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<!--
|
||||||
|
多图上传组件
|
||||||
|
@author: haoxr
|
||||||
|
@date 2022/11/20
|
||||||
|
@link https://element-plus.gitee.io/zh-CN/component/upload.html
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-upload
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
list-type="picture-card"
|
||||||
|
:before-upload="handleBeforeUpload"
|
||||||
|
:http-request="handleUpload"
|
||||||
|
:on-remove="handleRemove"
|
||||||
|
:on-preview="handlePreview"
|
||||||
|
:limit="props.limit"
|
||||||
|
>
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
</el-upload>
|
||||||
|
|
||||||
|
<el-dialog v-model="dialogVisible">
|
||||||
|
<img w-full :src="dialogImageUrl" alt="Preview Image" />
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { Plus } from '@element-plus/icons-vue';
|
||||||
|
import {
|
||||||
|
ElMessage,
|
||||||
|
ElUpload,
|
||||||
|
UploadRawFile,
|
||||||
|
UploadRequestOptions,
|
||||||
|
UploadUserFile,
|
||||||
|
UploadFile,
|
||||||
|
UploadProps
|
||||||
|
} from 'element-plus';
|
||||||
|
import { uploadFileApi, deleteFileApi } from '@/api/file';
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/**
|
||||||
|
* 文件路径集合
|
||||||
|
*/
|
||||||
|
modelValue: {
|
||||||
|
type: Array<string>,
|
||||||
|
default: [] as Array<string>
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 文件上传数量限制
|
||||||
|
*/
|
||||||
|
limit: {
|
||||||
|
type: Number,
|
||||||
|
default: 5
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialogImageUrl = ref('');
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
|
||||||
|
const fileList = ref([] as UploadUserFile[]);
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(newVal: string[]) => {
|
||||||
|
console.log('newVal', newVal);
|
||||||
|
const filePaths = fileList.value.map(file => file.url);
|
||||||
|
// 监听modelValue文件集合值未变化时,跳过赋值
|
||||||
|
if (
|
||||||
|
filePaths.length > 0 &&
|
||||||
|
filePaths.length === newVal.length &&
|
||||||
|
filePaths.every(x => newVal.some(y => y === x)) &&
|
||||||
|
newVal.every(y => filePaths.some(x => x === y))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileList.value = newVal.map(filePath => {
|
||||||
|
return { url: filePath } as UploadUserFile;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义图片上传
|
||||||
|
*
|
||||||
|
* @param params
|
||||||
|
*/
|
||||||
|
async function handleUpload(options: UploadRequestOptions): Promise<any> {
|
||||||
|
const { data: fileInfo } = await uploadFileApi(options.file);
|
||||||
|
|
||||||
|
// 上传成功需手动替换文件路径为远程URL,否则图片地址为预览地址 blob:http://
|
||||||
|
const fileIndex = fileList.value.findIndex(
|
||||||
|
file => file.uid == (options.file as any).uid
|
||||||
|
);
|
||||||
|
|
||||||
|
fileList.value.splice(fileIndex, 1, {
|
||||||
|
name: fileInfo.name,
|
||||||
|
url: fileInfo.url
|
||||||
|
} as UploadUserFile);
|
||||||
|
|
||||||
|
emit(
|
||||||
|
'update:modelValue',
|
||||||
|
fileList.value.map(file => file.url)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除图片
|
||||||
|
*/
|
||||||
|
function handleRemove(removeFile: UploadFile) {
|
||||||
|
console.log('removeFile', removeFile);
|
||||||
|
|
||||||
|
const fileUrl = removeFile.url;
|
||||||
|
|
||||||
|
if (fileUrl) {
|
||||||
|
let fileName = removeFile.name;
|
||||||
|
|
||||||
|
if (!fileName) {
|
||||||
|
// 文件名不存在,从URL得到文件名
|
||||||
|
// 例: https://oss.youlai.tech/default/2022/11/20/{uuid}.jpg 得到文件名 2022/11/20/{uuid}.jpg
|
||||||
|
fileName = fileUrl.substring(fileUrl.indexOf('/', -3) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteFileApi(fileName).then(() => {
|
||||||
|
// 删除成功回调
|
||||||
|
emit(
|
||||||
|
'update:modelValue',
|
||||||
|
fileList.value.map(file => file.url)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 限制用户上传文件的格式和大小
|
||||||
|
*/
|
||||||
|
function handleBeforeUpload(file: UploadRawFile) {
|
||||||
|
if (file.size > 2 * 1048 * 1048) {
|
||||||
|
ElMessage.warning('上传图片不能大于2M');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片预览
|
||||||
|
*/
|
||||||
|
const handlePreview: UploadProps['onPreview'] = uploadFile => {
|
||||||
|
dialogImageUrl.value = uploadFile.url!;
|
||||||
|
dialogVisible.value = true;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -1,42 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<!-- 上传组件 -->
|
||||||
<!-- 上传组件 -->
|
<el-upload
|
||||||
<el-upload
|
class="single-uploader"
|
||||||
ref="singleUploadRef"
|
v-model="imgUrl"
|
||||||
action=""
|
:show-file-list="false"
|
||||||
class="single-uploader"
|
list-type="picture-card"
|
||||||
:show-file-list="false"
|
:before-upload="handleBeforeUpload"
|
||||||
:before-upload="handleBeforeUpload"
|
:http-request="uploadFile"
|
||||||
:http-request="uploadImage"
|
>
|
||||||
>
|
<img v-if="imgUrl" :src="imgUrl" class="single" />
|
||||||
<img v-if="imgUrl" :src="imgUrl" class="single-uploader__image" />
|
<el-icon v-else class="single-uploader-icon"><Plus /></el-icon>
|
||||||
|
</el-upload>
|
||||||
<el-icon v-else class="single-uploader__plus">
|
|
||||||
<Plus />
|
|
||||||
</el-icon>
|
|
||||||
|
|
||||||
<!-- 删除图标 -->
|
|
||||||
<el-icon
|
|
||||||
v-if="props.showClose && imgUrl"
|
|
||||||
class="single-uploader__remove"
|
|
||||||
@click.stop="handleRemove(imgUrl)"
|
|
||||||
>
|
|
||||||
<Close />
|
|
||||||
</el-icon>
|
|
||||||
</el-upload>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { Plus, Close } from '@element-plus/icons-vue';
|
import { Plus } from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
ElMessage,
|
ElMessage,
|
||||||
ElUpload,
|
ElUpload,
|
||||||
UploadRawFile,
|
UploadRawFile,
|
||||||
UploadRequestOptions
|
UploadRequestOptions
|
||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
import { uploadFile, deleteFile } from '@/api/file';
|
import { uploadFileApi } from '@/api/file';
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue']);
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
@@ -44,13 +30,6 @@ const props = defineProps({
|
|||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 是否显示右上角的删除图片按钮
|
|
||||||
*/
|
|
||||||
showClose: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,73 +46,52 @@ const imgUrl = computed<string | undefined>({
|
|||||||
/**
|
/**
|
||||||
* 自定义图片上传
|
* 自定义图片上传
|
||||||
*
|
*
|
||||||
* @param params
|
* @param options
|
||||||
*/
|
*/
|
||||||
async function uploadImage(options: UploadRequestOptions): Promise<any> {
|
async function uploadFile(options: UploadRequestOptions): Promise<any> {
|
||||||
const response = await uploadFile(options.file);
|
const { data: fileInfo } = await uploadFileApi(options.file);
|
||||||
imgUrl.value = response.data;
|
imgUrl.value = fileInfo.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除图片
|
* 限制用户上传文件的格式和大小
|
||||||
*
|
|
||||||
* @param fileUrl
|
|
||||||
*/
|
|
||||||
function handleRemove(fileUrl?: string) {
|
|
||||||
if (fileUrl) {
|
|
||||||
deleteFile(fileUrl);
|
|
||||||
imgUrl.value = undefined; // 这里会触发imgUrl的computed的set方法
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 在 before-upload 钩子中限制用户上传文件的格式和大小
|
|
||||||
*/
|
*/
|
||||||
function handleBeforeUpload(file: UploadRawFile) {
|
function handleBeforeUpload(file: UploadRawFile) {
|
||||||
// const isJPG = file.type === "image/jpeg";
|
if (file.size > 2 * 1048 * 1048) {
|
||||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
|
||||||
|
|
||||||
if (!isLt2M) {
|
|
||||||
ElMessage.warning('上传图片不能大于2M');
|
ElMessage.warning('上传图片不能大于2M');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style scoped>
|
||||||
.single-uploader {
|
.single-uploader .single {
|
||||||
border: 1px dashed #d9d9d9;
|
width: 178px;
|
||||||
|
height: 178px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.single-uploader .el-upload {
|
||||||
|
border: 1px dashed var(--el-border-color);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: var(--el-transition-duration-fast);
|
transition: var(--el-transition-duration-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-uploader .el-upload:hover {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon.single-uploader-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #8c939d;
|
||||||
|
width: 178px;
|
||||||
|
height: 178px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
&:hover {
|
|
||||||
border-color: var(--el-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__image {
|
|
||||||
width: 146px;
|
|
||||||
height: 146px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__plus {
|
|
||||||
width: 146px;
|
|
||||||
height: 157px;
|
|
||||||
font-size: 28px;
|
|
||||||
color: #8c939d;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__remove {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #ff4d51 !important;
|
|
||||||
margin-top: 0px !important;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
color: #409eff;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { onBeforeUnmount, shallowRef, reactive, toRefs } from 'vue';
|
|||||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
|
import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
|
||||||
|
|
||||||
// API 引用
|
// API 引用
|
||||||
import { uploadFile } from '@/api/file';
|
import { uploadFileApi } from '@/api/file';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@@ -46,8 +46,8 @@ const state = reactive({
|
|||||||
uploadImage: {
|
uploadImage: {
|
||||||
// 自定义图片上传
|
// 自定义图片上传
|
||||||
async customUpload(file: any, insertFn: any) {
|
async customUpload(file: any, insertFn: any) {
|
||||||
uploadFile(file).then(response => {
|
uploadFileApi(file).then(response => {
|
||||||
const url = response.data;
|
const url = response.data.url;
|
||||||
insertFn(url);
|
insertFn(url);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/views/component/uploader.vue
Normal file
33
src/views/component/uploader.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import SingleUpload from '@/components/Upload/SingleUpload.vue';
|
||||||
|
import MultiUpload from '@/components/Upload/MultiUpload.vue';
|
||||||
|
import { ElForm } from 'element-plus';
|
||||||
|
import { reactive, ref, toRefs } from 'vue';
|
||||||
|
|
||||||
|
const dataFormRef = ref(ElForm);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
formData: {
|
||||||
|
picUrl:
|
||||||
|
'https://oss.youlai.tech/default/2022/11/20/18e206dae97b40329661537d1e433639.jpg',
|
||||||
|
picUrls: [
|
||||||
|
'https://oss.youlai.tech/default/2022/11/20/8af5567816094545b53e76b38ae9c974.webp',
|
||||||
|
'https://oss.youlai.tech/default/2022/11/20/13dbfd7feaf848c2acec2b21675eb9d3.webp'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { formData } = toRefs(state);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-form ref="dataFormRef" :model="formData">
|
||||||
|
<el-form-item label="单图上传">
|
||||||
|
<single-upload v-model="formData.picUrl"></single-upload>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="多图上传">
|
||||||
|
<multi-upload v-model="formData.picUrls"></multi-upload>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user