React项目实战之租房app项目(五)顶部导航组件封装&CSS Modules解决样式覆盖&地图找房模块封装

前言

一、顶部导航组件封装

1.1 基础布局

实现步骤:

1、封装NavHeader组件实现城市选择,地图找房页面的复用
2、在components目录中创建组件 NavHeader/index.js
3、在该组件中封装 antd-mobile 组件库中的 NavBar组件

代码示例:
在src/components/NavHeader/index.js中添加如下代码:

import React from 'react';
import {
    
    NavBar} from 'antd-mobile'

export default class extends React.Component {
    
    
    render() {
    
    
        return (
            <NavBar className="navbar"
                    // 模式 默认值是 dark
                    mode="light"
                    // 左侧小图片
                    icon={
    
    <i className='iconfont icon-back' />}
                    // 左侧按钮的点击事件
                    onLeftClick={
    
    () => this.props.history.go(-1)}
                // 标题内容不定的,所以我们通过外界来传入
                >{
    
    this.props.children}</NavBar>
        )
    }
}

1.2 样式调整

在src/components下的NavHeader文件夹中创建 index.scss 文件
把之前城市列表写过的样式,复制到这个文件下

1.3 功能处理

注意:默认情况下,只有路由 Route 直接渲染的组件才能够获取到路由信息,如果需要在其他组件中获取到路由信息,可以通过 withRouter 高阶组件来获取

实现步骤:

1、从 react-router-dom 中导入 withRouter 高阶组件
2、使用 withRouter 高阶组件包装 NavHeader 组件
3、从 props 中就能获取history对象,调用history对象的 go() 方法就能实现返回上一页功能了
4、由于头部的左侧按钮不一定是返回上一个页面的功能,所以我们需要把左侧点击逻辑处理一下,改成需要通过父组件传递进来,如果说外界传递了,那么我们就直接使用外界的行为,如果没有传递,那么就用默认的行为

代码示例:
在src/components/NavHeader/index.js中修改如下代码:

import React from 'react';
import {
    
     NavBar } from 'antd-mobile'
import './index.scss'
import {
    
     withRouter } from 'react-router-dom'

class NavHeader extends React.Component {
    
    
    render() {
    
    
        let defaultHandler = () => {
    
    this.props.history.go(-1)}
        return (
            <NavBar
                className="navbar"
                // 模式 默认值是 dark
                mode="light"
                // 左侧小图片
                icon={
    
    <i className='iconfont icon-back' />}
                // 左侧按钮的点击事件
                onLeftClick={
    
    this.props.onLeftClick || defaultHandler}
            >{
    
    this.props.children}</NavBar>
        )
    }
}
// 通过withRouter 包装一层后,返回的还是一个组件
export default withRouter(NavHeader)

1.4 添加props校验

我们可以通过添加props校验,来提示使用者,应该怎样正确的传递props
使用步骤:

1、安装 yarn add prop-types
2、导入 PropTypes
3、给NavHeader组件的 children 和 onLeftClick添加props校验

代码示例:
在src/components/NavHeader/index.js中添加如下代码:

import PropTypes from 'prop-types'

NavHeader.propTypes = {
    
    
    children: PropTypes.string.isRequired,
    onLeftClick:PropTypes.func
}

1.5 在城市找房页面使用NavHeader组件

在src/pages/Citylist/index.js中添加如下代码:

// 导入 NavHeader 组件
import NavHeader from '../../components/NavHeader'
        
{
    
    /* 顶部导航栏 */}
<NavHeader>城市选择</NavHeader>

二、使用CSS Modules解决组件之间样式覆盖的问题

2.1 组件间样式覆盖问题概述

1、问题:CityList组件的样式,会影响Map组件的样式
2、原因:在配置路由的时候,CityList组件与Map组件都会被导入到路由中,组件被导入,相关的样式也会被导入进来,如果两个组件的样式名称相同,那么就会影响另外一个组件的样式
3、小结:默认情况下,只要导入了组件,不管组件有没有显示在页面中,组件的样式就会生效
4、解决方式:1、写不同的类名   2CSS IN JS
5CSS IN JS
CSS IN JS 是使用JavaScript 编写 CSS 的统称,用来解决CSS样式冲突,覆盖等问题;
CSS IN JS 的具体实现有50多种,比如:CSS Modules、styled-components等
推荐使用CSS Modules,因为React脚手架已经集成进来了,可以直接使用

2.2 CSS Modules

概念:

1CSS Modules 通过对CSS类名重命名,保证每一个类名的唯一性,从而避免样式冲突问题
2、实现方式:webpack的css-loader 插件
3、命名采用:BEM(Block块、Element元素、Modifier三部分组成)命名规范。比如: .list_item_active
4、在React脚手架中演化成:文件名、类名、hash(随机)三部分,只需要指定类名即可

