patrolPhoto.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. <template>
  2. <div class="patrol-photo">
  3. <custom-header name="农场照片"></custom-header>
  4. <div class="patrol-content">
  5. <div class="photo-header">
  6. <el-date-picker style="width: 120px" v-model="value1" type="date" placeholder="全部日期" />
  7. <div class="select-wrap">
  8. <div
  9. :class="['select-item', { active: tabActive === index }]"
  10. v-for="(item, index) in tabsList"
  11. :key="index"
  12. @click="handleTabAct(index)"
  13. >
  14. {{ item }}
  15. </div>
  16. </div>
  17. </div>
  18. <div class="photo-wrap">
  19. <List
  20. class="photo-list"
  21. v-model:loading="loading"
  22. :finished="finished"
  23. :immediate-check="false"
  24. finished-text="没有更多了"
  25. @load="getPhotoList"
  26. >
  27. <div class="photo-item" v-for="(dateItem, dateIndex) in photoListByDate" :key="dateIndex">
  28. <div class="photo-item-top">
  29. <span class="date-text">{{ dateItem.dateText }}</span>
  30. <div class="weather-wrap">
  31. <div class="weather-item">
  32. <i :class="'qi-'+ dateItem.weather.iconDay + '-fill'"></i>
  33. <span>{{ dateItem.weather.textDay }}</span>
  34. </div>
  35. <div class="weather-item">
  36. <img src="@/assets/img/home/temperature.png" alt="">
  37. <span>{{ dateItem.weather.tempMax }}℃</span>
  38. </div>
  39. <div class="weather-item">
  40. <img src="@/assets/img/home/humidity.png" alt="">
  41. <span>{{ dateItem.weather.humidity }}%</span>
  42. </div>
  43. </div>
  44. </div>
  45. <div class="photo-img-wrap">
  46. <photo-provider :photo-closable="true">
  47. <photo-consumer
  48. v-for="src in dateItem.images"
  49. intro="执行照片"
  50. :key="src.uid || src.path"
  51. :src="src.path"
  52. >
  53. <div
  54. class="photo-img"
  55. :class="{
  56. 'is-disabled': isCheck && selectedImageIds.length >= MAX_SELECT_COUNT && !isImageSelected(src.uid)
  57. }"
  58. @click="handleImageClick($event, src)"
  59. >
  60. <img :src="src.path" />
  61. <div class="photo-check" v-if="isCheck" @click.stop="toggleImageSelect(src)">
  62. <span class="check-icon" :class="{ checked: isImageSelected(src.uid) }"></span>
  63. </div>
  64. </div>
  65. </photo-consumer>
  66. </photo-provider>
  67. </div>
  68. </div>
  69. </List>
  70. </div>
  71. </div>
  72. <div class="custom-bottom-fixed-btns" v-if="isCheck">
  73. <div class="secondary-btn">已选择 {{ selectedImageIds.length }} 项</div>
  74. <div class="bottom-btn primary-btn" @click="handleSend">发送</div>
  75. </div>
  76. </div>
  77. </template>
  78. <script setup>
  79. import { ref, onMounted, nextTick, watch } from "vue";
  80. import { useRouter, useRoute } from "vue-router";
  81. import { List } from "vant";
  82. import { ElMessage } from "element-plus";
  83. import { base_img_url2,resize_300 } from "@/api/config";
  84. import customHeader from "@/components/customHeader.vue";
  85. const router = useRouter();
  86. const route = useRoute();
  87. const tabActive = ref(0);
  88. const tabsList = ["物候", "病虫", "生长"];
  89. const handleTabAct = (index) => {
  90. tabActive.value = index;
  91. };
  92. const value1 = ref("");
  93. // 按日期分组的图片列表
  94. const photoListByDate = ref([]);
  95. const loading = ref(false);
  96. const finished = ref(false);
  97. // 日期列表
  98. const dateList = ref([]);
  99. // 当前加载的日期索引
  100. const currentDateIndex = ref(0);
  101. // 是否正在加载日期列表
  102. const isLoadingDates = ref(false);
  103. // 是否正在请求图片数据(独立标志,避免与 List 组件的 loading 冲突)
  104. const isRequestingImages = ref(false);
  105. // 当前页码
  106. const currentPageIndex = ref(0);
  107. const farmId = ref(766);
  108. const selectedImageIds = ref([]);
  109. const MAX_SELECT_COUNT = 3; // 最多选择3张
  110. const buildImageUid = (image) =>
  111. image?.id ?? image?.resId ?? image?.resFilename ?? image?.filename ?? image?.path;
  112. const isImageSelected = (uid) => selectedImageIds.value.includes(uid);
  113. const toggleImageSelect = (image) => {
  114. if (!image?.uid) return;
  115. const uid = image.uid;
  116. if (isImageSelected(uid)) {
  117. // 取消选择
  118. selectedImageIds.value = selectedImageIds.value.filter((item) => item !== uid);
  119. } else {
  120. // 尝试选择新图片
  121. if (selectedImageIds.value.length >= MAX_SELECT_COUNT) {
  122. ElMessage.warning(`最多只能选择${MAX_SELECT_COUNT}张图片`);
  123. return;
  124. }
  125. selectedImageIds.value = [...selectedImageIds.value, uid];
  126. }
  127. };
  128. const handleImageClick = (event, image) => {
  129. if (!isCheck.value) return;
  130. event?.preventDefault();
  131. event?.stopPropagation();
  132. toggleImageSelect(image);
  133. };
  134. // 格式化日期显示
  135. const formatDate = (dateStr) => {
  136. if (!dateStr) return "";
  137. const date = new Date(dateStr);
  138. const year = date.getFullYear();
  139. const month = date.getMonth() + 1;
  140. const day = date.getDate();
  141. return `${year}年${month}月${day}日`;
  142. };
  143. // 获取有图片的日期列表
  144. const findHasImagesDate = (isLoadMore = false) => {
  145. if (isLoadingDates.value) return Promise.resolve();
  146. isLoadingDates.value = true;
  147. const params = {
  148. farmId: farmId.value,
  149. pageIndex: currentPageIndex.value,
  150. limit: 10,
  151. };
  152. return VE_API.farm
  153. .findHasImagesDate(params)
  154. .then(({ data }) => {
  155. if (data && Array.isArray(data) && data.length > 0) {
  156. if (isLoadMore) {
  157. // 如果是加载更多,追加到现有日期列表
  158. dateList.value = [...dateList.value, ...data];
  159. } else {
  160. // 首次加载,重置日期列表
  161. dateList.value = data;
  162. currentDateIndex.value = 0;
  163. currentPageIndex.value = 1;
  164. }
  165. finished.value = false;
  166. loading.value = false;
  167. isLoadingDates.value = false;
  168. // 如果是首次加载,等待 List 组件触发加载
  169. if (!isLoadMore && data.length > 0) {
  170. loading.value = false;
  171. finished.value = false;
  172. isRequestingImages.value = false;
  173. // 使用 nextTick 确保 DOM 更新后,List 组件能检测到需要加载
  174. nextTick(() => {
  175. if (!finished.value && currentDateIndex.value < dateList.value.length) {
  176. getPhotoList();
  177. }
  178. });
  179. }
  180. } else {
  181. // 没有更多日期了
  182. finished.value = true;
  183. isLoadingDates.value = false;
  184. }
  185. })
  186. .catch((error) => {
  187. console.error("findHasImagesDate 请求失败:", error);
  188. isLoadingDates.value = false;
  189. finished.value = true;
  190. });
  191. };
  192. // 加载下一个日期的图片
  193. const loadNextDateImages = () => {
  194. // 如果已经加载完所有日期,尝试加载更多日期
  195. if (currentDateIndex.value >= dateList.value.length) {
  196. // 不是直接设置 finished,而是尝试获取更多日期
  197. loadMoreDates();
  198. return;
  199. }
  200. // 如果已经标记为完成,直接返回
  201. if (finished.value) {
  202. return;
  203. }
  204. // 如果正在请求,直接返回(避免重复请求)
  205. if (isRequestingImages.value) {
  206. return;
  207. }
  208. // 设置请求标志和 loading 状态
  209. isRequestingImages.value = true;
  210. loading.value = true;
  211. const currentDate = dateList.value[currentDateIndex.value];
  212. const params = {
  213. farmId: farmId.value,
  214. date: currentDate,
  215. };
  216. VE_API.farm
  217. .getImageInfo(params)
  218. .then(({ data }) => {
  219. if (data.images.length > 0) {
  220. // 处理图片数据,添加图片路径
  221. const photoArr = data.images.map((item) => ({
  222. ...item,
  223. uid: buildImageUid(item),
  224. path: base_img_url2 + (item.resFilename || item.filename) + resize_300,
  225. }));
  226. // 将当前日期的图片数据添加到列表中
  227. photoListByDate.value.push({
  228. date: currentDate,
  229. dateText: formatDate(currentDate),
  230. images: photoArr,
  231. weather: data.weather,
  232. });
  233. }
  234. // 移动到下一个日期
  235. currentDateIndex.value++;
  236. })
  237. .catch((error) => {
  238. console.error("获取图片失败:", error);
  239. // 请求失败时也移动到下一个日期,避免卡住
  240. currentDateIndex.value++;
  241. })
  242. .finally(() => {
  243. // 使用 finally 确保状态总是被重置
  244. isRequestingImages.value = false;
  245. loading.value = false;
  246. // 检查是否还有更多日期需要加载
  247. if (currentDateIndex.value >= dateList.value.length) {
  248. // 当前日期列表已加载完,尝试加载更多日期
  249. loadMoreDates();
  250. }
  251. });
  252. };
  253. // 加载更多日期
  254. const loadMoreDates = () => {
  255. if (isLoadingDates.value || finished.value) {
  256. return;
  257. }
  258. // pageIndex + 1,获取下一页日期
  259. currentPageIndex.value++;
  260. loading.value = true;
  261. findHasImagesDate(true).then(() => {
  262. // 如果获取到新日期,继续加载图片
  263. if (currentDateIndex.value < dateList.value.length) {
  264. loadNextDateImages();
  265. }
  266. });
  267. };
  268. // 滚动加载触发
  269. const getPhotoList = () => {
  270. // 如果已经完成,直接返回
  271. if (finished.value) {
  272. loading.value = false; // 确保 loading 状态被重置
  273. return;
  274. }
  275. // 调用加载函数(loadNextDateImages 内部会检查 loading 状态)
  276. loadNextDateImages();
  277. };
  278. const isCheck = ref(false);
  279. onMounted(() => {
  280. isCheck.value = route.query.isCheck;
  281. farmId.value = route.query.farmId;
  282. findHasImagesDate();
  283. });
  284. const handleSend = () => {
  285. const selectedFilenames = [];
  286. // 遍历所有日期的图片,找到选中的图片
  287. photoListByDate.value.forEach((dateItem) => {
  288. dateItem.images.forEach((image) => {
  289. if (isImageSelected(image.uid)) {
  290. selectedFilenames.push(base_img_url2 + image.filename);
  291. }
  292. });
  293. });
  294. router.push(`/chat_frame?userId=${route.query.userId}&farmId=${farmId.value}&formPage=monitor&selectedImgs=${JSON.stringify(selectedFilenames)}`);
  295. };
  296. </script>
  297. <style lang="scss" scoped>
  298. ::v-deep {
  299. .van-list__finished-text {
  300. width: 100%;
  301. }
  302. .el-input__wrapper {
  303. box-shadow: none;
  304. border: 1px solid #2199f8;
  305. --el-input-placeholder-color: #2199f8;
  306. }
  307. .el-input__inner,
  308. .el-input__prefix {
  309. color: #2199f8;
  310. }
  311. }
  312. .patrol-photo {
  313. width: 100%;
  314. height: 100vh;
  315. background: #fff;
  316. .patrol-content {
  317. padding: 0 12px;
  318. width: 100%;
  319. height: calc(100vh - 50px);
  320. box-sizing: border-box;
  321. margin-top: 10px;
  322. .photo-header {
  323. display: flex;
  324. width: 100%;
  325. margin-bottom: 20px;
  326. .select-wrap {
  327. display: flex;
  328. width: calc(100% - 120px);
  329. .select-item {
  330. margin-left: 12px;
  331. flex: 1;
  332. color: #666666;
  333. padding: 6px 0;
  334. text-align: center;
  335. background: #f5f5f5;
  336. border-radius: 5px;
  337. &.active {
  338. color: #2199f8;
  339. background: #e0f1fe;
  340. }
  341. }
  342. }
  343. }
  344. .photo-wrap {
  345. width: 100%;
  346. height: calc(100vh - 95px);
  347. overflow: auto;
  348. .photo-list {
  349. width: 100%;
  350. .photo-item {
  351. width: 100%;
  352. .photo-item-top {
  353. display: flex;
  354. align-items: center;
  355. margin-bottom: 12px;
  356. .date-text {
  357. font-weight: 500;
  358. font-size: 14px;
  359. color: #333;
  360. margin-right: 20px;
  361. }
  362. .weather-wrap {
  363. display: flex;
  364. align-items: center;
  365. .weather-item {
  366. display: flex;
  367. gap: 4px;
  368. align-items: center;
  369. font-size: 12px;
  370. margin-right: 10px;
  371. img {
  372. width: 16px;
  373. height: 16px;
  374. }
  375. }
  376. }
  377. }
  378. .photo-img-wrap {
  379. display: flex;
  380. flex-wrap: wrap;
  381. gap: 10px;
  382. // .region-text {
  383. // width: 68px;
  384. // text-align: center;
  385. // font-weight: 500;
  386. // font-size: 12px;
  387. // padding: 5px 0;
  388. // border-radius: 4px;
  389. // border: 1px solid #bbbbbb;
  390. // color: #666;
  391. // margin-bottom: 8px;
  392. // }
  393. .photo-img {
  394. width: 110px;
  395. height: 110px;
  396. position: relative;
  397. box-sizing: border-box;
  398. border: 2px solid transparent;
  399. border-radius: 8px;
  400. overflow: hidden;
  401. transition: border-color 0.2s ease, opacity 0.2s ease;
  402. &.is-disabled {
  403. opacity: 0.5;
  404. cursor: not-allowed;
  405. }
  406. img {
  407. width: 100%;
  408. height: 100%;
  409. border-radius: 8px;
  410. object-fit: cover;
  411. }
  412. .photo-check {
  413. position: absolute;
  414. top: 5px;
  415. right: 5px;
  416. width: 20px;
  417. height: 20px;
  418. border: 2px solid #fff;
  419. border-radius: 50%;
  420. background: rgba(217, 217, 217, 0.4);
  421. display: flex;
  422. align-items: center;
  423. justify-content: center;
  424. overflow: hidden;
  425. .check-icon {
  426. width: 100%;
  427. height: 100%;
  428. position: relative;
  429. border-radius: 50%;
  430. &.checked {
  431. background: #2199f8;
  432. border-color: #2199f8;
  433. &::after {
  434. content: "";
  435. position: absolute;
  436. top: 3px;
  437. left: 7px;
  438. width: 4px;
  439. height: 8px;
  440. border: 2px solid #fff;
  441. border-top: 0;
  442. border-left: 0;
  443. transform: rotate(45deg);
  444. }
  445. }
  446. }
  447. }
  448. }
  449. }
  450. }
  451. .photo-item + .photo-item {
  452. margin-top: 20px;
  453. }
  454. }
  455. }
  456. }
  457. }
  458. </style>