4 changed files with 1495 additions and 0 deletions
@ -0,0 +1,501 @@
|
||||
<template> |
||||
<main class="upload-container"> |
||||
<section class="file-list"> |
||||
<section |
||||
class="file" |
||||
v-for="(file, index) in fileList" |
||||
:key="file.uid" |
||||
v-if="fileList.length > 0" |
||||
> |
||||
<el-image :src="getfileUrl(file)" class="img" /> |
||||
<Icon |
||||
icon="ep:close" |
||||
class="icon close" |
||||
:size="13" |
||||
@click="remove(file, index)" |
||||
color="#fff" |
||||
/> |
||||
</section> |
||||
<section class="emty" v-else> |
||||
<Icon icon="svg-icon:customs-empty" :size="40" /> |
||||
<span>暂无图片</span> |
||||
</section> |
||||
</section> |
||||
<el-upload |
||||
:file-list="fileList" |
||||
:action="uploadUrl" |
||||
:data="uploadExtendParams" |
||||
:http-request="httpRequest" |
||||
:auto-upload="false" |
||||
:on-change="onChange" |
||||
:on-success="onSuccess" |
||||
:on-error="onError" |
||||
multiple |
||||
:limit="limit > 0 ? limit : ''" |
||||
:show-file-list="false" |
||||
:accept="accept" |
||||
ref="uploadRef" |
||||
:on-exceed="exceed" |
||||
class="flex justify-center items-center" |
||||
> |
||||
<section class="button"> |
||||
<Icon icon="svg-icon:customs-upload" :size="20" color="#00a3ff" /> |
||||
选取图片 |
||||
</section> |
||||
</el-upload> |
||||
|
||||
<section class="upload-loading" v-show="uploadLoading"> |
||||
<div class="loading"> <span></span><span></span><span></span><span></span> </div> |
||||
</section> |
||||
</main> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { useUpload } from '@/components/UploadFile/src/useUpload' |
||||
import { ElMessage, ElMessageBox } from 'element-plus' |
||||
defineOptions({ |
||||
name: 'InnerUploader' |
||||
}) |
||||
|
||||
const uploadLoading = ref(false) |
||||
|
||||
const emit = defineEmits(['handlerRemove', 'handlerSuccess', 'hanlderWriteState']) |
||||
|
||||
// 上传额外参数 |
||||
const uploadExtendParams = (file: any) => { |
||||
return { writeAble: extendParams.value[file.uid] } |
||||
} |
||||
|
||||
const props = defineProps({ |
||||
/** |
||||
* 文件列表 |
||||
* @default Array |
||||
*/ |
||||
uploadList: { |
||||
type: Array, |
||||
default: () => [] |
||||
}, |
||||
limit: { |
||||
type: Number, |
||||
default: '' |
||||
}, |
||||
/** |
||||
* 是否可已进行文件权限编辑 |
||||
* @default true |
||||
*/ |
||||
iswrite: { |
||||
type: Boolean, |
||||
default: true |
||||
}, |
||||
accept: { |
||||
type: String, |
||||
default: '*' |
||||
} |
||||
}) |
||||
|
||||
watch( |
||||
() => props.uploadList, |
||||
(newVal) => { |
||||
fileList.value = newVal |
||||
}, |
||||
{ deep: true } |
||||
) |
||||
|
||||
const fileList: any = ref([]) |
||||
|
||||
// 额外参数 |
||||
const extendParams = ref({}) |
||||
|
||||
// el-upload组件 |
||||
const uploadRef = ref() |
||||
|
||||
// 自定义上传地址与方式 |
||||
const { uploadUrl, httpRequest } = useUpload() |
||||
|
||||
// 文件状态改变 |
||||
const onChange = (file: any) => { |
||||
if (props.accept !== '*') { |
||||
if (props.accept.split(',').findIndex((i) => file.name.includes(i)) == -1) { |
||||
unref(uploadRef).handleRemove(file) |
||||
ElMessage({ message: `${file.name}文件格式不正确`, type: 'error' }) |
||||
return |
||||
} |
||||
} |
||||
if (file.status === 'ready') { |
||||
fileList.value.push(file) |
||||
} |
||||
} |
||||
|
||||
const getfileUrl = (file: any) => { |
||||
return file.attachmentPath || URL.createObjectURL(file.raw) |
||||
} |
||||
|
||||
// 上传错误的处理 |
||||
const onError = (e: any, file) => { |
||||
ElMessage({ message: `${file.name}上传失败,${e}`, type: 'error' }) |
||||
// unref(uploadRef).handleRemove(file) |
||||
} |
||||
|
||||
// 移除文件 |
||||
const remove = (file: any, index: any) => { |
||||
if (file.status === 'success') { |
||||
ElMessageBox.alert(`确认移除${file.name}吗?`, '删除', { |
||||
confirmButtonText: '确认', |
||||
showCancelButton: true, |
||||
cancelButtonText: '取消', |
||||
distinguishCancelAndClose: true, |
||||
showClose: false |
||||
}) |
||||
.then(() => { |
||||
fileList.value.splice(index, 1) |
||||
emit('handlerRemove', file) |
||||
}) |
||||
.catch(() => {}) |
||||
return |
||||
} |
||||
fileList.value.splice(index, 1) |
||||
unref(uploadRef).handleRemove(file) |
||||
} |
||||
|
||||
// 触发组件el的上传事件 |
||||
const upload = () => { |
||||
if (fileList.value.filter((file) => file.status == 'ready').length > 0) { |
||||
uploadLoading.value = true |
||||
unref(uploadRef).submit() |
||||
return |
||||
} |
||||
ElMessage('暂无可上传的文件') |
||||
} |
||||
|
||||
// 成功回调暂存 |
||||
const successMap: any = ref([]) |
||||
|
||||
// 上传成功的回调 |
||||
const onSuccess = (response: any) => { |
||||
successMap.value.push(response) |
||||
if (fileList.value.filter((file) => file.status == 'ready').length == 0) { |
||||
uploadLoading.value = false |
||||
emit('handlerSuccess', successMap.value) |
||||
} |
||||
} |
||||
|
||||
function exceed() { |
||||
ElMessage({ |
||||
message: `只允许上传${props.limit}张图片` |
||||
}) |
||||
} |
||||
|
||||
onMounted(() => { |
||||
fileList.value = props.uploadList |
||||
}) |
||||
|
||||
onBeforeUnmount(() => { |
||||
fileList.value = [] |
||||
unref(uploadRef).clearFiles() |
||||
}) |
||||
|
||||
onActivated(() => { |
||||
// console.log('onActivated') |
||||
}) |
||||
|
||||
// 在组件撒上暴露方法 |
||||
defineExpose({ |
||||
upload |
||||
}) |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
:deep(.el-upload-list__item:hover) { |
||||
background-color: transparent !important; |
||||
} |
||||
:deep(.el-upload) { |
||||
flex-flow: column; |
||||
gap: 10px; |
||||
align-items: flex-start; |
||||
} |
||||
.file-list { |
||||
width: 220px; |
||||
margin-bottom: 10px; |
||||
display: block; |
||||
border-radius: 6px; |
||||
padding: 8px 6px; |
||||
} |
||||
.file { |
||||
// width: fit-content; |
||||
margin-bottom: 5px; |
||||
// padding: 4px 10px; |
||||
display: flex; |
||||
flex-flow: row nowrap; |
||||
align-items: center; |
||||
gap: 5px; |
||||
cursor: pointer; |
||||
border-radius: 6px; |
||||
position: relative; |
||||
justify-content: center; |
||||
transition: 0.2s; |
||||
.img { |
||||
border-radius: 6px; |
||||
} |
||||
.info { |
||||
flex: 1; |
||||
overflow: hidden; |
||||
display: flex; |
||||
flex-flow: column nowrap; |
||||
.name { |
||||
font-weight: bold; |
||||
font-size: small; |
||||
letter-spacing: 1px; |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
} |
||||
.size { |
||||
font-size: 12px; |
||||
} |
||||
.time { |
||||
font-size: 12px; |
||||
} |
||||
} |
||||
|
||||
.state { |
||||
font-size: small; |
||||
} |
||||
.icon { |
||||
position: absolute; |
||||
top: 0; |
||||
right: 0; |
||||
transform: translateX(20%) translateY(-20%); |
||||
border-radius: 50%; |
||||
transition: 0.2s; |
||||
padding: 2px; |
||||
background-image: radial-gradient(circle at 60% 60%, #fcfcfb 2%, red 54%); |
||||
} |
||||
.close:hover { |
||||
// background-color: #f83f2a; |
||||
// box-shadow: 0 0 2px 0 #640404; |
||||
// filter: drop-shadow(0 0 1px #000000ab); |
||||
opacity: 0.9; |
||||
} |
||||
&:hover { |
||||
background-color: #fff; |
||||
box-shadow: 0 0 4px 0px #ccc; |
||||
border-radius: 4px; |
||||
} |
||||
.program { |
||||
position: absolute; |
||||
bottom: 0%; |
||||
// border-radius: 999px; |
||||
background-color: #89ffc2a0; |
||||
left: 0; |
||||
width: var(--p); |
||||
// width: 100%; |
||||
transition: 0.2s; |
||||
height: 100%; |
||||
} |
||||
} |
||||
.emty { |
||||
width: 100%; |
||||
display: flex; |
||||
flex-flow: column nowrap; |
||||
justify-content: center; |
||||
align-items: center; |
||||
gap: 5px; |
||||
color: #9ea3b4; |
||||
} |
||||
.button { |
||||
display: flex; |
||||
padding: 5px 16px; |
||||
color: #00a3ff; |
||||
justify-content: center; |
||||
align-items: center; |
||||
gap: 4px; |
||||
border-radius: 6px; |
||||
background: #f1faff; |
||||
} |
||||
.upload-container { |
||||
width: fit-content; |
||||
position: relative; |
||||
.upload-loading { |
||||
position: absolute; |
||||
top: 0; |
||||
left: 0; |
||||
width: 100%; |
||||
height: 100%; |
||||
background-color: transparent; |
||||
z-index: 100; |
||||
display: flex; |
||||
flex-flow: row nowrap; |
||||
align-items: center; |
||||
justify-content: center; |
||||
background-color: #ffffff84; |
||||
.loading { |
||||
--w: 13ch; |
||||
// font-weight: bold; |
||||
// font-family: monospace; |
||||
font-size: medium; |
||||
letter-spacing: var(--w); |
||||
width: var(--w); |
||||
overflow: hidden; |
||||
white-space: nowrap; |
||||
color: #0000; |
||||
text-shadow: |
||||
calc(0 * var(--w)) 0 #000, |
||||
calc(-1 * var(--w)) 0 #000, |
||||
calc(-2 * var(--w)) 0 #000, |
||||
calc(-3 * var(--w)) 0 #000, |
||||
calc(-4 * var(--w)) 0 #000, |
||||
calc(-5 * var(--w)) 0 #000, |
||||
calc(-6 * var(--w)) 0 #000, |
||||
calc(-7 * var(--w)) 0 #000, |
||||
calc(-8 * var(--w)) 0 #000, |
||||
calc(-9 * var(--w)) 0 #000; |
||||
animation: c10 2s infinite linear; |
||||
&:before { |
||||
content: '文件上传中...'; |
||||
} |
||||
|
||||
@keyframes c10 { |
||||
9.09% { |
||||
text-shadow: |
||||
calc(0 * var(--w)) -10px #000, |
||||
calc(-1 * var(--w)) 0 #000, |
||||
calc(-2 * var(--w)) 0 #000, |
||||
calc(-3 * var(--w)) 0 #000, |
||||
calc(-4 * var(--w)) 0 #000, |
||||
calc(-5 * var(--w)) 0 #000, |
||||
calc(-6 * var(--w)) 0 #000, |
||||
calc(-7 * var(--w)) 0 #000, |
||||
calc(-8 * var(--w)) 0 #000, |
||||
calc(-9 * var(--w)) 0 #000; |
||||
} |
||||
|
||||
18.18% { |
||||
text-shadow: |
||||
calc(0 * var(--w)) 0 #000, |
||||
calc(-1 * var(--w)) -10px #000, |
||||
calc(-2 * var(--w)) 0 #000, |
||||
calc(-3 * var(--w)) 0 #000, |
||||
calc(-4 * var(--w)) 0 #000, |
||||
calc(-5 * var(--w)) 0 #000, |
||||
calc(-6 * var(--w)) 0 #000, |
||||
calc(-7 * var(--w)) 0 #000, |
||||
calc(-8 * var(--w)) 0 #000, |
||||
calc(-9 * var(--w)) 0 #000; |
||||
} |
||||
|
||||
27.27% { |
||||
text-shadow: |
||||
calc(0 * var(--w)) 0 #000, |
||||
calc(-1 * var(--w)) 0 #000, |
||||
calc(-2 * var(--w)) -10px #000, |
||||
calc(-3 * var(--w)) 0 #000, |
||||
calc(-4 * var(--w)) 0 #000, |
||||
calc(-5 * var(--w)) 0 #000, |
||||
calc(-6 * var(--w)) 0 #000, |
||||
calc(-7 * var(--w)) 0 #000, |
||||
calc(-8 * var(--w)) 0 #000, |
||||
calc(-9 * var(--w)) 0 #000; |
||||
} |
||||
|
||||
36.36% { |
||||
text-shadow: |
||||
calc(0 * var(--w)) 0 #000, |
||||
calc(-1 * var(--w)) 0 #000, |
||||
calc(-2 * var(--w)) 0 #000, |
||||
calc(-3 * var(--w)) -10px #000, |
||||
calc(-4 * var(--w)) 0 #000, |
||||
calc(-5 * var(--w)) 0 #000, |
||||
calc(-6 * var(--w)) 0 #000, |
||||
calc(-7 * var(--w)) 0 #000, |
||||
calc(-8 * var(--w)) 0 #000, |
||||
calc(-9 * var(--w)) 0 #000; |
||||
} |
||||
|
||||
45.45% { |
||||
text-shadow: |
||||
calc(0 * var(--w)) 0 #000, |
||||
calc(-1 * var(--w)) 0 #000, |
||||
calc(-2 * var(--w)) 0 #000, |
||||
calc(-3 * var(--w)) 0 #000, |
||||
calc(-4 * var(--w)) -10px #000, |
||||
calc(-5 * var(--w)) 0 #000, |
||||
calc(-6 * var(--w)) 0 #000, |
||||
calc(-7 * var(--w)) 0 #000, |
||||
calc(-8 * var(--w)) 0 #000, |
||||
calc(-9 * var(--w)) 0 #000; |
||||
} |
||||
|
||||
54.54% { |
||||
text-shadow: |
||||
calc(0 * var(--w)) 0 #000, |
||||
calc(-1 * var(--w)) 0 #000, |
||||
calc(-2 * var(--w)) 0 #000, |
||||
calc(-3 * var(--w)) 0 #000, |
||||
calc(-4 * var(--w)) 0 #000, |
||||
calc(-5 * var(--w)) -10px #000, |
||||
calc(-6 * var(--w)) 0 #000, |
||||
calc(-7 * var(--w)) 0 #000, |
||||
calc(-8 * var(--w)) 0 #000, |
||||
calc(-9 * var(--w)) 0 #000; |
||||
} |
||||
|
||||
63.63% { |
||||
text-shadow: |
||||
calc(0 * var(--w)) 0 #000, |
||||
calc(-1 * var(--w)) 0 #000, |
||||
calc(-2 * var(--w)) 0 #000, |
||||
calc(-3 * var(--w)) 0 #000, |
||||
calc(-4 * var(--w)) 0 #000, |
||||
calc(-5 * var(--w)) 0 #000, |
||||
calc(-6 * var(--w)) -10px #000, |
||||
calc(-7 * var(--w)) 0 #000, |
||||
calc(-8 * var(--w)) 0 #000, |
||||
calc(-9 * var(--w)) 0 #000; |
||||
} |
||||
|
||||
72.72% { |
||||
text-shadow: |
||||
calc(0 * var(--w)) 0 #000, |
||||
calc(-1 * var(--w)) 0 #000, |
||||
calc(-2 * var(--w)) 0 #000, |
||||
calc(-3 * var(--w)) 0 #000, |
||||
calc(-4 * var(--w)) 0 #000, |
||||
calc(-5 * var(--w)) 0 #000, |
||||
calc(-6 * var(--w)) 0 #000, |
||||
calc(-7 * var(--w)) -10px #000, |
||||
calc(-8 * var(--w)) 0 #000, |
||||
calc(-9 * var(--w)) 0 #000; |
||||
} |
||||
|
||||
81.81% { |
||||
text-shadow: |
||||
calc(0 * var(--w)) 0 #000, |
||||
calc(-1 * var(--w)) 0 #000, |
||||
calc(-2 * var(--w)) 0 #000, |
||||
calc(-3 * var(--w)) 0 #000, |
||||
calc(-4 * var(--w)) 0 #000, |
||||
calc(-5 * var(--w)) 0 #000, |
||||
calc(-6 * var(--w)) 0 #000, |
||||
calc(-7 * var(--w)) 0 #000, |
||||
calc(-8 * var(--w)) -10px #000, |
||||
calc(-9 * var(--w)) 0 #000; |
||||
} |
||||
|
||||
90.90% { |
||||
text-shadow: |
||||
calc(0 * var(--w)) 0 #000, |
||||
calc(-1 * var(--w)) 0 #000, |
||||
calc(-2 * var(--w)) 0 #000, |
||||
calc(-3 * var(--w)) 0 #000, |
||||
calc(-4 * var(--w)) 0 #000, |
||||
calc(-5 * var(--w)) 0 #000, |
||||
calc(-6 * var(--w)) 0 #000, |
||||
calc(-7 * var(--w)) 0 #000, |
||||
calc(-8 * var(--w)) 0 #000, |
||||
calc(-9 * var(--w)) -10px #000; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,205 @@
|
||||
<template> |
||||
<main class="upload-container"> |
||||
<el-upload |
||||
:file-list="fileList" |
||||
:action="uploadUrl" |
||||
:http-request="httpRequest" |
||||
:auto-upload="false" |
||||
:on-success="onSuccess" |
||||
:on-error="onError" |
||||
multiple |
||||
:limit="limit" |
||||
:on-preview="handlerPerview" |
||||
:on-remove="remove" |
||||
:before-remove="beforeRemove" |
||||
list-type="picture-card" |
||||
accept=".jpg,.png,.jpeg,.gif" |
||||
:on-exceed="onExceed" |
||||
ref="uploadRef" |
||||
> |
||||
<Icon icon="ep:plus" /> |
||||
</el-upload> |
||||
<el-dialog v-model="perview.show" style="width: fit-content"> |
||||
<img w-full :src="perview.url" alt="Preview Image" style="max-height: 60vh" /> |
||||
</el-dialog> |
||||
</main> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { useUpload } from '@/components/UploadFile/src/useUpload' |
||||
import { ElMessage, ElMessageBox } from 'element-plus' |
||||
defineOptions({ |
||||
name: 'InnerUploadImg' |
||||
}) |
||||
|
||||
const emit = defineEmits(['handlerRemove', 'handlerSuccess', 'hanlderWriteState']) |
||||
|
||||
const props = defineProps({ |
||||
uploadList: { |
||||
type: Array, |
||||
default: () => [] |
||||
}, |
||||
limit: { |
||||
type: Number, |
||||
default: 3 |
||||
} |
||||
}) |
||||
|
||||
watch( |
||||
() => props.uploadList, |
||||
(newVal) => { |
||||
fileList.value = newVal |
||||
}, |
||||
{ deep: true } |
||||
) |
||||
|
||||
const fileList: any = ref([]) |
||||
|
||||
// el-upload组件 |
||||
const uploadRef = ref() |
||||
|
||||
// 自定义上传地址与方式 |
||||
const { uploadUrl, httpRequest } = useUpload() |
||||
|
||||
// 上传错误的处理 |
||||
const onError = (e: any, file) => { |
||||
ElMessage({ message: `${file.name}上传失败,${e}`, type: 'error' }) |
||||
// unref(uploadRef).handleRemove(file) |
||||
} |
||||
|
||||
// 移除文件 |
||||
const remove = (file: any) => { |
||||
emit('handlerRemove', file) |
||||
} |
||||
|
||||
// el upload 删除之前的处理 |
||||
const beforeRemove = (file) => { |
||||
return ElMessageBox.alert(`确认移除${file.name}吗?`, '删除', { |
||||
confirmButtonText: '确认', |
||||
showCancelButton: true, |
||||
cancelButtonText: '取消', |
||||
distinguishCancelAndClose: true, |
||||
showClose: false |
||||
}) |
||||
.then(() => { |
||||
return true |
||||
}) |
||||
.catch(() => { |
||||
return false |
||||
}) |
||||
} |
||||
|
||||
// 触发组件el的上传事件 |
||||
const upload = () => { |
||||
unref(uploadRef).submit() |
||||
} |
||||
|
||||
// 上传成功的回调 |
||||
const onSuccess = (response: any) => { |
||||
emit('handlerSuccess', response) |
||||
} |
||||
|
||||
// 预览控制 |
||||
const perview = reactive({ |
||||
show: false, |
||||
url: '' |
||||
}) |
||||
|
||||
// 预览事件 |
||||
const handlerPerview = (file) => { |
||||
perview.url = file.url |
||||
perview.show = true |
||||
} |
||||
|
||||
// 超出上传数量的回调 |
||||
const onExceed = () => { |
||||
ElMessage.warning(`只能上传${props.limit}张图片`) |
||||
} |
||||
|
||||
onMounted(() => { |
||||
fileList.value = props.uploadList |
||||
}) |
||||
|
||||
// 在组件撒上暴露方法 |
||||
defineExpose({ |
||||
upload |
||||
}) |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
:deep(.el-upload-list__item:hover) { |
||||
background-color: transparent !important; |
||||
} |
||||
|
||||
.file { |
||||
width: fit-content; |
||||
margin-bottom: 5px; |
||||
padding: 0px 10px; |
||||
display: flex; |
||||
flex-flow: row nowrap; |
||||
align-items: center; |
||||
gap: 10px; |
||||
cursor: pointer; |
||||
border-radius: 4px; |
||||
position: relative; |
||||
.name { |
||||
color: blue; |
||||
font-size: small; |
||||
letter-spacing: 1px; |
||||
width: 200px; |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
} |
||||
.state { |
||||
font-size: small; |
||||
} |
||||
.icon { |
||||
padding: 2px; |
||||
border-radius: 50%; |
||||
} |
||||
.edit:hover { |
||||
background-color: #46c9f7; |
||||
box-shadow: 0 0 2px 0 #144d61; |
||||
} |
||||
.close:hover { |
||||
background-color: #f83f2a; |
||||
box-shadow: 0 0 2px 0 #640404; |
||||
} |
||||
&:hover { |
||||
background-color: #fff; |
||||
box-shadow: 0 0 4px 0px #ccc; |
||||
border-radius: 4px; |
||||
} |
||||
.program { |
||||
position: absolute; |
||||
bottom: 0%; |
||||
// border-radius: 999px; |
||||
background-color: #89ffc2a0; |
||||
left: 0; |
||||
width: var(--p); |
||||
// width: 100%; |
||||
transition: 0.2s; |
||||
height: 100%; |
||||
} |
||||
} |
||||
|
||||
.upload-container { |
||||
width: fit-content; |
||||
position: relative; |
||||
.upload-loading { |
||||
position: absolute; |
||||
top: 0; |
||||
left: 0; |
||||
width: 100%; |
||||
height: 100%; |
||||
background-color: transparent; |
||||
z-index: 100; |
||||
display: flex; |
||||
flex-flow: row nowrap; |
||||
align-items: center; |
||||
justify-content: center; |
||||
background-color: #ffffff84; |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,568 @@
|
||||
<template> |
||||
<main class="upload-container"> |
||||
<section class="file-list"> |
||||
<section |
||||
class="file" |
||||
v-for="(file, index) in fileList" |
||||
:key="file.uid" |
||||
:style="setFileState(file)" |
||||
v-if="fileList.length > 0" |
||||
> |
||||
<Icon :icon="getFileIcon(file.name)" :size="40" /> |
||||
<el-tooltip class="box-item" effect="dark" :content="file.name" placement="top"> |
||||
<section class="info"> |
||||
<span class="name">{{ file.name }}</span> |
||||
<span class="time" v-if="file.id"> |
||||
{{ formatDate(file.createTime, 'YYYY年M月D日') }} |
||||
</span> |
||||
<span class="size" v-else>{{ niceBytes(file.size) }}</span> |
||||
</section> |
||||
</el-tooltip> |
||||
<!-- <span |
||||
class="state" |
||||
v-show="file.status !== 'fail' && iswrite" |
||||
> |
||||
{{ extendParams[file.uid] ? '可编辑' : '只读' }} |
||||
<Icon :icon="extendParams[file.uid] ? 'ep:edit-pen' : 'ep:view'" /> |
||||
</span> --> |
||||
<Icon |
||||
:icon="extendParams[file.uid] ? 'ep:edit-pen' : 'ep:view'" |
||||
class="icon edit" |
||||
:size="15" |
||||
@click="changeFileState(file)" |
||||
v-show="file.status !== 'fail' && iswrite" |
||||
/> |
||||
<Icon icon="ep:delete" class="icon close" :size="15" @click="remove(file, index)" /> |
||||
<section class="program" :style="{ '--p': `${processData[file.uid]}%` }"> </section> |
||||
</section> |
||||
<section class="emty" v-else> |
||||
<Icon icon="svg-icon:customs-empty" :size="40" /> |
||||
<span>暂无文件</span> |
||||
</section> |
||||
</section> |
||||
<el-upload |
||||
:file-list="fileList" |
||||
:action="uploadUrl" |
||||
:data="uploadExtendParams" |
||||
:http-request="httpRequest" |
||||
:auto-upload="false" |
||||
:on-change="onChange" |
||||
:on-success="onSuccess" |
||||
:on-error="onError" |
||||
:on-progress="onProgress" |
||||
multiple |
||||
:limit="limit > 0 ? limit : ''" |
||||
:show-file-list="false" |
||||
:accept="accept" |
||||
ref="uploadRef" |
||||
:on-exceed="exceed" |
||||
> |
||||
<section class="upload-button-group"> |
||||
<section class="upload-button mr10px"> |
||||
<Icon icon="svg-icon:customs-upload" :size="18" class="mr-5px" /> |
||||
选取文件 |
||||
</section> |
||||
<section class="button" v-if="showUploadButton" @click.stop="upload"> |
||||
<Icon icon="ep:upload-filled" :size="18" class="mr-5px" /> |
||||
上传 |
||||
</section> |
||||
</section> |
||||
</el-upload> |
||||
|
||||
<section class="upload-loading" v-show="uploadLoading"> |
||||
<div class="loading"> <span></span><span></span><span></span><span></span> </div> |
||||
</section> |
||||
</main> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { useUpload } from '@/components/UploadFile/src/useUpload' |
||||
import { getFileIcon, niceBytes } from '@/utils/filestate' |
||||
import { formatDate } from '@/utils/formatTime' |
||||
import { ElMessage, ElMessageBox } from 'element-plus' |
||||
defineOptions({ |
||||
name: 'InnerUploader' |
||||
}) |
||||
|
||||
const uploadLoading = ref(false) |
||||
|
||||
const emit = defineEmits(['handlerRemove', 'handlerSuccess', 'hanlderWriteState']) |
||||
|
||||
// 上传额外参数 |
||||
const uploadExtendParams = (file: any) => { |
||||
return { writeAble: extendParams.value[file.uid] } |
||||
} |
||||
|
||||
const props = defineProps({ |
||||
/** |
||||
* 文件列表 |
||||
* @default Array |
||||
*/ |
||||
uploadList: { |
||||
type: Array, |
||||
default: () => [] |
||||
}, |
||||
limit: { |
||||
type: Number, |
||||
default: '' |
||||
}, |
||||
/** |
||||
* 是否可已进行文件权限编辑 |
||||
* @default true |
||||
*/ |
||||
iswrite: { |
||||
type: Boolean, |
||||
default: true |
||||
}, |
||||
accept: { |
||||
type: String, |
||||
default: '*' |
||||
}, |
||||
showUploadButton: { |
||||
type: Boolean, |
||||
default: false |
||||
} |
||||
}) |
||||
|
||||
watch( |
||||
() => props.uploadList, |
||||
(newVal) => { |
||||
fileList.value = newVal |
||||
}, |
||||
{ deep: true } |
||||
) |
||||
|
||||
const fileList: any = ref([]) |
||||
|
||||
// 额外参数 |
||||
const extendParams = ref({}) |
||||
|
||||
// el-upload组件 |
||||
const uploadRef = ref() |
||||
|
||||
// 自定义上传地址与方式 |
||||
const { uploadUrl, httpRequest } = useUpload() |
||||
|
||||
// 文件状态改变 |
||||
const onChange = (file: any) => { |
||||
// console.log(Math.round(file.raw.size / 1024 / 1024) + 'MB') |
||||
if (props.accept !== '*') { |
||||
if (props.accept.split(',').findIndex((i) => file.name.includes(i)) == -1) { |
||||
unref(uploadRef).handleRemove(file) |
||||
ElMessage({ message: `${file.name}文件格式不正确`, type: 'error' }) |
||||
return |
||||
} |
||||
} |
||||
if (file.status === 'ready') { |
||||
extendParams.value[file.uid] = false |
||||
fileList.value.push(file) |
||||
} |
||||
} |
||||
|
||||
// 更改文件属性 |
||||
const changeFileState = (file: any) => { |
||||
extendParams.value[file.uid] = !extendParams.value[file.uid] |
||||
if (file.status === 'success') { |
||||
emit('hanlderWriteState', file) |
||||
} |
||||
} |
||||
|
||||
// 上传错误的处理 |
||||
const onError = (e: any, file) => { |
||||
ElMessage({ message: `${file.name}上传失败,${e}`, type: 'error' }) |
||||
// unref(uploadRef).handleRemove(file) |
||||
} |
||||
|
||||
// 移除文件 |
||||
const remove = (file: any, index: any) => { |
||||
if (file.status === 'success') { |
||||
ElMessageBox.alert(`确认移除${file.name}吗?`, '删除', { |
||||
confirmButtonText: '确认', |
||||
showCancelButton: true, |
||||
cancelButtonText: '取消', |
||||
distinguishCancelAndClose: true, |
||||
showClose: false |
||||
}) |
||||
.then(() => { |
||||
fileList.value.splice(index, 1) |
||||
emit('handlerRemove', file) |
||||
}) |
||||
.catch(() => {}) |
||||
return |
||||
} |
||||
fileList.value.splice(index, 1) |
||||
unref(uploadRef).handleRemove(file) |
||||
} |
||||
|
||||
// 触发组件el的上传事件 |
||||
const upload = () => { |
||||
if (fileList.value.filter((file) => file.status == 'ready').length > 0) { |
||||
uploadLoading.value = true |
||||
unref(uploadRef).submit() |
||||
return |
||||
} |
||||
ElMessage('暂无可上传的文件') |
||||
} |
||||
|
||||
// 成功回调暂存 |
||||
const successMap: any = ref([]) |
||||
|
||||
// 上传成功的回调 |
||||
const onSuccess = (response: any) => { |
||||
delete processData.value[response.uid] |
||||
successMap.value.push(response) |
||||
if (fileList.value.filter((file) => file.status == 'ready').length == 0) { |
||||
uploadLoading.value = false |
||||
emit('handlerSuccess', successMap.value) |
||||
} |
||||
} |
||||
|
||||
// 设置不同状态文件的背景 |
||||
const setFileState = (file: any) => { |
||||
const colorlist = { |
||||
success: '#89ffc3', |
||||
fail: '#ffd2d2' |
||||
} |
||||
return { backgroundColor: colorlist[file.status] } |
||||
} |
||||
|
||||
// 进度条对象 |
||||
const processData = ref({}) |
||||
// 上传进度 |
||||
const onProgress = (UploadProgressEvent, uploadFile) => { |
||||
// console.log('UploadProgressEvent', UploadProgressEvent) |
||||
processData.value[uploadFile.uid] = UploadProgressEvent.percent |
||||
} |
||||
|
||||
function exceed() { |
||||
ElMessage({ |
||||
message: `只允许上传${props.limit}个文件` |
||||
}) |
||||
} |
||||
|
||||
onMounted(() => { |
||||
fileList.value = props.uploadList |
||||
}) |
||||
|
||||
onBeforeUnmount(() => { |
||||
fileList.value = [] |
||||
unref(uploadRef).clearFiles() |
||||
}) |
||||
|
||||
onActivated(() => { |
||||
console.log('onActivated') |
||||
}) |
||||
|
||||
// 在组件撒上暴露方法 |
||||
defineExpose({ |
||||
upload |
||||
}) |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
* { |
||||
box-sizing: border-box; |
||||
} |
||||
:deep(.el-upload-list__item:hover) { |
||||
background-color: transparent !important; |
||||
} |
||||
:deep(.el-upload) { |
||||
flex-flow: column; |
||||
gap: 10px; |
||||
align-items: flex-start; |
||||
} |
||||
.file-list { |
||||
width: 220px; |
||||
max-height: 60vh; |
||||
overflow-y: scroll; |
||||
// margin: 10px 0; |
||||
margin-bottom: 10px; |
||||
display: block; |
||||
border-radius: 6px; |
||||
border: 1px solid #eff2f5; |
||||
background: #fcfdfd; |
||||
padding: 8px 6px; |
||||
} |
||||
.file { |
||||
// width: fit-content; |
||||
margin-bottom: 2px; |
||||
padding: 4px 10px; |
||||
display: flex; |
||||
flex-flow: row nowrap; |
||||
align-items: center; |
||||
gap: 5px; |
||||
cursor: pointer; |
||||
border-radius: 6px; |
||||
position: relative; |
||||
justify-content: center; |
||||
transition: 0.2s; |
||||
.info { |
||||
flex: 1; |
||||
overflow: hidden; |
||||
display: flex; |
||||
flex-flow: column nowrap; |
||||
.name { |
||||
font-weight: bold; |
||||
font-size: small; |
||||
letter-spacing: 1px; |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
} |
||||
.size { |
||||
font-size: 12px; |
||||
} |
||||
.time { |
||||
font-size: 12px; |
||||
} |
||||
} |
||||
|
||||
.state { |
||||
font-size: small; |
||||
} |
||||
.icon { |
||||
padding: 2px; |
||||
border-radius: 50%; |
||||
transition: 0.2s; |
||||
} |
||||
// .edit:hover { |
||||
// background-color: #46c9f7; |
||||
// box-shadow: 0 0 2px 0 #144d61; |
||||
// } |
||||
.close:hover, |
||||
.edit:hover { |
||||
// background-color: #f83f2a; |
||||
// box-shadow: 0 0 2px 0 #640404; |
||||
filter: drop-shadow(0 0 1px #000000ab); |
||||
} |
||||
&:hover { |
||||
background-color: #fff; |
||||
box-shadow: 0 0 4px 0px #ccc; |
||||
border-radius: 4px; |
||||
} |
||||
.program { |
||||
position: absolute; |
||||
bottom: 0%; |
||||
// border-radius: 999px; |
||||
background-color: #89ffc2a0; |
||||
left: 0; |
||||
width: var(--p); |
||||
// width: 100%; |
||||
transition: 0.2s; |
||||
height: 100%; |
||||
} |
||||
} |
||||
.emty { |
||||
width: 100%; |
||||
display: flex; |
||||
flex-flow: column nowrap; |
||||
justify-content: center; |
||||
align-items: center; |
||||
gap: 5px; |
||||
color: #9ea3b4; |
||||
} |
||||
.upload-button-group { |
||||
display: flex; |
||||
flex-flow: row nowrap; |
||||
} |
||||
.upload-button { |
||||
display: flex; |
||||
padding: 5px 10px; |
||||
color: #00a3ff; |
||||
justify-content: center; |
||||
align-items: center; |
||||
gap: 4px; |
||||
border-radius: 6px; |
||||
background: #f1faff; |
||||
border: 1px dashed #00a3ff; |
||||
&:hover { |
||||
border: 1px solid #00a3ff; |
||||
} |
||||
} |
||||
.upload-container { |
||||
width: fit-content; |
||||
position: relative; |
||||
.upload-loading { |
||||
position: absolute; |
||||
top: 0; |
||||
left: 0; |
||||
width: 100%; |
||||
height: 100%; |
||||
background-color: transparent; |
||||
z-index: 100; |
||||
display: flex; |
||||
flex-flow: row nowrap; |
||||
align-items: center; |
||||
justify-content: center; |
||||
background-color: #ffffff84; |
||||
.loading { |
||||
--w: 13ch; |
||||
// font-weight: bold; |
||||
// font-family: monospace; |
||||
font-size: medium; |
||||
letter-spacing: var(--w); |
||||
width: var(--w); |
||||
overflow: hidden; |
||||
white-space: nowrap; |
||||
color: #0000; |
||||
text-shadow: |
||||
calc(0 * var(--w)) 0 #000, |
||||
calc(-1 * var(--w)) 0 #000, |
||||
calc(-2 * var(--w)) 0 #000, |
||||
calc(-3 * var(--w)) 0 #000, |
||||
calc(-4 * var(--w)) 0 #000, |
||||
calc(-5 * var(--w)) 0 #000, |
||||
calc(-6 * var(--w)) 0 #000, |
||||
calc(-7 * var(--w)) 0 #000, |
||||
calc(-8 * var(--w)) 0 #000, |
||||
calc(-9 * var(--w)) 0 #000; |
||||
animation: c10 2s infinite linear; |
||||
&:before { |
||||
content: '文件上传中...'; |
||||
} |
||||
|
||||
@keyframes c10 { |
||||
9.09% { |
||||
text-shadow: |
||||
calc(0 * var(--w)) -10px #000, |
||||
calc(-1 * var(--w)) 0 #000, |
||||
calc(-2 * var(--w)) 0 #000, |
||||
calc(-3 * var(--w)) 0 #000, |
||||
calc(-4 * var(--w)) 0 #000, |
||||
calc(-5 * var(--w)) 0 #000, |
||||
calc(-6 * var(--w)) 0 #000, |
||||
calc(-7 * var(--w)) 0 #000, |
||||
calc(-8 * var(--w)) 0 #000, |
||||
calc(-9 * var(--w)) 0 #000; |
||||
} |
||||
|
||||
18.18% { |
||||
text-shadow: |
||||
calc(0 * var(--w)) 0 #000, |
||||
calc(-1 * var(--w)) -10px #000, |
||||
calc(-2 * var(--w)) 0 #000, |
||||
calc(-3 * var(--w)) 0 #000, |
||||
calc(-4 * var(--w)) 0 #000, |
||||
calc(-5 * var(--w)) 0 #000, |
||||
calc(-6 * var(--w)) 0 #000, |
||||
calc(-7 * var(--w)) 0 #000, |
||||
calc(-8 * var(--w)) 0 #000, |
||||
calc(-9 * var(--w)) 0 #000; |
||||
} |
||||
|
||||
27.27% { |
||||
text-shadow: |
||||
calc(0 * var(--w)) 0 #000, |
||||
calc(-1 * var(--w)) 0 #000, |
||||
calc(-2 * var(--w)) -10px #000, |
||||
calc(-3 * var(--w)) 0 #000, |
||||
calc(-4 * var(--w)) 0 #000, |
||||
calc(-5 * var(--w)) 0 #000, |
||||
calc(-6 * var(--w)) 0 #000, |
||||
calc(-7 * var(--w)) 0 #000, |
||||
calc(-8 * var(--w)) 0 #000, |
||||
calc(-9 * var(--w)) 0 #000; |
||||
} |
||||
|
||||
36.36% { |
||||
text-shadow: |
||||
calc(0 * var(--w)) 0 #000, |
||||
calc(-1 * var(--w)) 0 #000, |
||||
calc(-2 * var(--w)) 0 #000, |
||||
calc(-3 * var(--w)) -10px #000, |
||||
calc(-4 * var(--w)) 0 #000, |
||||
calc(-5 * var(--w)) 0 #000, |
||||
calc(-6 * var(--w)) 0 #000, |
||||
calc(-7 * var(--w)) 0 #000, |
||||
calc(-8 * var(--w)) 0 #000, |
||||
calc(-9 * var(--w)) 0 #000; |
||||
} |
||||
|
||||
45.45% { |
||||
text-shadow: |
||||
calc(0 * var(--w)) 0 #000, |
||||
calc(-1 * var(--w)) 0 #000, |
||||
calc(-2 * var(--w)) 0 #000, |
||||
calc(-3 * var(--w)) 0 #000, |
||||
calc(-4 * var(--w)) -10px #000, |
||||
calc(-5 * var(--w)) 0 #000, |
||||
calc(-6 * var(--w)) 0 #000, |
||||
calc(-7 * var(--w)) 0 #000, |
||||
calc(-8 * var(--w)) 0 #000, |
||||
calc(-9 * var(--w)) 0 #000; |
||||
} |
||||
|
||||
54.54% { |
||||
text-shadow: |
||||
calc(0 * var(--w)) 0 #000, |
||||
calc(-1 * var(--w)) 0 #000, |
||||
calc(-2 * var(--w)) 0 #000, |
||||
calc(-3 * var(--w)) 0 #000, |
||||
calc(-4 * var(--w)) 0 #000, |
||||
calc(-5 * var(--w)) -10px #000, |
||||
calc(-6 * var(--w)) 0 #000, |
||||
calc(-7 * var(--w)) 0 #000, |
||||
calc(-8 * var(--w)) 0 #000, |
||||
calc(-9 * var(--w)) 0 #000; |
||||
} |
||||
|
||||
63.63% { |
||||
text-shadow: |
||||
calc(0 * var(--w)) 0 #000, |
||||
calc(-1 * var(--w)) 0 #000, |
||||
calc(-2 * var(--w)) 0 #000, |
||||
calc(-3 * var(--w)) 0 #000, |
||||
calc(-4 * var(--w)) 0 #000, |
||||
calc(-5 * var(--w)) 0 #000, |
||||
calc(-6 * var(--w)) -10px #000, |
||||
calc(-7 * var(--w)) 0 #000, |
||||
calc(-8 * var(--w)) 0 #000, |
||||
calc(-9 * var(--w)) 0 #000; |
||||
} |
||||
|
||||
72.72% { |
||||
text-shadow: |
||||
calc(0 * var(--w)) 0 #000, |
||||
calc(-1 * var(--w)) 0 #000, |
||||
calc(-2 * var(--w)) 0 #000, |
||||
calc(-3 * var(--w)) 0 #000, |
||||
calc(-4 * var(--w)) 0 #000, |
||||
calc(-5 * var(--w)) 0 #000, |
||||
calc(-6 * var(--w)) 0 #000, |
||||
calc(-7 * var(--w)) -10px #000, |
||||
calc(-8 * var(--w)) 0 #000, |
||||
calc(-9 * var(--w)) 0 #000; |
||||
} |
||||
|
||||
81.81% { |
||||
text-shadow: |
||||
calc(0 * var(--w)) 0 #000, |
||||
calc(-1 * var(--w)) 0 #000, |
||||
calc(-2 * var(--w)) 0 #000, |
||||
calc(-3 * var(--w)) 0 #000, |
||||
calc(-4 * var(--w)) 0 #000, |
||||
calc(-5 * var(--w)) 0 #000, |
||||
calc(-6 * var(--w)) 0 #000, |
||||
calc(-7 * var(--w)) 0 #000, |
||||
calc(-8 * var(--w)) -10px #000, |
||||
calc(-9 * var(--w)) 0 #000; |
||||
} |
||||
|
||||
90.90% { |
||||
text-shadow: |
||||
calc(0 * var(--w)) 0 #000, |
||||
calc(-1 * var(--w)) 0 #000, |
||||
calc(-2 * var(--w)) 0 #000, |
||||
calc(-3 * var(--w)) 0 #000, |
||||
calc(-4 * var(--w)) 0 #000, |
||||
calc(-5 * var(--w)) 0 #000, |
||||
calc(-6 * var(--w)) 0 #000, |
||||
calc(-7 * var(--w)) 0 #000, |
||||
calc(-8 * var(--w)) 0 #000, |
||||
calc(-9 * var(--w)) -10px #000; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,221 @@
|
||||
import SparkMD5 from 'spark-md5' |
||||
import * as FileApi from '@/api/infra/file' |
||||
import axios from 'axios' |
||||
import Queue from 'promise-queue-plus' |
||||
|
||||
/** |
||||
* 文件分片上传 |
||||
* @param options 文件上传参数 |
||||
* { |
||||
* identifier:文件唯一标识, |
||||
* option:文件对象, |
||||
* configId:文件上传配置id, |
||||
* url:文件上传地址, |
||||
* path:文件名称, |
||||
* name:文件名称, |
||||
* size:文件大小, |
||||
* file:文件 |
||||
* } |
||||
*/ |
||||
export const sliceFileUpload = async (options: any) => { |
||||
const file = options.file |
||||
const task = await getTaskInfo(options) |
||||
if (task) { |
||||
const { finished, path, taskRecord } = task |
||||
const { fileIdentifier: identifier } = taskRecord |
||||
if (finished) { |
||||
return path |
||||
} else { |
||||
const errorList: any = await handleUpload(file, taskRecord, options) |
||||
if (errorList.length > 0) { |
||||
return |
||||
} |
||||
return await FileApi.merge({ |
||||
identifier, |
||||
configId: options.configId, |
||||
path: options.fileName, |
||||
name: options.fileName, |
||||
url: options.url, |
||||
size: file.size, |
||||
chunkSize: DEFAULT_SIZE |
||||
}) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 文件上传分块任务的队列(用于移除文件时,停止该文件的上传队列) key:fileUid value: queue object
|
||||
const fileUploadChunkQueue = ref({}).value |
||||
|
||||
// 分片大小
|
||||
const DEFAULT_SIZE = 100 * 1024 * 1024 |
||||
|
||||
// 分片加密
|
||||
const md5 = (file: any, chunkSize = DEFAULT_SIZE) => { |
||||
return new Promise((resolve, reject) => { |
||||
const startMs = new Date().getTime() |
||||
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice |
||||
const chunks = Math.ceil(file.size / chunkSize) |
||||
let currentChunk = 0 |
||||
const spark = new SparkMD5.ArrayBuffer() //追加数组缓冲区。
|
||||
const fileReader = new FileReader() //读取文件
|
||||
fileReader.onload = function (e: any) { |
||||
spark.append(e.target.result) |
||||
currentChunk++ |
||||
if (currentChunk < chunks) { |
||||
loadNext() |
||||
} else { |
||||
const md5 = spark.end() //完成md5的计算,返回十六进制结果。
|
||||
// console.log('文件md5计算结束,总耗时:', (new Date().getTime() - startMs) / 1000, 's')
|
||||
resolve(md5) |
||||
} |
||||
} |
||||
fileReader.onerror = function (e) { |
||||
reject(e) |
||||
} |
||||
|
||||
function loadNext() { |
||||
// console.log('当前part number:', currentChunk, '总块数:', chunks)
|
||||
const start = currentChunk * chunkSize |
||||
let end = start + chunkSize |
||||
end > file.size && (end = file.size) |
||||
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)) |
||||
} |
||||
loadNext() |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* 获取一个上传任务,没有则初始化一个 |
||||
*/ |
||||
const getTaskInfo = async (option: any) => { |
||||
let task |
||||
const identifier = await md5(option.file) |
||||
const initTaskData = { |
||||
identifier, |
||||
path: option.fileName, |
||||
name: option.fileName, |
||||
url: option.url, |
||||
size: option.file.size, |
||||
configId: option.configId, |
||||
chunkSize: DEFAULT_SIZE |
||||
} |
||||
const res = await FileApi.taskInfo(initTaskData) |
||||
task = res |
||||
if (!task) { |
||||
const initres = await FileApi.initTask(initTaskData) |
||||
task = initres |
||||
} |
||||
return task |
||||
} |
||||
|
||||
/** |
||||
* 上传逻辑处理,如果文件已经上传完成(完成分块合并操作),则不会进入到此方法中 |
||||
*/ |
||||
const handleUpload = (file, taskRecord, options) => { |
||||
let lastUploadedSize = 0 // 上次断点续传时上传的总大小
|
||||
let uploadedSize = 0 // 已上传的大小
|
||||
const totalSize = file.size || 0 // 文件总大小
|
||||
const startMs = new Date().getTime() // 开始上传的时间
|
||||
const { exitPartList, chunkSize, chunkNum, fileIdentifier } = taskRecord |
||||
|
||||
// 获取从开始上传到现在的平均速度(byte/s)
|
||||
const getSpeed = () => { |
||||
// 已上传的总大小 - 上次上传的总大小(断点续传)= 本次上传的总大小(byte)
|
||||
const intervalSize = uploadedSize - lastUploadedSize |
||||
const nowMs = new Date().getTime() |
||||
// 时间间隔(s)
|
||||
const intervalTime = (nowMs - startMs) / 1000 |
||||
return intervalSize / intervalTime |
||||
} |
||||
|
||||
const uploadNext = async (partNumber) => { |
||||
const start = Number(chunkSize) * (partNumber - 1) |
||||
const end = start + Number(chunkSize) |
||||
const blob = file.slice(start, end) |
||||
const data = await FileApi.preSignUrl({ |
||||
identifier: fileIdentifier, |
||||
partNumber: partNumber, |
||||
configId: options.configId, |
||||
path: options.fileName, |
||||
name: options.fileName, |
||||
size: blob.size, |
||||
chunkSize: DEFAULT_SIZE |
||||
}) |
||||
if (data) { |
||||
console.log('上传地址:', data) |
||||
await axios.request({ |
||||
url: data, |
||||
method: 'PUT', |
||||
data: blob, |
||||
headers: { |
||||
'Content-Type': 'application/octet-stream' |
||||
} |
||||
}) |
||||
return Promise.resolve({ |
||||
partNumber: partNumber, |
||||
uploadedSize: blob.size |
||||
}) |
||||
} |
||||
return Promise.reject(`分片${partNumber}, 获取上传地址失败`) |
||||
} |
||||
|
||||
/** |
||||
* 更新上传进度 |
||||
* @param increment 为已上传的进度增加的字节量 |
||||
*/ |
||||
const updateProcess = (increment) => { |
||||
increment = new Number(increment) |
||||
const { onProgress } = options |
||||
const factor = 1000 // 每次增加1000 byte
|
||||
let from = 0 |
||||
// 通过循环一点一点的增加进度
|
||||
while (from <= increment) { |
||||
from += factor |
||||
uploadedSize += factor |
||||
const percent = Math.round((uploadedSize / totalSize) * 100).toFixed(2) |
||||
onProgress({ percent: percent }) |
||||
} |
||||
|
||||
const speed = getSpeed() |
||||
const remainingTime = speed != 0 ? Math.ceil((totalSize - uploadedSize) / speed) + 's' : '未知' |
||||
console.log('剩余大小:', (totalSize - uploadedSize) / 1024 / 1024, 'mb') |
||||
console.log('当前速度:', (speed / 1024 / 1024).toFixed(2), 'mbps') |
||||
console.log('预计完成:', remainingTime) |
||||
} |
||||
|
||||
return new Promise((resolve) => { |
||||
const failArr: any = [] |
||||
const queue = Queue(5, { |
||||
retry: 3, //Number of retries
|
||||
retryIsJump: false, //retry now?
|
||||
workReject: function (reason: any, queue) { |
||||
failArr.push(reason) |
||||
}, |
||||
queueEnd: function (queue) { |
||||
resolve(failArr) |
||||
} |
||||
}) |
||||
fileUploadChunkQueue[file.uid] = queue |
||||
for (let partNumber = 1; partNumber <= chunkNum; partNumber++) { |
||||
const exitPart = (exitPartList || []).find((exitPart) => exitPart.partNumber == partNumber) |
||||
if (exitPart) { |
||||
// 分片已上传完成,累计到上传完成的总额中,同时记录一下上次断点上传的大小,用于计算上传速度
|
||||
lastUploadedSize += Number(exitPart.size) |
||||
updateProcess(exitPart.size) |
||||
} else { |
||||
queue.push(() => |
||||
uploadNext(partNumber).then((res) => { |
||||
// 单片文件上传完成再更新上传进度
|
||||
updateProcess(res.uploadedSize) |
||||
}) |
||||
) |
||||
} |
||||
} |
||||
if (queue.getLength() == 0) { |
||||
// 所有分片都上传完,但未合并,直接return出去,进行合并操作
|
||||
resolve(failArr) |
||||
return |
||||
} |
||||
queue.start() |
||||
}) |
||||
} |
Loading…
Reference in new issue