Files
smart-inspection/agv_app/static/js/setting.js
T
2026-05-21 21:28:27 +08:00

922 lines
34 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'],
// 机型(姿态组)
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: '',
}
},
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_pos) {
this.navCurrentPos = data.current_pos
}
}
} catch (e) {}
},
async onMapClick(e) {
if (!this.mapMeta || !this.agvConnected) return
const rect = e.target.getBoundingClientRect()
const px = (e.clientX - rect.left) / rect.width
const py = (e.clientY - rect.top) / rect.height
const { resolution, origin } = this.mapMeta
const wx = origin[0] + px * resolution * this.mapMeta.width
const wy = origin[1] + (1 - py) * resolution * this.mapMeta.height
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(' / ')
},
// === 机型管理 ===
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 || []
}
} 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 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)
}
},
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')