upload.vue 19 KB

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