albumMap.vue 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. <template>
  2. <div class="album-map-page">
  3. <custom-header :name="t('agriFile.agriAlbum')" />
  4. <div class="album-map-content">
  5. <div class="map-container" ref="mapContainer"></div>
  6. <div class="search-bar">
  7. <location-search
  8. class="search-bar__search"
  9. :user-location="userLocation"
  10. @change="handleLocationChange"
  11. />
  12. </div>
  13. <div class="locate-btn" @click="handleLocate">
  14. <img class="locate-btn__icon" src="@/assets/img/map/map-icon.png" alt="" />
  15. </div>
  16. </div>
  17. </div>
  18. </template>
  19. <script setup>
  20. import { nextTick, onActivated, ref } from "vue";
  21. import { useStore } from "vuex";
  22. import customHeader from "@/components/customHeader.vue";
  23. import locationSearch from "@/components/pageComponents/locationSearch.vue";
  24. import FileMap from "../fileMap";
  25. import * as util from "@/common/ol_common.js";
  26. import { useI18n } from "@/i18n";
  27. const { t } = useI18n();
  28. const store = useStore();
  29. const mapContainer = ref(null);
  30. const fileMap = new FileMap();
  31. const defaultCover = require("@/assets/img/home/banner.png");
  32. const DEFAULT_MAP_LOCATION = "POINT(113.6142086995688 23.585836479509055)";
  33. const ENTRY_FARM_DATA_KEY = "ENTRY_FARM_DATA";
  34. /** 与农情档案相册页共用:crop_question 配图 */
  35. const CROP_QUESTION_PIC_KEY = "CROP_QUESTION_PIC_URL";
  36. const userLocation = ref(
  37. store.state.home.miniUserLocation ||
  38. localStorage.getItem("MINI_USER_LOCATION") ||
  39. "113.61702297075017,23.584863449735067"
  40. );
  41. /** 不展示分区范围与分区名称,仅保留照片点 */
  42. const MOCK_ZONES = [];
  43. const MOCK_PHOTOS = [
  44. { id: 1, dlng: 0, dlat: 0 },
  45. ];
  46. function readAlbumCover() {
  47. return localStorage.getItem(CROP_QUESTION_PIC_KEY) || defaultCover;
  48. }
  49. function squarePolygonWkt(lng, lat, delta = 0.0014) {
  50. const ring = [
  51. [lng - delta, lat + delta * 0.7],
  52. [lng + delta * 0.35, lat + delta],
  53. [lng + delta, lat - delta * 0.2],
  54. [lng + delta * 0.15, lat - delta],
  55. [lng - delta * 0.85, lat - delta * 0.45],
  56. [lng - delta, lat + delta * 0.7],
  57. ];
  58. return `POLYGON((${ring.map((point) => point.join(" ")).join(", ")}))`;
  59. }
  60. function getFarmData() {
  61. try {
  62. return JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
  63. } catch {
  64. return {};
  65. }
  66. }
  67. function isPointWkt(value) {
  68. return typeof value === "string" && /^POINT\s*\(/i.test(value.trim());
  69. }
  70. function wkbHexToPointWkt(hex) {
  71. const text = String(hex || "").trim();
  72. if (!/^[0-9a-fA-F]+$/.test(text) || text.length < 42) return null;
  73. const bytes = new Uint8Array(text.length / 2);
  74. for (let i = 0; i < bytes.length; i++) {
  75. bytes[i] = parseInt(text.slice(i * 2, i * 2 + 2), 16);
  76. }
  77. const view = new DataView(bytes.buffer);
  78. const little = view.getUint8(0) === 1;
  79. let type = view.getUint32(1, little);
  80. const hasSrid = (type & 0x20000000) !== 0;
  81. type &= 0xff;
  82. if (type !== 1) return null;
  83. let offset = 5;
  84. if (hasSrid) offset += 4;
  85. if (offset + 16 > bytes.length) return null;
  86. const lng = view.getFloat64(offset, little);
  87. const lat = view.getFloat64(offset + 8, little);
  88. if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null;
  89. return `POINT(${lng} ${lat})`;
  90. }
  91. function toPointWkt(value) {
  92. if (!value || typeof value !== "string") return null;
  93. if (isPointWkt(value)) return value.trim();
  94. return wkbHexToPointWkt(value);
  95. }
  96. function getEntryFarmPoint() {
  97. try {
  98. const data = JSON.parse(localStorage.getItem(ENTRY_FARM_DATA_KEY) || "null");
  99. if (Array.isArray(data?.coordinate) && data.coordinate.length === 2) {
  100. return `POINT(${Number(data.coordinate[0])} ${Number(data.coordinate[1])})`;
  101. }
  102. const point = toPointWkt(data?.point);
  103. if (point) return point;
  104. } catch {
  105. // ignore
  106. }
  107. return (
  108. toPointWkt(localStorage.getItem("GROWTH_REPORT_MAP_POINT")) ||
  109. toPointWkt(localStorage.getItem("selectedFarmPoint"))
  110. );
  111. }
  112. function getFarmMapLocation() {
  113. const entryPoint = getEntryFarmPoint();
  114. if (entryPoint) return entryPoint;
  115. const farmData = getFarmData();
  116. const candidates = [farmData.wkt, farmData.geom_wkt, farmData.farm_location];
  117. for (const item of candidates) {
  118. const pointWkt = toPointWkt(item);
  119. if (pointWkt) return pointWkt;
  120. }
  121. return DEFAULT_MAP_LOCATION;
  122. }
  123. function getDefaultMapLocation() {
  124. return (
  125. getEntryFarmPoint() ||
  126. toPointWkt(localStorage.getItem("MINI_USER_LOCATION_POINT")) ||
  127. toPointWkt(store.state.home.miniUserLocationPoint) ||
  128. getFarmMapLocation()
  129. );
  130. }
  131. function getDefaultMapCoordinate() {
  132. return util.wktCastGeom(getDefaultMapLocation()).getFirstCoordinate();
  133. }
  134. const loadAlbumLayers = () => {
  135. const [lng, lat] = getDefaultMapCoordinate();
  136. const labels = MOCK_ZONES.map((item) => ({
  137. name: t(item.nameKey),
  138. longitude: lng + item.dlng,
  139. latitude: lat + item.dlat,
  140. }));
  141. // 与外面农情相册一致:crop_question 图 + 数量 1
  142. const cover = readAlbumCover();
  143. const photos = MOCK_PHOTOS.map((item) => ({
  144. count: 1,
  145. cover,
  146. longitude: lng + item.dlng,
  147. latitude: lat + item.dlat,
  148. }));
  149. const zoneRecords = MOCK_ZONES.map((item) => ({
  150. zone_name: "",
  151. polygon: squarePolygonWkt(lng + item.dlng, lat + item.dlat, item.delta),
  152. }));
  153. fileMap.setRecordPolygons(zoneRecords, "album");
  154. fileMap.setAlbumMarkers({ labels, photos });
  155. };
  156. const initAlbumMap = async () => {
  157. await nextTick();
  158. if (!mapContainer.value) return;
  159. fileMap.initMap(getDefaultMapLocation(), mapContainer.value);
  160. loadAlbumLayers();
  161. fileMap.kmap?.map?.updateSize?.();
  162. };
  163. const handleLocationChange = (payload) => {
  164. if (!payload?.coordinateArray || !fileMap.kmap) return;
  165. fileMap.kmap.getView().animate({
  166. center: payload.coordinateArray,
  167. zoom: 16,
  168. duration: 0,
  169. });
  170. };
  171. const handleLocate = () => {
  172. if (!fileMap.kmap) return;
  173. fileMap.kmap.getView().animate({
  174. center: getDefaultMapCoordinate(),
  175. zoom: 16,
  176. duration: 0,
  177. });
  178. };
  179. onActivated(() => {
  180. initAlbumMap();
  181. });
  182. </script>
  183. <style lang="scss" scoped>
  184. .album-map-page {
  185. display: flex;
  186. flex-direction: column;
  187. width: 100%;
  188. height: 100vh;
  189. overflow: hidden;
  190. background: #fff;
  191. }
  192. .album-map-content {
  193. position: relative;
  194. flex: 1;
  195. min-height: 0;
  196. overflow: hidden;
  197. .map-container {
  198. width: 100%;
  199. height: 100%;
  200. }
  201. }
  202. .search-bar {
  203. position: absolute;
  204. top: 12px;
  205. left: 12px;
  206. right: 12px;
  207. z-index: 2;
  208. display: flex;
  209. align-items: center;
  210. height: 40px;
  211. padding: 0 4px 0 12px;
  212. border-radius: 20px;
  213. background: rgba(0, 0, 0, 0.45);
  214. box-sizing: border-box;
  215. &__search {
  216. flex: 1;
  217. min-width: 0;
  218. :deep(.el-select__wrapper) {
  219. background: transparent;
  220. box-shadow: none;
  221. border: none;
  222. min-height: 40px;
  223. padding-left: 0;
  224. }
  225. :deep(.el-select__placeholder),
  226. :deep(.el-select__input) {
  227. color: rgba(255, 255, 255, 0.7);
  228. }
  229. :deep(.el-icon) {
  230. color: rgba(255, 255, 255, 0.85);
  231. }
  232. }
  233. }
  234. .locate-btn {
  235. position: absolute;
  236. right: 12px;
  237. bottom: 96px;
  238. z-index: 2;
  239. display: flex;
  240. align-items: center;
  241. justify-content: center;
  242. width: 36px;
  243. height: 36px;
  244. border-radius: 8px;
  245. background: #fff;
  246. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  247. &__icon {
  248. width: 16px;
  249. height: 18px;
  250. }
  251. }
  252. </style>