| | |
| | | ref="formRef"> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="产品名称" |
| | | prop="productModelId" |
| | | :rules="[ |
| | | <el-form-item label="绑定销售台账"> |
| | | <el-select v-model="formState.salesLedgerId" |
| | | placeholder="不绑定则按原有出库流程" |
| | | clearable |
| | | filterable |
| | | remote |
| | | :remote-method="searchSalesLedger" |
| | | :loading="salesLedgerLoading" |
| | | style="width: 100%" |
| | | @change="handleSalesLedgerChange"> |
| | | <el-option v-for="item in salesLedgerOptions" |
| | | :key="item.id" |
| | | :label="salesLedgerLabel(item)" |
| | | :value="item.id" /> |
| | | </el-select> |
| | | <div v-if="boundLedgerRow" |
| | | class="bound-ledger-tip"> |
| | | 已绑定:{{ salesLedgerLabel(boundLedgerRow) }};出库类别、客户{{ isStorageBound ? '、油库/储罐' : '' }}以台账为准 |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <template v-if="!isStorageBound"> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="产品名称" |
| | | prop="productModelId" |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: '请选择产品', |
| | | trigger: 'change', |
| | | } |
| | | ]"> |
| | | <el-button type="primary" |
| | | @click="showProductSelectDialog = true"> |
| | | {{ formState.productName ? formState.productName : '选择产品' }} |
| | | </el-button> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="规格" |
| | | prop="model"> |
| | | <el-input v-model="formState.model" |
| | | disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="单位" |
| | | prop="unit"> |
| | | <el-input v-model="formState.unit" |
| | | disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <!-- 绑定卖油台账后,产品只能从该台账明细里选(代储台账没有明细) --> |
| | | <el-select v-if="hasBoundLedger" |
| | | v-model="formState.productModelId" |
| | | placeholder="请选择台账产品" |
| | | filterable |
| | | clearable |
| | | style="width: 100%" |
| | | @change="handleLedgerProductChange"> |
| | | <el-option v-for="(item, index) in ledgerProductOptions" |
| | | :key="`${item.productModelId}-${index}`" |
| | | :label="ledgerProductLabel(item)" |
| | | :value="item.productModelId" /> |
| | | </el-select> |
| | | <el-button v-else |
| | | type="primary" |
| | | @click="showProductSelectDialog = true"> |
| | | {{ formState.productName ? formState.productName : '选择产品' }} |
| | | </el-button> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="规格" |
| | | prop="model"> |
| | | <el-input v-model="formState.model" |
| | | disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="单位" |
| | | prop="unit"> |
| | | <el-input v-model="formState.unit" |
| | | disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </template> |
| | | <div class="form-section-title">出库信息</div> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="8"> |
| | |
| | | <el-select v-model="formState.outCategory" |
| | | placeholder="请选择" |
| | | clearable |
| | | :disabled="hasBoundLedger" |
| | | style="width: 100%"> |
| | | <el-option label="客存" |
| | | value="客存" /> |
| | |
| | | <el-select v-model="formState.oilDepotId" |
| | | placeholder="请选择" |
| | | clearable |
| | | :disabled="isStorageBound" |
| | | popper-class="oil-depot-select-popper" |
| | | style="width: 100%" |
| | | @change="depotChange"> |
| | |
| | | <el-select v-model="formState.tankId" |
| | | placeholder="请选择" |
| | | clearable |
| | | :disabled="isStorageBound" |
| | | popper-class="tank-select-popper" |
| | | style="width: 100%" |
| | | @change="tankChange"> |
| | |
| | | <el-col :span="12"> |
| | | <el-form-item label="油品" |
| | | prop="oilProduct" |
| | | :rules="[{ required: true, message: '请选择油品', trigger: 'change' }]"> |
| | | :rules="isStorageBound ? [] : [{ required: true, message: '请选择油品', trigger: 'change' }]"> |
| | | <el-input :model-value="formState.oilProduct" |
| | | readonly |
| | | placeholder="--" /> |
| | |
| | | placeholder="请选择" |
| | | clearable |
| | | filterable |
| | | :disabled="hasBoundLedger" |
| | | popper-class="customer-select-popper" |
| | | style="width: 100%" |
| | | @change="customerChange"> |
| | |
| | | addOilOutRecordOnly, |
| | | getStockInventoryBatchNoQty, |
| | | } from "@/api/inventoryManagement/stockInventory.js"; |
| | | import { getOilOutAvailableQty } from "@/api/inventoryManagement/stockOut.js"; |
| | | import { |
| | | getOilOutAvailableQty, |
| | | bindableSalesLedger, |
| | | } from "@/api/inventoryManagement/stockOut.js"; |
| | | import { productList } from "@/api/salesManagement/salesLedger.js"; |
| | | import { productModelList } from "@/api/basicData/productModel"; |
| | | import { allOilDepot } from "@/api/basicData/oilDepot.js"; |
| | | import { allTankInfo } from "@/api/basicData/tankInfo.js"; |
| | | import { allVehicle } from "@/api/basicData/vehicle.js"; |
| | |
| | | const emit = defineEmits(["update:visible", "completed"]); |
| | | |
| | | const buildEmptyForm = () => ({ |
| | | // 绑定的销售台账 id;为空表示不绑定,走原有出库流程 |
| | | salesLedgerId: undefined, |
| | | productId: undefined, |
| | | productModelId: undefined, |
| | | productName: "", |
| | |
| | | const batchNoOptions = ref([]); |
| | | // 销售类出库的可用量提示(上次剩余 + 当前库存) |
| | | const availableQty = ref(null); |
| | | // 绑定销售台账:下拉候选、当前绑定行、台账明细限定出的产品 |
| | | const salesLedgerOptions = ref([]); |
| | | const salesLedgerLoading = ref(false); |
| | | const boundLedgerRow = ref(null); |
| | | const ledgerProductOptions = ref([]); |
| | | // 台账明细行只有 productModelId,产品名称/型号/单位要回产品规格表取 |
| | | const productModelCache = ref(null); |
| | | |
| | | const hasBoundLedger = computed(() => !!formState.value.salesLedgerId); |
| | | // 绑代储台账:不用选产品,油库/储罐由台账带入 |
| | | const isStorageBound = computed(() => boundLedgerRow.value?.ledgerType === "代储"); |
| | | |
| | | const fetchBatchNoOptions = () => { |
| | | batchNoOptions.value = []; |
| | |
| | | const closeModal = () => { |
| | | proxy.resetForm("formRef"); |
| | | formState.value = buildEmptyForm(); |
| | | boundLedgerRow.value = null; |
| | | ledgerProductOptions.value = []; |
| | | isShow.value = false; |
| | | }; |
| | | |
| | |
| | | } |
| | | }; |
| | | |
| | | const salesLedgerLabel = item => |
| | | [item.salesContractNo, item.customerName, item.projectName] |
| | | .filter(Boolean) |
| | | .join(" / "); |
| | | |
| | | // 搜索框只有一个,而后端三个参数是 AND 关系,所以依次按 合同号 → 客户名 → 项目名 试,命中即止 |
| | | const LEDGER_SEARCH_FIELDS = ["salesContractNo", "customerName", "projectName"]; |
| | | |
| | | const queryLedger = params => |
| | | bindableSalesLedger({ current: 1, size: 50, ...params }).then( |
| | | res => res.data?.records || [] |
| | | ); |
| | | |
| | | // 绑定下拉:卖油和代储都会返回,不做类型过滤,由用户自己选 |
| | | const fetchSalesLedgerOptions = async keyword => { |
| | | salesLedgerLoading.value = true; |
| | | try { |
| | | let records = []; |
| | | if (keyword) { |
| | | for (const field of LEDGER_SEARCH_FIELDS) { |
| | | records = await queryLedger({ [field]: keyword }); |
| | | if (records.length) { |
| | | break; |
| | | } |
| | | } |
| | | } else { |
| | | records = await queryLedger({}); |
| | | } |
| | | // 搜索后已选中的台账可能不在结果里,补上避免下拉框只显示一个 id |
| | | if ( |
| | | boundLedgerRow.value && |
| | | !records.some(item => item.id === boundLedgerRow.value.id) |
| | | ) { |
| | | records.unshift(boundLedgerRow.value); |
| | | } |
| | | salesLedgerOptions.value = records; |
| | | } catch (e) { |
| | | salesLedgerOptions.value = []; |
| | | } finally { |
| | | salesLedgerLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | const searchSalesLedger = keyword => |
| | | fetchSalesLedgerOptions(String(keyword || "").trim()); |
| | | |
| | | // 产品规格表全量取一次并缓存:台账明细只有 productModelId,名称/型号/单位得从这里补 |
| | | const getProductModels = () => { |
| | | if (productModelCache.value) { |
| | | return Promise.resolve(productModelCache.value); |
| | | } |
| | | return productModelList({ current: 1, size: 1000 }) |
| | | .then(res => { |
| | | const list = res?.records || res?.data?.records || res.data || []; |
| | | productModelCache.value = Array.isArray(list) ? list : []; |
| | | return productModelCache.value; |
| | | }) |
| | | .catch(() => []); |
| | | }; |
| | | |
| | | // 台账明细 → 产品下拉候选;代储台账没有明细,不会调到这里 |
| | | const fetchLedgerProducts = ledgerId => { |
| | | ledgerProductOptions.value = []; |
| | | return Promise.all([ |
| | | productList({ salesLedgerId: ledgerId, type: 1 }), |
| | | getProductModels(), |
| | | ]).then(([ledgerRes, models]) => { |
| | | const rows = Array.isArray(ledgerRes?.data) ? ledgerRes.data : []; |
| | | const modelMap = new Map((models || []).map(item => [String(item.id), item])); |
| | | ledgerProductOptions.value = rows.map(row => { |
| | | const model = modelMap.get(String(row.productModelId)) || {}; |
| | | return { |
| | | productId: row.productId ?? model.productId, |
| | | productModelId: row.productModelId ?? model.id, |
| | | productName: |
| | | model.productName || row.productCategory || row.specificationModel || "", |
| | | model: model.model || row.specificationModel || "", |
| | | unit: model.unit || row.unit || "", |
| | | }; |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | const ledgerProductLabel = item => |
| | | [item.productName, item.model].filter(Boolean).join(" / "); |
| | | |
| | | const clearProduct = () => { |
| | | formState.value.productId = undefined; |
| | | formState.value.productModelId = undefined; |
| | | formState.value.productName = ""; |
| | | formState.value.model = ""; |
| | | formState.value.unit = ""; |
| | | formState.value.oilProduct = undefined; |
| | | formState.value.batchNo = undefined; |
| | | batchNoOptions.value = []; |
| | | }; |
| | | |
| | | const handleLedgerProductChange = productModelId => { |
| | | const item = ledgerProductOptions.value.find( |
| | | row => row.productModelId === productModelId |
| | | ); |
| | | if (!item) { |
| | | clearProduct(); |
| | | return; |
| | | } |
| | | formState.value.productId = item.productId; |
| | | formState.value.productName = item.productName; |
| | | formState.value.model = item.model; |
| | | formState.value.unit = item.unit; |
| | | formState.value.oilProduct = item.productName || item.model || undefined; |
| | | formState.value.batchNo = undefined; |
| | | fetchBatchNoOptions(); |
| | | proxy.$refs["formRef"]?.validateField("productModelId"); |
| | | }; |
| | | |
| | | const handleSalesLedgerChange = id => { |
| | | const row = salesLedgerOptions.value.find(item => item.id === id); |
| | | // 清空选择时 el-select 会给空串,归一化成 undefined,避免把空串当台账 id 传后端 |
| | | if (!row) { |
| | | formState.value.salesLedgerId = undefined; |
| | | } |
| | | boundLedgerRow.value = row || null; |
| | | ledgerProductOptions.value = []; |
| | | clearProduct(); |
| | | if (!row) { |
| | | // 取消绑定:出库类别、客户恢复为手选 |
| | | return; |
| | | } |
| | | // 出库类别与客户以台账为准(后端也会强制覆盖),前端同步展示并置灰 |
| | | formState.value.outCategory = row.ledgerType === "代储" ? "代储" : "销售"; |
| | | customerChange(row.customerId); |
| | | formState.value.customerName = |
| | | row.customerName || formState.value.customerName || ""; |
| | | if (row.ledgerType === "代储") { |
| | | formState.value.oilDepotId = row.oilDepotId; |
| | | formState.value.oilDepotName = row.oilDepotName || ""; |
| | | formState.value.tankId = row.tankId; |
| | | formState.value.tankNo = row.tankNo || ""; |
| | | return; |
| | | } |
| | | fetchLedgerProducts(row.id); |
| | | }; |
| | | |
| | | const handleSubmit = () => { |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | | if (!formState.value.productModelId) { |
| | | // 绑代储台账不用选产品(代储台账没有产品明细) |
| | | if (!formState.value.productModelId && !isStorageBound.value) { |
| | | proxy.$modal.msgError("请选择产品"); |
| | | return; |
| | | } |
| | |
| | | }); |
| | | }; |
| | | |
| | | // 每次打开重新拉台账候选,避免在别处新建台账后本页看不到 |
| | | watch( |
| | | () => props.visible, |
| | | visible => { |
| | | if (visible) { |
| | | fetchSalesLedgerOptions(""); |
| | | } |
| | | } |
| | | ); |
| | | |
| | | onMounted(() => { |
| | | fetchSalesLedgerOptions(""); |
| | | allOilDepot().then(res => { |
| | | oilDepotList.value = res?.data || []; |
| | | }); |
| | |
| | | border-radius: 3px; |
| | | } |
| | | |
| | | // 绑定销售台账后的提示条 |
| | | .bound-ledger-tip { |
| | | margin-top: 6px; |
| | | padding: 6px 14px; |
| | | font-size: 13px; |
| | | color: #303133; |
| | | background: rgba(64, 158, 255, 0.08); |
| | | border-left: 3px solid #409eff; |
| | | border-radius: 3px; |
| | | } |
| | | |
| | | // 储库名称下拉选项:小卡片样式 |
| | | .oil-depot-option { |
| | | padding: 8px 10px; |