index.vue 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134
  1. <template>
  2. <custom-header name="农情互动" bgColor="#f2f4f5"></custom-header>
  3. <div class="interaction-list" ref="interactionListRef">
  4. <div class="list-item" v-for="(item, index) in listData" :key="item.id || index"
  5. :class="{ 'uploaded-item': item.questionStatus !== 3 }">
  6. <!-- 标题区域 -->
  7. <div class="item-header-wrapper" :class="{ 'has-status': item.questionStatus !== 3 }">
  8. <div class="item-header">
  9. <div class="title">{{ item.interactionTypeName }}</div>
  10. <div class="status" :class="['urgent-' + item.urgent]" v-if="item.questionStatus === 3">{{
  11. urgentType[item.urgent] }}</div>
  12. </div>
  13. <div class="upload-status" v-show="item.questionStatus !== 3">
  14. <el-icon class="status-icon">
  15. <SuccessFilled />
  16. </el-icon>
  17. <span class="status-text">提交成功</span>
  18. </div>
  19. </div>
  20. <!-- 未上传状态内容 -->
  21. <div class="uploaded-content" v-show="item.questionStatus === 3 || item.expanded">
  22. <div class="content-wrapper">
  23. <span>{{ item.remark }}</span>
  24. <text-ellipsis class="item-desc" rows="0" :content="item.reason" expand-text="展开"
  25. collapse-text="收起" />
  26. <div class="tip-box">如果不确定是否发生,直接上传照片即可</div>
  27. <div class="example-wrapper">
  28. <div class="example-header">
  29. <div>示例照片</div>
  30. <div class="more" v-if="item.exampleImageWithAnnotations.length > 3"
  31. @click="openMorePopup(item)">查看更多</div>
  32. </div>
  33. <div class="example-list" v-if="item.exampleImageWithAnnotations.length > 0">
  34. <div class="image-item-wrapper"
  35. v-for="(example, exIndex) in item.exampleImageWithAnnotations"
  36. :key="example.exampleImageUrl" @click="showExample(item, exIndex)">
  37. <img class="image-item" :src="example.exampleImageUrl" alt="" />
  38. </div>
  39. </div>
  40. </div>
  41. <text-ellipsis class="patrol-suggestion" rows="2" :content="'巡园建议:' + item.patrolSuggestion">
  42. <template #action="{ expanded }"><span class="action-text">{{ expanded ? '收起' : '展开'
  43. }}</span></template>
  44. </text-ellipsis>
  45. </div>
  46. <!-- 展开状态内容 -->
  47. <div class="expanded-content" v-show="item.imagePaths.length > 0">
  48. <!-- 原因说明 -->
  49. <div class="reason-text">上传照片({{ item.imagePaths.length }}张)</div>
  50. <!-- 图片展示 -->
  51. <div class="uploaded-images">
  52. <div class="uploaded-img-wrap" v-for="(image, imgIndex) in item.imagePaths" :key="image"
  53. @click="showExample(item, imgIndex, { hideLabel: true })">
  54. <img class="uploaded-img" :src="base_img_url2 + image" alt="" />
  55. <span v-show="item.questionStatus === 3" class="uploaded-img-remove"
  56. @click.stop="removeUploadedImage(item, imgIndex)">×</span>
  57. </div>
  58. </div>
  59. <uploader v-show="item.questionStatus === 3" class="upload-wrap continue-upload-btn"
  60. @click="handleUploadClick(item)" multiple :max-count="10" :after-read="afterReadUpload">
  61. <div class="upload-btn">
  62. <el-icon>
  63. <Plus />
  64. </el-icon>
  65. <span>继续上传照片</span>
  66. </div>
  67. </uploader>
  68. </div>
  69. <!-- 上传按钮 -->
  70. <uploader v-if="item.imagePaths.length === 0 && item.questionStatus === 3"
  71. @click="handleUploadClick(item)" class="upload-wrap" multiple :max-count="10"
  72. :after-read="afterReadUpload">
  73. <div class="upload-btn">
  74. <el-icon>
  75. <Plus />
  76. </el-icon>
  77. <span>点击上传照片</span>
  78. </div>
  79. </uploader>
  80. <div class="question-wrapper" v-if="item.imagePaths.length">
  81. <div class="question-cont">
  82. <div class="question-text"
  83. v-for="(q, qIdx) in (item.questionList || [item.question].filter(Boolean))" :key="qIdx">
  84. <span class="text-title">{{ q }}</span>
  85. <el-input v-model="item.answerValues[qIdx]" :disabled="item.questionStatus !== 3"
  86. type="number" style="width: 70px">
  87. <template #suffix>
  88. <span class="text-unit">{{ item.indicators[qIdx]?.unit ?? item.indicators[0]?.unit
  89. ?? '%' }}</span>
  90. </template>
  91. </el-input>
  92. </div>
  93. </div>
  94. <div class="draw-region-btn" v-if="item.interactionTypeId != 1 && item.questionStatus === 3"
  95. @click="handleDrawRegion(item)">
  96. 编辑发生区域</div>
  97. <div class="draw-region-btn" v-if="item.rangeWkt && item.questionStatus !== 3"
  98. @click="handleViewRegion(item)">
  99. 查看发生区域</div>
  100. </div>
  101. <!-- 输入框 -->
  102. <div class="input-wrapper">
  103. <el-input v-model="item.replyText" :disabled="item.questionStatus !== 3" placeholder="请输入您咨询的问题"
  104. clearable />
  105. </div>
  106. <!-- 按钮区域 -->
  107. <div class="button-group" v-show="item.questionStatus === 3">
  108. <div class="btn-not-reached" @click="handleConfirm(item, false)">{{ item.cancelButtonName }}</div>
  109. <div class="btn-default" :class="{ 'btn-confirm': item.imagePaths.length > 0 }"
  110. @click="handleConfirm(item, true)">
  111. 确认提交
  112. </div>
  113. </div>
  114. </div>
  115. <!-- 比例信息(已上传状态显示) -->
  116. <div class="proportion-info" v-show="item.questionStatus !== 3"
  117. :style="{ justifyContent: item.expanded ? 'center' : 'space-between' }">
  118. <template v-if="!item.expanded">
  119. <span class="proportion-text">{{(item.questionList && item.questionList.length
  120. ? item.questionList.map((q, i) => q + ':' + (item.answerValues[i] ?? '') +
  121. (item.indicators[i]?.unit
  122. ?? item.indicators[0]?.unit ?? '%')).join('')
  123. : item.question + ':' + (item.answerValues[0] ?? '') + (item.indicators[0]?.unit ?? '%'))
  124. }}</span>
  125. </template>
  126. <div class="toggle-btn" @click="toggleExpand(item)">
  127. <span>{{ item.expanded ? "收起" : "展开" }}</span>
  128. <el-icon :class="{ rotate: !item.expanded }">
  129. <CaretTop />
  130. </el-icon>
  131. </div>
  132. </div>
  133. </div>
  134. <div class="empty-data" v-if="!loading && listData.length === 0">暂无数据</div>
  135. </div>
  136. <!-- <div class="custom-bottom-fixed-btns" :class="{ 'center-btn': false }">
  137. <div class="bottom-btn secondary-btn" @click="handleShowDroneConsultPopup">获取自动飞行航线</div>
  138. <div class="bottom-btn secondary-btn">邀请农情互动</div>
  139. <div class="bottom-btn primary-btn" @click="handleSubmitAll">一键提交</div>
  140. </div> -->
  141. <!-- 农场信息完善弹窗 -->
  142. <farm-info-popup :oldUser="oldUser" :expertMiniUserId="query.expertMiniUserId" v-model:show="showFarmInfoPopup" />
  143. <!-- 无人机/飞行航线咨询弹窗 -->
  144. <drone-consult-popup v-model:show="showDroneConsultPopup" @copy="onCopyWechatId" />
  145. <!-- 示例照片轮播组件 -->
  146. <example-popup v-model:show="showExamplePopup" :images="exampleList" :tips="currentPhotData?.shootingMethod"
  147. :start-index="exampleStartIndex" :show-title-and-tips="exampleShowTitleAndTips" />
  148. <!-- 照片上传进度 -->
  149. <popup v-model:show="showUploadProgressPopup" round class="upload-progress-popup">
  150. <div class="upload-progress-title">
  151. <span>照片上传进度</span>
  152. <el-progress class="upload-progress" :percentage="uploadPercentage" :stroke-width="10" :format="format" />
  153. </div>
  154. <div class="upload-box">
  155. <!-- 把已经上传成功的图片传给 upload 组件做回显,同时保持原有上传事件不变 -->
  156. <upload :maxCount="10" :initImgArr="initImgArr" @handleUpload="handleUploadSuccess">
  157. </upload>
  158. </div>
  159. <div class="input-box">
  160. <div class="input-item"
  161. v-for="(q, qIdx) in (currentItem?.questionList?.length ? currentItem.questionList : (currentItem?.question ? [currentItem.question] : []))"
  162. :key="qIdx">
  163. <span class="label-text">{{ q }}</span>
  164. <el-input class="label-input" v-model="currentItem.answerValues[qIdx]" placeholder="请输入" type="number">
  165. <template #suffix>
  166. <span class="unit">{{ currentItem.indicators[qIdx]?.unit ?? currentItem.indicators[0]?.unit ??
  167. '%' }}</span>
  168. </template>
  169. </el-input>
  170. </div>
  171. <el-input class="input-item" v-model="currentItem.replyText" placeholder="请输入您咨询的问题" clearable />
  172. </div>
  173. <template v-if="currentItem.interactionTypeId != 1">
  174. <div class="region-tips">勾画新发生区域,精准匹配专属农事方案</div>
  175. <div class="region-map" ref="mapContainer" @click="handleDrawRegion(currentItem)">
  176. <div class="region-map-text">点击勾画新发生区域</div>
  177. </div>
  178. </template>
  179. <div class="confirm-btn" @click="handleConfirmUpload">确认上传</div>
  180. </popup>
  181. <!-- 查看更多弹窗 -->
  182. <more-popup ref="morePopupRef" />
  183. </template>
  184. <script setup>
  185. import { ref, onActivated, computed, onUnmounted } from "vue";
  186. import { ElMessage } from "element-plus";
  187. import { Uploader, Popup, TextEllipsis } from "vant";
  188. import customHeader from "@/components/customHeader.vue";
  189. import upload from "@/components/upload.vue";
  190. import FarmInfoPopup from "@/components/popup/farmInfoPopup.vue";
  191. import DroneConsultPopup from "@/components/popup/droneConsultPopup.vue";
  192. import ExamplePopup from "./components/examplePopup.vue";
  193. import { useRouter, useRoute } from "vue-router";
  194. import { base_img_url2 } from "@/api/config";
  195. import DrawRegionMap from "./map/drawRegionMap.js";
  196. import UploadFile from "@/utils/upliadFile";
  197. import { getFileExt } from "@/utils/util";
  198. import MorePopup from "./components/morePopup.vue";
  199. const interactionListRef = ref(null);
  200. const SCROLL_KEY = 'interactionListScrollTop';
  201. const showDroneConsultPopup = ref(false);
  202. const handleShowDroneConsultPopup = () => {
  203. // showDroneConsultPopup.value = true;
  204. router.push(
  205. `/confirm_area`
  206. );
  207. }
  208. const showFarmInfoPopup = ref(false);
  209. const loading = ref(false);
  210. const router = useRouter();
  211. const listData = ref([]);
  212. const query = ref(useRoute().query);
  213. const morePopupRef = ref(null);
  214. //照片上传进度
  215. const showUploadProgressPopup = ref(false);
  216. // 上传进度统计
  217. const totalUploadCount = ref(0); // 本次选择的总文件数(固定不随上传成功变化)
  218. const uploadedSuccessCount = ref(0); // 已上传成功的文件数
  219. const uploadPercentage = computed(() => {
  220. if (!totalUploadCount.value) return 0;
  221. return Math.round((uploadedSuccessCount.value / totalUploadCount.value) * 100);
  222. });
  223. const format = () => {
  224. if (!totalUploadCount.value) return '0/0';
  225. return `${uploadedSuccessCount.value}/${totalUploadCount.value}`;
  226. };
  227. const drawRegionMap = new DrawRegionMap();
  228. const mapContainer = ref(null);
  229. const renderRegionFromSession = () => {
  230. if (!drawRegionMap.kmap) return;
  231. // 先清空图层
  232. drawRegionMap.clearLayer && drawRegionMap.clearLayer();
  233. // 1)通过保存的互动项 ID,在 listData 中找到对应项,用接口 rangeWkt 回显只读区域
  234. const savedId = sessionStorage.getItem("drawRegionInteractionId");
  235. if (savedId && Array.isArray(listData.value) && listData.value.length > 0) {
  236. const targetItem = listData.value.find(
  237. (it) =>
  238. it &&
  239. it.id != null &&
  240. (String(it.id) === savedId || Number(it.id) === Number(savedId))
  241. );
  242. if (targetItem && targetItem.rangeWkt && targetItem.rangeWkt.length > 10) {
  243. // 使用 setStatusRegions 显示接口返回的区域
  244. renderRegionFromItemWkt(targetItem);
  245. }
  246. }
  247. // 2)再回显本地勾画的多边形到可编辑图层(两者可共存)
  248. const polygonStr = sessionStorage.getItem("drawRegionPolygonData");
  249. if (polygonStr) {
  250. try {
  251. const polygonData = JSON.parse(polygonStr);
  252. if (
  253. polygonData &&
  254. Array.isArray(polygonData.geometryArr) &&
  255. polygonData.geometryArr.length > 0 &&
  256. drawRegionMap.kmap &&
  257. drawRegionMap.kmap.polygonLayer
  258. ) {
  259. // 注意这里不再调用 clearLayer,避免清掉上一步 setStatusRegions 的只读区域
  260. // 此处也不单独做 fit,由下方统一调用 fitAllRegions 适配所有图层
  261. drawRegionMap.setAreaGeometry(polygonData.geometryArr, false, polygonData.mianji);
  262. }
  263. } catch (e) {
  264. console.error("解析 drawRegionPolygonData 失败:", e);
  265. }
  266. }
  267. // 3)统一根据只读图层 + 可编辑图层的范围自适应视图,保证所有区域都在视野内
  268. if (drawRegionMap.fitAllRegions) {
  269. drawRegionMap.fitAllRegions();
  270. }
  271. };
  272. // 用当前项的 rangeWkt(接口返回的已保存区域)回显到地图;使用 setStatusRegions 方法显示
  273. const renderRegionFromItemWkt = (item) => {
  274. const raw = item?.rangeWkt;
  275. if (!raw || typeof raw !== 'string' || raw.length <= 10) return;
  276. if (!drawRegionMap.kmap || !drawRegionMap.staticRegionLayer) return;
  277. let geometryArr = [];
  278. const trimmed = raw.trim();
  279. if (trimmed.startsWith('{')) {
  280. try {
  281. const parsed = JSON.parse(raw);
  282. if (parsed && Array.isArray(parsed.geometryArr) && parsed.geometryArr.length > 0) {
  283. geometryArr = parsed.geometryArr;
  284. }
  285. } catch (e) {
  286. console.error("解析 rangeWkt JSON 失败:", e);
  287. return;
  288. }
  289. } else {
  290. geometryArr = [raw];
  291. }
  292. if (geometryArr.length > 0) {
  293. // 转换为 setStatusRegions 需要的格式:[{ geometry: wkt, status?: 'resolved' | 'unresolved' }]
  294. const regions = geometryArr
  295. .filter(wkt => wkt && typeof wkt === 'string' && wkt.trim().length > 10)
  296. .map(wkt => ({
  297. geometry: wkt.trim(),
  298. status: 'unresolved' // 接口返回的区域默认显示为未解决状态
  299. }));
  300. if (regions.length > 0) {
  301. drawRegionMap.setStatusRegions(regions);
  302. }
  303. }
  304. };
  305. //
  306. const urgentType = {
  307. "0": "一般紧急",
  308. "1": "紧急",
  309. "2": "非常紧急",
  310. }
  311. const uploadFileObj = new UploadFile();
  312. const initImgArr = ref([]);
  313. const miniUserId = localStorage.getItem("MINI_USER_ID");
  314. //弹窗问题
  315. const afterReadUpload = async (data) => {
  316. // 继续上传:回显已有图片再追加新图;首次上传:直接清空,只显示本次新传
  317. initImgArr.value = (currentItem.value?.imagePaths?.length > 0)
  318. ? [...currentItem.value.imagePaths]
  319. : [];
  320. // 本次上传的总数 = 选择的文件数量(固定,用于做进度的“总数”)
  321. if (!Array.isArray(data)) {
  322. data = [data];
  323. }
  324. totalUploadCount.value = data.length;
  325. uploadedSuccessCount.value = 0;
  326. // 仅当「编辑发生区域」和本次上传是同一项时保留勾画数据并回显;否则清除,避免数据串
  327. const savedInteractionId = sessionStorage.getItem("drawRegionInteractionId");
  328. const isSameItem = savedInteractionId != null && String(currentItem.value?.id) === savedInteractionId;
  329. if (!isSameItem) {
  330. drawRegionMap.clearLayer && drawRegionMap.clearLayer();
  331. sessionStorage.removeItem("drawRegionPolygonData");
  332. sessionStorage.removeItem("drawRegionInteractionId");
  333. }
  334. for (let file of data) {
  335. // 将文件上传至服务器
  336. let fileVal = file.file;
  337. file.status = "uploading";
  338. file.message = "上传中...";
  339. let ext = getFileExt(fileVal.name);
  340. let key = `birdseye-look-mini/${miniUserId}/${new Date().getTime()}.${ext}`;
  341. let resFilename = await uploadFileObj.put(key, fileVal)
  342. if (resFilename) {
  343. file.status = "done";
  344. file.message = "";
  345. // 记录成功数量,用于进度条“当前成功数”
  346. uploadedSuccessCount.value += 1;
  347. // 记录已上传成功的图片路径,用于回显
  348. initImgArr.value.push(resFilename)
  349. } else {
  350. file.status = 'failed';
  351. file.message = '上传失败';
  352. ElMessage.error('图片上传失败,请稍后再试!')
  353. }
  354. }
  355. showUploadProgressPopup.value = true;
  356. // 所有文件上传结束后再打开进度弹窗,此时 imgArr 已包含全部图片
  357. if (initImgArr.value.length > 0 && currentItem.value.interactionTypeId != 1 && currentItem.value.questionStatus === 3) {
  358. setTimeout(() => {
  359. // 只在第一次时初始化地图,后续复用已有实例
  360. if (!drawRegionMap.kmap) {
  361. drawRegionMap.initMap(
  362. "POINT (113.6142086995688 23.585836479509055)",
  363. mapContainer.value,
  364. false,
  365. false,
  366. false
  367. );
  368. }
  369. // 先清空所有图层,再按规则分别回显:接口 rangeWkt 用 setStatusRegions,本地 session 用 setAreaGeometry,可共存
  370. drawRegionMap.clearLayer && drawRegionMap.clearLayer();
  371. const savedId = sessionStorage.getItem("drawRegionInteractionId");
  372. const currentId = currentItem.value?.id;
  373. const isSameItem =
  374. savedId != null &&
  375. currentId != null &&
  376. (String(currentId) === savedId || Number(currentId) === Number(savedId));
  377. const hasSessionPolygon = !!sessionStorage.getItem("drawRegionPolygonData");
  378. // 1)接口返回的 rangeWkt:始终用 setStatusRegions 渲染到只读图层
  379. if (currentItem.value?.rangeWkt?.length > 10) {
  380. renderRegionFromItemWkt(currentItem.value);
  381. }
  382. // 2)本地刚编辑的多边形(同一项且有 session 数据):用 setAreaGeometry 渲染到可编辑图层
  383. if (isSameItem && hasSessionPolygon) {
  384. const polygonStr = sessionStorage.getItem("drawRegionPolygonData");
  385. if (polygonStr) {
  386. try {
  387. const polygonData = JSON.parse(polygonStr);
  388. if (
  389. polygonData &&
  390. Array.isArray(polygonData.geometryArr) &&
  391. polygonData.geometryArr.length > 0 &&
  392. drawRegionMap.kmap &&
  393. drawRegionMap.kmap.polygonLayer
  394. ) {
  395. // 此处不再调用 clearLayer,避免清掉上一步 setStatusRegions 的只读区域
  396. drawRegionMap.setAreaGeometry(
  397. polygonData.geometryArr,
  398. true,
  399. polygonData.mianji
  400. );
  401. }
  402. } catch (e) {
  403. console.error("解析 drawRegionPolygonData 失败:", e);
  404. }
  405. }
  406. }
  407. if (drawRegionMap.fitAllRegions) {
  408. drawRegionMap.fitAllRegions();
  409. }
  410. // 3)其它情况:保持清空后的空地图
  411. }, 100);
  412. }
  413. };
  414. const currentItem = ref(null);
  415. const handleUploadClick = (item) => {
  416. currentItem.value = item;
  417. };
  418. // 示例照片轮播
  419. const showExamplePopup = ref(false);
  420. const exampleList = ref([]);
  421. const exampleStartIndex = ref(0);
  422. const exampleShowTitleAndTips = ref(true);
  423. const currentPhotData = ref({})
  424. const showExample = (item, index, options = {}) => {
  425. currentPhotData.value = item;
  426. exampleList.value = options?.hideLabel ? item.imagePaths.map(p => ({exampleImageUrl: base_img_url2 + p})) : item.exampleImageWithAnnotations || [];
  427. exampleStartIndex.value = index || 0;
  428. exampleShowTitleAndTips.value = options.hideLabel !== true;
  429. showExamplePopup.value = true;
  430. };
  431. // 加载数据
  432. /* */
  433. const loadData = async () => {
  434. loading.value = true;
  435. try {
  436. const { data } = await VE_API.home.listTriggeredByFarm({ farmId: localStorage.getItem("selectedFarmId") })
  437. listData.value = data.map(item => {
  438. // question 按 || 切割成数组,用于循环渲染
  439. const questionStr = item.question != null ? String(item.question) : '';
  440. item.questionList = questionStr
  441. ? questionStr.split('||').map(s => s.trim()).filter(Boolean)
  442. : [];
  443. if (!item.questionList.length && questionStr !== '') {
  444. item.questionList = [questionStr];
  445. }
  446. // 保证 answerValues 与 questionList 长度一致,便于每项对应一个输入框
  447. if (!Array.isArray(item.answerValues)) {
  448. item.answerValues = [];
  449. }
  450. while (item.answerValues.length < item.questionList.length) {
  451. item.answerValues.push('');
  452. }
  453. return {
  454. ...item
  455. };
  456. });
  457. } catch (error) {
  458. // 加载数据失败时静默处理或在需要时提示
  459. } finally {
  460. loading.value = false;
  461. }
  462. };
  463. // 刷新列表数据
  464. const refreshList = async () => {
  465. listData.value = [];
  466. await loadData();
  467. };
  468. // 删除已上传的图片
  469. const removeUploadedImage = (item, imgIndex) => {
  470. item.imagePaths.splice(imgIndex, 1);
  471. };
  472. // 确认上传 / 暂未到达进程
  473. const handleConfirm = async (item, isConfirm) => {
  474. if (isConfirm) {
  475. const list = item.questionList || [];
  476. // const needFill = list.length || 1;
  477. // const hasEmpty = Array.from({ length: needFill }, (_, i) => i).some(
  478. // (i) => item.answerValues[i] === '' || item.answerValues[i] === null || item.answerValues[i] === undefined
  479. // );
  480. // if (hasEmpty) {
  481. // ElMessage.warning("请填写当前果园比例");
  482. // return;
  483. // }
  484. if (item.imagePaths.length === 0 && uploadData.value.length === 0) {
  485. ElMessage.warning("请上传图片");
  486. return;
  487. }
  488. }
  489. const parmas = {
  490. farmId: localStorage.getItem("selectedFarmId"),
  491. imagePaths: item.imagePaths.concat(uploadData.value),
  492. isUploadPhoto: item.imagePaths.concat(uploadData.value).length > 0 ? true : false,
  493. rangeWkt: sessionStorage.getItem("drawRegionPolygonData") || '',
  494. interactionId: item.id,
  495. replyText: item.replyText,
  496. answerValues: item.answerValues
  497. }
  498. await VE_API.home.uploadAnswerData(parmas);
  499. const { code, msg } = await VE_API.home.uploadAnswer(parmas);
  500. if (code === 0) {
  501. ElMessage.success("上传成功");
  502. // 清空上传数据
  503. uploadData.value = [];
  504. // 刷新列表
  505. await refreshList();
  506. sessionStorage.removeItem("drawRegionPolygonData");
  507. sessionStorage.removeItem("drawRegionInteractionId");
  508. } else {
  509. ElMessage.error(msg || '上传失败');
  510. }
  511. };
  512. const handleConfirmUpload = async () => {
  513. // 校验是否有上传图片
  514. if (!uploadData.value || uploadData.value.length === 0) {
  515. ElMessage.warning("请先上传照片");
  516. return;
  517. }
  518. const item = currentItem.value;
  519. const len = item?.questionList?.length || 1;
  520. // const hasEmpty = Array.from({ length: len }, (_, i) => i).some(
  521. // (i) => item.answerValues[i] === '' || item.answerValues[i] === null || item.answerValues[i] === undefined
  522. // );
  523. // if (hasEmpty) {
  524. // ElMessage.warning("请填写占比数值");
  525. // return;
  526. // }
  527. const answerVals = (item.answerValues || []).slice(0, len);
  528. const parmas = {
  529. interactionId: item.id,
  530. farmId: localStorage.getItem("selectedFarmId"),
  531. imagePaths: uploadData.value,
  532. rangeWkt: sessionStorage.getItem("drawRegionPolygonData") || '',
  533. replyText: item.replyText,
  534. answerValues: answerVals,
  535. }
  536. const { code, msg } = await VE_API.home.uploadAnswerData(parmas);
  537. if (code === 0) {
  538. ElMessage.success("确认成功");
  539. // 清空上传数据及勾画关联
  540. uploadData.value = [];
  541. sessionStorage.removeItem("drawRegionPolygonData");
  542. sessionStorage.removeItem("drawRegionInteractionId");
  543. showUploadProgressPopup.value = false;
  544. // 刷新列表
  545. await refreshList();
  546. } else {
  547. ElMessage.error(msg || '确认失败');
  548. }
  549. };
  550. // 切换展开/收起
  551. const toggleExpand = (item) => {
  552. item.expanded = !item.expanded;
  553. };
  554. const handleViewRegion = (item) => {
  555. router.push(`/draw_region?viewOnly=1&rangeWkt=${encodeURIComponent(item.rangeWkt)}&updatedTime=${item.updatedTime.slice(0, 10)}`);
  556. }
  557. const handleDrawRegion = (item) => {
  558. if (sessionStorage.getItem("drawRegionInteractionId") != item.id) {
  559. sessionStorage.removeItem("drawRegionPolygonData");
  560. sessionStorage.removeItem("drawRegionInteractionId");
  561. }
  562. // 记录本次勾画对应的互动项 id,上传时若是同一项则保留回显,否则清除避免数据串
  563. sessionStorage.setItem("drawRegionInteractionId", String(item.id));
  564. const polygonData = sessionStorage.getItem("drawRegionPolygonData");
  565. // 记录当前滚动位置
  566. const container = interactionListRef.value;
  567. if (container && typeof container.scrollTop === 'number') {
  568. sessionStorage.setItem(SCROLL_KEY, String(container.scrollTop));
  569. }
  570. if (item.rangeWkt && item.rangeWkt.length > 10) {
  571. router.push(`/draw_region?polygonData=${polygonData}&rangeWkt=${item.rangeWkt}&updatedTime=${item.updatedTime.slice(0, 10)}`);
  572. } else {
  573. if (polygonData) {
  574. router.push(`/draw_region?polygonData=${polygonData}`);
  575. } else {
  576. router.push(`/draw_region`);
  577. }
  578. }
  579. };
  580. onUnmounted(() => {
  581. sessionStorage.removeItem("drawRegionPolygonData");
  582. sessionStorage.removeItem("drawRegionInteractionId");
  583. });
  584. const uploadData = ref([]);
  585. const handleUploadSuccess = (data) => {
  586. uploadData.value = data.imgArr;
  587. // 同步进度条:删除/增加图片时,总数和已上传数跟随当前列表变化
  588. const len = (data.imgArr && data.imgArr.length) || 0;
  589. totalUploadCount.value = len;
  590. uploadedSuccessCount.value = len;
  591. };
  592. const openMorePopup = (item) => {
  593. const data = item.exampleImageWithAnnotations.map(exampleItem => {
  594. return {
  595. ...exampleItem,
  596. shootingMethod: item.shootingMethod,
  597. }
  598. });
  599. morePopupRef.value.setItems(data);
  600. morePopupRef.value.openPopup();
  601. }
  602. const onCopyWechatId = () => {
  603. ElMessage.success("微信号已复制");
  604. };
  605. const oldUser = ref(false);
  606. // 页面从勾画页返回时,如果组件被 keep-alive 缓存,则会触发 onActivated,在此再做一次回显
  607. onActivated(() => {
  608. // 初始化加载
  609. getFarmList();
  610. // 加载数据
  611. loadData();
  612. renderRegionFromSession();
  613. // 恢复滚动位置
  614. const container = interactionListRef.value;
  615. const savedTop = sessionStorage.getItem(SCROLL_KEY);
  616. if (container && savedTop != null) {
  617. const top = Number(savedTop);
  618. if (!Number.isNaN(top)) {
  619. // 延迟一点,等 DOM 渲染完成后再滚动
  620. setTimeout(() => {
  621. container.scrollTop = top;
  622. }, 0);
  623. }
  624. }
  625. oldUser.value = query.value.oldUser && Boolean(query.value.oldUser);
  626. if (oldUser.value) {
  627. showFarmInfoPopup.value = true;
  628. }
  629. });
  630. const getFarmList = async () => {
  631. const { data } = await VE_API.farm.userFarmSelectOption();
  632. if (data && data.length === 0) {
  633. showFarmInfoPopup.value = true;
  634. }
  635. }
  636. const handleSubmitAll = () => {
  637. console.log("一键提交");
  638. };
  639. </script>
  640. <style scoped lang="scss">
  641. .interaction-list {
  642. width: 100%;
  643. height: calc(100vh - 40px);
  644. background: #f2f4f5;
  645. padding: 12px;
  646. box-sizing: border-box;
  647. overflow-y: auto;
  648. .list-item {
  649. background: #ffffff;
  650. border-radius: 6px;
  651. padding: 10px;
  652. border: 1px solid #ffffff;
  653. &.uploaded-item {
  654. border: 1px solid #2199f8;
  655. }
  656. .item-header-wrapper {
  657. .item-header {
  658. display: flex;
  659. align-items: center;
  660. justify-content: space-between;
  661. div {
  662. font-size: 16px;
  663. color: #6f6f6f;
  664. font-family: "PangMenZhengDao";
  665. width: fit-content;
  666. background: rgba(143, 143, 143, 0.1);
  667. padding: 5px 10px;
  668. border-radius: 2px;
  669. }
  670. .status {
  671. color: #999999;
  672. background: #F1F1F1;
  673. &.urgent-0 {
  674. color: #2199f8;
  675. background: rgba(33, 153, 248, 0.1);
  676. }
  677. &.urgent-1 {
  678. color: #FF953D;
  679. background: rgba(255, 149, 61, 0.1);
  680. }
  681. &.urgent-2 {
  682. color: #EE4646;
  683. background: rgba(238, 70, 70, 0.1);
  684. }
  685. }
  686. }
  687. .upload-status {
  688. display: flex;
  689. align-items: center;
  690. gap: 4px;
  691. margin-left: 10px;
  692. .status-icon {
  693. color: #2199f8;
  694. font-size: 17px;
  695. }
  696. .status-text {
  697. font-size: 14px;
  698. color: #2199f8;
  699. }
  700. }
  701. &.has-status {
  702. display: flex;
  703. justify-content: space-between;
  704. align-items: center;
  705. .item-header {
  706. div {
  707. color: #2199f8;
  708. background: rgba(33, 153, 248, 0.1);
  709. }
  710. }
  711. }
  712. }
  713. .expanded-content {
  714. background: rgba(33, 153, 248, 0.1);
  715. border-radius: 5px;
  716. padding: 10px;
  717. border: 0.5px solid #2199F8;
  718. margin-top: 12px;
  719. .uploaded-images {
  720. display: flex;
  721. flex-wrap: wrap;
  722. gap: 8px;
  723. margin-top: 8px;
  724. .uploaded-img-wrap {
  725. position: relative;
  726. width: calc((100vw - 68px) / 4);
  727. height: calc((100vw - 68px) / 4);
  728. }
  729. .uploaded-img {
  730. width: 100%;
  731. height: 100%;
  732. border-radius: 4px;
  733. object-fit: cover;
  734. }
  735. .uploaded-img-remove {
  736. position: absolute;
  737. top: 0;
  738. right: 0;
  739. width: 18px;
  740. height: 18px;
  741. display: flex;
  742. justify-content: center;
  743. background: rgba(0, 0, 0, 0.6);
  744. color: #fff;
  745. font-size: 14px;
  746. line-height: 16px;
  747. border-radius: 50%;
  748. }
  749. }
  750. .continue-upload-btn {
  751. border: 0.5px solid rgba(33, 153, 248, 0.5);
  752. border-radius: 4px;
  753. background: #FFFFFF;
  754. cursor: pointer;
  755. }
  756. }
  757. .uploaded-content {
  758. .content-wrapper {
  759. border: 0.5px solid rgba(0, 0, 0, 0.1);
  760. border-radius: 4px;
  761. padding: 10px;
  762. margin-top: 12px;
  763. .item-desc {
  764. color: #3C3C3C;
  765. margin-bottom: 10px;
  766. display: inline-block;
  767. }
  768. .tip-box {
  769. font-size: 12px;
  770. color: #2199F8;
  771. padding: 3px 5px;
  772. border-radius: 4px;
  773. margin-bottom: 10px;
  774. background: linear-gradient(90deg, rgba(33, 153, 248, 0.2) 0%, rgba(221, 221, 221, 0) 100%);
  775. }
  776. .example-wrapper {
  777. .example-header {
  778. display: flex;
  779. align-items: center;
  780. justify-content: space-between;
  781. margin-bottom: 8px;
  782. color: #BEB9B9;
  783. .more {
  784. font-size: 12px;
  785. }
  786. }
  787. .example-list {
  788. display: flex;
  789. align-items: center;
  790. overflow: hidden;
  791. .image-item-wrapper {
  792. position: relative;
  793. margin-right: 6px;
  794. &::after {
  795. content: '示例';
  796. position: absolute;
  797. top: 0;
  798. left: 0;
  799. background: rgba(0, 0, 0, 0.7);
  800. color: #F2F4F5;
  801. padding: 2px 6px;
  802. border-radius: 8px 0 2px 0;
  803. font-size: 10px;
  804. z-index: 1;
  805. }
  806. }
  807. .image-item {
  808. width: calc((100vw - 68px) / 4);
  809. height: calc((100vw - 68px) / 4);
  810. border-radius: 8px;
  811. object-fit: cover;
  812. }
  813. }
  814. }
  815. .patrol-suggestion {
  816. color: rgba(60, 60, 60, 0.4);
  817. margin-top: 10px;
  818. .action-text {
  819. color: #727272;
  820. }
  821. }
  822. }
  823. .upload-wrap {
  824. width: 100%;
  825. margin-top: 12px;
  826. ::v-deep {
  827. .van-uploader__input-wrapper {
  828. width: 100%;
  829. }
  830. }
  831. .upload-btn {
  832. width: 100%;
  833. display: flex;
  834. align-items: center;
  835. justify-content: center;
  836. gap: 4px;
  837. color: #0B84E4;
  838. padding: 6px;
  839. border: 0.5px solid rgba(33, 153, 248, 0.5);
  840. border-radius: 4px;
  841. box-sizing: border-box;
  842. }
  843. }
  844. }
  845. .input-wrapper {
  846. margin: 12px 0;
  847. }
  848. .button-group {
  849. display: flex;
  850. gap: 12px;
  851. .btn-not-reached,
  852. .btn-default {
  853. flex: 1;
  854. text-align: center;
  855. border-radius: 4px;
  856. padding: 6px;
  857. }
  858. .btn-not-reached {
  859. max-width: fit-content;
  860. background: #fff;
  861. color: #585858;
  862. border: 1px solid rgba(88, 88, 88, 0.2);
  863. }
  864. .btn-default {
  865. background: #F1F1F1;
  866. color: #999999;
  867. border: 1px solid #F1F1F1;
  868. }
  869. .btn-confirm {
  870. background: #2199f8;
  871. color: #ffffff;
  872. border: 1px solid #2199f8;
  873. }
  874. }
  875. .proportion-info {
  876. display: flex;
  877. justify-content: space-between;
  878. align-items: center;
  879. margin-top: 12px;
  880. .proportion-text {
  881. color: #969696;
  882. font-size: 14px;
  883. white-space: nowrap;
  884. overflow: hidden;
  885. text-overflow: ellipsis;
  886. flex: 1;
  887. margin-right: 10px;
  888. }
  889. .toggle-btn {
  890. display: flex;
  891. align-items: center;
  892. gap: 2px;
  893. color: #8D8D8D;
  894. padding: 2px 10px;
  895. border: 1px solid rgba(0, 0, 0, 0.2);
  896. font-size: 12px;
  897. border-radius: 25px;
  898. background: #FFFFFF;
  899. .rotate {
  900. transform: rotate(180deg);
  901. }
  902. }
  903. }
  904. }
  905. .list-item+.list-item {
  906. margin-top: 12px;
  907. }
  908. .empty-data {
  909. text-align: center;
  910. padding: 40px 0;
  911. color: #999999;
  912. font-size: 14px;
  913. }
  914. .question-wrapper {
  915. display: flex;
  916. align-items: flex-start;
  917. justify-content: space-between;
  918. gap: 10px;
  919. margin: 12px 0 6px;
  920. box-sizing: border-box;
  921. width: 100%;
  922. .question-text {
  923. background: #ffffff;
  924. color: #6f6f6f;
  925. display: flex;
  926. align-items: center;
  927. .text-title {
  928. margin-right: 5px;
  929. }
  930. .text-unit {
  931. margin-left: 4px;
  932. }
  933. }
  934. .question-text+.question-text {
  935. margin-top: 6px;
  936. }
  937. .draw-region-btn {
  938. background: rgba(33, 153, 248, 0.1);
  939. border-radius: 4px;
  940. padding: 6px 10px;
  941. color: #2199f8;
  942. }
  943. }
  944. }
  945. .center-btn {
  946. justify-content: center;
  947. .primary-btn {
  948. padding: 10px 32px;
  949. background: #2199F8;
  950. }
  951. }
  952. .upload-progress-popup {
  953. width: 100%;
  954. padding: 20px 16px;
  955. .upload-progress-title {
  956. font-size: 16px;
  957. color: #121212;
  958. margin-bottom: 12px;
  959. display: flex;
  960. align-items: center;
  961. justify-content: space-between;
  962. .upload-progress {
  963. width: 55%;
  964. ::v-deep {
  965. .el-progress__text {
  966. min-width: fit-content;
  967. }
  968. }
  969. }
  970. }
  971. .upload-box {
  972. margin-bottom: 12px;
  973. }
  974. .input-box {
  975. .input-item {
  976. display: flex;
  977. align-items: center;
  978. justify-content: space-between;
  979. gap: 6px;
  980. .label-input {
  981. width: 130px;
  982. }
  983. }
  984. .input-item+.input-item {
  985. margin-top: 6px;
  986. }
  987. }
  988. .region-tips {
  989. color: #2199F8;
  990. padding: 5px;
  991. background: rgba(33, 153, 248, 0.1);
  992. border-radius: 5px;
  993. margin: 16px 0 12px 0;
  994. text-align: center;
  995. }
  996. .region-map {
  997. width: 100%;
  998. height: 168px;
  999. clip-path: inset(0px round 5px);
  1000. position: relative;
  1001. .region-map-text {
  1002. position: absolute;
  1003. bottom: 0;
  1004. right: 0;
  1005. color: #FFFFFF;
  1006. padding: 5px 20px;
  1007. border-radius: 5px;
  1008. background: rgba(0, 0, 0, 0.6);
  1009. z-index: 1;
  1010. }
  1011. }
  1012. .confirm-btn {
  1013. background: #2199f8;
  1014. color: #ffffff;
  1015. border-radius: 4px;
  1016. padding: 8px;
  1017. text-align: center;
  1018. font-size: 16px;
  1019. margin-top: 16px;
  1020. }
  1021. }
  1022. </style>