Nuxt + Algolia - Google 地图不更新即时搜索的新结果

Nuxt + Algolia - Google Maps not updating the new results from instantsearch

我正在使用 nuxt 开发即时搜索。 Googlemaps 在页面加载时收到第一组结果并毫无问题地呈现标记 - 但是在搜索新结果时不会在地图上更新?有没有一种方法可以将新的结果集推送到地图以呈现标记?

我假设这将是 beforeMount 挂钩并将结果传递给 updateMap?

pages/index.vue

<template>
  <div class="root">

    <div class="container">

      <ais-instant-search-ssr
        :search-client="searchClient"
        index-name="dive_buddy"
      >
        <ais-configure :hits-per-page.camel="7">
          <ais-search-box placeholder="Search here…" class="searchbox" />
          <ais-stats />

          <div class="h-screen">
            <ais-hits>
              <template #default="{ items }">
                <div class="grid grid-cols-2 gap-2">
                  <div v-if="items.length">
                    <div
                      v-for="item in items"
                      :key="item.objectID"
                      class="my-2 p-2 border"
                    >
                      <SiteRow
                        :site="item"
                        @mouseover.native="highlightMarker(item.objectID, true)"
                        @mouseout.native="highlightMarker(item.objectID, false)"
                      />
                    </div>
                  </div>
                  <div v-else>No results found</div>
                  <Map
                    :items="items"
                    :lat="items[0].lat"
                    :lng="items[0].lng"
                  />
                </div>
              </template>
            </ais-hits>
            <ais-pagination />
          </div>
        </ais-configure>
      </ais-instant-search-ssr>
    </div>
  </div>
</template>

<script>
import {
  AisInstantSearchSsr,
  AisConfigure,
  AisHits,
  AisSearchBox,
  AisStats,
  AisPagination,
  createServerRootMixin,
} from 'vue-instantsearch'
import algoliasearch from 'algoliasearch/lite'
import _renderToString from 'vue-server-renderer/basic'

function renderToString(app) {
  return new Promise((resolve, reject) => {
    _renderToString(app, (err, res) => {
      if (err) reject(err)
      resolve(res)
    })
  })
}

const searchClient = algoliasearch(
  'xxx', // AppID
  'xxxxxxxx' // APIKey
)

export default {
  components: {
    AisInstantSearchSsr,
    AisConfigure,
    AisHits,
    AisSearchBox,
    AisStats,
    AisPagination,
  },
  mixins: [
    createServerRootMixin({
      searchClient,
      indexName: 'dive_buddy',
    }),
  ],
  data() {
    return {
      searchClient,
    }
  },
  head() {
    return {
      link: [
        {
          rel: 'stylesheet',
          href: 'https://cdn.jsdelivr.net/npm/instantsearch.css@7.4.5/themes/satellite-min.css',
        },
      ],
    }
  },

  serverPrefetch() {
    return this.instantsearch
      .findResultsState({
        component: this,
        renderToString,
      })
      .then((algoliaState) => {
        this.$ssrContext.nuxt.algoliaState = algoliaState
      })
  },

  beforeMount() {
    const results =
      (this.$nuxt.context && this.$nuxt.context.nuxtState.algoliaState) ||
      window.__NUXT__.algoliaState

    this.instantsearch.hydrate(results)

    // Remove the SSR state so it can't be applied again by mistake
    delete this.$nuxt.context.nuxtState.algoliaState
    delete window.__NUXT__.algoliaState
  },

  methods: {
    highlightMarker(id, isHighlighted) {
      document
        .getElementsByClassName(`id-${id}`)[0]
        ?.classList?.toggle('marker-highlight', isHighlighted)
    },
  },
}
</script>

components/Map.vue

<template>
  <div ref="map" class="h-screen"></div>
</template>

<script>
export default {
  props: {
    items: {
      type: Array,
      required: true,
    },
    lat: {
      type: Number,
      required: true,
    },
    lng: {
      type: Number,
      required: true,
    },
  },

  mounted() {
    this.updateMap()
  },

  methods: {
    // run showMap function in maps.client.js
    async updateMap() {
      await this.$maps.showMap(
        this.$refs.map,
        this.lat,
        this.lng,
        this.getMarkers()
      )
    },

    // pass through markers to googlemaps
    getMarkers() {
      return this.items.map((item) => {
        return {
          ...item,
        }
      })
    },
  },
}
</script>

plugins/maps.client.js

export default function(context, inject){
    let isLoaded = false
    let waiting = []
    
    addScript()
    inject('maps', {
        showMap,
    })


    function addScript(){
        const script = document.createElement('script')
        script.src = "https://maps.googleapis.com/maps/api/js?key=xxxx&libraries=places&callback=initGoogleMaps"
        script.async = true
        window.initGoogleMaps = initGoogleMaps
        document.head.appendChild(script)
    }

    function initGoogleMaps(){
        isLoaded =  true
        waiting.forEach((item) => {
            if(typeof item.fn === 'function'){
                item.fn(...item.arguments)
            }
        })
        waiting = []
    }

    function showMap(canvas, lat, lng, markers){
        if(!isLoaded){
            waiting.push({
                fn: showMap,
                arguments,
            })
            return
        }
        const mapOptions = {
            zoom: 18,
            center: new window.google.maps.LatLng(lat, lng),
            disableDefaultUI: true,
            zoomControl: true,
            styles:[{
                featureType: 'poi.business',
                elementType: 'labels.icon',
                stylers:[{ visibility: 'off' }]
            }]
        }
        const map = new window.google.maps.Map(canvas, mapOptions)
        if(!markers){
            const position = new window.google.maps.LatLng(lat, lng)
            const marker = new window.google.maps.Marker({ 
                position,
                clickable: false,
            })
            marker.setMap(map)
            return
        }

        const bounds = new window.google.maps.LatLngBounds()
        let index = 1
        markers.forEach((item) => {
            const position = new window.google.maps.LatLng(item.lat, item.lng)
            const marker = new window.google.maps.Marker({ 
                position, 
                label: {
                    text: `${index}`,
                    color: 'black',
                    className: `marker id-${item.objectID}`
                },
                icon: 'https://maps.gstatic.com/mapfiles/transparent.png',
                clickable: false,
            })
            marker.setMap(map)
            bounds.extend(position)
            index++
        })

        map.fitBounds(bounds)
        
    }
}

