spring
4 天以前 e7dc2205d169192eea267697061efd64d89f5d63
fix: 完成出库重构
已添加2个文件
已修改4个文件
1488 ■■■■ 文件已修改
src/api/product/outbound.ts 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index/index.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/outbound/index.vue 466 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/outbound/index0.vue 350 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/outbound/material.vue 627 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/product/outbound.ts
@@ -11,6 +11,38 @@
    });
  },
  // å‡ºåº“单查询
  queryErpOutboundOrder(params: {
    contractNo?: string;
    carNo?: string;
    current: number;
    size: number;
  }) {
    return request<BaseResult<any>>({
      url: "/app/queryErpOutboundOrder",
      method: "POST",
      data: params,
    });
  },
  // æŸ¥è¯¢å‡ºè´§å•明细
  queryErpOutboundOrderDetail(params: { vbillcode: string }) {
    return request<BaseResult<any[]>>({
      url: "/app/queryErpOutboundOrderDetail",
      method: "POST",
      data: params,
    });
  },
  // ç»žçº¿/拉丝二维码查询(支持出库状态判断)
  getTagByIdAll(params: { outPutId: string | number }) {
    return request<BaseResult<any[]>>({
      url: "/app/getTagByIdAll",
      method: "GET",
      data: params,
    });
  },
  // ç»žçº¿äºŒç»´ç æŸ¥è¯¢
  getTagByIdJx(params: { outPutId: string | number }) {
    return request<BaseResult<any>>({
src/pages.json
@@ -284,6 +284,12 @@
      }
    },
    {
      "path": "pages/outbound/material",
      "style": {
        "navigationBarTitleText": "物料详情"
      }
    },
    {
      "path": "pages/routingInspection/index",
      "style": {
        "navigationBarTitleText": "巡检"
src/pages/index/index.vue
@@ -278,14 +278,19 @@
    }
  }
  if (data.deviceGroupName.includes("丝") || data.deviceGroupName.includes("绞")) {
  if (
    (data.deviceGroupName && data.deviceGroupName.includes("丝")) ||
    (data.deviceGroupName && data.deviceGroupName.includes("绞"))
  ) {
    // å¦‚果是丝或绞,显示生产管理菜单
    navList[0].show = true;
  }
  console.log("isInspector.value", isInspector.value);
  if (isInspector.value) {
    // å¦‚果是巡检员,显示巡检菜单
    navList[1].show = true;
  }
  console.log("isStorage.value", isStorage.value);
  if (isStorage.value) {
    // å¦‚果是出库角色,显示出库菜单
    navList[2].show = true;
src/pages/outbound/index.vue
@@ -1,350 +1,212 @@
<template>
  <view class="list_box">
    <CardTitle title="出库" :hideAction="false">
      <template #action>
        <wd-button type="icon" icon="scan" color="#0D867F" @click="openScan"></wd-button>
  <view class="outbound-page">
    <view class="search-section">
      <wd-row>
        <wd-col :span="21">
          <wd-search
            v-model="searchKeyword"
            placeholder="请输入车牌号"
            placeholder-left
            hide-cancel
            @search="handleSearch"
            @clear="handleClear"
          ></wd-search>
        </wd-col>
        <wd-col :span="3">
          <view class="scan_box" @click="handleSearch">
            <wd-icon name="search" size="24px" color="#0D867F"></wd-icon>
          </view>
        </wd-col>
      </wd-row>
    </view>
    <view class="list-section">
      <z-paging
        ref="pagingRef"
        v-model="shippingList"
        :fixed="false"
        :auto-show-back-to-top="true"
        @query="getList"
      >
        <wd-card
          v-for="(item, index) in shippingList"
          :key="item.deliveryid || index"
          custom-class="card_bg"
          @click="toMaterialDetail(item)"
        >
          <template #title>
            <text class="font-medium text-[#252525]">发货单号: {{ item.vbillcode || "-" }}</text>
      </template>
    </CardTitle>
    <view class="list_content">
      <view v-if="outboundList.length === 0" class="empty_tip">
        <view class="empty_text">暂无出库数据</view>
          <wd-row class="my-2">
            <wd-col :span="24">
              <view class="flex">
                <view class="icon_box">
                  <wd-icon name="folder" color="#0D867F"></wd-icon>
      </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>
                <text class="text-[#646874] mx-2">
                  å‘货单日期:
                  <text class="text-[#252525]">{{ item.dbilldate || "-" }}</text>
                </text>
            </view>
            <view class="outbound_item_row">
              <text class="outbound_item_label">生产批次号:</text>
              <text class="outbound_item_value">{{ item.batchNo || "-" }}</text>
            </wd-col>
          </wd-row>
          <wd-row class="my-2">
            <wd-col :span="24">
              <view class="flex">
                <view class="icon_box">
                  <wd-icon name="folder" color="#0D867F"></wd-icon>
            </view>
            <view class="outbound_item_row">
              <text class="outbound_item_label">规格型号:</text>
              <text class="outbound_item_value">{{ item.model || "-" }}</text>
                <text class="text-[#646874] mx-2">
                  è½¦ç‰Œå·:
                  <text class="text-[#252525]">{{ item.carno || "-" }}</text>
                </text>
            </view>
            <view class="outbound_item_row">
              <text class="outbound_item_label">重量:</text>
              <text class="outbound_item_value">{{ item.weight || "-" }} kg</text>
            </wd-col>
          </wd-row>
          <wd-row class="my-2" v-if="item.vmemo">
            <wd-col :span="24">
              <view class="flex">
                <view class="icon_box">
                  <wd-icon name="folder" color="#0D867F"></wd-icon>
            </view>
            <view class="outbound_item_row">
              <text class="outbound_item_label">厂家:</text>
              <text class="outbound_item_value">{{ item.clienteleName || "-" }}</text>
                <text class="text-[#646874] mx-2">
                  å‘货单备注:
                  <text class="text-[#252525]">{{ item.vmemo || "-" }}</text>
                </text>
            </view>
            <view class="outbound_item_row">
              <text class="outbound_item_label">段长:</text>
              <text class="outbound_item_value">{{ item.actuallyLength || "-" }} M</text>
            </wd-col>
          </wd-row>
        </wd-card>
      </z-paging>
            </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 { ref } from "vue";
import { useToast } from "wot-design-uni";
import { dayjs } from "wot-design-uni";
import zPaging from "@/components/z-paging/z-paging.vue";
import OutboundApi from "@/api/product/outbound";
const scanRef = ref();
const toast = useToast();
const outboundList = ref<any[]>([]);
const projectId = ref<string | number>("");
const pagingRef = ref();
const searchKeyword = ref("");
const shippingList = ref<any[]>([]);
// æ ¼å¼åŒ–æ—¶é—´
const formatTime = (date: Date) => {
  return dayjs(date).format("YYYY-MM-DD HH:mm:ss");
// èŽ·å–åˆ—è¡¨æ•°æ®
const getList = async (pageNo: number, pageSize: number) => {
  const params: any = {
    contractNo: "",
    carNo: searchKeyword.value || "",
    current: pageNo,
    size: pageSize,
};
// æ‰«ç å›žè°ƒ
const getScanCode = async (code: any) => {
  try {
    // å¦‚æžœ 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;
    }
    // è§£æžæ‰«ç æ•°æ®ï¼ŒçŽ°åœ¨äºŒç»´ç åªåŒ…å«id
    let scanData;
    try {
      scanData = JSON.parse(scanCode);
    } catch (e) {
      toast.error("二维码格式错误");
      return;
    }
    const outPutId = scanData.id;
    if (!outPutId) {
      toast.error("二维码格式错误,缺少id信息");
      return;
    }
    // æ£€æŸ¥æ˜¯å¦å·²å­˜åœ¨ï¼ˆæ ¹æ®id判断)
    const exists = outboundList.value.some((item) => {
      const itemId = item.id;
      return itemId && itemId === outPutId && itemId !== "-";
    });
    if (exists) {
      toast.error("该条码已存在,请勿重复扫码");
      return;
    }
    // è°ƒç”¨æŽ¥å£èŽ·å–ç»žçº¿è¯¦ç»†ä¿¡æ¯
    const { data: tagData } = await OutboundApi.getTagByIdJx({
      outPutId: outPutId,
    });
    // æå–数据字段(根据接口返回的数据结构)
    const parsedData = {
      id: tagData?.id || outPutId,
      contractNo: tagData?.contractno || tagData?.contractNo || "-",
      batchNo: tagData?.systemno || tagData?.systemNo || tagData?.batchNo || "-",
      model: tagData?.model || "-",
      weight: tagData?.actuallyweight || tagData?.actuallyWeight || tagData?.weight || "-",
      clienteleName: tagData?.clientelename || tagData?.clienteleName || "-",
      actuallyLength: tagData?.actuallylength || tagData?.actuallyLength || "-",
      productionDate: tagData?.producttime || tagData?.productionDate || "-",
      projectId: tagData?.projectid || tagData?.projectId || projectId.value || "",
      // ä¿ç•™åŽŸå§‹æ•°æ®
      rawData: tagData,
      scanCode: scanCode,
    };
    // å¦‚果接口返回的数据中有 projectId,保存它
    if (parsedData.projectId && !projectId.value) {
      projectId.value = parsedData.projectId;
    }
    // æ·»åŠ åˆ°åˆ—è¡¨
    const newItem = {
      ...parsedData,
      scanTime: formatTime(new Date()),
    };
    outboundList.value.push(newItem);
    toast.success("扫码成功");
  } catch (error: any) {
    console.error("扫码处理失败:", error);
    toast.error(error.msg || "二维码异常,请更换二维码!");
  }
};
// è§¦å‘扫码
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 = [];
    const { code, data, msg } = await OutboundApi.queryErpOutboundOrder(params);
    if (code === 200 && data) {
      const records = data.records || [];
      if (!records.length) {
        pagingRef.value.complete(true);
    } else {
      toast.error(msg || "出库失败");
        pagingRef.value.complete(records);
    }
  } catch (error: any) {
    uni.hideLoading();
    console.error("出库失败:", error);
    } else {
      toast.error(msg || "获取出库单列表失败");
      pagingRef.value.complete(true);
    }
  } catch (error) {
    console.error("获取出库单列表失败:", error);
    toast.error("获取出库单列表失败");
    pagingRef.value.complete(true);
  }
};
// ç¡®ä¿å…ˆç§»é™¤å†æ·»åŠ ç›‘å¬
const setupScanListener = () => {
  uni.$off("scanOutbound", getScanCode); // å…ˆç§»é™¤æ—§çš„
  uni.$on("scanOutbound", getScanCode); // å†æ·»åŠ æ–°çš„
// æœç´¢å¤„理
const handleSearch = () => {
  // é‡æ–°åŠ è½½æ•°æ®
  pagingRef.value.reload();
};
onMounted(() => {
  setupScanListener();
});
// æ¸…空搜索
const handleClear = () => {
  searchKeyword.value = "";
  handleSearch();
};
onUnmounted(() => {
  uni.$off("scanOutbound", getScanCode);
// è·³è½¬åˆ°ç‰©æ–™è¯¦æƒ…页
const toMaterialDetail = (item: any) => {
  uni.navigateTo({
    url: `/pages/outbound/material?id=${item.deliveryid}&vbillcode=${item.vbillcode}`,
});
};
</script>
<style lang="scss" scoped>
.list_box {
  height: calc(100vh - 100px);
.outbound-page {
  min-height: 100vh;
  background: #f3f9f8;
  display: flex;
  flex-direction: column;
  padding: 10rpx;
}
.list_content {
  flex: 1;
  overflow-y: auto;
  padding-bottom: 120rpx;
.search-section {
  background: #fff;
  padding: 10rpx;
  margin-bottom: 10rpx;
}
.scan_box {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  padding: 6px;
  background: #fff;
}
.list-section {
  padding: 0 4px;
  height: calc(100vh - 150rpx);
}
.card_bg {
  box-shadow: 0px 0px 12px 0px rgba(0, 0, 0, 0.05);
  padding-bottom: 10px;
  margin-bottom: 10px;
  margin-left: 6px;
  margin-right: 6px;
}
.shipping-card {
  margin: 0 5px;
}
.icon_box {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  background: #e7f4ec99;
  border-radius: 50%;
}
.empty_tip {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 100rpx 0;
  color: #999;
  align-items: center;
  padding: 40px 0;
}
  .empty_text {
    font-size: 32rpx;
    margin-bottom: 20rpx;
  }
  color: #999;
  font-size: 14px;
}
.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;
:deep(.wd-search__block) {
  border-radius: unset;
}
</style>
src/pages/outbound/index0.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,350 @@
<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.weight || "-" }} kg</text>
            </view>
            <view class="outbound_item_row">
              <text class="outbound_item_label">厂家:</text>
              <text class="outbound_item_value">{{ item.clienteleName || "-" }}</text>
            </view>
            <view class="outbound_item_row">
              <text class="outbound_item_label">段长:</text>
              <text class="outbound_item_value">{{ item.actuallyLength || "-" }} M</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 getScanCode = async (code: any) => {
  try {
    // å¦‚æžœ 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;
    }
    // è§£æžæ‰«ç æ•°æ®ï¼ŒçŽ°åœ¨äºŒç»´ç åªåŒ…å«id
    let scanData;
    try {
      scanData = JSON.parse(scanCode);
    } catch (e) {
      toast.error("二维码格式错误");
      return;
    }
    const outPutId = scanData.id;
    if (!outPutId) {
      toast.error("二维码格式错误,缺少id信息");
      return;
    }
    // æ£€æŸ¥æ˜¯å¦å·²å­˜åœ¨ï¼ˆæ ¹æ®id判断)
    const exists = outboundList.value.some((item) => {
      const itemId = item.id;
      return itemId && itemId === outPutId && itemId !== "-";
    });
    if (exists) {
      toast.error("该条码已存在,请勿重复扫码");
      return;
    }
    // è°ƒç”¨æŽ¥å£èŽ·å–ç»žçº¿è¯¦ç»†ä¿¡æ¯
    const { data: tagData } = await OutboundApi.getTagByIdJx({
      outPutId: outPutId,
    });
    // æå–数据字段(根据接口返回的数据结构)
    const parsedData = {
      id: tagData?.id || outPutId,
      contractNo: tagData?.contractno || tagData?.contractNo || "-",
      batchNo: tagData?.systemno || tagData?.systemNo || tagData?.batchNo || "-",
      model: tagData?.model || "-",
      weight: tagData?.actuallyweight || tagData?.actuallyWeight || tagData?.weight || "-",
      clienteleName: tagData?.clientelename || tagData?.clienteleName || "-",
      actuallyLength: tagData?.actuallylength || tagData?.actuallyLength || "-",
      productionDate: tagData?.producttime || tagData?.productionDate || "-",
      projectId: tagData?.projectid || tagData?.projectId || projectId.value || "",
      // ä¿ç•™åŽŸå§‹æ•°æ®
      rawData: tagData,
      scanCode: scanCode,
    };
    // å¦‚果接口返回的数据中有 projectId,保存它
    if (parsedData.projectId && !projectId.value) {
      projectId.value = parsedData.projectId;
    }
    // æ·»åŠ åˆ°åˆ—è¡¨
    const newItem = {
      ...parsedData,
      scanTime: formatTime(new Date()),
    };
    outboundList.value.push(newItem);
    toast.success("扫码成功");
  } catch (error: any) {
    console.error("扫码处理失败:", error);
    toast.error(error.msg || "二维码异常,请更换二维码!");
  }
};
// è§¦å‘扫码
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: 5rpx;
  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>
src/pages/outbound/material.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,627 @@
<template>
  <view class="material-page">
    <view class="material-header">
      <CardTitle title="物料详情" :hideAction="false">
        <template #action>
          <wd-button
            type="icon"
            icon="scan"
            color="#0d867f"
            @click="openScan"
            style="color: #0d867f"
          ></wd-button>
        </template>
      </CardTitle>
      <!-- ç‰©æ–™ä¿¡æ¯ -->
      <view class="material-info">
        <wd-card
          v-for="(item, index) in materialList"
          :key="item.materialcode || index"
          custom-class="info-card"
        >
          <wd-row class="info-row">
            <wd-col :span="24">
              <view class="flex">
                <view class="icon_box">
                  <wd-icon name="folder" color="#0D867F"></wd-icon>
                </view>
                <text class="text-[#646874] mx-2">
                  ç‰©æ–™åç§°:
                  <text class="text-[#252525]">{{ item.materialname || "-" }}</text>
                </text>
              </view>
            </wd-col>
          </wd-row>
          <wd-row class="info-row">
            <wd-col :span="24">
              <view class="flex">
                <view class="icon_box">
                  <wd-icon name="folder" color="#0D867F"></wd-icon>
                </view>
                <text class="text-[#646874] mx-2">
                  ç‰©æ–™è§„æ ¼:
                  <text class="text-[#252525]">{{ item.materialspec || "-" }}</text>
                </text>
              </view>
            </wd-col>
          </wd-row>
          <wd-row class="info-row">
            <wd-col :span="24">
              <view class="flex">
                <view class="icon_box">
                  <wd-icon name="folder" color="#0D867F"></wd-icon>
                </view>
                <text class="text-[#646874] mx-2">
                  å·²å‘货数量:
                  <text class="text-[#252525]">{{ item.shippedQuantity || 0 }}</text>
                </text>
              </view>
            </wd-col>
          </wd-row>
        </wd-card>
      </view>
    </view>
    <!-- è´§ç‰©åˆ—表 -->
    <view class="list_content">
      <view v-if="goodsList.length === 0" class="empty_tip">
        <view class="empty_text">暂无货物数据</view>
      </view>
      <view v-for="(item, index) in goodsList" :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.monofilamentNumber || "-" }}</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.weight || "-" }} kg</text>
            </view>
            <view class="outbound_item_row">
              <text class="outbound_item_label">厂家:</text>
              <text class="outbound_item_value">{{ item.clienteleName || "-" }}</text>
            </view>
            <view class="outbound_item_row">
              <text class="outbound_item_label">段长:</text>
              <text class="outbound_item_value">{{ item.actuallyLength || "-" }} M</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="removeGoodsItem(index)"
          ></wd-button>
        </view>
      </view>
    </view>
    <!-- åº•部按钮 -->
    <view v-if="goodsList.length > 1" class="outbound_footer">
      <wd-button block @click="handleOutbound" style="background: #0d867f">
        <text class="text-[#fff]">出库</text>
      </wd-button>
    </view>
    <Scan ref="scanRef" emitName="scanMaterial" />
    <wd-toast />
  </view>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { onLoad } from "@dcloudio/uni-app";
