2026-01-28 14:42:14 +08:00

403 lines
9.7 KiB
Vue
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.

<script setup lang="ts">
import { ref } from 'vue';
import * as api from '../api.ts';
import Panel from '../components/Panel.vue';
import Button from '../components/Button.vue';
import Machine from '../components/Machine.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: any;
}>();
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;
}>();
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 = [];
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.reduce((acc: any, task: any) => (task.type == 'move' ? ({ remaining_steps: acc.remaining_steps + task.remaining_steps }) : acc), { remaining_steps: 0 })" />
</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" @click="openInput('start')">
<div class="input-display">{{ controlStart || '10' }}</div>
<span class="unit">mm</span>
</div>
</div>
<div class="input-group">
<label>终点</label>
<div class="input-wrapper" @click="openInput('end')">
<div class="input-display">{{ controlEnd || '30' }}</div>
<span class="unit">mm</span>
</div>
</div>
<div class="input-group">
<label>步进</label>
<div class="input-wrapper" @click="openInput('step')">
<div class="input-display">{{ controlStep || '0.1' }}</div>
<span class="unit">mm</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(1) }}mm
</div>
</div>
<div v-if="state.tasks.length === 0" class="empty-tasks">暂无任务</div>
</div>
</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;
}
.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; // 控制区
display: flex;
flex-direction: column;
justify-content: center;
padding: 20px;
}
.input-container {
display: flex;
flex-direction: column;
gap: 16px;
width: 100%;
}
.input-group {
display: flex;
align-items: center;
justify-content: space-between;
label {
font-weight: bold;
font-size: 16px;
color: #333;
width: 50px;
}
.input-wrapper {
display: flex;
align-items: center;
gap: 6px;
}
.input-display {
width: 80px;
height: 40px;
border: 1px solid #ccc;
border-radius: 4px;
text-align: center;
font-size: 18px;
background: white;
line-height: 40px;
cursor: pointer;
}
.unit {
font-size: 14px;
color: #666;
width: 30px;
}
}
.start-btn {
margin-top: 10px;
height: 48px;
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>