使用方式:

1、创建名为[name].module.css 的样式文件(React脚手架中的约定,与普通CSS区分开)
      示例:
           在 CityList 组件中创建的样式文件名称:index.module.css

2、组件中导入样式文件(注意语法)
      示例:
           在 CityList 组件中导入样式文件: import styles from './index.module.css'

3、通过styles对象访问对象中的样式名来设置样式
      示例:
           className={
    
    styles.navBar}

2.3 使用CSS Modules修改 NavHeader 样式

使用步骤:

1、在NavHeader目录中创建 index.module.css 的样式文件
2、在样式文件中修改当前组件的样式
3、对于组件库中已经有的全局样式,需要使用:global() 来指定

代码示例:
在src/components/NavHeader/index.module.css中添加如下代码:

.navbar {
    
    
    color: #333;
    background-color: #f6f5f6;
}

.navbar :global(.am-navbar-title) {
    
    
    color: #333;
}

三、地图找房模块-根据定位展示当前城市

实现步骤:

1、获取当前定位城市
2、使用 地址解析器 解析当前城市坐标
3、调用 centerAndZoom() 方法在地图中展示当前城市,并设置缩放级别为11
4、在地图中添加比例尺和平移缩放控件

代码示例:
在src/pages/Map/index.js中添加如下代码:

// 解决脚手架中全局变量访问的问题
const BMap = window.BMap

export default class Map extends React.Component {
    
    
  componentDidMount() {
    
    
    this.initMap()
  }

  // 初始化地图
  initMap() {
    
    
    // 获取当前定位城市
    const {
    
     label, value } = JSON.parse(localStorage.getItem('hkzf_city'))

    // 初始化地图实例
    // 注意:在 react 脚手架中全局对象需要使用 window 来访问,否则,会造成 ESLint 校验错误
    const map = new BMap.Map('container')

    // 创建地址解析器实例
    const myGeo = new BMap.Geocoder()
    // 将地址解析结果显示在地图上,并调整地图视野
    myGeo.getPoint(
      label,
      point => {
    
    
        if (point) {
    
    
          //  初始化地图
          map.centerAndZoom(point, 11)

          // 添加常用控件
          map.addControl(new BMap.NavigationControl())
          map.addControl(new BMap.ScaleControl())
        }
      },
      label
    )
  }

  render() {
    
    
    return (
      <div className={
    
    styles.map}>
        {
    
    /* 顶部导航栏组件 */}
        <NavHeader>地图找房</NavHeader>
        {
    
    /* 地图容器元素 */}
        <div id="container" className={
    
    styles.container} />
      </div>
    )
  }
}

四、地图找房模块-房源信息在地图中展示

4.1 房源信息在地图中展示效果图

在这里插入图片描述

这些房源信息其实就是用文本覆盖物来实现的,所以我们先查看百度地图开发文档,学习如何创建文本覆盖物

4.2 创建文本覆盖物

创建步骤:

1、打开百度地图添加文字标签DEMO 
2、创建 Label 实例对象
3、调用 setStyle() 方法设置样式 
4、在 map 对象上调用 addOverlay() 方法,将文本覆盖物添加到地图中。

代码示例:
在src/pages/Map/index.js中添加如下代码:

  const opts = {
    position: point
  }

  const label = new BMap.Label('文本覆盖物', opts)

  // 设置样式
  label.setStyle({
    color: 'green'
  })

  // 添加覆盖物到地图中
  map.addOverlay(label)

4.3 绘制房源信息覆盖物

绘制步骤:

1、由于默认提供的本文覆盖物与我们效果不符合,所以我们需要进行重新的绘制
2、调用Label的setContent方法,传入html结构,修改HTML的内容样式
3、调用setStyle方法修改覆盖物样式
4、给覆盖物添加点击事件

代码示例:
在src/pages/Map/index.js中添加如下代码:

// 覆盖物样式
const labelStyle = {
  cursor: 'pointer',
  border: '0px solid rgb(255, 0, 0)',
  padding: '0px',
  whiteSpace: 'nowrap',
  fontSize: '12px',
  color: 'rgb(255, 255, 255)',
  textAlign: 'center'
}

 const opts = {
    position: point,
    offset: new BMap.Size(-35, -35)
  }

  // 说明:设置 setContent 后,第一个参数中设置的文本内容就失效了,因此,直接清空即可
  const label = new BMap.Label('', opts)

  // 设置房源覆盖物内容
  label.setContent(`
    <div class="${styles.bubble}">
      <p class="${styles.name}">浦东</p>
      <p>99套</p>
    </div>
  `)

  // 设置覆盖物样式
  label.setStyle(labelStyle)

  // 添加单击事件
  label.addEventListener('click', () => {
    console.log('房源覆盖物被点击了')
  })

  // 添加覆盖物到地图中
  map.addOverlay(label)

