Browse Source

新的WS类型,添加蓝牙和同步状态到context Store

master
zhangjiming 4 months ago
parent
commit
073435f3c5
  1. 3
      .vscode/settings.json
  2. 27
      src/App.tsx
  3. 6
      src/components/CustomNavBar.tsx
  4. 25
      src/pages/Measure.tsx
  5. 31
      src/pages/Setting.tsx
  6. 66
      src/services/mobileWsType.ts
  7. 5
      src/services/socket.ts
  8. 57
      src/store/features/contextSlice.ts
  9. 27
      src/utils/bridge.ts
  10. 13
      src/utils/helper.ts

3
.vscode/settings.json

@ -0,0 +1,3 @@
{
"cSpell.words": ["Cascader"]
}

27
src/App.tsx

@ -14,7 +14,7 @@ import { appWebview, bridgeOb, emitBridgeEvent, registerBridgeFunc } from './uti
import { useAppDispatch } from './utils/hooks'; import { useAppDispatch } from './utils/hooks';
import { addNewPoint, updateTaskState } from './store/features/measureSlice'; import { addNewPoint, updateTaskState } from './store/features/measureSlice';
import { DeviceStatus, TrackRecordSig } from './services/wsTypes'; import { DeviceStatus, TrackRecordSig } from './services/wsTypes';
import { updateDevice } from './store/features/contextSlice';
import { updateBleList, updateDevice, updateSyncProgress } from './store/features/contextSlice';
import { createWebSocket, sharedWsUrl } from './services/socket'; import { createWebSocket, sharedWsUrl } from './services/socket';
import { updateUploadStatus } from './store/features/historySlice'; import { updateUploadStatus } from './store/features/historySlice';
@ -72,20 +72,17 @@ function App() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
useEffect(() => { useEffect(() => {
const subscription = bridgeOb.subscribe(({ func, data }) => {
if (func === 'measureTaskEvent') {
if (Array.isArray(data)) return;
dispatch(updateTaskState(data.event));
} else if (func === 'measurePointEvent') {
if (Array.isArray(data)) return;
dispatch(addNewPoint(data as TrackRecordSig['data']));
} else if (func === 'peripheralStatus') {
if (Array.isArray(data)) return;
dispatch(updateDevice(data as DeviceStatus['data']));
} else if (func === 'uploadRecordsStatus') {
if (Array.isArray(data)) {
dispatch(updateUploadStatus(data))
}
const subscription = bridgeOb.subscribe((datagram) => {
if (datagram.type === 'measure-event') {
dispatch(updateTaskState(datagram.data.event));
} else if (datagram.type === 'measure-point') {
dispatch(addNewPoint(datagram.data));
} else if (datagram.type === 'peripheral-status') {
dispatch(updateDevice(datagram.data));
} else if (datagram.type === 'ble-list') {
dispatch(updateBleList(datagram.data));
} else if (datagram.type === 'sync-progress') {
dispatch(updateSyncProgress(datagram.data));
} }
}); });
return () => subscription.unsubscribe(); return () => subscription.unsubscribe();

6
src/components/CustomNavBar.tsx

@ -15,7 +15,7 @@ export default function CustomNavBar({ title }: { title: string }) {
{/** 温度,水平仪 */} {/** 温度,水平仪 */}
<div <div
className="absolute h-[30px] w-full bg-white border border-[#D8D8D8] flex items-center gap-2 px-4" className="absolute h-[30px] w-full bg-white border border-[#D8D8D8] flex items-center gap-2 px-4"
style={{ top: device.isConnected && showDetail ? '100%' : 0, transition: 'top 300ms' }}
style={{ top: device.connected && showDetail ? '100%' : 0, transition: 'top 300ms' }}
> >
<span className="flex-1">: {device.temperature}°C</span> <span className="flex-1">: {device.temperature}°C</span>
<span className="flex-1">X轴倾斜: {device.inclinatorX}</span> <span className="flex-1">X轴倾斜: {device.inclinatorX}</span>
@ -25,7 +25,7 @@ export default function CustomNavBar({ title }: { title: string }) {
<div className="absolute left-0 top-0 w-full h-full flex items-center px-3 bg-white"> <div className="absolute left-0 top-0 w-full h-full flex items-center px-3 bg-white">
<h1 className="text-lg text-text ml-3">{title}</h1> <h1 className="text-lg text-text ml-3">{title}</h1>
{/** 蓝牙连接状态 */} {/** 蓝牙连接状态 */}
{device.isConnected ? (
{device.connected ? (
<div <div
className="ml-auto flex items-center cursor-pointer" className="ml-auto flex items-center cursor-pointer"
onClick={() => setShowDetail(!showDetail)} onClick={() => setShowDetail(!showDetail)}
@ -37,7 +37,7 @@ export default function CustomNavBar({ title }: { title: string }) {
src={icon_arr_d} src={icon_arr_d}
alt="arr" alt="arr"
style={{ style={{
transform: device.isConnected && showDetail ? 'rotate(180deg)' : 'rotate(0deg)',
transform: device.connected && showDetail ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 300ms', transition: 'transform 300ms',
}} }}
/> />

25
src/pages/Measure.tsx

@ -14,6 +14,8 @@ import { useAppDispatch, useAppSelector } from '../utils/hooks';
import { updateTaskState } from '../store/features/measureSlice'; import { updateTaskState } from '../store/features/measureSlice';
import Bridge from '../utils/bridge'; import Bridge from '../utils/bridge';
import { selectLabeledKtjOrgs } from '../store/features/baseData'; import { selectLabeledKtjOrgs } from '../store/features/baseData';
import { updateOrg } from '../store/features/contextSlice';
import { textsOfKeys } from '../utils/helper';
// declare global { // declare global {
// interface Window { // interface Window {
@ -38,11 +40,12 @@ export default function Measure() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const labeledKtjOrgs = useAppSelector((state) => selectLabeledKtjOrgs(state.baseData)); const labeledKtjOrgs = useAppSelector((state) => selectLabeledKtjOrgs(state.baseData));
const measureState = useAppSelector((state) => state.measure); const measureState = useAppSelector((state) => state.measure);
const contextState = useAppSelector((state) => state.context);
const canvasRef = useRef<MeasurementCanvasRef>(null); const canvasRef = useRef<MeasurementCanvasRef>(null);
const [railPickerVisible, setRailPickerVisible] = useState(false); const [railPickerVisible, setRailPickerVisible] = useState(false);
const [orgPickerVisible, setOrgPickerVisible] = useState(false);
const [railId, setRailId] = useState<(number | string | null)[]>([1]); const [railId, setRailId] = useState<(number | string | null)[]>([1]);
// 绘制轨型基准线 // 绘制轨型基准线
@ -66,6 +69,13 @@ export default function Measure() {
} }
}, [measureState.rightPoints]); }, [measureState.rightPoints]);
const orgTextArr = () => {
return textsOfKeys(
[contextState.currOrgCode, contextState.currGWDCode, contextState.currXMCode],
labeledKtjOrgs
);
};
const onSaveClick = () => { const onSaveClick = () => {
navigate('/measure/save'); navigate('/measure/save');
}; };
@ -90,7 +100,10 @@ export default function Measure() {
options: labeledKtjOrgs, options: labeledKtjOrgs,
placeholder: '请选择', placeholder: '请选择',
}); });
Toast.show(value ? `你选择了 ${value.join(' - ')}` : '你没有进行选择');
// Toast.show(value ? `你选择了 ${value.join(' - ')}` : '你没有进行选择');
if (value) {
dispatch(updateOrg(value as string[]));
}
}; };
function stepState(step: StepName): StepState { function stepState(step: StepName): StepState {
if (!measureState.taskState) { if (!measureState.taskState) {
@ -176,10 +189,10 @@ export default function Measure() {
className="h-10 bg-[#e3e8f5] flex justify-between items-center px-4" className="h-10 bg-[#e3e8f5] flex justify-between items-center px-4"
onClick={onOrgBarClick} onClick={onOrgBarClick}
> >
<p className="text-text"> //线/</p>
<Link to="/home/setting" className="text-primary underline">
</Link>
<p className="text-text" style={{ color: contextState.currOrgCode ? '#333' : 'red' }}>
{contextState.currOrgCode ? orgTextArr().join('/') : '点击此处选择铁路局和工务段'}
</p>
<span className="text-primary underline"></span>
</section> </section>
<section className="flex items-center gap-4 px-4 my-4"> <section className="flex items-center gap-4 px-4 my-4">

31
src/pages/Setting.tsx

@ -42,37 +42,6 @@ export default function Setting() {
</div> </div>
</div> </div>
</section> </section>
{/* <section>
<div className="h-[42px] px-5">
<h1 className="h-[42px] leading-[42px] text-base text-text font-medium"></h1>
</div>
<div className="bg-white px-5 text-sm text-text">
<div className="h-12 flex items-center border-b border-[#eee]">
<span></span>
<span className="ml-auto mr-4"></span>
<img src={icon_arr_r} alt="arr" />
</div>
<div className="h-12 flex items-center border-b border-[#eee]">
<span>线</span>
<span className="ml-auto mr-4">线</span>
<img src={icon_arr_r} alt="arr" />
</div>
<div className="h-12 flex items-center border-b border-[#eee]">
<span></span>
<span className="ml-auto mr-4"></span>
<img src={icon_arr_r} alt="arr" />
</div>
<div className="h-12 flex items-center">
<span></span>
<input
type="text"
placeholder="请填写"
className="border-0 outline-none self-stretch text-right flex-1 ml-4"
/>
</div>
</div>
</section> */}
<div className="btn-contained rounded-md h-12 mx-9 my-8 text-base font-medium"></div> <div className="btn-contained rounded-md h-12 mx-9 my-8 text-base font-medium"></div>
</div> </div>
</div> </div>

66
src/services/mobileWsType.ts

@ -0,0 +1,66 @@
export type PeripheralStatus = {
type: 'peripheral-status';
data: {
connected: boolean;
power: number;
inclinatorX: number;
inclinatorY: number;
temperature: number;
};
};
export type MeasureEvent = {
type: 'measure-event';
data: {
event:
| 'START_RECORD_LEFT'
| 'FINISH_RECORD_LEFT'
| 'START_RECORD_RIGHT'
| 'FINISH_RECORD_RIGHT'
| 'WRONG_SIDE';
};
};
export type MeasurePoint = {
type: 'measure-point';
data: {
x: number;
y: number;
};
};
export type BleList = {
type: 'ble-list';
data: Array<{
mac: string; // 蓝牙设备的 MAC 地址(唯一标识)
name: string; // 蓝牙设备的可读名称(如型号/别名)
linked: boolean; //该设备是否已链接
// ... 后续补充
}>;
};
export type SyncProgress = {
type: 'sync-progress'; // 数据类型:同步进度状态上报
data: {
remaining: number; // 剩余未同步数量
fail: number; // 同步失败数量
total: number; // 总数量
finish: boolean; // 是否同步完成(true 表示全部完成)
};
};
export type SyncItemFinish = {
type: 'sync-item-finish'; // 数据类型:单项数据同步完成上报
data: {
id: number; // 数据同步任务的 ID
success: boolean; // 是否同步成功(true 表示成功,false 表示失败)
};
};
export type MobileDatagram =
| PeripheralStatus
| MeasureEvent
| MeasurePoint
| BleList
| SyncProgress
| SyncItemFinish;

5
src/services/socket.ts

@ -1,4 +1,5 @@
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { MobileDatagram } from "./mobileWsType";
export type SocketState = "open" | "close" | "error"; export type SocketState = "open" | "close" | "error";
@ -9,7 +10,7 @@ class WebSocketClient {
private maxReconnectAttempts: number = 5; private maxReconnectAttempts: number = 5;
private reconnectInterval: number = 3000; private reconnectInterval: number = 3000;
private dataSub = new Subject<{ func: string; data: Record<string, any> | any[] }>();
private dataSub = new Subject<MobileDatagram>();
get dataOb() { get dataOb() {
return this.dataSub.asObservable(); return this.dataSub.asObservable();
} }
@ -53,7 +54,7 @@ class WebSocketClient {
// 接收消息的处理 // 接收消息的处理
this.ws.onmessage = (event: MessageEvent) => { this.ws.onmessage = (event: MessageEvent) => {
try { try {
const data = JSON.parse(event.data) as { func: string; data: Record<string, any> | any[] };
const data = JSON.parse(event.data) as MobileDatagram;
// console.log("🚀 ~ WebSocketClient ~ bindEvents ~ data:", data); // console.log("🚀 ~ WebSocketClient ~ bindEvents ~ data:", data);
// if (data.type === "cmd") { // if (data.type === "cmd") {
// this.dataSub.next({ type: data.type, data: { ...data.data, success: data.data.status === "D0000" } }); // this.dataSub.next({ type: data.type, data: { ...data.data, success: data.data.status === "D0000" } });

57
src/store/features/contextSlice.ts

@ -1,35 +1,78 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { DeviceStatus } from '../../services/wsTypes';
import { BleList, PeripheralStatus, SyncItemFinish, SyncProgress } from '../../services/mobileWsType';
interface ContextState { interface ContextState {
device: DeviceStatus['data'];
currRailTypeId: number;
device: PeripheralStatus['data'];
currRailTypeId: number; // 当前选择的轨型
currOrgCode: string; // 铁路局
currGWDCode: string; // 工务段
currXMCode: string; // 线名
bleList: BleList["data"];
syncProgress: SyncProgress["data"];
syncItems: Array<SyncItemFinish["data"]>
}
const orgGwdXmStr = localStorage.getItem('org_gwd_xm');
let orgGwdXm: string[] | undefined;
if (orgGwdXmStr) {
orgGwdXm = orgGwdXmStr.split(',');
} }
const initialState: ContextState = { const initialState: ContextState = {
device: { device: {
isConnected: true, //是否链接
connected: true, //是否已连接蓝牙
power: 60, //电量 power: 60, //电量
inclinatorX: 0.276, //x轴倾斜 inclinatorX: 0.276, //x轴倾斜
inclinatorY: 3.019, //y轴倾斜 inclinatorY: 3.019, //y轴倾斜
temperature: 32.026, //温度 temperature: 32.026, //温度
}, },
// 当前选择的轨型
currRailTypeId: 1, currRailTypeId: 1,
currOrgCode: orgGwdXm ? orgGwdXm[0] : '',
currGWDCode: orgGwdXm ? orgGwdXm[1] : '',
currXMCode: orgGwdXm ? orgGwdXm[2] : '',
bleList: [],
syncProgress: {
remaining: 0,
fail: 0,
total: 0,
finish: true,
},
syncItems:[]
}; };
export const contextSlice = createSlice({ export const contextSlice = createSlice({
name: 'context', name: 'context',
initialState, initialState,
reducers: { reducers: {
updateDevice: (state, action: PayloadAction<DeviceStatus['data']>) => {
updateOrg: (state, action: PayloadAction<string[]>) => {
state.currOrgCode = action.payload[0];
state.currGWDCode = action.payload[1];
state.currXMCode = action.payload[2];
localStorage.setItem('org_gwd_xm', action.payload.join(','))
},
updateDevice: (state, action: PayloadAction<PeripheralStatus['data']>) => {
state.device = action.payload; state.device = action.payload;
}, },
updateRailTypeId: (state, action: PayloadAction<number>) => { updateRailTypeId: (state, action: PayloadAction<number>) => {
state.currRailTypeId = action.payload; state.currRailTypeId = action.payload;
}, },
updateBleList:(state, action: PayloadAction<BleList["data"]>) => {
state.bleList = action.payload
},
updateSyncProgress: (state, action: PayloadAction<SyncProgress["data"]>) => {
state.syncProgress = action.payload;
},
}, },
}); });
export const { updateDevice, updateRailTypeId } = contextSlice.actions;
export const { updateOrg, updateDevice, updateRailTypeId, updateBleList, updateSyncProgress } = contextSlice.actions;
export default contextSlice.reducer; export default contextSlice.reducer;

27
src/utils/bridge.ts

@ -1,6 +1,7 @@
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import httpRequest from '../services/httpRequest'; import httpRequest from '../services/httpRequest';
import { Measurement } from '../services/apiTypes'; import { Measurement } from '../services/apiTypes';
import { MobileDatagram } from '../services/mobileWsType';
declare global { declare global {
interface Window { interface Window {
@ -81,18 +82,18 @@ type ShowModelParam = Partial<{
// 是否运行在 原生APP 的 WebView 中 // 是否运行在 原生APP 的 WebView 中
export 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[] }>();
const bridgeSub = new Subject<MobileDatagram>();
export const bridgeOb = bridgeSub.asObservable(); export const bridgeOb = bridgeSub.asObservable();
export function emitBridgeEvent(event: { func: string; data: Record<string, any> | any[] }) {
export function emitBridgeEvent(event: MobileDatagram) {
bridgeSub.next(event); bridgeSub.next(event);
} }
export function registerBridgeFunc() { export function registerBridgeFunc() {
window.bridgeFunc = {
peripheralStatus: (param: string) => {
bridgeSub.next({ func: 'peripheralStatus', data: JSON.parse(param) });
},
};
// window.bridgeFunc = {
// peripheralStatus: (param: string) => {
// bridgeSub.next({ func: 'peripheralStatus', data: JSON.parse(param) });
// },
// };
// window.bridgeCall = (func, param) => { // window.bridgeCall = (func, param) => {
// const res = window.bridgeFunc[func].call(null, param); // const res = window.bridgeFunc[func].call(null, param);
@ -113,16 +114,6 @@ export default class Bridge {
if (appWebview) { if (appWebview) {
window.WebViewJavascriptBridge.registerHandler(name, func); window.WebViewJavascriptBridge.registerHandler(name, func);
} }
// window.bridgeFunc.peripheralStatus(
// JSON.stringify({
// isConnected: true,
// power: 99,
// inclinatorx: 0.22,
// inclinatorY: 3.01,
// temperature: 32.02,
// })
// );
} }
static showModal(param: ShowModelParam) { static showModal(param: ShowModelParam) {
@ -147,7 +138,7 @@ export default class Bridge {
resolve(JSON.parse(res)); resolve(JSON.parse(res));
}); });
} else { } else {
return httpRequest<BridgeBaseResult>({ url: '/api/mobile/startMeasure', method: 'POST' });
return httpRequest<BridgeBaseResult>({ url: '/api/measure/start', method: 'POST' });
} }
} }

13
src/utils/helper.ts

@ -1,4 +1,4 @@
import { KTJOrg } from "../services/apiTypes";
import { KTJOrg } from '../services/apiTypes';
export type LabelKTJOrg = { export type LabelKTJOrg = {
label: string; label: string;
@ -14,6 +14,7 @@ export type LabelKTJOrg = {
}; };
export function labeledKtjOrgs(ktjOrgs: KTJOrg[]): LabelKTJOrg[] { export function labeledKtjOrgs(ktjOrgs: KTJOrg[]): LabelKTJOrg[] {
console.log('为科天健JSON数组加标签');
return ktjOrgs.map((org) => { return ktjOrgs.map((org) => {
const _org: LabelKTJOrg = { const _org: LabelKTJOrg = {
label: org.value || org.key, label: org.value || org.key,
@ -30,3 +31,13 @@ export function labeledKtjOrgs(ktjOrgs: KTJOrg[]): LabelKTJOrg[] {
return _org; return _org;
}); });
} }
export function textsOfKeys(keys: string[], orgs: LabelKTJOrg[]): string[] {
const l1 = orgs.find((item) => item.value === keys[0]);
if (!l1) return [];
const l2 = l1.children.find((item) => item.value === keys[1]);
if (!l2) return [];
const l3 = l2.children.find((item) => item.value === keys[2]);
if (!l3) return [];
return [l1.label, l2.label, l3.label];
}
Loading…
Cancel
Save