| | |
| | | <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> |
| | | <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.model" |
| | | readonly |
| | | border="none"></u-input> |
| | | </u-form-item> |
| | | <u-form-item label="可用库存" |
| | | border-bottom> |
| | | <u-input v-model="form.unLockedQuantity" |
| | | readonly |
| | | border="none"></u-input>{{form.unit}} |
| | | </u-form-item> |
| | | <u-form-item label="出库数量" |
| | | prop="qualitity" |
| | | required |
| | | border-bottom> |
| | | <u-number-box v-model="form.qualitity" |
| | | :min="1" |
| | | :max="form.unLockedQuantity" |
| | | :step="1"></u-number-box> |
| | | <text class="limit-tip">最大可领用: {{form.unLockedQuantity}}</text> |
| | | </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> |
| | | |
| | | <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="cancel-btn" |
| | | @click="cancelForm">取消</u-button> |
| | | <u-button class="save-btn" |
| | | @click="handleSubmit" |
| | | :loading="loading">确认出库</u-button> |
| | | |
| | | <u-button class="footer-cancel-btn" |
| | | |
| | | @click="cancelForm">返回</u-button> |
| | | |
| | | <u-button class="footer-confirm-btn" |
| | | |
| | | :loading="submitLoading" |
| | | |
| | | @click="confirmOutbound">确认</u-button> |
| | | |
| | | </view> |
| | | </view> |
| | | |
| | | </scroll-view> |
| | | |
| | | </view> |
| | | |
| | | </template> |
| | | |
| | | |
| | | |
| | | <script setup> |
| | | import { ref, reactive } from "vue"; |
| | | |
| | | import { ref, computed } from "vue"; |
| | | |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { |
| | | subtractStockInventory, |
| | | getStockInventoryListPage, |
| | | } from "@/api/inventoryManagement/stockInventory.js"; |
| | | import { |
| | | subtractStockUnInventory, |
| | | getStockUninventoryListPage, |
| | | } from "@/api/inventoryManagement/stockUninventory.js"; |
| | | |
| | | import { productList as salesProductList } from "@/api/salesManagement/salesLedger"; |
| | | |
| | | import modal from "@/plugins/modal"; |
| | | |
| | | const showForm = ref(false); |
| | | const type = ref("qualified"); // qualified | unqualified |
| | | const loading = ref(false); |
| | | const formRef = ref(null); |
| | | import { QUALITY_TYPE, CONTRACT_KIND } from "./scanOut.constants"; |
| | | import { useScanOutFieldRows } from "./scanOut.fields"; |
| | | import { |
| | | parseOptionalNumber, |
| | | defaultStockedQuantityFromRow, |
| | | resolveQrContractKind, |
| | | resolveListTypeForDetail, |
| | | resolveContractNo, |
| | | buildSalesLedgerProductList, |
| | | hasAnyPositiveStockedQty, |
| | | resolveSubmitSceneKey, |
| | | } from "./scanOut.logic"; |
| | | import { createSubmitConfig } from "./scanOut.submit"; |
| | | |
| | | const form = ref({ |
| | | id: undefined, |
| | | productId: undefined, |
| | | productModelId: undefined, |
| | | productName: "", |
| | | model: "", |
| | | unit: "", |
| | | qualitity: 1, |
| | | unLockedQuantity: 0, |
| | | remark: "", |
| | | 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 submitConfigByScene = createSubmitConfig(scanLedgerId); |
| | | |
| | | const cardTitleMain = computed(() => { |
| | | |
| | | const no = scanContractNo.value?.trim(); |
| | | |
| | | return no || "—"; |
| | | |
| | | }); |
| | | |
| | | const formRules = { |
| | | qualitity: [ |
| | | { |
| | | required: true, |
| | | type: "number", |
| | | message: "请输入出库数量", |
| | | trigger: ["blur", "change"], |
| | | }, |
| | | { |
| | | validator: (rule, value, callback) => { |
| | | if (value > form.value.unLockedQuantity) { |
| | | callback(new Error("不能超过可用库存")); |
| | | } else { |
| | | callback(); |
| | | } |
| | | }, |
| | | 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 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 => { |
| | | |
| | | 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 = []; |
| | | |
| | | }; |
| | | |
| | | |
| | | |
| | | /** 组装提交用的产品行(含数值化出库数量 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; |
| | | |
| | | } |
| | | |
| | | 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 => { |
| | | |
| | | type.value = scanType; |
| | | |
| | | uni.scanCode({ |
| | | |
| | | success: res => { |
| | | |
| | | handleScanResult(res.result); |
| | | |
| | | }, |
| | | fail: err => { |
| | | |
| | | 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; |
| | | |
| | | } |
| | | |
| | | // 获取实时库存详情 |
| | | modal.loading("获取产品库存详情..."); |
| | | const apiCall = |
| | | type.value === "qualified" |
| | | ? getStockInventoryListPage |
| | | : getStockUninventoryListPage; |
| | | const kind = resolveQrContractKind(scanData); |
| | | |
| | | const res = await apiCall({ productModelId: scanData.id }); |
| | | 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.records && res.data.records.length > 0) { |
| | | const detail = res.data.records[0]; |
| | | form.value.id = detail.id; |
| | | form.value.productId = detail.productId; |
| | | form.value.productName = detail.productName; |
| | | form.value.productModelId = detail.productModelId; |
| | | form.value.model = detail.model; |
| | | form.value.unit = detail.unit; |
| | | form.value.unLockedQuantity = detail.unLockedQuantity; |
| | | form.value.qualitity = 1; |
| | | form.value.remark = ""; |
| | | |
| | | if (form.value.unLockedQuantity <= 0) { |
| | | modal.msgError("当前库存不足,无法出库"); |
| | | return; |
| | | } |
| | | |
| | | 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 { |
| | | modal.msgError("未找到该产品型号的库存记录"); |
| | | |
| | | scanLedgerId.value = null; |
| | | |
| | | modal.msgError("未查询到明细数据"); |
| | | |
| | | } |
| | | |
| | | } catch (error) { |
| | | |
| | | modal.closeLoading(); |
| | | |
| | | scanLedgerId.value = null; |
| | | |
| | | console.error("处理扫码结果失败", error); |
| | | |
| | | modal.msgError("扫码处理失败,请重试"); |
| | | |
| | | } |
| | | |
| | | }; |
| | | |
| | | const handleSubmit = async () => { |
| | | try { |
| | | const valid = await formRef.value.validate(); |
| | | if (!valid) return; |
| | | |
| | | loading.value = true; |
| | | const apiCall = |
| | | type.value === "qualified" |
| | | ? subtractStockInventory |
| | | : subtractStockUnInventory; |
| | | |
| | | const res = await apiCall(form.value); |
| | | if (res.code === 200) { |
| | | modal.msgSuccess("出库成功"); |
| | | setTimeout(() => { |
| | | showForm.value = false; |
| | | }, 1500); |
| | | } |
| | | } catch (error) { |
| | | console.error("提交失败", error); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | </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; |
| | | |
| | | } |
| | | |
| | | .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); |
| | | |
| | | } |
| | | |
| | | .limit-tip { |
| | | font-size: 24rpx; |
| | | color: #999; |
| | | margin-left: 20rpx; |
| | | |
| | | |
| | | .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; |
| | | |
| | | } |
| | | |
| | | </style> |
| | | |