init
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
<!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">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- 顶部导航 -->
|
||||
<header class="topbar">
|
||||
<div class="logo">🤖 AGV 拍摄系统</div>
|
||||
<nav class="nav">
|
||||
<a href="/" class="nav-link active">🏠 首页</a>
|
||||
<a href="/setting" class="nav-link">⚙️ 设置</a>
|
||||
<a href="/running" class="nav-link">▶️ 运行</a>
|
||||
</nav>
|
||||
<div class="status-bar">
|
||||
<span class="status-item" :class="statusClass">
|
||||
[[ statusText ]]
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主内容 -->
|
||||
<main class="container">
|
||||
<!-- 系统状态卡片 -->
|
||||
<section class="card">
|
||||
<h2>📡 系统连接状态</h2>
|
||||
<div class="grid-3">
|
||||
<div class="status-card" :class="agvConnected ? 'ok' : 'error'"
|
||||
@click="connectDevice('agv')" style="cursor:pointer">
|
||||
<div class="status-icon">
|
||||
<span v-if="reconnectingDevice==='agv'">⏳</span>
|
||||
<span v-else>[[ agvConnected ? '✅' : '❌' ]]</span>
|
||||
</div>
|
||||
<div class="status-label">AGV</div>
|
||||
<div class="status-value">
|
||||
<span v-if="reconnectingDevice==='agv'">重连中...</span>
|
||||
<span v-else>[[ agvConnected ? '已连接' : '未连接' ]](点击重连)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-card" :class="armConnected ? 'ok' : 'error'"
|
||||
@click="connectDevice('arm')" style="cursor:pointer">
|
||||
<div class="status-icon">
|
||||
<span v-if="reconnectingDevice==='arm'">⏳</span>
|
||||
<span v-else>[[ armConnected ? '✅' : '❌' ]]</span>
|
||||
</div>
|
||||
<div class="status-label">机械臂</div>
|
||||
<div class="status-value">
|
||||
<span v-if="reconnectingDevice==='arm'">重连中...</span>
|
||||
<span v-else>[[ armConnected ? '已连接' : '未连接' ]](点击重连)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-card" :class="cameraOpened ? 'ok' : 'error'"
|
||||
@click="connectDevice('camera')" style="cursor:pointer">
|
||||
<div class="status-icon">
|
||||
<span v-if="reconnectingDevice==='camera'">⏳</span>
|
||||
<span v-else>[[ cameraOpened ? '✅' : '❌' ]]</span>
|
||||
</div>
|
||||
<div class="status-label">AGV摄像头</div>
|
||||
<div class="status-value">
|
||||
<span v-if="reconnectingDevice==='camera'">重连中...</span>
|
||||
<span v-else>[[ cameraOpened ? '已打开' : '未打开' ]](点击重连)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-card" :class="armCameraOpened ? 'ok' : 'error'"
|
||||
@click="connectDevice('arm_camera')" style="cursor:pointer">
|
||||
<div class="status-icon">
|
||||
<span v-if="reconnectingDevice==='arm_camera'">⏳</span>
|
||||
<span v-else>[[ armCameraOpened ? '✅' : '❌' ]]</span>
|
||||
</div>
|
||||
<div class="status-label">机械臂摄像头</div>
|
||||
<div class="status-value">
|
||||
<span v-if="reconnectingDevice==='arm_camera'">重连中...</span>
|
||||
<span v-else>[[ armCameraOpened ? '已打开' : '未打开' ]](点击重连)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-primary" @click="connectAll" :disabled="connecting">
|
||||
[[ connecting ? '连接中...' : '🔗 连接全部设备' ]]
|
||||
</button>
|
||||
<button class="btn btn-secondary" @click="disconnectAll" :disabled="!agvConnected && !armConnected && !cameraOpened">
|
||||
断开连接
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 双摄像头预览 -->
|
||||
<section class="card">
|
||||
<h2>📷 摄像头预览</h2>
|
||||
<div class="camera-row">
|
||||
<div class="camera-box">
|
||||
<div class="camera-label">AGV 摄像头 <button class="btn btn-small" @click="agvCameraSrc='/api/camera/refresh?t='+Date.now(); agvCameraError=false">刷新</button></div>
|
||||
<img v-if="cameraOpened && !agvCameraError" :src="agvCameraSrc" class="camera-img" @error="agvCameraError=true">
|
||||
<div v-if="cameraOpened && agvCameraError" class="camera-placeholder">AGV 摄像头异常</div>
|
||||
<div v-else-if="!cameraOpened" class="camera-placeholder">未打开(先点击连接设备)</div>
|
||||
</div>
|
||||
<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>
|
||||
<img v-if="armConnected && !armCameraError" :src="armCameraSrc" class="camera-img" @error="armCameraError=true">
|
||||
<div v-if="armConnected && armCameraError" class="camera-placeholder">机械臂摄像头异常</div>
|
||||
<div v-else-if="!armConnected" class="camera-placeholder">未连接</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 地图信息 -->
|
||||
<section class="card">
|
||||
<h2>🗺️ 地图信息</h2>
|
||||
<div v-if="mapLoaded">
|
||||
<p>地图目录: <code>[[ mapConfig.map_dir ]]</code></p>
|
||||
<p>地图文件: <code>[[ mapConfig.map_file ]]</code></p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p class="hint">尚未加载地图,请前往 <a href="/setting">设置页面</a> 配置地图</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 点位概览 -->
|
||||
<section class="card">
|
||||
<h2>📍 点位概览</h2>
|
||||
<p>已配置 <strong>[[ pointsCount ]]</strong> 个拍摄点位</p>
|
||||
<div class="btn-row">
|
||||
<a href="/setting" class="btn btn-primary">前往设置点位 →</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 快捷入口 -->
|
||||
<section class="card">
|
||||
<h2>🚀 快捷入口</h2>
|
||||
<div class="btn-row">
|
||||
<a href="/setting" class="btn btn-large btn-primary">进入设置模式 ⚙️</a>
|
||||
<a href="/running" class="btn btn-large btn-success" :class="{disabled: !allReady}" @click.prevent="checkReady">
|
||||
开始运行 ▶️
|
||||
</a>
|
||||
</div>
|
||||
<p v-if="!allReady" class="hint">请先连接所有设备并加载地图</p>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/vue3.global.prod.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,82 @@
|
||||
<!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">
|
||||
</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">⚙️ 设置</a>
|
||||
<a href="/running" class="nav-link active">▶️ 运行</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<!-- 状态概览 -->
|
||||
<section class="card">
|
||||
<div class="running-header">
|
||||
<div class="running-status" :class="missionState">
|
||||
<span class="pulse"></span>
|
||||
[[ missionStateText ]]
|
||||
</div>
|
||||
<div class="running-progress" v-if="missionState === 'running'">
|
||||
<span>点位 [[ currentPoint + 1 ]] / [[ totalPoints ]]</span>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{width: progressPercent + '%'}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-success btn-large" @click="startMission" :disabled="missionState !== 'idle'">
|
||||
▶️ 开始任务
|
||||
</button>
|
||||
<button class="btn btn-warning btn-large" @click="pauseMission" :disabled="missionState !== 'running'">
|
||||
⏸️ 暂停
|
||||
</button>
|
||||
<button class="btn btn-error btn-large" @click="stopMission" :disabled="missionState === 'idle'">
|
||||
⏹️ 停止
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 实时预览 -->
|
||||
<section class="card">
|
||||
<h2>📷 摄像头预览</h2>
|
||||
<div class="camera-full">
|
||||
<img :src="previewUrl" @error="onPreviewError">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 任务报告 -->
|
||||
<section class="card" v-if="report">
|
||||
<h2>📋 任务报告</h2>
|
||||
<div class="report-summary">
|
||||
<div class="stat ok">✅ 完成: [[ report.completed ]]</div>
|
||||
<div class="stat error">❌ 失败: [[ report.failed ]]</div>
|
||||
<div class="stat">总计: [[ report.total_points ]]</div>
|
||||
</div>
|
||||
<div class="report-details">
|
||||
<div v-for="(detail, i) in report.details" :key="i" class="report-item">
|
||||
<div class="report-point">
|
||||
<span class="report-status" :class="detail.status">[[ detail.status === 'completed' ? '✅' : '❌' ]]</span>
|
||||
[[ detail.point_name ]]
|
||||
</div>
|
||||
<div v-for="(pose, pi) in detail.poses" :key="pi" class="report-pose">
|
||||
[[ pose.photo_type ]] - [[ pose.status ]]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/vue3.global.prod.js"></script>
|
||||
<script src="/static/js/running.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,507 @@
|
||||
<!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=20260514e">
|
||||
</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 === \'model\'}" @click="tab = \'model\'">📦 机型配置</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">
|
||||
<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 class="form-section" style="background:#f5f5f5;padding:16px;border-radius:8px;margin-bottom:20px">
|
||||
<h3 style="margin-top:0">添加新机型</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group" style="flex:1">
|
||||
<label>机型名称</label>
|
||||
<input type="text" v-model="newModelName" placeholder="例如:SMT-A" style="width:100%;padding:8px;border:1px solid #ddd;border-radius:4px">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group" style="flex:1">
|
||||
<label>描述</label>
|
||||
<input type="text" v-model="newModelDesc" placeholder="描述信息" style="width:100%;padding:8px;border:1px solid #ddd;border-radius:4px">
|
||||
</div>
|
||||
<div class="form-group" style="flex:1">
|
||||
<label>备注</label>
|
||||
<input type="text" v-model="newModelNotes" placeholder="备注信息" style="width:100%;padding:8px;border:1px solid #ddd;border-radius:4px">
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="addModel" style="margin-top:8px">➕ 添加机型</button>
|
||||
</div>
|
||||
|
||||
<!-- 机型列表 -->
|
||||
<div v-if="models.length === 0" style="text-align:center;color:#888;padding:40px">
|
||||
<p>暂无机型配置,请添加新机型</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div v-for="m in models" :key="m.id" style="border:1px solid #ddd;border-radius:8px;margin-bottom:20px;overflow:hidden">
|
||||
<!-- 机型头部 -->
|
||||
<div style="background:#e8f4e8;padding:12px 16px;display:flex;justify-content:space-between;align-items:center">
|
||||
<div>
|
||||
<strong style="font-size:16px">{{ m.name }}</strong>
|
||||
<span style="margin-left:12px;color:#666;font-size:13px">ID: {{ m.id }}</span>
|
||||
<span v-if="m.description" style="margin-left:12px;color:#888;font-size:13px">{{ m.description }}</span>
|
||||
<span v-if="m.notes" style="margin-left:12px;color:#aaa;font-size:13px">【{{ m.notes }}】</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="btn btn-danger btn-small" @click="deleteModel(m.id)">🗑️ 删除机型</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 姿态配置 -->
|
||||
<div style="padding:16px">
|
||||
<!-- 正面姿态 -->
|
||||
<div style="margin-bottom:20px">
|
||||
<h4 style="margin:0 0 12px 0;color:#1976d2">🔵 正面姿态</h4>
|
||||
<div v-for="pose in m.poses.filter(p => p.photo_type === 'front')" :key="pose.id" style="background:#f8f8f8;padding:12px;border-radius:6px;margin-bottom:8px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
||||
<strong>{{ pose.name or '正面姿态' }}</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:#666">J{{j}}</span>
|
||||
<input type="number" step="0.1"
|
||||
:value="pose.arm_angles && pose.arm_angles[j-1] !== undefined ? pose.arm_angles[j-1] : 0"
|
||||
@change="updatePoseAngle(m.id, pose.id, j-1, )"
|
||||
style="width:70px;padding:4px;border:1px solid #ddd;border-radius:4px">
|
||||
<span style="font-size:11px;color:#999">°</span>
|
||||
</div>
|
||||
</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 #ddd;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:#888">
|
||||
当前机械臂角度:
|
||||
<span v-if="currentAngles.length">
|
||||
J1={{ currentAngles[0]?.toFixed(1) }}° J2={{ currentAngles[1]?.toFixed(1) }}° J3={{ currentAngles[2]?.toFixed(1) }}° J4={{ currentAngles[3]?.toFixed(1) }}° J5={{ currentAngles[4]?.toFixed(1) }}° J6={{ currentAngles[5]?.toFixed(1) }}°
|
||||
</span>
|
||||
<span v-else>(未连接机械臂)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 背面姿态 -->
|
||||
<div>
|
||||
<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:#fff0f0;padding:12px;border-radius:6px;margin-bottom:8px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
||||
<strong>{{ pose.name or '背面姿态' }}</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:#666">J{{j}}</span>
|
||||
<input type="number" step="0.1"
|
||||
:value="pose.arm_angles && pose.arm_angles[j-1] !== undefined ? pose.arm_angles[j-1] : 0"
|
||||
@change="updatePoseAngle(m.id, pose.id, j-1, )"
|
||||
style="width:70px;padding:4px;border:1px solid #ddd;border-radius:4px">
|
||||
<span style="font-size:11px;color:#999">°</span>
|
||||
</div>
|
||||
</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 #ddd;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>
|
||||
</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=20260514k"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,438 @@
|
||||
const { createApp } = Vue
|
||||
const API = ''
|
||||
|
||||
createApp({
|
||||
data() {
|
||||
return {
|
||||
tab: 'map',
|
||||
// 任务配置
|
||||
missionConfig: { rows: 3, cols: 3, grid: [], machines: [] },
|
||||
selectedMachine: null,
|
||||
// 地图
|
||||
mapForm: { map_dir: '/home/elephant/agv_pro_ros2/src/agv_pro_navigation2/map/', map_file: 'map.yaml' },
|
||||
mapMsg: '',
|
||||
mapLoaded: false,
|
||||
mapImageUrl: '',
|
||||
mapMeta: null,
|
||||
// 点位
|
||||
points: [],
|
||||
newPointName: '',
|
||||
newPointMode: 'front',
|
||||
newPointSequence: ['front', 'back'],
|
||||
// 机型(姿态组)
|
||||
models: [],
|
||||
selectedModelId: null,
|
||||
newModelName: '',
|
||||
newModelSerial: '',
|
||||
poseForm: {},
|
||||
// 机械臂
|
||||
armConnected: false,
|
||||
currentAngles: [],
|
||||
angleInputs: [],
|
||||
previewUrl: API + '/api/camera/preview',
|
||||
jogIntervals: {},
|
||||
// AGV
|
||||
cameraOpened: false,
|
||||
agvConnected: false,
|
||||
agvBattery: null,
|
||||
agvPosition: null,
|
||||
agvSpeed: 0.5,
|
||||
agvMoveInterval: null,
|
||||
agvCameraUrl: API + '/api/camera/refresh',
|
||||
agvCameraTimer: null,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.refresh()
|
||||
this.refreshAngles()
|
||||
},
|
||||
watch: {
|
||||
tab(val) {
|
||||
if (val === 'agv') {
|
||||
this.agvCameraTimer = setInterval(() => {
|
||||
this.agvCameraUrl = API + '/api/camera/refresh?t=' + Date.now()
|
||||
}, 1000)
|
||||
} else {
|
||||
if (this.agvCameraTimer) {
|
||||
clearInterval(this.agvCameraTimer)
|
||||
this.agvCameraTimer = null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
Object.values(this.jogIntervals).forEach(i => clearInterval(i))
|
||||
if (this.agvCameraTimer) clearInterval(this.agvCameraTimer)
|
||||
},
|
||||
methods: {
|
||||
async refresh() {
|
||||
try {
|
||||
const res = await fetch(API + '/api/status')
|
||||
const data = await res.json()
|
||||
this.agvConnected = data.agv_connected
|
||||
this.armConnected = data.arm_connected
|
||||
this.cameraOpened = data.camera_opened
|
||||
this.mapLoaded = data.map_loaded
|
||||
// 如果地图已加载,自动获取地图图像和元数据
|
||||
if (data.map_loaded) {
|
||||
this.mapImageUrl = API + '/api/map/image?t=' + Date.now()
|
||||
try {
|
||||
const metaRes = await fetch(API + '/api/map/meta')
|
||||
const meta = await metaRes.json()
|
||||
if (meta.ok) this.mapMeta = meta
|
||||
} catch (e) {}
|
||||
}
|
||||
} catch (e) {}
|
||||
await this.loadAllPoints()
|
||||
await this.loadAllModels()
|
||||
},
|
||||
// === 地图 ===
|
||||
async loadMap() {
|
||||
const res = await fetch(API + '/api/map/load', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.mapForm)
|
||||
})
|
||||
const data = await res.json()
|
||||
this.mapMsg = data.ok ? '✅ 地图加载成功' : '❌ ' + (data.error || '加载失败')
|
||||
this.mapLoaded = data.ok
|
||||
if (data.ok) {
|
||||
this.mapImageUrl = API + '/api/map/image?t=' + Date.now()
|
||||
try {
|
||||
const metaRes = await fetch(API + '/api/map/meta')
|
||||
const meta = await metaRes.json()
|
||||
if (meta.ok) this.mapMeta = meta
|
||||
} catch (e) {}
|
||||
}
|
||||
},
|
||||
onMapError() {
|
||||
this.mapMsg = '❌ 地图图像加载失败'
|
||||
},
|
||||
getMapX(coords) {
|
||||
if (!coords || !this.mapMeta) return 50
|
||||
const [x, y, yaw] = coords
|
||||
const { resolution, origin, width } = this.mapMeta
|
||||
const px = (x - origin[0]) / (resolution * width) * 100
|
||||
return Math.max(0, Math.min(100, px))
|
||||
},
|
||||
getMapY(coords) {
|
||||
if (!coords || !this.mapMeta) return 50
|
||||
const [x, y, yaw] = coords
|
||||
const { resolution, origin, height } = this.mapMeta
|
||||
const py = (y - origin[1]) / (resolution * height) * 100
|
||||
return Math.max(0, Math.min(100, 100 - py))
|
||||
},
|
||||
async saveMap() {
|
||||
await fetch(API + '/api/map/save', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.mapForm)
|
||||
})
|
||||
this.mapMsg = '✅ 地图配置已保存'
|
||||
},
|
||||
// === 点位 ===
|
||||
async loadAllPoints() {
|
||||
const res = await fetch(API + '/api/points/list')
|
||||
const data = await res.json()
|
||||
this.points = data.points || []
|
||||
},
|
||||
async addPoint() {
|
||||
const res = await fetch(API + '/api/points/add', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: this.newPointName || 'point_' + (this.points.length + 1),
|
||||
photo_mode: this.newPointMode,
|
||||
sequence: this.newPointSequence
|
||||
})
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.ok) {
|
||||
await this.loadAllPoints()
|
||||
this.newPointName = ''
|
||||
}
|
||||
},
|
||||
async deletePoint(id) {
|
||||
if (!confirm('确定删除该点位?')) return
|
||||
await fetch(API + '/api/points/delete/' + id, { method: 'DELETE' })
|
||||
await this.loadAllPoints()
|
||||
},
|
||||
async saveAllPoints() {
|
||||
await fetch(API + '/api/points/save', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ points: this.points })
|
||||
})
|
||||
alert('点位已保存')
|
||||
},
|
||||
getPoint(id) {
|
||||
return this.points.find(p => p.id === id)
|
||||
},
|
||||
formatAngles(angles) {
|
||||
if (!angles) return '—'
|
||||
return angles.map(a => (a || 0).toFixed(1) + '°').join(' / ')
|
||||
},
|
||||
// === 机型管理 ===
|
||||
async loadAllModels() {
|
||||
const res = await fetch(API + '/api/models/list')
|
||||
const data = await res.json()
|
||||
this.models = data.models || []
|
||||
// 初始化 poseForm
|
||||
this.models.forEach(m => {
|
||||
if (!this.poseForm[m.id]) {
|
||||
this.poseForm[m.id] = { name: '', photo_type: 'front', description: '' }
|
||||
}
|
||||
})
|
||||
},
|
||||
async addModel() {
|
||||
const res = await fetch(API + '/api/models/add', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: this.newModelName || 'model_' + (this.models.length + 1),
|
||||
serial_prefix: this.newModelSerial,
|
||||
description: ''
|
||||
})
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.ok) {
|
||||
await this.loadAllModels()
|
||||
this.newModelName = ''
|
||||
this.newModelSerial = ''
|
||||
}
|
||||
},
|
||||
async deleteModel(modelId) {
|
||||
if (!confirm('确定删除该机型?其下所有姿态将被删除!')) return
|
||||
await fetch(API + '/api/models/delete/' + modelId, { method: 'DELETE' })
|
||||
await this.loadAllModels()
|
||||
},
|
||||
// === 姿态管理(属于机型)===
|
||||
async addPose(modelId) {
|
||||
const form = this.poseForm[modelId]
|
||||
if (!form) return
|
||||
await fetch(API + '/api/models/poses/add', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model_id: modelId,
|
||||
name: form.name || '姿态' + ((this.getModel(modelId)?.poses?.length || 0) + 1),
|
||||
photo_type: form.photo_type,
|
||||
arm_angles: this.currentAngles,
|
||||
speed: 500,
|
||||
description: form.description || ''
|
||||
})
|
||||
})
|
||||
await this.loadAllModels()
|
||||
form.name = ''
|
||||
form.description = ''
|
||||
},
|
||||
async deletePose(modelId, poseId) {
|
||||
if (!confirm('确定删除该姿态?')) return
|
||||
await fetch(API + '/api/models/' + modelId + '/poses/' + poseId, { method: 'DELETE' })
|
||||
await this.loadAllModels()
|
||||
},
|
||||
getModel(id) {
|
||||
return this.models.find(m => m.id === id)
|
||||
},
|
||||
// === 机械臂 ===
|
||||
|
||||
clearSelection() { this.selectedMachine = null },
|
||||
async saveMachineCoords() {
|
||||
if (!this.selectedMachine) return
|
||||
try {
|
||||
const res = await fetch(API + '/api/mission/machines/' + this.selectedMachine.id, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
front: this.selectedMachine.front,
|
||||
back: this.selectedMachine.back
|
||||
})
|
||||
})
|
||||
if (res.ok) {
|
||||
this.mapMsg = '✅ 机器坐标已保存'
|
||||
setTimeout(() => this.mapMsg = '', 2000)
|
||||
} else {
|
||||
alert('保存失败: ' + res.status)
|
||||
}
|
||||
} catch (e) { alert('保存失败: ' + e.message) }
|
||||
},
|
||||
selectMachine(machine) {
|
||||
// 确保 front/back 永远有 coords 数组,避免 v-model 赋值失败
|
||||
if (!machine.front) machine.front = { coords: [0, 0, 0], poses: [] }
|
||||
else if (!Array.isArray(machine.front.coords)) machine.front.coords = [0, 0, 0]
|
||||
if (!machine.back) machine.back = { coords: [0, 0, 0], poses: [] }
|
||||
else if (!Array.isArray(machine.back.coords)) machine.back.coords = [0, 0, 0]
|
||||
this.selectedMachine = machine
|
||||
console.log('selectedMachine:', machine)
|
||||
},
|
||||
onCellClick(ri, ci) {
|
||||
let m = this.getMachineAt(ri, ci)
|
||||
if (!m) {
|
||||
// 自动创建机器记录
|
||||
this.createMachine(ri, ci).then(() => {
|
||||
m = this.getMachineAt(ri, ci)
|
||||
if (m) this.selectMachine(m)
|
||||
})
|
||||
} else {
|
||||
this.selectMachine(m)
|
||||
}
|
||||
},
|
||||
async createMachine(ri, ci) {
|
||||
try {
|
||||
const res = await fetch(API + '/api/mission/machines', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ row: ri, col: ci, front: { coords: [0, 0, 0], poses: [] }, back: { coords: [0, 0, 0], poses: [] } })
|
||||
})
|
||||
await this.loadAllMachines()
|
||||
return res.ok
|
||||
} catch (e) { alert('创建机器失败: ' + e.message); return false }
|
||||
},
|
||||
|
||||
async loadAllMachines() {
|
||||
try {
|
||||
const res = await fetch(API + '/api/mission/machines')
|
||||
const data = await res.json()
|
||||
this.missionConfig.machines = data.machines || []
|
||||
} catch (e) { console.error('加载机器列表失败', e) }
|
||||
},
|
||||
getMachineAt(ri, ci) {
|
||||
if (!this.missionConfig.machines) return null
|
||||
return this.missionConfig.machines.find(m => m.row === ri && m.col === ci) || null
|
||||
},
|
||||
getPositionAt(ri, ci) {
|
||||
if (!this.missionConfig.machines) return null
|
||||
const machine = this.getMachineAt(ri, ci)
|
||||
if (!machine) return null
|
||||
if (ri === 0) return machine.front
|
||||
const prevMachine = this.getMachineAt(ri - 1, ci)
|
||||
return prevMachine ? prevMachine.back : machine.front
|
||||
},
|
||||
async refreshAngles() {
|
||||
if (!this.armConnected) return
|
||||
try {
|
||||
const res = await fetch(API + '/api/arm/get_angles')
|
||||
const data = await res.json()
|
||||
if (data.ok && data.angles) {
|
||||
this.currentAngles = data.angles
|
||||
this.angleInputs = [...data.angles]
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
async setAngle(idx, val) {
|
||||
await fetch(API + '/api/arm/set_angle', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ joint: 'J' + (idx + 1), angle: val })
|
||||
})
|
||||
},
|
||||
async applyAngles() {
|
||||
await fetch(API + '/api/arm/set_angles', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ angles: this.angleInputs, speed: 500 })
|
||||
})
|
||||
},
|
||||
jogStart(idx, dir) {
|
||||
const joint = 'J' + (idx + 1)
|
||||
fetch(API + '/api/arm/jog', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ joint, direction: dir })
|
||||
})
|
||||
this.jogIntervals[idx] = setInterval(() => this.refreshAngles(), 200)
|
||||
},
|
||||
jogStop(idx) {
|
||||
clearInterval(this.jogIntervals[idx])
|
||||
const joint = 'J' + (idx + 1)
|
||||
fetch(API + '/api/arm/jog', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ joint, direction: 0 })
|
||||
})
|
||||
setTimeout(() => this.refreshAngles(), 300)
|
||||
},
|
||||
onPreviewError(e) {
|
||||
e.target.style.display = 'none'
|
||||
},
|
||||
// === AGV 控制 ===
|
||||
async refreshAgvPosition() {
|
||||
if (!this.agvConnected) return
|
||||
try {
|
||||
const res = await fetch(API + '/api/agv/position')
|
||||
const data = await res.json()
|
||||
if (data.ok) {
|
||||
this.agvPosition = data.position
|
||||
this.agvBattery = data.battery
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
agvMoveStart(dir) {
|
||||
if (!this.agvConnected) return
|
||||
fetch(API + '/api/agv/move', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ direction: dir, speed: this.agvSpeed })
|
||||
})
|
||||
},
|
||||
agvMoveStop() {
|
||||
fetch(API + '/api/agv/move', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ direction: 'stop' })
|
||||
})
|
||||
},
|
||||
async agvStop() {
|
||||
await fetch(API + '/api/agv/stop', { method: 'POST' })
|
||||
},
|
||||
async agvResetCollision() {
|
||||
if (!this.agvConnected) {
|
||||
alert('AGV 未连接')
|
||||
return
|
||||
}
|
||||
if (!confirm('确定执行撞物体后复位?')) return
|
||||
try {
|
||||
const res = await fetch(API + '/api/agv/reset', { method: 'POST' })
|
||||
const data = await res.json()
|
||||
if (data.ok) {
|
||||
alert('✅ ' + data.message)
|
||||
await this.refresh()
|
||||
await this.refreshAgvPosition()
|
||||
} else {
|
||||
alert('❌ 复位失败: ' + (data.error || '未知错误'))
|
||||
}
|
||||
} catch (e) {
|
||||
alert('❌ 复位请求失败: ' + e.message)
|
||||
}
|
||||
},
|
||||
async capturePosition(ri, ci, side) {
|
||||
if (!this.agvConnected) { alert('请先连接AGV'); return }
|
||||
let machine = this.getMachineAt(ri, ci)
|
||||
if (!machine) {
|
||||
try {
|
||||
const res = await fetch(API + '/api/mission/machines', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ row: ri, col: ci, front: { coords: [0, 0, 0], poses: [] }, back: { coords: [0, 0, 0], poses: [] } })
|
||||
})
|
||||
if (!res.ok) throw new Error('创建失败')
|
||||
await this.loadAllMachines()
|
||||
machine = this.getMachineAt(ri, ci)
|
||||
} catch (e) { alert('创建机器失败: ' + e.message); return }
|
||||
}
|
||||
try {
|
||||
const res = await fetch(API + '/api/agv/position')
|
||||
const pos = await res.json()
|
||||
let x = 0, y = 0, theta = 0
|
||||
if (pos.position && Array.isArray(pos.position)) { x = pos.position[0]||0; y = pos.position[1]||0; theta = pos.position[2]||0 }
|
||||
if (!machine) { machine = this.getMachineAt(ri, ci) }
|
||||
if (!machine) { alert('机器记录不存在'); return }
|
||||
if (side === 'front') { machine.front.coords = [x, y, theta] } else { machine.back.coords = [x, y, theta] }
|
||||
await fetch(API + '/api/mission/machines/' + machine.id, {
|
||||
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(machine)
|
||||
})
|
||||
alert((side==='front'?'正面':'背面')+'点位已更新: ('+x.toFixed(2)+','+y.toFixed(2)+','+theta.toFixed(2)+')')
|
||||
} catch (e) { alert('读取位置失败: '+e.message) }
|
||||
},
|
||||
}
|
||||
}).mount('#app')
|
||||
Reference in New Issue
Block a user