<template>
|
<div class="container">
|
<!-- 左侧区域 - 仅保留信号灯+成品/零件区域 -->
|
<div class="left-area">
|
<div class="left-top">
|
<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 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
|
/>
|
<!-- 新增:供方代码输入框 -->
|
<input
|
type="text"
|
class="input-box supplier-code-input"
|
v-model="leftPartSupplierCodes[index]"
|
disabled
|
placeholder="供方代码"
|
/>
|
<label class="checkbox-container">
|
<input
|
type="checkbox"
|
class="part-checkbox"
|
v-model="leftPartChecked[index]"
|
@change="handlePartCheck(index)"
|
:disabled="checkLoading[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 input-submit-row">
|
<span class="label">录入框:</span>
|
<!-- 关键修改1:添加ref标识 -->
|
<input
|
ref="inputBoxRef"
|
type="text"
|
class="input-box"
|
v-model="rightTopInput"
|
@blur="handleInputBlur"
|
/>
|
<button class="btn save-btn submit-input-btn" @click="saveData">
|
<i class="icon icon-submit"></i>提交
|
</button>
|
</div>
|
</div>
|
|
<div class="right-bottom">
|
<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>
|
<!-- 新增:删除当前托盘按钮 -->
|
<button
|
class="btn delete-tray-btn"
|
@click="deleteCurrentTray"
|
:disabled="deleteLoading"
|
>
|
<i class="icon icon-delete"></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, nextTick } from "vue";
|
import axios from "axios";
|
|
export default {
|
setup() {
|
// 基础数据定义
|
const finishedProduct = ref(""); // 左侧成品编号(接口返回)
|
const finishedProductId = ref(""); // 左侧成品ID(接口返回)
|
const rightTopInput = ref(""); // 右侧核心录入框
|
const leftPartCodes = ref(Array(10).fill("")); // 左侧零件编号数组
|
// 新增:左侧供方代码数组
|
const leftPartSupplierCodes = ref(Array(10).fill(""));
|
const rightPartCodes = ref(Array(10).fill("")); // 右侧零件编号数组
|
const leftPartChecked = ref(Array(10).fill(false)); // 左侧零件勾选状态
|
const leftPartIds = ref(Array(10).fill("")); // 左侧零件ID(接口返回)
|
const finishedProductCode = ref(""); // 右侧成品编号输入框
|
const fillIndex = ref(-1); // 录入框自动填充索引
|
const checkLoading = ref(Array(10).fill(false)); // 零件勾选加载锁
|
// 新增:删除托盘加载状态
|
const deleteLoading = ref(false);
|
|
// 关键修改2:定义录入框ref
|
const inputBoxRef = ref(null);
|
|
// 信号灯相关
|
const signalStates = ref([false, false, false, false, false]);
|
const signalLabels = ref([
|
"心跳信号",
|
"急停信号",
|
"自动运行信号",
|
"在线模式信号",
|
"故障信号",
|
]);
|
|
// 定时/防抖相关变量
|
let pollingTimer = null;
|
const pollingInterval = 5000; // 信号灯轮询间隔5秒
|
let checkDebounceTimer = null;
|
let destroyDelayTimer = null;
|
const destroyDelayTime = 500; // 录入框填充延迟500ms
|
let autoSubmitDebounceTimer = null;
|
let submitLock = ref(false); // 提交锁,防止重复提交
|
|
// 关键修改3:封装聚焦方法
|
const focusInputBox = () => {
|
nextTick(() => {
|
if (inputBoxRef.value) {
|
inputBoxRef.value.focus();
|
// 可选:选中输入框内容,方便直接覆盖输入
|
// inputBoxRef.value.select();
|
}
|
});
|
};
|
|
// 计算属性 - 统计有效勾选/填充数量(过滤空值)
|
const checkedCount = computed(() => {
|
return leftPartChecked.value.filter((checked) => checked === true).length;
|
});
|
const filledPartCount = computed(() => {
|
return rightPartCodes.value.filter((code) => code.trim() !== "").length;
|
});
|
|
// 获取左侧初始数据(成品+零件+勾选状态)
|
const fetchLeftInitialData = async () => {
|
try {
|
console.log("正在获取左侧初始数据...");
|
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] || "";
|
// 新增:赋值供方代码
|
leftPartSupplierCodes.value[i] = data.leftPartSupplierCodes?.[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);
|
} finally {
|
// 数据加载完成后聚焦
|
focusInputBox();
|
}
|
};
|
|
// 获取信号灯状态(定时轮询)
|
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;
|
}
|
} catch (error) {
|
console.error("获取信号状态失败:", error);
|
}
|
};
|
|
// 启动/停止信号灯轮询
|
const startPolling = () => {
|
if (pollingTimer) clearInterval(pollingTimer);
|
fetchSignalAndPLCStates();
|
pollingTimer = setInterval(fetchSignalAndPLCStates, pollingInterval);
|
};
|
const stopPolling = () => {
|
if (pollingTimer) clearInterval(pollingTimer);
|
pollingTimer = null;
|
};
|
|
// 左侧零件勾选状态变更处理(更新后端状态)
|
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;
|
focusInputBox(); // 提示后聚焦
|
return;
|
}
|
if (!partId) {
|
alert(`零件${index + 1}数据库ID不存在,无法更新扫码状态!`);
|
leftPartChecked.value[index] = !isChecked;
|
focusInputBox(); // 提示后聚焦
|
return;
|
}
|
if (!partCode.trim()) {
|
alert(`零件${index + 1}编号为空,无法更新扫码状态!`);
|
leftPartChecked.value[index] = !isChecked;
|
focusInputBox(); // 提示后聚焦
|
return;
|
}
|
|
checkLoading.value[index] = true;
|
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}扫码状态更新成功`);
|
} 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 {
|
checkLoading.value[index] = false;
|
if (checkDebounceTimer) clearTimeout(checkDebounceTimer);
|
checkDebounceTimer = null;
|
focusInputBox(); // 操作完成后聚焦
|
}
|
};
|
|
// 右侧录入框延迟填充逻辑(成品→零件依次填充)
|
const fillContent = () => {
|
if (!rightTopInput.value.trim()) return;
|
const inputValue = rightTopInput.value.trim();
|
|
if (destroyDelayTimer) clearTimeout(destroyDelayTimer);
|
destroyDelayTimer = setTimeout(() => {
|
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;
|
focusInputBox(); // 填充完成后聚焦
|
}, destroyDelayTime);
|
};
|
|
// 监听右侧录入框输入,触发自动填充
|
watch(
|
rightTopInput,
|
(newVal) => {
|
if (newVal.trim()) fillContent();
|
},
|
{ immediate: false }
|
);
|
|
// 关键修改4:失去焦点时重新聚焦
|
const handleInputBlur = () => {
|
// 延迟一点执行,避免和其他操作冲突
|
setTimeout(() => {
|
focusInputBox();
|
}, 50);
|
};
|
|
// 右侧输入框清除方法
|
const clearRightPart = (index) => {
|
rightPartCodes.value[index] = "";
|
focusInputBox(); // 清除后聚焦
|
};
|
const clearFinishedProductCode = () => {
|
finishedProductCode.value = "";
|
focusInputBox(); // 清除后聚焦
|
};
|
|
// 核心修改:恢复成品编号必填校验,未填写直接提示并终止提交
|
const saveData = async () => {
|
// 1. 成品编号必填校验【核心新增】
|
const productCode = finishedProductCode.value.trim();
|
if (!productCode) {
|
alert("请先填写成品编号,成品编号为必填项!");
|
focusInputBox(); // 提示后聚焦
|
return;
|
}
|
// 2. 提交锁:防止重复点击
|
if (submitLock.value) return;
|
submitLock.value = true;
|
|
try {
|
// 3. 传参适配:后端Dto是大驼峰字段!必须严格匹配(FinishedProductCode/PartsList)
|
const submitData = {
|
FinishedProductCode: productCode, // 直接使用已校验的非空值
|
PartsList: rightPartCodes.value.map((item) => item.trim()),
|
};
|
console.log("📤 提交到SaveToolingBoardNo的参数(匹配后端Dto):", submitData);
|
|
// 4. 调用后端接口,延长超时时间(后端有PLC交互,500ms休眠,设为10秒)
|
const response = await axios.post(
|
"/api/boxingDetail/SaveToolingBoardNo",
|
submitData,
|
{ timeout: 10000 }
|
);
|
const resData = response.data;
|
console.log("📥 SaveToolingBoardNo接口返回:", resData);
|
|
// 5. 适配后端返回格式:status为true表示成功,否则取message错误信息
|
if (resData.status === true) {
|
finishedProductCode.value = "";
|
rightPartCodes.value = Array(10).fill("");
|
rightTopInput.value = "";
|
fillIndex.value = -1;
|
const msg = resData.message;
|
|
// 核心修改:仅当 msg 不是 null 时才弹出提示
|
if (msg !== null) {
|
alert(msg);
|
}
|
} else {
|
alert(`${resData.message || "未知错误"}`);
|
}
|
} catch (error) {
|
// 6. 异常捕获:兼容后端未捕获异常(如PLC通讯异常、数组越界、500错误)
|
let errorMsg = "提交请求异常!";
|
if (error.code === "ECONNABORTED") {
|
errorMsg = "提交请求超时!后端PLC交互/数据库操作耗时过长";
|
} else if (error.response) {
|
// 服务器500错误:后端抛出未捕获异常(如PLC未连接、零件不匹配)
|
errorMsg = `服务器错误:${error.response.status} - ${error.response.statusText},请检查PLC通讯或配方配置`;
|
console.error("❌ 后端服务器错误详情:", error.response.data);
|
} else if (error.request) {
|
errorMsg = "网络异常!未收到后端响应,请检查接口地址和网络";
|
} else {
|
errorMsg = `请求错误:${error.message}`;
|
}
|
alert(errorMsg);
|
console.error("❌ 提交接口异常详情:", error);
|
} finally {
|
// 7. 释放提交锁:无论成功/失败都要释放
|
submitLock.value = false;
|
focusInputBox(); // 提交完成后聚焦
|
}
|
};
|
|
// 核心修改:自动提交逻辑同步增加成品编号必填校验(无成品编号不触发自动提交)
|
const checkAutoSubmit = () => {
|
if (autoSubmitDebounceTimer) clearTimeout(autoSubmitDebounceTimer);
|
autoSubmitDebounceTimer = setTimeout(() => {
|
const needCheckNum = checkedCount.value; // 左侧勾选零件数
|
const filledNum = filledPartCount.value; // 右侧有效填充零件数
|
const productCode = finishedProductCode.value.trim(); // 成品编号(去空格)
|
const hasProductCode = !!productCode; // 成品编号是否有效【保留原逻辑,实际是必填校验】
|
|
console.log(
|
`✅ 自动提交校验:勾选${needCheckNum}个 | 填充${filledNum}个 | 成品已填${hasProductCode}`
|
);
|
|
// 前置总校验:成品编号未填写,直接不触发任何自动提交【核心强化】
|
if (!hasProductCode) {
|
console.log("⚠️ 成品编号未填写,跳过自动提交");
|
autoSubmitDebounceTimer = null;
|
focusInputBox(); // 校验后聚焦
|
return;
|
}
|
|
// 场景1:左侧无勾选 → 仅成品编号有效即提交(已满足hasProductCode)
|
if (needCheckNum === 0) {
|
if (!submitLock.value) {
|
console.log("✅ 无零件勾选,成品已填,执行自动提交!");
|
saveData();
|
}
|
}
|
// 场景2:左侧有勾选 → 成品有效 + 填充数=勾选数 才提交(已满足hasProductCode)
|
else {
|
if (filledNum === needCheckNum && !submitLock.value) {
|
console.log("✅ 零件数量匹配,成品已填,执行自动提交!");
|
saveData();
|
}
|
}
|
autoSubmitDebounceTimer = null;
|
}, 300); // 300ms防抖,避免输入频繁触发
|
};
|
|
// 新增:删除当前托盘方法
|
const deleteCurrentTray = async () => {
|
// 确认操作,防止误删
|
if (!confirm("确认要删除当前托盘数据吗?此操作不可恢复!")) {
|
focusInputBox();
|
return;
|
}
|
|
// 设置加载状态,防止重复点击
|
deleteLoading.value = true;
|
try {
|
console.log("📤 调用删除当前托盘接口:/api/boxing/DeleteCurrentTray");
|
// 调用删除接口(根据实际需求选择GET/POST,这里默认用POST,若后端是GET可改为get)
|
const response = await axios.post(
|
"/api/boxing/DeleteCurrentTray",
|
{}, // 若无参数传空对象
|
{ timeout: 8000 }
|
);
|
const resData = response.data;
|
console.log("📥 删除当前托盘接口返回:", resData);
|
|
// 适配后端返回格式
|
const isSuccess = resData.status === true || resData.Status === true;
|
if (isSuccess) {
|
// 删除成功后清空右侧输入框
|
finishedProductCode.value = "";
|
rightPartCodes.value = Array(10).fill("");
|
rightTopInput.value = "";
|
fillIndex.value = -1;
|
alert(resData.message || "当前托盘删除成功!");
|
} else {
|
alert(`删除失败:${resData.message || "未知错误"}`);
|
}
|
} catch (error) {
|
// 异常处理
|
let errorMsg = "删除托盘请求异常!";
|
if (error.code === "ECONNABORTED") {
|
errorMsg = "删除请求超时!请稍后重试";
|
} else if (error.response) {
|
errorMsg = `服务器错误:${error.response.status} - ${error.response.statusText}`;
|
console.error("❌ 删除托盘服务器错误详情:", error.response.data);
|
} else if (error.request) {
|
errorMsg = "网络异常!未收到后端响应,请检查接口地址和网络";
|
} else {
|
errorMsg = `请求错误:${error.message}`;
|
}
|
alert(errorMsg);
|
console.error("❌ 删除托盘接口异常详情:", error);
|
} finally {
|
// 释放加载状态
|
deleteLoading.value = false;
|
focusInputBox(); // 操作完成后聚焦录入框
|
}
|
};
|
|
// 监听自动提交相关数据变化,触发校验
|
watch([checkedCount, filledPartCount, finishedProductCode], () => checkAutoSubmit(), {
|
deep: true,
|
immediate: false,
|
});
|
|
// 自动检测填充索引(成品填完后,指向第一个空零件框)
|
const detectFillIndex = () => {
|
if (!finishedProductCode.value.trim()) {
|
fillIndex.value = -1;
|
} else {
|
for (let i = 0; i < 10; i++) {
|
if (!rightPartCodes.value[i].trim()) {
|
fillIndex.value = i;
|
return;
|
}
|
}
|
fillIndex.value = 10;
|
}
|
};
|
|
// 监听成品/零件输入,更新填充索引
|
watch([finishedProductCode, () => [...rightPartCodes.value]], detectFillIndex, {
|
immediate: true,
|
deep: true,
|
});
|
|
// 生命周期:挂载时加载初始数据+启动轮询
|
onMounted(async () => {
|
await fetchLeftInitialData();
|
startPolling();
|
detectFillIndex();
|
focusInputBox(); // 页面挂载后立即聚焦
|
});
|
|
// 生命周期:卸载时清除所有定时器/防抖,防止内存泄漏
|
onUnmounted(() => {
|
stopPolling();
|
[checkDebounceTimer, destroyDelayTimer, autoSubmitDebounceTimer].forEach(
|
(t) => t && clearTimeout(t)
|
);
|
});
|
|
// 暴露模板所需属性/方法
|
return {
|
finishedProduct,
|
rightTopInput,
|
leftPartCodes,
|
// 新增:暴露供方代码数组
|
leftPartSupplierCodes,
|
rightPartCodes,
|
leftPartChecked,
|
signalStates,
|
signalLabels,
|
finishedProductCode,
|
checkLoading,
|
deleteLoading, // 暴露删除加载状态
|
handlePartCheck,
|
saveData,
|
deleteCurrentTray, // 暴露删除托盘方法
|
clearRightPart,
|
clearFinishedProductCode,
|
inputBoxRef, // 暴露ref
|
handleInputBlur, // 暴露方法
|
};
|
},
|
};
|
</script>
|
|
<style scoped>
|
/* 基础样式重置 */
|
* {
|
margin: 0;
|
padding: 0;
|
box-sizing: border-box;
|
font-family: "Microsoft Yahei", "PingFang SC", 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;
|
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);
|
}
|
|
/* 左侧顶部-信号灯区域 */
|
.left-top {
|
background: #f8fafc;
|
padding: 20px 15px;
|
border-radius: 12px;
|
flex-shrink: 0;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
.signal-status {
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
gap: 25px;
|
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%;
|
}
|
.input-submit-row {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
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;
|
}
|
|
/* 成品编号行样式 */
|
.right-bottom .finished-product-row {
|
margin-top: 16px;
|
background: #eff6ff;
|
border-left: 4px solid #3b82f6;
|
padding-top: 2px;
|
padding-bottom: 2px;
|
}
|
.left-bottom .finished-product-row {
|
background: #eff6ff;
|
border-left: 4px solid #3b82f6;
|
}
|
|
/* 零件项样式 */
|
.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;
|
}
|
|
/* 输入框样式 */
|
.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;
|
}
|
/* 新增:供方代码输入框样式(宽度稍窄) */
|
.supplier-code-input {
|
flex: 0 0 150px;
|
min-width: 120px;
|
}
|
.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;
|
}
|
/* 新增:删除托盘按钮宽度适配 */
|
.delete-tray-btn {
|
width: 140px !important;
|
}
|
.submit-input-btn {
|
width: 110px !important;
|
height: 42px !important;
|
flex: none !important;
|
padding: 0 15px !important;
|
font-size: 15px !important;
|
}
|
.submit-input-btn .icon {
|
width: 20px !important;
|
height: 20px !important;
|
}
|
.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-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");
|
}
|
/* 新增:删除图标 */
|
.icon-delete {
|
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 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z'/%3E%3C/svg%3E");
|
}
|
|
/* 按钮主题样式 */
|
.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);
|
}
|
/* 新增:删除托盘按钮样式 */
|
.delete-tray-btn {
|
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
color: #fff;
|
padding: 0 12px;
|
}
|
.delete-tray-btn:hover:not(:disabled) {
|
transform: translateY(-1px);
|
box-shadow: 0 4px 8px rgba(245, 158, 11, 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;
|
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;
|
}
|
.checkbox-container:has(.part-checkbox:disabled) {
|
opacity: 0.6;
|
cursor: not-allowed;
|
}
|
.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;
|
}
|
.signal-status {
|
flex-wrap: wrap;
|
justify-content: center;
|
}
|
}
|
|
@media (max-width: 768px) {
|
.form-row,
|
.input-submit-row {
|
flex-direction: column;
|
align-items: flex-start;
|
height: auto;
|
line-height: normal;
|
}
|
.label {
|
width: 100%;
|
text-align: left;
|
margin-bottom: 6px;
|
}
|
.input-box {
|
width: 100%;
|
}
|
/* 适配移动端供方代码输入框 */
|
.supplier-code-input {
|
flex: 1;
|
min-width: 100%;
|
margin-top: 6px;
|
}
|
/* 移动端按钮宽度适配 */
|
.btn,
|
.submit-input-btn,
|
.delete-tray-btn {
|
width: 100% !important;
|
margin-top: 6px;
|
}
|
.right-bottom .finished-product-row {
|
margin-top: 0 !important;
|
}
|
}
|
</style>
|