upload.vue 8.1 KB

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