multiple.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. <template>
  2. <div class="sc-upload-multiple">
  3. <el-upload ref="uploader" list-type="picture-card"
  4. v-model:file-list="defaultFileList"
  5. action=""
  6. :accept="accept"
  7. :limit="limit"
  8. :multiple="multiple"
  9. :disabled="disabled"
  10. show-file-list
  11. :http-request="request"
  12. :before-upload="before"
  13. :on-success="success"
  14. :on-error="error"
  15. :on-exceed="handleExceed">
  16. <slot>
  17. <el-icon><el-icon-plus /></el-icon>
  18. </slot>
  19. <template #tip>
  20. <div v-if="tip" class="el-upload__tip">{{ tip }}</div>
  21. </template>
  22. <template #file="{ file }">
  23. <div class="sc-upload-list-item">
  24. <el-image v-if="isImage(file.mineType)" class="el-upload-list__item-thumbnail" :src="'/api/folder/' + file.path" :preview-src-list="preview" fit="cover" preview-teleported :z-index="9999">
  25. <template #placeholder>
  26. <div class="sc-upload-multiple-image-slot">Loading...</div>
  27. </template>
  28. </el-image>
  29. <sc-video v-if="isVideo(file.mineType)" :src="'/api/folder/' + file.path" showMask @play="videoPlay(file)"></sc-video>
  30. <div v-if="!disabled && file.status == 'success'" class="sc-upload__item-actions">
  31. <el-button class="download" :loading="loading" type="primary" @click="handleDownload(file)">
  32. <sc-iconify icon="ant-design:download-outlined"></sc-iconify>
  33. </el-button>
  34. <el-button type="danger" @click="handleRemove(file)">
  35. <sc-iconify icon="ant-design:delete-outlined"></sc-iconify>
  36. </el-button>
  37. </div>
  38. <div v-if="file.status == 'ready' || file.status == 'uploading'" class="sc-upload__item-progress">
  39. <el-progress :percentage="file.percentage" text-inside :stroke-width="16" />
  40. </div>
  41. </div>
  42. </template>
  43. </el-upload>
  44. <span style="display:none!important"><el-input v-model="value"></el-input></span>
  45. </div>
  46. <file-viewer v-if="showViewer" ref="fileViewer" @closed="showViewer = false"></file-viewer>
  47. </template>
  48. <script>
  49. import { fileTypes } from "./main";
  50. export default {
  51. props: {
  52. modelValue: { type: Array, default: () => [] },
  53. accept: { type: String, default: "image/gif, image/jpeg, image/png, video/mp4, video/avi" },
  54. tip: { type: String, default: "" },
  55. maxSize: { type: Number, default: 50 },
  56. limit: { type: Number, default: 0 },
  57. multiple: { type: Boolean, default: true },
  58. disabled: { type: Boolean, default: false },
  59. onSuccess: { type: Function, default: () => { return true } }
  60. },
  61. data() {
  62. return {
  63. value: "",
  64. defaultFileList: [],
  65. loading: false,
  66. showViewer: false
  67. }
  68. },
  69. watch: {
  70. modelValue(val) {
  71. if (JSON.stringify(val) != JSON.stringify(this.formatArr(this.defaultFileList))) {
  72. this.defaultFileList = val;
  73. this.value = val;
  74. }
  75. },
  76. defaultFileList: {
  77. deep: true,
  78. handler(val) {
  79. this.$emit("update:modelValue", this.formatArr(val));
  80. this.value = val.map(v => v.path).join(",");
  81. }
  82. }
  83. },
  84. computed: {
  85. preview() {
  86. return this.defaultFileList.map(v => "/api/folder/" + v.path);
  87. }
  88. },
  89. mounted() {
  90. this.defaultFileList = this.modelValue;
  91. this.value = this.modelValue;
  92. },
  93. methods: {
  94. isImage(type) {
  95. return fileTypes[type] == "image"
  96. },
  97. isVideo(type) {
  98. return fileTypes[type] == "video"
  99. },
  100. // 格式化数组值
  101. formatArr(arr) {
  102. return arr.map(item => ({ id: item.id, name: item.name, mineType: item.mineType, path: item.path }));
  103. },
  104. before(file) {
  105. if (!this.isImage(file.type) && !this.isVideo(file.type)) {
  106. this.$message.warning({ title: "上传文件警告", message: "选择的文件非图像类/视频类文件" });
  107. return false;
  108. }
  109. const maxSize = file.size / 1024 / 1024 < this.maxSize;
  110. if (!maxSize) {
  111. this.$message.warning(`上传文件大小不能超过 ${this.maxSize}MB!`);
  112. return false;
  113. }
  114. },
  115. success(res, file) {
  116. let os = this.onSuccess(res, file);
  117. if (os != undefined && os == false) return false;
  118. file.name = res.fileName;
  119. file.path = res.path;
  120. file.mineType = res.mineType;
  121. },
  122. error(message) {
  123. message && this.$notify.error({ title: "上传文件未成功", message });
  124. },
  125. beforeRemove({ id, name }) {
  126. return this.$confirm(`是否移除 ${name}? 此操作不可逆!`, "提示", {
  127. type: "warning",
  128. confirmButtonText: "移除"
  129. }).then(() => {
  130. if (id) {
  131. this.$API.common.folder.rm(id).then(res => {
  132. if (res.code == 200) return true;
  133. else return false;
  134. }).catch(() => {
  135. return false;
  136. });
  137. } return true;
  138. }).catch(() => {
  139. return false;
  140. });
  141. },
  142. handleRemove(file) {
  143. this.$refs.uploader.handleRemove(file);
  144. },
  145. handleExceed() {
  146. this.$message.warning(`当前设置最多上传 ${this.limit} 个文件,请移除后上传!`);
  147. },
  148. request(param) {
  149. const data = new FormData();
  150. data.append(param.filename, param.file);
  151. this.$API.common.folder.up(data, {
  152. onUploadProgress: e => {
  153. const percent = parseInt(((e.loaded / e.total) * 100) | 0, 10)
  154. param.onProgress({ percent });
  155. }
  156. }).then(res => {
  157. if (res.code == 200) param.onSuccess({ path: res.expands.file, fileName: param.file.name, mineType: param.file.type })
  158. else param.onError();
  159. }).catch(() => param.onError());
  160. },
  161. videoPlay(file) {
  162. this.showViewer = true;
  163. nextTick(() => this.$refs.fileViewer.init(file));
  164. },
  165. handleDownload(file) {
  166. this.loading = true;
  167. this.$API.common.folder.download(file.path).then(res => {
  168. this.loading = false;
  169. const a = document.createElement("a");
  170. const blob = new Blob([res], { type: file.mineType });
  171. a.download = this.file.name;
  172. a.href = URL.createObjectURL(blob);
  173. a.click();
  174. }).catch(() => this.loading = false);
  175. }
  176. }
  177. }
  178. </script>
  179. <style scoped>
  180. .el-form-item.is-error .sc-upload-multiple:deep(.el-upload--picture-card) {
  181. border-color: var(--el-color-danger);
  182. }
  183. :deep(.el-upload-list__item) {
  184. transition: none;
  185. border-radius: 0;
  186. }
  187. .sc-upload-multiple:deep(.el-upload-list__item.el-list-leave-active) {
  188. position: static !important;
  189. }
  190. .sc-upload-multiple:deep(.el-upload--picture-card) {
  191. border-radius: 0;
  192. }
  193. .sc-upload-list-item {
  194. width: 100%;
  195. height: 100%;
  196. position: relative;
  197. }
  198. .sc-upload-multiple .el-image {
  199. display: block;
  200. }
  201. .sc-upload-multiple .el-image:deep(img) {
  202. -webkit-user-drag: none;
  203. }
  204. .sc-upload-multiple-image-slot {
  205. display: flex;
  206. justify-content: center;
  207. align-items: center;
  208. width: 100%;
  209. height: 100%;
  210. font-size: 12px;
  211. }
  212. .sc-upload-multiple .el-upload-list__item:hover .sc-upload__item-actions {
  213. display: block;
  214. }
  215. .sc-upload__item-actions {
  216. position: absolute;
  217. top: 0;
  218. right: 0;
  219. display: none;
  220. }
  221. .sc-upload__item-actions span {
  222. display: flex;
  223. justify-content: center;
  224. align-items: center;
  225. width: 25px;
  226. height: 25px;
  227. cursor: pointer;
  228. color: #fff;
  229. }
  230. .sc-upload__item-actions span i {
  231. font-size: 12px;
  232. }
  233. .sc-upload__item-progress {
  234. position: absolute;
  235. width: 100%;
  236. height: 100%;
  237. top: 0;
  238. left: 0;
  239. background-color: var(--el-overlay-color-lighter);
  240. }
  241. </style>