diff --git a/.prettierrc.js b/.prettierrc.js index adbcd12..83140a2 100644 --- a/.prettierrc.js +++ b/.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 的闭合括号是否在同一行 diff --git a/public/audio/alert_left.mp3 b/public/audio/alert_left.mp3 new file mode 100644 index 0000000..f597e87 Binary files /dev/null and b/public/audio/alert_left.mp3 differ diff --git a/public/audio/begin_left.mp3 b/public/audio/begin_left.mp3 new file mode 100644 index 0000000..084b48e Binary files /dev/null and b/public/audio/begin_left.mp3 differ diff --git a/public/audio/begin_right.mp3 b/public/audio/begin_right.mp3 new file mode 100644 index 0000000..7d741aa Binary files /dev/null and b/public/audio/begin_right.mp3 differ diff --git a/public/audio/end_left.mp3 b/public/audio/end_left.mp3 new file mode 100644 index 0000000..965b171 Binary files /dev/null and b/public/audio/end_left.mp3 differ diff --git a/public/audio/end_right.mp3 b/public/audio/end_right.mp3 new file mode 100644 index 0000000..cd7bd18 Binary files /dev/null and b/public/audio/end_right.mp3 differ diff --git a/src/App.tsx b/src/App.tsx index dc4a854..a5ef158 100644 --- a/src/App.tsx +++ b/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 ( - setRouteActive(value)}> - {tabs.map(item => ( + setRouteActive(value)}> + {tabs.map((item) => ( @@ -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 (
diff --git a/src/components/StepItem.tsx b/src/components/StepItem.tsx index 40fd110..cad53a3 100644 --- a/src/components/StepItem.tsx +++ b/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 (
+ }`} + >
+ state === 'done' + ? 'bg-[#52C41A]' + : state === 'ongoing' + ? 'bg-[#187ef9]' + : 'bg-[#c0c0c0]' + }`} + >

{text}

diff --git a/src/index.tsx b/src/index.tsx index 5742cd5..3b4748f 100644 --- a/src/index.tsx +++ b/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( } /> }> - } > + }> }> }> }> @@ -38,7 +41,9 @@ const router = createHashRouter( const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render( - + + + ); diff --git a/src/pages/Measure.tsx b/src/pages/Measure.tsx index e4b4158..45d83be 100644 --- a/src/pages/Measure.tsx +++ b/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(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() {
- - - - - - + + + + + +
({ ...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); }} /> diff --git a/src/services/wsTypes.ts b/src/services/wsTypes.ts new file mode 100644 index 0000000..47ac59e --- /dev/null +++ b/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; diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..0251611 --- /dev/null +++ b/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; +export type AppDispatch = typeof store.dispatch; diff --git a/src/store/measureSlice.ts b/src/store/measureSlice.ts new file mode 100644 index 0000000..e5245bd --- /dev/null +++ b/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) => { + state.taskState = action.payload; + if (action.payload === 'START_RECORD_SIG' || action.payload === 'WRONG_SIDE') { + state.leftPoints = []; + state.rightPoints = []; + } + }, + addNewPoint: (state, action: PayloadAction) => { + if (isLeftFinished(state)) { + state.rightPoints.push(action.payload); + } else { + state.leftPoints.push(action.payload); + } + }, + }, +}); + +export const { updateTaskState, addNewPoint } = measureSlice.actions; +export default measureSlice.reducer; diff --git a/src/utils/bridge.ts b/src/utils/bridge.ts new file mode 100644 index 0000000..3fd88ec --- /dev/null +++ b/src/utils/bridge.ts @@ -0,0 +1,95 @@ +import { Subject } from 'rxjs'; + +declare global { + interface Window { + WebViewJavascriptBridge: { + callHandler: ( + name: string, + args: Record, + 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 | 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); + } + } +} diff --git a/src/utils/hooks.ts b/src/utils/hooks.ts new file mode 100644 index 0000000..965a9be --- /dev/null +++ b/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 = useSelector \ No newline at end of file