index.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. <template>
  2. <div class="component-upload-image">
  3. <el-upload
  4. :action="uploadImgUrl"
  5. list-type="picture-card"
  6. :on-success="handleUploadSuccess"
  7. :before-upload="handleBeforeUpload"
  8. :limit="limit"
  9. :on-error="handleUploadError"
  10. :on-exceed="handleExceed"
  11. name="file"
  12. :on-remove="handleRemove"
  13. :show-file-list="true"
  14. :file-list="fileList"
  15. :on-preview="handlePictureCardPreview"
  16. :class="{hide: this.fileList.length >= this.limit}"
  17. >
  18. <i class="el-icon-plus"></i>
  19. </el-upload>
  20. <!-- 上传提示 -->
  21. <!-- <div class="el-upload__tip" slot="tip" v-if="showTip">
  22. 请上传
  23. <template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
  24. <template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
  25. 的文件
  26. </div> -->
  27. <el-dialog
  28. :visible.sync="dialogVisible"
  29. title="预览"
  30. width="800"
  31. append-to-body
  32. >
  33. <img
  34. :src="dialogImageUrl"
  35. style="display: block; max-width: 100%; margin: 0 auto"
  36. />
  37. </el-dialog>
  38. </div>
  39. </template>
  40. <script>
  41. import { getToken } from "@/utils/auth";
  42. import { Loading } from 'element-ui';
  43. export default {
  44. props: {
  45. value: [String, Object, Array],
  46. // 图片数量限制
  47. limit: {
  48. type: Number,
  49. default: 10,
  50. },
  51. // 大小限制(MB)
  52. fileSize: {
  53. type: Number,
  54. default: 500,
  55. },
  56. // 宽高比,如 "16:9";为空则不校验
  57. aspectRatio: {
  58. type: String,
  59. default: null,
  60. },
  61. // 宽高比允许误差(相对误差)
  62. aspectTolerance: {
  63. type: Number,
  64. default: 0.05,
  65. },
  66. // 文件类型, 例如['png', 'jpg', 'jpeg']
  67. fileType: {
  68. type: Array,
  69. default: () => ["png", "jpg", "jpeg"],
  70. },
  71. // 是否显示提示
  72. isShowTip: {
  73. type: Boolean,
  74. default: true
  75. }
  76. },
  77. data() {
  78. return {
  79. finalQuality:1,
  80. dialogImageUrl: "",
  81. dialogVisible: false,
  82. hideUpload: false,
  83. baseUrl: process.env.VUE_APP_BASE_API,
  84. uploadImgUrl: process.env.VUE_APP_BASE_API+"/common/uploadOSS", // 上传的图片服务器地址
  85. headers: {
  86. Authorization: "Bearer " + getToken(),
  87. },
  88. fileList: []
  89. };
  90. },
  91. watch: {
  92. value: {
  93. handler(val) {
  94. if (val) {
  95. // 首先将值转为数组
  96. const list = Array.isArray(val) ? val : this.value.split(',');
  97. // 然后将数组转为对象数组
  98. this.fileList = list.map(item => {
  99. if (typeof item === "string") {
  100. if (item.indexOf(this.baseUrl) === -1) {
  101. item = { name: item, url: item };
  102. } else {
  103. item = { name: item, url: item };
  104. }
  105. }
  106. return item;
  107. });
  108. } else {
  109. this.fileList = [];
  110. return [];
  111. }
  112. },
  113. deep: true,
  114. immediate: true
  115. }
  116. },
  117. computed: {
  118. // 是否显示提示
  119. showTip() {
  120. return this.isShowTip && (this.fileType || this.fileSize);
  121. },
  122. },
  123. methods: {
  124. // 删除图片
  125. handleRemove(file, fileList) {
  126. const findex = this.fileList.map(f => f.name).indexOf(file.name);
  127. if(findex > -1) {
  128. this.fileList.splice(findex, 1);
  129. this.$emit("input", this.listToString(this.fileList));
  130. }
  131. },
  132. // 上传成功回调
  133. handleUploadSuccess(res) {
  134. console.log(res)
  135. this.fileList.push({ name: res.url, url: res.url });
  136. this.$emit("input", this.listToString(this.fileList));
  137. this.loading.close();
  138. },
  139. // 上传前loading加载
  140. handleBeforeUpload(file) {
  141. let isImg = false;
  142. if (this.fileType.length) {
  143. let fileExtension = "";
  144. if (file.name.lastIndexOf(".") > -1) {
  145. fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
  146. }
  147. isImg = this.fileType.some(type => {
  148. if (file.type.indexOf(type) > -1) return true;
  149. if (fileExtension && fileExtension.indexOf(type) > -1) return true;
  150. return false;
  151. });
  152. } else {
  153. isImg = file.type.indexOf("image") > -1;
  154. }
  155. if (!isImg) {
  156. this.$message.error(
  157. `文件格式不正确, 请上传${this.fileType.join("/")}图片格式文件!`
  158. );
  159. return false;
  160. }
  161. // fileSize 默认 500 表示沿用历史 3MB;显式传入且 <=100 时按 MB 校验
  162. const maxMb = (this.fileSize != null && this.fileSize > 0 && this.fileSize <= 100) ? this.fileSize : 3;
  163. return new Promise((resolve, reject) => {
  164. const closeLoading = () => {
  165. if (this.loading && typeof this.loading.close === 'function') {
  166. this.loading.close();
  167. this.loading = null;
  168. }
  169. };
  170. const fail = (msg) => {
  171. if (msg) {
  172. this.$message.error(msg);
  173. }
  174. closeLoading();
  175. reject();
  176. };
  177. if (file.size / 1024 / 1024 > maxMb) {
  178. fail(`上传的图片不能超过${maxMb}MB`);
  179. return;
  180. }
  181. const afterRatioCheck = () => {
  182. // 校验通过后再进入「上传中」态
  183. this.loading = this.$loading({
  184. lock: true,
  185. text: "上传中",
  186. background: "rgba(0, 0, 0, 0.7)",
  187. });
  188. if (file.size / 1024 / 1024 > 1 && maxMb > 1) {
  189. const loadingInstance = Loading.service({ text: '图片内存过大正在压缩图片...' });
  190. this.compressImage(file).then((compressedFile) => {
  191. loadingInstance.close();
  192. if (compressedFile.size / 1024 / 1024 > maxMb) {
  193. fail(`图片压缩后仍大于${maxMb}MB`);
  194. } else {
  195. console.log(`图片压缩成功,最终质量为: ${this.finalQuality.toFixed(2)}`);
  196. console.log(`最终内存大小为: ${(compressedFile.size/1024).toFixed(2)}KB`);
  197. resolve(compressedFile);
  198. }
  199. }).catch((err) => {
  200. loadingInstance.close();
  201. console.error(err);
  202. fail();
  203. });
  204. } else {
  205. resolve(file);
  206. }
  207. };
  208. if (this.aspectRatio) {
  209. const parts = String(this.aspectRatio).split(':');
  210. const rw = Number(parts[0]);
  211. const rh = Number(parts[1]);
  212. if (!rw || !rh) {
  213. afterRatioCheck();
  214. return;
  215. }
  216. const expected = rw / rh;
  217. const reader = new FileReader();
  218. reader.onload = (e) => {
  219. const img = new Image();
  220. img.onload = () => {
  221. const actual = img.width / img.height;
  222. if (Math.abs(actual - expected) / expected > this.aspectTolerance) {
  223. fail(`封面比例须为 ${this.aspectRatio}(当前约 ${(actual).toFixed(2)})`);
  224. return;
  225. }
  226. afterRatioCheck();
  227. };
  228. img.onerror = () => {
  229. fail('无法读取图片尺寸');
  230. };
  231. img.src = e.target.result;
  232. };
  233. reader.onerror = () => {
  234. fail('无法读取图片');
  235. };
  236. reader.readAsDataURL(file);
  237. } else {
  238. afterRatioCheck();
  239. }
  240. });
  241. // if (this.fileSize) {
  242. // const isLt = file.size / 1024 < this.fileSize;
  243. // if (!isLt) {
  244. // this.$message.error(`上传头像图片大小不能超过 ${this.fileSize} KB!`);
  245. // return false;
  246. // }
  247. // }
  248. },
  249. compressImage(file) {
  250. return new Promise((resolve, reject) => {
  251. const reader = new FileReader();
  252. reader.readAsDataURL(file);
  253. reader.onload = (event) => {
  254. const img = new Image();
  255. img.src = event.target.result;
  256. img.onload = () => {
  257. const canvas = document.createElement('canvas');
  258. const ctx = canvas.getContext('2d');
  259. const width = img.width;
  260. const height = img.height;
  261. canvas.width = width;
  262. canvas.height = height;
  263. ctx.drawImage(img, 0, 0, width, height);
  264. let quality = 1; // 初始压缩质量
  265. let dataURL = canvas.toDataURL('image/jpeg', quality);
  266. // 逐步压缩,直到图片大小小于500KB并且压缩质量不再降低
  267. while (dataURL.length / 1024 > 500 && quality > 0.1) {
  268. quality -= 0.01;
  269. dataURL = canvas.toDataURL('image/jpeg', quality);
  270. }
  271. this.finalQuality = quality; // 存储最终的压缩质量
  272. if (dataURL.length / 1024 > 1000) {
  273. reject(new Error('压缩后图片仍然大于1000KB'));
  274. return;
  275. }
  276. const arr = dataURL.split(',');
  277. const mime = arr[0].match(/:(.*?);/)[1];
  278. const bstr = atob(arr[1]);
  279. let n = bstr.length;
  280. const u8arr = new Uint8Array(n);
  281. while (n--) {
  282. u8arr[n] = bstr.charCodeAt(n);
  283. }
  284. const compressedFile = new Blob([u8arr], { type: mime });
  285. compressedFile.name = file.name;
  286. resolve(compressedFile);
  287. };
  288. img.onerror = (error) => {
  289. reject(error);
  290. };
  291. };
  292. reader.onerror = (error) => {
  293. reject(error);
  294. };
  295. });
  296. },
  297. // 文件个数超出
  298. handleExceed() {
  299. this.$message.error(`上传文件数量不能超过 ${this.limit} 个!`);
  300. },
  301. // 上传失败
  302. handleUploadError() {
  303. this.$message({
  304. type: "error",
  305. message: "上传失败",
  306. });
  307. this.loading.close();
  308. },
  309. // 预览
  310. handlePictureCardPreview(file) {
  311. console.log(file)
  312. this.dialogImageUrl = file.url;
  313. this.dialogVisible = true;
  314. },
  315. // 对象转成指定字符串分隔
  316. listToString(list, separator) {
  317. let strs = "";
  318. separator = separator || ",";
  319. for (let i in list) {
  320. strs += list[i].url.replace(this.baseUrl, "") + separator;
  321. }
  322. return strs != '' ? strs.substr(0, strs.length - 1) : '';
  323. }
  324. }
  325. };
  326. </script>
  327. <style scoped lang="scss">
  328. // .el-upload--picture-card 控制加号部分
  329. ::v-deep.hide .el-upload--picture-card {
  330. display: none;
  331. }
  332. // 去掉动画效果
  333. ::v-deep .el-list-enter-active,
  334. ::v-deep .el-list-leave-active {
  335. transition: all 0s;
  336. }
  337. ::v-deep .el-list-enter, .el-list-leave-active {
  338. opacity: 0;
  339. transform: translateY(0);
  340. }
  341. </style>