index.vue 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160
  1. <template>
  2. <div class="agri-file-page" :style="{ height: `calc(100vh - ${tabBarHeight}px)` }">
  3. <farm-header />
  4. <div class="nav-cards">
  5. <div
  6. v-for="item in navCards"
  7. :key="item.key"
  8. class="nav-card-wrap"
  9. :class="{ 'nav-card-wrap--report': item.key === 'report' }"
  10. >
  11. <div
  12. class="nav-card"
  13. :class="{ 'is-active': activeNav === item.key }"
  14. @click="handleNavClick(item)"
  15. >
  16. <img class="nav-card__icon" :src="item.icon" alt="" />
  17. <span class="nav-card__label">{{ t(item.labelKey) }}</span>
  18. </div>
  19. <div v-if="item.key === 'report' && guideVisible" class="report-guide-bubble">
  20. <span class="report-guide-bubble__arrow"></span>
  21. <div class="report-guide-bubble__content">
  22. {{ t("agriFile.diagnosisReportGuidePrefix") }}
  23. <span class="report-guide-bubble__link" @click.stop="handleGuideUpload">
  24. {{ t("agriFile.uploadPhoto") }}
  25. </span>
  26. {{ t("agriFile.diagnosisReportGuideSuffix") }}
  27. </div>
  28. </div>
  29. </div>
  30. </div>
  31. <div class="content-panel">
  32. <div v-show="activeNav === 'patrol'">
  33. <div v-for="item in patrolList" :key="item.id" class="tracking-item"
  34. :class="`tracking-item--${item.theme}`" @click="goGrowthTrack(item)">
  35. <div class="tracking-item__header">
  36. <div class="tracking-item__header-row">
  37. <div class="tracking-item__header-left">
  38. <span class="tracking-item__level">
  39. <el-icon><BellFilled /></el-icon>
  40. {{ item.level }}
  41. </span>
  42. <span class="tracking-item__title van-ellipsis">
  43. {{ item.title }}<span v-if="item.zone" class="tracking-item__zone"> ({{ item.zone }})</span>
  44. </span>
  45. </div>
  46. <!-- <div class="tracking-item__share" @click.stop="handleShare(item)">
  47. <Icon size="14" color="#2199F8" name="share" />
  48. <span>{{ t("agriFile.forward") }}</span>
  49. </div> -->
  50. </div>
  51. </div>
  52. <div class="tracking-item__body">
  53. <div class="tracking-item__action">
  54. <div class="tracking-item__action-main">
  55. <div class="tracking-item__icon">
  56. <img :src="item.icon" alt="" />
  57. <span class="tracking-item__reason">{{ item.subject }}</span>
  58. </div>
  59. <div class="tracking-item__issue">{{ item.issue }}</div>
  60. </div>
  61. <!-- <div class="tracking-item__btn" @click.stop="handleUploadClick(item)">
  62. <el-icon>
  63. <Plus />
  64. </el-icon>
  65. <span>{{ t("agriFile.uploadPhoto") }}</span>
  66. </div> -->
  67. </div>
  68. </div>
  69. </div>
  70. <div class="crop-archive">
  71. <div class="crop-archive__title">{{ t("agriFile.cropArchive") }}</div>
  72. <PhenologyTrackTimelineItem :list="cropArchiveList" />
  73. </div>
  74. </div>
  75. <div v-show="activeNav === 'album'">
  76. <div class="map-card" @click="goAlbumMap">
  77. <div ref="mapContainer" class="map-container"></div>
  78. <!-- <div v-if="noFarmRange" class="map-mask" @click.stop="goAddZone('farm')">
  79. <div class="mask-content">
  80. <div class="mask-content__title">勾画种植区域,关联作物照片</div>
  81. <div class="mask-content__btn">去勾选</div>
  82. </div>
  83. </div> -->
  84. <!-- <div v-else class="add-zone-btn" @click.stop="goAddZone('region')">
  85. <el-icon>
  86. <Plus />
  87. </el-icon>
  88. <span>{{ t("agriFile.addZone") }}</span>
  89. </div> -->
  90. <div v-for="item in zoneList" :key="item.id" :ref="(el) => setMarkerEl(item.id, el)"
  91. class="zone-marker">
  92. <div class="zone-marker__label">{{ item.name }}</div>
  93. <div class="zone-marker__thumb">
  94. <img :src="item.cover" alt="" />
  95. <span class="zone-marker__badge">{{ item.count }}</span>
  96. </div>
  97. <div class="zone-marker__arrow"></div>
  98. </div>
  99. </div>
  100. <div class="album-grid" v-if="!noFarmRange">
  101. <!-- <div class="album-add" @click.stop="goAddZone('region')">
  102. <el-icon class="album-add__icon">
  103. <Plus />
  104. </el-icon>
  105. <span>{{ t("agriFile.addAlbum") }}</span>
  106. </div> -->
  107. <div v-for="item in albumList" :key="item.id" class="album-card" :class="{ 'is-empty': !item.count }">
  108. <div class="album-card__cover" @click.stop="goAlbumDetail(item)">
  109. <img v-if="item.count && item.cover" :src="item.cover" alt="" />
  110. <img v-else class="album-card__empty-icon" src="@/assets/img/agricultural/photo.png" alt="" />
  111. <span class="album-card__count">{{ t("agriFile.photoCount", { count: item.count }) }}</span>
  112. <!-- <div class="album-card__add" @click.stop="openUploadPopup">
  113. <el-icon>
  114. <Plus />
  115. </el-icon>
  116. </div> -->
  117. </div>
  118. <div class="album-card__name">{{ item.name }}</div>
  119. </div>
  120. </div>
  121. </div>
  122. <div v-show="activeNav === 'report'">
  123. <div v-for="item in reportList" :key="item.id" class="report-card">
  124. <div class="report-card__cover">
  125. <img :src="item.cover" alt="" />
  126. <span class="report-card__date">{{ item.date }}</span>
  127. <span class="report-card__status">
  128. <i class="report-card__dot"></i>
  129. {{ t("agriFile.reportUpdate") }}
  130. </span>
  131. </div>
  132. <div class="report-card__body">
  133. <div class="report-card__title">{{ item.title }}</div>
  134. <div class="report-card__desc">{{ item.desc }}</div>
  135. </div>
  136. </div>
  137. </div>
  138. </div>
  139. <album-upload-popup v-model:show="showUploadPopup" />
  140. <complete-farm-info-popup v-model:show="showCompleteFarmPopup" @confirm="goCompleteFarmInfo" />
  141. <!-- 诊断报告弹窗 -->
  142. <diagnosis-report-popup
  143. v-model:show="showDiagnosisReportPopup"
  144. @confirm="goDiagnosisReport" />
  145. <!-- 恢复种植弹窗 -->
  146. <restore-planting-popup />
  147. <!-- 代管弹窗:后续由接口控制是否展示 -->
  148. <proxy-auth-popup
  149. v-model:show="showProxyAuthPopup"
  150. :service-name="proxyServiceName"
  151. @reject="handleProxyReject"
  152. @allow="handleProxyAllow"
  153. />
  154. <!-- 申请更换物候期弹窗:后续由接口控制是否展示 -->
  155. <phenology-update-popup
  156. v-model:show="showPhenologyUpdatePopup"
  157. :service-name="phenologyUpdateInfo.serviceName"
  158. :current-stage="phenologyUpdateInfo.currentStage"
  159. :target-stage="phenologyUpdateInfo.targetStage"
  160. :current-image="phenologyUpdateInfo.currentImage"
  161. :farm-location="phenologyUpdateInfo.farmLocation"
  162. @reject="handlePhenologyUpdateReject"
  163. @confirm="handlePhenologyUpdateConfirm"
  164. />
  165. </div>
  166. </template>
  167. <script setup>
  168. import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from "vue";
  169. import { useStore } from "vuex";
  170. import { useRouter } from "vue-router";
  171. import Overlay from "ol/Overlay";
  172. import { useI18n } from "@/i18n";
  173. import { Icon } from 'vant';
  174. import FileMap from "./fileMap";
  175. import * as util from "@/common/ol_common.js";
  176. import farmHeader from "@/components/farmHeader.vue";
  177. import albumUploadPopup from "./components/albumUploadPopup.vue";
  178. import completeFarmInfoPopup from "@/components/popup/completeFarmInfoPopup.vue";
  179. import diagnosisReportPopup from "@/components/popup/diagnosisReportPopup.vue";
  180. import restorePlantingPopup from "@/components/popup/restorePlantingPopup.vue";
  181. import proxyAuthPopup from "@/components/popup/proxyAuthPopup.vue";
  182. import phenologyUpdatePopup from "@/components/popup/phenologyUpdatePopup.vue";
  183. import PhenologyTrackTimelineItem from "@/components/pageComponents/PhenologyTrackTimelineItem.vue";
  184. import wx from "weixin-js-sdk";
  185. const { t } = useI18n();
  186. const store = useStore();
  187. const router = useRouter();
  188. const tabBarHeight = computed(() => store.state.home.tabBarHeight);
  189. const DIAGNOSIS_GUIDE_STORAGE_KEY = "AGRI_FILE_DIAGNOSIS_REPORT_GUIDE";
  190. const ENTRY_FARM_DATA_KEY = "ENTRY_FARM_DATA";
  191. const CROP_ARCHIVE_KEY = "CROP_ARCHIVE_RECORDS";
  192. const guideVisible = ref(false);
  193. const defaultCover = require("@/assets/img/home/banner.png");
  194. const DEFAULT_CENTER = [113.6142086995688, 23.585836479509055];
  195. const activeNav = ref("patrol");
  196. const mapContainer = ref(null);
  197. const fileMap = new FileMap();
  198. const markerEls = {};
  199. const mapOverlays = [];
  200. const noFarmRange = ref(false);
  201. const navCards = [
  202. {
  203. key: "patrol",
  204. labelKey: "agriFile.patrolRecord",
  205. icon: require("@/assets/img/agricultural/icon-1.png"),
  206. },
  207. {
  208. key: "album",
  209. labelKey: "agriFile.agriAlbum",
  210. icon: require("@/assets/img/agricultural/icon-2.png"),
  211. },
  212. {
  213. key: "report",
  214. labelKey: "agriFile.diagnosisReport",
  215. icon: require("@/assets/img/agricultural/icon-3.png"),
  216. },
  217. ];
  218. /** 读取用户新增农场时选中的点位 */
  219. function readEntryFarmCoordinate() {
  220. try {
  221. const data = JSON.parse(localStorage.getItem(ENTRY_FARM_DATA_KEY) || "null");
  222. if (Array.isArray(data?.coordinate) && data.coordinate.length === 2) {
  223. return [Number(data.coordinate[0]), Number(data.coordinate[1])];
  224. }
  225. const point =
  226. data?.point ||
  227. localStorage.getItem("GROWTH_REPORT_MAP_POINT") ||
  228. localStorage.getItem("selectedFarmPoint");
  229. if (typeof point === "string" && /^POINT\s*\(/i.test(point.trim())) {
  230. return util.wktCastGeom(point).getFirstCoordinate();
  231. }
  232. } catch {
  233. // ignore
  234. }
  235. return null;
  236. }
  237. function buildZoneList(coordinate = readEntryFarmCoordinate() || DEFAULT_CENTER) {
  238. return [
  239. {
  240. id: 1,
  241. name: "",
  242. count: 2,
  243. cover: defaultCover,
  244. longitude: Number(coordinate[0]),
  245. latitude: Number(coordinate[1]),
  246. },
  247. ];
  248. }
  249. const zoneList = ref(buildZoneList());
  250. const albumList = ref([
  251. { id: 1, name: "", count: 125, cover: defaultCover },
  252. ]);
  253. const reportCover = require("@/assets/img/common/sd-1.jpg");
  254. const reportList = ref([
  255. { id: 1, title: "", desc: "", date: "", cover: reportCover },
  256. { id: 2, title: "", desc: "", date: "", cover: require("@/assets/img/common/sd-2.jpg") },
  257. { id: 3, title: "", desc: "", date: "", cover: require("@/assets/img/common/sd-3.jpg") },
  258. ]);
  259. const showUploadPopup = ref(false);
  260. const showCompleteFarmPopup = ref(false);
  261. const showDiagnosisReportPopup = ref(true);
  262. // 后续由接口控制是否展示代管授权弹窗
  263. const showProxyAuthPopup = ref(false);
  264. const proxyServiceName = ref("农服名称");
  265. const handleProxyReject = () => {
  266. // TODO: 调用拒绝代管接口
  267. };
  268. const handleProxyAllow = () => {
  269. // TODO: 调用允许代管接口
  270. };
  271. // 后续由接口控制是否展示申请更换物候期弹窗
  272. const showPhenologyUpdatePopup = ref(false);
  273. const phenologyUpdateInfo = ref({
  274. serviceName: "某某农服",
  275. currentStage: "花穗期",
  276. targetStage: "小果期",
  277. varietyName: "荔枝-桂味",
  278. farmLocation: "位置位置位置",
  279. });
  280. const handlePhenologyUpdateReject = () => {
  281. // TODO: 调用暂不更新接口
  282. };
  283. const handlePhenologyUpdateConfirm = () => {
  284. // TODO: 调用确认更新接口
  285. };
  286. const patrolList = ref([
  287. {
  288. id: 1,
  289. type: "growth",
  290. theme: "blue",
  291. level: "",
  292. title: "",
  293. subject: "",
  294. issue: "",
  295. icon: require("@/assets/img/report/wh-icon.png"),
  296. },
  297. {
  298. id: 2,
  299. type: "abnormal",
  300. theme: "blue",
  301. level: "",
  302. title: "",
  303. subject: "",
  304. issue: "",
  305. icon: require("@/assets/img/report/wh-icon.png"),
  306. },
  307. ]);
  308. const MOCK_CROP_ARCHIVE = [
  309. { id: "mock-2", date: "2025-04-18", contentKey: "agriFile.cropArchiveAbnormal" },
  310. { id: "mock-3", date: "2025-04-10", contentKey: "agriFile.cropArchiveMetric" },
  311. ];
  312. const cropArchiveList = ref([]);
  313. function readCropArchiveAnswers() {
  314. try {
  315. const list = JSON.parse(localStorage.getItem(CROP_ARCHIVE_KEY) || "[]");
  316. return Array.isArray(list) ? list : [];
  317. } catch {
  318. return [];
  319. }
  320. }
  321. const setMarkerEl = (id, el) => {
  322. if (el) markerEls[id] = el;
  323. else delete markerEls[id];
  324. };
  325. const getFarmCenter = () => {
  326. const entryCoordinate = readEntryFarmCoordinate();
  327. if (entryCoordinate) return entryCoordinate;
  328. try {
  329. const farm = JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
  330. const wkt = farm.wkt || farm.geom_wkt || farm.farm_location;
  331. if (typeof wkt === "string" && /^POINT\s*\(/i.test(wkt.trim())) {
  332. return util.wktCastGeom(wkt).getFirstCoordinate();
  333. }
  334. } catch {
  335. // ignore
  336. }
  337. return DEFAULT_CENTER;
  338. };
  339. const clearMapOverlays = () => {
  340. if (!fileMap.kmap?.map) {
  341. mapOverlays.length = 0;
  342. return;
  343. }
  344. mapOverlays.forEach((overlay) => fileMap.kmap.map.removeOverlay(overlay));
  345. mapOverlays.length = 0;
  346. };
  347. const bindZoneOverlays = () => {
  348. if (!fileMap.kmap?.map) return;
  349. clearMapOverlays();
  350. zoneList.value.forEach((zone) => {
  351. const el = markerEls[zone.id];
  352. if (!el || zone.longitude == null || zone.latitude == null) return;
  353. const overlay = new Overlay({
  354. element: el,
  355. position: [zone.longitude, zone.latitude],
  356. positioning: "bottom-center",
  357. stopEvent: false,
  358. });
  359. fileMap.kmap.map.addOverlay(overlay);
  360. mapOverlays.push(overlay);
  361. });
  362. };
  363. const syncZoneFromEntry = () => {
  364. const coordinate = readEntryFarmCoordinate() || getFarmCenter();
  365. const zoneName = zoneList.value[0]?.name || "";
  366. zoneList.value = buildZoneList(coordinate).map((item) => ({
  367. ...item,
  368. name: zoneName,
  369. }));
  370. };
  371. const initAlbumMap = async () => {
  372. await nextTick();
  373. if (!mapContainer.value) return;
  374. syncZoneFromEntry();
  375. const center = getFarmCenter();
  376. fileMap.initMap(`POINT(${center[0]} ${center[1]})`, mapContainer.value);
  377. await nextTick();
  378. if (noFarmRange.value) {
  379. zoneList.value = [];
  380. albumList.value = [];
  381. return;
  382. }
  383. bindZoneOverlays();
  384. fileMap.kmap?.map?.updateSize?.();
  385. };
  386. const fillMockLabels = () => {
  387. const zoneName = t("agriFile.zoneLabel", { n: 1 });
  388. const albumName = t("agriFile.zoneOne");
  389. zoneList.value = zoneList.value.map((item) => ({ ...item, name: zoneName }));
  390. albumList.value = albumList.value.map((item) => ({ ...item, name: albumName }));
  391. reportList.value = reportList.value.map((item) => ({
  392. ...item,
  393. title: t("agriFile.reportTitle"),
  394. desc: t("agriFile.reportDesc"),
  395. date: t("agriFile.reportDate"),
  396. }));
  397. patrolList.value = patrolList.value.map((item, index) => ({
  398. ...item,
  399. level: t("agriFile.riskLevel2"),
  400. title: t(index === 0 ? "agriFile.patrolGrowthTitle" : "agriFile.patrolAbnormalTitle"),
  401. zone: albumName,
  402. subject: t("agriFile.patrolSubject"),
  403. issue: t("agriFile.patrolIssue"),
  404. }));
  405. cropArchiveList.value = [
  406. ...readCropArchiveAnswers(),
  407. ...MOCK_CROP_ARCHIVE.map((item) => ({
  408. id: item.id,
  409. date: item.date,
  410. zone_name: albumName,
  411. content: t(item.contentKey),
  412. })),
  413. ];
  414. };
  415. const goAddZone = (type) => {
  416. router.push(
  417. {
  418. path: "/add_zone",
  419. query: {
  420. type,
  421. isSuccessPopup: type === 'farm' ? '1' : '0',
  422. },
  423. }
  424. );
  425. };
  426. const goAlbumMap = () => {
  427. router.push("/album_map");
  428. };
  429. const goGrowthTrack = (item) => {
  430. router.push({
  431. path: "/growth_track",
  432. query: { id: item?.id, type: item?.type || "growth" },
  433. });
  434. };
  435. const handleShare = (item) => {
  436. const query = {
  437. askInfo: { title: "转发巡园要点", content: "是否分享给好友" },
  438. shareText: item?.title || "速长窗口不追肥?后期发育慢人一步,直接掉队!",
  439. targetUrl: "growth_track",
  440. paramsPage: JSON.stringify({
  441. id: item?.id,
  442. type: item?.type || "growth",
  443. fromShare: 1,
  444. }),
  445. imageUrl: "https://birdseye-img.sysuimars.com/temp/field.png",
  446. };
  447. wx.miniProgram.navigateTo({
  448. url: `/pages/subPages/share_page/index?pageParams=${JSON.stringify(query)}&type=sharePage`,
  449. });
  450. };
  451. const needDraw = ref(true);
  452. const openUploadPopup = () => {
  453. showUploadPopup.value = true;
  454. };
  455. /** 与 growthTrack 上传一致:需勾画 → add_zone;异常 → pest_interact;否则弹上传 */
  456. const handleUploadClick = (item) => {
  457. const isAbnormal = item?.type === "abnormal";
  458. if (needDraw.value) {
  459. router.push({
  460. path: "/add_zone",
  461. query: {
  462. type: "farm",
  463. targetUrl: isAbnormal ? "/pest_interact" : "/growth_track",
  464. ...(isAbnormal ? { isAbnormal: "true" } : {}),
  465. ...(item?.id != null ? { trackId: item.id } : {}),
  466. ...(item?.type ? { trackType: item.type } : {}),
  467. },
  468. });
  469. return;
  470. }
  471. if (isAbnormal) {
  472. router.push({ path: "/pest_interact" });
  473. return;
  474. }
  475. showUploadPopup.value = true;
  476. };
  477. const dismissGuide = () => {
  478. if (!guideVisible.value) return;
  479. guideVisible.value = false;
  480. localStorage.setItem(DIAGNOSIS_GUIDE_STORAGE_KEY, "1");
  481. };
  482. const tryShowGuide = () => {
  483. if (localStorage.getItem(DIAGNOSIS_GUIDE_STORAGE_KEY)) return;
  484. guideVisible.value = true;
  485. };
  486. const handleGuideUpload = () => {
  487. dismissGuide();
  488. openUploadPopup();
  489. };
  490. const handleNavClick = (item) => {
  491. if (item.key === "report") dismissGuide();
  492. activeNav.value = item.key;
  493. };
  494. const handleOutsideClick = (event) => {
  495. if (!guideVisible.value) return;
  496. const target = event.target;
  497. if (
  498. target?.closest?.(".report-guide-bubble") ||
  499. target?.closest?.(".nav-card-wrap--report .nav-card")
  500. ) {
  501. return;
  502. }
  503. dismissGuide();
  504. };
  505. const goCompleteFarmInfo = () => {
  506. router.push("/entry_information");
  507. };
  508. const goDiagnosisReport = () => {
  509. router.push("/diagnosis_report");
  510. };
  511. const goAlbumDetail = (item) => {
  512. router.push({
  513. path: "/region_albums",
  514. query: {
  515. id: item?.id,
  516. name: item?.name || "",
  517. noImage: item?.count ? "false" : "true",
  518. },
  519. });
  520. };
  521. onMounted(() => {
  522. fillMockLabels();
  523. initAlbumMap();
  524. tryShowGuide();
  525. document.addEventListener("click", handleOutsideClick, true);
  526. });
  527. onActivated(() => {
  528. fillMockLabels();
  529. initAlbumMap();
  530. tryShowGuide();
  531. });
  532. onBeforeUnmount(() => {
  533. clearMapOverlays();
  534. document.removeEventListener("click", handleOutsideClick, true);
  535. });
  536. watch(activeNav, (key) => {
  537. if (key === "album") {
  538. initAlbumMap();
  539. }
  540. });
  541. </script>
  542. <style lang="scss" scoped>
  543. .agri-file-page {
  544. position: relative;
  545. display: flex;
  546. flex-direction: column;
  547. width: 100%;
  548. height: 100%;
  549. overflow: hidden;
  550. background: linear-gradient(180deg, #D1EBFF 0%, #FFFFFF 100%);
  551. .nav-cards {
  552. position: relative;
  553. z-index: 2;
  554. display: flex;
  555. justify-content: space-around;
  556. padding: 10px 10px;
  557. margin-bottom: 10px;
  558. .nav-card-wrap {
  559. position: relative;
  560. &--report {
  561. z-index: 5;
  562. }
  563. }
  564. .nav-card {
  565. position: relative;
  566. display: flex;
  567. flex-direction: column;
  568. align-items: center;
  569. justify-content: center;
  570. border-radius: 8px;
  571. background: #fff;
  572. width: 90px;
  573. height: 64px;
  574. box-shadow: 0px 4px 4px 0px #0000000D;
  575. .nav-card__icon {
  576. width: 26px;
  577. height: 26px;
  578. margin-bottom: 4px;
  579. }
  580. &.is-active {
  581. background: linear-gradient(180deg, #5BB8FF 0%, #2199F8 100%);
  582. box-shadow: 0 4px 10px rgba(33, 153, 248, 0.28);
  583. .nav-card__icon {
  584. filter: brightness(0) invert(1);
  585. }
  586. .nav-card__label {
  587. color: #fff;
  588. }
  589. &::after {
  590. content: "";
  591. position: absolute;
  592. left: 50%;
  593. bottom: -5px;
  594. transform: translateX(-50%);
  595. border-left: 6px solid transparent;
  596. border-right: 6px solid transparent;
  597. border-top: 6px solid #2199F8;
  598. z-index: 3;
  599. }
  600. }
  601. }
  602. .report-guide-bubble {
  603. position: absolute;
  604. top: calc(100% + 10px);
  605. right: 22px;
  606. width: 216px;
  607. padding: 6px 10px;
  608. border-radius: 6px;
  609. background: rgba(0, 0, 0, 0.8);
  610. backdrop-filter: blur(4px);
  611. box-sizing: border-box;
  612. z-index: 20;
  613. pointer-events: auto;
  614. &__arrow {
  615. position: absolute;
  616. top: -5px;
  617. right: 16px;
  618. width: 0;
  619. height: 0;
  620. border-left: 6px solid transparent;
  621. border-right: 6px solid transparent;
  622. border-bottom: 6px solid rgba(0, 0, 0, 0.8);
  623. }
  624. &__content {
  625. font-size: 14px;
  626. line-height: 22px;
  627. color: #fff;
  628. word-break: break-all;
  629. text-align: justify;
  630. }
  631. &__link {
  632. padding: 0 2px;
  633. color: #2199f8;
  634. cursor: pointer;
  635. }
  636. }
  637. }
  638. .content-panel {
  639. flex: 1;
  640. overflow-y: auto;
  641. background: linear-gradient(360deg, rgba(229, 243, 255, 0.55) 0%, rgba(255, 255, 255, 0.55) 100%);
  642. border-radius: 20px 20px 0 0;
  643. border: 1px solid #FFFFFF;
  644. padding: 18px 10px;
  645. &::-webkit-scrollbar {
  646. display: none;
  647. }
  648. .map-card {
  649. position: relative;
  650. height: 210px;
  651. }
  652. .map-container {
  653. width: 100%;
  654. height: 100%;
  655. clip-path: inset(0px round 8px);
  656. }
  657. .map-mask {
  658. position: absolute;
  659. top: 0;
  660. left: 0;
  661. z-index: 5;
  662. width: 100%;
  663. height: 100%;
  664. background: rgba(0, 0, 0, 0.5);
  665. border-radius: 8px;
  666. display: flex;
  667. align-items: center;
  668. justify-content: center;
  669. pointer-events: auto;
  670. cursor: pointer;
  671. .mask-content {
  672. background: rgba(255, 255, 255, 0.2);
  673. backdrop-filter: blur(4px);
  674. padding: 8px 10px;
  675. border-radius: 4px;
  676. width: fit-content;
  677. display: flex;
  678. flex-direction: column;
  679. align-items: center;
  680. justify-content: center;
  681. gap: 12px;
  682. color: #fff;
  683. pointer-events: none;
  684. &__title {
  685. font-size: 18px;
  686. font-family: "PangMenZhengDao";
  687. }
  688. &__btn {
  689. padding: 0 24px;
  690. height: 32px;
  691. line-height: 32px;
  692. border-radius: 24px;
  693. background: #2199F8;
  694. }
  695. }
  696. }
  697. .zone-marker {
  698. display: flex;
  699. flex-direction: column;
  700. align-items: center;
  701. pointer-events: none;
  702. &__label {
  703. padding: 3px 14px;
  704. border-radius: 25px;
  705. background: #fff;
  706. color: #0A0A0A;
  707. font-size: 12px;
  708. }
  709. &__thumb {
  710. position: relative;
  711. width: 42px;
  712. height: 42px;
  713. margin-top: 8px;
  714. img {
  715. width: 100%;
  716. height: 100%;
  717. object-fit: cover;
  718. border: 2px solid #fff;
  719. border-radius: 6px;
  720. box-sizing: border-box;
  721. }
  722. }
  723. &__badge {
  724. position: absolute;
  725. top: -6px;
  726. right: -6px;
  727. min-width: 18px;
  728. height: 18px;
  729. line-height: 16px;
  730. border-radius: 50%;
  731. background: #fff;
  732. color: #0A0A0A;
  733. font-size: 12px;
  734. text-align: center;
  735. }
  736. &__arrow {
  737. width: 0;
  738. height: 0;
  739. border-left: 5px solid transparent;
  740. border-right: 5px solid transparent;
  741. border-top: 6px solid #fff;
  742. }
  743. }
  744. .add-zone-btn {
  745. position: absolute;
  746. right: 5px;
  747. bottom: 10px;
  748. z-index: 5;
  749. display: flex;
  750. align-items: center;
  751. gap: 2px;
  752. padding: 5px 10px;
  753. border-radius: 4px;
  754. background: #fff;
  755. color: #2199F8;
  756. font-weight: 500;
  757. }
  758. .album-grid {
  759. display: grid;
  760. grid-template-columns: 1fr 1fr;
  761. gap: 16px;
  762. margin-top: 16px;
  763. box-sizing: border-box;
  764. .album-add {
  765. align-self: start;
  766. display: flex;
  767. flex-direction: column;
  768. align-items: center;
  769. justify-content: center;
  770. width: 100%;
  771. aspect-ratio: 1;
  772. border-radius: 5px;
  773. background: #FFFFFF;
  774. color: #969494;
  775. font-size: 16px;
  776. &__icon {
  777. margin-bottom: 6px;
  778. font-size: 20px;
  779. }
  780. }
  781. .album-card {
  782. width: 100%;
  783. &.is-empty {
  784. .album-card__cover {
  785. display: flex;
  786. align-items: center;
  787. justify-content: center;
  788. overflow: hidden;
  789. border-radius: 8px;
  790. background: linear-gradient(180deg, #E0F1FF 0%, #C2E4FF 100%);
  791. }
  792. }
  793. &__cover {
  794. position: relative;
  795. width: 100%;
  796. aspect-ratio: 1;
  797. img:not(.album-card__empty-icon) {
  798. width: 100%;
  799. height: 100%;
  800. object-fit: cover;
  801. border-radius: 6px;
  802. }
  803. }
  804. &__empty-icon {
  805. width: 50px;
  806. height: 50px;
  807. }
  808. &__count {
  809. position: absolute;
  810. top: 1px;
  811. right: 0;
  812. padding: 1px 5px;
  813. border-radius: 0 5px 0 5px;
  814. background: rgba(0, 0, 0, 0.4);
  815. backdrop-filter: blur(4px);
  816. color: #fff;
  817. }
  818. &__add {
  819. position: absolute;
  820. right: 8px;
  821. bottom: 8px;
  822. z-index: 1;
  823. display: flex;
  824. align-items: center;
  825. justify-content: center;
  826. width: 44px;
  827. height: 44px;
  828. border-radius: 50%;
  829. background: #2199F8;
  830. color: #fff;
  831. font-size: 20px;
  832. box-shadow: 0 2px 8px rgba(33, 153, 248, 0.35);
  833. }
  834. &__name {
  835. margin-top: 10px;
  836. color: #1F1F1F;
  837. }
  838. }
  839. }
  840. .report-card {
  841. margin-bottom: 10px;
  842. border-radius: 8px;
  843. background: #fff;
  844. &:last-child {
  845. margin-bottom: 0;
  846. }
  847. &__cover {
  848. position: relative;
  849. width: 100%;
  850. height: 158px;
  851. img {
  852. display: block;
  853. width: 100%;
  854. height: 100%;
  855. object-fit: cover;
  856. border-radius: 5px;
  857. }
  858. }
  859. &__date {
  860. position: absolute;
  861. top: 0;
  862. padding: 5px 12px;
  863. border-radius: 4px;
  864. background: rgba(0, 0, 0, 0.45);
  865. backdrop-filter: blur(4px);
  866. color: #fff;
  867. font-size: 16px;
  868. border-radius: 5px 0 5px 0;
  869. }
  870. &__status {
  871. position: absolute;
  872. top: 10px;
  873. right: 0;
  874. display: flex;
  875. align-items: center;
  876. gap: 4px;
  877. padding: 2px 13px;
  878. border-radius: 20px 0 0 20px;
  879. background: #FF5454;
  880. color: #fff;
  881. font-size: 12px;
  882. }
  883. &__dot {
  884. width: 6px;
  885. height: 6px;
  886. border-radius: 50%;
  887. background: #fff;
  888. }
  889. &__body {
  890. padding: 10px;
  891. margin-top: 4px;
  892. }
  893. &__title {
  894. font-size: 16px;
  895. font-weight: 500;
  896. }
  897. &__desc {
  898. margin-top: 4px;
  899. color: #9E9B9B;
  900. }
  901. }
  902. .tracking-item {
  903. width: 100%;
  904. margin-bottom: 16px;
  905. box-shadow: 0px 4px 4px 0px rgba(33, 153, 248, 0.1);
  906. &:last-of-type {
  907. margin-bottom: 0;
  908. }
  909. &__header {
  910. position: relative;
  911. padding: 10px 10px 20px;
  912. border-radius: 10px 10px 0 0;
  913. background: linear-gradient(180deg, #86CAFF 0%, #2199F8 48.5%);
  914. }
  915. &__header-row {
  916. display: flex;
  917. align-items: center;
  918. justify-content: space-between;
  919. }
  920. &__header-left {
  921. display: flex;
  922. align-items: center;
  923. gap: 5px;
  924. }
  925. &__level {
  926. display: flex;
  927. align-items: center;
  928. gap: 4px;
  929. padding: 1px 4px;
  930. border-radius: 2px;
  931. background: #fff;
  932. color: #FF6A6A;
  933. font-size: 12px;
  934. }
  935. &__title {
  936. color: #fff;
  937. font-size: 16px;
  938. font-weight: 500;
  939. }
  940. &__zone {
  941. font-size: 12px;
  942. }
  943. &__share {
  944. display: flex;
  945. align-items: center;
  946. gap: 4px;
  947. padding: 2px 10px;
  948. border-radius: 25px;
  949. background: #fff;
  950. color: #2199F8;
  951. font-size: 12px;
  952. }
  953. &__body {
  954. position: relative;
  955. padding: 10px;
  956. margin-top: -8px;
  957. border-radius: 10px;
  958. background: #fff;
  959. &::before {
  960. content: "";
  961. position: absolute;
  962. top: -6px;
  963. left: 21%;
  964. width: 0;
  965. height: 0;
  966. border-left: 7px solid transparent;
  967. border-right: 7px solid transparent;
  968. border-bottom: 7px solid #fff;
  969. transform: translateX(-50%);
  970. }
  971. }
  972. &__action {
  973. display: flex;
  974. align-items: center;
  975. justify-content: space-between;
  976. border: 1px solid rgba(33, 153, 248, 0.2);
  977. border-radius: 6px;
  978. padding: 8px;
  979. }
  980. &__icon {
  981. display: flex;
  982. align-items: center;
  983. gap: 5px;
  984. img {
  985. width: 18px;
  986. height: 16px;
  987. }
  988. }
  989. &__reason {
  990. color: #2199f8;
  991. font-weight: 500;
  992. }
  993. &__issue {
  994. margin-top: 2px;
  995. color: #2199f8;
  996. font-size: 12px;
  997. }
  998. &__action-main {
  999. flex: 1;
  1000. padding-right: 4px;
  1001. }
  1002. &__btn {
  1003. flex: none;
  1004. display: flex;
  1005. align-items: center;
  1006. gap: 2px;
  1007. padding: 7px 10px;
  1008. border-radius: 5px;
  1009. background: #2199f8;
  1010. color: #fff;
  1011. font-size: 12px;
  1012. }
  1013. }
  1014. .crop-archive {
  1015. padding: 10px;
  1016. border-radius: 10px;
  1017. background: #fff;
  1018. &__title {
  1019. position: relative;
  1020. display: inline-block;
  1021. z-index: 1;
  1022. font-size: 16px;
  1023. font-weight: 500;
  1024. margin-bottom: 5px;
  1025. &::after {
  1026. content: "";
  1027. position: absolute;
  1028. left: 0;
  1029. bottom: 2px;
  1030. z-index: -1;
  1031. width: 90px;
  1032. height: 6px;
  1033. border-radius: 25px;
  1034. background: linear-gradient(90deg, #2199f8 0%, rgba(255, 255, 255, 0.37) 100%);
  1035. opacity: 0.4;
  1036. }
  1037. }
  1038. }
  1039. }
  1040. }
  1041. </style>