编辑 根据@Bryan 的建议 - 我之前曾尝试使用 vue-googlemaps 插件遵循 algolia 的文档,但这在 Nuxt 上不起作用,因为地图无法加载以及这些错误 -
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'g')
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'h')

虽然这不是上述问题的解决方案 - 我找到了使用不同库的解决方法:vue2-google-maps 因为它适用于 Nuxt (see docs)。

pages/index.vue

<template>
  <div class="root">

    <div class="container">
      <ais-instant-search-ssr
        :search-client="searchClient"
        index-name="dive_centres"
      >
        <ais-configure :hits-per-page.camel="7">
          <ais-search-box placeholder="Search here…" class="searchbox" />
          <ais-stats />

          <div class="h-screen">
            <ais-hits>
              <template #default="{ items }">
                <div class="grid grid-cols-2 gap-2">
                  <div v-if="items.length">
                    <div
                      v-for="item in items"
                      :key="item.objectID"
                      class="my-2 p-2 border"
                    >
                      <SiteRow
                        :site="item"
                        @mouseover.native="highlightMarker(item.objectID, true)"
                        @mouseout.native="highlightMarker(item.objectID, false)"
                      />
                    </div>
                  </div>
                  <div v-else>No results found</div>
                  <Map />
                </div>
              </template>
            </ais-hits>
            <ais-pagination />
          </div>
        </ais-configure>
      </ais-instant-search-ssr>
    </div>
  </div>
</template>

<script>
import {
  AisInstantSearchSsr,
  AisConfigure,
  AisHits,
  AisSearchBox,
  AisStats,
  AisPagination,
  createServerRootMixin,
} from 'vue-instantsearch'
import algoliasearch from 'algoliasearch/lite'
import _renderToString from 'vue-server-renderer/basic'

function renderToString(app) {
  return new Promise((resolve, reject) => {
    _renderToString(app, (err, res) => {
      if (err) reject(err)
      resolve(res)
    })
  })
}

const searchClient = algoliasearch(
  'xxx',     // AppID
  'xxxxxxxx' // APIKey
)

export default {
  components: {
    AisInstantSearchSsr,
    AisConfigure,
    AisHits,
    AisSearchBox,
    AisStats,
    AisPagination,
  },
  mixins: [
    createServerRootMixin({
      searchClient,
      indexName: 'dive_sites',
    }),
  ],

  head() {
    return {
      link: [
        {
          rel: 'stylesheet',
          href: 'https://cdn.jsdelivr.net/npm/instantsearch.css@7.4.5/themes/satellite-min.css',
        },
      ],
    }
  },

  serverPrefetch() {
    return this.instantsearch
      .findResultsState({
        component: this,
        renderToString,
      })
      .then((algoliaState) => {
        this.$ssrContext.nuxt.algoliaState = algoliaState
      })
  },

  beforeMount() {
    const results =
      (this.$nuxt.context && this.$nuxt.context.nuxtState.algoliaState) ||
      window.__NUXT__.algoliaState

    this.instantsearch.hydrate(results)

    // Remove the SSR state so it can't be applied again by mistake
    delete this.$nuxt.context.nuxtState.algoliaState
    delete window.__NUXT__.algoliaState
  },

  methods: {
    highlightMarker(id, isHighlighted) {
      console.log(id)
      document
        .getElementsByClassName(`id-${id}`)[0]
        ?.classList?.toggle('marker-highlight', isHighlighted)
    },
  },
}
</script>

components/Map.vue

<template>
  <div v-if="state" class="ais-GeoSearch">
    
    <GmapMap
      :center="center"
      :zoom="zoom"
      map-type-id="terrain"
      class="h-full w-full"
      :options="options"
    >
      <GmapMarker
        v-for="item in state.items"
        :key="item.objectID"
        :position="item._geoloc"
        :clickable="false"
        :draggable="true"
        :icon="{ url: require('~/static/images/dive-site-small.svg') }"
      />
    </GmapMap>
  </div>
</template>

<script>
import { createWidgetMixin } from 'vue-instantsearch'
import { connectGeoSearch } from 'instantsearch.js/es/connectors'

export default {
  mixins: [createWidgetMixin({ connector: connectGeoSearch })],

  data() {
    return {
      zoom: 4,
      options: {
        disableDefaultUI: true,
      },
    }
  },
  computed: {
    center() {
      return this.state.items.length !== 0
        ? this.state.items[0]._geoloc
        : { lat: 48.864716, lng: 2.349014 }
    },
  },
}
</script>