You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

505 lines
16 KiB

5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
5 months ago
5 months ago
4 months ago
5 months ago
  1. import StepItem, { StepName, StepState } from '../components/StepItem';
  2. import { useNavigate } from 'react-router-dom';
  3. import CustomNavBar from '../components/CustomNavBar';
  4. import MeasurementCanvas, {
  5. BenchmarkShape,
  6. MeasurementCanvasRef,
  7. Point,
  8. } from '../components/konva/MeasurementCanvas';
  9. import { useEffect, useRef, useState } from 'react';
  10. import RailTypeBtn from '../components/RailTypeBtn';
  11. import { Cascader, Dialog, Input, Mask, Picker, SpinLoading, Toast } from 'antd-mobile';
  12. import { useAppDispatch, useAppSelector } from '../utils/hooks';
  13. import { updateMeasureData, updateTaskState } from '../store/features/measureSlice';
  14. import Bridge from '../utils/bridge';
  15. import { selectLabeledKtjOrgs, updateRailPoints } from '../store/features/baseData';
  16. import { updateOrg } from '../store/features/contextSlice';
  17. import { selectOrgTextArr } from '../store';
  18. import icon_left from "../assets/icon_left.svg";
  19. import icon_right from "../assets/icon_right.svg";
  20. import icon_up from "../assets/icon_up.svg";
  21. import icon_down from "../assets/icon_down.svg";
  22. import icon_leftR from "../assets/icon_leftR.svg";
  23. import icon_rightR from "../assets/icon_rightR.svg";
  24. export default function Measure() {
  25. const navigate = useNavigate();
  26. const dispatch = useAppDispatch();
  27. const labeledKtjOrgs = useAppSelector(selectLabeledKtjOrgs);
  28. const orgTextArr = useAppSelector(selectOrgTextArr);
  29. const measureState = useAppSelector((state) => state.measure);
  30. const contextState = useAppSelector((state) => state.context);
  31. const baseState = useAppSelector((state) => state.baseData);
  32. const [railPickerVisible, setRailPickerVisible] = useState(false);
  33. const [railId, setRailId] = useState<(number | string | null)[]>([]);
  34. const canvasRef = useRef<MeasurementCanvasRef>(null);
  35. const [railSize, setRailSize] = useState<(number | string | null)>();
  36. const iconWidth = 35;
  37. // 默认选中第一个轨型
  38. useEffect(() => {
  39. if (baseState.railTypes.length > 0) {
  40. let railData = baseState.railTypes[0]
  41. setRailId([railData.id]);
  42. setRailSize(railData.code)
  43. }
  44. }, [baseState.railTypes]);
  45. function drawRailBaseLine(points: string) {
  46. const benchmarkShapes = JSON.parse(points) as BenchmarkShape[];
  47. if (canvasRef.current) {
  48. canvasRef.current.setBenchmarkData(benchmarkShapes);
  49. }
  50. }
  51. // 检查轨型有没有坐标,如果有,绘制轨型基准线,如果没,拉取再绘制其线
  52. useEffect(() => {
  53. if (railId.length > 0) {
  54. const r = baseState.railTypes.find((rail) => rail.id === railId[0]);
  55. if (!r) return;
  56. if (!!r.points) {
  57. drawRailBaseLine(r.points);
  58. return;
  59. }
  60. Bridge.getTrackPoint({ code: r.code }).then((res) => {
  61. if (res.success) {
  62. dispatch(updateRailPoints(res.data));
  63. drawRailBaseLine(res.data.points!);
  64. } else {
  65. Toast.show(res.message);
  66. }
  67. });
  68. }
  69. }, [baseState.railTypes, dispatch, railId]);
  70. // 绘制测量坐标线
  71. useEffect(() => {
  72. if (canvasRef.current) {
  73. canvasRef.current.setMeasurementDataLeft(measureState.leftPoints);
  74. }
  75. }, [measureState.leftPoints]);
  76. useEffect(() => {
  77. if (canvasRef.current) {
  78. canvasRef.current.setMeasurementDataRight(measureState.rightPoints);
  79. }
  80. }, [measureState.rightPoints]);
  81. const onSaveClick = () => {
  82. dispatch(updateMeasureData(newMeasureData))
  83. navigate('/measure/save');
  84. };
  85. const [caloading, setCaloading] = useState(false)
  86. const [showCalibration, setshowCalibration] = useState(false)
  87. const onCalibrationBtnClick = () => {
  88. setCaloading(true)
  89. Bridge.alignPoints({railSize:railSize || 'GX-60'}).then(res=>{
  90. if(res.success){
  91. setshowCalibration(true)
  92. canvasRef.current?.setMeasurementCalibrationData(res.data)
  93. }else{
  94. }
  95. setCaloading(false)
  96. }).catch(e=>{
  97. setCaloading(false)
  98. Toast.show({
  99. content: <span></span>,
  100. position: 'top',
  101. })
  102. })
  103. }
  104. const onStartClick = () => {
  105. setshowCalibration(false)
  106. dispatch(updateMeasureData([]))
  107. if (!contextState.device.connected) {
  108. Dialog.alert({
  109. content: '蓝牙未连接,请先连接蓝牙',
  110. onConfirm: () => {
  111. navigate('/home/bluetooth');
  112. },
  113. });
  114. return;
  115. }
  116. if (baseState.ktjOrgs.length === 0) {
  117. Dialog.alert({
  118. content: '请在基础数据同步完成后重试',
  119. onConfirm: () => {
  120. navigate('/home/mine');
  121. },
  122. });
  123. return;
  124. }
  125. if (!contextState.currOrgCode) {
  126. Dialog.alert({
  127. content: '请选择铁路局/工务段/线路',
  128. onConfirm: () => {
  129. onOrgBarClick();
  130. },
  131. });
  132. return;
  133. }
  134. // if (contextState.device.power < 20) {
  135. // Toast.show("电量低于20%,请充电后测量");
  136. // return;
  137. // }
  138. Bridge.startMeasure().then((res) => {
  139. if (res.success) {
  140. dispatch(updateTaskState('START_RECORD_SIG'));
  141. } else {
  142. Toast.show(res.message);
  143. }
  144. openAudio()
  145. });
  146. };
  147. const openAudio = () => {
  148. const audioReady = new Audio("/audio/ready.mp3");
  149. // 播放音频
  150. audioReady
  151. .play()
  152. .then(() => {
  153. console.log("音频开始播放 已准备好");
  154. })
  155. .catch(err => {
  156. console.error("播放音频时出错:", err);
  157. });
  158. }
  159. const onOrgBarClick = async () => {
  160. if (baseState.ktjOrgs.length === 0) {
  161. Dialog.alert({
  162. content: '请在基础数据同步完成后重试',
  163. onConfirm: () => {
  164. navigate('/home/mine');
  165. },
  166. });
  167. return;
  168. }
  169. const value = await Cascader.prompt({
  170. options: labeledKtjOrgs,
  171. placeholder: '请选择',
  172. });
  173. // Toast.show(value ? `你选择了 ${value.join(' - ')}` : '你没有进行选择');
  174. if (value) {
  175. dispatch(updateOrg(value as string[]));
  176. }
  177. };
  178. function stepState(step: StepName): StepState {
  179. if (!measureState.taskState) {
  180. return 'none';
  181. }
  182. switch (measureState.taskState) {
  183. case 'START_RECORD_SIG':
  184. case 'WRONG_SIDE':
  185. if (step === 'left_ready') {
  186. return 'ongoing';
  187. } else {
  188. return 'none';
  189. }
  190. case 'START_RECORD_LEFT':
  191. if (step === 'left_ready') {
  192. return 'done';
  193. } else if (step === 'left_begin') {
  194. return 'ongoing';
  195. } else {
  196. return 'none';
  197. }
  198. case 'FINISH_RECORD_LEFT':
  199. if (step === 'left_ready' || step === 'left_begin' || step === 'left_end') {
  200. return 'done';
  201. } else if (step === 'right_ready') {
  202. return 'ongoing';
  203. } else {
  204. return 'none';
  205. }
  206. case 'START_RECORD_RIGHT':
  207. if (step === 'right_begin') {
  208. return 'ongoing';
  209. } else if (step === 'right_end') {
  210. return 'none';
  211. } else {
  212. return 'done';
  213. }
  214. case 'FINISH_RECORD_RIGHT': {
  215. return 'done';
  216. }
  217. default:
  218. return 'none';
  219. }
  220. }
  221. function railName() {
  222. return baseState.railTypes.find((r) => r.id === railId[0])?.name || '';
  223. }
  224. function onRailSizeChange(ids:(number | string | null)[]){
  225. if(ids && ids.length){
  226. setRailId(ids);
  227. let id = ids[0]
  228. const codes = baseState.railTypes.map(item => {
  229. if(item.id === id){
  230. return item.code
  231. }
  232. })
  233. if(codes && codes.length){
  234. setRailSize(codes[0])
  235. }
  236. }
  237. }
  238. //上下移动
  239. const timerRef = useRef<NodeJS.Timeout | null>(null);
  240. const handlePressStart = (type:string) => {
  241. timerRef.current = setInterval(() => {
  242. console.log('你进行了长按操作!');
  243. onHandleMove(type)
  244. }, 500);
  245. };
  246. const handlePressEnd = () => {
  247. if (timerRef.current) {
  248. clearInterval(timerRef.current);
  249. timerRef.current = null;
  250. }
  251. };
  252. const onMoveLine = (type:string) => {
  253. console.log('这是点击')
  254. onHandleMove(type)
  255. }
  256. const onHandleMove = (type:string) => {
  257. let list = canvasRef.current?.getMeasurementCalibrationData()
  258. if(list && list.length){
  259. list.forEach(item => {
  260. if(type === 'up'){//向上移动,原数据减y X轴不动
  261. item.y = item.y - distance/1000;
  262. }
  263. if(type === 'down'){//向上移动,原数据加y X轴不动
  264. item.y = item.y + distance/1000;
  265. }
  266. if(type === 'left'){//向左移动,原数据减x Y轴不动
  267. item.x = item.x - distance/1000;
  268. }
  269. if(type === 'right'){//向右移动,原数据加x Y轴不动
  270. item.x = item.x + distance/1000;
  271. }
  272. })
  273. canvasRef.current?.setMeasurementCalibrationData(list)
  274. setNewMeasureData(list)
  275. }
  276. }
  277. const handleRotationPressStart = (type:string) => {
  278. timerRef.current = setInterval(() => {
  279. onRotationLine(type)
  280. }, 500);
  281. }
  282. //旋转
  283. let [measurementRotation, setMeasurementRotation] = useState<number>(0)
  284. let [newMeasureData, setNewMeasureData] = useState<Point[]>()
  285. let [angle, setAngle] = useState<number>(5);//角度单位 分
  286. let [distance, setDistance] = useState<number>(100)
  287. const onRotationLine = (type:string) => {
  288. let mrValue = 0
  289. if(type === 'left'){//逆时针
  290. mrValue = measurementRotation - (angle/60) * Math.PI / 180;
  291. }
  292. if(type === 'right'){//顺时针
  293. mrValue = measurementRotation + (angle/60) * Math.PI / 180;
  294. }
  295. let list = canvasRef.current?.getMeasurementCalibrationData()
  296. if(list && list.length){
  297. list.forEach((item, index) => {
  298. let cloneItem = rotatePoint(item, mrValue)
  299. item.x = cloneItem.x
  300. item.y = cloneItem.y
  301. })
  302. canvasRef.current?.setMeasurementCalibrationData(list)
  303. setNewMeasureData(list)
  304. }
  305. }
  306. const rotatePoint = (pt:{x:number;y:number}, angle:number) => {
  307. const item = {
  308. x: pt.x * Math.cos(angle) - pt.y * Math.sin(angle),
  309. y: pt.x * Math.sin(angle) + pt.y * Math.cos(angle)
  310. };
  311. return item
  312. }
  313. const handleContextMenu = (e:any) => {
  314. e.preventDefault();
  315. };
  316. return (
  317. <>
  318. <div className="relative pt-[--navBarHeight]">
  319. <div className="absolute top-0 w-full z-10">
  320. <CustomNavBar title={'测量'}></CustomNavBar>
  321. </div>
  322. <main className="home-page-content overflow-x-hidden overflow-y-auto">
  323. <div className="relative h-0 p-0 pb-[70%]">
  324. {/**正在校准时的loading */}
  325. {caloading &&
  326. <Mask opacity='thin' className='h-[100vh] flex justify-center items-center'>
  327. <div style={{margin:"45%"}}>
  328. <SpinLoading color='#5c92b4'/>
  329. <div className='w-[100px] mt-[20px] text-[#5c92b4]'>...</div>
  330. </div>
  331. </Mask>
  332. }
  333. {/**测量区 */}
  334. <div className="absolute left-0 right-0 top-0 bottom-0 bg-title">
  335. <MeasurementCanvas
  336. width={window.innerWidth}
  337. height={window.innerWidth * 0.7}
  338. logicalExtent={{ minX: -45, maxX: 45, minY: -18, maxY: 52 }}
  339. gridStep={3}
  340. origin={{ x: 0, y: 20 }}
  341. pixelPerMm={window.innerWidth / 90}
  342. maxZoom={8}
  343. showGrid={true}
  344. showBenchmark={true}
  345. showAnalysis={false}
  346. showScale={false}
  347. scaleInterval={1}
  348. showCoordinates={false}
  349. showCalibration={showCalibration}
  350. ref={canvasRef}
  351. />
  352. {/**选择轨型区 */}
  353. {railId.length > 0 && (
  354. <div className="absolute left-1 bottom-1">
  355. <RailTypeBtn text={railName()} onClick={() => setRailPickerVisible(true)} />
  356. </div>
  357. )}
  358. </div>
  359. </div>
  360. {/**局段线区 */}
  361. <section
  362. className="h-10 bg-[#e3e8f5] flex justify-between items-center px-4"
  363. onClick={onOrgBarClick}
  364. >
  365. <p className="text-text" style={{ color: contextState.currOrgCode ? '#333' : 'red' }}>
  366. {contextState.currOrgCode ? orgTextArr.join('/') : '点击此处选择铁路局和工务段'}
  367. </p>
  368. <span className="text-primary underline"></span>
  369. </section>
  370. {/**手动校准区 */}
  371. {showCalibration &&
  372. <section className="h-10 bg-[#e3e8f5] flex justify-between items-center px-4">
  373. <img
  374. width={iconWidth}
  375. src={icon_left}
  376. onClick={()=>(onMoveLine("left"))}
  377. onTouchStart={()=>handlePressStart("left")}
  378. onTouchEnd={handlePressEnd}
  379. onContextMenu={handleContextMenu}
  380. className="text-[20px] ml-[5px]" alt="左移"/>
  381. <img
  382. width={iconWidth}
  383. src={icon_right}
  384. onClick={()=>(onMoveLine("right"))}
  385. onTouchStart={()=>handlePressStart("right")}
  386. onTouchEnd={handlePressEnd}
  387. onContextMenu={handleContextMenu}
  388. className="text-[20px] ml-[5px]" alt="右移"/>
  389. <img
  390. width={iconWidth}
  391. src={icon_up}
  392. onClick={()=>(onMoveLine("up"))}
  393. onTouchStart={()=>handlePressStart("up")}
  394. onTouchEnd={handlePressEnd}
  395. onContextMenu={handleContextMenu}
  396. className="text-[20px] ml-[5px]"
  397. alt="上移"/>
  398. <img
  399. width={iconWidth}
  400. src={icon_down}
  401. onClick={()=>(onMoveLine("down"))}
  402. onTouchStart={()=>handlePressStart("down")}
  403. onTouchEnd={handlePressEnd}
  404. onContextMenu={handleContextMenu}
  405. className="text-[20px] ml-[5px]" alt="下移"/>
  406. <img
  407. width={iconWidth}
  408. src={icon_leftR}
  409. onClick={()=>(onRotationLine("left"))}
  410. onTouchStart={()=>handleRotationPressStart("left")}
  411. onTouchEnd={handlePressEnd}
  412. onContextMenu={handleContextMenu}
  413. className="text-[20px] ml-[5px]"
  414. alt="逆时针旋转"/>
  415. <img
  416. width={iconWidth}
  417. src={icon_rightR}
  418. onClick={()=>(onRotationLine("right"))}
  419. onTouchStart={()=>handleRotationPressStart("right")}
  420. onTouchEnd={handlePressEnd}
  421. onContextMenu={handleContextMenu}
  422. className="text-[20px] ml-[5px]"
  423. alt="顺时针旋转"/>
  424. </section>
  425. }
  426. {/**按钮操作区 */}
  427. <section className="flex items-center gap-4 px-4 my-4">
  428. <button className="btn-contained rounded-md text-sm h-10 flex-1" onClick={onStartClick}>
  429. {measureState.leftPoints.length > 0 ? '重新测量' : '开始测量'}
  430. </button>
  431. <button
  432. className="btn-contained rounded-md text-sm h-10 flex-1"
  433. disabled={measureState.taskState !== 'FINISH_RECORD_RIGHT'}
  434. onClick={onSaveClick}
  435. >
  436. </button>
  437. <button
  438. className="btn-contained rounded-md text-sm h-10 flex-1"
  439. disabled={measureState.taskState !== 'FINISH_RECORD_RIGHT'}
  440. onClick={onCalibrationBtnClick}
  441. >
  442. </button>
  443. </section>
  444. {/**测量状态区 */}
  445. <section className="grid grid-cols-2 gap-[10px]">
  446. <StepItem state={stepState('left_ready')} text={'等待测量'} />
  447. <StepItem state={stepState('right_ready')} text={'等待测量另一侧'} />
  448. <StepItem state={stepState('left_begin')} text={'正在进行测量'} />
  449. <StepItem state={stepState('right_begin')} text={'正在进行测量'} />
  450. <StepItem state={stepState('left_end')} text={'一侧测量完成'} />
  451. <StepItem state={stepState('right_end')} text={'测量已完成'} />
  452. </section>
  453. </main>
  454. </div>
  455. <Picker
  456. columns={[baseState.railTypes.map((t) => ({ label: t.name, value: t.id }))]}
  457. visible={railPickerVisible}
  458. onClose={() => {
  459. setRailPickerVisible(false);
  460. }}
  461. value={railId}
  462. onConfirm={(v) => {
  463. onRailSizeChange(v)
  464. }}
  465. />
  466. </>
  467. );
  468. }