init
This commit is contained in:
165
src/components/Upload/Excel.vue
Normal file
165
src/components/Upload/Excel.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<!-- excel 导入组件 -->
|
||||
<template>
|
||||
<el-dialog :title="prop.title" v-model="state.upload.open" :close-on-click-modal="false" draggable>
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:limit="1"
|
||||
accept=".xlsx, .xls"
|
||||
:headers="headers"
|
||||
:action="baseURL + other.adaptationUrl(url)"
|
||||
:disabled="state.upload.isUploading"
|
||||
:on-progress="handleFileUploadProgress"
|
||||
:on-success="handleFileSuccess"
|
||||
:on-error="handleFileError"
|
||||
:auto-upload="false"
|
||||
drag
|
||||
>
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">
|
||||
{{ $t('excel.operationNotice') }}
|
||||
<em>{{ $t('excel.clickUpload') }}</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip text-center">
|
||||
<span>{{ $t('excel.fileFormat') }}</span>
|
||||
<el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline"
|
||||
@click="downExcelTemp" v-if="tempUrl"
|
||||
>{{ $t('excel.downloadTemplate') }}
|
||||
</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="submitFileForm">{{ $t('common.confirmButtonText') }}</el-button>
|
||||
<el-button @click="state.upload.open = false">{{ $t('common.cancelButtonText') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!--校验失败错误数据-->
|
||||
<el-dialog :title="$t('excel.validationFailureData')" v-model="state.errorVisible">
|
||||
<el-table :data="state.errorData">
|
||||
<el-table-column property="lineNum" :label="$t('excel.lineNumbers')" width="100"></el-table-column>
|
||||
<el-table-column property="errors" :label="$t('excel.misDescription')" show-overflow-tooltip>
|
||||
<template v-slot="scope">
|
||||
<el-tag type="danger" v-for="error in scope.row.errors" :key="error">{{ error }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="upload-excel">
|
||||
import {useMessage} from '/@/hooks/message';
|
||||
import other from '/@/utils/other';
|
||||
import {Session} from '/@/utils/storage';
|
||||
|
||||
const emit = defineEmits(['sizeChange', 'refreshDataList']);
|
||||
const prop = defineProps({
|
||||
url: {
|
||||
type: String,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
tempUrl: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const uploadRef = ref();
|
||||
|
||||
const state = reactive({
|
||||
errorVisible: false,
|
||||
errorData: [],
|
||||
dialog: {
|
||||
title: '',
|
||||
isShowDialog: false,
|
||||
},
|
||||
upload: {
|
||||
open: false,
|
||||
isUploading: false,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 下载模板文件
|
||||
*/
|
||||
const downExcelTemp = async () => {
|
||||
try {
|
||||
await other.downBlobFile(other.adaptationUrl(prop.tempUrl), {}, 'temp.xlsx');
|
||||
} catch (error) {
|
||||
useMessage().error('模板下载失败,请先维护模板文件');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 上传进度条变化事件
|
||||
*/
|
||||
const handleFileUploadProgress = () => {
|
||||
state.upload.isUploading = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 上传失败事件处理
|
||||
*/
|
||||
const handleFileError = () => {
|
||||
useMessage().error('上传失败,数据格式不合法!');
|
||||
state.upload.open = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 上传成功事件处理
|
||||
* @param {any} response - 上传成功的响应结果
|
||||
*/
|
||||
const handleFileSuccess = (response: any) => {
|
||||
state.upload.isUploading = false;
|
||||
state.upload.open = false;
|
||||
uploadRef.value.clearFiles();
|
||||
|
||||
// 校验失败
|
||||
if (response.code === 1) {
|
||||
useMessage().error('导入失败,以下数据不合法');
|
||||
state.errorVisible = true;
|
||||
state.errorData = response.data;
|
||||
uploadRef.value.clearFiles();
|
||||
// 刷新表格
|
||||
emit?.('refreshDataList');
|
||||
} else {
|
||||
useMessage().success(response.msg ? response.msg : '导入成功');
|
||||
// 刷新表格
|
||||
emit?.('refreshDataList');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 提交表单,触发上传
|
||||
*/
|
||||
const submitFileForm = () => {
|
||||
uploadRef.value.submit();
|
||||
};
|
||||
|
||||
/**
|
||||
* 显示上传文件对话框,并清除上传信息
|
||||
*/
|
||||
const show = () => {
|
||||
state.upload.isUploading = false;
|
||||
state.upload.open = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算请求头部信息
|
||||
*/
|
||||
const headers = computed(() => {
|
||||
return {
|
||||
Authorization: 'Bearer ' + Session.getToken(),
|
||||
'TENANT-ID': Session.getTenant(),
|
||||
};
|
||||
});
|
||||
|
||||
// 暴露变量
|
||||
defineExpose({
|
||||
show,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
361
src/components/Upload/Image.vue
Normal file
361
src/components/Upload/Image.vue
Normal file
@@ -0,0 +1,361 @@
|
||||
<!-- 单图图片上传组件, 推荐使用 ImagePlus 组件,后续将删除本组件-->
|
||||
<template>
|
||||
<div class="upload-box">
|
||||
<el-upload
|
||||
action="#"
|
||||
:id="uuid"
|
||||
:class="['upload', self_disabled ? 'disabled' : '', drag ? 'no-border' : '']"
|
||||
list-type="picture-card"
|
||||
:disabled="self_disabled"
|
||||
:show-file-list="false"
|
||||
:http-request="handleHttpUpload"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="uploadSuccess"
|
||||
:on-error="uploadError"
|
||||
:drag="drag"
|
||||
:accept="acceptType"
|
||||
>
|
||||
<template v-if="imageUrl || modelValue">
|
||||
<!-- 如果返回的是OSS 地址则不需要增加 baseURL -->
|
||||
<img
|
||||
:src="(imageUrl || modelValue || '').includes('http') ? imageUrl || modelValue : baseURL + (imageUrl || modelValue)"
|
||||
class="upload-image"
|
||||
/>
|
||||
<div class="upload-handle" @click.stop>
|
||||
<div class="handle-icon" @click="editImg" v-if="!self_disabled">
|
||||
<el-icon :size="props.iconSize"><Edit /></el-icon>
|
||||
<span v-if="!props.iconSize">{{ $t('common.editBtn') }}</span>
|
||||
</div>
|
||||
<div class="handle-icon" @click="imgViewVisible = true">
|
||||
<el-icon :size="props.iconSize"><ZoomIn /></el-icon>
|
||||
<span v-if="!props.iconSize">{{ $t('common.viewBtn') }}</span>
|
||||
</div>
|
||||
<div class="handle-icon" @click="deleteImg" v-if="!self_disabled">
|
||||
<el-icon :size="props.iconSize"><Delete /></el-icon>
|
||||
<span v-if="!props.iconSize">{{ $t('common.delBtn') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="upload-empty">
|
||||
<slot name="empty">
|
||||
<el-icon><Plus /></el-icon>
|
||||
<!-- <span>请上传图片</span> -->
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<div class="el-upload__tip">
|
||||
<slot name="tip"></slot>
|
||||
</div>
|
||||
<el-image-viewer
|
||||
:teleported="true"
|
||||
v-if="imgViewVisible"
|
||||
@close="imgViewVisible = false"
|
||||
:url-list="[_imageUrl.includes('http') ? _imageUrl : baseURL + _imageUrl]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="UploadImg">
|
||||
import { ref, computed, inject, watch } from 'vue';
|
||||
import { ElNotification, formContextKey, formItemContextKey } from 'element-plus';
|
||||
import type { UploadProps, UploadRequestOptions } from 'element-plus';
|
||||
import { generateUUID } from '/@/utils/other';
|
||||
import request from '/@/utils/request';
|
||||
|
||||
// 定义图片MIME类型
|
||||
type ImageMimeType = 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp' | 'image/svg+xml' | string;
|
||||
|
||||
interface UploadFileProps {
|
||||
imageUrl?: string; // 图片地址 ==> 必传
|
||||
modelValue?: string; // 图片地址 (v-model)
|
||||
uploadFileUrl?: string; // 上传图片的 api 方法,一般项目上传都是同一个 api 方法,在组件里直接引入即可 ==> 非必传
|
||||
drag?: boolean; // 是否支持拖拽上传 ==> 非必传(默认为 true)
|
||||
disabled?: boolean; // 是否禁用上传组件 ==> 非必传(默认为 false)
|
||||
fileSize?: number; // 图片大小限制 ==> 非必传(默认为 5M)
|
||||
fileType?: ImageMimeType[]; // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/gif"])
|
||||
height?: string; // 组件高度 ==> 非必传(默认为 150px)
|
||||
width?: string; // 组件宽度 ==> 非必传(默认为 150px)
|
||||
borderRadius?: string; // 组件边框圆角 ==> 非必传(默认为 8px)
|
||||
iconSize?: number;
|
||||
dir?: string; // 文件目录
|
||||
}
|
||||
|
||||
// 接受父组件参数
|
||||
const props = withDefaults(defineProps<UploadFileProps>(), {
|
||||
imageUrl: '',
|
||||
modelValue: '',
|
||||
uploadFileUrl: '/admin/sys-file/upload',
|
||||
drag: true,
|
||||
disabled: false,
|
||||
fileSize: 5,
|
||||
fileType: () => ['image/jpeg', 'image/png', 'image/gif'],
|
||||
height: '150px',
|
||||
width: '150px',
|
||||
borderRadius: '8px',
|
||||
dir: '',
|
||||
});
|
||||
|
||||
// 计算accept属性值
|
||||
const acceptType = computed(() => {
|
||||
return props.fileType.join(',');
|
||||
});
|
||||
|
||||
// 生成组件唯一id
|
||||
const uuid = ref('id-' + generateUUID());
|
||||
|
||||
// 查看图片
|
||||
const imgViewVisible = ref(false);
|
||||
// 获取 el-form 组件上下文
|
||||
const formContext = inject(formContextKey, void 0);
|
||||
// 获取 el-form-item 组件上下文
|
||||
const formItemContext = inject(formItemContextKey, void 0);
|
||||
// 本地 image url
|
||||
const _imageUrl = ref(props.imageUrl || props.modelValue);
|
||||
|
||||
// 监听 props 变化
|
||||
watch(
|
||||
() => props.imageUrl,
|
||||
(val) => {
|
||||
if (val !== _imageUrl.value) {
|
||||
_imageUrl.value = val || '';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val !== _imageUrl.value) {
|
||||
_imageUrl.value = val || '';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 判断是否禁用上传和删除
|
||||
const self_disabled = computed(() => {
|
||||
return props.disabled || formContext?.disabled;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 图片上传
|
||||
* @param options upload 所有配置项
|
||||
* */
|
||||
interface UploadEmits {
|
||||
(e: 'update:imageUrl', value: string): void;
|
||||
(e: 'update:modelValue', value: string): void;
|
||||
}
|
||||
const emit = defineEmits<UploadEmits>();
|
||||
const handleHttpUpload = async (options: UploadRequestOptions) => {
|
||||
let formData = new FormData();
|
||||
formData.append('file', options.file);
|
||||
formData.append('dir', props.dir);
|
||||
try {
|
||||
const { data } = await request({
|
||||
url: props.uploadFileUrl,
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'Enc-Flag': 'false',
|
||||
},
|
||||
data: formData,
|
||||
});
|
||||
emit('update:imageUrl', data.url);
|
||||
emit('update:modelValue', data.url);
|
||||
_imageUrl.value = data.url;
|
||||
// 调用 el-form 内部的校验方法(可自动校验)
|
||||
formItemContext?.prop && formContext?.validateField([formItemContext.prop as string]);
|
||||
} catch (error) {
|
||||
options.onError(error as any);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 删除图片
|
||||
* */
|
||||
const deleteImg = () => {
|
||||
emit('update:imageUrl', '');
|
||||
emit('update:modelValue', '');
|
||||
_imageUrl.value = '';
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 编辑图片
|
||||
* */
|
||||
const editImg = () => {
|
||||
const dom = document.querySelector(`#${uuid.value} .el-upload__input`);
|
||||
dom && dom.dispatchEvent(new MouseEvent('click'));
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 文件上传之前判断
|
||||
* @param rawFile 选择的文件
|
||||
* */
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
||||
const imgSize = rawFile.size / 1024 / 1024 < props.fileSize;
|
||||
const imgType = props.fileType.includes(rawFile.type as ImageMimeType);
|
||||
if (!imgType)
|
||||
ElNotification({
|
||||
title: '温馨提示',
|
||||
message: '上传图片不符合所需的格式!',
|
||||
type: 'warning',
|
||||
});
|
||||
if (!imgSize)
|
||||
setTimeout(() => {
|
||||
ElNotification({
|
||||
title: '温馨提示',
|
||||
message: `上传图片大小不能超过 ${props.fileSize}M!`,
|
||||
type: 'warning',
|
||||
});
|
||||
}, 0);
|
||||
return imgType && imgSize;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 图片上传成功
|
||||
* */
|
||||
const uploadSuccess = () => {
|
||||
ElNotification({
|
||||
title: '温馨提示',
|
||||
message: '图片上传成功!',
|
||||
type: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 图片上传错误
|
||||
* */
|
||||
const uploadError = () => {
|
||||
ElNotification({
|
||||
title: '温馨提示',
|
||||
message: '图片上传失败,请您重新上传!',
|
||||
type: 'error',
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.is-error {
|
||||
.upload {
|
||||
:deep(.el-upload),
|
||||
:deep(.el-upload-dragger) {
|
||||
border: 1px dashed var(--el-color-danger) !important;
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.disabled) {
|
||||
.el-upload,
|
||||
.el-upload-dragger {
|
||||
cursor: not-allowed !important;
|
||||
background: var(--el-disabled-bg-color);
|
||||
border: 1px dashed var(--el-border-color-darker) !important;
|
||||
&:hover {
|
||||
border: 1px dashed var(--el-border-color-darker) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.upload-box {
|
||||
.no-border {
|
||||
:deep(.el-upload) {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
:deep(.upload) {
|
||||
.el-upload {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: v-bind(width);
|
||||
height: v-bind(height);
|
||||
overflow: hidden;
|
||||
border: 1px dashed var(--el-border-color-darker);
|
||||
border-radius: v-bind(borderRadius);
|
||||
transition: var(--el-transition-duration-fast);
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
.upload-handle {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.el-upload-dragger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background-color: transparent;
|
||||
border: 1px dashed var(--el-border-color-darker);
|
||||
border-radius: v-bind(borderRadius);
|
||||
&:hover {
|
||||
border: 1px dashed var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.el-upload-dragger.is-dragover {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border: 2px dashed var(--el-color-primary) !important;
|
||||
}
|
||||
.upload-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
.upload-empty {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
line-height: 30px;
|
||||
color: var(--el-color-info);
|
||||
.el-icon {
|
||||
font-size: 28px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
.upload-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
background: rgb(0 0 0 / 60%);
|
||||
opacity: 0;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
.handle-icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 6%;
|
||||
color: aliceblue;
|
||||
.el-icon {
|
||||
margin-bottom: 40%;
|
||||
font-size: 130%;
|
||||
line-height: 130%;
|
||||
}
|
||||
span {
|
||||
font-size: 85%;
|
||||
line-height: 85%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-upload__tip {
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
278
src/components/Upload/ImagePlus.vue
Normal file
278
src/components/Upload/ImagePlus.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<!-- 图片上传组件, 推荐使用 ImagePlus 组件,后续将删除 Image组件-->
|
||||
<template>
|
||||
<div>
|
||||
<el-upload
|
||||
multiple
|
||||
:action="uploadImgUrl"
|
||||
list-type="picture-card"
|
||||
:on-success="handleUploadSuccess"
|
||||
:before-upload="handleBeforeUpload"
|
||||
:data="data"
|
||||
:limit="limit"
|
||||
:on-error="handleUploadError"
|
||||
:on-exceed="handleExceed"
|
||||
ref="imageUpload"
|
||||
:on-remove="handleDelete"
|
||||
:show-file-list="true"
|
||||
:headers="headers"
|
||||
:file-list="fileList"
|
||||
:on-preview="handlePictureCardPreview"
|
||||
:class="{ hide: fileList.length >= limit }"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
|
||||
<!-- 上传提示 -->
|
||||
<div class="el-upload__tip" v-if="showTip">
|
||||
{{ t('uploadTipPrefix') }}
|
||||
<template v-if="fileSize">
|
||||
{{ t('sizeLimitTip') }} <b style="color: #f56c6c">{{ fileSize }}MB</b>
|
||||
</template>
|
||||
<template v-if="fileType">
|
||||
{{ t('formatTip') }} <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
|
||||
</template>
|
||||
{{ t('fileSuffix') }}
|
||||
</div>
|
||||
|
||||
<el-image-viewer :teleported="true" v-if="imgViewVisible" @close="imgViewVisible = false" :url-list="previewImageList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick, onMounted } from 'vue';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElLoading, ElImageViewer } from 'element-plus';
|
||||
import Sortable from 'sortablejs';
|
||||
import { Session } from '/@/utils/storage';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Object, Array],
|
||||
default: '',
|
||||
},
|
||||
// 图片地址,用于向下兼容 Image 组件
|
||||
imageUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 上传接口地址
|
||||
action: {
|
||||
type: String,
|
||||
default: '/admin/sys-file/upload',
|
||||
},
|
||||
// 上传携带的参数
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
// 图片数量限制
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
// 大小限制(MB)
|
||||
fileSize: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
// 文件类型, 例如['png', 'jpg', 'jpeg']
|
||||
fileType: {
|
||||
type: Array,
|
||||
default: () => ['png', 'jpg', 'jpeg'],
|
||||
},
|
||||
// 是否显示提示
|
||||
isShowTip: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 拖动排序
|
||||
drag: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 是否禁用
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'update:imageUrl']);
|
||||
const number = ref(0);
|
||||
const uploadList = ref([]);
|
||||
const imgViewVisible = ref(false);
|
||||
const imageUpload = ref(null);
|
||||
const baseURL = import.meta.env.VITE_API_URL || '';
|
||||
const uploadImgUrl = computed(() => baseURL + props.action);
|
||||
const fileList = ref([]);
|
||||
const previewImageList = ref([]);
|
||||
const headers = computed(() => {
|
||||
return {
|
||||
Authorization: 'Bearer ' + Session.getToken(),
|
||||
'TENANT-ID': Session.getTenant(),
|
||||
};
|
||||
});
|
||||
|
||||
// 是否显示提示
|
||||
const showTip = computed(() => {
|
||||
return props.isShowTip && (props.fileType || props.fileSize);
|
||||
});
|
||||
|
||||
// 监听value变化
|
||||
watch(
|
||||
() => [props.modelValue, props.imageUrl],
|
||||
([modelVal, imageVal]) => {
|
||||
// 优先使用 modelValue,如果没有则使用 imageUrl
|
||||
const val = modelVal || imageVal;
|
||||
if (val) {
|
||||
// 首先将值转为数组
|
||||
const list = Array.isArray(val) ? val : val.split(',');
|
||||
// 然后将数组转为对象数组
|
||||
fileList.value = list.map((item) => {
|
||||
if (typeof item === 'string') {
|
||||
item = { name: item, url: item };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else {
|
||||
fileList.value = [];
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
// 拖拽排序
|
||||
onMounted(() => {
|
||||
if (props.drag) {
|
||||
nextTick(() => {
|
||||
const element = document.querySelector('.el-upload-list');
|
||||
if (element) {
|
||||
Sortable.create(element, {
|
||||
onEnd: (evt) => {
|
||||
const movedItem = fileList.value.splice(evt.oldIndex, 1)[0];
|
||||
fileList.value.splice(evt.newIndex, 0, movedItem);
|
||||
const resultString = listToString(fileList.value);
|
||||
emit('update:modelValue', resultString);
|
||||
emit('update:imageUrl', resultString);
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 上传前loading加载
|
||||
const handleBeforeUpload = (file) => {
|
||||
let isImg = false;
|
||||
if (props.fileType.length) {
|
||||
let fileExtension = '';
|
||||
if (file.name.lastIndexOf('.') > -1) {
|
||||
fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1);
|
||||
}
|
||||
isImg = props.fileType.some((type) => {
|
||||
if (file.type.indexOf(type) > -1) return true;
|
||||
if (fileExtension && fileExtension.indexOf(type) > -1) return true;
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
isImg = file.type.indexOf('image') > -1;
|
||||
}
|
||||
|
||||
if (!isImg) {
|
||||
ElMessage.error(t('invalidFormatError', { fileType: props.fileType.join('/') }));
|
||||
return false;
|
||||
}
|
||||
if (file.name.includes(',')) {
|
||||
ElMessage.error(t('invalidFilenameError'));
|
||||
return false;
|
||||
}
|
||||
if (props.fileSize) {
|
||||
const isLt = file.size / 1024 / 1024 < props.fileSize;
|
||||
if (!isLt) {
|
||||
ElMessage.error(t('sizeLimitError', { fileSize: props.fileSize }));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ElLoading.service({ text: t('uploading') });
|
||||
number.value++;
|
||||
return true;
|
||||
};
|
||||
|
||||
// 文件个数超出
|
||||
const handleExceed = () => {
|
||||
ElMessage.error(t('limitExceedError', { limit: props.limit }));
|
||||
};
|
||||
|
||||
// 上传成功回调
|
||||
const handleUploadSuccess = (res, file) => {
|
||||
if (res.code === 0) {
|
||||
uploadList.value.push({ name: res.data.fileName, url: baseURL + res.data.url });
|
||||
uploadedSuccessfully();
|
||||
} else {
|
||||
number.value--;
|
||||
ElLoading.service().close();
|
||||
ElMessage.error(t('uploadFailRetry'));
|
||||
imageUpload.value.handleRemove(file);
|
||||
uploadedSuccessfully();
|
||||
}
|
||||
};
|
||||
|
||||
// 删除图片
|
||||
const handleDelete = (file) => {
|
||||
const findex = fileList.value.map((f) => f.name).indexOf(file.name);
|
||||
if (findex > -1) {
|
||||
fileList.value.splice(findex, 1);
|
||||
const resultString = listToString(fileList.value);
|
||||
emit('update:modelValue', resultString);
|
||||
emit('update:imageUrl', resultString);
|
||||
}
|
||||
};
|
||||
|
||||
// 上传失败
|
||||
const handleUploadError = () => {
|
||||
ElMessage.error(t('uploadFail'));
|
||||
ElLoading.service().close();
|
||||
};
|
||||
|
||||
// 上传结束处理
|
||||
const uploadedSuccessfully = () => {
|
||||
if (number.value > 0 && uploadList.value.length === number.value) {
|
||||
fileList.value = fileList.value.concat(uploadList.value);
|
||||
uploadList.value = [];
|
||||
number.value = 0;
|
||||
const resultString = listToString(fileList.value);
|
||||
emit('update:modelValue', resultString);
|
||||
emit('update:imageUrl', resultString);
|
||||
ElLoading.service().close();
|
||||
}
|
||||
};
|
||||
|
||||
// 预览
|
||||
const handlePictureCardPreview = (file) => {
|
||||
previewImageList.value = [file.url];
|
||||
imgViewVisible.value = true;
|
||||
};
|
||||
|
||||
// 对象转成指定字符串分隔
|
||||
const listToString = (list, separator) => {
|
||||
let strs = '';
|
||||
separator = separator || ',';
|
||||
for (let i in list) {
|
||||
if (list[i].url) {
|
||||
strs += list[i].url + separator;
|
||||
}
|
||||
}
|
||||
return strs !== '' ? strs.substr(0, strs.length - 1) : '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 控制加号部分 */
|
||||
:deep(.hide .el-upload--picture-card) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
30
src/components/Upload/i18n/en.ts
Normal file
30
src/components/Upload/i18n/en.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export default {
|
||||
excel: {
|
||||
downloadTemplate: 'downloading the template',
|
||||
fileFormat: 'only xls, xlsx format files are allowed',
|
||||
operationNotice: 'Drag the file here and',
|
||||
clickUpload: 'click upload',
|
||||
lineNumbers: 'line numbers',
|
||||
misDescription: 'misDescription',
|
||||
validationFailureData: 'validation failure data',
|
||||
pleaseUpload: 'please upload',
|
||||
size: 'size not exceeding',
|
||||
format: 'format',
|
||||
file: 'file',
|
||||
sizeErrorText: 'file size error, max ',
|
||||
typeErrorText: 'file type error, upload ',
|
||||
uploadLimit: 'Upload limit exceeded. Maximum',
|
||||
files: 'files allowed',
|
||||
},
|
||||
uploadTipPrefix: 'Please upload',
|
||||
sizeLimitTip: 'size less than',
|
||||
formatTip: 'format is',
|
||||
fileSuffix: 'files',
|
||||
invalidFormatError: 'Incorrect file format, please upload {fileType} format pictures!',
|
||||
invalidFilenameError: 'Incorrect file name, cannot contain commas!',
|
||||
sizeLimitError: 'Upload image size cannot exceed {fileSize} MB!',
|
||||
uploading: 'Uploading image, please wait...',
|
||||
limitExceedError: 'Number of uploaded files cannot exceed {limit}!',
|
||||
uploadFailRetry: 'Upload failed, please try again',
|
||||
uploadFail: 'Image upload failed, please try again',
|
||||
};
|
||||
30
src/components/Upload/i18n/zh-cn.ts
Normal file
30
src/components/Upload/i18n/zh-cn.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export default {
|
||||
excel: {
|
||||
downloadTemplate: '下载模板',
|
||||
fileFormat: '仅允许导入xls、xlsx格式文件。',
|
||||
operationNotice: '将文件拖到此处,或',
|
||||
clickUpload: '点击上传',
|
||||
lineNumbers: '行号',
|
||||
misDescription: '错误描述',
|
||||
validationFailureData: '校验失败数据',
|
||||
pleaseUpload: '请上传',
|
||||
size: '大小不超过',
|
||||
format: '格式为',
|
||||
file: '的文件',
|
||||
sizeErrorText: '文件大小不超过',
|
||||
typeErrorText: '文件类型错误,请上传 ',
|
||||
uploadLimit: '上传文件数量超出限制,最多允许上传',
|
||||
files: '个文件',
|
||||
},
|
||||
uploadTipPrefix: '请上传',
|
||||
sizeLimitTip: '大小不超过',
|
||||
formatTip: '格式为',
|
||||
fileSuffix: '的文件',
|
||||
invalidFormatError: '文件格式不正确,请上传{fileType}图片格式文件!',
|
||||
invalidFilenameError: '文件名不正确,不能包含英文逗号!',
|
||||
sizeLimitError: '上传头像图片大小不能超过 {fileSize} MB!',
|
||||
uploading: '正在上传图片,请稍候...',
|
||||
limitExceedError: '上传文件数量不能超过 {limit} 个!',
|
||||
uploadFailRetry: '上传失败,请重试',
|
||||
uploadFail: '上传图片失败,请重试',
|
||||
};
|
||||
340
src/components/Upload/index.vue
Normal file
340
src/components/Upload/index.vue
Normal file
@@ -0,0 +1,340 @@
|
||||
<!--文件上传组件-->
|
||||
<template>
|
||||
<div class="w-full upload-file">
|
||||
<!-- 当禁用时只显示文件列表,不使用el-upload组件 -->
|
||||
<div v-if="props.disabled">
|
||||
<div v-if="fileList.length === 0" class="flex justify-center items-center px-4 text-gray-400 bg-gray-50 rounded-md p">
|
||||
<el-icon class="mr-2 text-lg"><Document /></el-icon>
|
||||
<span class="text-sm">{{ $t('excel.noFiles') }}</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div
|
||||
v-for="(file, index) in fileList"
|
||||
:key="index"
|
||||
class="flex items-center px-4 py-3 mb-1 rounded transition-colors duration-200 cursor-pointer group hover:bg-blue-50"
|
||||
@click="handlePreview(file)"
|
||||
>
|
||||
<el-icon class="mr-3 text-blue-500"><Document /></el-icon>
|
||||
<span class="flex-1 text-gray-700 truncate transition-colors duration-200 group-hover:text-blue-600">
|
||||
{{ getFileName(file) }}
|
||||
</span>
|
||||
<el-icon class="text-gray-400 transition-colors duration-200 group-hover:text-blue-500"><Download /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 默认上传组件 -->
|
||||
<el-upload
|
||||
ref="fileUpload"
|
||||
v-if="props.type === 'default' && !props.disabled"
|
||||
:action="baseUrl + other.adaptationUrl(props.uploadFileUrl)"
|
||||
:before-upload="handleBeforeUpload"
|
||||
:file-list="fileList"
|
||||
:headers="headers"
|
||||
:limit="limit"
|
||||
:on-error="handleUploadError"
|
||||
:on-remove="handleRemove"
|
||||
:on-preview="handlePreview"
|
||||
:on-exceed="handleExceed"
|
||||
:data="formData"
|
||||
:auto-upload="autoUpload"
|
||||
:on-success="handleUploadSuccess"
|
||||
:accept="fileAccept"
|
||||
class="upload-file-uploader"
|
||||
drag
|
||||
multiple
|
||||
>
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">
|
||||
{{ $t('excel.operationNotice') }}
|
||||
<em>{{ $t('excel.clickUpload') }}</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip" v-if="props.isShowTip">
|
||||
{{ $t('excel.pleaseUpload') }}
|
||||
<template v-if="props.fileSize">
|
||||
{{ $t('excel.size') }} <b style="color: #f56c6c">{{ props.fileSize }}MB</b></template
|
||||
>
|
||||
<template v-if="props.fileType">
|
||||
{{ $t('excel.format') }} <b style="color: #f56c6c">{{ props.fileType.join('/') }}</b>
|
||||
</template>
|
||||
{{ $t('excel.file') }}
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<!-- 简单上传组件 -->
|
||||
<el-upload
|
||||
ref="fileUpload"
|
||||
v-if="props.type === 'simple' && !props.disabled"
|
||||
:action="baseUrl + other.adaptationUrl(props.uploadFileUrl)"
|
||||
:before-upload="handleBeforeUpload"
|
||||
:file-list="fileList"
|
||||
:headers="headers"
|
||||
:limit="limit"
|
||||
:auto-upload="autoUpload"
|
||||
:on-error="handleUploadError"
|
||||
:on-remove="handleRemove"
|
||||
:on-exceed="handleExceed"
|
||||
:data="formData"
|
||||
:on-success="handleUploadSuccess"
|
||||
:accept="fileAccept"
|
||||
class="upload-file-uploader"
|
||||
multiple
|
||||
>
|
||||
<el-button type="primary" link>{{ $t('excel.clickUpload') }}</el-button>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip" v-if="props.isShowTip">
|
||||
{{ $t('excel.pleaseUpload') }}
|
||||
<template v-if="props.fileSize">
|
||||
{{ $t('excel.size') }} <b style="color: #f56c6c">{{ props.fileSize }}MB</b></template
|
||||
>
|
||||
<template v-if="props.fileType">
|
||||
{{ $t('excel.format') }} <b style="color: #f56c6c">{{ props.fileType.join('/') }}</b>
|
||||
</template>
|
||||
{{ $t('excel.file') }}
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="upload-file">
|
||||
import { useMessage } from '/@/hooks/message';
|
||||
import { Session } from '/@/utils/storage';
|
||||
import other from '/@/utils/other';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { Document, Download } from '@element-plus/icons-vue';
|
||||
|
||||
// 定义基础URL
|
||||
const baseUrl = import.meta.env.VITE_API_URL || '';
|
||||
|
||||
// 获取文件名
|
||||
const getFileName = (file: any): string => {
|
||||
return file.url ? other.getQueryString(file.url, 'fileName') || other.getQueryString(file.url, 'originalFileName') : 'File';
|
||||
};
|
||||
|
||||
// 根据文件类型生成accept属性值
|
||||
const fileAccept = computed(() => {
|
||||
if (!props.fileType || props.fileType.length === 0) return '';
|
||||
|
||||
let acceptValues: string[] = [];
|
||||
|
||||
for (const ext of props.fileType) {
|
||||
if (typeof ext === 'string') {
|
||||
acceptValues.push(`.${ext}`);
|
||||
}
|
||||
}
|
||||
|
||||
return acceptValues.join(',');
|
||||
});
|
||||
|
||||
interface FileItem {
|
||||
name?: string;
|
||||
url?: string;
|
||||
uid?: number;
|
||||
}
|
||||
|
||||
interface UploadFileItem {
|
||||
name: string;
|
||||
url: string;
|
||||
fileUrl: string;
|
||||
fileSize: number;
|
||||
fileName: string;
|
||||
fileType: string;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: [String, Array],
|
||||
// 数量限制
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
// 大小限制(MB)
|
||||
fileSize: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
fileType: {
|
||||
type: Array,
|
||||
default: () => ['png', 'jpg', 'jpeg', 'doc', 'xls', 'ppt', 'txt', 'pdf', 'docx', 'xlsx', 'pptx'],
|
||||
},
|
||||
// 是否显示提示
|
||||
isShowTip: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
uploadFileUrl: {
|
||||
type: String,
|
||||
default: '/admin/sys-file/upload',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator: (value: string) => {
|
||||
return ['default', 'simple'].includes(value);
|
||||
},
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
dir: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
autoUpload: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
const number = ref(0);
|
||||
const fileList = ref<FileItem[]>([]);
|
||||
const uploadList = ref<UploadFileItem[]>([]);
|
||||
const fileUpload = ref();
|
||||
const { t } = useI18n();
|
||||
|
||||
// 请求头处理
|
||||
const headers = computed(() => {
|
||||
return {
|
||||
Authorization: 'Bearer ' + Session.get('token'),
|
||||
'TENANT-ID': Session.getTenant(),
|
||||
};
|
||||
});
|
||||
|
||||
// 请求参数处理
|
||||
const formData = computed(() => {
|
||||
return Object.assign(props.data, { dir: props.dir });
|
||||
});
|
||||
|
||||
// 上传前校检格式和大小
|
||||
const handleBeforeUpload = (file: File) => {
|
||||
// 校检文件类型
|
||||
if (props.fileType.length) {
|
||||
const fileName = file.name.split('.');
|
||||
const fileExt = fileName[fileName.length - 1];
|
||||
const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
|
||||
if (!isTypeOk) {
|
||||
useMessage().error(`${t('excel.typeErrorText')} ${props.fileType.join('/')}!`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 校检文件大小
|
||||
if (props.fileSize) {
|
||||
const isLt = file.size / 1024 / 1024 < props.fileSize;
|
||||
if (!isLt) {
|
||||
useMessage().error(`${t('excel.sizeErrorText')} ${props.fileSize} MB!`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
number.value++;
|
||||
return true;
|
||||
};
|
||||
|
||||
// 上传成功回调
|
||||
function handleUploadSuccess(res: any, file: any) {
|
||||
if (res.code === 0) {
|
||||
uploadList.value.push({
|
||||
name: file.name,
|
||||
url: `${res.data?.url}&originalFileName=${file.name}`,
|
||||
fileUrl: res.data?.fileName,
|
||||
fileSize: file.size,
|
||||
fileName: file.name,
|
||||
fileType: file.raw.type,
|
||||
});
|
||||
uploadedSuccessfully();
|
||||
} else {
|
||||
number.value--;
|
||||
useMessage().error(res.msg);
|
||||
fileUpload.value.handleRemove(file);
|
||||
uploadedSuccessfully();
|
||||
}
|
||||
}
|
||||
|
||||
// 上传结束处理
|
||||
const uploadedSuccessfully = () => {
|
||||
if (number.value > 0 && uploadList.value.length === number.value) {
|
||||
fileList.value = fileList.value.filter((f) => f.url !== undefined).concat(uploadList.value);
|
||||
uploadList.value = [];
|
||||
number.value = 0;
|
||||
emit('update:modelValue', listToString(fileList.value));
|
||||
emit('change', listToString(fileList.value), fileList.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (file: { name: string }) => {
|
||||
fileList.value = fileList.value.filter((f) => f.name !== file.name);
|
||||
emit('update:modelValue', listToString(fileList.value));
|
||||
emit('change', listToString(fileList.value), fileList.value);
|
||||
};
|
||||
|
||||
const handlePreview = (file: any) => {
|
||||
other.downBlobFile(file.url, {}, file.name);
|
||||
};
|
||||
|
||||
// 添加 handleExceed 函数
|
||||
const handleExceed = () => {
|
||||
useMessage().warning(`${t('excel.uploadLimit')} ${props.limit} ${t('excel.files')}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 将对象数组转为字符串,以逗号分隔。
|
||||
* @param list 待转换的对象数组。
|
||||
* @param separator 分隔符,默认为逗号。
|
||||
* @returns {string} 返回转换后的字符串。
|
||||
*/
|
||||
const listToString = (list: FileItem[], separator = ','): string => {
|
||||
let strs = '';
|
||||
separator = separator || ',';
|
||||
for (let i in list) {
|
||||
if (list[i].url) {
|
||||
strs += list[i].url + separator;
|
||||
}
|
||||
}
|
||||
return strs !== '' ? strs.substr(0, strs.length - 1) : '';
|
||||
};
|
||||
|
||||
const handleUploadError = () => {
|
||||
useMessage().error('上传文件失败');
|
||||
};
|
||||
|
||||
/**
|
||||
* 监听 props 中的 modelValue 值变化,更新 fileList。
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val) {
|
||||
let temp = 1;
|
||||
// 首先将值转为数组
|
||||
const list = Array.isArray(val) ? val : (props.modelValue as string).split(',');
|
||||
// 然后将数组转为对象数组
|
||||
fileList.value = list.map((item: any) => {
|
||||
if (typeof item === 'string') {
|
||||
item = { name: other.getQueryString(item, 'originalFileName') || other.getQueryString(item, 'fileName'), url: item };
|
||||
}
|
||||
item.uid = item.uid || new Date().getTime() + temp++;
|
||||
return item as FileItem;
|
||||
});
|
||||
} else {
|
||||
fileList.value = [];
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
const submit = () => {
|
||||
fileUpload.value?.submit();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
submit,
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user