在此 Codelab 中,您可以利用面板小程序开发构建出一个可视对讲门锁面板,并通过 DP 协议、接口实现以下能力:
详见 面板小程序 > 搭建环境。
首先需要创建一个智能门锁类产品,定义产品有哪些功能点,然后在面板中一一实现这些功能点。
注册登录 涂鸦开发者平台,并在平台创建产品:



🎉 在这一步,成功完成创建了一个名为 videoIntercom 的可视对讲门锁产品。
面板小程序的开发在 小程序开发者 平台上进行操作,首先请前往 小程序开发者平台 完成平台的注册登录。
详细操作步骤可参考 面板小程序 > 创建面板小程序。
这部分在 Tuya MiniApp Tools 上操作,打开 IDE 创建一个基于可视对讲门锁模板的面板小程序项目。
详细操作步骤可参考 面板小程序 > 初始化项目工程。

完成以上步骤后,一个面板小程序的开发模板已经初始化完成,本章节为您介绍工程目录。
├── src
│ ├── @types // 全局类型定义
│ ├── api
│ │ ├── getCachedLaunchOptions.ts // 获取缓存的启动参数
│ │ ├── getCachedSystemInfo.ts // 获取缓存的系统信息
│ │ └── request.ts // 请求封装
│ ├── components
│ │ ├── action-sheet // 操作选择弹窗
│ │ ├── aes-image // AES 加密图片组件
│ │ ├── calendar // 日历组件
│ │ ├── date-range // 日期范围选择组件
│ │ ├── effective-time-config // 生效时间配置组件
│ │ ├── loading-data // 数据加载组件
│ │ ├── loading-more // 加载更多组件
│ │ ├── page-container // 页面容器组件
│ │ ├── time-picker // 时间选择器组件
│ │ ├── time-range-popup // 时间范围弹窗组件
│ │ └── video-player // 视频播放器组件
│ ├── constant // 常用配置
│ ├── devices // 设备模型
│ │ ├── index.ts // 设备初始化
│ │ ├── protocols // 协议配置
│ │ └── schema.ts // 设备 Schema
│ ├── hooks // 自定义 Hooks
│ │ ├── useSelectorWithEquality.ts // Redux 选择器 Hook
│ │ ├── useSystemInfo.tsx // 系统信息 Hook
│ │ └── useTempInfos.tsx // 临时密码信息 Hook
│ ├── i18n // 多语言
│ │ ├── index.ts // 多语言初始化
│ │ └── strings.ts // 语言字符串
│ ├── pages
│ │ ├── home // 首页
│ │ │ ├── components
│ │ │ │ ├── log // 日志预览组件
│ │ │ │ ├── open // 开关锁组件
│ │ │ │ └── options // 功能入口组件
│ │ │ ├── index.tsx // 首页主文件
│ │ │ ├── index.module.less // 首页样式
│ │ │ └── index.config.ts // 首页配置
│ │ ├── logs // 日志管理页
│ │ │ ├── components
│ │ │ │ ├── album-item // 相册项组件
│ │ │ │ ├── log-item // 日志项组件
│ │ │ │ └── tabs // 标签页组件
│ │ │ ├── index.tsx // 日志页主文件
│ │ │ ├── logs.tsx // 开门记录页
│ │ │ ├── alarm.tsx // 告警记录页
│ │ │ ├── album.tsx // 相册日志页
│ │ │ └── record-alarm.tsx // 告警详情页
│ │ ├── members // 成员管理页
│ │ │ ├── list // 成员列表页
│ │ │ ├── details // 成员详情页
│ │ │ └── components
│ │ │ └── member // 成员项组件
│ │ ├── unlock // 开锁方式管理页
│ │ │ ├── password // 密码管理页
│ │ │ ├── add // 添加开锁方式页
│ │ │ ├── edit // 编辑开锁方式页
│ │ │ └── unbind-list // 未绑定开锁方式列表页
│ │ ├── temporary // 临时密码管理页
│ │ │ ├── password // 创建临时密码页
│ │ │ ├── valid-list // 有效密码列表页
│ │ │ ├── invalid-list // 无效密码列表页
│ │ │ ├── edit-custom // 编辑自定义密码页
│ │ │ ├── success // 创建成功页
│ │ │ └── clear-one // 清除单个密码页
│ │ ├── video // 视频对讲页
│ │ │ ├── components
│ │ │ │ ├── clarity // 画质切换组件
│ │ │ │ └── tools // 工具组件
│ │ │ ├── index.tsx // 视频对讲页主文件
│ │ │ ├── video.tsx // 视频组件
│ │ │ └── utils.ts // 工具函数
│ │ ├── player // 视频回放页
│ │ │ ├── index.tsx // 视频回放页主文件
│ │ │ └── index.module.less // 样式文件
│ │ └── settings // 设置页
│ │ └── overview // 设置概览页
│ ├── redux // Redux 状态管理
│ │ ├── index.ts // Redux Store
│ │ └── modules // Redux 模块
│ │ ├── themeSlice.ts // 主题模块
│ │ └── systemInfoSlice.ts // 系统信息模块
│ ├── res // 本地资源
│ │ ├── icons.ts // 图标资源
│ │ └── *.png // 图片资源
│ ├── styles // 全局样式
│ │ └── index.less // 全局样式文件
│ ├── utils // 常用工具方法
│ │ ├── constant.tsx // 常量定义
│ │ ├── error.ts // 错误处理
│ │ ├── index.ts // 工具函数
│ │ ├── log.ts // 日志工具
│ │ └── transfer.ts // 数据传输工具
│ ├── app.config.ts // 应用配置
│ ├── app.less // 应用样式
│ ├── app.tsx // 应用入口
│ ├── composeLayout.tsx // 布局组合器,处理 SDK 初始化等
│ ├── global.config.ts // 全局配置
│ ├── routes.config.ts // 路由配置
│ └── variables.less // 样式变量
在 composeLayout.tsx 中,需要在应用启动时初始化门锁 SDK,配置密码相关参数和严格模式。
代码路径:src/composeLayout.tsx
import { init as initLock } from '@ray-js/lock-sdk';
async onLaunch(object: any) {
console.log('=== App onLaunch', object);
devices.common.init();
// 初始化门锁 SDK
initLock({
passwordDigitalBase: 10, // 密码数字进制,默认 10
passwordSupportZero: true, // 密码是否支持 0,默认 true
strictMode: false, // 严格模式,默认 true
});
devices.common.onInitialized(device => dpKit.init(device));
// ...
}
在首页的开关锁组件中,实现长按按钮远程控制门锁开关,实时查看设备状态。
代码路径:src/pages/home/components/open/index.tsx
import {
openDoor,
closeDoor,
isSleep,
getDeviceStatus,
onDeviceStatusChange,
offDeviceStatusChange,
} from '@ray-js/lock-sdk';
function Open() {
const doorLockStatus = useProps(d => d.lock_motor_state);
const [deviceStatus, setDeviceStatus] = useState(() => {
return getDeviceStatus();
});
const [operateing, setOperateing] = useState(false);
// 监听设备状态变化
useEffect(() => {
const handleDeviceStatusChange = (status: any) => {
setDeviceStatus(status);
};
onDeviceStatusChange(handleDeviceStatusChange);
return () => {
offDeviceStatusChange(handleDeviceStatusChange);
};
}, []);
// 执行开锁/关锁操作
const handleRemote = useCallback(async () => {
try {
setOperateing(true);
if (doorLockStatus) {
await closeDoor();
} else {
await openDoor();
}
} catch (e) {
ToastInstance.fail('操作失败');
} finally {
setOperateing(false);
}
}, [doorLockStatus]);
return (
<View onLongClick={handleRemote}>
{/* UI 展示 */}
</View>
);
}
实现查看门锁的开门记录、告警记录和相册日志,支持分页查询、下拉刷新、上拉加载。
功能展示:

