| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760 |
- <template>
- <!-- 上传组件 -->
- <uploader
- class="uploader-app"
- ref="uploaderRef"
- :options="options"
- :autoStart="false"
- :fileStatusText="fileStatusText"
- @files-added="handleFilesAdded"
- @file-success="handleFileSuccess"
- @file-error="handleFileError"
- @dragleave="hideUploadMask"
- @file-progress="handleFileProgress"
- >
- <uploader-unsupport></uploader-unsupport>
- <!-- 选择按钮 在这里隐藏 -->
- <uploader-btn class="select-file-btn" :attrs="attrs" ref="uploadBtn"> 选择文件 </uploader-btn>
- <uploader-btn class="select-file-btn" :attrs="attrs" :directory="true" ref="uploadDirBtn"> 选择目录 </uploader-btn>
- <!-- 拖拽上传 -->
- <uploader-drop class="drop-box" id="dropBox" @paste="handlePaste" @click="handleUpload">
- <span class="text"> 点击上传或将文件拖拽至此区域上传 </span>
- <p class="text">按住Ctrl可同时多选,支持上传PPT/excel/pdf/mp4/zip/rar,等单个文件不能超过2G</p>
- <UploadOutlined class="upload-icon" v-show="pasteImg.src" @click="handleUploadPasteImg" />
- </uploader-drop>
- <!-- 上传列表 -->
- <uploader-list v-show="true">
- <template #default="props">
- <div class="file-panel">
- <div class="file-title">
- <span class="title-span">
- 上传列表 <span class="count">({{ props.fileList.length }})</span>
- </span>
- </div>
- <transition name="collapse">
- <ul class="file-list" v-show="!collapse">
- <li
- v-for="file in props.fileList"
- :key="file.id"
- class="file-item"
- :class="{ 'custom-status-item': file.statusStr !== '' }"
- >
- <uploader-file ref="fileItem" :file="file" :list="true" />
- <span class="custom-status">{{ file.statusStr }}</span>
- <!-- 添加剩余时间显示 -->
- <!-- <span class="remaining-time" v-if="file.remainingTime"> 剩余: {{ file.remainingTime }} </span> -->
- </li>
- <div class="no-file" v-if="!props.fileList.length"><FileExclamationOutlined /> 暂无待上传文件</div>
- </ul>
- </transition>
- </div>
- </template>
- </uploader-list>
- </uploader>
- </template>
- <script setup>
- import { ref, reactive, computed, nextTick, getCurrentInstance } from 'vue'
- import { message } from 'ant-design-vue'
- import { useMyResourceStore } from '@/store/myResource'
- import SparkMD5 from 'spark-md5'
- import tool from '@/utils/tool'
- const props = defineProps({
- visible: {
- type: Boolean,
- default: true
- }
- })
- const emit = defineEmits(['update:visible', 'success'])
- const { proxy } = getCurrentInstance()
- const store = useMyResourceStore()
- // refs
- const formRef = ref(null)
- const uploaderRef = ref(null)
- const uploadBtn = ref(null)
- const uploadDirBtn = ref(null)
- const fileItem = ref(null)
- // 表单数据
- const formState = reactive({
- title: '',
- description: '',
- category: undefined,
- fileIds: []
- })
- const rules = {
- title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
- category: [{ required: true, message: '请选择分类', trigger: 'change' }]
- }
- // 上传相关数据
- const options = ref({
- target: `${proxy.$RESOURCE_CONFIG.baseContext}/resourceFile/uploadfile`,
- chunkSize: 1024 * 1024,
- fileParameterName: 'file',
- maxChunkRetries: 3,
- simultaneousUploads: 5, // 控制并发上传数
- testChunks: true,
- checkChunkUploadedByResponse: (chunk, message) => {
- let objMessage = JSON.parse(message)
- if (objMessage.success) {
- let data = objMessage.data
- if (data.skipUpload) {
- return true
- }
- return (data.uploaded || []).indexOf(chunk.offset + 1) >= 0
- } else {
- return true
- }
- },
- headers: {
- token: tool.data.get('TOKEN')
- },
- query: () => {}
- })
- const fileStatusText = ref({
- success: '上传成功',
- error: 'error',
- uploading: '上传中',
- paused: '暂停中',
- waiting: '等待中'
- })
- const attrs = ref({
- accept: '.doc,.docx,.ppt,.pptx,.xls,.xlsx,.mkv,.mp4,.wmv,.avi,.flv,.mpeg,.mpg,.rmvb,.mov'
- // accept: '*'
- })
- const dropBoxShow = ref(false)
- const pasteImg = ref({
- src: '',
- name: ''
- })
- const pasteImgObj = ref(null)
- const filesLength = ref(0)
- const uploadStatus = ref({})
- const submitting = ref(false)
- const canClose = ref(true)
- // 计算属性
- const uploaderInstance = computed(() => {
- return uploaderRef.value?.uploader
- })
- const remainderStorageValue = computed(() => {
- return store.remainderStorageValue
- })
- // 方法
- const hideUploadMask = (e) => {
- e.stopPropagation()
- e.preventDefault()
- dropBoxShow.value = false
- }
- const handleUpload = () => {
- if (uploadBtn.value?.$el) {
- uploadBtn.value.$el.click()
- }
- }
- const handleUploadDir = () => {
- if (uploadDirBtn.value?.$el) {
- uploadDirBtn.value.$el.click()
- }
- }
- const handlePasteUpload = () => {
- pasteImg.value.src = ''
- pasteImg.value.name = ''
- pasteImgObj.value = null
- dropBoxShow.value = true
- }
- const handlePaste = (event) => {
- let pasteItems = (event.clipboardData || window.clipboardData).items
- if (pasteItems && pasteItems.length) {
- let imgObj = pasteItems[0].getAsFile()
- pasteImgObj.value =
- imgObj !== null
- ? new File([imgObj], `qiwenshare_${new Date().valueOf()}.${imgObj.name.split('.')[1]}`, { type: imgObj.type })
- : null
- } else {
- message.error('当前浏览器不支持')
- return false
- }
- if (!pasteImgObj.value) {
- message.error('粘贴内容非图片')
- return false
- }
- pasteImg.value.name = pasteImgObj.value.name
- let reader = new FileReader()
- reader.onload = (event) => {
- pasteImg.value.src = event.target.result
- }
- reader.readAsDataURL(pasteImgObj.value)
- }
- const handleUploadPasteImg = () => {
- uploaderInstance.value.addFile(pasteImgObj.value)
- }
- const handleDeletePasteImg = () => {
- pasteImg.value.src = ''
- pasteImg.value.name = ''
- pasteImgObj.value = null
- }
- const handleFilesAdded = (filesSource) => {
- console.log('handleFilesAdded', filesSource)
- const filesTotalSize = filesSource
- .map((item) => {
- console.log(item, 'itemitemitemitem')
- // 为每个文件添加上传速度跟踪属性
- item.speed = 0
- item.lastLoaded = item.loaded || 0
- item.lastTime = Date.now()
- item.remainingTime = '计算中...'
- return item
- })
- .reduce((pre, next) => {
- return pre + next.size
- }, 0)
- console.log('handleFilesAdded', filesSource)
- if (remainderStorageValue.value < filesTotalSize) {
- // 批量选择的文件超出剩余存储空间
- message.warning(`剩余存储空间不足,请重新选择${filesSource.length > 1 ? '批量' : ''}文件`)
- filesSource.ignored = true // 本次选择的文件过滤掉
- } else {
- filesLength.value += filesSource.length
- // 手动控制上传队列,每次只处理5个文件
- const batchSize = 5
- for (let i = 0; i < filesSource.length; i += batchSize) {
- const batch = filesSource.slice(i, i + batchSize)
- batch.forEach((file) => {
- computeMD5(file)
- })
- }
- }
- }
- const handleFileProgress = (file) => {
- const now = Date.now()
- const duration = (now - file.lastTime) / 1000 // 秒
- const loadedDiff = file.lastTime - file.lastLoaded
- console.log('duration', now, file.lastTime, file.lastLoaded, duration, loadedDiff)
- if (duration > 0) {
- console.log('进来了', file)
- // 计算当前上传速度 (bytes/sec)
- file.speed = loadedDiff / duration
- // 计算剩余时间
- if (file.speed > 0) {
- const remainingBytes = file.size - file.loaded
- const remainingSeconds = remainingBytes / file.speed
- console.log('进来了计算剩余时间', remainingBytes, remainingSeconds)
- // 格式化剩余时间显示
- if (remainingSeconds < 60) {
- console.log('进来了', 60)
- file.remainingTime = `${Math.round(remainingSeconds)}秒`
- } else if (remainingSeconds < 3600) {
- console.log('进来了', 600)
- file.remainingTime = `${Math.round(remainingSeconds / 60)}分钟`
- } else {
- console.log('进来了', 3600)
- file.remainingTime = `${Math.round(remainingSeconds / 3600)}小时`
- }
- }
- }
- // 更新最后记录的时间和已上传量
- file.lastLoaded = file.loaded
- file.lastTime = now
- }
- // const handleFileProgress = (file) => {
- // console.log(file, '进来了filefilefile')
- // const now = Date.now()
- // // 确保文件对象有必要的属性
- // if (!file.lastTime) file.lastTime = now
- // if (file.lastLoaded === undefined) file.lastLoaded = 0
- // const duration = (now - file.lastTime) / 1000 // 转换为秒
- // console.log(duration, '进来了duration')
- // // 只有当时间间隔足够大时才计算速度(至少0.1秒)
- // if (duration > 0.1) {
- // console.log(duration, '进来了duration')
- // const loadedDiff = file.loaded - file.lastLoaded
- // // 只有当有新的数据上传时才计算
- // if (loadedDiff > 0) {
- // console.log(loadedDiff, '进来了loadedDiff')
- // file.speed = loadedDiff / duration
- // // 计算剩余时间(确保speed是有效数字)
- // if (file.speed > 0 && !isNaN(file.speed)) {
- // const remainingBytes = file.size - file.loaded
- // const remainingSeconds = remainingBytes / file.speed
- // // 格式化剩余时间
- // if (remainingSeconds < 60) {
- // file.remainingTime = `${Math.round(remainingSeconds)}秒`
- // } else if (remainingSeconds < 3600) {
- // const mins = Math.floor(remainingSeconds / 60)
- // const secs = Math.round(remainingSeconds % 60)
- // file.remainingTime = `${mins}分${secs}秒`
- // } else {
- // const hours = Math.floor(remainingSeconds / 3600)
- // const mins = Math.round((remainingSeconds % 3600) / 60)
- // file.remainingTime = `${hours}小时${mins}分`
- // }
- // } else {
- // file.remainingTime = '计算中...'
- // }
- // // 更新最后记录的时间和已上传量
- // file.lastLoaded = file.loaded
- // file.lastTime = now
- // }
- // }
- // }
- const handleFileSuccess = (rootFile, file, response) => {
- if (response === '') {
- uploadStatus.value[file.id] = '上传失败'
- return
- }
- const result = JSON.parse(response)
- if (result.success) {
- file.statusStr = ''
- // 将上传成功的文件ID添加到表单数据中
- formState.fileIds.push(result.data.userFileId)
- emit('success', formState.fileIds)
- } else {
- message.error(result.msg)
- uploadStatus.value[file.id] = '上传失败'
- }
- filesLength.value--
- // 所有文件上传完成后,允许关闭弹窗
- canClose.value = filesLength.value === 0
- // 当一个文件完成时,检查是否有待上传文件
- if (uploaderInstance.value.files.some((f) => !f.isComplete)) {
- uploaderInstance.value.upload()
- }
- console.log(formState.fileIds, response, result, 'formState.fileIdsformState.fileIdsformState.fileIds')
- }
- const handleFileError = (rootFile, file, response) => {
- message.error(response)
- filesLength.value--
- canClose.value = filesLength.value === 0
- }
- const computeMD5 = (file) => {
- let fileReader = new FileReader()
- let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
- let currentChunk = 0
- const chunkSize = 1024 * 1024
- let chunks = Math.ceil(file.size / chunkSize)
- let spark = new SparkMD5.ArrayBuffer()
- file.statusStr = '计算MD5'
- file.pause()
- const loadNext = () => {
- let start = currentChunk * chunkSize
- let end = start + chunkSize >= file.size ? file.size : start + chunkSize
- fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end))
- }
- fileReader.onload = (e) => {
- spark.append(e.target.result)
- if (currentChunk < chunks) {
- currentChunk++
- loadNext()
- file.statusStr = `校验MD5 ${((currentChunk / chunks) * 100).toFixed(0)}%`
- } else {
- let md5 = spark.end()
- calculateFileMD5End(md5, file)
- }
- }
- fileReader.onerror = () => {
- message.error({
- content: `文件${file.name}读取出错,请检查该文件`,
- duration: 2
- })
- file.cancel()
- filesLength.value--
- canClose.value = filesLength.value === 0
- }
- loadNext()
- }
- const uploadFileParams = computed(() => {
- return {
- filePath: '/',
- isDir: 0,
- funcType: 0
- }
- })
- const calculateFileMD5End = (md5, file) => {
- Object.assign(uploaderInstance.value.opts, {
- query: uploadFileParams.value
- })
- file.uniqueIdentifier = md5
- file.resume()
- file.statusStr = ''
- // 文件开始上传时,禁止关闭弹窗
- // canClose.value = false
- }
- const handleCancel = () => {
- if (!canClose.value) {
- message.warning('文件正在上传中,请等待上传完成后再关闭')
- return
- }
- // 重置表单和上传状态
- formRef.value?.resetFields()
- formState.fileIds = []
- if (uploaderInstance.value) {
- uploaderInstance.value.cancel()
- }
- emit('update:visible', false)
- }
- const handleSubmit = () => {
- formRef.value
- .validate()
- .then(() => {
- if (formState.fileIds.length === 0) {
- message.warning('请上传文件')
- return
- }
- if (filesLength.value > 0) {
- message.warning('文件正在上传中,请等待上传完成后再提交')
- return
- }
- submitting.value = true
- // 模拟提交数据
- setTimeout(() => {
- message.success('提交成功')
- submitting.value = false
- emit('success', formState)
- // 重置表单和上传状态
- formRef.value?.resetFields()
- formState.fileIds = []
- emit('update:visible', false)
- }, 1000)
- })
- .catch(() => {
- // 表单验证失败
- })
- }
- // 暴露方法给父组件
- defineExpose({
- handleUpload
- })
- </script>
- <style lang="less" scoped>
- @import '@/style/myResource/varibles.less';
- .upload-btn-wrapper {
- display: flex;
- align-items: center;
- }
- .uploader-file {
- width: 560px;
- }
- .select-file-btn {
- display: none;
- }
- .drop-box {
- position: relative;
- width: 100%;
- height: 200px;
- border: 2px dashed #e9e9e9;
- border-radius: 6px;
- background-color: #fafafa;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- margin-top: 10px;
- .text {
- font-size: 14px;
- color: #999;
- }
- .paste-img-wrapper {
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- .paste-name {
- font-size: 14px;
- margin-bottom: 10px;
- }
- .paste-img {
- max-width: 80%;
- max-height: 120px;
- }
- }
- .upload-icon,
- .delete-icon {
- position: absolute;
- bottom: 10px;
- font-size: 20px;
- cursor: pointer;
- }
- .upload-icon {
- right: 40px;
- color: #1890ff;
- }
- .delete-icon {
- right: 10px;
- color: #ff4d4f;
- }
- .close-icon {
- position: absolute;
- top: 10px;
- right: 10px;
- font-size: 16px;
- cursor: pointer;
- color: #999;
- }
- }
- .uploader-app {
- width: 560px;
- }
- .file-panel {
- width: 100%;
- margin-top: 15px;
- border: 1px solid #e9e9e9;
- border-radius: 4px;
- overflow: hidden;
- .file-title {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 8px 12px;
- background-color: #f5f5f5;
- border-bottom: 1px solid #e9e9e9;
- .title-span {
- font-size: 14px;
- font-weight: 500;
- .count {
- color: #999;
- font-weight: normal;
- }
- }
- }
- .file-list {
- position: relative;
- height: 240px;
- overflow-x: hidden;
- overflow-y: auto;
- background-color: #fff;
- font-size: 12px;
- list-style: none;
- &::-webkit-scrollbar {
- width: 6px;
- }
- &::-webkit-scrollbar-thumb {
- background: @scrollbar-thumb-color;
- border-radius: 4px;
- }
- &::-webkit-scrollbar-track {
- background: @scrollbar-track-color;
- }
- .file-item {
- position: relative;
- background-color: #fff;
- :deep(.uploader-file) {
- height: 40px;
- line-height: 40px;
- .uploader-file-progress {
- border: 1px solid @success-color;
- border-right: none;
- border-left: none;
- background: #e1f3d8;
- }
- .uploader-file-name {
- width: 44%;
- }
- .uploader-file-size {
- width: 16%;
- }
- .uploader-file-meta {
- display: none;
- }
- .uploader-file-status {
- width: 30%;
- text-indent: 0;
- }
- .uploader-file-actions>span {
- margin-top: 12px;
- }
- }
- :deep(.uploader-file[status='success']) {
- .uploader-file-progress {
- border: none;
- }
- }
- }
- .file-item.custom-status-item {
- :deep(.uploader-file-status) {
- visibility: hidden;
- }
- .custom-status {
- position: absolute;
- top: 0;
- right: 10%;
- width: 24%;
- height: 40px;
- line-height: 40px;
- }
- }
- .no-file {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- font-size: 14px;
- }
- :deep(.uploader-file-icon) {
- display: none;
- }
- :deep(.uploader-file-actions > span) {
- margin-right: 6px;
- }
- }
- }
- .collapse-enter-active,
- .collapse-leave-active {
- transition: all 0.3s;
- max-height: 300px;
- overflow: hidden;
- }
- .collapse-enter-from,
- .collapse-leave-to {
- max-height: 0;
- }
- .upload-area {
- border: 2px dashed #3ca9f5;
- padding: 40px;
- text-align: center;
- }
- .upload-area p {
- margin: 10px 0;
- }
- .file-item {
- display: flex;
- align-items: center;
- margin: 10px 0;
- }
- .file-item .ant-progress {
- flex: 1;
- margin: 0 10px;
- }
- /* 新增表单样式 */
- .ant-form-item {
- margin-bottom: 16px;
- }
- .public-status-buttons {
- display: flex;
- }
- .status-button {
- padding: 5px 10px;
- /* margin-right: 10px; */
- border: 1px solid #ccc;
- /* border-radius: 3px; */
- cursor: pointer;
- background-color: #fff;
- }
- .status-button.active {
- background-color: #40a9ff;
- color: #fff;
- border-color: #40a9ff;
- }
- .upload-area {
- border: 2px dashed #3ca9f5;
- padding: 40px;
- text-align: center;
- transition: border-color 0.3s;
- /* 平滑过渡效果 */
- }
- .upload-area.drag-over {
- border-color: #1890ff;
- background-color: rgba(24, 144, 255, 0.05);
- }
- .remaining-time {
- position: absolute;
- right: 120px;
- top: 50%;
- transform: translateY(-50%);
- color: #666;
- font-size: 12px;
- }
- </style>
|