spring
2025-11-29 dfe428b9d7e76c8172a8599d13db10c06fc336a2
fix: 完成扫码出库功能
已添加2个文件
已修改2个文件
443 ■■■■■ 文件已修改
src/api/product/outbound.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/outbound/index.vue 416 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/product/outbound.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
import request from "@/utils/request";
import { BaseResult } from "@/models/base";
const OutboundApi = {
  // å®Œæˆå‡ºåº“
  finishedOutbound(data: Array<{ outPutId: string | number; projectId: string | number }>) {
    return request<BaseResult<any>>({
      url: "/app/finishedOutbound",
      method: "POST",
      data: data,
    });
  },
};
export default OutboundApi;
src/pages.json
@@ -278,6 +278,12 @@
      }
    },
    {
      "path": "pages/outbound/index",
      "style": {
        "navigationBarTitleText": "出库"
      }
    },
    {
      "path": "pages/routingInspection/index",
      "style": {
        "navigationBarTitleText": "巡检"
src/pages/index/index.vue
@@ -201,6 +201,12 @@
    url: "/pages/routingInspection/index",
    show: false,
  },
  // {
  //   icon: "/static/icons/log.png",
  //   title: "出库",
  //   url: "/pages/outbound/index",
  //   show: true,
  // },
]);
// åŠ è½½è®¿é—®ç»Ÿè®¡æ•°æ®
src/pages/outbound/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,416 @@
<template>
  <view class="list_box">
    <CardTitle title="出库" :hideAction="false">
      <template #action>
        <wd-button type="icon" icon="scan" color="#0D867F" @click="openScan"></wd-button>
      </template>
    </CardTitle>
    <view class="list_content">
      <view v-if="outboundList.length === 0" class="empty_tip">
        <view class="empty_text">暂无出库数据</view>
      </view>
      <view v-for="(item, index) in outboundList" :key="index" class="outbound_item">
        <view class="outbound_item_left">
          <view class="outbound_item_content">
            <view class="outbound_item_row">
              <text class="outbound_item_label">合同号:</text>
              <text class="outbound_item_value">{{ item.contractNo || "-" }}</text>
            </view>
            <view class="outbound_item_row">
              <text class="outbound_item_label">生产批次号:</text>
              <text class="outbound_item_value">{{ item.batchNo || "-" }}</text>
            </view>
            <view class="outbound_item_row">
              <text class="outbound_item_label">规格型号:</text>
              <text class="outbound_item_value">{{ item.model || "-" }}</text>
            </view>
            <view class="outbound_item_row">
              <text class="outbound_item_label">生产日期:</text>
              <text class="outbound_item_value">{{ item.productionDate || "-" }}</text>
            </view>
          </view>
        </view>
        <view class="outbound_item_action">
          <wd-button
            type="icon"
            icon="delete"
            size="small"
            custom-class="delete-btn"
            @click.stop="removeOutboundItem(index)"
          ></wd-button>
        </view>
      </view>
    </view>
    <view v-if="outboundList.length > 0" class="outbound_footer">
      <wd-button block @click="handleOutbound">
        <text class="text-[#fff]">出库</text>
      </wd-button>
    </view>
    <Scan ref="scanRef" emitName="scanOutbound" />
    <wd-toast />
  </view>
