坐标问题
This commit is contained in:
+305
-21
@@ -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>
|
||||||
|
|||||||
@@ -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=$!
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user