坐标问题

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
+318 -34
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>设置 - AGV 拍摄系统</title>
<link rel="stylesheet" href="/static/css/style.css?v=20260514a">
<link rel="stylesheet" href="/static/css/style.css?v=20260520h">
</head>
<body>
<div id="app">
@@ -12,7 +12,7 @@
<div class="logo">⚙️ 系统设置</div>
<nav class="nav">
<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>
</nav>
</header>
@@ -21,6 +21,8 @@
<div class="tabs">
<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 === '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 === 'agv'}" @click="tab = 'agv'">🚗 AGV控制</button>
</div>
@@ -56,26 +58,197 @@
<button class="btn btn-secondary" style="padding:4px 10px;font-size:12px" @click="resetMapView">重置</button>
</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">
<img :src="mapImageUrl" @error="onMapError" @click="onMapClick" style="width:100%;display:block;cursor:crosshair" title="点击地图导航到该位置">
<div class="map-overlay">
<!-- AGV 实时位置 -->
<div v-if="navCurrentPos && nav2Available"
class="map-dot agv-dot"
:style="{ left: getMapX(navCurrentPos) + '%', top: getMapY(navCurrentPos) + '%' }"
title="AGV 当前位置">
</div>
<!-- 点位坐标点 -->
<div v-for="(p, pi) in missionConfig.positions" :key="'pdot-'+mapVersion+'-'+pi"
class="map-dot point-dot"
:style="{ left: getMapX(p.coords) + '%', top: getMapY(p.coords) + '%' }"
:title="p.coords ? p.coords.map(c => c.toFixed(2)).join(', ') : ''">
<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="点击地图导航到该位置">
<!-- 地图覆盖层:显示点位坐标 -->
<div class="map-overlay">
<!-- AGV 实时位置 -->
<div v-if="navCurrentPos && nav2Available"
class="map-dot agv-dot"
:style="{ left: getMapX(navCurrentPos) + '%', top: getMapY(navCurrentPos) + '%' }"
title="AGV 当前位置">
</div>
<!-- 点位坐标点 -->
<div v-for="(p, pi) in missionConfig.positions" :key="'pdot-'+mapVersion+'-'+pi"
class="map-dot point-dot"
:style="{ left: getMapX(p.coords) + '%', top: getMapY(p.coords) + '%' }"
:title="p.coords ? p.coords.map(c => c.toFixed(2)).join(', ') : ''">
</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>
</section>
</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 ========== -->
<div v-if="tab === 'mission'">
@@ -91,9 +264,10 @@
<label>列数 N</label>
<input type="number" v-model.number="missionConfig.cols" min="1" max="20" placeholder="4">
</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-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>
@@ -123,12 +297,16 @@
<div class="grid-cell grid-header">机器行 {% raw %}{{ ri }}{% endraw %}</div>
<div v-for="(ci) in missionConfig.cols" :key="'m'+ri+'_'+ci"
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)">
<template v-if="getMachineAt(ri-1, ci-1)">
<div class="cell-machine"></div>
</template>
<span v-else class="empty-cell"></span>
<label class="machine-toggle" @click.stop>
<input type="checkbox"
:checked="getMachineAt(ri-1, ci-1) !== null"
@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>
<!-- 点位行 ri+1 (pointRow=ri): 上面机器的背面 / 下面机器的正面 -->
@@ -145,12 +323,16 @@
<div class="grid-cell grid-header">机器行 {% raw %}{{ missionConfig.rows }}{% endraw %}</div>
<div v-for="(ci) in missionConfig.cols" :key="'m'+missionConfig.rows+'_'+ci"
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)">
<template v-if="getMachineAt(missionConfig.rows-1, ci-1)">
<div class="cell-machine"></div>
</template>
<span v-else class="empty-cell"></span>
<label class="machine-toggle" @click.stop>
<input type="checkbox"
:checked="getMachineAt(missionConfig.rows-1, ci-1) !== null"
@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>
<!-- 最后一个点位行 (pointRow=rows): 所有机器的背面拍摄点 -->
@@ -240,6 +422,39 @@
</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">
<button class="btn btn-danger" @click="deleteMachine(selectedMachine.id)">🗑️ 删除此机器</button>
<button class="btn btn-secondary" @click="saveMachineCoords">💾 保存此机器配置</button>
@@ -290,19 +505,78 @@
<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 %})
</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 %}
</div>
</div>
<div class="btn-row">
<button class="btn btn-primary" @click="loadPointFromAgv" :disabled="!agvConnected">📍 从AGV读取</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-secondary" @click="closePointEdit">取消</button>
</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'">
<section class="card">
@@ -312,7 +586,7 @@
</div>
<div v-else>
<div class="camera-preview">
<img :src="previewUrl" @error="onPreviewError">
<img :src="armCameraUrl" @error="onArmPreviewError">
</div>
<div class="joints-panel">
<h3>关节角度控制</h3>
@@ -325,7 +599,6 @@
<input type="number" v-model.number="angleInputs[j-1]" step="0.5" @change="setAngle(j-1, angleInputs[j-1])">
<button @mousedown="jogStart(j-1, 1)" @mouseup="jogStop(j-1)" @mouseleave="jogStop(j-1)"></button>
</div>
</div>
</div>
<div class="btn-row">
<button class="btn btn-primary" @click="refreshAngles">🔄 刷新角度</button>
@@ -349,8 +622,19 @@
</div>
<div class="agv-status-bar">
<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" :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 class="agv-control-panel">
<div class="agv-dir-row">
@@ -392,7 +676,7 @@
</main>
</div>
<script src="/static/js/vue3.global.prod.js?v=20260513b"></script>
<script src="/static/js/setting.js?v=20260514g"></script>
<script src="/static/js/vue3.global.prod.js?v=20260520h"></script>
<script src="/static/js/setting.js?v=20260520h"></script>
</body>
</html>