五、地图找房模块-业务逻辑实现

5.1 业务逻辑分析

地图找房模块需要实现以下功能:

1、获取房源数据,渲染覆盖物
2、点击覆盖物后放大地图,并获取数据,渲染下一级覆盖物
3、区、镇覆盖物点击后,清除现有的覆盖物,获取下一级数据,创建新的覆盖物
4、小区覆盖物点击后,不清除现有覆盖物,移动地图,展示该小区下的房源信息

5.2 获取所有区的信息并显示在地图中
实现步骤:

1、发送请求获取房源数据
2、遍历数据,创建覆盖物,给每一个覆盖物添加唯一标识
3、给覆盖物添加点击事件
4、在单击事件中,获取到当前单击项的唯一标识
5、放大地图(级别为13),调用clearOverlays()方法清除当前覆盖物

代码示例:
在src/pages/Map/index.js中添加如下代码:

// 请求接口,获取房源数据
let res = await axios.get(`http://localhost:8080/area/map?id=${
      
      value}`)
// 遍历房源信息,创建对应的覆盖物
res.data.body.map(item => {
    
    
    // 给每一条数据添加覆盖物
    // 得到返回的经纬度信息
    let {
    
     coord: {
    
     longitude, latitude }, label: areaName, count, value } = item
    // 创建覆盖物
    let label = new window.BMap.Label('', {
    
    
        position: new window.BMap.Point(longitude, latitude),
        offset: new window.BMap.Size(-35, -35)
    })
    // 设置覆盖物内容
    label.setContent(`<div class="${
      
      styles.bubble}">
    <p class="${
      
      styles.name}">${
      
      areaName}</p>
    <p>${
      
      count}套</p>
  </div>`)
    // 设置样式
    label.setStyle(labelStyle)
    // 添加点击事件
    label.addEventListener('click', function () {
    
    
        // 当点击了覆盖物,要以当前点击的覆盖物为中心来放大地图
        map.centerAndZoom(this.K.position, 13);
        // 解决清除覆盖物的时候,百度地图js报错问题
        setTimeout(function () {
    
    
            map.clearOverlays()
        }, 0)
    })
    // 给label添加唯一标识
    label.id = value
    // 添加到地图上
    map.addOverlay(label)
})

5.3 业务逻辑封装

经过分析业务逻辑我们发现,区,镇,小区要实现的业务逻辑是相似的,为了提高代码的复用性,可以对此进行一层封装
封装流程图如下图所示:
在这里插入图片描述

1、renderOverlays() 作为入口
* 接收区域id参数,获取该区域下的房源数据
* 获覆盖物类型以及下级地图缩放级别

2、createOverlays() 方法
* 根据传入的类型,调用对应方法,创建覆盖物,到底是创建区镇的覆盖物还是小区覆盖物

3、createCircle() 方法
* 根据传入的数据创建覆盖物,绑定事件(放大地图,清除覆盖物,渲染下一级房源数据)

4、createReact() 方法
* 根据传入的数据创建覆盖物,绑定事件(移动地图,渲染房源列表)

5.4 业务逻辑封装-renderOverlays 方法的封装

实现步骤:

1、接收区域 id 参数,获取该区域下的房源数据
2、获取房源类型以及下级地图缩放级别

代码示例:
在src/pages/Map/index.js中添加如下代码:

/**
* 根据id获取对应的房源信息
*/
async renderOverlays(id) {
    
    
    // 请求,拿到对应房源数据
    let res = await axios.get(`http://localhost:8080/area/map?id=${
      
      id}`)
    let data = res.data.body

    let {
    
    type,nextZoom} = this.getTypeAndZoom()

    // 遍历,调用createOverlays创建覆盖物
    data.map(item => {
    
    
        this.createOverlays(item,type,nextZoom)
    })
}

/**
* 获取对应要绘制的类型和缩放的比例
*/
  getTypeAndZoom() {
    
    
    // 调用地图的 getZoom() 方法,来获取当前缩放级别
    const zoom = this.map.getZoom()
    let nextZoom, type

    if (zoom >= 10 && zoom < 12) {
    
    
      // 区
      // 下一个缩放级别
      nextZoom = 13
      // circle 表示绘制圆形覆盖物(区、镇)
      type = 'circle'
    } else if (zoom >= 12 && zoom < 14) {
    
    
      // 镇
      nextZoom = 15
      type = 'circle'
    } else if (zoom >= 14 && zoom < 16) {
    
    
      // 小区
      type = 'rect'
    }

    return {
    
    
      nextZoom,
      type
    }
  }
