修复 拖动障碍物 优化dpadcontrol 未测试
This commit is contained in:
34
deploy.sh
Normal file
34
deploy.sh
Normal file
@@ -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 "===================================="
|
||||
@@ -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);
|
||||
// 通知父组件更新列表
|
||||
|
||||
// 通知父组件更新列表
|
||||
|
||||
@@ -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<string, string> = {
|
||||
D: 'right',
|
||||
};
|
||||
|
||||
const DPadControl: React.FC<DPadControlProps> = ({ onGamepadUpdate, onSpeedChange, disabled = false, onEmergencyStop }) => {
|
||||
const DIRECTIONS = ['up', 'down', 'left', 'right'];
|
||||
|
||||
const DPadControl: React.FC<DPadControlProps> = ({
|
||||
onGamepadUpdate,
|
||||
onSpeedChange,
|
||||
disabled = false,
|
||||
onEmergencyStop,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [pressed, setPressed] = useState<Record<string, boolean>>({});
|
||||
const [speed, setSpeed] = useState(0.5);
|
||||
const pressTimer = useRef<number | null>(null);
|
||||
// 用 ref 存储最新状态,避免 setInterval 闭包捕获旧值
|
||||
|
||||
// 每个物理输入单独记录,避免同方向不同按键互相干扰
|
||||
// 例如 ArrowUp 和 W 可以同时按下
|
||||
const activeInputsRef = useRef<Map<string, string>>(new Map());
|
||||
|
||||
const pressedRef = useRef<Record<string, boolean>>({});
|
||||
const speedRef = useRef(0.5);
|
||||
// 始终指向最新的 emit,保证「按住持续发送」不受旧闭包影响
|
||||
const emitRef = useRef<((overrides?: Record<string, boolean>) => void) | null>(null);
|
||||
// 记录当前按下的键盘按键,避免 keydown 重复触发
|
||||
const activeKeysRef = useRef<Set<string>>(new Set());
|
||||
// 用 ref 存储外部回调,避免 emit/startPress/endPress 因回调引用变化而重建,
|
||||
// 进而触发 useEffect 清理把持续发送的定时器清掉
|
||||
const pressTimer = useRef<number | null>(null);
|
||||
const disabledRef = useRef(disabled);
|
||||
const emergencyTimer = useRef<number | null>(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<string, boolean> = {}): GamepadState => {
|
||||
const btnMap = { ...pressedRef.current, ...overrides };
|
||||
// 根据所有仍然按住的输入,重新计算方向状态
|
||||
const syncPressed = useCallback(() => {
|
||||
const next: Record<string, boolean> = {};
|
||||
|
||||
// 方向键映射到左摇杆 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<string, boolean>) => {
|
||||
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<string, boolean> = {
|
||||
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<DPadControlProps> = ({ 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,
|
||||
) => (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
aria-label={ariaLabel}
|
||||
disabled={disabled}
|
||||
className={`${btnBase} ${pressed[key] ? btnActive : btnIdle} ${disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||
onMouseDown={() => startPress(key)}
|
||||
onMouseUp={() => endPress(key)}
|
||||
onMouseLeave={() => pressed[key] && endPress(key)}
|
||||
onTouchStart={(e) => { e.preventDefault(); startPress(key); }}
|
||||
onTouchEnd={(e) => { e.preventDefault(); endPress(key); }}
|
||||
className={`${btnBase} ${pressed[key] ? btnActive : btnIdle
|
||||
} ${disabled
|
||||
? 'opacity-40 cursor-not-allowed'
|
||||
: 'cursor-pointer'
|
||||
}`}
|
||||
onPointerDown={(e) => {
|
||||
if (disabled) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const sourceId = `pointer:${e.pointerId}`;
|
||||
startPress(sourceId, key);
|
||||
|
||||
try {
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
} catch {
|
||||
// 某些环境可能不支持 pointer capture
|
||||
}
|
||||
}}
|
||||
onPointerUp={(e) => {
|
||||
endPress(`pointer:${e.pointerId}`);
|
||||
}}
|
||||
onPointerCancel={(e) => {
|
||||
endPress(`pointer:${e.pointerId}`);
|
||||
}}
|
||||
onLostPointerCapture={(e) => {
|
||||
endPress(`pointer:${e.pointerId}`);
|
||||
}}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
);
|
||||
|
||||
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 = () => (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('devices.emergencyStop')}
|
||||
disabled={disabled}
|
||||
className={`${btnBase} ${pressed.center ? 'border-red-500 bg-red-500/40 text-white shadow-[0_0_16px_rgba(239,68,68,0.6)]' : 'border-red-500/70 bg-red-500/10 text-red-400 hover:bg-red-500/20'} ${disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||
onClick={() => startEmergency()}
|
||||
onTouchStart={(e) => { e.preventDefault(); startEmergency(); }}
|
||||
className={`${btnBase} ${pressed.center
|
||||
? 'border-red-500 bg-red-500/40 text-white shadow-[0_0_16px_rgba(239,68,68,0.6)]'
|
||||
: 'border-red-500/70 bg-red-500/10 text-red-400 hover:bg-red-500/20'
|
||||
} ${disabled
|
||||
? 'opacity-40 cursor-not-allowed'
|
||||
: 'cursor-pointer'
|
||||
}`}
|
||||
onClick={startEmergency}
|
||||
>
|
||||
<ShieldAlert className="w-5 h-5" />
|
||||
</button>
|
||||
@@ -212,9 +399,19 @@ const DPadControl: React.FC<DPadControlProps> = ({ onGamepadUpdate, onSpeedChang
|
||||
<div />
|
||||
{renderBtn('up', <ChevronUp className="w-6 h-6" />, t('devices.dpad.up'))}
|
||||
<div />
|
||||
|
||||
{renderBtn('left', <ChevronLeft className="w-6 h-6" />, t('devices.dpad.left'))}
|
||||
{onEmergencyStop ? renderEmergency() : renderBtn('center', <Circle className="w-5 h-5 fill-current" />, t('devices.dpad.center'))}
|
||||
|
||||
{onEmergencyStop
|
||||
? renderEmergency()
|
||||
: renderBtn(
|
||||
'center',
|
||||
<Circle className="w-5 h-5 fill-current" />,
|
||||
t('devices.dpad.center'),
|
||||
)}
|
||||
|
||||
{renderBtn('right', <ChevronRight className="w-6 h-6" />, t('devices.dpad.right'))}
|
||||
|
||||
<div />
|
||||
{renderBtn('down', <ChevronDown className="w-6 h-6" />, t('devices.dpad.down'))}
|
||||
<div />
|
||||
@@ -223,26 +420,38 @@ const DPadControl: React.FC<DPadControlProps> = ({ onGamepadUpdate, onSpeedChang
|
||||
{/* 速度滑块 */}
|
||||
<div className="w-full px-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-slate-400 font-medium">{t('devices.dpad.speed')}</span>
|
||||
<span className="text-sm font-bold text-cyan-300">{Math.round(speed * 100)}%</span>
|
||||
<span className="text-sm text-slate-400 font-medium">
|
||||
{t('devices.dpad.speed')}
|
||||
</span>
|
||||
<span className="text-sm font-bold text-cyan-300">
|
||||
{Math.round(speed * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={speed}
|
||||
disabled={disabled}
|
||||
onChange={(e) => {
|
||||
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%)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between mt-1.5 text-[11px] text-slate-500">
|
||||
<span>0%</span>
|
||||
<span>50%</span>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user