From 07470500e8baa4ac075ab02185515474054fc59b Mon Sep 17 00:00:00 2001 From: zhangjiming Date: Fri, 28 Mar 2025 21:46:50 +0800 Subject: [PATCH] =?UTF-8?q?bridge=20mock=E6=97=B6=E8=B5=B0http=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E5=92=8Cwebsocket?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 1 + package.json | 1 + public/index.html | 1 - public/manifest.json | 25 -------- src/App.tsx | 25 ++++++-- src/pages/Measure.tsx | 41 +++++-------- src/services/httpRequest.ts | 41 +++++++++++++ src/services/socket.ts | 120 +++++++++++++++++++++++++++++++++++++ src/store/features/measureSlice.ts | 16 +++++ src/utils/bridge.ts | 25 +++++++- 10 files changed, 237 insertions(+), 59 deletions(-) create mode 100644 .env delete mode 100644 public/manifest.json create mode 100644 src/services/httpRequest.ts create mode 100644 src/services/socket.ts diff --git a/.env b/.env new file mode 100644 index 0000000..371506d --- /dev/null +++ b/.env @@ -0,0 +1 @@ +REACT_APP_WS_URL=localhost:8080/ws diff --git a/package.json b/package.json index 7da2764..bbc9ebf 100644 --- a/package.json +++ b/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", diff --git a/public/index.html b/public/index.html index 93f5fc2..935f13f 100644 --- a/public/index.html +++ b/public/index.html @@ -4,7 +4,6 @@ - 廓形仪 diff --git a/public/manifest.json b/public/manifest.json deleted file mode 100644 index 080d6c7..0000000 --- a/public/manifest.json +++ /dev/null @@ -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" -} diff --git a/src/App.tsx b/src/App.tsx index 2ffa166..ac0c923 100644 --- a/src/App.tsx +++ b/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 (
diff --git a/src/pages/Measure.tsx b/src/pages/Measure.tsx index 0e4c6c6..56499a3 100644 --- a/src/pages/Measure.tsx +++ b/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() {
- - - - + + + +
diff --git a/src/services/httpRequest.ts b/src/services/httpRequest.ts new file mode 100644 index 0000000..695b1d8 --- /dev/null +++ b/src/services/httpRequest.ts @@ -0,0 +1,41 @@ + +type HttpReqParam = { + url: string; + method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE"; + params?: Record; + encode?: "form" | "json"; // 入参编码类型 + headers?: Record; +}; + +export default async function httpRequest({ 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; + } 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; + } +} + +export function urlEncode(params?: Record) { + 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; +} diff --git a/src/services/socket.ts b/src/services/socket.ts new file mode 100644 index 0000000..28c87da --- /dev/null +++ b/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 | any[] }>(); + get dataOb() { + return this.dataSub.asObservable(); + } + private stateSub = new Subject(); + 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 | 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(); + +// 导出 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}`; diff --git a/src/store/features/measureSlice.ts b/src/store/features/measureSlice.ts index d9ff0db..e86be54 100644 --- a/src/store/features/measureSlice.ts +++ b/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) => { if (isLeftFinished(state)) { diff --git a/src/utils/bridge.ts b/src/utils/bridge.ts index 3fd88ec..6d99f00 100644 --- a/src/utils/bridge.ts +++ b/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 = { + 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 | any[] }>(); export const bridgeOb = bridgeSub.asObservable(); +export function emitBridgeEvent(event: { func: string; data: Record | 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((resolve) => { + window.WebViewJavascriptBridge.callHandler('startMeasure', {}, (res) => { + resolve(JSON.parse(res)); + }); + }); + } else { + return httpRequest({ url: '/api/mobile/startMeasure', method: 'POST' }); + } + } + }