416 lines
9.5 KiB
Vue
416 lines
9.5 KiB
Vue
<script setup lang="ts">
|
||
import { ref } from 'vue';
|
||
import * as api from '../api.ts';
|
||
import type { AppState, BatchTask } from '../types.ts';
|
||
import Panel from '../components/Panel.vue';
|
||
import Button from '../components/Button.vue';
|
||
import Machine from '../components/Machine.vue';
|
||
import ManualAdjust from '../components/ManualAdjust.vue';
|
||
import Lissajous from '../components/Lissajous.vue';
|
||
import Window from '../components/Window.vue';
|
||
import Numpad from '../components/Numpad.vue';
|
||
import { showMessage } from '../utils/message';
|
||
|
||
defineProps<{
|
||
state: AppState;
|
||
}>();
|
||
|
||
const controlStart = ref('10');
|
||
const controlEnd = ref('20');
|
||
const controlStep = ref('0.1');
|
||
|
||
const showNumpad = ref(false);
|
||
const numpadValue = ref('');
|
||
const numpadTitle = ref('');
|
||
let currentField: 'start' | 'end' | 'step' | null = null;
|
||
|
||
function openInput(field: 'start' | 'end' | 'step') {
|
||
currentField = field;
|
||
if (field === 'start') {
|
||
numpadTitle.value = '设置起点';
|
||
numpadValue.value = controlStart.value;
|
||
} else if (field === 'end') {
|
||
numpadTitle.value = '设置终点';
|
||
numpadValue.value = controlEnd.value;
|
||
} else if (field === 'step') {
|
||
numpadTitle.value = '设置步进';
|
||
numpadValue.value = controlStep.value;
|
||
}
|
||
showNumpad.value = true;
|
||
}
|
||
|
||
function confirmInput() {
|
||
if (currentField === 'start') controlStart.value = numpadValue.value;
|
||
else if (currentField === 'end') controlEnd.value = numpadValue.value;
|
||
else if (currentField === 'step') controlStep.value = numpadValue.value;
|
||
showNumpad.value = false;
|
||
}
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'start'): void;
|
||
(e: 'showManualAdjust'): void;
|
||
(e: 'showMotorSpeedSetting'): void;
|
||
}>();
|
||
|
||
function startBatch(currentDis: number) {
|
||
const start = parseFloat(controlStart.value);
|
||
const end = parseFloat(controlEnd.value);
|
||
const step = parseFloat(controlStep.value);
|
||
|
||
if (isNaN(start) || isNaN(end) || isNaN(step)) {
|
||
showMessage('请输入有效的数字格式', 'error');
|
||
return;
|
||
}
|
||
if (step <= 0) {
|
||
showMessage('步进必须大于0', 'error');
|
||
return;
|
||
}
|
||
|
||
emit('start');
|
||
|
||
const tasks: BatchTask[] = [];
|
||
const currentSteps = currentDis;
|
||
const startSteps = start * 1600;
|
||
const diff = startSteps - currentSteps;
|
||
|
||
// 1. 移动到起点
|
||
if (Math.abs(diff) > 10) {
|
||
tasks.push({
|
||
cmd: 'move',
|
||
args: { steps: diff },
|
||
repeat: 1
|
||
});
|
||
}
|
||
|
||
// 2. 测量循环
|
||
const totalDis = end - start;
|
||
const count = Math.floor(Math.abs(totalDis) / step);
|
||
|
||
if (count > 0) {
|
||
const dir = totalDis >= 0 ? 1 : -1;
|
||
const stepSteps = step * 1600 * dir;
|
||
tasks.push({
|
||
cmd: 'move_measure',
|
||
args: { steps: stepSteps },
|
||
repeat: count
|
||
});
|
||
}
|
||
|
||
api.batch(tasks).then(() => {
|
||
showMessage('批量测量任务已下发', 'success');
|
||
});
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="control-page-content">
|
||
<div class="left-section">
|
||
<!-- 顶部:装置位置示意图 -->
|
||
<Panel class="machine-panel-wrapper">
|
||
<Machine :dis="state.dis" :task="state.tasks.find(t => t.type == 'move')" />
|
||
<ManualAdjust @show-input="emit('showManualAdjust')" class="manual-adjust-wrapper" />
|
||
|
||
</Panel>
|
||
|
||
<!-- 下方:李萨如 + 控制面板 -->
|
||
<div class="bottom-row">
|
||
<Panel class="oscilloscope-panel-wrapper">
|
||
<div class="chart-container">
|
||
<Lissajous :data="state.last_measurement" />
|
||
</div>
|
||
</Panel>
|
||
<Panel class="inputs-panel-wrapper">
|
||
<div class="input-container">
|
||
<div class="input-group">
|
||
<label>起点</label>
|
||
<div class="input-wrapper">
|
||
<Button class="input-display" @click="openInput('start')">{{ controlStart || '10' }}</Button>
|
||
<span class="unit">mm</span>
|
||
</div>
|
||
</div>
|
||
<div class="input-group">
|
||
<label>终点</label>
|
||
<div class="input-wrapper">
|
||
<Button class="input-display" @click="openInput('end')">{{ controlEnd || '30' }}</Button>
|
||
<span class="unit">mm</span>
|
||
</div>
|
||
</div>
|
||
<div class="input-group">
|
||
<label>步进</label>
|
||
<div class="input-wrapper">
|
||
<Button class="input-display" @click="openInput('step')">{{ controlStep || '0.1' }}</Button>
|
||
<span class="unit">mm</span>
|
||
</div>
|
||
</div>
|
||
<div class="input-group">
|
||
<label>电机速度</label>
|
||
<div class="input-wrapper">
|
||
<Button class="input-display" @click="emit('showMotorSpeedSetting')">{{ state.speed ? Math.round(state.speed / 1.6) : '--'
|
||
}}</Button>
|
||
<span class="unit">μm/s</span>
|
||
</div>
|
||
</div>
|
||
|
||
<Button @click="startBatch(state.dis)" bg="limegreen" class="start-btn">开始任务</Button>
|
||
</div>
|
||
</Panel>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="right-section">
|
||
<Panel class="task-list-panel">
|
||
<div class="task-head">任务列表 ({{ state.total_tasks }})</div>
|
||
<div class="task-list">
|
||
<div v-for="task in state.tasks.slice(-5)" :key="task.id" class="task-item">
|
||
<div class="task-info">
|
||
<span class="type" :class="task.type">{{ task.type === 'move' ? '移动' : '测量' }}</span>
|
||
<span class="status" :class="task.status">{{ task.status }}</span>
|
||
</div>
|
||
<div class="task-detail" v-if="task.type === 'move'">
|
||
{{ (task.steps / 1600).toFixed(3) }}mm
|
||
</div>
|
||
</div>
|
||
<div v-if="state.tasks.length === 0" class="empty-tasks">暂无任务</div>
|
||
</div>
|
||
<Button class="action-btn" @click="api.stopAll()" bg="red">停止</Button>
|
||
</Panel>
|
||
</div>
|
||
|
||
<Window v-model="showNumpad" :title="numpadTitle">
|
||
<div class="numpad-content">
|
||
<Numpad v-model="numpadValue" :label="numpadTitle" unit="mm" />
|
||
<div class="numpad-actions">
|
||
<Button class="action-btn" @click="showNumpad = false">取消</Button>
|
||
<Button class="action-btn" bg="limegreen" @click="confirmInput">确定</Button>
|
||
</div>
|
||
</div>
|
||
</Window>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
.control-page-content {
|
||
display: flex;
|
||
padding: 8px;
|
||
gap: 8px;
|
||
height: 100%;
|
||
box-sizing: border-box;
|
||
width: 100%;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.left-section {
|
||
flex: 3;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
height: 100%;
|
||
min-width: 0; // Prevent flex overflow
|
||
}
|
||
|
||
.right-section {
|
||
flex: 1.5;
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
.machine-panel-wrapper {
|
||
flex: 0 0 auto; // 不要压缩
|
||
padding: 10px;
|
||
display: flex;
|
||
}
|
||
|
||
.manual-adjust-wrapper {
|
||
width: 220px;
|
||
}
|
||
|
||
.bottom-row {
|
||
flex: 1;
|
||
display: flex;
|
||
gap: 8px;
|
||
min-height: 0; // Fix nested flex overflow
|
||
}
|
||
|
||
.oscilloscope-panel-wrapper {
|
||
flex: 3; // 左侧稍微大一点
|
||
overflow: hidden;
|
||
position: relative;
|
||
/* ensure inner absolute positioning works if needed, usually oscilloscope uses flex */
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 4px;
|
||
}
|
||
|
||
.chart-title {
|
||
font-weight: 600;
|
||
font-size: 16px;
|
||
color: #334155;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.chart-container {
|
||
flex: 1;
|
||
min-height: 200px;
|
||
width: 100%;
|
||
}
|
||
|
||
|
||
.inputs-panel-wrapper {
|
||
flex: 2.5; // 控制区
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
}
|
||
|
||
.input-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
width: 100%;
|
||
}
|
||
|
||
.input-group {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
|
||
label {
|
||
font-weight: bold;
|
||
font-size: 16px;
|
||
color: #333;
|
||
width: 64px;
|
||
}
|
||
|
||
.input-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.input-display {
|
||
width: 80px;
|
||
height: 40px;
|
||
}
|
||
|
||
.unit {
|
||
font-size: 14px;
|
||
color: #666;
|
||
width: 30px;
|
||
}
|
||
}
|
||
|
||
.start-btn {
|
||
margin-top: 4px;
|
||
height: 36px;
|
||
width: 100%;
|
||
font-size: 18px;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.numpad-content {
|
||
display: flex;
|
||
gap: 16px;
|
||
width: 100%;
|
||
}
|
||
|
||
.numpad-actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
min-width: 120px;
|
||
}
|
||
|
||
.action-btn {
|
||
width: 100%;
|
||
height: 52px;
|
||
font-size: 16px;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.task-list-panel {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 10px;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
|
||
.task-head {
|
||
font-weight: bold;
|
||
margin-bottom: 8px;
|
||
font-size: 16px;
|
||
border-bottom: 1px solid #eee;
|
||
padding-bottom: 6px;
|
||
}
|
||
|
||
.task-list {
|
||
flex: 1;
|
||
overflow-y: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.task-item {
|
||
background: #f8fafc;
|
||
padding: 8px;
|
||
border-radius: 4px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
border-left: 4px solid #cbd5e1;
|
||
|
||
.task-info {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
|
||
.type {
|
||
font-weight: bold;
|
||
font-size: 14px;
|
||
|
||
&.move {
|
||
color: #3b82f6;
|
||
}
|
||
|
||
&.measure {
|
||
color: #8b5cf6;
|
||
}
|
||
}
|
||
|
||
.status {
|
||
font-size: 12px;
|
||
padding: 2px 6px;
|
||
border-radius: 999px;
|
||
background: #e2e8f0;
|
||
|
||
&.running {
|
||
background: #dbface;
|
||
color: #166534;
|
||
border: 1px solid #166534;
|
||
}
|
||
|
||
&.pending {
|
||
background: #fef9c3;
|
||
color: #854d0e;
|
||
}
|
||
|
||
&.queued {
|
||
background: #e2e8f0;
|
||
color: #475569;
|
||
}
|
||
}
|
||
}
|
||
|
||
.empty-tasks {
|
||
text-align: center;
|
||
color: #999;
|
||
margin-top: 20px;
|
||
}
|
||
}
|
||
</style> |