Appearance
进阶使用
Tuya Robot Map 的进阶使用,包含了我们认为的最佳实践。
自适应缩放
自适应缩放是地图 SDK 的核心功能之一,它会自动调整地图的缩放比例和位置,确保地图能够以合适的大小居中显示在视口中。
什么是 scale?
scale 表示地图的缩放比例。scale = 1 表示地图按照真实像素尺寸显示。
例如,如果你的地图数据是 300×300 的尺寸:
scale = 1时,地图会用 300×300 像素显示scale = 2时,地图会放大到 600×600 像素scale = 0.5时,地图会缩小到 150×150 像素
自适应缩放的触发时机
自适应缩放会在以下情况自动触发:
- 地图首次绘制:当地图第一次加载时
- 地图 ID 变化:切换到不同的地图时
- 地图状态变化:地图的
status字段变化时(例如从建图中变为建图完成) - 地图原点变化:地图的
origin坐标变化超过阈值时 - 地图尺寸变化:地图的
size(宽度或高度) 变化超过阈值时 - 地图旋转:通过
runtime.mapRotation旋转地图后
自适应缩放的计算逻辑
当触发自适应缩放时,SDK 会按照以下步骤计算最终的缩放比例:
1. 计算初始缩放比例
SDK 会根据 map.autoPaddingHorizontalPercent 和 map.autoPaddingVerticalPercent 计算出一个初始的缩放比例,确保地图能够完整显示并保留指定的边距。
例如,如果视口宽度是 375px,autoPaddingHorizontalPercent 是 0.05(默认值),那么:
- 水平方向的可用空间 = 375 × (1 - 0.05 × 2) = 337.5px
- 如果地图实际宽度是 300px,则计算出的 scale = 337.5 / 300 ≈ 1.125
- 如果地图实际宽度是 600px,则计算出的 scale = 337.5 / 600 ≈ 0.5625
2. 应用缩放比例限制
计算出的初始缩放比例会受到 interaction.fitMinScale 和 interaction.fitMaxScale 的限制:
最终 scale = Math.max(fitMinScale, Math.min(初始 scale, fitMaxScale))常见问题与解决方案
问题 1:超大地图显示不全
现象:地图很大,但只能看到部分内容,无法看到完整地图。
原因:当地图尺寸超过视口时,SDK 计算出的缩放比例可能小于 1(需要缩小地图)。但是 fitMinScale 的默认值是 1,会限制地图不能缩小到小于原始尺寸,导致地图无法完整显示。
解决方案:将 fitMinScale 设置为小于 1 的值,允许地图缩小。
tsx
<RobotMap
config={{
interaction: {
// 允许地图缩小到原始尺寸的 50%
fitMinScale: 0.5,
},
}}
/>问题 2:超小地图被放大得过大
现象:地图很小,显示时被过度放大,体验不佳。
原因:当地图尺寸很小时,SDK 计算出的缩放比例可能很大。虽然 fitMaxScale 默认值是 4,但对于某些超小地图来说可能还是太大了。
解决方案:减小 fitMaxScale 的值,限制最大放大倍数。
tsx
<RobotMap
config={{
interaction: {
// 最多放大到原始尺寸的 2 倍
fitMaxScale: 2,
},
}}
/>相关配置项
以下配置项共同控制自适应缩放的行为:
| 配置项 | 默认值 | 说明 |
|---|---|---|
interaction.fitMinScale | 1 | 自适应缩放时的最小比例 |
interaction.fitMaxScale | 4 | 自适应缩放时的最大比例 |
map.autoPaddingHorizontalPercent | 0.05 | 水平方向保留的最小边距比例 |
map.autoPaddingVerticalPercent | 0.05 | 垂直方向保留的最小边距比例 |
map.originChangeAutoFitThreshold | 2 | 地图原点变化触发自适应的阈值 |
map.sizeChangeAutoFitThreshold | 10 | 地图尺寸变化触发自适应的阈值 |
手动触发自适应缩放
除了自动触发,你也可以通过 API 手动触发自适应缩放:
tsx
import React from 'react'
import { RobotMap } from '@ray-js/robot-map'
const MapPage = () => {
const [mapApi, setMapApi] = useState<MapApi | null>(null)
const handleMapReady = (api: MapApi) => {
setMapApi(api)
}
const handleResetView = () => {
// 手动触发自适应缩放
mapApi?.resetPanZoom()
}
return (
<>
<RobotMap onMapReady={handleMapReady} />
<Button onClick={handleResetView}>重置视图</Button>
</>
)
}房间智能配色
地图 SDK 会自动为房间分配颜色,确保相邻的房间始终使用不同的颜色,让地图更清晰易读。
核心特性
- 自动避免冲突:相邻的房间永远不会使用相同的颜色
- 灵活的配色策略:可以选择让某些颜色更突出,或让所有颜色均匀分布
- 稳定的配色结果:地图更新时颜色分配保持一致,避免用户困惑
TIP
根据四色定理(Four Color Theorem),理论上 4 种颜色就足以为任何平面地图着色。因此,建议至少提供 4 种颜色。
颜色分配策略
SDK 提供两种颜色分配策略,可通过 room.colors.strategy 配置:
priority(加权优先)
优先使用数组中靠前的颜色,但会保持一定的多样性,不会让某个颜色过度集中。
适用场景:
- 你的颜色数组是精心设计的,希望主要使用前几种颜色
- 产品设计中有主题色,希望这些颜色出现的频率更高
tsx
<RobotMap
config={{
room: {
colors: {
strategy: 'priority', // 默认值
active: ['#a8c8f5', '#9de5c7', '#d4b9f7', '#ffd399'],
// 蓝色会被最多使用,其次是绿色、紫色、橙色
},
},
}}
/>balanced(均匀分配)
让所有颜色的使用频率尽可能接近,呈现均衡的色彩分布。
适用场景:
- 颜色数组中的所有颜色都同等重要
- 希望地图整体看起来色彩更加丰富多样
- 不希望某种颜色过度出现
tsx
<RobotMap
config={{
room: {
colors: {
strategy: 'balanced',
active: ['#a8c8f5', '#9de5c7', '#d4b9f7', '#ffd399'],
// 4 种颜色会被尽可能均匀地使用
},
},
}}
/>房间排序方式
在进行颜色分配前,SDK 会先对房间进行排序。排序方式可通过 room.colors.sortBy 配置:
index(按 ID 排序)
特点:按房间 ID 从小到大排序,颜色分配结果稳定。
适用场景:
- 希望颜色分配结果在地图更新时保持稳定
- 房间 ID 本身具有业务意义(如按清扫顺序编号)
tsx
<RobotMap
config={{
room: {
colors: {
sortBy: 'index', // 默认值
},
},
}}
/>TIP
这是推荐的默认选项,可以确保即使机器在清扫过程中房间面积略有变化,颜色分配也不会突然改变,避免用户困惑。
area(按面积排序)
特点:按房间面积从大到小排序,大房间会优先获得数组中靠前的颜色。
适用场景:
- 希望大房间使用主题色或更醒目的颜色
- 配合
priority策略使用,让大房间更突出
tsx
<RobotMap
config={{
room: {
colors: {
sortBy: 'area',
strategy: 'priority',
active: ['#a8c8f5', '#9de5c7', '#d4b9f7', '#ffd399'],
// 面积最大的房间会优先使用蓝色
},
},
}}
/>注意
使用 area 排序时,如果机器在清扫过程中房间面积发生变化(例如从建图中到建图完成),可能导致房间颜色突然改变,建议谨慎使用。
配色建议
颜色数量
建议至少提供 4 种颜色。根据四色定理,4 种颜色在理论上足以为任何平面地图着色。
tsx
<RobotMap
config={{
room: {
colors: {
// ✅ 推荐:至少 4 种颜色
active: ['#a8c8f5', '#9de5c7', '#d4b9f7', '#ffd399'],
},
},
}}
/>什么时候需要更多颜色?
- 如果你的地图中有特别复杂的相邻关系(某个房间与 4 个以上的房间相邻)
- 使用
balanced策略希望获得更丰富的视觉效果 - 你可以尝试提供 5-6 种颜色,但通常 4 种已经足够
颜色数组一致性
强烈建议所有与房间相关的颜色数组保持相同长度:
tsx
<RobotMap
config={{
room: {
colors: {
// ✅ 推荐:所有数组长度一致
active: ['#a8c8f5', '#9de5c7', '#d4b9f7', '#ffd399'],
inactive: ['#d6e7fc', '#d1f4e5', '#ece0fb', '#fff0d9'],
name: ['#2563b8', '#26966b', '#7c3fb8', '#d97706'],
propertyTheme: ['#2563b8', '#26966b', '#7c3fb8', '#d97706'],
selectionIndicatorBackground: [
'#2563b8',
'#26966b',
'#7c3fb8',
'#d97706',
],
},
},
}}
/>这样可以确保房间的所有视觉元素(填充色、名称、属性图标等)都使用一致的配色主题。
颜色对比度
选择颜色时要考虑:
- 相邻房间的区分度:相邻房间的颜色应该有足够的对比,便于用户区分
- 文字可读性:房间名称、属性图标等文字元素的颜色要与背景色有足够对比
- 无障碍访问:考虑色盲用户,避免仅依靠红绿色进行区分
相关配置项
以下配置项与房间智能配色相关:
| 配置项 | 默认值 | 说明 |
|---|---|---|
room.colors.strategy | 'balanced' | 颜色分配策略(priority 或 balanced) |
room.colors.sortBy | 'index' | 房间排序方式(index 或 area) |
room.colors.active | 4 种颜色 | 房间激活状态的颜色数组 |
room.colors.inactive | 4 种颜色 | 房间非激活状态的颜色数组 |
room.colors.name | 4 种颜色 | 房间名称标签的颜色数组 |
room.colors.propertyTheme | 4 种颜色 | 房间属性图标主题颜色数组 |
map.adjacencyThreshold | 3 | 房间相邻判定阈值(单位:像素) |
选择房间
用于选区清扫、定时预约、房间分割等业务场景。
TIP
选择房间是完全受控的,取决于你传入的 enableRoomSelection 和 selectRoomIds。点击房间的时候组件内部并不会做任何事,只是抛出 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: {
/**
* 房间颜色数组(建议至少 4 种颜色)
* 使用四色定理算法自动分配,确保相邻房间颜色不同
* enableRoomSelection 为 true 时代表选中房间的颜色
* enableRoomSelection 为 false 时代表房间的默认颜色
*/
active: YOUR_ACTIVE_COLORS,
/**
* 非激活状态的房间颜色数组(建议与 active 长度一致)
* enableRoomSelection 为 true 时代表未选中房间的颜色
*/
inactive: YOUR_INACTIVE_COLORS,
},
// 选择指示器配置
selectionIndicator: {
/**
* 描边颜色
* - 设置为固定颜色值如 '#ffffff' 时,所有选择指示器的描边和尾部箭头都使用该颜色
* - 设置为 'auto' 时,描边和尾部箭头颜色将自动跟随 selectionIndicatorBackground 的主题色
*/
strokeColor: '#ffffff', // 或 'auto'
},
},
}}
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>(-1)
// 地图准备就绪时触发
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
/** 自定义属性 */
customData?: Record<string, any>
}对于不同的扫地机协议 roomProperties 的获取方式不同。
- 点阵协议:可以通过
decodeRoomProperties方法解析原始地图数据获取
ts
import { decodeRoomProperties } from '@ray-js/robot-map'
const mapData = 'your_map_data_string'
const roomProperties = decodeRoomProperties(mapData)- 结构化协议:使用
requestRoomProperty来获取
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)
})自定义属性
如果需要在房间气泡中显示自定义属性,可以这样:
tsx
import React from 'react'
import { RobotMap } from '@ray-js/robot-map'
const MapPage = () => {
const roomProperties: RoomProperty[] = [
{
/** 其他房间属性 */
customData: {
customProperty1: 1,
},
},
{
/** 其他房间属性 */
customData: {
customProperty2: 2,
},
},
]
return (
<RobotMap
config={{
room: {
property: {
displayOrders: [
'cleanMode',
'suction',
'cistern',
'cleanTimes',
// 自定义属性的字段,需要和customData里的字段一致 (自定义属性目前仅支持数值型字段)
'customProperty1',
],
},
customAssets: {
// 自定义属性的图标资源,其索引和customData.customProperty1的值对应
customProperty1: [
'your_custom_property_asset_url_0',
'your_custom_property_asset_url_1',
'your_custom_property_asset_url_2',
'your_custom_property_asset_url_3',
],
},
},
}}
roomProperties={roomProperties}
/>
)
}如果当前使用的地图数据是点阵协议的,一般通过 decodeRoomProperties 方法解析地图数据获取 roomProperties 数据。
如果想通过协议里的预留字段来添加自定义属性,可以这样:
ts
import { decodeRoomProperties } from '@ray-js/robot-map'
const mapData = 'your_map_data_string'
const handleReservedStr = (reservedStr: string) => {
// reservedStr 为协议里的字节型预留字段
// 解析预留字段,返回自定义属性
return {
customData: yourDecodeCustomDataFunction(reservedStr),
}
}
// 通过handleReservedStr函数解析得到的对象会被合并到roomProperties中
const roomProperties = decodeRoomProperties(mapData, handleReservedStr)设置清扫顺序
房间清扫顺序的显示完全受控于 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>
>({})
// 合并原始数据和临时的清扫顺序状态
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]: 0 }
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>
>(() =>
roomProperties.reduce((acc, room) => {
acc[room.id] = 0
return acc
}, {}),
)TIP
这体现了 Tuya Robot Map 纯受控设计的优势,你可以自由主导地图的状态。
途径点
途径点用于定义机器需要途径的位置,移动机器人产品可以依据途径点进行定点巡航。
tsx
import React, { useState } from 'react'
import { RobotMap, WayPointParam } from '@ray-js/robot-map'
const MapPage = () => {
const [wayPoints, setWayPoints] = useState<WayPointParam[]>([])
const [editingWayPointIds, setEditingWayPointIds] = useState<string[]>([])
// 通过点击地图获取的坐标来新增途径点
const handleClickMap = (point: Point) => {
const id = nanoid()
setWayPoints([...wayPoints, { id, point }])
setEditingWayPointIds([id])
}
// 更新途径点坐标
const handleUpdateWayPoint = (wayPoint: WayPointParam) => {
setWayPoints(
wayPoints.map((item) => (item.id === wayPoint.id ? wayPoint : item)),
)
}
// 点击途径点时切换到编辑状态
const handleClickWayPoint = (wayPoint: WayPointParam) => {
setEditingWayPointIds([wayPoint.id])
}
return (
<RobotMap
runtime={{
// 启用地图点击捕获,点击地图时会触发onClickMap回调
enableMapClickCapture: true,
// 正在编辑的途径点ID列表
editingWayPointIds,
}}
wayPoints={wayPoints}
onClickMap={handleClickMap}
onUpdateWayPoint={handleUpdateWayPoint}
onClickWayPoint={handleClickWayPoint}
/>
)
}检测物体
将扫地机识别到的物体显示在地图上。
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}
// ... 其他属性
/>
)
}TIP
自定义元素的 sizeFixed 默认值为 true,表示元素的尺寸不会跟随地图缩放。
WARNING
注意,如果将自定义 html 元素的 sizeFixed 设置为 false,请不要传入 width 和 height 属性,否则可能导致问题。
截图
为当前地图截图
你可以调用 snapshot 方法为当前地图截图。
tsx
import React from 'react'
import { RobotMap } from '@ray-js/robot-map'
const MapPage = () => {
const handleMapReady = async () => {
const base64 = await mapApi.snapshot()
console.log('截图成功:', base64)
}
return <RobotMap onMapReady={handleMapReady} />
}使用其他地图数据进行截图
你可以调用 snapshotByData 方法使用其他地图数据进行截图。
tsx
import React from 'react'
import { RobotMap } from '@ray-js/robot-map'
const MapPage = () => {
const handleMapReady = async() => {
const base64 = await mapApi.snapshotByData({
map: 'your_map_data_string',
path: 'your_path_data_string',
roomProperties: your_room_properties_data,
customElements: your_custom_elements_data,
runtime: {{
showPath: false,
showRoomProperty: true,
}}
})
console.log('截图成功:', base64)
return (
<RobotMap onMapReady={handleMapReady} />
)TIP
使用其他地图数据进行截图时,config 会沿用当前地图的配置,但你可以自由决定它的 runtime。
地图旋转
控制地图旋转非常简单,只需要设置 runtime.mapRotation 即可。在地图旋转后,组件会自动对地图做一次自适应居中。
TIP
房间信息 / 检测物体 / 自定义元素 会始终保持水平方向,不受地图旋转影响。
tsx
import React from 'react'
import { RobotMap } from '@ray-js/robot-map'
const MapPage = () => {
return <RobotMap runtime={{ mapRotation: 90 }} />
}在弹窗里使用 RjsRobotMap
通常在弹窗里需要展示地图的时候会使用 RjsRobotMap 组件。
当需要在弹窗(Popup)组件中使用 RjsRobotMap 时,需要特别注意组件的初始化时机。由于弹窗组件在未打开之前通常不会渲染 DOM,如果此时 RjsRobotMap 已经开始初始化,可能会因为无法获取到容器 DOM 的尺寸而导致地图加载异常。
正确的做法是通过弹窗的 onAfterEnter 回调来确保弹窗 DOM 渲染完成后再初始化地图组件。
同时,建议为 RjsRobotMap 包装一个带有明确宽高的容器,无需在 config 中设置 containerHeight 和 containerWidth。
tsx
import React, { useState } from 'react'
import { View } from '@ray-js/ray'
import { Popup } from '@ray-js/smart-ui'
import { RjsRobotMap } from '@ray-js/robot-map'
const MapPage = () => {
const [show, setShow] = useState(false)
const [isReady, setIsReady] = useState(false)
return (
<Popup
show={show}
position="bottom"
round
onAfterEnter={() => {
// 弹窗 DOM 渲染完成后,设置 isReady 为 true
setIsReady(true)
}}
onAfterLeave={() => {
// 弹窗关闭时,重置 isReady 状态
setIsReady(false)
}}
>
<View
className={styles.container}
style={{
position: 'relative',
overflow: 'hidden',
height: '560rpx',
width: '654rpx',
}}
>
{/* 只有当 isReady 为 true 时才渲染地图组件 */}
{isReady && (
<RjsRobotMap runtime={{ showPath: false, showRoomProperty: true }} />
)}
</View>
</Popup>
)
}