Commit 5c20c32e authored by zhushengjie's avatar zhushengjie

Initial

parents
Pipeline #475 failed with stages
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 80
trim_trailing_whitespace = true
[*.md]
max_line_length = 0
trim_trailing_whitespace = false
[COMMIT_EDITMSG]
max_line_length = 100
[Makefile]
indent_style = tab
tab_width = 2
[.gitmodules]
indent_style = tab
tab_width = 2
src/app.config.ts
\ No newline at end of file
module.exports = {
extends: ['tuya-panel'],
rules: {
camelcase: 0,
'@typescript-eslint/ban-ts-comment': 0,
'@typescript-eslint/explicit-module-boundary-types': 0,
},
};
node_modules
.ray
dist
app.config.ts
registry=https://registry.npmmirror.com
module.exports = require('eslint-config-tuya-panel/.prettierrc.js');
\ No newline at end of file
# 涂鸦面板模板 - jotai
该模板基于 Ray 框架开发,提供给需要在涂鸦智能、智能生活等 App 中开发面板的业务开发人员,代码提供了 Ray 开发的基本使用方法,包含路由使用、页面跳转、设备操作、多语言等内容的使用,开发人员可参照用例并根据业务需求自行调整代码。
## 使用须知
使用该模板开发前, 需要对 Ray 框架有基本的了解,建议先查阅 [Ray 开发文档](https://developer.tuya.com/cn/ray)
## 目录结构
```
.
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .npmrc
├── .prettierrc.js
├── README.md
├── commitlint.config.js
├── package.json
├── project.tuya.json // IDE 项目配置, 参照 https://developer.tuya.com/cn/ray/guide/tutorial/directory
├── ray.config.ts // Ray 工程配置文件, 参照 https://developer.tuya.com/cn/ray/guide/tutorial/directory
├── src
│ ├── api // 接口目录
│ ├── app.config.ts // 运行项目时默认生成的文件,该文件不需要进行 git 提交。
│ ├── app.tsx // 项目入口文件
│ ├── atoms // jotai 状态管理目录
│ ├── components // 组件目录
│ ├── global.config.ts // 项目全局配置项,参照 https://developer.tuya.com/cn/ray/guide/tutorial/global-config
│ ├── i18n // 多语言本地配置
│ ├── pages // 页面目录,存放所有页面组件源码,至少存在 index.tsx 文件。
│ ├── res // 静态资源目录
│ ├── routes.config.ts // 路由配置 参照 https://developer.tuya.com/cn/ray/guide/tutorial/routes
│ ├── utils // 工具方法目录
│ ├── variables.less // 全局 less 变量
│ └── withDevicePanel.tsx // 入口高阶,处理设备相关逻辑
├── tsconfig.json // TS 配置文件
├── typings // 全局项目 TS Typing 定义目录
│ └── index.d.ts
```
## 组件的使用
使用 Ray 框架开发,若要兼容多端方案,则需要使用 Ray 框架所提供的组件,具体可参照 [Ray 组件文档](https://developer.tuya.com/cn/ray/components)
## 基本操作及 API
Ray 框架提供了大量的 API 可供业务直接调用, 该 API 已对各平台做了兼容处理,开发者不再需要运行时区分平台编写代码。 具体可参照 [API 文档](https://developer.tuya.com/cn/ray/api/authorize)
## 设备操作
为满足面板开发需求,Ray 框架还提供了设备操作相关的 Api ,开发者对设备进行操作时,可直接使用框架提供的设备操作能力,具体可参照 [API 文档 - 设备相关能力](https://developer.tuya.com/cn/ray/api/device-kit/add-timer)
## 组件扩展
若 Ray 框架所提供的组件不能很好地满足业务需求,也可以在遵循 [跨端适配](https://developer.tuya.com/cn/ray/guide/tutorial/env) 规则的前提下,自行开发扩展组件,组件中的样式需要遵循 [样式规则](https://developer.tuya.com/cn/ray/guide/tutorial/stylesheet)
## 面板 SDK
面板开发需要的一些方法, 我们将其做了封装,可能直接使用方法所提供的能力, 具体可参照 [面板 SDK](https://developer.tuya.com/cn/ray/panel)
## 多语言
面板模板中使用的多语言,封装了产品多语言和 UI 多语言, 可在业务中直接使用,具体可参照 [多语言](https://developer.tuya.com/cn/ray/panel/i18n/i18n)。 若该库无法满足业务需求,开发人员可基于 Ray API 提供的 [getLangKey](https://developer.tuya.com/cn/ray/api/get-lang-key)[getLangContent](https://developer.tuya.com/cn/ray/api/get-lang-content) 自行扩展多语言方案。
## 状态管理
模板中使用了 jotai 进行数据管理,下方简单介绍下基础用法,具体可参照 [Jotai 官方文档](https://jotai.org/docs/introduction)
### 定义 atom
```typescript
import { atom } from 'jotai';
import { selectAtom } from 'jotai/utils';
import deepEqual from 'fast-deep-equal';
/**
* 在此处定义你当前产品的 DP 类型
*/
export interface DpState {
switch: boolean;
}
/**
* 定义一个 dpStateAtom
*
* 第一个参数传入默认值
* 第二个参数传入接受到 payload 时如何更新数据源
*
* @docs 详见 https://jotai.org/docs/basics/primitives
*/
export const dpStateAtom = atom(null as DpState, (get, set, payload: DpState) => {
set(dpStateAtom, { ...(get(dpStateAtom) || {}), ...payload });
});
/**
* 定义一个基于 dpStateAtom 的选择器
*
* 第一个参数传入源 atom 数据
* 第二个参数传入 selector 选择器匹配需要返回的数据
* 第三个参数传入 equalityFn 判断是否一致,有助于避免无效的渲染,提高性能,
*
* @docs 详见 https://jotai.org/docs/utils/select-atom
*/
export const selectDpStateAtom = selectAtom<DpState, DpState>(dpStateAtom, data => data, deepEqual);
```
### 获取 atom 数据
```typescript
import { useAtomValue } from 'jotai';
import { dpStateAtom, selectDpStateAtom } from '@/atoms';
const Home = () => {
const dpState = useAtomValue(selectDpStateAtom);
...
}
```
### 修改 atom 数据
```typescript
import { useSetAtom } from 'jotai';
const Home = () => {
const setDpState = useSetAtom(dpStateAtom);
...
setDpState({ switch: true })
}
```
## 能力分包
Ray 框架中设备控制、 小程序基本能力、 涂鸦特有能力等都需要 [能力包](https://developer.tuya.com/cn/miniapp/api#%E8%83%BD%E5%8A%9B%E5%88%86%E5%8C%85) 的支持, 模板默认添加了 BaseKit、 TYKit、MiniKit、DeviceKit 四大能力集, 开发者可根据需求进行管理。也可参照 [API 能力集成指南](https://developer.tuya.com/cn/miniapp/api) 自行调整。
## 依赖说明
- @ray-js/components: Ray 跨端基础组件库
- @ray-js/framework: Ray 跨端框架主包
- @ray-js/panel-sdk: 一系列可能在你开发控制设备面板业务的时候需要用到的工具库。
- fast-deep-equal: 对象深比较库,用于 jotai 的 selectAtom,提高渲染性能
- jotai: 原子化的 React 状态管理库
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-empty': [2, 'never'],
'subject-empty': [2, 'never'],
'scope-case': [1, 'always', 'pascal-case'],
'subject-case': [0],
'subject-full-stop': [0],
},
};
{
"name": "panel-jotai",
"version": "1.0.0",
"description": "",
"main": "index.js",
"private": true,
"scripts": {
"lint": "eslint src --ext .js,.jsx,.ts,.tsx --fix",
"watch": "ray start --target=tuya",
"build:tuya": "ray build --target=tuya",
"build:wechat": "ray build --target=wechat",
"build:web": "ray build --target=web",
"build:native": "ray build --target=native",
"start:tuya": "ray start --target=tuya",
"start:wechat": "ray start --target=wechat",
"start:web": "ray start --target=web",
"start:native": "ray start --target=native"
},
"dependencies": {
"@ray-js/ray": "^0.6.15",
"@ray-js/panel-sdk": "^1.1.4",
"fast-deep-equal": "^3.1.3",
"jotai": "^1.7.0",
"lodash": "^4.17.21",
"core-js": "^3.23.5"
},
"devDependencies": {
"@commitlint/cli": "^7.2.1",
"@commitlint/config-conventional": "^9.0.1",
"@ray-js/cli": "^0.6.15",
"@types/lodash": "^4.14.182",
"@types/react": "^17.0.24",
"@types/react-dom": "^17.0.9",
"eslint-config-tuya-panel": "^0.4.1",
"husky": "^1.2.0",
"lint-staged": "^10.2.11",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"typescript": "^4.4.3"
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS --config commitlint.config.js",
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"eslint --fix",
"git add"
],
"*.{json,md,yml,yaml}": [
"prettier --write",
"git add"
]
},
"author": "",
"license": "ISC"
}
\ No newline at end of file
{
"projectname": "通用面板模板",
"i18n": false,
"description": "项目描述",
"miniprogramRoot": "./dist/tuya",
"dependencies": {
"BaseKit": "2.4.11",
"MiniKit": "2.4.7",
"TYKit": "2.2.8",
"DeviceKit": "2.4.7"
},
"baseversion": "2.9.5",
"productId": "hewd8dsaiwxpznpr"
}
\ No newline at end of file
// 提供给 cli 构建使用的文件,使用 cjs 语法
const config = {
resolveAlias: {},
};
module.exports = config;
/**
* 放一些自定义的 api
*/
import React from 'react';
import 'ray';
import '@/i18n';
import { kit } from '@ray-js/panel-sdk';
const { getDevInfo, initPanelEnvironment } = kit;
interface Props {
children: JSX.Element[] | JSX.Element | React.ReactNode;
}
initPanelEnvironment({ useDefaultOffline: true });
class App extends React.Component<Props> {
componentDidMount() {
console.log('=== App did mount', getDevInfo());
}
onLaunch() {
console.info('=== App onLaunch');
}
render() {
return this.props.children;
}
}
export default App;
import { atom } from 'jotai';
import { selectAtom } from 'jotai/utils';
import deepEqual from 'fast-deep-equal';
/**
* 在此处定义你当前产品的 DP 类型
*/
export interface DpState {
switch_led: boolean;
temp_value: number;
bright_value: number;
work_mode: string;
scene_data: string;
countdown: number;
control_data: string;
colour_data: string;
do_not_disturb: boolean;
}
type UpdateDpStatePayload = Partial<DpState>;
/**
* 定义一个 dpStateAtom
*
* 第一个参数传入默认值
* 第二个参数传入接受到 payload 时如何更新数据源
*
* 第一个泛型定义数据源的类型
* 第二个泛型定义修改数据源的 payload
*
* @docs 详见 https://jotai.org/docs/basics/primitives
*/
export const dpStateAtom = atom<DpState, UpdateDpStatePayload>(null, (get, set, payload) => {
set(dpStateAtom, { ...(get(dpStateAtom) || {}), ...payload });
});
/**
* 定义一个基于 dpStateAtom 的选择器
*
* 第一个参数传入源 atom 数据
* 第二个参数传入 selector 选择器匹配需要返回的数据
* 第三个参数传入 equalityFn 判断是否一致,有助于避免无效的渲染,提高性能,
*
* 第一个泛型定义数据源的类型
* 第二个泛型定义 selector 选择器匹配返回的数据的类型
*
* @docs 详见 https://jotai.org/docs/utils/select-atom
*/
export const selectDpStateAtom = selectAtom<DpState, DpState>(dpStateAtom, data => data, deepEqual);
export * from './dpState';
.box {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
.header {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
.circleContent {
position: relative;
z-index: 30;
background-image: url(./tempBg.png);
border:none;
.circle {
position: absolute;
z-index: 10;
left: 0;
top: 0;
border-radius: 50%;
border: 10rpx solid #ffffff;
display: flex;
justify-content: center;
align-items: center;
color: #ffffff;
font-size: 40rpx;
-webkit-user-select: none;
}
.circleBar {
position: absolute;
z-index: 50;
border-radius: 50%;
background-color: transparent;
border: 4rpx solid rgba(255, 255, 255, 1);
cursor: pointer;
box-shadow: 0 5rpx 12rpx rgba(0, 0, 0, 0.6);
}
}
}
.panel {
.configList {
margin-bottom: 50rpx;
display: flex;
justify-content: center;
align-items: center;
.cutBar {
color: red;
font-size: 24rpx;
}
.addStyle {
color: white;
font-size: 24rpx;
}
.icoImg {
width: 30rpx;
height: 30rpx;
}
.slider {
width: 500rpx;
}
.SliderValue {
color: #fff;
font-size: 16rpx;
width: 20rpx;
}
.colorCircle {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
border: 1rpx solid #fff;
}
.colorCircle:nth-child(n + 1) {
margin-left: 15rpx;
}
.cutBar {
color: red;
}
.addBar {
color: #fff;
}
}
}
.circleContent {
width: 200rpx;
height: 200rpx;
border: 1rpx solid #000;
}
.circleBar {
width: 200rpx;
height: 200rpx;
}
}
This diff is collapsed.
import { DpState } from '@/atoms';
export interface Props {
DpStateData: DpState;
DpCodesMap: Record<string, DevInfo['schema'][number]>;
onSetDpState: (dpID: string, dpValue: any) => void;
}
import React, { useState } from 'react';
import { utils } from '@ray-js/panel-sdk';
import { getElementById } from '@ray-js/api';
import { View, Text, Slider, Image, usePageEvent } from '@ray-js/ray';
import styles from './index.module.less';
import { Props } from './index.type';
import imgs from '../../res';
const HubColorCircle: React.FC<Props> = props => {
const dpState = props.DpStateData;
const codeMap = props.DpCodesMap;
const toParseInt = (value: number): number => {
return Math.floor(value);
};
return (
<View className={styles.box}>
<View className={styles.header}>123</View>
<View className={styles.panel}>
<View className={styles.configList}>
<View className={styles.configList}>
<Image src={imgs.icon_bhd} className={styles.icoImg} />
<View className={styles.slider}>
<Slider
activeColor="#fff"
backgroundColor="#333333"
min={0}
max={1000}
step={codeMap.temp_value.property.step}
value={dpState.temp_value}
onChange={e => props.onSetDpState('temp_value', e.value)}
/>
</View>
<Text className={styles.SliderValue}>
{toParseInt(
(dpState.temp_value /
(codeMap.temp_value.property.max - codeMap.temp_value.property.min)) *
100
)}
%
</Text>
</View>
</View>
</View>
</View>
);
};
import { DpState } from '@/atoms';
export interface Props {
DpStateData: DpState;
DpCodesMap: Record<string, DevInfo['schema'][number]>;
onSetDpState: (dpID: string, dpValue: any) => void;
}
.section {
margin-bottom: 48rpx;
&:first-child {
margin-top: 24px;
}
}
.title {
padding: 0px 48rpx;
vertical-align: middle;
font-size: 28rpx;
color: rgba(0, 0, 0, 0.45);
}
import React from 'react';
import { View, Text } from '@ray-js/components';
import styles from './index.module.less';
import { Props } from './index.type';
const Section: React.FC<Props> = props => {
const { title } = props;
return (
<View className={styles.section}>
<Text className={styles.title}>{`· ${title}`}</Text>
{props.children}
</View>
);
};
export default Section;
export interface Props {
title: string;
children?: React.ReactNode;
}
import { GlobalConfig } from '@ray-js/types';
export const wechat = {
window: {
backgroundColor: '#f2f4f6',
navigationBarTitleText: 'Ray 小程序示例',
navigationBarBackgroundColor: '#f2f4f6',
navigationBarTextStyle: 'black',
},
};
export const web = {
window: {
backgroundColor: '#f2f4f6',
navigationBarTitleText: 'Ray Web App',
},
};
export const tuya = {
window: {
backgroundColor: '#f2f4f6',
navigationBarTitleText: '',
navigationBarBackgroundColor: '#f2f4f6',
navigationBarTextStyle: 'black',
},
};
const globalConfig: GlobalConfig = {
basename: '',
};
export default globalConfig;
import { kit } from '@ray-js/panel-sdk';
import strings from './strings';
const { I18N } = kit;
const Strings = new I18N(strings);
export default Strings;
export default {
en: {
checkDpState: 'Check Device {0} DP State',
deviceSchemaEmptyTip:
'The current device does not have any function points, and the template displays a blank screen. If the function points are normal, delete the logic in this step',
},
zh: {
checkDpState: '查看设备{0} DP 点状态',
deviceSchemaEmptyTip:
'当前设备不存在功能点,模板会白屏状态,如为正常需求,请自行删除此处判断逻辑',
},
};
export const web = {
backgroundColor: '#f2f4f6',
navigationBarTitleText: 'App 状态',
};
export const tuya = {
backgroundColor: '#f2f4f6',
navigationBarTitleText: 'App 状态',
};
export const wechat = {
backgroundColor: '#f2f4f6',
navigationBarTitleText: 'App 状态',
};
import { View } from '@ray-js/components';
import { hooks } from '@ray-js/panel-sdk';
import React from 'react';
import Section from '@/components/section';
const { useNetwork, useBluetooth } = hooks;
function PageAppState() {
const network = useNetwork();
const bluetooth = useBluetooth();
console.log('bluetooth', bluetooth);
return (
<View style={{ flex: 1 }}>
{Object.keys(network).map(key => {
return <Section key={key} title={`network.${key}: ${network[key]}`} />;
})}
{Object.keys(bluetooth).map(key => {
return <Section key={key} title={`bluetooth.${key}: ${bluetooth[key]}`} />;
})}
</View>
);
}
export default PageAppState;
export const web = {
backgroundColor: '#f2f4f6',
navigationBarTitleText: '设备 DP 点及操作',
};
export const tuya = {
backgroundColor: '#f2f4f6',
navigationBarTitleText: '设备 DP 点及操作',
};
export const wechat = {
backgroundColor: '#f2f4f6',
navigationBarTitleText: '设备 DP 点及操作',
};
import { View } from '@ray-js/components';
import { hooks } from '@ray-js/panel-sdk';
import Strings from '@/i18n';
import React from 'react';
import Section from '@/components/section';
function PageDpState() {
const [dpState] = hooks.useDpState();
const panelConfig = hooks.usePanelConfig();
console.log(panelConfig);
return (
<View style={{ flex: 1 }}>
<View>
{Object.keys(dpState).map(key => {
return <Section key={key} title={`${Strings.getDpLang(key)}(${key}): ${dpState[key]}`} />;
})}
</View>
</View>
);
}
export default PageDpState;
export const web = {
backgroundColor: '#f2f4f6',
navigationBarTitleText: '查看路由信息',
};
export const tuya = {
backgroundColor: '#f2f4f6',
navigationBarTitleText: '查看路由信息',
};
export const wechat = {
backgroundColor: '#f2f4f6',
navigationBarTitleText: '查看路由信息',
};
import { router, location, usePageEvent } from 'ray';
import React from 'react';
import { Button, Text, View } from '@ray-js/components';
import styles from '../../home/index.module.less';
function PageLocation() {
console.log('=== location', location);
usePageEvent('onShow', () => {
console.log('=== page-location onShow');
});
usePageEvent('onHide', () => {
console.log('=== page-location onHide');
});
return (
<View>
<Button
className={styles.button}
onClick={() => {
router.back();
}}
>
<Text className={styles.bTitle}>返回上一页</Text>
</Button>
</View>
);
}
export default PageLocation;
export const web = {
backgroundColor: '#f2f4f6',
navigationBarTitleText: 'Ray 面板模板',
};
export const wechat = {
backgroundColor: '#f2f4f6',
navigationBarTitleText: 'Ray 面板模板',
};
export const tuya = {
backgroundColor: '#f2f4f6',
navigationBarTitleText: 'Ray 面板模板',
};
@subtractHeight: 200rpx;
.home {
height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: #000;
}
.content {
display: flex;
flex-direction: column;
justify-content: space-between;
height: calc(100vh - @subtractHeight);
.tabCards {
display: flex;
align-items: flex-end;
border-radius: 0 0 20rpx 20rpx;
background-color: rgba(255, 255, 255, 0.2);
height: @subtractHeight;
.tabItem {
color: white;
margin: 0 50rpx;
}
.tabItemActive {
color: #43aaff;
}
}
.tabContent {
height: calc(100vh - @subtractHeight - @subtractHeight);
}
}
.subtract {
border-radius: 20rpx 20rpx 0 0;
height: @subtractHeight;
background-color: rgba(255, 255, 255, 0.2);
margin-top: 50rpx;
}
.switch {
width: 120rpx;
height: 120rpx;
background: #43aaff;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
position: absolute;
border: 4rpx solid rgba(0, 0, 0, 0.8);
z-index: 500;
left: calc(50% - 60rpx);
bottom: 100rpx;
}
.switchActive {
background: #979797;
}
.CombinedShape {
width: 60rpx;
height: 60rpx;
}
import React, { useState, useEffect } from 'react';
import { onDpDataChange, offDpDataChange } from '@ray-js/api';
import { useAtomValue, useSetAtom } from 'jotai';
import { DpState, dpStateAtom, selectDpStateAtom } from '@/atoms';
import { getDpStateMapByDevInfo, mapDpsMapToDpStateMap, getDpCodeMapByDevInfo } from '@/utils';
import { ScrollView, View, Text, Image } from '@ray-js/components';
import HubCircle from '@/components/hubCircle';
import { hooks, kit } from '@ray-js/panel-sdk';
import styles from './index.module.less';
import imgs from '../../res';
const { useDevInfo, useDpState, usePanelConfig } = hooks;
const { getDevInfo, initDevInfo } = kit;
export function Home() {
const devInfo = useDevInfo();
const panelConfig = usePanelConfig();
const [dpState, setDpState] = useDpState();
const dpStateInUseDpState = dpState as unknown as DpState;
const setDpStateAtom = useSetAtom(dpStateAtom);
const dpStateInAtom = useAtomValue(selectDpStateAtom);
const handleDpDataChange: DpDataChangeHandler = data => {
const initalDevInfo = getDevInfo();
// 转化为 dpState 格式
const newDpState = mapDpsMapToDpStateMap(data.dps, initalDevInfo) as DpState;
console.log(newDpState, '123', dpState);
// {switch: true} 输出更改的值
setDpStateAtom(newDpState);
};
useEffect(() => {
// 绑定设备
initDevInfo().then(initalDevInfo => {
// 转化为 dpState 格式
const initialDpState = getDpStateMapByDevInfo(initalDevInfo) as DpState;
// 初始化原子
setDpStateAtom(initialDpState);
// 监听设备状态变更
onDpDataChange(handleDpDataChange);
});
return () => {
// 取消监听
offDpDataChange(handleDpDataChange);
};
}, []);
useEffect(() => {
console.log(panelConfig);
}, [panelConfig.initialized]);
const DpStateData = dpStateInUseDpState || (getDpStateMapByDevInfo(devInfo) as DpState);
const DpCodesMap = getDpCodeMapByDevInfo(devInfo);
const handleSetPdState = (dpCode: string, dpValue: any) => {
setDpState({
[dpCode]: dpValue,
});
};
const handleSwitch = (e: any) => {
e.origin.stopPropagation();
handleSetPdState('switch_led', !DpStateData.switch_led);
};
return (
<ScrollView>
<View className={styles.home}>
<View className={styles.content}>
<View className={styles.tabCards}>
<View
className={DpStateData.work_mode === 'white' ? styles.tabItemActive : styles.tabItem}
onClick={() => {
handleSetPdState('work_mode', 'white');
}}
>
White
</View>
<View
className={DpStateData.work_mode === 'colour' ? styles.tabItemActive : styles.tabItem}
onClick={() => {
handleSetPdState('work_mode', 'colour');
}}
>
Color
</View>
</View>
{DpStateData.switch_led && (
<View className={styles.tabContent}>
{DpStateData.work_mode === 'white' ? (
<HubCircle
DpStateData={DpStateData}
DpCodesMap={DpCodesMap}
onSetDpState={handleSetPdState}
/>
) : (
<View>123</View>
)}
</View>
)}
</View>
<View className={styles.subtract}>
<View
onClick={handleSwitch}
className={`${styles.switch} ${!DpStateData.switch_led ? styles.switchActive : ''}`}
>
<Image
src={imgs.CombinedShapeWhite}
style={{ display: DpStateData.switch_led ? 'inline-block' : 'none' }}
className={styles.CombinedShape}
/>
<Image
src={imgs.CombinedShape}
style={{ display: DpStateData.switch_led ? 'none' : 'inline-block' }}
className={styles.CombinedShape}
/>
</View>
</View>
</View>
</ScrollView>
);
}
export default Home;
import React, { useState, useEffect } from 'react';
import { router } from 'ray';
import {
getAppInfo,
getDeviceInfo,
getLangContent,
getSystemInfo,
hideLoading,
openDeviceDetailPage,
openTimerPage,
showLoading,
showToast,
onDpDataChange,
offDpDataChange,
} from '@ray-js/api';
import { Button, ScrollView, Text, View } from '@ray-js/components';
import { hooks, kit } from '@ray-js/panel-sdk';
import { useAtomValue, useSetAtom } from 'jotai';
import { DpState, dpStateAtom, selectDpStateAtom } from '@/atoms';
import { getDpStateMapByDevInfo, mapDpsMapToDpStateMap } from '@/utils';
import Section from '@/components/section';
import Strings from '@/i18n';
import styles from './index.module.less';
const { useDevInfo, useDpState, usePanelConfig } = hooks;
const { getDevInfo, initDevInfo } = kit;
export function Home() {
const devInfo = useDevInfo();
const [dpState, setDpState] = useDpState<SmartRobotDpState>();
console.log('dpState');
const panelConfig = usePanelConfig();
const setDpStateAtom = useSetAtom(dpStateAtom);
const dpStateInAtom = useAtomValue(selectDpStateAtom);
/**
* 监听设备上下线状态变更
*/
const handleDpDataChange: DpDataChangeHandler = data => {
console.log('=== onDpDataChange', data);
const initalDevInfo = getDevInfo();
const newDpState = mapDpsMapToDpStateMap(data.dps, initalDevInfo) as DpState;
console.log(newDpState, 'new');
setDpStateAtom(newDpState);
};
React.useEffect(() => {
initDevInfo().then(initalDevInfo => {
const initialDpState = getDpStateMapByDevInfo(initalDevInfo) as DpState;
console.log(initialDpState, 'init');
setDpStateAtom(initialDpState);
onDpDataChange(handleDpDataChange);
});
return () => {
offDpDataChange(handleDpDataChange);
};
}, []);
React.useEffect(() => {
// console.log(panelConfig);
}, [panelConfig.initialized]);
// usePageEvent('onShow', () => {
// console.log('=== home onShow');
// });
// usePageEvent('onHide', () => {
// console.log('=== home onHide');
// });
// usePageEvent('onPageScroll', event => {
// console.log('=== onPageScroll', event);
// });
// usePageEvent('onReachBottom', () => {
// console.log('=== onReachBottom');
// });
// usePageEvent('onResize', event => {
// console.log('=== onResize', event);
// });
const boolDpSchema = devInfo?.schema.find(data => data?.property?.type === 'bool');
const data = [
{ key: 'page-dpState', title: Strings.formatValue('checkDpState', devInfo?.name) },
{ key: 'page-appState', title: '查看 App 信息' },
{ key: 'page-location', title: dpStateInAtom },
{
key: 'publishDps',
title: '调用 publishDps 下发 DP 点',
text: boolDpSchema
? `${boolDpSchema.code}: ${dpState[boolDpSchema.code]}`
: '当前产品不存在可下发的布尔类型 DP 点',
onPress: () => {
if (!boolDpSchema) {
return;
}
// both is ok
// const dps = {
// [boolDpSchema.id]: !dpState[boolDpSchema.code],
// };
const dps = {
[boolDpSchema.code]: !dpState[boolDpSchema.code],
};
setDpState(dps);
// publishDps({
// deviceId: devInfo.devId,
// dps, // {'dpid': dpValue, '2': false}
// mode: 2,
// pipelines: [],
// options: {}, // 0,静音; 1,震动;2,声音; 3,震动声音
// success: info => console.log('=== publishDps success', dps, info),
// fail: error => console.warn('=== publishDps fail', error),
// });
},
},
{
key: 'getAppInfo',
title: '调用 getAppInfo 获取 App 信息',
onPress: () => {
getAppInfo({
success: info => console.log('=== getAppInfo success', info),
fail: error => console.warn('=== getAppInfo fail', error),
});
},
},
{
key: 'getDeviceInfo',
title: '调用 getDeviceInfo 获取设备信息',
onPress: () => {
getDeviceInfo({
deviceId: devInfo.devId,
success: info => console.log('=== getDeviceInfo success', info),
fail: error => console.warn('=== getDeviceInfo fail', error),
});
},
},
{
key: 'getLangContent',
title: '调用 getLangContent 获取多语言',
onPress: () => {
getLangContent({
success: info => console.log('=== getLangContent success', info),
fail: error => console.warn('=== getLangContent fail', error),
});
},
},
{
key: 'getSystemInfo',
title: '调用 getSystemInfo 获取手机信息',
onPress: () => {
getSystemInfo({
success: info => console.log('=== getSystemInfo success', info),
fail: error => console.warn('=== getSystemInfo fail', error),
});
},
},
{
key: 'openTimerPage',
title: '调用 openTimerPage 去定时页面',
onPress: () => {
openTimerPage({
deviceId: devInfo.devId,
category: 'schedule',
data: [
{
dpId: '18',
dpName: '开关1',
selected: 0,
rangeKeys: [true, false],
rangeValues: ['开启', '关闭'],
},
],
success: info => console.log('=== openTimerPage success', info),
fail: error => console.warn('=== openTimerPage fail', error),
});
},
},
{
key: 'openDeviceDetailPage',
title: '调用 openDeviceDetailPage 去设备详情',
onPress: () => {
const { devId, groupId } = devInfo;
openDeviceDetailPage({
deviceId: devId,
groupId,
success: info => console.log('=== openDeviceDetailPage success', info),
fail: error => console.warn('=== openDeviceDetailPage fail', error),
});
},
},
{
key: 'showToast',
title: '调用 showToast 打开吐司',
onPress: () => {
showToast({
title: '这是一个toast',
});
},
},
{
key: 'showLoading',
title: '调用 showLoading 打开 Loading',
onPress: () => {
showLoading({
title: '这是一个Loading',
});
setTimeout(() => {
hideLoading();
}, 10000);
},
},
];
return (
<ScrollView className={styles.view}>
<View style={{ justifyContent: 'center', alignItems: 'center' }}>
{data.map(({ key, title, text, onPress }) => {
return (
<Section key={key} title={title}>
<Button
className={styles.button}
onClick={
onPress ||
(() => {
router.push(`/common/${key!}/index`);
})
}
>
<Text className={styles.bTitle}>{text || 'Click me'}</Text>
</Button>
</Section>
);
})}
</View>
</ScrollView>
);
}
export default Home;
import icon_ld from './icon_ld.png';
import tempBg from './tempBg.png';
import icon_bhd from './icon_bhd.png';
import colorBg from './colorBg.png';
import CombinedShape from './CombinedShape.png';
import CombinedShapeWhite from './CombinedShapeWhite.png';
export default {
icon_ld,
tempBg,
icon_bhd,
colorBg,
CombinedShape,
CombinedShapeWhite,
};
import { Routes, TabBar } from '@ray-js/types';
export const routes: Routes = [
{
route: '/',
path: '/pages/home/index',
name: 'Home',
},
{
route: '/common/page-dpState/index',
path: '/pages/common/page-dpState/index',
name: 'PageDpState',
},
{
route: '/common/page-appState/index',
path: '/pages/common/page-appState/index',
name: 'PageAppState',
},
{
route: '/common/page-location/index',
path: '/pages/common/page-location/index',
name: 'PageLocation',
},
];
export const tabBar = {};
/**
* 可以考虑 JS API 暴露
*/
import _ from 'lodash';
export const getDpIdMapByDevInfo = (
devInfo: DevInfo
): Record<string, DevInfo['schema'][number]> => {
const dpIdMap = _.mapKeys(devInfo.schema, schemaInfo => schemaInfo.id);
return dpIdMap;
};
export const getDpCodeMapByDevInfo = (
devInfo: DevInfo
): Record<string, DevInfo['schema'][number]> => {
const dpCodeMap = _.mapKeys(devInfo.schema, schemaInfo => schemaInfo.code);
return dpCodeMap;
};
export const mapDpsMapToDpStateMap = (dpsMap: Record<string, any>, devInfo: DevInfo) => {
const dpIdMap = getDpIdMapByDevInfo(devInfo);
const dpStateMap = _.mapKeys(dpsMap, (__, dpId) => dpIdMap[dpId].code);
return dpStateMap;
};
/**
*
* @param devInfo
* @returns dpState 对象
* ```
* {
* switch: true,
* ...
* }
* ```
*/
export const getDpStateMapByDevInfo = (devInfo: DevInfo): Record<string, any> => {
const { dps, schema } = devInfo;
const dpStateMap = {};
_.forEach(schema, schemaInfo => {
dpStateMap[schemaInfo.code] = dps[schemaInfo.id];
});
return dpStateMap;
};
@primary-color: #ff592a;
// font
@font-gray-0: #040a23;
@font-gray-1: #656979;
@font-gray-2: #9aa0b1;
@font-gray-3: #e1e5ec;
{
"compileOnSave": false,
"compilerOptions": {
"target": "ES2015",
"module": "CommonJS",
"lib": [
"ESNext",
"DOM"
],
"declaration": false,
"declarationMap": false,
"emitDeclarationOnly": false,
"moduleResolution": "node",
"esModuleInterop": true,
"isolatedModules": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"noEmitOnError": false,
"jsx": "react",
"baseUrl": "./",
"paths": {
"ray": [
"./.ray"
],
"@/*": [
"./src/*"
]
},
"typeRoots": [
"./node_modules/@types",
"./typings",
"./node_modules/@tuya-miniapp"
],
},
"include": [
"src/**/*.*",
"ray/**/*.*",
"typings"
],
"exclude": [
"node_modules",
"dist"
]
}
\ No newline at end of file
/**
* 面板小程序的通用 TS 类型
*/
/// 一些 TTT 通用工具泛型 ///
type GetTTTAllParams<Fn> = Parameters<Fn>['0'];
type GetTTTParams<Fn> = Omit<GetTTTAllParams<Fn>, 'complete' | 'success' | 'fail'>;
type GetTTTCompleteData<Fn> = Parameters<GetTTTAllParams<Fn>['complete']>['0'];
type GetTTTSuccessData<Fn> = Parameters<GetTTTAllParams<Fn>['success']>['0'];
type GetTTTFailData<Fn> = Parameters<GetTTTAllParams<Fn>['fail']>['0'];
type GetTTTEventListener<Fn> = Parameters<Fn>['0'];
type GetTTTEventListenerParams<Fn> = Parameters<GetTTTEventListener<Fn>>['0'];
/// ///
/**
* TTT 方法统一错误码
*/
type TTTCommonErrorCode = GetTTTFailData<typeof import('@ray-js/api').getDeviceInfo>;
/**
* 功能点描述信息
*/
interface DpSchema {
attr: number;
canTrigger: boolean;
/**
* 功能点标识码,如 switch
*/
code: string;
defaultRecommend: boolean;
editPermission: boolean;
executable: boolean;
extContent: string;
iconname: string;
/**
* 功能点 ID
*/
id: number;
/**
* 功能点模式类型
* rw: 可下发可上报(可读可写)
* ro: 只可上报(仅可读)
* wr: 只可下发(仅可写)
*/
mode: 'rw' | 'ro' | 'wr';
/**
* 功能点名称,一般用于语音等场景
*/
name: string;
/**
* 功能点属性
*/
property: {
/**
* 功能点类型
*/
type: 'bool' | 'value' | 'enum' | 'bitmap' | 'string' | 'raw';
range?: string[];
label?: string[];
maxlen: number;
unit: string;
min: number;
max: number;
scale: number;
step: number;
[key: string]: number | string | string[];
};
type: string;
}
type DeviceInfo = GetTTTSuccessData<typeof import('@ray-js/api').getDeviceInfo>;
/**
* 设备信息
*/
type DevInfo = Omit<DeviceInfo, 'schema' | 'panelConfig'> & {
schema: DpSchema[];
idCodes: Record<string, string>;
codeIds: Record<string, string>;
panelConfig: PanelConfig;
};
/**
* 设备物模型信息
*/
type ThingModelInfo = GetTTTSuccessData<typeof import('@ray-js/api').getDeviceThingModelInfo>;
type DeviceInfoUpdatedHandler = GetTTTEventListener<
typeof import('@ray-js/api').onDeviceInfoUpdated
>;
type DpDataChangeHandler = GetTTTEventListener<typeof import('@ray-js/api').onDpDataChange>;
type DeviceOnlineStatusUpdateHandler = GetTTTEventListener<
typeof import('@ray-js/api').onDeviceOnlineStatusUpdate
>;
type NetworkStatusChangeHandler = GetTTTEventListener<
typeof import('@ray-js/api').onNetworkStatusChange
>;
type NetworkStatus = GetTTTEventListenerParams<typeof import('@ray-js/api').onNetworkStatusChange>;
type BluetoothAdapterStateChangeHandler = GetTTTEventListener<
typeof import('@ray-js/api').onBluetoothAdapterStateChange
>;
type BluetoothState = GetTTTEventListenerParams<
typeof import('@ray-js/api').onBluetoothAdapterStateChange
>;
/**
* 当前设备的 TS 类型
*/
type SmartRobotDpState = {
/**
* 开关
*/
power: boolean;
/**
* 工作模式
*/
mode: string;
/**
* 故障告警
*/
fault: number;
};
declare module '*.png';
declare module '*.module.less' {
const classes: {
readonly [key: string]: string;
};
export default classes;
declare module '*.less';
}
declare global {
interface Window {
devToolsExtension?: () => any;
__DEV__: boolean;
}
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment