508 lines
29 KiB
HTML
508 lines
29 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>设置 - AGV 拍摄系统</title>
|
||
<link rel="stylesheet" href="/static/css/style.css?v=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>
|