<template>
|
<div class="container">
|
<!-- 左侧区域 - 无任何标题标注 -->
|
<div class="left-area">
|
<div class="left-top">
|
<!-- 按钮+信号 横向排列容器 -->
|
<div class="btn-signal-group">
|
<!-- 按钮组 - 修改为上下排列 -->
|
<div class="btn-group">
|
<button
|
class="btn"
|
:class="isPLCStarted ? 'stop-btn' : 'start-btn'"
|
@click="handleToggle"
|
>
|
<i class="icon" :class="isPLCStarted ? 'icon-stop' : 'icon-start'"></i>
|
{{ isPLCStarted ? "关闭" : "启动" }}
|
</button>
|
<button
|
class="btn"
|
:class="isPLCPaused ? 'resume-btn' : 'pause-btn'"
|
@click="handlePauseToggle"
|
:disabled="!isPLCStarted"
|
>
|
<i class="icon" :class="isPLCPaused ? 'icon-resume' : 'icon-pause'"></i>
|
{{ isPLCPaused ? "恢复" : "暂停" }}
|
</button>
|
</div>
|
|
<!-- 信号灯组 占两个按钮宽度 + 整体放大 -->
|
<div class="signal-status">
|
<div class="signal-item" v-for="(signal, index) in signalStates" :key="index">
|
<div
|
class="signal-light"
|
:class="signal ? 'signal-active' : 'signal-inactive'"
|
>
|
<div class="signal-light-inner"></div>
|
</div>
|
<span class="signal-label">{{ signalLabels[index] }}</span>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<div class="left-bottom">
|
<div class="form-row finished-product-row">
|
<span class="label">成品编号:</span>
|
<input type="text" class="input-box" v-model="finishedProduct" disabled />
|
</div>
|
<div class="parts-list">
|
<div
|
class="form-row part-item"
|
v-for="(item, index) in leftPartCodes"
|
:key="index"
|
>
|
<span class="label">零件{{ index + 1 }}:</span>
|
<input
|
type="text"
|
class="input-box"
|
v-model="leftPartCodes[index]"
|
disabled
|
/>
|
<label class="checkbox-container">
|
<input
|
type="checkbox"
|
class="part-checkbox"
|
v-model="leftPartChecked[index]"
|
@change="handlePartCheck(index)"
|
/>
|
<span class="checkmark"></span>
|
<span class="checkbox-label">{{
|
leftPartChecked[index] ? "扫码" : "不扫"
|
}}</span>
|
</label>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<!-- 右侧区域 - 无任何标题标注 【已删除清空+保存按钮】 -->
|
<div class="right-area">
|
<div class="right-top">
|
<div class="form-row">
|
<span class="label">录入框:</span>
|
<!-- ✅ 只保留纯录入框,清空/保存按钮已删除 -->
|
<input type="text" class="input-box" v-model="rightTopInput" />
|
</div>
|
</div>
|
|
<div class="right-bottom">
|
<div class="form-row tooling-board-row">
|
<span class="short-label">工装板编号:</span>
|
<input
|
type="text"
|
class="input-box short-input"
|
v-model="toolingBoardNo"
|
placeholder="请输入工装板编号"
|
/>
|
<button class="btn clear-btn" @click="clearToolingBoardNo">
|
<i class="icon icon-clear"></i>清除
|
</button>
|
<button class="btn save-btn" @click="saveToolingBoardNo">
|
<i class="icon icon-submit"></i>提交
|
</button>
|
</div>
|
<div class="parts-list">
|
<div class="form-row part-item finished-product-row">
|
<span class="label">成品编号:</span>
|
<input
|
type="text"
|
class="input-box"
|
v-model="finishedProductCode"
|
placeholder="请输入成品编号"
|
/>
|
<button class="btn clear-btn" @click="clearFinishedProductCode">
|
<i class="icon icon-clear"></i>清除
|
</button>
|
</div>
|
<div
|
class="form-row part-item"
|
v-for="(item, index) in rightPartCodes"
|
:key="index"
|
>
|
<span class="label">零件{{ index + 1 }}:</span>
|
<input
|
type="text"
|
class="input-box"
|
v-model="rightPartCodes[index]"
|
placeholder="请输入零件编号"
|
/>
|
<button class="btn clear-btn" @click="clearRightPart(index)">
|
<i class="icon icon-clear"></i>清除
|
</button>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script>
|
import { ref, onMounted, onUnmounted, watch, computed } from "vue";
|
import axios from "axios";
|
|
export default {
|
setup() {
|
// 基础数据定义 - 1:1精准对接后端 成品编号+零件编号 无冗余兼容
|
const finishedProduct = ref(""); // 左侧成品编号(GetLeftInitialData接口返回)
|
const finishedProductId = ref("");
|
const rightTopInput = ref("");
|
const leftPartCodes = ref(Array(10).fill("")); // 左侧零件编号数组
|
const rightPartCodes = ref(Array(10).fill("")); // 右侧零件编号数组
|
const leftPartChecked = ref(Array(10).fill(false));
|
const toolingBoardNo = ref("");
|
const fillIndex = ref(-1);
|
const leftPartIds = ref(Array(10).fill(""));
|
const finishedProductCode = ref(""); // 右侧成品编号(工装板接口返回)
|
|
// PLC状态
|
const isPLCStarted = ref(false);
|
const isPLCPaused = ref(false);
|
|
// 信号相关
|
const signalStates = ref([false, false, false, false, false]);
|
const signalLabels = ref([
|
"心跳信号",
|
"急停信号",
|
"自动运行信号",
|
"在线模式信号",
|
"故障信号",
|
]);
|
|
// 定时轮询核心配置
|
let pollingTimer = null;
|
const pollingInterval = 5000;
|
let checkDebounceTimer = null;
|
let destroyDelayTimer = null;
|
const destroyDelayTime = 500; // ✅ 核心:填充+清空 都延迟500毫秒
|
let boardCodeDebounceTimer = null;
|
// ✅ 新增:自动提交防抖定时器,防止重复提交
|
let autoSubmitDebounceTimer = null;
|
// ✅ 新增:提交锁,防止无勾选时重复触发提交
|
let submitLock = ref(false);
|
|
// ✅ ✅ ✅ 核心新增1:计算属性 - 实时统计左侧勾选的复选框数量 (自动更新)
|
const checkedCount = computed(() => {
|
// 统计leftPartChecked数组中为true的数量
|
return leftPartChecked.value.filter((checked) => checked === true).length;
|
});
|
|
// ✅ ✅ ✅ 核心新增2:计算属性 - 实时统计右侧已填充的零件数量 (自动更新)
|
const filledPartCount = computed(() => {
|
// 统计rightPartCodes数组中有值(非空)的零件数量
|
return rightPartCodes.value.filter((code) => code.trim() !== "").length;
|
});
|
|
// ✅ 获取左侧初始数据 - 对接 /api/scanStation/GetLeftInitialData
|
const fetchLeftInitialData = async () => {
|
try {
|
console.log("正在获取左侧初始数据(成品编号+零件编号+勾选状态+零件ID)...");
|
const response = await axios.get("/api/scanStation/GetLeftInitialData", {
|
timeout: 5000,
|
});
|
const resData = response.data;
|
const isSuccess = resData.Status === true || resData.status === true;
|
if (isSuccess) {
|
const data = resData.Data || resData.data || {};
|
if (data.finishedProductId) finishedProductId.value = data.finishedProductId;
|
if (data.finishedProduct) finishedProduct.value = data.finishedProduct;
|
// 赋值左侧零件编号
|
if (Array.isArray(data.leftPartCodes) && data.leftPartCodes.length >= 10) {
|
for (let i = 0; i < 10; i++) {
|
leftPartCodes.value[i] = data.leftPartCodes[i] || "";
|
leftPartIds.value[i] = data.leftPartIds?.[i] || "";
|
}
|
}
|
// 赋值勾选状态
|
if (Array.isArray(data.leftPartChecked) && data.leftPartChecked.length >= 10) {
|
for (let i = 0; i < 10; i++) {
|
leftPartChecked.value[i] = !!data.leftPartChecked[i];
|
}
|
}
|
}
|
} catch (error) {
|
console.error("获取左侧初始数据失败:", error);
|
}
|
};
|
|
// ✅ 获取信号+PLC状态
|
const fetchSignalAndPLCStates = async () => {
|
try {
|
const response = await axios.get("/api/scanStation/GetSignalStates", {
|
timeout: 5000,
|
});
|
const resData = response.data;
|
const isSuccess = resData.Status === true || resData.status === true;
|
if (isSuccess) {
|
const data = resData.Data || resData.data || {};
|
const newSignalStates = data.signalStates || [];
|
for (let i = 0; i < 5; i++) signalStates.value[i] = newSignalStates[i] ?? false;
|
const plcStatus = data.plcStatus || data.plc_status || {};
|
isPLCStarted.value = plcStatus.isStarted ?? isPLCStarted.value;
|
// ✅ 修复BUG:原代码是 isPLCStarted.value 导致暂停状态赋值错误
|
isPLCPaused.value = plcStatus.isPaused ?? isPLCPaused.value;
|
}
|
} catch (error) {
|
console.error("获取信号和PLC状态失败:", error);
|
}
|
};
|
|
// ✅ 【核心修改】工装板查询接口 - 有数据就填充,无数据/失败 完全保留原有内容,不做任何清空操作
|
const fetchProductAndPartsByBoardCode = async (boardCode) => {
|
if (!boardCode.trim()) return;
|
try {
|
console.log(`工装板编号变更,请求数据:${boardCode}`);
|
const response = await axios.get(
|
"/api/boxingDetail/GetProductAndPartsByBoardNo",
|
{
|
params: { palletCode: boardCode.trim() },
|
timeout: 5000,
|
}
|
);
|
const resData = response.data;
|
const isSuccess = resData.Status === true || resData.status === true;
|
// ✅ 只有【接口成功+有返回数据】的时候,才执行赋值覆盖
|
if (isSuccess) {
|
const data = resData.Data || resData.data || {};
|
// 有成品编号就赋值,没有就不操作
|
if (data.finishedProductCode) {
|
finishedProductCode.value = data.finishedProductCode;
|
}
|
// 有零件列表就填充,没有就不操作
|
const partsList = Array.isArray(data.partsList) ? data.partsList : [];
|
if (partsList.length > 0) {
|
for (let i = 0; i < 10; i++) {
|
if (partsList[i]) {
|
rightPartCodes.value[i] = partsList[i];
|
}
|
}
|
}
|
console.log("✅ 工装板查询成功,成品编号+零件编号填充完成");
|
} else {
|
// ✅ 无对应数据:只弹提示,不清空任何内容
|
alert(
|
"获取数据失败:" + (resData.Message || resData.message || "无对应工装板数据")
|
);
|
}
|
} catch (error) {
|
// ✅ 请求失败:只弹提示,不清空任何内容
|
alert("工装板数据请求失败,请检查网络或接口!");
|
console.error("工装板接口请求失败:", error);
|
} finally {
|
boardCodeDebounceTimer = null;
|
}
|
};
|
|
// 启动/停止定时轮询
|
const startPolling = () => {
|
if (pollingTimer) clearInterval(pollingTimer);
|
fetchSignalAndPLCStates();
|
pollingTimer = setInterval(fetchSignalAndPLCStates, pollingInterval);
|
};
|
const stopPolling = () => {
|
if (pollingTimer) clearInterval(pollingTimer);
|
pollingTimer = null;
|
};
|
|
// PLC启动/关闭逻辑
|
const handleToggle = async () => {
|
try {
|
const response = await axios.get("/api/scanStation/StartPLC", {
|
params: { isStop: isPLCStarted.value },
|
timeout: 5000,
|
});
|
const resData = response.data;
|
const isSuccess = resData.Status === true || resData.status === true;
|
if (isSuccess) {
|
isPLCStarted.value = !isPLCStarted.value;
|
isPLCPaused.value = false;
|
fetchSignalAndPLCStates();
|
} else {
|
alert(
|
resData.Message ||
|
resData.message ||
|
(isPLCStarted.value ? "关闭失败" : "启动失败")
|
);
|
}
|
} catch (error) {
|
alert(isPLCStarted.value ? "关闭PLC失败" : "启动PLC失败");
|
console.error("PLC启停失败:", error);
|
}
|
};
|
|
// PLC暂停/恢复逻辑
|
const handlePauseToggle = async () => {
|
try {
|
const response = await axios.get("/api/scanStation/PausePLC", {
|
params: { isPause: !isPLCPaused.value },
|
timeout: 5000,
|
});
|
const resData = response.data;
|
const isSuccess = resData.Status === true || resData.status === true;
|
if (isSuccess) {
|
isPLCPaused.value = !isPLCPaused.value;
|
fetchSignalAndPLCStates();
|
} else {
|
alert(
|
resData.Message ||
|
resData.message ||
|
(isPLCPaused.value ? "恢复失败" : "暂停失败")
|
);
|
}
|
} catch (error) {
|
alert(isPLCPaused.value ? "恢复PLC失败" : "暂停PLC失败");
|
console.error("PLC暂停恢复失败:", error);
|
}
|
};
|
|
// 零件勾选状态变更处理
|
const handlePartCheck = async (index) => {
|
const isChecked = leftPartChecked.value[index];
|
const partCode = leftPartCodes.value[index];
|
const partId = leftPartIds.value[index];
|
|
if (!finishedProductId.value) {
|
alert("成品ID不存在,无法更新零件扫码状态!");
|
leftPartChecked.value[index] = !isChecked;
|
return;
|
}
|
if (!partId) {
|
alert(`零件${index + 1}数据库ID不存在,无法更新扫码状态!`);
|
leftPartChecked.value[index] = !isChecked;
|
return;
|
}
|
if (!partCode.trim()) {
|
alert(`零件${index + 1}编号为空,无法更新扫码状态!`);
|
leftPartChecked.value[index] = !isChecked;
|
return;
|
}
|
|
if (checkDebounceTimer) clearTimeout(checkDebounceTimer);
|
checkDebounceTimer = setTimeout(async () => {
|
try {
|
const response = await axios.post(
|
"/api/scanStation/UpdatePartScannedStatus",
|
{ Id: partId, IsScanned: isChecked ? 1 : 0 },
|
{ timeout: 5000 }
|
);
|
const resData = response.data;
|
const isSuccess = resData.Status === true || resData.status === true;
|
if (isSuccess) {
|
console.log(`零件${index + 1}扫码状态更新成功`);
|
// ✅ 勾选状态变化时,重置提交锁
|
submitLock.value = false;
|
} else {
|
leftPartChecked.value[index] = !isChecked;
|
alert(
|
`零件${index + 1}状态更新失败:${
|
resData.Message || resData.message || "未知错误"
|
}`
|
);
|
}
|
} catch (error) {
|
leftPartChecked.value[index] = !isChecked;
|
alert(`零件${index + 1}状态更新请求失败,请检查网络或接口!`);
|
console.error(`更新零件${index + 1}扫码状态失败:`, error);
|
} finally {
|
checkDebounceTimer = null;
|
}
|
}, 500);
|
};
|
|
// ✅ 核心修改:延迟0.5秒填充 + 延迟0.5秒清空 同步执行 无需手动按钮
|
const fillContent = () => {
|
if (!rightTopInput.value.trim()) return;
|
const inputValue = rightTopInput.value.trim();
|
|
// 清除旧定时器,防止重复执行
|
if (destroyDelayTimer) clearTimeout(destroyDelayTimer);
|
// 统一延迟500ms执行【填充+清空】
|
destroyDelayTimer = setTimeout(() => {
|
if (!toolingBoardNo.value.trim()) {
|
toolingBoardNo.value = inputValue;
|
} else if (!finishedProductCode.value.trim()) {
|
finishedProductCode.value = inputValue;
|
} else if (fillIndex.value < 10) {
|
rightPartCodes.value[fillIndex.value] = inputValue;
|
fillIndex.value++;
|
} else {
|
alert("工装板编号、成品编号和零件1-10已全部填充完成,无法继续录入!");
|
rightTopInput.value = "";
|
destroyDelayTimer = null;
|
return;
|
}
|
// 填充完成后 自动清空录入框
|
rightTopInput.value = "";
|
destroyDelayTimer = null;
|
}, destroyDelayTime);
|
};
|
|
// ✅ 监听录入框内容变化自动触发填充逻辑
|
watch(
|
rightTopInput,
|
(newVal) => {
|
if (newVal.trim()) fillContent();
|
},
|
{ immediate: false }
|
);
|
|
// 监听工装板编号变化查询数据
|
watch(
|
toolingBoardNo,
|
(newVal) => {
|
const boardCode = newVal.trim();
|
if (boardCode) {
|
if (boardCodeDebounceTimer) clearTimeout(boardCodeDebounceTimer);
|
boardCodeDebounceTimer = setTimeout(
|
() => fetchProductAndPartsByBoardCode(boardCode),
|
300
|
);
|
} else {
|
if (boardCodeDebounceTimer) clearTimeout(boardCodeDebounceTimer);
|
}
|
},
|
{ immediate: false }
|
);
|
|
// 右侧页面操作方法 (已删除清空/保存录入框的方法,无冗余)
|
const clearToolingBoardNo = () => (toolingBoardNo.value = "");
|
const clearRightPart = (index) => (rightPartCodes.value[index] = "");
|
const clearFinishedProductCode = () => (finishedProductCode.value = "");
|
|
// 提交工装板数据到后端
|
const saveToolingBoardNo = async () => {
|
if (!toolingBoardNo.value.trim()) {
|
alert("工装板编号不能为空,请输入后再提交!");
|
return;
|
}
|
if (!finishedProductCode.value.trim()) {
|
alert("成品编号不能为空,请输入后再提交!");
|
return;
|
}
|
try {
|
const submitData = {
|
toolingBoardNo: toolingBoardNo.value.trim(),
|
finishedProductCode: finishedProductCode.value.trim(),
|
partsList: rightPartCodes.value.map((item) => item.trim()),
|
};
|
console.log("✅ 提交工装板数据:", submitData);
|
const response = await axios.post(
|
"/api/boxingDetail/SaveToolingBoardNo",
|
submitData,
|
{ timeout: 5000 }
|
);
|
const resData = response.data;
|
const isSuccess = resData.Status === true || resData.status === true;
|
if (isSuccess) {
|
alert("✅ 提交成功!");
|
toolingBoardNo.value = "";
|
finishedProductCode.value = "";
|
rightPartCodes.value = Array(10).fill("");
|
rightTopInput.value = "";
|
fillIndex.value = -1;
|
// ✅ 提交成功后,重置提交锁
|
submitLock.value = false;
|
} else {
|
alert("提交失败:" + (resData.Message || resData.message || "未知错误"));
|
}
|
} catch (error) {
|
alert("提交请求失败,请检查网络或接口!");
|
console.error("提交接口失败:", error);
|
// ✅ 请求失败也重置锁
|
submitLock.value = false;
|
}
|
};
|
|
// ✅ ✅ ✅ 核心升级:自动提交判断逻辑 - 新增无勾选时成品编号填充即提交
|
const checkAutoSubmit = () => {
|
// 防抖:防止短时间内多次触发提交
|
if (autoSubmitDebounceTimer) clearTimeout(autoSubmitDebounceTimer);
|
autoSubmitDebounceTimer = setTimeout(() => {
|
const needCheckNum = checkedCount.value; // 左侧勾选的数量
|
const filledNum = filledPartCount.value; // 右侧填充的零件数量
|
const hasBoardNo = toolingBoardNo.value.trim() !== ""; // 工装板有值
|
const hasProductCode = finishedProductCode.value.trim() !== ""; // 成品有值
|
|
console.log(`✅ 自动提交校验:左侧勾选${needCheckNum}个,右侧填充${filledNum}个`);
|
|
// 分支1:左侧有勾选 → 原有逻辑:零件填充数≥勾选数 才提交
|
if (needCheckNum > 0) {
|
if (hasBoardNo && hasProductCode && filledNum >= needCheckNum) {
|
console.log("✅ 满足勾选数量条件,执行自动提交!");
|
saveToolingBoardNo();
|
}
|
}
|
// 分支2:左侧无勾选 → 新增逻辑:工装板+成品都有值 就提交 (加锁防重复)
|
else {
|
if (hasBoardNo && hasProductCode && !submitLock.value) {
|
console.log("✅ 左侧无勾选,成品编号填充完成,执行自动提交!");
|
submitLock.value = true; // 加锁防止重复提交
|
saveToolingBoardNo();
|
}
|
}
|
autoSubmitDebounceTimer = null;
|
}, 300);
|
};
|
|
// ✅ ✅ ✅ 核心新增4:监听关键数据变化,触发自动提交校验
|
watch(
|
[checkedCount, filledPartCount, toolingBoardNo, finishedProductCode],
|
() => {
|
checkAutoSubmit();
|
},
|
{ deep: true, immediate: false }
|
);
|
|
// 自动检测填充索引逻辑
|
const detectFillIndex = () => {
|
if (!toolingBoardNo.value.trim() || !finishedProductCode.value.trim()) {
|
fillIndex.value = -1;
|
return;
|
}
|
for (let i = 0; i < 10; i++) {
|
if (!rightPartCodes.value[i].trim()) {
|
fillIndex.value = i;
|
return;
|
}
|
}
|
fillIndex.value = 10;
|
};
|
|
watch(
|
[toolingBoardNo, finishedProductCode, () => [...rightPartCodes.value]],
|
detectFillIndex,
|
{
|
immediate: true,
|
deep: true,
|
}
|
);
|
|
// 页面挂载/卸载生命周期
|
onMounted(async () => {
|
await fetchLeftInitialData();
|
startPolling();
|
detectFillIndex();
|
});
|
|
onUnmounted(() => {
|
stopPolling();
|
if (checkDebounceTimer) clearTimeout(checkDebounceTimer);
|
if (destroyDelayTimer) clearTimeout(destroyDelayTimer);
|
if (boardCodeDebounceTimer) clearTimeout(boardCodeDebounceTimer);
|
if (autoSubmitDebounceTimer) clearTimeout(autoSubmitDebounceTimer);
|
});
|
|
return {
|
finishedProduct,
|
finishedProductId,
|
rightTopInput,
|
leftPartCodes,
|
rightPartCodes,
|
leftPartChecked,
|
leftPartIds,
|
toolingBoardNo,
|
isPLCStarted,
|
isPLCPaused,
|
signalStates,
|
signalLabels,
|
finishedProductCode,
|
handleToggle,
|
handlePauseToggle,
|
handlePartCheck,
|
clearToolingBoardNo,
|
saveToolingBoardNo,
|
clearRightPart,
|
clearFinishedProductCode,
|
};
|
},
|
};
|
</script>
|
|
<style scoped>
|
/* 基础样式重置与全局样式 */
|
* {
|
margin: 0;
|
padding: 0;
|
box-sizing: border-box;
|
font-family: "Microsoft Yahei", "PingFang SC", "Inter", sans-serif;
|
scrollbar-width: none;
|
-ms-overflow-style: none;
|
}
|
*::-webkit-scrollbar {
|
display: none;
|
}
|
|
body {
|
background: linear-gradient(135deg, #f0f4f8 0%, #e9ecef 100%);
|
min-height: 100vh;
|
overflow: hidden;
|
font-size: 14px;
|
}
|
|
/* 容器样式 - 放大 间距加宽 */
|
.container {
|
display: flex;
|
width: 100%;
|
height: 100vh;
|
margin: 0;
|
gap: 15px;
|
padding: 15px;
|
overflow: hidden;
|
}
|
|
/* 面板通用样式 - 统一内边距 确保左右对齐 */
|
.left-area,
|
.right-area {
|
flex: 1;
|
width: 50%;
|
display: flex;
|
flex-direction: column;
|
gap: 15px;
|
padding: 18px;
|
background: #ffffff;
|
border: 1px solid #e2e8f0;
|
border-radius: 15px;
|
box-shadow: 0 6px 16px rgba(149, 157, 165, 0.15);
|
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
}
|
|
/* 按钮+信号 横向排列容器 - 核心布局 */
|
.btn-signal-group {
|
display: flex;
|
align-items: center;
|
gap: 20px;
|
width: 100%;
|
}
|
.btn-group {
|
display: flex;
|
flex-direction: column;
|
gap: 10px;
|
flex-shrink: 0;
|
}
|
.signal-status {
|
display: flex;
|
justify-content: flex-start;
|
align-items: center;
|
gap: 18px;
|
flex-shrink: 0;
|
padding: 0;
|
}
|
|
.left-top {
|
background: #f8fafc;
|
padding: 15px;
|
border-radius: 12px;
|
flex-shrink: 0;
|
width: 100%;
|
}
|
|
.left-bottom,
|
.right-bottom {
|
display: flex;
|
flex-direction: column;
|
gap: 12px;
|
flex: 1;
|
overflow: hidden !important;
|
}
|
|
.right-top {
|
padding: 15px;
|
background: #f8fafc;
|
border-radius: 12px;
|
flex-shrink: 0;
|
}
|
|
/* 表单行样式 - 统一高度和间距 确保左右对齐 */
|
.form-row {
|
display: flex;
|
align-items: center;
|
gap: 12px;
|
flex-wrap: nowrap;
|
padding: 6px 10px;
|
border-radius: 8px;
|
transition: all 0.2s ease;
|
height: 48px;
|
line-height: 48px;
|
flex-shrink: 0;
|
width: 100%;
|
}
|
|
.form-row:hover {
|
background: #f8fafc;
|
}
|
|
.finished-product-row {
|
background: #eff6ff;
|
border-left: 4px solid #3b82f6;
|
}
|
|
.tooling-board-row {
|
background: #f0fdf4;
|
border-left: 4px solid #22c55e;
|
}
|
|
.part-item {
|
border-bottom: 1px solid #f1f5f9;
|
margin-bottom: 3px;
|
}
|
|
.part-item:last-child {
|
border-bottom: none;
|
}
|
|
/* 标签样式 - 统一宽度 确保左右对齐 */
|
.label {
|
width: 90px;
|
text-align: right;
|
color: #334155;
|
font-size: 15px;
|
font-weight: 600;
|
flex-shrink: 0;
|
}
|
|
.short-label {
|
width: 110px;
|
text-align: right;
|
color: #334155;
|
font-size: 15px;
|
font-weight: 600;
|
flex-shrink: 0;
|
}
|
|
/* 输入框样式 - 统一尺寸 确保左右对齐 */
|
.input-box {
|
flex: 1;
|
min-width: 100px;
|
height: 42px;
|
padding: 0 15px;
|
border: 1px solid #e2e8f0;
|
border-radius: 8px;
|
outline: none;
|
font-size: 15px;
|
transition: all 0.2s ease;
|
background-color: #ffffff;
|
}
|
|
.short-input {
|
width: 150px !important;
|
flex: none !important;
|
}
|
|
.input-box:focus {
|
border-color: #3b82f6;
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
|
}
|
|
.input-box:disabled {
|
background-color: #f1f5f9;
|
color: #64748b;
|
cursor: not-allowed;
|
}
|
|
.input-box::placeholder {
|
color: #94a3b8;
|
font-size: 14px;
|
}
|
|
/* 按钮样式 - 固定宽度 不变 */
|
.btn {
|
width: 120px;
|
height: 42px;
|
padding: 0 16px;
|
border: none;
|
border-radius: 8px;
|
cursor: pointer;
|
font-size: 14px;
|
font-weight: 600;
|
transition: all 0.2s ease;
|
flex-shrink: 0;
|
display: inline-flex;
|
align-items: center;
|
justify-content: center;
|
gap: 8px;
|
position: relative;
|
overflow: hidden;
|
}
|
|
.btn:disabled {
|
opacity: 0.6;
|
cursor: not-allowed;
|
transform: none !important;
|
box-shadow: none !important;
|
}
|
|
.btn::after {
|
content: "";
|
position: absolute;
|
top: 0;
|
left: -100%;
|
width: 100%;
|
height: 100%;
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
transition: all 0.5s ease;
|
}
|
|
.btn:hover::after {
|
left: 100%;
|
}
|
|
/* 图标样式 - 图标放大 */
|
.icon {
|
display: inline-block;
|
width: 18px;
|
height: 18px;
|
background-size: contain;
|
background-repeat: no-repeat;
|
background-position: center;
|
}
|
|
.icon-start {
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M8 5v14l11-7z'/%3E%3C/svg%3E");
|
}
|
|
.icon-stop {
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M6 6h12v12H6z'/%3E%3C/svg%3E");
|
}
|
|
.icon-pause {
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M6 19h4V5H6v14zm8-14v14h4V5h-4z'/%3E%3C/svg%3E");
|
}
|
|
.icon-resume {
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M11 5L6 9H2v6h4l5 4V5zm7 0v14c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2z'/%3E%3C/svg%3E");
|
}
|
|
.icon-clear {
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3C/svg%3E");
|
}
|
|
.icon-submit {
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M2.01 21L15 13.4 23 21V5H2.01V21zM17 15l-5-5-5 5V7h10v8z'/%3E%3C/svg%3E");
|
}
|
|
/* 按钮类型样式 - 不变 */
|
.start-btn {
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
color: #fff;
|
}
|
.start-btn:hover:not(:disabled) {
|
transform: translateY(-1px);
|
box-shadow: 0 4px 8px rgba(16, 185, 129, 0.2);
|
}
|
|
.stop-btn {
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
color: #fff;
|
}
|
.stop-btn:hover:not(:disabled) {
|
transform: translateY(-1px);
|
box-shadow: 0 4px 8px rgba(239, 68, 68, 0.2);
|
}
|
|
.pause-btn {
|
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
color: #fff;
|
}
|
.pause-btn:hover:not(:disabled) {
|
transform: translateY(-1px);
|
box-shadow: 0 4px 8px rgba(245, 158, 11, 0.2);
|
}
|
|
.resume-btn {
|
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
color: #fff;
|
}
|
.resume-btn:hover:not(:disabled) {
|
transform: translateY(-1px);
|
box-shadow: 0 4px 8px rgba(139, 92, 246, 0.2);
|
}
|
|
.clear-btn {
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
color: #fff;
|
padding: 0 12px;
|
}
|
.clear-btn:hover:not(:disabled) {
|
transform: translateY(-1px);
|
box-shadow: 0 4px 8px rgba(239, 68, 68, 0.15);
|
}
|
|
.save-btn {
|
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
color: #fff;
|
padding: 0 12px;
|
}
|
.save-btn:hover:not(:disabled) {
|
transform: translateY(-1px);
|
box-shadow: 0 4px 8px rgba(59, 130, 246, 0.15);
|
}
|
|
/* 信号灯样式 醒目放大 */
|
.signal-item {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
gap: 5px;
|
min-width: 60px;
|
}
|
.signal-label {
|
font-size: 14px;
|
color: #334155;
|
white-space: nowrap;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
width: 100%;
|
text-align: center;
|
font-weight: 600;
|
}
|
.signal-light {
|
width: 32px;
|
height: 32px;
|
border-radius: 50%;
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
|
transition: all 0.3s ease;
|
position: relative;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
.signal-light-inner {
|
width: 14px;
|
height: 14px;
|
border-radius: 50%;
|
background: white;
|
opacity: 0.9;
|
transition: all 0.3s ease;
|
}
|
.signal-active {
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
box-shadow: 0 0 18px rgba(16, 185, 129, 0.7);
|
animation: pulse 2s infinite;
|
}
|
.signal-inactive {
|
background: linear-gradient(135deg, #94a3b8 0%, #64748b 100%);
|
}
|
@keyframes pulse {
|
0% {
|
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.8);
|
}
|
70% {
|
box-shadow: 0 0 0 15px rgba(16, 185, 129, 0);
|
}
|
100% {
|
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
|
}
|
}
|
|
/* 自定义复选框样式 - 放大 点击区域更大 */
|
.checkbox-container {
|
display: flex;
|
align-items: center;
|
gap: 6px;
|
cursor: pointer;
|
font-size: 14px;
|
color: #334155;
|
flex-shrink: 0;
|
}
|
.part-checkbox {
|
display: none;
|
}
|
.checkmark {
|
width: 22px;
|
height: 22px;
|
background-color: #f1f5f9;
|
border: 1px solid #cbd5e1;
|
border-radius: 4px;
|
transition: all 0.2s ease;
|
position: relative;
|
}
|
.part-checkbox:checked ~ .checkmark {
|
background-color: #3b82f6;
|
border-color: #3b82f6;
|
}
|
.checkmark:after {
|
content: "";
|
position: absolute;
|
display: none;
|
left: 7px;
|
top: 2px;
|
width: 6px;
|
height: 12px;
|
border: solid white;
|
border-width: 0 3px 3px 0;
|
transform: rotate(45deg);
|
}
|
.part-checkbox:checked ~ .checkmark:after {
|
display: block;
|
}
|
.checkbox-label {
|
font-size: 13px;
|
color: #475569;
|
width: 50px;
|
}
|
|
/* 响应式适配 */
|
@media (max-width: 1200px) {
|
.container {
|
flex-direction: column;
|
height: auto;
|
}
|
.left-area,
|
.right-area {
|
width: 100%;
|
flex: none;
|
}
|
.btn-signal-group {
|
flex-direction: column;
|
align-items: flex-start;
|
}
|
.signal-status {
|
width: 100%;
|
justify-content: flex-start;
|
}
|
}
|
|
@media (max-width: 768px) {
|
.form-row {
|
flex-direction: column;
|
align-items: flex-start;
|
height: auto;
|
line-height: normal;
|
}
|
.label,
|
.short-label {
|
width: 100%;
|
text-align: left;
|
margin-bottom: 6px;
|
}
|
.input-box {
|
width: 100%;
|
}
|
.short-input {
|
width: 100% !important;
|
}
|
.btn {
|
width: 100%;
|
margin-top: 6px;
|
}
|
.btn-group {
|
flex-direction: column;
|
gap: 10px;
|
width: 100%;
|
}
|
}
|
</style>
|