代码路径:src/pages/logs/index.tsx
import {
getLatestLogs,
getLogs,
getAlarms,
getAlbums,
onLogsRefresh,
offLogsRefresh,
} from '@ray-js/lock-sdk';
// 首页日志预览
const [logs, setLogs] = useState({
data: [],
unreadCount: 0,
});
useEffect(() => {
getLatestLogs().then(logs => {
setLogs(logs);
});
}, []);
// 监听日志刷新事件
useEffect(() => {
const handleRefresh = () => {
getLatestLogs().then(logs => {
setLogs(logs);
});
};
onLogsRefresh(handleRefresh);
return () => {
offLogsRefresh(handleRefresh);
};
}, []);
// 获取日志列表
const fetchLogs = async (pageNo: number) => {
const res = await getLogs({ page: pageNo });
setList(res.list);
setHasNext(res.hasMore);
};
实现管理门锁成员,包括查看成员列表、搜索成员、添加成员、查看成员详情、删除成员等操作。
功能展示:

代码路径:src/pages/members/list/index.tsx
import {
getUsers,
addUser,
removeUser,
} from '@ray-js/lock-sdk';
// 获取用户列表(支持搜索)
const getMembers = async (name: string) => {
const members = await getUsers({
keyword: name,
page: 1,
pageSize: 50,
});
setMembers(members.list);
};
// 添加成员
const handleAddMember = () => {
DialogInstance.input({
title: '添加普通成员',
value: '',
placeholder: '请输入昵称',
beforeClose: async (action, value) => {
if (action === 'confirm' && value) {
await addUser({ name: value });
ToastInstance.success('保存成功');
}
return true;
},
emptyDisabled: true,
});
};
实现以下功能:
功能展示:

