在此 Codelab 中,您将利用面板小程序开发构建出一个蓝牙门锁面板,通过 DP 协议、接口将实现以下能力:
支持蓝牙(通过网关)开关门、成员管理、临时密码设置、开关门日志、告警日志、常规门锁设置项的
详见 面板小程序 - 搭建环境
蓝牙门锁
首页
日志页
成员管理页
成员详情页
添加开锁页
添加开锁成功页
添加开锁失败页
临时密码页
临时密码列表页
临时密码失效列表页
临时密码清空码页
设置页
当前门锁模板必须的功能点,实现了蓝牙本地开锁逻辑
check_code_set,
ble_unlock_check,
参数 | 取值 |
dpid | 70 |
code | check_code_set |
type | 透传型(Raw) |
mode | 可下发可上报(rw) |
参数 | 取值 |
dpid | 71 |
code | ble_unlock_check |
type | 透传型(Raw) |
mode | 可下发可上报(rw) |
如果需要实现蓝牙关锁,请选择manual_lock
功能点;远程开锁请选择remote_no_dp_key
、ble_unlock_check
功能点。功能点详细说明请参考蓝牙门锁 DP。
首先需要创建一个门锁类产品,定义产品有哪些功能点,然后面板中再根据这些功能点一一实现。
进入IoT 平台,点击左侧产品菜单,产品开发,创建产品,选择标准类目 -> 智能锁 -> 家用门锁PRO -> 产品方案 -> 通讯协议选择蓝牙
。:
选择功能点,这里我们只需要默认的标准功能即可。
这部分我们在 小程序开发者
平台上进行操作,注册登录 小程序开发者平台 进行操作。
详细的操作路径可参考 面板小程序 - 创建面板小程序
这部分我们在 Tuya MiniApp Tools
上进行操作,打开 IDE 创建一个基于门锁蓝牙模板的面板小程序项目。
详细的操作路径可参考 面板小程序 - 初始化项目工程
上面的步骤我们已经初始化好了一个面板小程序
的开发模板,下面我们介绍下工程目录。
├── src
│ ├── api // 云端API
│ │ ├── base.ts // 基类API汇集
│ │ ├── bleApi.ts // 蓝牙相关API
│ │ ├── getCachedLaunchOptions.ts // 获取启动参数
│ │ ├── getCachedSystemInfo.ts // 获取设备信息
│ │ ├── homeApi.ts // 首页相关API
│ │ ├── interface.ts // API接口定义
│ │ ├── logsApi.ts // 日志相关API
│ │ ├── setting.ts // 设置相关API
│ │ └── index.ts
│ ├── components
│ │ ├── DataRangePicker // 日期范围选择组件
│ │ ├── DateTimePicker // 日期时间选择组件
│ │ ├── DefendTime // 布防组件
│ │ ├── Empty // 空状态组件
│ │ ├── FamilyIcon // 家庭图标组件
│ │ ├── LatestLog // 最近一条日志组件
│ │ ├── OfflinePwdFooter // 离线密码底部组件
│ │ ├── PanelList // 列表组件
│ │ ├── PasswordIcon // 密码图标组件
│ │ ├── PasswordNameInput // 密码名称输入组件
│ │ ├── PasswordPanel // 密码组件
│ │ ├── RandomPassword // 随机密码组件
│ │ ├── RecordItem // 记录组件
│ │ ├── RemoteOpenDoor // 远程开门组件
│ │ ├── RotateImage // 旋转图片组件
│ │ ├── StatusBar // 门锁状态栏
│ │ ├── connect.tsx
│ │ └── index.ts
│ ├── config // 常用配置
│ ├── constant // 放置常用变量
│ ├── i18n // 多语言
│ ├── hooks // 常用hooks
│ ├── models // redux
│ │ ├── combine.ts // combineReducers
│ │ ├── configureStore.ts
│ │ ├── index.ts
│ │ ├── store.ts
│ │ └── modules
│ │ ├── common.ts // 基础 action 及 reducer
│ ├── pages
│ │ ├── home // 首页
│ │ ├── log // 日志
│ │ ├── family // 成员管理
│ │ ├── family-user-detail // 成员详情
│ │ ├── family-add-unlock // 添加开锁
│ │ ├── family-edit-unlock // 编辑开锁
│ │ ├── setting // 设置
│ │ ├── temp // 临时密码
│ │ ├── temp-record // 临时密码列表
│ │ ├── temp-invalid-record // 临时密码失效列表
│ │ ├── temp-emptyCode // 清空码
│ │ ├── temp-update // 更新自定义密码
│ ├── res // 本地图片
│ ├── utils // 常用工具方法
│ ├── app.config.ts
│ ├── app.tsx
│ ├── composeLayout.tsx // 处理监听子设备添加,解绑,DP变化等
│ ├── global.config.ts
│ ├── routes.config.ts // 配置路由
src/pages/home/index.tsx
文件:import { hooks } from "@ray-js/panel-sdk";
import { connectBLEDevice } from "@/utils/ble";
const { useDeviceOnline } = hooks;
const Home = () => {
const { deviceOnline } = useDeviceOnline();
useEffect(() => {
// 当设备离线时,主动连接蓝牙设备
if (!deviceOnline) {
connectBLEDevice();
}
}, [deviceOnline]);
return (
<View>
<Text>Home</Text>
</View>
);
};
export default Home;
src/utils/ble.ts
文件:import { showLoading, hideLoading, showToast } from "@ray-js/ray";
import Strings from "@/i18n";
import { getCachedSystemInfo } from "@/api/getCachedSystemInfo";
import { getCachedLaunchOptions } from "@/api/getCachedLaunchOptions";
const devId = getCachedLaunchOptions()?.query?.deviceId;
const systemInfo = getCachedSystemInfo();
/**
* 连接蓝牙设备
* @param {number} [timeout=10000] - 超时时间
*/
const connectBLEDevice = (timeout = 10000) => {
// 检查蓝牙是否开启
ty.device.bluetoothIsPowerOn({
success: (res) => {
if (!res) {
// 安卓设备打开蓝牙设置
if (systemInfo.platform === "android") {
ty.openSystemBluetoothSetting({
success: () => {
showLoading({ title: Strings.getLang("connectBLEDevice") });
startConnectBLEDevice(timeout);
},
fail: (res) => {
console.log("openBluetoothAdapter fail", res);
},
});
} else {
showToast({ title: Strings.getLang("openPhone"), icon: "error" });
}
} else {
showLoading({ title: Strings.getLang("connectBLEDevice") });
startConnectBLEDevice(timeout);
}
},
fail: (res) => {
showToast({ title: Strings.getLang("connectFail"), icon: "error" });
hideLoading();
},
});
};
/**
* 开始连接蓝牙设备
* @param {number} [timeout=10000] - 超时时间
*/
const startConnectBLEDevice = (timeout = 10000) => {
console.log("主动连接蓝牙设备");
ty.device.connectBluetoothDevice({
devId: devId,
timeoutMillis: timeout,
souceType: 1,
connectType: 1,
success: () => {
showToast({ title: Strings.getLang("openSuccess"), icon: "success" });
},
fail: (res) => {
showToast({ title: Strings.getLang("connectFail"), icon: "error" });
},
});
};
export { connectBLEDevice };
在代码路径 src/components/RemoteOpenDoor/index.tsx
中,通过实时获取 lock_motor_state 功能点状态,当长按 UI 按钮时,根据功能点状态进行开、关门操作。
import { useMemo, useState, useRef, useEffect } from 'react';
// 导入UI组件和工具函数
import { Text, View, Image, router, showToast, hideToast, onDpDataChange, offDpDataChange, showModal } from '@ray-js/ray';
import { utils } from '@ray-js/panel-sdk';
import { useStore } from 'react-redux';
import { RotateImage } from '@/components';
// 导入资源文件和国际化字符串
import Res from '@/res';
import Strings from '@/i18n';
// 导入工具函数和API方法
import { formatDpChange, putDpData, connectBLEDevice } from '@/utils';
import { dpCodes, globalConfig } from '@/config';
import { remoteOpen as remoteOpenApi } from '@/api/homeApi';
// 导入自定义钩子
import { usePanelStore, useDeviceOnline, useDevInfo, useDpPermission } from '@/hooks';
// 导入样式文件
import styles from './index.module.less';
// 从配置文件中获取设备属性代码
const { remoteNoDpKey, remoteUnlockCheck, manualLock } = dpCodes;
// 定义门状态枚举
enum DoorStatus {
open = 'open',
close = 'close',
offline = 'offline'
}
// 定义不同门状态下的UI展示信息
const statusInfo = {
open: { bg: Res.showOpenImage, iconBg: Res.openImage, text: Strings.getLang('close'), color: '#F67352' },
close: { bg: Res.showImage, iconBg: Res.lockImage, text: Strings.getLang('clickOpen'), color: '#5EAAFF' },
offline: { bg: Res.outLineImage, iconBg: '', text: Strings.getLang('off'), color: '#A2A2A2' }
};
// 定义RemoteOpenDoor组件
const RemoteOpenDoor = () => {
// 使用Redux store
const store = useStore();
// 使用自定义钩子获取设备状态和属性
const { dpState, lockData, deviceProps, user } = usePanelStore();
const deviceOnline = useDeviceOnline();
const devInfo = useDevInfo();
const { dpRemotePdSetkeyCheck, dpUnlockRecordCheck } = useDpPermission();
const { remoteSwitch, bleOnlineState } = deviceProps;
const { lock_motor_state } = dpState;
const { isGW = false } = devInfo || {};
// 定义组件内部状态
const [loading, setLoading] = useState(false);
const timer = useRef(null);
const loadRef = useRef(loading);
const themeColor = globalConfig.getConfig('themeColor') as string;
// 使用useMemo计算门的状态
const status = useMemo(() => deviceOnline ? (lock_motor_state ? DoorStatus.open : DoorStatus.close) : DoorStatus.offline, [deviceOnline, lock_motor_state]);
// 根据门的状态获取显示文本
const text = useMemo(() => statusInfo[status].text, [status, loading, deviceOnline, lock_motor_state]);
// 监听数据点变化
useEffect(() => {
onDpDataChange(dpDataChangeHandle);
return () => offDpDataChange(dpDataChangeHandle);
}, []);
// 同步loading状态到ref
useEffect(() => {
loadRef.current = loading;
}, [loading]);
// 功能点变化处理函数
const dpDataChangeHandle = data => {
const { getState } = store;
const { devInfo: _devInfo } = getState();
const _dpState = formatDpChange(_devInfo.arrSchema, data.dps);
if (Object.keys(_dpState).length > 10) return;
// 处理功能点变化逻辑...
};
// 远程开门函数
const remoteOpen = async () => {
if (loading) return;
if (!deviceOnline) {
showModal({
title: Strings.getLang('off'), content: Strings.getLang('timeout'), confirmText: Strings.getLang('confirm'), confirmColor: themeColor, showCancel: false,
success: res => { if (res.confirm) { connectBLEDevice(); } }
});
return;
}
setLoading(true);
// 执行远程开门逻辑...
};
// 设置超时计时器
const setTimer = () => {
timer.current = setTimeout(() => {
if (loadRef.current) {
showToast({ title: Strings.getLang('timeout'), icon: 'none' });
} else {
hideToast();
}
setLoading(false);
}, 15000);
};
// 清除计时器
const clearTimer = () => {
timer.current && clearTimeout(timer.current);
};
// 组件渲染逻辑
return (
<View className={styles.container}>
{status !== DoorStatus.offline && (
<RotateImage active={loading} rotateImage={statusInfo[status].bg} onLongClick={() => remoteOpen()}>
<View className={styles.centerView}>
{!!statusInfo[status].iconBg && <Image src={statusInfo[status].iconBg} className={styles.centerImage} />}
<Text className={styles.lockText} style={{ color: statusInfo[status].color}}>{text}</Text>
</View>
</RotateImage>
)}
{status === DoorStatus.offline && (
<View className={styles.outLineWrap}>
<Image src={statusInfo[status].bg} className={styles.outLineBg} />
<Text className={styles.outLineText} style={{ color: statusInfo[status].color }}>{text}</Text>
</View>
)}
</View>
);
};
export default RemoteOpenDoor;
以上代码中的中文注释提供了对每个部分的详细解释,帮助开发者或其他阅读者更好地理解代码的结构和功能。
引入了代码之后,我们的首页有了一个可长按点击的圆形按钮,这时我们可以修改 IDE 右侧控制面板 -> lock_motor_state
-> false/true 进行虚拟面板 dp 点的下发与上报,查看 IDE 控制面板的状态来验证我们的产品功能
在代码路径src/pages/family-user-detail/index.tsx
中新增、编辑、删除开锁方式代码实现。
const addUnlock = (dpCode) => {
// 普通用户无权限新增
if (ordinaryUser) {
showToast({ title: Strings.getLang('hasNoPermissionAdd'), icon: 'none' });
return;
}
// 设备离线时尝试连接蓝牙
if (!deviceOnline) {
connectBLEDevice();
return;
}
// 组装 DP 数据
const type = getModeType[dpCode];
const phase = '00'; // 录入开始阶段
const adminType = getUserType[userType];
const _userId = utils.numToHexString(lockUserId || userId); // 用户ID
const hardwareNumber = 'ff'; // 硬件编号
const time = timeScheduleInfo.userTimeSet; // 时效性
const dpData = `${type}${phase}${adminType}${_userId}${hardwareNumber}${time}000000`;
putDpDatas.current = dpData;
// 密码直接进入录入页面
if (dpCode === unlockPassword) {
router.push(`/family-add-unlock?dpCode=${dpCode}&userId=${userId}&putDpDatas=${dpData}`);
return;
}
// 发送 DP 数据以创建解锁方式
showToast({ title: Strings.getLang('connecting'), icon: 'none' });
putDpData(unlockMethodCreate, dpData);
};
src/pages/family-user-detail/component/RecordItem/index.tsx
删除、修改成员解锁方式实现逻辑。import { FC, useState } from 'react';
import { View, useQuery, Text, Input, showLoading, hideLoading, router, showToast } from '@ray-js/ray';
import { useRequest } from 'ahooks';
import { usePageEvent } from 'ray';
import { globalConfig } from '@/config';
import { Api } from '@/api';
const Index: FC = () => {
const [detailInfo, setDetailInfo] = useState({ unlockName: '' });
const [name, setName] = useState('');
const query = useQuery();
// 页面展示时触发数据请求
usePageEvent('onShow', () => run());
// 获取解锁详情请求
const { run } = useRequest(() => Api.apis.fetchUnlockDetail(query.opModeId), {
onBefore: () => showLoading({ title: '', mask: true }),
onSuccess: (data) => {
setDetailInfo(data);
hideLoading();
},
onError: hideLoading,
manual: true,
ready: query && query.opModeId,
});
// 更新解锁信息请求
const { run: update } = useRequest(Api.apis.updateUnlock, {
onBefore: () => showLoading({ title: Strings.getLang('changing'), mask: true }),
onSuccess: () => {
hideLoading();
showToast({ title: Strings.getLang('Password_form_modifySuccess'), icon: 'success' });
router.back();
},
onError: () => {
hideLoading();
showToast({ title: Strings.getLang('Password_form_modifyError'), icon: 'fail' });
},
manual: true,
});
// 删除解锁信息请求
const { run: deleteUnlock } = useRequest(Api.apis.deleteUnlock, {
onBefore: () => showLoading({ title: Strings.getLang('deleting'), mask: true }),
onSuccess: () => {
hideLoading();
showToast({ title: Strings.getLang('Password_delete_success'), icon: 'success' });
router.back();
},
onError: () => {
hideLoading();
showToast({ title: Strings.getLang('Password_delete_failure'), icon: 'fail' });
},
manual: true,
});
};
export default Index;
在 src/pages/log/index.tsx
中展示门锁的开门、关门、告警、操作记录,支持按时间筛选、按类型筛选。
import React, { FC, useState, useReducer } from 'react';
import { useCreation, useRequest } from 'ahooks';
import { View, Text, ScrollView, showLoading, hideLoading } from '@ray-js/ray';
import List from '@ray-js/components-ty-cell';
import Icon from '@ray-js/components/lib/Icon';
import ActionSheet from '@ray-js/components-ty-actionsheet';
import _uniq from 'lodash/uniq';
import { globalConfig } from '@/config';
import Empty from '@/components/Empty';
import { getLogs } from '@/api/logsApi';
import { formatLog, logCategoryList, timeList, getSelectTime } from '@/utils';
import Strings from '@/i18n';
import styles from './index.module.less';
const Index: FC = () => {
// 状态管理
const [category, setCategory] = useState(''); // 当前选择的日志类别
const [tempCategory, setTempCategory] = useState(''); // 临时选择的日志类别
const [time, setTime] = useState('time'); // 当前选择的时间范围
const [tempTime, setTempTime] = useState('time'); // 临时选择的时间范围
const [, forceUpdate] = useReducer(x => x + 1, 0); // 强制更新组件
const themeColor = globalConfig.getConfig('themeColor') as string; // 主题颜色
// 引用管理
const Ref = useCreation(() => ({
lastRowKey: '', // 上次请求的最后一行的key
hasMore: false, // 是否有更多数据
logsData: [] // 日志数据
}), []);
// 获取日志数据的请求逻辑
const { run } = useRequest(() => getLogs({
logCategories: category,
startTime: getSelectTime(time)[1],
endTime: getSelectTime(time)[0],
lastRowKey: Ref.lastRowKey,
}), {
onBefore: () => showLoading({ title: '', mask: true }), // 请求前显示加载中
onSuccess(data) {
Ref.lastRowKey = data.lastRowKey;
Ref.hasMore = data.hasMore;
Ref.logsData = _uniq([...Ref.logsData, ...data.records]); // 合并去重日志数据
forceUpdate();
hideLoading(); // 请求成功后隐藏加载中
},
onError: hideLoading, // 请求失败后隐藏加载中
refreshDeps: [category, time], // 依赖项变化时重新请求
});
// 滚动到底部时触发加载更多数据
const onScrollToLower = () => {
if (Ref.hasMore) run();
};
// 类别选择模态框逻辑
const { modal: categoryModal, setVisible: setShowCategory, visible: showCategory } = ActionSheet.useActionSheet({
header: Strings.getLang('pleaseSelect'),
onOk: () => {
Ref.lastRowKey = '';
Ref.logsData = [];
setCategory(tempCategory);
setShowCategory(false);
},
onCancel: () => setShowCategory(false),
okText: Strings.getLang('confirm'),
cancelText: Strings.getLang('cancel'),
onClickOverlay: () => setShowCategory(false),
content: () => (
<ScrollView scrollY className={styles.modalWarp}>
<List.Row
dataSource={logCategoryList}
rowKey="key"
renderItem={item => (
<List.Item
className={styles.listItem}
title={item.title}
onClick={() => setTempCategory(item.key)}
content={item.key === tempCategory ? <Icon type="icon-checkmark" size={24} color={themeColor} /> : null}
/>
)}
splitStyle={{ width: '100%', left: 0 }}
/>
</ScrollView>
),
});
return (
<>
<View className={styles.container}>
<View className={styles.tabItem} onClick={() => setShowCategory(!showCategory)}>
<Text>{logCategoryList.find(item => item.key === category)?.title || Strings.getLang('allCategories')}</Text>
<Icon type="icon-down" size={24} color="#999" />
</View>
<View className={styles.tabItem} onClick={() => setShowTime(!showTime)}>
<Text>{timeList.find(item => item.key === time)?.title || Strings.getLang('allTimes')}</Text>
<Icon type="icon-down" size={24} color="#999" />
</View>
</View>
{categoryModal}
{timeModal}
<ScrollView scrollY className={styles.warp} onScrollToLower={onScrollToLower}>
{Ref.logsData.length > 0 ? (
Ref.logsData.map(item => {
const { formatTime, formatUserName, openText } = formatLog(item);
return (
<View key={item.logId} className={styles.item}>
<Text>{`${formatTime} ${formatUserName ? `【${formatUserName}】` : ''} ${openText}`}</Text>
</View>
);
})
) : (
<Empty />
)}
</ScrollView>
</>
);
};
export default Index;
因为整体临时密码代码量颇多,仅展示逻辑最复杂部分,在 src/api/bleApi.ts
中展示临时密码中的自定义密码的新增、删除、修改功能。
import { utils } from '@ray-js/panel-sdk';
import {
showToast,
onDpDataChange,
offDpDataChange,
deleteTemporaryPassword,
createTemporaryPassword,
updateTempPassword,
} from '@ray-js/ray';
import { padStart } from 'lodash';
import Strings from '@/i18n';
import { store } from '@/redux';
import { putDpData } from '@/utils/sendDp';
import { connectBLEDevice } from '@/utils/ble';
import { ApiCreatorBase } from './base';
import { getCachedLaunchOptions } from './getCachedLaunchOptions';
import {
ICreatePasswordParams,
IDeleteParams,
IUpdateParams,
IScheduleType,
ICreatePasswordResult,
} from './interface';
const { numToHexString } = utils;
/** 蓝牙公版接口 */
export default class BlueToothAPi extends ApiCreatorBase {
/**
* 创建临时密码
* @param params 创建密码的参数
* @returns 创建成功的结果
*/
createPwd<T = ICreatePasswordParams, R = ICreatePasswordResult>(params: T): Promise<R> {
return this.listenCreateDp(params)
.then((res: R) => {
offDpDataChange(() => {});
return res;
});
}
/**
* 更新临时密码
* @param params 更新密码的参数
* @returns 更新是否成功
*/
updatePwd(params: IUpdateParams): Promise<boolean> {
return this.listenUpdateDp(params)
.then(() => {
offDpDataChange(() => {});
return true;
});
}
/**
* 删除临时密码 API 调用
* @param params 删除密码的参数
* @returns 删除是否成功
*/
deletePwdApi(params: Omit<IDeleteParams, 'deviceOnline'>): Promise<boolean> {
const devId = getCachedLaunchOptions()?.query?.deviceId;
return deleteTemporaryPassword({
devId,
unlockBindingId: params.id,
});
}
/**
* 删除临时密码,先判断设备是否在线
* @param params 删除密码的参数
* @returns 删除是否成功
*/
deletePwd({ deviceOnline, ...restParams }: IDeleteParams): Promise<boolean> {
if (!deviceOnline) {
connectBLEDevice();
return Promise.reject({ message: Strings.getLang('off') });
}
if (restParams.pwdType === 0) {
putDpData('temporary_password_delete', numToHexString(restParams.sn as number, 2));
return this.listenDeleteDp(restParams.id as number).then(() => true);
}
return this.checkAndDeletePwd(restParams);
}
/**
* 监听删除临时密码的 DP 数据变化
* @param deleteId 删除密码的 ID
* @returns 删除是否成功的 Promise
*/
listenDeleteDp = (deleteId: number) => {
return new Promise((resolve, reject) => {
onDpDataChange(data => {
const { devInfo } = store.getState();
const { schema } = devInfo;
const dpId = schema['temporary_password_delete'].id;
const temPwdDelete = data.dps[dpId] as string;
if (temPwdDelete) {
const status = temPwdDelete.slice(2, 4); // 返回阶段
if (status === '00') {
this.deletePwdApi({ id: deleteId })
.then(resolve)
.catch(reject);
showToast({ title: Strings.getLang('Password_delete_success'), icon: 'success' });
} else {
reject();
showToast({ title: Strings.getLang('Password_delete_failure'), icon: 'error' });
}
}
});
});
};
/**
* 监听创建临时密码的 DP 数据变化
* @param params 创建密码的参数
* @returns 创建成功的结果
*/
listenCreateDp = async <R = any, T = ICreatePasswordResult>(params: any) => {
const scheduleInfo = {
beginTime: params.effectiveTime,
endTime: params.invalidTime,
allDay: true,
effectiveTime: 0,
invalidTime: 0,
workingDay: '',
};
Object.assign(scheduleInfo, params.schedule && JSON.parse(params.schedule)[0]);
const dpData = this.getCreatePwdDpData(scheduleInfo, params.originalPassword);
putDpData('temporary_password_creat', dpData);
return new Promise((resolve, reject) => {
onDpDataChange(data => {
const { devInfo } = store.getState();
const { schema } = devInfo;
const dpId = schema['temporary_password_creat'].id;
const temPwdCreate = data.dps[dpId] as string;
if (temPwdCreate) {
const devId = getCachedLaunchOptions()?.query?.deviceId;
const unLockId = parseInt(temPwdCreate.slice(0, 2), 16);
const status = temPwdCreate.slice(2, 4); // 返回阶段
if (status === '00') {
resolve(
createTemporaryPassword({
devId,
symbolic: true, // 是否仅保存静态数据
dpTunnel: 2, // symbolic 为 false 时,必填 1. app 2. 云端
availTime: 0,
sn: unLockId,
...params,
})
);
} else {
reject({ message: Strings.getLang(`Password_add_error_${status}` as any) });
}
}
});
});
};
/**
* 监听更新临时密码的 DP 数据变化
* @param params 更新密码的参数
* @returns 更新是否成功的 Promise
*/
listenUpdateDp = async (params: IUpdateParams) => {
const scheduleInfo = {
beginTime: params.effectiveTime,
endTime: params.invalidTime,
allDay: true,
effectiveTime: 0,
invalidTime: 0,
workingDay: '',
sn: params.sn,
};
Object.assign(scheduleInfo, params.schedule && JSON.parse(params.schedule)[0]);
const dpData = this.getUpdatePwdDpData(scheduleInfo);
putDpData('temporary_password_modify', dpData);
return new Promise((resolve, reject) => {
onDpDataChange(data => {
const { devInfo } = store.getState();
const { schema } = devInfo;
const dpId = schema['temporary_password_modify'].id;
const temPwdUpdate = data.dps[dpId] as string;
if (temPwdUpdate) {
const status = temPwdUpdate.slice(2, 4); // 返回阶段
if (status === '00') {
const updateData = { unlockBindingId: params.id, ...params };
delete updateData.id;
resolve(updateTempPassword(updateData));
} else {
reject({ message: Strings.getLang('Password_form_modifyError') });
}
}
});
});
};
/**
* 获取更新临时密码的 DP 数据
* @param params 调度信息
* @returns 更新临时密码的 DP 数据
*/
getUpdatePwdDpData = (params: IScheduleType): string => {
const type = '01';
const count = '00';
const pswLen = '00'; // 修改临时密码,密码长度直接设置为 0
const { allDay = true, beginTime, endTime, effectiveTime, invalidTime, workingDay } = params;
let time = '';
const unLockId = padStart(params.sn?.toString(16), 2, '0');
const beginTime2Hex = parseInt(String(beginTime)).toString(16);
const endTime2Hex = parseInt(String(endTime)).toString(16);
time += `${beginTime2Hex}${endTime2Hex}`;
if (allDay) {
time += '000000000000000000';
} else {
time += `02000000${padStart(parseInt(workingDay).toString(16), 2, '0')}`;
time += `${padStart(parseInt(`${effectiveTime / 60}`, 10).toString(16), 2, '0')}${padStart(
parseInt(`${+effectiveTime % 60}`, 10).toString(16),
2,
'0'
)}`;
time += `${padStart(parseInt(`${+invalidTime / 60}`, 10).toString(16), 2, '0')}${padStart(
parseInt(`${+invalidTime % 60}`, 10).toString(16),
2,
'0'
)}`;
}
return `${unLockId}${type}${time}${count}${pswLen}`;
};
/**
* 获取创建临时密码的 DP 数据
* @param params 调度信息
* @param originalPassword 原始密码
* @returns 创建临时密码的 DP 数据
*/
getCreatePwdDpData = (params: IScheduleType, originalPassword: string): string => {
const type = '01';
const count = '00';
const pswLen = padStart(originalPassword.length.toString(16), 2, '0');
const resPassword = [...originalPassword].map(i => padStart(Number(i).toString(16), 2, '0')).join('');
const { allDay = true, beginTime, endTime, effectiveTime, invalidTime, workingDay } = params;
let time = '';
const beginTime2Hex = parseInt(String(beginTime)).toString(16);
const endTime2Hex = parseInt(String(endTime)).toString(16);
time += `${beginTime2Hex}${endTime2Hex}`;
if (allDay) {
time += '000000000000000000';
} else {
time += `02000000${padStart(parseInt(workingDay).toString(16), 2, '0')}`;
time += `${padStart(parseInt(`${effectiveTime / 60}`, 10).toString(16), 2, '0')}${padStart(
parseInt(`${+effectiveTime % 60}`, 10).toString(16),
2,
'0'
)}`;
time += `${padStart(parseInt(`${+invalidTime / 60}`, 10).toString(16), 2, '0')}${padStart(
parseInt(`${+invalidTime % 60}`, 10).toString(16),
2,
'0'
)}`;
}
return `${type}${time}${count}${pswLen}${resPassword}`;
};
}
在 src/pages/setting/index.tsx
中展示设备的设置项,包括远程开锁开关、自动落锁、反锁、语音提示、离家布防、忽扰等功能。
import React, { FC, useState } from 'react';
import { useCreation } from 'ahooks';
import { DpSchema, utils } from '@ray-js/panel-sdk';
import { View, Text, ScrollView, Slider } from '@ray-js/ray';
import { globalConfig, Code } from '@/config';
import { putDpData } from '@/utils/sendDp';
import { connectBLEDevice } from '@/utils/ble';
import { saveDeviceProps } from '@/redux/actions';
import { getSettingDpSchema } from '@/utils/getSettingDpSchema';
import { usePanelStore, useDevInfo, useDeviceOnline, useUserPermission } from '@/hooks';
import { uploadSettingRecord, setDeviceProps } from '@/api/settingApi';
import Icon from '@ray-js/components/lib/Icon';
import List from '@ray-js/components-ty-cell';
import Switch from '@ray-js/components-ty-switch';
import ActionSheet from '@ray-js/components-ty-actionsheet';
import Strings from '@/i18n';
import Codes from '@/config/dpCodes';
import styles from './index.module.less';
const Index: FC = () => {
// 状态管理
const [dpCode, setDpCode] = useState('');
const [enumValue, setEnumValue] = useState('');
const [numberValue, setNumberValue] = useState(0);
const { dpState, deviceProps } = usePanelStore();
const { isOwnerOrAdmin } = useUserPermission();
const { schema } = useDevInfo();
const deviceOnline = useDeviceOnline();
const { remoteSwitch } = deviceProps;
const themeColor = globalConfig.getConfig('themeColor') as string;
// 获取设置的 DP Schema
const settingsSchema = useCreation(() => {
return getSettingDpSchema(
schema as DpSchema[],
isOwnerOrAdmin ? Code.logTypeFormDpCodes : Code.noAdminLogTypeFormDpCodes
);
}, [schema]);
// 获取枚举类型的 DP 数据源
const enumDpDataSource = useCreation(() => {
return settingsSchema
.find(d => d.code === dpCode)
?.property?.range?.map((rangeValue: string) => ({
label: Strings.getDpLang(dpCode, rangeValue),
value: rangeValue,
checked: rangeValue === enumValue,
}));
}, [dpCode, enumValue]);
// 获取数值类型的 DP 数据
const numberDpData = useCreation(() => {
return settingsSchema.find(d => d.code === dpCode)?.property;
}, [dpCode]);
// 枚举类型的模态框逻辑
const {
modal: enumModal,
setVisible: setShowEnum,
visible: showEnum,
} = ActionSheet.useActionSheet({
header: Strings.getLang('pleaseSelect'),
onOk: () => {
putDpData(dpCode, enumValue);
setShowEnum(false);
uploadSettingRecord(dpCode, [enumValue]);
},
onCancel: () => setShowEnum(false),
onClickOverlay: () => setShowEnum(false),
okText: Strings.getLang('confirm'),
cancelText: Strings.getLang('cancel'),
content: () => (
<ScrollView scrollY className={styles.listWarp}>
<List.Row
className={styles.list}
dataSource={enumDpDataSource}
rowKey="label"
renderItem={item => (
<List.Item
className={styles.listItem}
title={item.label}
onClick={() => setEnumValue(item.value)}
content={item.checked ? <Icon type="icon-checkmark" size={24} color={themeColor} /> : null}
/>
)}
splitStyle={{ width: '100%', left: 0 }}
/>
</ScrollView>
),
});
return (
<ScrollView scrollY className={styles.warp}>
<List>
{/* 远程开门开关 */}
<List.Item
title={Strings.getDpLang(Codes.remoteNoDpKey)}
content={
<Switch
style={{ marginRight: 0 }}
checkedColor={themeColor}
checked={remoteSwitch}
onChange={value => {
setDeviceProps(JSON.stringify({ UNLOCK_PHONE_REMOTE: value }));
uploadSettingRecord('remote_unlock_setting', [value.toString()]);
saveDeviceProps({ remoteSwitch: value });
}}
/>
}
/>
{/* 渲染 DP 项 */}
{settingsSchema.map(item => {
let content = <></>;
const dpMode = item.mode;
const dpType = item.property?.type;
const dpUnit = item.property?.unit ?? '';
const dpScale = item.property?.scale ?? 0;
switch (dpType) {
case 'bool':
content = (
<Switch
style={{ marginRight: 0 }}
checkedColor={themeColor}
checked={!!dpState[item.code]}
onChange={value => {
if (!deviceOnline) {
connectBLEDevice();
return;
}
putDpData(item.code, value);
}}
/>
);
break;
case 'enum':
content = (
<View className={styles.valueView}>
<Text className={styles.text}>{Strings.getDpLang(item.code, dpState[item.code])}</Text>
<Icon type="icon-right" size={24} color={'rgba(0,0,0,0.2)'} />
</View>
);
break;
case 'value':
content = (
<View className={styles.valueView}>
<Text className={styles.text}>{`${utils.scaleNumber(dpScale, dpState[item.code])}${dpUnit}`}</Text>
<Icon type="icon-right" size={24} color={'rgba(0,0,0,0.2)'} />
</View>
);
break;
default:
break;
}
return (
<List.Item
key={item.code}
title={Strings.getDpLang(item.code)}
content={content}
onClick={() => {
if (dpMode === 'ro') return;
if (!['enum', 'value'].includes(dpType)) return;
if (!deviceOnline) {
connectBLEDevice();
return;
}
setDpCode(item.code);
if (dpType === 'enum') {
setEnumValue(dpState[item.code]);
setShowEnum(true);
} else if (dpType === 'value') {
setNumberValue(dpState[item.code]);
setShowValue(true);
}
}}
/>
);
})}
</List>
{enumModal}
{valueModal}
</ScrollView>
);
};
export default Index;