Kaynağa Gözat

库存并库init

zh 3 ay önce
ebeveyn
işleme
8f8c1e86aa
1 değiştirilmiş dosya ile 474 ekleme ve 0 silme
  1. 474 0
      src/views/robot/merge/index.vue

+ 474 - 0
src/views/robot/merge/index.vue

@@ -0,0 +1,474 @@
+<script setup lang="ts">
+import { ref, reactive, computed } from 'vue'
+import { showToast } from 'vant'
+
+// 扫描料箱号
+const boxCode = ref('')
+
+// 商品信息
+const productInfo = reactive({
+  targetLocation: 'HK000001',
+  stockQty: '',
+  productName: '',
+  barcode: '',
+  qualityStatus: 'ZP',
+  warehouseType: 'CHZP',
+  batchNo: '',
+  productionDate: '',
+  expiryDate: '',
+  moveQty: '',
+  targetLocationNew: ''
+})
+
+// 移库数量编辑
+const isEditingMoveQty = ref(false)
+const editMoveQty = () => {
+  isEditingMoveQty.value = true
+}
+const confirmMoveQty = () => {
+  isEditingMoveQty.value = false
+}
+
+// 料箱数据 - 支持分割成多个库位
+interface SubLocation {
+  id: string
+  status: 'empty' | 'filled' | 'selected'
+}
+
+interface BoxItem {
+  id: number
+  status: 'empty' | 'filled' | 'emptyBox' | 'waiting'
+  label?: string
+  splitCount?: number // 分割数量: 2, 4, 6, 8
+  subLocations?: SubLocation[] // 子库位
+}
+
+// 生成子库位
+const generateSubLocations = (boxId: number, count: number, filledIndexes: number[] = []): SubLocation[] => {
+  return Array.from({ length: count }, (_, i) => ({
+    id: `${boxId}-${i + 1}`,
+    status: filledIndexes.includes(i) ? 'filled' : 'empty'
+  }))
+}
+
+const boxList = ref<BoxItem[]>([
+  { id: 1, status: 'filled' },
+  { 
+    id: 2, 
+    status: 'empty',
+    splitCount: 2,
+    subLocations: generateSubLocations(2, 2, [0])
+  },
+  { 
+    id: 3, 
+    status: 'empty',
+    splitCount: 4,
+    subLocations: generateSubLocations(3, 4, [0, 1])
+  },
+  { 
+    id: 4, 
+    status: 'empty',
+    splitCount: 6,
+    subLocations: generateSubLocations(4, 6, [0, 2])
+  },
+  { 
+    id: 5, 
+    status: 'empty',
+    splitCount: 8,
+    subLocations: generateSubLocations(5, 8, [0, 3, 5])
+  },
+  { id: 6, status: 'empty' },
+  { id: 7, status: 'filled' },
+  { id: 8, status: 'empty' },
+  { id: 9, status: 'emptyBox', label: '空箱' },
+  { id: 10, status: 'waiting', label: '等待调箱' },
+  { id: 11, status: 'empty' },
+  { id: 12, status: 'empty' }
+])
+
+// 获取子库位的grid样式
+const getSubGridStyle = (splitCount: number) => {
+  switch (splitCount) {
+    case 2:
+      return { gridTemplateColumns: 'repeat(2, 1fr)', gridTemplateRows: '1fr' }
+    case 4:
+      return { gridTemplateColumns: 'repeat(2, 1fr)', gridTemplateRows: 'repeat(2, 1fr)' }
+    case 6:
+      return { gridTemplateColumns: 'repeat(3, 1fr)', gridTemplateRows: 'repeat(2, 1fr)' }
+    case 8:
+      return { gridTemplateColumns: 'repeat(4, 1fr)', gridTemplateRows: 'repeat(2, 1fr)' }
+    default:
+      return { gridTemplateColumns: 'repeat(2, 1fr)', gridTemplateRows: 'repeat(2, 1fr)' }
+  }
+}
+
+// 选中的料箱/库位
+const selectedBox = ref<string | number | null>(null)
+
+// 选择料箱或子库位
+const selectBox = (box: BoxItem, subId?: string) => {
+  if (box.status === 'waiting' || box.status === 'emptyBox') return
+  selectedBox.value = subId || box.id
+}
+
+// 选择子库位
+const selectSubLocation = (box: BoxItem, sub: SubLocation) => {
+  selectedBox.value = sub.id
+}
+
+// 呼唤机器人
+const callRobot = () => {
+  showToast('正在呼唤机器人...')
+}
+
+// 重新输入
+const resetInput = () => {
+  boxCode.value = ''
+  selectedBox.value = null
+}
+
+// 提交移库
+const submitMove = () => {
+  if (!boxCode.value) {
+    showToast('请先扫描料箱号')
+    return
+  }
+  showToast('提交移库成功')
+}
+</script>
+
+<template>
+  <div class="merge-container">
+    <!-- 扫描输入框 -->
+    <div class="scan-section">
+      <van-field
+        v-model="boxCode"
+        placeholder="请扫描料箱号"
+        clearable
+      />
+    </div>
+
+    <!-- 信息展示表格 -->
+    <div class="info-table">
+      <div class="table-row">
+        <div class="cell label">目标库位</div>
+        <div class="cell value">{{ productInfo.targetLocation }}</div>
+        <div class="cell label">库存数量</div>
+        <div class="cell value">{{ productInfo.stockQty }}</div>
+      </div>
+      <div class="table-row">
+        <div class="cell label">商品名称</div>
+        <div class="cell value span-2">{{ productInfo.productName }}</div>
+      </div>
+      <div class="table-row">
+        <div class="cell label">商品条码</div>
+        <div class="cell value span-2">{{ productInfo.barcode }}</div>
+      </div>
+      <div class="table-row">
+        <div class="cell label">质量状态</div>
+        <div class="cell value">{{ productInfo.qualityStatus }}</div>
+        <div class="cell label">属性仓</div>
+        <div class="cell value">{{ productInfo.warehouseType }}</div>
+        <div class="cell label">批号</div>
+        <div class="cell value"></div>
+      </div>
+      <div class="table-row">
+        <div class="cell label">生产日期</div>
+        <div class="cell value">{{ productInfo.productionDate }}</div>
+        <div class="cell label">失效日期</div>
+        <div class="cell value">{{ productInfo.expiryDate }}</div>
+      </div>
+      <div class="table-row">
+        <div class="cell label">目标库位</div>
+        <div class="cell value">{{ productInfo.targetLocationNew }}</div>
+        <div class="cell label">移库数量</div>
+        <div class="cell value editable" @dblclick="editMoveQty">
+          <template v-if="isEditingMoveQty">
+            <van-field
+              v-model="productInfo.moveQty"
+              type="number"
+              autofocus
+              @blur="confirmMoveQty"
+              @keyup.enter="confirmMoveQty"
+            />
+          </template>
+          <template v-else>
+            <span>{{ productInfo.moveQty }}</span>
+            <span v-if="!productInfo.moveQty" class="placeholder">双击编辑</span>
+          </template>
+        </div>
+      </div>
+    </div>
+
+    <!-- 料箱选择区域 -->
+    <div class="grid-section">
+      <div class="grid-container">
+        <div
+          v-for="box in boxList"
+          :key="box.id"
+          class="box-wrapper"
+        >
+          <!-- 序号在料箱上方 -->
+          <div class="box-number">{{ box.id }}</div>
+          <div
+            class="box-item"
+            :class="{
+              'box-filled': box.status === 'filled' && !box.splitCount,
+              'box-empty-box': box.status === 'emptyBox',
+              'box-waiting': box.status === 'waiting',
+              'box-selected': selectedBox === box.id,
+              'box-split': box.splitCount
+            }"
+            @click="!box.splitCount && selectBox(box)"
+          >
+            <!-- 分割的料箱 -->
+            <template v-if="box.splitCount && box.subLocations">
+              <div class="sub-grid" :style="getSubGridStyle(box.splitCount)">
+                <div
+                  v-for="sub in box.subLocations"
+                  :key="sub.id"
+                  class="sub-location"
+                  :class="{
+                    'sub-filled': sub.status === 'filled',
+                    'sub-selected': selectedBox === sub.id
+                  }"
+                  @click.stop="selectSubLocation(box, sub)"
+                ></div>
+              </div>
+            </template>
+            <!-- 普通料箱 -->
+            <template v-else>
+              <span v-if="box.label" class="box-label">{{ box.label }}</span>
+            </template>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 底部按钮 -->
+    <div class="footer-buttons">
+      <van-button class="btn-robot" size="small" @click="callRobot">呼唤机器人</van-button>
+      <div class="btn-right">
+        <van-button type="primary" class="btn-reset" size="small" @click="resetInput">重新输入</van-button>
+        <van-button class="btn-submit" size="small" @click="submitMove">提交移库</van-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.merge-container {
+  min-height: 100vh;
+  background: #f5f5f5;
+  padding: 12px;
+  box-sizing: border-box;
+}
+
+.scan-section {
+  margin-bottom: 12px;
+  
+  :deep(.van-field) {
+    border-radius: 4px;
+  }
+}
+
+.info-table {
+  background: #fff;
+  border: 1px solid #ddd;
+  margin-bottom: 12px;
+
+  .table-row {
+    display: flex;
+    border-bottom: 1px solid #ddd;
+
+    &:last-child {
+      border-bottom: none;
+    }
+  }
+
+  .cell {
+    padding: 8px 6px;
+    font-size: 13px;
+    border-right: 1px solid #ddd;
+    flex: 1;
+    display: flex;
+    align-items: center;
+
+    &:last-child {
+      border-right: none;
+    }
+
+    &.label {
+      background: #f9f9f9;
+      color: #666;
+      flex: 0 0 70px;
+    }
+
+    &.value {
+      color: #333;
+
+      &.editable {
+        cursor: pointer;
+        min-height: 20px;
+
+        .placeholder {
+          color: #ccc;
+          font-size: 12px;
+        }
+
+        :deep(.van-field) {
+          padding: 0;
+          
+          .van-field__body {
+            height: 20px;
+          }
+          
+          .van-field__control {
+            font-size: 13px;
+          }
+        }
+      }
+    }
+
+    &.span-2 {
+      flex: 2;
+    }
+  }
+}
+
+.grid-section {
+  background: #fff;
+  padding: 12px;
+  margin-bottom: 80px;
+  border-radius: 4px;
+
+  .grid-container {
+    display: grid;
+    grid-template-columns: repeat(6, 1fr);
+    gap: 8px;
+  }
+
+  .box-wrapper {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+
+    .box-number {
+      font-size: 12px;
+      color: #333;
+      margin-bottom: 2px;
+    }
+  }
+
+  .box-item {
+    width: 100%;
+    aspect-ratio: 1;
+    border: 1px solid #ddd;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    font-size: 14px;
+    color: #333;
+    background: #fff;
+    cursor: pointer;
+    border-radius: 4px;
+    position: relative;
+
+    .box-label {
+      font-size: 11px;
+      color: #666;
+      text-align: center;
+      line-height: 1.2;
+    }
+
+    &.box-filled {
+      background: #d4e5f7;
+      border-color: #4a90d9;
+    }
+
+    &.box-empty-box {
+      background: #f5f5f5;
+      border-color: #ddd;
+    }
+
+    &.box-waiting {
+      background: #f5d4d4;
+      border-color: #e0b0b0;
+    }
+
+    &.box-selected {
+      border: 2px solid #1989fa;
+    }
+
+    &.box-split {
+      padding: 3px;
+      cursor: default;
+
+      .sub-grid {
+        width: 100%;
+        height: 100%;
+        display: grid;
+        gap: 2px;
+      }
+
+      .sub-location {
+        border: 1px solid #ddd;
+        background: #fff;
+        cursor: pointer;
+        border-radius: 2px;
+
+        &.sub-filled {
+          background: #d4e5f7;
+          border-color: #4a90d9;
+        }
+
+        &.sub-selected {
+          border: 2px solid #1989fa;
+        }
+      }
+    }
+  }
+}
+
+.footer-buttons {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 10px 12px;
+  background: #fff;
+  box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
+
+  .btn-right {
+    display: flex;
+    gap: 8px;
+  }
+
+  .van-button {
+    height: 32px;
+    font-size: 13px;
+    padding: 0 12px;
+  }
+
+  .btn-robot {
+    background: #fff;
+    border: 1px solid #ff9800;
+    color: #ff9800;
+  }
+
+  .btn-reset {
+    background: #5b9bd5;
+    border-color: #5b9bd5;
+  }
+
+  .btn-submit {
+    background: #ff9800;
+    border-color: #ff9800;
+    color: #fff;
+  }
+}
+</style>