Skip to content

进阶使用

Tuya Robot Map 的进阶使用,包含了我们认为的最佳实践。

选择房间

用于选区清扫、定时预约、房间分割等业务场景。

TIP

选择房间是完全受控的,取决于你传入的 enableRoomSelectionselectRoomIds。点击房间的时候组件内部并不会做任何事,只是抛出 onClickRoom 事件。

tsx
import React from 'react'
import { RobotMap } from '@ray-js/robot-map'

const MapPage = () => {
  const [mapApi, setMapApi] = useState<MapApi | null>(null)
  const [enableRoomSelection, setEnableRoomSelection] = useState(true)
  const [selectRoomIds, setSelectRoomIds] = useState<number[]>([])

  // 地图准备就绪时触发
  const handleMapReady = () => {
    setMapApi(mapApi)
  }

  // 点击房间时触发
  const handleClickRoom = (room: RoomProperty) => {
    if (selectRoomIds.includes(room.id)) {
      setSelectRoomIds(selectRoomIds.filter((id) => id !== room.id))
    } else {
      setSelectRoomIds([...selectRoomIds, room.id])
    }
  }

  return (
    <RobotMap
      config={{
        room: {
          colors: {
            /**
             * 循环使用的颜色数组
             * enableRoomSelection 为 true 时代表选中房间的颜色
             * enableRoomSelection 为 false 时代表房间的默认颜色
             */
            active: YOUR_ACTIVE_COLORS,
            /**
             * 循环使用的颜色数组
             * enableRoomSelection 为 true 时代表未选中房间的颜色
             */
            inactive: YOUR_INACTIVE_COLORS,
          },
        },
      }}
      runtime={{
        enableRoomSelection,
        selectRoomIds,
        // 选中的房间上显示✅标记
        roomSelectionMode: 'checkmark',
      }}
      onMapReady={handleMapReady}
      onClickRoom={handleClickRoom}
    />
  )
}

房间分割

通过分割线将一个房间分割成多个房间。

TIP

我们建议搭配选择房间的功能一起使用。

tsx
import React from 'react'
import { RobotMap } from '@ray-js/robot-map'

const MapPage = () => {
  const [mapApi, setMapApi] = useState<MapApi | null>(null)
  const [enableRoomSelection, setEnableRoomSelection] = useState(true)
  const [selectRoomIds, setSelectRoomIds] = useState<number[]>([])
  const [dividingRoomId, setDividingRoomId] = useState<number | null>(null)

  // 地图准备就绪时触发
  const handleMapReady = () => {
    setMapApi(mapApi)
  }

  // 点击房间时触发
  const handleClickRoom = (room: RoomProperty) => {
    // 该房间进入分割状态,会出现分割线
    setDividingRoomId(room.id)
    // (建议) 选中该房间让它处于高亮状态
    setSelectRoomIds([room.id])
  }

  const handleSaveDividing = async () => {
    // 获取分割线数据
    const points = await mapApi?.getEffectiveDividerPoints()

    yourSaveDividingFunction(points)
  }

  return (
    <RobotMap
      config={{
        divider: {
          lineColor: '#ff4444',
          dashLineWidth: 2,
        },
      }}
      runtime={{
        dividingRoomId,
        enableRoomSelection,
        selectRoomIds,
      }}
      onMapReady={handleMapReady}
      onClickRoom={handleClickRoom}
    />
  )
}

地图控制元素

包含虚拟墙、禁扫区域、禁拖区域、清扫划区、定点框等与扫地机设备进行交互的元素。

TIP

地图控制元素是纯受控的,取决于你传入的prop数据

WARNING

每个地图控制元素都需要有唯一的id,你可以自行定义或通过 nanoid 等库生成。重复的id可能会导致地图控制元素无法正常工作。

虚拟墙

虚拟墙是由两个端点坐标构成的线段。

tsx
import React from 'react'
import { RobotMap, VirtualWallParam } from '@ray-js/robot-map'

