index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. <template>
  2. <div class="agri-file" :style="{ height: `calc(100vh - ${tabBarHeight}px)` }">
  3. <!-- 天气遮罩 -->
  4. <div class="weather-mask" v-show="isExpanded" @click="handleMaskClick"></div>
  5. <!-- 天气 -->
  6. <weather-info ref="weatherInfoRef" :hasWeather="false" from="monitor" class="weather-info"
  7. @weatherExpanded="weatherExpanded" @changeGarden="changeGarden" @changeGardenTab="changeGardenTab"
  8. :isGarden="true" :gardenId="defaultGardenId">
  9. <template #types-content>
  10. <div class="type-tabs">
  11. <div class="type-item" @click="changeType(farmVarietyName)"
  12. :class="{ 'type-item-active': activeType === farmVarietyName }">{{ farmVarietyName }}</div>
  13. <!-- <div class="type-item" @click="changeType('水稻')" :class="{ 'type-item-active': activeType === '水稻' }">{{ $t('水稻') }}</div> -->
  14. <!-- <div class="type-item" @click="changeType('柑橘')" :class="{ 'type-item-active': activeType === '柑橘' }">{{ $t('柑橘') }}</div> -->
  15. <!-- <div class="type-item" @click="changeType('小麦')" :class="{ 'type-item-active': activeType === '小麦' }">{{ $t('小麦') }}</div> -->
  16. </div>
  17. </template>
  18. </weather-info>
  19. <!-- 农场列表 -->
  20. <div v-show="activeGardenTab === 'list'">
  21. <garden-list ref="gardenListRef" :garden-id="selectedGardenId" @loaded="handleGardenLoaded"
  22. @selectGarden="handleGardenSelected" />
  23. </div>
  24. <div class="file-content" v-show="activeGardenTab === 'current'">
  25. <!-- 地图图例:分类标识 + 长势等级 -->
  26. <div class="map-legend">
  27. <div
  28. v-for="item in mapLegendItems"
  29. :key="item.key"
  30. class="map-legend__item"
  31. >
  32. <span class="map-legend__pill" :class="item.pillClass"></span>
  33. <span class="map-legend__text">{{ item.label }}</span>
  34. </div>
  35. </div>
  36. <!-- <div class="map-legend-box">
  37. <div class="map-legend-box__labels">
  38. <span
  39. v-for="(label, index) in growthScaleLabels"
  40. :key="index"
  41. >{{ label }}</span>
  42. </div>
  43. <div class="map-legend-box__bar"></div>
  44. </div> -->
  45. <div class="map-tool-bar">
  46. <div
  47. v-for="(item, index) in mapToolItems"
  48. :key="item.key"
  49. class="map-tool-bar__item"
  50. :class="{ 'map-tool-bar__item--active': activeMapTool === index }"
  51. @click="changeMapTool(index)"
  52. >
  53. <img
  54. class="map-tool-bar__icon"
  55. :src="require(`@/assets/img/map/tool-${index + 1}.png`)"
  56. :alt="item.label"
  57. />
  58. <span class="map-tool-bar__label">{{ item.label }}</span>
  59. </div>
  60. </div>
  61. <div class="map-container" ref="mapContainer"></div>
  62. <file-float
  63. v-model:active-tab="activeRecordTab"
  64. v-model:active-sub-tab="activeRecordSubTab"
  65. :farm-record-data="farmRecordData"
  66. :loading="farmRecordLoading"
  67. :crop-variety="farmVarietyName"
  68. />
  69. </div>
  70. </div>
  71. </template>
  72. <script setup>
  73. import { computed, nextTick, onActivated, ref, watch } from "vue";
  74. import { useRoute } from "vue-router";
  75. import { useStore } from "vuex";
  76. import weatherInfo from "@/components/weatherInfo.vue";
  77. import gardenList from "@/components/gardenList.vue";
  78. import fileFloat from "./components/fileFloat.vue";
  79. import FileMap, { RECORD_TAB_KEYS, recordsToCenterPoint, hasFarmBaseImage } from "./fileMap.js";
  80. import { useI18n } from "@/i18n";
  81. const { t, locale } = useI18n();
  82. const store = useStore();
  83. const route = useRoute();
  84. const tabBarHeight = computed(() => store.state.home.tabBarHeight);
  85. const isExpanded = ref(false);
  86. const weatherInfoRef = ref(null);
  87. const defaultGardenId = ref(null);
  88. const selectedGardenId = ref(null);
  89. const gardenListRef = ref(null);
  90. const activeGardenTab = ref("current");
  91. const activeType = ref(null);
  92. const MAP_LEGEND_CONFIG = [
  93. { key: "zone", labelKey: "agriFile.legendZone", type: "zone" },
  94. { key: "growth", labelKey: "agriFile.legendGrowth", type: "growth" },
  95. { key: "pest", labelKey: "agriFile.legendPest", type: "pest" },
  96. ];
  97. const GROWTH_SCALE_LABEL_KEYS = [
  98. "agriFile.scaleNormal",
  99. "agriFile.scaleGood",
  100. "agriFile.scaleExcellent",
  101. ];
  102. const mapLegendItems = computed(() =>
  103. MAP_LEGEND_CONFIG.map(({ key, labelKey, type }) => ({
  104. key,
  105. label: t(labelKey),
  106. pillClass: `map-legend__pill--${type}`,
  107. }))
  108. );
  109. const growthScaleLabels = computed(() => GROWTH_SCALE_LABEL_KEYS.map((key) => t(key)));
  110. const MAP_TOOL_ITEMS = [
  111. { key: "all", label: "底图" },
  112. { key: "habitat", label: "植被" },
  113. { key: "light", label: "水体" },
  114. { key: "water", label: "降水" },
  115. // { key: "fengshui", label: "风水" },
  116. // { key: "soil", label: "土壤" },
  117. ];
  118. const mapToolItems = MAP_TOOL_ITEMS;
  119. const activeMapTool = ref(0);
  120. const changeMapTool = (index) => {
  121. activeMapTool.value = index;
  122. fileMap.toggleBaseImageLayers(index === 1 && hasFarmBaseImage(selectedGardenId.value));
  123. };
  124. const fileMap = new FileMap();
  125. const farmRecordData = ref({});
  126. const farmRecordLoading = ref(false);
  127. const activeRecordTab = ref(0);
  128. const activeRecordSubTab = ref(0);
  129. const getActiveTabKey = () =>
  130. RECORD_TAB_KEYS[activeRecordSubTab.value] || RECORD_TAB_KEYS[0];
  131. const getActiveRecords = () => farmRecordData.value[getActiveTabKey()] || [];
  132. const syncFarmRecordMap = () => {
  133. if (activeGardenTab.value !== "current") return;
  134. nextTick(() => {
  135. fileMap.setRecordPolygons(getActiveRecords(), getActiveTabKey());
  136. });
  137. };
  138. const initAgriFileMap = async () => {
  139. await nextTick();
  140. if (!mapContainer.value) return;
  141. fileMap.initMap(recordsToCenterPoint(getActiveRecords()), mapContainer.value);
  142. fileMap.showBaseImageByFarmId(selectedGardenId.value);
  143. fileMap.toggleBaseImageLayers(activeMapTool.value === 1 && hasFarmBaseImage(selectedGardenId.value));
  144. };
  145. const getFarmRecord = async () => {
  146. farmRecordLoading.value = true;
  147. try {
  148. const farmData = JSON.parse(localStorage.getItem("selectedFarmData"));
  149. const res = await VE_API.monitor.getFarmRecord({
  150. farm_id: selectedGardenId.value,
  151. variety_code: farmData.farm_variety,
  152. });
  153. if (res.code === 200) {
  154. farmRecordData.value = {
  155. phenology: res.data?.phenology ?? [],
  156. abnormal: res.data?.abnormal ?? [],
  157. farming: res.data?.farming ?? [],
  158. };
  159. syncFarmRecordMap();
  160. }
  161. } finally {
  162. farmRecordLoading.value = false;
  163. }
  164. };
  165. watch(activeRecordSubTab, syncFarmRecordMap);
  166. watch(activeGardenTab, syncFarmRecordMap);
  167. const weatherExpanded = (isExpandedValue) => {
  168. isExpanded.value = isExpandedValue;
  169. };
  170. const handleMaskClick = () => {
  171. if (weatherInfoRef.value?.toggleExpand) {
  172. weatherInfoRef.value.toggleExpand();
  173. }
  174. };
  175. const changeGardenTab = (tab) => {
  176. activeGardenTab.value = tab;
  177. };
  178. const changeType = (type) => {
  179. activeType.value = type;
  180. };
  181. const handleGardenLoaded = ({ hasFarm }) => {
  182. weatherInfoRef.value?.setGardenLoaded?.(hasFarm);
  183. };
  184. const handleGardenSelected = (garden) => {
  185. selectedGardenId.value = garden?.id ?? null;
  186. fileMap.showBaseImageByFarmId(selectedGardenId.value);
  187. weatherInfoRef.value?.setSelectedGarden?.(garden);
  188. };
  189. const farmVarietyName = ref(null);
  190. const changeGarden = (data) => {
  191. if (!data?.id) return;
  192. store.commit("home/SET_GARDEN_ID", data.id);
  193. selectedGardenId.value = data.id;
  194. fileMap.showBaseImageByFarmId(data.id);
  195. getFarmRecord();
  196. activeType.value = data.variety_name;
  197. farmVarietyName.value = data.variety_name;
  198. };
  199. const mapContainer = ref(null);
  200. onActivated(async () => {
  201. if (route.query?.farmId) {
  202. defaultGardenId.value = route.query.farmId;
  203. await getFarmRecord();
  204. }
  205. const savedFarmId = localStorage.getItem("selectedFarmId");
  206. selectedGardenId.value = savedFarmId ? Number(savedFarmId) : null;
  207. gardenListRef.value?.refreshFarmList?.();
  208. await initAgriFileMap();
  209. });
  210. </script>
  211. <style lang="scss" scoped>
  212. .agri-file {
  213. width: 100%;
  214. height: 100%;
  215. background: #F5F7FB;
  216. box-sizing: border-box;
  217. .weather-mask {
  218. position: fixed;
  219. top: 0;
  220. left: 0;
  221. width: 100%;
  222. height: 100%;
  223. background-color: rgba(0, 0, 0, 0.52);
  224. z-index: 11;
  225. }
  226. .weather-info {
  227. width: calc(100% - 20px);
  228. position: absolute;
  229. z-index: 12;
  230. left: 10px;
  231. top: 12px;
  232. }
  233. .file-content {
  234. position: relative;
  235. height: 100%;
  236. box-sizing: border-box;
  237. $map-legend-panel-bg: rgba(0, 0, 0, 0.46);
  238. $map-legend-text-color: #ffffff;
  239. $map-legend-text-size: 12px;
  240. $map-legend-scale-gradient: linear-gradient(
  241. 90deg,
  242. #007aff 0%,
  243. #00d4aa 25%,
  244. #ffe600 50%,
  245. #ff9500 75%,
  246. #ff3b30 100%
  247. );
  248. @mixin map-legend-panel-base {
  249. position: absolute;
  250. z-index: 15;
  251. background: $map-legend-panel-bg;
  252. backdrop-filter: blur(4px);
  253. box-sizing: border-box;
  254. }
  255. .map-legend {
  256. @include map-legend-panel-base;
  257. top: 110px;
  258. right: 12px;
  259. display: flex;
  260. align-items: center;
  261. justify-content: space-around;
  262. gap: 10px;
  263. padding: 8px 10px;
  264. border-radius: 4px;
  265. &__item {
  266. display: flex;
  267. align-items: center;
  268. gap: 5px;
  269. }
  270. &__pill {
  271. width: 16px;
  272. height: 5px;
  273. border-radius: 10px;
  274. &--zone {
  275. background: #1c9e80;
  276. }
  277. &--growth {
  278. background: #ff953d;
  279. }
  280. &--pest {
  281. background: #e03131;
  282. }
  283. }
  284. &__text {
  285. font-size: $map-legend-text-size;
  286. color: $map-legend-text-color;
  287. }
  288. }
  289. .map-legend-box {
  290. @include map-legend-panel-base;
  291. top: 150px;
  292. right: 10px;
  293. width: 222px;
  294. padding: 5px 10px;
  295. border-radius: 5px;
  296. &__labels {
  297. display: flex;
  298. justify-content: space-between;
  299. align-items: center;
  300. margin-bottom: 8px;
  301. font-size: $map-legend-text-size;
  302. color: $map-legend-text-color;
  303. }
  304. &__bar {
  305. height: 8px;
  306. border-radius: 4px;
  307. background: $map-legend-scale-gradient;
  308. }
  309. }
  310. .map-tool-bar {
  311. position: absolute;
  312. left: 12px;
  313. top: 50%;
  314. transform: translateY(-78%);
  315. z-index: 15;
  316. display: flex;
  317. flex-direction: column;
  318. align-items: center;
  319. padding: 10px 6px;
  320. background: #ffffff;
  321. border-radius: 7px;
  322. box-shadow: 0px 2.3px 2.3px 0px #0000001A;
  323. box-sizing: border-box;
  324. &__item {
  325. display: flex;
  326. flex-direction: column;
  327. align-items: center;
  328. justify-content: center;
  329. padding: 10px 5px;
  330. color: #C1C1C1;
  331. .map-tool-bar__icon {
  332. width: 18px;
  333. height: 16px;
  334. margin-bottom: 3px;
  335. filter: grayscale(1);
  336. }
  337. &--active {
  338. color: #2199f8;
  339. .map-tool-bar__icon {
  340. filter: grayscale(0);
  341. }
  342. }
  343. }
  344. &__label {
  345. font-size: 12px;
  346. }
  347. }
  348. .map-container {
  349. width: 100%;
  350. height: 100%;
  351. }
  352. }
  353. .type-tabs {
  354. width: 100%;
  355. background: #FFF;
  356. display: flex;
  357. align-items: center;
  358. flex-wrap: wrap;
  359. gap: 8px;
  360. padding: 8px;
  361. .type-item {
  362. height: 28px;
  363. line-height: 28px;
  364. text-align: center;
  365. padding: 0 6px;
  366. min-width: 78px;
  367. color: #9A9A9A;
  368. background: #EFEFEF;
  369. box-sizing: border-box;
  370. border-radius: 2px;
  371. &.type-item-active {
  372. background: #2199F8;
  373. color: #fff;
  374. }
  375. }
  376. }
  377. }
  378. </style>