面板是 IoT 智能设备在 App 终端上的产品形态。创建产品之前,首先来了解一下什么是面板,以及面板和产品、设备之间的关系。

下图描述了产品、面板和设备三者之间的关系:

由于产品定义了面板和设备所拥有的功能点,所以在开发一个智能设备面板之前,首先需要创建一个产品,定义产品有哪些功能点,然后再在面板中一一实现这些功能点。
注册登录 涂鸦开发者平台,并在平台创建产品:


产品创建完成后,进入到 添加标准功能 页面,这里列出了空调类目下可选的标准功能。单击 全部选择,然后单击 确定 完成产品初始功能点设置。

在完成产品创建和功能设置后,就可以开始 面板小程序 的开发流程。请先前往 涂鸦小程序开发者平台 完成平台的注册登录。
单击 新建,然后在 小程序信息 页,填写 小程序名称 为 万能面板,小程序类型 选择 面板小程序,然后单击 确定 完成创建。

安装并打开小程序 IDE 工具(前往下载小程序 IDE),使用涂鸦开发者平台账号登录 IDE。

完成后,单击 下一步。



涂鸦小程序 IDE 工具导入小程序后会自动安装依赖,并实时编译运行。如果出现 Error(MiniKit 不存在指定的版本 2.3.3) 类似的错误提示,单击 环境配置 > Kit 管理,选择推荐的版本即可。

面板小程序的开发主要围绕开发者平台、小程序开发平台、小程序 IDE 之间进行,整体流程参考下图:

前面的步骤结束后,一个面板小程序的创建流程即完成,接下来进入到实际代码的开发教程。
首先了解项目的目录结构,单击工具栏在 Visual Studio Code 中打开项目代码。
如果提示没有 code 命令,需要在 Visual Studio Code 帮助中选择 显示所有命令,搜索 code 安装即可,可参考 Visual Studio Code 官方文档 操作。

然后在 Visual Studio Code 中可以看到项目代码的文件目录结构。

编写代码主要在 pages 文件夹中进行。创建你的页面代码,然后将路由地址配置到 src/routes.config.ts 文件中。

关于其他文件的说明,会在下文开发步骤的介绍中提及。
关于 SDM 智能设备模型文档,请查看 SDM 智能设备模型参考文档。

