index.vue 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. <template>
  2. <u-upload
  3. :name="name"
  4. :width="width"
  5. :height="height"
  6. :accept="accept"
  7. :fileList="fileList"
  8. :multiple="multiple"
  9. :maxCount="maxCount"
  10. :disabled="disabled"
  11. v-if="$slots.content"
  12. @afterRead="afterRead"
  13. @delete="onDelete">
  14. <slot name="content" :data="fileList"></slot>
  15. </u-upload>
  16. <u-upload
  17. :name="name"
  18. :width="width"
  19. :height="height"
  20. :accept="accept"
  21. :fileList="fileList"
  22. :multiple="multiple"
  23. :maxCount="maxCount"
  24. :disabled="disabled"
  25. v-else
  26. @afterRead="afterRead"
  27. @delete="onDelete">
  28. </u-upload>
  29. </template>
  30. <script>
  31. // import baseUrl from '@/common/config.js';
  32. import configService from '@/common/service/config.service';
  33. export default {
  34. name: "",
  35. props: {
  36. api: {
  37. type: String,
  38. default: '/sys/common/upload'
  39. },
  40. name: {
  41. type: String,
  42. default: (() => {
  43. try {
  44. return uni.$u && uni.$u.props && uni.$u.props.upload && uni.$u.props.upload.name || 'file'
  45. } catch (e) {
  46. return 'file'
  47. }
  48. })()
  49. },
  50. accept: {
  51. type: String,
  52. default: (() => {
  53. try {
  54. return uni.$u && uni.$u.props && uni.$u.props.upload && uni.$u.props.upload.accept || 'image'
  55. } catch (e) {
  56. return 'image'
  57. }
  58. })()
  59. },
  60. width: {
  61. type: [String, Number],
  62. default: (() => {
  63. try {
  64. return uni.$u && uni.$u.props && uni.$u.props.upload && uni.$u.props.upload.width || 150
  65. } catch (e) {
  66. return 150
  67. }
  68. })()
  69. },
  70. height: {
  71. type: [String, Number],
  72. default: (() => {
  73. try {
  74. return uni.$u && uni.$u.props && uni.$u.props.upload && uni.$u.props.upload.height || 150
  75. } catch (e) {
  76. return 150
  77. }
  78. })()
  79. },
  80. maxCount: {
  81. type: Number,
  82. default: 5
  83. },
  84. multiple: {
  85. type: Boolean,
  86. default: true
  87. },
  88. disabled: {
  89. type: Boolean,
  90. default: false
  91. },
  92. // 初始图片列表
  93. initList: {
  94. type: Array,
  95. default: () => []
  96. }
  97. },
  98. data() {
  99. return {
  100. fileList: [],
  101. };
  102. },
  103. watch: {
  104. // 更新fileList
  105. initList: {
  106. handler(newVal) {
  107. if (newVal && newVal.length > 0) {
  108. this.fileList = newVal.map(item => ({
  109. ...item,
  110. status: 'success',
  111. message: ''
  112. }));
  113. } else {
  114. this.fileList = [];
  115. }
  116. },
  117. immediate: true,
  118. deep: true
  119. }
  120. },
  121. computed: {
  122. },
  123. mounted() {
  124. },
  125. beforeDestroy() {
  126. },
  127. methods: {
  128. // 上传后的处理函数
  129. async afterRead(event) {
  130. // 当设置 multiple 为 true 时, file 为数组格式,否则为对象格式
  131. let lists = [].concat(event.file);
  132. let fileListLen = this[`fileList`].length;
  133. // 遍历文件,对图片进行压缩(支持H5、App、小程序三端)
  134. // 支持两种判断方式:1. MIME类型以image/开头 2. 文件后缀为.jpg
  135. for (let i = 0; i < lists.length; i++) {
  136. const item = lists[i];
  137. const isImageType = item.type && item.type.startsWith('image');
  138. const isJpgFile = item.name && (item.name.toLowerCase().endsWith('.jpg') || item.name.toLowerCase().endsWith('.jpeg'));
  139. if (isImageType || isJpgFile) {
  140. // 压缩后可能是 File 对象(H5)或 URL 字符串(App/小程序)
  141. const compressedResult = await this.compressImage(item.url);
  142. // 保存原始 URL 用于预览,压缩结果用于上传
  143. lists[i].compressedFile = compressedResult;
  144. }
  145. }
  146. lists.map((item) => {
  147. this[`fileList`].push({
  148. ...item,
  149. status: "uploading",
  150. message: "上传中",
  151. localUrl: item.url, // 保存原始本地路径用于预览
  152. });
  153. });
  154. // 无预览时,使用toast
  155. if (!this.$attrs.previewImage) {
  156. uni.showLoading({
  157. title: '上传中...'
  158. });
  159. }
  160. for (let i = 0; i < lists.length; i++) {
  161. // 使用压缩后的文件或原文件上传
  162. const uploadTarget = lists[i].compressedFile || lists[i].url;
  163. const result = await this.uploadFilePromise(uploadTarget);
  164. let item = this[`fileList`][fileListLen];
  165. this[`fileList`].splice(
  166. fileListLen,
  167. 1,
  168. Object.assign(item, {
  169. status: "success",
  170. message: "",
  171. url: configService.apiUrl + '/' + result,
  172. localUrl: item.localUrl
  173. })
  174. );
  175. fileListLen++;
  176. // 无预览时,使用toast
  177. if (!this.$attrs.previewImage) {
  178. uni.hideLoading();
  179. uni.showToast({
  180. icon: 'none',
  181. title: result.message || '上传完成',
  182. duration: 2000
  183. });
  184. this.$emit('complete');
  185. }
  186. }
  187. this.$emit('fileList', this.fileList);
  188. },
  189. // 图片压缩 - 支持H5、App、小程序三端
  190. // 使用条件编译区分平台
  191. async compressImage(url) {
  192. // #ifdef H5
  193. // H5 环境直接使用 Canvas 压缩
  194. try {
  195. return await this.canvasCompress(url, 0.8);
  196. } catch (err) {
  197. console.warn('Canvas 压缩失败,使用原图片:', err);
  198. return url;
  199. }
  200. // #endif
  201. // #ifndef H5
  202. // App 和小程序使用 uni.compressImage
  203. return new Promise((resolve) => {
  204. uni.compressImage({
  205. src: url,
  206. quality: 0.8,
  207. success: (res) => {
  208. resolve(res.tempFilePath);
  209. },
  210. fail: (err) => {
  211. console.warn('uni.compressImage 失败,使用原图片:', err);
  212. resolve(url);
  213. }
  214. });
  215. });
  216. // #endif
  217. },
  218. // Canvas 图片压缩(H5 专用)
  219. canvasCompress(url, quality) {
  220. return new Promise((resolve, reject) => {
  221. const img = new Image();
  222. img.crossOrigin = 'anonymous';
  223. img.onload = () => {
  224. const canvas = document.createElement('canvas');
  225. const ctx = canvas.getContext('2d');
  226. // 计算压缩后的尺寸(保持宽高比)
  227. const maxWidth = 1200;
  228. const maxHeight = 1200;
  229. let width = img.width;
  230. let height = img.height;
  231. if (width > maxWidth) {
  232. height = (maxWidth / width) * height;
  233. width = maxWidth;
  234. }
  235. if (height > maxHeight) {
  236. width = (maxHeight / height) * width;
  237. height = maxHeight;
  238. }
  239. canvas.width = width;
  240. canvas.height = height;
  241. ctx.drawImage(img, 0, 0, width, height);
  242. // 将 Canvas 转换为 File 对象
  243. canvas.toBlob((blob) => {
  244. if (blob) {
  245. // 确保文件名以 .jpg 结尾
  246. const originalName = url.split('/').pop() || 'image';
  247. const fileName = originalName.endsWith('.jpg') || originalName.endsWith('.jpeg')
  248. ? originalName
  249. : originalName + '.jpg';
  250. const file = new File([blob], fileName, { type: 'image/jpeg' });
  251. resolve(file);
  252. } else {
  253. reject('Blob 创建失败');
  254. }
  255. }, 'image/jpeg', quality);
  256. };
  257. img.onerror = (err) => {
  258. reject('图片加载失败: ' + err);
  259. };
  260. img.src = url;
  261. });
  262. },
  263. uploadFilePromise(fileOrUrl) {
  264. return new Promise((resolve, reject) => {
  265. // #ifdef H5
  266. // H5 环境使用原生 FormData 上传
  267. if (fileOrUrl instanceof File) {
  268. const formData = new FormData();
  269. formData.append('file', fileOrUrl);
  270. formData.append('biz', 'temp');
  271. fetch( configService.apiUrl + this.api, {
  272. method: 'POST',
  273. body: formData,
  274. headers: {
  275. 'Accept': 'application/json'
  276. }
  277. }).then(response => response.json())
  278. .then(data => {
  279. if (data.success) {
  280. resolve(data.message);
  281. } else {
  282. reject(data.message);
  283. }
  284. })
  285. .catch(err => {
  286. reject('上传失败: ' + err);
  287. });
  288. return;
  289. }
  290. // #endif
  291. // App 和小程序使用 uni.uploadFile
  292. uni.uploadFile({
  293. url: configService.apiUrl + this.api,
  294. name: "file",
  295. formData: {
  296. 'biz': 'temp',
  297. },
  298. filePath: fileOrUrl,
  299. success: (res) => {
  300. let data = JSON.parse(res.data);
  301. if (data.success) {
  302. resolve(data.message);
  303. }
  304. },
  305. fail: (err) => {
  306. reject(err);
  307. }
  308. });
  309. });
  310. },
  311. onDelete(event) {
  312. uni.showModal({
  313. title: '提示',
  314. content: '确定要删除?',
  315. cancelText: '取消',
  316. confirmText: '确定',
  317. success: res => {
  318. if (res.confirm) {
  319. this[`fileList`].splice(event.index, 1);
  320. this.$emit('fileList', this.fileList);
  321. }
  322. }
  323. })
  324. },
  325. }
  326. }
  327. </script>
  328. <style lang="scss" scoped>
  329. </style>