<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="!isFullyOutbound(item)"
|
class="stocked-qty-block">
|
|
<view class="kv-row stocked-qty-row">
|
|
<text class="kv-label">{{ needScanShipFlow ? "本次发货数量" : "出库数量" }}</text>
|
|
<view class="kv-value stocked-qty-input-wrap">
|
|
<text v-if="needScanShipFlow"
|
|
class="scan-ship-qty-readonly">{{ resolveScanShipLineQuantity(item) }}</text>
|
|
<up-input v-else
|
|
:key="'stocked-' + idx"
|
|
v-model="item.operateQuantity"
|
|
type="number"
|
|
placeholder="请输入出库数量"
|
:clearable="!isSalesQualifiedOutboundQtyLocked"
|
|
:disabled="isSalesQualifiedOutboundQtyLocked"
|
|
|
border="surround"
|
|
@blur="onStockedQtyBlur(item)" />
|
|
</view>
|
|
</view>
|
|
<text v-if="needScanShipFlow"
|
|
class="scan-ship-qty-hint">整单一次性发货,数量以订单为准,不可修改</text>
|
|
</view>
|
|
</view>
|
|
<view v-if="needScanShipFlow"
|
|
class="scan-ship-card">
|
|
<view class="scan-ship-title">发货信息</view>
|
|
<u-form label-width="160rpx">
|
|
<u-form-item label="发货方式"
|
|
required>
|
|
<u-input v-model="scanShipTypeLabel"
|
|
readonly
|
|
placeholder="请选择"
|
|
@click="scanShipTypeSheetShow = true" />
|
|
</u-form-item>
|
|
<u-form-item v-if="scanShipTypeValue === '货车'"
|
|
label="车牌号"
|
|
required>
|
|
<u-input v-model="scanShipCarNumber"
|
|
placeholder="请输入车牌号"
|
|
clearable />
|
|
</u-form-item>
|
|
<u-form-item v-if="scanShipTypeValue === '快递'"
|
|
label="快递单号"
|
|
required>
|
|
<u-input v-model="scanShipExpress"
|
|
placeholder="请输入快递单号"
|
|
clearable />
|
|
</u-form-item>
|
|
</u-form>
|
|
<view class="scan-ship-approval">
|
|
<text class="scan-ship-subtitle">审批人</text>
|
|
<view v-if="scanShipApprover.nickName"
|
|
class="scan-ship-approver-pill">
|
|
<text>{{ scanShipApprover.nickName }}</text>
|
|
<text class="scan-ship-remove"
|
|
@click="clearScanShipApprover">×</text>
|
|
</view>
|
|
<u-button v-else
|
|
size="small"
|
|
type="primary"
|
|
plain
|
|
@click="openScanShipContactSelect">选择审批人</u-button>
|
|
</view>
|
|
<view class="scan-ship-files">
|
|
<text class="scan-ship-subtitle">发货附件(选填,最多10张)</text>
|
|
<u-button size="small"
|
|
type="primary"
|
|
:loading="scanShipUploading"
|
|
:disabled="scanShipFiles.length >= 10"
|
|
@click="chooseScanShipImage">添加图片</u-button>
|
|
<view v-if="scanShipFiles.length"
|
|
class="scan-ship-file-grid">
|
|
<view v-for="(f, fi) in scanShipFiles"
|
|
:key="fi"
|
|
class="scan-ship-thumb-wrap">
|
|
<image :src="scanShipFileUrl(f)"
|
|
class="scan-ship-thumb"
|
|
mode="aspectFill"
|
|
@click="previewScanShipImage(fi)" />
|
|
<text class="scan-ship-thumb-del"
|
|
@click.stop="removeScanShipFile(fi)">×</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<up-action-sheet :show="scanShipTypeSheetShow"
|
|
:actions="scanShipTypeActions"
|
|
title="发货方式"
|
|
@select="onScanShipTypeSelect"
|
|
@close="scanShipTypeSheetShow = false" />
|
|
<view class="footer-btns">
|
|
<u-button class="footer-cancel-btn"
|
|
@click="cancelForm">返回</u-button>
|
|
<u-button v-if="needScanShipFlow"
|
|
class="footer-confirm-btn"
|
|
:loading="submitLoading"
|
|
@click="confirmScanShipApply">提交发货审批</u-button>
|
|
<u-button v-else
|
|
class="footer-confirm-btn"
|
|
:loading="submitLoading"
|
|
@click="confirmOutbound">确认</u-button>
|
|
</view>
|
|
</scroll-view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted, onUnmounted } from "vue";
|
|
import PageHeader from "@/components/PageHeader.vue";
|
|
import { productList as salesProductList, scanShipApply } from "@/api/salesManagement/salesLedger";
|
|
import config from "@/config";
|
import { getToken } from "@/utils/auth";
|
import { getLedgerShippingLabel } from "@/utils/salesLedgerShip";
|
|
import modal from "@/plugins/modal";
|
|
import { QUALITY_TYPE, CONTRACT_KIND } from "./scanOut.constants";
|
import { useScanOutFieldRows } from "./scanOut.fields";
|
import {
|
parseOptionalNumber,
|
defaultStockedQuantityFromRow,
|
resolveQrContractKind,
|
resolveListTypeForDetail,
|
resolveContractNo,
|
buildSalesLedgerProductList,
|
buildScanShipProductList,
|
resolveScanShipLineQuantity,
|
hasAnyPositiveStockedQty,
|
resolveSubmitSceneKey,
|
} from "./scanOut.logic";
|
import { createSubmitConfig } from "./scanOut.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);
|
|
/** 二维码中的台账主键 id */
|
const scanLedgerId = ref(null);
|
|
const submitLoading = ref(false);
|
|
/** 销售 + 合格:走「提交发货审批」,不再直接出库或跳转原发货页 */
|
const needScanShipFlow = computed(
|
() =>
|
showForm.value &&
|
type.value === QUALITY_TYPE.qualified &&
|
contractKind.value === CONTRACT_KIND.sales
|
);
|
|
const scanShipTypeValue = ref("货车");
|
const scanShipTypeLabel = computed(() => scanShipTypeValue.value);
|
const scanShipTypeSheetShow = ref(false);
|
const scanShipTypeActions = ref([
|
{ name: "货车", value: "货车" },
|
{ name: "快递", value: "快递" },
|
]);
|
const scanShipCarNumber = ref("");
|
const scanShipExpress = ref("");
|
const scanShipApprover = ref({ userId: null, nickName: null });
|
const scanShipFiles = ref([]);
|
const scanShipUploading = ref(false);
|
|
const scanShipUploadConfig = {
|
action: "/file/upload",
|
limit: 10,
|
fileType: ["jpg", "jpeg", "png", "gif", "webp"],
|
formType: 10,
|
};
|
|
const uploadScanShipFileUrl = computed(
|
() => (config.baseUrl || "").replace(/\/$/, "") + scanShipUploadConfig.action
|
);
|
|
const onScanShipTypeSelect = item => {
|
scanShipTypeValue.value = item.name || item.value || "货车";
|
scanShipTypeSheetShow.value = false;
|
if (scanShipTypeValue.value === "货车") scanShipExpress.value = "";
|
else scanShipCarNumber.value = "";
|
};
|
|
const openScanShipContactSelect = () => {
|
uni.setStorageSync("stepIndex", 0);
|
uni.navigateTo({
|
url: "/pages/cooperativeOffice/collaborativeApproval/contactSelect?approveType=7&source=scanShip",
|
});
|
};
|
|
const clearScanShipApprover = () => {
|
scanShipApprover.value = { userId: null, nickName: null };
|
};
|
|
const handleScanShipSelectContact = data => {
|
if (data?.source !== "scanShip") return;
|
if (!needScanShipFlow.value) return;
|
const c = data?.contact;
|
if (!c) return;
|
scanShipApprover.value = { userId: c.userId, nickName: c.nickName };
|
};
|
|
const scanShipFileUrl = file => {
|
const base = config.fileUrl || "";
|
const link = file?.link || file?.url || "";
|
if (link && String(link).startsWith("http")) return link;
|
if (link && link.startsWith("/")) return base + link;
|
return file?.tempFilePath || file?.path || "";
|
};
|
|
const chooseScanShipImage = () => {
|
if (scanShipFiles.value.length >= scanShipUploadConfig.limit) {
|
modal.msgError(`最多${scanShipUploadConfig.limit}张`);
|
return;
|
}
|
uni.chooseImage({
|
count: 1,
|
sizeType: ["compressed"],
|
sourceType: ["album", "camera"],
|
success: res => {
|
const path = res?.tempFilePaths?.[0];
|
const tf = res?.tempFiles?.[0] || {};
|
if (!path) return;
|
const token = getToken();
|
if (!token) {
|
modal.msgError("未登录");
|
return;
|
}
|
scanShipUploading.value = true;
|
uni.uploadFile({
|
url: uploadScanShipFileUrl.value,
|
filePath: path,
|
name: "file",
|
formData: { type: scanShipUploadConfig.formType },
|
header: { Authorization: `Bearer ${token}` },
|
success: up => {
|
try {
|
const body = JSON.parse(up.data || "{}");
|
if (body.code === 200 && body.data) {
|
const d = body.data;
|
scanShipFiles.value.push({
|
tempId: d.tempId ?? d.tempFileId ?? d.id,
|
link: d.link || d.url,
|
url: d.url,
|
name: d.originalFilename || d.originalName || "图片",
|
tempFilePath: path,
|
});
|
modal.msgSuccess("上传成功");
|
} else {
|
modal.msgError(body.msg || "上传失败");
|
}
|
} catch (e) {
|
modal.msgError("上传解析失败");
|
}
|
},
|
fail: () => modal.msgError("上传失败"),
|
complete: () => {
|
scanShipUploading.value = false;
|
},
|
});
|
},
|
});
|
};
|
|
const removeScanShipFile = idx => {
|
scanShipFiles.value.splice(idx, 1);
|
};
|
|
const previewScanShipImage = idx => {
|
const urls = scanShipFiles.value.map(f => scanShipFileUrl(f)).filter(Boolean);
|
const cur = urls[idx];
|
if (urls.length && cur) uni.previewImage({ urls, current: cur });
|
};
|
|
const getScanShipTempFileIds = () =>
|
scanShipFiles.value.map(f => f.tempId).filter(id => id != null && id !== "");
|
|
const confirmScanShipApply = async () => {
|
if (scanLedgerId.value == null || scanLedgerId.value === "") {
|
modal.msgError("缺少订单信息,请重新扫码");
|
return;
|
}
|
if (!hasEditableOutboundItems.value) {
|
modal.msgError("该产品已经全部出库");
|
return;
|
}
|
const salesLedgerProductList = buildScanShipProductList(recordList.value);
|
if (!hasAnyPositiveStockedQty(salesLedgerProductList)) {
|
modal.msgError("当前订单无可发货数量");
|
return;
|
}
|
if (scanShipTypeValue.value === "货车" && !String(scanShipCarNumber.value || "").trim()) {
|
modal.msgError("请输入车牌号");
|
return;
|
}
|
if (scanShipTypeValue.value === "快递" && !String(scanShipExpress.value || "").trim()) {
|
modal.msgError("请输入快递单号");
|
return;
|
}
|
if (!scanShipApprover.value?.userId) {
|
modal.msgError("请选择审批人");
|
return;
|
}
|
if (scanShipUploading.value) {
|
modal.msgError("附件上传中,请稍候");
|
return;
|
}
|
const payload = {
|
salesLedgerId: scanLedgerId.value,
|
salesLedgerProductList,
|
approveUserIds: String(scanShipApprover.value.userId),
|
shipType: scanShipTypeValue.value,
|
shippingCarNumber:
|
scanShipTypeValue.value === "货车" ? String(scanShipCarNumber.value || "").trim() : "",
|
expressNumber:
|
scanShipTypeValue.value === "快递" ? String(scanShipExpress.value || "").trim() : "",
|
tempFileIds: getScanShipTempFileIds(),
|
};
|
try {
|
submitLoading.value = true;
|
modal.loading("提交中...");
|
const res = await scanShipApply(payload);
|
modal.closeLoading();
|
if (res.code === 200) {
|
modal.msgSuccess(res.msg || "发货审批已发起");
|
resetDetailView();
|
} else {
|
modal.msgError(res.msg || "提交失败");
|
}
|
} catch (e) {
|
modal.closeLoading();
|
console.error(e);
|
} finally {
|
submitLoading.value = false;
|
}
|
};
|
|
onMounted(() => {
|
uni.$on("selectContact", handleScanShipSelectContact);
|
});
|
onUnmounted(() => {
|
uni.$off("selectContact", handleScanShipSelectContact);
|
});
|
const isSalesQualifiedOutboundQtyLocked = computed(
|
() =>
|
type.value === QUALITY_TYPE.qualified &&
|
contractKind.value === CONTRACT_KIND.sales
|
);
|
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: rawDetailFieldRows, summaryFieldRows: rawSummaryFieldRows } = useScanOutFieldRows(
|
contractKind,
|
"outbound"
|
);
|
const shouldShowOutboundQuantityField = key => {
|
if (type.value === QUALITY_TYPE.qualified)
|
return key !== "unqualifiedShippedQuantity" && key !== "unqualifiedStockedQuantity";
|
if (type.value === QUALITY_TYPE.unqualified) return key !== "shippedQuantity" && key !== "remainingShippedQuantity";
|
return true;
|
};
|
const detailFieldRows = computed(() =>
|
rawDetailFieldRows.value.filter(row => shouldShowOutboundQuantityField(row.key))
|
);
|
const summaryFieldRows = computed(() =>
|
rawSummaryFieldRows.value.filter(row => shouldShowOutboundQuantityField(row.key))
|
);
|
|
|
|
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 parseOptionalNumberLocal = raw => {
|
|
if (raw === null || raw === undefined || raw === "") return null;
|
|
const n = Number(String(raw).trim());
|
|
return Number.isNaN(n) ? null : n;
|
|
};
|
|
|
|
const parseRemainingQuantityLocal = row => {
|
|
const remRaw =
|
|
row?.remainingQuantity ??
|
|
row?.remaining_quantity ??
|
|
row?.remainQuantity ??
|
|
row?.remain_quantity;
|
|
return parseOptionalNumberLocal(remRaw);
|
|
};
|
|
|
|
const defaultStockedQuantityFromRowLocal = row => {
|
|
const rem = parseRemainingQuantityLocal(row);
|
|
if (rem !== null) return String(Math.max(0, rem));
|
|
const avail = parseOptionalNumberLocal(
|
|
row?.availableQuality ?? row?.availableQuantity
|
|
);
|
|
if (avail !== null) return String(Math.max(0, avail));
|
|
const qty = parseOptionalNumberLocal(row?.quantity);
|
|
if (qty !== null) return String(Math.max(0, qty));
|
|
return "0";
|
|
};
|
|
|
|
const onStockedQtyBlur = item => {
|
|
const raw = item.operateQuantity;
|
|
if (raw === null || raw === undefined || String(raw).trim() === "") {
|
|
item.operateQuantity = "0";
|
|
return;
|
|
}
|
|
const n = Number(String(raw).trim());
|
|
if (Number.isNaN(n)) {
|
if (type.value === QUALITY_TYPE.unqualified) {
|
const unqualifiedInbound = parseOptionalNumber(item.unqualifiedStockedQuantity) ?? 0;
|
const unqualifiedOutbound = parseOptionalNumber(item.unqualifiedShippedQuantity) ?? 0;
|
item.operateQuantity = String(Math.max(0, unqualifiedInbound - unqualifiedOutbound));
|
} else {
|
item.operateQuantity = defaultStockedQuantityFromRow(item, "outbound");
|
}
|
|
return;
|
|
}
|
|
item.operateQuantity = String(Math.max(0, n));
|
|
};
|
|
const parseUnqualifiedInboundQty = item => {
|
return (
|
parseOptionalNumber(
|
item?.unqualifiedStockedQuantity ??
|
item?.unQualifiedStockedQuantity ??
|
item?.unqualifiedStockedQty ??
|
item?.unqualifiedInboundQuantity
|
) ?? 0
|
);
|
};
|
|
const parseUnqualifiedOutboundQty = item => {
|
return (
|
parseOptionalNumber(
|
item?.unqualifiedShippedQuantity ??
|
item?.unQualifiedShippedQuantity ??
|
item?.unqualifiedShippedQty ??
|
item?.unqualifiedOutboundQuantity
|
) ?? 0
|
);
|
};
|
|
const isFullyOutbound = item => {
|
if (type.value === QUALITY_TYPE.unqualified) {
|
return parseUnqualifiedInboundQty(item) - parseUnqualifiedOutboundQty(item) <= 0;
|
}
|
const remaining = parseOptionalNumber(item?.remainingShippedQuantity);
|
if (remaining !== null) return remaining <= 0;
|
const fallback = parseOptionalNumber(defaultStockedQuantityFromRow(item, "outbound"));
|
return fallback !== null ? fallback <= 0 : false;
|
};
|
|
const hasEditableOutboundItems = computed(() => {
|
if (!recordList.value?.length) return false;
|
return recordList.value.some(item => !isFullyOutbound(item));
|
});
|
|
|
|
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 === "ledgerShippingStatus") return getLedgerShippingLabel(item);
|
|
if (row.key === "heavyBox") return formatHeavyBox(item.heavyBox);
|
|
if (row.key === "remainingQuantity") {
|
|
const v = item.remainingQuantity;
|
|
return emptyDash(v);
|
|
}
|
if (row.key === "remainingShippedQuantity") {
|
const v = item.remainingShippedQuantity;
|
return emptyDash(v);
|
}
|
if (row.key === "shippedQuantity") {
|
const v = item.shippedQuantity;
|
return emptyDash(v);
|
}
|
if (row.key === "unqualifiedShippedQuantity") {
|
const v =
|
item.unqualifiedShippedQuantity ??
|
item.unQualifiedShippedQuantity ??
|
item.unqualifiedShippedQty ??
|
item.unqualifiedOutboundQuantity;
|
return emptyDash(v);
|
}
|
if (row.key === "stockedQuantity") {
|
const v = item.stockedQuantity;
|
return emptyDash(v);
|
}
|
if (row.key === "unqualifiedStockedQuantity") {
|
const v = item.unqualifiedStockedQuantity;
|
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 = [];
|
|
scanShipTypeValue.value = "货车";
|
scanShipCarNumber.value = "";
|
scanShipExpress.value = "";
|
scanShipApprover.value = { userId: null, nickName: null };
|
scanShipFiles.value = [];
|
scanShipUploading.value = false;
|
scanShipTypeSheetShow.value = false;
|
|
};
|
|
|
|
/** 组装提交用的产品行(含数值化出库数量 stockedQuantity) */
|
const buildSalesLedgerProductListLocal = () => {
|
|
return recordList.value.map(item => {
|
|
const n = parseOptionalNumber(item.stockedQuantity);
|
|
const qty = n !== null && !Number.isNaN(n) ? Math.max(0, n) : 0;
|
|
const { stockedQuantity: _sq, ...rest } = item;
|
|
return { ...rest, stockedQuantity: qty };
|
|
});
|
|
};
|
|
|
|
const confirmOutbound = async () => {
|
|
if (scanLedgerId.value == null || scanLedgerId.value === "") {
|
|
modal.msgError("缺少订单信息,请重新扫码");
|
|
return;
|
|
}
|
if (!hasEditableOutboundItems.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("扫码失败");
|
|
},
|
|
});
|
|
};
|
|
|
|
/** 根据二维码 JSON 判断销售(XS)/采购(CG),与接口 type:1 销售、2 采购 对应 */
|
const resolveQrContractKindLocal = scanData => {
|
|
const t = scanData?.type;
|
|
const ts =
|
|
t !== null && t !== undefined && t !== ""
|
|
? String(t).trim().toUpperCase()
|
|
: "";
|
|
if (ts === "CG" || t === 2 || t === "2") return CONTRACT_KIND.purchase;
|
|
if (ts === "XS" || t === 1 || t === "1") return CONTRACT_KIND.sales;
|
|
const pc = scanData?.purchaseContractNumber;
|
|
const sc = scanData?.salesContractNo;
|
|
if (
|
|
pc != null &&
|
|
String(pc).trim() !== "" &&
|
|
(sc == null || String(sc).trim() === "")
|
|
)
|
|
return CONTRACT_KIND.purchase;
|
|
return CONTRACT_KIND.sales;
|
|
};
|
|
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,
|
unqualifiedShippedQuantity:
|
row.unqualifiedShippedQuantity ??
|
row.unQualifiedShippedQuantity ??
|
row.unqualifiedShippedQty ??
|
row.unqualifiedOutboundQuantity,
|
unqualifiedStockedQuantity:
|
row.unqualifiedStockedQuantity ??
|
row.unQualifiedStockedQuantity ??
|
row.unqualifiedStockedQty ??
|
row.unqualifiedInboundQuantity,
|
operateQuantity:
|
type.value === QUALITY_TYPE.unqualified
|
? String(
|
Math.max(
|
0,
|
(parseOptionalNumber(
|
row.unqualifiedStockedQuantity ??
|
row.unQualifiedStockedQuantity ??
|
row.unqualifiedStockedQty ??
|
row.unqualifiedInboundQuantity
|
) ?? 0) -
|
(parseOptionalNumber(
|
row.unqualifiedShippedQuantity ??
|
row.unQualifiedShippedQuantity ??
|
row.unqualifiedShippedQty ??
|
row.unqualifiedOutboundQuantity
|
) ?? 0)
|
)
|
)
|
: defaultStockedQuantityFromRow(row, "outbound"),
|
|
}));
|
|
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;
|
|
}
|
|
.scan-ship-qty-readonly {
|
|
font-size: 30rpx;
|
|
font-weight: 600;
|
|
color: #1a1a1a;
|
|
}
|
|
.scan-ship-qty-hint {
|
|
display: block;
|
|
font-size: 24rpx;
|
|
color: #999;
|
|
padding: 8rpx 0 0;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.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>
|