Эх сурвалжийг харах

增加出库和待返回操作

liuyanpeng 11 сар өмнө
parent
commit
8b46d038f6

+ 45 - 13
src/components/PositionSelector.vue

@@ -37,7 +37,7 @@
 import { showFailToast } from 'vant';
 import { ajaxApiGet } from '../common/utils';
 import { processException } from '../common/Common.js';
-import { ref, watch, computed, onMounted, defineProps, defineEmits } from 'vue';
+import { ref, watch, computed, onMounted, defineProps, defineEmits, defineExpose } from 'vue';
 
 const props = defineProps({
   show: {
@@ -49,6 +49,10 @@ const props = defineProps({
     default: 'transfer', // 'transfer' 或 'idle'
     validator: value => ['transfer', 'idle'].includes(value),
   },
+  isUser: {
+    type: String,
+    default: 'stockIn',
+  },
   defaultSelectedId: {
     type: [String, Number],
     default: '',
@@ -68,11 +72,7 @@ const emit = defineEmits([
   'confirm',
   'close',
 ]);
-const popupStyle = {
-  height: '60%',
-  // 确保弹窗样式不受外部影响
-  overflow: 'hidden',
-};
+
 // 内部状态
 const visible = ref(props.show);
 const searchKey = ref('');
@@ -84,12 +84,33 @@ const selectedId = ref(props.defaultSelectedId);
 const selectedItem = ref(null);
 
 // 计算属性
-const title = computed(() => props.positionType === 'transfer' ? '选择中转区货位' : '选择入库货位');
+const title = computed(() =>{
+  const titleMap = {
+    stockIn: {
+      transfer: '选择中转区货位',
+      idle: '选择入库货位',
+    },
+    stockOut: {
+      transfer: '选择中转区货位',
+      using: '选择出库货位',
+    },
+  };
+  return titleMap[props.isUser]?.[props.positionType] || '选择货位';
+});
 const searchPlaceholder = computed(() => `请输入${title.value}关键词`);
 const apiUrl = computed(() => {
-  return props.positionType === 'transfer'
-    ? '/api/positionResource/getTransferPosition'
-    : '/api/positionResource/getIdlePosition';
+  const apiMap = {
+    stockIn: {
+      transfer: '/api/positionResource/getTransferPosition',
+      idle: '/api/positionResource/getIdlePosition',
+    },
+    stockOut: {
+      transfer: '/api/positionResource/getTransferPosition',
+      using: '/api/positionResource/getOccupiedPosition',
+    },
+  };
+  console.log(props.isUser, props.positionType);
+  return apiMap[props.isUser]?.[props.positionType] || '';
 });
 
 // 监听props变化
@@ -106,9 +127,9 @@ watch(() => visible.value, newVal => {
 });
 
 // 同步默认选中ID
-// watch(() => props.defaultSelectedId, newVal => {
-//   selectedId.value = newVal;
-// });
+watch(() => props.defaultSelectedId, newVal => {
+  selectedId.value = newVal;
+});
 
 // 搜索
 const onSearch = () => {
@@ -155,6 +176,7 @@ const getPositionData = (isReset = true) => {
           noMore.value = true;
         }
       } else {
+        noMore.value = true;
         showFailToast(errorMessage);
       }
     },
@@ -188,12 +210,22 @@ const close = () => {
   emit('close');
 };
 
+// 清除选中
+const clearSelected = () => {
+  selectedId.value = '';
+  selectedItem.value = null;
+};
+
 // 组件挂载时,如果show为true则加载数据
 onMounted(() => {
   if (props.show) {
     getPositionData();
   }
 });
+
+defineExpose({
+  clearSelected,
+});
 </script>
 
 <style scoped>

+ 29 - 14
src/components/StockIn.vue

@@ -23,7 +23,7 @@
         </div>
       </van-list>
     </div>
-    <van-empty v-if="stockInList.length === 0" description="暂无入库" />
+    <van-empty v-if="stockInList.length === 0" description="暂无入库物料" />
   </div>
   <van-dialog v-model:show="isShowStockIn" title="填写入库信息" :show-confirm-button="false">
     <van-form :scroll-to-error="true">
@@ -54,12 +54,12 @@
     </template>
   </van-dialog>
   <position-selector
-    v-model:show="isShowTransfer" position-type="transfer" :default-selected-id="formData.transferId"
-    @confirm="onTransferPositionSelected"
+    ref="transferPositionSelector" v-model:show="isShowTransfer" position-type="transfer"
+    :default-selected-id="formData.transferId" @confirm="onTransferPositionSelected"
   />
   <position-selector
-    v-model:show="isShowIdle" position-type="idle" :default-selected-id="formData.idleId"
-    @confirm="onIdlePositionSelected"
+    ref="idlePositionSelector" v-model:show="isShowIdle" position-type="idle"
+    :default-selected-id="formData.idleId" @confirm="onIdlePositionSelected"
   />
 </template>
 
@@ -67,13 +67,15 @@
 import { ref, onMounted } from 'vue';
 import { useRouter } from 'vue-router';
 import PositionSelector from './PositionSelector.vue';
-import { showFailToast, showSuccessToast } from 'vant';
 import { processException } from '../common/Common.js';
 import { ajaxApiGet, ajaxApiPost } from '../common/utils.js';
+import { showFailToast, showSuccessToast, showConfirmDialog } from 'vant';
 
 const router = useRouter();
 
 const stockInSearch = ref('');
+const transferPositionSelector = ref(null);
+const idlePositionSelector = ref(null);
 
 const stockInList = ref([]);
 const loading = ref(false);
@@ -108,7 +110,6 @@ const goBack = () => {
 
 // 搜索入库物料
 const searchStockIn = value => {
-  console.log(value);
   page.value = 1;
   total.value = 0;
   stockInList.value = [];
@@ -118,17 +119,17 @@ const searchStockIn = value => {
 // 处理中转区货位选择结果
 const onTransferPositionSelected = item => {
   console.log(item, '中转区货位选择结果');
-  formData.value.transferName = item.name;
   formData.value.transferId = item.id;
   formData.value.transferNo = item.no;
+  formData.value.transferName = item.name;
 };
 
 // 处理入库货位选择结果
 const onIdlePositionSelected = item => {
   console.log(item, '入库货位选择结果');
-  formData.value.idleName = item.name;
   formData.value.idleId = item.id;
   formData.value.idleNo = item.no;
+  formData.value.idleName = item.name;
 };
 
 // 入库
@@ -157,7 +158,16 @@ const stockInConfirm = () => {
     showFailToast('请选择入库货位');
     return;
   }
-  submitStockIn();
+  showConfirmDialog({
+    title: '确认要入库吗?',
+    message: '如果确认要入库,请点击【确认】按钮,否则点击【取消】按钮。',
+  })
+    .then(() => {
+      submitStockIn();
+    })
+    .catch(() => {
+      console.log('取消');
+    });
 };
 
 // 加载入库物料列表
@@ -180,8 +190,8 @@ const loadStockInList = async () => {
       page.value++; // 只有在成功加载后才增加页码
     }
   } catch (error) {
-    processException(error);
     finished.value = true;
+    processException(error);
   } finally {
     loading.value = false;
   }
@@ -191,7 +201,7 @@ const loadStockInList = async () => {
 const getList = (page, pageSize) => {
   const start = (page - 1) * pageSize;
   const length = pageSize;
-  const filter = '';
+  const filter = stockInSearch.value;
   const url = `/api/InventoryResource/queryStockInInventory?start=${start}&length=${length}&filter=${filter}`;
   return new Promise((resolve, reject) => {
     ajaxApiGet(url).then(
@@ -224,6 +234,9 @@ const submitStockIn = () => {
   ajaxApiPost(url, params).then(
     success => {
       if (success.errorCode === 0) {
+        const filter = stockInSearch.value;
+        clearFormData();
+        searchStockIn(filter);
         showSuccessToast('入库成功');
       } else {
         showFailToast(success.errorMessage);
@@ -239,7 +252,6 @@ const submitStockIn = () => {
 // 取消入库
 const cancelStockIn = () => {
   clearFormData();
-  isShowStockIn.value = false;
 };
 
 // 清除表单数据
@@ -257,6 +269,9 @@ const clearFormData = () => {
     idleName: '',
     idleNo: '',
   };
+  transferPositionSelector.value.clearSelected();
+  idlePositionSelector.value.clearSelected();
+  isShowStockIn.value = false;
 };
 
 
@@ -287,7 +302,7 @@ onMounted(() => {
   justify-content: space-between;
   align-items: center;
   border-bottom: 1px solid #ccc;
-  padding-bottom: 4px;
+  padding-bottom: 3px;
 }
 
 .footer-btn {

+ 13 - 9
src/components/StockInPhoto.vue

@@ -51,8 +51,8 @@ import { ref } from 'vue';
 import { useRouter } from 'vue-router';
 import PositionSelector from './PositionSelector.vue';
 import { processException } from '../common/Common.js';
-import { showSuccessToast, showFailToast } from 'vant';
 import { ajaxApiGet, ajaxApiPost } from '../common/utils';
+import { showSuccessToast, showFailToast, showConfirmDialog } from 'vant';
 import { showFullscreenLoading, hideFullscreenLoading } from '../common/loading';
 
 
@@ -62,8 +62,6 @@ const imageUrl = ref('');
 const fileInput = ref(null);
 const photoFile = ref(null);
 
-const isCanSelect = ref(false);
-
 // 状态
 const isShowTransfer = ref(false);
 const isShowIdle = ref(false);
@@ -132,7 +130,6 @@ const clearFormData = () => {
   };
   imageUrl.value = '';
   photoFile.value = null;
-  isCanSelect.value = false;
 };
 
 // 提交入库
@@ -153,7 +150,16 @@ const submit = () => {
     showFailToast('请选择入库货位');
     return;
   }
-  submitStockIn();
+  showConfirmDialog({
+    title: '确认要入库吗?',
+    message: '如果确认要入库,请点击【确认】按钮,否则点击【取消】按钮。',
+  })
+    .then(() => {
+      submitStockIn();
+    })
+    .catch(() => {
+      console.log('取消');
+    });
 };
 
 // 上传图片获取入库信息
@@ -162,7 +168,7 @@ const uploadImage = async () => {
     showFailToast('请先拍摄图片后再进行识别');
     return;
   }
-  const url = 'http://192.168.1.114:10020/AppApi/OcrResource';
+  const url = 'http://192.168.1.109:10020/AppApi/OcrResource';
   const fileData = new FormData();
   fileData.append('image', photoFile.value);
 
@@ -179,7 +185,6 @@ const uploadImage = async () => {
       if (result.errorCode === 0) {
         if (result.recognizeResult) {
           stockData.value = { ...result.recognizeResult };
-          console.log(stockData.value);
           getInfo(result.recognizeResult.no);
         }
       } else {
@@ -192,7 +197,7 @@ const uploadImage = async () => {
     }
   } catch (error) {
     hideFullscreenLoading();
-    showFailToast('识别图片失败: \'请调整角度后重新拍摄\'');
+    showFailToast('识别图片失败: 请调整角度后重新拍摄');
   }
 };
 
@@ -205,7 +210,6 @@ const getInfo = no => {
       const { errorCode, errorMessage, datas } = success;
       if (errorCode === 0) {
         if (datas && datas.length) {
-          isCanSelect.value = true;
           stockData.value = { ...stockData.value, ...datas[0] };
           showSuccessToast({ duration: 5000, message: '获取信息成功,请先确认生产数量是否准确,若不准确请先修改数量。' });
         }

+ 346 - 3
src/components/StockOut.vue

@@ -1,11 +1,354 @@
 <template>
-  <div>
-    <h1>出库管理</h1>
+  <van-nav-bar
+    title="出库" left-arrow left-text="返回" right-text="拍照" fixed placeholder @click-left="goBack()"
+    @click-right="goTakePhoto"
+  />
+  <van-search v-model="stockOutSearch" placeholder="请输入搜索关键词" @search="searchStockOut" />
+  <div class="content">
+    <div class="van-list-stock-in">
+      <van-list
+        v-model:loading="loading" class="list-stock-in" :finished="finished" finished-text=""
+        :immediate-check="false" @load="loadStockOutList"
+      >
+        <div v-for="(item, index) in stockOutList" :key="item.id" class="list-container">
+          <van-form :scroll-to-error="true">
+            <div class="in-header">
+              <strong>{{ index + 1 }}. {{ item.inventoryName }}</strong>
+              <van-button type="primary" plain @click="stockOut(item)">出库</van-button>
+            </div>
+            <van-field v-model="item.inventoryNo" name="inventoryNo" label="物料编号:" readonly />
+            <van-field v-model="item.inventoryName" name="inventoryName" label="物料名称:" readonly />
+            <van-field v-model="item.inventoryType" name="inventoryType" label="规格型号:" readonly />
+            <van-field v-model="item.quantity" name="quantity" label="库存:" readonly />
+            <van-field v-model="item.positionName" name="positionName" label="货位:" readonly />
+          </van-form>
+        </div>
+      </van-list>
+    </div>
+    <van-empty v-if="stockOutList.length === 0" description="暂无出库库存" />
   </div>
+  <van-dialog v-model:show="isShowStockOut" title="填写出库信息" :show-confirm-button="false">
+    <van-form :scroll-to-error="true">
+      <van-field v-model="formData.inventoryNo" name="inventoryNo" label="物料编号:" readonly />
+      <van-field v-model="formData.inventoryName" name="inventoryName" label="物料名称:" readonly />
+      <van-field v-model="formData.inventoryType" name="inventoryType" label="规格型号:" readonly />
+      <van-field v-model="formData.batchNo" name="batchNo" label="批号:" placeholder="点击输入批号" />
+      <van-field name="outAll" label="是否全部出库">
+        <template #input>
+          <van-switch v-model="formData.outAll" size="20px" />
+        </template>
+      </van-field>
+      <van-field
+        v-model="formData.quantity" name="quantity" label="出库数量:" placeholder="点击输入数量"
+        :readonly="formData.outAll"
+      />
+      <van-field
+        v-model="formData.transferName" is-link readonly name="transfer" label="中转区货位:" placeholder="点击选择中转区货位"
+        @click="isShowTransfer = true"
+      />
+      <van-field
+        v-model="formData.positionName" is-link readonly name="warehouse" label="出库货位:" placeholder="点击选择出库货位"
+        @click="isShowUsing = true"
+      />
+    </van-form>
+
+    <template #footer>
+      <div class="footer-btn">
+        <van-button style="width: 40%;" plain type="primary" @click="cancelStockOut">
+          取消
+        </van-button>
+        <van-button style="width: 40%;" type="primary" @click="stockOutConfirm">
+          提交
+        </van-button>
+      </div>
+    </template>
+  </van-dialog>
+  <position-selector
+    ref="transferPositionSelector" v-model:show="isShowTransfer" position-type="transfer"
+    is-user="stockOut" :default-selected-id="formData.transferId" @confirm="onTransferPositionSelected"
+  />
+  <position-selector
+    ref="usingPositionSelector" v-model:show="isShowUsing" position-type="using" is-user="stockOut"
+    :default-selected-id="formData.positionId"
+    @confirm="onUsingPositionSelected"
+  />
 </template>
 
 <script setup>
+import { ref, onMounted } from 'vue';
+import { useRouter } from 'vue-router';
+import PositionSelector from './PositionSelector.vue';
+import { processException } from '../common/Common.js';
+import { ajaxApiGet, ajaxApiPost } from '../common/utils.js';
+import { showFailToast, showSuccessToast, showConfirmDialog } from 'vant';
+
+const router = useRouter();
+
+const stockOutSearch = ref('');
+const transferPositionSelector = ref(null);
+const usingPositionSelector = ref(null);
+
+const stockOutList = ref([]);
+const loading = ref(false);
+const finished = ref(false);
+
+const page = ref(1);
+const total = ref(0);
+const pageSize = ref(10);
+
+const formData = ref({
+  inventoryId: '',
+  inventoryNo: '',
+  inventoryName: '',
+  inventoryType: '',
+  quantity: '',
+  batchNo: '',
+  transferName: '',
+  transferId: '',
+  transferNo: '',
+  positionId: '',
+  positionNo: '',
+  positionName: '',
+  outAll: true,
+});
+
+const isShowStockOut = ref(false);
+const isShowTransfer = ref(false);
+const isShowUsing = ref(false);
+
+const goBack = () => {
+  router.push('/app-menus');
+};
+
+// 搜索出库物料
+const searchStockOut = value => {
+  page.value = 1;
+  total.value = 0;
+  stockOutList.value = [];
+  loadStockOutList();
+};
+
+// 处理中转区货位选择结果
+const onTransferPositionSelected = item => {
+  console.log(item, '中转区货位选择结果');
+  formData.value.transferId = item.id;
+  formData.value.transferNo = item.no;
+  formData.value.transferName = item.name;
+};
+
+// 处理出库货位选择结果
+const onUsingPositionSelected = item => {
+  console.log(item, '出库货位选择结果');
+  getStockOutInfo(item);
+};
+
+// 出库
+const stockOut = item => {
+  console.log(item, '出库');
+  formData.value.inventoryId = item.inventoryId;
+  formData.value.inventoryNo = item.inventoryNo;
+  formData.value.inventoryName = item.inventoryName;
+  formData.value.inventoryType = item.inventoryType;
+  formData.value.quantity = item.quantity;
+  formData.value.batchNo = item.batchNo;
+  formData.value.positionId = item.positionId;
+  formData.value.positionName = item.positionName;
+  formData.value.positionNo = item.positionNo;
+  isShowStockOut.value = true;
+};
+
+// 出库确认
+const stockOutConfirm = () => {
+  console.log(formData.value, '出库确认');
+
+  if (!formData.value.quantity) {
+    showFailToast('请输入出库数量');
+    return;
+  }
+  if (!formData.value.transferId) {
+    showFailToast('请选择中转区货位');
+    return;
+  }
+  if (!formData.value.positionId) {
+    showFailToast('请选择出库货位');
+    return;
+  }
+  showConfirmDialog({
+    title: '确认要出库吗?',
+    message: '如果确认要出库,请点击【确认】按钮,否则点击【取消】按钮。',
+  })
+    .then(() => {
+      submitStockOut();
+    })
+    .catch(() => {
+      console.log('取消');
+    });
+};
+
+// 加载出库物料列表
+const loadStockOutList = async () => {
+  try {
+    const res = await getList(page.value, pageSize.value);
+
+    // 搜索时替换数据,上拉加载时追加数据
+    stockOutList.value =
+      page.value === 1
+        ? res.data
+        : [...stockOutList.value, ...res.data];
+
+    total.value = res.total;
+
+    // 检查是否已加载全部数据
+    if (stockOutList.value.length >= total.value) {
+      finished.value = true;
+    } else {
+      page.value++; // 只有在成功加载后才增加页码
+    }
+  } catch (error) {
+    finished.value = true;
+    if(error.responseText === '未查询出对应库存信息。') return;
+    processException(error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 获取出库物料列表API
+const getList = (page, pageSize) => {
+  const start = (page - 1) * pageSize;
+  const length = pageSize;
+  const filter = '';
+  const url = `/api/StockOutResource/queryCurrentStockStockOut?start=${start}&length=${length}&filter=${filter}`;
+  return new Promise((resolve, reject) => {
+    ajaxApiGet(url).then(
+      success => {
+        const { errorCode, errorMessage, datas, total } = success;
+        if (errorCode === 0) {
+          if (datas && datas.length) {
+            resolve({ data: datas, total: total });
+          } else {
+            resolve({ data: [], total: 0 });
+          }
+        } else {
+          const error = { status: 200, responseText: errorMessage };
+          reject(error);
+        } 
+      },
+      error => {
+        reject(error);
+      },
+    );
+  });
+};
+
+// 提交API
+const submitStockOut = () => {
+  const url = '/api/StockOutResource/scanGeneratorStockOut';
+  const params = {
+    ...formData.value,
+  };
+  ajaxApiPost(url, params).then(
+    success => {
+      if (success.errorCode === 0) {
+        const filter = stockOutSearch.value;
+        clearFormData();
+        searchStockOut(filter);
+        showSuccessToast('出库成功');
+      } else {
+        showFailToast(success.errorMessage);
+      }
+    },
+    error => {
+      processException(error);
+    },
+  );
+};
+
+// 根据货位、物料、批号查询库存
+const getStockOutInfo = info => {
+  const positionId = info.id;
+  const inventoryId = formData.value.inventoryId;
+  const url = `/api/StockOutResource/queryByInventoryAndPosition?positionId=${positionId}&inventoryId=${inventoryId}`;
+  ajaxApiGet(url).then(
+    success => {
+      const { errorCode, errorMessage, data } = success;
+      if (errorCode === 0) {
+        console.log(data, '根据货位、物料、批号查询库存');
+        if (data) {
+          formData.value = { ...formData.value, ...data };
+        }
+      } else {
+        showFailToast(errorMessage);
+      }
+    },
+    error => {
+      processException(error);
+    },
+  );
+};
+
+// 取消出库
+const cancelStockOut = () => {
+  clearFormData();
+};
+
+// 清除表单数据
+const clearFormData = () => {
+  formData.value = {
+    inventoryId: '',
+    inventoryNo: '',
+    inventoryName: '',
+    inventoryType: '',
+    quantity: '',
+    batchNo: '',
+    transferId: '',
+    transferName: '',
+    transferNo: '',
+    positionId: '',
+    positionName: '',
+    positionNo: '',
+    outAll: true,
+  };
+  transferPositionSelector.value.clearSelected();
+  usingPositionSelector.value.clearSelected();
+  isShowStockOut.value = false;
+};
+
+
+// 拍照
+const goTakePhoto = () => {
+  router.push('/stock-out-photo');
+};
+onMounted(() => {
+  loadStockOutList();
+});
 
 </script>
 
-<style scoped></style>
+<style scoped>
+.van-search {
+  padding-bottom: 4px !important;
+}
+
+.list-container {
+  padding: 3px 10px;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  margin: 6px 12px;
+}
+
+.in-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  border-bottom: 1px solid #ccc;
+  padding-bottom: 3px;
+}
+
+.footer-btn {
+  display: flex;
+  justify-content: space-around;
+  align-items: center;
+  margin-bottom: 10px;
+}
+</style>

+ 184 - 0
src/components/StockOutBack.vue

@@ -0,0 +1,184 @@
+<template>
+  <van-nav-bar
+    title="待返回出库单" left-arrow left-text="返回" fixed placeholder @click-left="goBack()"
+  />
+  <van-search v-model="stockBackSearch" placeholder="请输入搜索关键词" @search="searchStockBack" />
+  <div class="content">
+    <div class="van-list-stock-in">
+      <van-list
+        v-model:loading="loading" class="list-stock-in" :finished="finished" finished-text=""
+        :immediate-check="false" @load="loadStockOutBackList"
+      >
+        <div v-for="(item, index) in stockBackList" :key="item.id" class="list-container">
+          <van-form :scroll-to-error="true">
+            <div class="in-header">
+              <strong>{{ index + 1 }}. {{ item.inventoryName }}</strong>
+              <van-button type="primary" plain @click="stockOutConfirm(item)">返回</van-button>
+            </div>
+            <van-field v-model="item.inventoryNo" name="inventoryNo" label="物料编号:" readonly />
+            <van-field v-model="item.inventoryName" name="inventoryName" label="物料名称:" readonly />
+            <van-field v-model="item.inventoryType" name="inventoryType" label="规格型号:" readonly />
+            <van-field v-model="item.quantity" name="quantity" label="数量:" readonly />
+            <van-field v-model="item.batchNo" name="batchNo" label="批号:" readonly />
+            <van-field v-model="item.positionName" name="positionName" label="货位:" readonly />
+          </van-form>
+        </div>
+      </van-list>
+    </div>
+    <van-empty v-if="stockBackList.length === 0" description="暂无待返回出库单" />
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue';
+import { useRouter } from 'vue-router';
+import { processException } from '../common/Common.js';
+import { ajaxApiGet, ajaxApiPost } from '../common/utils.js';
+import { showFailToast, showSuccessToast, showConfirmDialog } from 'vant';
+
+const router = useRouter();
+
+const stockBackSearch = ref('');
+
+const stockBackList = ref([]);
+const loading = ref(false);
+const finished = ref(false);
+
+const page = ref(1);
+const total = ref(0);
+const pageSize = ref(10);
+
+const goBack = () => {
+  router.push('/app-menus');
+};
+
+// 搜索返回物料
+const searchStockBack = value => {
+  page.value = 1;
+  total.value = 0;
+  stockBackList.value = [];
+  loadStockOutBackList();
+};
+
+// 返回确认
+const stockOutConfirm = item => {
+  console.log(item, '返回确认');
+  showConfirmDialog({
+    title: '确认要返回吗?',
+    message: '如果确认要返回,请点击【确认】按钮,否则点击【取消】按钮。',
+  })
+    .then(() => {
+      submitStockBack(item.stockOutId);
+    })
+    .catch(() => {
+      console.log('取消');
+    });
+};
+
+// 加载返回出库单列表
+const loadStockOutBackList = async () => {
+  try {
+    const res = await getList(page.value, pageSize.value);
+
+    // 搜索时替换数据,上拉加载时追加数据
+    stockBackList.value =
+      page.value === 1
+        ? res.data
+        : [...stockBackList.value, ...res.data];
+
+    total.value = res.total;
+
+    // 检查是否已加载全部数据
+    if (stockBackList.value.length >= total.value) {
+      finished.value = true;
+    } else {
+      page.value++; // 只有在成功加载后才增加页码
+    }
+  } catch (error) {
+    finished.value = true;
+    if(error.responseText === '未查询出部分出库的出库单') return;
+    processException(error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 获取返回出库单列表API
+const getList = (page, pageSize) => {
+  const start = (page - 1) * pageSize;
+  const length = pageSize;
+  const filter = stockBackSearch.value;
+  const url = `/api/StockOutResource/queryReturnStockOut?start=${start}&length=${length}&filter=${filter}`;
+  return new Promise((resolve, reject) => {
+    ajaxApiGet(url).then(
+      success => {
+        const { errorCode, errorMessage, datas, total } = success;
+        if (errorCode === 0) {
+          if (datas && datas.length) {
+            resolve({ data: datas, total: total });
+          } else {
+            resolve({ data: [], total: 0 });
+          }
+        } else {
+          const error = { status: 200, responseText: errorMessage };
+          reject(error);
+        }
+      },
+      error => {
+        reject(error);
+      },
+    );
+  });
+};
+
+// 返回API
+const submitStockBack = stockOutId => {
+  const url = `/api/StockOutResource/generateTask?stockOutId=${stockOutId}`;
+  ajaxApiGet(url).then(
+    success => {
+      if (success.errorCode === 0) {
+        searchStockBack(stockBackSearch.value);
+        showSuccessToast('返回成功');
+      } else {
+        showFailToast(success.errorMessage);
+      }
+    },
+    error => {
+      processException(error);
+    },
+  );
+};
+
+onMounted(() => {
+  loadStockOutBackList();
+});
+
+</script>
+
+<style scoped>
+.van-search {
+  padding-bottom: 4px !important;
+}
+
+.list-container {
+  padding: 3px 10px;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  margin: 6px 12px;
+}
+
+.in-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  border-bottom: 1px solid #ccc;
+  padding-bottom: 3px;
+}
+
+.footer-btn {
+  display: flex;
+  justify-content: space-around;
+  align-items: center;
+  margin-bottom: 10px;
+}
+</style>

+ 335 - 0
src/components/StockOutPhoto.vue

@@ -0,0 +1,335 @@
+<template>
+  <van-nav-bar title="拍照出库" left-arrow left-text="返回" fixed placeholder @click-left="goBack()" />
+  <van-image v-if="imageUrl" width="100%" height="100%" :src="imageUrl" />
+  <div class="content">
+    <div class="scan-btn">
+      <input
+        ref="fileInput" type="file" accept="image/*" capture="camera" style="display: none"
+        @change="onFileChange"
+      />
+      <van-button block plain icon="scan" type="primary" @click="takePhoto">
+        拍摄
+      </van-button>
+      <van-button style="margin-top: 10px" block plain icon="orders-o" type="primary" @click="uploadImage">
+        识别
+      </van-button>
+    </div>
+    <van-form :scroll-to-error="true">
+      <van-field v-model="stockData.inventoryNo" name="inventoryNo" label="物料编号:" :readonly="true" />
+
+      <van-field v-model="stockData.inventoryName" name="inventoryName" label="物料名称:" :readonly="true" />
+
+      <van-field v-model="stockData.inventoryType" name="inventoryType" label="规格型号:" :readonly="true" />
+
+      <van-field v-model="stockData.batchNo" name="batchNo" label="批号:" placeholder="点击输入批号" />
+
+      <van-field name="outAll" label="是否全部出库">
+        <template #input>
+          <van-switch v-model="stockData.outAll" size="16px" />
+        </template>
+      </van-field>
+
+      <van-field
+        v-model="stockData.quantity" name="quantity" label="出库数量:" placeholder="点击输入数量"
+        :readonly="stockData.outAll"
+      />
+
+      <van-field
+        v-model="stockData.transferName" is-link readonly name="transfer" label="中转区货位:"
+        placeholder="点击选择中转区货位" @click="isShowTransfer = true"
+      />
+      <van-field
+        v-model="stockData.positionName" is-link readonly name="warehouse" label="出库货位:" placeholder="点击选择出库货位"
+        @click="isShowUsing = true"
+      />
+      <div style="margin: 16px">
+        <van-button round block type="primary" @click="submit">
+          提交
+        </van-button>
+      </div>
+    </van-form>
+  </div>
+  <div>
+    <position-selector
+      ref="transferPositionSelector" v-model:show="isShowTransfer" position-type="transfer"
+      is-user="stockOut" :default-selected-id="stockData.transferId" @confirm="onTransferPositionSelected"
+    />
+    <position-selector
+      ref="usingPositionSelector" v-model:show="isShowUsing" position-type="using" is-user="stockOut"
+      :default-selected-id="stockData.positionId" @confirm="onUsingPositionSelected"
+    />
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+import PositionSelector from './PositionSelector.vue';
+import { processException } from '../common/Common.js';
+import { ajaxApiGet, ajaxApiPost } from '../common/utils';
+import { showSuccessToast, showFailToast, showConfirmDialog } from 'vant';
+import { showFullscreenLoading, hideFullscreenLoading } from '../common/loading';
+
+
+const router = useRouter();
+
+const imageUrl = ref('');
+const fileInput = ref(null);
+const photoFile = ref(null);
+
+// 状态
+const isShowTransfer = ref(false);
+const isShowUsing = ref(false);
+
+const stockData = ref({
+  inventoryId: '',
+  inventoryNo: '',
+  inventoryName: '',
+  inventoryType: '',
+  quantity: '',
+  batchNo: '',
+  transferName: '',
+  transferId: '',
+  transferNo: '',
+  positionId: '',
+  positionNo: '',
+  positionName: '',
+  outAll: true,
+});
+
+const goBack = () => {
+  router.back();
+};
+
+const takePhoto = () => {
+  fileInput.value.click();
+};
+
+// 处理文件选择/拍照结果
+const onFileChange = event => {
+  const file = event.target.files[0];
+  if (!file) return;
+
+  imageUrl.value = URL.createObjectURL(file);
+
+  photoFile.value = file;
+
+};
+// 处理中转区货位选择结果
+const onTransferPositionSelected = item => {
+  stockData.value.transferId = item.id;
+  stockData.value.transferNo = item.no;
+  stockData.value.transferName = item.name;
+};
+
+// 处理出库货位选择结果
+const onUsingPositionSelected = item => {
+  console.log(item, '出库货位选择结果');
+  getStockOutInfo(item);
+};
+
+// 清除表单数据
+const clearFormData = () => {
+  stockData.value = {
+    inventoryId: '',
+    inventoryNo: '',
+    inventoryName: '',
+    inventoryType: '',
+    quantity: '',
+    batchNo: '',
+    transferName: '',
+    transferId: '',
+    transferNo: '',
+    positionId: '',
+    positionNo: '',
+    positionName: '',
+    outAll: true,
+  };
+  imageUrl.value = '';
+  photoFile.value = null;
+};
+
+// 提交出库
+const submit = () => {
+  if (!stockData.value.inventoryNo) {
+    showFailToast({ duration: 4000, message: '请先拍摄并识别图片确认出库信息后再提交' });
+    return;
+  }
+  if (!stockData.value.quantity) {
+    showFailToast('请输入出库数量');
+    return;
+  }
+  if (!stockData.value.transferId) {
+    showFailToast('请选择中转区货位');
+    return;
+  }
+  if (!stockData.value.positionId) {
+    showFailToast('请选择出库货位');
+    return;
+  }
+  showConfirmDialog({
+    title: '确认要出库吗?',
+    message: '如果确认要出库,请点击【确认】按钮,否则点击【取消】按钮。',
+  })
+    .then(() => {
+      submitStockOut();
+    })
+    .catch(() => {
+      console.log('取消');
+    });
+};
+
+// 上传图片获取出库信息
+const uploadImage = async () => {
+  if (!photoFile.value) {
+    showFailToast('请先拍摄图片后再进行识别');
+    return;
+  }
+  const url = 'http://192.168.1.109:10020/AppApi/OcrResource';
+  const fileData = new FormData();
+  fileData.append('image', photoFile.value);
+
+  showFullscreenLoading();
+  try {
+    const response = await fetch(url, {
+      method: 'POST',
+      body: fileData,
+    });
+
+    if (response.ok) {
+      const result = await response.json();
+
+      if (result.errorCode === 0) {
+        if (result.recognizeResult) {
+          // stockData.value = { ...result.recognizeResult };
+          getInfo(result.recognizeResult.no, result.recognizeResult.batchNo);
+        }
+      } else {
+        showFailToast({ duration: 4000, message: result.errorMessage });
+      }
+      hideFullscreenLoading();
+    } else {
+      hideFullscreenLoading();
+      showFailToast(`识别失败 (${response.status}): ${response.statusText}`);
+    }
+  } catch (error) {
+    hideFullscreenLoading();
+    showFailToast('识别图片失败: 请调整角度后重新拍摄');
+  }
+};
+
+// 获取出库物料详情
+const getInfo = (no, batchNo) => {
+  showFullscreenLoading();
+  const url = `/api/StockOutResource/queryByInventoryNoAndBatchNo?no=${no}&batchNo=${batchNo}`;
+  ajaxApiGet(url).then(
+    success => {
+      const { errorCode, errorMessage, data } = success;
+      if (errorCode === 0) {
+        if (data) {
+          stockData.value = { ...stockData.value, ...data };
+          showSuccessToast({ duration: 5000, message: '获取信息成功,请先确认生产数量是否准确,若不准确请先修改数量。' });
+        }
+      } else {
+        showFailToast({ duration: 5000, message: errorMessage });
+      }
+      hideFullscreenLoading();
+    },
+    error => {
+      hideFullscreenLoading();
+      processException(error);
+    },
+  );
+};
+
+// 提交API
+const submitStockOut = () => {
+  const url = '/api/StockOutResource/scanGeneratorStockOut';
+  const params = JSON.parse(JSON.stringify(stockData.value));
+  delete params.workDate;
+  ajaxApiPost(url, params).then(
+    success => {
+      if (success.errorCode === 0) {
+        showSuccessToast('出库成功');
+        clearFormData();
+      } else {
+        showFailToast(success.errorMessage);
+      }
+    },
+    error => {
+      processException(error);
+    },
+  );
+};
+
+// 根据货位、物料、批号查询库存
+const getStockOutInfo = info => {
+  const positionId = info.id;
+  const inventoryId = stockData.value.inventoryId;
+  const url = `/api/StockOutResource/queryByInventoryAndPosition?positionId=${positionId}&inventoryId=${inventoryId}`;
+  ajaxApiGet(url).then(
+    success => {
+      const { errorCode, errorMessage, data } = success;
+      if (errorCode === 0) {
+        console.log(data, '根据货位、物料、批号查询库存');
+        if (data) {
+          stockData.value = { ...stockData.value, ...data };
+        }
+      } else {
+        showFailToast(errorMessage);
+      }
+    },
+    error => {
+      processException(error);
+    },
+  );
+};
+</script>
+
+<style scoped>
+.content {
+  margin-top: 10px;
+}
+
+.scan-btn {
+  margin: 0 10px;
+}
+
+.custom-picker {
+  display: flex !important;
+  flex-direction: column !important;
+  height: 100% !important;
+}
+
+.picker-header {
+  display: flex !important;
+  justify-content: space-between !important;
+  align-items: center !important;
+  padding: 10px 16px !important;
+  border-bottom: 1px solid #ebedf0 !important;
+}
+
+.picker-title {
+  font-size: 16px !important;
+  font-weight: 500 !important;
+}
+
+.picker-content {
+  flex: 1 !important;
+  overflow-y: auto !important;
+}
+
+.picker-footer {
+  padding: 10px 16px !important;
+  border-top: 1px solid #ebedf0 !important;
+  display: flex !important;
+  flex-direction: column !important;
+  gap: 8px !important;
+}
+
+.loading-more {
+  text-align: center !important;
+  color: #969799 !important;
+  padding: 10px 0 !important;
+}
+</style>

+ 10 - 0
src/menu/AppMenus.vue

@@ -32,6 +32,16 @@
           <p class="menu-desc">管理调拨任务</p>
         </template>
       </van-grid-item>
+
+      <van-grid-item to="/stock-out-back" class="menu-item">
+        <template #icon>
+          <van-icon name="revoke" class="menu-icon" />
+        </template>
+        <template #text>
+          <span class="menu-title">待返回出库</span>
+          <p class="menu-desc">管理待返回出库任务</p>
+        </template>
+      </van-grid-item>
     </van-grid>
   </div>
 </template>

+ 8 - 0
src/router/router.js

@@ -15,6 +15,12 @@ const StockInPhoto = () => import('../components/StockInPhoto.vue')
 // 出库
 const StockOut = () => import('../components/StockOut.vue')
 
+// 出库拍照
+const StockOutPhoto = () => import('../components/StockOutPhoto.vue')
+
+// 待返回出库
+const StockOutBack = () => import('../components/StockOutBack.vue')
+
 // 调拨
 const StockTransfer = () => import('../components/StockTtansfer.vue')
 
@@ -26,5 +32,7 @@ export default [
   { path: '/stock-in', component: StockIn, meta: { requiresAuth: true }, },
   { path: '/stock-in-photo', component: StockInPhoto, meta: { requiresAuth: true }, },
   { path: '/stock-out', component: StockOut, meta: { requiresAuth: true }, },
+  { path: '/stock-out-photo', component: StockOutPhoto, meta: { requiresAuth: true }, },
   { path: '/stock-transfer', component: StockTransfer, meta: { requiresAuth: true }, },
+  { path: '/stock-out-back', component: StockOutBack, meta: { requiresAuth: true }, },
 ];