坐标问题

This commit is contained in:
ywb
2026-05-26 14:26:43 +08:00
parent c7042b1bba
commit 9e90b68bf1
7 changed files with 535 additions and 45 deletions
+305 -21
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>设置 - AGV 拍摄系统</title> <title>设置 - AGV 拍摄系统</title>
<link rel="stylesheet" href="/static/css/style.css?v=20260514a"> <link rel="stylesheet" href="/static/css/style.css?v=20260520h">
</head> </head>
<body> <body>
<div id="app"> <div id="app">
@@ -12,7 +12,7 @@
<div class="logo">⚙️ 系统设置</div> <div class="logo">⚙️ 系统设置</div>
<nav class="nav"> <nav class="nav">
<a href="/" class="nav-link">🏠 首页</a> <a href="/" class="nav-link">🏠 首页</a>
<a href="/setting" class="nav-link active">⚙️ 设置</a> <href="/setting" class="nav-link active">⚙️ 设置</a>
<a href="/running" class="nav-link">▶️ 运行</a> <a href="/running" class="nav-link">▶️ 运行</a>
</nav> </nav>
</header> </header>
@@ -21,6 +21,8 @@
<div class="tabs"> <div class="tabs">
<button class="tab" :class="{active: tab === 'map'}" @click="tab = 'map'">🗺️ 地图</button> <button class="tab" :class="{active: tab === 'map'}" @click="tab = 'map'">🗺️ 地图</button>
<button class="tab" :class="{active: tab === 'mission'}" @click="tab = 'mission'">🎯 任务配置</button> <button class="tab" :class="{active: tab === 'mission'}" @click="tab = 'mission'">🎯 任务配置</button>
<button class="tab" :class="{active: tab === 'qr'}" @click="tab = 'qr'">📷 二维码配置</button>
<button class="tab" :class="{active: tab === 'model'}" @click="tab = 'model'">📦 机型配置</button>
<button class="tab" :class="{active: tab === 'arm'}" @click="tab = 'arm'">🤖 机械臂</button> <button class="tab" :class="{active: tab === 'arm'}" @click="tab = 'arm'">🤖 机械臂</button>
<button class="tab" :class="{active: tab === 'agv'}" @click="tab = 'agv'">🚗 AGV控制</button> <button class="tab" :class="{active: tab === 'agv'}" @click="tab = 'agv'">🚗 AGV控制</button>
</div> </div>
@@ -56,8 +58,11 @@
<button class="btn btn-secondary" style="padding:4px 10px;font-size:12px" @click="resetMapView">重置</button> <button class="btn btn-secondary" style="padding:4px 10px;font-size:12px" @click="resetMapView">重置</button>
</div> </div>
</div> </div>
<div class="map-container" :style="{ transform: 'rotate(' + mapRotation + 'deg)', transition: 'transform 0.3s ease' }" style="position:relative;background:#111;border-radius:8px;overflow:hidden"> <div class="map-container" style="position:relative;background:#111;border-radius:8px;overflow:hidden">
<!-- 地图旋转 wrapper -->
<div :style="{ transform: 'rotate(' + mapRotation + 'deg)', transition: 'transform 0.3s ease' }">
<img :src="mapImageUrl" @error="onMapError" @click="onMapClick" style="width:100%;display:block;cursor:crosshair" title="点击地图导航到该位置"> <img :src="mapImageUrl" @error="onMapError" @click="onMapClick" style="width:100%;display:block;cursor:crosshair" title="点击地图导航到该位置">
<!-- 地图覆盖层:显示点位坐标 -->
<div class="map-overlay"> <div class="map-overlay">
<!-- AGV 实时位置 --> <!-- AGV 实时位置 -->
<div v-if="navCurrentPos && nav2Available" <div v-if="navCurrentPos && nav2Available"
@@ -71,11 +76,179 @@
:style="{ left: getMapX(p.coords) + '%', top: getMapY(p.coords) + '%' }" :style="{ left: getMapX(p.coords) + '%', top: getMapY(p.coords) + '%' }"
:title="p.coords ? p.coords.map(c => c.toFixed(2)).join(', ') : ''"> :title="p.coords ? p.coords.map(c => c.toFixed(2)).join(', ') : ''">
</div> </div>
<!-- 二维码位置点 -->
<div v-for="(m, mi) in missionConfig.machines" :key="'qrdot-'+mapVersion+'-'+mi"
v-if="machineHasQr(m)"
class="map-dot qr-dot"
:style="qrMarkerStyle(m)"
:title="qrMarkerTitle(m)">
</div>
</div>
</div> </div>
</div> </div>
</section> </section>
</div> </div>
<!-- ========== 机型配置 Tab ========== -->
<div v-if="tab === 'model'">
<section class="card">
<h2>📦 机型配置</h2>
<!-- 添加机型按钮 -->
<div style="display:flex;justify-content:flex-end;margin-bottom:16px">
<button class="btn btn-primary" @click="showAddModelModal = true"> 添加机型</button>
</div>
<!-- 机型表格列表 -->
<div v-if="models.length === 0" style="text-align:center;color:#9aa0a6;padding:40px">
<p>暂无机型配置,请点击上方按钮添加</p>
</div>
<table v-else style="width:100%;border-collapse:collapse;margin-bottom:16px">
<thead>
<tr style="background:#1a2332;text-align:left">
<th style="padding:10px 12px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:13px">ID</th>
<th style="padding:10px 12px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:13px">机型名称</th>
<th style="padding:10px 12px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:13px">描述</th>
<th style="padding:10px 12px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:13px">备注</th>
<th style="padding:10px 12px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:13px;text-align:center">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="m in models" :key="m.id" style="border-bottom:1px solid #1a2332">
<td style="padding:10px 12px">{% raw %}{{ m.id }}{% endraw %}</td>
<td style="padding:10px 12px"><strong>{% raw %}{{ m.name }}{% endraw %}</strong></td>
<td style="padding:10px 12px;color:#9aa0a6">{% raw %}{{ m.description || '—' }}{% endraw %}</td>
<td style="padding:10px 12px;color:#9aa0a6">{% raw %}{{ m.notes || '—' }}{% endraw %}</td>
<td style="padding:10px 12px;text-align:center;white-space:nowrap">
<button class="btn btn-secondary btn-small" @click="expandedModelId = expandedModelId === m.id ? null : m.id">🤲 姿态</button>
<button class="btn btn-danger btn-small" @click="deleteModel(m.id)" style="margin-left:6px">🗑️ 删除</button>
</td>
</tr>
</tbody>
</table>
<!-- 姿态展开面板 -->
<div v-if="expandedModelId" style="border:1px solid #2a3441;border-radius:8px;overflow:hidden;margin-bottom:16px">
<div v-for="m in models.filter(m => m.id === expandedModelId)" :key="m.id">
<!-- 正面姿态 -->
<div style="padding:16px;background:#0f1923">
<h4 style="margin:0 0 12px 0;color:#388e3c">🟢 正面姿态</h4>
<div v-for="pose in m.poses.filter(p => p.photo_type === 'front')" :key="pose.id" style="background:#0f1923;padding:12px;border:1px solid #2a3441;border-radius:6px;margin-bottom:8px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<strong>{% raw %}{{ pose.name || '正面姿态' }}{% endraw %}</strong>
<button class="btn btn-danger btn-small" @click="deletePose(m.id, pose.id)">删除</button>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<div v-for="j in 6" :key="j" style="display:flex;align-items:center;gap:4px">
<span style="font-size:12px;color:#9aa0a6">J{% raw %}{{ j }}{% endraw %}</span>
<button class="btn btn-small" @click="adjustPoseAngle(m.id, pose.id, j-1, -0.5)" style="width:24px;height:24px;padding:0;font-size:12px">-</button>
<input type="number" step="0.5"
:value="pose.arm_angles && pose.arm_angles[j-1] !== undefined ? pose.arm_angles[j-1] : 0"
@change="updatePoseAngleAndMove(m.id, pose.id, j-1, $event.target.value)"
style="width:70px;padding:4px;border:1px solid #2a3441;border-radius:4px">
<button class="btn btn-small" @click="adjustPoseAngle(m.id, pose.id, j-1, 0.5)" style="width:24px;height:24px;padding:0;font-size:12px">+</button>
<span style="font-size:11px;color:#999">°</span>
</div>
</div>
<div style="margin-top:8px;display:flex;gap:8px">
<button class="btn btn-secondary btn-small" @click="refreshPoseAngles(m.id, pose.id)">🔄 刷新角度</button>
<button class="btn btn-primary btn-small" @click="applyPoseAngles(m.id, pose.id)">✅ 应用角度</button>
</div>
</div>
<div style="margin-top:8px">
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<input type="text" v-model="newPoseForm[m.id + '_front']"
placeholder="姿态名称(如:取料)"
style="flex:1;min-width:120px;padding:6px;border:1px solid #2a3441;border-radius:4px">
<button class="btn btn-secondary btn-small" @click="addPose(m.id, 'front', newPoseForm[m.id + '_front'])"> 添加正面姿态(当前角度)</button>
</div>
<div style="margin-top:6px;font-size:12px;color:#9aa0a6">
当前机械臂角度:
<span v-if="currentAngles && currentAngles.length">
J{% raw %}{{ currentAngles[0] ? currentAngles[0].toFixed(1) : '—' }}{% endraw %}°
J{% raw %}{{ currentAngles[1] ? currentAngles[1].toFixed(1) : '—' }}{% endraw %}°
J{% raw %}{{ currentAngles[2] ? currentAngles[2].toFixed(1) : '—' }}{% endraw %}°
J{% raw %}{{ currentAngles[3] ? currentAngles[3].toFixed(1) : '—' }}{% endraw %}°
J{% raw %}{{ currentAngles[4] ? currentAngles[4].toFixed(1) : '—' }}{% endraw %}°
J{% raw %}{{ currentAngles[5] ? currentAngles[5].toFixed(1) : '—' }}{% endraw %}°
</span>
<span v-else>(未连接机械臂)</span>
</div>
</div>
</div>
<!-- 背面姿态 -->
<div style="padding:16px;background:#0d1420">
<h4 style="margin:0 0 12px 0;color:#d32f2f">🔴 背面姿态</h4>
<div v-for="pose in m.poses.filter(p => p.photo_type === 'back')" :key="pose.id" style="background:#0f1923;padding:12px;border:1px solid #2a3441;border-radius:6px;margin-bottom:8px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<strong>{% raw %}{{ pose.name || '背面姿态' }}{% endraw %}</strong>
<button class="btn btn-danger btn-small" @click="deletePose(m.id, pose.id)">删除</button>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<div v-for="j in 6" :key="j" style="display:flex;align-items:center;gap:4px">
<span style="font-size:12px;color:#9aa0a6">J{% raw %}{{ j }}{% endraw %}</span>
<button class="btn btn-small" @click="adjustPoseAngle(m.id, pose.id, j-1, -0.5)" style="width:24px;height:24px;padding:0;font-size:12px">-</button>
<input type="number" step="0.5"
:value="pose.arm_angles && pose.arm_angles[j-1] !== undefined ? pose.arm_angles[j-1] : 0"
@change="updatePoseAngleAndMove(m.id, pose.id, j-1, $event.target.value)"
style="width:70px;padding:4px;border:1px solid #2a3441;border-radius:4px">
<button class="btn btn-small" @click="adjustPoseAngle(m.id, pose.id, j-1, 0.5)" style="width:24px;height:24px;padding:0;font-size:12px">+</button>
<span style="font-size:11px;color:#999">°</span>
</div>
</div>
<div style="margin-top:8px;display:flex;gap:8px">
<button class="btn btn-secondary btn-small" @click="refreshPoseAngles(m.id, pose.id)">🔄 刷新角度</button>
<button class="btn btn-primary btn-small" @click="applyPoseAngles(m.id, pose.id)">✅ 应用角度</button>
</div>
</div>
<div style="margin-top:8px">
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<input type="text" v-model="newPoseForm[m.id + '_back']"
placeholder="姿态名称(如:放料)"
style="flex:1;min-width:120px;padding:6px;border:1px solid #2a3441;border-radius:4px">
<button class="btn btn-secondary btn-small" @click="addPose(m.id, 'back', newPoseForm[m.id + '_back'])"> 添加背面姿态(当前角度)</button>
</div>
</div>
</div>
</div>
</div>
<!-- 添加机型 Modal -->
<div v-if="showAddModelModal" class="modal-overlay" @click.self="showAddModelModal = false">
<div class="modal-box" style="min-width:420px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px">
<h3 style="margin:0">📦 添加新机型</h3>
<button class="btn-icon" @click="showAddModelModal = false"></button>
</div>
<div class="form-group" style="margin-bottom:12px">
<label>机型名称</label>
<input type="text" v-model="newModelName" placeholder="例如:SMT-A" style="width:100%;padding:8px;border:1px solid #2a3441;border-radius:4px">
</div>
<div class="form-group" style="margin-bottom:12px">
<label>机型ID(数值)</label>
<input type="number" v-model.number="newModelId" placeholder="例如:100" style="width:100%;padding:8px;border:1px solid #2a3441;border-radius:4px">
</div>
<div class="form-group" style="margin-bottom:12px">
<label>描述</label>
<input type="text" v-model="newModelDesc" placeholder="描述信息" style="width:100%;padding:8px;border:1px solid #2a3441;border-radius:4px">
</div>
<div class="form-group" style="margin-bottom:12px">
<label>备注</label>
<input type="text" v-model="newModelNotes" placeholder="备注信息" style="width:100%;padding:8px;border:1px solid #2a3441;border-radius:4px">
</div>
<div class="btn-row">
<button class="btn btn-primary" @click="addModel">✅ 确认添加</button>
<button class="btn btn-secondary" @click="showAddModelModal = false">取消</button>
</div>
</div>
</div>
</section>
</div>
<!-- ========== 任务配置 Tab ========== --> <!-- ========== 任务配置 Tab ========== -->
<div v-if="tab === 'mission'"> <div v-if="tab === 'mission'">
@@ -91,9 +264,10 @@
<label>列数 N</label> <label>列数 N</label>
<input type="number" v-model.number="missionConfig.cols" min="1" max="20" placeholder="4"> <input type="number" v-model.number="missionConfig.cols" min="1" max="20" placeholder="4">
</div> </div>
<div class="form-group" style="align-self:end"> <div class="form-group" style="align-self:end;display:flex;gap:6px;flex-wrap:nowrap">
<button class="btn btn-primary" @click="generateGrid">🔲 生成网格</button> <button class="btn btn-primary" @click="generateGrid">🔲 生成网格</button>
<button class="btn btn-secondary" @click="saveMissionConfig" style="margin-left:6px">💾 保存网格</button> <button class="btn btn-secondary" @click="saveMissionConfig">💾 保存网格</button>
<button class="btn btn-warning" @click="initPose" :disabled="initPoseLoading">📍 初始化位置</button>
</div> </div>
</div> </div>
@@ -123,12 +297,16 @@
<div class="grid-cell grid-header">机器行 {% raw %}{{ ri }}{% endraw %}</div> <div class="grid-cell grid-header">机器行 {% raw %}{{ ri }}{% endraw %}</div>
<div v-for="(ci) in missionConfig.cols" :key="'m'+ri+'_'+ci" <div v-for="(ci) in missionConfig.cols" :key="'m'+ri+'_'+ci"
class="grid-cell" class="grid-cell"
:class="{ active: getMachineAt(ri-1, ci-1) }" :class="{ active: selectedMachine && selectedMachine.row === ri-1 && selectedMachine.col === ci-1 }"
@click="onCellClick(ri-1, ci-1)"> @click="onCellClick(ri-1, ci-1)">
<template v-if="getMachineAt(ri-1, ci-1)"> <label class="machine-toggle" @click.stop>
<div class="cell-machine"></div> <input type="checkbox"
</template> :checked="getMachineAt(ri-1, ci-1) !== null"
<span v-else class="empty-cell"></span> @change="toggleMachine(ri-1, ci-1, $event)">
<span class="machine-status" :class="getMachineAt(ri-1, ci-1) ? 'on' : 'off'">
{% raw %}{{ getMachineAt(ri-1, ci-1) ? '有机器' : '无机器' }}{% endraw %}
</span>
</label>
</div> </div>
<!-- 点位行 ri+1 (pointRow=ri): 上面机器的背面 / 下面机器的正面 --> <!-- 点位行 ri+1 (pointRow=ri): 上面机器的背面 / 下面机器的正面 -->
@@ -145,12 +323,16 @@
<div class="grid-cell grid-header">机器行 {% raw %}{{ missionConfig.rows }}{% endraw %}</div> <div class="grid-cell grid-header">机器行 {% raw %}{{ missionConfig.rows }}{% endraw %}</div>
<div v-for="(ci) in missionConfig.cols" :key="'m'+missionConfig.rows+'_'+ci" <div v-for="(ci) in missionConfig.cols" :key="'m'+missionConfig.rows+'_'+ci"
class="grid-cell" class="grid-cell"
:class="{ active: getMachineAt(missionConfig.rows-1, ci-1) }" :class="{ active: selectedMachine && selectedMachine.row === missionConfig.rows-1 && selectedMachine.col === ci-1 }"
@click="onCellClick(missionConfig.rows-1, ci-1)"> @click="onCellClick(missionConfig.rows-1, ci-1)">
<template v-if="getMachineAt(missionConfig.rows-1, ci-1)"> <label class="machine-toggle" @click.stop>
<div class="cell-machine"></div> <input type="checkbox"
</template> :checked="getMachineAt(missionConfig.rows-1, ci-1) !== null"
<span v-else class="empty-cell"></span> @change="toggleMachine(missionConfig.rows-1, ci-1, $event)">
<span class="machine-status" :class="getMachineAt(missionConfig.rows-1, ci-1) ? 'on' : 'off'">
{% raw %}{{ getMachineAt(missionConfig.rows-1, ci-1) ? '有机器' : '无机器' }}{% endraw %}
</span>
</label>
</div> </div>
<!-- 最后一个点位行 (pointRow=rows): 所有机器的背面拍摄点 --> <!-- 最后一个点位行 (pointRow=rows): 所有机器的背面拍摄点 -->
@@ -240,6 +422,39 @@
</div> </div>
</div> </div>
<!-- 二维码位置 -->
<div class="machine-form" style="margin-top:16px" v-if="hasQr">
<h3>🔍 二维码位置</h3>
<div class="form-row">
<div class="form-group">
<label>X 坐标</label>
<input type="number" step="0.01" :value="safeQrCoord(0)" @input="setQrCoord(0, Number($event.target.value))" placeholder="0.00">
</div>
<div class="form-group">
<label>Y 坐标</label>
<input type="number" step="0.01" :value="safeQrCoord(1)" @input="setQrCoord(1, Number($event.target.value))" placeholder="0.00">
</div>
<div class="form-group">
<label>Yaw (弧度)</label>
<input type="number" step="0.01" :value="safeQrCoord(2)" @input="setQrCoord(2, Number($event.target.value))" placeholder="0.00">
</div>
<div class="form-group" style="align-self:end">
<button class="btn btn-small btn-primary" @click="readQRPosition" :disabled="!agvConnected">📍 读取当前位置</button>
</div>
</div>
<!-- QR 扫描结果 -->
<div v-if="hasQrValue" style="margin-top:8px;padding:8px;background:#0f1923;border-radius:6px">
<span style="font-size:13px">📱 二维码值: <strong>{% raw %}{{ safeQr('qr_value') }}{% endraw %}</strong></span>
<span v-if="hasQrModelId" style="margin-left:12px;font-size:13px">🏷️ 匹配机型: <strong>{% raw %}{{ safeQrModelName() }}{% endraw %}</strong></span>
<span v-else style="margin-left:12px;font-size:13px;color:#ff9800">⚠️ 未匹配到机型</span>
</div>
<div class="btn-row" style="margin-top:8px">
<button class="btn btn-small btn-success" @click="scanQRCode(selectedMachine.id)" :disabled="!cameraOpened || qrScanning">
{% raw %}{{ qrScanning ? '扫描中...' : '📷 扫描二维码' }}{% endraw %}
</button>
</div>
</div>
<div class="btn-row" style="margin-top:16px"> <div class="btn-row" style="margin-top:16px">
<button class="btn btn-danger" @click="deleteMachine(selectedMachine.id)">🗑️ 删除此机器</button> <button class="btn btn-danger" @click="deleteMachine(selectedMachine.id)">🗑️ 删除此机器</button>
<button class="btn btn-secondary" @click="saveMachineCoords">💾 保存此机器配置</button> <button class="btn btn-secondary" @click="saveMachineCoords">💾 保存此机器配置</button>
@@ -290,19 +505,78 @@
<div class="hint" style="margin-top:4px"> <div class="hint" style="margin-top:4px">
当前: ({% raw %}{{ pointEditor.x.toFixed(2) }}{% endraw %}, {% raw %}{{ pointEditor.y.toFixed(2) }}{% endraw %}, {% raw %}{{ pointEditor.yaw.toFixed(2) }}{% endraw %}) 当前: ({% raw %}{{ pointEditor.x.toFixed(2) }}{% endraw %}, {% raw %}{{ pointEditor.y.toFixed(2) }}{% endraw %}, {% raw %}{{ pointEditor.yaw.toFixed(2) }}{% endraw %})
</div> </div>
<div class="hint" style="margin-top:6px;font-size:12px;color:#888"> <div class="hint" style="margin-top:6px;font-size:12px;color:#9aa0a6">
💡 此点位服务于: {% raw %}{{ getPointOwnerLabel(editingPoint.pointRow, editingPoint.col).split('·')[1] || '无' }}{% endraw %} 💡 此点位服务于: {% raw %}{{ getPointOwnerLabel(editingPoint.pointRow, editingPoint.col).split('·')[1] || '无' }}{% endraw %}
</div> </div>
</div> </div>
<div class="btn-row"> <div class="btn-row">
<button class="btn btn-primary" @click="loadPointFromAgv" :disabled="!agvConnected">📍 从AGV读取</button> <button class="btn btn-primary" @click="loadPointFromAgv" :disabled="!agvConnected">📍 从AGV读取</button>
<button class="btn btn-success" @click="savePoint">💾 保存</button> <button class="btn btn-success" @click="savePoint">💾 保存</button>
<button class="btn btn-secondary" @click="navigateToPoint" :disabled="!agvConnected || !nav2Available">🚗 导航到该点位</button>
<button class="btn btn-warning" @click="clearPoint" :disabled="canClearPoint(editingPoint.pointRow, editingPoint.col)">🗑️ 清空</button> <button class="btn btn-warning" @click="clearPoint" :disabled="canClearPoint(editingPoint.pointRow, editingPoint.col)">🗑️ 清空</button>
<button class="btn btn-secondary" @click="closePointEdit">取消</button> <button class="btn btn-secondary" @click="closePointEdit">取消</button>
</div> </div>
</div> </div>
</div> </div>
<!-- ========== 二维码配置 Tab ========== -->
<div v-if="tab === 'qr'">
<section class="card">
<h2>📷 二维码配置</h2>
<p style="color:#9aa0a6;font-size:13px;margin-bottom:16px">配置机械臂姿态(6个关节角度),通过机械臂摄像头识别二维码并匹配机型。</p>
<!-- 机械臂摄像头画面 -->
<div style="margin-bottom:16px">
<div class="camera-preview" style="max-width:640px">
<img :src="armCameraUrl" @error="onArmPreviewError" style="width:100%;border-radius:8px">
</div>
</div>
<div style="display:flex;justify-content:flex-end;margin-bottom:16px">
<input type="text" v-model="newQrName" placeholder="输入名称..." style="background:#0f1923;border:1px solid #2a3441;color:#fff;padding:8px 12px;border-radius:6px;margin-right:8px;width:180px">
<button class="btn btn-primary" @click="addQrConfig()"> 添加</button>
</div>
<div v-if="qrConfigs.length === 0" style="text-align:center;color:#9aa0a6;padding:40px">
<p>暂无二维码配置,请点击上方按钮添加</p>
</div>
<table v-else style="width:100%;border-collapse:collapse;margin-bottom:16px">
<thead>
<tr style="background:#1a2332;text-align:left">
<th style="padding:10px 8px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:12px">名称</th>
<th style="padding:10px 4px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:12px">J1</th>
<th style="padding:10px 4px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:12px">J2</th>
<th style="padding:10px 4px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:12px">J3</th>
<th style="padding:10px 4px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:12px">J4</th>
<th style="padding:10px 4px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:12px">J5</th>
<th style="padding:10px 4px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:12px">J6</th>
<th style="padding:10px 8px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:12px">二维码值</th>
<th style="padding:10px 8px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:12px">匹配机型</th>
<th style="padding:10px 8px;border-bottom:1px solid #2a3441;color:#9aa0a6;font-size:12px;text-align:center">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="q in qrConfigs" :key="q.id" style="border-bottom:1px solid #1a2332">
<td style="padding:10px 8px">
<input type="text" v-model="q.name" @change="saveQrConfig(q.id)"
style="background:transparent;border:1px solid transparent;color:#fff;padding:4px 6px;width:100px;font-size:13px"
@focus="$event.target.style.borderColor='#388e3c'" @blur="$event.target.style.borderColor='transparent'">
</td>
<td style="padding:10px 4px" v-for="ji in 6" :key="ji">
<input type="number" step="0.1" :value="getQrAngle(q, ji - 1)" @input="updateQrAngle(q.id, ji - 1, $event.target.value)" style="width:62px;padding:3px 4px;border:1px solid #2a3441;border-radius:4px;background:#0f1923;color:#fff;font-size:12px;text-align:center">
</td>
<td style="padding:10px 8px;color:#4fc3f7;font-size:12px;max-width:120px;overflow:hidden;text-overflow:ellipsis">{% raw %}{{ q.qr_value || '—' }}{% endraw %}</td>
<td style="padding:10px 8px;color:#9aa0a6;font-size:12px">{% raw %}{{ getQrModelName(q.model_id) }}{% endraw %}</td>
<td style="padding:10px 8px;text-align:center;white-space:nowrap">
<button class="btn btn-secondary btn-small" @click="readQrAngles(q.id)" :disabled="!armConnected" title="读取当前机械臂关节角度">📋 加载姿态</button>
<button class="btn btn-primary btn-small" @click="applyQrAngles(q.id)" :disabled="!armConnected" style="margin-left:3px" title="将姿态应用到机械臂">🤖 应用姿态</button>
<button class="btn btn-success btn-small" @click="scanQrEntry(q.id)" :disabled="qrScanningId === q.id" style="margin-left:3px" title="扫描二维码">📷</button>
<button class="btn btn-danger btn-small" @click="deleteQrConfig(q.id)" style="margin-left:3px" title="删除">🗑️</button>
</td>
</tr>
</tbody>
</table>
</section>
</div>
<!-- 机械臂控制 (保持不变) --> <!-- 机械臂控制 (保持不变) -->
<div v-if="tab === 'arm'"> <div v-if="tab === 'arm'">
<section class="card"> <section class="card">
@@ -312,7 +586,7 @@
</div> </div>
<div v-else> <div v-else>
<div class="camera-preview"> <div class="camera-preview">
<img :src="previewUrl" @error="onPreviewError"> <img :src="armCameraUrl" @error="onArmPreviewError">
</div> </div>
<div class="joints-panel"> <div class="joints-panel">
<h3>关节角度控制</h3> <h3>关节角度控制</h3>
@@ -326,7 +600,6 @@
<button @mousedown="jogStart(j-1, 1)" @mouseup="jogStop(j-1)" @mouseleave="jogStop(j-1)"></button> <button @mousedown="jogStart(j-1, 1)" @mouseup="jogStop(j-1)" @mouseleave="jogStop(j-1)"></button>
</div> </div>
</div> </div>
</div>
<div class="btn-row"> <div class="btn-row">
<button class="btn btn-primary" @click="refreshAngles">🔄 刷新角度</button> <button class="btn btn-primary" @click="refreshAngles">🔄 刷新角度</button>
<button class="btn btn-secondary" @click="applyAngles">✅ 应用角度</button> <button class="btn btn-secondary" @click="applyAngles">✅ 应用角度</button>
@@ -349,8 +622,19 @@
</div> </div>
<div class="agv-status-bar"> <div class="agv-status-bar">
<span>🔋 电压: <strong>{% raw %}{{ agvBattery !== null ? agvBattery + 'V' : '—' }}{% endraw %}</strong></span> <span>🔋 电压: <strong>{% raw %}{{ agvBattery !== null ? agvBattery + 'V' : '—' }}{% endraw %}</strong></span>
<span v-if="agvPosition">📍 位置: <strong>X={% raw %}{{ agvPosition[0] ? agvPosition[0].toFixed(2) : '?' }}{% endraw %} Y={% raw %}{{ agvPosition[1] ? agvPosition[1].toFixed(2) : '?' }}{% endraw %}</strong></span> <span v-if="agvPosition">📍 位置: <strong>X={% raw %}{{ agvPosition[0] !== undefined ? agvPosition[0].toFixed(2) : '?' }}{% endraw %} Y={% raw %}{{ agvPosition[1] !== undefined ? agvPosition[1].toFixed(2) : '?' }}{% endraw %} yaw={% raw %}{{ agvPosition[2] !== undefined ? (agvPosition[2] * 180 / Math.PI).toFixed(1) : '?' }}{% endraw }}°</strong></span>
<button class="btn btn-small" @click="refreshAgvPosition">🔄 刷新</button> <button class="btn btn-small" @click="refreshAgvPosition">🔄 刷新</button>
<button class="btn btn-small" :class="{'btn-primary': !initPoseLoading}" :disabled="initPoseLoading" @click="initAmclPose">
{% raw %}{{ initPoseLoading ? '初始化中...' : '🎯 初始化定位' }}{% endraw %}
</button>
<span v-if="initPoseMsg" style="margin-left:8px;color:#4caf50;font-size:13px">{% raw %}{{ initPoseMsg }}{% endraw %}</span>
</div>
<!-- Nav2 导航状态 -->
<div v-if="nav2Available" class="nav2-status-bar" style="margin-top:8px;padding:8px 12px;background:#1a2332;border-radius:6px;font-size:13px">
<span>🧭 Nav2: <strong :style="navStatus === 'succeeded' ? 'color:#4caf50' : navStatus === 'navigating' ? 'color:#ff9800' : 'color:#9aa0a6'">{% raw %}{{ navStatus }}{% endraw %}</strong></span>
<span v-if="navCurrentPos" style="margin-left:12px">📍 当前位置: <strong>X={% raw %}{{ navCurrentPos[0] !== undefined ? navCurrentPos[0].toFixed(2) : '?' }}{% endraw %} Y={% raw %}{{ navCurrentPos[1] !== undefined ? navCurrentPos[1].toFixed(2) : '?' }}{% endraw %}</strong></span>
<button class="btn btn-small" style="margin-left:8px" @click="refreshNavStatus">🔄 刷新</button>
<button v-if="navStatus === 'navigating'" class="btn btn-small btn-secondary" @click="cancelNav" style="margin-left:4px">取消导航</button>
</div> </div>
<div class="agv-control-panel"> <div class="agv-control-panel">
<div class="agv-dir-row"> <div class="agv-dir-row">
@@ -392,7 +676,7 @@
</main> </main>
</div> </div>
<script src="/static/js/vue3.global.prod.js?v=20260513b"></script> <script src="/static/js/vue3.global.prod.js?v=20260520h"></script>
<script src="/static/js/setting.js?v=20260514g"></script> <script src="/static/js/setting.js?v=20260520h"></script>
</body> </body>
</html> </html>
+1
View File
@@ -247,6 +247,7 @@ echo " ✅ 精度参数已设置"
# ---------- 7. 启动 Flask ---------- # ---------- 7. 启动 Flask ----------
echo "[7/8] 启动 Flask API..." echo "[7/8] 启动 Flask API..."
export ROS_DOMAIN_ID=1
cd "$AGV_APP_DIR" cd "$AGV_APP_DIR"
nohup python3 app.py > /tmp/agv_flask.log 2>&1 & nohup python3 app.py > /tmp/agv_flask.log 2>&1 &
FLASK_PID=$! FLASK_PID=$!
+14 -3
View File
@@ -162,20 +162,30 @@ createApp({
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json()
this.nav2Available = data.nav2_available this.nav2Available = data.nav2_available
if (data.current_pos) { if (data.current_position) {
this.navCurrentPos = data.current_pos this.navCurrentPos = data.current_position
} }
} }
} catch (e) {} } catch (e) {}
}, },
async onMapClick(e) { async onMapClick(e) {
if (!this.mapMeta || !this.agvConnected) return if (!this.mapMeta) {
this.mapMsg = '❌ 地图未加载'
setTimeout(() => { this.mapMsg = '' }, 3000)
return
}
if (!this.agvConnected) {
this.mapMsg = '❌ AGV 未连接,无法导航'
setTimeout(() => { this.mapMsg = '' }, 3000)
return
}
const rect = e.target.getBoundingClientRect() const rect = e.target.getBoundingClientRect()
const px = (e.clientX - rect.left) / rect.width const px = (e.clientX - rect.left) / rect.width
const py = (e.clientY - rect.top) / rect.height const py = (e.clientY - rect.top) / rect.height
const { resolution, origin } = this.mapMeta const { resolution, origin } = this.mapMeta
const wx = origin[0] + px * resolution * this.mapMeta.width const wx = origin[0] + px * resolution * this.mapMeta.width
const wy = origin[1] + (1 - py) * resolution * this.mapMeta.height const wy = origin[1] + (1 - py) * resolution * this.mapMeta.height
if (!confirm(`是否导航到该坐标?\nX: ${wx.toFixed(3)}\nY: ${wy.toFixed(3)}`)) return
try { try {
const res = await fetch(API + '/api/navigate/to', { const res = await fetch(API + '/api/navigate/to', {
method: 'POST', method: 'POST',
@@ -325,6 +335,7 @@ createApp({
} catch (e) { alert('保存失败: ' + e.message) } } catch (e) { alert('保存失败: ' + e.message) }
}, },
async navigateToPoint() { async navigateToPoint() {
if (!confirm(`确认导航到该点位?\nX: ${this.pointEditor.x} Y: ${this.pointEditor.y} Yaw: ${this.pointEditor.yaw}`)) return
try { try {
const res = await fetch(API + '/api/navigate/to', { const res = await fetch(API + '/api/navigate/to', {
method: 'POST', method: 'POST',
Regular → Executable
+3 -1
View File
@@ -21,6 +21,7 @@ pkill -f "agv_pro_node" 2>/dev/null || true
pkill -f "lslidar_driver_node" 2>/dev/null || true pkill -f "lslidar_driver_node" 2>/dev/null || true
pkill -f "component_container" 2>/dev/null || true pkill -f "component_container" 2>/dev/null || true
pkill -f "fix_scan_timestamp" 2>/dev/null || true pkill -f "fix_scan_timestamp" 2>/dev/null || true
pkill -f "clock_publisher" 2>/dev/null || true
pkill -f "robot_state_publisher" 2>/dev/null || true pkill -f "robot_state_publisher" 2>/dev/null || true
pkill -f "start_all.sh" 2>/dev/null || true pkill -f "start_all.sh" 2>/dev/null || true
sleep 2 sleep 2
@@ -48,6 +49,7 @@ fi
# 清理 scan_fixer 锁文件 # 清理 scan_fixer 锁文件
rm -f /tmp/scan_fixer.lock rm -f /tmp/scan_fixer.lock
rm -f /tmp/clock_publisher.lock
echo " ✅ FastRTPS 清理完成" echo " ✅ FastRTPS 清理完成"
# ---------- 4. 【关键】重置 ros2 daemon ---------- # ---------- 4. 【关键】重置 ros2 daemon ----------
@@ -61,7 +63,7 @@ echo " ✅ ros2 daemon 已重置"
# ---------- 5. 验证清理结果 ---------- # ---------- 5. 验证清理结果 ----------
echo "[5/5] 验证清理结果..." echo "[5/5] 验证清理结果..."
PROC_COUNT=$(ps aux | grep -E 'agv_pro_node|lslidar_driver_node|component_container|fix_scan_timestamp|app.py|ros2-daemon' | grep -v grep | wc -l || echo 0) PROC_COUNT=$(ps aux | grep -E 'agv_pro_node|lslidar_driver_node|component_container|fix_scan_timestamp|clock_publisher|app.py|ros2-daemon' | grep -v grep | wc -l || echo 0)
FASTRTPS_LEFT=$(ls /dev/shm/fastrtps_* 2>/dev/null | wc -l || echo 0) FASTRTPS_LEFT=$(ls /dev/shm/fastrtps_* 2>/dev/null | wc -l || echo 0)
echo " 残留进程数: $PROC_COUNT" echo " 残留进程数: $PROC_COUNT"
+193 -1
View File
@@ -707,7 +707,7 @@ a:hover { text-decoration: underline; }
.map-container { position: relative; } .map-container { position: relative; }
.map-overlay { .map-overlay {
position: absolute; top: 0; left: 0; right: 0; bottom: 0; position: absolute; top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none; pointer-events: none; z-index: 10;
} }
.map-dot { .map-dot {
position: absolute; position: absolute;
@@ -720,3 +720,195 @@ a:hover { text-decoration: underline; }
border: 2px solid #fff; border: 2px solid #fff;
box-shadow: 0 0 6px rgba(243,156,18,0.9); box-shadow: 0 0 6px rgba(243,156,18,0.9);
} }
/* AGV 实时位置点 */
.agv-dot {
width: 14px !important;
height: 14px !important;
background: #ff5722 !important;
border: 2px solid #fff !important;
border-radius: 50% !important;
box-shadow: 0 0 8px #ff5722, 0 0 16px #ff572288 !important;
animation: agv-pulse 1.5s ease-in-out infinite !important;
z-index: 10 !important;
}
@keyframes agv-pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.3); opacity: 0.7; }
}
/* 二维码位置点 */
.qr-dot {
width: 12px !important;
height: 12px !important;
background: #ff9800 !important;
border: 2px solid #fff !important;
border-radius: 3px !important;
box-shadow: 0 0 8px #ff9800, 0 0 14px #ff980088 !important;
animation: qr-pulse 2s ease-in-out infinite !important;
z-index: 10 !important;
}
@keyframes qr-pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.2); opacity: 0.6; }
}
/* 机器行:有机/无机器 切换 */
.machine-toggle {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
user-select: none;
}
.machine-toggle input[type="checkbox"] {
accent-color: #4caf50;
width: 16px;
height: 16px;
cursor: pointer;
}
.machine-status.on {
color: #4caf50;
font-size: 12px;
}
.machine-status.off {
color: #666;
font-size: 12px;
}
/* ========== 实时日志 ========== */
.log-box {
background: #0a0a0a;
color: #00ff88;
font-family: 'Courier New', 'Menlo', monospace;
font-size: 13px;
line-height: 1.6;
max-height: 320px;
overflow-y: auto;
padding: 12px 16px;
border-radius: 6px;
margin-top: 8px;
border: 1px solid #1a1a1a;
}
.log-line {
padding: 2px 0;
border-bottom: 1px solid #111;
word-break: break-all;
}
.log-empty {
color: #555;
font-style: italic;
padding: 12px 0;
}
/* ========== 弹窗 ========== */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.75);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.modal {
background: #1a1a2e;
padding: 28px 32px;
border-radius: 12px;
min-width: 400px;
max-width: 90%;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
}
.modal h3 {
margin: 0 0 12px 0;
color: #e0e0e0;
}
.modal p {
color: #aaa;
margin: 0 0 16px 0;
}
.modal input[type="text"] {
width: 100%;
padding: 10px 12px;
background: #0a0a0a;
border: 1px solid #333;
border-radius: 6px;
color: #e0e0e0;
font-size: 15px;
outline: none;
box-sizing: border-box;
}
.modal input[type="text"]:focus {
border-color: #409eff;
}
.modal-actions {
display: flex;
gap: 12px;
margin-top: 20px;
}
.modal-actions .btn {
flex: 1;
}
/* ========== 任务清单 ========== */
.task-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
gap: 10px;
margin-top: 10px;
}
.task-cell {
background: #0a0a0a;
border: 1px solid #1a1a1a;
border-radius: 8px;
padding: 12px;
text-align: center;
transition: all 0.3s;
}
.task-cell.task-active {
border-color: #409eff;
background: #0d1b2a;
}
.task-cell.task-completed {
border-color: #4caf50;
opacity: 0.7;
}
.task-cell.task-active .task-step-text {
color: #409eff;
font-weight: bold;
}
.task-pos {
font-size: 16px;
font-weight: bold;
color: #e0e0e0;
margin-bottom: 6px;
}
.task-status-icon {
font-size: 20px;
margin-bottom: 4px;
}
.task-step-text {
font-size: 12px;
color: #888;
margin-bottom: 4px;
}
.task-info {
font-size: 11px;
color: #666;
}
.task-qr {
font-family: monospace;
color: #aaa;
}
.task-photos {
color: #888;
}
.pulse-icon {
animation: taskPulse 1s infinite;
}
@keyframes taskPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
+3 -3
View File
@@ -100,9 +100,9 @@
</div> </div>
<div class="camera-box"> <div class="camera-box">
<div class="camera-label">机械臂摄像头 <button class="btn btn-small" @click="armCameraSrc='/api/camera/arm_refresh?t='+Date.now(); armCameraError=false">刷新</button></div> <div class="camera-label">机械臂摄像头 <button class="btn btn-small" @click="armCameraSrc='/api/camera/arm_refresh?t='+Date.now(); armCameraError=false">刷新</button></div>
<img v-if="armConnected && !armCameraError" :src="armCameraSrc" class="camera-img" @error="armCameraError=true"> <img v-if="armCameraOpened && !armCameraError" :src="armCameraSrc" class="camera-img" @error="armCameraError=true">
<div v-if="armConnected && armCameraError" class="camera-placeholder">机械臂摄像头异常</div> <div v-if="armCameraOpened && armCameraError" class="camera-placeholder">机械臂摄像头异常</div>
<div v-else-if="!armConnected" class="camera-placeholder">未连接</div> <div v-else-if="!armCameraOpened" class="camera-placeholder">未连接</div>
</div> </div>
</div> </div>
</section> </section>
+3 -3
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>设置 - AGV 拍摄系统</title> <title>设置 - AGV 拍摄系统</title>
<link rel="stylesheet" href="/static/css/style.css?v=20260525a"> <link rel="stylesheet" href="/static/css/style.css?v=20260526a">
</head> </head>
<body> <body>
<div id="app"> <div id="app">
@@ -581,7 +581,7 @@
</main> </main>
</div> </div>
<script src="/static/js/vue3.global.prod.js?v=20260525a"></script> <script src="/static/js/vue3.global.prod.js?v=20260526a"></script>
<script src="/static/js/setting.js?v=20260525a"></script> <script src="/static/js/setting.js?v=20260526c"></script>
</body> </body>
</html> </html>