前提条件

构建内容

在此 Codelab 中,您可以利用面板小程序开发构建出一个可视对讲门锁面板,并通过 DP 协议、接口实现以下能力:

学习内容

所需条件

详见 面板小程序 > 搭建环境

首先需要创建一个智能门锁类产品,定义产品有哪些功能点,然后在面板中一一实现这些功能点。

注册登录 涂鸦开发者平台,并在平台创建产品:

  1. 单击页面左侧 AI产品> 产品开发,在 产品开发 页面单击 创建产品
  2. 标准类目 下选择 智能锁,产品品类选择 高端可视对讲门锁
  3. 选择智能化方式和产品方案,完善产品信息,例如填写产品名称为 videoIntercom
  4. 单击 创建产品,完成产品创建。
  5. 产品创建完成后,进入到 添加标准功能 页面,选择全部功能点。

🎉 在这一步,成功完成创建了一个名为 videoIntercom 的可视对讲门锁产品。

开发者平台创建面板小程序

面板小程序的开发在 小程序开发者 平台上进行操作,首先请前往 小程序开发者平台 完成平台的注册登录。

详细操作步骤可参考 面板小程序 > 创建面板小程序

IDE 基于模板创建项目工程

这部分在 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);
};