Browse Source

bridge mock时走http接口和websocket

master
zhangjiming 4 months ago
parent
commit
07470500e8
  1. 1
      .env
  2. 1
      package.json
  3. 1
      public/index.html
  4. 25
      public/manifest.json
  5. 25
      src/App.tsx
  6. 41
      src/pages/Measure.tsx
  7. 41
      src/services/httpRequest.ts
  8. 120
      src/services/socket.ts
  9. 16
      src/store/features/measureSlice.ts
  10. 25
      src/utils/bridge.ts

1
.env

@ -0,0 +1 @@
REACT_APP_WS_URL=localhost:8080/ws

1
package.json

@ -2,6 +2,7 @@
"name": "outline-mobile",
"version": "0.1.0",
"private": true,
"proxy": "http://127.0.0.1:8080",
"dependencies": {
"@babel/core": "^7.16.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",

1
public/index.html

@ -4,7 +4,6 @@
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>廓形仪</title>
</head>
<body>

25
public/manifest.json

@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

25
src/App.tsx

@ -10,11 +10,12 @@ 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 { appWebview, bridgeOb, emitBridgeEvent, registerBridgeFunc } from './utils/bridge';
import { useAppDispatch } from './utils/hooks';
import { addNewPoint, updateTaskState } from './store/features/measureSlice';
import { DeviceStatus, TrackRecordSig } from './services/wsTypes';
import { updateDevice } from './store/features/contextSlice';
import { createWebSocket, sharedWsUrl } from './services/socket';
const BottomBar = () => {
const navigate = useNavigate();
@ -71,13 +72,13 @@ function App() {
useEffect(() => {
const subscription = bridgeOb.subscribe(({ func, data }) => {
if (func === '/measurement-task/event') {
if (func === 'measureTaskEvent') {
if (Array.isArray(data)) return;
dispatch(updateTaskState(data.data));
} else if (func === '/measurement-task/point-report') {
dispatch(updateTaskState(data.event));
} else if (func === 'measurePointEvent') {
if (Array.isArray(data)) return;
dispatch(addNewPoint(data as TrackRecordSig['data']));
} else if (func === '/profiler-state/get-state') {
} else if (func === 'peripheralStatus') {
if (Array.isArray(data)) return;
dispatch(updateDevice(data as DeviceStatus['data']));
}
@ -85,6 +86,20 @@ function App() {
return () => subscription.unsubscribe();
}, [dispatch]);
useEffect(() => {
if (appWebview) {
registerBridgeFunc();
} else {
//连接websocket
const wsClient = createWebSocket(sharedWsUrl);
const subscription = wsClient.dataOb.subscribe((data) => {
emitBridgeEvent(data);
});
wsClient.connect();
return () => subscription.unsubscribe();
}
}, []);
return (
<div className="App">
<SafeArea position="top" />

41
src/pages/Measure.tsx

@ -9,9 +9,10 @@ import { useEffect, useRef, useState } from 'react';
import { rail6001, railTypes } from '../utils/constant';
import RailTypeBtn from '../components/RailTypeBtn';
import { Picker } from 'antd-mobile';
import { Picker, Toast } from 'antd-mobile';
import { useAppDispatch, useAppSelector } from '../utils/hooks';
import { updateTaskState } from '../store/features/measureSlice';
import Bridge from '../utils/bridge';
declare global {
interface Window {
@ -58,30 +59,10 @@ export default function Measure() {
useEffect(() => {
if (canvasRef.current) {
canvasRef.current.setMeasurementDataLeft(measureState.rightPoints);
canvasRef.current.setMeasurementDataRight(measureState.rightPoints);
}
}, [measureState.rightPoints]);
// 播放音频 步骤
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]);
const onSaveClick = () => {
navigate('/measure/save');
};
@ -92,7 +73,13 @@ export default function Measure() {
// } else {
// console.log("当前环境不支持 React Native WebView");
// }
dispatch(updateTaskState('START_RECORD_SIG'));
Bridge.startMeasure().then((res) => {
if (res.success) {
dispatch(updateTaskState('START_RECORD_SIG'));
} else {
Toast.show(res.message);
}
});
};
function stepState(step: StepName): StepState {
@ -193,10 +180,10 @@ export default function Measure() {
<section className="grid grid-cols-2 gap-[10px] px-3">
<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_ready')} text={'移到顶部停留2秒'} />
<StepItem state={stepState('left_begin')} text={'开始测量左侧'} />
<StepItem state={stepState('right_begin')} text={'开始测量右侧'} />
<StepItem state={stepState('left_end')} text={'左侧测量完成'} />
<StepItem state={stepState('right_end')} text={'右侧测量完成'} />
</section>
</main>

41
src/services/httpRequest.ts

@ -0,0 +1,41 @@
type HttpReqParam = {
url: string;
method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
params?: Record<string, any>;
encode?: "form" | "json"; // 入参编码类型
headers?: Record<string, any>;
};
export default async function httpRequest<T>({ url, method = "GET", params = {}, encode = "json", headers = {} }: HttpReqParam) {
// const token = sessionStorage.getItem("token");
// if (token) {
// headers = { Authorization: token, ...headers };
// }
if (method === "GET") {
const query = urlEncode(params);
const _url = query ? url + "?" + query : url;
const res = await fetch(_url, { headers });
return res.json() as Promise<T>;
} else {
const body = encode === "json" ? JSON.stringify(params) : urlEncode(params);
const _headers =
encode === "json"
? { "Content-Type": "application/json; charset=utf-8", ...headers }
: { "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", ...headers };
const res = await fetch(url, { method, headers: _headers, body });
return res.json() as Promise<T>;
}
}
export function urlEncode(params?: Record<string, any>) {
let query = "";
if (params && Object.keys(params).length > 0) {
const qs = [];
for (let attr in params) {
qs.push(`${attr}=${encodeURIComponent(params[attr])}`);
}
query = qs.join("&");
}
return query;
}

120
src/services/socket.ts

@ -0,0 +1,120 @@
import { Subject } from "rxjs";
export type SocketState = "open" | "close" | "error";
class WebSocketClient {
private ws: WebSocket | null = null;
private url: string;
private reconnectAttempts: number = -1;
private maxReconnectAttempts: number = 5;
private reconnectInterval: number = 3000;
private dataSub = new Subject<{ func: string; data: Record<string, any> | any[] }>();
get dataOb() {
return this.dataSub.asObservable();
}
private stateSub = new Subject<SocketState>();
get stateOb() {
return this.stateSub.asObservable();
}
constructor(url: string) {
this.url = url;
}
// 连接 WebSocket
connect(): void {
try {
// WebSocket.CONNECTING (0) WebSocket.OPEN (1)
if (this.ws && this.ws.readyState <= 1) {
// 已连接
console.log(`${this.url} 正在连接或已连接,无需重复连接`);
} else {
this.ws = new WebSocket(this.url);
this.bindEvents();
}
localStorage.setItem('wsReadyState', `${this.ws.readyState}`)
} catch (error) {
console.error("WebSocket 连接失败:", error);
this.reconnect();
}
}
// 绑定事件
private bindEvents(): void {
if (!this.ws) return;
// 连接建立时的处理
this.ws.onopen = () => {
console.log("WebSocket 连接已建立");
this.reconnectAttempts = -1; // 重置重连次数
this.stateSub.next("open");
};
// 接收消息的处理
this.ws.onmessage = (event: MessageEvent) => {
try {
const data = JSON.parse(event.data) as { func: string; data: Record<string, any> | any[] };
// console.log("🚀 ~ WebSocketClient ~ bindEvents ~ data:", data);
// if (data.type === "cmd") {
// this.dataSub.next({ type: data.type, data: { ...data.data, success: data.data.status === "D0000" } });
// } else {
this.dataSub.next(data);
// }
} catch (error) {
console.error("消息解析错误:", error);
}
};
this.ws.onclose = () => {
this.stateSub.next("close");
console.log("WebSocket 连接已关闭");
this.reconnect();
};
this.ws.onerror = error => {
this.stateSub.next("error");
console.error("WebSocket 错误:", error);
};
}
// 重连机制
private reconnect(): void {
if (this.reconnectAttempts === -1) {
this.reconnectAttempts = 0;
}
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.log("达到最大重连次数,停止重连");
this.reconnectAttempts = -1;
return;
}
setTimeout(() => {
console.log(`尝试第 ${this.reconnectAttempts + 1} 次重连...`);
this.reconnectAttempts++;
this.connect();
}, this.reconnectInterval);
}
// 关闭连接
disconnect(): void {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
}
const urlSocketMap = new Map<string, WebSocketClient>();
// 导出 WebSocket 客户端
export const createWebSocket = (url: string): WebSocketClient => {
if (urlSocketMap.has(url)) {
return urlSocketMap.get(url)!;
} else {
const client = new WebSocketClient(url);
urlSocketMap.set(url, client);
return client;
}
};
export const sharedWsUrl = `ws://${process.env.REACT_APP_WS_URL}`;

16
src/store/features/measureSlice.ts

@ -34,6 +34,22 @@ export const measureSlice = createSlice({
state.leftPoints = [];
state.rightPoints = [];
}
if (action.payload === 'START_RECORD_LEFT') {
const audio1 = new Audio('/audio/begin_left.mp3');
audio1.play().then(() => {});
} else if (action.payload === 'FINISH_RECORD_LEFT') {
const audio2 = new Audio('/audio/end_left.mp3');
audio2.play().then(() => {});
} else if (action.payload === 'START_RECORD_RIGHT') {
const audio3 = new Audio('/audio/begin_right.mp3');
audio3.play().then(() => {});
} else if (action.payload === 'FINISH_RECORD_RIGHT') {
const audio4 = new Audio('/audio/end_right.mp3');
audio4.play().then(() => {});
} else if (action.payload === 'WRONG_SIDE') {
const audio5 = new Audio('/audio/alert_left.mp3');
audio5.play().then(() => {});
}
},
addNewPoint: (state, action: PayloadAction<TrackRecordSig['data']>) => {
if (isLeftFinished(state)) {

25
src/utils/bridge.ts

@ -1,4 +1,5 @@
import { Subject } from 'rxjs';
import httpRequest from '../services/httpRequest';
declare global {
interface Window {
@ -47,6 +48,12 @@ export function setupWebViewJavascriptBridge(
}
}
export type BridgeBaseResult<T = any> = {
success: boolean;
data: T;
message: string;
};
type ShowModelParam = Partial<{
title: string; // 非必填,但 title 和 content 至少要填一个
content: string; // 非必填,但 title 和 content 至少要填一个
@ -59,10 +66,13 @@ type ShowModelParam = Partial<{
}>;
// 是否运行在 原生APP 的 WebView 中
const appWebview = navigator.userAgent.includes('iFlyTop-mobile');
export 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 emitBridgeEvent(event: { func: string; data: Record<string, any> | any[] }) {
bridgeSub.next(event);
}
export function registerBridgeFunc() {
const jsFuncs = ['funcInJs'];
@ -92,4 +102,17 @@ export default class Bridge {
return Promise.resolve('confirm' as const);
}
}
static startMeasure() {
if (appWebview) {
return new Promise<BridgeBaseResult>((resolve) => {
window.WebViewJavascriptBridge.callHandler('startMeasure', {}, (res) => {
resolve(JSON.parse(res));
});
});
} else {
return httpRequest<BridgeBaseResult>({ url: '/api/mobile/startMeasure', method: 'POST' });
}
}
}
Loading…
Cancel
Save