Browse Source

上传图片控件

master
zy 2 months ago
parent
commit
c18352f65c
  1. 501
      src/components/UploadFile/src/InnerImage.vue
  2. 205
      src/components/UploadFile/src/InnerUploadImg.vue
  3. 568
      src/components/UploadFile/src/InnerUploader.vue
  4. 221
      src/components/UploadFile/src/sliceFileUPload.ts

501
src/components/UploadFile/src/InnerImage.vue

@ -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>

205
src/components/UploadFile/src/InnerUploadImg.vue

@ -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>

568
src/components/UploadFile/src/InnerUploader.vue

@ -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>

221
src/components/UploadFile/src/sliceFileUPload.ts

@ -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…
Cancel
Save