927 lines
28 KiB
Vue
927 lines
28 KiB
Vue
<template>
|
|
<div class="robot-teach-container">
|
|
<!-- 主体内容区 -->
|
|
<div class="main-wrap">
|
|
<!-- 左侧数据面板 -->
|
|
<div class="left-panel" :class="{ 'left-panel--dual-arm': isExplanationRobot }">
|
|
<!-- TCP 笛卡尔坐标 -->
|
|
<div class="panel-card">
|
|
<div class="row dual-row">
|
|
<div class="coord-item">
|
|
<span class="key">X</span>
|
|
<!-- <el-input v-model.number="tcp.x" suffix="mm" /> -->
|
|
<div class="number">{{ tcp.x.toFixed(3) }}</div>
|
|
</div>
|
|
<div class="coord-item">
|
|
<span class="key">RX</span>
|
|
<!-- <el-input v-model.number="tcp.rx" suffix="°" /> -->
|
|
<div class="number">{{ tcp.rx.toFixed(3) }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="row dual-row">
|
|
<div class="coord-item">
|
|
<span class="key">Y</span>
|
|
<!-- <el-input v-model.number="tcp.y" suffix="mm" /> -->
|
|
<div class="number">{{ tcp.y.toFixed(3) }}</div>
|
|
</div>
|
|
<div class="coord-item">
|
|
<span class="key">RY</span>
|
|
<div class="number">{{ tcp.ry.toFixed(3) }}</div>
|
|
<!-- <el-input v-model.number="tcp.ry" suffix="°" /> -->
|
|
</div>
|
|
</div>
|
|
<div class="row dual-row">
|
|
<div class="coord-item">
|
|
<span class="key">Z</span>
|
|
<!-- <el-input v-model.number="tcp.z" suffix="mm" /> -->
|
|
<div class="number">{{ tcp.z.toFixed(3) }}</div>
|
|
</div>
|
|
<div class="coord-item">
|
|
<span class="key">RZ</span>
|
|
<div class="number">{{ tcp.rz.toFixed(3) }}</div>
|
|
<!-- <el-input v-model.number="tcp.rz" suffix="°" /> -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="!isExplanationRobot" class="joint-list">
|
|
<div class="title">关节控制</div>
|
|
<div v-for="joint in jointControls" :key="joint.name" class="joint-item">
|
|
<label :for="joint.name">{{ joint.name }}</label>
|
|
<el-input v-model="joint.value" readonly>
|
|
<template #prepend>
|
|
<el-button class="press-control" @pointerdown="startPointerControl($event, () => updateJoint(joint.name, '-'))"
|
|
@pointerup="endPointerControl" @pointercancel="endPointerControl">
|
|
-
|
|
</el-button>
|
|
</template>
|
|
<template #append>
|
|
<el-button class="press-control" @pointerdown="startPointerControl($event, () => updateJoint(joint.name, '+'))"
|
|
@pointerup="endPointerControl" @pointercancel="endPointerControl">
|
|
+
|
|
</el-button>
|
|
</template>
|
|
</el-input>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="joint-list joint-list--dual-arm">
|
|
<div class="title">关节控制</div>
|
|
<div class="arm-columns">
|
|
<div v-for="arm in armJointGroups" :key="arm.key" class="arm-column">
|
|
<div class="arm-title">{{ arm.title }}</div>
|
|
<div v-for="joint in arm.joints" :key="joint.name" class="joint-item">
|
|
<label :for="joint.name">{{ joint.name }}</label>
|
|
<el-input v-model="joint.value" readonly>
|
|
<template #prepend>
|
|
<el-button class="press-control"
|
|
@pointerdown="startPointerControl($event, () => updateJoint(joint.name, '-'))"
|
|
@pointerup="endPointerControl" @pointercancel="endPointerControl">
|
|
-
|
|
</el-button>
|
|
</template>
|
|
<template #append>
|
|
<el-button class="press-control"
|
|
@pointerdown="startPointerControl($event, () => updateJoint(joint.name, '+'))"
|
|
@pointerup="endPointerControl" @pointercancel="endPointerControl">
|
|
+
|
|
</el-button>
|
|
</template>
|
|
</el-input>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="speed-container">
|
|
<div class="label">速度:</div>
|
|
<div class="speed-slider" @mouseenter="hoveredSlider = 'joint'" @mouseleave="hoveredSlider = null">
|
|
<el-slider v-model="jointSpeed" :min="0.01" :step="0.01" :max="1" :show-tooltip="false" />
|
|
<span v-show="hoveredSlider === 'joint'" class="speed-tooltip"
|
|
:style="getTooltipPosition(jointSpeed)">{{ formatSpeedTooltip(jointSpeed) }}</span>
|
|
</div>
|
|
<div class="proportion">{{ (jointSpeed / 1 * 100).toFixed(0) }} %</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 中间3D机械臂空白区域 -->
|
|
<div class="center-view">
|
|
<UrdfView ref="urdfViewerRef" :modelPath="modelPath"/>
|
|
<!-- 底部功能按钮 -->
|
|
<!-- <div class="center-bottom-btns">
|
|
<el-button type="success" size="large">零力示教</el-button>
|
|
<el-button size="large">回到原点</el-button>
|
|
<el-button size="large">Z轴对齐</el-button>
|
|
</div> -->
|
|
</div>
|
|
|
|
<!-- 右侧摇杆操作面板 -->
|
|
<div class="right-panel">
|
|
<div class="controller">
|
|
<div class="title">末端控制</div>
|
|
<div v-if="isExplanationRobot" class="arm-selector">
|
|
<div class="arm-selector__label">控制机械臂</div>
|
|
<el-radio-group v-model="selectedEndArm" class="arm-selector__options">
|
|
<el-radio-button value="left">左臂</el-radio-button>
|
|
<el-radio-button value="right">右臂</el-radio-button>
|
|
</el-radio-group>
|
|
</div>
|
|
<div class="axis-group">
|
|
<!-- X Axis -->
|
|
<div class="axis-row x">
|
|
<div class="axis-label x">X</div>
|
|
<div class="axis-slider-wrap">
|
|
<div class="axis-name">前后平移</div>
|
|
<div class="axis-control-buttons">
|
|
<el-button class="press-control" type="primary" circle @pointerdown="startPointerControl($event, () => move('x', '-', 'end'))"
|
|
@pointerup="endPointerControl" @pointercancel="endPointerControl">
|
|
<el-icon size="32">
|
|
<Remove />
|
|
</el-icon>
|
|
</el-button>
|
|
<el-button class="press-control" type="primary" circle @pointerdown="startPointerControl($event, () => move('x', '+', 'end'))"
|
|
@pointerup="endPointerControl" @pointercancel="endPointerControl">
|
|
<el-icon size="32">
|
|
<CirclePlus />
|
|
</el-icon>
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Y Axis -->
|
|
<div class="axis-row y">
|
|
<div class="axis-label y">Y</div>
|
|
<div class="axis-slider-wrap">
|
|
<div class="axis-name">左右平移</div>
|
|
<div class="axis-control-buttons">
|
|
<el-button class="press-control" type="primary" circle @pointerdown="startPointerControl($event, () => move('y', '-', 'end'))"
|
|
@pointerup="endPointerControl" @pointercancel="endPointerControl">
|
|
<el-icon size="32">
|
|
<Remove />
|
|
</el-icon>
|
|
</el-button>
|
|
<el-button class="press-control" type="primary" circle @pointerdown="startPointerControl($event, () => move('y', '+', 'end'))"
|
|
@pointerup="endPointerControl" @pointercancel="endPointerControl">
|
|
<el-icon size="32">
|
|
<CirclePlus />
|
|
</el-icon>
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Z Axis -->
|
|
<div class="axis-row z">
|
|
<div class="axis-label z">Z</div>
|
|
<div class="axis-slider-wrap">
|
|
<div class="axis-name">上下平移</div>
|
|
<div class="axis-control-buttons">
|
|
<el-button class="press-control" type="primary" circle @pointerdown="startPointerControl($event, () => move('z', '-', 'end'))"
|
|
@pointerup="endPointerControl" @pointercancel="endPointerControl">
|
|
<el-icon size="32">
|
|
<Remove />
|
|
</el-icon>
|
|
</el-button>
|
|
<el-button class="press-control" type="primary" circle @pointerdown="startPointerControl($event, () => move('z', '+', 'end'))"
|
|
@pointerup="endPointerControl" @pointercancel="endPointerControl">
|
|
<el-icon size="32">
|
|
<CirclePlus />
|
|
</el-icon>
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="speed-container">
|
|
<div class="label">速度:</div>
|
|
<div class="speed-slider" @mouseenter="hoveredSlider = 'end'" @mouseleave="hoveredSlider = null">
|
|
<el-slider v-model="endSpeed" :min="0.01" :step="0.01" :max="1" :show-tooltip="false" />
|
|
<span v-show="hoveredSlider === 'end'" class="speed-tooltip"
|
|
:style="getTooltipPosition(endSpeed)">{{ formatSpeedTooltip(endSpeed) }}</span>
|
|
</div>
|
|
<div class="proportion">{{ (endSpeed / 1 * 100).toFixed(0) }} %</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="controller">
|
|
<div class="title">姿态控制</div>
|
|
<div v-if="isExplanationRobot" class="arm-selector">
|
|
<div class="arm-selector__label">控制机械臂</div>
|
|
<el-radio-group v-model="selectedPostureArm" class="arm-selector__options">
|
|
<el-radio-button value="left">左臂</el-radio-button>
|
|
<el-radio-button value="right">右臂</el-radio-button>
|
|
</el-radio-group>
|
|
</div>
|
|
<div class="axis-group">
|
|
<!-- X Axis -->
|
|
<div class="axis-row x">
|
|
<div class="axis-label x">X</div>
|
|
<div class="axis-slider-wrap">
|
|
<div class="axis-name">前后平移</div>
|
|
<div class="axis-control-buttons">
|
|
<el-button class="press-control" type="primary" circle @pointerdown="startPointerControl($event, () => move('x', '-', 'posture'))"
|
|
@pointerup="endPointerControl" @pointercancel="endPointerControl">
|
|
<el-icon size="32">
|
|
<Remove />
|
|
</el-icon>
|
|
</el-button>
|
|
<el-button class="press-control" type="primary" circle @pointerdown="startPointerControl($event, () => move('x', '+', 'posture'))"
|
|
@pointerup="endPointerControl" @pointercancel="endPointerControl">
|
|
<el-icon size="32">
|
|
<CirclePlus />
|
|
</el-icon>
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Y Axis -->
|
|
<div class="axis-row y">
|
|
<div class="axis-label y">Y</div>
|
|
<div class="axis-slider-wrap">
|
|
<div class="axis-name">左右平移</div>
|
|
<div class="axis-control-buttons">
|
|
<el-button class="press-control" type="primary" circle @pointerdown="startPointerControl($event, () => move('y', '-', 'posture'))"
|
|
@pointerup="endPointerControl" @pointercancel="endPointerControl">
|
|
<el-icon size="32">
|
|
<Remove />
|
|
</el-icon>
|
|
</el-button>
|
|
<el-button class="press-control" type="primary" circle @pointerdown="startPointerControl($event, () => move('y', '+', 'posture'))"
|
|
@pointerup="endPointerControl" @pointercancel="endPointerControl">
|
|
<el-icon size="32">
|
|
<CirclePlus />
|
|
</el-icon>
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Z Axis -->
|
|
<div class="axis-row z">
|
|
<div class="axis-label z">Z</div>
|
|
<div class="axis-slider-wrap">
|
|
<div class="axis-name">上下平移</div>
|
|
<div class="axis-control-buttons">
|
|
<el-button class="press-control" type="primary" circle @pointerdown="startPointerControl($event, () => move('z', '-', 'posture'))"
|
|
@pointerup="endPointerControl" @pointercancel="endPointerControl">
|
|
<el-icon size="32">
|
|
<Remove />
|
|
</el-icon>
|
|
</el-button>
|
|
<el-button class="press-control" type="primary" circle @pointerdown="startPointerControl($event, () => move('z', '+', 'posture'))"
|
|
@pointerup="endPointerControl" @pointercancel="endPointerControl">
|
|
<el-icon size="32">
|
|
<CirclePlus />
|
|
</el-icon>
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="speed-container">
|
|
<div class="label">速度:</div>
|
|
<div class="speed-slider" @mouseenter="hoveredSlider = 'posture'" @mouseleave="hoveredSlider = null">
|
|
<el-slider v-model="postureSpeed" :min="0.01" :step="0.01" :max="1" :show-tooltip="false" />
|
|
<span v-show="hoveredSlider === 'posture'" class="speed-tooltip"
|
|
:style="getTooltipPosition(postureSpeed)">{{ formatSpeedTooltip(postureSpeed) }}</span>
|
|
</div>
|
|
<div class="proportion">{{ (postureSpeed / 1 * 100).toFixed(0) }} %</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
|
|
import UrdfView from './UrdfView.vue'
|
|
import { getPoseApi, getJointStateApi, moveJApi, torqueOnApi, speedJApi, stopMotionApi, speedLApi } from '@/api/arm'
|
|
import { Remove, CirclePlus } from '@element-plus/icons-vue'
|
|
import { useRobotStore } from '@/stores/robot'
|
|
|
|
const robotStore = useRobotStore()
|
|
|
|
const urdfViewerRef = ref()
|
|
|
|
const modelPath = computed(() => {
|
|
if (isExplanationRobot.value) {
|
|
return 'upper_body/robot.urdf'
|
|
} else {
|
|
return '/inspection/elfin10/elfin10.urdf.xacro'
|
|
}
|
|
})
|
|
|
|
const jointControls = ref([])
|
|
const isExplanationRobot = computed(() => robotStore.robotType === 'explanation')
|
|
const selectedEndArm = ref('left')
|
|
const selectedPostureArm = ref('left')
|
|
const armJointGroups = computed(() => [
|
|
{
|
|
key: 'left',
|
|
title: '左臂',
|
|
joints: jointControls.value.filter(joint => /^left_arm_/i.test(joint.name))
|
|
},
|
|
{
|
|
key: 'right',
|
|
title: '右臂',
|
|
joints: jointControls.value.filter(joint => /^right_arm_/i.test(joint.name))
|
|
}
|
|
])
|
|
|
|
// TCP坐标数据
|
|
const tcp = ref({
|
|
x: 46.691,
|
|
y: 385.321,
|
|
z: 141.877,
|
|
rx: -174.595,
|
|
ry: 4.212,
|
|
rz: -72.937
|
|
})
|
|
|
|
const torqueOn = async () => {
|
|
await torqueOnApi()
|
|
}
|
|
|
|
const getPose = async () => {
|
|
const res = await getPoseApi()
|
|
if (res.code === 200) {
|
|
const { x, y, z, rx, ry, rz } = res.data
|
|
tcp.value = { x, y, z, rx, ry, rz }
|
|
}
|
|
}
|
|
|
|
const jointNamsMap = {
|
|
'joint_1': 'elfin_joint1',
|
|
'joint_2': 'elfin_joint2',
|
|
'joint_3': 'elfin_joint3',
|
|
'joint_4': 'elfin_joint4',
|
|
'joint_5': 'elfin_joint5',
|
|
'joint_6': 'elfin_joint6',
|
|
}
|
|
|
|
/**
|
|
* 获取关节状态
|
|
* @param robot
|
|
*/
|
|
const getJointState = async (robot) => {
|
|
try {
|
|
const res = await getJointStateApi()
|
|
if (res.code === 200) {
|
|
setTimeout(() => {
|
|
const names = res.data.name
|
|
const positions = res.data.position
|
|
names.forEach((name, index) => {
|
|
urdfViewerRef.value.updateJoint(jointNamsMap[name] || name, positions[index].toFixed(3))
|
|
})
|
|
}, 500)
|
|
} else {
|
|
console.error('获取关节状态失败:', res.message)
|
|
}
|
|
} catch (error) {
|
|
console.error('获取关节状态失败:', error)
|
|
}
|
|
}
|
|
|
|
// 速度
|
|
const jointSpeed = ref(0.8)
|
|
const updateJoint = async (jointName, _dir) => {
|
|
const velocities = jointControls.value.map(joint => {
|
|
return 0
|
|
})
|
|
const index = jointControls.value.findIndex(joint => joint.name === jointName)
|
|
velocities[index] = jointSpeed.value * (_dir === '+' ? 1 : -1)
|
|
const res = await speedJApi({
|
|
acceleration: jointSpeed.value * (_dir === '+' ? 1 : -1) + 0.1,
|
|
duration: 60,
|
|
velocities: velocities
|
|
})
|
|
}
|
|
|
|
const postureSpeed = ref(0.8)
|
|
const endSpeed = ref(0.8)
|
|
const hoveredSlider = ref(null)
|
|
|
|
const getTooltipPosition = (value) => ({
|
|
left: `${((value - 0.01) / 0.99) * 100}%`
|
|
})
|
|
|
|
const formatSpeedTooltip = (value) => Number(value).toFixed(2)
|
|
|
|
const speedData = reactive({
|
|
vx: 0,
|
|
vy: 0,
|
|
vz: 0,
|
|
wx: 0,
|
|
wy: 0,
|
|
wz: 0
|
|
})
|
|
|
|
/**
|
|
* 机械臂移动
|
|
* @param axis 坐标轴
|
|
* @param direction 方向
|
|
*/
|
|
const move = async (axis, direction, type) => {
|
|
speedData.vx = 0
|
|
speedData.vy = 0
|
|
speedData.vz = 0
|
|
speedData.wx = 0
|
|
speedData.wy = 0
|
|
speedData.wz = 0
|
|
if (type === 'end') {
|
|
// 末端移动
|
|
const speedValue = endSpeed.value
|
|
switch (axis) {
|
|
case 'x':
|
|
speedData.vx = direction === '+' ? speedValue : -speedValue
|
|
break
|
|
case 'y':
|
|
speedData.vy = direction === '+' ? speedValue : -speedValue
|
|
break
|
|
case 'z':
|
|
speedData.vz = direction === '+' ? speedValue : -speedValue
|
|
break
|
|
}
|
|
} else if (type === 'posture') {
|
|
// 关节移动
|
|
const jointSpeed = postureSpeed.value
|
|
switch (axis) {
|
|
case 'x':
|
|
speedData.wx = direction === '+' ? jointSpeed : -jointSpeed
|
|
break
|
|
case 'y':
|
|
speedData.wy = direction === '+' ? jointSpeed : -jointSpeed
|
|
break
|
|
case 'z':
|
|
speedData.wz = direction === '+' ? jointSpeed : -jointSpeed
|
|
break
|
|
}
|
|
|
|
}
|
|
|
|
speedLApi({
|
|
duration: 60,
|
|
acceleration: type === 'posture' ? postureSpeed.value + 0.1 : endSpeed.value + 0.1,
|
|
velocity: {
|
|
...speedData
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 停止机械臂移动
|
|
*/
|
|
const stop = async () => {
|
|
await stopMotionApi()
|
|
}
|
|
|
|
const activePointerIds = new Set()
|
|
|
|
const startPointerControl = (event, action) => {
|
|
if (event.pointerType === 'mouse' && event.button !== 0) return
|
|
|
|
event.preventDefault()
|
|
activePointerIds.add(event.pointerId)
|
|
event.currentTarget?.setPointerCapture?.(event.pointerId)
|
|
action()
|
|
}
|
|
|
|
const endPointerControl = (event) => {
|
|
event.preventDefault()
|
|
if (!activePointerIds.has(event.pointerId)) return
|
|
|
|
activePointerIds.delete(event.pointerId)
|
|
if (event.currentTarget?.hasPointerCapture?.(event.pointerId)) {
|
|
event.currentTarget.releasePointerCapture(event.pointerId)
|
|
}
|
|
stop()
|
|
}
|
|
|
|
const stopActiveControls = () => {
|
|
if (!activePointerIds.size) return
|
|
activePointerIds.clear()
|
|
stop()
|
|
}
|
|
|
|
const timer = ref(null)
|
|
onMounted(() => {
|
|
window.addEventListener('blur', stopActiveControls)
|
|
nextTick(() => {
|
|
jointControls.value = urdfViewerRef.value.jointControls
|
|
torqueOn()
|
|
getPose()
|
|
if (timer.value) {
|
|
clearInterval(timer.value)
|
|
}
|
|
timer.value = setInterval(() => {
|
|
getPose()
|
|
getJointState()
|
|
}, 100)
|
|
})
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('blur', stopActiveControls)
|
|
stopActiveControls()
|
|
if (timer.value) {
|
|
clearInterval(timer.value)
|
|
}
|
|
})
|
|
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.robot-teach-container {
|
|
height: 100%;
|
|
background: $color-background-base;
|
|
color: $color-text-primary;
|
|
font-family: $font-family-body;
|
|
overflow: hidden;
|
|
user-select: none;
|
|
|
|
.press-control {
|
|
touch-action: none;
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
min-width: 48px;
|
|
min-height: 48px;
|
|
font-size: 22px;
|
|
}
|
|
|
|
// 主体三栏布局
|
|
.main-wrap {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
overflow: hidden;
|
|
|
|
// 左侧面板
|
|
.left-panel {
|
|
width: 400px;
|
|
flex-shrink: 0;
|
|
padding: 20px;
|
|
overflow-y: auto;
|
|
background: $color-background-panel;
|
|
border-right: 1px solid $color-accent-border;
|
|
|
|
&--dual-arm {
|
|
width: 640px;
|
|
}
|
|
|
|
.panel-card {
|
|
padding: 12px;
|
|
|
|
.row {
|
|
display: flex;
|
|
gap: 20px;
|
|
margin-bottom: 12px;
|
|
|
|
.coord-item {
|
|
display: flex;
|
|
flex: 1;
|
|
align-items: center;
|
|
gap: 8px;
|
|
min-width: 0;
|
|
|
|
.key {
|
|
width: 26px;
|
|
flex-shrink: 0;
|
|
}
|
|
}
|
|
|
|
.number {
|
|
flex: 1;
|
|
min-width: 0;
|
|
border-radius: 5px;
|
|
padding: 6px;
|
|
background: $color-background-card;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
}
|
|
}
|
|
|
|
.joint-list {
|
|
margin-top: 24px;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
color: $color-text-primary;
|
|
|
|
.title {
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.arm-columns {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 16px;
|
|
}
|
|
|
|
.arm-title {
|
|
margin-bottom: 10px;
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
}
|
|
|
|
|
|
.joint-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 10px;
|
|
padding: 10px;
|
|
border: 1px solid $color-accent-border;
|
|
border-radius: 5px;
|
|
background: $color-background-base;
|
|
|
|
&:hover {
|
|
border-color: $color-accent-border-strong;
|
|
background: $color-background-card-hover;
|
|
}
|
|
|
|
label {
|
|
width: 120px;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
input[type=range] {
|
|
flex: 1;
|
|
}
|
|
|
|
.angle {
|
|
width: 50px;
|
|
text-align: right;
|
|
}
|
|
|
|
:deep(.el-input-group__prepend) {
|
|
background: $color-background-base;
|
|
border-right: 0px;
|
|
box-shadow: 0 0 0 1px $color-accent-border inset;
|
|
}
|
|
|
|
:deep(.el-input-group__append) {
|
|
background: $color-background-base;
|
|
border-left: 0px;
|
|
box-shadow: 0 0 0 1px $color-accent-border inset;
|
|
}
|
|
|
|
:deep(.el-input__wrapper) {
|
|
background-color: $color-background-base;
|
|
box-shadow: 0 0 0 1px $color-accent-border inset;
|
|
min-height: 48px;
|
|
font-size: 17px;
|
|
}
|
|
|
|
:deep(.el-input-group__prepend),
|
|
:deep(.el-input-group__append) {
|
|
padding: 0;
|
|
}
|
|
|
|
.press-control {
|
|
width: 52px;
|
|
min-height: 48px;
|
|
margin: 0;
|
|
}
|
|
}
|
|
|
|
&--dual-arm {
|
|
.joint-item {
|
|
padding: 8px;
|
|
|
|
label {
|
|
width: 82px;
|
|
font-size: 14px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
// 中间3D区域
|
|
.center-view {
|
|
flex: 1;
|
|
display: flex;
|
|
}
|
|
|
|
// 右侧摇杆面板
|
|
.right-panel {
|
|
width: 400px;
|
|
padding: 20px;
|
|
background: $color-background-panel;
|
|
border-left: 1px solid $color-accent-border;
|
|
|
|
.arm-selector {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
margin-bottom: 24px;
|
|
padding: 12px 14px;
|
|
border: 1px solid $color-accent-border;
|
|
border-radius: 8px;
|
|
background: $color-background-base;
|
|
|
|
&__label {
|
|
flex-shrink: 0;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
&__options {
|
|
flex: 1;
|
|
|
|
:deep(.el-radio-button) {
|
|
width: 50%;
|
|
}
|
|
|
|
:deep(.el-radio-button__inner) {
|
|
width: 100%;
|
|
}
|
|
}
|
|
}
|
|
|
|
.controller {
|
|
margin-bottom: 48px;
|
|
|
|
.title {
|
|
font-size: 22px;
|
|
font-weight: bold;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.axis-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
margin-bottom: 16px;
|
|
|
|
.axis-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
min-height: 68px;
|
|
padding: 10px 14px;
|
|
background: $color-background-base;
|
|
border: 1px solid $color-accent-border;
|
|
border-radius: 10px;
|
|
transition: all 0.2s ease;
|
|
|
|
&:hover {
|
|
border-color: $color-accent-border-strong;
|
|
background: $color-background-card-hover;
|
|
}
|
|
|
|
.axis-label {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 6px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.x {
|
|
background: $color-axis-x-background;
|
|
color: $color-axis-x;
|
|
border: 1px solid $color-axis-x-border;
|
|
}
|
|
|
|
.y {
|
|
background: $color-axis-y-background;
|
|
color: $color-axis-y;
|
|
border: 1px solid $color-axis-y-border;
|
|
}
|
|
|
|
.z {
|
|
background: $color-axis-z-background;
|
|
color: $color-axis-z;
|
|
border: 1px solid $color-axis-z-border;
|
|
}
|
|
|
|
|
|
.axis-slider-wrap {
|
|
position: relative;
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 0;
|
|
|
|
.axis-name {
|
|
position: absolute;
|
|
left: 50%;
|
|
z-index: 1;
|
|
font-size: 16px;
|
|
white-space: nowrap;
|
|
pointer-events: none;
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
.axis-control-buttons {
|
|
position: relative;
|
|
z-index: 2;
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
|
|
.press-control {
|
|
width: 50px;
|
|
height: 50px;
|
|
margin: 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.speed-container {
|
|
min-height: 48px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
|
|
.label,
|
|
.proportion {
|
|
width: 100px;
|
|
font-size: 16px;
|
|
text-align: center;
|
|
}
|
|
|
|
.speed-slider {
|
|
position: relative;
|
|
flex: 1;
|
|
min-width: 0;
|
|
|
|
:deep(.el-slider) {
|
|
height: 40px;
|
|
}
|
|
|
|
:deep(.el-slider__button) {
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
|
|
.speed-tooltip {
|
|
position: absolute;
|
|
bottom: calc(100% + 7px);
|
|
z-index: 10;
|
|
padding: 5px 8px;
|
|
border: 1px solid $color-accent-border-strong;
|
|
border-radius: 4px;
|
|
color: $color-text-bright;
|
|
background: $color-background-tooltip-translucent;
|
|
box-shadow: 0 4px 12px $color-shadow-tooltip;
|
|
font-size: 12px;
|
|
line-height: 1;
|
|
white-space: nowrap;
|
|
pointer-events: none;
|
|
transform: translateX(-50%);
|
|
|
|
&::after {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 50%;
|
|
width: 7px;
|
|
height: 7px;
|
|
border-right: 1px solid $color-accent-border-strong;
|
|
border-bottom: 1px solid $color-accent-border-strong;
|
|
background: $color-background-tooltip-translucent;
|
|
content: '';
|
|
transform: translate(-50%, -50%) rotate(45deg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// element-plus深色输入框覆盖
|
|
:deep(.el-input__wrapper) {
|
|
background-color: $color-background-card;
|
|
box-shadow: $shadow-input;
|
|
|
|
.el-input__inner {
|
|
color: $color-text-bright;
|
|
}
|
|
}
|
|
|
|
:deep(.el-slider__runway) {
|
|
background: $color-accent-medium;
|
|
}
|
|
</style>
|