// @ts-ignore
import CardTitle from "@/components/card-title/index.vue";
// @ts-ignore
import Scan from "@/components/scan/index.vue";
import { useToast, dayjs } from "wot-design-uni";
import OutboundApi from "@/api/product/outbound";
const toast = useToast();
const scanRef = ref();
const materialId = ref<string>("");
const vbillcode = ref<string>("");
// ç‰©æ–™ä¿¡æ¯åˆ—表
const materialList = ref<any[]>([]);
// è´§ç‰©åˆ—表
const goodsList = ref<any[]>([]);
// æ ¼å¼åŒ–æ—¶é—´
const formatTime = (date: Date) => {
  return dayjs(date).format("YYYY-MM-DD HH:mm:ss");
};
// èŽ·å–ç‰©æ–™è¯¦æƒ…
const getMaterialDetail = async () => {
  if (!vbillcode.value) {
    toast.error("发货单号不能为空");
    return;
  }
  try {
    uni.showLoading({
      title: "加载中...",
      mask: true,
    });
    const { code, data, msg } = await OutboundApi.queryErpOutboundOrderDetail({
      vbillcode: vbillcode.value,
    });
    uni.hideLoading();
    if (code === 200 && data) {
      // å°†æŽ¥å£è¿”回的数据映射到 materialList
      materialList.value = data.map((item: any) => ({
        materialcode: item.materialcode,
        materialname: item.materialname || "-",
        materialspec: item.materialspec || "-",
        shippedQuantity: item.nnum || 0,
        cdeliveryid: item.cdeliveryid,
        cdeliverybid: item.cdeliverybid,
        vsrccode: item.vsrccode,
      }));
    } else {
      toast.error(msg || "获取物料详情失败");
      materialList.value = [];
    }
  } catch (error: any) {
    uni.hideLoading();
    console.error("获取物料详情失败:", error);
    toast.error(error.msg || "获取物料详情失败");
    materialList.value = [];
  }
};
// ç›´æŽ¥æ‰«ç 
const openScan = () => {
  scanRef.value?.triggerScan();
};
// æ‰«ç å›žè°ƒ
const getScanCode = async (code: any) => {
  try {
    // å¦‚æžœ 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;
    }
    // è§£æžæ‰«ç æ•°æ®ï¼ŒçŽ°åœ¨äºŒç»´ç åªåŒ…å«id
    let scanData;
    try {
      scanData = JSON.parse(scanCode);
    } catch (e) {
      toast.error("二维码格式错误");
      return;
    }
    const outPutId = scanData.id;
    if (!outPutId) {
      toast.error("二维码格式错误,缺少id信息");
      return;
    }
    // æ£€æŸ¥æ˜¯å¦å·²å­˜åœ¨ï¼ˆæ ¹æ®id判断)
    const exists = goodsList.value.some((item) => {
      const itemId = item.id;
      return itemId && itemId === outPutId && itemId !== "-";
    });
    if (exists) {
      toast.error("该条码已存在,请勿重复扫码");
      return;
    }
    // è°ƒç”¨æŽ¥å£èŽ·å–ç»žçº¿/拉丝详细信息(含出库状态)
    const { data } = await OutboundApi.getTagByIdAll({
      outPutId: outPutId,
    });
    const list = data || [];
    if (!list.length) {
      toast.error("未查询到条码信息");
      return;
    }
    const tagData = list[0];
    // å·²å‡ºåº“校验
    if (tagData?.state === "已出库") {
      toast.error("该条码已出库,无法重复出库");
      return;
    }
    // æå–数据字段(根据接口返回的数据结构)
    const parsedData = {
      id: tagData?.id || outPutId,
      contractNo: tagData?.contractno || tagData?.contractNo || "-",
      monofilamentNumber:
        tagData?.monofilamentnumber ||
        tagData?.monofilamentNumber ||
        tagData?.systemno ||
        tagData?.systemNo ||
        "-",
      model: tagData?.model || "-",
      weight: tagData?.actuallylength || tagData?.actuallyLength || tagData?.weight || "-",
      clienteleName: tagData?.clientelename || tagData?.clienteleName || "-",
      actuallyLength: tagData?.actuallylength || tagData?.actuallyLength || "-",
      productionDate: tagData?.producttime || tagData?.productionDate || "-",
      type: tagData?.type || "",
      devicemodel: tagData?.devicemodel || "",
      state: tagData?.state || "",
      projectId: tagData?.projectid || tagData?.projectId || "",
      productuser: tagData?.productuser || "",
      // ä¿ç•™åŽŸå§‹æ•°æ®
      rawData: tagData,
      scanCode: scanCode,
    };
    // æ·»åŠ åˆ°åˆ—è¡¨
    const newItem = {
      ...parsedData,
      scanTime: formatTime(new Date()),
    };
    goodsList.value.push(newItem);
    toast.success("扫码成功");
  } catch (error: any) {
    console.error("扫码处理失败:", error);
    toast.error(error.msg || "二维码异常,请更换二维码!");
  }
};
// åˆ é™¤è´§ç‰©é¡¹
const removeGoodsItem = (index: number) => {
  const item = goodsList.value[index];
  const itemName = item.contractNo || item.monofilamentNumber || `第${index + 1}项`;
  uni.showModal({
    title: "确认删除",
    content: `确定要删除"${itemName}"吗?`,
    confirmText: "删除",
    cancelText: "取消",
    confirmColor: "#ff4444",
    success: (res) => {
      if (res.confirm) {
        goodsList.value.splice(index, 1);
        toast.success("删除成功");
      }
    },
  });
};
// ç»Ÿè®¡ç‰©æ–™æ•°é‡å’Œç±»åž‹è§„æ ¼
const getMaterialStatistics = () => {
  const statistics: Record<string, { type: string; model: string; count: number }> = {};
  goodsList.value.forEach((item) => {
    const type = item.type || "未知类型";
    const model = item.model || "未知规格";
    const key = `${type}_${model}`;
    if (!statistics[key]) {
      statistics[key] = {
        type,
        model,
        count: 0,
      };
    }
    statistics[key].count++;
  });
  return Object.values(statistics);
};
// å¤„理出库
const handleOutbound = async () => {
  if (goodsList.value.length === 0) {
    toast.error("暂无货物数据");
    return;
  }
  // ç»Ÿè®¡ç‰©æ–™æ•°é‡å’Œç±»åž‹è§„æ ¼
  const statistics = getMaterialStatistics();
  // æž„建统计信息文本
  let statisticsText = "出库物料统计:\n\n";
  statistics.forEach((item, index) => {
    statisticsText += `${index + 1}. ${item.type} - ${item.model}:${item.count}ä»¶\n`;
  });
  statisticsText += `\n总计:${goodsList.value.length}ä»¶\n\n确认出库吗?`;
  // æ˜¾ç¤ºç¡®è®¤å¼¹æ¡†
  uni.showModal({
    title: "确认出库",
    content: statisticsText,
    confirmText: "确认",
    cancelText: "取消",
    confirmColor: "#0d867f",
    success: async (res) => {
      if (res.confirm) {
        // æž„建请求数据
        const requestData = goodsList.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("出库成功");
            // æ¸…空列表
            goodsList.value = [];
            // è¿”回上一页或刷新物料详情
            setTimeout(() => {
              uni.navigateBack();
            }, 1500);
          } else {
            toast.error(msg || "出库失败");
          }
        } catch (error: any) {
          uni.hideLoading();
          console.error("出库失败:", error);
          toast.error(error.msg || "出库失败");
        }
      }
    },
  });
};
// è®¾ç½®æ‰«ç ç›‘听
const setupScanListener = () => {
  uni.$off("scanMaterial", getScanCode);
  uni.$on("scanMaterial", getScanCode);
};
onLoad((options: any) => {
  materialId.value = options.id || "";
  vbillcode.value = options.vbillcode || "";
  getMaterialDetail();
});
onMounted(() => {
  setupScanListener();
});
onUnmounted(() => {
  uni.$off("scanMaterial", getScanCode);
});
</script>
<style lang="scss" scoped>
.material-page {
  min-height: 100vh;
  background: #f3f9f8;
  padding-bottom: 200rpx;
  display: flex;
  flex-direction: column;
}
.material-header {
  position: sticky;
  top: 0;
  z-index: 10;
  background: #f3f9f8;
  padding-bottom: 10rpx;
}
.material-info {
  padding: 20rpx;
}
.info-card {
  box-shadow: 0px 0px 12px 0px rgba(0, 0, 0, 0.05);
  margin-bottom: 20rpx;
  margin-left: 6px;
  margin-right: 6px;
  padding: 10px 18px;
  background: #f3f9f8;
  &:last-child {
    margin-bottom: 0;
  }
}
.info-row {
  margin-bottom: 20rpx;
  &:last-child {
    margin-bottom: 0;
  }
}
.icon_box {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  background: #e7f4ec99;
  border-radius: 50%;
}
.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: 30rpx;
  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;
  }
}
.outbound_footer {
  position: fixed;
  bottom: 20rpx;
  left: 0;
  right: 0;
  padding: 0 30rpx;
  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
  z-index: 100;
  background: transparent;
}
.shipping-btn-wrapper {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  padding: 20rpx 30rpx;
  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
  background: #fff;
  z-index: 100;
  box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.shipping-dialog {
  background: #fff;
  border-radius: 24rpx 24rpx 0 0;
  padding-bottom: env(safe-area-inset-bottom);
}
.dialog-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 32rpx;
  border-bottom: 1px solid #e6e6e6;
}
.dialog-title {
  font-size: 32rpx;
  font-weight: 500;
  color: #333;
}
.close-icon {
  font-size: 40rpx;
  color: #999;
  padding: 8rpx;
}
.dialog-content {
  padding: 32rpx;
}
.scan-tip {
  text-align: center;
  color: #666;
  font-size: 28rpx;
  padding: 40rpx 0;
}
</style>