整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

去哪网开发实战记录(9):城市选择页(中)

去哪网开发实战记录(9):城市选择页(中)

弟组件之间的联动

所谓的兄弟组件之间的联动,其实就是实现点击右侧的字母就能跳转至对应的首字母城市,因此列表组件需要知道右侧的字母列表的点击事件所对应的元素字母,这就需要兄弟组件间的数据传递了(Alphabet组件与List组件之间的通信),可以使用到中央数据总线Bus,但是由于这里的业务逻辑不是很复杂,因此可以将Alphabet组件内的信息传递到City组件(子组件向父组件传递信息,发布订阅模式),然后City组件向List组件传递信息(父组件向子组件传递信息,属性传值方式)。

在gitee的分支栏点击新建分支city-components,然后记得将本地master分支切换到city-components分支。

字母点击跳转

打开Alphabet.vue文件,给字母表中的字母添加一个click事件,然后尝试将点击的字母在控制台上进行输出显示:

<template>
<ul class="list">
  <li class="item"
      v-for="(item,key) of cities"
      :key="key"
      @click="handleLetterClick"
  >
    {{key}}
  </li>
</ul>
</template>

然后在script标签添加这个handleLetterClick方法:

methods: {
    handleLetterClick (e) {
      console.log(e.target.innerText)
    }
  }

可以测试一下当你点击右侧列表中的字母,控制台是否真的输出了对应的字母:

前面说了由于这里的业务逻辑不是很复杂,因此可以将Alphabet组件内的信息传递到City组件(子组件向父组件传递信息,发布订阅模式),然后City组件向List组件传递信息(父组件向子组件传递信息,属性传值方式)。

那就开始编写使用发布订阅模式实现Alphabet组件内的信息传递到City组件的代码。修改子组件Alphabet.vue中handleLetterClick函数代码为:

methods: {
    handleLetterClick (e) {
    /** 注意此处必须使用innerText而不是innerHTML **/
      this.$emit('change', e.target.innerText)
    }
  }

接着去父组件City.vue中监听该chang事件,请定义相应的handleLetterChange去接收子组件传递过来的信息:

<template>
  <div>
    <city-header></city-header>
    <city-search></city-search>
    <city-list :cities="cities" :hotCities="hotCities"></city-list>
    <city-alphabet :cities="cities" @change="handleLetterChange"></city-alphabet>
  </div>
</template>

handleLetterChange (letter) {
      console.log(letter)
    }

控制台测试发现当你点击右侧列表中的字母,控制台仍然输出了对应的字母。接下来就是父组件City.vue通过属性传值的方式将数据传递给子组件List.vue。修改父组件City.vue中handleLetterChange函数代码为:

   handleLetterChange (letter) {
      this.letter=letter
    }

然后在父组件中的data中返回letter,并将其通过属性传值给city-list组件:

<template>
  <div>
   ...
    <city-list :cities="cities" :hotCities="hotCities" :letter="letter"></city-list>
...
  </div>
</template>


