pan
2025-11-15 4476740c214edb7ab667c48fcab00488fbdd9879
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/outbound/PickingConfirm.vue
@@ -3,516 +3,375 @@
    <div class="page-header">
      <el-page-header @back="goBack">
        <template #content>
          <span class="title">出库拣选确认 - {{ orderInfo.orderNo }}</span>
          <span class="title">出库拣选确认 - {{ this.$route.query.orderNo }}</span>
        </template>
      </el-page-header>
    </div>
    <el-row :gutter="20" class="main-content">
      <el-col :span="8">
    <div class="content-layout">
      <!-- å·¦ä¾§ï¼šæ‰«ç åŒºåŸŸ -->
      <div class="left-section">
        <div class="scan-section">
          <el-card header="扫码区域">
            <el-form label-width="100px" size="small">
              <el-form-item label="托盘条码">
          <el-alert
            title="请使用扫码枪扫描托盘码和物料条码,扫码枪带回车功能,扫完物料条码自动确认"
            type="info"
            :closable="false"
            class="scan-alert"
          />
          <el-form :model="scanForm" label-width="100px" class="scan-form">
            <el-form-item label="托盘码" required>
                <el-input 
                ref="palletInput"
                  v-model="scanForm.palletCode" 
                  placeholder="扫描或输入托盘条码"
                placeholder="请扫描托盘码"
                  @keyup.enter="handlePalletScan"
                @blur="loadPalletSummary"
                  clearable
                >
                  <template #append>
                    <el-button @click="handlePalletScan">确认</el-button>
                  </template>
                </el-input>
              </el-form-item>
              <el-form-item label="物料条码">
                <el-input
                  v-model="scanForm.barcode"
                  placeholder="扫描或输入物料条码"
                  @keyup.enter="handleBarcodeScan"
                  :disabled="!currentPallet"
                  clearable
                >
                  <template #append>
                    <el-button @click="handleBarcodeScan" :disabled="!currentPallet">确认</el-button>
                  </template>
                </el-input>
              </el-form-item>
              <el-form-item label="拣选数量">
                <el-input-number
                  v-model="scanForm.quantity"
                  :min="1"
                  :max="maxPickQuantity"
                  :disabled="!currentLockInfo"
                />
              </el-form-item>
              <!-- ç‰©æ–™ä¿¡æ¯æ˜¾ç¤º -->
              <el-form-item label="物料编码" v-if="currentMaterialInfo">
                <el-input v-model="currentMaterialInfo.materielCode" readonly />
              </el-form-item>
              <el-form-item label="物料名称" v-if="currentMaterialInfo">
                <el-input v-model="currentMaterialInfo.materielName" readonly />
            <el-form-item label="物料条码" required>
              <el-input
                ref="materialInput"
                v-model="scanForm.materialBarcode"
                placeholder="请扫描物料条码"
                :disabled="!scanForm.palletCode"
                @keyup.enter="handleMaterialScan"
                clearable
              />
              </el-form-item>
            </el-form>
            <div class="current-info" v-if="currentPallet">
              <p>当前托盘: {{ currentPallet.palletCode }}</p>
              <p>货位: {{ currentPallet.locationCode }}</p>
              <p>状态: {{ currentPallet.statusText }}</p>
            </div>
          <!-- æ‰˜ç›˜æ‹£è´§ç»Ÿè®¡ -->
          <div v-if="palletSummary" class="pallet-summary">
            <el-card header="托盘拣货统计">
              <el-descriptions :column="3" border>
                <el-descriptions-item label="托盘号">
                  {{ scanForm.palletCode }}
                </el-descriptions-item>
                <el-descriptions-item label="未拣货条数">
                  <el-text type="warning">{{ palletSummary.unpickedCount }}</el-text>
                </el-descriptions-item>
                <el-descriptions-item label="未拣货总数">
                  <el-text type="danger">{{ palletSummary.unpickedTotal }}</el-text>
                </el-descriptions-item>
              </el-descriptions>
          </el-card>
          </div>
          <div class="action-buttons">
            <el-button
              type="warning"
              @click="handleBackToStock"
              :disabled="!currentPallet"
              style="margin-bottom: 10px;"
            >
              å›žåº“
            <el-button type="primary" @click="handleConfirm" :loading="confirmLoading">
              æ‰‹åŠ¨ç¡®è®¤
            </el-button>
            <el-button
              type="success"
              @click="handleDirectOutbound"
              :disabled="!currentPallet"
              style="margin-bottom: 10px;"
            >
              ç›´æŽ¥å‡ºåº“
            </el-button>
            <el-button
              type="primary"
              @click="handleOpenSplit"
              :disabled="!currentLockInfo"
            >
              æ‹†åŒ…
            </el-button>
            <el-button @click="handleReset">重置</el-button>
            <el-button @click="$emit('close')">取消</el-button>
          </div>
        </div>
      </el-col>
      <el-col :span="16">
        <el-card header="拣选结果">
          <div class="summary-info">
            <el-alert
              :title="`未拣货: ${unpickedCount} æ¡, ${unpickedQuantity} ä¸ª`"
              type="warning"
              :closable="false"
            />
          </div>
      <!-- å³ä¾§ï¼šå‡ºåº“详情列表 -->
      <div class="right-section">
        <el-card class="outbound-details-card" header="出库详情">
          <vol-table
            :data="pickedList"
            :columns="pickedColumns"
            :pagination="false"
            :height="400"
          >
            <template #action="{ row }">
              <el-button type="text" @click="handleCancelPick(row)">撤销</el-button>
            </template>
          </vol-table>
        </el-card>
      </el-col>
    </el-row>
    <!-- æ‹†åŒ…弹窗 -->
    <vol-box
      v-model="splitVisible"
      title="拆包操作"
      :width="600"
      :height="500"
    >
      <SplitPackageModal
        v-if="splitVisible"
        :lockInfo="currentLockInfo"
        @success="handleSplitSuccess"
        @close="splitVisible = false"
            ref="outboundTable"
            :table-config="outboundTableConfig"
            :height="300"
      />
    </vol-box>
        </el-card>
      </div>
    </div>
    <!-- å·²åˆ†æ‹£è®°å½•列表 -->
    <div class="picked-records">
      <el-card header="已分拣记录">
        <vol-table
          ref="pickedTable"
          :table-config="pickedTableConfig"
          :height="300"
        />
      </el-card>
    </div>
  </div>
