上传
This commit is contained in:
@@ -6,7 +6,7 @@ import { propTypes } from '@/utils/propTypes'
|
||||
import { isNumber } from '@/utils/is'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useLocaleStore } from '@/store/modules/locale'
|
||||
import { getAccessToken, getTenantId } from '@/utils/auth'
|
||||
import { getAccessToken, getTenantId, getAppId } from '@/utils/auth'
|
||||
|
||||
type InsertFnType = (url: string, alt: string, href: string) => void
|
||||
|
||||
@@ -103,7 +103,8 @@ const editorConfig = computed((): IEditorConfig => {
|
||||
headers: {
|
||||
Accept: '*',
|
||||
Authorization: 'Bearer ' + getAccessToken(),
|
||||
'tenant-id': getTenantId()
|
||||
'tenant-id': getTenantId(),
|
||||
'instance-id': getAppId()
|
||||
},
|
||||
|
||||
// 跨域是否传递 cookie ,默认为 false
|
||||
@@ -140,6 +141,63 @@ const editorConfig = computed((): IEditorConfig => {
|
||||
customInsert(res: any, insertFn: InsertFnType) {
|
||||
insertFn(res.data, 'image', res.data)
|
||||
}
|
||||
},
|
||||
['uploadVideo']: {
|
||||
server: import.meta.env.VITE_UPLOAD_URL,
|
||||
// 单个文件的最大体积限制,默认为 2M
|
||||
maxFileSize: 100 * 1024 * 1024,
|
||||
// 最多可上传几个文件,默认为 100
|
||||
maxNumberOfFiles: 10,
|
||||
// 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
|
||||
allowedFileTypes: ['video/*'],
|
||||
|
||||
// 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。
|
||||
meta: { updateSupport: 0 },
|
||||
// 将 meta 拼接到 url 参数中,默认 false
|
||||
metaWithUrl: true,
|
||||
|
||||
// 自定义增加 http header
|
||||
headers: {
|
||||
Accept: '*',
|
||||
Authorization: 'Bearer ' + getAccessToken(),
|
||||
'tenant-id': getTenantId(),
|
||||
'instance-id': getAppId()
|
||||
},
|
||||
|
||||
// 跨域是否传递 cookie ,默认为 false
|
||||
withCredentials: true,
|
||||
|
||||
// 超时时间,默认为 10 秒
|
||||
timeout: 10 * 1000, // 5 秒
|
||||
|
||||
// form-data fieldName,后端接口参数名称,默认值wangeditor-uploaded-image
|
||||
fieldName: 'file',
|
||||
|
||||
// 上传之前触发
|
||||
onBeforeUpload(file: File) {
|
||||
console.log(file)
|
||||
return file
|
||||
},
|
||||
// 上传进度的回调函数
|
||||
onProgress(progress: number) {
|
||||
// progress 是 0-100 的数字
|
||||
console.log('progress', progress)
|
||||
},
|
||||
onSuccess(file: File, res: any) {
|
||||
console.log('onSuccess', file, res)
|
||||
},
|
||||
onFailed(file: File, res: any) {
|
||||
alert(res.message)
|
||||
console.log('onFailed', file, res)
|
||||
},
|
||||
onError(file: File, err: any, res: any) {
|
||||
alert(err.message)
|
||||
console.error('onError', file, err, res)
|
||||
},
|
||||
// 自定义插入图片
|
||||
customInsert(res: any, insertFn: InsertFnType) {
|
||||
insertFn(res.data, 'video', res.data)
|
||||
}
|
||||
}
|
||||
},
|
||||
uploadImgShowBase64: true
|
||||
|
||||
@@ -33,8 +33,8 @@ const props = defineProps({
|
||||
.validate((v: string) => ['left', 'center', 'right'].includes(v))
|
||||
.def('center'),
|
||||
showLabel: propTypes.bool.def(false),
|
||||
showSearch: propTypes.bool.def(true),
|
||||
showReset: propTypes.bool.def(true),
|
||||
showSearch: propTypes.bool.def(false),
|
||||
showReset: propTypes.bool.def(false),
|
||||
// 是否显示伸缩
|
||||
expand: propTypes.bool.def(false),
|
||||
// 伸缩的界限字段
|
||||
@@ -199,10 +199,10 @@ initSearch()
|
||||
</ElButton>
|
||||
<!-- add by 芋艿:补充在搜索后的按钮 -->
|
||||
<slot name="actionMore"></slot>
|
||||
<ElButton v-if="expand" text @click="setVisible">
|
||||
<!-- <ElButton v-if="expand" text @click="setVisible">
|
||||
{{ t(visible ? 'common.shrink' : 'common.expand') }}
|
||||
<!-- <Icon :icon="visible ? 'ep:arrow-up' : 'ep:arrow-down'" /> -->
|
||||
</ElButton>
|
||||
<Icon :icon="visible ? 'ep:arrow-up' : 'ep:arrow-down'" />
|
||||
</ElButton> -->
|
||||
</div>
|
||||
</template>
|
||||
<template v-for="name in Object.keys($slots)" :key="name" #[name]>
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
<el-upload
|
||||
v-model:file-list="fileList"
|
||||
:accept="fileType.join(',')"
|
||||
:action="updateUrl"
|
||||
:action="uploadUrl"
|
||||
:before-upload="beforeUpload"
|
||||
:class="['upload', drag ? 'no-border' : '']"
|
||||
:disabled="disabled"
|
||||
:drag="drag"
|
||||
:headers="uploadHeaders"
|
||||
:http-request="httpRequest"
|
||||
:limit="limit"
|
||||
:multiple="true"
|
||||
:on-error="uploadError"
|
||||
@@ -28,7 +29,7 @@
|
||||
<Icon icon="ep:zoom-in" />
|
||||
<span>查看</span>
|
||||
</div>
|
||||
<div class="handle-icon" @click="handleRemove(file)">
|
||||
<div v-if="!disabled" class="handle-icon" @click="handleRemove(file)">
|
||||
<Icon icon="ep:delete" />
|
||||
<span>删除</span>
|
||||
</div>
|
||||
@@ -45,13 +46,14 @@
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" name="UploadImgs" setup>
|
||||
import { PropType } from 'vue'
|
||||
<script lang="ts" setup>
|
||||
import type { UploadFile, UploadProps, UploadUserFile } from 'element-plus'
|
||||
import { ElNotification } from 'element-plus'
|
||||
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { getAccessToken, getTenantId } from '@/utils/auth'
|
||||
import { useUpload } from './useUpload'
|
||||
|
||||
defineOptions({ name: 'UploadImgs' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
@@ -68,11 +70,7 @@ type FileTypes =
|
||||
| 'image/x-icon'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array as PropType<UploadUserFile[]>,
|
||||
required: true
|
||||
},
|
||||
updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL),
|
||||
modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
|
||||
drag: propTypes.bool.def(true), // 是否支持拖拽上传 ==> 非必传(默认为 true)
|
||||
disabled: propTypes.bool.def(false), // 是否禁用上传组件 ==> 非必传(默认为 false)
|
||||
limit: propTypes.number.def(5), // 最大图片上传数 ==> 非必传(默认为 5张)
|
||||
@@ -80,27 +78,14 @@ const props = defineProps({
|
||||
fileType: propTypes.array.def(['image/jpeg', 'image/png', 'image/gif']), // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/gif"])
|
||||
height: propTypes.string.def('150px'), // 组件高度 ==> 非必传(默认为 150px)
|
||||
width: propTypes.string.def('150px'), // 组件宽度 ==> 非必传(默认为 150px)
|
||||
borderRadius: propTypes.string.def('8px') // 组件边框圆角 ==> 非必传(默认为 8px)
|
||||
borderradius: propTypes.string.def('8px') // 组件边框圆角 ==> 非必传(默认为 8px)
|
||||
})
|
||||
|
||||
const uploadHeaders = ref({
|
||||
Authorization: 'Bearer ' + getAccessToken(),
|
||||
'tenant-id': getTenantId()
|
||||
})
|
||||
const { uploadUrl, httpRequest } = useUpload()
|
||||
|
||||
const fileList = ref<UploadUserFile[]>()
|
||||
// fix: 改为动态监听赋值解决图片回显问题
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(data) => {
|
||||
if (!data) return
|
||||
fileList.value = data
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
const fileList = ref<UploadUserFile[]>([])
|
||||
const uploadNumber = ref<number>(0)
|
||||
const uploadList = ref<UploadUserFile[]>([])
|
||||
/**
|
||||
* @description 文件上传之前判断
|
||||
* @param rawFile 上传的文件
|
||||
@@ -120,29 +105,60 @@ const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
||||
message: `上传图片大小不能超过 ${props.fileSize}M!`,
|
||||
type: 'warning'
|
||||
})
|
||||
uploadNumber.value++
|
||||
return imgType.includes(rawFile.type as FileTypes) && imgSize
|
||||
}
|
||||
|
||||
// 图片上传成功
|
||||
interface UploadEmits {
|
||||
(e: 'update:modelValue', value: UploadUserFile[]): void
|
||||
(e: 'update:modelValue', value: string[]): void
|
||||
}
|
||||
|
||||
const emit = defineEmits<UploadEmits>()
|
||||
const uploadSuccess = (response, uploadFile: UploadFile) => {
|
||||
if (!response) return
|
||||
// TODO 多图上传组件成功后只是把保存成功后的url替换掉组件选图时的文件路径,所以返回的fileList包含的是一个包含文件信息的对象列表
|
||||
uploadFile.url = response.data
|
||||
emit('update:modelValue', fileList.value)
|
||||
const uploadSuccess: UploadProps['onSuccess'] = (res: any): void => {
|
||||
message.success('上传成功')
|
||||
// 删除自身
|
||||
const index = fileList.value.findIndex((item) => item.response?.data === res.data)
|
||||
fileList.value.splice(index, 1)
|
||||
uploadList.value.push({ name: res.data, url: res.data })
|
||||
if (uploadList.value.length == uploadNumber.value) {
|
||||
fileList.value.push(...uploadList.value)
|
||||
uploadList.value = []
|
||||
uploadNumber.value = 0
|
||||
emitUpdateModelValue()
|
||||
}
|
||||
}
|
||||
|
||||
// 监听模型绑定值变动
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val: string | string[]) => {
|
||||
if (!val) {
|
||||
fileList.value = [] // fix:处理掉缓存,表单重置后上传组件的内容并没有重置
|
||||
return
|
||||
}
|
||||
|
||||
fileList.value = [] // 保障数据为空
|
||||
fileList.value.push(
|
||||
...(val as string[]).map((url) => ({ name: url.substring(url.lastIndexOf('/') + 1), url }))
|
||||
)
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
// 发送图片链接列表更新
|
||||
const emitUpdateModelValue = () => {
|
||||
let result: string[] = fileList.value.map((file) => file.url!)
|
||||
emit('update:modelValue', result)
|
||||
}
|
||||
// 删除图片
|
||||
const handleRemove = (uploadFile: UploadFile) => {
|
||||
fileList.value = fileList.value.filter(
|
||||
(item) => item.url !== uploadFile.url || item.name !== uploadFile.name
|
||||
)
|
||||
emit('update:modelValue', fileList.value)
|
||||
emit(
|
||||
'update:modelValue',
|
||||
fileList.value.map((file) => file.url!)
|
||||
)
|
||||
}
|
||||
|
||||
// 图片上传错误提示
|
||||
@@ -216,7 +232,7 @@ const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border: 1px dashed var(--el-border-color-darker);
|
||||
border-radius: v-bind(borderRadius);
|
||||
border-radius: v-bind(borderradius);
|
||||
|
||||
&:hover {
|
||||
border: 1px dashed var(--el-color-primary);
|
||||
@@ -233,7 +249,7 @@ const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
|
||||
width: v-bind(width);
|
||||
height: v-bind(height);
|
||||
background-color: transparent;
|
||||
border-radius: v-bind(borderRadius);
|
||||
border-radius: v-bind(borderradius);
|
||||
}
|
||||
|
||||
.upload-image {
|
||||
@@ -246,16 +262,16 @@ const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
|
||||
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;
|
||||
box-sizing: border-box;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.handle-icon {
|
||||
display: flex;
|
||||
|
||||
93
src/components/UploadFile/src/useUpload.ts
Normal file
93
src/components/UploadFile/src/useUpload.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import * as FileApi from '@/api/infra/file'
|
||||
import CryptoJS from 'crypto-js'
|
||||
import { UploadRawFile, UploadRequestOptions } from 'element-plus/es/components/upload/src/upload'
|
||||
import axios from 'axios'
|
||||
|
||||
export const useUpload = () => {
|
||||
// 后端上传地址
|
||||
const uploadUrl = import.meta.env.VITE_UPLOAD_URL
|
||||
// 是否使用前端直连上传
|
||||
const isClientUpload = UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE
|
||||
// 重写ElUpload上传方法
|
||||
const httpRequest = async (options: UploadRequestOptions) => {
|
||||
// 模式一:前端上传
|
||||
if (isClientUpload) {
|
||||
// 1.1 生成文件名称
|
||||
const fileName = await generateFileName(options.file)
|
||||
// 1.2 获取文件预签名地址
|
||||
const presignedInfo = await FileApi.getFilePresignedUrl(fileName)
|
||||
// 1.3 上传文件(不能使用 ElUpload 的 ajaxUpload 方法的原因:其使用的是 FormData 上传,Minio 不支持)
|
||||
return axios.put(presignedInfo.uploadUrl, options.file).then(() => {
|
||||
// 1.4. 记录文件信息到后端(异步)
|
||||
createFile(presignedInfo, fileName, options.file)
|
||||
// 通知成功,数据格式保持与后端上传的返回结果一致
|
||||
return { data: presignedInfo.url }
|
||||
})
|
||||
} else {
|
||||
// 模式二:后端上传
|
||||
// 重写 el-upload httpRequest 文件上传成功会走成功的钩子,失败走失败的钩子
|
||||
return new Promise((resolve, reject) => {
|
||||
FileApi.updateFile({ file: options.file })
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
resolve(res)
|
||||
} else {
|
||||
reject(res)
|
||||
}
|
||||
})
|
||||
.catch((res) => {
|
||||
reject(res)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
uploadUrl,
|
||||
httpRequest
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文件信息
|
||||
* @param vo 文件预签名信息
|
||||
* @param name 文件名称
|
||||
* @param file 文件
|
||||
*/
|
||||
function createFile(vo: FileApi.FilePresignedUrlRespVO, name: string, file: UploadRawFile) {
|
||||
const fileVo = {
|
||||
configId: vo.configId,
|
||||
url: vo.url,
|
||||
path: name,
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size
|
||||
}
|
||||
FileApi.createFile(fileVo)
|
||||
return fileVo
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文件名称(使用算法SHA256)
|
||||
* @param file 要上传的文件
|
||||
*/
|
||||
async function generateFileName(file: UploadRawFile) {
|
||||
// 读取文件内容
|
||||
const data = await file.arrayBuffer()
|
||||
const wordArray = CryptoJS.lib.WordArray.create(data)
|
||||
// 计算SHA256
|
||||
const sha256 = CryptoJS.SHA256(wordArray).toString()
|
||||
// 拼接后缀
|
||||
const ext = file.name.substring(file.name.lastIndexOf('.'))
|
||||
return `${sha256}${ext}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传类型
|
||||
*/
|
||||
enum UPLOAD_TYPE {
|
||||
// 客户端直接上传(只支持S3服务)
|
||||
CLIENT = 'client',
|
||||
// 客户端发送到后端上传
|
||||
SERVER = 'server'
|
||||
}
|
||||
Reference in New Issue
Block a user