const MapPage = () => {
  // 将设备上报的虚拟墙数据绘制到地图上
  const [virtualWalls, setVirtualWalls] = useState<VirtualWallParam[]>([
    {
      id: 'wall1',
      points: [
        { x: 0, y: 0 },
        { x: 20, y: 20 },
      ],
    },
    {
      id: 'wall2',
      points: [
        { x: -10, y: 0 },
        { x: -10, y: 10 },
      ],
    },
  ])

  const [editingVirtualWallIds, setEditingVirtualWallIds] = useState<string[]>(
    [],
  )

  // 点击虚拟墙的删除按钮时触发
  const handleRemoveVirtualWall = (id: string) => {
    setVirtualWalls(virtualWalls.filter((wall) => wall.id !== id))
  }

  // 手势操作虚拟墙后触发
  const handleUpdateVirtualWall = (wall: VirtualWallParam) => {
    // 强烈建议在onUpdate回调中更新业务侧的数据
    setVirtualWalls(
      virtualWalls.map((item) => (item.id === wall.id ? wall : item)),
    )
  }

  // 点击虚拟墙时触发
  const handleClickVirtualWall = (wall: VirtualWallParam) => {
    // 切换到编辑状态
    setEditingVirtualWallIds([wall.id])
  }

  const handleSaveVirtualWall = () => {
    // 保存下发虚拟墙数据
    yourSaveVirtualWallFunction(virtualWalls)
  }

  return (
    <RobotMap
      config={{
        controls: {
          virtualWall: {
            lineColor: '#ff4444',
            lineWidth: 2,
            // ... 其他配置
          },
        },
      }}
      runtime={{
        editingVirtualWallIds,
      }}
      virtualWalls={virtualWalls}
      onRemoveVirtualWall={handleRemoveVirtualWall}
      onUpdateVirtualWall={handleUpdateVirtualWall}
      onClickVirtualWall={handleClickVirtualWall}
    />
  )
}

禁扫区域/禁拖区域/清扫划区

禁扫区域、禁拖区域和清扫划区都是由四个顶点坐标构成的矩形区域,各自具有独立的配置、参数和方法。

tsx
// 以禁扫区域为例
import React from 'react'
import { RobotMap, ZoneParam } from '@ray-js/robot-map'

const MapPage = () => {
  const [forbiddenSweepZones, setForbiddenSweepZones] = useState<ZoneParam[]>([
    {
      id: 'forbiddenSweepZone',
      points: [
        { x: 0, y: 0 },
        { x: 20, y: 0 },
        { x: 20, y: 20 },
        { x: 0, y: 20 },
      ],
    },
  ])
  const [editingForbiddenSweepZoneIds, setEditingForbiddenSweepZoneIds] =
    useState<string[]>([])

  // 点击禁扫区域的删除按钮时触发
  const handleRemoveForbiddenSweepZone = (id: string) => {
    setForbiddenSweepZones(forbiddenSweepZones.filter((zone) => zone.id !== id))
  }

  // 手势操作禁扫区域后触发
  const handleUpdateForbiddenSweepZone = (zone: ZoneParam) => {
    setForbiddenSweepZones(
      forbiddenSweepZones.map((item) => (item.id === zone.id ? zone : item)),
    )
  }

  // 点击禁扫区域时触发
  const handleClickForbiddenSweepZone = (zone: ZoneParam) => {
    // 切换到编辑状态
    setEditingForbiddenSweepZoneIds([zone.id])
  }

  const handleSaveForbiddenSweepZone = () => {
    // 保存下发禁扫区域数据
    yourSaveForbiddenSweepZoneFunction(forbiddenSweepZones)
  }

  return (
    <RobotMap
      config={{
        controls: {
          forbiddenSweepZone: {
            strokeColor: '#ff4444',
            strokeWidth: 2,
            fillColor: 'rgba(255, 68, 68, 0.1)',
            // ... 其他配置
          },
        },
      }}
      runtime={{
        editingForbiddenSweepZoneIds,
      }}
      forbiddenSweepZones={forbiddenSweepZones}
      onRemoveForbiddenSweepZone={handleRemoveForbiddenSweepZone}
      onUpdateForbiddenSweepZone={handleUpdateForbiddenSweepZone}
      onClickForbiddenSweepZone={handleClickForbiddenSweepZone}
    />
  )
}

定点框

定点框是由一个中心点坐标和固定尺寸参数构成的矩形区域。

tsx
import React from 'react'
import { RobotMap, SpotParam } from '@ray-js/robot-map'

const MapPage = () => {
  const [spots, setSpots] = useState<SpotParam[]>([
    {
      id: 'spot',
      point: { x: 0, y: 0 },
    },
  ])
  const [editingSpotIds, setEditingSpotIds] = useState<string[]>([])

  // 手势操作定点框后触发
  const handleUpdateSpot = (spot: SpotParam) => {
    setSpots(spots.map((item) => (item.id === spot.id ? spot : item)))
  }

  // 点击定点框时触发
  const handleClickSpot = (spot: SpotParam) => {
    // 切换到编辑状态
    setEditingSpotIds([spot.id])
  }

  return (
    <RobotMap
      config={{
        controls: {
          spot: {
            // 这里的单位是米
            size: 1,
            strokeColor: '#5d68fe',
            strokeWidth: 2,
            fillColor: 'rgba(93, 104, 254, 0.1)',
          },
        },
      }}
      runtime={{
        editingSpotIds,
      }}
      spots={spots}
      onUpdateSpot={handleUpdateSpot}
      onClickSpot={handleClickSpot}
    />
  )
}

