Box.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. <template>
  2. <div class="upload-file-wrapper">
  3. <!-- 上传文件组件 -->
  4. <uploader
  5. class="uploader-app"
  6. ref="uploaderRef"
  7. :options="options"
  8. :autoStart="false"
  9. :fileStatusText="fileStatusText"
  10. @files-added="handleFilesAdded"
  11. @file-success="handleFileSuccess"
  12. @file-error="handleFileError"
  13. @dragleave="hideUploadMask"
  14. >
  15. <uploader-unsupport></uploader-unsupport>
  16. <!-- 选择按钮 在这里隐藏 -->
  17. <uploader-btn class="select-file-btn" :attrs="attrs" ref="uploadBtn"> 选择文件 </uploader-btn>
  18. <uploader-btn class="select-file-btn" :attrs="attrs" :directory="true" ref="uploadDirBtn">
  19. 选择目录
  20. </uploader-btn>
  21. <!-- 拖拽上传 -->
  22. <uploader-drop class="drop-box" id="dropBox" @paste="handlePaste" v-show="dropBoxShow">
  23. <div class="paste-img-wrapper" v-show="pasteImg.src">
  24. <div class="paste-name">{{ pasteImg.name }}</div>
  25. <img class="paste-img" :src="pasteImg.src" :alt="pasteImg.name" v-if="pasteImg.src" />
  26. </div>
  27. <span class="text" v-show="!pasteImg.src"> 截图粘贴或将文件拖拽至此区域上传 </span>
  28. <UploadOutlined class="upload-icon" v-show="pasteImg.src" @click="handleUploadPasteImg" />
  29. <DeleteOutlined class="delete-icon" v-show="pasteImg.src" @click="handleDeletePasteImg" />
  30. <CloseCircleOutlined class="close-icon" @click="dropBoxShow = false" />
  31. </uploader-drop>
  32. <!-- 上传列表 -->
  33. <uploader-list v-show="panelShow">
  34. <template #default="props">
  35. <div class="file-panel">
  36. <div class="file-title">
  37. <span class="title-span">
  38. 上传列表 <span class="count">({{ props.fileList.length }})</span>
  39. </span>
  40. <div class="operate">
  41. <a-button type="link" :title="collapse ? '展开' : '折叠'" @click="handleCollapse">
  42. <template #icon>
  43. <FullscreenOutlined v-if="collapse" />
  44. <MinusOutlined v-else />
  45. </template>
  46. </a-button>
  47. <a-button type="link" title="关闭" @click="handleClosePanel">
  48. <template #icon>
  49. <CloseOutlined />
  50. </template>
  51. </a-button>
  52. </div>
  53. </div>
  54. <transition name="collapse">
  55. <ul class="file-list" v-show="!collapse">
  56. <li
  57. v-for="file in props.fileList"
  58. :key="file.id"
  59. class="file-item"
  60. :class="{ 'custom-status-item': file.statusStr !== '' }"
  61. >
  62. <uploader-file ref="fileItem" :file="file" :list="true" />
  63. <span class="custom-status">{{ file.statusStr }}</span>
  64. </li>
  65. <div class="no-file" v-if="!props.fileList.length"><FileExclamationOutlined /> 暂无待上传文件</div>
  66. </ul>
  67. </transition>
  68. </div>
  69. </template>
  70. </uploader-list>
  71. </uploader>
  72. </div>
  73. </template>
  74. <script setup>
  75. import { ref, computed, defineExpose, nextTick, inject, getCurrentInstance, onMounted } from 'vue'
  76. import { message } from 'ant-design-vue'
  77. import { useMyResourceStore } from '@/store/myResource'
  78. import SparkMD5 from 'spark-md5'
  79. import tool from '@/utils/tool'
  80. import {
  81. UploadOutlined,
  82. DeleteOutlined,
  83. CloseCircleOutlined,
  84. FullscreenOutlined,
  85. MinusOutlined,
  86. CloseOutlined,
  87. FileExclamationOutlined
  88. } from '@ant-design/icons-vue'
  89. // 使用 inject 接收响应式数据
  90. const uploadProps = inject('uploadProps', {
  91. params: {},
  92. uploadWay: 1,
  93. serviceEl: null,
  94. callType: 1,
  95. callback: () => {}
  96. })
  97. const { proxy } = getCurrentInstance()
  98. const store = useMyResourceStore()
  99. // refs
  100. const uploaderRef = ref(null)
  101. const uploadBtn = ref(null)
  102. const uploadDirBtn = ref(null)
  103. const fileItem = ref(null)
  104. // 响应式数据
  105. const imageTypes = ref(['image/gif', 'image/jpg', 'image/jpeg', 'image/png', 'image/bmp', 'image/webp'])
  106. const documentTypes = ref([
  107. 'application/msword',
  108. 'text/plain',
  109. 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  110. 'application/pdf',
  111. 'application/vnd.ms-excel',
  112. 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  113. 'application/vnd.ms-powerpoint',
  114. 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
  115. ])
  116. const opts = ref({
  117. categoryMap: {
  118. image: ['gif', 'jpg', 'jpeg', 'png', 'bmp', 'webp'],
  119. document: ['doc', 'txt', 'docx', 'pdf', 'xls', 'xlsx', 'ppt', 'pptx']
  120. }
  121. })
  122. const options = ref({
  123. target: `${proxy.$RESOURCE_CONFIG.baseContext}/filetransfer/uploadfile`,
  124. chunkSize: 1024 * 1024,
  125. fileParameterName: 'file',
  126. maxChunkRetries: 3,
  127. testChunks: true,
  128. checkChunkUploadedByResponse: (chunk, message) => {
  129. let objMessage = JSON.parse(message)
  130. if (objMessage.success) {
  131. let data = objMessage.data
  132. if (data.skipUpload) {
  133. return true
  134. }
  135. return (data.uploaded || []).indexOf(chunk.offset + 1) >= 0
  136. } else {
  137. return true
  138. }
  139. },
  140. headers: {
  141. token: tool.data.get('TOKEN')
  142. },
  143. query: () => {}
  144. })
  145. const fileStatusText = ref({
  146. success: '上传成功',
  147. error: 'error',
  148. uploading: '上传中',
  149. paused: '暂停中',
  150. waiting: '等待中'
  151. })
  152. const attrs = ref({
  153. accept: '*'
  154. })
  155. const panelShow = ref(false)
  156. const collapse = ref(false)
  157. const dropBoxShow = ref(false)
  158. const pasteImg = ref({
  159. src: '',
  160. name: ''
  161. })
  162. const pasteImgObj = ref(null)
  163. const filesLength = ref(0)
  164. const uploadStatus = ref({})
  165. // 计算属性
  166. const uploaderInstance = computed(() => {
  167. return uploaderRef.value.uploader
  168. })
  169. const remainderStorageValue = computed(() => {
  170. return store.remainderStorageValue
  171. })
  172. // 方法
  173. const hideUploadMask = (e) => {
  174. e.stopPropagation()
  175. e.preventDefault()
  176. dropBoxShow.value = false
  177. }
  178. const handlePrepareUpload = async () => {
  179. // options.value.headers.token = getCookies(getConfig().tokenKeyName)
  180. await nextTick()
  181. switch (uploadProps.uploadWay) {
  182. case 1: {
  183. if (uploadBtn.value?.$el) {
  184. uploadBtn.value.$el.click()
  185. }
  186. break
  187. }
  188. case 2: {
  189. if (uploadDirBtn.value?.$el) {
  190. uploadDirBtn.value.$el.click()
  191. }
  192. break
  193. }
  194. case 3: {
  195. pasteImg.value.src = ''
  196. pasteImg.value.name = ''
  197. pasteImgObj.value = null
  198. dropBoxShow.value = true
  199. break
  200. }
  201. }
  202. }
  203. const handlePaste = (event) => {
  204. let pasteItems = (event.clipboardData || window.clipboardData).items
  205. if (pasteItems && pasteItems.length) {
  206. let imgObj = pasteItems[0].getAsFile()
  207. pasteImgObj.value =
  208. imgObj !== null
  209. ? new File([imgObj], `qiwenshare_${new Date().valueOf()}.${imgObj.name.split('.')[1]}`, { type: imgObj.type })
  210. : null
  211. } else {
  212. message.error('当前浏览器不支持')
  213. return false
  214. }
  215. if (!pasteImgObj.value) {
  216. message.error('粘贴内容非图片')
  217. return false
  218. }
  219. pasteImg.value.name = pasteImgObj.value.name
  220. let reader = new FileReader()
  221. reader.onload = (event) => {
  222. pasteImg.value.src = event.target.result
  223. }
  224. reader.readAsDataURL(pasteImgObj.value)
  225. }
  226. const handleUploadPasteImg = () => {
  227. uploaderInstance.value.addFile(pasteImgObj.value)
  228. }
  229. const handleDeletePasteImg = () => {
  230. pasteImg.value.src = ''
  231. pasteImg.value.name = ''
  232. pasteImgObj.value = null
  233. }
  234. const handleFilesAdded = (filesSource) => {
  235. console.log('filesSource.filesSource', filesSource)
  236. // const inconformityFileArr = []
  237. // const files = filesSource.filter((file) => {
  238. // const isFile = isImageOrDocument(file)
  239. // if (isFile) {
  240. // return file
  241. // } else {
  242. // inconformityFileArr.push(file)
  243. // }
  244. // })
  245. // if (inconformityFileArr.length > 0) {
  246. // filesSource.ignored = true
  247. // message.warning('文件夹中有不符合要求的文件', {
  248. // duration: 0,
  249. // content: '无'
  250. // // content: () => {
  251. // // let warringHtml = ''
  252. // // inconformityFileArr.forEach((item, index) => {
  253. // // warringHtml += `<p><i style="margin-right:5px">${index + 1}.</i>${item.name}</p>`
  254. // // })
  255. // // return (
  256. // // <div>
  257. // // <div style="color:#E6A23C;width:290px">{warringHtml}</div>
  258. // // <p style="color:#f56c6c">只可以上传图片和文档!文件大小不能为0!</p>
  259. // // </div>
  260. // // )
  261. // // }
  262. // })
  263. // return false
  264. // }
  265. // const filesTotalSize = files.reduce((pre, next) => pre + next.size, 0)
  266. const filesTotalSize = filesSource
  267. .map((item) => item.size)
  268. .reduce((pre, next) => {
  269. return pre + next
  270. }, 0)
  271. if (remainderStorageValue.value < filesTotalSize) {
  272. // 批量选择的文件超出剩余存储空间
  273. message.warning(`剩余存储空间不足,请重新选择${filesSource.length > 1 ? '批量' : ''}文件`)
  274. filesSource.ignored = true // 本次选择的文件过滤掉
  275. } else {
  276. // 批量或单个选择的文件未超出剩余存储空间,正常上传
  277. filesLength.value += filesSource.length
  278. filesSource.forEach((file) => {
  279. dropBoxShow.value = false
  280. panelShow.value = true
  281. collapse.value = false
  282. computeMD5(file)
  283. })
  284. }
  285. }
  286. const handleFileSuccess = (rootFile, file, response) => {
  287. if (response === '') {
  288. uploadStatus.value[file.id] = '上传失败'
  289. uploadProps.callback(false)
  290. return
  291. }
  292. const result = JSON.parse(response)
  293. if (result.success) {
  294. file.statusStr = ''
  295. if (filesLength.value === 1) {
  296. message.success('上传完毕')
  297. if (uploadProps.callType === 1) {
  298. uploadProps.serviceEl.$emit('getTableDataByType')
  299. } else {
  300. // uploadProps.serviceEl.getTableDataByType()
  301. }
  302. store.showStorage()
  303. uploadProps.callback(true)
  304. }
  305. } else {
  306. message.error(result.message)
  307. uploadStatus.value[file.id] = '上传失败'
  308. }
  309. filesLength.value--
  310. }
  311. const handleFileError = (rootFile, file, response) => {
  312. message.error(response)
  313. }
  314. const computeMD5 = (file) => {
  315. let fileReader = new FileReader()
  316. let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
  317. let currentChunk = 0
  318. const chunkSize = 1024 * 1024
  319. let chunks = Math.ceil(file.size / chunkSize)
  320. let spark = new SparkMD5.ArrayBuffer()
  321. file.statusStr = '计算MD5'
  322. file.pause()
  323. const loadNext = () => {
  324. let start = currentChunk * chunkSize
  325. let end = start + chunkSize >= file.size ? file.size : start + chunkSize
  326. fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end))
  327. }
  328. fileReader.onload = (e) => {
  329. spark.append(e.target.result)
  330. if (currentChunk < chunks) {
  331. currentChunk++
  332. loadNext()
  333. file.statusStr = `校验MD5 ${((currentChunk / chunks) * 100).toFixed(0)}%`
  334. } else {
  335. let md5 = spark.end()
  336. calculateFileMD5End(md5, file)
  337. }
  338. }
  339. fileReader.onerror = () => {
  340. message.error({
  341. content: `文件${file.name}读取出错,请检查该文件`,
  342. duration: 2
  343. })
  344. file.cancel()
  345. }
  346. loadNext()
  347. }
  348. const calculateFileMD5End = (md5, file) => {
  349. Object.assign(uploaderInstance.value.opts, {
  350. query: {
  351. ...uploadProps.params
  352. }
  353. })
  354. file.uniqueIdentifier = md5
  355. file.resume()
  356. file.statusStr = ''
  357. }
  358. const handleClosePanel = () => {
  359. uploaderInstance.value.cancel()
  360. panelShow.value = false
  361. uploadProps.callback('cancel')
  362. }
  363. const handleCollapse = () => {
  364. collapse.value = !collapse.value
  365. }
  366. const isImageOrDocument = (file) => {
  367. const isImage = imageTypes.value.includes(file.fileType)
  368. const isDocument = documentTypes.value.includes(file.fileType)
  369. const fileSize = file.size
  370. return (isImage || isDocument) && fileSize !== 0
  371. }
  372. // 暴露方法给父组件
  373. defineExpose({
  374. handlePrepareUpload
  375. })
  376. </script>
  377. <style lang="less" scoped>
  378. @import '@/style/myResource/varibles.less';
  379. .upload-file-wrapper {
  380. position: fixed;
  381. z-index: 20;
  382. right: 16px;
  383. bottom: 16px;
  384. .drop-box {
  385. position: fixed;
  386. z-index: 19;
  387. top: 0;
  388. left: 0;
  389. border: 5px dashed #8091a5 !important;
  390. background: #ffffffd9;
  391. color: #8091a5 !important;
  392. text-align: center;
  393. box-sizing: border-box;
  394. height: 100%;
  395. line-height: 100%;
  396. width: 100%;
  397. .text {
  398. position: absolute;
  399. top: 50%;
  400. left: 50%;
  401. width: 100%;
  402. transform: translate(-50%, -50%);
  403. font-size: 30px;
  404. }
  405. .upload-icon {
  406. position: absolute;
  407. right: 176px;
  408. top: 16px;
  409. cursor: pointer;
  410. &:hover {
  411. color: @primary-color;
  412. }
  413. }
  414. .delete-icon {
  415. position: absolute;
  416. right: 80px;
  417. top: 16px;
  418. cursor: pointer;
  419. &:hover {
  420. color: @error-color;
  421. }
  422. }
  423. .close-icon {
  424. position: absolute;
  425. right: 16px;
  426. top: 16px;
  427. cursor: pointer;
  428. &:hover {
  429. color: @success-color;
  430. }
  431. }
  432. .paste-img-wrapper {
  433. width: 100%;
  434. height: 100%;
  435. }
  436. .paste-img {
  437. margin-top: 16px;
  438. max-width: 90%;
  439. max-height: 80%;
  440. }
  441. .paste-name {
  442. height: 24px;
  443. line-height: 24px;
  444. font-size: 18px;
  445. color: @text-color;
  446. }
  447. }
  448. .uploader-app {
  449. width: 560px;
  450. }
  451. .file-panel {
  452. background-color: #fff;
  453. border: 1px solid #e2e2e2;
  454. border-radius: 7px 7px 0 0;
  455. box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
  456. .file-title {
  457. display: flex;
  458. height: 40px;
  459. line-height: 40px;
  460. padding: 0 15px;
  461. border-bottom: 1px solid #ddd;
  462. .title-span {
  463. padding-left: 0;
  464. margin-bottom: 0;
  465. font-size: 16px;
  466. .count {
  467. color: @text-color-secondary;
  468. }
  469. }
  470. .operate {
  471. flex: 1;
  472. text-align: right;
  473. :deep(.ant-btn-link) {
  474. color: @text-color;
  475. &:hover {
  476. .anticon-fullscreen,
  477. .anticon-minus {
  478. color: @success-color;
  479. }
  480. .anticon-close {
  481. color: @error-color;
  482. }
  483. }
  484. }
  485. }
  486. }
  487. .file-list {
  488. position: relative;
  489. height: 240px;
  490. overflow-x: hidden;
  491. overflow-y: auto;
  492. background-color: #fff;
  493. font-size: 12px;
  494. list-style: none;
  495. &::-webkit-scrollbar {
  496. width: 6px;
  497. }
  498. &::-webkit-scrollbar-thumb {
  499. background: @scrollbar-thumb-color;
  500. border-radius: 4px;
  501. }
  502. &::-webkit-scrollbar-track {
  503. background: @scrollbar-track-color;
  504. }
  505. .file-item {
  506. position: relative;
  507. background-color: #fff;
  508. :deep(.uploader-file) {
  509. height: 40px;
  510. line-height: 40px;
  511. .uploader-file-progress {
  512. border: 1px solid @success-color;
  513. border-right: none;
  514. border-left: none;
  515. background: #e1f3d8;
  516. }
  517. .uploader-file-name {
  518. width: 44%;
  519. }
  520. .uploader-file-size {
  521. width: 16%;
  522. }
  523. .uploader-file-meta {
  524. display: none;
  525. }
  526. .uploader-file-status {
  527. width: 30%;
  528. text-indent: 0;
  529. }
  530. .uploader-file-actions > span {
  531. margin-top: 12px;
  532. }
  533. }
  534. :deep(.uploader-file[status='success']) {
  535. .uploader-file-progress {
  536. border: none;
  537. }
  538. }
  539. }
  540. .file-item.custom-status-item {
  541. :deep(.uploader-file-status) {
  542. visibility: hidden;
  543. }
  544. .custom-status {
  545. position: absolute;
  546. top: 0;
  547. right: 10%;
  548. width: 24%;
  549. height: 40px;
  550. line-height: 40px;
  551. }
  552. }
  553. }
  554. &.collapse {
  555. .file-title {
  556. background-color: #e7ecf2;
  557. }
  558. }
  559. }
  560. .no-file {
  561. position: absolute;
  562. top: 50%;
  563. left: 50%;
  564. transform: translate(-50%, -50%);
  565. font-size: 16px;
  566. }
  567. :deep(.uploader-file-icon) {
  568. display: none;
  569. }
  570. :deep(.uploader-file-actions > span) {
  571. margin-right: 6px;
  572. }
  573. }
  574. .select-file-btn {
  575. display: none;
  576. }
  577. .collapse-enter-active,
  578. .collapse-leave-active {
  579. transition: all 0.3s ease-in-out;
  580. max-height: 1000px;
  581. overflow: hidden;
  582. }
  583. .collapse-enter-from,
  584. .collapse-leave-to {
  585. max-height: 0;
  586. opacity: 0;
  587. overflow: hidden;
  588. }
  589. </style>