upload.vue 17 KB

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