data () {
    return {
      ...
      letter: ''
    }

然后在子组件List.vue中通过props来接收数据:

  props: {
    cities: Object,
    hotCities: Array,
    letter: String
  },

当List.vue发现letter有变化的时候,就显示跟letter首字母相同的城市列表,这种功能可以通过侦听器来实现:

watch: {
    letter () {
      console.log(this.letter)
    }
  }

控制台测试发现当你点击右侧列表中的字母,控制台仍然输出了对应的字母。

还记得之前推荐的那篇关于better-scroll的文章:当 better-scroll 遇见 Vue么,里面介绍了如何滚动至某个元素。既然是滚动至某个元素,那么肯定需要选择某个DOM节点了,Vue提供了ref来选择节点(List.vue):

<template>
   <div class="area"
           v-for="(item, key) of cities"
           :key="key"
           :ref="key"
      >
</template>

然后使用侦听器来监听letter的变化:

  watch: {
    letter () {
      if (this.letter) {
        console.log(this.$refs[this.letter])
      }
    }
  }

当你点击某个字母时,控制台输出:

而这个索引为0的div.area区域中包含了我们所需要的的城市列表信息:

既然这样就可以直接获取到对应的DOM节点,并将该节点传递给better-scroll,里面有一个scrollToElement方法,之后就能实现点击某个字母,城市列表页就会显示对应的城市信息:

  watch: {
    letter () {
      if (this.letter) {
        const element=this.$refs[this.letter][0]
        this.scroll.scrollToElement(element)
      }
    }
  }

拇指滑动跳转

前面介绍的是点击右侧的字母就能跳转至对应的首字母城市,其实比这个更普通的 就是当你拇指按住字母表上下滑动时,左边List组件也会相应的上下跳动。

实现这个需要,首先我们要监听使用者的手指,记录时候开始点击字母列表(touchstart),什么时候开始滚动(touchmove),以及什么时候离开字母列表(touchend)等等,自然而然地想到使用事件监听。注意我们还需要定义一个标志状态(touchStatus),默认为flase,只有当你手指触摸的时候才会变成true,其实就是指定这三个函数的执行顺序:

<template>
<ul class="list">
  <li class="item"
      v-for="(item,key) of cities"
      :key="key"
      @touchstart="handleTouchStart"
      @touchmove="handleTouchMove"
      @touchend="handleTouchEnd"
      @click="handleLetterClick"
  >
    {{key}}
  </li>
</ul>
</template>


<script>
...
  methods: {
    handleLetterClick (e) {
     ...
    handleTouchStart () {
      this.touchStatus=true
    },
    handleTouchMove () {
      if (this.touchStatus) {

      }
    },
    handleTouchEnd () {
      this.touchStatus=false
    }
  }
}
</script>

现在我们需要知道当你拇指在滑动的时候,你的拇指停留在哪个字母上,这件事情其实是较为复杂的,可以提供一种思路仅供参考:先获取A字母距离顶部的高度,然后当你滑动的时候获取你当前字母距离顶部的高度,接着将后者减去前者得到一个差值,最后用这个差值除于每个字母的长度就能得到这是第几个字母。然后让该字母触发对应的事件即可,那么这样你需要新建一个数组用于存放字母,然后根据索引来获取字母,可以使用计算属性:

  computed: {
    letters () {
      const letters=[]
      for (let i in this.cities) {
        letters.push(i)
      }
      return letters
    }
  },

上面的计算属性其实就是得到一个类似于['A','B','C','D']的数组。然后你城市字母表遍历的对象就不再是cities,而是letters了(那就不需要使用key了,直接遍历输出item就可以):

<template>
<ul class="list">
  <li class="item"
      v-for="item of letters"
      :key="item"
      @touchstart="handleTouchStart"
      @touchmove="handleTouchMove"
      @touchend="handleTouchEnd"
      @click="handleLetterClick"
  >
    {{item}}
  </li>
</ul>
</template>

接下来继续编写handleTouchMove函数的内容,这个函数就是用于计算当你拇指在滑动的时候,你的拇指停留在哪个字母上。实现的逻辑是先获取A字母距离顶部的高度,然后当你滑动的时候获取你当前字母距离顶部的高度,接着将后者减去前者得到一个差值,最后用这个差值除于每个字母的长度就能得到这是第几个字母。

第一步在template中给DOM节点添加ref属性,用于获取某个DOM节点:ref="item";

第二步,修改handleTouchMove函数为:

   handleTouchMove () {
      if (this.touchStatus) {
        const startY=this.$refs['A'][0].offsetTop
        console.log(startY)
      }
    },

通过测试发现当你拇指在滑动的时候,控制台始终输出61,这个61就是A字母距离页面顶部(注意是蓝色区域底部)的高度:

然后就可以获取每个字母距离整个页面的高度,里面有一个最小的值就是拇指距离整个页面最小的高度:

如果你想要获取某个字母距离蓝色区域底部的高度,可以将其高度减去蓝色区域底部的高度79(43(Header组件高度)+36(Search组件告高度)=79),然后除以20(每个字母高度)并向下取整就能得到每个字母的索引:

  handleTouchMove (e) {
      if (this.touchStatus) {
        const startY=this.$refs['A'][0].offsetTop
        const touchY=e.touches[0].clientY - 79
        const index=Math.floor((touchY - startY) / 20)
        console.log(index)
      }
    },

测试发现拇指移动到字母M处,右侧显示正常:

最后使用子组件Alphabet使用发布订阅模式向City组件传递数据:

handleTouchMove (e) {
      if (this.touchStatus) {
        const startY=this.$refs['A'][0].offsetTop
        const touchY=e.touches[0].clientY - 79
        const index=Math.floor((touchY - startY) / 20)
        if (index >=0 && index < this.letters.length) {
          this.$emit('change', this.letters[index])
        }
      }
    },

测试发现功能显示正常。

城市列表页新能优化

接下来就是对上面的代码进行优化,因为startY是一个定值,而按照目前写的代码则是每次都需要去执行,这会造成性能低下。可以在data中return一个startY(初始值为0),然后定义一个生命周期函数update,只有页面的数据被更新同时页面完成了渲染时,该方法才会被执行:

  data () {
    return {
      touchStatus: false,
      startY: 0
    }
  },
  updated () {
    this.startY=this.$refs['A'][0].offsetTop
  },

handleTouchMove (e) {
      if (this.touchStatus) {
        const touchY=e.touches[0].clientY - 79
        const index=Math.floor((touchY - this.startY) / 20)
        if (index >=0 && index < this.letters.length) {
          this.$emit('change', this.letters[index])
        }
      }
    },

我们知道页面一开始的时候cities变量是空值,也就是Alphabet组件内不会显示任何信息,然后通过ajax获取到数据时Alphabet组才会被重新渲染,之后会触发生命周期函数updated,这时候开始计算蓝色区域底部与字母A标签的距离。

还有一个优化就是函数节流。函数节流就是限制一个函数在一定时间内只能执行一次,这里就是当你拇指在字母表中上下移动时,touchmove函数执行的频率非常高会造成性能低下,因此可以借助于函数节流来优化该代码。具体的函数节流介绍可以参看这篇文章JS进阶篇1---函数节流(throttle),此处采用计时器规定在一定时间内函数才允许执行。打开Alphabet.vue文件,先return一个timer对象,初始值是null,其次修改handleTouchMove函数代码为:

handleTouchMove (e) {
      if (this.touchStatus) {
        if (this.timer) {
          clearTimeout(this.timer)
        }
        this.timer=setInterval(()=> {
          const touchY=e.touches[0].clientY - 79
          const index=Math.floor((touchY - this.startY) / 20)
          if (index >=0 && index < this.letters.length) {
            this.$emit('change', this.letters[index])
          }
        }, 16)
      }
    },

原理非常简单,先判断timer计时器对象是否存在,存在就清空该计时器避免缓存,否则就定义一个计时器并设置计时器时间为16毫秒,即每16毫秒才计算一次,这样可避免不必要的计算工作。

搜索功能实现

在gitee的分支栏点击新建分支city-search-logic,然后记得将本地master分支切换到city-search-logic分支,接下来开始进行搜索框的业务逻辑开发。

打开city文件夹中的Search.vue组件,修改其中的template代码为:

<template>
  <div>
    <div class="search">
      <input class="search-input" type="text" placeholder="输入城市名称或者拼音"/>
    </div>

    <div class="search-content">
      <ul>
        <li>123</li>
      </ul>
    </div>
  </div>
</template>

这里面其实就是新增了一个搜索结果展示的区域,也就是.search-content类所占区域,接着在style标签中新增.search-content类所对应的样式(注意它应该和.search类是平级关系):

.search-content
  z-index: 1
  overflow: hidden
  position: absolute
  top: 1.58rem
  left: 0
  right: 0
  bottom: 0
  background: green

然后需要实现搜索信息与结果展示区域的联动,就需要使用到数据的双向绑定:

<script>
export default {
  name: 'CitySearch',
  data () {
    return {
      keyword: ''
    }
  }
}
</script>


<template>
  <div>
    <div class="search">
      <input v-model="keyword" class="search-input" type="text" placeholder="输入城市名称或者拼音"/>
    </div>
...
  </div>
</template>

最后结果肯定是显示城市,那么需要从父组件City中接收cities,修改City.vue组件中的template代码为:

<template>
  <div>
    <city-header></city-header>
    <city-search :cities="cities"></city-search>
....
  </div>
</template>

然后在子组件Search.vue中通过props来接收cities:

  props: {
    cities: Object
  },

然后我们在子组件Search.vue中return一个数组list(该数组用于存储搜索结果)和计时器timer(函数节流使用):

  data () {
    return {
      keyword: '',
      list: [],
      timer: null
    }
  }

接着我们定义一个侦听器用于监听keyword的变化:

  watch: {
    keyword () {
      if (this.timer) {
        clearTimeout(this.timer)
      }
      this.timer=setInterval(()=> {
        // 书写在cities对象中查找某个元素的逻辑
        const result=[]
        for (let i in this.cities) {
          // i就是字母A,B...,而this.city[i]就是数组,value就是数组中的每一项
          this.cities[i].forEach((value)=> {
            if (value.spell.indexOf(this.keyword) > -1 ||
              value.name.indexOf(this.keyword) > -1
            ) {
              result.push(value)
            }
          })
        }
        this.list=result
      }, 100)
    }
  }

在city.json文件中,我们定义的cities结构为:

"cities": {
            "A": [{
                "id": 56,
                "spell": "aba",
                "name": "阿坝"
            }, {
                "id": 57,
                "spell": "akesu",
                "name": "阿克苏"
            }...

也就是cities本身是一个对象,里面又包含了一个数组作为值,而数组中包含的则是一个个对象。注意if语句中的判断条件为value.spell.indexOf(this.keyword) > -1 ||value.name.indexOf(this.keyword) > -1`也就是通过拼音或中文都可以查找是否能找到匹配的数据。

接下来就是对搜索结果的布局进行优化,给搜索结果添加一个.search-item类和一个边框类.border-bottom:

<div class="search-content">
      <ul>
        <li class="search-item" v-for="item of list" :key="item.id">{{ item.name }}</li>
      </ul>
</div>

然后给.search-item类添加样式,并修改.search-content类的背景颜色为#eee:

.search-content
  z-index: 1
  overflow: hidden
  position: absolute
  top: 1.58rem
  left: 0
  right: 0
  bottom: 0
  background: #eee
  .search-item
    line-height: .62rem
    padding-left: .2rem
    background: #fff
    color: #666

测试发现页面显示正常,但是搜索结果是无法滚动的,此时可以借助于bertter-scroll来实现。

第一步获取到search-content类节点DOM:

  <div class="search-content" ref="search">
      <ul>
        <li class="search-item border-bottom" v-for="item of list" :key="item.id">{{ item.name }}</li>
      </ul>
    </div>

第二步导入Bscroll类及创建该对象:

<script>
import BScroll from 'better-scroll'
export default {
   name: 'CitySearch',
  props: {
    cities: Object
  },
  mounted () {
    this.scroll=new BScroll(this.$refs.search)
  }
}
</script>

只需这两步就完成了页面滚动的效果。还有一个问题就是当你输入完并清空搜索框的时候,搜索结果依旧还是存在:

其实只需要当你的keyword为空的时候,你将这个list设置为[]即可,在侦听器中添加实现上述功能的逻辑:

  watch: {
    keyword () {
      if (this.timer) {
        clearTimeout(this.timer)
      }
      if (!this.keyword) {
        this.list=[]
        return
      }
     ...
  }

这样就解决了这个问题。新的问题又来了就是当你输入一串非常长的字母或者说是输入的关键词不能匹配任何城市时,前面我们是什么也不显示,其实这个是有一点问题的,我们最好是在页面上添加诸如“找不到对应的城市”等信息。最简单的方式就是使用v-show来进行显示:

 <div class="search-content" ref="search">
      <ul>
        <li class="search-item border-bottom" v-for="item of list" :key="item.id">{{ item.name }}</li>
      </ul>
      <ul>
        <li class="search-item border-bottom" v-show="!list.length">没有找到匹配的城市</li>
      </ul>
    </div>

这样我们就实现了当list的长度为0的时候才显示“没有找到匹配的城市”字眼,但是这样会造成一个问题,就是刚开始你不输入关键词的时候也会出现这个“没有找到匹配的城市”字眼,把热门城市和城市列表给遮住了。这个问题还是可以通过v-show来解决(只不过这次将v-show 标签加到search-content类上):

    <div class="search-content" v-show="keyword" ref="search">
      <ul>
        <li class="search-item border-bottom" v-for="item of list" :key="item.id">{{ item.name }}</li>
      </ul>
      <ul>
        <li class="search-item border-bottom" v-show="!list.length">没有找到匹配的城市</li>
      </ul>
    </div>

这样就完美地解决了上述问题。前面介绍过html中最好不需要包含计算逻辑(在“没有找到匹配的城市”标签上使用了取反运算):

<li class="search-item border-bottom" v-show="!list.length">没有找到匹配的城市</li>

因此需要将这个取反的逻辑使用计算属性来代替:

  computed: {
    hasNoData () {
      return !this.list.length
    }
  },

自然而然就需要修改上面的template中的取反运算代码为:

<li class="search-item border-bottom" v-show="hasNoData">没有找到匹配的城市</li>

这样就完成了搜索框的业务逻辑。最后就是将我们的代码上传到city-search-logic分支,并且将其与master分支进行合并,相应的步骤如下:

于企业多地点业务而言,SEO 策略与业务策略的协调对于成功至关重要。

无论企业是经营特许经营模式、零售连锁店,还是以服务区业务的形式运营多个中心,您的本地 SEO方法都需要量身定制,以满足您的特定目标。它还需要具有足够的可扩展性和效率,以便在获得长期投资回报的同时进行维护。

另一个关键要求是,您的内容方法要为用户和 Google 创造足够的价值,以使其高于索引质量阈值。

这意味着超越本地 SEO 的标准最佳实践 ,并创建可持续提高品牌知名度和转化率的本地 SEO 活动。

协调 SEO 与业务战略

多地点经营的企业有不同的目标。

虽然多地点管理的基础相同,但您的方法需要与整体战略相结合并与整体业务目标保持一致。

例如,在多个城镇、城市和州经营服务业务的多家运营商的战略特许经营业务与在多个州拥有数百家分店的大型仓储式商店有所不同。

成功指标也各不相同。通常,企业本地 SEO 活动的 KPI 属于以下类别之一:

  • 提高各个地点的知名度和客流量。
  • 将本地意向搜索引导至在线商店进行直接投放,或将来与本地商店进行互动。
  • 以上两者的结合。

企业对“成功”的定义将极大地影响您为用户创建选择架构的方法以及报告成功的方式。

批量创建本地页面的方法

多年来,我们描述和制作多区域服务页面的方法发生了变化。

十年前,我们会将质量低下的版本(只有细微修改且内容基本相同)称为门页,而谷歌随着时间的推移逐渐降低了门页的价值。

近年来,随着程序化 SEO(pSEO)的日益普及,这种方法已经成为大规模创建这些页面的流行方法。

本地服务页面的程序化内容创建

对于运营数百或数千个门店的企业来说,程序化或部分程序化内容创建可能是一个有吸引力的选择。

程序化 SEO(简称 pSEO)可让您大规模生成大量内容。这种方法已帮助许多企业扩大规模,但如果所创建的页面无法提供足够独特的价值主张,让 Google 不愿投入资源,那么这种方法也会带来问题。

如果我们看一下本地服务页面的两个常见网站架构,我们通常有一个中央服务页面,然后是本地服务页面,或者有一个充当区域设置服务页面网关的中央页面 - 例如商店定位器。

图片来自作者,2024 年 7 月

根据您的业务类型,您可能会默认选择一种结构,但两者都可能带来挑战。

使用中央服务页面结构,您可能会遇到创建独特价值主张的问题,并确保每个页面都具有足够的差异化并高于 Google 索引的质量阈值。

商店定位器页面方法可能会导致 PageRank 分布以及内部链接到不同位置的方式出现问题。大多数用户友好的商店位置应用程序不会加载 HTML 链接,因此虽然可以直观地链接到所有商店,但 Google 无法抓取这些链接。

然而,这两种方法的共同问题是如何捕获位置周围“更广泛”的搜索。

本地内容价值主张

当本地页面最适合位置时,它们会发挥最大的帮助。

从历史上看,我看到一些公司通过在页面上“夸大”该地区的额外信息来做到这一点,比如一两段关于当地基础设施、学校和运动队的段落——如果你想让人们访问你的五金店或询问你的上门安全装配服务,这些信息都与你无关。

仅仅更改 URL、H1、标题标签和整个正文中的位置名称也是不够的。

当这种情况发生时,谷歌实际上会看到近似重复的页面,这些页面在与用户查询相关的价值主张上几乎没有区别。

这种情况的症状是,当页面在 Search Console 中显示为未编入索引时,Google 要么选择覆盖用户声明的规范,要么停留在“已发现”或“已抓取”的阶段,而当前尚未编入索引。

本地服务和位置页面之间总会存在一定程度的重复。Google 对此并不介意。某些内容在多个页面上重复并不意味着其质量低下。

创造价值主张差异化

这是我倾向于采用部分程序化方法的地方。

程序化可以满足 70% (+) 的页面内容;它可以涵盖您针对特定位置的服务产品、定价和公司信息。

该页面的剩余百分比是手动的,但允许您创建与其他页面的价值主张差异化。

假设您是一家跨州快递服务公司,拥有多条市场路线,您在德克萨斯州的主要配送中心位于奥斯汀、圣安东尼奥和达拉斯,而您想要瞄准尤利斯的潜在客户。

您为尤利斯提供的服务与您为普弗拉格维尔、凯尔和利安德的客户提供的服务相同 - 因此每个位置页面的这些部分在所有位置上都是相同的。

但是尤利斯由达拉斯枢纽提供服务,而其他机场则由奥斯汀枢纽提供服务——这是您要强调的第一个内容差异点。

然后,您可以使用来自企业内部的数据和关键字研究,用旅行时间数据充实这些页面。

在尤利斯寻找快递服务的客户可能正在寻找从尤利斯到奥斯汀或从尤利斯到休斯顿的服务——因此,将此构建到本地页面并提供从目的地到热门地点的时间估算,可以显示出本地的专业性并帮助客户更好地了解服务和计划。

您的业?务数据还将帮助您识别客户类型。例如,在尤利斯预订的许多工作可能针对的是搬出校园居住的大学生,因此这又是针对可以包含在页面上的客户群的更具本地化的定位。

内部链接

当涉及到内部链接时,使用伪 HTML 站点地图可以帮助实现这一点,它不仅可以充当通过页面的干净内部链接,而且还对用户有益,并允许您创建其他登录页面来定位县或地区级搜索。

十年前,在一个房产查找页面上,我所在的团队构建了“县 > 镇/市”的页面结构模式,同时将相关位置拉入登录页面。

作者截图,2024 年 7 月

从视觉上看,这只是一种更“手动”的方法,让用户可以从非特定位置的页面筛选到他们所在的当地区域。

Google 商业资料链接

另一个经常被忽视的关键组件是将 Google 商业资料(GBP) 直接链接到网站上的相关位置页面。

我遇到过许多跨国公司和国内公司,他们链接回自己的公司主页,有时还带有一个参数来突出显示用户点击的是哪个 GBP——但这既是糟糕的网络架构,也是糟糕的用户选择架构。

如果用户正在寻找 XYZ 中的服务/商店,他们不希望在点击网站链接时看到主页或通用信息页面。

就用户选择架构而言,从这里,用户可以导航到不同的商店或页面,并错过与他们相关的关键信息,否则这些信息可能会推动销售或询问。

谷歌的本地算法

除了 Google 的核心算法和更通用的搜索排名信号外,Google 还发布了专门针对本地查询的更新。主要有两个:

  • Pigeon 2014:此次更新旨在通过将本地搜索结果与一般搜索排名信号更紧密地结合起来,提供更相关、更准确的本地搜索结果。用户接近度(作为信号)也得到了提升。
  • Possum 2016:此次更新旨在提高位于城市边界外的商家的排名,使搜索结果更符合用户与商家的距离。还引入了基于地址的过滤,以避免重复列出共享同一地址的商家(例如虚拟办公室)。

这些更新使企业更难以欺骗自己在当地市场的存在,并且可能无法提供符合或满足搜索者需求的价值主张。

据传,谷歌似乎优先对提供最全面信息的企业进行排名。

这包括开业日期、现场餐饮选择(如果适用)、特殊营业时间、业务类别、服务列表以及定义服务区域和服务类型。

Google 商业档案的重要性

遵循指南是必须的,但即便如此,你仍可能会违反 Google 的自动检测检查。

与一家在亚洲拥有多个办事处的国际软件公司合作,在共享办公室中租用了许多楼层。

我们假设,Google 偶尔会检测到共享地址并误认为它们是虚拟办公室/虚假地址,而Possum 算法更新旨在减少这种情况。

当您与拥有大量实际位置的企业组织合作时,通过内部利益相关者管理以及了解 GBP 如何适应并贡献于总体目标和生态系统,Google 商家资料管理方法可能会变得更加复杂。

报告英镑数据

根据您的目标,报告成功的方式将因活动而异。

通过 Google API,您可以访问展示次数的列表级数据,以及不同用户互动的细分(从 GSC 镜像指标推断展示次数和点击次数)。

非典型的 Google Business Profile 报告仪表板。(作者截图,2024 年 7 月)

在我看来,任何在多个城镇、城市、县或州运营的企业都需要拥有某种形式的 GBP 监控和报告可见性,而不仅仅是在 Google Search Console 和其他分析平台中跟踪参数化 URL(假设您在 GBP 网站链接上使用参数)。

备工作

省市区三级联动所需文件:评论回复区

文件说明:

整个文件为一个大的对象(非数组类型)

其中对象的属性名 100000 中包含所有省份,它也是一个对象格式的;

对象名为100000的每个属性中的属性名都为数字类型(其实是该地区的行政代码)。数字属性名对应的属性值是该地区的名称,同时也可以通过该属性名访问到它下级的市列表。

eg:

			import districts from './js/districts.js'
			console.log(districts[100000])

比如当我们想获取到行政代码130000河北省下面的市级列表时:

			import districts from './js/districts.js'
			console.log(districts[130000])

然后当我们想获得行政代码130100石家庄市下的区级列表时:

			import districts from './js/districts.js'
			console.log(districts[130100])

没错,省市中的每一个行政代码属性名都对应着它下一级(省行政代码对应市列表,市行政代码对应区列表)的对象列表。不存在数组对象嵌套。及其简单方便。

创建 option 标签简便API

new Option('innerText', 'value属性对应的值')

类似于传统写法:

let option_1=document.createElement('option')

option_1.text='innerText'

option_1.value='value属性对应的值'

嗯,没什么区别,但是第一种简单很多:

清空 select标签中的option标签极简方法:

selectDOM.options.length=0

.html文件中使用模块化

script 标签改为 type 属性改为 module <script type="module">

创建三个 select标签,分别用来存放 省级列表,市级列表,区级列表,并定义好变量:

HTML:

		<!-- 省级列表 -->
		<select id="province"></select>
		<!-- 市级列表 -->
		<select id="city"></select>
		<!-- 区级列表 -->
		<select id="area"></select>

JS:

			// 获取省市区selectDOM节点
			let oProvince=document.getElementById('province')
			let oCity=document.getElementById('city')
			let oArea=document.getElementById('area')

定义一个特定对应,用户将用户选中的结果获取到:

			let data={
				province: {},
				city: {},
				area: {}
			}

第一步,渲染省份列表

1,引入省市区文件 import districts from './js/districts.js'

直接一个for in 循环 districts[100000] 即可;

然后在循环中创建option标签,并插入到省级select节点中:

			let provinceList=districts[100000] 
			for (let province in provinceList) {
				oProvince.options.add( new Option(provinceList[province], province) )
			}

效果:

这样我们的第一个select标签就完成了

第二步,渲染市级列表

ps: 市级列表市基于省级之后才开始渲染的,所以应该在省级select改变之后触发(onchange事件)

1,首先拿到用户选中的是第几个option;

通过selectDOM的selectedIndex可拿到对应索引值:

然后通过children获取option子元素列表,之后直接拿对应索引即可!

eg: this.children[this.selectedIndex], 因为是onChange事件,this指代selectDOM。

在操作之前,可以先把用户选中的省份数据存放在我们提前定义好的data对象中的province对象中:

data.province.code=this.children[this.selectedIndex].value
data.province.value=this.children[this.selectedIndex].text

即此时数据应为是:

在数据存完之后我们就可以着手渲染我们的市级select标签了。

1,首先不管市级select中有没有option标签,我们先给它清除一遍(因为每个省份对应的市都不一样,如果不清除则下面渲染的市级option列表中含有上一个省份的市级option列表

oCity.options.length=0

2,清除完成之后我们用省级行政代码(code)去获取对应的市级列表即可:

districts['省级行政代码'] 等于对应市级列表

districts[data.province.code] // 获取用户选中的省份的市级列表

eg:

拿到该省份的对应市级列表之后,我们只需要向渲染省级列表一样渲染市级列表即可:

第三步,渲染区级列表

第三步同第二步基本完全一样,我们完全可以让它两共用一个函数:

在市级列表的onChange事件触发之后执行,

存储用户选中的市的code以及市名。

用获取到的市级code去换对应的区级列表即可(因为不存在数组对象嵌套,所以同省级code换市列表一样);

districts[data.province.code] // 获取用户选中的市级的区级列表

在渲染区级之前先清空一直区级select下的option标签列表

eg:

代码同上基本一样,除了需要注意此时我们的存值的对应是我们事先定义好的市级对应city而不是province即可。

当区级select标签onChange事件触发时

区级的 onChange 事件触发,此时我们已经不需要再给渲染新的 select 标签了(因为不是四级联动,其实都一样啦,大不了再复制一层呗)。所以我们只需要把用户选中的结果存放在我们事先定义好的 data 中的 area 区级对象即可。

这样,当用户选择完毕之后,我们就会得到一个很清晰的对象了。

至此基本完工。

完善

初步渲染完成时应该给它下级一个默认值,这样如果用户不选了不至于获取到的数据不完整:

1,渲染完省级列表时默认给它一个选中的市,并存值。

使用element.dispatchEvent().。实现onChange事件初始化触发

参考资料:https://www.jianshu.com/p/5f9027722204

此时浏览器刚刷新数据为:

源码(不含省市区文件,省市区文件将开头):

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<title></title>
		<style type="text/css">
			select {
				height: 30px;
				margin-right: 10px;
			}
		</style>
	</head>
	<body>
		<!-- 省级列表 -->
		<select id="province"></select>
		<!-- 市级列表 -->
		<select id="city"></select>
		<!-- 区级列表 -->
		<select id="area"></select>
		
		<script type="module">
			// 引入省市区文件
			import districts from './js/districts.js'
			
			// 获取省市区selectDOM节点
			let oProvince=document.getElementById('province')
			let oCity=document.getElementById('city')
			let oArea=document.getElementById('area')
			
			// 存储用户选中的值
			let data={
				province: {},
				city: {},
				area: {}
			}
			
			// 渲染省级select标签
			let provinceList=districts[100000] 
			for (let province in provinceList) {
				oProvince.options.add( new Option(provinceList[province], province) )
			}
			
			
			// 当用户选中某个省份时触发
			oProvince.addEventListener('change', function() {
				// 先清空之前的市级<select>标签下的option列表
				oCity.options.length=0
				// 保存省级数据
				data.province.code=this.children[this.selectedIndex].value
				data.province.value=this.children[this.selectedIndex].text
				// 渲染市级列表
				for(let city in districts[data.province.code]) {
					oCity.options.add( new Option(districts[data.province.code][city], city) )
				}
			}, false)
			
			
			// 当用户选中某个市时触发
			oCity.addEventListener('change', function() {
				oArea.options.length=0
				// 保存市级数据
				data.city.code=this.children[this.selectedIndex].value
				data.city.value=this.children[this.selectedIndex].text
				// 渲染区级列表
				for(let city in districts[data.city.code]) {
					oArea.options.add( new Option(districts[data.city.code][city], city) )
				}
			}, false)
			
			// 当用户选中每个区时触发
			oArea.onchange=function () {
				// 保存区级数据
				data.area.code=this.children[this.selectedIndex].value
				data.area.value=this.children[this.selectedIndex].text
			}
			
			/**
			 * 渲染完成初始化
			 * @params dom selectDOM 
			 */
			function initSelect (dom) {
				let createE=document.createEvent('HTMLEvents')
				createE.initEvent("change", true, true)
				dom.dispatchEvent(createE)
			} 
			initSelect(oProvince)
			initSelect(oCity)
			initSelect(oArea)
			console.log(data)
		</script>
	</body>
</html>

至此基本完成啦。