Files
smart-inspection/agv_app/static/js/setting.js
T
ywb 9b8e2e233a 点击“设置”的时候,“运行”也处于选中状态。
任务配置->机械臂初始姿态 加入“应用当前姿态”按钮。点击,把机械臂调整为该姿态。
任务配置->网格配置 本来点位行3已经设置好了数据,但现在没有了。
机械臂的IP地址换到了192.168.50.89
2026-05-26 18:23:21 +08:00

1124 lines
41 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 = ''
createApp({
data() {
return {
tab: 'map',
// 任务配置
missionConfig: { rows: 3, cols: 3, grid: [], machines: [] },
selectedMachine: null,
sequence: [],
poseForm: {},
newPoseForm: {},
// 地图
mapForm: { map_dir: '/home/elephant/agv_pro_ros2/src/agv_pro_navigation2/map/', map_file: 'map.yaml' },
mapMsg: '',
mapLoaded: false,
mapImageUrl: '',
mapMeta: null,
mapRotation: 0,
mapVersion: 0,
navCurrentPos: null,
nav2Available: false,
// 点位
points: [],
newPointName: '',
newPointMode: 'front',
newPointSequence: ['front', 'back'],
// 点位编辑器弹窗
editingPoint: null,
pointEditor: { x: 0, y: 0, yaw: 0 },
// 机型(姿态组)
models: [],
selectedModelId: null,
newModelName: '',
newModelSerial: '',
// 机械臂
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,
armCameraTimer: null,
// 机型展开
expandedModelId: null,
showAddModelModal: false,
// QR
qrScanning: false,
qrConfigs: [],
qrScanningId: null,
armCameraUrl: API + '/api/camera/arm_refresh',
newQrName: '',
armInitialPose: [0, 0, 0, 0, 0, 0],
}
},
mounted() {
this.refresh()
this.refreshAngles()
this.loadQrConfigs()
this.nav2Timer = setInterval(this.refreshNavStatus, 3000)
// 机械臂摄像头自动刷新(每2秒)
this.armCameraUrl = API + '/api/camera/arm_refresh?t=' + Date.now()
this.armCameraTimer = setInterval(() => {
this.armCameraUrl = API + '/api/camera/arm_refresh?t=' + Date.now()
}, 2000)
},
computed: {
hasQr() {
return !!(this.selectedMachine && this.selectedMachine.qr)
},
hasQrValue() {
return !!(this.selectedMachine && this.selectedMachine.qr && this.selectedMachine.qr.qr_value)
},
hasQrModelId() {
return !!(this.selectedMachine && this.selectedMachine.qr && this.selectedMachine.qr.model_id)
}
},
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)
if (this.armCameraTimer) { clearInterval(this.armCameraTimer); this.armCameraTimer = null }
if (this.nav2Timer) clearInterval(this.nav2Timer)
},
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 = '❌ 地图图像加载失败'
},
rotateMap(deg) {
this.mapRotation = (this.mapRotation + deg) % 360
},
resetMapView() {
this.mapRotation = 0
this.mapVersion++
},
async refreshNavStatus() {
try {
const res = await fetch(API + '/api/navigate/status')
if (res.ok) {
const data = await res.json()
this.nav2Available = data.nav2_available
if (data.current_position) {
this.navCurrentPos = data.current_position
}
}
} catch (e) {}
},
async onMapClick(e) {
if (!this.mapMeta) {
this.mapMsg = '❌ 地图未加载'
setTimeout(() => { this.mapMsg = '' }, 3000)
return
}
if (!this.agvConnected) {
this.mapMsg = '❌ AGV 未连接,无法导航'
setTimeout(() => { this.mapMsg = '' }, 3000)
return
}
const rect = e.target.getBoundingClientRect()
let px = (e.clientX - rect.left) / rect.width
let py = (e.clientY - rect.top) / rect.height
// 逆旋转补偿:地图 CSS transform: rotate() 后,点击坐标需反向旋转
// 使同一物理点在不同旋转角度下返回相同的世界坐标
const rotation = (this.mapRotation || 0) * Math.PI / 180
if (rotation !== 0) {
const cx = px - 0.5
const cy = py - 0.5
const cos = Math.cos(-rotation)
const sin = Math.sin(-rotation)
px = cx * cos - cy * sin + 0.5
py = cx * sin + cy * cos + 0.5
}
const { resolution, origin } = this.mapMeta
const wx = origin[0] + px * resolution * this.mapMeta.width
const wy = origin[1] + (1 - py) * resolution * this.mapMeta.height
if (!confirm(`是否导航到该坐标?\nX: ${wx.toFixed(3)}\nY: ${wy.toFixed(3)}`)) return
try {
const res = await fetch(API + '/api/navigate/to', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ x: wx, y: wy })
})
const data = await res.json()
if (data.ok) {
this.mapMsg = '✅ 导航目标已发送'
this.mapVersion++
} else {
this.mapMsg = '❌ ' + (data.error || '导航失败')
}
} catch (e) {
this.mapMsg = '❌ 导航请求失败'
}
setTimeout(() => { this.mapMsg = '' }, 3000)
},
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(' / ')
},
// === 点位编辑器弹窗 ===
openPointEdit(ri, ci) {
const point = this.getPointAt(ri, ci)
this.editingPoint = { pointRow: ri, col: ci }
if (point && point.coords && point.coords.length >= 3) {
this.pointEditor = { x: point.coords[0], y: point.coords[1], yaw: point.coords[2] || 0 }
} else {
this.pointEditor = { x: 0, y: 0, yaw: 0 }
}
},
closePointEdit() {
this.editingPoint = null
},
getPointOwnerLabel(pointRow, col) {
const rows = this.missionConfig.rows || 0
if (pointRow === 0) {
return `机器行1·正面`
} else if (pointRow >= rows) {
return `机器行${rows}·背面`
} else {
return `机器行${pointRow}·背面 + 机器行${pointRow+1}·正面`
}
},
async loadPointFromAgv() {
try {
const res = await fetch(API + '/api/agv/position')
const data = await res.json()
if (data.ok && data.position && data.position.length >= 3) {
this.pointEditor.x = data.position[0] || 0
this.pointEditor.y = data.position[1] || 0
this.pointEditor.yaw = data.position[2] || 0
} else {
alert('读取AGV位置失败')
}
} catch (e) { alert('读取AGV位置失败: ' + e.message) }
},
async savePoint() {
if (!this.editingPoint) return
const { pointRow, col } = this.editingPoint
const coords = [this.pointEditor.x, this.pointEditor.y, this.pointEditor.yaw]
const rows = this.missionConfig.rows || 0
// 根据点位行确定 side
const sides = []
if (pointRow === 0) {
sides.push('front')
} else if (pointRow >= rows) {
sides.push('back')
} else {
sides.push('back')
sides.push('front')
}
try {
for (const side of sides) {
const res = await fetch(API + '/api/mission/positions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ row: pointRow, col, side, coords, poses: [] })
})
const data = await res.json()
if (!data.ok) { alert(`保存失败(${side}): ` + (data.error || '')); return }
}
alert('点位已保存')
await this.loadMissionConfig()
this.closePointEdit()
} catch (e) { alert('保存失败: ' + e.message) }
},
async navigateToPoint() {
if (!confirm(`确认导航到该点位?\nX: ${this.pointEditor.x} Y: ${this.pointEditor.y} Yaw: ${this.pointEditor.yaw}`)) return
try {
const 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,
yaw: this.pointEditor.yaw
})
})
const data = await res.json()
if (!data.ok) { alert('导航失败: ' + (data.error || '')) }
} catch (e) { alert('导航失败: ' + e.message) }
},
async goToOrigin() {
if (!confirm('确认导航到原点 (0, 0, 0)?')) return
try {
const res = await fetch(API + '/api/navigate/to', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ x: 0, y: 0, yaw: 0 })
})
const data = await res.json()
if (data.ok) {
this.mapMsg = '✅ 已发送导航到原点'
} else {
this.mapMsg = '❌ ' + (data.error || '导航失败')
}
} catch (e) {
this.mapMsg = '❌ 导航请求失败: ' + e.message
}
setTimeout(() => { this.mapMsg = '' }, 3000)
},
async clearPoint() {
if (!this.editingPoint) return
const { pointRow, col } = this.editingPoint
const rows = this.missionConfig.rows || 0
const sides = pointRow === 0 ? ['front'] : pointRow >= rows ? ['back'] : ['front', 'back']
try {
for (const side of sides) {
await fetch(API + '/api/mission/positions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ row: pointRow, col, side, coords: [0, 0, 0], poses: [] })
})
}
await this.loadMissionConfig()
this.closePointEdit()
} catch (e) { alert('清空失败: ' + e.message) }
},
canClearPoint(pointRow, col) {
const point = this.getPointAt(pointRow, col)
if (!point || !point.coords) return true
return point.coords[0] === 0 && point.coords[1] === 0
},
// === 机型管理 ===
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 || '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, type, name) {
if (!name) name = '姿态' + (((this.getModel(modelId)?.poses?.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 || 'front',
arm_angles: this.currentAngles,
speed: 500,
description: ''
})
})
await this.loadAllModels()
const key = modelId + '_' + (type || 'front')
if (this.newPoseForm[key] !== undefined) this.newPoseForm[key] = ''
},
async deletePose(modelId, poseId) {
if (!confirm('确定删除该姿态?')) return
await fetch(API + '/api/models/' + modelId + '/poses/' + poseId, { method: 'DELETE' })
await this.loadAllModels()
},
async refreshPoseAngles(modelId, poseId) {
if (!this.armConnected) { alert('机械臂未连接'); return }
try {
const res = await fetch(API + '/api/arm/get_angles')
const data = await res.json()
if (data.ok && data.angles) {
const model = this.getModel(modelId)
if (model && model.poses) {
const pose = model.poses.find(p => p.id === poseId)
if (pose) {
// Update local immediately for reactive UI
pose.arm_angles = [...data.angles]
// Persist to backend
await fetch(API + '/api/models/' + modelId + '/poses/' + poseId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ arm_angles: data.angles })
})
await this.loadAllModels()
}
}
}
} catch (e) { alert('刷新角度失败: ' + e.message) }
},
async applyPoseAngles(modelId, poseId) {
const model = this.getModel(modelId)
if (!model || !model.poses) return
const pose = model.poses.find(p => p.id === poseId)
if (!pose || !pose.arm_angles) { alert('无效的姿态数据'); return }
try {
const res = await fetch(API + '/api/arm/set_angles', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ angles: pose.arm_angles, speed: 500 })
})
const data = await res.json()
if (data.ok) { alert('姿态已应用到机械臂') }
else { alert('应用失败: ' + (data.error || '未知错误')) }
} catch (e) { alert('应用姿态失败: ' + e.message) }
},
adjustPoseAngle(modelId, poseId, jointIdx, delta) {
const model = this.getModel(modelId)
if (!model || !model.poses) 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]
if (!pose.arm_angles[jointIdx]) pose.arm_angles[jointIdx] = 0
pose.arm_angles[jointIdx] = Math.round((pose.arm_angles[jointIdx] + delta) * 10) / 10
this.setAngle(jointIdx, pose.arm_angles[jointIdx])
},
async updatePoseAngleAndMove(modelId, poseId, jointIdx, value) {
const model = this.getModel(modelId)
if (!model || !model.poses) 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[jointIdx] = parseFloat(value) || 0
await this.setAngle(jointIdx, pose.arm_angles[jointIdx])
},
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.positions = data.config.positions || []
this.armInitialPose = data.config.arm_initial_pose || [0, 0, 0, 0, 0, 0]
}
} 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,
arm_initial_pose: this.armInitialPose
})
})
const data = await res.json()
if (data.ok) {
alert('✅ 网格配置已保存')
}
} catch (e) { alert('保存失败: ' + e.message) }
},
async saveArmInitialPose() {
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,
arm_initial_pose: this.armInitialPose
})
})
const data = await res.json()
if (data.ok) alert('✅ 机械臂初始姿态已保存')
else alert('❌ 保存失败')
} catch (e) { alert('保存失败: ' + e.message) }
},
async loadArmCurrentAngles() {
if (!this.armConnected) { alert('机械臂未连接'); return }
try {
const res = await fetch(API + '/api/arm/get_angles')
const data = await res.json()
if (data.ok && data.angles) {
this.armInitialPose = [...data.angles]
}
} catch (e) { alert('读取角度失败: ' + e.message) }
},
async applyArmInitialPose() {
if (!this.armConnected) { alert('机械臂未连接'); return }
if (!confirm('确认应用初始姿态到机械臂?')) return
try {
const res = await fetch(API + '/api/arm/set_angles', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ angles: this.armInitialPose, speed: 500 })
})
const data = await res.json()
if (data.ok) alert('✅ 机械臂已移动到初始姿态')
else alert('❌ 应用失败: ' + (data.error || ''))
} catch (e) { alert('应用失败: ' + e.message) }
},
async loadAllMachines() {
try {
const res = await fetch(API + '/api/mission/machines')
const data = await res.json()
this.missionConfig.machines = (data.machines || []).map(m => {
if (!m.front) m.front = { coords: [0,0,0], poses: [] }
if (!m.back) m.back = { coords: [0,0,0], poses: [] }
if (!m.qr) m.qr = { coords: [0,0,0], qr_value: '', model_id: '' }
return m
})
} 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
},
getPointAt(ri, ci) {
if (!this.missionConfig.positions) return null
return this.missionConfig.positions.find(p => p.row === ri && p.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
},
onCellClick(ri, ci) {
const m = this.getMachineAt(ri, ci)
if (!m) {
// 无机器 → 创建机器记录并选中
this.createMachine(ri, ci).then(ok => {
if (ok) {
const created = this.getMachineAt(ri, ci)
if (created) this.selectMachine(created)
}
})
} else {
// 有机器 → 选中
this.selectMachine(m)
}
},
toggleMachine(ri, ci, event) {
if (event.target.checked) {
// 无机器 → 创建机器
this.createMachine(ri, ci)
} else {
// 有机器 → 删除机器
const m = this.getMachineAt(ri, ci)
if (m) this.deleteMachine(m.id)
}
},
async createMachine(ri, ci) {
try {
const machineId = 'm_' + ri + '_' + ci
const 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: [] },
qr: { coords: [0, 0, 0], qr_value: '', model_id: '' }
})
})
const 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]
if (!machine.qr) machine.qr = { coords: [0, 0, 0], qr_value: '', model_id: '' }
else if (!Array.isArray(machine.qr.coords)) machine.qr.coords = [0, 0, 0]
this.selectedMachine = machine
},
clearSelection() {
this.selectedMachine = null
},
async deleteMachine(machineId) {
if (!confirm('确定删除此机器?')) return
try {
await fetch(API + '/api/mission/machines/' + machineId, { method: 'DELETE' })
this.selectedMachine = null
await this.loadAllMachines()
} catch (e) { alert('删除失败: ' + e.message) }
},
async saveMachineCoords() {
if (!this.selectedMachine) return
try {
const 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,
qr: this.selectedMachine.qr || { coords: [0, 0, 0], qr_value: '', model_id: '' }
})
})
if (res.ok) {
this.mapMsg = '✅ 机器坐标已保存'
setTimeout(() => this.mapMsg = '', 2000)
} else {
alert('保存失败: ' + res.status)
}
} catch (e) { alert('保存失败: ' + e.message) }
},
async readPosition(side) {
if (!this.agvConnected) { alert('AGV 未连接'); return }
try {
const res = await fetch(API + '/api/agv/position')
const data = await res.json()
if (data.ok && data.position) {
const [x, y, theta] = data.position
if (side === 'front') {
this.selectedMachine.front.coords = [x, y, theta]
} else {
this.selectedMachine.back.coords = [x, y, theta]
}
} else {
alert('读取位置失败: ' + (data.error || '未知错误'))
}
} catch (e) { alert('读取位置失败: ' + e.message) }
},
async addPoseToMachine(machineId, side) {
const name = this.poseForm.name || '姿态' + (((this.selectedMachine && this.selectedMachine[side] && this.selectedMachine[side].poses) || []).length + 1)
try {
const 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: ''
})
})
const data = await res.json()
if (data.ok) {
this.poseForm.name = ''
await this.loadAllMachines()
// 重新选中当前机器以刷新姿态列表
const 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) {
const 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 }
let machine = this.getMachineAt(ri, ci)
if (!machine) {
try {
const machineId = 'm_' + ri + '_' + ci
const 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 {
const res = await fetch(API + '/api/agv/position')
const pos = await res.json()
let 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 {
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 {
const res = await fetch(API + '/api/mission/generate_sequence')
const data = await res.json()
if (data.ok) {
this.sequence = data.sequence || []
}
} catch (e) { console.error('刷新序列失败', e) }
},
// === 机械臂 ===
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' })
},
// === QR 安全访问器(避免 v-model 在 v-if 内因 Vue 编译器优化导致 undefined 报错)===
machineHasQr(m) {
if (!m || !m.qr || !m.qr.coords || m.qr.coords.length < 2) return false
return m.qr.coords[0] !== 0 || m.qr.coords[1] !== 0
},
qrMarkerStyle(m) {
if (!this.machineHasQr(m)) return { display: 'none' }
return { left: this.getMapX(m.qr.coords) + '%', top: this.getMapY(m.qr.coords) + '%' }
},
qrMarkerTitle(m) {
if (!m || !m.qr) return ''
return 'QR: ' + (m.qr.qr_value || '未扫描')
},
safeQr(key) {
if (!this.selectedMachine || !this.selectedMachine.qr) return ''
return this.selectedMachine.qr[key] ?? ''
},
safeQrCoord(idx) {
if (!this.selectedMachine || !this.selectedMachine.qr || !this.selectedMachine.qr.coords) return 0
return this.selectedMachine.qr.coords[idx] !== undefined ? this.selectedMachine.qr.coords[idx] : 0
},
setQrCoord(idx, val) {
if (this.selectedMachine && this.selectedMachine.qr && this.selectedMachine.qr.coords) {
this.selectedMachine.qr.coords[idx] = val
}
},
safeQrModelName() {
if (!this.selectedMachine || !this.selectedMachine.qr || !this.selectedMachine.qr.model_id) return ''
return this.getModelName(this.selectedMachine.qr.model_id)
},
// === QR 二维码 ===
async readQRPosition() {
if (!this.agvConnected) { alert('AGV 未连接'); return }
try {
const res = await fetch(API + '/api/agv/position')
const data = await res.json()
if (data.ok && data.position) {
const [x, y, theta] = data.position
if (!this.selectedMachine.qr) this.selectedMachine.qr = { coords: [0, 0, 0], qr_value: '', model_id: '' }
this.selectedMachine.qr.coords = [x, y, theta]
} else {
alert('读取位置失败: ' + (data.error || '未知错误'))
}
} catch (e) { alert('读取位置失败: ' + e.message) }
},
async scanQRCode(machineId) {
if (!this.cameraOpened) { alert('AGV 摄像头未打开'); return }
this.qrScanning = true
try {
const res = await fetch(API + '/api/mission/qr_scan/' + machineId, { method: 'POST' })
const data = await res.json()
if (data.ok) {
await this.loadAllMachines()
const updated = this.getMachineAt(this.selectedMachine.row, this.selectedMachine.col)
if (updated) this.selectMachine(updated)
let msg = '✅ 扫描成功: ' + data.qr_value
if (data.model_name) msg += ' → 匹配机型: ' + data.model_name
else msg += ' → 未匹配到机型'
alert(msg)
} else {
alert('❌ ' + (data.error || '扫描失败'))
}
} catch (e) { alert('扫描失败: ' + e.message) }
this.qrScanning = false
},
// ========== 二维码配置(独立 Tab==========
async loadQrConfigs() {
try {
const res = await fetch(API + '/api/qr/configs')
const data = await res.json()
this.qrConfigs = (data.configs || []).map(c => {
// 兼容旧版的 coords → 转为 joint_angles
if (!c.joint_angles || !Array.isArray(c.joint_angles)) {
c.joint_angles = [0, 0, 0, 0, 0, 0]
}
return c
})
} catch (e) { console.error('加载二维码配置失败', e) }
},
async addQrConfig() {
const name = this.newQrName.trim() || ''
try {
const res = await fetch(API + '/api/qr/configs', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ name: name || undefined })
})
const data = await res.json()
if (data.ok) {
this.qrConfigs.push(data.entry)
this.newQrName = ''
}
} catch (e) { alert('添加失败: ' + e.message) }
},
getQrAngle(q, idx) {
if (!q || !q.joint_angles || !Array.isArray(q.joint_angles)) return 0
return q.joint_angles[idx] !== undefined ? q.joint_angles[idx] : 0
},
async updateQrAngle(qrId, idx, val) {
const q = this.qrConfigs.find(x => x.id === qrId)
if (!q) return
if (!q.joint_angles || !Array.isArray(q.joint_angles)) q.joint_angles = [0,0,0,0,0,0]
q.joint_angles[idx] = parseFloat(val) || 0
try {
await fetch(API + '/api/qr/configs/' + qrId, {
method: 'PUT', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ joint_angles: q.joint_angles })
})
} catch (e) { console.error('保存角度失败', e) }
},
async readQrAngles(qrId) {
if (!this.armConnected) { alert('机械臂未连接'); return }
try {
const res = await fetch(API + '/api/qr/configs/' + qrId + '/read-angles', { method: 'POST' })
const data = await res.json()
if (data.ok) {
const q = this.qrConfigs.find(x => x.id === qrId)
if (q && data.joint_angles) {
q.joint_angles = data.joint_angles
}
} else {
alert('读取角度失败: ' + (data.error || '未知错误'))
}
} catch (e) { alert('读取角度失败: ' + e.message) }
},
async saveQrConfig(qrId) {
const q = this.qrConfigs.find(x => x.id === qrId)
if (!q) return
try {
const res = await fetch(API + '/api/qr/configs/' + qrId, {
method: 'PUT', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ name: q.name })
})
if (!res.ok) alert('保存名称失败')
} catch (e) { alert('保存失败: ' + e.message) }
},
async deleteQrConfig(qrId) {
if (!confirm('确定删除此二维码点位?')) return
try {
await fetch(API + '/api/qr/configs/' + qrId, { method: 'DELETE' })
this.qrConfigs = this.qrConfigs.filter(x => x.id !== qrId)
} catch (e) { alert('删除失败: ' + e.message) }
},
getQrModelName(modelId) {
const model = this.models.find(m => m.id === modelId)
return model ? model.name : ''
},
getModelName(modelId) {
const model = this.models.find(m => m.id === modelId)
return model ? model.name : ''
},
async scanQrEntry(qrId) {
this.qrScanningId = qrId
try {
const res = await fetch(API + '/api/qr/scan/' + qrId, { method: 'POST' })
const data = await res.json()
if (data.ok) {
await this.loadQrConfigs()
let msg = '扫描成功: ' + data.qr_value
if (data.model_name) msg += ' 匹配机型: ' + data.model_name
else msg += ' 未匹配到机型'
alert(msg)
} else { alert(data.error || '扫描失败') }
} catch (e) { alert('扫描失败: ' + e.message) }
this.qrScanningId = null
},
async applyQrAngles(qrId) {
if (!this.armConnected) { alert('机械臂未连接'); return }
const q = this.qrConfigs.find(x => x.id === qrId)
if (!q || !q.joint_angles || !Array.isArray(q.joint_angles)) { alert('无效的姿态数据'); return }
try {
const res = await fetch(API + '/api/arm/set_angles', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ angles: q.joint_angles, speed: 500 })
})
const data = await res.json()
if (data.ok) { alert('姿态已应用到机械臂')
} else { alert('应用失败: ' + (data.error || '未知错误')) }
} catch (e) { alert('应用姿态失败: ' + e.message) }
},
onArmPreviewError() {
// 机械臂摄像头预览失败,静默处理
},
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)
}
},
}
}).mount('#app')