<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 scannedModel = tagData?.model || "";
|
const modelExists = materialList.value.some(
|
(material) => material.materialspec === scannedModel
|
);
|
if (!modelExists && scannedModel) {
|
toast.error(`规格型号"${scannedModel}"不在当前发货单的物料列表中`);
|
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>
|