From 8aae660d1dd2455d300d7738509f12b33d3865e0 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期六, 18 四月 2026 15:24:48 +0800
Subject: [PATCH] 新增销售和采购订单扫码入库功能的前端页面支持,优化API接口以处理合格和不合格入库情况
---
src/pages/inventoryManagement/scanIn/index.vue | 559 +++++++++++++++++++++++++++++++++++++++++--------------
1 files changed, 417 insertions(+), 142 deletions(-)
diff --git a/src/pages/inventoryManagement/scanIn/index.vue b/src/pages/inventoryManagement/scanIn/index.vue
index 3e17f31..8479acd 100644
--- a/src/pages/inventoryManagement/scanIn/index.vue
+++ b/src/pages/inventoryManagement/scanIn/index.vue
@@ -13,7 +13,7 @@
</view>
<view class="module-info">
<text class="module-label">鍚堟牸鍏ュ簱</text>
- <text class="module-desc">鎵弿鍚堟牸浜у搧淇℃伅</text>
+ <text class="module-desc">鎵弿鍚堟牸浜у搧杩涜鍏ュ簱</text>
</view>
</view>
<view class="module-card"
@@ -25,124 +25,297 @@
</view>
<view class="module-info">
<text class="module-label">涓嶅悎鏍煎叆搴�</text>
- <text class="module-desc">褰曞叆涓嶅悎鏍煎搧璁板綍</text>
+ <text class="module-desc">璁板綍涓嶅悎鏍煎搧鐨勫叆搴�</text>
</view>
</view>
</view>
- <view class="form-content"
- v-if="showForm">
- <u-form ref="formRef"
- :model="form"
- :rules="formRules"
- label-width="100px">
- <u-form-item label="鍏ュ簱绫诲瀷"
- border-bottom>
- <u-tag :text="type === 'qualified' ? '鍚堟牸鍏ュ簱' : '涓嶅悎鏍煎叆搴�'"
- :type="type === 'qualified' ? 'success' : 'error'"></u-tag>
- </u-form-item>
- <u-form-item label="浜у搧鍚嶇О"
- border-bottom>
- <u-input v-model="form.productName"
- readonly
- border="none"></u-input>
- </u-form-item>
- <u-form-item label="瑙勬牸鍨嬪彿"
- border-bottom>
- <u-input v-model="form.productModelName"
- readonly
- border="none"></u-input>
- </u-form-item>
- <u-form-item label="鍗曚綅"
- border-bottom>
- <u-input v-model="form.unit"
- readonly
- border="none"></u-input>
- </u-form-item>
- <u-form-item label="鍏ュ簱鏁伴噺"
- prop="qualitity"
- required
- border-bottom>
- <u-number-box v-model="form.qualitity"
- :min="1"
- :step="1"></u-number-box>
- </u-form-item>
- <u-form-item label="棰勮鏁伴噺"
- prop="warnNum"
- v-if="type === 'qualified'"
- border-bottom>
- <u-number-box v-model="form.warnNum"
- :min="0"
- :step="1"></u-number-box>
- </u-form-item>
- <u-form-item label="澶囨敞"
- prop="remark"
- border-bottom>
- <u-textarea v-model="form.remark"
- placeholder="璇疯緭鍏ュ娉�"
- count></u-textarea>
- </u-form-item>
- </u-form>
- <view class="footer-btns">
- <u-button class="cancel-btn"
- @click="cancelForm">鍙栨秷</u-button>
- <u-button class="save-btn"
- @click="handleSubmit"
- :loading="loading">纭鍏ュ簱</u-button>
+ <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>
+ <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, reactive } from "vue";
+ import { ref, computed } from "vue";
import PageHeader from "@/components/PageHeader.vue";
- import {
- createStockInventory,
- getStockInventoryListPage,
- } from "@/api/inventoryManagement/stockInventory.js";
- import {
- createStockUnInventory,
- getStockUninventoryListPage,
- } from "@/api/inventoryManagement/stockUninventory.js";
+ 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("qualified"); // qualified | unqualified
- const loading = ref(false);
- const formRef = ref(null);
+ 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 form = ref({
- productId: undefined,
- productModelId: undefined,
- productName: "",
- productModelName: "",
- unit: "",
- qualitity: 1,
- warnNum: 0,
- remark: "",
+ const cardTitleMain = computed(() => {
+ const no = scanContractNo.value?.trim();
+ return no || "鈥�";
});
- const formRules = {
- qualitity: [
- {
- required: true,
- type: "number",
- message: "璇疯緭鍏ュ叆搴撴暟閲�",
- trigger: ["blur", "change"],
- },
- ],
+ 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) {
- showForm.value = false;
+ resetDetailView();
} else {
uni.navigateBack();
}
};
const cancelForm = () => {
- showForm.value = false;
+ resetDetailView();
};
const startScan = scanType => {
@@ -151,7 +324,7 @@
success: res => {
handleScanResult(res.result);
},
- fail: err => {
+ fail: () => {
modal.msgError("鎵爜澶辫触");
},
});
@@ -159,52 +332,38 @@
const handleScanResult = async result => {
try {
- // 瑙f瀽浜岀淮鐮佹暟鎹�
const scanData = JSON.parse(result);
if (!scanData.id) {
modal.msgError("鏃犳晥鐨勪簩缁寸爜鏁版嵁");
return;
}
-
- // 鐩存帴浠庝簩缁寸爜淇℃伅涓幏鍙栦骇鍝佽鎯�
- form.value.productId = scanData.productId; // 濡傛灉浜岀淮鐮佷腑鏈� productId
- form.value.productName = scanData.productName;
- form.value.productModelId = scanData.id; // 浜岀淮鐮佷腑鐨� id 鏄骇鍝佸瀷鍙� ID
- form.value.productModelName = scanData.model;
- form.value.unit = scanData.unit;
- form.value.qualitity = 1;
- form.value.warnNum = 0;
- form.value.remark = "";
-
- showForm.value = true;
- } catch (error) {
- console.error("瑙f瀽浜岀淮鐮佸け璐�", error);
- modal.msgError("瑙f瀽浜岀淮鐮佸け璐ワ紝璇风‘淇濇壂鐮佸唴瀹规纭�");
- }
- };
-
- const handleSubmit = async () => {
- try {
- const valid = await formRef.value.validate();
- if (!valid) return;
-
- loading.value = true;
- const apiCall =
- type.value === "qualified"
- ? createStockInventory
- : createStockUnInventory;
-
- const res = await apiCall(form.value);
- if (res.code === 200) {
- modal.msgSuccess("鍏ュ簱鎴愬姛");
- setTimeout(() => {
- showForm.value = false;
- }, 1500);
+ 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) {
- console.error("鎻愪氦澶辫触", error);
- } finally {
- loading.value = false;
+ modal.closeLoading();
+ scanLedgerId.value = null;
+ console.error("澶勭悊鎵爜缁撴灉澶辫触", error);
+ modal.msgError("鎵爜澶勭悊澶辫触锛岃閲嶈瘯");
}
};
</script>
@@ -277,29 +436,145 @@
color: #999;
}
- .form-content {
+ .detail-scroll {
+ max-height: calc(100vh - 120rpx);
+ box-sizing: border-box;
+ }
+
+ .detail-card {
background-color: #fff;
margin: 20rpx;
- padding: 30rpx;
+ 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 {
- margin-top: 60rpx;
display: flex;
justify-content: space-between;
- padding-bottom: 40rpx;
+ align-items: center;
+ gap: 24rpx;
+ padding: 20rpx 40rpx 60rpx;
}
- .cancel-btn {
- width: 30%;
+ .footer-cancel-btn {
+ flex: 1;
background-color: #f5f5f5;
color: #666;
border: none;
}
- .save-btn {
- width: 65%;
+ .footer-confirm-btn {
+ flex: 1;
background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
color: #fff;
border: none;
--
Gitblit v1.9.3