新疆-融邦环保
1.油品出库表单改「多台账分摊绑定」:各台账数量之和必须等于出库吨位,产品取各台账明细交集,代储不锁行可自由增删
2.新增到货记录模块:销售台账页入口,车辆随出库单联动,到货上限=该台账分摊量,附件走永久链接
3.发货台账改层级父行:油品出库为父行,children 为到货记录,来源列区分发货单/油品出库/到货
4.开票/收款候选按「出库单×客户」拆行,bindingId 与出库单 id 分两串回传,防重复开票/收款
5.销售台账下拉补产品/规格型号
6.代储入库绑定销售订单:油品入库表单加销售订单下拉,入库台账列表加「销售订单」列
7.采购申请补齐台账字段(油库、采购合同号、提货方式、物流方式、最佳到站日期、采购类别、付款方式、预警数量、附件等)
8.修复采购申请「转订单后跳转无反应」
9.采购台账删掉「销售合同号」的搜索条件、列表列和表单字段
10.公海客户新增「信托基金(元)」,代储金额合计超限标红提示
已添加2个文件
已修改12个文件
2609 ■■■■■ 文件已修改
src/api/inventoryManagement/stockInRecord.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/salesLedgerArrival.js 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AttachmentUpload/image/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/customerFileOpenSea/index.vue 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/receivable/invoiceApply.vue 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/receivable/receipt.vue 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/receiptManagement/Record.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/OilStockInDialog.vue 252 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/OilStockOutDialog.vue 729 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementLedger/index.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/purchaseApplication/index.vue 460 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/deliveryLedger/index.vue 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/components/ArrivalDialog.vue 510 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 191 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockInRecord.js
@@ -9,6 +9,15 @@
    });
};
// ä»£å‚¨å…¥åº“可绑定的销售订单(后端只出 ledgerType='代储' çš„台账)
export const bindableSalesLedger = (params) => {
    return request({
        url: "/stockInRecord/bindableSalesLedger",
        method: "get",
        params,
    });
};
export const updateStockInRecord = (id, data) => {
    return request({
src/api/salesManagement/salesLedgerArrival.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
// åˆ°è´§è®°å½•接口:挂在「出库单 Ã— é”€å”®å°è´¦ã€ä¸Šï¼ŒåŒæ—¶æ˜¯å‘货台账油品出库父行的子行
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢ï¼ˆæ”¯æŒ salesLedgerId / stockOutRecordId / truckPlateNo / salesContractNo / customerName / arrivalDate)
export function salesLedgerArrivalListPage(query) {
  return request({
    url: "/salesLedgerArrival/listPage",
    method: "get",
    params: query,
  });
}
// å¯åˆ°è´§è½¦è¾†ä¸‹æ‹‰ï¼šåªå‡ºã€Œç»‘定了该台账、且审批通过的出库记录里用过的车」
export function bindableArrivalVehicles(query) {
  return request({
    url: "/salesLedgerArrival/bindableVehicles",
    method: "get",
    params: query,
  });
}
export function addSalesLedgerArrival(data) {
  return request({
    url: "/salesLedgerArrival/add",
    method: "post",
    data,
  });
}
export function updateSalesLedgerArrival(data) {
  return request({
    url: "/salesLedgerArrival/update",
    method: "post",
    data,
  });
}
// æ‰¹é‡åˆ é™¤ï¼ŒBody æ˜¯ [id, id]
export function delSalesLedgerArrival(ids) {
  return request({
    url: "/salesLedgerArrival/delete",
    method: "delete",
    data: ids,
  });
}
src/components/AttachmentUpload/image/index.vue
@@ -105,7 +105,10 @@
function getItemUrl(item) {
  if (!item) return ''
  if (typeof item === 'string') return item
  return item.url || item.previewURL || ''
  // æ°¸ä¹…模式优先 previewURL:url å¯èƒ½æ˜¯ 2 å°æ—¶å¤±æ•ˆçš„签名地址
  return props.permanent
      ? item.previewURL || item.previewUrl || item.url || ''
      : item.url || item.previewURL || ''
}
function getItemName(item, index = 0) {
@@ -121,8 +124,11 @@
      url: item,
    }
  }
  const url = props.permanent
      ? item.previewURL || item.previewUrl || item.url || ''
      : item.url || item.previewURL || item.previewUrl || ''
  return Object.assign({}, item, {
    url: item.url || item.previewURL || item.previewUrl || '',
    url,
    name: item.name || item.originalFilename || item.fileName || `image-${currentList.value.length + index + 1}`,
  })
}
src/views/basicData/customerFileOpenSea/index.vue
@@ -45,7 +45,16 @@
                :isSelection="true"
                @selection-change="handleSelectionChange"
                :tableLoading="tableLoading"
                @pagination="pagination"></PIMTable>
                @pagination="pagination">
        <template #depositAmountSlot="{ row }">
          <el-tooltip v-if="row.exceedsTrustFund"
                      :content="`代储金额合计已超出该客户信托基金(信托基金:${row.trustFund})`"
                      placement="top">
            <el-text type="danger">{{ formatAmount(row.depositTotalAmount) }}</el-text>
          </el-tooltip>
          <span v-else>{{ formatAmount(row.depositTotalAmount) }}</span>
        </template>
      </PIMTable>
    </div>
    <el-dialog v-model="dialogFormVisible"
               :title="operationType === 'add' ? '新增客户信息' : '编辑客户信息'"
@@ -190,6 +199,19 @@
                              type="date"
                              placeholder="请选择"
                              clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="信托基金(元):"
                          prop="trustFund">
              <el-input-number v-model="form.trustFund"
                               :min="0"
                               :precision="2"
                               :controls="false"
                               placeholder="请输入,可为空"
                               style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>
