前提条件

构建内容

在此 Codelab 中,您将利用面板小程序开发构建出一个出行两轮车面板,通过 DP 协议、接口将实现以下能力:

学习内容

所需条件

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

产品名称:新滑板车蓝牙

需求原型

  1. 首页
    • 标题栏点击切换按钮可以切换设备需在涂鸦出行 APP 上使用
    • 顶部区域展现了 4G 信号值、GPS 信号、蓝牙连接状态以及总里程。
    • GPS 和 4G 信号值都分为了五个等级,对应不同的图标。
    • 中心区域分为两部分。一部分是设备信息相关,包括设备电量、续航里程、档位切换以及产品图标;另一部分则是常用的操作。
    • 滑动上锁和解锁,点击‘GO'跳转至骑行导航二级页。

  1. 灯控页
    • 展示了一些灯关控制,如:前大灯、尾灯、氛围灯等

  1. 更多设置页
    • 展示了一些常见的功能设置,如:设备信息、骑行模式、丢失模式、骑行诊断等

  1. 骑行模式页
    • 展示了一些骑行相关的功能设置,如:速度限制、单位设置、定速巡航等

首先需要创建一个户外出行类产品,定义产品有哪些功能点,然后面板中再根据这些功能点一一实现。

进入IoT 平台,点击左侧产品菜单,产品开发,创建产品,选择标准类目 -> 户外出行 -> 滑板车:

选择功能点,这里根据自己需求选择即可,这些功能未选择不影响视频预览。

🎉 在这一步,我们创建了一个名为 PanelOutdoorScooter的出行滑板车产品。

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

这部分我们在 小程序开发者 平台上进行操作,注册登录 小程序开发者平台 进行操作。

详细的操作路径可参考 面板小程序 - 创建面板小程序

IDE 基于模板创建项目工程

这部分我们在 Tuya MiniApp Tools 上进行操作,打开 IDE 创建一个基于出行两轮车模板的面板小程序项目。

详细的操作路径可参考 面板小程序 - 初始化项目工程

上面的步骤我们已经初始化好了一个面板小程序的开发模板,下面我们介绍下工程目录。

├── src
│  	├── @types // 全局类型定义
│  	├── api
│  	│   └── atop.ts // 云端API
│  	├── components
│  	│   ├── action-sheet-enum // 多选弹窗
│  	│   ├── battery // 电池组件
│  	│   ├── control-menu // 首页控制栏
│  	│   ├── dp-list-item // dp数据展示项
│  	│   ├── dynamic-number // 文字动态变化组件
│  	│   ├── icon-svg // svg
│  	│   ├── outdoor-slider // 滑动解锁组件
│  	│   ├── outdoor-top // 首页TopBar组件
│  	│   ├── signal-view // 4G、GPS信号展示组件
│  	│   ├── top-bar // 其他页TopBar组件
│  	│   └── index.ts
│  	├── constant // 常用配置
│  	├── devices // 设备模型
│  	├── hooks // hooks
│  	├── i18n // 多语言
│  	├── pages
│   │   ├── alarm // 告警页设置
│   │   ├── control // 骑行模式设置
│   │   ├── devInfo // 产品设备页
│   │   ├── home // 首页
│   │   ├── infoDetail // 设备详情页
│   │   ├── light // 灯控页
│   │   ├── more // 更多设置页
│  	├── redux // redux
│   ├── res // 本地图片
│   ├── styles // 全局样式
│   ├── utils // 常用工具方法
│   ├── app.config.ts
│   ├── app.less
│   ├── app.tsx
│   ├── composeLayout.tsx // 处理监听子设备添加,解绑,DP变化等
│   ├── global.config.ts
│   ├── routes.config.ts // 配置路由

产品需要开通出行增值服务才能对车辆进行操作,在代码路径 hooks/useCheckPermissioncheckServiceAbility 方法通过调用 ty.outdoor.getValueAddedServicesAbility 获取设备增值服务能力,其中 outdoor_data_cloud_store 表示出行增值服务;如果未购买该服务或服务已过期,则需要提示去购买或续费,跳转到出行续费小程序

