<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>
|