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