组件封装

This commit is contained in:
2025-09-22 17:45:53 +08:00
parent fb974f1100
commit 614e5ad34e
42 changed files with 4200 additions and 552 deletions

View File

View File

975
src/views/index.vue Normal file
View File

@@ -0,0 +1,975 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, reactive, nextTick } from 'vue';
import { useRealTime } from '@/utils/dateUtils';
import { checkOrderNumberApi, addProcessInfoApi } from '@/api/detect';
import type { Rule } from 'ant-design-vue/es/form';
// 检测设备表单规则
const rules: Record<string, Rule[]> = {
workOrderCode: [{ required: true, message: '请输入工单号', trigger: 'change' }],
productCode: [{ required: true, message: '请输入产品编码', trigger: 'change' }],
employeeCode: [{ required: true, message: '请输入员工号', trigger: 'change' }],
processName: [{ required: true, message: '请输入工序名称', trigger: 'change' }],
resourceName: [{ required: true, message: '请输入资源名称', trigger: 'change' }],
equipmentCode: [{ required: true, message: '请输入设备编码', trigger: 'change' }],
fixtureCode: [{ required: true, message: '请输入治具编码', trigger: 'change' }],
};
// 包装设备表单规则
const packageRules: Record<string, Rule[]> = {
customerSelection: [{ required: true, message: '请选择客户', trigger: 'change' }],
productModel: [{ required: true, message: '请输入产品型号', trigger: 'change' }],
employeeCode: [{ required: true, message: '请输入员工号', trigger: 'change' }],
processName: [{ required: true, message: '请输入工序名称', trigger: 'change' }],
resourceName: [{ required: true, message: '请输入资源名称', trigger: 'change' }],
equipmentCode: [{ required: true, message: '请输入设备编码', trigger: 'change' }],
fixtureCode: [{ required: true, message: '请输入治具编码', trigger: 'change' }],
packingResult: [{ required: true, message: '请选择装箱结果', trigger: 'change' }],
};
// 实时时间
const { currentTime } = useRealTime('HH:mm:ss');
const currentDate = ref('');
// 可编辑表单状态管理
const isDetectingEditMode = ref(false);
const isPackageEditMode = ref(false);
// 初始副本存储
let detectFormBackup: any = null;
let packageFormBackup: any = null;
// 表单引用
const detectFormRef = ref();
const packageFormRef = ref();
// 执行结果日志
const executionLogs = ref([
'14:25:32 - 开始检测流程 SN-A538-09-2023-00018',
'14:25:33 - 读取产品参数完成',
'14:25:36 - 尺寸检测: 通过 (0.15mm)',
'14:25:36 - 电压检测: 通过 (3.25V)',
'14:25:37 - 精度检测: 通过 (98.7%)',
'14:25:38 - 检测结果: 合格'
]);
const packageLogs = ref([
'14:25:45 - 包装流程: SN-A538-09-2023-00018',
'14:25:46 - 读取产品标签完成',
'14:25:49 - 包装材料准备完成',
'14:25:52 - 产品包装完成',
'14:25:53 - 更新库存: 已包装 (18/25)',
'14:25:54 - 包装结果: 正常'
]);
// 滚动到检测设备日志底部的函数
const scrollToExecutionBottom = () => {
const logsContainer = document.querySelector('.execution-logs-content');
if (logsContainer) {
logsContainer.scrollTop = logsContainer.scrollHeight;
}
};
// 底部操作按钮配置
const actionButtons = ref([
{ label: '工序过站', handler: 'handleProcessPass', type: 'primary' as const, status: 'idle' as const },
{ label: '复检序号检查', handler: 'handleRecheckSequence', type: 'default' as const, status: 'idle' as const },
{ label: '扫码', handler: 'handleScanCode', type: 'default' as const, status: 'idle' as const },
{ label: '序号检查', handler: 'handleSequenceCheck', type: 'default' as const, status: 'idle' as const },
{ label: '包装过站', handler: 'handlePackagePass', type: 'primary' as const, status: 'idle' as const }
]);
// 执行按钮操作
const executeButtonAction = (handlerName: string) => {
const handlers: { [key: string]: () => void } = {
handleOrderCheck,
handleProcessPass,
handleRecheckSequence,
handleSequenceCheck,
handlePackagePass
};
const handler = handlers[handlerName];
if (handler) {
handler();
}
};
// 日志记录工具函数
const addDetectionLog = (message: string) => {
const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false });
const logEntry = `${timestamp} - ${message}`;
executionLogs.value.push(logEntry);
// 自动滚动到底部
nextTick(() => {
scrollToExecutionBottom();
});
};
const addPackageLog = (message: string) => {
const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false });
const logEntry = `${timestamp} - ${message}`;
packageLogs.value.push(logEntry);
// 自动滚动到底部
nextTick(() => {
const logContainers = document.querySelectorAll('.log-container');
logContainers.forEach(container => {
container.scrollTop = container.scrollHeight;
});
});
};
// 清空检测设备执行日志
const clearExecutionLogs = () => {
executionLogs.value = [];
};
// 清空自动包装执行日志
const clearPackageLogs = () => {
packageLogs.value = [];
};
// 单号检查
const handleOrderCheck = () => {
checkOrderNumberApi({
workOrderCode: detectForm.workOrderCode,
productCode: detectForm.productCode
}).then(res => {
if (res) {
addDetectionLog('单号检查成功');
} else {
addDetectionLog('单号检查失败');
}
})
addDetectionLog('开始执行单号检查操作');
console.log('单号检查');
};
// 工序过站
const handleProcessPass = () => {
addDetectionLog('开始执行工序过站操作');
console.log('工序过站');
};
// 复检序号检查
const handleRecheckSequence = () => {
addPackageLog('开始执行复检序号检查操作');
console.log('复检序号检查');
};
// 序号检查
const handleSequenceCheck = () => {
addPackageLog('开始执行序号检查操作');
console.log('序号检查');
};
// 包装过站
const handlePackagePass = () => {
addPackageLog('开始执行包装过站操作');
console.log('包装过站');
};
// SSE事件处理函数
const handleL1Event = (data: any) => {
// L1_EVENT放到自动包装日志区
addPackageLog(`L1事件: ${JSON.stringify(data)}`);
};
const handleL4Event = (data: any) => {
// L4_EVENT放到检测设备日志区
addDetectionLog(`L4事件: ${JSON.stringify(data)}`);
if(data.code === 201) {
detectForm.confirmSequence = data.data
}
};
const handleMESEvent = (data: any) => {
// MES_EVENT放到检测设备日志区
addDetectionLog(`MES事件: ${JSON.stringify(data)}`);
};
const handleSseMessage = (data: string) => {
// 处理SSE原始消息如果需要的话
console.log('收到SSE消息:', data);
};
// Status handlers are now managed by individual modal components
// 检测设备表单数据
const detectForm = reactive({
workOrderCode: 'SKT202507220001',
productCode: 'JH1008611',
employeeCode: 'ZDXTEST',
processName: 'T-自动组装测试',
resourceName: 'T-A自动组装测试',
equipmentCode: 'JH.02.101.13-219',
fixtureCode: 'JH0001',
confirmSequence: '',
detectionData: '',
sequenceCount: 1
});
// 包装设备表单数据
const packageForm = reactive({
customerSelection: '上海电子科技有限公司',
productModel: 'A538-09-高精度传感器',
employeeCode: 'OP-7853',
processName: '自动包装',
resourceName: '包装台2号',
equipmentCode: 'EQ-PK-002',
fixtureCode: 'FX-PK-002-B',
packingResult: '正常',
sequenceNumber: 'SN-A538-09-2023-00018',
passStationCount: 18,
fullBoxCount: 25,
passStationSequence: 'SN-A538-09-2023-00018'
});
// 编辑模式切换
const toggleDetectingEditMode = () => {
if (!isDetectingEditMode.value) {
// 进入编辑模式,保存初始副本
detectFormBackup = JSON.parse(JSON.stringify(detectForm));
}
isDetectingEditMode.value = !isDetectingEditMode.value;
};
const togglePackageEditMode = () => {
if (!isPackageEditMode.value) {
// 进入编辑模式,保存初始副本
packageFormBackup = JSON.parse(JSON.stringify(packageForm));
}
isPackageEditMode.value = !isPackageEditMode.value;
};
// 保存检测设备编辑
const saveDetectingEdit = () => {
detectFormRef.value.validate().then(() => {
console.log('保存检测设备数据:', detectForm);
isDetectingEditMode.value = false;
// 保存成功,清除备份
detectFormBackup = null;
addProcessInfoApi({
workOrderCode: detectForm.workOrderCode,
productCode: detectForm.productCode,
employeeCode: detectForm.employeeCode,
processName: detectForm.processName,
resourceName: detectForm.resourceName,
equipmentCode: detectForm.equipmentCode,
fixtureCode: detectForm.fixtureCode
}).then(res => {
console.log(res)
})
}).catch(() => {
console.log('检测设备表单验证失败');
});
};
// 保存包装设备编辑
const savePackageEdit = () => {
packageFormRef.value.validate().then(() => {
console.log('保存包装设备数据:', packageForm);
isPackageEditMode.value = false;
// 保存成功,清除备份
packageFormBackup = null;
}).catch(() => {
console.log('包装设备表单验证失败');
});
};
// 取消检测设备编辑
const cancelDetectingEdit = () => {
isDetectingEditMode.value = false;
if (detectFormBackup) {
// 恢复到初始副本
Object.assign(detectForm, detectFormBackup);
detectFormBackup = null;
}
// 清除表单验证状态
detectFormRef.value?.clearValidate();
};
// 取消包装设备编辑
const cancelPackageEdit = () => {
isPackageEditMode.value = false;
if (packageFormBackup) {
// 恢复到初始副本
Object.assign(packageForm, packageFormBackup);
packageFormBackup = null;
}
// 清除表单验证状态
packageFormRef.value?.clearValidate();
};
// 初始化
onMounted(() => {
// 设置当前日期
const now = new Date();
currentDate.value = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
// SSE连接逻辑已迁移到SseStatus组件中
});
// 清理事件监听
onBeforeUnmount(() => {
// SSE断开连接逻辑已迁移到SseStatus组件中
});
</script>
<template>
<div class="hmi-container">
<!-- 顶部状态栏 -->
<div class="header-status">
<div class="left-info">
<span class="app-title">
<i-lucide-cpu class="app-icon" /> 工业控制系统 HMI
</span>
</div>
<div class="center-status">
<!-- MES状态 -->
<MesStatus />
<!-- 网络通讯状态 -->
<NetworkStatus />
<!-- PLC通讯状态 -->
<PlcStatus />
<!-- 摄像头状态 -->
<CameraStatus />
<!-- SSE 日志 -->
<SseStatus
:on-l1-event="handleL1Event"
:on-l4-event="handleL4Event"
:on-mes-event="handleMESEvent"
:on-sse-message="handleSseMessage"
/>
</div>
<SettingsModal />
<!-- <div class="right-info" @click="showSettings">
<span class="time-info">{{ currentTime }}</span>
<span class="date-info">{{ currentDate }}</span>
</div> -->
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 检测设备区域 -->
<div class="device-section detecting-section">
<div class="section-header">
<h3 class="section-title">
<i-lucide-monitor class="title-icon" />
检测设备
</h3>
<div class="edit-controls" v-if="!isDetectingEditMode">
<a-button size="small" @click="toggleDetectingEditMode" class="edit-btn">
<i-lucide-edit class="btn-icon" />
编辑
</a-button>
</div>
<div class="edit-controls" v-else>
<a-button size="small" type="primary" @click="saveDetectingEdit" class="save-btn">
<i-lucide-save class="btn-icon" />
保存
</a-button>
<a-button size="small" @click="cancelDetectingEdit" class="cancel-btn">
<i-lucide-x class="btn-icon" />
取消
</a-button>
</div>
</div>
<a-form :model="detectForm" :rules="rules" class="device-info" ref="detectFormRef" :colon="false"
hideRequiredMark :labelCol="{ span: 4 }" :wrapperCol="{ span: 20 }">
<a-row :gutter="32">
<a-col :span="12">
<a-form-item label="工单号" name="workOrderCode">
<a-input-group compact>
<a-input v-model:value="detectForm.workOrderCode" class="edit-input"
:disabled="!isDetectingEditMode" />
<a-button type="primary" @click="handleOrderCheck">单号检查</a-button>
</a-input-group>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="产品编码" name="productCode">
<a-input v-model:value="detectForm.productCode" class="edit-input"
:disabled="!isDetectingEditMode" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="员工工号" name="employeeCode">
<a-input v-model:value="detectForm.employeeCode" class="edit-input"
:disabled="!isDetectingEditMode" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="工序名称" name="processName">
<a-input v-model:value="detectForm.processName" class="edit-input"
:disabled="!isDetectingEditMode" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="资源名称" name="resourceName">
<a-input v-model:value="detectForm.resourceName" class="edit-input"
:disabled="!isDetectingEditMode" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="设备编码" name="equipmentCode">
<a-input v-model:value="detectForm.equipmentCode" class="edit-input"
:disabled="!isDetectingEditMode" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="治具编码" name="fixtureCode">
<a-input v-model:value="detectForm.fixtureCode" class="edit-input"
:disabled="!isDetectingEditMode" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="序号数量">
<a-input v-model:value="detectForm.sequenceCount" class="edit-input" disabled />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="确认序号" name="confirmSequence" :labelCol="{ span: 2 }" :wrapperCol="{ span: 22 }">
<a-input-group compact>
<a-input v-model:value="detectForm.confirmSequence" class="edit-input" disabled />
<a-button type="primary" @click="handleSequenceCheck">序号检查</a-button>
<a-button type="primary" @click="handleRecheckSequence" style="border-left: 1px solid #8395B6;">复检序号检查</a-button>
</a-input-group>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="检测数据" name="fixtureCode" :labelCol="{ span: 2 }" :wrapperCol="{ span: 22 }">
<a-input v-model:value="detectForm.detectionData" class="edit-input"
disabled />
</a-form-item>
</a-col>
</a-row>
</a-form>
<ExecutionResult title="执行结果" :logs="executionLogs" @clear="clearExecutionLogs" />
</div>
<!-- 自动包装区域 -->
<div class="device-section package-section">
<div class="section-header">
<h3 class="section-title">
<i-lucide-package class="title-icon" />
自动包装
</h3>
<div class="edit-controls" v-if="!isPackageEditMode">
<a-button size="small" @click="togglePackageEditMode" class="edit-btn">
<i-lucide-square-pen class="btn-icon" />
编辑
</a-button>
</div>
<div class="edit-controls" v-else>
<a-button size="small" type="primary" @click="savePackageEdit" class="save-btn">
<i-lucide-save class="btn-icon" />
保存
</a-button>
<a-button size="small" @click="cancelPackageEdit" class="cancel-btn">
<i-lucide-x class="btn-icon" />
取消
</a-button>
</div>
</div>
<a-form :model="packageForm" :rules="packageRules" class="device-info" ref="packageFormRef" :colon="false"
hideRequiredMark :labelCol="{ span: 4 }" :wrapperCol="{ span: 20 }">
<a-row :gutter="32">
<a-col :span="12">
<a-form-item label="客户选择" name="customerSelection">
<a-input v-model:value="packageForm.customerSelection" class="edit-input"
:disabled="!isPackageEditMode" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="产品型号" name="productModel">
<a-input v-model:value="packageForm.productModel" class="edit-input"
:disabled="!isPackageEditMode" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="工号" name="employeeCode">
<a-input v-model:value="packageForm.employeeCode" class="edit-input"
:disabled="!isPackageEditMode" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="工序名称" name="processName">
<a-input v-model:value="packageForm.processName" class="edit-input"
:disabled="!isPackageEditMode" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="资源名称" name="resourceName">
<a-input v-model:value="packageForm.resourceName" class="edit-input"
:disabled="!isPackageEditMode" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="设备编码" name="equipmentCode">
<a-input v-model:value="packageForm.equipmentCode" class="edit-input"
:disabled="!isPackageEditMode" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="治具编码" name="fixtureCode">
<a-input v-model:value="packageForm.fixtureCode" class="edit-input"
:disabled="!isPackageEditMode" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="装箱结果" name="packingResult">
<a-select v-model:value="packageForm.packingResult" class="edit-input"
:disabled="!isPackageEditMode">
<a-select-option value="正常">正常</a-select-option>
<a-select-option value="异常">异常</a-select-option>
<a-select-option value="待检">待检</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="序号" name="fixtureCode">
<a-input v-model:value="packageForm.sequenceNumber" class="edit-input" disabled />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="过站数量" name="fixtureCode">
<a-input v-model:value="packageForm.passStationCount" class="edit-input" disabled />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="满箱数量" name="fixtureCode">
<a-input v-model:value="packageForm.fullBoxCount" class="edit-input" disabled />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="过站序号" name="fixtureCode">
<a-input v-model:value="packageForm.passStationSequence" class="edit-input" disabled />
</a-form-item>
</a-col>
</a-row>
</a-form>
<ExecutionResult title="执行结果" :logs="packageLogs" @clear="clearPackageLogs" />
</div>
</div>
<!-- 底部操作按钮区域 -->
<!-- <ActionButtons :buttons="actionButtons" @execute="executeButtonAction" /> -->
</div>
</template>
<style scoped lang="scss">
@import '@/assets/styles/_variables.scss';
.hmi-container {
width: 100vw;
height: 100vh;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: $white;
font-family: $font-family;
display: flex;
flex-direction: column;
overflow: hidden;
font-size: clamp(14px, 1.4vw, 18px);
/* 响应式字体大小 */
@media (max-width: 1024px) {
font-size: clamp(12px, 1.7vw, 16px);
}
@media (min-width: 1025px) and (max-width: 1366px) {
font-size: clamp(14px, 1.5vw, 17px);
}
@media (min-width: 1367px) {
font-size: clamp(16px, 1.3vw, 20px);
}
}
// 响应式字体大小的具体元素
@media (max-width: 1024px) {
.app-title {
font-size: clamp(16px, 2.8vw, 20px) !important;
}
.status-label,
.status-value {
font-size: clamp(12px, 1.4vw, 14px) !important;
}
.section-header h3 {
font-size: clamp(16px, 2.0vw, 18px) !important;
}
.info-item .label,
.info-item .value {
font-size: clamp(12px, 1.4vw, 14px) !important;
}
.action-button {
font-size: clamp(12px, 1.4vw, 14px) !important;
}
}
@media (min-width: 1025px) and (max-width: 1366px) {
.app-title {
font-size: clamp(18px, 2.4vw, 20px) !important;
}
.status-label,
.status-value {
font-size: clamp(13px, 1.3vw, 15px) !important;
}
.section-header h3 {
font-size: clamp(17px, 1.8vw, 19px) !important;
}
.info-item .label,
.info-item .value {
font-size: clamp(13px, 1.3vw, 15px) !important;
}
.action-button {
font-size: clamp(14px, 1.3vw, 16px) !important;
}
}
@media (min-width: 1367px) {
.app-title {
font-size: clamp(20px, 2.0vw, 22px) !important;
}
.status-label,
.status-value {
font-size: clamp(14px, 1.2vw, 16px) !important;
}
.section-header h3 {
font-size: clamp(18px, 1.6vw, 20px) !important;
}
.info-item .label,
.info-item .value {
font-size: clamp(14px, 1.2vw, 16px) !important;
}
.action-button {
font-size: clamp(15px, 1.2vw, 17px) !important;
}
}
/* 顶部状态栏 */
.header-status {
height: 50px;
background: $bg-dark;
border-bottom: 2px solid $primary-color;
@include flex-between;
padding: 0 $spacing-lg;
flex-shrink: 0;
.left-info {
.app-title {
font-size: 18px;
font-weight: bold;
color: $white;
}
}
.center-status {
display: flex;
gap: $spacing-xxl;
align-items: center;
}
.status-button {
padding: $spacing-sm $spacing-md;
@include button-base;
@include button-hover($bg-overlay, rgba(255, 255, 255, 0.2), $white);
}
}
/* 主要内容区域 */
.main-content {
flex: 1;
display: flex;
gap: $spacing-lg;
padding: $spacing-lg;
overflow: hidden;
}
.device-section {
flex: 1;
background: $bg-overlay;
border: 2px solid $primary-color;
border-radius: $border-radius-lg;
padding: $spacing-lg;
display: flex;
flex-direction: column;
overflow: hidden;
}
.section-header {
margin-bottom: $spacing-md;
border-bottom: 1px solid $primary-color;
padding-bottom: 6px;
@include flex-between;
h3 {
margin: 0;
color: $white;
font-size: 18px;
font-weight: bold;
}
}
.section-title {
display: flex;
align-items: center;
gap: $spacing-sm;
margin: 0;
color: $white;
font-size: 18px;
font-weight: bold;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.title-icon {
width: 20px;
height: 20px;
filter: drop-shadow(0 2px 4px rgba(74, 144, 226, 0.3));
}
.app-icon {
width: 18px;
height: 18px;
color: $primary-color;
margin-right: $spacing-sm;
}
.status-icon {
width: 14px;
height: 14px;
margin-right: 6px;
color: $text-light;
}
.btn-icon {
width: 12px;
height: 12px;
margin-right: $spacing-xs;
}
.rotate {
animation: rotation 2s infinite linear;
/* 添加旋转动画 */
}
@keyframes rotation {
from {
transform: rotate(0deg);
/* 从 0 度开始 */
}
to {
transform: rotate(360deg);
/* 旋转到 360 度 */
}
}
/* 编辑控制按钮 */
.edit-controls {
display: flex;
gap: $spacing-sm;
}
.edit-btn {
@include status-button($bg-primary, $bg-primary-hover, $primary-color, $primary-light, $text-light, $white);
}
.save-btn {
@include status-button($success-bg, $success-bg-hover, $success-color, $success-light, $text-success, $white);
}
.cancel-btn {
@include status-button($error-bg, $error-bg-hover, $error-color, $error-light, $text-error, $white);
}
/* 编辑输入框样式 */
.edit-input {
background: $bg-input !important;
border: $border-light !important;
color: $white !important;
width: 100%;
&.bn {
border: none !important;
}
&:focus {
background: $bg-input-focus !important;
border-color: #fff !important;
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2) !important;
}
&.ant-input-disabled {
color: $text-light !important;
}
input {
background: transparent !important;
color: $white !important;
}
&.ant-select.ant-select-in-form-item {
border: none !important;
border-radius: 6px;
}
}
:deep(.ant-select.ant-select-disabled) {
.ant-select-selection-item {
color: $white;
}
.ant-select-selector {
border: $border-light !important;
}
.ant-select-arrow {
color: #bdbdbd;
}
}
.device-info {
flex-shrink: 0;
margin-bottom: $spacing-xl;
:deep(.ant-form-item-label > label) {
color: $text-light;
font-size: 16px;
}
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 5px;
min-height: 20px;
.value {
color: $white;
font-size: 14px;
font-weight: 500;
&.success {
color: $success-color;
}
}
}
/* SSE日志对话框样式 */
.sse-logs-dialog {
:deep(.ant-modal-content) {
background: rgba(30, 30, 45, 0.95);
border: 1px solid $primary-color;
}
.sse-controls {
display: flex;
gap: $spacing-sm;
margin-bottom: $spacing-md;
.sse-control-btn {
display: flex;
align-items: center;
.btn-icon {
margin-right: 4px;
}
}
}
}
/* 响应式设计 */
@media (max-width: 1600px) {
.main-content {
gap: $spacing-lg;
padding: $spacing-lg;
}
.device-section {
padding: $spacing-lg;
}
.info-item {
.label {
min-width: 70px;
font-size: 13px;
}
.value {
font-size: 13px;
}
}
}
@media (max-width: 1366px) {
.center-status {
gap: $spacing-xl;
}
.status-label,
.status-value {
font-size: 13px;
}
.main-content {
gap: 10px;
padding: 10px;
}
.device-section {
padding: $spacing-md;
}
}
@media (max-width: 1024px) {
.main-content {
flex-direction: column;
gap: 10px;
}
.device-section {
flex: 1;
height: calc(50vh - 100px);
}
}
/* Dialog样式已迁移到各个子组件中 */
:deep(.ant-input-group.ant-input-group-compact) {
display: flex;
.ant-btn[disabled] {
border-color: #8396B7;
color: #b4b4b4;
}
}
</style>

