Ver Fonte

feat: 优化指纹录入和门禁控制功能

fix: 修复灯光配置常量命名错误问题

refactor: 重构门禁操作逻辑并添加状态检查功能

style: 更新物料入库确认页面的提示文本

feat: 添加入库离开确认页面及逻辑

fix: 修正用户主页按钮显示逻辑

feat: 在各操作完成时自动触发开门操作

refactor: 合并出入库离开页面为统一离开页面

docs: 移除不再使用的路由组件引用

fix: 修复密码修改失败提示信息不完整问题
yangzhijie há 5 meses atrás
pai
commit
960f6799d8

+ 7 - 0
src/Fingerprint/FingerprintEnroll.vue

@@ -281,6 +281,13 @@ const handleFingerprintResponse = data => {
 
     if (code === 0) {
         statusText.value = msg || '指纹录入成功!';
+
+        // 返回登录界面
+        router.push('/');
+
+
+
+
         if (msg === '上传成功' || msg === '当前用户已经录入过指纹,更新成功。') {
             isScanning.value = false;
         }

+ 4 - 0
src/finishProduct/FinishProductIn.vue

@@ -189,6 +189,7 @@ import { showNotify } from 'vant';
 import { generateStockIn, getLocatorList } from '../api/finishProduct';
 import vSelect from 'vue-select';
 import 'vue-select/dist/vue-select.css';
+import { gateController } from '../hardware/GateOperate.js';
 
 // 图片资源
 import bgImg from '../assets/images/bj.png';
@@ -297,6 +298,9 @@ const executeInbound = async () => {
     try {
         const res = await generateStockIn(params);
         if (res.errorCode === 0) {
+            // 调用开门操作
+            gateController('SHOTOPEN');
+
             showNotify({ type: 'success', message: `已提交 ${count} 条成品入库记录` });
         } else {
             showNotify({ type: 'danger', message: res.errorMessage || '添加失败' });

+ 3 - 0
src/finishProduct/FinishProductOut.vue

@@ -171,6 +171,7 @@ import { getWarehouseList } from '../api/stock.js';
 import { queryFinishProduct, generateStockOut } from '../api/finishProduct.js';
 import vSelect from 'vue-select';
 import 'vue-select/dist/vue-select.css';
+import { gateController } from '../hardware/GateOperate.js';
 
 // 图片资源
 import bgImg from '../assets/images/bj.png';
@@ -362,6 +363,8 @@ const executeOutbound = async () => {
     try {
         const res = await generateStockOut(selectedProducts.value);
         if (res.errorCode === 0) {
+            // 调用开门操作
+            gateController('SHOTOPEN');
             showNotify({ type: 'success', message: '出库成功' });
             selectedProducts.value = [];
             await getList(); // 重新加载列表

+ 21 - 5
src/hardware/GateOperate.js

@@ -20,16 +20,16 @@ const gateController = command => {
         // 根据命令类型控制对应的灯光状态
         if (command === 'OPEN' || command === 'SHOTOPEN') {
             // 开门命令:控制灯光显示开门状态(绿灯常亮)
-            plugin.gateConfig.controlLight(LIGHT_OPEN);
+            plugin.gateConfig.controlLight(JSON.stringify(LIGHT_OPEN));
         } else if (command === 'CLOSE') {
             // 关门命令:控制灯光显示关门状态(红灯常亮)
-            plugin.gateConfig.controlLight(LIGHT_CLOSE);
+            plugin.gateConfig.controlLight(JSON.stringify(LIGHT_CLOSE));
         } else if (command === 'REBOOT') {
             // 重启命令:控制灯光显示重启状态(黄灯闪烁)
-            plugin.gateConfig.controlLight(LIGHT_RESTART);
+            plugin.gateConfig.controlLight(JSON.stringify(LIGHT_RESTART));
         } else if (command === 'ALARM') {
             // 报警命令:控制灯光显示报警状态(红灯闪烁)
-            plugin.gateConfig.controlLight(LIGHT_ALARM);
+            plugin.gateConfig.controlLight(JSON.stringify(LIGHT_ALARM));
         } else {
             // 未知命令处理
             console.warn(`Unknown gate control command: ${command}`);
@@ -42,5 +42,21 @@ const gateController = command => {
     }
 };
 
+
+const isGateOpen = () => {
+    // 调用插件API查询当前大门状态
+    const gateStatus = plugin.gateConfig.getGateStatus();
+
+    // '关闸到位';
+    // '左开到位';
+    // '右开到位';
+    // '运动中或者未到位';
+    // '闸门常开';
+    // '闸机自检中';
+    // '未知门闸状态';
+
+    return !(gateStatus === '关闸到位');
+};
+
 // 导出函数供其他模块使用
-export { gateController };
+export { gateController, isGateOpen };

+ 2 - 2
src/hardware/Light.js

@@ -21,7 +21,7 @@ const LIGHT_NORMAL = {
  * 用于闸机开门允许通行状态
  * 效果:流动灯 - 白色单向流动效果
  */
-const LIGHT_OPEN = {
+const LIGHT_CLOSE = {
     'lightEffect': 'FLOW',     // 流动灯效果
     'flowParams': {
         'color': 0xFFFFFF,      // 流动灯颜色 (白色: #FFFFFF)
@@ -35,7 +35,7 @@ const LIGHT_OPEN = {
  * 用于闸机关闭禁止通行状态
  * 效果:反向流动灯 - 白色反向流动效果
  */
-const LIGHT_CLOSE = {
+const LIGHT_OPEN = {
     'lightEffect': 'FLOW_RESERVE', // 反向流动灯效果
     'flowParams': {
         'color': 0xFFFFFF,      // 流动灯颜色 (白色: #FFFFFF)

+ 2 - 2
src/login/FingerprintLogin.vue

@@ -226,7 +226,7 @@ const startRecognize = () => {
 const handleRestart = () => {
     userInfo.value = null;
     isConnected.value = true;
-    updateStatus('waiting', '指纹重启中,请稍后...');
+    updateStatus('waiting', '指纹模块启动中,请稍候...');
 
     setTimeout(() => {
         // startRecognize();
@@ -297,7 +297,7 @@ onMounted(() => {
 
     // 延迟500ms后开始识别
     setTimeout(() => {
-        startRecognize();
+        handleRestart();
     }, 500);
 });
 

+ 51 - 14
src/login/UserHome.vue

@@ -248,8 +248,8 @@
         </div>
         <div class="modal-footer">
           <button class="modal-btn cancel-btn" @click="closeBigTransportModal">取消</button>
-          <button class="modal-btn confirm-btn" @click="selectTransportType('OPEN')">大件运输常开</button>
-          <button class="modal-btn confirm-btn" @click="selectTransportType('CLOSE')">关闭常开</button>
+          <button v-if="!gateOpen" class="modal-btn confirm-btn" @click="selectTransportType('OPEN')">大件运输常开</button>
+          <button v-if="gateOpen" class="modal-btn confirm-btn" @click="selectTransportType('CLOSE')">关闭常开</button>
         </div>
       </div>
     </div>
@@ -311,6 +311,7 @@ import arrowUpIcon from '../assets/images/up.png';
 import arrowDownIcon from '../assets/images/down.png';
 import headerImg from '../assets/images/header.png';
 import bottomImg from '../assets/images/bottom.png';
+import { gateController, isGateOpen } from '../hardware/GateOperate.js';
 
 import { getStaticInfo, getUserRole } from '../api/login.js';
 
@@ -379,6 +380,12 @@ const config = ref({
 
 let infoTimer = null;
 
+let gateStatusTimer = null;
+
+
+// 门禁打开的状态
+const gateOpen = ref(false);
+
 // 从 localStorage 获取用户名和isOut状态
 onMounted(() => {
     const loginInfo = localStorage.getItem('#LoginInfo');
@@ -409,6 +416,16 @@ onMounted(() => {
     setTimeout(() => {
         checkAbnormalMessage();
     }, 1000);
+
+
+    // 每隔1秒查询一次大门的状态isGateOpen
+
+    gateStatusTimer = window.setInterval(() => {
+        gateOpen.value = isGateOpen();
+        // console.log('gateOpen.value:', gateOpen.value);
+    }, 1000);
+
+
 });
 
 onUnmounted(() => {
@@ -416,6 +433,10 @@ onUnmounted(() => {
     document.removeEventListener('click', handleClickOutside);
     clearInterval(infoTimer);
     infoTimer = null;
+
+    // 清除查询大门状态的定时器
+    clearInterval(gateStatusTimer);
+    gateStatusTimer = null;
 });
 
 // 统计数据
@@ -473,7 +494,7 @@ const goToAbnormalArea = () => {
 const outButtons = reactive([
     { label: '领料', action: 'materialOut' },
     { label: '拣货', action: 'materialReturn' },
-    { label: '还料', action: 'materialIn' },
+    { label: '入库', action: 'materialIn' },
     // { label: 'AGV RFID\n校验', action: 'agvRfidRecognition' },
     { label: '成品\n入库', action: 'finishedProductIn' },
     { label: '大件\n运输', action: 'bigProductTransport' },
@@ -481,8 +502,9 @@ const outButtons = reactive([
 
 // 内侧屏幕操作按钮
 const inButtons = reactive([
-    { label: '出库\n确认', action: 'materialOutLeave' },
-    { label: '入库\n离开', action: 'materialInLeave' },
+    // { label: '出库\n确认', action: 'materialOutLeave' },
+    // { label: '入库\n离开', action: 'materialInLeave' },
+    { label: '离开', action: 'materialInOutLeave' },
     { label: '成品\n出库', action: 'finishedProductOut' },
     { label: '大件\n运输', action: 'bigProductTransport' },
     // { label: '料箱\n上架', action: 'feedingArea' },
@@ -586,12 +608,16 @@ const handleAction = action => {
         router.push('/finish-product-in');
         break;
     // 内侧屏幕操作
-    case 'materialOutLeave':
-        router.push('/outbound-confirm');
-        break;
-    case 'materialInLeave':
-        router.push('/returned-leave');
+    // case 'materialOutLeave':
+    //     router.push('/outbound-confirm');
+    //     break;
+    // case 'materialInLeave':
+    //     router.push('/returned-leave');
+    //     break;
+    case 'materialInOutLeave':
+        router.push('/material-in-out-leave');
         break;
+        
     case 'finishedProductOut':
         router.push('/finish-product-out');
         break;
@@ -710,9 +736,14 @@ const handleControl = async (command, actionName) => {
         if (res.errorCode === 0) {
             showNotify({ type: 'success', message: `${actionName}操作成功`, duration: 2000 });
             if (actionName.includes('关闭')) {
-                plugin.soundPlay.playClose();
+                gateController('CLOSE');
+                // 后端调用了
+                // plugin.soundPlay.playClose();
             } else if (actionName.includes('开')) {
-                plugin.soundPlay.playOpen();
+                // 打开闸机
+                gateController('OPEN');
+                // 后端调用了
+                // plugin.soundPlay.playOpen();
             }
         } else {
             showNotify({ type: 'danger', message: res.errorMessage || `${actionName}操作失败`, duration: 3000 });
@@ -814,11 +845,11 @@ const handleUpdatePassword = async () => {
             showNotify({ type: 'success', message: '密码修改成功' });
             router.push('/login');
         } else {
-            showNotify({ type: 'danger', message: res.errorMessage || '密码修改失败' });
+            showNotify({ type: 'danger', message: res || res.errorMessage || '密码修改失败' });
         }
     } catch (error) {
         console.error('密码修改失败:', error);
-        showNotify({ type: 'danger', message: '密码修改失败' });
+        showNotify({ type: 'danger', message:  '密码修改失败' });
     }
 };
 
@@ -852,6 +883,12 @@ const confirmBigTransport = async () => {
     closeBigTransportConfirmModal();
 
 };
+
+
+
+
+
+
 </script>
 
 <style scoped>

+ 9 - 4
src/router/routes.js

@@ -11,9 +11,12 @@ const StockPickingCar = () => import('../stock/StockPickingCar.vue');
 // 拣货管理
 const OrderPicking = () => import('../stock-out/OrderPicking.vue');
 // 出库确认
-const OutboundConfirm = () => import('../stock-out/OutboundConfirm.vue');
+// const OutboundConfirm = () => import('../stock-out/OutboundConfirm.vue');
 // 还料离开
-const ReturnedLeave = () => import('../stock-in/ReturnedLeave.vue');
+// const ReturnedLeave = () => import('../stock-in/ReturnedLeave.vue');
+// 离开
+const MaterialInOutConfirm = () => import('../stock-out/MaterialInOutConfirm.vue');
+
 // AGV RFID识别
 const AgvRfidRecognition = () => import('../stock-out/AgvRfidRecognition.vue');
 // 入库确认
@@ -51,8 +54,10 @@ const routes = [
     { path: '/login', component: UserLogin, meta: { title: '用户登录' } },
     { path: '/home', component: UserHome, meta: { title: '首页' } },
     { path: '/order-picking', component: OrderPicking, meta: { title: '拣货管理' } },
-    { path: '/outbound-confirm', component: OutboundConfirm, meta: { title: '出库确认' } },
-    { path: '/returned-leave', component: ReturnedLeave, meta: { title: '还料离开' } },    
+    // { path: '/outbound-confirm', component: OutboundConfirm, meta: { title: '出库确认' } },
+    { path: '/material-in-out-leave', component: MaterialInOutConfirm, meta: { title: '离开' } },
+
+    // { path: '/returned-leave', component: ReturnedLeave, meta: { title: '还料离开' } },    
     { path: '/stock-picking-car', component: StockPickingCar, meta: { title: '领料车' } },
     { path: '/stock-requisition', component: StockRequisition, meta: { title: '领料管理' } },
     { path: '/regular-requisition', component: RegularRequisition, meta: { title: '常用领料' } },

+ 1 - 1
src/stock-in/ReturnedLeave.vue

@@ -24,7 +24,7 @@
     <main class="main-content">
       <!-- 统计信息 -->
       <div v-if="materialList.length > 0" class="stats-section">
-        <span class="stats-text">共 {{ materialList.length }} 条数据</span>
+        <span class="stats-text">您共有 {{ materialList.length }} 个物料入库,请确认物料已经放置到对应的货位,确认无误以后,再点击【入库离开】按钮。</span>
       </div>
 
       <!-- 卡片网格区域 -->

+ 1203 - 0
src/stock-out/MaterialInOutConfirm.vue

@@ -0,0 +1,1203 @@
+<!-- 出库确认 - 智能仓储风格 -->
+<template>
+  <dv-border-box-8>
+    <div class="stock-requisition-page">
+      <!-- 背景层 -->
+      <div class="bg-layer" :style="{ backgroundImage: `url(${bgImg})` }" />
+
+      <!-- 顶部标题区域 -->
+      <div class="header-section">
+        <button class="logout-btn" @click="goHome">
+          <i class="fas fa-home" />
+          <span>主页</span>
+        </button>
+        <h1 class="page-title">离开</h1>
+        <!-- 右侧操作按钮 -->
+        <div class="header-actions">
+          <button class="action-btn refresh-btn" @click="resetView()">
+            <i class="fas fa-sync-alt" />
+            <span>重新校验</span>
+          </button>
+        </div>
+      </div>
+
+      <!-- 主内容区域 -->
+      <main class="main-content">
+        <div v-if="needConfirmStockInMaterialList.length > 0" class="stats-section">
+          <span class="stats-text">
+            您共有 {{ needConfirmStockInMaterialList.length }} 个物料入库,请确认物料已经放置到对应的货位,确认无误以后,再点击【入库离开】按钮。
+          </span>
+
+          <span v-if="confirmInvalidCount > 0" style="color: red; font-weight: bolder;">
+            其中 {{ confirmInvalidCount }} 个物料被闸机读取到了,请把物料放到对应的位置,再点击【重新校验】按钮。
+          </span>
+        </div>
+        <!-- 卡片网格区域 -->
+        <div class="card-grid-wrapper">
+          <!-- 空状态 -->
+          <div v-if=" needConfirmStockInMaterialList.length === 0 && !loading" class="empty-state">
+            <i class="fas fa-inbox empty-icon" />
+            <p>暂无数据</p>
+          </div>
+
+          <!-- 卡片网格 -->
+          <div v-else class="card-grid">
+            <div
+              v-for="(item, index) in needConfirmStockInMaterialList"
+              :key="item.id || index"
+              class="inventory-card"
+              :class="getCardClass(item)"
+            >
+              <!-- 卡片序号 -->
+              <div class="card-index" :class="getCardClass(item)">{{ index + 1 }}</div>
+
+              <!-- 图片区域 -->
+              <div class="card-image">
+                <div class="image-placeholder" :class="getCardClass(item)">
+                  <i :class="getInventoryIcon(item.inventoryType)" />
+                </div>
+              </div>
+
+              <!-- 信息区域 -->
+              <div class="card-info">
+                <div class="info-row">
+                  <span class="info-label">类型:</span>
+                  <span class="info-value">{{ item.inventoryType || '-' }}</span>
+                </div>
+                <div class="info-row">
+                  <span class="info-label">名称:</span>
+                  <span class="info-value">{{ item.inventoryName || '-' }}</span>
+                </div>
+                <div class="info-row location-row">
+                  <i class="fas fa-map-marker-alt location-icon" />
+                  <span class="info-value">{{ item.positionName || '-' }}</span>
+                </div>
+              </div>
+
+              <!-- 状态区域(固定高度) -->
+              <div class="card-status-section">
+                <div class="status-badge" :class="getCardClass(item)">
+                  <i class="fas fa-exclamation-circle" />
+                  {{ item.remarks || '-' }}
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+
+
+        <!-- 统计信息 -->
+        <div v-if="materialList.length > 0" class="stats-section">
+          <span class="stats-text">共 {{ materialList.length }} 条数据,已完成 {{ completedCount }} 条, 不识别 {{ unrecognizedCount }} 条</span>
+        </div>
+
+        <!-- 卡片网格区域 -->
+        <div class="card-grid-wrapper">
+          <!-- 空状态 -->
+          <div v-if="materialList.length === 0 && !loading" class="empty-state">
+            <i class="fas fa-inbox empty-icon" />
+            <p>暂无数据</p>
+          </div>
+
+          <!-- 卡片网格 -->
+          <div v-else class="card-grid">
+            <template v-for="(item, index) in materialList" :key="item.epc">
+              <div
+                v-if="item.inventoryId != null && item.isValid === true"
+                class="inventory-card"
+                :class="{ 'completed': item.status === 1 }"
+              >
+                <!-- 卡片序号 -->
+                <div class="card-index" :class="{ 'completed': item.status === 1 }">{{ index + 1 }}</div>
+
+                <!-- 图片区域 -->
+                <div class="card-image">
+                  <div class="image-placeholder" :style="{ backgroundColor: getStatusColor(item.status) }">
+                    <i :class="getInventoryIcon(item.inventoryType)" />
+                  </div>
+                </div>
+
+                <!-- 信息区域 -->
+                <div class="card-info">
+                  <div class="info-row">
+                    <span class="info-label">名称:</span>
+                    <span class="info-value">{{ item.inventoryName || '-' }}</span>
+                  </div>
+                  <div class="info-row">
+                    <span class="info-label">编号:</span>
+                    <span class="info-value">{{ item.inventoryNo || '-' }}</span>
+                  </div>
+                  <div class="info-row location-row">
+                    <i class="fas fa-map-marker-alt location-icon" />
+                    <span class="info-value">{{ item.positionName || '-' }} / {{ item.warehouseName || '-' }}</span>
+                  </div>
+                </div>
+
+                <!-- 操作区域(固定高度) -->
+                <div class="card-action-section">
+                  <template v-if="item.positionName !== '不在库' && item.inventoryName !== '未识别epc'">
+                    <button v-if="item.status === 1" class="status-btn completed-btn" disabled>
+                      <i class="fas fa-check" /> 领料完成
+                    </button>
+                    <button v-if="item.status === 2" class="status-btn start-btn" @click.stop="handleStart(item)">
+                      <i class="fas fa-play" /> 开始领料
+                    </button>
+                    <button v-if="item.status === 3" class="status-btn apply-btn" @click.stop="handleApply(item)">
+                      <i class="fas fa-hand-paper" /> 申请领料
+                    </button>
+                  </template>
+                  <template v-else>
+                    <span class="status-placeholder">无需操作</span>
+                  </template>
+                </div>
+              </div>
+            </template>
+          </div>
+        </div>
+      </main>
+
+      <!-- 底部操作按钮 -->
+      <div class="bottom-actions">
+        <button class="submit-btn" :disabled="confirmInvalidCount > 0 || !isCanLeave" @click="handleLeave">
+          确认离开
+        </button>
+      </div>
+
+      <!-- Loading -->
+      <div v-if="loading" class="loading-overlay">
+        <div class="loading-dots">
+          <div class="dot" />
+          <div class="dot" />
+          <div class="dot" />
+        </div>
+        <span class="loading-text">加载中...</span>
+      </div>
+    </div>
+  </dv-border-box-8>
+</template>
+
+<script setup>
+import { useRouter } from 'vue-router';
+import { ref, computed, onMounted, onUnmounted } from 'vue';
+import { showNotify } from 'vant';
+import { cfStockOut, createStockOut, cfStockOutLeave,leaveCFWarehouse } from '../api/stockOut.js';
+import { areArraysEqual } from '../common/utils.js';
+import { gateController } from '../hardware/GateOperate.js';
+import { queryCFStockInByUser, backfillCFStockInTime, cfStockInLeave } from '../api/stockIn.js';
+
+// 图片资源
+import bgImg from '../assets/images/bj.png';
+
+// 路由
+const router = useRouter();
+
+// 操作的id
+const operationId = ref(null);
+
+// 返回主页
+const goHome = () => {
+    router.push('/home');
+};
+
+// 表格加载状态
+const loading = ref(false);
+
+// 物料列表数据
+const materialList = ref([]);
+const epcs = ref([]);
+
+
+
+// 完成数量统计
+const confirmInvalidCount = computed(() => {
+    return needConfirmStockInMaterialList.value.filter(item => item.isValid === false).length;
+});
+
+
+// 完成数量统计
+const completedCount = computed(() => {
+    return materialList.value.filter(item => item.status === 1 && item.inventoryId != null).length;
+});
+
+// 不识别的数据统计
+const unrecognizedCount = computed(() => {
+    return materialList.value.filter(item => item.inventoryId == null).length;
+});
+
+// 获取设备类型图标
+const getInventoryIcon = type => {
+    const iconMap = {
+        '工装': 'fas fa-cube',
+        '设备': 'fas fa-cogs',
+        '成品': 'fas fa-box',
+    };
+    return iconMap[type] || 'fas fa-cube';
+};
+
+// 获取状态颜色
+const getStatusColor = status => {
+    const colorMap = {
+        1: '#10b981', // 完成 - 绿色
+        2: '#f59e0b', // 进行中 - 橙色
+        3: '#3b82f6', // 待开始 - 蓝色
+    };
+    return colorMap[status] || '#6b7280';
+};
+
+// 判断是否可以离开
+const isCanLeave = computed(() => {
+    // 1. 先判断 materialList 是否存在且是数组
+    // if (!materialList.value || !Array.isArray(materialList.value)) {
+    //     return false;
+    // }
+
+    // 2. 判断是否有数据(空数组不能离开)
+    // if (materialList.value.length === 0) {
+    //     return false;
+    // }
+
+    // 3. 判断所有项的 status 是否都为 1
+    // 使用 every 时需要确保 item 和 item.status 都存在
+    // return materialList.value.every(item => {
+    //     return item && typeof item.status !== 'undefined' && item.status === 1;
+    // });
+
+    return true;
+});
+
+// 开始领料
+const handleStart = record => {
+    operationId.value = record.inventoryId;
+    generateCFStockOut(record.stockOutPrepareLineId);
+};
+
+// 申请领料
+const handleApply = async record => {
+    showNotify({ type: 'success', message: '申请领料成功' });
+    try {
+        const res = await leaveCFWarehouse(record.inventoryId);
+
+        if (res.errorCode === 0) {
+            showNotify({ type: 'success', message: '申请领料成功' });
+            operationId.value = record.inventoryId;
+            getStockOutList();
+        } else {
+            showNotify({ type: 'danger', message: res.errorMessage });
+        }
+    } catch (error) {
+        console.error('申请领料API调用失败:', error);
+        showNotify({ type: 'danger', message: '申请领料API调用失败' });
+    } finally {
+        loading.value = false;
+    }
+};
+
+// 领料完成后离开
+const handleLeave = async () => {
+
+    await getCFStockInByUser();
+
+    let invalidCount = 0;
+    needConfirmStockInMaterialList.value.map(item => {
+        if(item.isValid === false) {
+            invalidCount++;
+        }
+    });
+    if(invalidCount > 0) {
+        showNotify({ 
+            type: 'danger', 
+            message: '请检查有' + invalidCount + '条物料已经入库,但是被闸机扫描到了。请自己检查以后,点击【重新校验】按钮' });
+        return;
+    }
+
+
+    await handleComplete();
+
+    const params = [];
+    materialList.value.map(item => {
+        if(item.isValid === true && item.inventoryId != null){
+            params.push({
+                stockOutPrepareLineId: item.stockOutPrepareLineId,
+                stockOutId: item.stockOutId,
+                inventoryId: item.inventoryId,
+            });
+        }
+    });
+    if(params.length == 0){
+        // 直接开门离开
+        gateController('SHOTOPEN');
+        router.push('/');
+        return;
+    }
+
+    materialList.value.map(item => {
+        return {
+            stockOutPrepareLineId: item.stockOutPrepareLineId,
+            stockOutId: item.stockOutId,
+            inventoryId: item.inventoryId,
+        };
+    });
+    loading.value = true;
+    try {
+        const res = await cfStockOutLeave(params);
+
+        if (res.errorCode === 0) {
+            gateController('SHOTOPEN');
+            showNotify({ type: 'success', message: '领料离开成功' });
+            router.push('/home');
+        } else {
+            showNotify({ type: 'danger', message: res.errorMessage });
+        }
+    } catch (error) {
+        console.error('领料离开API调用失败:', error);
+        showNotify({ type: 'danger', message: '领料离开API调用失败' });
+    } finally {
+        loading.value = false;
+    }
+};
+
+// 获取扫描到的领料数据
+const getStockOutList = async () => {
+    const params = {
+        epcList: [],
+    };
+
+    for(let i = 0; i < materialList.value.length; i++) {
+        if(materialList.value[i].queryStatus === 0) {
+            params.epcList.push(materialList.value[i].epc);
+            materialList.value[i].queryStatus = 1;
+        }
+    }
+
+    if(params.epcList.length == 0){
+        return;
+    }
+
+    
+    try {
+        const res = await cfStockOut(params);
+
+        if (res.errorCode === 0) {
+            if (res.datas && res.datas.length > 0) {
+                res.datas.forEach(i => {
+                    materialList.value.forEach(j => {
+                        if (j.epc === i.epc) {
+                            j.stockOutId = i.stockOutId;
+                            j.inventoryName = i.inventoryName;
+                            j.inventoryNo = i.inventoryNo;
+                            j.inventoryType = i.inventoryType;
+                            j.warehouseName = i.warehouseName;
+                            j.positionName = i.positionName;
+                            j.stockOutPrepareNo = i.stockOutPrepareNo;
+                            j.stockOutNo = i.stockOutNo;
+                            j.inventoryId = i.inventoryId;
+                            j.stockOutPrepareLineId = i.stockOutPrepareLineId;
+                            j.status = 1;
+                            j.queryStatus = 2;
+                        }
+                    });
+                });
+            }
+        } else {
+            showNotify({ type: 'danger', message: res.errorMessage });
+        }
+        computerErrorData();
+    } catch (error) {
+        console.error('获取扫描到的领料数据API调用失败:', error);
+        showNotify({ type: 'danger', message: '获取扫描到的领料数据API调用失败' });
+    } finally {
+        loading.value = false;
+    }
+};
+
+// 生成出库单
+const generateCFStockOut = async id => {
+    loading.value = true;
+    const params = [
+        {
+            stockOutPrepareLineId: id,
+            deliveryMethod: 'Manual_Delivery',
+        },
+    ];
+    try {
+        const res = await createStockOut(params);
+
+        if (res.errorCode === 0) {
+            getStockOutList();
+        } else {
+            showNotify({ type: 'danger', message: res.errorMessage });
+        }
+
+    } catch (error) {
+        console.error('生成出库单API调用失败:', error);
+        showNotify({ type: 'danger', message: '生成出库单API调用失败' });
+    } finally {
+        loading.value = false;
+    }
+};
+
+let timer = null; // 用于保存当前定时器引用
+
+const addEpc = data => {
+    const newEpcs = data.map(item => item.epc);
+    
+    // 将新的EPC数据添加到临时数组中,使用Set进行去重
+    const uniqueEpcs = new Set([...newEpcs]);
+
+    if(newEpcs != null && newEpcs.length > 0) {
+        newEpcs.forEach(item => {
+            let exist = false;
+            for(let i =0; i < materialList.value.length; i++) {
+                if(materialList.value[i].epc === item) {
+                    exist = true;
+                    break;
+                }
+            }
+            if(exist == false) {
+                materialList.value.push({
+                    epc: item,
+                    queryStatus: 0, // 0: 未查询, 1: 查询中, 2: 已查询, 3: 未查询到
+                });
+            }
+        });
+    }
+};
+
+onMounted(() => {
+    getCFStockInByUser();
+    plugin.gateConfig.sendEpc = function(data){
+        if (typeof(GATE_CONFIG) == 'undefined') {
+            console.log('设备不支持读写器功能。');
+        } else {
+            addEpc(data);
+        }
+    };
+
+    // 创建全局唯一定时器,每2秒执行一次
+    timer = setInterval(() => {
+        // 执行获取出库列表
+        // addEpc([{
+        //     'epc': 'FFF000000000000000000001',
+        // }]);
+        getStockOutList();
+    }, 1000);
+});
+
+onUnmounted(() => {
+    plugin.gateConfig.sendEpc = null;
+    // 清除定时器
+    if (timer) {
+        clearInterval(timer);
+        timer = null;
+    }
+});
+
+
+
+
+// 已入库但是未确认的工装设备列表数据
+const needConfirmStockInMaterialList = ref([]);
+
+// 查询用户已经入库但是未确认的工装设备信息
+const getCFStockInByUser = async () => {
+    const info = localStorage.getItem('#LoginInfo');
+    const userId = JSON.parse(info).userId;
+    try {
+        const res = await queryCFStockInByUser(userId);
+
+        if (res.errorCode === 0) {
+            if (res.datas && res.datas.length > 0) {
+                needConfirmStockInMaterialList.value = res.datas;
+                computerErrorData();
+                // userMaterialList.value = res.datas;
+                // getInnerList();
+            } else {
+                needConfirmStockInMaterialList.value = [];
+                computerErrorData();
+                // userMaterialList.value = [];
+            }
+        } else {
+            showNotify({ type: 'danger', message: res.errorMessage,zIndex: 99999999 });
+            // getInnerList();
+        }
+    } catch (error) {
+        console.error('获取物料列表API调用失败:', error);
+        showNotify({ type: 'danger', message: '获取物料列表API调用失败' });
+    }
+};
+
+
+// 还料离开,回填入库单中的离开时间
+const handleComplete = async () => {
+    const params = needConfirmStockInMaterialList.value.map(item => {
+        return item.stockInId;
+    });
+
+    console.log('提交参数:', params);
+    if(params.length > 0){
+        await generateCFStockIn(params);
+    }
+};
+
+
+// 还料离开(回填归还入库单入库时间)
+const generateCFStockIn = async params => {
+    loading.value = true;
+    try {
+        const res = await backfillCFStockInTime(params);
+
+        if (res.errorCode === 0) {
+            // 调用开门操作
+            //gateController('SHOTOPEN');
+            //showNotify({ type: 'success', message: '还料离开已完成!' });
+            //router.push('/home');
+        } else {
+            showNotify({ type: 'danger', message: res.errorMessage });
+        }
+
+    } catch (error) {
+        console.error('生成CF离开单API调用失败:', error);
+        showNotify({ type: 'danger', message: '生成CF离开单API调用失败' });
+    } finally {
+        loading.value = false;
+    }
+};
+
+// 待入库确认的数据,与扫描到的数据进行比对,待入库确认的数据不能被扫描到
+const computerErrorData = data => {
+    if (!needConfirmStockInMaterialList.value || needConfirmStockInMaterialList.value.length === 0) {
+        return;
+    }
+    if (!materialList.value || materialList.value.length === 0) {
+        return;
+    }
+    needConfirmStockInMaterialList.value.forEach(item => {
+        item.isValid = true;
+    });
+    materialList.value.forEach(i => {
+        i.isValid = true;
+    });
+
+    needConfirmStockInMaterialList.value.forEach(item => {
+        materialList.value.forEach(i => {
+            if (i.epc === item.epc) {
+                i.isValid = false;
+                item.isValid = false;
+            }
+        });
+    });
+};
+// 根据 isValid 返回卡片样式类
+const getCardClass = card => {
+    if (card.isValid == null || card.isValid === true) {
+        return 'in-stock-card';
+    } else if (card.isValid === false) {
+        return 'not-in-stock-card';
+    }
+    return '';
+};
+
+
+const resetView = () => {
+    epcs.value = [];
+    materialList.value = [];
+    needConfirmStockInMaterialList.value = [];
+    getCFStockInByUser();
+};
+</script>
+
+<style scoped>
+/* ========== 基础样式 ========== */
+.stock-requisition-page {
+  width: 100%;
+  height: 100vh;
+  max-height: 100vh;
+  position: relative;
+  font-family: 'Microsoft YaHei', sans-serif;
+  color: #fff;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+
+/* 背景层 */
+.bg-layer {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: #041c3d;
+  background-size: cover;
+  background-position: center;
+  background-repeat: no-repeat;
+  z-index: 0;
+}
+
+/* ========== 主内容区域 ========== */
+.main-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  padding: 0 30px;
+  position: relative;
+  z-index: 10;
+  min-height: 0;
+  overflow: hidden;
+}
+
+/* 统计信息 */
+.stats-section {
+  background: rgba(9, 61, 140, 0.5);
+  border: 1px solid #049FD8;
+  border-radius: 12px;
+  padding: 15px 20px;
+  margin-bottom: 15px;
+  flex-shrink: 0;
+}
+
+.stats-text {
+  color: #7ec8ff;
+  font-size: 16px;
+}
+
+/* ========== 卡片网格区域 ========== */
+.card-grid-wrapper {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+  overflow-y: auto;
+  overflow-x: hidden;
+  padding-bottom: 10px;
+  scrollbar-width: none;
+  -ms-overflow-style: none;
+}
+
+.card-grid-wrapper::-webkit-scrollbar {
+  display: none;
+}
+
+/* 空状态 */
+.empty-state {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  color: #7ec8ff;
+  padding: 60px 0;
+}
+
+.empty-icon {
+  font-size: 80px;
+  margin-bottom: 20px;
+  opacity: 0.5;
+}
+
+.empty-state p {
+  font-size: 18px;
+  margin: 0;
+}
+
+/* 卡片网格 - 4列布局 */
+.card-grid {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 20px;
+}
+
+/* ========== 设备卡片样式 ========== */
+.inventory-card {
+  background: rgba(5, 30, 60, 0.8);
+  border: 2px solid #0d4a8a;
+  border-radius: 12px;
+  overflow: hidden;
+  transition: all 0.3s ease;
+  position: relative;
+}
+
+.inventory-card:hover {
+  border-color: #1e90ff;
+  box-shadow: 0 0 20px rgba(30, 144, 255, 0.3);
+  transform: translateY(-3px);
+}
+
+.inventory-card.completed {
+  border-color: #10b981;
+  box-shadow: 0 0 15px rgba(16, 185, 129, 0.3);
+}
+
+/* 卡片序号 - 左上角 */
+.card-index {
+  position: absolute;
+  top: 10px;
+  left: 10px;
+  width: 36px;
+  height: 36px;
+  background: linear-gradient(135deg, #1e90ff 0%, #00bfff 100%);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 16px;
+  font-weight: bold;
+  color: #fff;
+  z-index: 2;
+  box-shadow: 0 2px 8px rgba(0, 191, 255, 0.4);
+}
+
+.card-index.completed {
+  background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
+  box-shadow: 0 2px 8px rgba(16, 185, 129, 0.4);
+}
+
+/* 图片区域 */
+.card-image {
+  width: 100%;
+  aspect-ratio: 3 / 4;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  overflow: hidden;
+  margin: 15px;
+  margin-bottom: 10px;
+  border-radius: 8px;
+  width: calc(100% - 30px);
+}
+
+.image-placeholder {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+  background: linear-gradient(135deg, #f0f4f8 0%, #e8ecf0 100%);
+  color: #fff;
+  font-size: 48px;
+}
+
+/* 信息区域 */
+.card-info {
+  padding: 10px 15px 15px;
+}
+
+.info-row {
+  display: flex;
+  align-items: baseline;
+  margin-bottom: 6px;
+  font-size: 14px;
+}
+
+.info-row:last-child {
+  margin-bottom: 0;
+}
+
+.info-label {
+  color: #7ec8ff;
+  white-space: nowrap;
+  margin-right: 5px;
+}
+
+.info-value {
+  color: #fff;
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+/* 位置行样式 */
+.location-row {
+  display: flex;
+  align-items: center;
+  margin-top: 4px;
+  padding-top: 4px;
+  border-top: 1px solid rgba(30, 144, 255, 0.2);
+}
+
+.location-icon {
+  color: #00bfff;
+  font-size: 12px;
+  margin-right: 6px;
+}
+
+/* ========== 操作区域(固定高度) ========== */
+.card-action-section {
+  padding: 12px 15px;
+  border-top: 1px solid rgba(30, 144, 255, 0.3);
+  background: rgba(9, 61, 140, 0.3);
+  min-height: 50px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.status-placeholder {
+  color: #5a8abf;
+  font-size: 12px;
+  opacity: 0.7;
+}
+
+/* 状态按钮 */
+.status-btn {
+  width: 100%;
+  padding: 10px 15px;
+  border-radius: 6px;
+  font-size: 13px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.3s;
+  border: none;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 6px;
+}
+
+.completed-btn {
+  background: linear-gradient(90deg, #10b981 0%, #34d399 100%);
+  color: #fff;
+  opacity: 0.7;
+  cursor: not-allowed;
+}
+
+.start-btn {
+  background: linear-gradient(90deg, #f59e0b 0%, #fbbf24 100%);
+  color: #fff;
+}
+
+.start-btn:hover {
+  box-shadow: 0 0 15px rgba(245, 158, 11, 0.5);
+}
+
+.apply-btn {
+  background: linear-gradient(90deg, #1e90ff 0%, #00bfff 100%);
+  color: #fff;
+}
+
+.apply-btn:hover {
+  box-shadow: 0 0 15px rgba(30, 144, 255, 0.5);
+}
+
+/* ========== 底部操作按钮 ========== */
+.bottom-actions {
+  width: 100%;
+  padding: 15px 30px;
+  box-sizing: border-box;
+  background: rgba(4, 28, 61, 0.95);
+  z-index: 20;
+  flex-shrink: 0;
+}
+
+.submit-btn {
+  width: 100%;
+  padding: 18px 0;
+   background: #4a99e2;
+  border: none;
+  border-radius: 12px;
+  font-size: 24px;
+  font-weight: bold;
+  color: #fff;
+  cursor: pointer;
+  transition: all 0.3s;
+  letter-spacing: 8px;
+  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.submit-btn:hover:not(:disabled) {
+  box-shadow: 0 0 30px rgba(16, 185, 129, 0.6);
+  transform: translateY(-2px);
+}
+
+.submit-btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+  background: linear-gradient(90deg, #6b7280 0%, #9ca3af 100%);
+}
+
+/* ========== 响应式 - 横屏 ========== */
+@media screen and (orientation: landscape) {
+  .header-section {
+    padding: 30px 20px 10px 20px;
+  }
+
+  .page-title {
+    font-size: 22px;
+  }
+
+  .logout-btn {
+    padding: 6px 14px;
+    font-size: 13px;
+    left: 20px;
+  }
+
+  .logout-btn i {
+    font-size: 14px;
+  }
+
+  .header-actions {
+    right: 20px;
+  }
+
+  .action-btn {
+    padding: 6px 14px;
+    font-size: 13px;
+  }
+
+  .action-btn i {
+    font-size: 14px;
+  }
+
+  .main-content {
+    padding: 0 20px;
+  }
+
+  .stats-section {
+    padding: 10px 15px;
+    margin-bottom: 10px;
+  }
+
+  .stats-text {
+    font-size: 14px;
+  }
+
+  /* 卡片网格响应式 */
+  .card-grid {
+    grid-template-columns: repeat(6, 1fr);
+    gap: 15px;
+  }
+
+  .card-index {
+    width: 24px;
+    height: 24px;
+    font-size: 11px;
+    top: 6px;
+    left: 6px;
+  }
+
+  .card-image {
+    margin: 10px;
+    margin-bottom: 8px;
+    width: calc(100% - 20px);
+  }
+
+  .image-placeholder {
+    font-size: 36px;
+  }
+
+  .card-info {
+    padding: 6px 10px 8px;
+  }
+
+  .info-row {
+    font-size: 11px;
+    margin-bottom: 3px;
+  }
+
+  .location-icon {
+    font-size: 10px;
+  }
+
+  .card-action-section {
+    padding: 8px 10px;
+    min-height: 40px;
+  }
+
+  .status-btn {
+    padding: 6px 10px;
+    font-size: 11px;
+    gap: 4px;
+  }
+
+  .status-placeholder {
+    font-size: 10px;
+  }
+
+  .bottom-actions {
+    padding: 10px 20px;
+  }
+
+  .submit-btn {
+    padding: 12px 0;
+    font-size: 18px;
+    letter-spacing: 6px;
+    border-radius: 8px;
+  }
+}
+</style>
+
+
+<style scoped>
+
+/* ========== 设备卡片样式 ========== */
+.inventory-card {
+  background: rgba(5, 30, 60, 0.8);
+  border: 2px solid #0d4a8a;
+  border-radius: 12px;
+  overflow: hidden;
+  transition: all 0.3s ease;
+  position: relative;
+}
+
+.inventory-card:hover {
+  border-color: #1e90ff;
+  box-shadow: 0 0 20px rgba(30, 144, 255, 0.3);
+  transform: translateY(-3px);
+}
+
+/* 校验失败卡片 - 红色警告 */
+.inventory-card.not-in-stock-card {
+  border-color: #ef4444;
+}
+
+/* 校验通过卡片 - 绿色 */
+.inventory-card.in-stock-card {
+  border-color: #10b981;
+}
+
+/* 卡片序号 - 左上角 */
+.card-index {
+  position: absolute;
+  top: 10px;
+  left: 10px;
+  width: 36px;
+  height: 36px;
+  background: linear-gradient(135deg, #1e90ff 0%, #00bfff 100%);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 16px;
+  font-weight: bold;
+  color: #fff;
+  z-index: 2;
+  box-shadow: 0 2px 8px rgba(0, 191, 255, 0.4);
+}
+
+.card-index.not-in-stock-card {
+  background: linear-gradient(135deg, #ef4444 0%, #f87171 100%);
+  box-shadow: 0 2px 8px rgba(239, 68, 68, 0.4);
+}
+
+.card-index.in-stock-card {
+  background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
+  box-shadow: 0 2px 8px rgba(16, 185, 129, 0.4);
+}
+
+/* 图片区域 */
+.card-image {
+  width: 100%;
+  aspect-ratio: 3 / 4;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  overflow: hidden;
+  margin: 15px;
+  margin-bottom: 10px;
+  border-radius: 8px;
+  width: calc(100% - 30px);
+}
+
+.image-placeholder {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+  background: linear-gradient(135deg, #f0f4f8 0%, #e8ecf0 100%);
+  color: #94a3b8;
+  font-size: 48px;
+}
+
+.image-placeholder.not-in-stock-card {
+  background: linear-gradient(135deg, #ef4444 0%, #f87171 100%);
+  color: #fff;
+}
+
+.image-placeholder.in-stock-card {
+  background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
+  color: #fff;
+}
+
+/* 信息区域 */
+.card-info {
+  padding: 10px 15px 15px;
+}
+
+.info-row {
+  display: flex;
+  align-items: baseline;
+  margin-bottom: 6px;
+  font-size: 14px;
+}
+
+.info-row:last-child {
+  margin-bottom: 0;
+}
+
+.info-label {
+  color: #7ec8ff;
+  white-space: nowrap;
+  margin-right: 5px;
+}
+
+.info-value {
+  color: #fff;
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+/* 位置行样式 */
+.location-row {
+  display: flex;
+  align-items: center;
+  margin-top: 4px;
+  padding-top: 4px;
+  border-top: 1px solid rgba(30, 144, 255, 0.2);
+}
+
+.location-icon {
+  color: #00bfff;
+  font-size: 12px;
+  margin-right: 6px;
+}
+
+/* ========== 状态区域(固定高度) ========== */
+.card-status-section {
+  padding: 12px 15px;
+  border-top: 1px solid rgba(30, 144, 255, 0.3);
+  background: rgba(9, 61, 140, 0.3);
+  min-height: 50px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+/* 状态标签 */
+.status-badge {
+  padding: 8px 20px;
+  border-radius: 20px;
+  font-size: 13px;
+  font-weight: 500;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.status-badge.not-in-stock-card {
+  background: rgba(239, 68, 68, 0.3);
+  border: 1px solid #ef4444;
+  color: #fca5a5;
+}
+
+.status-badge.in-stock-card {
+  background: rgba(16, 185, 129, 0.3);
+  border: 1px solid #10b981;
+  color: #6ee7b7;
+}
+
+</style>

+ 3 - 1
src/stock-out/OrderPicking.vue

@@ -220,7 +220,7 @@ import 'vue-select/dist/vue-select.css';
 import { createPopper } from '@popperjs/core';
 import { list, createStockOut } from '../api/stockOut.js';
 import { getWarehouseList, queryIdleLocator } from '../api/stock.js';
-
+import { gateController } from '../hardware/GateOperate.js';
 // 图片资源
 import bgImg from '../assets/images/bj.png';
 
@@ -563,6 +563,8 @@ const generateCFStockOut = async params => {
 
 // 处理完成确认
 const handleCompleteConfirm = () => {
+    // 调用开门操作
+    gateController('SHOTOPEN');
     completeModalVisible.value = false;
     router.push('/home');
     // showToast('领料申请已完成,配送任务已创建!');