diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..aae03df --- /dev/null +++ b/deploy.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# =============================== +# 配置区 +# =============================== +# 服务器用户名 +SERVER_USER="root" +# 服务器IP或域名 +SERVER_HOST="1.95.137.212" +# 服务器部署目录(打包文件将上传到这里) +SERVER_PATH="/www/wwwroot/dist1" +# 本地打包目录 +BUILD_DIR="dist" + +# =============================== +# 自动化部署流程 +# =============================== + +echo "========== 前端自动部署脚本 ==========" + +# 1. 打包 +echo "[1/3] 正在打包项目..." +npm run build:prod || { echo "打包失败!"; exit 1; } +echo "[+] 正在复制 txt 文件到 dist ..." +cp 2X9Hgqt14u.txt dist/ # <-- 这里加你要复制的文件名 +echo "[+] 已复制 2X9Hgqt14u.txt 到 dist/" + +# 2. 上传到服务器(使用 rsync 增量同步) +echo "[2/3] 正在上传文件到服务器 $SERVER_HOST ..." +scp -r $BUILD_DIR/* $SERVER_USER@$SERVER_HOST:$SERVER_PATH || { echo "上传失败!"; exit 1; } + +# 3. 完成 +echo "[3/3] 部署完成!访问服务器检查页面。" +echo "====================================" diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 1becfc2..c5561e2 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -1,6 +1,5 @@ import React, { useEffect, useRef, useState, useMemo, useImperativeHandle, forwardRef, useCallback } from 'react'; import carImg from '../assets/images/carplane.png'; -import inspectImg from "../assets/images/inspectCar.png" import planeImg from '../assets/images/planeicon.png'; import cameraImg from '../assets/images/camera.png'; import droneInspectImg from '../assets/images/xunjiancar.png'; @@ -11,8 +10,7 @@ import { TracePoint, TPAction, TPMode } from '../lib/TracePoint/tracePoint'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { getSiteMapInfo, getStationRegions } from '../api/stationManage'; -import { Button } from "antd"; -import { EnvironmentOutlined } from "@ant-design/icons"; + import * as Cesium from 'cesium'; @@ -37,7 +35,6 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine, const onMarkPointRef = useRef(onMarkPoint); // 总是指向最新的 onMarkPoint,避免闭包滞后 const onCameraClickRef = useRef(onCameraClick); // 同样处理,避免闭包滞后 const lastHoverPositionRef = useRef(null); - const hasInitialDevicesFlyRef = useRef(false); const { t } = useTranslation(); // 场站已配置的返航点 / 分区叠加显示 @@ -83,7 +80,6 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine, //}; const resetFlightPriority = () => { lastFlightPriorityRef.current = -1; }; const getObstacleHandlePosition = ( - Cesium, lon, lat, radius @@ -1685,7 +1681,6 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine, }, position: getObstacleHandlePosition( - Cesium, lon, lat, radius @@ -1891,7 +1886,6 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine, if (handle) { handle.position = new Cesium.ConstantPositionProperty( getObstacleHandlePosition( - Cesium, dragging.lon, dragging.lat, nextRadius @@ -1928,7 +1922,6 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine, if (viewer) { viewer.scene.screenSpaceCameraController.enableRotate = true; } - console.log('radius', radius, index, siteMapInfoId); // 通知父组件更新列表 // 通知父组件更新列表 diff --git a/src/components/devices/DPadControl.tsx b/src/components/devices/DPadControl.tsx index 6ebb4db..e0682d6 100644 --- a/src/components/devices/DPadControl.tsx +++ b/src/components/devices/DPadControl.tsx @@ -1,6 +1,14 @@ + import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { ChevronUp, ChevronDown, ChevronLeft, ChevronRight, Circle, ShieldAlert } from 'lucide-react'; +import { + ChevronUp, + ChevronDown, + ChevronLeft, + ChevronRight, + Circle, + ShieldAlert, +} from 'lucide-react'; export interface GamepadState { buttons: boolean[]; @@ -32,118 +40,234 @@ const KEY_MAP: Record = { D: 'right', }; -const DPadControl: React.FC = ({ onGamepadUpdate, onSpeedChange, disabled = false, onEmergencyStop }) => { +const DIRECTIONS = ['up', 'down', 'left', 'right']; + +const DPadControl: React.FC = ({ + onGamepadUpdate, + onSpeedChange, + disabled = false, + onEmergencyStop, +}) => { const { t } = useTranslation(); + const [pressed, setPressed] = useState>({}); const [speed, setSpeed] = useState(0.5); - const pressTimer = useRef(null); - // 用 ref 存储最新状态,避免 setInterval 闭包捕获旧值 + + // 每个物理输入单独记录,避免同方向不同按键互相干扰 + // 例如 ArrowUp 和 W 可以同时按下 + const activeInputsRef = useRef>(new Map()); + const pressedRef = useRef>({}); const speedRef = useRef(0.5); - // 始终指向最新的 emit,保证「按住持续发送」不受旧闭包影响 - const emitRef = useRef<((overrides?: Record) => void) | null>(null); - // 记录当前按下的键盘按键,避免 keydown 重复触发 - const activeKeysRef = useRef>(new Set()); - // 用 ref 存储外部回调,避免 emit/startPress/endPress 因回调引用变化而重建, - // 进而触发 useEffect 清理把持续发送的定时器清掉 + const pressTimer = useRef(null); + const disabledRef = useRef(disabled); + const emergencyTimer = useRef(null); + const onGamepadUpdateRef = useRef(onGamepadUpdate); - onGamepadUpdateRef.current = onGamepadUpdate; const onSpeedChangeRef = useRef(onSpeedChange); + const onEmergencyStopRef = useRef(onEmergencyStop); + + onGamepadUpdateRef.current = onGamepadUpdate; onSpeedChangeRef.current = onSpeedChange; + onEmergencyStopRef.current = onEmergencyStop; + disabledRef.current = disabled; - // 构建状态:只传输 xy 坐标(axes),buttons 全部为 false - const buildState = useCallback( - (overrides: Record = {}): GamepadState => { - const btnMap = { ...pressedRef.current, ...overrides }; + // 根据所有仍然按住的输入,重新计算方向状态 + const syncPressed = useCallback(() => { + const next: Record = {}; - // 方向键映射到左摇杆 axes[0] X, axes[1] Y - let axisX = 0; - let axisY = 0; - const spd = speedRef.current; - // 向右为正、向左为负;向上为正、向下为负 - if (btnMap.left) axisX -= spd; - if (btnMap.right) axisX += spd; - if (btnMap.up) axisY += spd; - if (btnMap.down) axisY -= spd; + for (const direction of DIRECTIONS) { + next[direction] = false; + } - return { - buttons: new Array(17).fill(false), - axes: [0, axisY, axisX, 0], - connected: true, - ts: Date.now(), - }; - }, - [], - ); + for (const direction of activeInputsRef.current.values()) { + next[direction] = true; + } - const emit = useCallback( - (overrides?: Record) => { - if (disabled) return; - const state = overrides ? buildState(overrides) : buildState(); - onGamepadUpdateRef.current?.(state); - }, - [buildState, disabled], - ); - emitRef.current = emit; + // center 不参与摇杆坐标计算 + next.center = pressedRef.current.center ?? false; + pressedRef.current = next; + setPressed(next); + + return next; + }, []); + + // 生成摇杆状态 + const buildState = useCallback((): GamepadState => { + const current = pressedRef.current; + const spd = speedRef.current; + + let axisX = 0; + let axisY = 0; + + if (current.left) axisX -= spd; + if (current.right) axisX += spd; + if (current.up) axisY -= spd; + if (current.down) axisY += spd; + + // 对角线归一化,避免斜向速度超过直线速度 + if (axisX !== 0 && axisY !== 0) { + const scale = 1 / Math.sqrt(2); + axisX *= scale; + axisY *= scale; + } + + return { + buttons: new Array(17).fill(false), + axes: [0, axisY, axisX, 0], + connected: true, + ts: Date.now(), + }; + }, []); + + const emit = useCallback(() => { + if (disabledRef.current) return; + onGamepadUpdateRef.current?.(buildState()); + }, [buildState]); + + // 始终只保留一个定时器 + const startTimer = useCallback(() => { + if (pressTimer.current !== null) return; + + pressTimer.current = window.setInterval(() => { + emit(); + }, 100); + }, [emit]); + + const stopTimer = useCallback(() => { + if (pressTimer.current !== null) { + window.clearInterval(pressTimer.current); + pressTimer.current = null; + } + }, []); + + // 开始按住某个输入 const startPress = useCallback( - (key: string) => { - if (disabled) return; - pressedRef.current = { ...pressedRef.current, [key]: true }; - setPressed(pressedRef.current); - emit({ [key]: true }); - // 持续发送(模拟手柄按住) - if (pressTimer.current) window.clearInterval(pressTimer.current); - pressTimer.current = window.setInterval(() => { - emitRef.current?.(); - }, 100); + (sourceId: string, direction: string) => { + if (disabledRef.current) return; + + // 防止同一物理输入重复触发 + if (activeInputsRef.current.has(sourceId)) return; + + activeInputsRef.current.set(sourceId, direction); + + syncPressed(); + emit(); + startTimer(); }, - [disabled, emit], + [syncPressed, emit, startTimer], ); + // 释放某个输入,但保留其他仍按住的方向 const endPress = useCallback( - (key: string) => { - if (disabled) return; - if (pressTimer.current) { - window.clearInterval(pressTimer.current); - pressTimer.current = null; + (sourceId: string) => { + if (!activeInputsRef.current.has(sourceId)) return; + + activeInputsRef.current.delete(sourceId); + + syncPressed(); + + if (activeInputsRef.current.size === 0) { + stopTimer(); } - pressedRef.current = { ...pressedRef.current, [key]: false }; - setPressed(pressedRef.current); - emit({ [key]: false }); + + // 立即发送最新状态,避免松开后残留旧方向 + emit(); }, - [disabled, emit], + [syncPressed, stopTimer, emit], ); - // 键盘控制:方向键 / WASD + // 清除所有输入并发送停止状态 + const releaseAll = useCallback(() => { + activeInputsRef.current.clear(); + + const next: Record = { + up: false, + down: false, + left: false, + right: false, + center: pressedRef.current.center ?? false, + }; + + pressedRef.current = next; + setPressed(next); + + stopTimer(); + emit(); + }, [stopTimer, emit]); + const startEmergency = useCallback(() => { + if (disabledRef.current) return; + + pressedRef.current = { + ...pressedRef.current, + center: true, + }; + setPressed({ ...pressedRef.current }); + + onEmergencyStopRef.current?.(); + + if (emergencyTimer.current !== null) { + window.clearTimeout(emergencyTimer.current); + } + + emergencyTimer.current = window.setTimeout(() => { + pressedRef.current = { + ...pressedRef.current, + center: false, + }; + setPressed({ ...pressedRef.current }); + emergencyTimer.current = null; + }, 350); + }, []); + + // 键盘控制:支持方向键 + WASD 组合 useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - // 焦点在输入框时不触发 - const target = e.target as HTMLElement; - if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) { + const target = e.target as HTMLElement | null; + + if ( + target && + (target.tagName === 'INPUT' || + target.tagName === 'TEXTAREA' || + target.isContentEditable) + ) { return; } - const key = KEY_MAP[e.key]; - if (!key) return; - // 防止 keydown 持续触发(系统自动重复) - if (activeKeysRef.current.has(key)) return; - activeKeysRef.current.add(key); + if (e.code === 'Space') { + e.preventDefault(); + + // 防止长按空格时重复触发急停 + if (!e.repeat) { + startEmergency(); + } + + return; + } + + + const direction = KEY_MAP[e.key]; + if (!direction) return; + e.preventDefault(); - startPress(key); + + // 使用物理按键标识,而不是方向名 + // 这样 ArrowUp 和 W 不会被当成同一个输入 + + startPress(`keyboard:${e.code}`, direction); + }; const handleKeyUp = (e: KeyboardEvent) => { - const key = KEY_MAP[e.key]; - if (!key) return; - activeKeysRef.current.delete(key); + const direction = KEY_MAP[e.key]; + if (!direction) return; + e.preventDefault(); - endPress(key); + endPress(`keyboard:${e.code}`); }; - // 窗口失焦时清空所有按键状态 const handleBlur = () => { - activeKeysRef.current.forEach((key) => endPress(key)); - activeKeysRef.current.clear(); + releaseAll(); }; window.addEventListener('keydown', handleKeyDown); @@ -154,52 +278,115 @@ const DPadControl: React.FC = ({ onGamepadUpdate, onSpeedChang window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keyup', handleKeyUp); window.removeEventListener('blur', handleBlur); - // 清理定时器 - if (pressTimer.current) { - window.clearInterval(pressTimer.current); - pressTimer.current = null; - } }; - }, [startPress, endPress]); + }, [startPress, endPress, releaseAll, startEmergency]); + + // 禁用时立即停止;卸载时清理定时器 + useEffect(() => { + if (disabled) { + activeInputsRef.current.clear(); + pressedRef.current = { + up: false, + down: false, + left: false, + right: false, + center: false, + }; + setPressed(pressedRef.current); + stopTimer(); + + // 直接发送中立轴状态,避免 emit 因 disabled 而跳过 + onGamepadUpdateRef.current?.({ + buttons: new Array(17).fill(false), + axes: [0, 0, 0, 0], + connected: true, + ts: Date.now(), + }); + } + + return () => { + stopTimer(); + }; + }, [disabled, stopTimer]); + + // 紧急停止按钮的短暂按压动画 + + + useEffect(() => { + return () => { + if (emergencyTimer.current !== null) { + window.clearTimeout(emergencyTimer.current); + } + stopTimer(); + }; + }, [stopTimer]); const btnBase = - 'flex items-center justify-center w-8 h-8 rounded-xl border transition-all duration-150 select-none active:scale-95'; - const btnActive = 'border-cyan-400 bg-cyan-500/30 text-cyan-300 shadow-[0_0_16px_rgba(34,211,238,0.4)]'; - const btnIdle = 'border-slate-600 bg-slate-800/60 text-slate-300 hover:border-cyan-500/60 hover:text-cyan-300'; + 'flex items-center justify-center w-8 h-8 rounded-xl border transition-all duration-150 select-none active:scale-95 touch-none'; - const renderBtn = (key: string, icon: React.ReactNode, ariaLabel: string) => ( + const btnActive = + 'border-cyan-400 bg-cyan-500/30 text-cyan-300 shadow-[0_0_16px_rgba(34,211,238,0.4)]'; + + const btnIdle = + 'border-slate-600 bg-slate-800/60 text-slate-300 hover:border-cyan-500/60 hover:text-cyan-300'; + + const renderBtn = ( + key: string, + icon: React.ReactNode, + ariaLabel: string, + ) => ( ); - const startEmergency = () => { - if (disabled) return; - pressedRef.current = { ...pressedRef.current, center: true }; - setPressed(pressedRef.current); - onEmergencyStop?.(); - window.setTimeout(() => { - pressedRef.current = { ...pressedRef.current, center: false }; - setPressed(pressedRef.current); - }, 350); - }; - const renderEmergency = () => ( @@ -212,9 +399,19 @@ const DPadControl: React.FC = ({ onGamepadUpdate, onSpeedChang
{renderBtn('up', , t('devices.dpad.up'))}
+ {renderBtn('left', , t('devices.dpad.left'))} - {onEmergencyStop ? renderEmergency() : renderBtn('center', , t('devices.dpad.center'))} + + {onEmergencyStop + ? renderEmergency() + : renderBtn( + 'center', + , + t('devices.dpad.center'), + )} + {renderBtn('right', , t('devices.dpad.right'))} +
{renderBtn('down', , t('devices.dpad.down'))}
@@ -223,26 +420,38 @@ const DPadControl: React.FC = ({ onGamepadUpdate, onSpeedChang {/* 速度滑块 */}
- {t('devices.dpad.speed')} - {Math.round(speed * 100)}% + + {t('devices.dpad.speed')} + + + {Math.round(speed * 100)}% +
+ { - const v = parseFloat(e.target.value); - setSpeed(v); - speedRef.current = v; - onSpeedChangeRef.current?.(v); + const value = parseFloat(e.target.value); + + setSpeed(value); + speedRef.current = value; + onSpeedChangeRef.current?.(value); + + // 改速后立即发送新速度对应的轴值 + emit(); }} className="w-full h-2.5 rounded-full appearance-none cursor-pointer bg-slate-700 accent-cyan-400" style={{ - background: `linear-gradient(to right, #22d3ee 0%, #22d3ee ${speed * 100}%, #334155 ${speed * 100}%, #334155 100%)`, + background: `linear-gradient(to right, #22d3ee 0%, #22d3ee ${speed * 100 + }%, #334155 ${speed * 100}%, #334155 100%)`, }} /> +
0% 50% diff --git a/src/components/devices/MapEditorPage.tsx b/src/components/devices/MapEditorPage.tsx index f956c51..b0e2525 100644 --- a/src/components/devices/MapEditorPage.tsx +++ b/src/components/devices/MapEditorPage.tsx @@ -746,53 +746,61 @@ export default function MapEditorPage() { const handleObstacleRadiusChange = async ( index: number, radius: number, - siteMapInfoId: number | null + idFromMap: number | null = null ) => { - console.log("nextList") - const roundedRadius = Number(radius.toFixed(2)); - const nextList = obstaclePointConfigList.map( - (item, idx) => - idx === index - ? { ...item, radius: roundedRadius } - : item - ); - console.log(nextList, "333", siteMapInfoId, "siteMapInfoIdnextList") - const payload = { - id: siteMapInfoId, - tempObstaclePoints: nextList?.map((item) => ({ - radius: item.radius, - position: { - lat: item.lat, - lng: item.lon, - }, + // 以服务端最新数据为准:本地列表可能为空,或顺序与地图上的不一致 + let infoRes: any = null; + try { + infoRes = await getSiteMapInfo({ siteId: stationId }); + } catch (e) { + infoRes = null; + } - alias: item.name, - })), + const remoteList: any[] = + infoRes?.code === 200 && Array.isArray(infoRes.data?.tempObstaclePoints) + ? infoRes.data.tempObstaclePoints + : []; + const infoId: number | null = + (infoRes?.code === 200 ? infoRes.data?.id : null) ?? idFromMap ?? null; + + // 远端列表优先,为空时退回本地已配置列表 + const baseList: any[] = remoteList.length > 0 ? remoteList : obstaclePointConfigList; + + if (!baseList.length || index < 0 || index >= baseList.length) { + messageApi.error('未找到障碍物点数据,请刷新后重试'); + return; + } + + const nextPoints = baseList.map((item: any, idx: number) => ({ + radius: idx === index ? roundedRadius : Number(item.radius) || 0, + position: { + lat: Number(item.position?.lat ?? item.lat ?? 0), + lng: Number(item.position?.lng ?? item.position?.lon ?? item.lon ?? 0), + }, + alias: item.alias ?? item.name ?? '', + })); + + const payload: any = { + tempObstaclePoints: nextPoints, siteId: stationId, orgId: currentStation?.orgId, - } + }; + // id 为空时不要传 id 字段,否则后端会把 null 主键当成非法新增 + if (infoId != null) payload.id = infoId; + const res: any = await saveSiteMapInfo(payload); if (res?.code === 200) { messageApi.success('障碍物点半径修改成功'); - getSiteMapInfoFn(); setOverlayRefreshKey(k => k + 1); - } else { messageApi.error( res?.msg || '障碍物点半径修改失败' ); } - - - console.log(nextList, "nextList") - - //setObstaclePointConfigList(nextList); - - // 使用 nextList 调用你现有的保存接口 }; // ========== 点击预设路径在地图上预览(覆盖=框,单点=线) ========== const handlePreviewRoute = async (route: any) => { diff --git a/vite.config.ts b/vite.config.ts index 0c64226..2dd039c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,5 +1,5 @@ - // CESIUM_BASE_URL 由 vite-plugin-cesium 自动注入(值为 base + '/cesium/',小写带尾斜杠) - // 手动覆写会与 viteStaticCopy 的小写 cesium/ 目标路径冲突,因此注释掉 +// CESIUM_BASE_URL 由 vite-plugin-cesium 自动注入(值为 base + '/cesium/',小写带尾斜杠) +// 手动覆写会与 viteStaticCopy 的小写 cesium/ 目标路径冲突,因此注释掉 import tailwindcss from '@tailwindcss/vite'; import react from '@vitejs/plugin-react'; import path, { resolve } from 'path'; @@ -24,29 +24,30 @@ export default defineConfig(({ mode }) => { react(), tailwindcss(), - cesiumPlugin(), viteStaticCopy({ - targets: [ - { - src: "node_modules/cesium/Build/Cesium", - dest: "" - }, { - src: 'node_modules/cesium/Build/Cesium/Workers/**', - dest: 'cesium/Workers', - }, - { - src: 'node_modules/cesium/Build/Cesium/ThirdParty/**', - dest: 'cesium/ThirdParty', - }, - { - src: 'node_modules/cesium/Build/Cesium/Assets/**', - dest: 'cesium/Assets', - }, - { - src: 'node_modules/cesium/Build/Cesium/Widgets/**', - dest: 'cesium/Widgets', - }, - ] - }) + cesiumPlugin(), + // viteStaticCopy({ + // targets: [ + // { + // src: "node_modules/cesium/Build/Cesium", + // dest: "" + // }, { + // src: 'node_modules/cesium/Build/Cesium/Workers/**', + // dest: 'cesium/Workers', + // }, + // { + // src: 'node_modules/cesium/Build/Cesium/ThirdParty/**', + // dest: 'cesium/ThirdParty', + // }, + // { + // src: 'node_modules/cesium/Build/Cesium/Assets/**', + // dest: 'cesium/Assets', + // }, + // { + // src: 'node_modules/cesium/Build/Cesium/Widgets/**', + // dest: 'cesium/Widgets', + // }, + // ] + //}) ], @@ -67,7 +68,7 @@ export default defineConfig(({ mode }) => { rollupOptions: { output: { - + }, }, },