upload.vue 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. <template>
  2. <div class="upload-wrap">
  3. <div class="tips tips-text" v-if="tipText?.length > 0">
  4. <span>{{ tipText }}</span>
  5. </div>
  6. <div class="tips" v-if="!textShow">
  7. <el-icon class="icon" size="16">
  8. <Warning />
  9. </el-icon>
  10. <span>上传照片可精准预测最佳病虫防治时间</span>
  11. </div>
  12. <div class="upload-content">
  13. <img v-if="exampleImg" @click="showExample" class="example" src="@/assets/img/home/example-4.png" alt="" />
  14. <uploader class="uploader" :class="{ 'uploader-list': exampleImg }" v-model="fileList"
  15. :multiple="props.maxCount > 1" :max-count="props.maxCount" :after-read="afterRead" @delete="deleteImg">
  16. <template v-if="exampleImg">
  17. <slot v-if="!fileList.length"></slot>
  18. <img class="plus" v-else src="@/assets/img/home/plus.png" alt="">
  19. </template>
  20. <img class="plus" v-else src="@/assets/img/home/plus.png" alt="">
  21. </uploader>
  22. </div>
  23. </div>
  24. <!-- 示例照片 -->
  25. <popup v-model:show="showExamplePopup" overlay-class="example-overlay" class="example-popup">
  26. <div class="example-content">
  27. <!-- <img src="@/assets/img/home/example-4.png" alt="" /> -->
  28. <img class="example-img"
  29. :src="exampleImgData || 'https://birdseye-img-ali-cdn.sysuimars.com/birdseye-look-mini/94379/1768801082504.png'"
  30. alt="" />
  31. <div class="example-tips">
  32. 拍摄要求:请采集代表农场作物物候期的照片,请采集代表农场作物物候期的照片。
  33. </div>
  34. </div>
  35. </popup>
  36. </template>
  37. <script setup>
  38. import { onMounted, ref, watch } from "vue";
  39. import { Uploader, Popup } from "vant";
  40. import eventBus from "@/api/eventBus";
  41. import { base_img_url2 } from "@/api/config";
  42. import { getFileExt } from "@/utils/util";
  43. import { ElMessage } from "element-plus";
  44. import UploadFile from "@/utils/upliadFile";
  45. import 'vant/lib/uploader/style';
  46. import { useStore } from "vuex";
  47. const props = defineProps({
  48. tipText: {
  49. type: String,
  50. default: ''
  51. },
  52. fullPath: {
  53. type: Boolean,
  54. default: true
  55. },
  56. textShow: {
  57. type: Boolean,
  58. default: true
  59. },
  60. exampleImg: {
  61. type: Boolean,
  62. default: false
  63. },
  64. maxCount: {
  65. type: Number,
  66. default: 3
  67. },
  68. // 外部传入已上传的图片(兼容回显,不影响原有用法)
  69. // 数组内容为后端保存的相对路径,与 emit 出去的 imgArr 一致
  70. initImgArr: {
  71. type: Array,
  72. default: () => []
  73. }
  74. })
  75. const store = useStore();
  76. const miniUserId = store.state.home.miniUserId;
  77. const emit = defineEmits(['handleUpload'])
  78. //上传照片
  79. const fileList = ref([]);
  80. const fileArr = ref([])
  81. const imgArr = ref([])
  82. const uploadFileObj = new UploadFile();
  83. const showExamplePopup = ref(false);
  84. const exampleImgData = ref(null);
  85. const showExample = (example) => {
  86. exampleImgData.value = example;
  87. showExamplePopup.value = true;
  88. };
  89. const afterRead = async (files) => {
  90. if (!Array.isArray(files)) {
  91. files = [files];
  92. }
  93. // 如果 maxCount 为 1,先清空之前的图片数组
  94. if (props.maxCount === 1) {
  95. fileArr.value = [];
  96. imgArr.value = [];
  97. }
  98. for (let file of files) {
  99. // 将文件上传至服务器
  100. let fileVal = file.file;
  101. file.status = "uploading";
  102. file.message = "上传中...";
  103. let ext = getFileExt(fileVal.name);
  104. let key = `birdseye-look-mini/${miniUserId}/${new Date().getTime()}.${ext}`;
  105. let resFilename = await uploadFileObj.put(key, fileVal)
  106. file.status = "done";
  107. file.message = "";
  108. if (resFilename) {
  109. fileArr.value.push(props.fullPath ? base_img_url2 + resFilename : resFilename)
  110. imgArr.value.push(resFilename)
  111. eventBus.emit('upload:change', fileArr.value)
  112. eventBus.emit('upload:changeArr', imgArr.value)
  113. emit('handleUpload', { imgArr: imgArr.value, fileList: fileArr.value })
  114. } else {
  115. fileList.value.pop()
  116. file.status = 'failed';
  117. file.message = '上传失败';
  118. ElMessage.error('图片上传失败,请稍后再试!')
  119. }
  120. }
  121. };
  122. const deleteImg = (file, e) => {
  123. fileArr.value.splice(e.index, 1)
  124. imgArr.value.splice(e.index, 1)
  125. eventBus.emit('upload:change', fileArr.value)
  126. eventBus.emit('upload:changeArr', imgArr.value)
  127. emit('handleUpload', { imgArr: imgArr.value, fileList: fileArr.value })
  128. }
  129. // 外部传入的已上传图片回显处理(只做补充,不改变原有上传逻辑)
  130. watch(
  131. () => props.initImgArr,
  132. (val) => {
  133. if (!val || val.length === 0) {
  134. return;
  135. }
  136. // 重置本地状态,使用外部传入的图片重新构建
  137. fileList.value = [];
  138. fileArr.value = [];
  139. imgArr.value = [];
  140. val.forEach((item) => {
  141. // item 为后端相对路径,和组件内部 imgArr 一致
  142. const url = props.fullPath ? base_img_url2 + item : item;
  143. fileArr.value.push(url);
  144. imgArr.value.push(item);
  145. // Vant Uploader 预览所需结构
  146. fileList.value.push({
  147. url,
  148. isImage: true,
  149. });
  150. });
  151. // 同步给外部一次,保证父组件拿到的 imgArr/fileList 跟展示一致
  152. eventBus.emit('upload:change', fileArr.value);
  153. eventBus.emit('upload:changeArr', imgArr.value);
  154. emit('handleUpload', { imgArr: imgArr.value, fileList: fileArr.value });
  155. },
  156. { immediate: true ,deep: true}
  157. );
  158. function uploadReset() {
  159. fileList.value = []
  160. fileArr.value = []
  161. imgArr.value = []
  162. }
  163. defineExpose({
  164. uploadReset
  165. })
  166. onMounted(() => {
  167. eventBus.off('upload:reset', uploadReset)
  168. eventBus.on('upload:reset', uploadReset)
  169. })
  170. </script>
  171. <style lang="scss" scoped>
  172. .upload-wrap {
  173. position: relative;
  174. .upload-content {
  175. display: flex;
  176. }
  177. .tips {
  178. display: flex;
  179. align-items: center;
  180. color: #2199F8;
  181. background: #E9F5FF;
  182. border-radius: 4px;
  183. padding: 2px 10px;
  184. box-sizing: border-box;
  185. margin-bottom: 10px;
  186. .icon {
  187. margin-right: 2px;
  188. }
  189. }
  190. .tips-text {
  191. background: linear-gradient(240deg, #FFFFFF 22%, rgba(33, 153, 248, 0.2) 100%);
  192. border-radius: 20px 0 0 20px;
  193. }
  194. ::v-deep {
  195. .van-uploader__input-wrapper {
  196. text-align: center;
  197. }
  198. .el-select__wrapper:hover {
  199. box-shadow: 0 0 0 1px #dcdfe6 inset;
  200. }
  201. .avatar-uploader .el-upload {
  202. width: 100%;
  203. border: 1px dashed #dddddd;
  204. border-radius: 6px;
  205. cursor: pointer;
  206. position: relative;
  207. overflow: hidden;
  208. }
  209. .el-icon.avatar-uploader-icon {
  210. font-size: 28px;
  211. color: #8c939d;
  212. width: 100%;
  213. height: 128px;
  214. text-align: center;
  215. background: #f6f6f6;
  216. }
  217. }
  218. .uploader {
  219. .plus,
  220. .example {
  221. width: calc((100vw - 68px) / 4);
  222. height: calc((100vw - 68px) / 4);
  223. }
  224. ::v-deep {
  225. .van-uploader__wrapper {
  226. --van-uploader-size: 76.7px;
  227. --van-padding-xs: 6px;
  228. }
  229. }
  230. }
  231. .uploader-list {
  232. ::v-deep {
  233. .van-uploader__wrapper {
  234. >div:first-child {
  235. margin-left: calc((100vw - 68px) / 4 + 8px);
  236. }
  237. }
  238. .van-uploader__preview-image {
  239. width: calc((100vw - 68px) / 4);
  240. height: calc((100vw - 68px) / 4);
  241. }
  242. }
  243. }
  244. .example {
  245. width: calc((100vw - 68px) / 4);
  246. height: calc((100vw - 68px) / 4);
  247. margin: 0 12px 8px 0;
  248. position: absolute;
  249. z-index: 2;
  250. top: 0;
  251. left: 0;
  252. }
  253. }
  254. .example-popup {
  255. width: 100%;
  256. border-radius: 0;
  257. background: none;
  258. max-width: 100%;
  259. .example-content {
  260. text-align: center;
  261. .example-img {
  262. width: 100%;
  263. }
  264. }
  265. .example-tips {
  266. margin: 16px 12px 6px 12px;
  267. background: #3d3d3d;
  268. padding: 8px 10px;
  269. border-radius: 4px;
  270. backdrop-filter: blur(4px);
  271. color: #fff;
  272. font-size: 14px;
  273. line-height: 21px;
  274. text-align: left;
  275. }
  276. }
  277. </style>