</template>
<script setup lang="ts">
import CardTitle from "@/components/card-title/index.vue";
import Scan from "@/components/scan/index.vue";
import { useToast } from "wot-design-uni";
import { dayjs } from "wot-design-uni";
import OutboundApi from "@/api/product/outbound";
const scanRef = ref();
const toast = useToast();
const outboundList = ref<any[]>([]);
const projectId = ref<string | number>("");
// æ ¼å¼åŒ–æ—¶é—´
const formatTime = (date: Date) => {
  return dayjs(date).format("YYYY-MM-DD HH:mm:ss");
};
// è§£æžæ‰«ç å†…容
const parseScanCode = (scanCode: string) => {
  try {
    // ç¬¬ä¸€æ¬¡è§£æžï¼šè§£æžå¤–层JSON
    const outerParsed = JSON.parse(scanCode);
    // å¦‚果外层有 code å­—段,且是字符串,需要再次解析
    let innerData = null;
    if (outerParsed.code && typeof outerParsed.code === "string") {
      try {
        const innerParsed = JSON.parse(outerParsed.code);
        // æŸ¥æ‰¾æ‰€æœ‰æ•°å­—key(如 "12480"),这个数字key就是id
        const keys = Object.keys(innerParsed);
        // æ‰¾åˆ°æ‰€æœ‰æ•°å­—key,排除 "code" å­—段
        const numberKeys = keys.filter((key) => !isNaN(Number(key)) && key !== "code");
        if (numberKeys.length > 0) {
          // å–第一个数字key(这个就是id)
          const dataKey = numberKeys[0];
          const idValue = Number(dataKey); // æ•°å­—key就是id值
          // æå–æ•°å­—key对应的数据对象
          const extractedData = innerParsed[dataKey];
          if (extractedData) {
            innerData = { ...extractedData }; // å¤åˆ¶æ•°æ®å¯¹è±¡
            // ç¡®ä¿æ•°æ®å¯¹è±¡ä¸­æœ‰id字段,如果没有则使用数字key作为id
            if (!innerData.id) {
              innerData.id = idValue;
            }
          }
        } else {
          // å¦‚果没有数字key,尝试直接使用对象(排除 code å­—段)
          const { code, ...rest } = innerParsed;
          if (Object.keys(rest).length > 0) {
            innerData = rest;
          }
        }
      } catch (e) {
        console.error("内层JSON解析失败:", e);
      }
    } else {
      // å¦‚果没有 code å­—段,直接使用外层数据
      innerData = outerParsed;
    }
    // å¦‚æžœ innerData ä»ç„¶ä¸ºç©ºï¼Œå°è¯•直接解析
    if (!innerData) {
      innerData = outerParsed;
    }
    // å¦‚æžœ innerData ä»ç„¶åŒ…含数字key结构(如 { "12480": {...}, "code": "..." }),需要再次提取
    if (innerData && typeof innerData === "object" && !innerData.id && !innerData.contractno) {
      const keys = Object.keys(innerData);
      const numberKeys = keys.filter((key) => !isNaN(Number(key)) && key !== "code");
      if (numberKeys.length > 0) {
        const dataKey = numberKeys[0];
        const extractedData = innerData[dataKey];
        if (extractedData) {
          innerData = { ...extractedData };
        }
      }
    }
    // æå–数据字段(根据二维码实际字段名统一)
    const data = {
      id: innerData?.id,
      contractNo: innerData?.contractno,
      batchNo: innerData?.monofilamentnumber,
      model: innerData?.model,
      weight: innerData?.actuallyweight,
      productionDate: innerData?.producttime,
      projectId: innerData?.projectid,
      // ä¿ç•™åŽŸå§‹æ•°æ®ï¼ˆä¿å­˜æå–åŽçš„æ•°æ®å¯¹è±¡ï¼‰
      rawData: innerData,
      scanCode: scanCode,
    };
    // å¦‚果扫码数据中有 projectId,保存它
    if (data.projectId && !projectId.value) {
      projectId.value = data.projectId;
    }
    return data;
  } catch (error) {
    console.error("JSON解析失败:", error);
    // å¦‚果不是JSON,尝试按逗号分割(可能是CSV格式)
    const parts = scanCode.split(",");
    if (parts.length >= 5) {
      return {
        id: parts[0] || "-", // å‡è®¾ç¬¬ä¸€ä¸ªæ˜¯id
        contractNo: parts[1] || "-",
        batchNo: parts[2] || "-",
        model: parts[3] || "-",
        weight: parts[4] || "-",
        productionDate: parts[5] || "-",
        projectId: projectId.value || "-",
        scanCode: scanCode,
      };
    }
    // å¦‚果都无法解析,返回原始字符串
    return {
      id: scanCode, // ä½¿ç”¨åŽŸå§‹å­—ç¬¦ä¸²ä½œä¸ºid
      contractNo: scanCode,
      batchNo: "-",
      model: "-",
      weight: "-",
      productionDate: "-",
      projectId: projectId.value || "-",
      scanCode: scanCode,
    };
  }
};
// æ‰«ç å›žè°ƒ
const getScanCode = (code: any) => {
  // å¦‚æžœ code æ˜¯å¯¹è±¡ä¸”有 code å­—段,使用 code.code;否则直接使用 code
  let scanCode = code.code || code;
  // å¦‚æžœ scanCode æ˜¯å¯¹è±¡ï¼Œå°è¯•获取其 code å­—段
  if (typeof scanCode === "object" && scanCode.code) {
    scanCode = scanCode.code;
  }
  // å¦‚æžœ scanCode æ˜¯å­—符串,直接使用;如果是对象,转为字符串
  if (typeof scanCode !== "string") {
    scanCode = JSON.stringify(scanCode);
  }
  if (!scanCode) {
    toast.error("扫码内容为空");
    return;
  }
  // è§£æžæ‰«ç å†…容
  const parsedData = parseScanCode(scanCode);
  // ä½¿ç”¨id作为唯一标识
  const uniqueId = parsedData.id;
  // å¦‚果没有id,提示错误
  if (!uniqueId || uniqueId === "-" || uniqueId === null || uniqueId === undefined) {
    toast.error("扫码内容缺少唯一标识,无法添加");
    return;
  }
  // æ£€æŸ¥æ˜¯å¦å·²å­˜åœ¨ï¼ˆæ ¹æ®id判断)
  const exists = outboundList.value.some((item) => {
    const itemId = item.id;
    return itemId && itemId === uniqueId && itemId !== "-";
  });
  if (exists) {
    toast.error("该条码已存在,请勿重复扫码");
    return;
  }
  // æ·»åŠ åˆ°åˆ—è¡¨
  const newItem = {
    ...parsedData,
    scanTime: formatTime(new Date()),
  };
  outboundList.value.push(newItem);
  toast.success("扫码成功");
};
// è§¦å‘扫码
const openScan = () => {
  scanRef.value.triggerScan();
};
// åˆ é™¤é¡¹
const removeOutboundItem = (index: number) => {
  const item = outboundList.value[index];
  const itemName = item.contractNo || item.batchNo || `第${index + 1}项`;
  uni.showModal({
    title: "确认删除",
    content: `确定要删除"${itemName}"吗?`,
    confirmText: "删除",
    cancelText: "取消",
    confirmColor: "#ff4444",
    success: (res) => {
      if (res.confirm) {
        outboundList.value.splice(index, 1);
        toast.success("删除成功");
      }
    },
  });
};
// å¤„理出库
const handleOutbound = async () => {
  if (outboundList.value.length === 0) {
    toast.error("暂无出库数据");
    return;
  }
  // æž„建请求数据
  const requestData = outboundList.value.map((item) => ({
    outPutId: item.id,
    projectId: item.projectId,
  }));
  try {
    uni.showLoading({
      title: "出库中...",
      mask: true,
    });
    console.log("requestData", requestData);
    const { code, msg } = await OutboundApi.finishedOutbound(requestData);
    uni.hideLoading();
    if (code === 200) {
      toast.success("出库成功");
      // æ¸…空列表
      outboundList.value = [];
    } else {
      toast.error(msg || "出库失败");
    }
  } catch (error: any) {
    uni.hideLoading();
    console.error("出库失败:", error);
  }
};
// ç¡®ä¿å…ˆç§»é™¤å†æ·»åŠ ç›‘å¬
const setupScanListener = () => {
  uni.$off("scanOutbound", getScanCode); // å…ˆç§»é™¤æ—§çš„
  uni.$on("scanOutbound", getScanCode); // å†æ·»åŠ æ–°çš„
};
onMounted(() => {
  setupScanListener();
});
onUnmounted(() => {
  uni.$off("scanOutbound", getScanCode);
});
</script>
<style lang="scss" scoped>
.list_box {
  height: calc(100vh - 100px);
  background: #f3f9f8;
  display: flex;
  flex-direction: column;
}
.list_content {
  flex: 1;
  overflow-y: auto;
  padding-bottom: 120rpx;
}
.empty_tip {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 100rpx 0;
  color: #999;
  .empty_text {
    font-size: 32rpx;
    margin-bottom: 20rpx;
  }
}
.outbound_item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 30rpx;
  margin: 20rpx;
  background: #fff;
  border-radius: 16rpx;
  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  .outbound_item_left {
    display: flex;
    align-items: center;
    flex: 1;
  }
  .outbound_item_content {
    flex: 1;
  }
  .outbound_item_row {
    display: flex;
    align-items: center;
    margin-bottom: 12rpx;
    font-size: 28rpx;
    &:last-child {
      margin-bottom: 0;
    }
  }
  .outbound_item_label {
    color: #666;
    min-width: 140rpx;
    flex-shrink: 0;
  }
  .outbound_item_value {
    color: #333;
    flex: 1;
    word-break: break-all;
  }
  .outbound_item_action {
    display: flex;
    align-items: center;
    justify-content: center;
    margin-left: 20rpx;
    flex-shrink: 0;
  }
}
:deep(.delete-btn) {
  .wd-button__content {
    color: #ff4444 !important;
  }
  &:active {
    opacity: 0.7;
  }
}
:deep(.wd-button__content) {
  color: #0d867f;
}
.outbound_footer {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  padding: 20rpx 30rpx;
  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
  z-index: 100;
}
</style>