index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. <template>
  2. <div class="fm-uplaod-container"
  3. :id="uploadId"
  4. >
  5. <draggable class="drag-img-list"
  6. v-model="fileList"
  7. v-bind="{group: uploadId, ghostClass: 'ghost', animation: 200}"
  8. :no-transition-on-drag="true"
  9. >
  10. <div
  11. :id="item.key"
  12. :style="{width: width+'px', height: height+'px'}"
  13. :class="{uploading: item.status=='uploading', 'is-success': item.status=='success', 'is-disabled': disabled}"
  14. class="upload-file" v-for="(item) in fileList" :key="item.key">
  15. <img :src="item.url" />
  16. <el-progress v-if="item.status=='uploading' && ui == 'element'" :width="miniWidth*0.9" class="upload-progress" type="circle" :percentage="item.percent"></el-progress>
  17. <a-progress v-if="item.status=='uploading' && ui == 'antd'" :width="miniWidth*0.9" class="upload-progress" type="circle" :percent="item.percent"></a-progress>
  18. <label class="item-status" v-if="item.status=='success'">
  19. <i class="el-icon-upload-success el-icon-check" v-if="ui=='element'"></i>
  20. <a-icon type="check" v-if="ui=='antd'" />
  21. </label>
  22. <div class="uplaod-action" :style="{height: miniWidth / 4 + 'px'}">
  23. <i class="fm-iconfont icon-tupianyulan" :title="$t('fm.upload.preview')" @click="handlePreviewFile(item.key)" :style="{'font-size': miniWidth/8+'px'}"></i>
  24. <i v-if="isEdit && !disabled && !printRead" class="fm-iconfont icon-sync1" :title="$t('fm.upload.edit')" @click="handleEdit(item.key)" :style="{'font-size': miniWidth/8+'px'}"></i>
  25. <i v-if="isDelete && fileList.length > min && !disabled && !printRead" class="fm-iconfont icon-delete" :title="$t('fm.upload.delete')" @click="handleRemove(item.key)" :style="{'font-size': miniWidth/8+'px'}"></i>
  26. </div>
  27. </div>
  28. </draggable>
  29. <div
  30. :class="{'is-disabled': disabled, 'el-upload': ui == 'element', 'el-upload--picture-card': ui == 'element', 'ant-upload': ui == 'antd', 'ant-upload-select' : ui == 'antd', 'ant-upload-select-picture-card': ui == 'antd'}"
  31. v-show="(!isQiniu || (isQiniu && token)) && fileList.length < limit"
  32. :style="{width: width+'px', height: height+'px'}"
  33. @click.self="handleAdd"
  34. v-if="!readonly"
  35. >
  36. <a-icon @click.native="handleAdd" type="plus" v-if="ui=='antd'" :style="{fontSize:miniWidth/4+'px',marginTop: (-miniWidth/8)+'px', marginLeft: (-miniWidth/8)+'px', position: 'absolute', left: '50%', top: '50%'}" />
  37. <i @click.self="handleAdd" v-if="ui == 'element'" class="el-icon-plus" :style="{fontSize:miniWidth/4+'px',marginTop: (-miniWidth/8)+'px', marginLeft: (-miniWidth/8)+'px'}"></i>
  38. <input accept="image/*" v-if="multiple" multiple ref="uploadInput" @change="handleChange" type="file" :style="{width: 0, height: 0}" name="file" class=" upload-input">
  39. <input accept="image/*" v-else ref="uploadInput" @change="handleChange" type="file" :style="{width:0, height: 0}" name="file" class=" upload-input">
  40. </div>
  41. </div>
  42. </template>
  43. <script>
  44. import Viewer from 'viewerjs'
  45. import Draggable from 'vuedraggable'
  46. import { EventBus } from '../../util/event-bus.js'
  47. import * as qiniu from 'qiniu-js'
  48. require('viewerjs/dist/viewer.css')
  49. import { executeExpression } from '../../util/expression'
  50. import {getToken} from "../../util/token";
  51. export default {
  52. components: {
  53. Draggable
  54. },
  55. props: {
  56. value: {
  57. type: Array,
  58. default: () => []
  59. },
  60. width: {
  61. type: Number,
  62. default: 100
  63. },
  64. height: {
  65. type: Number,
  66. default: 100
  67. },
  68. token: {
  69. type: String,
  70. default: ''
  71. },
  72. domain: {
  73. type: String,
  74. default: ''
  75. },
  76. multiple: {
  77. type: Boolean,
  78. default: false
  79. },
  80. limit: {
  81. type: Number,
  82. default: 9
  83. },
  84. isQiniu: {
  85. type: Boolean,
  86. default: false
  87. },
  88. isDelete: {
  89. type: Boolean,
  90. default: false
  91. },
  92. min: {
  93. type: Number,
  94. default: 0
  95. },
  96. meitu: {
  97. type: Boolean,
  98. default: false
  99. },
  100. isEdit: {
  101. type: Boolean,
  102. default: false
  103. },
  104. action: {
  105. type: String,
  106. default: ''
  107. },
  108. disabled: {
  109. type: Boolean,
  110. default: false
  111. },
  112. readonly: {
  113. type: Boolean,
  114. default: false
  115. },
  116. headers: {
  117. type: Array,
  118. default: () => []
  119. },
  120. ui: {
  121. type: String,
  122. default: 'element'
  123. },
  124. containerKey: {
  125. type: String,
  126. default: ''
  127. },
  128. withCredentials: {
  129. type: Boolean,
  130. default: false
  131. },
  132. printRead: {
  133. type: Boolean,
  134. default: false
  135. },
  136. onSelect: {
  137. type: Function,
  138. default: undefined
  139. }
  140. },
  141. inject: {
  142. formContext: {
  143. default: {}
  144. }
  145. },
  146. data () {
  147. return {
  148. fileList: this.value.map(item => {
  149. return {
  150. ...item,
  151. key: item.key ? item.key : (new Date().getTime()) + '_' + Math.ceil(Math.random() * 99999),
  152. }
  153. }),
  154. viewer: null,
  155. uploadId: 'upload_' + new Date().getTime(),
  156. editIndex: -1,
  157. meituIndex: -1,
  158. }
  159. },
  160. computed: {
  161. miniWidth () {
  162. if (this.width > this.height) {
  163. return this.height
  164. } else {
  165. return this.width
  166. }
  167. }
  168. },
  169. methods: {
  170. handleChange () {
  171. console.log(this.$refs.uploadInput.files)
  172. const files = this.$refs.uploadInput.files
  173. if (this.editIndex < 0 && this.fileList.length + files.length > this.limit) {
  174. this.$refs.uploadInput.value = []
  175. return false
  176. }
  177. for (let i = 0; i < files.length; i++) {
  178. let file = files[i]
  179. let rFile = this.onSelect(file)
  180. if (rFile === false) {
  181. return false
  182. }
  183. if (rFile instanceof File) {
  184. file = rFile
  185. }
  186. const reader = new FileReader()
  187. const key = (new Date().getTime()) + '_' + Math.ceil(Math.random() * 99999)
  188. reader.readAsDataURL(file)
  189. reader.onload = () => {
  190. if (this.editIndex >= 0) {
  191. this.$set(this.fileList, this.editIndex, {
  192. key,
  193. url: reader.result,
  194. percent: 0,
  195. status: 'uploading'
  196. })
  197. this.editIndex = -1
  198. } else {
  199. this.fileList.push({
  200. key,
  201. url: reader.result,
  202. percent: 0,
  203. status: 'uploading'
  204. })
  205. }
  206. this.$nextTick(() => {
  207. if (this.isQiniu) {
  208. this.uploadAction2(reader.result, file, key)
  209. } else {
  210. this.uploadAction(reader.result, file, key)
  211. }
  212. })
  213. }
  214. }
  215. this.$refs.uploadInput.value = []
  216. },
  217. uploadAction (res, file, key) {
  218. let changeIndex = this.fileList.findIndex(item => item.key === key)
  219. console.log(this.fileList.findIndex(item => item.key === key))
  220. const xhr = new XMLHttpRequest()
  221. const url = this.action
  222. xhr.open('POST', url, true)
  223. // xhr.setRequestHeader('Content-Type', 'multipart/form-data')
  224. this.headers.map(item => {
  225. item.key && xhr.setRequestHeader(item.key, item.fx ? executeExpression(item.value, {}, this.formContext) : item.value)
  226. })
  227. this.headers = [{
  228. key: Object.keys(getToken())[0],
  229. value: Object.values(getToken())[0]
  230. }]
  231. let formData = new FormData()
  232. formData.append('multiPartFile', file)
  233. formData.append('fname', file.name)
  234. formData.append('key', key)
  235. //formData.append('module', 'file')
  236. xhr.withCredentials = this.withCredentials
  237. xhr.onreadystatechange = () => {
  238. console.log(xhr)
  239. if (xhr.readyState === 4) {
  240. let resData = JSON.parse(xhr.response)
  241. if (resData && resData.code == 0) {
  242. this.$set(this.fileList, this.fileList.findIndex(item => item.key === key), {
  243. ...this.fileList[this.fileList.findIndex(item => item.key === key)],
  244. percent: 100,
  245. ...resData.data
  246. })
  247. setTimeout(() => {
  248. this.$set(this.fileList, this.fileList.findIndex(item => item.key === key), {
  249. ...this.fileList[this.fileList.findIndex(item => item.key === key)],
  250. status: 'success',
  251. ...resData.data
  252. })
  253. this.$emit('on-upload-success', {
  254. ...this.fileList[this.fileList.findIndex(item => item.key === key)],
  255. status: 'success',
  256. ...resData.data
  257. })
  258. console.log(this.fileList, '====');
  259. this.$emit('input', this.fileList)
  260. }, 200)
  261. } else if (resData && resData.url) {
  262. this.$set(this.fileList, this.fileList.findIndex(item => item.key === key), {
  263. ...this.fileList[this.fileList.findIndex(item => item.key === key)],
  264. url: resData.url,
  265. percent: 100,
  266. ...resData
  267. })
  268. setTimeout(() => {
  269. this.$set(this.fileList, this.fileList.findIndex(item => item.key === key), {
  270. ...this.fileList[this.fileList.findIndex(item => item.key === key)],
  271. status: 'success'
  272. })
  273. this.$emit('on-upload-success', {
  274. ...this.fileList[this.fileList.findIndex(item => item.key === key)],
  275. status: 'success'
  276. })
  277. this.$emit('input', this.fileList)
  278. }, 200)
  279. } else {
  280. this.$set(this.fileList, this.fileList.findIndex(item => item.key === key), {
  281. ...this.fileList[this.fileList.findIndex(item => item.key === key)],
  282. status: 'error'
  283. })
  284. this.$emit('on-upload-error', {
  285. ...this.fileList[this.fileList.findIndex(item => item.key === key)],
  286. status: 'error'
  287. })
  288. this.fileList.splice(this.fileList.findIndex(item => item.key === key), 1)
  289. }
  290. }
  291. }
  292. xhr.upload.onprogress = (res) => {
  293. console.log('progress', res)
  294. if (res.total && res.loaded) {
  295. this.$set(this.fileList[this.fileList.findIndex(item => item.key === key)], 'percent', (res.loaded == res.total ? 99 : parseInt(res.loaded/res.total*100)))
  296. this.$emit('on-upload-progress', {
  297. ...this.fileList[this.fileList.findIndex(item => item.key === key)],
  298. status: 'uploading',
  299. percent: (res.loaded == res.total ? 99 : parseInt(res.loaded/res.total*100))
  300. })
  301. }
  302. }
  303. xhr.send(formData)
  304. },
  305. uploadAction2 (res, file, key) {
  306. const _this = this
  307. const observable = qiniu.upload(file, key, this.token, {
  308. fname: key,
  309. mimeType: []
  310. }, {
  311. useCdnDomain: true,
  312. // region: qiniu.region.z2
  313. })
  314. observable.subscribe({
  315. next (res) {
  316. _this.$set(_this.fileList[_this.fileList.findIndex(item => item.key === key)], 'percent', parseInt(res.total.percent))
  317. _this.$emit('on-upload-progress', {
  318. ..._this.fileList[_this.fileList.findIndex(item => item.key === key)],
  319. status: 'uploading',
  320. percent: parseInt(res.total.percent)
  321. })
  322. },
  323. error (err) {
  324. _this.$message.error(err.message)
  325. _this.$set(_this.fileList, _this.fileList.findIndex(item => item.key === key), {
  326. ..._this.fileList[_this.fileList.findIndex(item => item.key === key)],
  327. status: 'error'
  328. })
  329. _this.$emit('on-upload-error', {
  330. ..._this.fileList[_this.fileList.findIndex(item => item.key === key)],
  331. status: 'error'
  332. })
  333. _this.fileList.splice(_this.fileList.findIndex(item => item.key === key), 1)
  334. },
  335. complete (res) {
  336. _this.$set(_this.fileList, _this.fileList.findIndex(item => item.key === key), {
  337. ..._this.fileList[_this.fileList.findIndex(item => item.key === key)],
  338. url: _this.domain + res.key,
  339. percent: 100,
  340. ...res,
  341. })
  342. setTimeout(() => {
  343. _this.$set(_this.fileList, _this.fileList.findIndex(item => item.key === key), {
  344. ..._this.fileList[_this.fileList.findIndex(item => item.key === key)],
  345. status: 'success'
  346. })
  347. _this.$emit('on-upload-success', {
  348. ..._this.fileList[_this.fileList.findIndex(item => item.key === key)],
  349. status: 'success'
  350. })
  351. _this.$emit('input', _this.fileList)
  352. }, 200)
  353. }
  354. })
  355. },
  356. handleRemove (key) {
  357. this.$emit('on-upload-remove', this.fileList[this.fileList.findIndex(item => item.key === key)])
  358. this.fileList.splice(this.fileList.findIndex(item => item.key === key), 1)
  359. this.$nextTick(() => {
  360. this.$emit('input', this.fileList)
  361. })
  362. },
  363. handleEdit (key) {
  364. this.editIndex = this.fileList.findIndex(item => item.key === key)
  365. this.$refs.uploadInput.click()
  366. },
  367. handleMeitu (key) {
  368. this.$emit('on-meitu', this.fileList.findIndex(item => item.key === key))
  369. },
  370. handleAdd () {
  371. if (!this.disabled) {
  372. this.editIndex = -1
  373. this.$refs.uploadInput.click()
  374. }
  375. },
  376. handlePreviewFile (key) {
  377. this.viewer && this.viewer.destroy()
  378. this.uploadId = 'upload_' + new Date().getTime()
  379. this.$nextTick(() => {
  380. this.viewer = new Viewer(document.getElementById(this.uploadId))
  381. this.viewer.view(this.fileList.findIndex(item => item.key === key))
  382. })
  383. }
  384. },
  385. watch: {
  386. value (val) {
  387. this.fileList = this.value.map(item => {
  388. return {
  389. ...item,
  390. key: item.key ? item.key : (new Date().getTime()) + '_' + Math.ceil(Math.random() * 99999),
  391. }
  392. })
  393. }
  394. }
  395. }
  396. </script>
  397. <style lang="scss">
  398. .fm-uplaod-container{
  399. .is-disabled{
  400. position: relative;
  401. &::before{
  402. position: absolute;
  403. top: 0;
  404. bottom: 0;
  405. left: 0;
  406. right: 0;
  407. // background: rgba(0,0,0,.1);
  408. content: '';
  409. display: block;
  410. cursor:not-allowed;
  411. }
  412. }
  413. .upload-file{
  414. margin: 0 10px 10px 0;
  415. display: inline-flex;
  416. justify-content: center;
  417. align-items: center;
  418. // background: #fff;
  419. overflow: hidden;
  420. background-color: #fff;
  421. border: 1px solid #c0ccda;
  422. border-radius: 6px;
  423. box-sizing: border-box;
  424. position: relative;
  425. vertical-align: top;
  426. &:hover{
  427. .uplaod-action{
  428. display: flex;
  429. }
  430. }
  431. .uplaod-action{
  432. position: absolute;
  433. // top: 0;
  434. // height: 30px;
  435. bottom: 0;
  436. left: 0;
  437. right: 0;
  438. background: rgba(0,0,0,0.6);
  439. display: none;
  440. justify-content: center;
  441. align-items: center;
  442. i{
  443. color: #fff;
  444. cursor: pointer;
  445. margin: 0 5px;
  446. }
  447. }
  448. &.is-success{
  449. .item-status{
  450. position: absolute;
  451. right: -15px;
  452. top: -6px;
  453. width: 40px;
  454. height: 24px;
  455. background: #13ce66;
  456. text-align: center;
  457. transform: rotate(45deg);
  458. box-shadow: 0 0 1pc 1px rgba(0,0,0,.2);
  459. &>i{
  460. font-size: 12px;
  461. margin-top: 11px;
  462. color: #fff;
  463. transform: rotate(-45deg);
  464. }
  465. }
  466. }
  467. &.uploading{
  468. &:before{
  469. display: block;
  470. content: '';
  471. position: absolute;
  472. top: 0;
  473. left: 0;
  474. right: 0;
  475. bottom: 0;
  476. background: rgba(0,0,0,0.3);
  477. }
  478. }
  479. .upload-progress{
  480. position: absolute;
  481. .el-progress__text{
  482. color: #fff;
  483. font-size: 16px !important;
  484. }
  485. }
  486. img{
  487. max-width: 100%;
  488. max-height: 100%;
  489. vertical-align: middle;
  490. }
  491. }
  492. .el-upload--picture-card{
  493. position: relative;
  494. overflow: hidden;
  495. .el-icon-plus{
  496. position: absolute;
  497. top: 50%;
  498. left: 50%;
  499. }
  500. }
  501. .upload-input{
  502. position: absolute;
  503. top: 0;
  504. left: 0;
  505. right: 0;
  506. bottom: 0;
  507. display: block;
  508. opacity: 0;
  509. cursor: pointer;
  510. }
  511. .drag-img-list{
  512. display: inline;
  513. .ghost{
  514. position: relative;
  515. &::after {
  516. width: 100%;
  517. height: 100%;
  518. display: block;
  519. content: '';
  520. background: #fbfdff;
  521. position: absolute;
  522. top: 0;
  523. bottom: 0;
  524. left: 0;
  525. right: 0;
  526. border: 1px dashed #3bb3c2;
  527. }
  528. }
  529. &>div{
  530. cursor: move;
  531. }
  532. }
  533. .ant-upload{
  534. display: inline-block;
  535. position: relative;
  536. &.ant-upload-select-picture-card{
  537. float: none;
  538. }
  539. }
  540. }
  541. .viewer-container{
  542. z-index: 9999 !important;
  543. }
  544. </style>