Files
smart-inspection/agv_app/templates/setting.html
T
2026-06-05 10:27:42 +08:00

627 lines
40 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<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=20260529b">
</head>
<body>
<div id="app">
<header class="topbar">
<div class="logo">⚙️ 系统设置</div>
<nav class="nav">
<a href="/" class="nav-link">🏠 首页</a>
<a href="/setting" class="nav-link active">⚙️ 设置</a>
<a href="/running" class="nav-link">▶️ 运行</a>
</nav>
</header>
<!-- Tabs -->
<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>
<main class="container">
<!-- 地图配置 (保持不变) -->
<div v-if="tab === 'map'">
<section class="card">
<h2>地图配置</h2>
<div class="form-row">
<div class="form-group">
<label>地图目录</label>
<input type="text" v-model="mapForm.map_dir" placeholder="/home/elephant/...">
</div>
<div class="form-group">
<label>地图文件</label>
<input type="text" v-model="mapForm.map_file" placeholder="map.yaml">
</div>
<div class="form-group" style="align-self:end">
<button class="btn btn-primary" @click="loadMap">📂 加载地图</button>
<button class="btn btn-secondary" @click="saveMap" style="margin-left:6px">💾 保存</button>
</div>
</div>
<p v-if="mapMsg" class="hint">{% raw %}{{ mapMsg }}{% endraw %}</p>
</section>
<section class="card" v-if="mapLoaded" style="margin-top:16px">
<div style="display:flex;align-items:center;justify-content:space-between">
<h2>地图可视化</h2>
<div style="display:flex;gap:8px;align-items:center">
<span style="font-size:12px;color:#888">旋转:</span>
<button class="btn btn-secondary" style="padding:4px 10px;font-size:12px" @click="rotateMap(-90)">↶ 90°</button>
<button class="btn btn-secondary" style="padding:4px 10px;font-size:12px" @click="rotateMap(90)">↷ 90°</button>
<button class="btn btn-secondary" style="padding:4px 10px;font-size:12px" @click="resetMapView">重置</button>
</div>
</div>
<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'">
<!-- 上:网格配置 -->
<section class="card">
<h2>① 网格配置 (M×N)</h2>
<div class="form-row">
<div class="form-group">
<label>行数 M</label>
<input type="number" v-model.number="missionConfig.rows" min="1" max="20" placeholder="3">
</div>
<div class="form-group">
<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;display:flex;gap:6px;flex-wrap:nowrap">
<button class="btn btn-primary" @click="generateGrid">🔲 生成网格</button>
<button class="btn btn-secondary" @click="saveMissionConfig">💾 保存网格</button>
<button class="btn btn-warning" @click="initPose" :disabled="initPoseLoading">📍 初始化位置</button>
<button class="btn btn-primary" @click="goToOrigin">🏠 回到原点</button>
</div>
</div>
<!-- 网格可视化 - 点位行独立于机器,始终可配置 -->
<div v-if="missionConfig.rows > 0" class="mission-grid-wrap" style="margin-top:12px">
<div class="mission-grid" :style="{ gridTemplateColumns: '90px repeat(' + missionConfig.cols + ', 100px)' }">
<!-- 表头: 列号 -->
<div class="grid-cell grid-header"></div>
<div v-for="c in missionConfig.cols" :key="'h'+c" class="grid-cell grid-header">第{% raw %}{{ c }}{% endraw %}列</div>
<!-- 循环渲染: 点位行(0) → 机器行(1) → 点位行(1) → 机器行(2) → ... → 点位行(rows) -->
<!-- pointRow 从 0 到 rows(共 rows+1 个点位行)-->
<!-- machineRow 从 1 到 rows(共 rows 个机器行)-->
<!-- 第一个点位行 (pointRow=0): 所有机器的正面拍摄点 -->
<div class="grid-cell grid-header">点位行 1</div>
<div v-for="(ci) in missionConfig.cols" :key="'p0_'+ci"
class="grid-cell point-cell"
@click="openPointEdit(0, ci-1)">
<span class="point-coords">{% raw %}{{ getPointAt(0, ci-1)?.coords?.[0]?.toFixed(1) || '-' }},{{ getPointAt(0, ci-1)?.coords?.[1]?.toFixed(1) || '-' }}{% endraw %}</span>
<button class="btn-icon-small" title="配置坐标" @click.stop="openPointEdit(0, ci-1)">+</button>
</div>
<!-- 中间循环: 机器行 ri + 点位行 ri (ri from 1 to rows-1) -->
<template v-for="ri in (missionConfig.rows - 1)" :key="'mr'+ri">
<!-- 机器行 ri -->
<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: selectedMachine && selectedMachine.row === ri-1 && selectedMachine.col === ci-1 }"
@click="onCellClick(ri-1, ci-1)">
<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): 上面机器的背面 / 下面机器的正面 -->
<div class="grid-cell grid-header">点位行 {% raw %}{{ ri+1 }}{% endraw %}</div>
<div v-for="(ci) in missionConfig.cols" :key="'p'+(ri)+'_'+ci"
class="grid-cell point-cell"
@click="openPointEdit(ri, ci-1)">
<span class="point-coords">{% raw %}{{ getPointAt(ri, ci-1)?.coords?.[0]?.toFixed(1) || '-' }},{{ getPointAt(ri, ci-1)?.coords?.[1]?.toFixed(1) || '-' }}{% endraw %}</span>
<button class="btn-icon-small" title="配置坐标" @click.stop="openPointEdit(ri, ci-1)">+</button>
</div>
</template>
<!-- 最后一个机器行 (机器行 rows) -->
<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: selectedMachine && selectedMachine.row === missionConfig.rows-1 && selectedMachine.col === ci-1 }"
@click="onCellClick(missionConfig.rows-1, ci-1)">
<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): 所有机器的背面拍摄点 -->
<div class="grid-cell grid-header">点位行 {% raw %}{{ missionConfig.rows+1 }}{% endraw %}</div>
<div v-for="(ci) in missionConfig.cols" :key="'p'+missionConfig.rows+'_'+ci"
class="grid-cell point-cell"
@click="openPointEdit(missionConfig.rows, ci-1)">
<span class="point-coords">{% raw %}{{ getPointAt(missionConfig.rows, ci-1)?.coords?.[0]?.toFixed(1) || '-' }},{{ getPointAt(missionConfig.rows, ci-1)?.coords?.[1]?.toFixed(1) || '-' }}{% endraw %}</span>
<button class="btn-icon-small" title="配置坐标" @click.stop="openPointEdit(missionConfig.rows, ci-1)">+</button>
</div>
</div>
<p class="hint" style="margin-top:8px">点击「点位行」配置拍摄坐标;点击「机器行」切换有无机器<br>中间点位同时服务于上下两台机器(上机器背面 / 下机器正面),删除机器不影响点位配置</p>
</div>
</section>
<!-- 机械臂初始姿态 -->
<section class="card" style="margin-top:16px">
<h2>② 🦾 机械臂初始姿态</h2>
<p class="hint" style="margin-bottom:12px">每个机器执行前恢复的初始姿态(6个关节角度,单位:度)</p>
<div class="form-row" style="flex-wrap:wrap;gap:12px">
<div v-for="j in 6" :key="'armInit'+j" class="form-group" style="min-width:100px">
<label>J{% raw %}{{ j }}{% endraw %}</label>
<input type="number" step="0.5"
v-model.number="armInitialPose[j-1]"
style="width:100%;padding:8px;border:1px solid #2a3441;border-radius:4px">
</div>
<div class="form-group" style="align-self:end">
<button class="btn btn-primary" @click="saveArmInitialPose">💾 保存初始姿态</button>
<button class="btn btn-secondary" @click="loadArmCurrentAngles" :disabled="!armConnected" style="margin-left:6px">📋 读取当前角度</button>
<button class="btn btn-primary" @click="applyArmInitialPose" :disabled="!armConnected" style="margin-left:6px">🎯 应用当前姿态</button>
</div>
</div>
</section>
<!-- 下:序列预览 -->
<section class="card" v-if="sequence && sequence.length > 0" style="margin-top:16px">
<h2>③ 🐍 蛇形拍摄序列预览</h2>
<div class="sequence-preview">
<div v-for="(step, idx) in sequence" :key="idx" class="sequence-step">
<span class="step-index">{% raw %}{{ idx+1 }}{% endraw %}</span>
<span class="step-info">
第{% raw %}{{ step.row+1 }}{% endraw %}行 第{% raw %}{{ step.col+1 }}{% endraw %}列
<span class="step-side" :class="step.side">{% raw %}{{ step.side === 'front' ? '正面' : '背面' }}{% endraw %}</span>
</span>
</div>
</div>
<div class="btn-row" style="margin-top:12px">
<button class="btn btn-secondary" @click="refreshSequence">🔄 刷新序列</button>
</div>
</section>
</div>
<!-- 点位编辑弹窗(基于独立点位行模型) -->
<div v-if="editingPoint" class="modal-overlay" @click.self="closePointEdit">
<div class="modal-box" style="min-width:460px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px">
<h3 style="margin:0">📍 点位配置 — {% raw %}{{ getPointOwnerLabel(editingPoint.pointRow, editingPoint.col) }}{% endraw %}</h3>
<button class="btn-icon" @click="closePointEdit"></button>
</div>
<div style="margin-bottom:14px">
<div class="form-row">
<div class="form-group">
<label>X</label>
<input type="number" step="0.01" v-model.number="pointEditor.x" placeholder="0.00">
</div>
<div class="form-group">
<label>Y</label>
<input type="number" step="0.01" v-model.number="pointEditor.y" placeholder="0.00">
</div>
<div class="form-group">
<label>Yaw (rad)</label>
<input type="number" step="0.01" v-model.number="pointEditor.yaw" placeholder="0.00">
</div>
</div>
<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:#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:8px">
<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">
<button class="btn btn-secondary btn-small" @click="captureArmSnapshot" :disabled="armSnapshotLoading">
📸 获取图片
</button>
</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-secondary btn-small" @click="qrInputId = q.id; qrInputValue = q.qr_value || ''; showQrInputDialog = true" 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">
<h2>🤖 机械臂控制</h2>
<div v-if="!armConnected" class="alert alert-error">
⚠️ 机械臂未连接,请先在首页连接设备
</div>
<div v-else>
<div class="camera-preview">
<img :src="armCameraUrl" @error="onArmPreviewError">
</div>
<div class="joints-panel">
<h3>关节角度控制</h3>
<div class="joint-grid">
<div v-for="j in 6" :key="j" class="joint-control">
<label>J{% raw %}{{ j }}{% endraw %}</label>
<div class="joint-value">{% raw %}{{ currentAngles[j-1] ? currentAngles[j-1].toFixed(1) : '—' }}{% endraw %}°</div>
<div class="joint-buttons">
<button @mousedown="jogStart(j-1, -1)" @mouseup="jogStop(j-1)" @mouseleave="jogStop(j-1)"></button>
<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 class="btn-row">
<button class="btn btn-primary" @click="refreshAngles">🔄 刷新角度</button>
<button class="btn btn-secondary" @click="applyAngles()">✅ 应用角度</button>
<button class="btn btn-warning" @click="mirrorAngles">🔄 镜像角度</button>
</div>
</div>
</div>
</section>
</div>
<!-- AGV 移动控制 (保持不变) -->
<div v-if="tab === 'agv'">
<section class="card">
<h2>🚗 AGV 移动控制</h2>
<div v-if="!agvConnected" class="alert alert-error">
⚠️ AGV 未连接,请先在首页连接设备
</div>
<div v-else>
<div v-show="cameraOpened" class="camera-preview" style="margin-bottom:16px">
<img :src="agvCameraUrl" style="width:100%;max-width:480px;aspect-ratio:16/9;object-fit:cover;border-radius:8px" @error="agvCameraUrl=''">
</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] !== 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">
<div class="agv-dir-placeholder"></div>
<button class="agv-btn agv-btn-up" @mousedown="agvMoveStart('forward')" @mouseup="agvMoveStop" @mouseleave="agvMoveStop">⬆️ 前进</button>
<div class="agv-dir-placeholder"></div>
</div>
<div class="agv-dir-row">
<button class="agv-btn agv-btn-left" @mousedown="agvMoveStart('left')" @mouseup="agvMoveStop" @mouseleave="agvMoveStop">↺ 左转</button>
<button class="agv-btn agv-btn-stop" @click="agvStop">🛑</button>
<button class="agv-btn agv-btn-right" @mousedown="agvMoveStart('right')" @mouseup="agvMoveStop" @mouseleave="agvMoveStop">↻ 右转</button>
</div>
<div class="agv-dir-row">
<div class="agv-dir-placeholder"></div>
<button class="agv-btn agv-btn-down" @mousedown="agvMoveStart('backward')" @mouseup="agvMoveStop" @mouseleave="agvMoveStop">⬇️ 后退</button>
<div class="agv-dir-placeholder"></div>
</div>
</div>
<div class="agv-lateral-row">
<button class="agv-btn agv-btn-lateral" @mousedown="agvMoveStart('left_lateral')" @mouseup="agvMoveStop" @mouseleave="agvMoveStop">⬅️ 向左平移</button>
<button class="agv-btn agv-btn-lateral" @mousedown="agvMoveStart('right_lateral')" @mouseup="agvMoveStop" @mouseleave="agvMoveStop">向右平移 ➡️</button>
</div>
<div class="form-row" style="margin-top:16px; max-width:400px">
<div class="form-group">
<label>移动速度</label>
<div class="speed-control">
<input type="range" v-model.number="agvSpeed" min="0.1" max="1.0" step="0.1" style="flex:1">
<span class="speed-value">{% raw %}{{ (agvSpeed * 100).toFixed(0) }}{% endraw %}%</span>
</div>
</div>
</div>
<div class="btn-row" style="margin-top:12px">
<button class="btn btn-danger" @click="agvResetCollision">🔄 撞物体后复位</button>
<button class="btn btn-secondary" @click="agvStop">🛑 立即停止</button>
</div>
</div>
</section>
</div>
</main>
<!-- 手动输入二维码弹窗 -->
<div class="modal-overlay" v-if="showQrInputDialog">
<div class="modal" style="max-width:420px">
<h3>⌨️ 手动输入二维码</h3>
<p style="color:#9aa0a6;font-size:13px;margin:8px 0 16px">自动扫码未识别到二维码,请手动输入二维码内容:</p>
<input type="text" v-model="qrInputValue" placeholder="输入二维码内容..."
style="width:100%;background:#0f1923;border:1px solid #2a3441;color:#fff;padding:10px 12px;border-radius:6px;font-size:14px;box-sizing:border-box"
autofocus @keyup.enter="submitManualQr">
<div class="modal-actions" style="margin-top:16px">
<button class="btn btn-primary" @click="submitManualQr">确认</button>
<button class="btn" @click="showQrInputDialog = false; qrInputId = null; qrInputValue = ''">取消</button>
</div>
</div>
</div>
<!-- 机械臂截图弹窗 -->
<div class="modal-overlay" v-if="showArmSnapshot" @click.self="showArmSnapshot = false">
<div class="modal" style="max-width:800px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<h3>📸 机械臂摄像头截图</h3>
<button class="btn btn-small" @click="showArmSnapshot = false" style="font-size:18px;background:none;border:none;color:#9aa0a6;cursor:pointer"></button>
</div>
<div style="background:#000;border-radius:8px;overflow:hidden">
<img :src="armSnapshotUrl" style="width:100%;display:block">
</div>
<div style="margin-top:8px;text-align:center;color:#9aa0a6;font-size:12px">
点击弹窗外关闭
</div>
</div>
</div>
<script src="/static/js/vue3.global.prod.js?v=20260526a"></script>
<script src="/static/js/setting.js?v=20260605a"></script>
</body>
</html>