// C端续费能力判断 (未购买增值服务或服务已过期 弹窗提示)
const checkServiceAbility = async () => {
  const res = await getCServicesAbility(devId)
  const { inService, isPidHadVAS, commodityUrl, hadPopup } = res
  dispatch(
    updateCommonInfo({
      inService,
      isPidHadVAS,
      commodityUrl: commodityUrl || '',
    })
  )
  if (!hadPopup && isPidHadVAS && inService) {
    // 品牌方有配置C端用户续费套餐有赠送
    if (commodityUrl && commodityUrl !== '') {
      ty.showModal({
        title: '',
        content: Strings.getLang('serviceActive'),
        confirmText: Strings.getLang('goCheck'),
        cancelText: Strings.getLang('confirm'),
        showCancel: true,
        cancelColor:
          theme === 'dark' ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)',
        success: ({ confirm, cancel }) => {
          if (confirm) {
            // 跳转详情
            ty.openInnerH5({ url: commodityUrl })
          }
        },
      })
    } else {
      // 涂鸦默认赠送
      ty.showModal({
        title: '',
        content: Strings.getLang('serviceActive'),
        showCancel: false,
        confirmText: Strings.getLang('confirm'),
      })
    }
    await setCServicesPop(devId)
  }
}

因为有些设备是 ble + x 设置,下发 DP 操作时需要做一些校验

代码路径 hooks/useCheckPermission

// 检查DP下发权限
const checkPermissions = ({
  dpCode,
  successCb,
  cancelCb,
}: {
  dpCode: string
  successCb: () => void
  cancelCb?: () => void
}) => {
  const { extContent, dpId } = dpSchema[dpCode]
  const isExtNull = extContent === '' || extContent === undefined
  const route = isExtNull ? 0 : JSON.parse(extContent).route
  // 若智能服务已到期/未配置赠送用户未购买时,当用户通过蜂窝通道控制车辆时
  if (
    !inService &&
    isPidHadVAS &&
    (route === 0 || (route === 1 && !isBleOnline) || isExtNull)
  ) {
    // 当用户通过蜂窝通道控制车辆时:route === 0 || isExtNull || route === 1 && !isBleOnline
    ty.showModal({
      title: '',
      content: Strings.getLang('expireControl'),
      showCancel: false,
      confirmText: Strings.getLang('confirm'),
      success: ({ confirm }) => {
        if (confirm && dpCode === 'blelock_switch') {
          cancelCb()
        }
      },
    })
    return
  }
  // 非ble+x设备或者蜂窝未激活, 拓展模块是禁用的则提示:扩展模块未激活,该功能无法使用
  const notCheckHide = isBleXDevice && isActive
  if (
    !notCheckHide &&
    interactionType === 'disable' &&
    assocaitedDps.indexOf(dpId) !== -1
  ) {
    ty.showModal({
      title: '',
      content: Strings.getLang('extensionDisable'),
      confirmText: Strings.getLang('confirm'),
      cancelText: Strings.getLang('cancel'),
      success: ({ confirm, cancel }) => {
        if (confirm && dpCode === 'blelock_switch') {
          cancelCb()
        }
        if (cancel && dpCode === 'blelock_switch') {
          cancelCb()
        }
      },
    })
    return
  }

  // 设备为蜂窝+蓝牙设备,当DP的route=0或1时,并且仅蜂窝在线时,部分DP进行二次确认弹窗提示:
  const checkDpArray = [
    dpCodes.start,
    dpCodes.blelockSwitch,
    dpCodes.bucketLock,
    dpCodes.tailBoxLock,
  ]
  if (
    isBleXDevice &&
    checkDpArray.indexOf(dpCode) !== -1 &&
    isOnline &&
    !isBleOnline
  ) {
    if ([0, 1].indexOf(route) === -1) return
    const val = dpValueS[dpCode]
    if (val) return
    ty.showModal({
      title: '',
      content: Strings.getLang(`blex_wifi_${dpCode}` as any),
      confirmText: Strings.getLang('confirm'),
      cancelText: Strings.getLang('cancel'),
      success: ({ confirm, cancel }) => {
        if (confirm) {
          if (dpCode === 'blelock_switch') {
            successCb()
          } else {
            actions[dpCode].set(true)
          }
        }
        if (cancel) {
          if (dpCode === 'blelock_switch') {
            cancelCb && cancelCb()
          }
          console.log('取消 :>> ')
        }
      },
    })
    return
  }
  // ble+x设备并且蜂窝模组激活后,判断DP的高级能力
  if (isBleXDevice && isActive && route === 2 && !isBleOnline) {
    ty.showModal({
      title: '',
      content: Strings.getLang('retryConnectBle'),
      showCancel: false,
      confirmText: Strings.getLang('confirm'),
    })
    return
  }
  successCb()
}

定位

车辆体检

电池信息

骑行轨迹

骑行记录