<template>
|
<view class="scan-container">
|
<PageHeader title="扫码入库"
|
@back="goBack" />
|
<view class="module-selector"
|
v-if="!showForm">
|
<view class="module-card"
|
@click="startScan('qualified')">
|
<view class="module-icon qualified">
|
<u-icon name="checkbox-mark"
|
color="#fff"
|
size="40"></u-icon>
|
</view>
|
<view class="module-info">
|
<text class="module-label">合格入库</text>
|
<text class="module-desc">扫描合格产品进行入库</text>
|
</view>
|
</view>
|
<view class="module-card"
|
@click="startScan('unqualified')">
|
<view class="module-icon unqualified">
|
<u-icon name="close"
|
color="#fff"
|
size="40"></u-icon>
|
</view>
|
<view class="module-info">
|
<text class="module-label">不合格入库</text>
|
<text class="module-desc">记录不合格品的入库</text>
|
</view>
|
</view>
|
</view>
|
<scroll-view v-if="showForm"
|
scroll-y
|
class="detail-scroll">
|
<view class="detail-card"
|
v-for="(item, idx) in recordList"
|
:key="idx">
|
<view class="detail-card-title"
|
:class="{
|
'detail-card-title--collapsible': recordList.length > 1,
|
'is-collapsed': recordList.length > 1 && !isCardExpanded(idx),
|
}"
|
@click="recordList.length > 1 && toggleCardDetail(idx)">
|
<view class="detail-card-title-text">
|
<text class="detail-card-title-no">{{ cardTitleMain }}</text>
|
<text v-if="recordList.length > 1"
|
class="detail-card-title-seq">({{ idx + 1 }}/{{ recordList.length }})</text>
|
</view>
|
<u-icon v-if="recordList.length > 1"
|
:name="isCardExpanded(idx) ? 'arrow-up' : 'arrow-down'"
|
color="#999"
|
size="18"></u-icon>
|
</view>
|
<view v-show="recordList.length > 1 && !isCardExpanded(idx)"
|
class="detail-card-summary"
|
@click="toggleCardDetail(idx)">
|
<view class="kv-row kv-row--summary"
|
v-for="row in summaryFieldRows"
|
:key="'sum-' + row.key">
|
<text class="kv-label">{{ row.label }}</text>
|
<view class="kv-value kv-value--tag"
|
v-if="row.key === 'approveStatus'">
|
<u-tag :type="approveStatusTagType(item)"
|
size="small">{{ formatApproveStatus(item) }}</u-tag>
|
</view>
|
<view class="kv-value kv-value--tag"
|
v-else-if="row.key === 'productStockStatus'">
|
<u-tag :type="productStockStatusTagType(item.productStockStatus)"
|
size="small">{{ formatProductStockStatus(item.productStockStatus) }}</u-tag>
|
</view>
|
<text class="kv-value"
|
v-else>{{ formatCell(item, row, idx) }}</text>
|
</view>
|
<text class="summary-tip">点击查看全部</text>
|
</view>
|
<view v-show="isCardExpanded(idx)"
|
class="detail-card-body">
|
<view class="kv-row"
|
v-for="row in detailFieldRows"
|
:key="row.key">
|
<text class="kv-label">{{ row.label }}</text>
|
<view class="kv-value kv-value--tag"
|
v-if="row.key === 'approveStatus'">
|
<u-tag :type="approveStatusTagType(item)"
|
size="small">{{ formatApproveStatus(item) }}</u-tag>
|
</view>
|
<view class="kv-value kv-value--tag"
|
v-else-if="row.key === 'productStockStatus'">
|
<u-tag :type="productStockStatusTagType(item.productStockStatus)"
|
size="small">{{ formatProductStockStatus(item.productStockStatus) }}</u-tag>
|
</view>
|
<text class="kv-value"
|
v-else>{{ formatCell(item, row, idx) }}</text>
|
</view>
|
</view>
|
<view v-if="!isFullyStocked(item)"
|
class="stocked-qty-block">
|
<view class="kv-row stocked-qty-row">
|
<text class="kv-label">入库数量</text>
|
<view class="kv-value stocked-qty-input-wrap">
|
<up-input :key="'stocked-' + idx"
|
v-model="item.stockedQuantity"
|
type="number"
|
placeholder="请输入入库数量"
|
clearable
|
border="surround"
|
@blur="onStockedQtyBlur(item)" />
|
</view>
|
</view>
|
</view>
|
</view>
|
<view class="footer-btns">
|
<u-button class="footer-cancel-btn"
|
@click="cancelForm">返回</u-button>
|
<u-button class="footer-confirm-btn"
|
:loading="submitLoading"
|
@click="confirmInbound">确认入库</u-button>
|
</view>
|
</scroll-view>
|
</view>
|
</template>
|
|
<script setup>
|
import { ref, computed } from "vue";
|
import PageHeader from "@/components/PageHeader.vue";
|
import { productList as salesProductList } from "@/api/salesManagement/salesLedger";
|
import modal from "@/plugins/modal";
|
import { QUALITY_TYPE, CONTRACT_KIND } from "../scanOut/scanOut.constants";
|
import { useScanOutFieldRows } from "../scanOut/scanOut.fields";
|
import {
|
defaultStockedQuantityFromRow,
|
resolveQrContractKind,
|
resolveListTypeForDetail,
|
resolveContractNo,
|
buildSalesLedgerProductList,
|
hasAnyPositiveStockedQty,
|
resolveSubmitSceneKey,
|
} from "../scanOut/scanOut.logic";
|
import { createSubmitConfig } from "./scanIn.submit";
|
|
const showForm = ref(false);
|
const type = ref(QUALITY_TYPE.qualified);
|
const recordList = ref([]);
|
const expandedByIndex = ref({});
|
const scanContractNo = ref("");
|
const contractKind = ref(CONTRACT_KIND.sales);
|
const scanLedgerId = ref(null);
|
const submitLoading = ref(false);
|
const submitConfigByScene = createSubmitConfig(scanLedgerId);
|
|
const cardTitleMain = computed(() => {
|
const no = scanContractNo.value?.trim();
|
return no || "—";
|
});
|
|
const isCardExpanded = idx => {
|
if (recordList.value.length <= 1) return true;
|
return !!expandedByIndex.value[idx];
|
};
|
|
const toggleCardDetail = idx => {
|
if (recordList.value.length <= 1) return;
|
expandedByIndex.value = {
|
...expandedByIndex.value,
|
[idx]: !expandedByIndex.value[idx],
|
};
|
};
|
|
const { detailFieldRows, summaryFieldRows } = useScanOutFieldRows(contractKind);
|
|
const emptyDash = v => {
|
if (v === null || v === undefined || v === "") return "-";
|
return v;
|
};
|
|
const formatApproveStatus = row => {
|
const a = row.approveStatus;
|
const noShipInfo = !row.shippingDate || !row.shippingCarNumber;
|
const hasShipInfo = !!(row.shippingDate || row.shippingCarNumber);
|
if ((a === 1 || a === "1") && noShipInfo) return "充足";
|
if ((a === 0 || a === "0") && hasShipInfo) return "已出库";
|
return "不足";
|
};
|
|
const formatProductStockStatus = v => {
|
if (v == 1) return "部分入库";
|
if (v == 2) return "已入库";
|
if (v == 0) return "未出库";
|
return "不足";
|
};
|
|
const approveStatusTagType = row => {
|
const a = row.approveStatus;
|
const noShipInfo = !row.shippingDate || !row.shippingCarNumber;
|
const hasShipInfo = !!(row.shippingDate || row.shippingCarNumber);
|
if ((a === 1 || a === "1") && noShipInfo) return "success";
|
if ((a === 0 || a === "0") && hasShipInfo) return "success";
|
return "error";
|
};
|
|
const productStockStatusTagType = v => {
|
if (v == 1) return "warning";
|
if (v == 2) return "success";
|
if (v == 0) return "info";
|
return "error";
|
};
|
|
const formatHeavyBox = v => {
|
if (v === 1 || v === true || v === "1") return "是";
|
if (v === 0 || v === false || v === "0") return "否";
|
return emptyDash(v);
|
};
|
|
const isFullyStocked = item => {
|
const s = item?.productStockStatus;
|
return s == 2 || s === "2";
|
};
|
|
const onStockedQtyBlur = item => {
|
if (isFullyStocked(item)) return;
|
const raw = item.stockedQuantity;
|
if (raw === null || raw === undefined || String(raw).trim() === "") {
|
item.stockedQuantity = "0";
|
return;
|
}
|
const n = Number(String(raw).trim());
|
if (Number.isNaN(n)) {
|
item.stockedQuantity = defaultStockedQuantityFromRow(item);
|
return;
|
}
|
item.stockedQuantity = String(Math.max(0, n));
|
};
|
|
const formatCell = (item, row, idx) => {
|
if (row.key === "index") {
|
const v = item.index;
|
if (v !== null && v !== undefined && v !== "") return String(v);
|
return String(idx + 1);
|
}
|
if (row.key === "approveStatus") return formatApproveStatus(item);
|
if (row.key === "productStockStatus")
|
return formatProductStockStatus(item.productStockStatus);
|
if (row.key === "heavyBox") return formatHeavyBox(item.heavyBox);
|
if (row.key === "remainingQuantity") {
|
const v =
|
item.remainingQuantity ??
|
item.remaining_quantity ??
|
item.remainQuantity ??
|
item.remain_quantity;
|
return emptyDash(v);
|
}
|
if (row.key === "availableQuality") {
|
const v = item.availableQuality ?? item.availableQuantity;
|
return emptyDash(v);
|
}
|
if (row.key === "returnQuality") {
|
const v = item.returnQuality ?? item.returnQuantity;
|
return emptyDash(v);
|
}
|
return emptyDash(item[row.key]);
|
};
|
|
const resetDetailView = () => {
|
showForm.value = false;
|
scanContractNo.value = "";
|
contractKind.value = CONTRACT_KIND.sales;
|
scanLedgerId.value = null;
|
expandedByIndex.value = {};
|
recordList.value = [];
|
};
|
|
const confirmInbound = async () => {
|
if (scanLedgerId.value == null || scanLedgerId.value === "") {
|
modal.msgError("缺少订单信息,请重新扫码");
|
return;
|
}
|
const salesLedgerProductList = buildSalesLedgerProductList(recordList.value);
|
if (!hasAnyPositiveStockedQty(salesLedgerProductList)) {
|
modal.msgError("请至少填写一行大于 0 的入库数量");
|
return;
|
}
|
const sceneKey = resolveSubmitSceneKey(contractKind.value, type.value);
|
const currentSubmitConfig = submitConfigByScene[sceneKey];
|
if (!currentSubmitConfig) {
|
modal.msgError("暂不支持当前入库场景");
|
return;
|
}
|
const runApi = currentSubmitConfig.runApi;
|
const payload = currentSubmitConfig.payloadBuilder(salesLedgerProductList);
|
try {
|
submitLoading.value = true;
|
modal.loading("提交中...");
|
const res = await runApi(payload);
|
modal.closeLoading();
|
if (res.code === 200) {
|
modal.msgSuccess("入库成功");
|
resetDetailView();
|
} else {
|
modal.msgError(res.msg || "提交失败");
|
}
|
} catch (e) {
|
modal.closeLoading();
|
console.error("扫码入库提交失败", e);
|
} finally {
|
submitLoading.value = false;
|
}
|
};
|
|
const goBack = () => {
|
if (showForm.value) {
|
resetDetailView();
|
} else {
|
uni.navigateBack();
|
}
|
};
|
|
const cancelForm = () => {
|
resetDetailView();
|
};
|
|
const startScan = scanType => {
|
type.value = scanType;
|
uni.scanCode({
|
success: res => {
|
handleScanResult(res.result);
|
},
|
fail: () => {
|
modal.msgError("扫码失败");
|
},
|
});
|
};
|
|
const handleScanResult = async result => {
|
try {
|
const scanData = JSON.parse(result);
|
if (!scanData.id) {
|
modal.msgError("无效的二维码数据");
|
return;
|
}
|
const kind = resolveQrContractKind(scanData);
|
contractKind.value = kind;
|
scanLedgerId.value = scanData.id;
|
scanContractNo.value = resolveContractNo(scanData, kind);
|
const listType = resolveListTypeForDetail(kind);
|
modal.loading("获取产品库存详情...");
|
const res = await salesProductList({
|
salesLedgerId: scanData.id,
|
type: listType,
|
});
|
modal.closeLoading();
|
if (res.code === 200 && res.data && res.data.length > 0) {
|
recordList.value = res.data.map(row => ({
|
...row,
|
stockedQuantity: defaultStockedQuantityFromRow(row),
|
}));
|
expandedByIndex.value = {};
|
showForm.value = true;
|
} else {
|
scanLedgerId.value = null;
|
modal.msgError("未查询到明细数据");
|
}
|
} catch (error) {
|
modal.closeLoading();
|
scanLedgerId.value = null;
|
console.error("处理扫码结果失败", error);
|
modal.msgError("扫码处理失败,请重试");
|
}
|
};
|
</script>
|
|
<style scoped lang="scss">
|
.scan-container {
|
min-height: 100vh;
|
background-color: #f5f7fa;
|
}
|
|
.module-selector {
|
display: flex;
|
flex-direction: column;
|
padding: 40rpx;
|
height: 80vh;
|
justify-content: center;
|
}
|
|
.module-card {
|
display: flex;
|
align-items: center;
|
background-color: #fff;
|
padding: 80rpx 50rpx;
|
border-radius: 32rpx;
|
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.05);
|
margin-bottom: 50rpx;
|
transition: all 0.3s ease;
|
border: 2rpx solid transparent;
|
|
&:active {
|
transform: scale(0.98);
|
background-color: #f9f9f9;
|
}
|
}
|
|
.module-icon {
|
width: 140rpx;
|
height: 140rpx;
|
border-radius: 32rpx;
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
margin-right: 40rpx;
|
|
&.qualified {
|
background: linear-gradient(135deg, #52c41a, #73d13d);
|
box-shadow: 0 10rpx 20rpx rgba(82, 196, 26, 0.2);
|
}
|
|
&.unqualified {
|
background: linear-gradient(135deg, #ff4d4f, #ff7875);
|
box-shadow: 0 10rpx 20rpx rgba(255, 77, 79, 0.2);
|
}
|
}
|
|
.module-info {
|
display: flex;
|
flex-direction: column;
|
}
|
|
.module-label {
|
font-size: 40rpx;
|
font-weight: 700;
|
color: #1a1a1a;
|
margin-bottom: 12rpx;
|
}
|
|
.module-desc {
|
font-size: 28rpx;
|
color: #999;
|
}
|
|
.detail-scroll {
|
max-height: calc(100vh - 120rpx);
|
box-sizing: border-box;
|
}
|
|
.detail-card {
|
background-color: #fff;
|
margin: 20rpx;
|
padding: 28rpx;
|
border-radius: 16rpx;
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
}
|
|
.detail-card-title {
|
font-size: 30rpx;
|
font-weight: 600;
|
color: #333;
|
margin-bottom: 20rpx;
|
padding-bottom: 16rpx;
|
border-bottom: 1rpx solid #eee;
|
}
|
|
.detail-card-title--collapsible {
|
display: flex;
|
flex-direction: row;
|
justify-content: space-between;
|
align-items: center;
|
}
|
|
.detail-card-title--collapsible.is-collapsed {
|
margin-bottom: 0;
|
padding-bottom: 0;
|
border-bottom: none;
|
}
|
|
.detail-card-title-text {
|
flex: 1;
|
min-width: 0;
|
margin-right: 16rpx;
|
display: flex;
|
flex-wrap: wrap;
|
align-items: baseline;
|
}
|
|
.detail-card-title-no {
|
word-break: break-all;
|
line-height: 1.4;
|
}
|
|
.detail-card-title-seq {
|
flex-shrink: 0;
|
font-size: 26rpx;
|
font-weight: 500;
|
color: #888;
|
margin-left: 8rpx;
|
}
|
|
.detail-card-body {
|
padding-top: 4rpx;
|
}
|
|
.detail-card-summary {
|
padding-top: 8rpx;
|
}
|
|
.kv-row--summary {
|
padding: 12rpx 0;
|
font-size: 26rpx;
|
}
|
|
.summary-tip {
|
display: block;
|
font-size: 24rpx;
|
color: #999;
|
text-align: center;
|
padding: 20rpx 0 8rpx;
|
}
|
|
.kv-row {
|
display: flex;
|
align-items: flex-start;
|
padding: 16rpx 0;
|
border-bottom: 1rpx solid #f0f0f0;
|
font-size: 28rpx;
|
}
|
|
.kv-label {
|
flex-shrink: 0;
|
width: 220rpx;
|
color: #888;
|
line-height: 1.5;
|
}
|
|
.kv-value {
|
flex: 1;
|
color: #1a1a1a;
|
line-height: 1.5;
|
word-break: break-all;
|
text-align: right;
|
}
|
|
.kv-value--tag {
|
display: flex;
|
justify-content: flex-end;
|
align-items: center;
|
}
|
|
.stocked-qty-block {
|
margin-top: 8rpx;
|
padding-top: 8rpx;
|
border-top: 1rpx solid #f0f0f0;
|
}
|
|
.stocked-qty-row {
|
border-bottom: none;
|
align-items: center;
|
}
|
|
.stocked-qty-input-wrap {
|
min-width: 0;
|
}
|
|
.footer-btns {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
gap: 24rpx;
|
padding: 20rpx 40rpx 60rpx;
|
}
|
|
.footer-cancel-btn {
|
flex: 1;
|
background-color: #f5f5f5;
|
color: #666;
|
border: none;
|
}
|
|
.footer-confirm-btn {
|
flex: 1;
|
background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
|
color: #fff;
|
border: none;
|
}
|
</style>
|