@@ -864,6 +886,22 @@
      width: 220,
    },
    {
      label: "信托基金(元)",
      prop: "trustFund",
      width: 140,
      formatData: value =>
        value === null || value === undefined || value === ""
          ? "-"
          : Number(value).toFixed(2),
    },
    {
      label: "代储金额(元)",
      prop: "depositTotalAmount",
      dataType: "slot",
      slot: "depositAmountSlot",
      width: 150,
    },
    {
      label: "开户行号",
      prop: "bankCode",
      width: 220,
@@ -1007,6 +1045,8 @@
      bankAccount: "",
      bankCode: "",
      customerType: "",
      // é‡‘额字段给 null è€Œéž "",避免 el-input-number æŠŠç©ºä¸²æŠ˜ç®—成 0
      trustFund: null,
      type: 1
    },
    rules: {
@@ -1693,6 +1733,13 @@
    return true;
  };
  // é‡‘额展示:空值显示 -,否则保留两位小数
  const formatAmount = value => {
    return value === null || value === undefined || value === ""
      ? "-"
      : Number(value).toFixed(2);
  };
  // æ ¼å¼åŒ–文件大小
  const formatFileSize = size => {
    if (size < 1024) {
src/views/financialManagement/receivable/invoiceApply.vue
@@ -231,7 +231,7 @@
        ref="outboundTableRef"
        v-loading="outboundBatchLoading"
        :data="outboundBatchList"
        row-key="id"
        :row-key="getOutboundOptionValue"
        border
        stripe
        max-height="480"
@@ -339,6 +339,8 @@
const outboundSelectVisible = ref(false);
const outboundTableRef = ref(null);
const dialogOutboundSelection = ref([]);
// å·²ç¡®è®¤é€‰ä¸­çš„出库候选整行(含 bindingId),提交时按它拆分 id ç©ºé—´
const selectedOutboundRows = ref([]);
const getCustomerList = () => {
  listCustomer({ current: -1, size: -1, type: 0 }).then((res) => {
@@ -347,6 +349,39 @@
    }
  });
};
// ä¸€ä¸ª CK å·å¯èƒ½æŒ‰å®¢æˆ·æ‹†æˆå¤šè¡Œï¼ˆæ²¹å“å‡ºåº“按「出库单 Ã— å®¢æˆ·ã€å‡ºå‚),
// æ‰€ä»¥ä¸èƒ½ç”¨å‡ºåº“单 id å½“唯一键:有 bindingId çš„行用 bindingId,其余用出库单 id
const getOutboundOptionValue = (row) => {
  const bindingId = row?.bindingId;
  if (bindingId !== undefined && bindingId !== null && bindingId !== "") {
    return `binding-${bindingId}`;
  }
  const id = row?.id ?? row?.stockOutRecordId;
  return id === undefined || id === null ? undefined : `record-${id}`;
};
// ä¸¤ä¸ª id ç©ºé—´éƒ½æ˜¯å°æ•´æ•°ï¼Œæ··ç€å­˜ä¼šæ’žå·ï¼ˆFIND_IN_SET åˆ†ä¸å‡ºæ¥ï¼‰å¯¼è‡´é‡å¤å¼€ç¥¨ï¼Œ
// æ‰€ä»¥å¿…须按 bindingId æ˜¯å¦ä¸ºç©ºæ‹†æˆä¸¤ä¸²åˆ†å¼€å›žä¼ 
const splitOutboundIds = (rows) => {
  const recordIds = [];
  const bindingIds = [];
  rows.forEach((row) => {
    const bindingId = row?.bindingId;
    if (bindingId !== undefined && bindingId !== null && bindingId !== "") {
      bindingIds.push(bindingId);
      return;
    }
    const id = row?.id ?? row?.stockOutRecordId;
    if (id !== undefined && id !== null) {
      recordIds.push(id);
    }
  });
  return { recordIds, bindingIds };
};
// ç¼–辑回显时表单里存的是带前缀的选项值,回传前要还原成裸 id
const stripOutboundPrefix = (value) => String(value).replace(/^(record|binding)-/, "");
const normalizeOutboundBatchOptions = (data) => {
  const list = Array.isArray(data) ? data : [];
@@ -362,7 +397,7 @@
      item.outboundNo ??
      item.label ??
      `出库单${index + 1}`;
    const value = item.id ?? item.stockOutRecordId ?? item.stockOutRecordIds ?? label;
    const value = getOutboundOptionValue(item) ?? label;
    const outboundAmount = Number(item.outboundAmount) || 0;
    const taxRate =
      item.taxRate !== undefined && item.taxRate !== null && item.taxRate !== ""
@@ -413,8 +448,6 @@
  form.amount = sum > 0 ? Number(sum.toFixed(2)) : 0;
};
const getOutboundRowId = (row) => row?.id ?? row?.stockOutRecordId;
const outboundBatchDisplayText = computed(() => {
  if (isEdit.value || isView.value) {
    return form.outboundBatches || "";
@@ -440,7 +473,7 @@
    table.clearSelection();
    const selectedIds = new Set((form.outboundBatchNos || []).map((id) => String(id)));
    outboundBatchList.value.forEach((row) => {
      const rowId = getOutboundRowId(row);
      const rowId = getOutboundOptionValue(row);
      if (rowId !== undefined && rowId !== null && selectedIds.has(String(rowId))) {
        table.toggleRowSelection(row, true);
      }
@@ -468,7 +501,7 @@
  const prevIds = [...(form.outboundBatchNos || [])];
  const prevBatches = form.outboundBatches;
  form.outboundBatchNos = dialogOutboundSelection.value
    .map((row) => getOutboundRowId(row))
    .map((row) => getOutboundOptionValue(row))
    .filter((id) => id !== undefined && id !== null);
  form.outboundBatches = dialogOutboundSelection.value
    .map((row) => row.outboundBatches ?? row.batchNo ?? row.shippingNo ?? "")
@@ -479,6 +512,8 @@
    form.outboundBatches = prevBatches;
    return;
  }
  // ä¿ç•™æ•´è¡Œï¼šæäº¤æ—¶æ‰çŸ¥é“每行是走 stockOutRecordIds è¿˜æ˜¯ stockOutBindingIds
  selectedOutboundRows.value = [...dialogOutboundSelection.value];
  outboundSelectVisible.value = false;
  syncInvoiceAmount();
  formRef.value?.validateField("outboundBatchNos");
@@ -494,6 +529,8 @@
    outboundBatchOptions.value = [];
    if (!keepSelected) {
      form.outboundBatchNos = [];
      form.stockOutBindingIds = [];
      selectedOutboundRows.value = [];
      form.amount = 0;
    }
    return Promise.resolve();
@@ -525,6 +562,8 @@
const handleCustomerChange = (customerId) => {
  form.outboundBatchNos = [];
  form.stockOutBindingIds = [];
  selectedOutboundRows.value = [];
  form.outboundBatches = "";
  form.amount = 0;
  loadOutboundBatches(customerId);
@@ -534,6 +573,8 @@
  applyCode: "",
  customerId: "",
  outboundBatchNos: [],
  // æ²¹å“å‡ºåº“的绑定行 id(与出库单 id åˆ†å¼€å­˜ï¼Œæ··ç”¨ä¼šæ’žå·ï¼‰
  stockOutBindingIds: [],
  outboundBatches: "",
  amount: 0,
  taxRate: 13,
@@ -740,9 +781,12 @@
};
const fillFormFromRow = (row) => {
  const outboundBatchNos = Array.isArray(row.outboundBatchNos)
  // å¸¦å‰ç¼€å›žæ˜¾æ‰èƒ½å’Œå€™é€‰é€‰é¡¹çš„值对上(绑定行与出库单是两个 id ç©ºé—´ï¼‰
  const outboundBatchNos = (
    Array.isArray(row.outboundBatchNos)
    ? row.outboundBatchNos
    : parseStockOutRecordIds(row.stockOutRecordIds ?? row.stockOutRecordId);
      : parseStockOutRecordIds(row.stockOutRecordIds ?? row.stockOutRecordId)
  ).map((id) => `record-${id}`);
  Object.assign(form, {
    ...row,
    applyCode: row.applyCode ?? row.invoiceApplicationNo ?? "",
@@ -750,6 +794,10 @@
    content: row.content ?? row.invoiceContent,
    status: normalizeStatus(row.status ?? row.auditStatus),
    outboundBatchNos,
    // ç¼–辑时不重开选择弹框,原样带回去才能保住绑定行关联
    stockOutBindingIds: parseStockOutRecordIds(row.stockOutBindingIds).map(
      (id) => `binding-${id}`
    ),
    outboundBatches: formatOutboundBatches(row.outboundBatches),
    createTime: row.createTime ?? "",
  });
@@ -763,6 +811,7 @@
    applyCode: "",
    customerId: "",
    outboundBatchNos: [],
    stockOutBindingIds: [],
    outboundBatches: "",
    amount: 0,
    taxRate: 13,
@@ -774,6 +823,7 @@
  });
  outboundBatchList.value = [];
  outboundBatchOptions.value = [];
  selectedOutboundRows.value = [];
  dialogVisible.value = true;
};
@@ -788,9 +838,17 @@
};
const buildSubmitPayload = (forUpdate = false) => {
  // æ–°é€‰çš„行按 bindingId æ‹†æˆä¸¤ä¸²ï¼›ç¼–辑时选择弹框没重开,沿用记录里带回来的两串
  const { recordIds, bindingIds } = selectedOutboundRows.value.length
    ? splitOutboundIds(selectedOutboundRows.value)
    : {
        recordIds: (form.outboundBatchNos || []).map(stripOutboundPrefix),
        bindingIds: (form.stockOutBindingIds || []).map(stripOutboundPrefix),
      };
  const payload = {
    customerId: form.customerId,
    stockOutRecordIds: (form.outboundBatchNos || []).join(","),
    stockOutRecordIds: recordIds.join(","),
    stockOutBindingIds: bindingIds.join(","),
    invoiceApplicationNo: form.applyCode || "",
    invoiceType: form.invoiceType,
    applyDate: form.applyDate,
src/views/financialManagement/receivable/receipt.vue
@@ -232,7 +232,7 @@
      <el-table ref="outboundTableRef"
                v-loading="outboundBatchLoading"
                :data="outboundBatchList"
                row-key="id"
                :row-key="getOutboundOptionValue"
                border
                stripe
                max-height="480"
@@ -378,6 +378,8 @@
  const outboundSelectVisible = ref(false);
  const outboundTableRef = ref(null);
  const dialogOutboundSelection = ref([]);
  // å‹¾é€‰è¡Œçš„完整明细:提交时按 bindingId æ˜¯å¦ä¸ºç©ºæ‹†æˆä¸¤ä¸ª id å­—段
  const selectedOutboundRows = ref([]);
  const getReceiptMethodLabel = value => {
    if (value === undefined || value === null || value === "") return "-";
@@ -396,6 +398,8 @@
    amount: 0,
    receiptMethod: "",
    stockOutRecordIds: [],
    // æ²¹å“å‡ºåº“的绑定行 id(与出库单 id åˆ†å¼€å­˜ï¼Œæ··ç”¨ä¼šæ’žå·ï¼‰
    stockOutBindingIds: [],
    outboundBatches: "",
    remark: "",
    createTime: "",
@@ -538,7 +542,7 @@
        item.outboundNo ??
        item.label ??
        `出库单${index + 1}`;
      const value = item.id ?? item.stockOutRecordId ?? label;
      const value = getOutboundOptionValue(item) ?? label;
      const outboundAmount = Number(item.outboundAmount) || 0;
      const amountReceived = Number(item.amountReceived) || 0;
      const availableAmount = outboundAmount - amountReceived;
@@ -554,7 +558,38 @@
  const isSameOutboundId = (a, b) => String(a) === String(b);
  const getOutboundRowId = row => row?.id ?? row?.stockOutRecordId;
  // ä¸€ä¸ª CK å·å¯èƒ½æŒ‰å®¢æˆ·æ‹†æˆå¤šè¡Œï¼ˆæ²¹å“å‡ºåº“按「出库单 Ã— å®¢æˆ·ã€å‡ºå‚),
  // æ‰€ä»¥ä¸èƒ½ç”¨å‡ºåº“单 id å½“唯一键:有 bindingId çš„行用 bindingId,其余用出库单 id
  const getOutboundOptionValue = row => {
    const bindingId = row?.bindingId;
    if (bindingId !== undefined && bindingId !== null && bindingId !== "") {
      return `binding-${bindingId}`;
    }
    const id = row?.id ?? row?.stockOutRecordId;
    return id === undefined || id === null ? undefined : `record-${id}`;
  };
  // ä¸¤ä¸ª id ç©ºé—´éƒ½æ˜¯å°æ•´æ•°ï¼Œæ··ç€å­˜ä¼šæ’žå·ï¼ˆFIND_IN_SET åˆ†ä¸å‡ºæ¥ï¼‰å¯¼è‡´åŒä¸€æ‰¹è´§é‡å¤æ”¶æ¬¾ï¼Œ
  // æ‰€ä»¥å¿…须按 bindingId æ˜¯å¦ä¸ºç©ºæ‹†æˆä¸¤ä¸²åˆ†å¼€å›žä¼ 
  const splitOutboundIds = rows => {
    const recordIds = [];
    const bindingIds = [];
    rows.forEach(row => {
      const bindingId = row?.bindingId;
      if (bindingId !== undefined && bindingId !== null && bindingId !== "") {
        bindingIds.push(bindingId);
        return;
      }
      const id = row?.id ?? row?.stockOutRecordId;
      if (id !== undefined && id !== null) {
        recordIds.push(id);
      }
    });
    return { recordIds, bindingIds };
  };
  // ç¼–辑回显时表单里存的是带前缀的选项值,回传前要还原成裸 id
  const stripOutboundPrefix = value => String(value).replace(/^(record|binding)-/, "");
  const outboundBatchDisplayText = computed(() => {
    if (isEdit.value || isView.value) {
@@ -584,7 +619,7 @@
      );
      if (exists) return;
      const fromList = outboundBatchList.value.find(row =>
        isSameOutboundId(getOutboundRowId(row), id)
        isSameOutboundId(getOutboundOptionValue(row), id)
      );
      if (fromList) {
        const [option] = normalizeOutboundBatchOptions([fromList]);
@@ -617,7 +652,7 @@
        (form.stockOutRecordIds || []).map(id => String(id))
      );
      outboundBatchList.value.forEach(row => {
        const rowId = getOutboundRowId(row);
        const rowId = getOutboundOptionValue(row);
        if (
          rowId !== undefined &&
          rowId !== null &&
@@ -635,6 +670,8 @@
      outboundBatchOptions.value = [];
      if (!keepSelected) {
        form.stockOutRecordIds = [];
        form.stockOutBindingIds = [];
        selectedOutboundRows.value = [];
        form.amount = 0;
      }
      return Promise.resolve();
@@ -666,6 +703,8 @@
  const handleCustomerChange = customerId => {
    form.stockOutRecordIds = [];
    form.stockOutBindingIds = [];
    selectedOutboundRows.value = [];
    form.outboundBatches = "";
    form.amount = 0;
    loadOutboundBatches(customerId);
@@ -689,12 +728,14 @@
      return;
    }
    form.stockOutRecordIds = dialogOutboundSelection.value
      .map(row => getOutboundRowId(row))
      .map(row => getOutboundOptionValue(row))
      .filter(id => id !== undefined && id !== null);
    form.outboundBatches = dialogOutboundSelection.value
      .map(row => row.outboundBatches ?? row.batchNo ?? row.shippingNo ?? "")
      .filter(Boolean)
      .join("、");
    // ä¿ç•™æ•´è¡Œï¼šæäº¤æ—¶æ‰çŸ¥é“每行是走 stockOutRecordIds è¿˜æ˜¯ stockOutBindingIds
    selectedOutboundRows.value = [...dialogOutboundSelection.value];
    outboundSelectVisible.value = false;
    syncCollectionAmount();
    formRef.value?.validateField("stockOutRecordIds");
@@ -731,6 +772,13 @@
  const buildExportParams = () => appendFilterParams({});
  const buildSubmitPayload = (forUpdate = false) => {
    // æ–°é€‰çš„行按 bindingId æ‹†æˆä¸¤ä¸²ï¼›ç¼–辑时选择弹框没重开,沿用记录里带回来的两串
    const { recordIds, bindingIds } = selectedOutboundRows.value.length
      ? splitOutboundIds(selectedOutboundRows.value)
      : {
          recordIds: (form.stockOutRecordIds || []).map(stripOutboundPrefix),
          bindingIds: (form.stockOutBindingIds || []).map(stripOutboundPrefix),
        };
    const payload = {
      customerId: form.customerId,
      collectionDate: form.receiptDate,
@@ -738,7 +786,8 @@
      collectionMethod: form.receiptMethod,
      collectionNumber: form.receiptCode || "",
      remark: form.remark || "",
      stockOutRecordIds: (form.stockOutRecordIds || []).join(","),
      stockOutRecordIds: recordIds.join(","),
      stockOutBindingIds: bindingIds.join(","),
      createTime: form.createTime,
    };
    if (forUpdate) {
@@ -748,9 +797,10 @@
  };
  const fillFormFromRow = row => {
    // å¸¦å‰ç¼€å›žæ˜¾æ‰èƒ½å’Œå€™é€‰é€‰é¡¹çš„值对上(绑定行与出库单是两个 id ç©ºé—´ï¼‰
    const stockOutRecordIds = parseStockOutRecordIds(
      row.stockOutRecordIds ?? row.stockOutRecordId
    );
    ).map(id => `record-${id}`);
    Object.assign(form, {
      receiptCode: row.receiptCode ?? row.collectionNumber ?? "",
      customerId: row.customerId,
@@ -758,6 +808,10 @@
      amount: Number(row.amount ?? row.collectionAmount ?? 0),
      receiptMethod: row.receiptMethod ?? row.collectionMethod ?? "",
      stockOutRecordIds,
      // ç¼–辑时不重开选择弹框,原样带回去才能保住绑定行关联
      stockOutBindingIds: parseStockOutRecordIds(row.stockOutBindingIds).map(
        id => `binding-${id}`
      ),
      outboundBatches: formatOutboundBatches(row.outboundBatches),
      remark: row.remark ?? "",
      createTime: row.createTime ?? "",
@@ -837,12 +891,14 @@
      amount: 0,
      receiptMethod: getDefaultReceiptMethod(),
      stockOutRecordIds: [],
      stockOutBindingIds: [],
      outboundBatches: "",
      remark: "",
      createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
    });
    outboundBatchList.value = [];
    outboundBatchOptions.value = [];
    selectedOutboundRows.value = [];
    dialogVisible.value = true;
  };
src/views/inventoryManagement/receiptManagement/Record.vue
@@ -118,6 +118,15 @@
                           prop="inboundCategory"
                           min-width="100"
                           show-overflow-tooltip />
          <!-- ä»£å‚¨å…¥åº“绑定的销售订单;老数据未绑定,显示 - -->
          <el-table-column label="销售订单"
                           prop="salesContractNo"
                           min-width="150"
                           show-overflow-tooltip>
            <template #default="scope">
              {{ scope.row.salesContractNo || "-" }}
            </template>
          </el-table-column>
          <el-table-column label="油库"
                           prop="oilDepotName"
                           min-width="130"
src/views/inventoryManagement/stockManagement/OilStockInDialog.vue
@@ -9,31 +9,52 @@
               label-position="top"
               ref="formRef">
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="产品名称"
          <el-col :span="12">
            <el-form-item label="产品大类"
                          prop="productId"
                          :rules="[
                {
                required: true,
                message: '请选择产品大类',
                trigger: 'change',
              }
            ]">
              <el-tree-select v-model="formState.productId"
                              placeholder="请选择"
                              clearable
                              filterable
                              check-strictly
                              @change="handleProductCategoryChange"
                              :data="productOptions"
                              :render-after-expand="false"
                              style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="规格型号"
                          prop="productModelId"
                          :rules="[
                {
                required: true,
                message: '请选择产品',
                message: '请选择产品规格',
                trigger: 'change',
              }
            ]">
              <el-button type="primary"
                         @click="showProductSelectDialog = true">
                {{ formState.productName ? formState.productName : '选择产品' }}
              </el-button>
              <el-select v-model="formState.productModelId"
                         placeholder="请选择"
                         filterable
                         clearable
                         style="width: 100%"
                         @change="handleProductModelChange">
                <el-option v-for="item in modelOptions"
                           :key="item.id"
                           :label="item.model"
                           :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="规格"
                          prop="productModelName">
              <el-input v-model="formState.productModelName"
                        disabled />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="单位"
                          prop="unit">
@@ -87,12 +108,46 @@
              <el-select v-model="formState.inboundCategory"
                         placeholder="请选择"
                         clearable
                         style="width: 100%">
                         style="width: 100%"
                         @change="handleInboundCategoryChange">
                <el-option label="自购入库"
                           value="自购入库" />
                <el-option label="代储入库"
                           value="代储入库" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row v-if="isDepositInbound"
                :gutter="30">
          <el-col :span="12">
            <el-form-item label="销售订单"
                          prop="salesLedgerId"
                          :rules="[
                {
                required: true,
                message: '请选择销售订单',
                trigger: 'change',
              }
            ]">
              <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) }};客户、油库、储罐仍以本表单填写的为准
              </div>
            </el-form-item>
          </el-col>
        </el-row>
@@ -445,10 +500,6 @@
          </el-col>
        </el-row>
      </el-form>
      <!-- äº§å“é€‰æ‹©å¼¹çª— -->
      <ProductSelectDialog v-model="showProductSelectDialog"
                           @confirm="handleProductSelect"
                           single />
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
@@ -461,10 +512,11 @@
</template>
<script setup>
  import { ref, computed, getCurrentInstance, onMounted } from "vue";
  import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
  import { ref, computed, watch, getCurrentInstance, onMounted } from "vue";
  import ImageUpload from "@/components/AttachmentUpload/image/index.vue";
  import { addStockInRecordOnly } from "@/api/inventoryManagement/stockInventory.js";
  import { bindableSalesLedger } from "@/api/inventoryManagement/stockInRecord.js";
  import { modelList, productTreeList } from "@/api/basicData/product.js";
  import { allOilDepot } from "@/api/basicData/oilDepot.js";
  import { allTankInfo } from "@/api/basicData/tankInfo.js";
  import { allVehicle } from "@/api/basicData/vehicle.js";
@@ -487,6 +539,8 @@
    unit: "",
    oilDepotId: undefined,
    inboundCategory: undefined,
    // ä»£å‚¨å…¥åº“必填:绑定的销售台账(销售订单)id
    salesLedgerId: undefined,
    tankId: undefined,
    inboundTime: "",
    oilProduct: undefined,
@@ -516,6 +570,14 @@
  const tankList = ref([]);
  const customerList = ref([]);
  const vehicleList = ref([]);
  // ä»£å‚¨å…¥åº“才需要绑定销售订单;后端下拉只出 ledgerType='代储' çš„台账
  const isDepositInbound = computed(
    () => formState.value.inboundCategory === "代储入库"
  );
  const salesLedgerOptions = ref([]);
  const salesLedgerLoading = ref(false);
  const boundLedgerRow = ref(null);
  const isShow = computed({
    get() {
      return props.visible;
@@ -553,13 +615,77 @@
    return Number((netWeight.value * Number(d)).toFixed(2));
  });
  const showProductSelectDialog = ref(false);
  // å…¥åº“类别改变:切回自购入库时销售订单作废,避免把台账 id å¸¦è¿›ä¸ç»‘定的单子
  const handleInboundCategoryChange = value => {
    if (value !== "代储入库") {
      formState.value.salesLedgerId = undefined;
      boundLedgerRow.value = null;
      return;
    }
    fetchSalesLedgerOptions("");
  };
  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());
  const handleSalesLedgerChange = id => {
    const row = salesLedgerOptions.value.find(item => item.id === id);
    boundLedgerRow.value = row || null;
  };
  // äº§å“é€‰æ‹©ï¼šäº§å“å¤§ç±»æ ‘ + è¯¥å¤§ç±»ä¸‹çš„规格型号,与销售台账选产品的方式一致
  const productOptions = ref([]);
  const modelOptions = ref([]);
  let { proxy } = getCurrentInstance();
  const closeModal = () => {
    proxy.resetForm("formRef");
    formState.value = buildEmptyForm();
    boundLedgerRow.value = null;
    salesLedgerOptions.value = [];
    isShow.value = false;
  };
@@ -597,22 +723,48 @@
    }
  };
  // äº§å“é€‰æ‹©å¤„理
  const handleProductSelect = async products => {
    if (products && products.length > 0) {
      const product = products[0];
      formState.value.productId = product.productId;
      formState.value.productName = product.productName;
      formState.value.productModelName = product.model;
      formState.value.productModelId = product.id;
      formState.value.unit = product.unit;
      // æœ¬ç§Ÿæˆ·ã€Œæ²¹å“ã€å°±æ˜¯äº§å“åç§°ï¼ˆåº“存明细/出库台账的油品列绑的都是 productName),
      // ç›´æŽ¥å–所选产品名,不再去产品树里比对,避免比对不上时油品栏空白
      formState.value.oilProduct = product.productName || undefined;
      showProductSelectDialog.value = false;
      // è§¦å‘表单验证更新
      proxy.$refs["formRef"]?.validateField("productModelId");
  const getProductOptions = () =>
    productTreeList().then(res => {
      productOptions.value = convertIdToValue(res || []);
      return productOptions.value;
    });
  function convertIdToValue(data) {
    return (data || []).map(item => {
      const { id, children, ...rest } = item;
      const newItem = { ...rest, value: id };
      if (children && children.length > 0) {
        newItem.children = convertIdToValue(children);
    }
      return newItem;
    });
  }
  // äº§å“å¤§ç±»æ”¹å˜ï¼šè§„格候选换一批,已选规格作废
  const handleProductCategoryChange = value => {
    formState.value.productModelId = undefined;
    formState.value.productName = "";
    formState.value.productModelName = "";
    formState.value.unit = "";
    formState.value.oilProduct = undefined;
    modelOptions.value = [];
    if (!value) {
      return;
    }
    modelList({ id: value }).then(res => {
      modelOptions.value = res || [];
    });
  };
  // è§„格型号改变:产品名称/规格/单位由规格主数据带出,油品取产品名称
  const handleProductModelChange = value => {
    const item = modelOptions.value.find(row => row.id === value);
    formState.value.productName = item?.productName || "";
    formState.value.productModelName = item?.model || "";
    formState.value.unit = item?.unit || "";
    // æœ¬ç§Ÿæˆ·ã€Œæ²¹å“ã€å°±æ˜¯äº§å“åç§°ï¼ˆåº“存明细/出库台账的油品列绑的都是 productName)
    formState.value.oilProduct = item?.productName || undefined;
    proxy.$refs["formRef"]?.validateField("productModelId");
  };
  const handleSubmit = () => {
@@ -621,6 +773,10 @@
        if (!formState.value.productModelId) {
          proxy.$modal.msgError("请选择产品");
          return;
        }
        // è‡ªè´­å…¥åº“不绑销售订单,字段一并传后端的做法是丢弃,这里直接不传更干净
        if (!isDepositInbound.value) {
          delete formState.value.salesLedgerId;
        }
        // è®¡ç®—结果随表单提交:油品入库固定为合格库存,库存数量取净重
        formState.value.type = "qualified";
@@ -639,7 +795,18 @@
    });
  };
  // æ¯æ¬¡æ‰“开重新拉产品大类,避免首次加载失败或别处新增产品后本页看不到全量数据
  watch(
    () => props.visible,
    visible => {
      if (visible) {
        getProductOptions();
      }
    }
  );
  onMounted(() => {
    getProductOptions();
    allOilDepot().then(res => {
      oilDepotList.value = res?.data || [];
    });
@@ -668,6 +835,17 @@
    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;
src/views/inventoryManagement/stockManagement/OilStockOutDialog.vue
@@ -8,81 +8,9 @@
               :model="formState"
               label-position="top"
               ref="formRef">
        <el-row :gutter="30">
          <el-col :span="24">
            <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>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="产品名称"
                          prop="productModelId"
                          :rules="[
              {
              required: true,
              message: '请选择产品规格',
              trigger: 'change',
            }
          ]">
              <!-- ç»‘定卖油台账后产品只能从该台账明细里选;代储台账没有明细,仍走产品选择弹窗 -->
              <el-select v-if="useLedgerProducts"
                         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>
        <div class="form-section-title">出库信息</div>
        <el-row :gutter="30">
          <el-col :span="8">
          <el-col :span="12">
            <el-form-item label="出库类别"
                          prop="outCategory"
                          :rules="[{ required: true, message: '请选择出库类别', trigger: 'change' }]">
@@ -100,23 +28,7 @@
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="出库批次"
                          prop="batchNo"
                          :rules="[{ required: true, message: '油品出库必须选择出库批次', trigger: 'change' }]">
              <el-select v-model="formState.batchNo"
                         placeholder="请选择"
                         clearable
                         filterable
                         style="width: 100%">
                <el-option v-for="item in batchNoOptions"
                           :key="item"
                           :label="item"
                           :value="item" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="8">
          <el-col :span="12">
            <el-form-item label="储库名称"
                          prop="oilDepotId"
                          :rules="[{ required: true, message: '请选择储库名称', trigger: 'change' }]">
@@ -242,6 +154,174 @@
                        maxlength="11"
                        @input="phoneInput('contactPhone')"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <!-- ä¸€æ¬¡å‡ºåº“可分摊给多个客户:各台账明细都要含本次出库规格,分摊数量之和 = å‡ºåº“吨位 -->
        <div class="form-section-title">销售台账绑定</div>
        <el-row v-for="(ledger, index) in ledgerRows"
                :key="ledger.key"
                :gutter="30">
          <el-col :span="14">
            <el-form-item :label="`销售台账${index + 1}`">
              <el-select v-model="ledger.salesLedgerId"
                         placeholder="不绑定则按原有出库流程"
                         clearable
                         filterable
                         remote
                         popper-class="ledger-select-popper"
                         :remote-method="keyword => searchSalesLedger(keyword, index)"
                         :loading="salesLedgerLoading"
                         style="width: 100%"
                         @change="handleSalesLedgerChange(index)">
                <el-option v-for="item in visibleLedgerOptions(ledger)"
                           :key="item.id"
                           :label="salesLedgerLabel(item)"
                           :value="item.id">
                  <div class="ledger-option">
                    <div class="ledger-option-title">{{ salesLedgerLabel(item) }}</div>
                    <div class="ledger-option-line">产品/规格:{{ ledgerSpecsText(item) || "--" }}</div>
                  </div>
                </el-option>
                <el-option v-if="!visibleLedgerOptions(ledger).length"
                           disabled
                           value="__empty__"
                           :label="emptyLedgerOptionTip" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="本次分摊数量(吨)">
              <el-input-number v-model="ledger.quantity"
                               :min="0"
                               :precision="2"
                               :controls="false"
                               placeholder="请输入数字,最多保留两位小数"
                               style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="2">
            <div class="ledger-row-remove">
              <el-button link
                         type="danger"
                         @click="removeLedgerRow(index)">删除</el-button>
            </div>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <div class="ledger-actions">
              <el-button type="primary"
                         plain
                         @click="addLedgerRow">添加销售台账</el-button>
              <span class="ledger-sum-tip"
                    :class="{ 'is-mismatch': isLedgerSumMismatch }">
                å·²åˆ†æ‘Š {{ ledgerSum }} å¨ / å‡ºåº“吨位 {{ ledgerOutTonnage }} å¨ï¼Œä¸¤è€…必须一致
              </span>
            </div>
            <div v-if="boundLedgerTip"
                 class="bound-ledger-tip">
              {{ boundLedgerTip }}
            </div>
          </el-col>
        </el-row>
        <!-- ç»‘定卖油台账后产品只能从各台账明细的交集里选;代储台账没有明细,走产品大类+规格型号 -->
        <el-row v-if="useLedgerProducts"
                :gutter="30">
          <el-col :span="24">
            <el-form-item label="产品名称"
                          prop="productModelId"
                          :rules="[
              {
              required: true,
              message: '请选择产品规格',
              trigger: 'change',
            }
          ]">
              <el-select 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-form-item>
          </el-col>
        </el-row>
        <el-row v-else
                :gutter="30">
          <el-col :span="12">
            <el-form-item label="产品大类"
                          prop="productId"
                          :rules="[
              {
              required: true,
              message: '请选择产品大类',
              trigger: 'change',
            }
          ]">
              <el-tree-select v-model="formState.productId"
                              placeholder="请选择"
                              clearable
                              filterable
                              check-strictly
                              @change="handleProductCategoryChange"
                              :data="productOptions"
                              :render-after-expand="false"
                              style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="规格型号"
                          prop="productModelId"
                          :rules="[
              {
              required: true,
              message: '请选择产品规格',
              trigger: 'change',
            }
          ]">
              <el-select v-model="formState.productModelId"
                         placeholder="请选择"
                         filterable
                         clearable
                         style="width: 100%"
                         @change="handleProductModelChange">
                <el-option v-for="item in modelOptions"
                           :key="item.id"
                           :label="item.model"
                           :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <!-- æ‰¹æ¬¡å€™é€‰æ˜¯æŒ‰äº§å“è§„格去库存里捞的,所以放在规格后面,选完规格再选批次 -->
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="单位"
                          prop="unit">
              <el-input v-model="formState.unit"
                        disabled />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="出库批次"
                          prop="batchNo"
                          :rules="[{ required: true, message: '油品出库必须选择出库批次', trigger: 'change' }]">
              <el-select v-model="formState.batchNo"
                         placeholder="请选择"
                         clearable
                         filterable
                         style="width: 100%">
                <el-option v-for="item in batchNoOptions"
                           :key="item"
                           :label="item"
                           :value="item" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