代码路径:src/pages/unlock/add/index.tsx
import {
startAddUnlockMethod,
onAddUnlockMethod,
offAddUnlockMethod,
cancelAddUnlockMethod,
addPassword,
} from '@ray-js/lock-sdk';
// 开始添加开锁方式
const handleAdd = async () => {
try {
const { total } = await startAddUnlockMethod({
userId,
type: 'finger', // 'finger' | 'face' | 'card' | 'hand' | 'fingerVein'
});
setTotal(total);
setAddState(1);
} catch (e) {
// 处理错误
}
};
// 监听添加进度
useEffect(() => {
const handleStep = (event) => {
switch (event.stage) {
case 'step':
setStep(event.step);
break;
case 'success':
setAddState(2);
break;
case 'fail':
setAddState(3);
break;
}
};
onAddUnlockMethod(handleStep);
return () => {
offAddUnlockMethod(handleStep);
};
}, []);
实现创建和管理临时密码,包括自定义密码、限时密码、一次性密码、动态密码等类型。
功能展示:

代码路径:src/pages/temporary/password/index.tsx
import {
createTempCustom,
createTempLimit,
createTempOnce,
createTempDynamic,
getTempEffectiveList,
renameTemp,
updateTempCustom,
freezeTemp,
unfreezeTemp,
removeTempCustom,
} from '@ray-js/lock-sdk';
// 创建自定义密码
const save = async () => {
const res = await createTempCustom({
password,
name,
effective: {
effectiveDate: Math.floor(effectConfig.effectiveTime.valueOf() / 1000),
expiredDate: Math.floor(effectConfig.invalidTime.valueOf() / 1000),
repeat: effectConfig.repeat ? 'week' : 'none',
weeks: effectConfig.weeks,
effectiveTime: effectConfig.startTime,
expiredTime: effectConfig.endTime,
},
});
};
// 创建限时密码
await createTempLimit({
name: '限时密码',
effectiveTime: Math.floor(startTime.valueOf() / 1000),
invalidTime: Math.floor(endTime.valueOf() / 1000),
});
// 创建一次性密码
await createTempOnce({ name: '一次性密码' });
// 创建动态密码
await createTempDynamic();
在视频对讲页面中,实现实时视频查看,支持切换画质、静音等功能。
功能展示:

