新疆-融邦
1. 委外管理:新增合作商档案、委外订单、往来台账三个页面及接口;合作商附件弹框支持直接上传/删除并即时保存。
2. 信托基金与代储台账:客户档案加「信托基金 / 代储金额」并对超限标红提示;销售台账拆「卖油 / 代储」页签,代储走油库+储罐+合同金额,卖油保留产品明细;台账列表加审批状态与超限红字;审批中心显示销售合同号并能下钻台账明细。
3. 储罐号带入库存:储罐信息表单加「是否启用 / 用途 / 租客信息」联动与校验;库存管理、批号明细、入库/出库表单与列表补齐储罐号、油品、物流、过磅等字段展示。
4. 出库即发货与往来备件:出库台账区分「发货单 / 油品出库」两类来源并分别处理详情;新增备件出入库流水页面。
5. 车辆管理与残油联动:新增车辆管理页面;油品出入库表单物流信息顺序调整,车牌/车挂牌号联动只读。
6. 油品出库联动发货台账:发货台账加「发货单号 / 销售合同号」筛选,修复油品出库行批号列取错字段。
7. 采购申请模块:新增采购申请页面及接口。
8. 付款流水组件:抽出公共「付款流水」弹框组件,采购付款分栏、收付款台账与委外往来台账复用。
9. 列调整:收付款台账删除「付款金额 / 退货金额」;委外往来台账删除「回款金额」,并去掉金额列右对齐。
已添加14个文件
已修改22个文件
6403 ■■■■■ 文件已修改
src/api/basicData/vehicle.js 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/sparePartsRecord.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/paymentRecord.js 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockManage.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockOut.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/outsourcing/order.js 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/outsourcing/partner.js 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/outsourcing/report.js 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/procurementManagement/purchaseApplication.js 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/PaymentRecordDialog/index.vue 330 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/customerFile/index.vue 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/tankInfo/index.vue 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/vehicle/index.vue 371 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/spareParts/index.vue 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/sparePartsRecord/index.vue 215 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/dispatchLog/Record.vue 291 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/issueManagement/index.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/receiptManagement/Record.vue 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/BatchNoQtyDetail.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/OilStockInDialog.vue 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/OilStockOutDialog.vue 286 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Record.vue 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/outsourcingManagement/order/index.vue 952 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/outsourcingManagement/partner/index.vue 668 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/outsourcingManagement/transactions/index.vue 255 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/paymentLedger/index.vue 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/purchaseApplication/index.vue 1087 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/transferManagement/index.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/deliveryLedger/index.vue 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/receiptPaymentLedger/index.vue 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 303 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/vehicle.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢è½¦è¾†ä¿¡æ¯åˆ—表
export function listVehicle(query) {
  return request({
    url: "/basic/vehicle/list",
    method: "get",
    params: query,
  });
}
// æŸ¥è¯¢å…¨éƒ¨å¯ç”¨è½¦è¾†ï¼ˆä¾›ä¸‹æ‹‰é€‰æ‹©ï¼‰
export function allVehicle() {
  return request({
    url: "/basic/vehicle/all",
    method: "get",
  });
}
// æŸ¥è¯¢è½¦è¾†è¯¦æƒ…
export function getVehicle(id) {
  return request({
    url: "/basic/vehicle/" + id,
    method: "get",
  });
}
// æ–°å¢žè½¦è¾†
export function addVehicle(data) {
  return request({
    url: "/basic/vehicle/add",
    method: "post",
    data: data,
  });
}
// ä¿®æ”¹è½¦è¾†
export function updateVehicle(data) {
  return request({
    url: "/basic/vehicle/update",
    method: "post",
    data: data,
  });
}
// åˆ é™¤è½¦è¾†
export function delVehicle(ids) {
  return request({
    url: "/basic/vehicle/del",
    method: "delete",
    data: ids,
  });
}
src/api/equipmentManagement/sparePartsRecord.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
import request from "@/utils/request";
/**
 * å¤‡ä»¶å‡ºå…¥åº“流水 - åˆ†é¡µæŸ¥è¯¢
 * params: { current, size, sparePartsId?, direction?, sourceType? }
 * direction: 1=入库 2=出库
 * sourceType: 1=采购入库 2=维修领用 3=保养领用
 */
export const getSparePartsRecordPage = (params) => {
  return request({
    url: "/sparePartsRecord/listPage",
    method: "get",
    params,
  });
};
/**
 * å¤‡ä»¶å‡ºå…¥åº“流水 - æ‰¹é‡åˆ é™¤ï¼ˆbody ä¸º id æ•°ç»„)
 */
export const delSparePartsRecord = (ids) => {
  const idList = Array.isArray(ids) ? ids : [ids];
  return request({
    url: "/sparePartsRecord/delete",
    method: "delete",
    data: idList,
  });
};
src/api/financialManagement/paymentRecord.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
import request from "@/utils/request";
/** ä»˜æ¬¾å›žæ¬¾æµæ°´åˆ†é¡µåˆ—表 */
export function paymentRecordPage(params) {
  return request({
    url: "/paymentRecord/listPage",
    method: "get",
    params,
  });
}
/** æ–°å¢žä»˜æ¬¾å›žæ¬¾æµæ°´ */
export function addPaymentRecord(data) {
  return request({
    url: "/paymentRecord/add",
    method: "post",
    data,
  });
}
/** ä¿®æ”¹ä»˜æ¬¾å›žæ¬¾æµæ°´ */
export function updatePaymentRecord(data) {
  return request({
    url: "/paymentRecord/update",
    method: "post",
    data,
  });
}
/** åˆ é™¤ä»˜æ¬¾å›žæ¬¾æµæ°´ï¼ˆbody ä¸º id æ•°ç»„) */
export function delPaymentRecord(ids) {
  const idList = Array.isArray(ids) ? ids : [ids];
  return request({
    url: "/paymentRecord/delete",
    method: "delete",
    data: idList,
  });
}
src/api/inventoryManagement/stockManage.js
@@ -80,4 +80,13 @@
    })
}
//出库管理-查询规格型号下的可用批次(可用量 = åº“存量 - é”å®šæ•°é‡ï¼‰
export function getBatchNoList(productModelId) {
    return request({
        url: '/stockmanagement/batchNoList',
        method: 'get',
        params: { productModelId }
    })
}
//
src/api/inventoryManagement/stockOut.js
@@ -44,3 +44,21 @@
        data,
    });
}
// æ²¹å“å‡ºåº“可用量查询(上次剩余油量 + å½“前库存)
export const getOilOutAvailableQty = (params) => {
    return request({
        url: "/stockOutRecord/oilOutAvailableQty",
        method: "get",
        params,
    });
}
// å½•入送完货后的剩余油量(只更新 residualOil ä¸€åˆ—)
export const updateResidualOil = (id, data) => {
    return request({
        url: `/stockOutRecord/${id}/residualOil`,
        method: "put",
        data,
    });
}
src/api/outsourcing/order.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,92 @@
import request from "@/utils/request";
// å§”外订单分页查询(RuoYi ç»“构:顶层 rows)
export function listOutsourcingOrder(query) {
  return request({
    url: "/outsourcing/order/list",
    method: "get",
    params: query,
  });
}
// å§”外订单详情(含 productData æ˜Žç»†ï¼‰
export function getOutsourcingOrder(id) {
  return request({
    url: "/outsourcing/order/getById",
    method: "get",
    params: { id },
  });
}
// æ–°å¢žå§”外订单(强制草稿)
export function addOutsourcingOrder(data) {
  return request({
    url: "/outsourcing/order/add",
    method: "post",
    data: data,
  });
}
// ä¿®æ”¹å§”外订单(仅草稿,明细全量提交)
export function editOutsourcingOrder(data) {
  return request({
    url: "/outsourcing/order/edit",
    method: "post",
    data: data,
  });
}
// åˆ é™¤å§”外订单(仅草稿,body ä¸º id æ•°ç»„)
export function delOutsourcingOrder(ids) {
  return request({
    url: "/outsourcing/order/delete",
    method: "delete",
    data: ids,
  });
}
// æäº¤ï¼ˆè‰ç¨¿ -> å·²ç¡®è®¤ï¼‰ï¼Œid ä¸º query å‚æ•°
export function submitOutsourcingOrder(id) {
  return request({
    url: "/outsourcing/order/submit",
    method: "post",
    params: { id },
  });
}
// å®Œæˆï¼ˆå·²ç¡®è®¤ -> å·²å®Œæˆï¼‰
export function completeOutsourcingOrder(id) {
  return request({
    url: "/outsourcing/order/complete",
    method: "post",
    params: { id },
  });
}
// ä½œåºŸï¼ˆè‰ç¨¿/已确认 -> å·²ä½œåºŸï¼‰
export function cancelOutsourcingOrder(id, cancelReason) {
  return request({
    url: "/outsourcing/order/cancel",
    method: "post",
    params: { id, cancelReason },
  });
}
// å¯¼å‡ºå§”外订单(返回 Excel æ–‡ä»¶æµï¼‰
export function exportOutsourcingOrder(query) {
  return request({
    url: "/outsourcing/order/export",
    method: "post",
    data: query,
    responseType: "blob",
  });
}
// æŠ¥åºŸå“é€‰æ–™åˆ†é¡µï¼ˆåªè¯»ï¼Œè¿”回 IPage:data.records)
export function scrapStockPage(query) {
  return request({
    url: "/outsourcing/order/scrapStockPage",
    method: "get",
    params: query,
  });
}
src/api/outsourcing/partner.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
import request from "@/utils/request";
// åˆä½œå•†æ¡£æ¡ˆåˆ†é¡µæŸ¥è¯¢ï¼ˆè¿”回 IPage:data.records)
export function listPartner(query) {
  return request({
    url: "/outsourcing/partner/listPage",
    method: "get",
    params: query,
  });
}
// åˆä½œå•†æ¡£æ¡ˆè¯¦æƒ…(含 storageBlobVOS é™„件回显)
export function getPartner(id) {
  return request({
    url: "/outsourcing/partner/" + id,
    method: "get",
  });
}
// æ–°å¢žåˆä½œå•†
export function addPartner(data) {
  return request({
    url: "/outsourcing/partner/add",
    method: "post",
    data: data,
  });
}
// ä¿®æ”¹åˆä½œå•†ï¼ˆstorageBlobDTOS ä¸ºå…¨é‡è¦†ç›–)
export function updatePartner(data) {
  return request({
    url: "/outsourcing/partner/update",
    method: "post",
    data: data,
  });
}
// åˆ é™¤åˆä½œå•†ï¼ˆbody ä¸º id æ•°ç»„)
export function delPartner(ids) {
  return request({
    url: "/outsourcing/partner/del",
    method: "delete",
    data: ids,
  });
}
// å¯¼å‡ºåˆä½œå•†ï¼ˆè¿”回 Excel æ–‡ä»¶æµï¼‰
export function exportPartner(query) {
  return request({
    url: "/outsourcing/partner/export",
    method: "post",
    data: query,
    responseType: "blob",
  });
}
// ä¸‹è½½å¯¼å…¥æ¨¡æ¿ï¼ˆè¿”回 Excel æ–‡ä»¶æµï¼‰
export function downloadPartnerTemplate() {
  return request({
    url: "/outsourcing/partner/downloadTemplate",
    method: "post",
    responseType: "blob",
  });
}
// åˆä½œå•†ä¸‹æ‹‰é€‰é¡¹ï¼ˆä»… id + partnerName)
export function getPartnerOptions() {
  return request({
    url: "/outsourcing/partner/getOptions",
    method: "get",
  });
}
src/api/outsourcing/report.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
import request from "@/utils/request";
// åˆä½œå•†å¾€æ¥æ±‡æ€»ï¼ˆè¿”回 IPage:data.records)
export function partnerTransactions(query) {
  return request({
    url: "/outsourcing/report/partnerTransactions",
    method: "get",
    params: query,
  });
}
// åˆä½œå•†å¾€æ¥æ˜Žç»†ï¼ˆé€è®¢å•,返回 IPage:data.records)
// æ³¨æ„ï¼šæ¯è¡Œ paidAmount æ˜¯åˆä½œå•†çº§åˆè®¡ï¼Œä¸æ˜¯è®¢å•级
export function partnerTransactionsDetails(partnerId) {
  return request({
    url: "/outsourcing/report/partnerTransactionsDetails",
    method: "get",
    params: { partnerId },
  });
}
src/api/procurementManagement/purchaseApplication.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
// é‡‡è´­ç”³è¯·é¡µé¢æŽ¥å£
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢é‡‡è´­ç”³è¯·åˆ—表
export function applicationList(query) {
  return request({
    url: "/purchase/application/list",
    method: "get",
    params: query,
  });
}
// æŸ¥è¯¢é‡‡è´­ç”³è¯·è¯¦æƒ…(含产品明细)
export function getApplicationById(query) {
  return request({
    url: "/purchase/application/getById",
    method: "get",
    params: query,
  });
}
// æ–°å¢žé‡‡è´­ç”³è¯·ï¼ˆè‰ç¨¿ï¼‰
export function addApplication(data) {
  return request({
    url: "/purchase/application/add",
    method: "post",
    data: data,
  });
}
// ä¿®æ”¹é‡‡è´­ç”³è¯·
export function editApplication(data) {
  return request({
    url: "/purchase/application/edit",
    method: "post",
    data: data,
  });
}
// åˆ é™¤é‡‡è´­ç”³è¯·ï¼ˆæ”¯æŒæ‰¹é‡ï¼‰
export function delApplication(ids) {
  return request({
    url: "/purchase/application/delete",
    method: "delete",
    data: ids,
  });
}
// é‡‡è´­ç”³è¯·è½¬è®¢å•,返回生成的采购台账id
export function convertToOrder(id) {
  return request({
    url: "/purchase/application/convertToOrder",
    method: "post",
    params: { id },
  });
}
src/components/PaymentRecordDialog/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,330 @@
<template>
  <el-dialog :model-value="visible"
             :title="title"
             width="60%"
             :close-on-click-modal="false"
             @update:model-value="handleVisibleChange"
             @close="closeDialog">
    <div class="payment-record-dialog">
      <div class="dialog-header">
        <span class="party-name">{{ partyName }}</span>
        <el-button type="primary"
                   @click="openAddForm">新增</el-button>
      </div>
      <el-table :data="tableData"
                border
                v-loading="tableLoading"
                height="380px">
        <el-table-column align="center"
                         label="序号"
                         type="index"
                         width="60" />
        <el-table-column label="发生日期"
                         prop="recordDate"
                         width="130"
                         show-overflow-tooltip />
        <el-table-column label="金额(元)"
                         prop="amount"
                         width="140"
                         show-overflow-tooltip>
          <template #default="{ row }">
            {{ formatAmount(row.amount) }}
          </template>
        </el-table-column>
        <el-table-column label="方式"
                         prop="paymentMethod"
                         width="140"
                         show-overflow-tooltip />
        <el-table-column label="备注"
                         prop="remark"
                         show-overflow-tooltip />
        <el-table-column label="操作"
                         align="center"
                         width="140"
                         fixed="right">
          <template #default="{ row }">
            <el-button link
                       type="primary"
                       @click="openEditForm(row)">编辑</el-button>
            <el-button link
                       type="danger"
                       @click="handleDelete(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" />
    </div>
    <el-dialog v-model="formVisible"
               :title="form.id ? '编辑流水' : '新增流水'"
               width="500px"
               append-to-body
               :close-on-click-modal="false"
               @close="resetForm">
      <el-form :model="form"
               label-width="100px"
               :rules="rules"
               ref="formRef">
        <el-form-item label="发生日期"
                      prop="recordDate">
          <el-date-picker v-model="form.recordDate"
                          type="date"
                          value-format="YYYY-MM-DD"
                          format="YYYY-MM-DD"
                          placeholder="请选择"
                          style="width: 100%" />
        </el-form-item>
        <el-form-item label="金额(元)"
                      prop="amount">
          <el-input-number v-model="form.amount"
                           :min="0"
                           :precision="2"
                           :step="0.01"
                           placeholder="请输入"
                           style="width: 100%" />
        </el-form-item>
        <el-form-item label="方式"
                      prop="paymentMethod">
          <el-input v-model="form.paymentMethod"
                    placeholder="请输入"
                    clearable />
        </el-form-item>
        <el-form-item label="备注"
                      prop="remark">
          <el-input v-model="form.remark"
                    type="textarea"
                    :rows="3"
                    placeholder="请输入"
                    clearable />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
                     @click="submitForm">确认</el-button>
          <el-button @click="formVisible = false">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </el-dialog>
