|
|
@@ -0,0 +1,180 @@
|
|
|
+<template>
|
|
|
+ <div class="multi-uploader">
|
|
|
+ <!-- 上传按钮 -->
|
|
|
+ <div
|
|
|
+ class="btntext"
|
|
|
+ :style="{
|
|
|
+ width: '24px',
|
|
|
+ cursor: props.disabled ? 'not-allowed' : 'pointer',
|
|
|
+ opacity: props.disabled ? 0.5 : 1
|
|
|
+ }"
|
|
|
+ @click="selectFiles"
|
|
|
+ >
|
|
|
+ <img :src="props.imgSrc" alt="upload icon" class="custom-icon" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 隐藏文件选择框 -->
|
|
|
+ <input
|
|
|
+ ref="fileInput"
|
|
|
+ type="file"
|
|
|
+ multiple
|
|
|
+ :accept="props.accept"
|
|
|
+ style="display: none"
|
|
|
+ @change="handleFilesChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref } from 'vue'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
+import SparkMD5 from 'spark-md5'
|
|
|
+import { uploadFile } from '@/utils/request'
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ accept: String,
|
|
|
+ imgSrc: String,
|
|
|
+ disabled: { type: Boolean, default: false }
|
|
|
+})
|
|
|
+
|
|
|
+const emit = defineEmits([
|
|
|
+ 'upload-success',
|
|
|
+ 'update-fileName',
|
|
|
+ 'update-percentage',
|
|
|
+ 'upload-status'
|
|
|
+])
|
|
|
+
|
|
|
+const fileInput = ref(null)
|
|
|
+
|
|
|
+// 点击按钮触发文件选择
|
|
|
+function selectFiles() {
|
|
|
+ if (props.disabled) return
|
|
|
+ fileInput.value.click()
|
|
|
+}
|
|
|
+
|
|
|
+// 多文件选择后触发
|
|
|
+async function handleFilesChange(event) {
|
|
|
+ const files = Array.from(event.target.files)
|
|
|
+ if (files.length === 0) return
|
|
|
+
|
|
|
+ emit('upload-status', '上传中')
|
|
|
+
|
|
|
+ // 并发上传所有文件
|
|
|
+ await Promise.all(
|
|
|
+ files.map(async (file) => {
|
|
|
+ const fileName = file.name
|
|
|
+ const uuid = generateUUID()
|
|
|
+ emit('update-fileName', fileName)
|
|
|
+
|
|
|
+ try {
|
|
|
+ await uploadInChunks(file, uuid, fileName)
|
|
|
+ await mergeChunks(uuid)
|
|
|
+ emit('upload-success', { bfid: uuid, fname: fileName })
|
|
|
+ ElMessage.success(`${fileName} 上传成功`)
|
|
|
+ } catch (err) {
|
|
|
+ ElMessage.error(`${fileName} 上传失败: ${err.message}`)
|
|
|
+ emit('update-percentage', { fileName, percent: 0 })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ )
|
|
|
+
|
|
|
+ emit('upload-status', '全部上传完成')
|
|
|
+ event.target.value = ''
|
|
|
+}
|
|
|
+
|
|
|
+// 分片上传
|
|
|
+async function uploadInChunks(file, uuid, fileName) {
|
|
|
+ const chunkSize = 2 * 1024 * 1024 // 2MB
|
|
|
+ const totalChunks = Math.ceil(file.size / chunkSize)
|
|
|
+
|
|
|
+ for (let i = 0; i < totalChunks; i++) {
|
|
|
+ const start = i * chunkSize
|
|
|
+ const end = Math.min(file.size, start + chunkSize)
|
|
|
+ const chunkFile = file.slice(start, end)
|
|
|
+
|
|
|
+ const formData = new FormData()
|
|
|
+ formData.append('bfid', uuid)
|
|
|
+ formData.append('chunk', i + 1)
|
|
|
+ formData.append('chunks', totalChunks)
|
|
|
+ formData.append('file', chunkFile)
|
|
|
+ formData.append('fileName', fileName)
|
|
|
+ formData.append('transCode', 'B00028')
|
|
|
+
|
|
|
+ await uploadFile(formData, 'service', (progress) => {
|
|
|
+ const percent = Math.min(
|
|
|
+ Math.floor(((i + progress / 100) / totalChunks) * 100),
|
|
|
+ 100
|
|
|
+ )
|
|
|
+ console.log('上传进度:', percent)
|
|
|
+ emit('update-percentage', { fileName, percent })
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 合并分片
|
|
|
+async function mergeChunks(uuid) {
|
|
|
+ const formData = new FormData()
|
|
|
+ formData.append('bfid', uuid)
|
|
|
+ formData.append('transCode', 'B00029')
|
|
|
+ await uploadFile(formData, 'service')
|
|
|
+}
|
|
|
+
|
|
|
+// 生成 UUID
|
|
|
+function generateUUID() {
|
|
|
+ return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
|
+ const r = (Math.random() * 16) | 0
|
|
|
+ const v = c === 'x' ? r : (r & 0x3) | 0x8
|
|
|
+ return v.toString(16)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 计算文件 MD5(可选,用于秒传验证)
|
|
|
+function calculateMD5(file) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ const chunkSize = 2 * 1024 * 1024
|
|
|
+ const chunks = Math.ceil(file.size / chunkSize)
|
|
|
+ let currentChunk = 0
|
|
|
+ const spark = new SparkMD5.ArrayBuffer()
|
|
|
+ const fileReader = new FileReader()
|
|
|
+
|
|
|
+ fileReader.onload = function (e) {
|
|
|
+ spark.append(e.target.result)
|
|
|
+ currentChunk++
|
|
|
+ if (currentChunk < chunks) {
|
|
|
+ loadNext()
|
|
|
+ } else {
|
|
|
+ resolve(spark.end())
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ fileReader.onerror = function () {
|
|
|
+ reject(new Error('文件读取失败'))
|
|
|
+ }
|
|
|
+
|
|
|
+ function loadNext() {
|
|
|
+ const start = currentChunk * chunkSize
|
|
|
+ const end = Math.min(file.size, start + chunkSize)
|
|
|
+ fileReader.readAsArrayBuffer(file.slice(start, end))
|
|
|
+ }
|
|
|
+
|
|
|
+ loadNext()
|
|
|
+ })
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.multi-uploader {
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+.btntext {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+.custom-icon {
|
|
|
+ width: 100%;
|
|
|
+ height: auto;
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+</style>
|