@@ -482,16 +562,18 @@
        </div>
      </template>
    </el-dialog>
    <!-- äº§å“é€‰æ‹©å¼¹çª— -->
    <ProductSelectDialog v-model="showProductSelectDialog"
                         @confirm="handleProductSelect"
                         single />
  </div>
</template>
<script setup>
  import { ref, computed, watch, getCurrentInstance, onMounted } from "vue";
  import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
  import {
    ref,
    reactive,
    computed,
    watch,
    getCurrentInstance,
    onMounted,
  } from "vue";
  import ImageUpload from "@/components/AttachmentUpload/image/index.vue";
  import {
    addOilOutRecordOnly,
@@ -502,6 +584,7 @@
    bindableSalesLedger,
  } from "@/api/inventoryManagement/stockOut.js";
  import { productList } from "@/api/salesManagement/salesLedger.js";
  import { modelList, productTreeList } from "@/api/basicData/product.js";
  import { productModelList } from "@/api/basicData/productModel";
  import { allOilDepot } from "@/api/basicData/oilDepot.js";
  import { allTankInfo } from "@/api/basicData/tankInfo.js";
@@ -518,8 +601,6 @@
  const emit = defineEmits(["update:visible", "completed"]);
  const buildEmptyForm = () => ({
    // ç»‘定的销售台账 id;为空表示不绑定,走原有出库流程
    salesLedgerId: undefined,
    productId: undefined,
    productModelId: undefined,
    productName: "",
@@ -561,24 +642,72 @@
  const tankList = ref([]);
  const customerList = ref([]);
  const vehicleList = ref([]);
  const showProductSelectDialog = ref(false);
  // æœªç»‘定卖油台账时走「产品大类树 + è§„格型号」,与销售台账选产品的方式一致
  const productOptions = ref([]);
  const modelOptions = ref([]);
  // å‡ºåº“批次候选取该规格的入库批次
  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);
  // å¤šå°è´¦ç»‘定:一次出库可分摊给多个客户,各台账明细须都含本次出库规格
  let ledgerRowSeed = 0;
  const ledgerRows = ref([]);
  const buildEmptyLedgerRow = () => ({
    // è¡Œçš„稳定标识:删除时不能靠索引定位
    key: ++ledgerRowSeed,
    salesLedgerId: undefined,
    quantity: null,
    ledgerType: "",
    // è¯¥è¡Œçš„下拉候选与选中行:不同行可以搜不同关键词
    options: [],
    raw: null,
    // è¯¥å°è´¦ type=1 æ˜Žç»†çš„ productModelId é›†åˆï¼ˆå­—符串),用于算多台账交集
    detailModelIds: null,
  });
  const boundLedgerRows = computed(() =>
    ledgerRows.value.filter(item => item.salesLedgerId)
  );
  const hasBoundLedger = computed(() => boundLedgerRows.value.length > 0);
  // ä»£å‚¨å°è´¦çš„æ²¹æ˜¯å®¢æˆ·è‡ªå·±çš„:既不产绑定行,也只能单独绑(混绑由提交时校验拦)
  const depositRow = computed(() =>
    boundLedgerRows.value.find(item => item.ledgerType === "代储")
  );
  // ç»‘代储台账:油库/储罐由台账带入,但产品仍要选(代储出库也要扣库存)
  const isStorageBound = computed(() => boundLedgerRow.value?.ledgerType === "代储");
  // åªæœ‰å–油台账有产品明细可挑;代储台账没明细,走产品选择弹窗
  const useLedgerProducts = computed(() => hasBoundLedger.value && !isStorageBound.value);
  const isStorageBound = computed(() => !!depositRow.value);
  // åªæœ‰å–油台账有产品明细可挑;纯代储没明细,走产品大类+规格型号
  const hasOilLedger = computed(() =>
    boundLedgerRows.value.some(item => item.ledgerType !== "代储")
  );
  const useLedgerProducts = computed(() => hasOilLedger.value);
  const ledgerSum = computed(() =>
    Number(
      boundLedgerRows.value
        .reduce((acc, item) => acc + (Number(item.quantity) || 0), 0)
        .toFixed(2)
    )
  );
  const ledgerOutTonnage = computed(
    () => Number(formState.value.outTonnage) || 0
  );
  // å‡ºåº“吨位是分摊的依据,两边不一致后端会直接报错,这里先提示
  const isLedgerSumMismatch = computed(
    () => hasBoundLedger.value && ledgerSum.value !== ledgerOutTonnage.value
  );
  const boundLedgerTip = computed(() => {
    const rows = boundLedgerRows.value;
    if (!rows.length) return "";
    const names = rows.map(item => salesLedgerLabel(item.raw)).join(";");
    return `已绑定:${names};出库类别、客户${
      isStorageBound.value ? "、油库/储罐" : ""
    }以台账为准`;
  });
  const fetchBatchNoOptions = () => {
    batchNoOptions.value = [];
@@ -678,7 +807,7 @@
  const closeModal = () => {
    proxy.resetForm("formRef");
    formState.value = buildEmptyForm();
    boundLedgerRow.value = null;
    ledgerRows.value = [buildEmptyLedgerRow()];
    ledgerProductOptions.value = [];
    isShow.value = false;
  };
@@ -690,24 +819,22 @@
      .slice(0, 11);
  };
  // äº§å“é€‰æ‹©å¤„理
  const handleProductSelect = async products => {
    if (products && products.length > 0) {
      const product = products[0];
      formState.value.productId = product.productId;
      formState.value.productName = product.productName;
      formState.value.model = product.model;
      formState.value.productModelId = product.id;
      formState.value.unit = product.unit;
      // æœ¬ç§Ÿæˆ·ã€Œæ²¹å“ã€å°±æ˜¯äº§å“åç§°ï¼ˆåº“存明细/出库台账的油品列绑的都是 productName),
      // ç›´æŽ¥å–所选产品名,不再去产品树里比对,避免比对不上时油品栏空白
      formState.value.oilProduct = product.productName || undefined;
      formState.value.batchNo = undefined;
      fetchBatchNoOptions();
      showProductSelectDialog.value = false;
      proxy.$refs["formRef"]?.validateField("productModelId");
  const getProductOptions = () =>
    productTreeList().then(res => {
      productOptions.value = convertIdToValue(res || []);
      return productOptions.value;
    });
  function convertIdToValue(data) {
    return (data || []).map(item => {
      const { id, children, ...rest } = item;
      const newItem = { ...rest, value: id };
      if (children && children.length > 0) {
        newItem.children = convertIdToValue(children);
    }
  };
      return newItem;
    });
  }
  // å‚¨åº“选择改变:记录库名-公司名
  const depotChange = id => {
@@ -781,6 +908,24 @@
      .filter(Boolean)
      .join(" / ");
  // å‡ºåº“类别决定能绑哪类台账:销售 â†’ å–油台账,代储 â†’ ä»£å‚¨å°è´¦ï¼Œå®¢å­˜æ²¡æœ‰å¯¹åº”台账
  const visibleLedgerOptions = row => {
    const category = formState.value.outCategory;
    if (!category) return row.options;
    if (category === "客存") return [];
    return row.options.filter(item =>
      category === "代储"
        ? item.ledgerType === "代储"
        : item.ledgerType !== "代储"
    );
  };
  const emptyLedgerOptionTip = computed(() => {
    const category = formState.value.outCategory;
    if (category === "客存") return "客存出库不需要绑定销售台账";
    return category === "代储" ? "没有查到代储台账" : "没有查到销售台账";
  });
  // æœç´¢æ¡†åªæœ‰ä¸€ä¸ªï¼Œè€ŒåŽç«¯ä¸‰ä¸ªå‚数是 AND å…³ç³»ï¼Œæ‰€ä»¥ä¾æ¬¡æŒ‰ åˆåŒå· â†’ å®¢æˆ·å â†’ é¡¹ç›®å è¯•,命中即止
  const LEDGER_SEARCH_FIELDS = ["salesContractNo", "customerName", "projectName"];
@@ -790,7 +935,7 @@
    );
  // ç»‘定下拉:卖油和代储都会返回,不做类型过滤,由用户自己选
  const fetchSalesLedgerOptions = async keyword => {
  const fetchSalesLedgerOptions = async (keyword, row) => {
    salesLedgerLoading.value = true;
    try {
      let records = [];
@@ -805,22 +950,34 @@
        records = await queryLedger({});
      }
      // æœç´¢åŽå·²é€‰ä¸­çš„台账可能不在结果里,补上避免下拉框只显示一个 id
      if (
        boundLedgerRow.value &&
        !records.some(item => item.id === boundLedgerRow.value.id)
      ) {
        records.unshift(boundLedgerRow.value);
      if (row.raw && !records.some(item => item.id === row.raw.id)) {
        records.unshift(row.raw);
      }
      salesLedgerOptions.value = records;
      row.options = records;
      enrichLedgerSpecs(records);
    } catch (e) {
      salesLedgerOptions.value = [];
      row.options = row.raw ? [row.raw] : [];
    } finally {
      salesLedgerLoading.value = false;
    }
  };
  const searchSalesLedger = keyword =>
    fetchSalesLedgerOptions(String(keyword || "").trim());
  const searchSalesLedger = (keyword, index) => {
    const row = ledgerRows.value[index];
    if (!row) return;
    return fetchSalesLedgerOptions(String(keyword || "").trim(), row);
  };
  const addLedgerRow = () => {
    const row = buildEmptyLedgerRow();
    ledgerRows.value.push(row);
    fetchSalesLedgerOptions("", row);
  };
  const removeLedgerRow = index => {
    ledgerRows.value.splice(index, 1);
    syncLedgerDerived();
  };
  // äº§å“è§„格表全量取一次并缓存:台账明细只有 productModelId,名称/型号/单位得从这里补
  const getProductModels = () => {
@@ -836,16 +993,7 @@
      .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 toLedgerProduct = (row, modelMap) => {
        const model = modelMap.get(String(row.productModelId)) || {};
        return {
          productId: row.productId ?? model.productId,
@@ -855,8 +1003,104 @@
          model: model.model || row.specificationModel || "",
          unit: model.unit || row.unit || "",
        };
      });
    });
  };
  // å°è´¦æ˜Žç»†ç¼“存:下拉要显示产品/规格、产品交集也要用,同一个台账只拉一次
  const ledgerDetailCache = reactive(new Map());
  const loadLedgerDetails = ledgerId => {
    const key = String(ledgerId);
    if (ledgerDetailCache.has(key)) {
      return Promise.resolve(ledgerDetailCache.get(key));
    }
    return productList({ salesLedgerId: ledgerId, type: 1 })
      .then(res => {
        const list = Array.isArray(res?.data) ? res.data : [];
        ledgerDetailCache.set(key, list);
        return list;
      })
      .catch(() => null);
  };
  // ä¸‹æ‹‰é‡Œå±•示「产品大类 è§„格型号」,多个合同号不至于长得一模一样挑错
  const ledgerSpecsText = item => {
    const list = ledgerDetailCache.get(String(item.id));
    if (!list || !list.length) return "";
    const texts = [
      ...new Set(
        list
          .map(row =>
            [row.productCategory, row.specificationModel].filter(Boolean).join(" ")
          )
          .filter(Boolean)
      ),
    ];
    if (!texts.length) return "";
    return texts.length > 3
      ? `${texts.slice(0, 3).join("、")} ç­‰${texts.length}项`
      : texts.join("、");
  };
  // å€™é€‰å°è´¦å¯èƒ½ä¸€æ¬¡å‡ åä¸ªï¼Œåˆ†æ‰¹æ‹‰æ˜Žç»†ï¼Œåˆ«æŠŠåŽç«¯æ‰“满
  const enrichLedgerSpecs = async records => {
    const pending = records.filter(item => !ledgerDetailCache.has(String(item.id)));
    const BATCH = 6;
    for (let i = 0; i < pending.length; i += BATCH) {
      await Promise.all(
        pending.slice(i, i + BATCH).map(item => loadLedgerDetails(item.id))
      );
    }
  };
  // æ‰€æœ‰è¢«ç»‘卖油台账的明细都必须含本次出库规格,所以产品候选取各台账明细的交集
  const refreshLedgerProducts = async () => {
    const sellRows = boundLedgerRows.value.filter(
      item => item.ledgerType !== "代储"
    );
    if (!sellRows.length) {
      ledgerProductOptions.value = [];
      clearProduct();
      return;
    }
    const models = await getProductModels();
    const modelMap = new Map((models || []).map(item => [String(item.id), item]));
    let options = null;
    for (const item of sellRows) {
      const detailRows = (await loadLedgerDetails(item.salesLedgerId)) || [];
      const rowOptions = detailRows.map(detail => toLedgerProduct(detail, modelMap));
      const ids = new Set(rowOptions.map(opt => String(opt.productModelId)));
      item.detailModelIds = [...ids];
      options = options === null
        ? rowOptions
        : options.filter(opt => ids.has(String(opt.productModelId)));
    }
    ledgerProductOptions.value = options || [];
    // æ¢è¿‡å°è´¦åŽå·²é€‰è§„格可能不在交集里,作废掉,避免提交时后端报「所选产品不在该台账明细中」
    if (
      formState.value.productModelId &&
      !ledgerProductOptions.value.some(
        opt =>
          String(opt.productModelId) === String(formState.value.productModelId)
      )
    ) {
      clearProduct();
    }
  };
  // å°è´¦é€‰æ‹©å˜åŒ–后重算派生状态:客户取首个台账,产品候选取各台账明细交集
  const syncLedgerDerived = () => {
    const rows = boundLedgerRows.value;
    if (!rows.length) return refreshLedgerProducts();
    applyLedgerCustomer(rows[0].raw);
    // ä»£å‚¨è¡Œçš„æ²¹åº“/储罐由台账带入:代储不在第一行时也要填,否则储库控件被锁住又没值
    const storage = rows.find(item => item.ledgerType === "代储");
    formState.value.outCategory = storage ? "代储" : "销售";
    if (storage) {
      formState.value.oilDepotId = storage.raw.oilDepotId;
      formState.value.oilDepotName = storage.raw.oilDepotName || "";
      formState.value.tankId = storage.raw.tankId;
      formState.value.tankNo = storage.raw.tankNo || "";
    }
    return refreshLedgerProducts();
  };
  const ledgerProductLabel = item =>