</template>
<script setup>
  import { ref, reactive, watch, getCurrentInstance } from "vue";
  import { ElMessageBox } from "element-plus";
  import pagination from "@/components/PIMTable/Pagination.vue";
  import {
    paymentRecordPage,
    addPaymentRecord,
    updatePaymentRecord,
    delPaymentRecord,
  } from "@/api/financialManagement/paymentRecord.js";
  const props = defineProps({
    visible: {
      type: Boolean,
      default: false,
    },
    title: {
      type: String,
      default: "收付款流水",
    },
    // 1=客户,2=供应商
    partyType: {
      type: [String, Number],
      required: true,
    },
    partyId: {
      type: [String, Number],
      default: "",
    },
    partyName: {
      type: String,
      default: "",
    },
    // 1=付款,2=回款
    direction: {
      type: [String, Number],
      required: true,
    },
  });
  const emit = defineEmits(["update:visible", "changed"]);
  const { proxy } = getCurrentInstance();
  const tableData = ref([]);
  const tableLoading = ref(false);
  const total = ref(0);
  const page = reactive({
    current: 1,
    size: 10,
  });
  const formVisible = ref(false);
  const formRef = ref(null);
  const form = ref(buildEmptyForm());
  const rules = {
    recordDate: [{ required: true, message: "请选择发生日期", trigger: "change" }],
    amount: [{ required: true, message: "请输入金额", trigger: "blur" }],
  };
  function buildEmptyForm() {
    return {
      id: "",
      recordDate: "",
      amount: undefined,
      paymentMethod: "",
      remark: "",
    };
  }
  const formatAmount = amount => {
    return amount ? parseFloat(amount).toFixed(2) : "0.00";
  };
  const handleVisibleChange = val => {
    emit("update:visible", val);
  };
  const closeDialog = () => {
    page.current = 1;
    tableData.value = [];
    total.value = 0;
    formVisible.value = false;
  };
  const getList = () => {
    if (!props.partyId) {
      tableData.value = [];
      total.value = 0;
      return;
    }
    tableLoading.value = true;
    paymentRecordPage({
      pageNum: page.current,
      pageSize: page.size,
      partyType: props.partyType,
      partyId: props.partyId,
      direction: props.direction,
    })
      .then(res => {
        tableLoading.value = false;
        const result = res.data || {};
        tableData.value = result.records || [];
        total.value = result.total || 0;
      })
      .catch(() => {
        tableLoading.value = false;
      });
  };
  const paginationChange = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  const openAddForm = () => {
    form.value = buildEmptyForm();
    formVisible.value = true;
  };
  const openEditForm = row => {
    form.value = {
      id: row.id,
      recordDate: row.recordDate || "",
      amount: row.amount === null || row.amount === undefined ? undefined : Number(row.amount),
      paymentMethod: row.paymentMethod || "",
      remark: row.remark || "",
    };
    formVisible.value = true;
  };
  const resetForm = () => {
    formRef.value?.resetFields();
    form.value = buildEmptyForm();
  };
  const submitForm = () => {
    formRef.value?.validate(valid => {
      if (!valid) {
        return;
      }
      const payload = {
        partyType: props.partyType,
        partyId: props.partyId,
        direction: props.direction,
        amount: form.value.amount,
        recordDate: form.value.recordDate || null,
        paymentMethod: form.value.paymentMethod,
        remark: form.value.remark,
      };
      const isEdit = !!form.value.id;
      const submitRequest = isEdit
        ? updatePaymentRecord({ ...payload, id: form.value.id })
        : addPaymentRecord(payload);
      submitRequest.then(() => {
        proxy.$modal.msgSuccess(isEdit ? "修改成功" : "新增成功");
        formVisible.value = false;
        getList();
        emit("changed");
      });
    });
  };
  const handleDelete = row => {
    // è¯¥æŽ¥å£åŽç«¯æ— å½’属校验,任何登录用户可删任意流水,故确认框带上日期金额定位
    ElMessageBox.confirm(
      `确定删除「${row.recordDate || "未填日期"} / ${formatAmount(row.amount)} å…ƒã€è¿™æ¡æµæ°´å—?删除后不可恢复。`,
      "删除流水",
      {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
      }
    )
      .then(() => {
        delPaymentRecord([row.id]).then(() => {
          proxy.$modal.msgSuccess("删除成功");
          getList();
          emit("changed");
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  watch(
    () => props.visible,
    val => {
      if (val) {
        page.current = 1;
        getList();
      }
    }
  );
</script>
<style scoped lang="scss">
  .payment-record-dialog {
    .dialog-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 12px;
      .party-name {
        font-size: 15px;
        font-weight: 600;
        color: #303133;
      }
    }
  }
</style>
src/views/basicData/customerFile/index.vue
@@ -48,7 +48,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' ? '新增客户信息' : '编辑客户信息'"
@@ -133,6 +142,19 @@
                           :label="dict.label"
                           :value="dict.value" />
              </el-select>
            </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>
@@ -461,6 +483,32 @@
              </div>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <div class="info-item">
                <span class="info-label">信托基金(元):</span>
                <span class="info-value">{{ formatAmount(detailForm.trustFund) }}</span>
              </div>
            </el-col>
            <el-col :span="12">
              <div class="info-item">
                <span class="info-label">代储金额(元):</span>
                <span class="info-value"
                      :class="{ 'text-danger': detailForm.exceedsTrustFund }">
                  {{ formatAmount(detailForm.depositTotalAmount) }}
                </span>
              </div>
            </el-col>
          </el-row>
          <el-row v-if="detailForm.exceedsTrustFund"
                  :gutter="20">
            <el-col :span="24">
              <el-alert type="warning"
                        show-icon
                        :closable="false"
                        :title="`该客户代储金额合计 ${formatAmount(detailForm.depositTotalAmount)} å…ƒï¼Œå·²è¶…出信托基金 ${formatAmount(detailForm.trustFund)} å…ƒ`" />
            </el-col>
          </el-row>
        </div>
      </div>
      <!-- æ´½è°ˆè¿›åº¦è®°å½• -->
@@ -675,6 +723,11 @@
    content: [{ required: true, message: "请输入内容", trigger: "blur" }],
  };
  const formatAmount = value => {
    return value === null || value === undefined || value === ""
      ? "-"
      : Number(value).toFixed(2);
  };
  // è¯¦æƒ…相关
  const detailDialogVisible = ref(false);
  const detailForm = reactive({
@@ -690,6 +743,9 @@
    contactPhone: "",
    maintainer: "",
    maintenanceTime: "",
    trustFund: "",
    depositTotalAmount: "",
    exceedsTrustFund: false,
  });
  const negotiationRecords = ref([]);
@@ -759,6 +815,22 @@
      label: "银行账号",
      prop: "bankAccount",
      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: "开户行号",
@@ -877,6 +949,8 @@
      bankAccount: "",
      bankCode: "",
      customerType: "",
      // é‡‘额字段给 null è€Œéž "",避免 el-input-number æŠŠç©ºä¸²æŠ˜ç®—成 0
      trustFund: null,
      type: 0
    },
    rules: {
@@ -1576,6 +1650,11 @@
    word-break: break-word;
  }
  .text-danger {
    color: var(--el-color-danger);
    font-weight: 600;
  }
  .no-records {
    text-align: center;
    padding: 30px;
src/views/basicData/tankInfo/index.vue
@@ -18,6 +18,30 @@
                      prefix-icon="Search"
                      @change="handleQuery" />
          </el-form-item>
          <el-form-item label="是否启用:">
            <el-select v-model="searchForm.isEnabled"
                       placeholder="请选择"
                       clearable
                       style="width: 140px"
                       @change="handleQuery">
              <el-option label="启用"
                         :value="1" />
              <el-option label="未启用"
                         :value="0" />
            </el-select>
          </el-form-item>
          <el-form-item label="用途:">
            <el-select v-model="searchForm.usageType"
                       placeholder="请选择"
                       clearable
                       style="width: 140px"
                       @change="handleQuery">
              <el-option label="租赁"
                         :value="1" />
              <el-option label="自用"
                         :value="2" />
            </el-select>
          </el-form-item>
          <el-form-item>
            <el-button type="primary"
                       @click="handleQuery"> æœç´¢
@@ -64,6 +88,25 @@
        <el-table-column label="库存余量(m³)"
                         prop="remaining"
                         min-width="140" />
        <el-table-column label="是否启用"
                         prop="isEnabled"
                         align="center"
                         width="100">
          <template #default="scope">
            <el-tag :type="scope.row.isEnabled === 1 ? 'success' : 'info'"
                    size="small">
              {{ scope.row.isEnabled === 1 ? "启用" : "未启用" }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="用途"
                         prop="usageType"
                         align="center"
                         width="90">
          <template #default="scope">
            {{ getUsageTypeText(scope.row.usageType) }}
          </template>
        </el-table-column>
        <el-table-column label="租客信息"
                         prop="tenantInfo"
                         min-width="160"
@@ -172,8 +215,38 @@
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="是否启用:"
                          prop="isEnabled">
              <el-switch v-model="form.isEnabled"
                         :active-value="1"
                         :inactive-value="0"
                         active-text="启用"
                         inactive-text="未启用" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30"
                v-if="form.isEnabled === 1">
          <el-col :span="12">
            <el-form-item label="用途:"
                          prop="usageType">
              <el-radio-group v-model="form.usageType">
                <el-radio :value="1">租赁</el-radio>
                <el-radio :value="2">自用</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
          <el-col :span="12"
                  v-if="form.usageType === 1">
            <el-form-item label="租客信息:"
                          prop="tenantInfo">
                          prop="tenantInfo"
                          :rules="[
                {
                required: true,
                message: '租赁的储罐必须填写租客信息',
                trigger: 'blur',
              }
            ]">
              <el-input v-model="form.tenantInfo"
                        placeholder="请输入租客信息"
                        clearable />
@@ -188,7 +261,7 @@
<script setup>
  import FormDialog from "@/components/Dialog/FormDialog.vue";
  import pagination from "@/components/PIMTable/Pagination.vue";
  import { ref, reactive, toRefs, getCurrentInstance } from "vue";
  import { ref, reactive, toRefs, watch, getCurrentInstance } from "vue";
  import { Search } from "@element-plus/icons-vue";
  import { ElMessageBox } from "element-plus";
  import {
@@ -214,6 +287,8 @@
    searchForm: {
      tankNo: "",
      category: "",
      isEnabled: "",
      usageType: "",
    },
    form: {
      id: null,
@@ -222,6 +297,8 @@
      oilProduct: "",
      capacity: null,
      remaining: null,
      isEnabled: 1,
      usageType: null,
      tenantInfo: "",
    },
    rules: {
@@ -230,6 +307,24 @@
    },
  });
  const { searchForm, form, rules } = toRefs(data);
  // ç”¨é€”文案:1 ç§Ÿèµ / 2 è‡ªç”¨
  const getUsageTypeText = value => ({ 1: "租赁", 2: "自用" })[value] || "--";
  // æœªå¯ç”¨æ—¶ç”¨é€”与租客信息一律清空;选自用时租客信息自动清空(租赁才需要租客)
  watch(
    () => [form.value.isEnabled, form.value.usageType],
    () => {
      if (form.value.isEnabled !== 1) {
        form.value.usageType = null;
        form.value.tenantInfo = "";
        return;
      }
      if (form.value.usageType !== 1) {
        form.value.tenantInfo = "";
      }
    }
  );
  const getList = () => {
    tableLoading.value = true;
@@ -259,14 +354,49 @@
    selectedRows.value = selection;
  };
  const buildEmptyForm = () => ({
    id: null,
    tankNo: "",
    category: "",
    oilProduct: "",
    capacity: null,
    remaining: null,
    isEnabled: 1,
    usageType: null,
    tenantInfo: "",
  });
  const openForm = (type, row) => {
    operationType.value = type;
    form.value = {};
    // å…ˆé‡ç½®å†èµ‹å€¼ï¼šresetFields ä¼šæŠŠå­—段打回挂载时的初值,放后面会把刚填好的值清掉
    proxy.resetForm("formRef");
    form.value = buildEmptyForm();
    dialogFormVisible.value = true;
    if (type === "edit" && row) {
      const { id, tankNo, category, oilProduct, capacity, remaining, tenantInfo } = row;
      form.value = { id, tankNo, category, oilProduct, capacity, remaining, tenantInfo };
      const {
        id,
        tankNo,
        category,
        oilProduct,
        capacity,
        remaining,
        isEnabled,
        usageType,
        tenantInfo,
      } = row;
      form.value = {
        ...buildEmptyForm(),
        id,
        tankNo,
        category,
        oilProduct,
        capacity,
        remaining,
        // åŽ†å²æ•°æ®æ²¡æœ‰è¯¥å­—æ®µï¼Œç¼ºå¤±æ—¶æŒ‰ã€Œå¯ç”¨ã€å¤„ç†ï¼Œé¿å…ä¿å­˜æ—¶è¢«è¯¯ç½®ä¸ºæœªå¯ç”¨
        isEnabled: isEnabled ?? 1,
        usageType: usageType ?? null,
        tenantInfo: tenantInfo ?? "",
      };
    }
  };
src/views/basicData/vehicle/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,371 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <div>
        <el-form :model="searchForm"
                 :inline="true">
          <el-form-item label="车牌号:">
            <el-input v-model="searchForm.plateNumber"
                      placeholder="请输入"
                      clearable
                      prefix-icon="Search"
                      @change="handleQuery" />
          </el-form-item>
          <el-form-item label="司机姓名:">
            <el-input v-model="searchForm.driverName"
                      placeholder="请输入"
                      clearable
                      prefix-icon="Search"
                      @change="handleQuery" />
          </el-form-item>
          <el-form-item label="状态:">
            <el-select v-model="searchForm.status"
                       placeholder="请选择"
                       clearable
                       style="width: 160px"
                       @change="handleQuery">
              <el-option label="启用"
                         value="启用" />
              <el-option label="停用"
                         value="停用" />
            </el-select>
          </el-form-item>
          <el-form-item>
            <el-button type="primary"
                       @click="handleQuery"> æœç´¢
            </el-button>
          </el-form-item>
        </el-form>
      </div>
    </div>
    <div class="table_list">
      <div style="display: flex;justify-content: flex-end;margin-bottom: 20px;">
        <el-button type="primary"
                   @click="openForm('add')">新增车辆</el-button>
        <el-button type="danger"
                   plain
                   @click="handleDelete()">删除
        </el-button>
      </div>
      <el-table :data="tableData"
                border
                v-loading="tableLoading"
                @selection-change="handleSelectionChange">
        <el-table-column align="center"
                         type="selection"
                         width="55" />
        <el-table-column align="center"
                         label="序号"
                         type="index"
                         width="60" />
        <el-table-column label="车牌号"
                         prop="plateNumber"
                         min-width="140"
                         show-overflow-tooltip />
        <el-table-column label="默认司机"
                         prop="driverName"
                         min-width="120"
                         show-overflow-tooltip />
        <el-table-column label="司机电话"
                         prop="driverPhone"
                         min-width="140"
                         show-overflow-tooltip />
        <el-table-column label="核定载重(吨)"
                         prop="loadCapacity"
                         min-width="120" />
        <el-table-column label="状态"
                         prop="status"
                         min-width="90">
          <template #default="scope">
            <el-tag :type="scope.row.status === '停用' ? 'info' : 'success'"
                    size="small">
              {{ scope.row.status || '启用' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="备注"
                         prop="remark"
                         min-width="160"
                         show-overflow-tooltip />
        <el-table-column label="创建时间"
                         prop="createTime"
                         min-width="180">
          <template #default="scope">
            {{ scope.row.createTime ? scope.row.createTime.replace('T', ' ').slice(0, 19) : '--' }}
          </template>
        </el-table-column>
        <el-table-column fixed="right"
                         label="操作"
                         width="120"
                         align="center">
          <template #default="scope">
            <el-button link
                       type="primary"
                       @click="openForm('edit', 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" />
    </div>
    <FormDialog v-model="dialogFormVisible"
                :title="operationType === 'add' ? '新增车辆' : '编辑车辆'"
                :width="'40%'"
                :operation-type="operationType"
                @close="closeDia"
                @confirm="submitForm"
                @cancel="closeDia">
      <el-form :model="form"
               label-width="120px"
               label-position="top"
               :rules="rules"
               ref="formRef">
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="车牌号:"
                          prop="plateNumber">
              <el-input v-model="form.plateNumber"
                        placeholder="请输入车牌号"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="状态:"
                          prop="status">
              <el-select v-model="form.status"
                         placeholder="请选择"
                         style="width: 100%">
                <el-option label="启用"
                           value="启用" />
                <el-option label="停用"
                           value="停用" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="默认司机:"
                          prop="driverName">
              <el-input v-model="form.driverName"
                        placeholder="请输入司机姓名"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="司机电话:"
                          prop="driverPhone">
              <el-input v-model="form.driverPhone"
                        placeholder="请输入11位数字"
                        maxlength="11"
                        @input="phoneInput"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="核定载重(吨):"
                          prop="loadCapacity">
              <el-input-number v-model="form.loadCapacity"
                               :precision="2"
                               :step="1"
                               :min="0"
                               style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="备注:"
                          prop="remark">
              <el-input v-model="form.remark"
                        type="textarea"
                        :rows="2"
                        placeholder="请输入备注"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
    </FormDialog>
  </div>
</template>
<script setup>
  import FormDialog from "@/components/Dialog/FormDialog.vue";
  import pagination from "@/components/PIMTable/Pagination.vue";
  import { ref, reactive, toRefs, getCurrentInstance } from "vue";
  import { ElMessageBox } from "element-plus";
  import {
    listVehicle,
    addVehicle,
    updateVehicle,
    delVehicle,
  } from "@/api/basicData/vehicle.js";
  const { proxy } = getCurrentInstance();
  const tableData = ref([]);
  const selectedRows = ref([]);
  const tableLoading = ref(false);
  const dialogFormVisible = ref(false);
  const operationType = ref("");
  const page = reactive({
    current: 1,
    size: 10,
  });
  const total = ref(0);
  const data = reactive({
    searchForm: {
      plateNumber: "",
      driverName: "",
      status: "",
    },
    form: {
      id: null,
      plateNumber: "",
      driverName: "",
      driverPhone: "",
      loadCapacity: null,
      status: "启用",
      remark: "",
    },
    rules: {
      plateNumber: [{ required: true, message: "请输入车牌号", trigger: "blur" }],
      driverPhone: [
        { pattern: /^1[3-9]\d{9}$/, message: "请输入有效的11位手机号码", trigger: "blur" },
      ],
    },
  });
  const { searchForm, form, rules } = toRefs(data);
  const getList = () => {
    tableLoading.value = true;
    listVehicle({ ...searchForm.value, ...page })
      .then(res => {
        tableLoading.value = false;
        tableData.value = res.data.records || [];
        total.value = res.data.total || 0;
      })
      .catch(() => {
        tableLoading.value = false;
      });
  };
  const handleQuery = () => {
    page.current = 1;
    getList();
  };
  const paginationChange = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  const handleSelectionChange = selection => {
    selectedRows.value = selection;
  };
  // ç”µè¯è¾“入限制:只允许数字,最多11位
  const phoneInput = val => {
    form.value.driverPhone = String(val ?? "")
      .replace(/\D/g, "")
      .slice(0, 11);
  };
  const openForm = (type, row) => {
    operationType.value = type;
    form.value = {};
    proxy.resetForm("formRef");
    dialogFormVisible.value = true;
    if (type === "edit" && row) {
      const { id, plateNumber, driverName, driverPhone, loadCapacity, status, remark } = row;
      form.value = {
        id,
        plateNumber,
        driverName,
        driverPhone,
        loadCapacity,
        status: status || "启用",
        remark,
      };
    } else {
      form.value = {
        id: null,
        plateNumber: "",
        driverName: "",
        driverPhone: "",
        loadCapacity: null,
        status: "启用",
        remark: "",
      };
    }
  };
  const submitForm = () => {
    proxy.$refs["formRef"].validate(valid => {
      if (valid) {
        const api = form.value.id ? updateVehicle : addVehicle;
        api(form.value).then(() => {
          proxy.$modal.msgSuccess("操作成功");
          closeDia();
          getList();
        });
      }
    });
  };
  const closeDia = () => {
    proxy.resetForm("formRef");
    dialogFormVisible.value = false;
  };
  const handleDelete = rows => {
    let ids = [];
    if (rows) {
      ids = rows.map(item => item.id);
    } else if (selectedRows.value.length > 0) {
      ids = selectedRows.value.map(item => item.id);
    } else {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    ElMessageBox.confirm(
      "选中的内容将被删除,是否确认删除?建议用「停用」下线车辆,删除不会校验是否已被出库记录引用。",
      "删除",
      {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
      }
    )
      .then(() => {
        delVehicle(ids).then(() => {
          proxy.$modal.msgSuccess("删除成功");
          getList();
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  getList();
</script>
<style scoped lang="scss">
  .search_form {
    margin-bottom: 16px;
  }
</style>
src/views/equipmentManagement/spareParts/index.vue
@@ -63,6 +63,35 @@
            <el-form-item label="备件编号" prop="sparePartsNo">
              <el-input v-model="form.sparePartsNo"></el-input>
            </el-form-item>
            <el-form-item label="产品大类" prop="productId">
              <el-tree-select
                v-model="form.productId"
                placeholder="请选择"
                clearable
                filterable
                check-strictly
                :data="productOptions"
                :render-after-expand="false"
                @change="handleProductChange"
                style="width: 100%"
              />
            </el-form-item>
            <el-form-item label="规格型号" prop="productModelId">
              <el-select
                v-model="form.productModelId"
                placeholder="请选择"
                clearable
                filterable
                style="width: 100%"
              >
                <el-option
                  v-for="item in modelOptions"
                  :key="item.id"
                  :label="item.model"
                  :value="item.id"
                />
              </el-select>
            </el-form-item>
            <el-form-item label="数量" prop="quantity">
              <el-input type="number" v-model="form.quantity"></el-input>
            </el-form-item>
@@ -134,6 +163,7 @@
import { ElMessage, ElMessageBox } from 'element-plus';
import { getSparePartsList, addSparePart, editSparePart, delSparePart } from "@/api/equipmentManagement/spareParts";
import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
import { productTreeList, modelList } from "@/api/basicData/product.js";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import { getSparePartsUsagePage } from "@/api/equipmentManagement/sparePartsUsage";
@@ -153,6 +183,9 @@
const operationType = ref('add')
// è®¾å¤‡é€‰é¡¹
const deviceOptions = ref([]);
// äº§å“å¤§ç±»æ ‘/规格型号选项
const productOptions = ref([]);
const modelOptions = ref([]);
// è¡¨å•引用
const formRef = ref(null);
// æŸ¥è¯¢å‚æ•°
@@ -253,6 +286,8 @@
  id:'',
  name: '',
  sparePartsNo: '',
  productId: '',
  productModelId: '',
  status: '',
  description: '',
  deviceLedgerIds: [],
@@ -415,17 +450,55 @@
  }
};
// åŠ è½½äº§å“å¤§ç±»æ ‘ï¼ˆid -> value,供 el-tree-select ä½¿ç”¨ï¼‰
const loadProductOptions = async () => {
  try {
    const res = await productTreeList();
    productOptions.value = convertIdToValue(Array.isArray(res) ? res : res?.data || []);
  } catch (error) {
    ElMessage.error('获取产品大类失败');
  }
};
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 handleProductChange = async value => {
  form.productModelId = '';
  modelOptions.value = [];
  if (!value) return;
  try {
    const res = await modelList({ id: value });
    modelOptions.value = Array.isArray(res) ? res : res?.data || [];
  } catch (error) {
    ElMessage.error('获取规格型号失败');
  }
};
// æ–°å¢žåˆ†ç±»
const addCategory = async () => {
  await loadDeviceName();
  await loadProductOptions();
  form.id = '';
  form.name = '';
  form.sparePartsNo = '';
  form.productId = '';
  form.productModelId = '';
  form.status = '';
  form.description = '';
  form.deviceLedgerIds = [];
  form.quantity = undefined;
  form.price = null;
  modelOptions.value = [];
  operationType.value = 'add'
  dialogVisible.value = true;
};
@@ -433,7 +506,21 @@
// ç¼–辑分类
const editCategory = async (row) => {
  await loadDeviceName();
  await loadProductOptions();
  Object.assign(form, row);
  form.productId = row.productId ?? '';
  form.productModelId = row.productModelId ?? '';
  // åæ˜¾è§„格型号选项(编辑时不会触发产品大类 change)
  if (form.productId) {
    try {
      const res = await modelList({ id: form.productId });
      modelOptions.value = Array.isArray(res) ? res : res?.data || [];
    } catch (error) {
      modelOptions.value = [];
    }
  } else {
    modelOptions.value = [];
  }
  // å¦‚果后端返回的是 deviceIds å­—符串,需要转换为数组
  if (row.deviceIds && typeof row.deviceIds === 'string') {
    // ç¡®ä¿ID类型与设备选项中的ID类型一致
src/views/equipmentManagement/sparePartsRecord/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,215 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <el-form :inline="true" :model="searchForm">
        <el-form-item label="备件">
          <el-select v-model="searchForm.sparePartsId"
                     placeholder="请选择"
                     clearable
                     filterable
                     style="width: 240px">
            <el-option v-for="item in sparePartsOptions"
                       :key="item.id"
                       :label="item.name"
                       :value="item.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="方向">
          <el-select v-model="searchForm.direction"
                     placeholder="请选择"
                     clearable
                     style="width: 160px">
            <el-option label="入库" :value="1" />
            <el-option label="出库" :value="2" />
          </el-select>
        </el-form-item>
        <el-form-item label="来源类型">
          <el-select v-model="searchForm.sourceType"
                     placeholder="请选择"
                     clearable
                     style="width: 160px">
            <el-option v-for="item in sourceTypeOptions"
                       :key="item.value"
                       :label="item.label"
                       :value="item.value" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleQuery">查询</el-button>
          <el-button @click="resetQuery">重置</el-button>
        </el-form-item>
      </el-form>
      <div>
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
    </div>
    <div class="table_list">
      <el-table :data="tableData"
                border
                v-loading="tableLoading"
                @selection-change="handleSelectionChange"
                :row-key="row => row.id"
                height="calc(100vh - 18.5em)">
        <el-table-column align="center" type="selection" width="55" />
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column label="备件名称" prop="sparePartsName" show-overflow-tooltip />
        <el-table-column label="方向" width="100">
          <template #default="{ row }">
            <el-tag :type="row.direction === 1 || row.direction === '1' ? 'success' : 'warning'"
                    size="small">
              {{ getDirectionText(row.direction) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="数量" prop="quantity" width="120" show-overflow-tooltip />
        <el-table-column label="来源类型" prop="sourceType" width="120" show-overflow-tooltip>
          <template #default="{ row }">
            {{ getSourceTypeText(row.sourceType) }}
          </template>
        </el-table-column>
        <el-table-column label="来源单号" prop="sourceNo" show-overflow-tooltip />
        <el-table-column label="备注" prop="remark" show-overflow-tooltip />
        <el-table-column label="时间" prop="createTime" width="170" show-overflow-tooltip />
      </el-table>
      <pagination v-show="total > 0"
                  :total="total"
                  layout="total, sizes, prev, pager, next, jumper"
                  :page="page.current"
                  :limit="page.size"
                  @pagination="paginationChange" />
    </div>
  </div>
</template>
<script setup>
  import pagination from "@/components/PIMTable/Pagination.vue";
  import { ref, reactive, getCurrentInstance } from "vue";
  import { ElMessageBox } from "element-plus";
  import {
    getSparePartsRecordPage,
    delSparePartsRecord,
  } from "@/api/equipmentManagement/sparePartsRecord.js";
  import { getSparePartsList } from "@/api/equipmentManagement/spareParts.js";
  const { proxy } = getCurrentInstance();
  const tableData = ref([]);
  const selectedRows = ref([]);
  const tableLoading = ref(false);
  const total = ref(0);
  const page = reactive({
    current: 1,
    size: 10,
  });
  const searchForm = ref({
    sparePartsId: "",
    direction: "",
    sourceType: "",
  });
  // å¤‡ä»¶ä¸‹æ‹‰é€‰é¡¹
  const sparePartsOptions = ref([]);
  // æ¥æºç±»åž‹æžšä¸¾
  const sourceTypeOptions = [
    { label: "采购入库", value: 1 },
    { label: "维修领用", value: 2 },
    { label: "保养领用", value: 3 },
  ];
  const getDirectionText = direction => {
    const map = { 1: "入库", 2: "出库" };
    return map[direction] || "";
  };
  const getSourceTypeText = sourceType => {
    return (
      sourceTypeOptions.find(item => item.value === sourceType)?.label || ""
    );
  };
  const getList = () => {
    tableLoading.value = true;
    getSparePartsRecordPage({
      ...searchForm.value,
      current: page.current,
      size: page.size,
    })
      .then(res => {
        tableLoading.value = false;
        const result = res.data || {};
        tableData.value = result.records || [];
        total.value = result.total || 0;
      })
      .catch(() => {
        tableLoading.value = false;
      });
  };
  const loadSparePartsOptions = () => {
    getSparePartsList({ current: 1, size: 1000 })
      .then(res => {
        sparePartsOptions.value = res?.data?.records || [];
      })
      .catch(() => {
        sparePartsOptions.value = [];
      });
  };
  const handleQuery = () => {
    page.current = 1;
    getList();
  };
  const resetQuery = () => {
    searchForm.value = {
      sparePartsId: "",
      direction: "",
      sourceType: "",
    };
    page.current = 1;
    getList();
  };
  const paginationChange = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  const handleSelectionChange = selection => {
    selectedRows.value = selection.filter(item => item.id);
  };
  const handleDelete = () => {
    if (selectedRows.value.length === 0) {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    const ids = selectedRows.value.map(item => item.id);
    ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        delSparePartsRecord(ids).then(() => {
          proxy.$modal.msgSuccess("删除成功");
          getList();
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  getList();
  loadSparePartsOptions();
</script>
<style scoped lang="scss">
  .search_form {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    margin-bottom: 10px;
  }
</style>
src/views/inventoryManagement/dispatchLog/Record.vue
@@ -18,7 +18,7 @@
            </el-form-item>
          </el-col>
          <el-col :span="4">
            <el-form-item label="产品大类"
            <el-form-item label="油品"
                          prop="productName">
              <el-input v-model="searchForm.productName"
                        style="width: 240px"
@@ -102,33 +102,72 @@
                         label="序号"
                         type="index"
                         width="60" />
        <el-table-column label="出库信息"
                         align="center"
                         :header-cell-style="groupHeaderStyle">
        <el-table-column label="出库批次"
                         prop="outboundBatches"
                         min-width="100"
                           min-width="120"
                         show-overflow-tooltip />
        <el-table-column label="出库日期"
                         prop="createTime"
                           prop="outDate"
                           min-width="110"
                         show-overflow-tooltip />
        <el-table-column label="产品大类"
          <el-table-column label="出库类别"
                           prop="outCategory"
                           min-width="90"
                           show-overflow-tooltip />
          <el-table-column label="油库"
                           prop="oilDepotName"
                           min-width="160"
                           show-overflow-tooltip />
          <el-table-column label="储罐号"
                           prop="tankNo"
                           min-width="90"
                           show-overflow-tooltip />
          <el-table-column label="油品"
                         prop="productName"
                           min-width="90"
                           show-overflow-tooltip />
          <el-table-column label="出库吨数(吨)"
                           prop="outTonnage"
                           min-width="110"
                         show-overflow-tooltip />
        <el-table-column label="规格型号"
                         prop="model"
                           min-width="110"
                         show-overflow-tooltip />
        <el-table-column label="批号"
                         prop="batchNo"
                           min-width="140"
                         show-overflow-tooltip />
        <el-table-column label="单位"
                         prop="unit"
                           min-width="70"
                         show-overflow-tooltip />
        <el-table-column label="出库数量"
                         prop="stockOutNum"
                           min-width="100"
                           show-overflow-tooltip />
          <el-table-column label="实际扣减数量"
                           prop="deductQuantity"
                           min-width="120"
                           show-overflow-tooltip />
          <el-table-column label="剩余油量(吨)"
                           prop="residualOil"
                           min-width="110"
                         show-overflow-tooltip />
        <el-table-column label="出库人"
                         prop="createBy"
                           min-width="90"
                           show-overflow-tooltip />
          <el-table-column label="发货单号"
                           prop="shippingNo"
                           min-width="140"
                         show-overflow-tooltip />
        <el-table-column label="来源"
                         prop="recordType"
                           min-width="90"
                         show-overflow-tooltip>
          <template #default="scope">
            {{ getRecordType(scope.row.recordType) }}
@@ -136,12 +175,138 @@
        </el-table-column>
        <el-table-column label="审批状态"
                         prop="approvalStatus"
                           min-width="100"
                         show-overflow-tooltip>
          <template #default="scope">
            <el-tag :type="getApprovalStatusTagType(scope.row.approvalStatus)"
                    size="small">
              {{ getApprovalStatusLabel(scope.row.approvalStatus) }}
            </el-tag>
            </template>
          </el-table-column>
        </el-table-column>
        <el-table-column label="客户信息"
                         align="center"
                         :header-cell-style="groupHeaderStyle">
          <el-table-column label="客户名称"
                           prop="customerName"
                           min-width="160"
                           show-overflow-tooltip />
          <el-table-column label="客户对接人"
                           prop="contactPerson"
                           min-width="100"
                           show-overflow-tooltip />
          <el-table-column label="对接人电话"
                           prop="contactPhone"
                           min-width="120"
                           show-overflow-tooltip />
        </el-table-column>
        <el-table-column label="物流信息"
                         align="center"
                         :header-cell-style="groupHeaderStyle">
          <el-table-column label="司机姓名"
                           prop="driverName"
                           min-width="100"
                           show-overflow-tooltip />
          <el-table-column label="司机电话"
                           prop="driverPhone"
                           min-width="120"
                           show-overflow-tooltip />
          <el-table-column label="押运员姓名"
                           prop="escortName"
                           min-width="100"
                           show-overflow-tooltip />
          <el-table-column label="押运员电话"
                           prop="escortPhone"
                           min-width="120"
                           show-overflow-tooltip />
          <el-table-column label="车牌号"
                           prop="truckPlateNo"
                           min-width="110"
                           show-overflow-tooltip />
          <el-table-column label="车挂牌号"
                           prop="trailerPlateNo"
                           min-width="110"
                           show-overflow-tooltip />
        </el-table-column>
        <el-table-column label="装油操作信息"
                         align="center"
                         :header-cell-style="groupHeaderStyle">
          <el-table-column label="装油检验"
                           prop="loadingInspectionStatus"
                           min-width="90"
                           show-overflow-tooltip />
          <el-table-column label="装车鹤位"
                           prop="loadingCranePosition"
                           min-width="100"
                           show-overflow-tooltip />
          <el-table-column label="密度"
                           prop="density"
                           min-width="80"
                           show-overflow-tooltip />
          <el-table-column label="装油拍照"
                           prop="loadingPhotos"
                           align="center"
                           min-width="90">
            <template #default="scope">
              <el-image v-if="toPhotoArray(scope.row.loadingPhotos).length"
                        :src="getPhotoUrl(toPhotoArray(scope.row.loadingPhotos)[0])"
                        :preview-src-list="getPhotoUrls(scope.row.loadingPhotos)"
                        preview-teleported
                        fit="cover"
                        style="width: 40px; height: 40px; border-radius: 4px" />
              <span v-else>--</span>
            </template>
          </el-table-column>
        </el-table-column>
        <el-table-column label="出库过磅数据"
                         align="center"
                         :header-cell-style="groupHeaderStyle">
          <el-table-column label="毛重(吨)"
                           prop="grossWeight"
                           min-width="90"
                           show-overflow-tooltip />
          <el-table-column label="皮重(吨)"
                           prop="tareWeight"
                           min-width="90"
                           show-overflow-tooltip />
          <el-table-column label="净重(吨)"
                           prop="netWeight"
                           min-width="90"
                           show-overflow-tooltip />
          <el-table-column label="亏量(吨)"
                           prop="lossQuantity"
                           min-width="90"
                           show-overflow-tooltip />
          <el-table-column label="实际出库体积(升)"
                           prop="actualVolume"
                           min-width="130"
                           show-overflow-tooltip />
          <el-table-column label="磅单"
                           prop="poundPhotos"
                           align="center"
                           min-width="90">
            <template #default="scope">
              <el-image v-if="toPhotoArray(scope.row.poundPhotos).length"
                        :src="getPhotoUrl(toPhotoArray(scope.row.poundPhotos)[0])"
                        :preview-src-list="getPhotoUrls(scope.row.poundPhotos)"
                        preview-teleported
                        fit="cover"
                        style="width: 40px; height: 40px; border-radius: 4px" />
              <span v-else>--</span>
            </template>
          </el-table-column>
        </el-table-column>
        <el-table-column fixed="right"
                         label="操作"
                         width="110"
                         align="center">
          <template #default="scope">
            <el-button v-if="isSalesOutRecord(scope.row)"
                       link
                       type="primary"
                       @click="openResidualOil(scope.row)">录入残油
            </el-button>
          </template>
        </el-table-column>
      </el-table>
@@ -152,6 +317,41 @@
                  :limit="page.size"
                  @pagination="paginationChange" />
    </div>
    <!-- æ®‹æ²¹å½•入:送完货后回填剩余油量,只更新 residualOil ä¸€åˆ— -->
    <el-dialog v-model="residualDialogVisible"
               title="录入剩余油量"
               width="30%"
               append-to-body>
      <el-form label-width="130px"
               label-position="top">
        <el-form-item label="出库批次">
          <el-input :model-value="residualForm.outboundBatches || '--'"
                    readonly />
        </el-form-item>
        <el-form-item label="车牌号">
          <el-input :model-value="residualForm.truckPlateNo || '--'"
                    readonly />
        </el-form-item>
        <el-form-item label="出库数量(吨)">
          <el-input :model-value="residualForm.stockOutNum ?? '--'"
                    readonly />
        </el-form-item>
        <el-form-item label="剩余油量(吨)">
          <el-input-number v-model="residualForm.residualOil"
                           :precision="4"
                           :step="1"
                           :min="0"
                           style="width: 100%" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
                     @click="submitResidualOil">确认</el-button>
          <el-button @click="residualDialogVisible = false">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
@@ -166,6 +366,7 @@
    delPendingStockOut,
    batchApproveStockOutRecords,
    batchUnapproveStockOutRecords,
    updateResidualOil,
  } from "@/api/inventoryManagement/stockOut.js";
  import {
    findAllQualifiedStockOutRecordTypeOptions,
@@ -290,6 +491,40 @@
    return "warning";
  };
  // åˆ†ç»„父表头样式:区分各分类
  const groupHeaderStyle = {
    background: "#e8f3ff",
    color: "#1d6fd8",
    fontWeight: "700",
    fontSize: "13px",
  };
  // ç…§ç‰‡å­—段后端存的是 JSON å­—符串,出参需自行解析
  const toPhotoArray = value => {
    if (!value) {
      return [];
    }
    if (Array.isArray(value)) {
      return value;
    }
    if (typeof value === "string") {
      try {
        const parsed = JSON.parse(value);
        return Array.isArray(parsed) ? parsed : [];
      } catch (e) {
        return [];
      }
    }
    return [];
  };
  const getPhotoUrl = photo =>
    typeof photo === "string" ? photo : photo?.url || photo?.previewURL || "";
  // å›¾ç‰‡åˆ—:取全部地址用于预览
  const getPhotoUrls = photos =>
    toPhotoArray(photos).map(getPhotoUrl).filter(Boolean);
  // èŽ·å–æ¥æºç±»åž‹é€‰é¡¹
  const fetchStockRecordTypeOptions = () => {
    if (props.type === "0") {
@@ -349,6 +584,54 @@
  const expandedRowKeys = ref([]);
  // æ®‹æ²¹å½•入:只对销售类出库开放(残油链只对 outCategory='销售' ç”Ÿæ•ˆï¼‰
  const residualDialogVisible = ref(false);
  const residualForm = ref({
    id: null,
    outboundBatches: "",
    truckPlateNo: "",
    stockOutNum: null,
    residualOil: null,
  });
  const isSalesOutRecord = row => row?.outCategory === "销售";
  const openResidualOil = row => {
    residualForm.value = {
      id: row.id,
      outboundBatches: row.outboundBatches || "",
      truckPlateNo: row.truckPlateNo || "",
      stockOutNum: row.stockOutNum ?? null,
      residualOil: row.residualOil ?? null,
    };
    residualDialogVisible.value = true;
  };
  const submitResidualOil = () => {
    const { id, residualOil, stockOutNum } = residualForm.value;
    if (residualOil === null || residualOil === undefined || residualOil === "") {
      proxy.$modal.msgWarning("请输入剩余油量");
      return;
    }
    if (Number(residualOil) < 0) {
      proxy.$modal.msgWarning("剩余油量不能小于0");
      return;
    }
    if (
      stockOutNum !== null &&
      stockOutNum !== undefined &&
      Number(residualOil) > Number(stockOutNum)
    ) {
      proxy.$modal.msgWarning("剩余油量不能大于出库数量");
      return;
    }
    updateResidualOil(id, { residualOil }).then(() => {
      proxy.$modal.msgSuccess("录入成功");
      residualDialogVisible.value = false;
      getList();
    });
  };
  const handleBatchApprove = () => {
    if (selectedRows.value.length === 0) {
      proxy.$modal.msgWarning("请选择数据");
src/views/inventoryManagement/issueManagement/index.vue
@@ -53,8 +53,15 @@
    </div>
    <el-dialog v-model="dialogFormVisible" :title="'新增出库'" width="40%" @close="closeDia">
      <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
        <el-form-item label="出库数量:" prop="salesContractNo">
        <el-form-item label="出库数量:" prop="inboundQuantity">
          <el-input-number :step="0.01" :min="0" style="width: 100%" v-model="form.inboundQuantity" placeholder="请输入" clearable />
        </el-form-item>
        <el-form-item label="出库批次:" prop="batchNo">
          <el-select v-model="form.batchNo" placeholder="请选择出库批次" clearable filterable
            v-loading="batchLoading" style="width: 100%">
            <el-option v-for="item in batchOptions" :key="item.batchNo"
              :label="`${item.batchNo}(可用 ${item.availableQuantity})`" :value="item.batchNo" />
          </el-select>
        </el-form-item>
        <el-form-item label="出库日期:" prop="projectName">
          <el-date-picker style="width: 100%" v-model="form.inboundTime" value-format="YYYY-MM-DD" format="YYYY-MM-DD"
@@ -89,6 +96,7 @@
  getStockManagePage,
    delStockManage,
    stockOut,
    getBatchNoList,
} from "@/api/inventoryManagement/stockManage.js";
const userStore = useUserStore()
@@ -121,6 +129,7 @@
  rules: {
    inboundTime: [{ required: true, message: "请选择", trigger: "change" }],
    inboundQuantity: [{ required: true, message: "请输入", trigger: "blur" }],
    batchNo: [{ required: true, message: "请选择出库批次", trigger: "change" }],
    nickname: [{ required: true, message: "请选择", trigger: "change" }]
  }
})
@@ -179,6 +188,10 @@
const currentRowNum = ref(0)
const salesLedgerProductId = ref(null);
const currentRowProductModelId = ref(null)
// å‡ºåº“批次可选项(可用量 = åº“存量 - é”å®šæ•°é‡ï¼‰
const batchOptions = ref([])
const batchLoading = ref(false)
// æ‰“开弹框
const openForm = async (row) => {
@@ -186,15 +199,18 @@
  currentRowId.value = row.id
  currentRowNum.value = row.inboundNum0
  salesLedgerProductId.value = row.salesLedgerProductId
  currentRowProductModelId.value = row.productModelId
  form.value = {}
  // åˆå§‹åŒ–表单数据
  form.value = {
    productrecordId: '',
    inboundQuantity: '', // å‡ºåº“数量清空
    batchNo: '', // å‡ºåº“批次清空
    inboundTime: getCurrentDate(), // é»˜è®¤å½“前日期
    nickName: '', // é»˜è®¤å½“前用户
  }
  console.log('form',form.value)
  loadBatchOptions()
  // åŠ è½½ç”¨æˆ·åˆ—è¡¨
  try {
    const userLists = await userListNoPageByTenantId()
@@ -204,17 +220,47 @@
  }
}
// åŠ è½½è§„æ ¼åž‹å·ä¸‹çš„å¯ç”¨æ‰¹æ¬¡
const loadBatchOptions = async () => {
  batchOptions.value = []
  if (!currentRowProductModelId.value) {
    proxy.$modal.msgWarning('当前记录缺少规格型号,无法查询出库批次')
    return
  }
  batchLoading.value = true
  try {
    const res = await getBatchNoList(currentRowProductModelId.value)
    batchOptions.value = res.data || []
  } catch (error) {
    console.error('查询出库批次失败:', error)
  } finally {
    batchLoading.value = false
  }
}
// æäº¤è¡¨å•
const submitForm = () => {
  let num = Number(form.value.inboundQuantity)
  if(num <= 0 || num > currentRowNum.value){
    return proxy.$modal.msgWarning("请填入有效数字")
  }
  // åº“存按批次管理、无先进先出,数量不能超过所选批次的可用量
  const selectedBatch = batchOptions.value.find(
    item => item.batchNo === form.value.batchNo
  )
  if (selectedBatch && num > Number(selectedBatch.availableQuantity)) {
    return proxy.$modal.msgWarning(
      `出库数量不能超过批次「${selectedBatch.batchNo}」的可用量 ${selectedBatch.availableQuantity}`
    )
  }
  proxy.$refs["formRef"].validate(valid => {
    if (valid && currentRowId.value) {
      const outData = {
        id: currentRowId.value, // åŽŸå§‹è®°å½•ID
        type: 2, // 2=销售出库,出库即自动生成发货台账
        salesLedgerProductId: salesLedgerProductId.value,
        productModelId: currentRowProductModelId.value,
        batchNo: form.value.batchNo, // å‡ºåº“批次(销售出库必填)
        quantity: form.value.inboundQuantity, // å‡ºåº“数量
        time: form.value.inboundTime, // å‡ºåº“æ—¶é—´
        userId: form.value.nickName // æ“ä½œäºº
src/views/inventoryManagement/receiptManagement/Record.vue
@@ -18,7 +18,7 @@
            </el-form-item>
          </el-col>
          <el-col :span="4">
            <el-form-item label="产品大类"
            <el-form-item label="油品"
                          prop="productName">
              <el-input v-model="searchForm.productName"
                        style="width: 240px"
@@ -103,33 +103,60 @@
                         label="序号"
                         type="index"
                         width="60" />
        <el-table-column label="入库信息"
                         align="center"
                         :header-cell-style="groupHeaderStyle">
        <el-table-column label="入库批次"
                         prop="inboundBatches"
                         width="200"
                           min-width="150"
                         show-overflow-tooltip />
        <el-table-column label="入库时间"
                         prop="createTime"
                           prop="inboundTime"
                           min-width="160"
                         show-overflow-tooltip />
        <el-table-column label="产品大类"
          <el-table-column label="入库类别"
                           prop="inboundCategory"
                           min-width="100"
                           show-overflow-tooltip />
          <el-table-column label="油库"
                           prop="oilDepotName"
                           min-width="130"
                           show-overflow-tooltip />
          <el-table-column label="储罐号"
                           prop="tankNo"
                           min-width="90"
                           show-overflow-tooltip />
          <el-table-column label="油品"
                         prop="productName"
                           min-width="90"
                           show-overflow-tooltip />
          <el-table-column label="原厂数量(吨)"
                           prop="originalQuantity"
                           min-width="110"
                         show-overflow-tooltip />
        <el-table-column label="规格型号"
                         prop="model"
                           min-width="110"
                         show-overflow-tooltip />
        <el-table-column label="批号"
                         prop="batchNo"
                           min-width="140"
                         show-overflow-tooltip />
        <el-table-column label="单位"
                         prop="unit"
                           min-width="70"
                         show-overflow-tooltip />
        <el-table-column label="入库数量"
                         prop="stockInNum"
                           min-width="100"
                         show-overflow-tooltip />
        <el-table-column label="入库人"
                         prop="createBy"
                           min-width="90"
                         show-overflow-tooltip />
        <el-table-column label="来源"
                         prop="recordType"
                           min-width="90"
                         show-overflow-tooltip>
          <template #default="scope">
            {{ getRecordType(scope.row.recordType) }}
@@ -137,7 +164,7 @@
        </el-table-column>
        <el-table-column v-if="showSourceOrderNoColumn"
                         label="源单号"
                         width="150"
                           min-width="150"
                         prop="sourceOrderNo"
                         show-overflow-tooltip>
          <template #default="scope">
@@ -146,6 +173,7 @@
        </el-table-column>
        <el-table-column label="审批状态"
                         prop="approvalStatus"
                           min-width="100"
                         show-overflow-tooltip>
          <template #default="scope">
            <el-tag :type="getApprovalStatusTagType(scope.row.approvalStatus)"
@@ -153,6 +181,119 @@
              {{ getApprovalStatusLabel(scope.row.approvalStatus) }}
            </el-tag>
          </template>
          </el-table-column>
        </el-table-column>
        <el-table-column label="客户信息"
                         align="center"
                         :header-cell-style="groupHeaderStyle">
          <el-table-column label="客户名称"
                           prop="customerName"
                           min-width="160"
                           show-overflow-tooltip />
          <el-table-column label="客户对接人"
                           prop="contactPerson"
                           min-width="100"
                           show-overflow-tooltip />
          <el-table-column label="对接人电话"
                           prop="contactPhone"
                           min-width="120"
                           show-overflow-tooltip />
        </el-table-column>
        <el-table-column label="物流信息"
                         align="center"
                         :header-cell-style="groupHeaderStyle">
          <el-table-column label="司机姓名"
                           prop="driverName"
                           min-width="100"
                           show-overflow-tooltip />
          <el-table-column label="司机电话"
                           prop="driverPhone"
                           min-width="120"
                           show-overflow-tooltip />
          <el-table-column label="押运员姓名"
                           prop="escortName"
                           min-width="100"
                           show-overflow-tooltip />
          <el-table-column label="押运员电话"
                           prop="escortPhone"
                           min-width="120"
                           show-overflow-tooltip />
          <el-table-column label="车牌号"
                           prop="truckPlateNo"
                           min-width="110"
                           show-overflow-tooltip />
          <el-table-column label="车挂牌号"
                           prop="trailerPlateNo"
                           min-width="110"
                           show-overflow-tooltip />
        </el-table-column>
        <el-table-column label="卸油操作信息"
                         align="center"
                         :header-cell-style="groupHeaderStyle">
          <el-table-column label="车体/铅封检验"
                           prop="tankInspectionStatus"
                           min-width="120"
                           show-overflow-tooltip />
          <el-table-column label="卸车鹤位"
                           prop="unloadingCranePosition"
                           min-width="100"
                           show-overflow-tooltip />
          <el-table-column label="密度"
                           prop="density"
                           min-width="80"
                           show-overflow-tooltip />
          <el-table-column label="卸油拍照"
                           prop="unloadPhotos"
                           align="center"
                           min-width="90">
            <template #default="scope">
              <el-image v-if="toPhotoArray(scope.row.unloadPhotos).length"
                        :src="getPhotoUrl(toPhotoArray(scope.row.unloadPhotos)[0])"
                        :preview-src-list="getPhotoUrls(scope.row.unloadPhotos)"
                        preview-teleported
                        fit="cover"
                        style="width: 40px; height: 40px; border-radius: 4px" />
              <span v-else>--</span>
            </template>
          </el-table-column>
        </el-table-column>
        <el-table-column label="过磅数据"
                         align="center"
                         :header-cell-style="groupHeaderStyle">
          <el-table-column label="毛重(吨)"
                           prop="grossWeight"
                           min-width="90"
                           show-overflow-tooltip />
          <el-table-column label="皮重(吨)"
                           prop="tareWeight"
                           min-width="90"
                           show-overflow-tooltip />
          <el-table-column label="净重(吨)"
                           prop="netWeight"
                           min-width="90"
                           show-overflow-tooltip />
          <el-table-column label="亏量(吨)"
                           prop="lossQuantity"
                           min-width="90"
                           show-overflow-tooltip />
          <el-table-column label="实际入库体积(升)"
                           prop="actualVolume"
                           min-width="130"
                           show-overflow-tooltip />
          <el-table-column label="磅单"
                           prop="poundPhotos"
                           align="center"
                           min-width="90">
            <template #default="scope">
              <el-image v-if="toPhotoArray(scope.row.poundPhotos).length"
                        :src="getPhotoUrl(toPhotoArray(scope.row.poundPhotos)[0])"
                        :preview-src-list="getPhotoUrls(scope.row.poundPhotos)"
                        preview-teleported
                        fit="cover"
                        style="width: 40px; height: 40px; border-radius: 4px" />
              <span v-else>--</span>
            </template>
          </el-table-column>
        </el-table-column>
      </el-table>
      <pagination v-show="total > 0"
@@ -356,6 +497,40 @@
      });
  };
  // åˆ†ç»„父表头样式:区分各分类
  const groupHeaderStyle = {
    background: "#e8f3ff",
    color: "#1d6fd8",
    fontWeight: "700",
    fontSize: "13px",
  };
  // ç…§ç‰‡å­—段后端存的是 JSON å­—符串,出参需自行解析
  const toPhotoArray = value => {
    if (!value) {
      return [];
    }
    if (Array.isArray(value)) {
      return value;
    }
    if (typeof value === "string") {
      try {
        const parsed = JSON.parse(value);
        return Array.isArray(parsed) ? parsed : [];
      } catch (e) {
        return [];
      }
    }
    return [];
  };
  const getPhotoUrl = photo =>
    typeof photo === "string" ? photo : photo?.url || photo?.previewURL || "";
  // å›¾ç‰‡åˆ—:取全部地址用于预览
  const getPhotoUrls = photos =>
    toPhotoArray(photos).map(getPhotoUrl).filter(Boolean);
  // èŽ·å–æ¥æºç±»åž‹é€‰é¡¹
  const fetchStockRecordTypeOptions = () => {
    if (props.type === "0") {
src/views/inventoryManagement/stockManagement/BatchNoQtyDetail.vue
@@ -21,7 +21,7 @@
              height="100%"
            >
          <el-table-column
            label="产品名称"
            label="油品"
            prop="productName"
            show-overflow-tooltip
          />
@@ -34,18 +34,8 @@
            show-overflow-tooltip
          />
          <el-table-column
            label="不合格库存数量"
            prop="unQualifiedQuantity"
            show-overflow-tooltip
          />
          <el-table-column
            label="合格冻结数量"
            prop="qualifiedLockedQuantity"
            show-overflow-tooltip
          />
          <el-table-column
            label="不合格冻结数量"
            prop="unQualifiedLockedQuantity"
            show-overflow-tooltip
          />
          <el-table-column
@@ -111,7 +101,7 @@
          <el-table-column label="入库信息"
                           align="center"
                           :header-cell-style="groupHeaderStyle">
            <el-table-column label="油品" prop="oilProduct" min-width="90" show-overflow-tooltip />
            <el-table-column label="油品" prop="productName" min-width="90" show-overflow-tooltip />
            <el-table-column label="入库名称" prop="oilDepotName" min-width="130" show-overflow-tooltip />
            <el-table-column label="入库类别" prop="inboundCategory" min-width="100" show-overflow-tooltip />
            <el-table-column label="储罐号" prop="tankNo" min-width="100" show-overflow-tooltip />
@@ -191,7 +181,7 @@
            <el-table-column label="储库名称" prop="outOilDepotName" min-width="130" show-overflow-tooltip />
            <el-table-column label="储罐号" prop="outTankNo" min-width="100" show-overflow-tooltip />
            <el-table-column label="出库日期" prop="outDate" min-width="110" show-overflow-tooltip />
            <el-table-column label="油品" prop="outOilProduct" min-width="90" show-overflow-tooltip />
            <el-table-column label="油品" prop="productName" min-width="90" show-overflow-tooltip />
            <el-table-column label="出库吨位(吨)" prop="outTonnage" min-width="110" show-overflow-tooltip />
            <el-table-column label="客户名称" prop="outCustomerName" min-width="120" show-overflow-tooltip />
            <el-table-column label="客户对接人" prop="outContactPerson" min-width="100" show-overflow-tooltip />
src/views/inventoryManagement/stockManagement/OilStockInDialog.vue
@@ -151,22 +151,10 @@
          <el-col :span="12">
            <el-form-item label="油品"
                          prop="oilProduct"
                          :rules="isOilProduct ? [
                {
                required: true,
                message: '请选择油品',
                trigger: 'change',
              }
            ] : []">
              <el-input v-if="isOilProduct"
                        :model-value="formState.oilProduct"
                          :rules="[{ required: true, message: '请选择油品', trigger: 'change' }]">
              <el-input :model-value="formState.oilProduct"
                        readonly
                        placeholder="--" />
              <el-select v-else
                         v-model="formState.oilProduct"
                         placeholder="请选择"
                         disabled
                         style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
@@ -461,7 +449,6 @@
  import { allOilDepot } from "@/api/basicData/oilDepot.js";
  import { allTankInfo } from "@/api/basicData/tankInfo.js";
  import { listCustomer } from "@/api/basicData/customer.js";
  import { productTreeList } from "@/api/basicData/product.js";
  const props = defineProps({
    visible: {
@@ -507,8 +494,6 @@
  const oilDepotList = ref([]);
  const tankList = ref([]);
  const customerList = ref([]);
  const oilProductOptions = ref([]);
  const isShow = computed({
    get() {
      return props.visible;
@@ -517,32 +502,6 @@
      emit("update:visible", val);
    },
  });
  // åœ¨äº§å“æ ‘中查找"油品"分类节点
  const findOilNode = nodes => {
    for (const node of nodes || []) {
      if (node.productName === "油品") {
        return node;
      }
      if (node.children && node.children.length) {
        const found = findOilNode(node.children);
        if (found) {
          return found;
        }
      }
    }
    return null;
  };
  // æ‰€é€‰å•†å“åç§°å‘½ä¸­çš„æ²¹å“é€‰é¡¹ï¼ˆæ²¹å“èŠ‚ç‚¹å­é¡¹ï¼Œæ¥è‡ªäº§å“ç»´æŠ¤æ¨¡å—ï¼‰
  const matchedOilOption = computed(() =>
    oilProductOptions.value.find(
      item => item.productName === formState.value.productName
    )
  );
  // åªæœ‰é€‰ä¸­æ²¹å“å­—段选项里有的具体油品才启用油品字段;"油品"大类本身不启用
  const isOilProduct = computed(() => !!matchedOilOption.value);
  // å‡€é‡(吨) = æ¯›é‡ - çš®é‡
  const netWeight = computed(() => {
@@ -609,10 +568,9 @@
      formState.value.productModelName = product.model;
      formState.value.productModelId = product.id;
      formState.value.unit = product.unit;
      // æ²¹å“é€‰é¡¹ä¸Žå•†å“åç§°åŒæ­¥ï¼šå‘½ä¸­æ²¹å“é€‰é¡¹åˆ™è‡ªåŠ¨å¸¦å‡ºè¯¥æ²¹å“ï¼Œæœªå‘½ä¸­åˆ™æ¸…ç©ºå¹¶ç¦ç”¨
      formState.value.oilProduct = matchedOilOption.value
        ? product.productName
        : undefined;
      // æœ¬ç§Ÿæˆ·ã€Œæ²¹å“ã€å°±æ˜¯äº§å“åç§°ï¼ˆåº“存明细/出库台账的油品列绑的都是 productName),
      // ç›´æŽ¥å–所选产品名,不再去产品树里比对,避免比对不上时油品栏空白
      formState.value.oilProduct = product.productName || undefined;
      showProductSelectDialog.value = false;
      // è§¦å‘表单验证更新
      proxy.$refs["formRef"]?.validateField("productModelId");
@@ -652,10 +610,6 @@
    });
    listCustomer({ current: 1, size: 1000, type: 0 }).then(res => {
      customerList.value = res?.data?.records || [];
    });
    productTreeList().then(res => {
      const oilNode = findOilNode(res);
      oilProductOptions.value = oilNode?.children || [];
    });
  });
</script>
src/views/inventoryManagement/stockManagement/OilStockOutDialog.vue
@@ -44,7 +44,7 @@
        </el-row>
        <div class="form-section-title">出库信息</div>
        <el-row :gutter="30">
          <el-col :span="12">
          <el-col :span="8">
            <el-form-item label="出库类别"
                          prop="outCategory"
                          :rules="[{ required: true, message: '请选择出库类别', trigger: 'change' }]">
@@ -61,7 +61,25 @@
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
          <el-col :span="8">
            <el-form-item label="出库批次"
                          prop="batchNo"
                          :rules="isSalesOut
                ? [{ 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-form-item label="储库名称"
                          prop="oilDepotId"
                          :rules="[{ required: true, message: '请选择储库名称', trigger: 'change' }]">
@@ -130,22 +148,10 @@
          <el-col :span="12">
            <el-form-item label="油品"
                          prop="oilProduct"
                          :rules="isOilProduct ? [
                {
                required: true,
                message: '请选择油品',
                trigger: 'change',
              }
            ] : []">
              <el-input v-if="isOilProduct"
                        :model-value="formState.oilProduct"
                          :rules="[{ required: true, message: '请选择油品', trigger: 'change' }]">
              <el-input :model-value="formState.oilProduct"
                        readonly
                        placeholder="--" />
              <el-select v-else
                         v-model="formState.oilProduct"
                         placeholder="请选择"
                         disabled
                         style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
@@ -317,6 +323,45 @@
        <div class="form-section-title">物流信息</div>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="车辆"
                          prop="vehicleId"
                          :rules="isSalesOut
                ? [{ required: true, message: '销售类出库必须选择车辆', trigger: 'change' }]
                : []">
              <el-select v-model="formState.vehicleId"
                         placeholder="请选择"
                         clearable
                         filterable
                         popper-class="vehicle-select-popper"
                         style="width: 100%"
                         @change="vehicleChange">
                <el-option v-for="item in vehicleList"
                           :key="item.id"
                           :label="item.plateNumber"
                           :value="item.id">
                  <div class="vehicle-option">
                    <div class="vehicle-option-title">{{ item.plateNumber }}</div>
                    <div class="vehicle-option-line">默认司机:{{ item.driverName || '--' }}</div>
                    <div class="vehicle-option-line">司机电话:{{ item.driverPhone || '--' }}</div>
                    <div class="vehicle-option-line">核定载重(吨):{{ item.loadCapacity ?? '--' }}</div>
                  </div>
                </el-option>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="车挂牌号"
                          prop="trailerPlateNo"
                          :rules="[{ required: true, message: '请输入车挂牌号', trigger: 'blur' }]">
              <el-input v-model="formState.trailerPlateNo"
                        :readonly="!!formState.vehicleId"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="司机姓名"
                          prop="driverName"
                          :rules="[{ required: true, message: '请输入司机姓名', trigger: 'blur' }]">
@@ -365,23 +410,16 @@
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="车牌号"
                          prop="truckPlateNo"
                          :rules="[{ required: true, message: '请输入车牌号', trigger: 'blur' }]">
              <el-input v-model="formState.truckPlateNo"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="车挂牌号"
                          prop="trailerPlateNo"
                          :rules="[{ required: true, message: '请输入车挂牌号', trigger: 'blur' }]">
              <el-input v-model="formState.trailerPlateNo"
                        placeholder="请输入"
                        clearable />
        <el-row :gutter="30"
                v-if="isSalesOut">
          <el-col :span="24">
            <el-form-item label="可用量">
              <div class="available-qty-tip">
                <span>上次剩余(吨):{{ availableQtyText.lastResidualOil }}</span>
                <span>当前库存(吨):{{ availableQtyText.stockQuantity }}</span>
                <span class="available-qty-strong">可用量(吨):{{ availableQtyText.availableQty }}</span>
                <span class="available-qty-rule">可用量 = ä¸Šæ¬¡å‰©ä½™ + å½“前库存</span>
              </div>
            </el-form-item>
          </el-col>
        </el-row>
@@ -402,14 +440,18 @@
</template>
<script setup>
  import { ref, computed, getCurrentInstance, onMounted } from "vue";
  import { ref, computed, watch, getCurrentInstance, onMounted } from "vue";
  import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
  import ImageUpload from "@/components/AttachmentUpload/image/index.vue";
  import { addOilOutRecordOnly } from "@/api/inventoryManagement/stockInventory.js";
  import {
    addOilOutRecordOnly,
    getStockInventoryBatchNoQty,
  } from "@/api/inventoryManagement/stockInventory.js";
  import { getOilOutAvailableQty } from "@/api/inventoryManagement/stockOut.js";
  import { allOilDepot } from "@/api/basicData/oilDepot.js";
  import { allTankInfo } from "@/api/basicData/tankInfo.js";
  import { allVehicle } from "@/api/basicData/vehicle.js";
  import { listCustomer } from "@/api/basicData/customer.js";
  import { productTreeList } from "@/api/basicData/product.js";
  const props = defineProps({
    visible: {
@@ -427,6 +469,7 @@
    model: "",
    unit: "",
    outCategory: undefined,
    batchNo: undefined,
    oilDepotId: undefined,
    oilDepotName: "",
    tankId: undefined,
@@ -448,6 +491,7 @@
    driverPhone: "",
    escortName: "",
    escortPhone: "",
    vehicleId: undefined,
    truckPlateNo: "",
    trailerPlateNo: "",
  });
@@ -457,34 +501,67 @@
  const oilDepotList = ref([]);
  const tankList = ref([]);
  const customerList = ref([]);
  const oilProductOptions = ref([]);
  const vehicleList = ref([]);
  const showProductSelectDialog = ref(false);
  // å‡ºåº“批次候选取该规格的入库批次
  const batchNoOptions = ref([]);
  // é”€å”®ç±»å‡ºåº“的可用量提示(上次剩余 + å½“前库存)
  const availableQty = ref(null);
  // åœ¨äº§å“æ ‘中查找"油品"分类节点
  const findOilNode = nodes => {
    for (const node of nodes || []) {
      if (node.productName === "油品") {
        return node;
  const fetchBatchNoOptions = () => {
    batchNoOptions.value = [];
    const productModelId = formState.value.productModelId;
    if (!productModelId) {
      return;
      }
      if (node.children && node.children.length) {
        const found = findOilNode(node.children);
        if (found) {
          return found;
        }
      }
    }
    return null;
    getStockInventoryBatchNoQty({
      current: 1,
      size: 200,
      productModelId,
    }).then(res => {
      const records = res?.data?.records || [];
      batchNoOptions.value = [
        ...new Set(records.map(item => item.batchNo).filter(Boolean)),
      ];
    });
  };
  // æ‰€é€‰å•†å“åç§°å‘½ä¸­çš„æ²¹å“é€‰é¡¹ï¼ˆæ²¹å“èŠ‚ç‚¹å­é¡¹ï¼Œæ¥è‡ªäº§å“ç»´æŠ¤æ¨¡å—ï¼‰
  const matchedOilOption = computed(() =>
    oilProductOptions.value.find(
      item => item.productName === formState.value.productName
    )
  );
  // é”€å”®ç±»å‡ºåº“必须给出库批次(后端新增时即校验,且审批通过会按批次扣库存)
  const isSalesOut = computed(() => formState.value.outCategory === "销售");
  // åªæœ‰é€‰ä¸­æ²¹å“å­—段选项里有的具体油品才启用油品字段;"油品"大类本身不启用
  const isOilProduct = computed(() => !!matchedOilOption.value);
  // å¯ç”¨é‡æç¤ºï¼šè½¦è¾† + è§„格(+批次)确定后向后端取「上次剩余 / å½“前库存 / å¯ç”¨é‡ã€
  const fetchAvailableQty = () => {
    availableQty.value = null;
    const { vehicleId, productModelId, batchNo } = formState.value;
    if (!isSalesOut.value || !vehicleId || !productModelId) {
      return;
    }
    getOilOutAvailableQty({ vehicleId, productModelId, batchNo })
      .then(res => {
        availableQty.value = res?.data || null;
      })
      .catch(() => {
        availableQty.value = null;
      });
  };
  const availableQtyText = computed(() => ({
    lastResidualOil: availableQty.value?.lastResidualOil ?? "--",
    stockQuantity: availableQty.value?.stockQuantity ?? "--",
    availableQty: availableQty.value?.availableQty ?? "--",
  }));
  watch(
    () => [
      formState.value.outCategory,
      formState.value.vehicleId,
      formState.value.productModelId,
      formState.value.batchNo,
    ],
    () => {
      fetchAvailableQty();
    }
  );
  const isShow = computed({
    get() {
@@ -549,10 +626,11 @@
      formState.value.model = product.model;
      formState.value.productModelId = product.id;
      formState.value.unit = product.unit;
      // æ²¹å“é€‰é¡¹ä¸Žå•†å“åç§°åŒæ­¥ï¼šå‘½ä¸­æ²¹å“é€‰é¡¹åˆ™è‡ªåŠ¨å¸¦å‡ºè¯¥æ²¹å“ï¼Œæœªå‘½ä¸­åˆ™æ¸…ç©ºå¹¶ç¦ç”¨
      formState.value.oilProduct = matchedOilOption.value
        ? product.productName
        : undefined;
      // æœ¬ç§Ÿæˆ·ã€Œæ²¹å“ã€å°±æ˜¯äº§å“åç§°ï¼ˆåº“存明细/出库台账的油品列绑的都是 productName),
      // ç›´æŽ¥å–所选产品名,不再去产品树里比对,避免比对不上时油品栏空白
      formState.value.oilProduct = product.productName || undefined;
      formState.value.batchNo = undefined;
      fetchBatchNoOptions();
      showProductSelectDialog.value = false;
      proxy.$refs["formRef"]?.validateField("productModelId");
    }
@@ -586,6 +664,21 @@
    }
  };
  // è½¦è¾†é€‰æ‹©æ”¹å˜ï¼šè½¦ç‰Œå·ã€è½¦æŒ‚牌号与司机信息由车辆主数据带出,出库时司机可手动覆盖
  const vehicleChange = id => {
    const vehicle = vehicleList.value.find(item => item.id === id);
    if (vehicle) {
      formState.value.truckPlateNo = vehicle.plateNumber || "";
      // è½¦æŒ‚牌号与车牌号是同一个牌,选了车辆就带出并置为只读,避免手填出两个不一致的值
      formState.value.trailerPlateNo = vehicle.plateNumber || "";
      formState.value.driverName = vehicle.driverName || "";
      formState.value.driverPhone = vehicle.driverPhone || "";
    } else {
      formState.value.truckPlateNo = "";
      formState.value.trailerPlateNo = "";
    }
  };
  const handleSubmit = () => {
    proxy.$refs["formRef"].validate(valid => {
      if (valid) {
@@ -615,15 +708,14 @@
    allOilDepot().then(res => {
      oilDepotList.value = res?.data || [];
    });
    productTreeList().then(res => {
      const oilNode = findOilNode(res);
      oilProductOptions.value = oilNode?.children || [];
    });
    allTankInfo().then(res => {
      tankList.value = res?.data || [];
    });
    listCustomer({ current: 1, size: 1000, type: 0 }).then(res => {
      customerList.value = res?.data?.records || [];
    });
    allVehicle().then(res => {
      vehicleList.value = res?.data || [];
    });
  });
</script>
@@ -671,6 +763,48 @@
    }
    .tank-option-line {
      font-size: 12px;
      color: #909399;
      line-height: 1.8;
    }
  }
  // å¯ç”¨é‡æç¤ºï¼šä¸Šæ¬¡å‰©ä½™ + å½“前库存 = å¯ç”¨é‡
  .available-qty-tip {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 20px;
    width: 100%;
    padding: 6px 14px;
    font-size: 13px;
    color: #303133;
    background: rgba(64, 158, 255, 0.08);
    border-left: 3px solid #409eff;
    border-radius: 3px;
    .available-qty-strong {
      font-weight: 600;
      color: #1d6fd8;
    }
    .available-qty-rule {
      color: #909399;
    }
  }
  // è½¦è¾†ä¸‹æ‹‰é€‰é¡¹ï¼šå°å¡ç‰‡æ ·å¼
  .vehicle-option {
    padding: 8px 10px;
    .vehicle-option-title {
      font-size: 14px;
      font-weight: 600;
      color: #303133;
      margin-bottom: 4px;
    }
    .vehicle-option-line {
      font-size: 12px;
      color: #909399;
      line-height: 1.8;
@@ -737,6 +871,26 @@
    }
  }
  // è½¦è¾†ä¸‹æ‹‰é¢æ¿ï¼ˆbody ä¸‹ï¼Œéœ€å…¨å±€æ ·å¼ï¼‰
  .vehicle-select-popper {
    .el-select-dropdown__item {
      height: auto;
      line-height: normal;
      padding: 4px 0;
    }
    .vehicle-option {
      border: 1px solid #e4e7ed;
      border-radius: 6px;
      background-color: #fafbfc;
      &:hover {
        border-color: #409eff;
        background-color: #f0f7ff;
      }
    }
  }
  // å®¢æˆ·åç§°ä¸‹æ‹‰é¢æ¿ï¼ˆbody ä¸‹ï¼Œéœ€å…¨å±€æ ·å¼ï¼‰
  .customer-select-popper {
    .el-select-dropdown__item {
src/views/inventoryManagement/stockManagement/Record.vue
@@ -3,37 +3,34 @@
    <div class="search_form mb10">
      <el-form ref="searchFormRef"
               :model="searchForm"
               class="demo-form-inline">
               class="demo-form-inline"
               style="width: 100%">
        <el-row :gutter="20">
          <el-col :span="4">
          <el-col :span="6">
            <el-form-item label="产品大类"
                          prop="productName">
              <el-input v-model="searchForm.productName"
                        style="width: 240px"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="4">
          <el-col :span="6">
            <el-form-item label="规格型号"
                          prop="model">
              <el-input v-model="searchForm.model"
                        style="width: 240px"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="4">
          <el-col :span="6">
            <el-form-item label="批号"
                          prop="batchNo">
              <el-input v-model="searchForm.batchNo"
                        style="width: 240px"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <!-- æŒ‰é’® -->
          <el-col :span="4">
          <el-col :span="6">
            <el-form-item>
              <el-button type="primary"
                         @click="getList">
@@ -46,7 +43,11 @@
          </el-col>
        </el-row>
      </el-form>
      <div style="display: flex; gap: 10px; flex-wrap: nowrap;">
    </div>
    <div class="table_list">
      <div class="actions">
        <div></div>
        <div>
        <el-button type="primary"
                   @click="isShowNewModal = true">新增库存</el-button>
        <el-button type="primary"
@@ -62,7 +63,6 @@
        <el-button @click="handleOut">导出</el-button>
      </div>
    </div>
    <div class="table_list">
      <el-table :data="tableData"
                border
                v-loading="tableLoading"
@@ -71,18 +71,30 @@
                :row-key="(row, index) => index"
                style="width: 100%"
                :row-class-name="tableRowClassName"
                height="calc(100vh - 18.5em)">
                :span-method="spanMethod"
                height="calc(100vh - 21em)">
        <el-table-column align="center"
                         type="selection"
                         :selectable="isRowSelectable"
                         width="55" />
        <el-table-column align="center"
                         label="序号"
                         type="index"
                         :index="indexMethod"
                         width="60" />
        <el-table-column label="产品名称"
                         prop="productName"
                         min-width="120"
                         show-overflow-tooltip />
                         min-width="120">
          <template #default="scope">
            <div v-if="scope.row.__isGroup"
                 class="tank-group-band">
              <span class="tank-group-band__name">储罐号 {{ scope.row.tankNo || "未分配" }}</span>
              <span class="tank-group-band__stat">合格 {{ scope.row.qualifiedQuantity }}</span>
              <span class="tank-group-band__stat">冻结 {{ scope.row.qualifiedLockedQuantity }}</span>
            </div>
            <span v-else>{{ scope.row.productName }}</span>
          </template>
        </el-table-column>
        <el-table-column label="规格型号"
                         prop="model"
                         min-width="120"
@@ -95,30 +107,26 @@
                         prop="batchNo"
                         min-width="150"
                         show-overflow-tooltip />
        <el-table-column label="储罐号"
                         prop="tankNo"
                         min-width="100"
                         show-overflow-tooltip />
        <el-table-column label="合格库存数量"
                         prop="qualifiedQuantity"
                         min-width="110"
                         show-overflow-tooltip />
        <el-table-column label="不合格库存数量"
                         prop="unQualifiedQuantity"
                         min-width="120"
                         show-overflow-tooltip />
        <el-table-column label="合格冻结数量"
                         prop="qualifiedLockedQuantity"
                         min-width="110"
                         show-overflow-tooltip />
        <el-table-column label="不合格冻结数量"
                         prop="unQualifiedLockedQuantity"
                         min-width="120"
                         show-overflow-tooltip />
        <el-table-column label="库存预警数量"
                         prop="warnNum"
                         min-width="110"
                         show-overflow-tooltip />
        <el-table-column label="备注"
                         prop="remark"
                         min-width="150"
                         show-overflow-tooltip />
<!--        <el-table-column label="备注"-->
<!--                         prop="remark"-->
<!--                         min-width="150"-->
<!--                         show-overflow-tooltip />-->
        <el-table-column label="最近更新时间"
                         prop="updateTime"
                         min-width="160"
@@ -179,7 +187,7 @@
<script setup>
  import pagination from "@/components/PIMTable/Pagination.vue";
  import { ref, reactive, toRefs, onMounted, getCurrentInstance } from "vue";
  import { ref, reactive, toRefs, computed, onMounted, getCurrentInstance } from "vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import { getStockInventoryListPageCombined } from "@/api/inventoryManagement/stockInventory.js";
  const props = defineProps({
@@ -187,6 +195,10 @@
      type: Number,
      required: true,
      default: 0,
    },
    productName: {
      type: String,
      default: "",
    },
  });
@@ -221,6 +233,72 @@
    size: 100,
  });
  const total = ref(0);
  // å‚¨ç½å·åˆ†ç±»è¡Œå æ»¡æ•´è¡Œï¼Œæ˜Žç»†è¡Œæ­£å¸¸æ¸²æŸ“
  const TABLE_COLUMN_COUNT = 13;
  const GROUP_ROW_LEAD_COLUMNS = 2; // é€‰æ‹©åˆ— + åºå·åˆ—
  const isRowSelectable = row => !row?.__isGroup;
  // åªæœ‰æˆå“ã€åŽŸæ–™ä¸¤ç±»ä¼šæŒ‰å‚¨ç½åˆ†ç½å­˜æ”¾ï¼Œå…¶ä½™å¤§ç±»ä¸åšå‚¨ç½å·åˆ†ç»„
  const TANK_GROUP_CATEGORIES = ["成品", "原料"];
  const shouldGroupByTank = computed(() =>
    TANK_GROUP_CATEGORIES.includes(props.productName)
  );
  const sumBy = (rows, key) =>
    rows.reduce((acc, row) => acc + Number(row[key] || 0), 0);
  const buildGroupedRows = rows => {
    const groups = new Map();
    rows.forEach(row => {
      const tankNo = row.tankNo || "";
      if (!groups.has(tankNo)) {
        groups.set(tankNo, []);
      }
      groups.get(tankNo).push(row);
    });
    const sortedTankNos = [...groups.keys()].sort((a, b) => {
      if (!a) return 1;
      if (!b) return -1;
      return a.localeCompare(b, "zh-CN", { numeric: true });
    });
    return sortedTankNos.flatMap(tankNo => {
      const groupRows = groups.get(tankNo);
      return [
        {
          __isGroup: true,
          tankNo,
          qualifiedQuantity: sumBy(groupRows, "qualifiedQuantity"),
          qualifiedLockedQuantity: sumBy(groupRows, "qualifiedLockedQuantity"),
        },
        ...groupRows,
      ];
    });
  };
  const spanMethod = ({ row, columnIndex }) => {
    if (!row?.__isGroup) {
      return { rowspan: 1, colspan: 1 };
    }
    if (columnIndex === 0) {
      return { rowspan: 1, colspan: GROUP_ROW_LEAD_COLUMNS };
    }
    if (columnIndex === GROUP_ROW_LEAD_COLUMNS) {
      return {
        rowspan: 1,
        colspan: TABLE_COLUMN_COUNT - GROUP_ROW_LEAD_COLUMNS,
      };
    }
    return { rowspan: 0, colspan: 0 };
  };
  // åºå·åªå¯¹æ˜Žç»†è¡Œè¿žç»­ç¼–号,分类行留空
  const indexMethod = index => {
    if (tableData.value[index]?.__isGroup) {
      return "";
    }
    let count = 0;
    for (let i = 0; i <= index; i++) {
      if (!tableData.value[i]?.__isGroup) {
        count += 1;
      }
    }
    return count;
  };
  // æ˜¯å¦æ˜¾ç¤ºæ–°å¢žå¼¹æ¡†
  const isShowNewModal = ref(false);
  // æ˜¯å¦æ˜¾ç¤ºæ²¹å“å…¥åº“弹框
@@ -270,7 +348,9 @@
    getStockInventoryListPageCombined({ ...searchForm.value, ...page })
      .then(res => {
        tableLoading.value = false;
        tableData.value = res.data.records;
        tableData.value = shouldGroupByTank.value
          ? buildGroupedRows(res.data.records)
          : res.data.records;
        total.value = res.data.total;
        // æ•°æ®åŠ è½½å®ŒæˆåŽæ£€æŸ¥åº“å­˜
        // checkStockAndCreatePurchase();
@@ -346,6 +426,9 @@
  // è¡¨æ ¼è¡Œç±»å
  const tableRowClassName = ({ row }) => {
    if (row?.__isGroup) {
      return "row-tank-group";
    }
    const stock = Number(row?.qualifiedUnLockedQuantity ?? 0);
    const warn = Number(row?.warnNum ?? 0);
    if (!Number.isFinite(stock) || !Number.isFinite(warn)) {
@@ -379,6 +462,12 @@
</script>
<style scoped lang="scss">
  .actions {
    display: flex;
    justify-content: space-between;
    margin-bottom: 12px;
  }
  :deep(.row-low-stock td) {
    background-color: #fde2e2;
    color: #c45656;
@@ -387,4 +476,34 @@
  :deep(.row-low-stock:hover > td) {
    background-color: #fcd4d4;
  }
  :deep(.row-tank-group > td) {
    background-color: #eef5ff;
  }
  :deep(.row-tank-group:hover > td) {
    background-color: #e4eefc;
  }
  :deep(.tank-group-band) {
    display: flex;
    align-items: center;
    width: 100%;
    font-size: 13px;
    line-height: 20px;
  }
  :deep(.tank-group-band__name) {
    margin-right: auto;
    padding-left: 8px;
    border-left: 3px solid #1d6fd8;
    font-weight: 700;
    color: #1d6fd8;
  }
  :deep(.tank-group-band__stat) {
    margin-left: 24px;
    font-weight: 600;
    color: #3a7bd5;
  }
</style>
src/views/inventoryManagement/stockManagement/index.vue
@@ -6,7 +6,7 @@
                     :label="tab.productName"
                     :name="tab.id"
                     :key="tab.id">
          <Record :product-id="tab.id" v-if="tab.id === activeTab" />
          <Record :product-id="tab.id" :product-name="tab.productName" v-if="tab.id === activeTab" />
        </el-tab-pane>
      </el-tabs>
    </div>
src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
@@ -460,6 +460,7 @@
    purchaseContractNumber: row.purchaseContractNumber || "",
    quotationNo: row.quotationNo || "",
    shippingNo: row.shippingNo || "",
    salesContractNo: row.salesContractNo || "",
  };
}
src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue
@@ -145,6 +145,53 @@
          </div>
        </div>
      </template>
      <!-- é”€å”®å°è´¦å®¡æ‰¹è¯¦æƒ…(卖油 / ä»£å‚¨ï¼‰ -->
      <template v-else-if="row.businessType === 19">
        <div v-if="detailData && Object.keys(detailData).length" class="sales-ledger-detail">
          <el-divider content-position="left">销售台账详情</el-divider>
          <el-descriptions :column="2" border>
            <el-descriptions-item label="销售合同号">{{ detailData.salesContractNo || "--" }}</el-descriptions-item>
            <el-descriptions-item label="销售类型">{{ detailData.ledgerType || "--" }}</el-descriptions-item>
            <el-descriptions-item label="客户名称">{{ detailData.customerName || "--" }}</el-descriptions-item>
            <el-descriptions-item label="业务员">{{ detailData.salesman || "--" }}</el-descriptions-item>
            <el-descriptions-item label="项目名称">{{ detailData.projectName || "--" }}</el-descriptions-item>
            <el-descriptions-item label="付款方式">{{ detailData.paymentMethod || "--" }}</el-descriptions-item>
            <el-descriptions-item v-if="detailData.ledgerType === '代储'"
                                  label="油库">{{ detailData.oilDepotName || "--" }}</el-descriptions-item>
            <el-descriptions-item v-if="detailData.ledgerType === '代储'"
                                  label="储罐号">{{ detailData.tankNo || "--" }}</el-descriptions-item>
            <el-descriptions-item label="合同金额" :span="2">
              <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">
                Â¥{{ Number(detailData.contractAmount ?? 0).toFixed(2) }}
              </span>
              <span v-if="detailData.exceedsTrustFund"
                    class="reject-text"
                    style="margin-left: 12px;">
                å·²è¶…出该客户信托基金{{ detailData.trustFund != null ? `(信托基金:${detailData.trustFund})` : "" }}
              </span>
            </el-descriptions-item>
          </el-descriptions>
          <div v-if="detailData.productData && detailData.productData.length"
               style="margin-top: 20px;">
            <h4>产品明细</h4>
            <el-table :data="detailData.productData"
                      border
                      style="width: 100%">
              <el-table-column prop="productCategory" label="产品大类" />
              <el-table-column prop="specificationModel" label="规格型号" />
              <el-table-column prop="unit" label="单位" />
              <el-table-column prop="quantity" label="数量" />
              <el-table-column prop="taxInclusiveUnitPrice" label="含税单价">
                <template #default="scope">Â¥{{ Number(scope.row.taxInclusiveUnitPrice ?? 0).toFixed(2) }}</template>
              </el-table-column>
              <el-table-column prop="taxInclusiveTotalPrice" label="含税总价">
                <template #default="scope">Â¥{{ Number(scope.row.taxInclusiveTotalPrice ?? 0).toFixed(2) }}</template>
              </el-table-column>
            </el-table>
          </div>
        </div>
      </template>
    </div>
    <div v-if="attachmentList.length"
         class="detail-block">
@@ -199,9 +246,9 @@
  const formResolved = computed(() => resolveInstanceFormFields(props.row));
  // æ˜¯å¦ä¸ºç‰¹æ®Šå®¡æ‰¹ç±»åž‹ï¼ˆé‡‡è´­ã€å‘货、报价)
  // æ˜¯å¦ä¸ºç‰¹æ®Šå®¡æ‰¹ç±»åž‹ï¼ˆé‡‡è´­ã€å‘货、报价、销售台账)
  const isSpecialApprovalType = computed(() => {
    return [5, 7, 6].includes(props.row.businessType);
    return [5, 7, 6, 19].includes(props.row.businessType);
  });
  // è¯¦æƒ…数据(直接使用传入的 detail-data å‚数)
src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
@@ -63,6 +63,9 @@
        :total="page.total"
        @pagination="pagination"
      >
        <template #bizNo="{ row }">
          <span>{{ businessNoOf(row) || "-" }}</span>
        </template>
        <template #approveType="{ row }">
          <span class="approve-type-cell" :style="approvalTypeStyle(row.approvalType)">
            {{ approvalTypeLabel(row.approvalType) }}
@@ -361,6 +364,7 @@
  onBusinessTypePick,
  backToBusinessTypePick,
  approvalTypeLabel,
  businessNoOf,
  approvalActionLabel,
  searchForm,
  tableLoading,
src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
@@ -13,6 +13,7 @@
import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js";
import { getDeliveryDetailByShippingNo } from "@/api/salesManagement/deliveryLedger.js";
import { ledgerListPage } from "@/api/salesManagement/salesLedger.js";
import useUserStore from "@/store/modules/user";
import { Search } from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox } from "element-plus";
@@ -120,6 +121,18 @@
    return hit?.label || "";
  }
  // åˆ—表「业务单号」:按业务类型取对应的单号(19=销售台账审批)
  function businessNoOf(row) {
    return (
      row?.salesContractNo ||
      row?.quotationNo ||
      row?.purchaseContractNumber ||
      row?.shippingNo ||
      row?.bizId ||
      ""
    );
  }
  function countTemplatesByBusinessType(type) {
    return allSubmitTemplates.value.filter((card) => matchBusinessTypeValue(card.businessType, type)).length;
  }
@@ -142,6 +155,13 @@
    // { label: "申请人编号", prop: "applicantNo", width: 110 },
    { label: "发起人", prop: "applicantName", minWidth: 100 },
    { label: "模板类型", prop: "businessName", minWidth: 120 },
    {
      label: "业务单号",
      prop: "businessNo",
      minWidth: 170,
      dataType: "slot",
      slot: "bizNo",
    },
    {
      label: "审批类型",
      prop: "approvalType",
@@ -327,12 +347,28 @@
      }
    }
    
    // é”€å”®å°è´¦å®¡æ‰¹ï¼šç”¨å®¡æ‰¹äº‹ç”±å­—段承载的"销售合同号"去查台账列表
    else if (row.businessType === 19) {
      detailData.value = await fetchSalesLedgerByContractNo(row?.salesContractNo);
    }
    // å…¶ä»–审批类型
    else {
      detailData.value = {};
    }
    
    detailDialog.visible = true;
  }
  async function fetchSalesLedgerByContractNo(salesContractNo) {
    if (!salesContractNo) return {};
    // /sales/ledger/listPage ç›´æŽ¥è¿”回 IPage(顶层 records),没有 code/msg å¤–层
    const res = await ledgerListPage({
      salesContractNo,
      pageNum: 1,
      pageSize: 1,
    });
    return res?.records?.[0] || {};
  }
  async function openApprove(row) {
@@ -373,6 +409,11 @@
        const res = await getDeliveryDetailByShippingNo({ shippingNo });
        detailData.value = res?.data || res || {};
      }
    }
    // é”€å”®å°è´¦å®¡æ‰¹ï¼šç”¨å®¡æ‰¹äº‹ç”±å­—段承载的"销售合同号"去查台账列表
    else if (row.businessType === 19) {
      detailData.value = await fetchSalesLedgerByContractNo(row?.salesContractNo);
    }
    // å…¶ä»–审批类型
@@ -686,6 +727,7 @@
    selectedBusinessType,
    selectedBusinessTypeLabel,
    businessTypeLabel,
    businessNoOf,
    countTemplatesByBusinessType,
    submitTemplatesLoading,
    handleQuery,
src/views/outsourcingManagement/order/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,952 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <el-form :model="searchForm"
               :inline="true">
        <el-form-item label="委外订单号:">
          <el-input v-model="searchForm.orderNo"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 180px"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="合作商:">
          <el-select v-model="searchForm.partnerId"
                     placeholder="请选择"
                     clearable
                     filterable
                     style="width: 200px"
                     @change="handleQuery">
            <el-option v-for="item in partnerOptions"
                       :key="item.id"
                       :label="item.partnerName"
                       :value="item.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="项目名称:">
          <el-input v-model="searchForm.projectName"
                    placeholder="请输入"
                    clearable
                    style="width: 160px"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="状态:">
          <el-select v-model="searchForm.status"
                     placeholder="请选择"
                     clearable
                     style="width: 140px"
                     @change="handleQuery">
            <el-option v-for="item in statusOptions"
                       :key="item.value"
                       :label="item.label"
                       :value="item.value" />
          </el-select>
        </el-form-item>
        <el-form-item label="申请日期:">
          <el-date-picker v-model="dateRange"
                          type="daterange"
                          value-format="YYYY-MM-DD"
                          format="YYYY-MM-DD"
                          range-separator="至"
                          start-placeholder="开始日期"
                          end-placeholder="结束日期"
                          @change="handleQuery" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary"
                     @click="handleQuery">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
        </el-form-item>
      </el-form>
    </div>
    <div class="table_list">
      <div class="actions">
        <div></div>
        <div>
          <el-button type="primary"
                     @click="openForm('add')">新增委外订单</el-button>
          <el-button @click="handleExport">导出</el-button>
          <el-button type="danger"
                     plain
                     @click="handleBatchDelete">删除</el-button>
        </div>
      </div>
      <el-table :data="tableData"
                border
                v-loading="tableLoading"
                @selection-change="handleSelectionChange"
                style="width: 100%"
                height="calc(100vh - 21.5em)">
        <el-table-column align="center"
                         type="selection"
                         :selectable="isDraft"
                         width="55" />
        <el-table-column align="center"
                         label="序号"
                         type="index"
                         width="60" />
        <el-table-column label="委外订单号"
                         prop="orderNo"
                         min-width="160"
                         show-overflow-tooltip />
        <el-table-column label="合作商"
                         prop="partnerName"
                         min-width="180"
                         show-overflow-tooltip />
        <el-table-column label="项目名称"
                         prop="projectName"
                         min-width="140"
                         show-overflow-tooltip />
        <el-table-column label="申请日期"
                         prop="applyDate"
                         min-width="110"
                         show-overflow-tooltip />
        <el-table-column label="预计交付日期"
                         prop="expectedDeliveryDate"
                         min-width="120"
                         show-overflow-tooltip />
        <el-table-column label="合同金额(元)"
                         prop="contractAmount"
                         min-width="120"
                         align="right">
          <template #default="scope">
            {{ formatAmount(scope.row.contractAmount) }}
          </template>
        </el-table-column>
        <el-table-column label="付款方式"
                         prop="paymentMethod"
                         min-width="100"
                         show-overflow-tooltip />
        <el-table-column label="状态"
                         prop="status"
                         align="center"
                         width="100">
          <template #default="scope">
            <el-tag :type="statusTagType(scope.row.status)"
                    size="small">
              {{ statusLabel(scope.row.status) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="确认时间"
                         prop="confirmTime"
                         min-width="160"
                         show-overflow-tooltip />
        <el-table-column label="作废原因"
                         prop="cancelReason"
                         min-width="140"
                         show-overflow-tooltip />
        <el-table-column label="创建时间"
                         prop="createTime"
                         min-width="160"
                         show-overflow-tooltip />
        <el-table-column fixed="right"
                         label="操作"
                         width="220"
                         align="center">
          <template #default="scope">
            <el-button link
                       type="primary"
                       @click="openForm('view', scope.row)">详情</el-button>
            <el-button v-if="scope.row.status === 0"
                       link
                       type="primary"
                       @click="openForm('edit', scope.row)">编辑</el-button>
            <el-button v-if="scope.row.status === 0"
                       link
                       type="primary"
                       @click="handleSubmitOrder(scope.row)">提交</el-button>
            <el-button v-if="scope.row.status === 1"
                       link
                       type="primary"
                       @click="handleCompleteOrder(scope.row)">完成</el-button>
            <el-button v-if="scope.row.status === 0 || scope.row.status === 1"
                       link
                       type="danger"
                       @click="handleCancelOrder(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" />
    </div>
    <el-dialog v-model="dialogFormVisible"
               :title="dialogTitle"
               width="90%"
               top="5vh"
               :close-on-click-modal="false"
               @close="closeDia">
      <el-form :model="form"
               label-width="130px"
               label-position="top"
               :rules="rules"
               ref="formRef"
               :disabled="isView">
        <el-row :gutter="30">
          <el-col :span="8">
            <el-form-item label="合作商:"
                          prop="partnerId">
              <el-select v-model="form.partnerId"
                         placeholder="请选择"
                         clearable
                         filterable
                         style="width: 100%">
                <el-option v-for="item in partnerOptions"
                           :key="item.id"
                           :label="item.partnerName"
                           :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="项目名称:"
                          prop="projectName">
              <el-input v-model="form.projectName"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <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="applyDate">
              <el-date-picker v-model="form.applyDate"
                              type="date"
                              value-format="YYYY-MM-DD"
                              format="YYYY-MM-DD"
                              placeholder="请选择"
                              style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="预计交付日期:"
                          prop="expectedDeliveryDate">
              <el-date-picker v-model="form.expectedDeliveryDate"
                              type="date"
                              value-format="YYYY-MM-DD"
                              format="YYYY-MM-DD"
                              placeholder="请选择"
                              style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="合同金额(元)">
              <el-input :model-value="formatAmount(totalAmount)"
                        readonly />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="备注:"
                          prop="remarks">
              <el-input v-model="form.remarks"
                        type="textarea"
                        :rows="2"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <div class="detail-header">
        <span class="detail-title">产品明细</span>
        <el-button v-if="!isView"
                   type="primary"
                   size="small"
                   @click="openScrapSelect">选报废品</el-button>
        <el-button v-if="!isView"
                   size="small"
                   @click="addEmptyDetail">新增行</el-button>
      </div>
      <el-table :data="form.productData"
                border
                size="small"
                style="width: 100%">
        <el-table-column align="center"
                         label="序号"
                         type="index"
                         width="55" />
        <el-table-column label="产品大类"
                         prop="productCategory"
                         min-width="120"
                         show-overflow-tooltip />
        <el-table-column label="规格型号"
                         prop="specificationModel"
                         min-width="110"
                         show-overflow-tooltip />
        <el-table-column label="单位"
                         prop="unit"
                         min-width="70" />
        <el-table-column label="数量"
                         prop="quantity"
                         min-width="110">
          <template #default="scope">
            <el-input-number v-if="!isView"
                             v-model="scope.row.quantity"
                             :min="0"
                             :precision="4"
                             :step="1"
                             style="width: 100%" />
            <span v-else>{{ scope.row.quantity }}</span>
          </template>
        </el-table-column>
        <el-table-column label="税率(%)"
                         prop="taxRate"
                         min-width="110">
          <template #default="scope">
            <el-input-number v-if="!isView"
                             v-model="scope.row.taxRate"
                             :min="0"
                             :max="100"
                             :precision="2"
                             :step="1"
                             style="width: 100%" />
            <span v-else>{{ scope.row.taxRate }}</span>
          </template>
        </el-table-column>
        <el-table-column label="含税单价(元)"
                         prop="taxInclusiveUnitPrice"
                         min-width="130">
          <template #default="scope">
            <el-input-number v-if="!isView"
                             v-model="scope.row.taxInclusiveUnitPrice"
                             :min="0"
                             :precision="2"
                             :step="0.01"
                             style="width: 100%" />
            <span v-else>{{ scope.row.taxInclusiveUnitPrice }}</span>
          </template>
        </el-table-column>
        <el-table-column label="含税总价(元)"
                         prop="taxInclusiveTotalPrice"
                         min-width="120"
                         align="right">
          <template #default="scope">
            {{ formatAmount(lineTotal(scope.row)) }}
          </template>
        </el-table-column>
        <el-table-column label="报废品"
                         prop="isScrap"
                         align="center"
                         width="80">
          <template #default="scope">
            <el-tag :type="scope.row.isScrap ? 'warning' : 'info'"
                    size="small">
              {{ scope.row.isScrap ? "是" : "否" }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="批号"
                         prop="batchNo"
                         min-width="130"
                         show-overflow-tooltip />
        <el-table-column label="可用量"
                         prop="availableQuantity"
                         min-width="100"
                         align="right" />
        <el-table-column label="备注"
                         prop="remark"
                         min-width="130">
          <template #default="scope">
            <el-input v-if="!isView"
                      v-model="scope.row.remark"
                      placeholder="请输入"
                      clearable />
            <span v-else>{{ scope.row.remark }}</span>
          </template>
        </el-table-column>
        <el-table-column v-if="!isView"
                         fixed="right"
                         label="操作"
                         width="70"
                         align="center">
          <template #default="scope">
            <el-button link
                       type="danger"
                       @click="removeDetail(scope.$index)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <template #footer>
        <div class="dialog-footer">
          <el-button v-if="!isView"
                     type="primary"
                     @click="submitForm">确认</el-button>
          <el-button @click="closeDia">{{ isView ? "关闭" : "取消" }}</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- æŠ¥åºŸå“é€‰æ–™ï¼ˆåªè¯»ï¼Œä¸æ‰£åº“存、不占用) -->
    <el-dialog v-model="scrapDialogVisible"
               title="选择报废品"
               width="70%"
               append-to-body>
      <el-form :model="scrapSearch"
               :inline="true">
        <el-form-item label="产品名称:">
          <el-input v-model="scrapSearch.productName"
                    placeholder="请输入"
                    clearable
                    style="width: 160px" />
        </el-form-item>
        <el-form-item label="批号:">
          <el-input v-model="scrapSearch.batchNo"
                    placeholder="请输入"
                    clearable
                    style="width: 160px" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary"
                     @click="handleScrapQuery">搜索</el-button>
        </el-form-item>
      </el-form>
      <el-table :data="scrapTableData"
                border
                v-loading="scrapLoading"
                highlight-current-row
                style="width: 100%"
                height="360px"
                @current-change="handleScrapCurrentChange">
        <el-table-column align="center"
                         label="序号"
                         type="index"
                         width="60" />
        <el-table-column label="产品名称"
                         prop="productName"
                         min-width="140"
                         show-overflow-tooltip />
        <el-table-column label="规格型号"
                         prop="model"
                         min-width="120"
                         show-overflow-tooltip />
        <el-table-column label="批号"
                         prop="batchNo"
                         min-width="140"
                         show-overflow-tooltip />
        <el-table-column label="单位"
                         prop="unit"
                         min-width="70" />
        <el-table-column label="库存数量"
                         prop="qualitity"
                         min-width="100"
                         align="right" />
        <el-table-column label="锁定数量"
                         prop="lockedQuantity"
                         min-width="100"
                         align="right" />
        <el-table-column label="可用数量"
                         prop="unLockedQuantity"
                         min-width="100"
                         align="right" />
        <el-table-column label="备注"
                         prop="remark"
                         min-width="120"
                         show-overflow-tooltip />
      </el-table>
      <pagination v-show="scrapTotal > 0"
                  :total="scrapTotal"
                  layout="total, sizes, prev, pager, next, jumper"
                  :page="scrapPage.current"
                  :limit="scrapPage.size"
                  @pagination="scrapPaginationChange" />
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
                     @click="confirmScrapSelect">确认</el-button>
          <el-button @click="scrapDialogVisible = false">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
  import { ref, reactive, computed, toRefs, getCurrentInstance, onMounted } from "vue";
  import pagination from "@/components/PIMTable/Pagination.vue";
  import { ElMessageBox } from "element-plus";
  import {
    listOutsourcingOrder,
    getOutsourcingOrder,
    addOutsourcingOrder,
    editOutsourcingOrder,
    delOutsourcingOrder,
    submitOutsourcingOrder,
    completeOutsourcingOrder,
    cancelOutsourcingOrder,
    exportOutsourcingOrder,
    scrapStockPage,
  } from "@/api/outsourcing/order.js";
  import { getPartnerOptions } from "@/api/outsourcing/partner.js";
  const { proxy } = getCurrentInstance();
  const tableData = ref([]);
  const selectedRows = ref([]);
  const tableLoading = ref(false);
  const dialogFormVisible = ref(false);
  const operationType = ref("");
  const partnerOptions = ref([]);
  const dateRange = ref([]);
  const page = reactive({
    current: 1,
    size: 10,
  });
  const total = ref(0);
  const statusOptions = [
    { label: "草稿", value: 0 },
    { label: "已确认", value: 1 },
    { label: "已完成", value: 2 },
    { label: "已作废", value: 3 },
  ];
  const statusLabelMap = {
    0: "草稿",
    1: "已确认",
    2: "已完成",
    3: "已作废",
  };
  const statusTagMap = {
    0: "info",
    1: "primary",
    2: "success",
    3: "danger",
  };
  const statusLabel = status => statusLabelMap[status] ?? status;
  const statusTagType = status => statusTagMap[status] || "info";
  const data = reactive({
    searchForm: {
      orderNo: "",
      partnerId: "",
      projectName: "",
      status: "",
    },
    form: {},
    rules: {
      partnerId: [{ required: true, message: "请选择合作商", trigger: "change" }],
      projectName: [{ required: true, message: "请输入项目名称", trigger: "blur" }],
      applyDate: [{ required: true, message: "请选择申请日期", trigger: "change" }],
    },
  });
  const { searchForm, form, rules } = toRefs(data);
  const isView = computed(() => operationType.value === "view");
  const isDraft = row => row.status === 0;
  const dialogTitle = computed(() => {
    if (operationType.value === "add") {
      return "新增委外订单";
    }
    return operationType.value === "edit" ? "编辑委外订单" : "委外订单详情";
  });
  const buildEmptyForm = () => ({
    id: null,
    partnerId: undefined,
    projectName: "",
    applyDate: today(),
    expectedDeliveryDate: "",
    paymentMethod: "",
    remarks: "",
    productData: [],
  });
  function today() {
    const d = new Date();
    const month = String(d.getMonth() + 1).padStart(2, "0");
    const day = String(d.getDate()).padStart(2, "0");
    return `${d.getFullYear()}-${month}-${day}`;
  }
  const formatAmount = value => {
    const num = Number(value);
    return Number.isFinite(num) ? num.toFixed(2) : "0.00";
  };
  // å«ç¨Žæ€»ä»·ç”±æ•°é‡ Ã— å«ç¨Žå•价得出,后端会按明细合计覆盖 contractAmount
  const lineTotal = row => {
    const quantity = Number(row?.quantity) || 0;
    const price = Number(row?.taxInclusiveUnitPrice) || 0;
    return Math.round(quantity * price * 100) / 100;
  };
  const totalAmount = computed(() =>
    (form.value.productData || []).reduce((sum, row) => sum + lineTotal(row), 0)
  );
  // åŽ»æŽ‰ç©ºå€¼ï¼Œé¿å… status='' è¿™ç±»ç©ºä¸²è¢«åŽç«¯å½“成 0(草稿)解析
  const compact = obj =>
    Object.fromEntries(
      Object.entries(obj).filter(([, v]) => v !== "" && v !== null && v !== undefined)
    );
  const getList = () => {
    tableLoading.value = true;
    const [startDate, endDate] = dateRange.value || [];
    listOutsourcingOrder({
      pageNum: page.current,
      pageSize: page.size,
      ...compact(searchForm.value),
      startDate,
      endDate,
    })
      .then(res => {
        tableLoading.value = false;
        // å§”外订单列表返回的是 RuoYi ç»“构,行在顶层 rows
        tableData.value = res.rows || [];
        total.value = res.total || 0;
      })
      .catch(() => {
        tableLoading.value = false;
      });
  };
  const handleQuery = () => {
    page.current = 1;
    getList();
  };
  const resetSearch = () => {
    searchForm.value = { orderNo: "", partnerId: "", projectName: "", status: "" };
    dateRange.value = [];
    handleQuery();
  };
  const paginationChange = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  const handleSelectionChange = selection => {
    selectedRows.value = selection;
  };
  const openForm = (type, row) => {
    operationType.value = type;
    form.value = buildEmptyForm();
    proxy.resetForm("formRef");
    dialogFormVisible.value = true;
    if (type !== "add" && row) {
      getOutsourcingOrder(row.id).then(res => {
        const detail = res.data || {};
        form.value = {
          ...buildEmptyForm(),
          ...detail,
          productData: (detail.productData || []).map(item => ({ ...item })),
        };
      });
    }
  };
  const addEmptyDetail = () => {
    form.value.productData.push({
      productId: undefined,
      productModelId: undefined,
      productCategory: "",
      specificationModel: "",
      unit: "",
      quantity: undefined,
      taxRate: undefined,
      taxInclusiveUnitPrice: undefined,
      taxInclusiveTotalPrice: undefined,
      isScrap: false,
      stockInventoryId: undefined,
      batchNo: "",
      availableQuantity: undefined,
      remark: "",
    });
  };
  const removeDetail = index => {
    form.value.productData.splice(index, 1);
  };
  const submitForm = () => {
    proxy.$refs["formRef"].validate(valid => {
      if (!valid) {
        return;
      }
      if (!form.value.productData.length) {
        proxy.$modal.msgWarning("请至少添加一条产品明细");
        return;
      }
      // åªæäº¤æ–‡æ¡£çº¦å®šçš„字段:单号/状态/确认时间/合同金额/合作商名称等由后端维护
      const payload = {
        partnerId: form.value.partnerId,
        projectName: form.value.projectName,
        applyDate: form.value.applyDate,
        expectedDeliveryDate: form.value.expectedDeliveryDate,
        paymentMethod: form.value.paymentMethod,
        remarks: form.value.remarks,
        productData: form.value.productData.map(row => ({
          productId: row.productId,
          productModelId: row.productModelId,
          productCategory: row.productCategory,
          specificationModel: row.specificationModel,
          unit: row.unit,
          quantity: row.quantity,
          taxRate: row.taxRate,
          taxInclusiveUnitPrice: row.taxInclusiveUnitPrice,
          taxInclusiveTotalPrice: lineTotal(row),
          isScrap: !!row.isScrap,
          stockInventoryId: row.stockInventoryId,
          batchNo: row.batchNo,
          availableQuantity: row.availableQuantity,
          remark: row.remark,
        })),
        ...(form.value.id ? { id: form.value.id } : {}),
      };
      const api = form.value.id ? editOutsourcingOrder : addOutsourcingOrder;
      api(payload).then(() => {
        proxy.$modal.msgSuccess("操作成功");
        closeDia();
        getList();
      });
    });
  };
  const closeDia = () => {
    proxy.resetForm("formRef");
    dialogFormVisible.value = false;
  };
  const handleSubmitOrder = row => {
    ElMessageBox.confirm(
      `是否确认提交订单 ${row.orderNo}?提交后将占用所选报废品批次的可用量,需到出库管理审批通过后才能完成订单。`,
      "提交",
      {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
      }
    )
      .then(() => {
        submitOutsourcingOrder(row.id).then(() => {
          proxy.$modal.msgSuccess("提交成功");
          getList();
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  const handleCompleteOrder = row => {
    ElMessageBox.confirm(`是否确认完成订单 ${row.orderNo}?`, "完成", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        completeOutsourcingOrder(row.id).then(() => {
          proxy.$modal.msgSuccess("操作成功");
          getList();
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  const handleCancelOrder = row => {
    ElMessageBox.prompt("请输入作废原因(可不填)", `作废订单 ${row.orderNo}`, {
      confirmButtonText: "确认作废",
      cancelButtonText: "取消",
      inputPlaceholder: "作废原因",
    })
      .then(({ value }) => {
        cancelOutsourcingOrder(row.id, value || "").then(() => {
          proxy.$modal.msgSuccess("作废成功");
          getList();
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  const handleBatchDelete = () => {
    if (selectedRows.value.length === 0) {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    const ids = selectedRows.value.map(item => item.id);
    ElMessageBox.confirm("选中的内容将被删除,是否确认删除?仅草稿状态允许删除。", "删除", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        delOutsourcingOrder(ids).then(() => {
          proxy.$modal.msgSuccess("删除成功");
          getList();
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  const saveExcelBlob = (blobData, filename) => {
    const blob = new Blob([blobData], {
      type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    });
    const url = window.URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = url;
    link.download = filename;
    document.body.appendChild(link);
    link.click();
    setTimeout(() => {
      document.body.removeChild(link);
      window.URL.revokeObjectURL(url);
    }, 100);
  };
  const handleExport = () => {
    ElMessageBox.confirm("是否确认导出?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        const [startDate, endDate] = dateRange.value || [];
        exportOutsourcingOrder({
          ...compact(searchForm.value),
          startDate,
          endDate,
        }).then(res => {
          saveExcelBlob(res, "委外订单.xlsx");
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  // æŠ¥åºŸå“é€‰æ–™ï¼šåªè¯»å¸¦å‡ºï¼Œä¸æ‰£åº“存、不占用
  const scrapDialogVisible = ref(false);
  const scrapLoading = ref(false);
  const scrapTableData = ref([]);
  const scrapSelectedRow = ref(null);
  const scrapTotal = ref(0);
  const scrapPage = reactive({ current: 1, size: 10 });
  const scrapSearch = reactive({ productName: "", batchNo: "" });
  const getScrapList = () => {
    scrapLoading.value = true;
    scrapStockPage({
      pageNum: scrapPage.current,
      pageSize: scrapPage.size,
      productName: scrapSearch.productName || undefined,
      batchNo: scrapSearch.batchNo || undefined,
    })
      .then(res => {
        scrapLoading.value = false;
        scrapTableData.value = res.data?.records || [];
        scrapTotal.value = res.data?.total || 0;
      })
      .catch(() => {
        scrapLoading.value = false;
      });
  };
  const openScrapSelect = () => {
    scrapSelectedRow.value = null;
    scrapSearch.productName = "";
    scrapSearch.batchNo = "";
    scrapPage.current = 1;
    scrapDialogVisible.value = true;
    getScrapList();
  };
  const handleScrapQuery = () => {
    scrapPage.current = 1;
    getScrapList();
  };
  const scrapPaginationChange = obj => {
    scrapPage.current = obj.page;
    scrapPage.size = obj.limit;
    getScrapList();
  };
  const handleScrapCurrentChange = row => {
    scrapSelectedRow.value = row;
  };
  const confirmScrapSelect = () => {
    const row = scrapSelectedRow.value;
    if (!row) {
      proxy.$modal.msgWarning("请选择一条报废品批次");
      return;
    }
    form.value.productData.push({
      productId: row.productId,
      productModelId: row.productModelId,
      productCategory: row.productName || "",
      specificationModel: row.model || "",
      unit: row.unit || "",
      quantity: undefined,
      taxRate: undefined,
      taxInclusiveUnitPrice: undefined,
      taxInclusiveTotalPrice: undefined,
      isScrap: true,
      stockInventoryId: row.id,
      batchNo: row.batchNo || "",
      availableQuantity: row.unLockedQuantity,
      remark: "",
    });
    scrapDialogVisible.value = false;
  };
  onMounted(() => {
    getList();
    getPartnerOptions().then(res => {
      partnerOptions.value = res.data || [];
    });
  });
</script>
<style scoped lang="scss">
  .search_form {
    margin-bottom: 16px;
  }
  .actions {
    display: flex;
    justify-content: flex-end;
    margin-bottom: 12px;
  }
  .detail-header {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 8px 0 12px;
    .detail-title {
      flex: 1;
      font-size: 15px;
      font-weight: 600;
      color: #1d6fd8;
    }
  }
</style>
src/views/outsourcingManagement/partner/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,668 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <el-form :model="searchForm"
               :inline="true">
        <el-form-item label="合作商名称:">
          <el-input v-model="searchForm.partnerName"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="合作商类型:">
          <el-input v-model="searchForm.partnerType"
                    placeholder="请输入"
                    clearable
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="是否白名单:">
          <el-select v-model="searchForm.isWhite"
                     placeholder="请选择"
                     clearable
                     style="width: 140px"
                     @change="handleQuery">
            <el-option label="是"
                       :value="1" />
            <el-option label="否"
                       :value="0" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary"
                     @click="handleQuery">搜索</el-button>
        </el-form-item>
      </el-form>
    </div>
    <div class="table_list">
      <div class="actions">
        <div></div>
        <div>
          <el-button type="primary"
                     @click="openForm('add')">新增合作商</el-button>
          <el-button @click="handleExport">导出</el-button>
          <el-button type="info"
                     plain
                     icon="Upload"
                     @click="handleImport">导入</el-button>
          <el-button type="danger"
                     plain
                     @click="handleDelete">删除</el-button>
        </div>
      </div>
      <el-table :data="tableData"
                border
                v-loading="tableLoading"
                @selection-change="handleSelectionChange"
                style="width: 100%"
                height="calc(100vh - 19em)">
        <el-table-column align="center"
                         type="selection"
                         width="55" />
        <el-table-column align="center"
                         label="序号"
                         type="index"
                         width="60" />
        <el-table-column label="合作商名称"
                         prop="partnerName"
                         min-width="200"
                         show-overflow-tooltip />
        <el-table-column label="合作商类型"
                         prop="partnerType"
                         min-width="110"
                         show-overflow-tooltip />
        <el-table-column label="纳税人识别号"
                         prop="taxpayerIdentificationNum"
                         min-width="190"
                         show-overflow-tooltip />
        <el-table-column label="公司地址"
                         prop="companyAddress"
                         min-width="180"
                         show-overflow-tooltip />
        <el-table-column label="公司电话"
                         prop="companyPhone"
                         min-width="130"
                         show-overflow-tooltip />
        <el-table-column label="开户行"
                         prop="bankAccountName"
                         min-width="170"
                         show-overflow-tooltip />
        <el-table-column label="账号"
                         prop="bankAccountNum"
                         min-width="170"
                         show-overflow-tooltip />
        <el-table-column label="联系人"
                         prop="contactUserName"
                         min-width="90"
                         show-overflow-tooltip />
        <el-table-column label="联系电话"
                         prop="contactUserPhone"
                         min-width="130"
                         show-overflow-tooltip />
        <el-table-column label="是否白名单"
                         prop="isWhite"
                         align="center"
                         width="100">
          <template #default="scope">
            <el-tag :type="scope.row.isWhite === 1 ? 'success' : 'info'"
                    size="small">
              {{ scope.row.isWhite === 1 ? "是" : "否" }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="维护人"
                         prop="maintainUserName"
                         min-width="90"
                         show-overflow-tooltip />
        <el-table-column label="维护时间"
                         prop="maintainTime"
                         min-width="110"
                         show-overflow-tooltip />
        <el-table-column label="备注"
                         prop="remark"
                         min-width="140"
                         show-overflow-tooltip />
        <el-table-column fixed="right"
                         label="操作"
                         width="160"
                         align="center">
          <template #default="scope">
            <el-button link
                       type="primary"
                       @click="openForm('edit', scope.row)">编辑</el-button>
            <el-button link
                       type="primary"
                       @click="openAttachment(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" />
    </div>
    <el-dialog v-model="dialogFormVisible"
               :title="operationType === 'add' ? '新增合作商' : '编辑合作商'"
               width="70%"
               :close-on-click-modal="false"
               @close="closeDia">
      <el-form :model="form"
               label-width="140px"
               label-position="top"
               :rules="rules"
               ref="formRef">
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="合作商名称:"
                          prop="partnerName">
              <el-input v-model="form.partnerName"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="纳税人识别号:"
                          prop="taxpayerIdentificationNum">
              <el-input v-model="form.taxpayerIdentificationNum"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="公司地址:"
                          prop="companyAddress">
              <el-input v-model="form.companyAddress"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="公司电话:"
                          prop="companyPhone">
              <el-input v-model="form.companyPhone"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="开户行:"
                          prop="bankAccountName">
              <el-input v-model="form.bankAccountName"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="账号:"
                          prop="bankAccountNum">
              <el-input v-model="form.bankAccountNum"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="联系人:"
                          prop="contactUserName">
              <el-input v-model="form.contactUserName"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="联系电话:"
                          prop="contactUserPhone">
              <el-input v-model="form.contactUserPhone"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="合作商类型:"
                          prop="partnerType">
              <el-input v-model="form.partnerType"
                        placeholder="请输入,如:危废处置"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="是否白名单:"
                          prop="isWhite">
              <el-select v-model="form.isWhite"
                         placeholder="请选择"
                         clearable
                         style="width: 100%">
                <el-option label="是"
                           :value="1" />
                <el-option label="否"
                           :value="0" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="备注:"
                          prop="remark">
              <el-input v-model="form.remark"
                        type="textarea"
                        :rows="2"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="附件:">
              <AttachmentUploadFile v-model:file-list="partnerFileList"
                                    :limit="10" />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
                     @click="submitForm">确认</el-button>
          <el-button @click="closeDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- å¯¼å…¥å¯¹è¯æ¡† -->
    <el-dialog :title="upload.title"
               v-model="upload.open"
               width="400px"
               append-to-body>
      <el-upload ref="uploadRef"
                 :limit="1"
                 accept=".xlsx, .xls"
                 :headers="upload.headers"
                 :action="upload.url"
                 :disabled="upload.isUploading"
                 :on-progress="handleFileUploadProgress"
                 :on-success="handleFileSuccess"
                 :on-error="handleFileError"
                 :auto-upload="false"
                 drag>
        <el-icon class="el-icon--upload">
          <upload-filled />
        </el-icon>
        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
        <template #tip>
          <div class="el-upload__tip text-center">
            <span>仅允许导入xls、xlsx格式文件。</span>
            <el-link type="primary"
                     :underline="false"
                     style="font-size: 12px; vertical-align: baseline"
                     @click="importTemplate">下载模板</el-link>
          </div>
        </template>
      </el-upload>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
                     @click="submitFileForm">ç¡® å®š</el-button>
          <el-button @click="upload.open = false">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- é™„件:可直接上传/删除,改动即时保存到该合作商 -->
    <el-dialog v-model="attachmentVisible"
               title="附件"
               width="600px"
               append-to-body>
      <div v-loading="attachmentLoading"
           style="min-height: 140px">
        <AttachmentUploadFile v-model:file-list="attachmentList"
                              :limit="10"
                              @change="saveAttachments" />
      </div>
    </el-dialog>
  </div>
</template>
<script setup>
  import { ref, reactive, toRefs, getCurrentInstance, onMounted } from "vue";
  import pagination from "@/components/PIMTable/Pagination.vue";
  import AttachmentUploadFile from "@/components/AttachmentUpload/file/index.vue";
  import { ElMessageBox } from "element-plus";
  import { getToken } from "@/utils/auth.js";
  import {
    listPartner,
    getPartner,
    addPartner,
    updatePartner,
    delPartner,
    exportPartner,
    downloadPartnerTemplate,
  } from "@/api/outsourcing/partner.js";
  const { proxy } = getCurrentInstance();
  const tableData = ref([]);
  const selectedRows = ref([]);
  const tableLoading = ref(false);
  const dialogFormVisible = ref(false);
  const attachmentVisible = ref(false);
  const attachmentList = ref([]);
  const attachmentLoading = ref(false);
  // é™„件弹框对应的合作商详情:列表行字段不全,保存时用详情里的字段整体回传
  const attachmentDetail = ref({});
  const operationType = ref("");
  const page = reactive({
    current: 1,
    size: 10,
  });
  const total = ref(0);
  const data = reactive({
    searchForm: {
      partnerName: "",
      partnerType: "",
      isWhite: "",
    },
    form: {},
    rules: {
      partnerName: [{ required: true, message: "请输入合作商名称", trigger: "blur" }],
    },
  });
  const { searchForm, form, rules } = toRefs(data);
  // é™„件随表单一并提交,后端按 storageBlobDTOS å…¨é‡è¦†ç›–,清空即解绑
  const partnerFileList = ref([]);
  // åˆä½œå•†ç±»åž‹ç­‰å­—段后端未给字典,类型按自由文本维护
  const buildEmptyForm = () => ({
    id: null,
    partnerName: "",
    taxpayerIdentificationNum: "",
    companyAddress: "",
    companyPhone: "",
    bankAccountName: "",
    bankAccountNum: "",
    contactUserName: "",
    contactUserPhone: "",
    partnerType: "",
    isWhite: 0,
    remark: "",
  });
  // åŽ»æŽ‰ç©ºå€¼ï¼Œé¿å… isWhite='' è¿™ç±»ç©ºä¸²è¢«åŽç«¯å½“成 0 è§£æž
  const compact = obj =>
    Object.fromEntries(
      Object.entries(obj).filter(([, v]) => v !== "" && v !== null && v !== undefined)
    );
  const getList = () => {
    tableLoading.value = true;
    listPartner({
      pageNum: page.current,
      pageSize: page.size,
      ...compact(searchForm.value),
    })
      .then(res => {
        tableLoading.value = false;
        tableData.value = res.data?.records || [];
        total.value = res.data?.total || 0;
      })
      .catch(() => {
        tableLoading.value = false;
      });
  };
  const handleQuery = () => {
    page.current = 1;
    getList();
  };
  const paginationChange = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  const handleSelectionChange = selection => {
    selectedRows.value = selection;
  };
  const openForm = (type, row) => {
    operationType.value = type;
    form.value = buildEmptyForm();
    partnerFileList.value = [];
    proxy.resetForm("formRef");
    dialogFormVisible.value = true;
    if (type === "edit" && row) {
      // è¯¦æƒ…返回的附件可能是 storageBlobVOS æˆ– storageBlobDTOS,回显后统一转成 storageBlobDTOS å…¨é‡æäº¤
      getPartner(row.id).then(res => {
        const { storageBlobVOS, storageBlobDTOS, ...detail } = res.data || {};
        form.value = { ...buildEmptyForm(), ...detail };
        partnerFileList.value = storageBlobVOS || storageBlobDTOS || [];
      });
    }
  };
  // åªæäº¤æ–‡æ¡£çº¦å®šçš„字段,维护人/维护时间/创建时间等由后端维护
  // source/files å¯ä¼ å‚:附件弹框保存时用的是详情数据 + å¼¹æ¡†å†…的附件列表
  const buildPayload = (source = form.value, files = partnerFileList.value) => {
    const {
      id,
      partnerName,
      taxpayerIdentificationNum,
      companyAddress,
      companyPhone,
      bankAccountName,
      bankAccountNum,
      contactUserName,
      contactUserPhone,
      partnerType,
      isWhite,
      remark,
    } = source || {};
    return {
      partnerName,
      taxpayerIdentificationNum,
      companyAddress,
      companyPhone,
      bankAccountName,
      bankAccountNum,
      contactUserName,
      contactUserPhone,
      partnerType,
      isWhite,
      remark,
      storageBlobDTOS: (files || [])
        .filter(item => item && item.id != null)
        .map(item => ({ id: item.id })),
      ...(id ? { id } : {}),
    };
  };
  const submitForm = () => {
    proxy.$refs["formRef"].validate(valid => {
      if (valid) {
        const api = form.value.id ? updatePartner : addPartner;
        api(buildPayload()).then(() => {
          proxy.$modal.msgSuccess("操作成功");
          closeDia();
          getList();
        });
      }
    });
  };
  const closeDia = () => {
    proxy.resetForm("formRef");
    partnerFileList.value = [];
    dialogFormVisible.value = false;
  };
  // å…ˆå¼¹æ¡†å†æ‹‰é™„件:详情接口慢或失败时,按钮也不会看起来像没反应
  const openAttachment = row => {
    attachmentList.value = [];
    attachmentDetail.value = {};
    attachmentVisible.value = true;
    attachmentLoading.value = true;
    getPartner(row.id)
      .then(res => {
        attachmentDetail.value = res.data || {};
        // åˆä½œå•†è¯¦æƒ…后端把附件放在 storageBlobDTOS(其它模块的 VO æ‰å« storageBlobVOS),两个都兜住
        attachmentList.value = res.data?.storageBlobVOS || res.data?.storageBlobDTOS || [];
      })
      .catch(() => {
        attachmentList.value = [];
      })
      .finally(() => {
        attachmentLoading.value = false;
      });
  };
  // å¼¹æ¡†å†…上传/删除后立即保存:后端按 storageBlobDTOS å…¨é‡è¦†ç›–,所以整份列表回传
  const saveAttachments = files => {
    const list = Array.isArray(files) ? files : attachmentList.value;
    attachmentList.value = list;
    if (!attachmentDetail.value?.id) {
      proxy.$modal.msgError("未获取到合作商信息,附件保存失败");
      return;
    }
    attachmentLoading.value = true;
    updatePartner(buildPayload(attachmentDetail.value, list))
      .then(() => {
        attachmentDetail.value = {
          ...attachmentDetail.value,
          storageBlobDTOS: list,
        };
        proxy.$modal.msgSuccess("附件已保存");
      })
      .finally(() => {
        attachmentLoading.value = false;
      });
  };
  const handleDelete = () => {
    if (selectedRows.value.length === 0) {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    const ids = selectedRows.value.map(item => item.id);
    ElMessageBox.confirm(
      "选中的内容将被删除,是否确认删除?已存在委外订单或往来收付款记录的合作商不允许删除。",
      "删除",
      {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
      }
    )
      .then(() => {
        delPartner(ids).then(() => {
          proxy.$modal.msgSuccess("删除成功");
          getList();
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  // ä¿å­˜ Excel æ–‡ä»¶æµ
  const saveExcelBlob = (data, filename) => {
    const blob = new Blob([data], {
      type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    });
    const url = window.URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = url;
    link.download = filename;
    document.body.appendChild(link);
    link.click();
    setTimeout(() => {
      document.body.removeChild(link);
      window.URL.revokeObjectURL(url);
    }, 100);
  };
  const handleExport = () => {
    ElMessageBox.confirm("是否确认导出?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        exportPartner(compact(searchForm.value)).then(res => {
          saveExcelBlob(res, "合作商档案.xlsx");
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  const upload = reactive({
    open: false,
    title: "",
    isUploading: false,
    headers: { Authorization: "Bearer " + getToken() },
    url: import.meta.env.VITE_APP_BASE_API + "/outsourcing/partner/import",
  });
  const handleImport = () => {
    upload.title = "合作商导入";
    upload.open = true;
  };
  const importTemplate = () => {
    downloadPartnerTemplate().then(res => {
      saveExcelBlob(res, "合作商导入模板.xlsx");
    });
  };
  const handleFileUploadProgress = () => {
    upload.isUploading = true;
  };
  const handleFileSuccess = response => {
    upload.isUploading = false;
    if (response.code === 200) {
      proxy.$modal.msgSuccess("文件上传成功");
      upload.open = false;
      proxy.$refs["uploadRef"].clearFiles();
      getList();
    } else {
      proxy.$modal.msgError(response.msg);
    }
  };
  const handleFileError = () => {
    upload.isUploading = false;
    proxy.$modal.msgError("文件上传失败");
  };
  const submitFileForm = () => {
    upload.isUploading = true;
    proxy.$refs["uploadRef"].submit();
  };
  onMounted(() => {
    getList();
  });
</script>
<style scoped lang="scss">
  .search_form {
    margin-bottom: 16px;
  }
  .actions {
    display: flex;
    justify-content: flex-end;
    margin-bottom: 12px;
  }
</style>
src/views/outsourcingManagement/transactions/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,255 @@
<template>
  <div class="app-container">
    <div class="search_form"
         style="margin-bottom: 20px;">
      <span class="search_title">合作商名称:</span>
      <el-input v-model="searchForm.partnerName"
                style="width: 240px"
                placeholder="输入合作商名称"
                @change="handleQuery"
                clearable
                :prefix-icon="Search" />
      <el-button type="primary"
                 @click="handleQuery"
                 style="margin-left: 10px">搜索</el-button>
    </div>
    <el-row :gutter="20">
      <el-col :span="14">
        <div class="table_list">
          <el-table border
                    v-loading="tableLoading"
                    :data="tableData"
                    highlight-current-row
                    style="width: 100%"
                    height="calc(100vh - 18.5em)"
                    @row-click="rowClick">
            <el-table-column align="center"
                             label="序号"
                             type="index"
                             width="60" />
            <el-table-column label="合作商名称"
                             prop="partnerName"
                             min-width="180"
                             show-overflow-tooltip />
            <el-table-column label="合同金额(元)"
                             prop="contractAmounts"
                             min-width="130">
              <template #default="scope">
                {{ formatAmount(scope.row.contractAmounts) }}
              </template>
            </el-table-column>
            <el-table-column label="付款金额(元)"
                             prop="paidAmount"
                             min-width="130">
              <template #default="scope">
                {{ formatAmount(scope.row.paidAmount) }}
              </template>
            </el-table-column>
            <el-table-column label="应付金额(元)"
                             prop="payableAmount"
                             min-width="130">
              <template #default="scope">
                <el-text type="danger">{{ formatAmount(scope.row.payableAmount) }}</el-text>
              </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" />
        </div>
      </el-col>
      <el-col :span="10">
        <div class="table_list">
          <div class="detail-tip">
            åº”付余额 = å·²ç¡®è®¤/已完成的委外订单金额 âˆ’ å·²ä»˜æ¬¾ï¼›æ˜Žç»†æ¯è¡Œçš„「已付款」为合作商级合计,非订单级。
          </div>
          <el-table border
                    v-loading="detailLoading"
                    :data="detailTableData"
                    style="width: 100%"
                    height="calc(100vh - 21em)">
            <el-table-column align="center"
                             label="序号"
                             type="index"
                             width="60" />
            <el-table-column label="委外订单号"
                             prop="orderNo"
                             min-width="150"
                             show-overflow-tooltip />
            <el-table-column label="申请日期"
                             prop="applyDate"
                             min-width="110"
                             show-overflow-tooltip />
            <el-table-column label="订单金额(元)"
                             prop="contractAmount"
                             min-width="120">
              <template #default="scope">
                {{ formatAmount(scope.row.contractAmount) }}
              </template>
            </el-table-column>
            <el-table-column label="状态"
                             prop="status"
                             align="center"
                             width="90">
              <template #default="scope">
                <el-tag :type="statusTagType(scope.row.status)"
                        size="small">
                  {{ statusLabel(scope.row.status) }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column label="合作商已付款(元)"
                             prop="paidAmount"
                             min-width="140">
              <template #default="scope">
                {{ formatAmount(scope.row.paidAmount) }}
              </template>
            </el-table-column>
            <el-table-column label="应付金额(元)"
                             prop="payableAmount"
                             min-width="120">
              <template #default="scope">
                <el-text type="danger">{{ formatAmount(scope.row.payableAmount) }}</el-text>
              </template>
            </el-table-column>
            <el-table-column fixed="right"
                             label="操作"
                             width="110"
                             align="center">
              <template #default="scope">
                <el-button link
                           type="primary"
                           @click.stop="openPaymentDialog(scope.row)">新增付款</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </el-col>
    </el-row>
    <PaymentRecordDialog v-model:visible="paymentDialogVisible"
                         title="合作商付款流水"
                         :party-type="3"
                         :party-id="paymentPartyId"
                         :party-name="paymentPartyName"
                         :direction="1"
                         @changed="refreshAll" />
  </div>
</template>
<script setup>
  import { ref, reactive, toRefs } from "vue";
  import { Search } from "@element-plus/icons-vue";
  import pagination from "@/components/PIMTable/Pagination.vue";
  import PaymentRecordDialog from "@/components/PaymentRecordDialog/index.vue";
  import {
    partnerTransactions,
    partnerTransactionsDetails,
  } from "@/api/outsourcing/report.js";
  const tableData = ref([]);
  const detailTableData = ref([]);
  const tableLoading = ref(false);
  const detailLoading = ref(false);
  const total = ref(0);
  const page = reactive({ current: 1, size: 10 });
  const currentPartnerId = ref("");
  const paymentDialogVisible = ref(false);
  const paymentPartyId = ref("");
  const paymentPartyName = ref("");
  const statusLabelMap = { 0: "草稿", 1: "已确认", 2: "已完成", 3: "已作废" };
  const statusTagMap = { 0: "info", 1: "primary", 2: "success", 3: "danger" };
  const statusLabel = status => statusLabelMap[status] ?? status;
  const statusTagType = status => statusTagMap[status] || "info";
  const data = reactive({
    searchForm: {
      partnerName: "",
    },
  });
  const { searchForm } = toRefs(data);
  const formatAmount = value => {
    const num = Number(value);
    return Number.isFinite(num) ? num.toFixed(2) : "0.00";
  };
  const getList = () => {
    tableLoading.value = true;
    partnerTransactions({
      pageNum: page.current,
      pageSize: page.size,
      ...searchForm.value,
    })
      .then(res => {
        tableLoading.value = false;
        tableData.value = res.data?.records || [];
        total.value = res.data?.total || 0;
      })
      .catch(() => {
        tableLoading.value = false;
      });
  };
  const getDetailList = partnerId => {
    if (!partnerId) {
      detailTableData.value = [];
      return;
    }
    detailLoading.value = true;
    partnerTransactionsDetails(partnerId)
      .then(res => {
        detailLoading.value = false;
        detailTableData.value = res.data?.records || [];
      })
      .catch(() => {
        detailLoading.value = false;
      });
  };
  const handleQuery = () => {
    page.current = 1;
    getList();
  };
  const paginationChange = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  const rowClick = row => {
    currentPartnerId.value = row.partnerId;
    getDetailList(row.partnerId);
  };
  const openPaymentDialog = row => {
    paymentPartyId.value = row.partnerId;
    paymentPartyName.value = row.partnerName || "";
    paymentDialogVisible.value = true;
  };
  const refreshAll = () => {
    getList();
    getDetailList(currentPartnerId.value);
  };
  getList();
</script>
<style scoped lang="scss">
  .detail-tip {
    margin-bottom: 10px;
    padding: 8px 12px;
    font-size: 12px;
    line-height: 1.6;
    color: #909399;
    background: rgba(64, 158, 255, 0.08);
    border-left: 3px solid #409eff;
    border-radius: 3px;
  }
</style>
src/views/procurementManagement/paymentLedger/index.vue
@@ -46,17 +46,31 @@
                             prop="paymentAmount"
                             show-overflow-tooltip
                             :formatter="formattedNumber" />
            <el-table-column label="退货金额(元)"
                             prop="returnAmount"
                             show-overflow-tooltip
                             :formatter="formattedNumber" />
<!--            <el-table-column label="回款金额(元)"-->
<!--                             prop="receiptAmount"-->
<!--                             show-overflow-tooltip-->
<!--                             :formatter="formattedNumber" />-->
<!--            <el-table-column label="退货金额(元)"-->
<!--                             prop="returnAmount"-->
<!--                             show-overflow-tooltip-->
<!--                             :formatter="formattedNumber" />-->
            <el-table-column label="应付金额(元)"
                             prop="payableAmount"
                             prop="paymentRecordAmount"
                             show-overflow-tooltip>
              <template #default="{ row, column }">
                <el-text type="danger">
                  {{ formattedNumber(row, column, row.payableAmount) }}
                  {{ formattedNumber(row, column, row.paymentRecordAmount) }}
                </el-text>
              </template>
            </el-table-column>
            <el-table-column label="操作"
                             align="center"
                             width="90"
                             fixed="right">
              <template #default="{ row }">
                <el-button link
                           type="primary"
                           @click.stop="openRecordDialog(row)">流水</el-button>
              </template>
            </el-table-column>
          </el-table>
@@ -90,6 +104,14 @@
        </div>
      </el-col>
    </el-row>
    <!-- ä¾›åº”商回款流水维护 -->
    <PaymentRecordDialog v-model:visible="recordDialogVisible"
                         title="供应商回款流水"
                         :party-type="2"
                         :party-id="recordPartyId"
                         :party-name="recordPartyName"
                         :direction="2"
                         @changed="getList" />
  </div>
</template>
@@ -101,6 +123,7 @@
    paymentRecordList,
  } from "@/api/procurementManagement/paymentLedger.js";
  import Pagination from "../../../components/PIMTable/Pagination.vue";
  import PaymentRecordDialog from "@/components/PaymentRecordDialog/index.vue";
  const tableData = ref([]);
  const tableLoading = ref(false);
@@ -123,6 +146,16 @@
  const isShowSummary = ref(true);
  const { searchForm } = toRefs(data);
  const currentSupplierId = ref("");
  // å›žæ¬¾æµæ°´å¼¹çª—
  const recordDialogVisible = ref(false);
  const recordPartyId = ref("");
  const recordPartyName = ref("");
  const openRecordDialog = row => {
    recordPartyId.value = row.supplierId;
    recordPartyName.value = row.supplierName || "";
    recordDialogVisible.value = true;
  };
  const rowClick = row => {
    currentSupplierId.value = row.supplierId;
    sonPage.current = 1;
@@ -156,19 +189,28 @@
        return params ? parseFloat(params).toFixed(2) : 0;
      },
    },
    {
      label: "退货金额(元)",
      prop: "returnAmount",
      width: 200,
      formatData: params => {
        return params ? parseFloat(params).toFixed(2) : 0;
      },
    },
    // å¾€æ¥æ–¹ç»´åº¦åˆè®¡ï¼Œæ˜Žç»†å„行同值,故不参与合计行
    // {
    //   label: "回款金额(元)",
    //   prop: "receiptAmount",
    //   width: 200,
    //   formatData: params => {
    //     return params ? parseFloat(params).toFixed(2) : 0;
    //   },
    // },
    // {
    //   label: "退货金额(元)",
    //   prop: "returnAmount",
    //   width: 200,
    //   formatData: params => {
    //     return params ? parseFloat(params).toFixed(2) : 0;
    //   },
    // },
    {
      label: "应付金额(元)",
      dataType: "slot",
      width: 200,
      prop: "payableAmount",
      prop: "paymentRecordAmount",
      slot: "payableAmountSlot",
    },
  ]);
@@ -182,7 +224,7 @@
  const summarizeMainTable = param => {
    return proxy.summarizeTable(
      param,
      ["contractAmounts", "paymentAmount", "returnAmount", "payableAmount"],
      ["contractAmounts", "paymentAmount", "receiptAmount", "returnAmount", "payableAmount"],
      {
        ticketsNum: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
        futureTickets: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
src/views/procurementManagement/purchaseApplication/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1087 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <div>
        <el-form :model="searchForm"
                 :inline="true">
          <el-form-item label="申请单号:">
            <el-input v-model="searchForm.applicationNo"
                      placeholder="请输入"
                      clearable
                      :prefix-icon="Search"
                      @change="handleQuery" />
          </el-form-item>
          <el-form-item label="项目名称:">
            <el-input v-model="searchForm.projectName"
                      placeholder="请输入"
                      clearable
                      :prefix-icon="Search"
                      @change="handleQuery" />
          </el-form-item>
          <el-form-item label="供应商:">
            <el-select v-model="searchForm.supplierId"
                       placeholder="请选择"
                       filterable
                       clearable
                       style="width: 240px"
                       @change="handleQuery">
              <el-option v-for="item in supplierOptions"
                         :key="item.id"
                         :label="item.supplierName"
                         :value="item.id" />
            </el-select>
          </el-form-item>
          <el-form-item label="状态:">
            <el-select v-model="searchForm.status"
                       placeholder="请选择"
                       clearable
                       style="width: 160px"
                       @change="handleQuery">
              <el-option label="草稿"
                         :value="0" />
              <el-option label="已生成订单"
                         :value="1" />
            </el-select>
          </el-form-item>
          <el-form-item label="申请日期:">
            <el-date-picker v-model="searchForm.applyDate"
                            value-format="YYYY-MM-DD"
                            format="YYYY-MM-DD"
                            type="daterange"
                            placeholder="请选择"
                            clearable
                            @change="changeDaterange" />
          </el-form-item>
          <el-form-item>
            <el-button type="primary"
                       @click="handleQuery"> æœç´¢ </el-button>
          </el-form-item>
        </el-form>
      </div>
    </div>
    <div class="table_list">
      <div style="display: flex;justify-content: flex-end;margin-bottom: 20px;">
        <el-button type="primary"
                   @click="openForm('add')">新增申请单</el-button>
        <el-button type="danger"
                   plain
                   @click="handleDelete()">删除</el-button>
      </div>
      <el-table :data="tableData"
                border
                v-loading="tableLoading"
                @selection-change="handleSelectionChange"
                :row-key="(row) => row.id"
                show-summary
                :summary-method="summarizeMainTable"
                height="calc(100vh - 21.5em)">
        <el-table-column align="center"
                         type="selection"
                         width="55"
                         :selectable="row => row.status === 0" />
        <el-table-column align="center"
                         label="序号"
                         type="index"
                         width="60" />
        <el-table-column label="申请单号"
                         prop="applicationNo"
                         width="170"
                         show-overflow-tooltip />
        <el-table-column label="供应商名称"
                         prop="supplierName"
                         width="200"
                         show-overflow-tooltip />
        <el-table-column label="项目名称"
                         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"
                         show-overflow-tooltip />
        <el-table-column label="申请日期"
                         prop="applyDate"
                         width="110"
                         show-overflow-tooltip />
        <el-table-column label="期望到货日期"
                         prop="expectedArrivalDate"
                         width="130"
                         show-overflow-tooltip />
        <el-table-column label="合同金额(元)"
                         prop="contractAmount"
                         width="140"
                         show-overflow-tooltip
                         :formatter="formattedNumber" />
        <el-table-column label="付款方式"
                         prop="paymentMethod"
                         width="100"
                         show-overflow-tooltip />
        <el-table-column label="状态"
                         prop="status"
                         width="110"
                         align="center">
          <template #default="scope">
            <el-tag :type="scope.row.status === 1 ? 'success' : 'info'"
                    size="small">
              {{ statusText[scope.row.status] || '--' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="备注"
                         prop="remarks"
                         width="200"
                         show-overflow-tooltip />
        <el-table-column fixed="right"
                         label="操作"
                         width="200"
                         align="center">
          <template #default="scope">
            <template v-if="scope.row.status === 0">
              <el-button link
                         type="primary"
                         @click="openForm('edit', scope.row)">编辑</el-button>
              <el-button link
                         type="primary"
                         @click="handleConvert(scope.row)">转订单</el-button>
              <el-button link
                         type="danger"
                         @click="handleDelete(scope.row)">删除</el-button>
            </template>
            <el-button v-else
                       link
                       type="primary"
                       @click="openForm('detail', 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" />
    </div>
    <FormDialog v-model="dialogFormVisible"
                :title="dialogTitle"
                :width="'70%'"
                :operation-type="operationType"
                @close="closeDia"
                @confirm="submitForm"
                @cancel="closeDia">
      <el-form :model="form"
               :disabled="isDetail"
               label-width="140px"
               label-position="top"
               :rules="rules"
               ref="formRef">
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="申请单号:"
                          prop="applicationNo">
              <el-input v-model="form.applicationNo"
                        placeholder="保存后自动生成"
                        disabled />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="申请日期:"
                          prop="applyDate">
              <el-date-picker style="width: 100%"
                              v-model="form.applyDate"
                              value-format="YYYY-MM-DD"
                              format="YYYY-MM-DD"
                              type="date"
                              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"
                         placeholder="请选择"
                         filterable
                         clearable
                         style="width: 100%">
                <el-option v-for="item in supplierList"
                           :key="item.id"
                           :label="item.supplierName"
                           :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="项目名称"
                          prop="projectName">
              <el-input v-model="form.projectName"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </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"
                         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-row>
        <el-row :gutter="30">
          <el-col :span="8">
            <el-form-item label="期望到货日期:"
                          prop="expectedArrivalDate">
              <el-date-picker style="width: 100%"
                              v-model="form.expectedArrivalDate"
                              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="contractAmount">
              <el-input-number v-model="form.contractAmount"
                               :precision="2"
                               :step="0.1"
                               :min="0"
                               placeholder="请输入"
                               style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="付款方式:"
                          prop="paymentMethod">
              <el-input v-model="form.paymentMethod"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-form-item label="产品信息:">
            <el-button v-if="!isDetail"
                       type="primary"
                       @click="openProductForm('add')">添加</el-button>
            <el-button v-if="!isDetail"
                       plain
                       type="danger"
                       @click="deleteProduct">删除</el-button>
          </el-form-item>
        </el-row>
        <el-table :data="productData"
                  border
                  @selection-change="productSelected"
                  show-summary
                  :summary-method="summarizeProTable">
          <el-table-column align="center"
                           type="selection"
                           width="55"
                           :selectable="() => !isDetail" />
          <el-table-column align="center"
                           label="序号"
                           type="index"
                           width="60" />
          <el-table-column label="产品大类"
                           prop="productCategory"
                           show-overflow-tooltip />
          <el-table-column label="规格型号"
                           prop="specificationModel"
                           show-overflow-tooltip />
          <el-table-column label="单位"
                           prop="unit"
                           width="70" />
          <el-table-column label="数量"
                           prop="quantity"
                           width="80" />
          <el-table-column label="税率(%)"
                           prop="taxRate"
                           width="80" />
          <el-table-column label="含税单价(元)"
                           prop="taxInclusiveUnitPrice"
                           width="120"
                           :formatter="formattedNumber" />
          <el-table-column label="含税总价(元)"
                           prop="taxInclusiveTotalPrice"
                           width="120"
                           :formatter="formattedNumber" />
          <el-table-column label="不含税总价(元)"
                           prop="taxExclusiveTotalPrice"
                           width="130"
                           :formatter="formattedNumber" />
          <el-table-column label="发票类型"
                           prop="invoiceType"
                           width="110"
                           show-overflow-tooltip />
          <el-table-column label="是否质检"
                           prop="isChecked"
                           width="90">
            <template #default="scope">
              <el-tag :type="scope.row.isChecked ? 'success' : 'info'"
                      size="small">
                {{ scope.row.isChecked ? '是' : '否' }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column label="备注"
                           prop="remark"
                           show-overflow-tooltip />
          <el-table-column v-if="!isDetail"
                           fixed="right"
                           label="操作"
                           width="80"
                           align="center">
            <template #default="scope">
              <el-button link
                         type="primary"
                         @click="openProductForm('edit', scope.row, scope.$index)">编辑
              </el-button>
            </template>
          </el-table-column>
        </el-table>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="备注:"
                          prop="remarks">
              <el-input v-model="form.remarks"
                        placeholder="请输入"
                        clearable
                        type="textarea"
                        :rows="2" />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
    </FormDialog>
    <FormDialog v-model="productFormVisible"
                :title="productOperationType === 'add' ? '新增产品' : '编辑产品'"
                :width="'40%'"
                :operation-type="productOperationType"
                @close="closeProductDia"
                @confirm="submitProduct"
                @cancel="closeProductDia">
      <el-form :model="productForm"
               label-width="140px"
               label-position="top"
               :rules="productRules"
               ref="productFormRef">
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="产品大类:"
                          prop="productId">
              <el-tree-select v-model="productForm.productId"
                              placeholder="请选择"
                              clearable
                              filterable
                              check-strictly
                              @change="getModels"
                              :data="productOptions"
                              :render-after-expand="false"
                              style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="规格型号:"
                          prop="productModelId">
              <el-select v-model="productForm.productModelId"
                         placeholder="请选择"
                         filterable
                         clearable
                         style="width: 100%"
                         @change="getProductModel">
                <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="productForm.unit"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="税率(%):"
                          prop="taxRate">
              <el-select v-model="productForm.taxRate"
                         placeholder="请选择"
                         clearable
                         style="width: 100%"
                         @change="calcTotals">
                <el-option v-for="dict in tax_rate"
                           :key="dict.value"
                           :label="dict.label"
                           :value="dict.value" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="含税单价(元):"
                          prop="taxInclusiveUnitPrice">
              <el-input-number v-model="productForm.taxInclusiveUnitPrice"
                               :precision="2"
                               :step="0.1"
                               :min="0"
                               style="width: 100%"
                               @change="calcTotals" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="数量:"
                          prop="quantity">
              <el-input-number v-model="productForm.quantity"
                               :precision="2"
                               :step="0.1"
                               :min="0"
                               placeholder="请输入"
                               style="width: 100%"
                               @change="calcTotals" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="含税总价(元):"
                          prop="taxInclusiveTotalPrice">
              <el-input-number v-model="productForm.taxInclusiveTotalPrice"
                               :precision="2"
                               disabled
                               style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="不含税总价(元):"
                          prop="taxExclusiveTotalPrice">
              <el-input-number v-model="productForm.taxExclusiveTotalPrice"
                               :precision="2"
                               disabled
                               style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="发票类型:"
                          prop="invoiceType">
              <el-select v-model="productForm.invoiceType"
                         placeholder="请选择"
                         clearable
                         style="width: 100%">
                <el-option label="增普票"
                           value="增普票" />
                <el-option label="增专票"
                           value="增专票" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="是否质检:"
                          prop="isChecked">
              <el-radio-group v-model="productForm.isChecked">
                <el-radio label="是"
                          :value="true" />
                <el-radio label="否"
                          :value="false" />
              </el-radio-group>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="备注:"
                          prop="remark">
              <el-input v-model="productForm.remark"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
    </FormDialog>
  </div>
</template>
<script setup>
  import FormDialog from "@/components/Dialog/FormDialog.vue";
  import pagination from "@/components/PIMTable/Pagination.vue";
  import {
    ref,
    reactive,
    toRefs,
    computed,
    getCurrentInstance,
    nextTick,
    onMounted,
  } from "vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import { Search } from "@element-plus/icons-vue";
  import dayjs from "dayjs";
  import router from "@/router";
  import usePermissionStore from "@/store/modules/permission";
  import {
    applicationList,
    getApplicationById,
    addApplication,
    editApplication,
    delApplication,
    convertToOrder,
  } from "@/api/procurementManagement/purchaseApplication.js";
  import {
    getSalesNo,
    getOptions,
  } from "@/api/procurementManagement/procurementLedger.js";
  import { modelList, productTreeList } from "@/api/basicData/product.js";
  import { userListNoPage } from "@/api/system/user.js";
  const { proxy } = getCurrentInstance();
  const { tax_rate } = proxy.useDict("tax_rate");
  const permissionStore = usePermissionStore();
  const statusText = {
    0: "草稿",
    1: "已生成订单",
  };
  const tableData = ref([]);
  const productData = ref([]);
  const selectedRows = ref([]);
  const productSelectedRows = ref([]);
  const supplierOptions = ref([]);
  const supplierList = ref([]);
  const salesContractList = ref([]);
  const userList = ref([]);
  const productOptions = ref([]);
  const modelOptions = ref([]);
  const tableLoading = ref(false);
  const dialogFormVisible = ref(false);
  const productFormVisible = ref(false);
  const operationType = ref("");
  const productOperationType = ref("");
  const productOperationIndex = ref("");
  const currentId = ref("");
  const total = ref(0);
  const page = reactive({ current: 1, size: 10 });
  const isDetail = computed(() => operationType.value === "detail");
  const dialogTitle = computed(() => {
    if (operationType.value === "add") return "新增采购申请";
    if (operationType.value === "edit") return "编辑采购申请";
    return "采购申请详情";
  });
  const searchForm = reactive({
    applicationNo: "",
    projectName: "",
    supplierId: "",
    status: "",
    applyDate: null,
    startDate: undefined,
    endDate: undefined,
  });
  const data = reactive({
    form: {
      id: "",
      applicationNo: "",
      supplierId: "",
      projectName: "",
      salesContractNo: "",
      salesLedgerId: "",
      applicantId: "",
      applyDate: "",
      expectedArrivalDate: "",
      contractAmount: "",
      paymentMethod: "",
      remarks: "",
    },
    rules: {
      applyDate: [{ required: true, message: "请选择", trigger: "change" }],
    },
  });
  const { form, rules } = toRefs(data);
  const productFormData = reactive({
    productForm: {
      productId: "",
      productCategory: "",
      productModelId: "",
      specificationModel: "",
      unit: "",
      quantity: "",
      taxRate: "",
      taxInclusiveUnitPrice: "",
      taxInclusiveTotalPrice: "",
      taxExclusiveTotalPrice: "",
      invoiceType: "",
      isChecked: false,
      remark: "",
    },
    productRules: {
      productId: [{ required: true, message: "请选择", trigger: "change" }],
      productModelId: [{ required: true, message: "请选择", trigger: "change" }],
      unit: [{ required: true, message: "请输入", trigger: "blur" }],
      quantity: [{ required: true, message: "请输入", trigger: "blur" }],
      taxRate: [{ required: true, message: "请选择", trigger: "change" }],
      taxInclusiveUnitPrice: [
        { required: true, message: "请输入", trigger: "blur" },
      ],
    },
  });
  const { productForm, productRules } = toRefs(productFormData);
  const formattedNumber = (row, column, cellValue) => {
    if (cellValue === null || cellValue === undefined || cellValue === "") {
      return "--";
    }
    return Number(cellValue).toFixed(2);
  };
  const changeDaterange = value => {
    if (value) {
      searchForm.startDate = dayjs(value[0]).format("YYYY-MM-DD");
      searchForm.endDate = dayjs(value[1]).format("YYYY-MM-DD");
    } else {
      searchForm.startDate = undefined;
      searchForm.endDate = undefined;
    }
    handleQuery();
  };
  const handleQuery = () => {
    page.current = 1;
    getList();
  };
  const paginationChange = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  const getList = () => {
    tableLoading.value = true;
    const { applyDate, ...rest } = searchForm;
    applicationList({ ...rest, pageNum: page.current, pageSize: page.size })
      .then(res => {
        const rows = res.rows || res.data?.rows || res.data?.records || [];
        tableData.value = rows;
        total.value = res.total ?? res.data?.total ?? 0;
      })
      .catch(() => {
        tableData.value = [];
        total.value = 0;
      })
      .finally(() => {
        tableLoading.value = false;
      });
  };
  const handleSelectionChange = selection => {
    selectedRows.value = selection;
  };
  const productSelected = selection => {
    productSelectedRows.value = selection;
  };
  const summarizeMainTable = param => {
    return proxy.summarizeTable(param, ["contractAmount"]);
  };
  const summarizeProTable = param => {
    return proxy.summarizeTable(param, [
      "taxInclusiveUnitPrice",
      "taxInclusiveTotalPrice",
      "taxExclusiveTotalPrice",
    ]);
  };
  // ä¾›åº”商下拉:筛选用全量,表单仅可选白名单供应商(与采购台账一致)
  const getSupplierOptions = async () => {
    const res = await getOptions();
    const list = res.data || [];
    supplierOptions.value = list;
    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 getProductOptions = async () => {
    const res = await productTreeList();
    productOptions.value = convertIdToValue(res || []);
  };
  function convertIdToValue(list) {
    return list.map(item => {
      const { id, children, ...rest } = item;
      const newItem = { ...rest, value: id };
      if (children && children.length > 0) {
        newItem.children = convertIdToValue(children);
      }
      return newItem;
    });
  }
  const findNodeLabelById = (nodes, value) => {
    for (const node of nodes) {
      if (node.value === value) {
        return node.label;
      }
      if (node.children && node.children.length > 0) {
        const found = findNodeLabelById(node.children, value);
        if (found) return found;
      }
    }
    return "";
  };
  const findNodeValueByLabel = (nodes, label) => {
    for (const node of nodes) {
      if (node.label === label) {
        return node.value;
      }
      if (node.children && node.children.length > 0) {
        const found = findNodeValueByLabel(node.children, label);
        if (found) return found;
      }
    }
    return "";
  };
  const getModels = value => {
    if (!value) {
      productForm.value.productCategory = "";
      modelOptions.value = [];
      return Promise.resolve([]);
    }
    productForm.value.productCategory = findNodeLabelById(
      productOptions.value,
      value
    );
    return modelList({ id: value }).then(res => {
      modelOptions.value = res || [];
      return res;
    });
  };
  const getProductModel = value => {
    const matched = modelOptions.value.find(item => item.id === value);
    if (matched) {
      productForm.value.specificationModel = matched.model;
      productForm.value.unit = matched.unit;
    } else {
      productForm.value.specificationModel = "";
      productForm.value.unit = "";
    }
  };
  // å«ç¨Žæ€»ä»· = å«ç¨Žå•ä»· Ã— æ•°é‡ï¼›ä¸å«ç¨Žæ€»ä»·ç”±å«ç¨Žæ€»ä»·ä¸Žç¨ŽçŽ‡åç®—
  const calcTotals = () => {
    const { taxInclusiveUnitPrice, quantity, taxRate } = productForm.value;
    if (!taxInclusiveUnitPrice && taxInclusiveUnitPrice !== 0) return;
    if (!quantity && quantity !== 0) return;
    productForm.value.taxInclusiveTotalPrice =
      proxy.calculateTaxIncludeTotalPrice(taxInclusiveUnitPrice, quantity);
    if (taxRate) {
      productForm.value.taxExclusiveTotalPrice =
        proxy.calculateTaxExclusiveTotalPrice(
          productForm.value.taxInclusiveTotalPrice,
          taxRate
        );
    }
  };
  const openProductForm = async (type, row, index) => {
    productOperationType.value = type;
    productOperationIndex.value = index;
    productForm.value = {};
    proxy.resetForm("productFormRef");
    productFormVisible.value = true;
    if (productOptions.value.length === 0) {
      await getProductOptions();
    }
    await nextTick();
    if (type === "add") {
      productForm.value.isChecked = false;
      return;
    }
    productForm.value = { ...row };
    if (!productForm.value.productId && productForm.value.productCategory) {
      productForm.value.productId = findNodeValueByLabel(
        productOptions.value,
        productForm.value.productCategory
      );
    }
    if (productForm.value.productId) {
      await getModels(productForm.value.productId);
      await nextTick();
      if (productForm.value.productModelId) {
        getProductModel(productForm.value.productModelId);
      }
    }
  };
  const submitProduct = () => {
    proxy.$refs["productFormRef"].validate(valid => {
      if (!valid) return;
      if (productOperationType.value === "add") {
        productData.value.push({ ...productForm.value });
      } else {
        productData.value[productOperationIndex.value] = {
          ...productForm.value,
        };
      }
      closeProductDia();
    });
  };
  const deleteProduct = () => {
    if (productSelectedRows.value.length === 0) {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    productData.value = productData.value.filter(
      item => !productSelectedRows.value.includes(item)
    );
    productSelectedRows.value = [];
  };
  const closeProductDia = () => {
    proxy.resetForm("productFormRef");
    productFormVisible.value = false;
  };
  const salesLedgerChange = value => {
    const matched = salesContractList.value.find(item => item.id === value);
    form.value.salesContractNo = matched ? matched.salesContractNo : "";
  };
  const openForm = async (type, row) => {
    operationType.value = type;
    currentId.value = row?.id || "";
    proxy.resetForm("formRef");
    form.value = {
      id: "",
      applicationNo: "",
      supplierId: "",
      projectName: "",
      salesContractNo: "",
      salesLedgerId: "",
      applicantId: "",
      applyDate: dayjs().format("YYYY-MM-DD"),
      expectedArrivalDate: "",
      contractAmount: "",
      paymentMethod: "",
      remarks: "",
    };
    productData.value = [];
    productSelectedRows.value = [];
    try {
      await Promise.all([
        getSupplierOptions(),
        getSalesContractList(),
        getUserList(),
        getProductOptions(),
      ]);
    } catch (error) {
      proxy.$modal.msgError("加载基础数据失败");
      return;
    }
    if (type === "add") {
      dialogFormVisible.value = true;
      return;
    }
    try {
      const res = await getApplicationById({ id: row.id });
      form.value = { ...(res.data || {}), id: row.id };
      productData.value = res.data?.productData || [];
    } catch (error) {
      proxy.$modal.msgError("加载采购申请详情失败");
      return;
    }
    dialogFormVisible.value = true;
  };
  const closeDia = () => {
    proxy.resetForm("formRef");
    dialogFormVisible.value = false;
  };
  // åªæäº¤åŽç«¯å…è®¸çš„字段,自动字段(单号、状态、租户等)一律不传
  const buildPayload = () => {
    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,
      productData: productData.value.map(
        ({ id, purchaseApplicationId, ...rest }) => rest
      ),
    };
    // ç”³è¯·äººç•™ç©ºè¡¨ç¤ºç”±åŽç«¯å–当前登录用户
    if (form.value.applicantId) {
      payload.applicantId = form.value.applicantId;
    }
    return payload;
  };
  const submitForm = () => {
    proxy.$refs["formRef"].validate(async valid => {
      if (!valid) return;
      const payload = buildPayload();
      try {
        if (operationType.value === "add") {
          await addApplication(payload);
        } else {
          await editApplication({ id: currentId.value, ...payload });
        }
        proxy.$modal.msgSuccess("保存成功");
        closeDia();
        getList();
      } catch (error) {
        // é”™è¯¯ä¿¡æ¯ç”± request æ‹¦æˆªå™¨ç»Ÿä¸€æç¤º
      }
    });
  };
  const handleDelete = row => {
    let ids = [];
    if (row) {
      ids = [row.id];
    } else if (selectedRows.value.length > 0) {
      ids = selectedRows.value.map(item => item.id);
    } else {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(async () => {
        await delApplication(ids);
        proxy.$modal.msgSuccess("删除成功");
        getList();
      })
      .catch(() => {});
  };
  const handleConvert = row => {
    ElMessageBox.confirm(
      `确认将申请单「${row.applicationNo}」转为采购订单吗?转订单后将自动提交采购审批。`,
      "转订单",
      {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
      }
    )
      .then(async () => {
        const res = await convertToOrder(row.id);
        getList();
        ElMessageBox.confirm(
          `转订单成功,已生成采购台账(id:${res.data})。是否前往采购台账查看?`,
          "提示",
          {
            confirmButtonText: "前往查看",
            cancelButtonText: "留在本页",
            type: "success",
          }
        )
          .then(() => {
            const path = findLedgerPath(permissionStore.sidebarRouters);
            if (path) {
              router.push(path);
            } else {
              ElMessage.info("请前往「采购台账」查看生成的订单");
            }
          })
          .catch(() => {});
      })
      .catch(() => {});
  };
  // ä»Žä¾§è¾¹æ èœå•中定位采购台账路由,找不到时不跳转仅提示
  const findLedgerPath = (routes, prefix = "") => {
    for (const route of routes || []) {
      const fullPath = String(route.path || "").startsWith("/")
        ? route.path
        : `${prefix}/${route.path || ""}`;
      if (route.meta?.title === "采购台账") {
        return fullPath;
      }
      const found = findLedgerPath(route.children, fullPath);
      if (found) return found;
    }
    return "";
  };
  onMounted(() => {
    getList();
    getSupplierOptions();
  });
</script>
src/views/procurementManagement/transferManagement/index.vue
@@ -191,9 +191,9 @@
    });
    // ä½¿ç”¨ Promise.all å¤„理所有异步请求
    const processedOrders = await Promise.all(response.data.records.map(async (order) => {
      // ç­‰å¾…异步获取产品记录
      // ç­‰å¾…异步获取产品记录(按采购台账id精确反显,合同号可能重复/为空)
      const productRes = await selectProductRecordListByPuechaserId({
        purchaseContractNumber: order.purchaseContractNumber
        purchaseLedgerId: order.id
      });
      // ç¡®ä¿ productRes.data å­˜åœ¨
@@ -317,9 +317,9 @@
{
  try {
    loadingProducts.value = true
    // æ ¹æ®åˆåŒæŸ¥è¯¢äº§å“è®°å½•
    // æ ¹æ®é‡‡è´­å°è´¦id查询产品记录
    const productRes = await selectProductRecordListByPuechaserId({
      purchaseContractNumber: receiptForm.value.purchaseContractNumber
      purchaseLedgerId: receiptForm.value.purchaseLedgerId
    });
    console.log('productRes:', productRes)
    operationType.value = 'add'
src/views/salesManagement/deliveryLedger/index.vue
@@ -2,7 +2,17 @@
  <div class="app-container">
    <div class="search_form">
      <el-form :model="searchForm" :inline="true">
        <el-form-item label="销售订单号:">
        <el-form-item label="发货单号:">
          <el-input
            v-model="searchForm.shippingNo"
            placeholder="请输入"
            clearable
            prefix-icon="Search"
            style="width: 200px"
            @change="handleQuery"
          />
        </el-form-item>
        <el-form-item label="销售合同号:">
          <el-input
            v-model="searchForm.salesContractNo"
            placeholder="请输入"
@@ -15,16 +25,6 @@
        <el-form-item label="车牌号:">
          <el-input
            v-model="searchForm.shippingCarNumber"
            placeholder="请输入"
            clearable
            prefix-icon="Search"
            style="width: 200px"
            @change="handleQuery"
          />
        </el-form-item>
        <el-form-item label="快递单号:">
          <el-input
            v-model="searchForm.expressNumber"
            placeholder="请输入"
            clearable
            prefix-icon="Search"
@@ -50,17 +50,27 @@
        border
        v-loading="tableLoading"
        @selection-change="handleSelectionChange"
        :row-key="(row) => row.id"
        :row-key="(row) => `${row.source || 'shipping'}-${row.stockOutRecordId ?? row.id}`"
        style="width: 100%"
        height="calc(100vh - 21.5em)"
      >
        <el-table-column align="center" type="selection" width="55" />
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column
          label="销售订单"
          prop="salesContractNo"
          show-overflow-tooltip
          align="center"
          type="selection"
          :selectable="isRowSelectable"
          width="55"
        />
        <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>
          </template>
        </el-table-column>
        <el-table-column
          label="发货订单号"
          prop="shippingNo"
@@ -72,6 +82,16 @@
          show-overflow-tooltip
        />
        <el-table-column
          label="客户对接人"
          prop="contactPerson"
          show-overflow-tooltip
        />
        <el-table-column
          label="对接人电话"
          prop="contactPhone"
          show-overflow-tooltip
        />
        <el-table-column
          label="产品名称"
          prop="productName"
          show-overflow-tooltip
@@ -79,6 +99,27 @@
        <el-table-column
          label="规格型号"
          prop="specificationModel"
          show-overflow-tooltip
        />
        <el-table-column label="批号" show-overflow-tooltip>
          <!-- æ²¹å“å‡ºåº“行的批号是 outBatchNo(batch_no),发货单行没有该字段 -->
          <template #default="scope">
            {{
              isOilOutRow(scope.row)
                ? scope.row.outBatchNo || "--"
                : scope.row.batchNo || "--"
            }}
          </template>
        </el-table-column>
        <el-table-column
          label="出库类别"
          prop="outCategory"
          show-overflow-tooltip
        />
        <el-table-column label="储罐号" prop="tankNo" show-overflow-tooltip />
        <el-table-column
          label="油库"
          prop="oilDepotName"
          show-overflow-tooltip
        />
        <el-table-column
@@ -97,13 +138,8 @@
          show-overflow-tooltip
        />
        <el-table-column
          label="快递公司"
          prop="expressCompany"
          show-overflow-tooltip
        />
        <el-table-column
          label="快递单号"
          prop="expressNumber"
          label="司机姓名"
          prop="driverName"
          show-overflow-tooltip
        />
        <el-table-column
@@ -173,21 +209,6 @@
      >
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="发货类型:" prop="type">
              <el-select
                v-model="form.type"
                placeholder="请选择发货类型"
                style="width: 100%"
                @change="handleShippingTypeChange"
              >
                <el-option label="货车" value="货车" />
                <el-option label="快递" value="快递" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="发货日期:" prop="shippingDate">
              <el-date-picker
                style="width: 100%"
@@ -202,31 +223,11 @@
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24" v-if="form.type === '货车'">
          <el-col :span="24">
            <el-form-item label="发货车牌号:" prop="shippingCarNumber">
              <el-input
                v-model="form.shippingCarNumber"
                placeholder="请输入发货车牌号"
                clearable
              />
            </el-form-item>
          </el-col>
          <el-col :span="24" v-else>
            <el-form-item label="快递公司:" prop="expressCompany">
              <el-input
                v-model="form.expressCompany"
                placeholder="请输入快递公司"
                clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30" v-if="form.type === '快递'">
          <el-col :span="24">
            <el-form-item label="快递单号:" prop="expressNumber">
              <el-input
                v-model="form.expressNumber"
                placeholder="请输入快递单号"
                clearable
              />
            </el-form-item>
@@ -257,14 +258,17 @@
    >
      <div v-if="detailRow" class="detail-wrapper">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="销售订单">{{
            detailRow.salesContractNo || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="发货订单号">{{
            detailRow.shippingNo || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="客户名称">{{
            detailRow.customerName || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="客户对接人">{{
            detailRow.contactPerson || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="对接人电话">{{
            detailRow.contactPhone || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="产品名称">{{
            detailRow.productName || "--"
@@ -272,8 +276,14 @@
          <el-descriptions-item label="规格型号">{{
            detailRow.specificationModel || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="发货类型">{{
            detailRow.type || "--"
          <el-descriptions-item label="出库类别">{{
            detailRow.outCategory || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="储罐号">{{
            detailRow.tankNo || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="油库">{{
            detailRow.oilDepotName || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="发货日期">{{
            detailRow.shippingDate || "--"
@@ -284,11 +294,8 @@
          <el-descriptions-item label="发货车牌号">{{
            detailRow.shippingCarNumber || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="快递公司">{{
            detailRow.expressCompany || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="快递单号" :span="2">{{
            detailRow.expressNumber || "--"
          <el-descriptions-item label="司机姓名">{{
            detailRow.driverName || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="出库单号" :span="2">{{
            detailRow.outboundBatches || "--"
@@ -321,7 +328,7 @@
          />
          <el-table-column
            label="发货数量"
            prop="deliveryQuantity"
            prop="totalQuantity"
            min-width="120"
            align="center"
          />
@@ -356,7 +363,6 @@
const tableData = ref([]);
const selectedRows = ref([]);
const tableLoading = ref(false);
const salesOrderOptions = ref([]);
const page = reactive({
  current: 1,
  size: 100,
@@ -373,46 +379,27 @@
const dialogFormVisible = ref(false);
const data = reactive({
  searchForm: {
    salesContractNo: "", // é”€å”®è®¢å•号
    shippingNo: "", // å‘货单号(两类来源都匹配)
    salesContractNo: "", // é”€å”®åˆåŒå·ï¼ˆåªåŒ¹é…å‘货单行)
    shippingCarNumber: "", // è½¦ç‰Œå·
    expressNumber: "", // å¿«é€’单号
  },
  form: {
    id: null,
    salesContractNo: "",
    customerName: "",
    specificationModel: "",
    productName: "",
    type: "货车", // è´§è½¦, å¿«é€’
    shippingDate: "",
    shippingCarNumber: "",
    expressCompany: "",
    expressNumber: "", // å¿«é€’单号
  },
  rules: {
    salesContractNo: [
      { required: true, message: "请选择销售订单", trigger: "change" },
    ],
    customerName: [
      { required: true, message: "请输入客户名称", trigger: "blur" },
    ],
    type: [{ required: true, message: "请选择发货类型", trigger: "change" }],
    shippingDate: [
      { required: true, message: "请选择发货时间", trigger: "change" },
    ],
    shippingCarNumber: [
      {
        validator: (_, value, callback) =>
          validateShippingCarNumber(value, callback),
        trigger: "blur",
      },
    ],
    expressCompany: [
      {
        validator: (_, value, callback) =>
          validateExpressCompany(value, callback),
        trigger: "blur",
      },
      { required: true, message: "请输入发货车牌号", trigger: "blur" },
    ],
  },
});
@@ -444,19 +431,14 @@
    });
};
// é”€å”®è®¢å•变化时自动填充客户名称
const handleSalesOrderChange = (value) => {
  const selectedOrder = salesOrderOptions.value.find(
    (item) => item.salesContractNo === value
  );
  if (selectedOrder) {
    form.value.customerName = selectedOrder.customerName;
  }
};
// æ²¹å“å‡ºåº“行来自 stock_out_record,id ä¸Ž shipping_info ä¼šæ’žå·ï¼Œ
// å‘è´§/删除/详情等接口背后都是 shipping_info,不能用于这类行
const isOilOutRow = (row) => row?.source === "oilOut";
const isRowSelectable = (row) => !isOilOutRow(row);
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
  selectedRows.value = selection.filter((item) => !isOilOutRow(item));
};
// æ‰“开弹框
@@ -472,13 +454,9 @@
  if (type === "edit" && row) {
    form.value = {
      id: row.id ?? null,
      salesContractNo: row.salesContractNo ?? "",
      customerName: row.customerName ?? "",
      type: row.type || "货车",
      shippingDate: row.shippingDate || getCurrentDate(),
      shippingCarNumber: row.shippingCarNumber ?? "",
      expressCompany: row.expressCompany ?? "",
      expressNumber: row.expressNumber ?? "",
    };
    deliveryFileList.value = row.storageBlobVOs || [];
  }
@@ -491,7 +469,8 @@
  detailRow.value = row || null;
  detailProductList.value = [];
  detailDialogVisible.value = true;
  if (!row?.id) return;
  // æ²¹å“å‡ºåº“行不走发货单详情接口(id ä¼šæ’žå·ï¼‰ï¼Œç›´æŽ¥ç”¨åˆ—表行数据展示
  if (!row?.id || isOilOutRow(row)) return;
  try {
    const res = await getDeliveryDetail(row.id);
    const detailData = res?.data;
@@ -554,7 +533,9 @@
    productName: item.productName ?? row.productName ?? "--",
    specificationModel:
      item.specificationModel ?? item.model ?? row.specificationModel ?? "--",
    deliveryQuantity:
    totalQuantity:
      item.totalQuantity ??
      row.totalQuantity ??
      item.deliveryQuantity ??
      item.quantity ??
      item.shippingQuantity ??
@@ -582,14 +563,8 @@
    if (valid) {
      const payload = {
        id: form.value.id,
        type: form.value.type,
        shippingDate: form.value.shippingDate,
        shippingCarNumber:
          form.value.type === "货车" ? form.value.shippingCarNumber : "",
        expressCompany:
          form.value.type === "快递" ? form.value.expressCompany : "",
        expressNumber:
          form.value.type === "快递" ? form.value.expressNumber : "",
        shippingCarNumber: form.value.shippingCarNumber,
        storageBlobDTOs: deliveryFileList.value || [],
      };
      deductStock(payload).then((res) => {
@@ -680,20 +655,6 @@
    });
};
// å‘货类型校验:货车时要求车牌,快递时要求快递公司
const validateShippingCarNumber = (value, callback) => {
  if (form.value.type === "货车") {
    if (!value) return callback(new Error("请输入发货车牌号"));
  }
  callback();
};
const validateExpressCompany = (value, callback) => {
  if (form.value.type === "快递") {
    if (!value) return callback(new Error("请输入快递公司"));
  }
  callback();
};
// å‘货图片上传前校检
function handleDeliveryBeforeUpload(file) {
  // æ ¡æ£€æ–‡ä»¶ç±»åž‹
@@ -764,16 +725,6 @@
    }
  }
}
// å‘货类型切换时清空对应字段
const handleShippingTypeChange = (val) => {
  if (val === "货车") {
    form.value.expressCompany = "";
    form.value.expressNumber = "";
  } else {
    form.value.shippingCarNumber = "";
  }
};
// èŽ·å–å®¡æ ¸çŠ¶æ€æ–‡æœ¬
const getApprovalStatusText = (status) => {
src/views/salesManagement/receiptPaymentLedger/index.vue
@@ -42,12 +42,7 @@
                             :formatter="formattedNumber"
                             width="200" />
            <el-table-column label="回款金额(元)"
                             prop="receiptPaymentAmount"
                             show-overflow-tooltip
                             :formatter="formattedNumber"
                             width="200" />
            <el-table-column label="退货金额(元)"
                             prop="returnAmount"
                             prop="receiptRecordAmount"
                             show-overflow-tooltip
                             :formatter="formattedNumber"
                             width="200" />
@@ -59,6 +54,16 @@
                <el-text type="danger">
                  {{ formattedNumber(row, column, row.receiptableAmount) }}
                </el-text>
              </template>
            </el-table-column>
            <el-table-column label="操作"
                             align="center"
                             width="90"
                             fixed="right">
              <template #default="{ row }">
                <el-button link
                           type="primary"
                           @click.stop="openRecordDialog(row)">流水</el-button>
              </template>
            </el-table-column>
          </el-table>
@@ -97,12 +102,7 @@
                             :formatter="formattedNumber"
                             width="200" />
            <el-table-column label="回款金额(元)"
                             prop="receiptPaymentAmount"
                             show-overflow-tooltip
                             :formatter="formattedNumber"
                             width="200" />
            <el-table-column label="退货金额(元)"
                             prop="returnAmount"
                             prop="receiptRecordAmount"
                             show-overflow-tooltip
                             :formatter="formattedNumber"
                             width="200" />
@@ -126,6 +126,14 @@
        </div>
      </el-col>
    </el-row>
    <!-- å®¢æˆ·ä»˜æ¬¾æµæ°´ç»´æŠ¤ -->
    <PaymentRecordDialog v-model:visible="recordDialogVisible"
                         title="客户付款流水"
                         :party-type="1"
                         :party-id="recordPartyId"
                         :party-name="recordPartyName"
                         :direction="1"
                         @changed="getList" />
  </div>
</template>
@@ -136,6 +144,7 @@
    customewTransactionsDetails,
  } from "@/api/salesManagement/indicatorStats.js";
  import Pagination from "../../../components/PIMTable/Pagination.vue";
  import PaymentRecordDialog from "@/components/PaymentRecordDialog/index.vue";
  const { proxy } = getCurrentInstance();
  const tableData = ref([]);
  const receiptRecord = ref([]);
@@ -159,6 +168,16 @@
  const customerId = ref("");
  const { searchForm } = toRefs(data);
  const originReceiptRecord = ref([]);
  // ä»˜æ¬¾æµæ°´å¼¹çª—
  const recordDialogVisible = ref(false);
  const recordPartyId = ref("");
  const recordPartyName = ref("");
  const openRecordDialog = row => {
    recordPartyId.value = row.customerId;
    recordPartyName.value = row.customerName || "";
    recordDialogVisible.value = true;
  };
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
  const handleQuery = () => {
@@ -190,12 +209,7 @@
  const summarizeMainTable = param => {
    return proxy.summarizeTable(
      param,
      [
        "contractAmounts",
        "receiptPaymentAmount",
        "returnAmount",
        "receiptableAmount",
      ],
      ["contractAmounts", "receiptPaymentAmount", "receiptableAmount"],
      {
        ticketsNum: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
        futureTickets: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
@@ -206,12 +220,7 @@
  const summarizeMainTable1 = param => {
    var summarizeTable = proxy.summarizeTable(
      param,
      [
        "contractAmount",
        "receiptPaymentAmount",
        "returnAmount",
        "receiptableAmount",
      ],
      ["contractAmount", "receiptPaymentAmount", "receiptableAmount"],
      {
        ticketsNum: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
        futureTickets: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
src/views/salesManagement/salesLedger/index.vue
@@ -40,11 +40,32 @@
            @change="changeDaterange"
          />
        </el-form-item>
        <el-form-item label="审批状态:">
          <el-select
            v-model="searchForm.approvalStatus"
            placeholder="请选择"
            clearable
            style="width: 160px"
            @change="handleQuery"
          >
            <el-option
              v-for="item in approvalStatusOptions"
              :key="item"
              :label="item"
              :value="item"
            />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleQuery"> æœç´¢ </el-button>
        </el-form-item>
      </el-form>
    </div>
    <!-- å–æ²¹ / ä»£å‚¨æ‹†æˆä¸¤ä¸ªé¡µç­¾ï¼Œå„自固定带 ledgerType è°ƒåŒä¸€ä¸ªåˆ—表接口 -->
    <el-tabs v-model="activeTab" @tab-change="handleTabChange" class="ledger-tabs">
      <el-tab-pane label="销售台账(卖油)" name="卖油" />
      <el-tab-pane label="代储台账" name="代储" />
    </el-tabs>
    <div class="table_list">
      <div class="actions">
        <div></div>
@@ -72,7 +93,7 @@
        style="width: 100%"
        :summary-method="summarizeMainTable"
        @expand-change="expandChange"
        height="calc(100vh - 18.5em)"
        height="calc(100vh - 21.5em)"
      >
        <el-table-column
          align="center"
@@ -221,9 +242,22 @@
          label="合同金额(元)"
          prop="contractAmount"
          width="220"
          show-overflow-tooltip
          :formatter="formattedNumber"
        />
        >
          <template #default="{ row, column }">
            <el-tooltip
              v-if="row.exceedsTrustFund"
              :content="`合同金额已超出该客户信托基金(信托基金:${row.trustFund})`"
              placement="top"
            >
              <el-text type="danger">
                {{ formattedNumber(row, column, row.contractAmount) }}
              </el-text>
            </el-tooltip>
            <span v-else>
              {{ formattedNumber(row, column, row.contractAmount) }}
            </span>
          </template>
        </el-table-column>
        <el-table-column
          label="实际合同金额(元)"
          prop="netContractAmount"
@@ -255,6 +289,33 @@
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          label="销售类型"
          prop="ledgerType"
          width="100"
          show-overflow-tooltip
        />
        <el-table-column
          v-if="activeTab === '代储'"
          label="油库"
          prop="oilDepotName"
          width="180"
          show-overflow-tooltip
        />
        <el-table-column
          v-if="activeTab === '代储'"
          label="储罐号"
          prop="tankNo"
          width="140"
          show-overflow-tooltip
        />
        <el-table-column label="审批状态" width="110" align="center">
          <template #default="{ row }">
            <el-tag :type="getApprovalStatusType(row.approvalStatus)" size="small">
              {{ row.approvalStatus || "-" }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column
          label="备注"
          prop="remarks"
@@ -314,8 +375,24 @@
        :rules="rules"
        ref="formRef"
      >
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="销售类型:" prop="ledgerType">
              <el-radio-group
                v-model="form.ledgerType"
                :disabled="operationType === 'view'"
              >
                <el-radio value="卖油">卖油</el-radio>
                <el-radio value="代储">卖存储位置(代储)</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
        </el-row>
        <!-- æŠ¥ä»·å•导入入口:放在表单顶部,选择后反显客户/业务员等 -->
        <el-row v-if="operationType === 'add'" style="margin-bottom: 10px">
        <el-row
          v-if="operationType === 'add' && form.ledgerType !== '代储'"
          style="margin-bottom: 10px"
        >
          <el-col :span="24" style="text-align: right">
            <el-button type="primary" plain @click="openQuotationDialog">
              ä»Žé”€å”®æŠ¥ä»·å¯¼å…¥
@@ -379,6 +456,7 @@
                clearable
                :disabled="operationType === 'view'"
                filterable
                @change="loadCustomerTrustFund"
              >
                <el-option
                  v-for="item in customerOption"
@@ -483,7 +561,72 @@
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
        <!-- ä»£å‚¨ï¼šå–存储位置,只选油库+储罐并手工填合同金额 -->
        <el-row v-if="form.ledgerType === '代储'" :gutter="30">
          <el-col :span="12">
            <el-form-item label="油库:" prop="oilDepotId">
              <el-select
                v-model="form.oilDepotId"
                placeholder="请选择"
                clearable
                filterable
                :disabled="operationType === 'view'"
                style="width: 100%"
              >
                <el-option
                  v-for="item in oilDepotOptions"
                  :key="item.id"
                  :label="item.depotName"
                  :value="item.id"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="储罐号:" prop="tankId">
              <el-select
                v-model="form.tankId"
                placeholder="请选择"
                clearable
                filterable
                :disabled="operationType === 'view'"
                style="width: 100%"
              >
                <el-option
                  v-for="item in tankOptions"
                  :key="item.id"
                  :label="item.tankNo"
                  :value="item.id"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row v-if="form.ledgerType === '代储'" :gutter="30">
          <el-col :span="12">
            <el-form-item label="合同金额(元):" prop="contractAmount">
              <el-input-number
                v-model="form.contractAmount"
                :min="0"
                :precision="2"
                :controls="false"
                placeholder="请输入"
                :disabled="operationType === 'view'"
                style="width: 100%"
              />
              <!-- é€‰ä¸­å®¢æˆ·åŽæœ¬åœ°æ¯”对信托基金,实时预览是否超限 -->
              <el-alert
                v-if="isContractOverTrustFund"
                type="warning"
                show-icon
                :closable="false"
                class="trust-fund-alert"
                :title="`合同金额已超出该客户信托基金(信托基金:${selectedCustomerTrustFund})`"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row v-if="form.ledgerType !== '代储'">
          <el-form-item label="产品信息:" prop="entryDate">
            <el-button
              v-if="operationType !== 'view'"
@@ -501,6 +644,7 @@
          </el-form-item>
        </el-row>
        <el-table
          v-if="form.ledgerType !== '代储'"
          :data="productData"
          border
          @selection-change="productSelected"
@@ -1172,7 +1316,7 @@
<script setup>
import { getToken } from "@/utils/auth";
import pagination from "@/components/PIMTable/Pagination.vue";
import { onMounted, ref, getCurrentInstance } from "vue";
import { computed, onMounted, ref, getCurrentInstance } from "vue";
import { addShippingInfo } from "@/api/salesManagement/deliveryLedger.js";
import { ElMessageBox, ElMessage } from "element-plus";
import useUserStore from "@/store/modules/user";
@@ -1198,7 +1342,9 @@
import FileUpload from "@/components/AttachmentUpload/file/index.vue";
import ImageUpload from "@/components/AttachmentUpload/image/index.vue";
import { getCurrentDate } from "@/utils/index.js";
import { listCustomer } from "@/api/basicData/customer.js";
import { getCustomer, listCustomer } from "@/api/basicData/customer.js";
import { allOilDepot } from "@/api/basicData/oilDepot.js";
import { allTankInfo } from "@/api/basicData/tankInfo.js";
const FileList = defineAsyncComponent(() =>
  import("@/components/Dialog/FileList.vue")
@@ -1209,6 +1355,14 @@
const userStore = useUserStore();
const { proxy } = getCurrentInstance();
const { tax_rate } = proxy.useDict("tax_rate");
// å–æ²¹ / ä»£å‚¨ä¸¤ä¸ªé¡µç­¾ï¼Œå›ºå®šå¸¦ ledgerType è°ƒåŒä¸€ä¸ªåˆ—表接口
const activeTab = ref("卖油");
const approvalStatusOptions = ["待审批", "审核中", "通过", "拒绝"];
// ä»£å‚¨è¡¨å•:油库、储罐下拉(两者互相独立,储罐不挂在油库下)
const oilDepotOptions = ref([]);
const tankOptions = ref([]);
// ä»£å‚¨è¡¨å•:选中客户的信托基金,用于合同金额超限实时预览
const selectedCustomerTrustFund = ref(null);
const tableData = ref([]);
const productData = ref([]);
const selectedRows = ref([]);
@@ -1236,6 +1390,7 @@
    entryDate: null, // å½•入日期
    entryDateStart: undefined,
    entryDateEnd: undefined,
    approvalStatus: "", // å®¡æ‰¹çŠ¶æ€
  },
  form: {
    salesContractNo: "",
@@ -1250,8 +1405,13 @@
    executionDate: "",
    hasProductionRecord: false,
    createTime: "",
    ledgerType: "卖油", // é”€å”®ç±»åž‹ï¼šå–æ²¹ / ä»£å‚¨
    oilDepotId: "", // ä»£å‚¨ï¼šæ²¹åº“
    tankId: "", // ä»£å‚¨ï¼šå‚¨ç½
    contractAmount: null, // ä»£å‚¨ï¼šåˆåŒé‡‘额由前端手工填
  },
  rules: {
    ledgerType: [{ required: true, message: "请选择", trigger: "change" }],
    salesman: [{ required: true, message: "请选择", trigger: "change" }],
    customerId: [{ required: true, message: "请选择", trigger: "change" }],
    entryPerson: [{ required: true, message: "请选择", trigger: "change" }],
@@ -1262,6 +1422,14 @@
});
const { form, rules } = toRefs(data);
const { form: searchForm } = useFormData(data.searchForm);
// ä»£å‚¨åˆåŒé‡‘额超信托基金(客户未维护信托基金时不提示)
const isContractOverTrustFund = computed(() => {
  const fund = selectedCustomerTrustFund.value;
  if (fund === null || fund === undefined || fund === "") return false;
  const amount = Number(form.value.contractAmount);
  if (!(amount > 0)) return false;
  return amount > Number(fund);
});
// äº§å“è¡¨å•弹框数据
const productFormVisible = ref(false);
const productOperationType = ref("");
@@ -1514,14 +1682,14 @@
const getList = () => {
  tableLoading.value = true;
  const { entryDate, ...rest } = searchForm;
  // å°†èŒƒå›´æ—¥æœŸå­—段传递给后端
  const params = { ...rest, ...page };
  // å°†èŒƒå›´æ—¥æœŸå­—段传递给后端;ledgerType ç”±å½“前页签决定
  const params = { ...rest, ...page, ledgerType: activeTab.value };
  // ç§»é™¤å½•入日期的默认值设置,只保留范围日期字段
  delete params.entryDate;
  return ledgerListPage(params)
    .then((res) => {
      tableLoading.value = false;
      tableData.value = res.records;
      tableData.value = res.records || [];
      tableData.value.map((item) => {
        item.children = [];
      });
@@ -1692,19 +1860,40 @@
// æ·»åŠ è¡¨è¡Œç±»åæ–¹æ³•
const tableRowClassName = ({ row }) => {
  if (!row.deliveryDate) return "";
  if (row.isFh) return "";
  let className = "";
  if (row.deliveryDate && !row.isFh) {
  const diff = row.deliveryDaysDiff;
  if (diff === 15) {
    return "yellow";
      className = "yellow";
  } else if (diff === 10) {
    return "pink";
      className = "pink";
  } else if (diff === 2) {
    return "purple";
      className = "purple";
  } else if (diff < 2) {
    return "red";
      className = "red";
  }
  }
  // æœ¬ç¬”合同金额超客户信托基金:左侧红边,不覆盖上面的交期底色
  if (row.exceedsTrustFund) {
    className = className ? `${className} trust-fund-exceed` : "trust-fund-exceed";
  }
  return className;
};
// å°è´¦å®¡æ‰¹çŠ¶æ€ï¼ˆåŽç«¯ç»´æŠ¤ï¼Œä¸Žå‘è´§çŠ¶æ€ statusTextMap æ— å…³ï¼‰
const getApprovalStatusType = status => {
  const map = {
    å¾…审批: "warning",
    å®¡æ ¸ä¸­: "primary",
    é€šè¿‡: "success",
    æ‹’绝: "danger",
  };
  return map[status] || "info";
};
// é¡µç­¾åˆ‡æ¢ï¼šæ¢ ledgerType é‡æ–°æŸ¥åˆ—表
const handleTabChange = () => {
  page.current = 1;
  expandedRowKeys.value = [];
  getList();
};
// ä¸»è¡¨åˆè®¡æ–¹æ³•
const summarizeMainTable = (param) => {
@@ -1750,14 +1939,24 @@
  form.value = {};
  productData.value = [];
  selectedQuotation.value = null;
  selectedCustomerTrustFund.value = null;
  let userLists = await userListNoPage();
  userList.value = userLists.data;
  listCustomer({ current: -1, size: -1, type: 0 }).then((res) => {
    customerOption.value = res.data.records;
  });
  // æ²¹åº“/储罐下拉(代储用),两者互相独立
  allOilDepot().then((res) => {
    oilDepotOptions.value = res.data || [];
  });
  allTankInfo().then((res) => {
    tankOptions.value = res.data || [];
  });
  form.value.entryPerson = userStore.id;
  if (type === "add") {
    // æ–°å¢žæ—¶è®¾ç½®å½•入日期为当天
    // æ–°å¢žæ—¶é»˜è®¤è·Ÿéšå½“前页签的销售类型
    form.value.ledgerType = activeTab.value;
    // å½•入日期默认为当天
    form.value.entryDate = getCurrentDate();
    // ç­¾è®¢æ—¥æœŸé»˜è®¤ä¸ºå½“天
    form.value.executionDate = getCurrentDate();
@@ -1770,10 +1969,15 @@
    getSalesLedgerWithProducts({ id: row.id, type: 1 }).then((res) => {
      form.value = { ...res };
      form.value.entryPerson = Number(res.entryPerson);
      productData.value = form.value.productData;
      // åŽ†å²å•æ®/后端未返回时兜底为卖油,否则必填校验会卡住编辑
      form.value.ledgerType = form.value.ledgerType || "卖油";
      // ä»£å‚¨æ²¡æœ‰äº§å“æ˜Žç»†
      productData.value = form.value.productData || [];
      fileList.value = form.value.storageBlobVOs;
      // ç¼–辑时设置自动生成为false,允许手动修改
      form.value.autoGenerateContractNo = false;
      // å›žæ˜¾ä¿¡æ‰˜åŸºé‡‘,供代储合同金额实时比对
      loadCustomerTrustFund();
    });
  }
  // let userAll = await userStore.getInfo()
@@ -1784,6 +1988,18 @@
  // });
  form.value.entryDate = getCurrentDate(); // è®¾ç½®é»˜è®¤å½•入日期为当前日期
  dialogFormVisible.value = true;
};
// å–该客户的信托基金,供代储合同金额实时比对(客户未维护即为空,不提示)
const loadCustomerTrustFund = () => {
  const id = form.value.customerId;
  if (!id) {
    selectedCustomerTrustFund.value = null;
    return;
  }
  getCustomer(id).then((res) => {
    selectedCustomerTrustFund.value = res?.data?.trustFund ?? null;
  });
};
// æ‰“开报价单选择弹窗(仅审批通过)
@@ -1901,15 +2117,36 @@
const submitForm = () => {
  proxy.$refs["formRef"].validate((valid) => {
    if (valid) {
      console.log("productData.value--", productData.value);
      form.value.type = 1;
      form.value.ledgerType = form.value.ledgerType || "卖油";
      if (form.value.ledgerType === "代储") {
        // ä»£å‚¨ï¼šå–存储位置,没有产品明细,合同金额手工填
        if (!form.value.oilDepotId) {
          proxy.$modal.msgWarning("代储必须选择油库");
          return;
        }
        if (!form.value.tankId) {
          proxy.$modal.msgWarning("代储必须选择储罐");
          return;
        }
        if (!(Number(form.value.contractAmount) > 0)) {
          proxy.$modal.msgWarning("代储必须填写合同金额");
          return;
        }
        delete form.value.productData;
      } else {
        // å–油:合同金额由产品明细汇总,代储字段清空
      if (productData.value !== null && productData.value.length > 0) {
        form.value.productData = proxy.HaveJson(productData.value);
      } else {
        proxy.$modal.msgWarning("请添加产品信息");
        return;
      }
        form.value.oilDepotId = "";
        form.value.tankId = "";
        form.value.contractAmount = null;
      }
      form.value.storageBlobDTOs = fileList;
      form.value.type = 1;
      if (form.value.autoGenerateContractNo) {
        form.value.salesContractNo = "";
      }
@@ -3024,6 +3261,28 @@
  background-color: #f4defa;
}
/* æœ¬ç¬”合同金额超客户信托基金:只加左侧红边,避免覆盖上面的交期底色 */
:deep(.trust-fund-exceed) td:first-child {
  box-shadow: inset 3px 0 0 var(--el-color-danger);
}
.trust-fund-alert {
  margin-top: 6px;
  width: 100%;
}
.ledger-tabs {
  margin-bottom: 8px;
}
.ledger-tabs :deep(.el-tabs__header) {
  margin-bottom: 0;
}
.ledger-tabs :deep(.el-tabs__content) {
  display: none;
}
.table_list {
  margin-top: unset;
}