| | |
| | | <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 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> |
| | | </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> |
| | | </wd-col> |
| | | </wd-row> |
| | | </view> |
| | | <view v-if="outboundList.length > 0" class="outbound_footer"> |
| | | <wd-button block @click="handleOutbound"> |
| | | <text class="text-[#fff]">出库</text> |
| | | </wd-button> |
| | | |
| | | <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> |
| | | <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> |
| | | <text class="text-[#646874] mx-2"> |
| | | 发货单日期: |
| | | <text class="text-[#252525]">{{ item.dbilldate || "-" }}</text> |
| | | </text> |
| | | </view> |
| | | </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> |
| | | <text class="text-[#646874] mx-2"> |
| | | 车牌号: |
| | | <text class="text-[#252525]">{{ item.carno || "-" }}</text> |
| | | </text> |
| | | </view> |
| | | </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> |
| | | <text class="text-[#646874] mx-2"> |
| | | 发货单备注: |
| | | <text class="text-[#252525]">{{ item.vmemo || "-" }}</text> |
| | | </text> |
| | | </view> |
| | | </wd-col> |
| | | </wd-row> |
| | | </wd-card> |
| | | </z-paging> |
| | | </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 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()), |
| | | // 获取列表数据 |
| | | const getList = async (pageNo: number, pageSize: number) => { |
| | | const params: any = { |
| | | contractNo: "", |
| | | carNo: searchKeyword.value || "", |
| | | current: pageNo, |
| | | size: pageSize, |
| | | }; |
| | | |
| | | 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("删除成功"); |
| | | try { |
| | | const { code, data, msg } = await OutboundApi.queryErpOutboundOrder(params); |
| | | if (code === 200 && data) { |
| | | const records = data.records || []; |
| | | if (!records.length) { |
| | | pagingRef.value.complete(true); |
| | | } else { |
| | | pagingRef.value.complete(records); |
| | | } |
| | | }, |
| | | } else { |
| | | toast.error(msg || "获取出库单列表失败"); |
| | | pagingRef.value.complete(true); |
| | | } |
| | | } catch (error) { |
| | | console.error("获取出库单列表失败:", error); |
| | | toast.error("获取出库单列表失败"); |
| | | pagingRef.value.complete(true); |
| | | } |
| | | }; |
| | | |
| | | // 搜索处理 |
| | | const handleSearch = () => { |
| | | // 重新加载数据 |
| | | pagingRef.value.reload(); |
| | | }; |
| | | |
| | | // 清空搜索 |
| | | const handleClear = () => { |
| | | searchKeyword.value = ""; |
| | | handleSearch(); |
| | | }; |
| | | |
| | | // 跳转到物料详情页 |
| | | const toMaterialDetail = (item: any) => { |
| | | uni.navigateTo({ |
| | | url: `/pages/outbound/material?id=${item.deliveryid}&vbillcode=${item.vbillcode}`, |
| | | }); |
| | | }; |
| | | |
| | | // 处理出库 |
| | | 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); |
| | | .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; |
| | | |
| | | .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; |
| | | } |
| | | padding: 40px 0; |
| | | } |
| | | |
| | | :deep(.delete-btn) { |
| | | .wd-button__content { |
| | | color: #ff4444 !important; |
| | | } |
| | | |
| | | &:active { |
| | | opacity: 0.7; |
| | | } |
| | | .empty_text { |
| | | color: #999; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | :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> |