@@ -891,40 +1135,103 @@
    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 = [];
  // äº§å“å¤§ç±»æ”¹å˜ï¼šè§„格候选换一批,已选规格作废
  const handleProductCategoryChange = value => {
    clearProduct();
    if (!row) {
      // å–消绑定:出库类别、客户恢复为手选
    formState.value.productId = value || undefined;
    modelOptions.value = [];
    if (!value) {
      return;
    }
    // å‡ºåº“类别与客户以台账为准(后端也会强制覆盖),前端同步展示并置灰
    formState.value.outCategory = row.ledgerType === "代储" ? "代储" : "销售";
    applyLedgerCustomer(row);
    if (row.ledgerType === "代储") {
      formState.value.oilDepotId = row.oilDepotId;
      formState.value.oilDepotName = row.oilDepotName || "";
      formState.value.tankId = row.tankId;
      formState.value.tankNo = row.tankNo || "";
      return;
    modelList({ id: value }).then(res => {
      modelOptions.value = res || [];
    });
  };
  // è§„格型号改变:产品名称/规格/单位由规格主数据带出,油品取产品名称
  const handleProductModelChange = value => {
    const item = modelOptions.value.find(row => row.id === value);
    formState.value.productName = item?.productName || "";
    formState.value.model = item?.model || "";
    formState.value.unit = item?.unit || "";
    formState.value.oilProduct = item?.productName || undefined;
    formState.value.batchNo = undefined;
    fetchBatchNoOptions();
    proxy.$refs["formRef"]?.validateField("productModelId");
  };
  const handleSalesLedgerChange = async index => {
    const row = ledgerRows.value[index];
    if (!row) return;
    // æ¸…空选择时 el-select ä¼šç»™ç©ºä¸²ï¼Œå½’一化成 undefined,避免把空串当台账 id ä¼ åŽç«¯
    const picked = row.options.find(item => item.id === row.salesLedgerId) || null;
    row.raw = picked;
    row.salesLedgerId = picked?.id;
    row.ledgerType = picked?.ledgerType || "";
    row.detailModelIds = null;
    clearProduct();
    await syncLedgerDerived();
  };
  // å°è´¦ç»‘定校验:文案与后端保持一致,能在提交前拦住的就先拦住
  const validateLedgerBinding = () => {
    const rows = boundLedgerRows.value;
    if (!rows.length) return true;
    if (depositRow.value && rows.length > 1) {
      proxy.$modal.msgError(
        "代储台账不能与其他销售台账一起绑定,一次出库只能绑一个代储台账"
      );
      return false;
    }
    fetchLedgerProducts(row.id);
    const zeroRow = rows.find(item => !(Number(item.quantity) > 0));
    if (zeroRow) {
      proxy.$modal.msgError(
        `每个销售台账的绑定数量必须大于0,销售合同号:${
          zeroRow.raw?.salesContractNo || ""
        }`
      );
      return false;
    }
    if (ledgerSum.value !== ledgerOutTonnage.value) {
      proxy.$modal.msgError(
        `各销售台账绑定数量之和必须等于本次出库数量,出库数量:${ledgerOutTonnage.value},绑定数量之和:${ledgerSum.value}`
      );
      return false;
    }
    // é”€å”®/代储/客存都要扣库存,绑了台账就必须选规格(代储台账没明细,从产品弹窗选)
    if (!formState.value.productModelId) {
      proxy.$modal.msgError(
        depositRow.value
          ? `代储出库需要扣减库存,必须选择产品规格,销售合同号:${
              depositRow.value.raw?.salesContractNo || ""
            }`
          : `绑定销售台账时必须选择产品规格,销售合同号:${
              rows[0].raw?.salesContractNo || ""
            }`
      );
      return false;
    }
    const missingRow = rows.find(
      item =>
        item.ledgerType !== "代储" &&
        item.detailModelIds &&
        !item.detailModelIds.includes(String(formState.value.productModelId))
    );
    if (missingRow) {
      proxy.$modal.msgError(
        `所选产品不在该销售台账明细中,销售合同号:${
          missingRow.raw?.salesContractNo || ""
        }`
      );
      return false;
    }
    return true;
  };
  const handleSubmit = () => {
    proxy.$refs["formRef"].validate(valid => {
      if (valid) {
        // é”€å”®/代储/客存都要扣库存,一律必须选规格(代储台账没明细,从产品弹窗选)
        if (!formState.value.productModelId) {
          proxy.$modal.msgError("请选择产品规格");
          return;
        }
        if (!validateLedgerBinding()) return;
        // è®¡ç®—结果随表单提交(出库吨位是手填的,不再覆盖)
        formState.value.netWeight = netWeight.value;
        formState.value.lossQuantity = lossQuantity.value;
@@ -935,6 +1242,11 @@
          ...formState.value,
          loadingPhotos: JSON.stringify(formState.value.loadingPhotos || []),
          poundPhotos: JSON.stringify(formState.value.poundPhotos || []),
          // å–油台账可多绑;空数组是「不绑台账」的明确语义,不能省略
          ledgerList: boundLedgerRows.value.map(item => ({
            salesLedgerId: item.salesLedgerId,
            quantity: Number(item.quantity),
          })),
        };
        addOilOutRecordOnly(payload).then(res => {
@@ -946,18 +1258,24 @@
    });
  };
  // æ¯æ¬¡æ‰“开重新拉台账候选,避免在别处新建台账后本页看不到
  // æ¯æ¬¡æ‰“开重新拉台账候选与产品大类树,避免别处新建后本页看不到
  watch(
    () => props.visible,
    visible => {
      if (visible) {
        fetchSalesLedgerOptions("");
        getProductOptions();
        const row = ledgerRows.value[0];
        if (row) {
          fetchSalesLedgerOptions("", row);
        }
      }
    }
  );
  onMounted(() => {
    fetchSalesLedgerOptions("");
    getProductOptions();
    ledgerRows.value = [buildEmptyLedgerRow()];
    fetchSalesLedgerOptions("", ledgerRows.value[0]);
    allOilDepot().then(res => {
      oilDepotList.value = res?.data || [];
    });
@@ -986,9 +1304,32 @@
    border-radius: 3px;
  }
  // å°è´¦è¡Œå°¾çš„「删除」:与输入框对齐(label-position=top,标签占一行)
  .ledger-row-remove {
    padding-top: 30px;
  }
  // ã€Œæ·»åŠ é”€å”®å°è´¦ã€æŒ‰é’® + åˆ†æ‘Šåˆè®¡æç¤º
  .ledger-actions {
    display: flex;
    align-items: center;
    gap: 16px;
    margin-bottom: 10px;
    .ledger-sum-tip {
      font-size: 13px;
      color: #909399;
      &.is-mismatch {
        color: #f56c6c;
        font-weight: 600;
      }
    }
  }
  // ç»‘定销售台账后的提示条
  .bound-ledger-tip {
    margin-top: 6px;
    margin-bottom: 10px;
    padding: 6px 14px;
    font-size: 13px;
    color: #303133;
@@ -1092,6 +1433,24 @@
      line-height: 1.8;
    }
  }
  // é”€å”®å°è´¦ä¸‹æ‹‰é€‰é¡¹ï¼šå°å¡ç‰‡æ ·å¼
  .ledger-option {
    padding: 8px 10px;
    .ledger-option-title {
      font-size: 14px;
      font-weight: 600;
      color: #303133;
      margin-bottom: 4px;
    }
    .ledger-option-line {
      font-size: 12px;
      color: #909399;
      line-height: 1.8;
    }
  }
</style>
<style lang="scss">
@@ -1174,4 +1533,24 @@
      }
    }
  }
  // é”€å”®å°è´¦ä¸‹æ‹‰é¢æ¿ï¼ˆbody ä¸‹ï¼Œéœ€å…¨å±€æ ·å¼ï¼‰
  .ledger-select-popper {
    .el-select-dropdown__item {
      height: auto;
      line-height: normal;
      padding: 4px 0;
    }
    .ledger-option {
      border: 1px solid #e4e7ed;
      border-radius: 6px;
      background-color: #fafbfc;
      &:hover {
        border-color: #409eff;
        background-color: #f0f7ff;
      }
    }
  }
</style>
src/views/procurementManagement/procurementLedger/index.vue
@@ -19,13 +19,6 @@
                      clearable
                      :prefix-icon="Search" />
          </el-form-item>
          <el-form-item label="销售合同号:">
            <el-input v-model="searchForm.salesContractNo"
                      placeholder="请输入"
                      clearable
                      prefix-icon="Search"
                      @change="handleQuery" />
          </el-form-item>
          <el-form-item label="项目名称:">
            <el-input v-model="searchForm.projectName"
                      placeholder="请输入"
@@ -144,10 +137,6 @@
                         width="60" />
        <el-table-column label="采购合同号"
                         prop="purchaseContractNumber"
                         width="160"
                         show-overflow-tooltip />
        <el-table-column label="销售合同号"
                         prop="salesContractNo"
                         width="160"
                         show-overflow-tooltip />
        <el-table-column label="供应商名称"
@@ -288,21 +277,6 @@
              <el-input v-model="form.purchaseContractNumber"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="销售合同号:"
                          prop="salesLedgerId">
              <el-select v-model="form.salesLedgerId"
                         placeholder="请选择"
                         filterable
                         clearable
                         @change="salesLedgerChange">
                <el-option v-for="item in salesContractList"
                           :key="item.id"
                           :label="item.salesContractNo"
                           :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
@@ -881,7 +855,6 @@
    addOrUpdateSalesLedgerProduct,
    delProduct,
    delLedgerFile,
    getProductInfoByContractNo,
  } from "@/api/salesManagement/salesLedger.js";
  import {
    addOrEditPurchase,
@@ -889,7 +862,6 @@
    updatePurchaseTemplate,
    createPurchaseNo,
    delPurchase,
    getSalesNo,
    purchaseListPage,
    productList,
    getPurchaseById,
@@ -912,7 +884,6 @@
  const modelOptions = ref([]);
  const userList = ref([]);
  const productOptions = ref([]);
  const salesContractList = ref([]);
  const supplierList = ref([]);
  const oilDepotList = ref([]);
  const tableLoading = ref(false);
@@ -1079,7 +1050,6 @@
    searchForm: {
      supplierName: "", // ä¾›åº”商名称
      purchaseContractNumber: "", // é‡‡è´­åˆåŒç¼–号
      salesContractNo: "", // é”€å”®åˆåŒç¼–号
      projectName: "", // é¡¹ç›®åç§°
      entryDate: null, // å½•入日期
      entryDateStart: undefined,
@@ -1087,7 +1057,6 @@
    },
    form: {
      purchaseContractNumber: "",
      salesLedgerId: "",
      projectName: "",
      recorderId: "",
      entryDate: "",
@@ -1499,13 +1468,11 @@
    isTemplateNameDuplicate.value = false;
    try {
      // å¹¶è¡ŒåŠ è½½åŸºç¡€æ•°æ®
      const [salesRes, supplierRes, oilDepotRes] = await Promise.all([
        getSalesNo(),
      const [supplierRes, oilDepotRes] = await Promise.all([
        getOptions(),
        allOilDepot(),
      ]);
      salesContractList.value = salesRes || [];
      // ä¾›åº”商过滤出isWhite=0 çš„æ•°æ®
      supplierList.value = (supplierRes.data || []).filter(
        item => item.isWhite === 0
@@ -1616,11 +1583,6 @@
        }
        form.value.storageBlobDTOS = fileList.value;
        form.value.type = 2;
        // å¦‚æžœsalesLedgerId为空,则不传递salesContractNo
        if (!form.value.salesLedgerId) {
          form.value.salesContractNo = "";
        }
        // æ–°å¢žæ—¶ä¸ä¼ é€’id
        const submitData = { ...form.value };
@@ -2059,25 +2021,6 @@
  const purchaseCategoryChange = val => {
    if (!["代储", "采购直销"].includes(val)) {
      form.value.customerName = "";
    }
  };
  // é”€å”®åˆåŒé€‰æ‹©æ”¹å˜æ–¹æ³•
  const salesLedgerChange = async row => {
    console.log("row", row);
    var index = salesContractList.value.findIndex(item => item.id == row);
    console.log("index", index);
    if (index > -1) {
      await querygProductInfoByContractNo();
    }
  };
  const querygProductInfoByContractNo = async () => {
    const { code, data } = await getProductInfoByContractNo({
      contractNo: form.value.salesLedgerId,
    });
    if (code == 200) {
      productData.value = data || [];
    }
  };
src/views/procurementManagement/purchaseApplication/index.vue
@@ -95,10 +95,6 @@
                         prop="projectName"
                         width="280"
                         show-overflow-tooltip />
        <el-table-column label="销售合同号"
                         prop="salesContractNo"
                         width="170"
                         show-overflow-tooltip />
        <el-table-column label="申请人"
                         prop="applicantName"
                         width="100"
@@ -202,6 +198,16 @@
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="采购合同号:"
                          prop="purchaseContractNumber">
              <el-input v-model="form.purchaseContractNumber"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="供应商名称:"
                          prop="supplierId">
              <el-select v-model="form.supplierId"
@@ -227,22 +233,6 @@
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="销售合同号:"
                          prop="salesLedgerId">
              <el-select v-model="form.salesLedgerId"
                         placeholder="请选择"
                         filterable
                         clearable
                         style="width: 100%"
                         @change="salesLedgerChange">
                <el-option v-for="item in salesContractList"
                           :key="item.id"
                           :label="item.salesContractNo"
                           :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="申请人:"
                          prop="applicantId">
              <el-select v-model="form.applicantId"
@@ -257,9 +247,7 @@
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="8">
          <el-col :span="12">
            <el-form-item label="期望到货日期:"
                          prop="expectedArrivalDate">
              <el-date-picker style="width: 100%"
@@ -271,7 +259,160 @@
                              clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="最佳到站日期:"
                          prop="bestArrivalDate">
              <el-date-picker style="width: 100%"
                              v-model="form.bestArrivalDate"
                              value-format="YYYY-MM-DD"
                              format="YYYY-MM-DD"
                              type="date"
                              placeholder="请选择"
                              clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="付款方式:"
                          prop="paymentMethod">
              <el-input v-model="form.paymentMethod"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="8">
            <el-form-item label="录入人:"
                          prop="recorderId">
              <el-select v-model="form.recorderId"
                         placeholder="请选择"
                         filterable
                         clearable
                         style="width: 100%">
                <el-option v-for="item in userList"
                           :key="item.userId"
                           :label="item.nickName"
                           :value="item.userId" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="签订日期:"
                          prop="executionDate">
              <el-date-picker style="width: 100%"
                              v-model="form.executionDate"
                              value-format="YYYY-MM-DD"
                              format="YYYY-MM-DD"
                              type="date"
                              placeholder="请选择"
                              clearable />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="录入日期:"
                          prop="entryDate">
              <el-date-picker style="width: 100%"
                              v-model="form.entryDate"
                              value-format="YYYY-MM-DD"
                              format="YYYY-MM-DD"
                              type="date"
                              placeholder="请选择"
                              clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <div class="form-section-title">采购入库提交</div>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="申请提交时间:"
                          prop="applicationSubmitTime">
              <el-date-picker style="width: 100%"
                              v-model="form.applicationSubmitTime"
                              value-format="YYYY-MM-DD HH:mm:ss"
                              format="YYYY-MM-DD HH:mm:ss"
                              type="datetime"
                              placeholder="请选择"
                              clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="采购类别:"
                          prop="purchaseCategory">
              <el-radio-group v-model="form.purchaseCategory"
                              @change="purchaseCategoryChange">
                <el-radio value="客存">客存</el-radio>
                <el-radio value="代储">代储</el-radio>
                <el-radio value="采购入库">采购入库</el-radio>
                <el-radio value="采购直销">采购直销</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="物流方式:"
                          prop="logisticsMethod">
              <el-select v-model="form.logisticsMethod"
                         placeholder="请选择"
                         clearable
                         style="width: 100%">
                <el-option label="厂家配送"
                           value="厂家配送" />
                <el-option label="委外物流"
                           value="委外物流" />
                <el-option label="客户自提"
                           value="客户自提" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="提货方式:"
                          prop="pickupMethod">
              <el-radio-group v-model="form.pickupMethod">
                <el-radio value="暂不提货">暂不提货</el-radio>
                <el-radio value="提货">提货</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="采购油库:"
                          prop="oilDepotId">
              <el-select v-model="form.oilDepotId"
                         placeholder="请选择"
                         clearable
                         popper-class="oil-depot-select-popper"
                         style="width: 100%">
                <el-option v-for="item in oilDepotList"
                           :key="item.id"
                           :label="item.depotName"
                           :value="item.id">
                  <div class="oil-depot-option">
                    <div class="oil-depot-option-title">{{ item.depotName }}</div>
                    <div class="oil-depot-option-line">负责人:{{ item.chargePerson }}</div>
                    <div class="oil-depot-option-line">联系电话:{{ item.phone }}</div>
                    <div class="oil-depot-option-line">所属公司:{{ item.company }}</div>
                    <div class="oil-depot-option-line">启用日期:{{ item.enableDate }}</div>
                  </div>
                </el-option>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="客户名称:"
                          prop="customerName">
              <el-input v-model="form.customerName"
                        placeholder="仅采购类别为代储/采购直销时可填写"
                        clearable
                        :disabled="!['代储', '采购直销'].includes(form.purchaseCategory)" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="合同金额(元):"
                          prop="contractAmount">
              <el-input-number v-model="form.contractAmount"
@@ -282,12 +423,34 @@
                               style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="付款方式:"
                          prop="paymentMethod">
              <el-input v-model="form.paymentMethod"
          <el-col :span="12">
            <el-form-item label="采购数量(吨):"
                          prop="purchaseQuantity">
              <el-input-number v-model="form.purchaseQuantity"
                               :precision="0"
                               :step="1"
                               :min="0"
                        placeholder="请输入"
                        clearable />
                               style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>
        <div class="form-section-title">系统信息</div>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="采购申请订单状态:"
                          prop="purchaseOrderStatus">
              <el-select v-model="form.purchaseOrderStatus"
                         placeholder="请选择"
                         clearable
                         style="width: 100%">
                <el-option label="已申请,未装车"
                           value="已申请,未装车" />
                <el-option label="以出厂,未入库"
                           value="以出厂,未入库" />
                <el-option label="已入库"
                           value="已入库" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
@@ -327,6 +490,10 @@
          <el-table-column label="数量"
                           prop="quantity"
                           width="80" />
          <el-table-column label="库存预警数量"
                           prop="warnNum"
                           width="120"
                           show-overflow-tooltip />
          <el-table-column label="税率(%)"
                           prop="taxRate"
                           width="80" />
@@ -381,6 +548,14 @@
                        clearable
                        type="textarea"
                        :rows="2" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="附件材料:"
                          prop="storageBlobDTOS">
              <FileUpload v-model:file-list="fileList" />
            </el-form-item>
          </el-col>
        </el-row>
@@ -518,6 +693,19 @@
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="库存预警数量:"
                          prop="warnNum">
              <el-input-number v-model="productForm.warnNum"
                               :precision="2"
                               :step="0.1"
                               :min="0"
                               clearable
                               style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="是否质检:"
                          prop="isChecked">
              <el-radio-group v-model="productForm.isChecked">
@@ -569,12 +757,11 @@
    delApplication,
    convertToOrder,
  } from "@/api/procurementManagement/purchaseApplication.js";
  import {
    getSalesNo,
    getOptions,
  } from "@/api/procurementManagement/procurementLedger.js";
  import { getOptions } from "@/api/procurementManagement/procurementLedger.js";
  import { modelList, productTreeList } from "@/api/basicData/product.js";
  import { allOilDepot } from "@/api/basicData/oilDepot.js";
  import { userListNoPage } from "@/api/system/user.js";
  import FileUpload from "@/components/AttachmentUpload/file/index.vue";
  const { proxy } = getCurrentInstance();
  const { tax_rate } = proxy.useDict("tax_rate");
@@ -591,8 +778,9 @@
  const productSelectedRows = ref([]);
  const supplierOptions = ref([]);
  const supplierList = ref([]);
  const salesContractList = ref([]);
  const userList = ref([]);
  const oilDepotList = ref([]);
  const fileList = ref([]);
  const productOptions = ref([]);
  const modelOptions = ref([]);
  const tableLoading = ref(false);
@@ -626,19 +814,46 @@
    form: {
      id: "",
      applicationNo: "",
      // ä»¥ä¸‹å­—段与「新增采购台账」保持一致
      purchaseContractNumber: "",
      supplierId: "",
      projectName: "",
      salesContractNo: "",
      salesLedgerId: "",
      applicantId: "",
      applyDate: "",
      expectedArrivalDate: "",
      contractAmount: "",
      bestArrivalDate: "",
      paymentMethod: "",
      recorderId: "",
      executionDate: "",
      entryDate: "",
      applicationSubmitTime: "",
      purchaseCategory: "",
      logisticsMethod: "",
      pickupMethod: "",
      oilDepotId: "",
      customerName: "",
      purchaseQuantity: "",
      purchaseOrderStatus: "",
      contractAmount: "",
      remarks: "",
    },
    rules: {
      applyDate: [{ required: true, message: "请选择", trigger: "change" }],
      projectName: [
        { required: true, message: "请输入项目名称", trigger: "blur" },
      ],
      supplierId: [{ required: true, message: "请输入", trigger: "blur" }],
      entryDate: [{ required: true, message: "请选择", trigger: "change" }],
      executionDate: [{ required: true, message: "请选择", trigger: "change" }],
      purchaseCategory: [
        { required: true, message: "请选择采购类别", trigger: "change" },
      ],
      oilDepotId: [
        { required: true, message: "请选择采购油库", trigger: "change" },
      ],
      purchaseQuantity: [
        { required: true, message: "请输入采购数量", trigger: "change" },
      ],
    },
  });
  const { form, rules } = toRefs(data);
@@ -656,6 +871,7 @@
      taxInclusiveTotalPrice: "",
      taxExclusiveTotalPrice: "",
      invoiceType: "",
      warnNum: "",
      isChecked: false,
      remark: "",
    },
@@ -668,6 +884,7 @@
      taxInclusiveUnitPrice: [
        { required: true, message: "请输入", trigger: "blur" },
      ],
      warnNum: [{ required: true, message: "请选择", trigger: "change" }],
    },
  });
  const { productForm, productRules } = toRefs(productFormData);
