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' });
+ }
+ }
+
}