TIP

  • 虽然在设计上是纯受控的,但地图控制元素在手势操作时会先在内部实时进行更新,并在手势结束后通过 onUpdate 回调抛出最新的数据。

  • 为保证一致性,我们强烈建议始终在 onUpdate 回调更新业务侧的数据。这个更新不会产生额外的渲染,你无需担心。

新增地图控制元素

借助受控的设计,新增一个地图控制元素非常简单,以虚拟墙为例:

TIP

新增其他地图控制元素的方式也是类似的,重点在于如何维护好数据和运行时状态。

tsx
import React from 'react'
import { RobotMap } from '@ray-js/robot-map'

const MapPage = () => {
  const [virtualWalls, setVirtualWalls] = useState<VirtualWallParam[]>([])

  return (
    <View>
      <RobotMap virtualWalls={virtualWalls} />
      <Button
        onClick={() => {
          setVirtualWalls([
            ...virtualWalls,
            {
              id: 'newVirtualWall',
              points: [
                { x: 0, y: 0 },
                { x: 10, y: 10 },
              ],
            },
          ])
        }}
      >
        新增虚拟墙
      </Button>
    </View>
  )
}

但通常你会基于视口中心来新增地图控制元素,我们提供了对应的API:

tsx
import React from 'react'
import { RobotMap } from '@ray-js/robot-map'

const MapPage = () => {
  const [virtualWalls, setVirtualWalls] = useState<VirtualWallParam[]>([])
  const [editingVirtualWallIds, setEditingVirtualWallIds] = useState<string[]>(
    [],
  )

  const handleAddVirtualWall = () => {
    // 获取基于视口中心的虚拟墙端点坐标
    const wallPoints = mapApi.getWallPointsByViewportCenter({
      // 这里的单位是米
      width: 2,
      direction: 'horizontal',
    })
    setVirtualWalls([
      ...virtualWalls,
      { id: 'newVirtualWall', points: wallPoints },
    ])
    // 通常新增的元素会被立刻设置为编辑状态
    setEditingVirtualWallIds(['newVirtualWall'])
  }

  return (
    <View>
      <RobotMap
        runtime={{
          editingVirtualWallIds,
        }}
        virtualWalls={virtualWalls}
      />
      <Button onClick={handleAddVirtualWall}>新增虚拟墙</Button>
    </View>
  )
}

房间信息

数据处理

房间信息的展示完全受控于你传入的 roomProperties 数据。

ts
// 房间属性类型
export type RoomProperty = {
  /** 房间唯一标识符 */
  id: number
  /** 房间名称 */
  name: string
  /** 清洁次数 */
  cleanTimes: number
  /** 清洁顺序 */
  order: number
  /** 地面类型 */
  floorType: number
  /** 拖地模式 */
  yMop: number
  /** 吸力 */
  suction?: number | null
  /** 水量 */
  cistern?: number | null
  /** 清洁模式 */
  cleanMode?: number | null
}

对于不同的扫地机协议 roomProperties 的获取方式不同。

  • 点阵协议:可以通过 decodeRoomProperties 方法解析原始地图数据获取
ts
import { decodeRoomProperties } from '@ray-js/robot-map'

const mapData = 'your_map_data_string'
const roomProperties = decodeRoomProperties(mapData)
ts
import { useRoomProperty } from '@ray-js/robot-data-stream'
const { devId } = useDevice((device) => device.devInfo)
const { requestRoomProperty } = useRoomProperty(devId)

requestRoomProperty()
  .then((response) => {
    console.log('房间属性数据:', response)

    // 这里需要将响应数据转换为 `RoomProperty[]` 类型作为 `roomProperties`
  })
  .catch((error) => {
    console.error('请求失败:', error)
  })

设置清扫顺序

房间清扫顺序的显示完全受控于 roomProperties 中的 order 字段。借助这个设计,你可以灵活地在业务侧实现设置清扫顺序的功能。

tsx
import React, { useMemo, useState } from 'react'
import { RobotMap, RoomData, RoomProperty } from '@ray-js/robot-map'