代码路径:src/pages/video/video.tsx
import Player from '@ray-js/ray-ipc-player';
import {
getDeviceStatus,
getMediaRotate,
onDeviceStatusChange,
offDeviceStatusChange,
} from '@ray-js/lock-sdk';
const VideoView = () => {
const { devId } = useDevice(d => d.devInfo);
const [deviceStatus, setDeviceStatus] = useState(() => {
return getDeviceStatus();
});
const [muted, setMuted] = useState(false);
const [clarity, setClarity] = useState<'normal' | 'hd'>('normal');
const [isPlaying, setIsPlaying] = useState(false);
const rotateInfo = getMediaRotate();
const handleChangeStreamStatus = useCallback((data: number) => {
setIsPlaying(data === 1002);
}, []);
return (
<>
<CoverView className={styles.menus}>
<NavBar title="" leftArrow border={false} onClickLeft={handlBack} />
<Clarity clarity={clarity} onChange={setClarity} />
<Tools disabled={!isPlaying} muted={muted} onMutedChange={setMuted} />
</CoverView>
<View className={styles.content}>
<Player
defaultMute={muted}
devId={devId}
onlineStatus={deviceStatus.type === 'online'}
rotateZ={rotateInfo.videoAngle}
onChangeStreamStatus={handleChangeStreamStatus}
clarity={clarity}
brandColor="#FF592A"
/>
</View>
</>
);
};
在视频回放页面中,实现播放视频记录,支持播放、暂停、下载功能。
代码路径:src/pages/player/index.tsx
import { IpcPlayer, createIpcPlayerContext, ipc } from '@ray-js/ray';
import { getMediaRotate, getMediaUrl, MediaInfo } from '@ray-js/lock-sdk';
const Player = () => {
const { devId } = useDevice(d => d.devInfo);
const data = useMemo(() => {
return transfer.receive<MediaInfo>('mediaData');
}, []);
const ctx = useRef<ReturnType<typeof createIpcPlayerContext>>(null);
const [status, setStatus] = useState<'init' | 'playing' | 'pause' | 'stop' | 'error'>('init');
const [mediaUrl, setMediaUrl] = useState('');
const rotateInfo = getMediaRotate();
// 初始化播放器
const initPlayer = useCallback(() => {
ctx.current?.createMessageDevice({
devId,
success: () => {
ctx.current?.setMessageVideoMute({ mute: 0 });
ctx.current?.setRotateZ(rotateInfo.videoAngle);
ipc.onPlayMessageVideoFinish(handlePlayMessageVideoFinish);
},
});
ipc.createMediaDevice({ deviceId: devId });
}, []);
// 播放视频
const handlePlay = useCallback(async () => {
if (status === 'init' || status === 'stop' || status === 'error') {
const res = await getMediaUrl({
mediaPath: data.mediaPath,
mediaBucket: data.mediaBucket,
});
ctx.current?.startMessageVideoPlay({
encryptKey: data.mediaKey,
path: res.mediaUrl,
startTime: 0,
success: () => {
setStatus('playing');
},
});
} else if (status === 'playing') {
ctx.current?.pauseMessageVideoPlay({
success: () => {
setStatus('pause');
},
});
} else if (status === 'pause') {
ctx.current?.resumeMessageVideoPlay({
success: () => {
setStatus('playing');
},
});
}
}, [data, status]);
// 下载视频
const handleDownload = useCallback(() => {
ipc.startDownloadMessageVideo({
deviceId: devId,
path: mediaUrl,
encryptKey: data.mediaKey,
savePath: 2,
rotateMode: rotateInfo.videoAngle || 0,
});
}, [mediaUrl]);
return (
<>
<NavBar title="" background="#000" leftArrow border={false} onClickLeft={handlBack} />
<View className={styles.content}>
<IpcPlayer
type={2}
deviceId={devId}
autoplay={false}
objectFit="contain"
orientation="vertical"
rotateZ={rotateInfo.videoAngle}
onCreateViewSuccess={initPlayer}
className={styles.player}
/>
<CoverView className={styles.preview}>
{status !== 'playing' && status !== 'pause' && (
<AESImage
devId={devId}
className={styles.mediaImage}
fileKey={data.fileKey}
url={data.fileUrl}
fillType="width"
rotate={rotateInfo.imageAngle}
/>
)}
<View className={styles.tools}>
<View className={styles.playButton} onClick={handlePlay}>
<Icon name={status === 'playing' ? Pause : Play} size="48rpx" />
</View>
<View className={styles.downloadButton} onClick={handleDownload}>
<Icon name={Download} size="48rpx" />
</View>
</View>
</CoverView>
</View>
</>
);
};
在设置页面中,实现设备休眠时间段的配置。
代码路径:src/pages/settings/overview/components/sleep-settings/index.tsx
import {
getSleepPeriod,
setSleepPeriod,
isSleep,
onSleepStatusChange,
offSleepStatusChange,
} from '@ray-js/lock-sdk';
// 获取休眠时间段
const fetchSleepPeriod = async () => {
const period = await getSleepPeriod();
setSleepPeriod(period);
};
// 设置休眠时间段
const handleSave = async () => {
await setSleepPeriod({
start: startTime,
end: endTime,
});
};
在设置页面中,实现远程开锁权限的配置。
代码路径:src/pages/settings/overview/components/remote-unlock/index.tsx
import {
getRemotePermission,
updateRemotePermission,
remoteEnabled,
checkRemoteEnabled,
} from '@ray-js/lock-sdk';
// 获取远程开锁权限
const fetchPermission = async () => {
const enabled = await checkRemoteEnabled();
const permission = await getRemotePermission();
setEnabled(enabled);
setPermission(permission);
};
// 更新远程开锁权限
const handleUpdate = async () => {
await updateRemotePermission(permission);
};