559 lines
34 KiB
HTML
559 lines
34 KiB
HTML
<!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=20260517a">
|
||
</head>
|
||
<body>
|
||
<div id="app">
|
||
<header class="topbar">
|
||
<div class="logo">⚙️ 系统设置</div>
|
||
<nav class="nav">
|
||
<a href="/" class="nav-link">🏠 首页</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 === '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">
|
||
<h2>地图可视化</h2>
|
||
<div class="map-container" style="position:relative;background:#111;border-radius:8px;overflow:hidden">
|
||
<img :src="mapImageUrl" @error="onMapError" style="width:100%;display:block">
|
||
<!-- 地图覆盖层:显示点位坐标 -->
|
||
<div class="map-overlay">
|
||
<!-- 点位坐标点 -->
|
||
<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>
|
||
</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>
|
||
</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: getMachineAt(ri-1, 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>
|
||
</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: getMachineAt(missionConfig.rows-1, 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>
|
||
</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" v-if="selectedMachine && selectedMachine.front && selectedMachine.back" style="margin-top:16px">
|
||
<h2>② 点位配置 — 第{% raw %}{{ selectedMachine.row+1 }}{% endraw %}行 第{% raw %}{{ selectedMachine.col+1 }}{% endraw %}列 <button class="btn btn-small" @click="clearSelection()">← 返回</button></h2>
|
||
|
||
<!-- 正面点位 -->
|
||
<div class="machine-form">
|
||
<h3>📷 正面点位</h3>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>X 坐标</label>
|
||
<input type="number" step="0.01" v-model.number="selectedMachine.front.coords[0]" placeholder="0.00">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Y 坐标</label>
|
||
<input type="number" step="0.01" v-model.number="selectedMachine.front.coords[1]" placeholder="0.00">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Yaw (弧度)</label>
|
||
<input type="number" step="0.01" v-model.number="selectedMachine.front.coords[2]" placeholder="0.00">
|
||
</div>
|
||
<div class="form-group" style="align-self:end">
|
||
<button class="btn btn-small btn-primary" @click="readPosition('front')" :disabled="!agvConnected">📍 读取当前位置</button>
|
||
</div>
|
||
</div>
|
||
<!-- 正面姿态列表 -->
|
||
<div v-if="selectedMachine.front.poses && selectedMachine.front.poses.length > 0" class="pose-list">
|
||
<h4>正面姿态 ({% raw %}{{ selectedMachine.front.poses.length }}{% endraw %} 个)</h4>
|
||
<div v-for="pose in selectedMachine.front.poses" :key="pose.id" class="pose-item">
|
||
<span class="pose-name">{% raw %}{{ pose.name }}{% endraw %}</span>
|
||
<span class="pose-angles" v-if="pose.arm_angles">角度: {% raw %}{{ formatAngles(pose.arm_angles) }}{% endraw %}</span>
|
||
<button class="btn-icon" @click="deletePose(selectedMachine.id, 'front', pose.id)">🗑️</button>
|
||
</div>
|
||
</div>
|
||
<div class="pose-add">
|
||
<input type="text" v-model="poseForm.name" placeholder="姿态名称(如:正面全景)">
|
||
<button class="btn btn-small btn-success" @click="addPoseToMachine(selectedMachine.id, 'front')">➕ 添加姿态</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 背面点位 -->
|
||
<div class="machine-form" style="margin-top:16px">
|
||
<h3>📷 背面点位</h3>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>X 坐标</label>
|
||
<input type="number" step="0.01" v-model.number="selectedMachine.back.coords[0]" placeholder="0.00">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Y 坐标</label>
|
||
<input type="number" step="0.01" v-model.number="selectedMachine.back.coords[1]" placeholder="0.00">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Yaw (弧度)</label>
|
||
<input type="number" step="0.01" v-model.number="selectedMachine.back.coords[2]" placeholder="0.00">
|
||
</div>
|
||
<div class="form-group" style="align-self:end">
|
||
<button class="btn btn-small btn-primary" @click="readPosition('back')" :disabled="!agvConnected">📍 读取当前位置</button>
|
||
</div>
|
||
</div>
|
||
<!-- 背面姿态列表 -->
|
||
<div v-if="selectedMachine.back.poses && selectedMachine.back.poses.length > 0" class="pose-list">
|
||
<h4>背面姿态 ({% raw %}{{ selectedMachine.back.poses.length }}{% endraw %} 个)</h4>
|
||
<div v-for="pose in selectedMachine.back.poses" :key="pose.id" class="pose-item">
|
||
<span class="pose-name">{% raw %}{{ pose.name }}{% endraw %}</span>
|
||
<span class="pose-angles" v-if="pose.arm_angles">角度: {% raw %}{{ formatAngles(pose.arm_angles) }}{% endraw %}</span>
|
||
<button class="btn-icon" @click="deletePose(selectedMachine.id, 'back', pose.id)">🗑️</button>
|
||
</div>
|
||
</div>
|
||
<div class="pose-add">
|
||
<input type="text" v-model="poseForm.name" placeholder="姿态名称(如:背面细节)">
|
||
<button class="btn btn-small btn-success" @click="addPoseToMachine(selectedMachine.id, 'back')">➕ 添加姿态</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>
|
||
</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>
|
||
|
||
<!-- 机械臂控制 (保持不变) -->
|
||
<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="previewUrl" @error="onPreviewError">
|
||
</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>
|
||
</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>
|
||
|
||
<script src="/static/js/vue3.global.prod.js?v=20260517a"></script>
|
||
<script src="/static/js/setting.js?v=20260517a"></script>
|
||
</body>
|
||
</html>
|