| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- <template>
- <div class="album-map-page">
- <custom-header :name="t('agriFile.agriAlbum')" />
- <div class="album-map-content">
- <div class="map-container" ref="mapContainer"></div>
- <div class="search-bar">
- <location-search
- class="search-bar__search"
- :user-location="userLocation"
- @change="handleLocationChange"
- />
- </div>
- <div class="locate-btn" @click="handleLocate">
- <img class="locate-btn__icon" src="@/assets/img/map/map-icon.png" alt="" />
- </div>
- <div
- v-for="item in zoneList"
- :key="item.id"
- :ref="(el) => setMarkerEl(item.id, el)"
- class="zone-marker"
- >
- <div class="zone-marker__label">{{ item.name }}</div>
- <div class="zone-marker__thumb">
- <img :src="item.cover" alt="" />
- <span v-if="item.count" class="zone-marker__badge">{{ item.count }}</span>
- </div>
- <div class="zone-marker__arrow"></div>
- </div>
- </div>
- </div>
- </template>
- <script setup>
- import { nextTick, onActivated, onBeforeUnmount, onMounted, ref } from "vue";
- import { useStore } from "vuex";
- import Overlay from "ol/Overlay";
- import customHeader from "@/components/customHeader.vue";
- import locationSearch from "@/components/pageComponents/locationSearch.vue";
- import FileMap from "../fileMap";
- import * as util from "@/common/ol_common.js";
- import { base_img_url2 } from "@/api/config";
- import { useI18n } from "@/i18n";
- const { t } = useI18n();
- const store = useStore();
- // ---------- 常量 ----------
- const defaultCover = require("@/assets/img/agricultural/photo.png");
- const DEFAULT_CENTER = [113.6142086995688, 23.585836479509055];
- // ---------- 页面状态 ----------
- const mapContainer = ref(null);
- const fileMap = new FileMap();
- fileMap.fitPadding = [68, 50, 50, 50];
- const markerEls = {};
- const mapOverlays = [];
- const zoneList = ref([]);
- const userLocation = ref(
- store.state.home.miniUserLocation ||
- localStorage.getItem("MINI_USER_LOCATION") ||
- "113.61702297075017,23.584863449735067"
- );
- // ---------- 工具函数 ----------
- function getSelectedFarmId() {
- const stored = localStorage.getItem("selectedFarmId");
- if (stored != null && stored !== "") {
- return Number(stored);
- }
- try {
- const farm = JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
- const id = farm.farm_id ?? farm.id;
- return id != null && id !== "" ? Number(id) : null;
- } catch {
- return null;
- }
- }
- function parsePointCoordinates(pointWkt) {
- if (!pointWkt || !/^POINT\s*\(/i.test(String(pointWkt).trim())) {
- return null;
- }
- try {
- const coord = util.wktCastGeom(pointWkt).getFirstCoordinate();
- if (!coord || coord.length < 2) return null;
- return { longitude: coord[0], latitude: coord[1] };
- } catch {
- return null;
- }
- }
- function resolveZoneCoverUrl(path) {
- if (!path) return defaultCover;
- const text = String(path).trim();
- if (!text) return defaultCover;
- if (/^https?:\/\//i.test(text)) return text;
- return base_img_url2 + text.replace(/^\//, "");
- }
- function mapZoneItem(zone) {
- const coords = parsePointCoordinates(zone?.point);
- return {
- id: zone.zone_id,
- zoneId: zone.zone_id,
- zone_id: zone.zone_id,
- name: zone.zone_name || "",
- count: zone.image_count ?? 0,
- cover: resolveZoneCoverUrl(zone.cover_url),
- longitude: coords?.longitude,
- latitude: coords?.latitude,
- point: zone.point,
- };
- }
- const setMarkerEl = (id, el) => {
- if (el) markerEls[id] = el;
- else delete markerEls[id];
- };
- const getFarmCenter = () => {
- try {
- const farm = JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
- const wkt = farm.wkt || farm.geom_wkt || farm.farm_location;
- if (typeof wkt === "string" && /^POINT\s*\(/i.test(wkt.trim())) {
- return util.wktCastGeom(wkt).getFirstCoordinate();
- }
- } catch {
- // ignore
- }
- const firstZone = zoneList.value.find((z) => z.longitude != null && z.latitude != null);
- if (firstZone) {
- return [firstZone.longitude, firstZone.latitude];
- }
- return DEFAULT_CENTER;
- };
- // ---------- 业务逻辑:地图 / 区域 ----------
- const clearMapOverlays = () => {
- if (!fileMap.kmap?.map) {
- mapOverlays.length = 0;
- return;
- }
- mapOverlays.forEach((overlay) => fileMap.kmap.map.removeOverlay(overlay));
- mapOverlays.length = 0;
- };
- const bindZoneOverlays = () => {
- if (!fileMap.kmap?.map) return;
- clearMapOverlays();
- zoneList.value.forEach((zone) => {
- const el = markerEls[zone.id];
- if (!el || zone.longitude == null || zone.latitude == null) return;
- const overlay = new Overlay({
- element: el,
- position: [zone.longitude, zone.latitude],
- positioning: "bottom-center",
- stopEvent: false,
- });
- fileMap.kmap.map.addOverlay(overlay);
- mapOverlays.push(overlay);
- });
- };
- const fitZonesOnMap = () => {
- const records = zoneList.value
- .filter((z) => z.longitude != null && z.latitude != null)
- .map((z) => ({
- zone_name: "",
- polygon: `POINT(${z.longitude} ${z.latitude})`,
- }));
- if (!records.length) return;
- fileMap.setRecordPolygons(records, "album");
- };
- async function fetchZoneList() {
- const farmId = getSelectedFarmId();
- if (!farmId) {
- zoneList.value = [];
- return;
- }
- try {
- const res = await VE_API.questionnaire.getZones({ farm_id: farmId });
- const zones = res?.data?.zones;
- zoneList.value = Array.isArray(zones) ? zones.map(mapZoneItem) : [];
- } catch (e) {
- console.warn("[albumMap] getZones failed", e);
- zoneList.value = [];
- }
- }
- const initAlbumMap = async () => {
- await nextTick();
- if (!mapContainer.value) return;
- await fetchZoneList();
- const center = getFarmCenter();
- fileMap.initMap(`POINT(${center[0]} ${center[1]})`, mapContainer.value);
- await nextTick();
- bindZoneOverlays();
- fileMap.kmap?.map?.updateSize?.();
- fitZonesOnMap();
- };
- // ---------- 事件处理 ----------
- const handleLocationChange = (payload) => {
- if (!payload?.coordinateArray || !fileMap.kmap) return;
- fileMap.kmap.getView().animate({
- center: payload.coordinateArray,
- zoom: 16,
- duration: 0,
- });
- };
- const handleLocate = () => {
- if (!fileMap.kmap) return;
- const center = getFarmCenter();
- fileMap.kmap.getView().animate({
- center,
- zoom: 16,
- duration: 0,
- });
- };
- // ---------- 生命周期 ----------
- onMounted(initAlbumMap);
- onActivated(initAlbumMap);
- onBeforeUnmount(clearMapOverlays);
- </script>
- <style lang="scss" scoped>
- .album-map-page {
- display: flex;
- flex-direction: column;
- width: 100%;
- height: 100vh;
- overflow: hidden;
- background: #fff;
- }
- .album-map-content {
- position: relative;
- flex: 1;
- min-height: 0;
- overflow: hidden;
- .map-container {
- width: 100%;
- height: 100%;
- }
- }
- .zone-marker {
- display: flex;
- flex-direction: column;
- align-items: center;
- pointer-events: none;
- &__label {
- padding: 3px 14px;
- border-radius: 25px;
- background: #fff;
- color: #0a0a0a;
- font-size: 12px;
- }
- &__thumb {
- position: relative;
- width: 42px;
- height: 42px;
- margin-top: 8px;
- img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- border: 2px solid #fff;
- border-radius: 6px;
- box-sizing: border-box;
- }
- }
- &__badge {
- position: absolute;
- top: -6px;
- right: -6px;
- min-width: 18px;
- height: 18px;
- line-height: 16px;
- border-radius: 50%;
- background: #fff;
- color: #0a0a0a;
- font-size: 12px;
- text-align: center;
- }
- &__arrow {
- width: 0;
- height: 0;
- border-left: 5px solid transparent;
- border-right: 5px solid transparent;
- border-top: 6px solid #fff;
- }
- }
- .search-bar {
- position: absolute;
- top: 12px;
- left: 12px;
- right: 12px;
- z-index: 2;
- display: flex;
- align-items: center;
- height: 40px;
- padding: 0 4px 0 12px;
- border-radius: 20px;
- background: rgba(0, 0, 0, 0.45);
- box-sizing: border-box;
- &__search {
- flex: 1;
- min-width: 0;
- :deep(.el-select__wrapper) {
- background: transparent;
- box-shadow: none;
- border: none;
- min-height: 40px;
- padding-left: 0;
- }
- :deep(.el-select__placeholder),
- :deep(.el-select__input) {
- color: rgba(255, 255, 255, 0.7);
- }
- :deep(.el-icon) {
- color: rgba(255, 255, 255, 0.85);
- }
- }
- }
- .locate-btn {
- position: absolute;
- right: 12px;
- bottom: 96px;
- z-index: 2;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 36px;
- height: 36px;
- border-radius: 8px;
- background: #fff;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
- &__icon {
- width: 16px;
- height: 18px;
- }
- }
- </style>
|