Commit 51d18ad1 authored by fenghen777's avatar fenghen777

feat: WebSocket代理+项目拖拽排序

- vite.config.js: 添加 /rosbridge 代理转发到 ws://localhost:9090
- useRosBridge.js: WebSocket URL 改为通过 Vite 代理连接,添加诊断日志
- ProjectPanel.jsx: WebSocket URL 改为代理路径,项目列表支持拖拽排序
- LiveChart.jsx: 硬件面板 WebSocket 改为代理路径
- useProjectStore.js: 新增 moveProject 排序方法
- ProjectPanel.module.css: 拖拽上/下方紫色提示线样式
parent cf0265ab
......@@ -48,6 +48,7 @@ export default function ProjectPanel() {
stopHil,
hilDone,
fetchHilResults,
moveProject,
} = useProjectStore();
const nodes = useFlowStore(s => s.nodes);
......@@ -64,6 +65,10 @@ export default function ProjectPanel() {
const toastTimerRef = useRef(null);
const [confirmState, setConfirmState] = useState(null); // { id, name }
// ── 拖拽排序状态 ──
const dragIndexRef = useRef(null);
const [dragOver, setDragOver] = useState(null); // { index, position: 'above'|'below' }
const showToast = useCallback((msg) => {
setToastMsg(msg);
setToastFading(false);
......@@ -99,7 +104,8 @@ export default function ProjectPanel() {
function createWs() {
if (done) return;
ws = new WebSocket(`ws://${window.location.hostname}:9090`);
const wsProto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${wsProto}//${window.location.host}/rosbridge`);
ws.onopen = () => {
retryCount = 0;
ws.send(JSON.stringify({ op: 'subscribe', topic: '/hil/done', type: 'std_msgs/String' }));
......@@ -246,10 +252,51 @@ export default function ProjectPanel() {
</div>
</div>
) : (
projects.map(p => {
projects.map((p, index) => {
const isActive = p.id === activeProjectId;
return (
<div key={p.id} className={`${styles.projectItem} ${isActive ? styles.active : ''}`}>
<div
key={p.id}
className={[
styles.projectItem,
isActive ? styles.active : '',
dragOver?.index === index && dragOver?.position === 'above' ? styles.dragOverAbove : '',
dragOver?.index === index && dragOver?.position === 'below' ? styles.dragOverBelow : '',
].filter(Boolean).join(' ')}
draggable
onDragStart={(e) => {
dragIndexRef.current = index;
e.dataTransfer.effectAllowed = 'move';
e.currentTarget.style.opacity = '0.4';
}}
onDragEnd={(e) => {
e.currentTarget.style.opacity = '';
dragIndexRef.current = null;
setDragOver(null);
}}
onDragOver={(e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (dragIndexRef.current === null || dragIndexRef.current === index) return;
const rect = e.currentTarget.getBoundingClientRect();
const pos = (e.clientY - rect.top) < rect.height / 2 ? 'above' : 'below';
setDragOver({ index, position: pos });
}}
onDragLeave={() => setDragOver(null)}
onDrop={(e) => {
e.preventDefault();
const from = dragIndexRef.current;
if (from !== null && from !== index) {
const rect = e.currentTarget.getBoundingClientRect();
const pos = (e.clientY - rect.top) < rect.height / 2 ? 'above' : 'below';
let to = pos === 'above' ? index : index + 1;
if (from < to) to--;
if (from !== to) moveProject(from, to);
}
dragIndexRef.current = null;
setDragOver(null);
}}
>
{/* 项目标题行 */}
<div className={styles.projectHeader} onClick={() => isActive ? openProject(null) : openProject(p.id)}>
<span className={styles.projectIcon}>
......@@ -566,7 +613,7 @@ export default function ProjectPanel() {
{/* 实时图表弹窗 */}
{showLiveChart && (
<LiveChart
rosBridgeUrl={`ws://${window.location.hostname}:9090`}
rosBridgeUrl={`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/rosbridge`}
sessionId={activeProject?.hilResult?.sessionId}
onClose={() => setShowLiveChart(false)}
onDone={() => {
......
......@@ -115,6 +115,21 @@
transition: all 0.15s ease;
background: rgba(255, 255, 255, 0.02);
overflow: hidden;
cursor: grab;
}
.projectItem:active {
cursor: grabbing;
}
.projectItem.dragOverAbove {
border-top: 2px solid #6366f1;
margin-top: -1px;
}
.projectItem.dragOverBelow {
border-bottom: 2px solid #6366f1;
margin-bottom: -1px;
}
.projectItem:hover {
......
......@@ -105,7 +105,8 @@ export default function LiveChart({ rosBridgeUrl, sessionId, onClose, onDone })
// ── 硬件面板 WebSocket ──
useEffect(() => {
if (!hwPorts.length) return;
const ws = new WebSocket(`ws://${window.location.hostname}:9090`);
const wsProto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${wsProto}//${window.location.host}/rosbridge`);
hwWsRef.current = ws;
ws.onopen = () => {
ws.send(JSON.stringify({ op: 'advertise', topic: '/hil/user_override', type: 'std_msgs/String' }));
......
......@@ -167,6 +167,15 @@ const useProjectStore = create((set, get) => ({
});
},
/** 移动项目位置 (拖拽排序) */
moveProject: (fromIndex, toIndex) => {
const projects = [...get().projects];
const [moved] = projects.splice(fromIndex, 1);
projects.splice(toIndex, 0, moved);
persistProjects(projects);
set({ projects });
},
/** 更新项目的 Modelica 模型名 */
updateModelName: (id, modelName) => {
const updated = get().projects.map(p =>
......
......@@ -47,11 +47,13 @@ export default function useRosBridge({ onDone } = {}) {
const connectInternal = useCallback((url) => {
if (wsRef.current) return;
console.log('[RosBridge] connecting to', url, 'attempt', retryCountRef.current);
setStatus('connecting');
const ws = new WebSocket(url);
wsRef.current = ws;
ws.onopen = () => {
console.log('[RosBridge] connected!');
setStatus('connected');
retryCountRef.current = 0; // 连接成功, 重置重试计数
// 订阅仿真数据
......@@ -101,8 +103,8 @@ export default function useRosBridge({ onDone } = {}) {
}
};
ws.onerror = () => {
// onerror 后通常会触发 onclose, 重连逻辑放在 onclose 里
ws.onerror = (e) => {
console.warn('[RosBridge] error', e);
};
ws.onclose = () => {
......@@ -127,7 +129,11 @@ export default function useRosBridge({ onDone } = {}) {
};
}, [flushToState]);
const connect = useCallback((url = 'ws://localhost:9090') => {
const connect = useCallback((url) => {
if (!url) {
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
url = `${proto}//${window.location.host}/rosbridge`;
}
if (wsRef.current) return;
urlRef.current = url;
manualDisconnectRef.current = false;
......
......@@ -10,6 +10,12 @@ export default defineConfig({
target: 'http://localhost:8888',
changeOrigin: true,
},
'/rosbridge': {
target: 'ws://localhost:9090',
ws: true,
rewriteWsOrigin: true,
rewrite: (path) => path.replace(/^\/rosbridge/, ''),
},
},
},
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment