index.vue 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. <template>
  2. <div class="sc-upload" :class="{ 'sc-upload-round': round }" :style="style">
  3. <div v-if="file && file.status != 'success'" class="sc-upload__uploading">
  4. <div class="sc-upload__progress">
  5. <el-progress :percentage="file.percentage" text-inside :stroke-width="16" />
  6. </div>
  7. <el-image class="image" :src="file.tempFile" fit="cover"></el-image>
  8. </div>
  9. <div v-if="file && file.status=='success'" class="sc-upload__img">
  10. <el-image v-if="isImage(file.mineType)" class="image" :src="'/zcxt/folder/' + file.path" :preview-src-list="['/zcxt/folder/' + file.path]" fit="cover" preview-teleported :z-index="9999">
  11. <template #placeholder>
  12. <div class="sc-upload__img-slot">Loading...</div>
  13. </template>
  14. </el-image>
  15. <sc-video v-if="isVideo(file.mineType)" :src="'/zcxt/folder/' + file.path" showMask @play="videoPlay"></sc-video>
  16. <div class="sc-upload__img-actions" v-if="!disabled">
  17. <span class="del" @click="handleRemove()"><el-icon><el-icon-delete /></el-icon></span>
  18. </div>
  19. </div>
  20. <el-upload v-if="!file" class="uploader" ref="uploader"
  21. action=""
  22. :auto-upload="cropper ? false : true"
  23. :disabled="disabled"
  24. :show-file-list="false"
  25. :accept="accept"
  26. :limit="1"
  27. :http-request="request"
  28. :on-change="change"
  29. :before-upload="before"
  30. :on-success="success"
  31. :on-error="error"
  32. :on-exceed="handleExceed">
  33. <slot>
  34. <div class="el-upload--picture-card" :class="disabled && 'is-disabled'">
  35. <div class="file-empty">
  36. <el-icon><component :is="icon" /></el-icon>
  37. <h4 v-if="title">{{ title }}</h4>
  38. </div>
  39. </div>
  40. </slot>
  41. </el-upload>
  42. <span style="display:none!important"><el-input v-model="value"></el-input></span>
  43. <el-dialog title="剪裁" draggable v-model="cropperDialogVisible" :width="580" @closed="cropperClosed" destroy-on-close>
  44. <sc-cropper :src="cropperFile.tempCropperFile" :compress="compress" :aspectRatio="aspectRatio" ref="cropper"></sc-cropper>
  45. <template #footer>
  46. <el-button @click="cropperDialogVisible=false" >取 消</el-button>
  47. <el-button type="primary" @click="cropperSave">确 定</el-button>
  48. </template>
  49. </el-dialog>
  50. </div>
  51. <sc-video-viewer v-if="showVideoViewer" :videoUrl="previewVideoUrl" hideOnModal @close="showVideoViewer = false"></sc-video-viewer>
  52. </template>
  53. <script>
  54. import config from "@/config/upload";
  55. export default {
  56. props: {
  57. modelValue: { type: Object, default: () => {} },
  58. width: { type: Number, default: 148 },
  59. height: { type: Number, default: 148 },
  60. title: { type: String, default: "" },
  61. accept: { type: String, default: "image/gif, image/jpeg, image/png, video/mp4 , video/avi" },
  62. icon: { type: String, default: "el-icon-plus" },
  63. maxSize: { type: Number, default: 50 },
  64. disabled: { type: Boolean, default: false },
  65. round: { type: Boolean, default: false },
  66. onSuccess: { type: Function, default: () => { return true } },
  67. cropper: { type: Boolean, default: false },
  68. compress: { type: Number, default: 1 },
  69. aspectRatio: {type: Number, default: NaN }
  70. },
  71. data() {
  72. return {
  73. value: "{}",
  74. file: null,
  75. style: {
  76. width: this.width + "px",
  77. height: this.height + "px"
  78. },
  79. cropperDialogVisible: false,
  80. cropperFile: null,
  81. showVideoViewer: false,
  82. previewVideoUrl: ""
  83. }
  84. },
  85. watch: {
  86. modelValue(val) {
  87. this.value = JSON.stringify(val);
  88. this.newFile(val);
  89. },
  90. value(val) {
  91. this.$emit("update:modelValue", JSON.parse(val));
  92. }
  93. },
  94. mounted() {
  95. if (this.modelValue) {
  96. this.value = JSON.stringify(this.modelValue);
  97. this.newFile(this.modelValue);
  98. }
  99. },
  100. methods: {
  101. isImage(type) {
  102. return config.imageIncludes(type);
  103. },
  104. isVideo(type) {
  105. return config.videoIncludes(type);
  106. },
  107. newFile(data) {
  108. this.file = Object.keys(data).length ? { status: "success", ...data } : null;
  109. },
  110. cropperSave() {
  111. this.$refs.cropper.getCropFile(file => {
  112. file.uid = this.cropperFile.uid;
  113. this.cropperFile.raw = file;
  114. this.file = this.cropperFile;
  115. this.file.tempFile = URL.createObjectURL(this.file.raw);
  116. this.$refs.uploader.submit();
  117. }, this.cropperFile.name, this.cropperFile.type);
  118. this.cropperDialogVisible = false;
  119. },
  120. cropperClosed() {
  121. URL.revokeObjectURL(this.cropperFile.tempCropperFile);
  122. delete this.cropperFile.tempCropperFile;
  123. },
  124. handleRemove() {
  125. const file = JSON.parse(this.value)
  126. this.$confirm(`是否移除 ${file.name}? 此操作不可逆!`, "提示", {
  127. type: "warning",
  128. confirmButtonText: "移除"
  129. }).then(() => {
  130. if (file.id) {
  131. this.$API.common.folder.rm(file.id).then(res => {
  132. if (res.code == 200) this.clearFiles();
  133. }).catch(() => {});
  134. } else this.clearFiles();
  135. }).catch(() => {});
  136. },
  137. clearFiles() {
  138. URL.revokeObjectURL(this.file.tempFile);
  139. this.value = "{}";
  140. this.file = null;
  141. this.$nextTick(() => this.$refs.uploader.clearFiles());
  142. },
  143. change(file, files) {
  144. if (files.length > 1) files.splice(0, 1);
  145. if (this.cropper && file.status == "ready") {
  146. if (!this.isImage(file.raw.type)) return false;
  147. this.cropperFile = file;
  148. this.cropperFile.tempCropperFile = URL.createObjectURL(file.raw);
  149. this.cropperDialogVisible = true;
  150. return false;
  151. }
  152. this.file = file;
  153. if (file.status == "ready") file.tempFile = URL.createObjectURL(file.raw);
  154. },
  155. before(file) {
  156. if (!this.isImage(file.type) && !this.isVideo(file.type)) {
  157. this.$message.warning({ title: "上传文件警告", message: "选择的文件非图像类/视频类文件" });
  158. this.clearFiles();
  159. return false;
  160. }
  161. const maxSize = file.size / 1024 / 1024 < this.maxSize;
  162. if (!maxSize) {
  163. this.$message.warning(`上传文件大小不能超过 ${this.maxSize}MB!`);
  164. this.clearFiles();
  165. return false;
  166. }
  167. },
  168. handleExceed(files) {
  169. const file = files[0];
  170. file.uid = genFileId();
  171. this.$refs.uploader.handleStart(file);
  172. },
  173. success(res, file) {
  174. // 释放内存删除blob
  175. URL.revokeObjectURL(file.tempFile);
  176. delete file.tempFile;
  177. let os = this.onSuccess(res, file);
  178. if (os != undefined && os == false) {
  179. this.$nextTick(() => {
  180. this.file = null;
  181. this.value = "{}";
  182. });
  183. return false;
  184. }
  185. file.name = res.fileName;
  186. file.path = res.path;
  187. file.mineType = res.mineType;
  188. this.value = JSON.stringify({ path: res.path, name: res.fileName, mineType: res.mineType });
  189. },
  190. error(message) {
  191. this.$nextTick(() => this.clearFiles());
  192. this.$notify.error({ title: "上传文件未成功", message });
  193. },
  194. request(param) {
  195. const data = new FormData();
  196. data.append(param.filename, param.file);
  197. this.$API.common.folder.up(data, {
  198. onUploadProgress: e => {
  199. const percent = parseInt(((e.loaded / e.total) * 100) | 0, 10);
  200. param.onProgress({ percent });
  201. }
  202. }).then(res => {
  203. if (res.code == 200) param.onSuccess({ path: res.expands.file, fileName: param.file.name, mineType: param.file.type });
  204. else param.onError(res.message || "未知错误");
  205. }).catch(err => param.onError(err));
  206. },
  207. videoPlay() {
  208. this.showVideoViewer = true;
  209. this.previewVideoUrl = "/zcxt/folder/" + this.file.path;
  210. }
  211. }
  212. }
  213. </script>
  214. <style scoped>
  215. .el-form-item.is-error .sc-upload .el-upload--picture-card {
  216. border-color: var(--el-color-danger);
  217. }
  218. .sc-upload .el-upload--picture-card {
  219. width: 100%;
  220. height: 100%;
  221. border-radius: 0;
  222. }
  223. .sc-upload .el-upload--picture-card.is-disabled {
  224. border-color: var(--el-border-color-darker);
  225. cursor: not-allowed;
  226. }
  227. .sc-upload .uploader,
  228. .sc-upload:deep(.el-upload) {
  229. width: 100%;
  230. height: 100%;
  231. justify-content: unset;
  232. }
  233. .sc-upload__img {
  234. width: 100%;
  235. height: 100%;
  236. position: relative;
  237. }
  238. .sc-upload__img .image {
  239. width: 100%;
  240. height: 100%;
  241. border: 1px solid var(--el-border-color);
  242. }
  243. .sc-upload__img-actions {
  244. position: absolute;
  245. top: 0;
  246. right: 0;
  247. display: none;
  248. }
  249. .sc-upload__img-actions span {
  250. display: flex;
  251. justify-content: center;
  252. align-items: center;
  253. width: 25px;
  254. height: 25px;
  255. cursor: pointer;
  256. color: #fff;
  257. }
  258. .sc-upload__img-actions span i {
  259. font-size: 12px;
  260. }
  261. .sc-upload__img-actions .del {
  262. background: #f56c6c;
  263. }
  264. .sc-upload__img:hover .sc-upload__img-actions {
  265. display: block;
  266. }
  267. .sc-upload__img-slot {
  268. display: flex;
  269. justify-content: center;
  270. align-items: center;
  271. width: 100%;
  272. height: 100%;
  273. font-size: 12px;
  274. background-color: var(--el-fill-color-lighter);
  275. }
  276. .sc-upload__uploading {
  277. width: 100%;
  278. height: 100%;
  279. position: relative;
  280. }
  281. .sc-upload__progress {
  282. position: absolute;
  283. width: 100%;
  284. height: 100%;
  285. display: flex;
  286. justify-content: center;
  287. align-items: center;
  288. background-color: var(--el-overlay-color-lighter);
  289. z-index: 1;
  290. padding: 10px;
  291. }
  292. .sc-upload__progress .el-progress {
  293. width: 100%;
  294. }
  295. .sc-upload__uploading .image {
  296. width: 100%;
  297. height: 100%;
  298. }
  299. .sc-upload .file-empty {
  300. width: 100%;
  301. height: 100%;
  302. display: flex;
  303. justify-content: center;
  304. align-items: center;
  305. flex-direction: column;
  306. }
  307. .sc-upload .file-empty i {
  308. font-size: 28px;
  309. }
  310. .sc-upload .file-empty h4 {
  311. font-size: 12px;
  312. font-weight: normal;
  313. color: #8c939d;
  314. margin-top: 8px;
  315. }
  316. .sc-upload.sc-upload-round {
  317. border-radius: 50%;
  318. overflow: hidden;
  319. }
  320. .sc-upload.sc-upload-round .el-upload--picture-card {
  321. border-radius: 50%;
  322. }
  323. .sc-upload.sc-upload-round .sc-upload__img-actions {
  324. top: auto;
  325. left: 0;
  326. right: 0;
  327. bottom: 0;
  328. }
  329. .sc-upload.sc-upload-round .sc-upload__img-actions span {
  330. width: 100%;
  331. }
  332. </style>