This commit is contained in:
ywb
2026-05-20 22:15:03 +08:00
parent a5be69bfaa
commit a5aaddc4fc
3 changed files with 155 additions and 23 deletions
+75 -9
View File
@@ -9,7 +9,8 @@ createApp({
missionConfig: { rows: 3, cols: 3, grid: [], machines: [] }, missionConfig: { rows: 3, cols: 3, grid: [], machines: [] },
selectedMachine: null, selectedMachine: null,
sequence: [], sequence: [],
poseForm: { name: '', photo_type: 'front', description: '' }, poseForm: {},
newPoseForm: {},
// 地图 // 地图
mapForm: { map_dir: '/home/elephant/agv_pro_ros2/src/agv_pro_navigation2/map/', map_file: 'map.yaml' }, mapForm: { map_dir: '/home/elephant/agv_pro_ros2/src/agv_pro_navigation2/map/', map_file: 'map.yaml' },
mapMsg: '', mapMsg: '',
@@ -45,6 +46,7 @@ createApp({
agvMoveInterval: null, agvMoveInterval: null,
agvCameraUrl: API + '/api/camera/refresh', agvCameraUrl: API + '/api/camera/refresh',
agvCameraTimer: null, agvCameraTimer: null,
armCameraTimer: null,
// 机型展开 // 机型展开
expandedModelId: null, expandedModelId: null,
showAddModelModal: false, showAddModelModal: false,
@@ -61,6 +63,11 @@ createApp({
this.refreshAngles() this.refreshAngles()
this.loadQrConfigs() this.loadQrConfigs()
this.nav2Timer = setInterval(this.refreshNavStatus, 3000) 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: { computed: {
hasQr() { hasQr() {
@@ -90,6 +97,7 @@ createApp({
beforeUnmount() { beforeUnmount() {
Object.values(this.jogIntervals).forEach(i => clearInterval(i)) Object.values(this.jogIntervals).forEach(i => clearInterval(i))
if (this.agvCameraTimer) clearInterval(this.agvCameraTimer) if (this.agvCameraTimer) clearInterval(this.agvCameraTimer)
if (this.armCameraTimer) { clearInterval(this.armCameraTimer); this.armCameraTimer = null }
if (this.nav2Timer) clearInterval(this.nav2Timer) if (this.nav2Timer) clearInterval(this.nav2Timer)
}, },
methods: { methods: {
@@ -280,30 +288,88 @@ createApp({
await this.loadAllModels() await this.loadAllModels()
}, },
// === 姿态管理(属于机型)=== // === 姿态管理(属于机型)===
async addPose(modelId) { async addPose(modelId, type, name) {
const form = this.poseForm[modelId] if (!name) name = '姿态' + (((this.getModel(modelId)?.poses?.length) || 0) + 1)
if (!form) return
await fetch(API + '/api/models/poses/add', { await fetch(API + '/api/models/poses/add', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
model_id: modelId, model_id: modelId,
name: form.name || '姿态' + ((this.getModel(modelId)?.poses?.length || 0) + 1), name: name,
photo_type: form.photo_type, photo_type: type || 'front',
arm_angles: this.currentAngles, arm_angles: this.currentAngles,
speed: 500, speed: 500,
description: form.description || '' description: ''
}) })
}) })
await this.loadAllModels() await this.loadAllModels()
form.name = '' const key = modelId + '_' + (type || 'front')
form.description = '' if (this.newPoseForm[key] !== undefined) this.newPoseForm[key] = ''
}, },
async deletePose(modelId, poseId) { async deletePose(modelId, poseId) {
if (!confirm('确定删除该姿态?')) return if (!confirm('确定删除该姿态?')) return
await fetch(API + '/api/models/' + modelId + '/poses/' + poseId, { method: 'DELETE' }) await fetch(API + '/api/models/' + modelId + '/poses/' + poseId, { method: 'DELETE' })
await this.loadAllModels() 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: 50 })
})
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) { getModel(id) {
return this.models.find(m => m.id === id) return this.models.find(m => m.id === id)
}, },
+75 -9
View File
@@ -9,7 +9,8 @@ createApp({
missionConfig: { rows: 3, cols: 3, grid: [], machines: [] }, missionConfig: { rows: 3, cols: 3, grid: [], machines: [] },
selectedMachine: null, selectedMachine: null,
sequence: [], sequence: [],
poseForm: { name: '', photo_type: 'front', description: '' }, poseForm: {},
newPoseForm: {},
// 地图 // 地图
mapForm: { map_dir: '/home/elephant/agv_pro_ros2/src/agv_pro_navigation2/map/', map_file: 'map.yaml' }, mapForm: { map_dir: '/home/elephant/agv_pro_ros2/src/agv_pro_navigation2/map/', map_file: 'map.yaml' },
mapMsg: '', mapMsg: '',
@@ -45,6 +46,7 @@ createApp({
agvMoveInterval: null, agvMoveInterval: null,
agvCameraUrl: API + '/api/camera/refresh', agvCameraUrl: API + '/api/camera/refresh',
agvCameraTimer: null, agvCameraTimer: null,
armCameraTimer: null,
// 机型展开 // 机型展开
expandedModelId: null, expandedModelId: null,
showAddModelModal: false, showAddModelModal: false,
@@ -61,6 +63,11 @@ createApp({
this.refreshAngles() this.refreshAngles()
this.loadQrConfigs() this.loadQrConfigs()
this.nav2Timer = setInterval(this.refreshNavStatus, 3000) 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: { computed: {
hasQr() { hasQr() {
@@ -90,6 +97,7 @@ createApp({
beforeUnmount() { beforeUnmount() {
Object.values(this.jogIntervals).forEach(i => clearInterval(i)) Object.values(this.jogIntervals).forEach(i => clearInterval(i))
if (this.agvCameraTimer) clearInterval(this.agvCameraTimer) if (this.agvCameraTimer) clearInterval(this.agvCameraTimer)
if (this.armCameraTimer) { clearInterval(this.armCameraTimer); this.armCameraTimer = null }
if (this.nav2Timer) clearInterval(this.nav2Timer) if (this.nav2Timer) clearInterval(this.nav2Timer)
}, },
methods: { methods: {
@@ -280,30 +288,88 @@ createApp({
await this.loadAllModels() await this.loadAllModels()
}, },
// === 姿态管理(属于机型)=== // === 姿态管理(属于机型)===
async addPose(modelId) { async addPose(modelId, type, name) {
const form = this.poseForm[modelId] if (!name) name = '姿态' + (((this.getModel(modelId)?.poses?.length) || 0) + 1)
if (!form) return
await fetch(API + '/api/models/poses/add', { await fetch(API + '/api/models/poses/add', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
model_id: modelId, model_id: modelId,
name: form.name || '姿态' + ((this.getModel(modelId)?.poses?.length || 0) + 1), name: name,
photo_type: form.photo_type, photo_type: type || 'front',
arm_angles: this.currentAngles, arm_angles: this.currentAngles,
speed: 500, speed: 500,
description: form.description || '' description: ''
}) })
}) })
await this.loadAllModels() await this.loadAllModels()
form.name = '' const key = modelId + '_' + (type || 'front')
form.description = '' if (this.newPoseForm[key] !== undefined) this.newPoseForm[key] = ''
}, },
async deletePose(modelId, poseId) { async deletePose(modelId, poseId) {
if (!confirm('确定删除该姿态?')) return if (!confirm('确定删除该姿态?')) return
await fetch(API + '/api/models/' + modelId + '/poses/' + poseId, { method: 'DELETE' }) await fetch(API + '/api/models/' + modelId + '/poses/' + poseId, { method: 'DELETE' })
await this.loadAllModels() 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: 50 })
})
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) { getModel(id) {
return this.models.find(m => m.id === id) return this.models.find(m => m.id === id)
}, },
+5 -5
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>设置 - AGV 拍摄系统</title> <title>设置 - AGV 拍摄系统</title>
<link rel="stylesheet" href="/static/css/style.css?v=20260520h"> <link rel="stylesheet" href="/static/css/style.css?v=20260520i">
</head> </head>
<body> <body>
<div id="app"> <div id="app">
@@ -135,7 +135,7 @@
<!-- 正面姿态 --> <!-- 正面姿态 -->
<div style="padding:16px;background:#0f1923"> <div style="padding:16px;background:#0f1923">
<h4 style="margin:0 0 12px 0;color:#388e3c">🟢 正面姿态</h4> <h4 style="margin:0 0 12px 0;color:#388e3c">🟢 正面姿态</h4>
<div v-for="pose in m.poses.filter(p => p.photo_type === 'front')" :key="pose.id" style="background:#0f1923;padding:12px;border:1px solid #2a3441;border-radius:6px;margin-bottom:8px"> <div v-for="pose in (m.poses || []).filter(p => p.photo_type === 'front')" :key="pose.id" style="background:#0f1923;padding:12px;border:1px solid #2a3441;border-radius:6px;margin-bottom:8px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px"> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<strong>{% raw %}{{ pose.name || '正面姿态' }}{% endraw %}</strong> <strong>{% raw %}{{ pose.name || '正面姿态' }}{% endraw %}</strong>
<button class="btn btn-danger btn-small" @click="deletePose(m.id, pose.id)">删除</button> <button class="btn btn-danger btn-small" @click="deletePose(m.id, pose.id)">删除</button>
@@ -182,7 +182,7 @@
<!-- 背面姿态 --> <!-- 背面姿态 -->
<div style="padding:16px;background:#0d1420"> <div style="padding:16px;background:#0d1420">
<h4 style="margin:0 0 12px 0;color:#d32f2f">🔴 背面姿态</h4> <h4 style="margin:0 0 12px 0;color:#d32f2f">🔴 背面姿态</h4>
<div v-for="pose in m.poses.filter(p => p.photo_type === 'back')" :key="pose.id" style="background:#0f1923;padding:12px;border:1px solid #2a3441;border-radius:6px;margin-bottom:8px"> <div v-for="pose in (m.poses || []).filter(p => p.photo_type === 'back')" :key="pose.id" style="background:#0f1923;padding:12px;border:1px solid #2a3441;border-radius:6px;margin-bottom:8px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px"> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<strong>{% raw %}{{ pose.name || '背面姿态' }}{% endraw %}</strong> <strong>{% raw %}{{ pose.name || '背面姿态' }}{% endraw %}</strong>
<button class="btn btn-danger btn-small" @click="deletePose(m.id, pose.id)">删除</button> <button class="btn btn-danger btn-small" @click="deletePose(m.id, pose.id)">删除</button>
@@ -676,7 +676,7 @@
</main> </main>
</div> </div>
<script src="/static/js/vue3.global.prod.js?v=20260520h"></script> <script src="/static/js/vue3.global.prod.js?v=20260520i"></script>
<script src="/static/js/setting.js?v=20260520h"></script> <script src="/static/js/setting.js?v=20260520i"></script>
</body> </body>
</html> </html>