Browse Source

添加store和bridge

master
zhangjiming 4 months ago
parent
commit
2be29d7952
  1. 4
      .prettierrc.js
  2. BIN
      public/audio/alert_left.mp3
  3. BIN
      public/audio/begin_left.mp3
  4. BIN
      public/audio/begin_right.mp3
  5. BIN
      public/audio/end_left.mp3
  6. BIN
      public/audio/end_right.mp3
  7. 29
      src/App.tsx
  8. 28
      src/components/StepItem.tsx
  9. 9
      src/index.tsx
  10. 101
      src/pages/Measure.tsx
  11. 146
      src/services/wsTypes.ts
  12. 14
      src/store/index.ts
  13. 49
      src/store/measureSlice.ts
  14. 95
      src/utils/bridge.ts
  15. 6
      src/utils/hooks.ts

4
.prettierrc.js

@ -3,8 +3,8 @@ module.exports = {
semi: true, // 使用分号
trailingComma: 'es5', // 在对象或数组的最后一个元素后添加逗号
printWidth: 100, // 每行的最大字符数
tabWidth: 4, // 缩进宽度
useTabs: true, // 使用制表符缩进
tabWidth: 2, // 缩进宽度
useTabs: false, // 使用制表符缩进
jsxSingleQuote: false, // JSX 中使用单引号
bracketSpacing: true, // 对象字面量的括号是否换行
jsxBracketSameLine: false, // JSX 的闭合括号是否在同一行

BIN
public/audio/alert_left.mp3

BIN
public/audio/begin_left.mp3

BIN
public/audio/begin_right.mp3

BIN
public/audio/end_left.mp3

BIN
public/audio/end_right.mp3

29
src/App.tsx

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router';
import { SafeArea, TabBar } from 'antd-mobile';
@ -10,6 +10,10 @@ import icon_3_s from './assets/tabIcon/icon_tab3_s.svg';
import icon_3_u from './assets/tabIcon/icon_tab3_u.svg';
import icon_4_s from './assets/tabIcon/icon_tab4_s.svg';
import icon_4_u from './assets/tabIcon/icon_tab4_u.svg';
import { bridgeOb } from './utils/bridge';
import { useAppDispatch } from './utils/hooks';
import { addNewPoint, updateTaskState } from './store/measureSlice';
import { TrackRecordSig } from './services/wsTypes';
const BottomBar = () => {
const navigate = useNavigate();
@ -47,8 +51,8 @@ const BottomBar = () => {
];
return (
<TabBar activeKey={pathname} onChange={value => setRouteActive(value)}>
{tabs.map(item => (
<TabBar activeKey={pathname} onChange={(value) => setRouteActive(value)}>
{tabs.map((item) => (
<TabBar.Item
key={item.key}
icon={(active: boolean) =>
@ -62,6 +66,25 @@ const BottomBar = () => {
};
function App() {
const dispatch = useAppDispatch();
useEffect(() => {
const subscription = bridgeOb.subscribe(({ func, data }) => {
if (func === '/measurement-task/event') {
if (Array.isArray(data)) return;
dispatch(updateTaskState(data.data));
// switch (data.data) {
// case "START_RECORD_LEFT":
// break;
// }
} else if (func === '/measurement-task/point-report') {
if (Array.isArray(data)) return;
dispatch(addNewPoint(data as TrackRecordSig['data']));
}
});
return () => subscription.unsubscribe();
}, [dispatch]);
return (
<div className="App">
<SafeArea position="top" />

28
src/components/StepItem.tsx

@ -1,18 +1,34 @@
export default function StepItem({ state = 0, text }: { state: 0 | 1 | 2; text: string }) {
export type StepName =
| 'left_ready'
| 'left_begin'
| 'left_end'
| 'right_ready'
| 'right_begin'
| 'right_end';
export type StepState = 'none' | 'ongoing' | 'done';
export default function StepItem({ state = 'none', text }: { state: StepState; text: string }) {
return (
<div className="flex items-center px-4 h-10 bg-white rounded gap-3">
<div
className={`rounded-full w-[14px] h-[14px] flex justify-center items-center ${
state === 2
state === 'done'
? 'bg-[#52C41A]/[0.35]'
: state === 1
: state === 'ongoing'
? 'bg-[#187ef9]/[0.35]'
: 'bg-[#c0c0c0]/[0.35]'
}`}>
}`}
>
<div
className={`rounded-full w-[10px] h-[10px] ${
state === 2 ? 'bg-[#52C41A]' : state === 1 ? 'bg-[#187ef9]' : 'bg-[#c0c0c0]'
}`}></div>
state === 'done'
? 'bg-[#52C41A]'
: state === 'ongoing'
? 'bg-[#187ef9]'
: 'bg-[#c0c0c0]'
}`}
></div>
</div>
<p>{text}</p>
</div>

9
src/index.tsx

@ -10,6 +10,9 @@ import {
Route,
RouterProvider,
} from 'react-router-dom';
import { Provider } from 'react-redux';
import store from './store/index';
import Measure from './pages/Measure';
import Setting from './pages/Setting';
import Bluetooth from './pages/Bluetooth';
@ -22,7 +25,7 @@ const router = createHashRouter(
<Route path="/">
<Route index element={<Navigate to="home/measure" replace />} />
<Route path="home" element={<App />}>
<Route path="measure" element={<Measure />} ></Route>
<Route path="measure" element={<Measure />}></Route>
<Route path="setting" element={<Setting />}></Route>
<Route path="bluetooth" element={<Bluetooth />}></Route>
<Route path="mine" element={<Mine />}></Route>
@ -38,7 +41,9 @@ const router = createHashRouter(
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<RouterProvider router={router} />
<Provider store={store}>
<RouterProvider router={router} />
</Provider>
</React.StrictMode>
);

101
src/pages/Measure.tsx

@ -1,4 +1,4 @@
import StepItem from '../components/StepItem';
import StepItem, { StepName, StepState } from '../components/StepItem';
import { Link, useNavigate } from 'react-router-dom';
import CustomNavBar from '../components/CustomNavBar';
import MeasurementCanvas, {
@ -10,6 +10,9 @@ import { rail6001, railTypes } from '../utils/constant';
import RailTypeBtn from '../components/RailTypeBtn';
import { Picker } from 'antd-mobile';
import { bridgeOb } from '../utils/bridge';
import { useAppDispatch, useAppSelector } from '../utils/hooks';
import { updateTaskState } from '../store/measureSlice';
declare global {
interface Window {
@ -31,6 +34,8 @@ window.bridgeCall = (func, ...args) => {
export default function Measure() {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const measureState = useAppSelector((state) => state.measure);
const canvasRef = useRef<MeasurementCanvasRef>(null);
@ -38,12 +43,12 @@ export default function Measure() {
const [railId, setRailId] = useState<(number | string | null)[]>([1]);
const onStartClick = () => {
if (typeof window.ReactNativeWebView !== 'undefined') {
// 发送消息给 React Native
window.ReactNativeWebView.postMessage(JSON.stringify(['add', 2, 3]));
} else {
console.log('当前环境不支持 React Native WebView');
}
// if (typeof window.ReactNativeWebView !== "undefined") {
// window.ReactNativeWebView.postMessage(JSON.stringify(["add", 2, 3]));
// } else {
// console.log("当前环境不支持 React Native WebView");
// }
dispatch(updateTaskState('START_RECORD_SIG'));
};
const onSaveClick = () => {
navigate('/measure/save');
@ -56,8 +61,72 @@ export default function Measure() {
}
}, []);
// 播放音频 步骤
useEffect(() => {
if (measureState.taskState === 'START_RECORD_LEFT') {
const audio1 = new Audio('/audio/begin_left.mp3');
audio1.play();
} else if (measureState.taskState === 'FINISH_RECORD_LEFT') {
const audio2 = new Audio('/audio/end_left.mp3');
audio2.play();
} else if (measureState.taskState === 'START_RECORD_RIGHT') {
const audio3 = new Audio('/audio/begin_right.mp3');
audio3.play();
} else if (measureState.taskState === 'FINISH_RECORD_RIGHT') {
const audio4 = new Audio('/audio/end_right.mp3');
audio4.play();
} else if (measureState.taskState === 'WRONG_SIDE') {
const audio5 = new Audio('/audio/alert_left.mp3');
audio5.play();
}
}, [measureState.taskState]);
function stepState(step: StepName): StepState {
if (!measureState.taskState) {
return 'none';
}
switch (measureState.taskState) {
case 'START_RECORD_SIG':
case 'WRONG_SIDE':
if (step === 'left_ready') {
return 'ongoing';
} else {
return 'none';
}
case 'START_RECORD_LEFT':
if (step === 'left_ready') {
return 'done';
} else if (step === 'left_begin') {
return 'ongoing';
} else {
return 'none';
}
case 'FINISH_RECORD_LEFT':
if (step === 'left_ready' || step === 'left_begin' || step === 'left_end') {
return 'done';
} else if (step === 'right_ready') {
return 'ongoing';
} else {
return 'none';
}
case 'START_RECORD_RIGHT':
if (step === 'right_begin') {
return 'ongoing';
} else if (step === 'right_end') {
return 'none';
} else {
return 'done';
}
case 'FINISH_RECORD_RIGHT': {
return 'done';
}
default:
return 'none';
}
}
function railName() {
return railTypes.find(r => r.id === railId[0])?.name || '';
return railTypes.find((r) => r.id === railId[0])?.name || '';
}
return (
@ -109,24 +178,24 @@ export default function Measure() {
</section>
<section className="grid grid-cols-2 gap-[10px] px-3">
<StepItem state={2} text={'移到顶部停留2秒'} />
<StepItem state={2} text={'移到顶部停留2秒'} />
<StepItem state={0} text={'开始测量左侧'} />
<StepItem state={0} text={'开始测量右侧'} />
<StepItem state={1} text={'左侧测量完成'} />
<StepItem state={1} text={'右侧测量完成'} />
<StepItem state={stepState('left_ready')} text={'移到顶部停留2秒'} />
<StepItem state={stepState('left_begin')} text={'移到顶部停留2秒'} />
<StepItem state={stepState('left_end')} text={'开始测量左侧'} />
<StepItem state={stepState('right_ready')} text={'开始测量右侧'} />
<StepItem state={stepState('right_begin')} text={'左侧测量完成'} />
<StepItem state={stepState('right_end')} text={'右侧测量完成'} />
</section>
</main>
</div>
<Picker
columns={[railTypes.map(t => ({ ...t, label: t.name, value: t.id }))]}
columns={[railTypes.map((t) => ({ ...t, label: t.name, value: t.id }))]}
visible={railPickerVisible}
onClose={() => {
setRailPickerVisible(false);
}}
value={railId}
onConfirm={v => {
onConfirm={(v) => {
setRailId(v);
}}
/>

146
src/services/wsTypes.ts

@ -0,0 +1,146 @@
// 开始、停止绘制
export type TaskState = {
messageType: 'EVENT';
data:
| 'START_RECORD_SIG'
// | "END_RECORD_SIG"
// | "FINISHED"
| 'START_RECORD_LEFT'
| 'FINISH_RECORD_RIGHT'
| 'FINISH_RECORD'
| 'FINISH_RECORD_LEFT'
// | "END_RECORD_SIG"
| 'END_RECORD'
| 'START_RECORD_RIGHT'
| 'WRONG_SIDE';
// data: {
// event: "START_RECORD_SIG" | "END_RECORD_SIG" | "FINISHED" | "START_RECORD_LEFT" | "FINISH_RECORD_RIGHT" | "FINISH_RECORD" | "FINISH_RECORD_LEFT" | "END_RECORD_SIG" | "END_RECORD" | "START_RECORD_RIGHT";
// };
path: '/api/measurement-task/event';
};
// 连续上报坐标点
export type TrackRecordSig = {
messageType: 'STATE';
data: {
x: number;
y: number;
};
path: '/api/measurement-task/point-report';
};
export const defaultContext: ContextMessage['data'] = {
loginFlag: true,
loginUser: {
id: 3, //数据主键id
account: 'test001', //用户账户
nickname: '测试账户001', //用户昵称
userRole: 'User', //用户角色,可用值:User,Admin,Dev
isBuiltInUser: false, //是否内置用户(内置用户不可删除)
},
};
// 上下文状态
export type ContextMessage = {
messageType: 'DeviceContext';
data: {
loginFlag: Boolean;
loginUser: Partial<{
id: number;
account: string;
nickname: string;
password: string;
userRole: 'Admin' | 'User' | 'Dev';
isBuiltInUser: boolean;
}>;
};
path: '/api/deviceContext';
};
export type loginUser = Partial<{
id: 3; //数据主键id
account: 'test001'; //用户账户
nickname: '测试账户001'; //用户昵称
userRole: 'User'; //用户角色,可用值:User,Admin,Dev
isBuiltInUser: false; //是否内置用户(内置用户不可删除)
}>;
export const taskStatusDescMap: { [k in MeasureState['data']['taskStatus']]: string } = {
IDLE: '空闲',
MEASURING: '测量中',
WAITING_FOR_MEASURING: '等待测量',
FINISHED: '测量完成',
START_RECORD_LEFT: '',
FINISH_RECORD_RIGHT: '',
FINISH_RECORD: '',
FINISH_RECORD_LEFT: '',
END_RECORD_SIG: '',
END_RECORD: '',
START_RECORD_RIGHT: '',
};
// 测量任务状态
export type MeasureState = {
messageType: 'STATE';
data: {
taskStatus:
| 'IDLE'
| 'MEASURING'
| 'WAITING_FOR_MEASURING'
| 'FINISHED'
| 'START_RECORD_LEFT'
| 'FINISH_RECORD_RIGHT'
| 'FINISH_RECORD'
| 'FINISH_RECORD_LEFT'
| 'END_RECORD_SIG'
| 'END_RECORD'
| 'START_RECORD_RIGHT';
measureSideCnt: 0 | 1 | 2; //已测量数量,0,1,2 最多两边(左边和右边)
isMeasuringLeftEnd: boolean; //测量左侧完成
isMeasuringRightEnd: boolean; //测量右侧完成
motionlessSigFlag: boolean; //滑轮质心是否静止
inStartMeasuringPos: boolean; //是否在允许开始测量的位置
isWrongSide: boolean; //测量方向是错误的
// profileRecordDescription: null; //用户填写的新测量信息
};
path: '/api/measurement-task/get-task-state';
};
export const defaultMeasureState = {
taskStatus: 'IDLE',
measureSideCnt: 0, //已测量数量,0,1,2 最多两边(左边和右边)
isMeasuringLeftEnd: false, //测量左侧完成
isMeasuringRightEnd: false, //测量右侧完成
motionlessSigFlag: true, //滑轮质心是否静止
inStartMeasuringPos: true, //是否在允许开始测量的位置
};
export type ChannelMessage = {
messageType: 'STATE';
data: {
isConnect: boolean;
connectPort: string;
sn: string;
descriptivePortName: string;
};
path: '/api/subdevice/uartchanel/get-channel-state';
};
export type DeviceStatus = {
messageType: 'STATE';
data: {
isConnected: boolean; //是否链接
power: number; //电量
inclinatorX: number; //x轴倾斜
inclinatorY: number; //y轴倾斜
temperature: number; //温度
};
path: '/api/profiler-state/get-state';
};
export type Datagram =
| TrackRecordSig
| TaskState
| ContextMessage
| MeasureState
| ChannelMessage
| DeviceStatus;

14
src/store/index.ts

@ -0,0 +1,14 @@
import { configureStore } from '@reduxjs/toolkit';
import measureSlice from './measureSlice';
// configureStore创建一个redux数据
const store = configureStore({
// 合并多个Slice
reducer: {
measure: measureSlice,
},
});
export default store;
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

49
src/store/measureSlice.ts

@ -0,0 +1,49 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { TaskState, TrackRecordSig } from '../services/wsTypes';
export interface MeasureState {
taskState?: TaskState['data'];
// leftFinished: boolean;
leftPoints: TrackRecordSig['data'][];
rightPoints: TrackRecordSig['data'][];
}
const initialState: MeasureState = {
taskState: undefined,
// leftFinished: false,
leftPoints: [],
rightPoints: [],
};
function isLeftFinished(state: MeasureState) {
if (
state.taskState === 'FINISH_RECORD_LEFT' ||
state.taskState === 'START_RECORD_RIGHT' ||
state.taskState === 'FINISH_RECORD_RIGHT'
)
return true;
return false;
}
export const measureSlice = createSlice({
name: 'measure',
initialState,
reducers: {
updateTaskState: (state, action: PayloadAction<TaskState['data']>) => {
state.taskState = action.payload;
if (action.payload === 'START_RECORD_SIG' || action.payload === 'WRONG_SIDE') {
state.leftPoints = [];
state.rightPoints = [];
}
},
addNewPoint: (state, action: PayloadAction<TrackRecordSig['data']>) => {
if (isLeftFinished(state)) {
state.rightPoints.push(action.payload);
} else {
state.leftPoints.push(action.payload);
}
},
},
});
export const { updateTaskState, addNewPoint } = measureSlice.actions;
export default measureSlice.reducer;

95
src/utils/bridge.ts

@ -0,0 +1,95 @@
import { Subject } from 'rxjs';
declare global {
interface Window {
WebViewJavascriptBridge: {
callHandler: (
name: string,
args: Record<string, any>,
callback: (res: string) => void
) => void;
registerHandler: (
name: string,
func: (data: string, callback: (res: string) => void) => void
) => void;
};
WVJBCallbacks: Array<(bridge: typeof window.WebViewJavascriptBridge) => void>;
}
}
export function setupWebViewJavascriptBridge(
callback: (bridge: typeof window.WebViewJavascriptBridge) => void
) {
if (window.WebViewJavascriptBridge) {
return callback(window.WebViewJavascriptBridge);
}
if (/android/i.test(navigator.userAgent)) {
document.addEventListener(
'WebViewJavascriptBridgeReady',
function () {
callback(window.WebViewJavascriptBridge);
},
false
);
} else {
if (window.WVJBCallbacks) {
return window.WVJBCallbacks.push(callback);
}
window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function () {
document.documentElement.removeChild(WVJBIframe);
}, 0);
}
}
type ShowModelParam = Partial<{
title: string; // 非必填,但 title 和 content 至少要填一个
content: string; // 非必填,但 title 和 content 至少要填一个
contentAlignCenter: boolean; // 内容文本水平居中? 默认 false, 居左
showCancel: boolean; // 默认 true
cancelText: string; //默认 '取消'
// cancelColor: string; // 默认'#333333'
confirmText: string; // 默认 '确定'
// confirmColor: string; // 默认 APP主题色
}>;
// 是否运行在 原生APP 的 WebView 中
const appWebview = navigator.userAgent.includes('iFlyTop-mobile');
const bridgeSub = new Subject<{ func: string; data: Record<string, any> | any[] }>();
export const bridgeOb = bridgeSub.asObservable();
export function registerBridgeFunc() {
const jsFuncs = ['funcInJs'];
jsFuncs.forEach((funcName) => {
window.WebViewJavascriptBridge.registerHandler(funcName, (data, callback) => {
bridgeSub.next({ func: funcName, data: JSON.parse(data) });
callback(JSON.stringify({ success: true }));
});
});
}
export default class Bridge {
static register(name: string, func: (data: string, callback: (res: string) => void) => void) {
if (appWebview) {
window.WebViewJavascriptBridge.registerHandler(name, func);
}
}
static showModal(param: ShowModelParam) {
if (appWebview) {
return new Promise<'confirm' | 'cancel'>((resolve) => {
window.WebViewJavascriptBridge.callHandler('showModal', param, (res) => {
resolve(res as 'confirm' | 'cancel');
});
});
} else {
return Promise.resolve('confirm' as const);
}
}
}

6
src/utils/hooks.ts

@ -0,0 +1,6 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from '../store'
// 在整个应用程序中使用,而不是简单的 `useDispatch` 和 `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
Loading…
Cancel
Save