527
src/views/login.vue Normal file
View File

@@ -0,0 +1,527 @@
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import { login, getCaptcha } from '@/api/system'
import type { LoginInfo } from '@/api/system/model'
import type { Rule } from 'ant-design-vue/es/form';
import { setToken } from '@/utils/auth';
const router = useRouter()
// 表单数据
const formData = reactive<LoginInfo>({
username: '',
password: '',
uuid: '',
code: ''
})
// 验证码图片
const captchaImg = ref('')
const loading = ref(false)
const captchaLoading = ref(false)
// 表单验证规则
const rules: Record<string, Rule[]> = {
username: [
{ required: true, message: '请输入用户名', trigger: 'change' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'change' }
],
code: [
{ required: true, message: '请输入验证码', trigger: 'change' }
]
}
// 获取验证码
const refreshCaptcha = async () => {
try {
captchaLoading.value = true
await getCaptcha().then((res: any) => {
if (res.captchaOnOff) {
captchaImg.value = "data:image/gif;base64," + res.img
}
formData.uuid = res.uuid
formData.code = '' // 清空验证码输入
})
} catch (error) {
message.error('获取验证码失败')
console.error('获取验证码失败:', error)
} finally {
captchaLoading.value = false
}
}
// 登录处理
const handleLogin = async () => {
try {
loading.value = true
await login(formData).then(async (res: any) => {
if (res.code === 200) {
message.success('登录成功')
setToken(res.token)
// 可以在这里保存用户信息到store
// 跳转到主页
router.push('/')
} else {
message.error(res.msg || '登录失败')
// 登录失败后重新获取验证码
await refreshCaptcha()
}
})
} catch (error: any) {
message.error(error.message || error.msg || '登录失败,请检查网络连接')
console.error('登录失败:', error)
// 登录失败后重新获取验证码
await refreshCaptcha()
} finally {
loading.value = false
}
}
// 组件挂载时获取验证码
refreshCaptcha()
</script>
<template>
<div class="login-container">
<!-- 背景图 -->
<div class="login-background"></div>
<!-- 登录框 -->
<div class="login-box">
<div class="login-header">
<h2 class="login-title">
<i-lucide-cpu class="title-icon" />
工业控制系统 HMI
</h2>
<p class="login-subtitle">请登录您的账户</p>
</div>
<a-form
:model="formData"
:rules="rules"
@finish="handleLogin"
class="login-form"
layout="vertical"
>
<!-- 用户名 -->
<a-form-item name="username" class="form-item">
<a-input
v-model:value="formData.username"
placeholder="请输入用户名"
size="large"
class="login-input"
>
<template #prefix>
<i-lucide-user class="input-icon" />
</template>
</a-input>
</a-form-item>
<!-- 密码 -->
<a-form-item name="password" class="form-item">
<a-input-password
v-model:value="formData.password"
placeholder="请输入密码"
size="large"
class="login-input"
>
<template #prefix>
<i-lucide-lock class="input-icon" />
</template>
</a-input-password>
</a-form-item>
<!-- 验证码 -->
<a-form-item name="code" class="form-item">
<div class="captcha-container">
<a-input
v-model:value="formData.code"
placeholder="请输入验证码"
size="large"
class="login-input captcha-input"
>
<template #prefix>
<i-lucide-shield-check class="input-icon" />
</template>
</a-input>
<div class="captcha-image-container" @click="refreshCaptcha">
<img
v-if="captchaImg"
:src="captchaImg"
alt="验证码"
class="captcha-image"
/>
<div v-else class="captcha-loading">
<i-lucide-loader class="loading-icon" />
</div>
<div class="captcha-refresh-hint">
<i-lucide-refresh-cw class="refresh-icon" />
点击刷新
</div>
</div>
</div>
</a-form-item>
<!-- 登录按钮 -->
<a-form-item class="form-item">
<a-button
type="primary"
html-type="submit"
size="large"
:loading="loading"
class="login-button"
block
>
{{ loading ? '登录中...' : '登录' }}
</a-button>
</a-form-item>
</a-form>
</div>
</div>
</template>
<style scoped lang="scss">
// 变量定义
$primary-color: #4a90e2;
$primary-light: #5ba0f2;
$primary-dark: #3a7bd5;
$success-color: #52c41a;
$warning-color: #faad14;
$error-color: #ff4d4f;
$white: #ffffff;
$text-light: #b8d4f0;
$text-dark: #333333;
$bg-overlay: rgba(255, 255, 255, 0.1);
$bg-glass: rgba(255, 255, 255, 0.15);
$border-light: rgba(255, 255, 255, 0.3);
$shadow-light: rgba(0, 0, 0, 0.1);
$shadow-medium: rgba(0, 0, 0, 0.2);
$border-radius: 8px;
$border-radius-lg: 12px;
$spacing-sm: 8px;
$spacing-md: 16px;
$spacing-lg: 24px;
$spacing-xl: 32px;
$transition: all 0.3s ease;
.login-container {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.login-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('/bg.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
filter: blur(2px);
z-index: 1;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
135deg,
rgba(74, 144, 226, 0.3) 0%,
rgba(58, 123, 213, 0.4) 50%,
rgba(91, 160, 242, 0.3) 100%
);
}
}
.login-box {
position: relative;
z-index: 2;
width: 420px;
padding: $spacing-xl;
background: $bg-glass;
backdrop-filter: blur(20px);
border: 1px solid $border-light;
border-radius: $border-radius-lg;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.1),
0 4px 16px rgba(0, 0, 0, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
animation: slideInUp 0.6s ease-out;
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.login-header {
text-align: center;
margin-bottom: $spacing-xl;
.login-title {
display: flex;
align-items: center;
justify-content: center;
gap: $spacing-sm;
font-size: 24px;
font-weight: 600;
color: $white;
margin: 0 0 $spacing-sm 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
.title-icon {
font-size: 28px;
color: $primary-light;
}
}
.login-subtitle {
font-size: 14px;
color: $text-light;
margin: 0;
opacity: 0.9;
}
}
.login-form {
.form-item {
margin-bottom: $spacing-lg;
&:last-child {
margin-bottom: 0;
}
}
.login-input {
height: 48px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: $border-radius;
transition: $transition;
&:hover {
border-color: rgba(255, 255, 255, 0.4);
background: rgba(255, 255, 255, 0.15);
}
&:focus-within {
border-color: $primary-light;
background: rgba(255, 255, 255, 0.2);
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
}
:deep(.ant-input) {
background: transparent;
border: none;
color: $white;
font-size: 14px;
&::placeholder {
color: rgba(255, 255, 255, 0.6);
}
}
:deep(.ant-input-password-icon) {
color: rgba(255, 255, 255, 0.6);
&:hover {
color: $white;
}
}
}
.input-icon {
color: rgba(255, 255, 255, 0.6);
font-size: 16px;
}
}
.captcha-container {
display: flex;
gap: $spacing-md;
align-items: stretch;
.captcha-input {
flex: 1;
}
.captcha-image-container {
position: relative;
width: 120px;
height: 48px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: $border-radius;
overflow: hidden;
cursor: pointer;
transition: $transition;
background: rgba(255, 255, 255, 0.1);
&:hover {
border-color: $primary-light;
.captcha-refresh-hint {
opacity: 1;
}
}
.captcha-image {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.captcha-loading {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.1);
.loading-icon {
color: $white;
font-size: 20px;
animation: spin 1s linear infinite;
}
}
.captcha-refresh-hint {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: $white;
font-size: 12px;
opacity: 0;
transition: $transition;
.refresh-icon {
font-size: 16px;
margin-bottom: 2px;
}
}
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.login-button {
height: 48px;
background: linear-gradient(135deg, $primary-color 0%, $primary-dark 100%);
border: none;
border-radius: $border-radius;
font-size: 16px;
font-weight: 600;
transition: $transition;
box-shadow: 0 4px 12px rgba(74, 144, 226, 0.3);
&:hover {
background: linear-gradient(135deg, $primary-light 0%, $primary-color 100%);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(74, 144, 226, 0.4);
}
&:active {
transform: translateY(0);
}
.button-icon {
margin-right: $spacing-sm;
font-size: 18px;
}
:deep(.ant-btn-loading-icon) {
margin-right: $spacing-sm;
}
}
// 表单验证错误样式
:deep(.ant-form-item-has-error) {
.login-input {
border-color: $error-color;
&:focus-within {
box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.2);
}
}
.captcha-image-container {
border-color: $error-color;
}
}
:deep(.ant-form-item-explain-error) {
color: rgba(255, 255, 255, 0.9);
background: rgba(255, 77, 79, 0.1);
padding: 4px 8px;
border-radius: 4px;
margin-top: 4px;
font-size: 12px;
}
// 响应式设计
@media (max-width: 768px) {
.login-box {
width: 90%;
max-width: 380px;
padding: $spacing-lg;
}
.captcha-container {
flex-direction: column;
.captcha-image-container {
width: 100%;
height: 60px;
}
}
}
@media (max-width: 480px) {
.login-box {
padding: $spacing-md;
}
.login-header .login-title {
font-size: 20px;
.title-icon {
font-size: 24px;
}
}
}
</style>

View File

@@ -0,0 +1,684 @@
<template>
<div class="package-station-container">
<!-- 顶部控制面板 -->
<a-form class="control-panel">
<!-- 左侧箱号 -->
<div class="control-left">
<label class="control-label">箱号</label>
<div class="control-input-group">
<a-input
v-model:value="boxNumber"
placeholder="请输入箱号"
class="control-input"
/>
<a-button
type="primary"
@click="applyBoxNumber"
class="apply-btn"
>
申请箱号
</a-button>
</div>
</div>
<!-- 中间包装规格本箱累计 -->
<div class="control-center">
<div class="control-row">
<div class="control-item">
<label class="control-label">包装规格</label>
<div class="control-input-group">
<a-input
v-model:value="packageSpec"
placeholder="请输入包装规格"
class="control-input"
suffix="个"
/>
<a-button
type="primary"
@click="updateSpec"
class="update-btn"
>
更新
</a-button>
</div>
</div>
<div class="control-item">
<label class="control-label">本箱累计</label>
<div class="total-display">{{ totalCount }}</div>
</div>
</div>
</div>
<!-- 右侧打印箱签提交装箱 -->
<div class="control-right">
<a-button
type="primary"
@click="printBoxLabel"
:disabled="!boxNumber"
class="action-btn print-btn"
>
<template #icon>
<PrinterOutlined />
</template>
打印箱签
</a-button>
<a-button
type="primary"
@click="submitPackage"
:disabled="!boxNumber || scannedItems.length === 0"
class="action-btn submit-btn"
>
<template #icon>
<BoxPlotOutlined />
</template>
提交装箱
</a-button>
</div>
</a-form>
<!-- 已扫描件号表格 -->
<div class="table-section">
<a-table
:columns="columns"
:data-source="scannedItems"
:pagination="pagination"
:scroll="{ y: 500 }"
bordered
class="scanned-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-button
type="link"
danger
@click="deleteItem(record.key)"
class="delete-btn"
>
<DeleteOutlined />
</a-button>
</template>
</template>
</a-table>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import { PrinterOutlined, BoxPlotOutlined, DeleteOutlined } from '@ant-design/icons-vue';
const router = useRouter();
// 响应式数据
const boxNumber = ref<string>('');
const packageSpec = ref<string>('175');
const scannedItems = ref<any[]>([
{ key: '1', itemCode: 'GK-2022-0001', specification: '175', quantity: 2, operationTime: '2023-07-15 14:32:45', operator: '张工' },
{ key: '2', itemCode: 'GK-2022-0002', specification: '175', quantity: 1, operationTime: '2023-07-15 14:35:12', operator: '张工' },
{ key: '3', itemCode: 'GK-2022-0015', specification: '175', quantity: 3, operationTime: '2023-07-15 14:38:27', operator: '张工' },
{ key: '4', itemCode: 'GK-2022-0023', specification: '175', quantity: 1, operationTime: '2023-07-15 14:41:53', operator: '李工' },
{ key: '5', itemCode: 'GK-2022-0037', specification: '175', quantity: 2, operationTime: '2023-07-15 14:45:18', operator: '李工' },
{ key: '6', itemCode: 'GK-2022-0042', specification: '175', quantity: 1, operationTime: '2023-07-15 14:48:36', operator: '王工' },
{ key: '7', itemCode: 'GK-2022-0055', specification: '175', quantity: 2, operationTime: '2023-07-15 14:52:09', operator: '王工' },
{ key: '8', itemCode: 'GK-2022-0078', specification: '175', quantity: 3, operationTime: '2023-07-15 14:56:24', operator: '赵工' },
]);
// 计算属性
const totalCount = computed(() => scannedItems.value.length);
// 分页配置
const pagination = {
current: 1,
pageSize: 10,
total: scannedItems.value.length,
showSizeChanger: false,
showQuickJumper: false,
showTotal: () => null,
};
// 表格列定义
const columns = [
{
title: '序号',
dataIndex: 'key',
key: 'key',
width: 80,
align: 'center' as const,
},
{
title: '件号',
dataIndex: 'itemCode',
key: 'itemCode',
width: 200,
align: 'center' as const,
},
{
title: '规格',
dataIndex: 'specification',
key: 'specification',
width: 180,
align: 'center' as const,
},
{
title: '数量',
dataIndex: 'quantity',
key: 'quantity',
width: 100,
align: 'center' as const,
},
{
title: '操作时间',
dataIndex: 'operationTime',
key: 'operationTime',
width: 180,
align: 'center' as const,
},
{
title: '操作人',
dataIndex: 'operator',
key: 'operator',
width: 100,
align: 'center' as const,
},
{
title: '操作',
key: 'action',
width: 80,
fixed: 'right' as const,
align: 'center' as const,
},
];
// 方法
const applyBoxNumber = () => {
// 生成箱号逻辑
const timestamp = new Date().getTime();
boxNumber.value = `BOX${timestamp}`;
};
const printBoxLabel = () => {
console.log('打印箱签:', boxNumber.value);
// 这里可以添加打印逻辑
};
const updateSpec = () => {
console.log('更新包装规格:', packageSpec.value);
// 这里可以添加更新规格的逻辑
};
const submitPackage = () => {
console.log('提交装箱:', {
boxNumber: boxNumber.value,
packageSpec: packageSpec.value,
totalCount: totalCount.value,
items: scannedItems.value
});
// 这里可以添加提交逻辑
};
const deleteItem = (key: string) => {
scannedItems.value = scannedItems.value.filter(item => item.key !== key);
};
const goBack = () => {
router.go(-1);
};
</script>
<style scoped lang="scss">
.package-station-container {
height: 100vh;
background-color: #1E293B;
color: #fff;
padding: 20px;
font-family: 'Microsoft YaHei', sans-serif;
display: flex;
flex-direction: column;
overflow: hidden;
.control-panel {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 40px;
margin-bottom: 20px;
padding: 20px 0;
border-bottom: 2px solid #475569;
// 左侧:箱号
.control-left {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
.control-label {
font-size: 18px;
font-weight: bold;
color: #fff;
margin: 0;
}
.control-input-group {
display: flex;
align-items: center;
gap: 12px;
.control-input {
flex: 1;
max-width: 200px;
:deep(.ant-input) {
background-color: #fff;
border-color: #d1d5db;
color: #1f2937;
font-size: 16px;
height: 40px;
border-radius: 6px;
&::placeholder {
color: #9ca3af;
}
&:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}
}
}
.apply-btn {
background-color: #3b82f6;
border-color: #3b82f6;
font-size: 16px;
height: 40px;
padding: 0 20px;
border-radius: 6px;
&:hover {
background-color: #2563eb;
border-color: #2563eb;
}
&:disabled {
background-color: #64748b;
border-color: #64748b;
}
}
}
}
// 中间:包装规格、本箱累计
.control-center {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
.control-row {
display: flex;
gap: 30px;
.control-item {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
.control-label {
font-size: 18px;
font-weight: bold;
color: #fff;
margin: 0;
}
.control-input-group {
display: flex;
align-items: center;
gap: 12px;
.control-input {
flex: 1;
max-width: 150px;
:deep(.ant-input) {
background-color: #fff;
border-color: #d1d5db;
color: #1f2937;
font-size: 16px;
height: 40px;
border-radius: 6px;
&::placeholder {
color: #9ca3af;
}
&:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}
}
}
.update-btn {
background-color: #8b5cf6;
border-color: #8b5cf6;
font-size: 16px;
height: 40px;
padding: 0 20px;
border-radius: 6px;
&:hover {
background-color: #7c3aed;
border-color: #7c3aed;
}
&:disabled {
background-color: #64748b;
border-color: #64748b;
}
}
}
.total-display {
width: 150px;
height: 40px;
background-color: #475569;
border: 2px solid #64748b;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 18px;
font-weight: bold;
}
}
}
}
// 右侧:打印箱签、提交装箱
.control-right {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
.action-btn {
flex: 1;
height: 40px;
padding: 0 20px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
border-radius: 6px;
&.print-btn {
background-color: #10b981;
border-color: #10b981;
&:hover {
background-color: #059669;
border-color: #059669;
}
&:disabled {
background-color: #64748b;
border-color: #64748b;
}
}
&.submit-btn {
background-color: #f59e0b;
border-color: #f59e0b;
&:hover {
background-color: #d97706;
border-color: #d97706;
}
&:disabled {
background-color: #64748b;
border-color: #64748b;
}
}
}
}
}
.table-section {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
.scanned-table {
flex: 1;
display: flex;
flex-direction: column;
:deep(.ant-table) {
background-color: transparent;
color: #fff;
flex: 1;
display: flex;
flex-direction: column;
.ant-table-container {
flex: 1;
display: flex;
flex-direction: column;
}
.ant-table-content {
flex: 1;
overflow: auto;
}
.ant-table-thead > tr > th {
background-color: #475569;
border-color: #64748b;
color: #fff;
font-size: 16px;
font-weight: bold;
padding: 16px 12px;
position: sticky;
top: 0;
z-index: 1;
}
.ant-table-tbody > tr > td {
background-color: #475569;
border-color: #64748b;
color: #e2e8f0;
font-size: 15px;
padding: 12px;
}
.ant-table-tbody > tr:nth-child(even) > td {
background-color: #4a5568;
}
.ant-table-tbody > tr:hover > td {
background-color: #64748b !important;
}
.ant-table-pagination {
background-color: #475569;
padding: 16px;
margin: 0;
border-top: 1px solid #64748b;
flex-shrink: 0;
.ant-pagination-item {
background-color: #475569;
border-color: #64748b;
color: #fff;
font-size: 14px;
&:hover {
background-color: #64748b;
}
&.ant-pagination-item-active {
background-color: #3b82f6;
border-color: #3b82f6;
}
}
.ant-pagination-prev,
.ant-pagination-next {
background-color: #475569;
border-color: #64748b;
color: #fff;
&:hover {
background-color: #64748b;
}
}
}
}
.delete-btn {
color: #ef4444;
font-size: 16px;
padding: 0;
height: auto;
&:hover {
color: #dc2626;
}
}
}
}
}
// 响应式设计
@media (max-width: 1200px) {
.package-station-container {
.control-panel {
gap: 20px;
.control-left,
.control-center,
.control-right {
.control-label {
font-size: 16px;
}
.control-input-group {
.control-input {
:deep(.ant-input) {
font-size: 14px;
height: 36px;
}
}
.apply-btn,
.update-btn {
font-size: 14px;
height: 36px;
padding: 0 16px;
}
}
.total-display {
width: 130px;
height: 36px;
font-size: 16px;
}
}
.control-center {
.control-row {
gap: 20px;
.control-item {
.control-input-group {
.control-input {
max-width: 120px;
}
}
.total-display {
width: 120px;
}
}
}
}
.control-right {
gap: 10px;
.action-btn {
font-size: 14px;
height: 36px;
padding: 0 16px;
}
}
}
.table-section {
.scanned-table {
:deep(.ant-table) {
.ant-table-thead > tr > th {
font-size: 14px;
padding: 12px 8px;
}
.ant-table-tbody > tr > td {
font-size: 13px;
padding: 8px;
}
}
}
}
}
}
@media (max-width: 768px) {
.package-station-container {
padding: 10px;
.control-panel {
flex-direction: column;
gap: 15px;
.control-left,
.control-center,
.control-right {
width: 100%;
flex-direction: column;
gap: 10px;
.action-btn {
width: 100%;
}
}
.control-center {
.control-row {
flex-direction: column;
gap: 15px;
.control-item {
.control-input-group {
.control-input {
max-width: none;
}
}
.total-display {
width: 100%;
}
}
}
}
}
}
}
</style>