Files
smart-inspection/agv_app/setting.html
T

399 lines
23 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=20260514a">
</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 === '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">
<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>
</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">
<button class="btn btn-primary" @click="generateGrid">🔲 生成网格</button>
<button class="btn btn-secondary" @click="saveMissionConfig" style="margin-left:6px">💾 保存网格</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:#888">
💡 此点位服务于: {% 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-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>
<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] ? agvPosition[0].toFixed(2) : '?' }}{% endraw %} Y={% raw %}{{ agvPosition[1] ? agvPosition[1].toFixed(2) : '?' }}{% endraw %}</strong></span>
<button class="btn btn-small" @click="refreshAgvPosition">🔄 刷新</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=20260513b"></script>
<script src="/static/js/setting.js?v=20260514g"></script>
</body>
</html>