fix: add CSS rotate transform to map-container div (was missing, causing no visual rotation)

This commit is contained in:
ywb
2026-05-17 21:54:22 +08:00
parent 8222ff7bae
commit 4fd124e4bd
2 changed files with 279 additions and 79 deletions
+1 -1
View File
@@ -56,7 +56,7 @@
<button class="btn btn-secondary" style="padding:4px 10px;font-size:12px" @click="resetMapView">重置</button>
</div>
</div>
<div class="map-container" style="position:relative;background:#111;border-radius:8px;overflow:hidden">
<div class="map-container" :style="{ transform: 'rotate(' + mapRotation + 'deg)', transition: 'transform 0.3s ease' }" style="position:relative;background:#111;border-radius:8px;overflow:hidden">
<img :src="mapImageUrl" @error="onMapError" @click="onMapClick" style="width:100%;display:block;cursor:crosshair" title="点击地图导航到该位置">
<div class="map-overlay">
<!-- AGV 实时位置 -->
+278 -78
View File
@@ -8,12 +8,18 @@ createApp({
// 任务配置
missionConfig: { rows: 3, cols: 3, grid: [], machines: [] },
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,
mapRotation: 0,
mapVersion: 0,
navCurrentPos: null,
nav2Available: false,
// 点位
points: [],
newPointName: '',
@@ -24,7 +30,6 @@ createApp({
selectedModelId: null,
newModelName: '',
newModelSerial: '',
poseForm: {},
// 机械臂
armConnected: false,
currentAngles: [],
@@ -45,6 +50,7 @@ createApp({
mounted() {
this.refresh()
this.refreshAngles()
this.nav2Timer = setInterval(this.refreshNavStatus, 3000)
},
watch: {
tab(val) {
@@ -63,6 +69,7 @@ createApp({
beforeUnmount() {
Object.values(this.jogIntervals).forEach(i => clearInterval(i))
if (this.agvCameraTimer) clearInterval(this.agvCameraTimer)
if (this.nav2Timer) clearInterval(this.nav2Timer)
},
methods: {
async refresh() {
@@ -73,7 +80,6 @@ createApp({
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 {
@@ -85,6 +91,8 @@ createApp({
} catch (e) {}
await this.loadAllPoints()
await this.loadAllModels()
await this.loadAllMachines()
await this.loadMissionConfig()
},
// === 地图 ===
async loadMap() {
@@ -108,6 +116,51 @@ createApp({
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
@@ -177,7 +230,6 @@ createApp({
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: '' }
@@ -234,61 +286,55 @@ createApp({
getModel(id) {
return this.models.find(m => m.id === id)
},
// === 机械臂 ===
clearSelection() { this.selectedMachine = null },
async saveMachineCoords() {
if (!this.selectedMachine) return
// === 任务配置 ===
async loadMissionConfig() {
try {
const res = await fetch(API + '/api/mission/machines/' + this.selectedMachine.id, {
method: 'PATCH',
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({
front: this.selectedMachine.front,
back: this.selectedMachine.back
rows: this.missionConfig.rows,
cols: this.missionConfig.cols,
grid: []
})
})
if (res.ok) {
this.mapMsg = '✅ 机器坐标已保存'
setTimeout(() => this.mapMsg = '', 2000)
const data = await res.json()
if (data.ok) {
this.missionConfig.grid = data.config.grid || []
alert('✅ 网格已生成 (' + this.missionConfig.rows + '×' + this.missionConfig.cols + ')')
} else {
alert('保存失败: ' + res.status)
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) }
},
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')
@@ -308,6 +354,189 @@ createApp({
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: [] }
})
})
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]
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
})
})
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 {
@@ -405,34 +634,5 @@ createApp({
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')