@@ -747,14 +964,14 @@
    supplierList.value = list.filter(item => item.isWhite === 0);
  };
  const getSalesContractList = async () => {
    const res = await getSalesNo();
    salesContractList.value = Array.isArray(res) ? res : res?.data || [];
  };
  const getUserList = async () => {
    const res = await userListNoPage();
    userList.value = res.data || [];
  };
  const getOilDepotList = async () => {
    const res = await allOilDepot();
    oilDepotList.value = res?.data || [];
  };
  const getProductOptions = async () => {
@@ -905,9 +1122,11 @@
    productFormVisible.value = false;
  };
  const salesLedgerChange = value => {
    const matched = salesContractList.value.find(item => item.id === value);
    form.value.salesContractNo = matched ? matched.salesContractNo : "";
  // é‡‡è´­ç±»åˆ«æ”¹å˜ï¼šä»…代储/采购直销时客户名称可用,切换后清空
  const purchaseCategoryChange = val => {
    if (!["代储", "采购直销"].includes(val)) {
      form.value.customerName = "";
    }
  };
  const openForm = async (type, row) => {
@@ -917,26 +1136,38 @@
    form.value = {
      id: "",
      applicationNo: "",
      purchaseContractNumber: "",
      supplierId: "",
      projectName: "",
      salesContractNo: "",
      salesLedgerId: "",
      applicantId: "",
      applyDate: dayjs().format("YYYY-MM-DD"),
      expectedArrivalDate: "",
      contractAmount: "",
      bestArrivalDate: "",
      paymentMethod: "",
      recorderId: "",
      executionDate: "",
      entryDate: dayjs().format("YYYY-MM-DD"),
      applicationSubmitTime: "",
      purchaseCategory: "",
      logisticsMethod: "",
      pickupMethod: "",
      oilDepotId: "",
      customerName: "",
      purchaseQuantity: "",
      purchaseOrderStatus: "",
      contractAmount: "",
      remarks: "",
    };
    productData.value = [];
    productSelectedRows.value = [];
    fileList.value = [];
    try {
      await Promise.all([
        getSupplierOptions(),
        getSalesContractList(),
        getUserList(),
        getProductOptions(),
        getOilDepotList(),
      ]);
    } catch (error) {
      proxy.$modal.msgError("加载基础数据失败");
@@ -952,6 +1183,7 @@
      const res = await getApplicationById({ id: row.id });
      form.value = { ...(res.data || {}), id: row.id };
      productData.value = res.data?.productData || [];
      fileList.value = res.data?.storageBlobVOS || [];
    } catch (error) {
      proxy.$modal.msgError("加载采购申请详情失败");
      return;
@@ -969,14 +1201,28 @@
    const payload = {
      supplierId: form.value.supplierId || null,
      projectName: form.value.projectName,
      salesContractNo: form.value.salesContractNo,
      salesLedgerId: form.value.salesLedgerId || null,
      applyDate: form.value.applyDate,
      expectedArrivalDate: form.value.expectedArrivalDate || null,
      contractAmount:
        form.value.contractAmount === "" ? null : form.value.contractAmount,
      paymentMethod: form.value.paymentMethod,
      remarks: form.value.remarks,
      // ä¸Žã€Œæ–°å¢žé‡‡è´­å°è´¦ã€å¯¹é½çš„字段
      purchaseContractNumber: form.value.purchaseContractNumber,
      bestArrivalDate: form.value.bestArrivalDate || null,
      recorderId: form.value.recorderId || null,
      executionDate: form.value.executionDate || null,
      entryDate: form.value.entryDate || null,
      applicationSubmitTime: form.value.applicationSubmitTime || null,
      purchaseCategory: form.value.purchaseCategory,
      logisticsMethod: form.value.logisticsMethod,
      pickupMethod: form.value.pickupMethod,
      oilDepotId: form.value.oilDepotId || null,
      customerName: form.value.customerName,
      purchaseQuantity:
        form.value.purchaseQuantity === "" ? null : form.value.purchaseQuantity,
      purchaseOrderStatus: form.value.purchaseOrderStatus,
      storageBlobDTOS: fileList.value,
      productData: productData.value.map(
        ({ id, purchaseApplicationId, ...rest }) => rest
      ),
@@ -1030,8 +1276,9 @@
      .catch(() => {});
  };
  const handleConvert = row => {
    ElMessageBox.confirm(
  const handleConvert = async row => {
    try {
      await ElMessageBox.confirm(
      `确认将申请单「${row.applicationNo}」转为采购订单吗?转订单后将自动提交采购审批。`,
      "转订单",
      {
@@ -1039,40 +1286,59 @@
        cancelButtonText: "取消",
        type: "warning",
      }
    )
      .then(async () => {
      );
    } catch {
      return;
    }
    try {
        const res = await convertToOrder(row.id);
        getList();
        ElMessageBox.confirm(
          `转订单成功,已生成采购台账(id:${res.data})。是否前往采购台账查看?`,
      await ElMessageBox.confirm(
        `转订单成功,已生成采购台账(id:${res?.data})。是否前往采购台账查看?`,
          "提示",
          {
            confirmButtonText: "前往查看",
            cancelButtonText: "留在本页",
            type: "success",
          }
        )
          .then(() => {
            const path = findLedgerPath(permissionStore.sidebarRouters);
            if (path) {
              router.push(path);
            } else {
              ElMessage.info("请前往「采购台账」查看生成的订单");
      );
      await gotoLedger();
    } catch {
      // è½¬è®¢å•失败由请求拦截器提示;点「留在本页」也走这里,不额外处理
            }
          })
          .catch(() => {});
      })
      .catch(() => {});
  };
  // ä»Žä¾§è¾¹æ èœå•中定位采购台账路由,找不到时不跳转仅提示
  // è·³è½¬é‡‡è´­å°è´¦ã€‚路由匹配不上或 push è¢«æ‹¦æˆªéƒ½ç»™å‡ºå¯è§æç¤ºï¼Œä¸é™é»˜
  const gotoLedger = async () => {
    const path = resolveLedgerPath();
    try {
      await router.push(path);
    } catch {
      ElMessage.warning(`跳转采购台账失败(${path}),请从侧边栏进入查看`);
    }
  };
  // èœå•标题可能与页面标题有空格差异,归一化后再比
  const normalizeMenuTitle = title => String(title ?? "").replace(/\s+/g, "").trim();
  const resolveLedgerPath = () => {
    const path =
      findLedgerPath(permissionStore.defaultRoutes) ||
      findLedgerPath(permissionStore.sidebarRouters);
    // æ‰¾ä¸åˆ°èœå•时兜底到仓库里多处使用的固定地址(采购管理首页卡片、首页快捷入口)
    return path || "/procurementManagement/procurementLedger";
  };
  // ä»Žä¾§è¾¹æ èœå•中定位采购台账路由
  const findLedgerPath = (routes, prefix = "") => {
    for (const route of routes || []) {
      if (!route) continue;
      const fullPath = String(route.path || "").startsWith("/")
        ? route.path
        : `${prefix}/${route.path || ""}`;
      if (route.meta?.title === "采购台账") {
        return fullPath;
      if (normalizeMenuTitle(route.meta?.title ?? route.title) === "采购台账") {
        return fullPath.replace(/\/{2,}/g, "/").replace(/\/$/, "");
      }
      const found = findLedgerPath(route.children, fullPath);
      if (found) return found;
@@ -1085,3 +1351,57 @@
    getSupplierOptions();
  });
</script>
<style scoped lang="scss">
  // è¡¨å•分栏标题:蓝色竖条 + æµ…蓝底色,突出分区
  .form-section-title {
    margin: 18px 0 14px;
    padding: 8px 14px;
    font-size: 15px;
    font-weight: 600;
    color: #1d6fd8;
    background: linear-gradient(90deg, rgba(64, 158, 255, 0.14), rgba(64, 158, 255, 0.02));
    border-left: 4px solid #409eff;
    border-radius: 3px;
  }
  // é‡‡è´­æ²¹åº“下拉选项:小卡片样式
  .oil-depot-option {
    padding: 8px 10px;
    .oil-depot-option-title {
      font-size: 14px;
      font-weight: 600;
      color: #303133;
      margin-bottom: 4px;
    }
    .oil-depot-option-line {
      font-size: 12px;
      color: #909399;
      line-height: 1.8;
    }
  }
</style>
<style lang="scss">
  // ä¸‹æ‹‰é¢æ¿æ¸²æŸ“在 body ä¸‹ï¼Œéœ€å…¨å±€æ ·å¼
  .oil-depot-select-popper {
    .el-select-dropdown__item {
      height: auto;
      line-height: normal;
      padding: 4px 0;
    }
    .oil-depot-option {
      border: 1px solid #e4e7ed;
      border-radius: 6px;
      background-color: #fafbfc;
      &:hover {
        border-color: #409eff;
        background-color: #f0f7ff;
      }
    }
  }
</style>
src/views/salesManagement/deliveryLedger/index.vue
@@ -45,12 +45,18 @@
          <el-button type="danger" plain @click="handleDelete">删除</el-button>
        </div>
      </div>
      <!-- æ²¹å“å‡ºåº“父行的 children æ˜¯åˆ°è´§è®°å½•,靠 row-key + children æ¸²æŸ“成子行 -->
      <el-table
        :data="tableData"
        border
        v-loading="tableLoading"
        @selection-change="handleSelectionChange"
        :row-key="(row) => `${row.source || 'shipping'}-${row.stockOutRecordId ?? row.id}`"
        :row-key="
          (row) =>
            row.source
              ? `${row.source}-${row.stockOutRecordId ?? row.id}`
              : `arrival-${row.id}`
        "
        style="width: 100%"
        height="calc(100vh - 21.5em)"
      >
@@ -63,11 +69,8 @@
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column label="来源" prop="source" align="center" width="100">
          <template #default="scope">
            <el-tag
              :type="isOilOutRow(scope.row) ? 'success' : 'info'"
              size="small"
            >
              {{ isOilOutRow(scope.row) ? "油品出库" : "发货单" }}
            <el-tag :type="sourceTagType(scope.row)" size="small">
              {{ sourceLabel(scope.row) }}
            </el-tag>
          </template>
        </el-table-column>
@@ -128,21 +131,40 @@
          prop="oilDepotName"
          show-overflow-tooltip
        />
        <el-table-column
          label="发货时间"
          prop="shippingDate"
          show-overflow-tooltip
        <!-- åˆ°è´§å­è¡Œçš„对应列落在到货记录字段上,跟父行共用表头 -->
        <el-table-column label="发货时间" show-overflow-tooltip>
          <template #default="scope">
            {{ scope.row.shippingDate || scope.row.arrivalDate || "--" }}
          </template>
        </el-table-column>
        <el-table-column label="发货数量" show-overflow-tooltip>
          <template #default="scope">
            {{ scope.row.totalQuantity ?? scope.row.arrivalQuantity ?? "--" }}
          </template>
        </el-table-column>
        <el-table-column label="发货车牌号" show-overflow-tooltip>
          <template #default="scope">
            {{ scope.row.shippingCarNumber || scope.row.truckPlateNo || "--" }}
          </template>
        </el-table-column>
        <el-table-column label="备注" show-overflow-tooltip>
          <template #default="scope">
            {{ scope.row.remark || "--" }}
          </template>
        </el-table-column>
        <el-table-column label="附件" align="center" width="90">
          <template #default="scope">
            <el-image
              v-if="toAttachmentList(scope.row.attachments).length"
              :src="toAttachmentList(scope.row.attachments)[0]"
              :preview-src-list="toAttachmentList(scope.row.attachments)"
              preview-teleported
              fit="cover"
              style="width: 40px; height: 40px; border-radius: 4px"
        />
        <el-table-column
          label="发货数量"
          prop="totalQuantity"
          show-overflow-tooltip
        />
        <el-table-column
          label="发货车牌号"
          prop="shippingCarNumber"
          show-overflow-tooltip
        />
            <span v-else>--</span>
          </template>
        </el-table-column>
        <el-table-column
          label="司机姓名"
          prop="driverName"
@@ -175,6 +197,7 @@
            <!--                @click="openForm('edit', scope.row)">发货-->
            <!--            </el-button>-->
            <el-button
              v-if="!isArrivalRow(scope.row)"
              link
              type="primary"
              style="color: #67c23a"
@@ -339,6 +362,35 @@
            align="center"
          />
        </el-table>
        <!-- æ²¹å“å‡ºåº“父行的到货记录(子行) -->
        <template v-if="detailArrivalList.length">
          <div class="detail-arrival-title">到货记录</div>
          <el-table
            :data="detailArrivalList"
            border
            size="small"
            style="width: 100%"
          >
            <el-table-column label="车牌号" prop="truckPlateNo" min-width="120" />
            <el-table-column label="客户名称" prop="customerName" min-width="160" show-overflow-tooltip />
            <el-table-column label="到货数量(吨)" prop="arrivalQuantity" min-width="110" align="center" />
            <el-table-column label="到货日期" prop="arrivalDate" min-width="120" />
            <el-table-column label="附件" align="center" min-width="90">
              <template #default="scope">
                <el-image
                  v-if="toAttachmentList(scope.row.attachments).length"
                  :src="toAttachmentList(scope.row.attachments)[0]"
                  :preview-src-list="toAttachmentList(scope.row.attachments)"
                  preview-teleported
                  fit="cover"
                  style="width: 40px; height: 40px; border-radius: 4px"
                />
                <span v-else>--</span>
              </template>
            </el-table-column>
            <el-table-column label="备注" prop="remark" min-width="140" show-overflow-tooltip />
          </el-table>
        </template>
        <ImagePreview :file-list="detailRow.storageBlobVOs || []" />
      </div>
      <template #footer>
@@ -379,6 +431,30 @@
const detailDialogVisible = ref(false);
const detailRow = ref(null);
const detailProductList = ref([]);
// æ²¹å“å‡ºåº“父行的到货记录(子行)
const detailArrivalList = ref([]);
// åˆ°è´§é™„件后端存 JSON å­—符串,出参可能是数组也可能是串
const toAttachmentList = (value) => {
  if (!value) return [];
  const list = Array.isArray(value)
    ? value
    : typeof value === "string"
    ? (() => {
        try {
          const parsed = JSON.parse(value);
          return Array.isArray(parsed) ? parsed : [];
        } catch (e) {
          return [];
        }
      })()
    : [];
  return list
    .map((item) =>
      typeof item === "string" ? item : item?.previewURL || item?.url || ""
    )
    .filter(Boolean);
};
// ç”¨æˆ·ä¿¡æ¯è¡¨å•弹框数据
const operationType = ref("");
@@ -440,11 +516,21 @@
// æ²¹å“å‡ºåº“行来自 stock_out_record,id ä¸Ž shipping_info ä¼šæ’žå·ï¼Œ
// å‘è´§/删除/详情等接口背后都是 shipping_info,不能用于这类行
const isOilOutRow = (row) => row?.source === "oilOut";
const isRowSelectable = (row) => !isOilOutRow(row);
// å±‚级展示时到货记录挂在父行的 children ä¸Šï¼Œå­è¡Œæ²¡æœ‰ source
const isArrivalRow = (row) => !row?.source;
const sourceLabel = (row) => {
  if (isArrivalRow(row)) return "到货";
  return isOilOutRow(row) ? "油品出库" : "发货单";
};
const sourceTagType = (row) => {
  if (isArrivalRow(row)) return "warning";
  return isOilOutRow(row) ? "success" : "info";
};
const isRowSelectable = (row) => !isOilOutRow(row) && !isArrivalRow(row);
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  selectedRows.value = selection.filter((item) => !isOilOutRow(item));
  selectedRows.value = selection.filter(item => isRowSelectable(item));
};
// æ‰“开弹框
@@ -474,6 +560,7 @@
const openDetail = async (row) => {
  detailRow.value = row || null;
  detailProductList.value = [];
  detailArrivalList.value = Array.isArray(row?.children) ? row.children : [];
  detailDialogVisible.value = true;
  // æ²¹å“å‡ºåº“行不走发货单详情接口(id ä¼šæ’žå·ï¼‰ï¼Œç›´æŽ¥ç”¨åˆ—表行数据展示
  if (!row?.id || isOilOutRow(row)) return;
@@ -561,6 +648,7 @@
  detailDialogVisible.value = false;
  detailRow.value = null;
  detailProductList.value = [];
  detailArrivalList.value = [];
};
// æäº¤è¡¨å•
@@ -849,6 +937,12 @@
  padding: 8px 0;
}
.detail-arrival-title {
  margin: 16px 0 10px;
  font-weight: 600;
  color: #303133;
}
.detail-images {
  margin-top: 16px;
}
src/views/salesManagement/salesLedger/components/ArrivalDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,510 @@
<template>
  <el-dialog v-model="isShow"
             title="到货记录"
             width="70%"
             @close="closeModal">
    <div v-if="ledgerLabel"
         class="ledger-tip">
      é”€å”®å°è´¦ï¼š{{ ledgerLabel }}
    </div>
    <div class="actions">
      <el-button type="primary"
                 @click="openForm()">新增到货</el-button>
    </div>
    <el-table :data="tableData"
              border
              v-loading="tableLoading"
              style="width: 100%">
      <el-table-column align="center"
                       label="序号"
                       type="index"
                       width="60" />
      <el-table-column label="出库单号"
                       prop="outboundBatches"
                       min-width="150"
                       show-overflow-tooltip />
      <el-table-column label="车牌号"
                       prop="truckPlateNo"
                       min-width="110"
                       show-overflow-tooltip />
      <el-table-column label="到货数量(吨)"
                       prop="arrivalQuantity"
                       min-width="110"
                       align="center" />
      <el-table-column label="到货日期"
                       prop="arrivalDate"
                       min-width="120"
                       show-overflow-tooltip />
      <el-table-column label="附件"
                       align="center"
                       min-width="120">
        <template #default="scope">
          <el-image v-if="toPhotoArray(scope.row.attachments).length"
                    :src="toPhotoArray(scope.row.attachments)[0]"
                    :preview-src-list="toPhotoArray(scope.row.attachments)"
                    preview-teleported
                    fit="cover"
                    style="width: 40px; height: 40px; border-radius: 4px" />
          <span v-else>--</span>
        </template>
      </el-table-column>
      <el-table-column label="备注"
                       prop="remark"
                       min-width="140"
                       show-overflow-tooltip />
      <el-table-column fixed="right"
                       label="操作"
                       width="140"
                       align="center">
        <template #default="scope">
          <el-button link
                     type="primary"
                     @click="openForm(scope.row)">编辑
          </el-button>
          <el-button link
                     type="danger"
                     @click="handleDelete(scope.row)">删除
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <pagination v-show="total > 0"
                :total="total"
                layout="total, sizes, prev, pager, next, jumper"
                :page="page.current"
                :limit="page.size"
                @pagination="paginationChange" />
    <!-- æ–°å¢ž / ç¼–辑到货 -->
    <el-dialog v-model="formVisible"
               :title="form.id ? '编辑到货记录' : '新增到货记录'"
               width="55%"
               append-to-body
               @close="closeForm">
      <el-form ref="formRef"
               :model="form"
               label-width="140px"
               label-position="top">
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="出库单号"
                          prop="stockOutRecordId"
                          :rules="[
                {
                required: true,
                message: '请选择出库单',
                trigger: 'change',
              }
              ]">
              <!-- å‡ºåº“单 + å°è´¦æ˜¯è¿™æ¡è®°å½•的身份,编辑时不允许改 -->
              <el-select v-model="form.stockOutRecordId"
                         placeholder="请选择出库单"
                         clearable
                         filterable
                         :disabled="!!form.id"
                         style="width: 100%"
                         @change="handleOutboundChange">
                <el-option v-for="item in outboundOptions"
                           :key="item.stockOutRecordId"
                           :label="outboundLabel(item)"
                           :value="item.stockOutRecordId" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="车辆"
                          prop="vehicleId"
                          :rules="[
                {
                required: true,
                message: '请选择车辆',
                trigger: 'change',
              }
              ]">
              <el-select v-model="form.vehicleId"
                         placeholder="请先选择出库单"
                         clearable
                         filterable
                         :disabled="!form.stockOutRecordId"
                         style="width: 100%"
                         @change="handleVehicleChange">
                <el-option v-for="item in vehicleOptions"
                           :key="item.vehicleId"
                           :label="item.truckPlateNo"
                           :value="item.vehicleId" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="到货数量(吨)"
                          prop="arrivalQuantity"
                          :rules="[
                {
                required: true,
                message: '请输入到货数量',
                trigger: 'blur',
              }
              ]">
              <el-input-number v-model="form.arrivalQuantity"
                               :min="0"
                               :max="arrivalMax"
                               :precision="2"
                               :controls="false"
                               placeholder="请输入数字,最多保留两位小数"
                               style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="到货日期"
                          prop="arrivalDate">
              <el-date-picker v-model="form.arrivalDate"
                              type="date"
                              value-format="YYYY-MM-DD"
                              format="YYYY-MM-DD"
                              placeholder="请选择"
                              style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="附件">
              <!-- åˆ°è´§é™„件要永久可看,必须走 /common/public/upload(previewURL) -->
              <ImageUpload v-model:file-list="form.attachmentList"
                           :permanent="true"
                           :limit="9" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="备注">
              <el-input v-model="form.remark"
                        type="textarea"
                        :rows="3"
                        maxlength="500"
                        show-word-limit
                        placeholder="请输入" />
            </el-form-item>
          </el-col>
        </el-row>
        <div v-if="arrivalTip"
             class="arrival-tip">
          {{ arrivalTip }}
        </div>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
                     @click="submitForm">确认</el-button>
          <el-button @click="closeForm">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </el-dialog>
</template>
<script setup>
  import { ref, reactive, computed, watch, getCurrentInstance, toRefs } from "vue";
  import { ElMessageBox } from "element-plus";
  import pagination from "@/components/PIMTable/Pagination.vue";
  import ImageUpload from "@/components/AttachmentUpload/image/index.vue";
  import {
    salesLedgerArrivalListPage,
    bindableArrivalVehicles,
    addSalesLedgerArrival,
    updateSalesLedgerArrival,
    delSalesLedgerArrival,
  } from "@/api/salesManagement/salesLedgerArrival.js";
  const props = defineProps({
    visible: {
      type: Boolean,
      required: true,
    },
    // å½“前销售台账 id:到货记录挂在「出库单 Ã— å°è´¦ã€ä¸Šï¼Œå°è´¦ç”±å¤–面带进来
    ledgerId: {
      type: [String, Number],
      default: undefined,
    },
    ledgerLabel: {
      type: String,
      default: "",
    },
  });
  const emit = defineEmits(["update:visible", "changed"]);
  const { proxy } = getCurrentInstance();
  const isShow = computed({
    get() {
      return props.visible;
    },
    set(val) {
      emit("update:visible", val);
    },
  });
  const tableData = ref([]);
  const tableLoading = ref(false);
  const total = ref(0);
  const page = reactive({
    current: 1,
    size: 10,
  });
  // å¯åˆ°è´§çš„「出库单 + è½¦è¾†ã€å€™é€‰ï¼šä¸€æ¬¡æ‹‰å›žæ¥ï¼Œå‡ºåº“单去重、车辆按出库单联动
  const bindableRows = ref([]);
  const formVisible = ref(false);
  const formRef = ref(null);
  const buildEmptyForm = () => ({
    id: null,
    stockOutRecordId: undefined,
    vehicleId: undefined,
    truckPlateNo: "",
    arrivalQuantity: null,
    arrivalDate: "",
    attachmentList: [],
    remark: "",
  });
  const form = ref(buildEmptyForm());
  // å‡ºåº“单候选:同一个 CK å·åªå‡ºä¸€ä¸ªé€‰é¡¹
  const outboundOptions = computed(() => {
    const map = new Map();
    bindableRows.value.forEach(item => {
      if (!item.stockOutRecordId || map.has(item.stockOutRecordId)) {
        return;
      }
      map.set(item.stockOutRecordId, item);
    });
    return [...map.values()];
  });
  const vehicleOptions = computed(() =>
    bindableRows.value.filter(
      item => item.stockOutRecordId === form.value.stockOutRecordId
    )
  );
  const selectedOutbound = computed(() =>
    outboundOptions.value.find(
      item => item.stockOutRecordId === form.value.stockOutRecordId
    )
  );
  const outboundLabel = item =>
    [item.outboundBatches, `已到货 ${item.arrivedQuantity ?? 0} / ${item.outQuantity ?? 0} å¨`]
      .filter(Boolean)
      .join("|");
  // ç¼–辑时本条已占的量要从「已到货」里加回来,否则数量改不大于原值
  const originalArrivalQuantity = ref(0);
  // åˆ°è´§ä¸Šé™ = è¯¥å°è´¦åœ¨è¿™å¼ å‡ºåº“单上分摊到的数量
  const arrivalMax = computed(() => {
    const base = selectedOutbound.value;
    if (!base) return undefined;
    const remain =
      (Number(base.outQuantity) || 0) - (Number(base.arrivedQuantity) || 0);
    return Number((remain + originalArrivalQuantity.value).toFixed(2));
  });
  const arrivalTip = computed(() => {
    const base = selectedOutbound.value;
    if (!base) return "";
    return `该台账在这张出库单上分摊 ${base.outQuantity ?? 0} å¨ï¼Œå·²åˆ°è´§ ${
      base.arrivedQuantity ?? 0
    } å¨ï¼Œæœ¬æ¬¡æœ€å¤šå¯ç™»è®° ${arrivalMax.value} å¨`;
  });
  const getList = () => {
    if (!props.ledgerId) {
      tableData.value = [];
      total.value = 0;
      return;
    }
    tableLoading.value = true;
    salesLedgerArrivalListPage({
      salesLedgerId: props.ledgerId,
      ...page,
    })
      .then(res => {
        tableData.value = res.data?.records || [];
        total.value = res.data?.total || 0;
      })
      .finally(() => {
        tableLoading.value = false;
      });
  };
  const paginationChange = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  const loadBindableRows = () => {
    if (!props.ledgerId) {
      bindableRows.value = [];
      return Promise.resolve([]);
    }
    return bindableArrivalVehicles({ salesLedgerId: props.ledgerId })
      .then(res => {
        bindableRows.value = Array.isArray(res?.data) ? res.data : [];
        return bindableRows.value;
      })
      .catch(() => {
        bindableRows.value = [];
        return [];
      });
  };
  // é™„件后端存 JSON å­—符串,出参可能是数组也可能是串
  const toPhotoArray = value => {
    if (!value) return [];
    if (Array.isArray(value)) {
      return value.map(getPhotoUrl).filter(Boolean);
    }
    if (typeof value === "string") {
      try {
        const parsed = JSON.parse(value);
        return Array.isArray(parsed)
          ? parsed.map(getPhotoUrl).filter(Boolean)
          : [];
      } catch (e) {
        return [];
      }
    }
    return [];
  };
  const getPhotoUrl = item =>
    typeof item === "string" ? item : item?.previewURL || item?.url || "";
  const openForm = row => {
    form.value = buildEmptyForm();
    originalArrivalQuantity.value = 0;
    loadBindableRows().then(() => {
      if (row) {
        form.value.id = row.id ?? null;
        form.value.stockOutRecordId = row.stockOutRecordId;
        // è½¦è¾†/车牌以出库单登记为准,但列表行里带回的是当时的值,直接回显
        form.value.vehicleId = row.vehicleId;
        form.value.truckPlateNo = row.truckPlateNo || "";
        form.value.arrivalQuantity = row.arrivalQuantity ?? null;
        form.value.arrivalDate = row.arrivalDate || "";
        form.value.remark = row.remark || "";
        form.value.attachmentList = toPhotoArray(row.attachments).map(url => ({
          url,
        }));
        originalArrivalQuantity.value = Number(row.arrivalQuantity) || 0;
      }
      formVisible.value = true;
    });
  };
  const handleOutboundChange = () => {
    // è½¦è¾†éšå‡ºåº“单联动:换了出库单,原来选的车辆可能不在候选里
    form.value.vehicleId = undefined;
    form.value.truckPlateNo = "";
  };
  const handleVehicleChange = id => {
    const vehicle = vehicleOptions.value.find(item => item.vehicleId === id);
    form.value.truckPlateNo = vehicle?.truckPlateNo || "";
  };
  const closeForm = () => {
    proxy.resetForm("formRef");
    form.value = buildEmptyForm();
    originalArrivalQuantity.value = 0;
    formVisible.value = false;
  };
  const submitForm = () => {
    proxy.$refs["formRef"].validate(valid => {
      if (!valid) return;
      const payload = {
        stockOutRecordId: form.value.stockOutRecordId,
        salesLedgerId: props.ledgerId,
        vehicleId: form.value.vehicleId,
        truckPlateNo: form.value.truckPlateNo,
        arrivalQuantity: form.value.arrivalQuantity,
        arrivalDate: form.value.arrivalDate,
        // åªå­˜æ°¸ä¹…链接,2 å°æ—¶å¤±æ•ˆçš„签名 URL è¿‡æœŸåŽé™„件会打不开
        attachments: form.value.attachmentList.map(getPhotoUrl).filter(Boolean),
        remark: form.value.remark,
      };
      const submit = form.value.id
        ? updateSalesLedgerArrival({ ...payload, id: form.value.id })
        : addSalesLedgerArrival(payload);
      submit.then(() => {
        proxy.$modal.msgSuccess("操作成功");
        closeForm();
        getList();
        emit("changed");
      });
    });
  };
  const handleDelete = row => {
    ElMessageBox.confirm("此操作将删除该到货记录,是否确认?", "删除", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        delSalesLedgerArrival([row.id]).then(() => {
          proxy.$modal.msgSuccess("删除成功");
          getList();
          emit("changed");
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  const closeModal = () => {
    isShow.value = false;
  };
  watch(
    () => props.visible,
    visible => {
      if (visible) {
        page.current = 1;
        getList();
      }
    }
  );
</script>
<style scoped lang="scss">
  .actions {
    display: flex;
    justify-content: flex-end;
    margin-bottom: 10px;
  }
  .ledger-tip,
  .arrival-tip {
    margin-bottom: 10px;
    padding: 6px 14px;
    font-size: 13px;
    color: #303133;
    background: rgba(64, 158, 255, 0.08);
    border-left: 3px solid #409eff;
    border-radius: 3px;
  }
  .arrival-tip {
    margin-bottom: 0;
  }
</style>
src/views/salesManagement/salesLedger/index.vue
@@ -63,7 +63,7 @@
    </div>
    <!-- å–æ²¹ / ä»£å‚¨æ‹†æˆä¸¤ä¸ªé¡µç­¾ï¼Œå„自固定带 ledgerType è°ƒåŒä¸€ä¸ªåˆ—表接口 -->
    <el-tabs v-model="activeTab" @tab-change="handleTabChange" class="ledger-tabs">
      <el-tab-pane label="销售台账(卖油)" name="卖油" />
      <el-tab-pane label="销售台账" name="卖油" />
      <el-tab-pane label="代储台账" name="代储" />
    </el-tabs>
    <div class="table_list">
@@ -115,44 +115,11 @@
              <el-table-column label="产品大类" prop="productCategory" />
              <el-table-column label="规格型号" prop="specificationModel" />
              <el-table-column label="单位" prop="unit" />
              <el-table-column label="产品状态" width="100px" align="center">
                <template #default="scope">
                  <el-tag v-if="scope.row.approveStatus === 1" type="success"
                    >充足
                  </el-tag>
                  <el-tag
                    v-else-if="
                      scope.row.approveStatus === 0 &&
                      scope.row.noQuantity === 0
                    "
                    type="success"
                    >已出库
                  </el-tag>
                  <el-tag v-else type="danger">不足 </el-tag>
                </template>
              </el-table-column>
              <el-table-column label="发货状态" width="140" align="center">
                <template #default="scope">
                  <el-tag :type="getShippingStatusType(scope.row)" size="small">
                    {{ getShippingStatusText(scope.row) }}
                  </el-tag>
                </template>
              </el-table-column>
              <el-table-column label="审批中数量" width="120" align="center">
                <template #default="scope">
                  <span>{{ scope.row.pendingApprovalQuantity || 0 }}</span>
                </template>
              </el-table-column>
              <el-table-column
                label="快递公司"
                prop="expressCompany"
                show-overflow-tooltip
              />
              <el-table-column
                label="快递单号"
                prop="expressNumber"
                show-overflow-tooltip
              />
              <el-table-column label="发货车牌" minWidth="100px" align="center">
                <template #default="scope">
                  <div>
@@ -175,7 +142,6 @@
              </el-table-column>
              <el-table-column label="数量" prop="quantity" />
              <el-table-column label="退货数量" prop="returnQuantity" />
              <el-table-column label="待发货数量" prop="noQuantity" />
              <el-table-column label="税率(%)" prop="taxRate" />
              <el-table-column
                label="含税单价(元)"
@@ -237,6 +203,7 @@
          label="付款方式"
          prop="paymentMethod"
          show-overflow-tooltip
          :formatter="paymentMethodFormatter"
        />
        <el-table-column
          label="合同金额(元)"
@@ -294,6 +261,7 @@
          prop="ledgerType"
          width="100"
          show-overflow-tooltip
          :formatter="ledgerTypeFormatter"
        />
        <el-table-column
          v-if="activeTab === '代储'"
@@ -309,6 +277,13 @@
          width="140"
          show-overflow-tooltip
        />
        <el-table-column
          v-if="activeTab === '代储'"
          label="吨位(吨)"
          prop="tonnage"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column label="审批状态" width="110" align="center">
          <template #default="{ row }">
            <el-tag :type="getApprovalStatusType(row.approvalStatus)" size="small">
@@ -322,7 +297,7 @@
          width="200"
          show-overflow-tooltip
        />
        <el-table-column fixed="right" label="操作" width="220" align="center">
        <el-table-column fixed="right" label="操作" width="300" align="center">
          <template #default="scope">
            <el-button link type="primary" @click="openForm('view', scope.row)"
              >详情
@@ -340,6 +315,12 @@
            </el-button>
            <el-button link type="primary" @click="openFileDialog(scope.row)"
              >附件
            </el-button>
            <el-button
              link
              type="primary"
              @click="openArrivalDialog(scope.row)"
              >到货记录
            </el-button>
          </template>
        </el-table-column>
@@ -382,8 +363,8 @@
                v-model="form.ledgerType"
                :disabled="operationType === 'view'"
              >
                <el-radio value="卖油">卖油</el-radio>
                <el-radio value="代储">卖存储位置(代储)</el-radio>
                <el-radio value="卖油">销售</el-radio>
                <el-radio value="代储">代储费</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
@@ -502,13 +483,21 @@
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="付款方式">
              <el-input
            <el-form-item label="付款方式" prop="paymentMethod">
              <el-select
                v-model="form.paymentMethod"
                placeholder="请输入"
                placeholder="请选择"
                clearable
                :disabled="operationType === 'view'"
                style="width: 100%"
              >
                <el-option
                  v-for="item in payment_methods"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
              />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
@@ -568,7 +557,7 @@
            </el-form-item>
          </el-col>
        </el-row>
        <!-- ä»£å‚¨ï¼šå–存储位置,只选油库+储罐并手工填合同金额 -->
        <!-- ä»£å‚¨è´¹ï¼šåªé€‰æ²¹åº“+储罐并手工填合同金额、吨位 -->
        <el-row v-if="form.ledgerType === '代储'" :gutter="30">
          <el-col :span="12">
            <el-form-item label="油库:" prop="oilDepotId">
@@ -629,6 +618,19 @@
                :closable="false"
                class="trust-fund-alert"
                :title="`合同金额已超出该客户信托基金(信托基金:${selectedCustomerTrustFund})`"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="吨位(吨):" prop="tonnage">
              <el-input-number
                v-model="form.tonnage"
                :min="0"
                :precision="2"
                :controls="false"
                placeholder="请输入"
                :disabled="operationType === 'view'"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
@@ -1302,6 +1304,13 @@
        </div>
      </template>
    </el-dialog>
    <!-- åˆ°è´§è®°å½•:挂在「出库单 Ã— æœ¬å°è´¦ã€ä¸Šï¼ŒåŒæ—¶æ˜¯å‘货台账油品出库父行的子行 -->
    <ArrivalDialog
      v-model:visible="arrivalDialogVisible"
      :ledger-id="arrivalLedgerId"
      :ledger-label="arrivalLedgerLabel"
      @changed="getList"
    />
  </div>
</template>
@@ -1337,6 +1346,7 @@
import { getCustomer, listCustomer } from "@/api/basicData/customer.js";
import { allOilDepot } from "@/api/basicData/oilDepot.js";
import { allTankInfo } from "@/api/basicData/tankInfo.js";
import ArrivalDialog from "./components/ArrivalDialog.vue";
const FileList = defineAsyncComponent(() =>
  import("@/components/Dialog/FileList.vue")
@@ -1347,8 +1357,22 @@
const userStore = useUserStore();
const { proxy } = getCurrentInstance();
const { tax_rate } = proxy.useDict("tax_rate");
const { payment_methods } = proxy.useDict("payment_methods");
// ä»˜æ¬¾æ–¹å¼å­˜çš„æ˜¯å­—å…¸ value,回显要转成 label;改造前的老数据是自由文本,取不到就原样显示
const getPaymentMethodLabel = (value) => {
  if (value === undefined || value === null || value === "") return "-";
  const item = payment_methods.value?.find(
    (m) => String(m.value) === String(value)
  );
  return item?.label ?? value;
};
const paymentMethodFormatter = (row) => getPaymentMethodLabel(row.paymentMethod);
// å–æ²¹ / ä»£å‚¨ä¸¤ä¸ªé¡µç­¾ï¼Œå›ºå®šå¸¦ ledgerType è°ƒåŒä¸€ä¸ªåˆ—表接口
const activeTab = ref("卖油");
// ç•Œé¢å£å¾„:ledgerType=卖油 æ˜¾ç¤ºã€Œé”€å”®ã€ï¼Œ=代储 æ˜¾ç¤ºã€Œä»£å‚¨è´¹ã€ï¼›æäº¤ç»™åŽç«¯ä»æ˜¯å–æ²¹ / ä»£å‚¨
const ledgerTypeLabel = (value) =>
  value === "卖油" ? "销售" : value === "代储" ? "代储费" : value;
const ledgerTypeFormatter = (row) => ledgerTypeLabel(row.ledgerType);
const approvalStatusOptions = ["待审批", "审核中", "通过", "拒绝"];
// ä»£å‚¨è¡¨å•:油库、储罐下拉(两者互相独立,储罐不挂在油库下)
const oilDepotOptions = ref([]);
@@ -1401,6 +1425,7 @@
    oilDepotId: "", // ä»£å‚¨ï¼šæ²¹åº“
    tankId: "", // ä»£å‚¨ï¼šå‚¨ç½
    contractAmount: null, // ä»£å‚¨ï¼šåˆåŒé‡‘额由前端手工填
    tonnage: null, // ä»£å‚¨è´¹ï¼šå¨ä½ï¼Œé€‰å¡«
  },
  rules: {
    ledgerType: [{ required: true, message: "请选择", trigger: "change" }],
@@ -2118,7 +2143,7 @@
      form.value.type = 1;
      form.value.ledgerType = form.value.ledgerType || "卖油";
      if (form.value.ledgerType === "代储") {
        // ä»£å‚¨ï¼šå–存储位置,没有产品明细,合同金额手工填
        // ä»£å‚¨è´¹ï¼šæ²¡æœ‰äº§å“æ˜Žç»†ï¼ŒåˆåŒé‡‘额手工填
        if (!form.value.oilDepotId) {
          proxy.$modal.msgWarning("代储必须选择油库");
          return;
@@ -2130,6 +2155,10 @@
        if (!(Number(form.value.contractAmount) > 0)) {
          proxy.$modal.msgWarning("代储必须填写合同金额");
          return;
        }
        // å¨ä½é€‰å¡«ï¼šæ¸…空输入框时 el-input-number ä¼šç»™ undefined,统一成 null å†æäº¤
        if (form.value.tonnage === "" || form.value.tonnage === undefined) {
          form.value.tonnage = null;
        }
        delete form.value.productData;
      } else {
@@ -2143,6 +2172,7 @@
        form.value.oilDepotId = "";
        form.value.tankId = "";
        form.value.contractAmount = null;
        form.value.tonnage = null;
      }
      form.value.storageBlobDTOs = fileList;
      if (form.value.autoGenerateContractNo) {
@@ -2984,69 +3014,6 @@
  isCalculating.value = false;
};
/**
 * èŽ·å–å‘è´§çŠ¶æ€æ–‡æœ¬
 * @param row è¡Œæ•°æ®
 */
const getShippingStatusText = (row) => {
  // å¦‚果已发货(有发货日期或车牌号),显示"已发货"
  // if (row.shippingDate || row.shippingCarNumber) {
  //   return "已发货";
  // }
  // èŽ·å–å‘è´§çŠ¶æ€å­—æ®µ
  const status = row.shippingStatus;
  // å¦‚果状态为空或未定义,默认为"待发货"
  if (status === null || status === undefined || status === "") {
    return "待发货";
  }
  // çŠ¶æ€æ˜¯å­—ç¬¦ä¸²
  const statusStr = String(status).trim();
  const statusTextMap = {
    å¾…发货: "待发货",
    å¾…审核: "待审核",
    å®¡æ ¸ä¸­: "审核中",
    å®¡æ ¸æ‹’绝: "审核拒绝",
    å®¡æ ¸é€šè¿‡: "审核通过",
    å·²å‘è´§: "已发货",
    éƒ¨åˆ†å‘è´§: "部分发货",
  };
  return statusTextMap[statusStr] || "待发货";
};
/**
 * èŽ·å–å‘è´§çŠ¶æ€æ ‡ç­¾ç±»åž‹ï¼ˆé¢œè‰²ï¼‰
 * @param row è¡Œæ•°æ®
 */
const getShippingStatusType = (row) => {
  // å¦‚果已发货(有发货日期或车牌号),显示绿色
  if (row.shippingStatus === "已发货") {
    return "success";
  }
  // èŽ·å–å‘è´§çŠ¶æ€å­—æ®µ
  const status = row.shippingStatus;
  // å¦‚果状态为空或未定义,默认为灰色(待发货)
  if (status === null || status === undefined || status === "") {
    return "info";
  }
  // çŠ¶æ€æ˜¯å­—ç¬¦ä¸²
  const statusStr = String(status).trim();
  const typeTextMap = {
    å¾…发货: "info",
    å¾…审核: "info",
    å®¡æ ¸ä¸­: "warning",
    å®¡æ ¸æ‹’绝: "danger",
    å®¡æ ¸é€šè¿‡: "success",
    å·²å‘è´§: "success",
    éƒ¨åˆ†å‘è´§: "warning",
  };
  return typeTextMap[statusStr] || "info";
};
/**
 * åˆ¤æ–­æ˜¯å¦å¯ä»¥å‘è´§
@@ -3087,6 +3054,18 @@
  fileDialogVisible.value = true;
};
// åˆ°è´§è®°å½•弹框:按当前台账筛选,台账 id/名由列表行带入
const arrivalDialogVisible = ref(false);
const arrivalLedgerId = ref(undefined);
const arrivalLedgerLabel = ref("");
const openArrivalDialog = (row) => {
  arrivalLedgerId.value = row.id;
  arrivalLedgerLabel.value = [row.salesContractNo, row.customerName]
    .filter(Boolean)
    .join(" / ");
  arrivalDialogVisible.value = true;
};
// æ‰“开发货弹框
const openDeliveryForm = async (row) => {
  // æ£€æŸ¥æ˜¯å¦å¯ä»¥å‘è´§