Files
smart-inspection/agv_app/static/js/setting.js
T
2026-05-16 22:58:04 +08:00

1087 lines
39 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { createApp } = Vue
const API = ''
const app = createApp({
data() {
return {
tab: 'map',
// 任务配置
missionConfig: { rows: 3, cols: 3, grid: [], machines: [], positions: [] },
// 点位编辑弹窗
editingPoint: null, // 当前编辑的点位 {pointRow, col} — pointRow是点位行号(0~rows)
pointEditor: { x: 0, y: 0, yaw: 0 },
selectedMachine: null,
sequence: [],
poseForm: { name: '', photo_type: 'front', description: '' },
// 地图
mapForm: { map_dir: '/home/elephant/agv_pro_ros2/src/agv_pro_navigation2/map/', map_file: 'map.yaml' },
mapMsg: '',
mapLoaded: false,
mapImageUrl: '',
mapMeta: null,
mapVersion: 0, // 地图点位版本号,用于强制重新渲染
// 点位
points: [],
newPointName: '',
newPointMode: 'front',
newPointSequence: ['front', 'back'],
// 机型(姿态组)
models: [],
selectedModelId: null,
newModelName: '',
newModelId: null,
newModelDesc: '',
newModelNotes: '',
newPoseForm: {}, // 机型配置:新建姿态的表单
showAddModelModal: false,
expandedModelId: null,
// 机械臂
armConnected: false,
currentAngles: [],
angleInputs: [],
previewUrl: API + '/api/camera/preview',
jogIntervals: {},
// AGV
cameraOpened: false,
agvConnected: false,
agvBattery: null,
agvPosition: null,
agvSpeed: 0.5,
agvMoveInterval: null,
initPoseLoading: false,
initPoseMsg: '',
nav2Available: false,
navStatus: 'idle',
navCurrentPos: null,
agvCameraUrl: API + '/api/camera/refresh',
agvCameraTimer: null,
initPoseLoading: false,
}
},
mounted() {
this.refresh()
this.refreshAngles()
this.refreshNavStatus()
setInterval(() => {
if (this.nav2Available && this.navStatus === 'navigating') {
this.refreshNavStatus()
}
}, 3000)
},
watch: {
// 监听点位数据变化,自动刷新地图
'missionConfig.positions'() {
this.mapVersion++
},
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()
await this.loadAllMachines()
await this.loadMissionConfig()
},
// === 地图 ===
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) {
//console.log('[getMapX] mapMeta not loaded, returning default 50');
return 50
}
const [x, y, yaw] = coords
const { resolution, origin, width } = this.mapMeta
const px = (x - origin[0]) / (resolution * width) * 100
const result = Math.max(0, Math.min(100, px));
//console.log('[getMapX]', coords, '→ px%:', result);
return result
},
getMapY(coords) {
if (!coords || !this.mapMeta) {
//console.log('[getMapY] mapMeta not loaded, returning default 50');
return 50
}
const [x, y, yaw] = coords
const { resolution, origin, height } = this.mapMeta
const py = (y - origin[1]) / (resolution * height) * 100
const result = Math.max(0, Math.min(100, 100 - py));
//console.log('[getMapY]', coords, '→ py%:', result);
return result
},
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 || []
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 || '型号_' + (this.models.length + 1),
id: this.newModelId,
description: this.newModelDesc || '',
notes: this.newModelNotes || '',
serial_prefix: this.newModelSerial || ''
})
})
const data = await res.json()
if (data.ok) {
await this.loadAllModels()
this.newModelName = ''
this.newModelId = null
this.newModelDesc = ''
this.newModelNotes = ''
this.newModelSerial = ''
this.showAddModelModal = false
}
},
async deleteModel(modelId) {
if (!confirm('确定删除该机型?其下所有姿态将被删除!')) return
await fetch(API + '/api/models/delete/' + modelId, { method: 'DELETE' })
await this.loadAllModels()
},
// === 姿态管理(属于机型)===
async addPose(modelId, photoType, customName) {
const form = this.poseForm[modelId]
const type = photoType || form?.photo_type || 'front'
const name = customName || form?.name || type + '_' + ((this.getModel(modelId)?.poses?.filter(p => p.photo_type === type).length || 0) + 1)
await fetch(API + '/api/models/poses/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model_id: modelId,
name: name,
photo_type: type,
arm_angles: this.currentAngles && this.currentAngles.length === 6 ? this.currentAngles : [0, 0, 0, 0, 0, 0],
speed: 500,
description: form?.description || ''
})
})
await this.loadAllModels()
if (form) {
form.name = ''
form.description = ''
}
},
async deletePose(modelId, poseId) {
if (!confirm('确定删除该姿态?')) return
await fetch(API + '/api/models/' + modelId + '/poses/' + poseId, { method: 'DELETE' })
await this.loadAllModels()
},
async updatePoseAngle(modelId, poseId, jointIndex, event) {
const value = parseFloat(event.target.value)
if (isNaN(value)) return
// Find the pose and update its angle
const model = this.getModel(modelId)
if (!model) return
const pose = model.poses.find(p => p.id === poseId)
if (!pose) return
if (!pose.arm_angles) pose.arm_angles = [0, 0, 0, 0, 0, 0]
pose.arm_angles[jointIndex] = value
// Save to backend
await fetch(API + '/api/models/' + modelId + '/poses/' + poseId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ arm_angles: pose.arm_angles })
})
},
getModel(id) {
return this.models.find(m => m.id === id)
},
// === 任务配置 ===
async loadMissionConfig() {
try {
const res = await fetch(API + '/api/mission/config')
const data = await res.json()
if (data.ok && data.config) {
this.missionConfig.rows = data.config.rows || 3
this.missionConfig.cols = data.config.cols || 3
this.missionConfig.grid = data.config.grid || []
this.missionConfig.machines = data.machines || []
this.missionConfig.positions = data.config.positions || []
this.mapVersion++
}
} catch (e) { console.error('加载任务配置失败', e) }
},
async generateGrid() {
try {
const res = await fetch(API + '/api/mission/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
rows: this.missionConfig.rows,
cols: this.missionConfig.cols,
grid: []
})
})
const data = await res.json()
if (data.ok) {
this.missionConfig.grid = data.config.grid || []
alert('✅ 网格已生成 (' + this.missionConfig.rows + '×' + this.missionConfig.cols + ')')
} else {
alert('❌ 网格生成失败')
}
} catch (e) { alert('请求失败: ' + e.message) }
},
async saveMissionConfig() {
try {
const res = await fetch(API + '/api/mission/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
rows: this.missionConfig.rows,
cols: this.missionConfig.cols,
grid: this.missionConfig.grid
})
})
const data = await res.json()
if (data.ok) {
alert('✅ 网格配置已保存')
}
} catch (e) { alert('保存失败: ' + e.message) }
},
async initPose() {
try {
this.initPoseLoading = true
const res = await fetch(API + '/api/mission/init_pose', { method: 'POST' })
const data = await res.json()
if (data.ok) {
alert('✅ 初始位置已设为 (0, 0, 0)')
} else {
alert('❌ 初始化失败: ' + (data.error || '未知错误'))
}
} catch (e) {
alert('❌ 初始化位置请求失败: ' + e.message)
} finally {
this.initPoseLoading = 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) }
},
// ========== 点位行模型(独立于机器) ==========
/**
* 获取指定点位行的数据
* @param {number} pointRow - 点位行号,范围 0 ~ missionConfig.rows
* - pointRow=0 → 第1台机器的正面拍摄点
* - pointRow=rows → 最后1台机器的背面拍摄点
* - 中间 pointRow=i → 上面机器(i)的背面 + 下面机器(i+1)的正面
* @param {number} col - 列号(0-based
* @returns {object|null} 点位对象 { coords: [x,y,z], ... } 或 null
*/
getPointAt(pointRow, col) {
var positions = this.missionConfig.positions || []
// 在独立 positions 数组中查找 (pointRow, col)
for (var i = 0; i < positions.length; i++) {
var p = positions[i]
if (parseInt(p.row) === pointRow && parseInt(p.col) === col) {
// 优先找 shoot,其次找 front/back
if (p.side === 'shoot') return p
// 再看有没有同一(row,col)的shoot
}
}
// 再找同(row,col)的shoot类型
for (var i = 0; i < positions.length; i++) {
var p = positions[i]
if (parseInt(p.row) === pointRow && parseInt(p.col) === col && p.side === 'shoot') {
return p
}
}
// 兼容旧数据:尝试从机器对象的 front/back 获取
var machineAbove = this.getMachineAt(pointRow - 1, col) // 上面的机器
var machineBelow = this.getMachineAt(pointRow, col) // 下面的机器
if (pointRow === 0 && machineBelow) {
// 第一个点位行 → 下面机器的正面
return machineBelow.front || { coords: [0, 0, 0], poses: [] }
}
if (pointRow === this.missionConfig.rows && machineAbove) {
// 最后一个点位行 → 上面机器的背面
return machineAbove.back || { coords: [0, 0, 0], poses: [] }
}
// 中间点位行:优先返回上面机器的背面
if (machineAbove && machineAbove.back) {
return machineAbove.back
}
if (machineBelow && machineBelow.front) {
return machineBelow.front
}
return null
},
/**
* 获取点位的归属描述(用于弹窗标题)
* @param {number} pointRow - 点位行号
* @param {number} col - 列号
* @returns {string} 如 "第2列 · 机器2背面/机器3正面"
*/
getPointOwnerLabel(pointRow, col) {
var rows = this.missionConfig.rows
var labels = []
if (pointRow === 0) {
// 第一行点位:下面机器的正面
var m = this.getMachineAt(0, col)
if (m) labels.push('机器' + (m.row + 1) + '正面')
} else if (pointRow === rows) {
// 最后行点位:上面机器的背面
var m2 = this.getMachineAt(rows - 1, col)
if (m2) labels.push('机器' + (m2.row + 1) + '背面')
} else {
// 中间点位行:上面机器的背面 + 下面机器的正面
var mAbove = this.getMachineAt(pointRow - 1, col)
var mBelow = this.getMachineAt(pointRow, col)
if (mAbove) labels.push('机器' + (mAbove.row + 1) + '背面')
if (mBelow) labels.push('机器' + (mBelow.row + 1) + '正面')
}
if (labels.length === 0) return '第' + (col + 1) + '列 · 无归属'
return '第' + (col + 1) + '列 · ' + labels.join('/')
},
/**
* 检查点位是否可以清空
* 只有当上下两台机器都不需要这个点位时才能清空
*/
canClearPoint(pointRow, col) {
var rows = this.missionConfig.rows
// 如果上面有机器且机器存在 → 不能清空(上面机器需要此点位拍背面)
if (pointRow > 0 && pointRow <= rows) {
var mAbove = this.getMachineAt(pointRow - 1, col)
if (mAbove) return false
}
// 如果下面有机器且机器存在 → 不能清空(下面机器需要此点位拍正面)
if (pointRow >= 0 && pointRow < rows) {
var mBelow = this.getMachineAt(pointRow, col)
if (mBelow) return false
}
return true
},
getMachineAt(ri, ci) {
if (!this.missionConfig.machines) return null
return this.missionConfig.machines.find(function(m) { return m.row === ri && m.col === ci }) || null
},
// 打开点位编辑弹窗(基于点位行号)
openPointEdit(pointRow, col) {
var existing = this.getPointAt(pointRow, col)
if (existing && existing.coords) {
this.pointEditor.x = existing.coords[0] !== undefined ? existing.coords[0] : 0
this.pointEditor.y = existing.coords[1] !== undefined ? existing.coords[1] : 0
this.pointEditor.yaw = existing.coords[2] !== undefined ? existing.coords[2] : 0
} else {
this.pointEditor.x = 0
this.pointEditor.y = 0
this.pointEditor.yaw = 0
}
this.editingPoint = { pointRow: pointRow, col: col }
},
// 关闭点位编辑弹窗
closePointEdit() {
this.editingPoint = null
},
// 从AGV读取当前坐标到点位编辑器
async loadPointFromAgv() {
if (!this.agvConnected) { alert('请先连接AGV'); return }
try {
const res = await fetch(API + '/api/agv/position')
const pos = await res.json()
if (pos.ok && pos.position && Array.isArray(pos.position)) {
this.pointEditor.x = pos.position[0] ?? 0
this.pointEditor.y = pos.position[1] ?? 0
this.pointEditor.yaw = pos.position[2] ?? 0
alert('✅ 已读取AGV位置: (' + this.pointEditor.x.toFixed(2) + ', ' + this.pointEditor.y.toFixed(2) + ', ' + this.pointEditor.yaw.toFixed(2) + ')')
} else if (pos.ok && (!pos.position || !Array.isArray(pos.position))) {
alert('⚠️ AGV 未发布位置数据,请检查 AGV 传感器是否正常')
} else {
alert('读取AGV位置失败: ' + (pos.error || '未知错误'))
}
} catch (e) { alert('读取AGV位置失败: ' + e.message) }
},
// 保存点位配置到独立 positions 数组
async savePoint() {
if (!this.editingPoint) return
var pointRow = this.editingPoint.pointRow
var col = this.editingPoint.col
var coords = [this.pointEditor.x, this.pointEditor.y, this.pointEditor.yaw]
try {
// 保存到后端独立点位表(使用 point_row 标识点位行)
var saveRes = await fetch(API + '/api/mission/positions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
row: pointRow,
col: col,
side: 'shoot', // 统一用 'shoot' 表示拍摄点位
coords: coords,
poses: []
})
})
var saveData = await saveRes.json()
if (!saveData.ok) {
alert('保存点位失败: ' + (saveData.error || '未知错误'))
return
}
// 同步更新关联机器的坐标(如果有的话)
this.syncPointToMachines(pointRow, col, coords)
await this.loadMissionConfig()
alert('✅ 点位坐标已保存: (' + coords[0].toFixed(2) + ', ' + coords[1].toFixed(2) + ', ' + coords[2].toFixed(2) + ')')
this.closePointEdit()
} catch (e) { alert('保存点位失败: ' + e.message) }
},
// 将点位坐标同步到关联的机器对象
syncPointToMachines(pointRow, col, coords) {
var rows = this.missionConfig.rows
// pointRow=0 → 同步到下面机器(0,col)的front
if (pointRow === 0) {
var m0 = this.getMachineAt(0, col)
if (m0) {
this.updateMachineSide(m0, 'front', coords)
}
return
}
// pointRow=rows → 同步到上面机器(rows-1,col)的back
if (pointRow === rows) {
var mLast = this.getMachineAt(rows - 1, col)
if (mLast) {
this.updateMachineSide(mLast, 'back', coords)
}
return
}
// 中间点位行 → 同步到上面机器的back + 下面机器的front
var mAbove = this.getMachineAt(pointRow - 1, col)
var mBelow = this.getMachineAt(pointRow, col)
if (mAbove) {
this.updateMachineSide(mAbove, 'back', coords)
}
if (mBelow) {
this.updateMachineSide(mBelow, 'front', coords)
}
},
// 更新机器某侧的坐标
async updateMachineSide(machine, side, coords) {
try {
var update = {}
update[side] = {
coords: coords,
poses: (machine[side] && machine[side].poses) ? machine[side].poses : []
}
await fetch(API + '/api/mission/machines/' + machine.id, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(update)
})
} catch (e) { console.error('同步机器坐标失败', e) }
},
// 清空点位(带保护检查)
async clearPoint() {
if (!this.editingPoint) return
var pointRow = this.editingPoint.pointRow
var col = this.editingPoint.col
// 检查是否可以清空
if (!this.canClearPoint(pointRow, col)) {
var ownerLabel = this.getPointOwnerLabel(pointRow, col)
alert('⚠️ 无法清空!此点位服务于: ' + ownerLabel + '\n必须先移除相关机器才能清空此点位。')
return
}
if (!confirm('确定清空此点位坐标?')) return
try {
// 发送清空请求(坐标归零或删除记录)
await fetch(API + '/api/mission/positions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
row: pointRow,
col: col,
side: 'shoot',
coords: [0, 0, 0],
poses: []
})
})
this.pointEditor.x = 0
this.pointEditor.y = 0
this.pointEditor.yaw = 0
await this.loadMissionConfig()
} catch (e) { alert('清空点位失败: ' + e.message) }
},
onCellClick(ri, ci) {
var m = this.getMachineAt(ri, ci)
if (!m) {
// 无机器 → 创建机器记录
this.createMachine(ri, ci).then(function(ok) {
if (ok) {
var created = this.getMachineAt(ri, ci)
if (created) this.selectMachine(created)
}
}.bind(this))
} else {
// 有机器 → 切换为无机器(删除)
this.deleteMachine(m.id)
}
},
async createMachine(ri, ci) {
try {
var machineId = 'm_' + ri + '_' + ci
var res = await fetch(API + '/api/mission/machines/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: machineId,
row: ri,
col: ci,
front: { coords: [0, 0, 0], poses: [] },
back: { coords: [0, 0, 0], poses: [] }
})
})
var data = await res.json()
if (!data.ok && data.error !== '该位置已有机器') {
alert('创建机器失败: ' + (data.error || '未知错误'))
return false
}
await this.loadAllMachines()
return true
} catch (e) { alert('创建机器失败: ' + e.message); return false }
},
selectMachine(machine) {
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
},
clearSelection() {
this.selectedMachine = null
},
async deleteMachine(machineId) {
if (!confirm('确定删除此机器?\n\n注意:删除后其上方/下方的点位仍可继续使用。')) return
try {
await fetch(API + '/api/mission/machines/' + machineId, { method: 'DELETE' })
this.selectedMachine = null
await this.loadAllMachines()
await this.loadMissionConfig() // 重新加载点位,确保数据同步
} catch (e) { alert('删除失败: ' + e.message) }
},
async saveMachineCoords() {
if (!this.selectedMachine) return
try {
var res = await fetch(API + '/api/mission/machines/' + this.selectedMachine.id, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
front: this.selectedMachine.front,
back: this.selectedMachine.back
})
})
if (res.ok) {
this.mapMsg = '✅ 机器坐标已保存'
setTimeout(function() { this.mapMsg = '' }.bind(this), 2000)
} else {
alert('保存失败: ' + res.status)
}
} catch (e) { alert('保存失败: ' + e.message) }
},
async readPosition(side) {
if (!this.agvConnected) { alert('AGV 未连接'); return }
try {
var res = await fetch(API + '/api/agv/position')
var data = await res.json()
if (data.ok && data.position && Array.isArray(data.position)) {
var x = data.position[0]
var y = data.position[1]
var theta = data.position[2]
if (side === 'front') {
this.selectedMachine.front.coords = [x, y, theta]
} else {
this.selectedMachine.back.coords = [x, y, theta]
}
} else if (data.ok && (!data.position || !Array.isArray(data.position))) {
alert('⚠️ AGV 未发布位置数据,请检查 AGV 传感器是否正常')
} else {
alert('读取位置失败: ' + (data.error || '未知错误'))
}
} catch (e) { alert('读取位置失败: ' + e.message) }
},
async addPoseToMachine(machineId, side) {
var name = this.poseForm.name || '姿态' + (((this.selectedMachine && this.selectedMachine[side] && this.selectedMachine[side].poses) || []).length + 1)
try {
var res = await fetch(API + '/api/mission/poses/' + machineId + '/' + side, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: name,
arm_angles: this.currentAngles.length === 6 ? this.currentAngles : [0, 0, 0, 0, 0, 0],
speed: 500,
description: ''
})
})
var data = await res.json()
if (data.ok) {
this.poseForm.name = ''
await this.loadAllMachines()
var updated = this.getMachineAt(this.selectedMachine.row, this.selectedMachine.col)
if (updated) this.selectMachine(updated)
} else {
alert('添加姿态失败: ' + (data.error || '未知错误'))
}
} catch (e) { alert('添加姿态失败: ' + e.message) }
},
async deletePose(machineId, side, poseId) {
if (!confirm('确定删除此姿态?')) return
try {
await fetch(API + '/api/mission/poses/' + machineId + '/' + side + '/' + poseId, { method: 'DELETE' })
await this.loadAllMachines()
if (this.selectedMachine) {
var updated = this.getMachineAt(this.selectedMachine.row, this.selectedMachine.col)
if (updated) this.selectMachine(updated)
}
} catch (e) { alert('删除姿态失败: ' + e.message) }
},
async capturePosition(ri, ci, side) {
if (!this.agvConnected) { alert('请先连接AGV'); return }
var machine = this.getMachineAt(ri, ci)
if (!machine) {
try {
var machineId = 'm_' + ri + '_' + ci
var res = await fetch(API + '/api/mission/machines/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: machineId,
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 {
var res = await fetch(API + '/api/agv/position')
var pos = await res.json()
var x = 0, y = 0, theta = 0
if (pos.ok && pos.position && Array.isArray(pos.position)) {
x = pos.position[0] || 0
y = pos.position[1] || 0
theta = pos.position[2] || 0
} else if (pos.ok && (!pos.position || !Array.isArray(pos.position))) {
alert('⚠️ AGV 未发布位置数据,请检查 AGV 传感器是否正常')
return
} else {
alert('读取位置失败: ' + (pos.error || '未知错误'))
return
}
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) }
},
async refreshSequence() {
try {
var res = await fetch(API + '/api/mission/generate_sequence')
var data = await res.json()
if (data.ok) {
this.sequence = data.sequence || []
}
} catch (e) { console.error('刷新序列失败', e) }
},
// === 机械臂 ===
async refreshAngles() {
if (!this.armConnected) return
try {
var res = await fetch(API + '/api/arm/get_angles')
var 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) {
var 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(function() { this.refreshAngles() }.bind(this), 200)
},
jogStop(idx) {
clearInterval(this.jogIntervals[idx])
var joint = 'J' + (idx + 1)
fetch(API + '/api/arm/jog', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ joint, direction: 0 })
})
setTimeout(function() { this.refreshAngles() }.bind(this), 300)
},
onPreviewError(e) {
e.target.style.display = 'none'
},
// === 姿态角度控制(机型配置 Tab) ===
async refreshPoseAngles(modelId, poseId) {
// 获取当前机械臂角度,填入姿态,保存到后端
if (!this.armConnected) {
alert('机械臂未连接')
return
}
try {
var res = await fetch(API + '/api/arm/get_angles')
var data = await res.json()
if (data.ok && data.angles) {
// Find the pose and update
var model = this.models.find(m => m.id === modelId)
if (model) {
var pose = model.poses.find(p => p.id === poseId)
if (pose) {
pose.arm_angles = [...data.angles]
// Save to backend
await fetch(API + '/api/models/' + modelId + '/poses/' + poseId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ arm_angles: pose.arm_angles })
})
}
}
}
} catch (e) {
console.error('refreshPoseAngles error:', e)
}
},
async applyPoseAngles(modelId, poseId) {
// 调整机械臂到姿态的角度
var model = this.models.find(m => m.id === modelId)
if (model) {
var pose = model.poses.find(p => p.id === poseId)
if (pose && pose.arm_angles) {
try {
await fetch(API + '/api/arm/set_angles', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ angles: pose.arm_angles, speed: 500 })
})
} catch (e) {
console.error('applyPoseAngles error:', e)
}
}
}
},
async adjustPoseAngle(modelId, poseId, jointIndex, delta) {
// 微调姿态角度,并实时调整机械臂
var model = this.models.find(m => m.id === modelId)
if (model) {
var pose = model.poses.find(p => p.id === poseId)
if (pose) {
if (!pose.arm_angles) pose.arm_angles = [0, 0, 0, 0, 0, 0]
pose.arm_angles[jointIndex] = (pose.arm_angles[jointIndex] || 0) + delta
// Save to backend
await fetch(API + '/api/models/' + modelId + '/poses/' + poseId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ arm_angles: pose.arm_angles })
})
// Move arm
await fetch(API + '/api/arm/set_angle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ joint: 'J' + (jointIndex + 1), angle: pose.arm_angles[jointIndex] })
})
}
}
},
async updatePoseAngleAndMove(modelId, poseId, jointIndex, value) {
// 更新姿态角度(输入框),并实时调整机械臂
var model = this.models.find(m => m.id === modelId)
if (model) {
var pose = model.poses.find(p => p.id === poseId)
if (pose) {
if (!pose.arm_angles) pose.arm_angles = [0, 0, 0, 0, 0, 0]
pose.arm_angles[jointIndex] = parseFloat(value) || 0
// Save to backend
await fetch(API + '/api/models/' + modelId + '/poses/' + poseId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ arm_angles: pose.arm_angles })
})
// Move arm
await fetch(API + '/api/arm/set_angle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ joint: 'J' + (jointIndex + 1), angle: pose.arm_angles[jointIndex] })
})
}
}
},
// === AGV 控制 ===
async refreshAgvPosition() {
if (!this.agvConnected) return
try {
var res = await fetch(API + '/api/agv/position')
var 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 {
var res = await fetch(API + '/api/agv/reset', { method: 'POST' })
var 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)
}
},
// === AMCL 初始化定位 ===
async initAmclPose() {
if (!this.agvConnected) { alert('请先连接AGV'); return }
this.initPoseLoading = true
this.initPoseMsg = ''
try {
var res = await fetch(API + '/api/mission/init_pose', { method: 'POST' })
var data = await res.json()
if (data.ok) {
this.initPoseMsg = data.message || '✅ 已初始化定位'
setTimeout(() => { this.initPoseMsg = '' }, 5000)
await this.refreshNavStatus()
} else {
alert('❌ 初始化失败: ' + (data.error || '未知错误'))
}
} catch (e) {
alert('❌ 请求失败: ' + e.message)
} finally {
this.initPoseLoading = false
}
},
// === 导航到点位 ===
async navigateToPoint() {
if (!this.pointEditor.x && !this.pointEditor.y) {
alert('该点位坐标无效,请先读取或输入坐标'); return
}
if (!this.nav2Available) { alert('Nav2 不可用,请先连接AGV并初始化定位'); return }
if (!confirm(`确定导航到点位 (${this.pointEditor.x.toFixed(2)}, ${this.pointEditor.y.toFixed(2)})`)) return
try {
var res = await fetch(API + '/api/navigate/to', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ x: this.pointEditor.x, y: this.pointEditor.y })
})
var data = await res.json()
if (data.ok) {
alert('✅ 导航已启动,请观察AGV移动')
this.closePointEdit()
this.$nextTick(() => this.refreshNavStatus())
} else {
alert('❌ 导航失败: ' + (data.error || '未知错误'))
}
} catch (e) {
alert('❌ 请求失败: ' + e.message)
}
},
// === Nav2 状态刷新 ===
async refreshNavStatus() {
try {
var res = await fetch(API + '/api/navigate/status')
var data = await res.json()
this.nav2Available = data.nav2_available
this.navStatus = data.status
this.navCurrentPos = data.current_position
} catch (e) {
this.nav2Available = false
}
},
// === 取消导航 ===
async cancelNav() {
if (!this.agvConnected) return
try {
await fetch(API + '/api/navigate/cancel', { method: 'POST' })
await this.refreshNavStatus()
} catch (e) {}
},
}
})
const vm = app.mount('#app')
window.vm = vm // 暴露组件实例