const MapPage = () => {
  const roomProperties = YOUR_ROOM_PROPERTIES
  // 临时的清扫顺序状态
  const [tempCleaningOrder, setTempCleaningOrder] = useState<
    Record<number, number | null>
  >({})

  // 合并原始数据和临时的清扫顺序状态
  const finalRoomProperties = useMemo(() => {
    return roomProperties.map((room) => ({
      ...room,
      order: tempCleaningOrder[room.id] ?? room.order ?? 0,
    }))
  }, [roomProperties, tempCleaningOrder])

  // (可选) 有顺序的房间设置为选中状态
  const selectRoomIds = useMemo(() => {
    return finalRoomProperties
      .filter((room) => room.order > 0)
      .map((room) => room.id)
  }, [finalRoomProperties])

  const handleClickRoom = (room: RoomData) => {
    const currentOrder =
      finalRoomProperties.find((r) => r.id === room.id)?.order || 0

    setTempCleaningOrder((prev) => {
      if (currentOrder > 0) {
        // 取消顺序,其他房间顺序递减
        const updates = { ...prev, [room.id]: null }
        finalRoomProperties.forEach((r) => {
          if (r.order > currentOrder) {
            const originalOrder =
              roomProperties.find((orig) => orig.id === r.id)?.order || 0
            updates[r.id] = (prev[r.id] ?? originalOrder) - 1
          }
        })
        return updates
      }

      // 设置新顺序
      const maxOrder = Math.max(0, ...finalRoomProperties.map((r) => r.order))
      return { ...prev, [room.id]: maxOrder + 1 }
    })
  }

  return (
    <RobotMap
      runtime={{
        enableRoomSelection: true,
        roomSelectionMode: 'order',
        selectRoomIds,
        showRoomOrder: true,
      }}
      // 传入合并后的房间数据
      roomProperties={finalRoomProperties}
      onClickRoom={handleClickRoom}
    />
  )
}

如果你的产品偏好从头开始设置,可以这样初始化 tempCleaningOrder

tsx
// 初始返回一个所有房间order为0的对象即可
const [tempCleaningOrder, setTempCleaningOrder] = useState<
  Record<number, number | null>
>(() =>
  roomProperties.reduce((acc, room) => {
    acc[room.id] = 0
    return acc
  }, {}),
)

TIP

这体现了 Tuya Robot Map 纯受控设计的优势,你可以自由主导地图的状态。

检测物体

将扫地机识别到的物体显示在地图上。

tsx
import React from 'react'
import { RobotMap } from '@ray-js/robot-map'

const MapPage = () => {
  const [detectedObjects, setDetectedObjects] = useState<DetectedObjectParam[]>(
    [
      {
        id: 'detectedObject',
        x: 0,
        y: 0,
        src: 'xxx',
      },
    ],
  )

  const handleClickDetectedObject = (object: DetectedObjectParam) => {
    console.log('点击了检测物体:', object.id)
    console.log('物体位置:', object.x, object.y)
    console.log('物体图标:', object.src)
  }

  return (
    <RobotMap
      config={{
        detectedObject: {
          height: 24,
          width: 24,
          // 是否可点击
          interactive: true,
        },
      }}
      detectedObjects={detectedObjects}
      onClickDetectedObject={handleClickDetectedObject}
    />
  )
}

自定义元素

你可以在地图上显示自定义的元素,它们完全受控于 customElements 中的数据。

目前支持自定义的元素有:

  • 图片
  • GIF
  • HTML
tsx
import React from 'react'
import { RobotMap } from '@ray-js/robot-map'

const MapPage = () => {
  const handleClickCustomElement = (element: CustomElementParam) => {
    console.log('点击了自定义元素:', element.id)
    console.log('元素类型:', element.type)
    console.log('自定义数据:', element.customData)
  }

  return (
    <RobotMap
      customElements={[
        {
          id: 'customImage',
          type: 'image',
          src: 'xxx',
          x: 30,
          y: 30,
          width: 32,
          height: 32,
          interactive: true,
          customData: {
            value: 'xxx',
          },
        },
        {
          id: 'customGif',
          type: 'gif',
          src: 'xxx',
          x: -60,
          y: -60,
          width: 32,
          height: 32,
        },
        {
          id: 'customHtml',
          type: 'html',
          htmlContent: 'xxx',
          x: 0,
          y: 0,
        },
      ]}
      onClickCustomElement={handleClickCustomElement}
      // ... 其他属性
    />
  )
}

截图