src/devices/schema.ts 文件,在生成的 Schema 列表后设置 as const,以便于类型提示,并在 src/devices/index.ts 中使用 deviceSchema 类型。
src/devices/index 中你可以看到 SDM 的初始化逻辑,简化一下就是:// 创建 SDM 智能设备模型
const value = new SmartDeviceModel({ deviceId });
value.init(); // 执行初始化
// 初始化完成事件
value.onInitialized(() => {
��始化完成后可以进行设备信息获取和操作
�� value.getDpSchema()
...
});
``src/app.tsx 应用根组件设置如下,使用 SdmProvider 将智能设备模型实例放在 Context 中:
import React, { Component } from ‘react' import { SdmProvider } from "@ray-js/panel-sdk"; import { devices } from ‘@/devices'; import { initPanelEnvironment } from "@ray-js/ray";
initPanelEnvironment({ useDefaultOffline: true });
class App extends Component {
render() {
return <SdmProvider value={devices.common}>{this.props.children}</SdmProvider>
}
}
```
src/pages/home/index.tsx 文件,输入以下代码:import React from "react";
import { useDpSchema, useProps } from "@ray-js/panel-sdk";
import { View } from "@ray-js/ray";
export default () => {
// 项目启动时,会自动拉取开发者平台 productId 对应的产品 schema 信息
const dpSchema = useDpSchema();
// 从设备模型中读取 dpState 数据
const dpState = useProps(state => state); // 获取所有 dpState
const switch = useProps(state => state.switch); // 获取 dpCode 为 switch 的值
console.log("dpSchema", dpSchema); // 打印查看 dpSchema 内容
console.log("dpState", dpState); // 打印查看 dpState 内容
return <View>hello world</View>;
};
``注意 useProps 使用时,建议精确到读取哪个 DP,例如精确到读取 switch state => state.switch,只有当 switch 变化时组件才会重新渲染。查看 IDE 调试 Console,可以看到打印出 devInfo 内容。
代码中获取 devInfo:
import { useDevInfo } from "@ray-js/panel-sdk";
const devInfo = useDevInfo();
console.log("devInfo:", devInfo);

devInfo 字段说明在 IDE 调试器 Console 面板中默认可以看到有很多输出,其中最重要的就是 devInfo ,即设备信息(Device Information)。
需要了解 devInfo 中几个重要的属性:
codeIds: 一个对象,key 是 dpCode,值是 dpId。devId: 设备的 id,虚拟设备会以 vdevo 开头。deviceOnline: 设备是否在线。dps: 一个对象,key 是 dpId,值是 DP 的状态。idCodes: 一个对象,与 codeIds 相反。panelConfig: 面板配置,其中 bic 是云功能配置。productId: 当前设备绑定的产品。schema: 产品的功能点定义,其中描述了 DP 的 code、类型、值范围属性、icon 图标。state: 一个对象,key 是 dpCode,值是 DP 的状态。ui: 面板的 uiId。通常代码中获取 devInfo 是通过 useDevInfo API 来读取。(本文中的 DP 即 "功能点",来自开发者平台功能定义。)
使用 useDpSchema 可以获取到产品的功能定义:
import { useDpSchema, useProps } from "@ray-js/panel-sdk";
const dpSchema = useDpSchema(); // 功能点定义
const dpState = useProps((state) => state); // 设备功能点状态
遍历 Schema 可以获取到产品所有的 DP 功能点及属性:
schema.map((item) => {
// item.code
// item.property
// ...
return <Text>{item.name}</Text>;
});
这里根据 item.property.type 可以判断并渲染不同类型 DP 功能点的 UI 展示。
rpx 单位Ray 开发样式使用 Less 语言,支持 CSS Module,新建文件 index.module.less,编写内容例如:src/pages/home/index.module.less。
.container {
border-radius: 24rpx;
background-color: #fff;
margin-bottom: 24rpx;
}
rpx 单位是 Ray 框架提供的特有单位,能够做到不同设备上的自适应。
在代码中使用样式:
import React from "react";
import { useDpSchema } from "@ray-js/panel-sdk";
import { View } from "@ray-js/components";
import styles from "./index.module.less"; // 注意样式引入方式
export default () => {
const dpSchema = useDpSchema();
// 添加 className
return <View className={styles.container}>hello world</View>;
};
通过 Console 日志可以了解到 Schema 的数据结构:

详细请参考文档 自定义功能。
src/home/index.tsx 编写输入以下代码:
import React from "react";
import { useDevInfo, useDpSchema, useProps } from "@ray-js/panel-sdk";
import { View } from "@ray-js/components";
export default () => {
const dpSchema = useDpSchema();
const dpState = useProps((state) => state);
console.log("dpSchema", dpSchema);
console.log("dpState", dpState);
return (
<View>
{Object.keys(dpSchema || {}).map((dpCode) => {
// 遍历渲染每个功能点
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
重新编译,可以看到 IDE 左边面板中显示了所有 DP 的 Code 及对应设备状态 :

在 IDE Console 面板中,可以看到 Bool 类型功能点的数据结构,例如:
{
dptype: "obj";
id: "39";
type: "bool";
}
使用工具包 @ray-js/components-ty 提供的开关组件 TySwitch 来渲染,执行以下命令,安装 Ray 提供的扩展组件库:
yarn add @ray-js/components-ty
TySwitch继续编写代码,输入以下内容:
因为是 Bool 类型 DP,所以在 actions 中有 toggle 方法,调用时会使 DP 值在 true 和 false 之间切换。
import React from "react";
import { useDpSchema } from "@ray-js/panel-sdk";
import { TySwitch } from "@ray-js/components-ty";
import { View } from "@ray-js/components";
const BoolCard = ({ dpCode }) => {
const dpValue = useProps((state) => state[dpCode]);
const actions = useActions();
return (
<View>
{dpCode}:{" "}
<TySwitch
checked={dpValue}
onChange={() => {
actions[dpCode].toggle();
}}
/>
</View>
);
};
export default () => {
const dpSchema = useDpSchema();
const dpState = useProps((state) => state);
console.log("dpSchema", dpSchema);
return (
<View>
{Object.keys(dpSchema || {}).map((dpCode) => {
const props = dpSchema[dpCode];
// 判断如果是 bool 类型,返回 TySwitch
if (props.type === "bool") {
return <BoolCard dpCode={dpCode} />;
}
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
编译后,IDE 中渲染界面如下图所示,能看到所有的 Bool 型 DP 已经成功渲染出了开关组件。

DP 在功能定义时,在产品信息中已经有相应的多语言文本,这里使用 src/i18n 下的 Strings 工具获取 DP 文本,输入以下代码:
import React from "react";
import { useDpSchema, useProps } from "@ray-js/panel-sdk";
import { TySwitch } from "@ray-js/components-ty";
import { View } from "@ray-js/components";
import Strings from "@/i18n";
export default () => {
const dpSchema = useDpSchema();
const dpState = useProps((state) => state);
console.log("dpSchema", dpSchema);
return (
<View>
{Object.keys(dpSchema || {}).map((dpCode) => {
const props = dpSchema[dpCode];
if (props.type === "bool") {
// 使用 Strings.getDpLang 方法获取多语言
return (
<View>
{Strings.getDpLang(dpCode)}:{" "}
<TySwitch checked={dpState[dpCode]} />
</View>
);
}
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
再次编译查看 IDE 渲染结果,可以看到所有的 Bool 类型 DP 文本都已显示出中文:

此时 Bool 类型 DP 功能点的 UI 渲染已完成。
上文介绍了如何编写代码渲染 DP 点,但是要控制设备运行,还需要进行 DP 下,使用 SDM 智能设备模型 提供的 Hooks useActions 下发能力,例如:
import { useActions } from "@ray-js/panel-sdk";
const actions = useActions();
// 下发给对应设备,actions.xxx 中的 xxx 是 dpCode,set 是下发指令的方法,传入 dpValue 调用即可下发指令
actions.switch.set(true); // 下发 switch 为 true
actions.switch.on(); // 打开当前布尔类型功能点
actions.switch.off(); // 关闭当前布尔类型功能点
actions.switch.toggle(); // 在 true 和 false 之间切换 bool 值
现在来示例开关功能点的 DP 下发 操作的代码编写。
首先实现 Bool 下发上报组件:
import React from "react";
import { useActions, useProps } from "@ray-js/panel-sdk";
import { View } from "@ray-js/components";
const BoolCard = ({ dpCode }) => {
const actions = useActions();
const dpValue = useProps((state) => state[dpCode]);
return (
<View>
{dpCode}:{" "}
<TySwitch checked={dpValue} onChange={() => actions[dpCode].toggle()} />
</View>
);
};
对于 Bool 类型的 DP 功能点,以 switch 开关 DP 点的 actions 为例,actions.xxx 会有以下几个工具方法:
actions.switch.on; // switch 功能点下发 true
actions.switch.off; // switch 功能点下发 false
actions.switch.toggle; // switch 功能点 true/false 切换下发
export default () => {
const schema = useDpSchema();
const dpState = useProps((state) => state);
const actions = useActions();
return (
<View>
{Object.keys(schema || {}).map((dpCode) => {
const props = schema[dpCode];
// 判断如果是 bool 类型,返回 TySwitch
if (props.type === "bool") {
return <BoolCard key={dpCode} dpCode={dpCode} />;
}
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
IDE 编译后运行,使用虚拟设备插件进行调试。在调试器 Virtual Device 面板中,选择 可视化面板,单击调试器中的开关,可以看到左边模拟器中的开关收到了 DP 上报,并做出了相同的切换动作。

Enum 类型 DP 点的 Schema 属性配置如下:
{
dptype: "obj";
id: "20";
range: ["cancel", "1h", "2h", "3h", "4h", "5h", "6h"];
type: "enum";
}
TyActionsheet 和列表组件 TyCellrange 中的即枚举条目,使用 @ray-js/components-ty 中的 ActionSheet 弹窗组件 + Cell 列表组件 进行渲染,编写以下代码。
首先实现 Enum 枚举展示控制组件:
import React from "react";
import { useActions, useProps } from "@ray-js/panel-sdk";
import { View } from "@ray-js/components";
import Strings from "@/i18n";
const EnumCard = ({ dpCode }) => {
const actions = useActions();
const dpValue = useProps((state) => state[dpCode]);
const [showEnumDp, setShowEnumDp] = useState(null);
return (
<View>
{Strings.getDpLang(dpCode)}:
<Button onClick={() => setShowEnumDp(dpCode)}>
{Strings.getDpLang(dpCode, dpState[dpCode])}
</Button>
<TyActionsheet
header={Strings.getDpLang(dpCode)}
show={showEnumDp === dpCode}
onCancel={() => setShowEnumDp(null)}
>
<View style={{ overflow: "auto", height: "200rpx" }}>
<TyCell.Row
rowKey="title"
dataSource={props.range.map((item) => ({
title: Strings.getDpLang(dpCode, item),
onClick: () => {
actions[dpCode].set(item);
},
}))}
/>
</View>
</TyActionsheet>
</View>
);
};
对于 Enum 类型的 DP 功能点,以 work_mode 工作模式 DP 点的 actions 为例,actions.xxx 会有以下几个工具方法:
actions.work_mode.set("white"); // work_mode 下发 white
actions.work_mode.next(); // 切换下一个枚举
actions.work_mode.prev(); // 切换上一个枚举
actions.work_mode.random(); // 切换到随机一个枚举值
遍历 Schema 渲染:
import React, { useState } from "react";
import { useDpSchema, useProps } from "@ray-js/panel-sdk";
import { TyActionsheet, TyCell, TySwitch } from "@ray-js/components-ty";
import { Button, ScrollView, View } from "@ray-js/components";
import Strings from "@/i18n";
export default () => {
const schema = useDpSchema();
const dpState = useProps((state) => state);
console.log("schema", schema);
const [showEnumDp, setShowEnumDp] = useState(null);
return (
<View>
{Object.keys(schema).map((dpCode) => {
const props = schema[dpCode];
// 这里为了文章篇幅,忽略其他类型的。本地代码可复制上节 bool 类型的到这里
if (props.type === "enum") {
return <EnumCard key={dpCode} dpCode={dpCode} />;
}
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
单击按钮,触发 ActionSheet 弹窗,弹窗中使用 Cell 列表组件 渲染多个枚举项,效果如下:

String 类型 DP 点的 Schema 属性配置如下:
{
dptype: "obj";
id: "20";
type: "string";
}
Input首先实现 Input 输入组件:
import React from "react";
import { useActions, useProps } from "@ray-js/panel-sdk";
import { Input } from "@ray-js/components";
import Strings from "@/i18n";
const InputCard = ({ dpCode }) => {
const actions = useActions();
const dpValue = useProps((state) => state[dpCode]);
const [showEnumDp, setShowEnumDp] = useState(null);
return (
<Input
value={dpState[dpCode]}
onInput={(event) => {
actions[dpCode].set(event.detail.value);
}}
/>
);
};
对于 String 类型的 DP 功能点,以 name 名称 DP 点的 actions 为例,actions.xxx 会有以下几个工具方法:
actions.name.set("my device"); // name 下发 my device
遍历 Schema 渲染:
import { useDpSchema, useActions } from "@ray-js/panel-sdk";
import { Input } from "@ray-js/components";
export default () => {
const schema = useDpSchema();
const dpState = useProps((state) => state);
console.log("schema", schema);
return (
<View>
{Object.keys(schema || {}).map((dpCode) => {
const props = schema[dpCode];
// 这里为了文章篇幅,忽略其他类型的。本地代码可复制上节类型的到这里
if (props.type === "string") {
return <InputCard value={dpState[dpCode]} />;
}
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
Raw 类型 DP 一般不做渲染,如果需要可以按照 String 类型进行处理。
Value 类型 DP 点的 Schema 属性配置如下:
{
dptype: "obj";
id: "18";
max: 100;
min: 0;
scale: 0;
step: 1;
type: "value";
unit: "%";
}
首先实现 Slider 输入组件。
其中定义了数值范围和单位,使用 Slider 组件进行渲染:
import React from "react";
import { useActions, useProps } from "@ray-js/panel-sdk";
import { Slider } from "@ray-js/components";
import Strings from "@/i18n";
const ValueCard = ({ dpCode }) => {
const actions = useActions();
const dpValue = useProps((state) => state[dpCode]);
return (
<View>
{Strings.getDpLang(dpCode)}:
<Slider
step={props?.step}
max={props?.max}
min={props?.min}
value={dpState[dpCode]}
onChange={(event) => {
actions[dpCode].set();
}}
/>
</View>
);
};
对于 Value 类型的 DP 功能点,以 add_ele 用电量 DP 点的 actions 为例,actions.xxx 会有以下几个工具方法:
actions.add_ele.set(1000); // add_ele 下发 1000
actions.add_ele.inc(); // 根据当前功能点步长 step 进行递增
actions.add_ele.dec(); // 根据当前功能点步长 step 进行递减
遍历 Schema 渲染:
import { useDpSchema, useActions, useProps } from "@ray-js/panel-sdk";
import { Slider } from "@ray-js/components";
export default () => {
const schema = useDpSchema();
const dpState = useProps((state) => state);
console.log("schema", schema);
return (
<View>
{Object.keys(schema).map((dpCode) => {
const props = schema[dpCode];
// 这里为了文章篇幅,忽略其他类型的。本地代码可复制上节类型的到这里
if (props.type === "value") {
return <ValueCard dpCode={dpCode} />;
}
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
Slider 组件是一个滑动条组件,可以根据数值范围进行约束,IDE 渲染如下,所有 Value 类型 DP 都渲染出了滑动条。

Bitmap 类型一般用作故障上报,属性配置如下:
{
dptype: "obj";
id: "22";
label: ["sensor_fault", "temp_fault"];
maxlen: 2;
type: "bitmap";
}
故障型 DP 使用弹窗渲染,这里使用 @ray-js/ray-components-plus 提供的 Notification 来实现:
import { useDpSchema, useActions, useProps } from "@ray-js/panel-sdk";
import { Notification } from "@ray-js/ray-components-plus";
export default () => {
const schema = useDpSchema();
const dpState = useProps((state) => state);
console.log("schema", schema);
// 弹窗消息在 useEffect 中进行
useEffect(() => {
Object.keys(schema).forEach((dpCode) => {
const props = schema[dpCode];
if (props.type === "bitmap") {
Notification.show({
message: Strings.getFaultStrings(dpCode, dpState[dpCode]),
icon: "warning",
});
}
});
}, [schema, dpState]);
return (
<View>
{Object.keys(schema).map((dpCode) => {
const props = schema[dpCode];
// 这里为了文章篇幅,忽略其他类型的。本地代码可复制上节类型的到这里
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
Bitmap 类型通常用来做故障上报,以弹窗消息提示,所以需要在 useEffect 中遍历 Schema 找到 Bitmap 类型并弹窗提醒。
以上 Schema 中是家电类目自带的标准 DP 点,还可以增加自定义的 DP。在开发者平台,进入到产品的开发页面 > 功能定义 > 自定义功能 区域,单击 添加功能。

新建一个 Bool 型 DP:

然后回到 IDE 中,点击 编译 按钮刷新面板,可以看到渲染出了新增加的一个 DP 点:

面板运行时需要获取 设备信息 来显示 设备的运行状态。在开发阶段,如果没有真实的 智能硬件设备,可以借助 虚拟设备 来辅助调试,可以达到和真实设备一样的效果。虚拟设备和真实设备对于面板小程序来说可以是等效的,其关系如下图所示:

在使用虚拟设备前,请先确认已授权登录态,单击右上角登录,使用 智能生活 App 扫描二维码授权登录。

登录后进入 万能面板 项目,单击调试工具 Virtual Device,使用 智能生活 App 扫码创建虚拟设备。

虚拟设备面板中分为 3 个区域。如下图,左侧是兜底面板,用于展示当前产品的 DP 功能点;中间是虚拟设备 DP 控制列表,可以改变 DP 状态并单击 上报 按钮来上报;右侧下方是 Log 面板,用于输出 MQTT 日志。单击 可视化面板,切换到兜底面板视图,它与控制面板功能相同,更加直观的展现产品功能点。

下面来调试刚刚编写的代码,参考下图:

在虚拟设备界面的右侧,单击 设备信息,然后在 deviceId 处复制下设备 ID。

单击 编译参数设置,按照格式填入真机调试参数。 例如:deviceId=vdevo165398364416684

设置编译参数后,单击 真机调试 按钮(真机需要前往 智能生活 App 下载)。

使用 智能生活 App 扫描二维码,打开并进入小程序面板,可以在真机上进行调试万能面板。
到此你已经熟悉了 6 种 DP 点的渲染及下发,此处附上万能面板代码开源地址:panel-universal
经过样式优化后的万能面板展示如下:

在 IDE 中完成调试后,单击 上传源码 按钮,输入版本号及说明,单击 上传 上传预览包。

上传完成后,在小程序开发者平台版本管理中,可以看到已经上传的版本列表。




非官方主体的面板小程序,在提交审核前,需要完善开发者平台展示信息。其余审核上线注意项可参考 上线审核。
