upload.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. <template>
  2. <div
  3. class="czr-form-column-upload-com"
  4. :class="{
  5. 'czr-form-column-upload-com_view': isValue($attrs.disabled)
  6. ? $attrs.disabled
  7. : isLimitCpt || isViewCpt,
  8. 'czr-form-column-upload-com_list': uploadLayout === 'list',
  9. 'czr-form-column-upload-com_card': uploadLayout === 'card',
  10. 'czr-form-column-upload-com_limit-no-upload':
  11. isLimitCpt && limitNoUpload !== false,
  12. }"
  13. v-loading="state.loading"
  14. :element-loading-background="state.elementLoadingBackground"
  15. >
  16. <el-upload
  17. class="el-upload-com"
  18. ref="ref_upload"
  19. v-bind="$attrs"
  20. :file-list="state.paramVal"
  21. action="#"
  22. :list-type="uploadLayout === 'card' ? 'picture-card' : 'text'"
  23. :http-request="handleRequest"
  24. :before-upload="handleBeforeUpload"
  25. :accept="acceptTypeCpt"
  26. :limit="limit > 0 ? limit : null"
  27. :on-remove="handleRemove"
  28. :disabled="
  29. isValue($attrs.disabled) ? $attrs.disabled : isLimitCpt || isViewCpt
  30. "
  31. >
  32. <el-tooltip
  33. :content="acceptTooltipCpt"
  34. placement="top"
  35. popper-class="upload-tooltip-popper"
  36. >
  37. <template v-if="uploadLayout === 'card'">
  38. <div
  39. class="upload-layout-card __hover"
  40. :class="{
  41. 'limit-disabled':
  42. isLimitCpt || (isViewCpt && state.paramVal.length === 0),
  43. }"
  44. >
  45. <SvgIcon name="add" color="#0062E9" size="24" />
  46. <template v-if="title">
  47. <div>{{ title }}</div>
  48. </template>
  49. <template v-else>
  50. <div>选择{{ acceptTypeFormatCpt }}</div>
  51. <div v-if="limit > 0">(最多{{ limit }}个)</div>
  52. </template>
  53. </div>
  54. </template>
  55. <template v-else>
  56. <div
  57. class="upload-layout-list_button"
  58. :class="{
  59. 'limit-disabled':
  60. isLimitCpt || (isViewCpt && state.paramVal.length === 0),
  61. }"
  62. >
  63. <SvgIcon name="add" color="#ffffff" size="14" />
  64. <template v-if="title">
  65. {{ title }}
  66. </template>
  67. <template v-else>
  68. 选择{{ acceptTypeFormatCpt }}
  69. <template v-if="limit > 0">(最多{{ limit }}个)</template>
  70. </template>
  71. </div>
  72. </template>
  73. </el-tooltip>
  74. <template #file="{ file }">
  75. <template v-if="uploadLayout === 'card'">
  76. <el-tooltip
  77. placement="top"
  78. :content="file[nameKey] ?? file[urlKey]"
  79. :disabled="true"
  80. >
  81. <div
  82. class="upload-layout-card_item"
  83. :class="{ 'item-view': isViewCpt || !delRule(file) }"
  84. @mouseenter="file.hover = true"
  85. @mouseleave="file.hover = false"
  86. >
  87. <template v-if="validImgByUrl(file[urlKey])">
  88. <img
  89. class="img __hover"
  90. :src="file[urlKey]"
  91. @click="viewImg(file[urlKey])"
  92. />
  93. </template>
  94. <template v-else-if="validVideoByUrl(file[urlKey])">
  95. <video class="video __hover" controls :src="file[urlKey]" />
  96. </template>
  97. <template v-else>
  98. <img
  99. class="file __hover"
  100. :src="getFileImgByUrl(file[urlKey])"
  101. @click="
  102. downloadFileByUrl(
  103. file[urlKey],
  104. file[nameKey] ?? file[urlKey],
  105. )
  106. "
  107. />
  108. </template>
  109. <img
  110. class="del __hover"
  111. src="../imgs/file-del.png"
  112. v-if="file.hover && !isViewCpt && delRule(file)"
  113. @click.stop="ref_upload.handleRemove(file)"
  114. />
  115. </div>
  116. </el-tooltip>
  117. </template>
  118. <template v-else>
  119. <div
  120. class="upload-layout-list_item __hover"
  121. :class="{ 'item-view': isViewCpt || !delRule(file) }"
  122. @click="
  123. validImgByUrl(file[urlKey])
  124. ? viewImg(file[urlKey])
  125. : downloadFileByUrl(file[urlKey], file[nameKey] ?? file[urlKey])
  126. "
  127. >
  128. <img class="file-type-img" :src="getFileImgByUrl(file[urlKey])" />
  129. <CzrEllipsis
  130. v-if="file[nameKey] ?? file[urlKey]"
  131. class="label"
  132. :value="file[nameKey] ?? file[urlKey]"
  133. />
  134. <SvgIcon
  135. v-if="!isViewCpt && delRule(file)"
  136. class="close"
  137. name="close_3"
  138. size="12"
  139. @click.stop="ref_upload.handleRemove(file)"
  140. />
  141. </div>
  142. </template>
  143. </template>
  144. </el-upload>
  145. <el-image-viewer
  146. v-if="state.currentImg.show"
  147. :zoom-rate="2"
  148. :max-scale="10"
  149. :min-scale="0.2"
  150. :hide-on-click-modal="true"
  151. :url-list="[state.currentImg.url]"
  152. :initial-index="0"
  153. @close="imageClose"
  154. :teleported="true"
  155. :crossorigin="null"
  156. />
  157. </div>
  158. </template>
  159. <script setup lang="ts">
  160. import {
  161. defineComponent,
  162. computed,
  163. onMounted,
  164. ref,
  165. reactive,
  166. watch,
  167. getCurrentInstance,
  168. ComponentInternalInstance,
  169. toRefs,
  170. nextTick,
  171. onBeforeMount,
  172. inject,
  173. } from 'vue'
  174. import { useRouter, useRoute } from 'vue-router'
  175. import { ElMessage } from 'element-plus'
  176. import FileDefaultImg from '../imgs/file-type/file-type_default.png'
  177. import FilePDFImg from '../imgs/file-type/file-type_pdf.png'
  178. import FilePPTImg from '../imgs/file-type/file-type_ppt.png'
  179. import FileRARImg from '../imgs/file-type/file-type_rar.png'
  180. import FileTXTImg from '../imgs/file-type/file-type_txt.png'
  181. import FileWORDImg from '../imgs/file-type/file-type_word.png'
  182. import FileEXCELImg from '../imgs/file-type/file-type_excel.png'
  183. import { isValue } from '@/utils/czr-util'
  184. import { fileUploadFile } from '@/api/modules/global/upload'
  185. const emit = defineEmits(['emitParam'])
  186. const props = defineProps({
  187. param: {},
  188. label: {},
  189. limit: {
  190. default: 0,
  191. },
  192. type: {
  193. default: 'all',
  194. validator(val: string) {
  195. return ['all', 'img', 'file'].includes(val)
  196. },
  197. },
  198. uploadLayout: {
  199. default: 'list',
  200. validator(val: string) {
  201. return ['list', 'card'].includes(val)
  202. },
  203. },
  204. acceptType: { default: '', type: String },
  205. acceptMax: { default: 0, type: Number },
  206. acceptFunc: { default: null },
  207. view: { default: null },
  208. delRule: { default: () => () => false },
  209. cardWidth: { default: '8rem' },
  210. cardHeight: { default: '8rem' },
  211. urlKey: { default: 'url' },
  212. nameKey: { default: 'name' },
  213. limitNoUpload: { default: false },
  214. title: {},
  215. })
  216. const state = reactive({
  217. paramVal: <any>props.param,
  218. loading: false,
  219. elementLoadingBackground: inject('element-loading-background', null),
  220. currentImg: {
  221. show: false,
  222. url: '',
  223. },
  224. formView: inject('form-view', false),
  225. config: {
  226. fileMax: 50,
  227. fileType: '.doc,.docx,.ppt,.pptx,.pdf,.xls,.xlsx,.txt,.rar,.zip',
  228. imgMax: 50,
  229. imgType: '.jpg,.jpeg,.png,.gif,.svg,.bmp,.webp',
  230. },
  231. })
  232. watch(
  233. () => state.paramVal,
  234. (n) => {
  235. emit('emitParam', n)
  236. },
  237. )
  238. watch(
  239. () => props.param,
  240. (n) => {
  241. state.paramVal = n
  242. },
  243. )
  244. const isViewCpt = computed(() => {
  245. return isValue(props.view) ? props.view : state.formView
  246. })
  247. const ref_upload = ref()
  248. const validImgByUrl = (url) => {
  249. if (url) {
  250. const imgList = [
  251. 'bmp',
  252. 'jpg',
  253. 'jpeg',
  254. 'png',
  255. 'tif',
  256. 'gif',
  257. 'pcx',
  258. 'tga',
  259. 'exif',
  260. 'fpx',
  261. 'svg',
  262. 'psd',
  263. 'cdr',
  264. 'pcd',
  265. 'dxf',
  266. 'ufo',
  267. 'eps',
  268. 'ai',
  269. 'raw',
  270. 'WMF',
  271. 'webp',
  272. 'avif',
  273. 'apng',
  274. 'jfif',
  275. ]
  276. //进行图片匹配
  277. let result =
  278. [
  279. ...imgList.map((v) => v.toLowerCase()),
  280. ...imgList.map((v) => v.toUpperCase()),
  281. ].some((t) => url.includes('.' + t)) ||
  282. url.includes('/ngx/proxy') ||
  283. url.includes('/pic?')
  284. return result
  285. }
  286. return false
  287. }
  288. const validVideoByUrl = (url) => {
  289. if (url) {
  290. const videoList = ['mp4']
  291. //进行图片匹配
  292. let result = [
  293. ...videoList.map((v) => v.toLowerCase()),
  294. ...videoList.map((v) => v.toUpperCase()),
  295. ].some((t) => url.includes('.' + t))
  296. return result
  297. }
  298. return false
  299. }
  300. const acceptTypeCpt = computed(() => {
  301. let str = ''
  302. if (isValue(props.acceptType)) {
  303. str += props.acceptType
  304. } else {
  305. if (props.type === 'all') {
  306. str += state.config.imgType
  307. str += ',' + state.config.fileType
  308. } else if (props.type === 'img') {
  309. str += state.config.imgType
  310. } else if (props.type === 'file') {
  311. str += state.config.fileType
  312. }
  313. }
  314. return str
  315. })
  316. const acceptTypeFormatCpt = computed(() => {
  317. let str = ''
  318. if (isValue(props.acceptType)) {
  319. str += `自定义文件`
  320. } else {
  321. if (props.type === 'all') {
  322. str += `图片、文件`
  323. } else if (props.type === 'img') {
  324. str += `图片`
  325. } else if (props.type === 'file') {
  326. str += `文件`
  327. }
  328. }
  329. return str
  330. })
  331. const acceptTooltipCpt = computed(() => {
  332. let str = ''
  333. if (isValue(props.acceptType)) {
  334. str += `只支持大小在0~${props.acceptMax}M内,格式为${props.acceptType}的自定义文件`
  335. } else {
  336. if (props.type === 'all') {
  337. str += `只支持大小在0~${state.config.imgMax}M内,格式为${state.config.imgType}的图片,`
  338. str += `或大小在0~${state.config.fileMax}M内,格式为${state.config.fileType}的文件`
  339. } else if (props.type === 'img') {
  340. str += `只支持大小在0~${state.config.imgMax}M内,格式为${state.config.imgType}的图片`
  341. } else if (props.type === 'file') {
  342. str += `只支持大小在0~${state.config.fileMax}M内,格式为${state.config.fileType}的文件`
  343. }
  344. }
  345. if (isLimitCpt.value) {
  346. str = `最多上传${props.limit}个${acceptTypeFormatCpt.value}`
  347. }
  348. return str
  349. })
  350. const isLimitCpt = computed(() => {
  351. return props.limit > 0 && props.limit <= state.paramVal.length
  352. })
  353. const handleBeforeUpload = (file) => {
  354. if (isValue(props.acceptType)) {
  355. if (file.size / (1024 * 1024) > props.acceptMax) {
  356. ElMessage.warning(`自定义文件大小不可超过${props.acceptMax}M`)
  357. return false
  358. } else if (
  359. !props.acceptType.split(',').some((v) => file.name.includes(v))
  360. ) {
  361. ElMessage.warning(`自定义文件类型仅支持${props.acceptType}`)
  362. return false
  363. }
  364. } else {
  365. if (props.type === 'img') {
  366. if (file.size / (1024 * 1024) > state.config.imgMax) {
  367. ElMessage.warning(`图片大小不可超过${state.config.imgMax}M`)
  368. return false
  369. } else if (
  370. !state.config.imgType.split(',').some((v) => file.name.includes(v))
  371. ) {
  372. ElMessage.warning(`图片类型仅支持${state.config.imgType}`)
  373. return false
  374. }
  375. } else if (props.type === 'file') {
  376. if (file.size / (1024 * 1024) > state.config.fileMax) {
  377. ElMessage.warning(`文件大小不可超过${state.config.fileMax}M`)
  378. return false
  379. } else if (
  380. !state.config.fileType.split(',').some((v) => file.name.includes(v))
  381. ) {
  382. ElMessage.warning(`文件类型仅支持${state.config.fileType}`)
  383. return false
  384. }
  385. } else if (props.type === 'all') {
  386. if (!acceptTypeCpt.value.split(',').some((v) => file.name.includes(v))) {
  387. ElMessage.warning(`文件或图片类型仅支持${acceptTypeCpt.value}`)
  388. return false
  389. } else {
  390. if (
  391. state.config.imgType.split(',').some((v) => file.name.includes(v))
  392. ) {
  393. if (file.size / (1024 * 1024) > state.config.imgMax) {
  394. ElMessage.warning(`图片大小不可超过${state.config.imgMax}M`)
  395. return false
  396. }
  397. } else if (
  398. state.config.fileType.split(',').some((v) => file.name.includes(v))
  399. ) {
  400. if (file.size / (1024 * 1024) > state.config.fileMax) {
  401. ElMessage.warning(`文件大小不可超过${state.config.fileMax}M`)
  402. return false
  403. }
  404. }
  405. }
  406. }
  407. }
  408. return file
  409. }
  410. const handleRequest = async (options) => {
  411. state.loading = true
  412. if (isValue(props.acceptFunc)) {
  413. const result: any = await props.acceptFunc?.(options)
  414. if (result?.[props.urlKey] && result?.[props.nameKey]) {
  415. state.paramVal = [...state.paramVal, result]
  416. } else {
  417. state.paramVal = [...state.paramVal]
  418. }
  419. state.loading = false
  420. } else if (
  421. [
  422. ...state.config.imgType.split(','),
  423. ...state.config.fileType.split(','),
  424. ].some((v) => options.file.name.includes(v))
  425. ) {
  426. const formData = new FormData()
  427. formData.append('file', options.file)
  428. fileUploadFile(formData)
  429. .then(({ data }: any) => {
  430. state.paramVal = [
  431. ...state.paramVal,
  432. {
  433. [props.urlKey]: data.path,
  434. [props.nameKey]: data.filename.replace(/,/g, '_'),
  435. },
  436. ]
  437. })
  438. .catch(() => {})
  439. .finally(() => {
  440. state.loading = false
  441. })
  442. }
  443. }
  444. const fileTypeMapper = new Map([
  445. ['doc', FileWORDImg],
  446. ['docx', FileWORDImg],
  447. ['ppt', FilePPTImg],
  448. ['pptx', FilePPTImg],
  449. ['pdf', FilePDFImg],
  450. ['xls', FileEXCELImg],
  451. ['xlsx', FileEXCELImg],
  452. ['txt', FileTXTImg],
  453. ['rar', FileRARImg],
  454. ['zip', FileRARImg],
  455. ])
  456. const getFileImgByUrl = (url) => {
  457. if (url) {
  458. const regex = /\.([^.]*)$/
  459. const match = url.match(regex)
  460. const t = match ? match[1] : ''
  461. if (t && fileTypeMapper.has(t)) {
  462. return fileTypeMapper.get(t)
  463. }
  464. }
  465. return FileDefaultImg
  466. }
  467. const handleRemove = (file, files) => {
  468. state.paramVal = files
  469. }
  470. const downloadFileByUrl = (url, name) => {
  471. ElMessage.info('开始下载!')
  472. const xhr = new XMLHttpRequest()
  473. xhr.open('GET', url, true)
  474. xhr.responseType = 'blob'
  475. xhr.onload = function () {
  476. if (xhr.status === 200) {
  477. const blob = xhr.response
  478. const url = window.URL.createObjectURL(blob)
  479. const a = document.createElement('a')
  480. a.href = url
  481. a.download = name || 'default'
  482. a.click()
  483. ElMessage.success('下载成功!')
  484. window.URL.revokeObjectURL(url)
  485. }
  486. }
  487. xhr.send()
  488. }
  489. const viewImg = (url) => {
  490. state.currentImg.url = url
  491. state.currentImg.show = true
  492. }
  493. const imageClose = () => {
  494. state.currentImg.show = false
  495. state.currentImg.url = ''
  496. }
  497. </script>
  498. <style scoped lang="scss">
  499. .czr-form-column-upload-com {
  500. $uploadWidth: v-bind(cardWidth);
  501. $uploadHeight: v-bind(cardHeight);
  502. width: 100%;
  503. position: relative;
  504. .el-upload-com {
  505. display: flex;
  506. flex-direction: column;
  507. align-items: flex-start;
  508. :deep(.el-upload) {
  509. .upload-layout-list_button {
  510. height: 32px;
  511. display: flex;
  512. align-items: center;
  513. justify-content: center;
  514. padding: 0 12px;
  515. background-color: var(--czr-main-color);
  516. border-radius: 4px;
  517. font-size: 14px;
  518. font-family: Microsoft YaHei;
  519. font-weight: 400;
  520. color: #ffffff;
  521. .svg-icon {
  522. margin-right: 7px;
  523. }
  524. &.limit-disabled {
  525. cursor: no-drop;
  526. opacity: 0.5;
  527. }
  528. }
  529. .upload-layout-card {
  530. width: 100%;
  531. height: 100%;
  532. display: flex;
  533. flex-direction: column;
  534. line-height: 1;
  535. align-items: center;
  536. justify-content: center;
  537. background-color: rgba(46, 129, 255, 0.04);
  538. .svg-icon {
  539. margin-bottom: 10px;
  540. opacity: 0.5;
  541. }
  542. > div {
  543. font-size: 14px;
  544. font-family:
  545. PingFang SC-Regular,
  546. PingFang SC;
  547. font-weight: 400;
  548. color: rgba(0, 98, 233, 0.5);
  549. }
  550. &.limit-disabled {
  551. cursor: no-drop;
  552. opacity: 0.5;
  553. }
  554. }
  555. }
  556. :deep(.el-upload-list) {
  557. margin: 0;
  558. .el-upload-list__item {
  559. transition: none;
  560. .upload-layout-list_item {
  561. display: flex;
  562. align-items: center;
  563. padding: 0 6px;
  564. .file-type-img {
  565. width: 14px;
  566. margin: 0 6px 0 6px;
  567. }
  568. .label {
  569. width: calc(100% - 6px - 6px - 14px - 30px);
  570. }
  571. .close {
  572. margin-left: auto;
  573. }
  574. &.item-view {
  575. .label {
  576. width: calc(100% - 6px - 6px - 14px);
  577. }
  578. }
  579. }
  580. .upload-layout-card_item {
  581. width: 100%;
  582. height: 100%;
  583. position: relative;
  584. .img {
  585. width: 100%;
  586. height: 100%;
  587. }
  588. .video {
  589. width: 100%;
  590. height: 100%;
  591. object-fit: fill;
  592. }
  593. .file {
  594. width: 100%;
  595. height: 100%;
  596. }
  597. .del {
  598. position: absolute;
  599. top: 0;
  600. right: 0;
  601. z-index: 2;
  602. width: 20px;
  603. height: 20px;
  604. }
  605. }
  606. }
  607. }
  608. }
  609. &.czr-form-column-upload-com_view,
  610. &.czr-form-column-upload-com_limit-no-upload {
  611. .el-upload-com {
  612. :deep(.el-upload) {
  613. display: none;
  614. }
  615. }
  616. }
  617. &.czr-form-column-upload-com_card {
  618. .el-upload-com {
  619. :deep(.el-upload) {
  620. width: $uploadWidth;
  621. height: $uploadHeight;
  622. }
  623. :deep(.el-upload-list) {
  624. .el-upload-list__item {
  625. width: $uploadWidth;
  626. height: $uploadHeight;
  627. }
  628. }
  629. }
  630. }
  631. }
  632. </style>