This commit is contained in:
ywb
2026-05-14 21:43:35 +08:00
commit 94c1043f4d
40 changed files with 8602 additions and 0 deletions
+148
View File
@@ -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>
+82
View File
@@ -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>
+507
View File
@@ -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>
+438
View File
@@ -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')