最后,在初始化地图方法initMap中调用:

  // 初始化地图
  initMap() {
    
    
    // 获取当前定位城市
    const {
    
     label, value } = JSON.parse(localStorage.getItem('hkzf_city'))
    // 初始化地图实例
    const map = new BMap.Map('container')
    // 作用:能够在其他方法中通过 this 来获取到地图对象
    this.map = map
    // 创建地址解析器实例
    const myGeo = new BMap.Geocoder()
    // 将地址解析结果显示在地图上,并调整地图视野
    myGeo.getPoint(
      label,
      async point => {
    
    
        if (point) {
    
    
          //  初始化地图
          map.centerAndZoom(point, 11)
          // 添加常用控件
          map.addControl(new BMap.NavigationControl())
          map.addControl(new BMap.ScaleControl())

          // 调用 renderOverlays 方法
          this.renderOverlays(value)
        }
      },
      label
    )
  }

5.5 业务逻辑封装-createOverlays 方法的封装

实现步骤:
1、这个方法主要是逻辑判断,然后根据不同条件调用不同渲染的方法

代码示例:
在src/pages/Map/index.js中添加如下代码:

  // 创建覆盖物
  createOverlays(data, zoom, type) {
    
    
    const {
    
    
      coord: {
    
     longitude, latitude },
      label: areaName,
      count,
      value
    } = data

    // 创建坐标对象
    const areaPoint = new BMap.Point(longitude, latitude)

    if (type === 'circle') {
    
    
      // 区或镇
      this.createCircle(areaPoint, areaName, count, value, zoom)
    } else {
    
    
      // 小区
      this.createRect(areaPoint, areaName, count, value)
    }
  }

5.6 业务逻辑封装-createCircle 方法的封装

实现步骤:
1、复用之前的创建覆盖物的代码逻辑
2、在覆盖物的单击事件中,调用 renderOverlays(id)方法,重新渲染该区域的房屋数据

代码示例:
在src/pages/Map/index.js中添加如下代码:

// 创建区、镇覆盖物
  createCircle(point, name, count, id, zoom) {
    
    
    // 创建覆盖物
    const label = new BMap.Label('', {
    
    
      position: point,
      offset: new BMap.Size(-35, -35)
    })

    // 给 label 对象添加一个唯一标识
    label.id = id

    // 设置房源覆盖物内容
    label.setContent(`
      <div class="${
      
      styles.bubble}">
        <p class="${
      
      styles.name}">${
      
      name}</p>
        <p>${
      
      count}套</p>
      </div>
    `)

    // 设置样式
    label.setStyle(labelStyle)

    // 添加单击事件
    label.addEventListener('click', () => {
    
    
      // 调用 renderOverlays 方法,获取该区域下的房源数据
      this.renderOverlays(id)

      // 放大地图,以当前点击的覆盖物为中心放大地图
      this.map.centerAndZoom(point, zoom)

      // 解决清除覆盖物时,百度地图API的JS文件自身报错的问题
      setTimeout(() => {
    
    
        // 清除当前覆盖物信息
        this.map.clearOverlays()
      }, 0)
    })

    // 添加覆盖物到地图中
    this.map.addOverlay(label)
  }

5.7 业务逻辑封装-createRect 方法的封装

实现步骤:
1、创建Label、设置样式、设置html内容,绑定事件

代码示例:
在src/pages/Map/index.js中添加如下代码:

  // 创建小区覆盖物
  createRect(point, name, count, id) {
    
    
    // 创建覆盖物
    const label = new BMap.Label('', {
    
    
      position: point,
      offset: new BMap.Size(-50, -28)
    })

    // 给 label 对象添加一个唯一标识
    label.id = id

    // 设置房源覆盖物内容
    label.setContent(`
      <div class="${
      
      styles.rect}">
        <span class="${
      
      styles.housename}">${
      
      name}</span>
        <span class="${
      
      styles.housenum}">${
      
      count}套</span>
        <i class="${
      
      styles.arrow}"></i>
      </div>
    `)

    // 设置样式
    label.setStyle(labelStyle)

    // 添加单击事件
    label.addEventListener('click', () => {
    
    
      console.log('小区被点击了')
    })

    // 添加覆盖物到地图中
    this.map.addOverlay(label)
  }

现在只是封装了创建小区覆盖物的方法,点击获取小区下的所有房源数据和渲染房源列表还未实现

总结

猜你喜欢

转载自blog.csdn.net/qq_40652101/article/details/127958359