</template>
<script>
import SplitPackageModal from './SplitPackageModal.vue'
import http from '@/api/http.js'
import { ref, defineComponent } from "vue";
import { ElMessage } from "element-plus";
import { useRoute } from 'vue-router'
export default {
  components: { SplitPackageModal },
export default defineComponent({
  name: 'PickingConfirm',
  components: {
  },
  props: {
    orderNo: {
      type: String,
      required: true
    }
  },
  emits: ['confirm', 'close'],
  data() {
    return {
      orderInfo: {},
      scanForm: {
        palletCode: '',
        barcode: '',
        quantity: 1
        materialBarcode: ''
      },
      currentPallet: null,
      currentLockInfo: null,
      currentMaterialInfo: null, // æ–°å¢žï¼šå½“前物料信息
      pickedList: [],
      pickedColumns: [
        { field: 'barcode', title: '物料条码', width: 150 },
        { field: 'materielCode', title: '物料编码', width: 120 },
        { field: 'materielName', title: '物料名称', width: 150 },
        { field: 'pickQuantity', title: '拣选数量', width: 100 },
        { field: 'palletCode', title: '托盘编号', width: 120 },
        { field: 'pickTime', title: '拣选时间', width: 160 },
        { field: 'operator', title: '操作人', width: 100 },
        { field: 'action', title: '操作', width: 80, slot: true }
      ],
      splitVisible: false,
      maxPickQuantity: 0,
      allLockInfos: [] // æ–°å¢žï¼šä¿å­˜æ‰€æœ‰é”å®šä¿¡æ¯ï¼Œç”¨äºŽæ¡ç åŒ¹é…
    }
      palletSummary: null,
      confirmLoading: false,
      pickedTableConfig: {
        url: '/api/outbound/getPickingRecords',
        query: { orderNo: this.orderNo },
        columns: [
          { prop: 'TaskNo', label: '任务号', width: 150 },
          { prop: 'Barcode', label: '物料条码', width: 150 },
          { prop: 'MaterielName', label: '物料名称', width: 150 },
          { prop: 'PickQuantity', label: '拣货数量', width: 100 },
          { prop: 'LocationCode', label: '货位', width: 120 },
          { prop: 'CreateTime', label: '拣货时间', width: 180 }
        ]
  },
  computed: {
    unpickedCount() {
      return this.orderInfo.unpickedCount || 0
      // å‡ºåº“详情表格配置
      outboundTableConfig: {
        url: '/api/outbound/getOutboundDetails',
        query: { orderNo: this.orderNo },
        columns: [
          { prop: 'OrderNo', label: '出库单号', width: 150 },
          { prop: 'MaterialCode', label: '物料编号', width: 120 },
          { prop: 'MaterialBarcode', label: '物料条码', width: 150 },
          { prop: 'BatchNo', label: '批次号', width: 120 },
          { prop: 'AssignQuantity', label: '分配出库量', width: 100 },
          { prop: 'PalletCode', label: '托盘编号', width: 120 },
          { prop: 'Unit', label: '单位', width: 80 }
        ]
    },
    unpickedQuantity() {
      return this.orderInfo.unpickedQuantity || 0
    }
  },
  methods: {
    goBack() {
      this.$router.back()
    },
    async loadOrderInfo() {
      const orderId = this.$route.query.orderId
      if (!orderId) return
      try {
        const result = await this.http.get(`api/OutboundOrder/GetById?id=${orderId}`)
        if (result.status) {
          this.orderInfo = result.data
        }
      } catch (error) {
        this.$message.error('加载出库单信息失败')
      }
    },
    async handlePalletScan() {
      if (!this.scanForm.palletCode) {
        this.$message.warning('请输入托盘条码')
        return
      }
      try {
        const result = await this.http.get(
          `api/OutboundPicking/GetPalletOutboundStatus?palletCode=${this.scanForm.palletCode}`
        )
        if (result.status) {
          this.currentPallet = result.data
          await this.loadPalletLockInfo()
          this.$message.success(`托盘 ${this.scanForm.palletCode} è¯†åˆ«æˆåŠŸ`)
        } else {
          this.$message.error(result.message)
        }
      } catch (error) {
        this.$message.error('托盘识别失败')
      }
    },
    async loadPalletLockInfo() {
      if (!this.currentPallet) return
      try {
        const result = await this.http.get(
          `api/OutboundPicking/GetPalletLockInfos?palletCode=${this.currentPallet.palletCode}`
        )
        if (result.status) {
          this.allLockInfos = result.data
          // é»˜è®¤é€‰æ‹©ç¬¬ä¸€ä¸ªé”å®šä¿¡æ¯
          if (this.allLockInfos.length > 0) {
            this.currentLockInfo = this.allLockInfos[0]
            this.currentMaterialInfo = {
              materielCode: this.currentLockInfo.materielCode,
              materielName: this.currentLockInfo.materielName
            }
            this.maxPickQuantity = this.currentLockInfo.assignQuantity - this.currentLockInfo.pickedQty
          }
        }
      } catch (error) {
        console.error('加载锁定信息失败:', error)
      }
    },
    // æ ¹æ®æ¡ç æŸ¥æ‰¾å¯¹åº”的锁定信息和物料信息
    findLockInfoByBarcode(barcode) {
      if (!this.allLockInfos || this.allLockInfos.length === 0) {
        return null
      }
      // é¦–先精确匹配当前条码
      let lockInfo = this.allLockInfos.find(x => x.currentBarcode === barcode)
      if (lockInfo) {
        return lockInfo
      }
      // å¦‚果没有精确匹配,查找该条码对应的物料是否在锁定信息中
      // è¿™é‡Œéœ€è¦è°ƒç”¨åŽç«¯æŽ¥å£éªŒè¯æ¡ç å¯¹åº”的物料
      return null
    },
    async handleBarcodeScan() {
      if (!this.scanForm.barcode) {
        this.$message.warning('请输入物料条码')
        return
      }
      if (!this.currentPallet) {
        this.$message.warning('请先扫描托盘条码')
        return
      }
      if (this.scanForm.quantity <= 0) {
        this.$message.warning('请输入有效的拣选数量')
        return
      }
      try {
        // éªŒè¯æ¡ç å¹¶èŽ·å–ç‰©æ–™ä¿¡æ¯
        const materialInfo = await this.validateBarcode(this.scanForm.barcode)
        if (!materialInfo) {
          this.$message.error('无效的物料条码')
          return
        }
        // æŸ¥æ‰¾å¯¹åº”的锁定信息
        const targetLockInfo = this.findLockInfoByBarcodeAndMaterial(this.scanForm.barcode, materialInfo.materielCode)
        if (!targetLockInfo) {
          this.$message.error('该物料条码不在当前托盘的锁定信息中')
          return
        }
        // æ£€æŸ¥æ‹£é€‰æ•°é‡
        const availableQuantity = targetLockInfo.assignQuantity - targetLockInfo.pickedQty
        if (this.scanForm.quantity > availableQuantity) {
          this.$message.error(`拣选数量超过可用数量,剩余可拣选:${availableQuantity}`)
          return
        }
        // å‡†å¤‡è¯·æ±‚数据
        const request = {
          orderDetailId: targetLockInfo.orderDetailId,
          barcode: this.scanForm.barcode,
          materielCode: materialInfo.materielCode, // ä¼ é€’物料编码
          pickQuantity: this.scanForm.quantity,
          locationCode: this.currentPallet.locationCode,
          palletCode: this.currentPallet.palletCode,
          stockId: targetLockInfo.stockId,
          outStockLockInfoId: targetLockInfo.id // ä¼ é€’锁定信息ID
        }
        const result = await this.http.post('api/OutboundPicking/ConfirmPicking', request)
        if (result.status) {
          this.$message.success('拣选确认成功')
          // é‡ç½®è¡¨å•
          this.scanForm.barcode = ''
          this.scanForm.quantity = 1
          this.currentMaterialInfo = null
          // åˆ·æ–°æ•°æ®
          this.loadOrderInfo()
          this.loadPickedHistory()
          this.loadPalletLockInfo()
        } else {
          this.$message.error(result.message)
        }
      } catch (error) {
        this.$message.error('拣选确认失败: ' + (error.message || '未知错误'))
      }
    },
    // æ ¹æ®æ¡ç å’Œç‰©æ–™ç¼–码查找锁定信息
    findLockInfoByBarcodeAndMaterial(barcode, materielCode) {
      if (!this.allLockInfos || this.allLockInfos.length === 0) {
        return null
      }
      // é¦–先尝试精确匹配条码
      let lockInfo = this.allLockInfos.find(x =>
        x.currentBarcode === barcode && x.materielCode === materielCode
      )
      if (lockInfo) {
        return lockInfo
      }
      // å¦‚果精确匹配失败,只匹配物料编码(允许从同一物料的不同条码拣选)
      lockInfo = this.allLockInfos.find(x =>
        x.materielCode === materielCode &&
        (x.assignQuantity - x.pickedQty) > 0
      )
      return lockInfo
    },
    // éªŒè¯æ¡ç å¹¶èŽ·å–ç‰©æ–™ä¿¡æ¯
    async validateBarcode(barcode) {
      try {
        const result = await this.http.get(`api/OutboundPicking/ValidateBarcode?barcode=${barcode}`)
        if (result.status) {
          return result.data
        } else {
          this.$message.error(result.message)
          return null
        }
      } catch (error) {
        this.$message.error('条码验证失败')
        return null
      }
    },
    async handleBackToStock() {
      if (!this.currentPallet) return
      try {
        await this.$confirm(`确定将托盘 ${this.currentPallet.palletCode} å›žåº“吗?`, '提示', {
          type: 'warning'
        })
        const result = await this.http.post('api/BackToStock/GenerateBackToStockTask', {
          palletCode: this.currentPallet.palletCode,
          currentLocation: '拣选位',
          operator: '当前用户'
        })
        if (result.status) {
          this.$message.success('回库任务已生成')
          this.resetCurrentPallet()
        }
      } catch (error) {
        // ç”¨æˆ·å–消
      }
    },
    async handleDirectOutbound() {
      if (!this.currentPallet) return
      try {
        await this.$confirm(`确定将托盘 ${this.currentPallet.palletCode} ç›´æŽ¥å‡ºåº“吗?`, '提示', {
          type: 'warning'
        })
        const result = await this.http.post('api/OutboundPicking/DirectOutbound', {
          palletCode: this.currentPallet.palletCode
        })
        if (result.status) {
          this.$message.success('直接出库成功')
          this.resetCurrentPallet()
          this.loadOrderInfo()
        }
      } catch (error) {
        // ç”¨æˆ·å–消
      }
    },
    handleOpenSplit() {
      if (!this.currentLockInfo) {
        this.$message.warning('请先选择锁定信息')
        return
      }
      this.splitVisible = true
    },
    handleSplitSuccess() {
      this.$message.success('拆包成功')
      this.loadPalletLockInfo()
    },
    resetCurrentPallet() {
      this.currentPallet = null
      this.currentLockInfo = null
      this.currentMaterialInfo = null
      this.allLockInfos = []
      this.scanForm.palletCode = ''
    },
    async loadPickedHistory() {
      const orderId = this.$route.query.orderId
      if (!orderId) return
      try {
        const result = await this.http.get(`api/OutboundPicking/GetPickingHistory?orderId=${orderId}`)
        if (result.status) {
          this.pickedList = result.data
        }
      } catch (error) {
        console.error('加载拣选历史失败:', error)
      }
    },
    async handleCancelPick(row) {
      try {
        await this.$confirm('确定撤销这条拣选记录吗?', '提示', { type: 'warning' })
        const result = await this.http.post('api/OutboundPicking/CancelPicking', {
          pickingHistoryId: row.id
        })
        if (result.status) {
          this.$message.success('撤销成功')
          this.loadPickedHistory()
          this.loadOrderInfo()
        }
      } catch (error) {
        // ç”¨æˆ·å–消
      }
      orderInfo: {orderNo:''}
    }
  },
  mounted() {
    this.loadOrderInfo()
    this.loadPickedHistory()
    this.loadOrderInfo();
    this.$nextTick(() => {
      if (this.$refs.palletInput) {
        this.$refs.palletInput.focus()
      }
    })
  },
  methods: {
    loadOrderInfo() {
      const orderId = this.$route.query.orderId
      if (!orderId) return
      try {
        this.http.get(`/api/OutboundOrder/GetById?id=${orderId}`).then(response => {debugger;
          if (response.status) {
            this.orderInfo = response.data
          }
        })
      } catch (error) {
        ElMessage.error('加载出库单信息失败')
      }
    },
     goBack() {
      this.$router.back()
    },
    async handlePalletScan() {
      if (this.scanForm.palletCode) {
        ElMessage.success(`已扫描托盘: ${this.scanForm.palletCode}`)
        await this.loadPalletSummary()
        this.$nextTick(() => {
          if (this.$refs.materialInput) {
            this.$refs.materialInput.focus()
          }
        })
      }
    },
    async handleMaterialScan() {
      if (!this.scanForm.palletCode) {
        ElMessage.warning('请先扫描托盘码')
        this.$refs.palletInput.focus()
        return
      }
      if (!this.scanForm.materialBarcode) {
        ElMessage.warning('请扫描物料条码')
        return
      }
      await this.executePickingConfirm()
    },
    async loadPalletSummary() {
      if (!this.scanForm.palletCode) {
        this.palletSummary = null
        return
      }
      try {
        const result = await http.get('/api/outbound/getPalletPickingSummary', {
          params: {
            orderNo: this.orderNo,
            palletCode: this.scanForm.palletCode
          }
        })
        if (result.success) {
          // å¤„理统计信息
          const summary = result.data
          const assigned = summary.find(x => x.Status === '已分配') || { TotalAssignQty: 0, TotalPickedQty: 0 }
          const picked = summary.find(x => x.Status === '已拣选') || { TotalPickedQty: 0 }
          this.palletSummary = {
            unpickedCount: assigned.TotalAssignQty > 0 ? 1 : 0, // ç®€åŒ–计算
            unpickedTotal: assigned.TotalAssignQty - assigned.TotalPickedQty
  }
}
      } catch (error) {
        console.error('加载托盘统计失败:', error)
      }
    },
    async handleConfirm() {
      if (!this.scanForm.palletCode || !this.scanForm.materialBarcode) {
        ElMessage.warning('请填写完整的扫码信息')
        return
      }
      await this.executePickingConfirm()
    },
    async executePickingConfirm() {
      this.confirmLoading = true
      try {
        // å…ˆæ‰¾åˆ°å¯¹åº”的出库锁定信息
        const lockInfoResult = await this.http.get('/api/outbound/getOutStockLockInfo', {
          params: {
            orderNo: this.orderNo,
            palletCode: this.scanForm.palletCode,
            materialBarcode: this.scanForm.materialBarcode
          }
        })
        if (!lockInfoResult.success || !lockInfoResult.data || lockInfoResult.data.length === 0) {
          ElMessage.error('未找到对应的出库锁定信息')
          return
        }
        const lockInfo = lockInfoResult.data[0]
        const request = {
          outStockLockId: lockInfo.Id,
          taskNo: `TASK_${Date.now()}`,
          palletCode: this.scanForm.palletCode,
          materialBarcode: this.scanForm.materialBarcode,
          locationCode: lockInfo.LocationCode
        }
        const result = await this.http.post('/api/outbound/pickingConfirm', request)
        if (result.success) {
          ElMessage.success('分拣确认成功')
          this.handleReset()
          this.$emit('confirm')
          // åˆ·æ–°è¡¨æ ¼
          if (this.$refs.pickedTable) {
            this.$refs.pickedTable.refresh()
          }
          // åˆ·æ–°å‡ºåº“详情表格
          if (this.$refs.outboundTable) {
            this.$refs.outboundTable.refresh()
          }
          // é‡æ–°åŠ è½½æ‰˜ç›˜ç»Ÿè®¡
          await this.loadPalletSummary()
        } else {
          ElMessage.error(result.ElMessage)
        }
      } catch (error) {
        ElMessage.error('分拣确认失败')
      } finally {
        this.confirmLoading = false
      }
    },
    handleReset() {
      this.scanForm.materialBarcode = ''
      this.$nextTick(() => {
        if (this.$refs.materialInput) {
          this.$refs.materialInput.focus()
        }
      })
    }
  }
})
</script>
<style scoped>
.picking-confirm {
  padding: 20px;
  display: flex;
  flex-direction: column;
  height: 70vh;
}
.page-header {
  margin-bottom: 20px;
.content-layout {
  display: flex;
  gap: 16px;
  margin-bottom: 16px;
  flex: 1;
  min-height: 0; /* é‡è¦ï¼šé˜²æ­¢flex子元素溢出 */
}
.title {
  font-size: 18px;
  font-weight: bold;
.left-section {
  flex: 1;
  display: flex;
  flex-direction: column;
}
.right-section {
  flex: 1;
  display: flex;
  flex-direction: column;
}
.scan-section {
  margin-bottom: 20px;
  flex-shrink: 0;
}
.scan-alert {
  margin-bottom: 16px;
}
.scan-form {
  max-width: 500px;
}
.pallet-summary {
  margin: 16px 0;
}
.action-buttons {
  margin-top: 16px;
}
.outbound-details-card {
  height: 100%;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.action-buttons .el-button {
  width: 100%;
.outbound-details-card :deep(.el-card__body) {
  flex: 1;
  padding: 0;
}
.current-info {
  margin-top: 15px;
  padding: 10px;
  background-color: #f5f7fa;
  border-radius: 4px;
.picked-records {
  flex-shrink: 0;
  height: 300px;
}
.current-info p {
  margin: 5px 0;
  font-size: 14px;
}
.summary-info {
  margin-bottom: 15px;
.picked-records :deep(.el-card__body) {
  padding: 0;
}
</style>