gaoluyang
2026-08-04 760433983d2ab722774bc3384a789f3ddf2c4dd8
src/views/salesManagement/salesQuotation/index.vue
@@ -16,8 +16,8 @@
          </el-input>
        </el-col>
        <el-col :span="8">
          <el-select v-model="searchForm.customer" placeholder="请选择客户" clearable>
                  <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName" :value="item.customerName">
          <el-select v-model="searchForm.customerId" placeholder="请选择客户" clearable>
                  <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName" :value="item.id">
                     {{
                        item.customerName + "——" + item.taxpayerIdentificationNumber
                     }}
@@ -52,6 +52,7 @@
      >
            <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column prop="quotationNo" label="报价单号" />
        <el-table-column prop="masterContractNo" label="总合同号" width="150" show-overflow-tooltip />
        <el-table-column prop="customer" label="客户名称" />
        <el-table-column prop="salesperson" label="业务员" width="100" />
        <el-table-column prop="quotationDate" label="报价日期" width="120" />
@@ -70,9 +71,9 @@
        </el-table-column>
        <el-table-column label="操作" width="200" fixed="right" align="center">
          <template #default="scope">
            <el-button link type="primary" @click="handleEdit(scope.row)" :disabled="!['待审批','拒绝'].includes(scope.row.status)">编辑</el-button>
            <el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button>
            <el-button link type="primary" @click="handleView(scope.row)" style="color: #67C23A">查看</el-button>
            <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
            <el-button link type="danger" @click="handleDelete(scope.row)" v-hasPermi="['salesManagement:salesQuotation:remove']">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
@@ -102,12 +103,21 @@
          <div class="form-content">
            <el-row :gutter="24">
              <el-col :span="12">
                <el-form-item label="客户名称" prop="customer">
                  <el-select v-model="form.customer" placeholder="请选择客户" style="width: 100%" clearable filterable>
                <el-form-item label="客户名称" prop="customerId">
                  <el-select v-model="form.customerId" placeholder="请选择客户" style="width: 100%" clearable filterable @change="handleCustomerChange">
                    <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName" :value="item.id"></el-option>
                  </el-select>
                </el-form-item>
              </el-col>
              <el-col :span="12">
                <el-form-item label="总合同号">
                  <el-select v-model="form.masterContractNo" placeholder="请先选择客户" style="width: 100%" clearable filterable :disabled="!form.customerId">
                    <el-option v-for="item in contractList" :key="item.id" :label="item.masterContractNo" :value="item.masterContractNo"></el-option>
                  </el-select>
                </el-form-item>
              </el-col>
            </el-row>
            <el-row :gutter="24">
              <el-col :span="12">
                <el-form-item label="业务员" prop="salesperson">
                  <el-select v-model="form.salesperson" placeholder="请选择业务员" style="width: 100%" clearable filterable>
@@ -148,7 +158,13 @@
            <el-row :gutter="24">
              <el-col :span="12">
                <el-form-item label="付款方式" prop="paymentMethod">
                  <el-input v-model="form.paymentMethod" placeholder="请输入付款方式" clearable />
                  <el-select v-model="form.paymentMethod" placeholder="请选择付款方式" clearable style="width: 100%">
                    <el-option label="现金" value="现金" />
                    <el-option label="电汇" value="电汇" />
                    <el-option label="微信" value="微信" />
                    <el-option label="支付宝" value="支付宝" />
                    <el-option label="支票" value="支票" />
                  </el-select>
                </el-form-item>
              </el-col>
            </el-row>
@@ -187,9 +203,9 @@
            </el-table-column>
            <el-table-column prop="specification" label="规格型号" width="200">
              <template #default="scope">
                <el-form-item :prop="`products.${scope.$index}.specificationId`" class="product-table-form-item">
                <el-form-item :prop="`products.${scope.$index}.productModelId`" class="product-table-form-item">
                  <el-select
                    v-model="scope.row.specificationId"
                    v-model="scope.row.productModelId"
                    placeholder="请选择"
                    clearable
                    @change="getProductModel($event, scope.row)"
@@ -239,10 +255,10 @@
          </template>
          <div class="form-content">
            <el-form-item label="备注" prop="remark">
              <el-input
                type="textarea"
                v-model="form.remark"
                placeholder="请输入备注信息(选填)"
              <el-input
                type="textarea"
                v-model="form.remark"
                placeholder="请输入备注信息(选填)"
                :rows="4"
                maxlength="500"
                show-word-limit
@@ -258,6 +274,7 @@
    <el-dialog v-model="viewDialogVisible" title="报价详情" width="800px">
      <el-descriptions :column="2" border>
        <el-descriptions-item label="报价单号">{{ currentQuotation.quotationNo }}</el-descriptions-item>
        <el-descriptions-item label="总合同号">{{ currentQuotation.masterContractNo || '--' }}</el-descriptions-item>
        <el-descriptions-item label="客户名称">{{ currentQuotation.customer }}</el-descriptions-item>
        <el-descriptions-item label="业务员">{{ currentQuotation.salesperson }}</el-descriptions-item>
        <el-descriptions-item label="报价日期">{{ currentQuotation.quotationDate }}</el-descriptions-item>
@@ -270,7 +287,7 @@
          <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">¥{{ currentQuotation.totalAmount?.toFixed(2) }}</span>
        </el-descriptions-item>
      </el-descriptions>
      <div style="margin: 20px 0;">
        <h4>产品明细</h4>
        <el-table :data="currentQuotation.products" border style="width: 100%">
@@ -301,14 +318,15 @@
import FormDialog from '@/components/Dialog/FormDialog.vue'
import {getQuotationList,addQuotation,updateQuotation,deleteQuotation} from '@/api/salesManagement/salesQuotation.js'
import {modelList, productTreeList} from "@/api/basicData/product.js";
import {listCustomerPrivatePool} from "@/api/basicData/customerFile.js";
import {listCustomer} from "@/api/basicData/customer.js";
import { userListNoPage } from "@/api/system/user.js";
import { getCustomerContractList } from "@/api/basicData/customerContract.js";
// 响应式数据
const loading = ref(false)
const searchForm = reactive({
  quotationNo: '',
  customer: '',
  customerId: '',
  status: ''
})
@@ -316,6 +334,7 @@
const userList = ref([])
const productOptions = ref([]);
const modelOptions  = ref([]);
const contractList = ref([]);
const pagination = reactive({
  total: 3,
  currentPage: 1,
@@ -327,6 +346,7 @@
const dialogTitle = ref('新增报价')
const form = reactive({
  quotationNo: '',
  customerId: undefined,
  customer: '',
  salesperson: '',
  quotationDate: '',
@@ -334,12 +354,10 @@
  paymentMethod: '',
  status: '草稿',
  remark: '',
  masterContractNo: '',
  products: [],
  subtotal: 0,
  freight: 0,
  otherFee: 0,
  discountRate: 0,
  discountAmount: 0,
  totalAmount: 0
})
@@ -348,12 +366,12 @@
  salesperson: [{ required: true, message: '请选择业务员', trigger: 'change' }],
  quotationDate: [{ required: true, message: '请选择报价日期', trigger: 'change' }],
  validDate: [{ required: true, message: '请选择有效期', trigger: 'change' }],
  paymentMethod: [{ required: true, message: '请输入付款方式', trigger: 'blur' }]
  paymentMethod: [{ required: true, message: '请选择付款方式', trigger: 'change' }]
}
const productRowRules = {
  productId: [{ required: true, message: '请选择产品名称', trigger: 'change' }],
  specificationId: [{ required: true, message: '请选择规格型号', trigger: 'change' }],
  productModelId: [{ required: true, message: '请选择规格型号', trigger: 'change' }],
  unit: [{ required: true, message: '请填写单位', trigger: 'blur' }],
  unitPrice: [{ required: true, message: '请填写单价', trigger: 'change' }]
}
@@ -361,7 +379,7 @@
  const r = { ...baseRules }
  ;(form.products || []).forEach((_, i) => {
    r[`products.${i}.productId`] = productRowRules.productId
    r[`products.${i}.specificationId`] = productRowRules.specificationId
    r[`products.${i}.productModelId`] = productRowRules.productModelId
    r[`products.${i}.unit`] = productRowRules.unit
    r[`products.${i}.unitPrice`] = productRowRules.unitPrice
  })
@@ -377,6 +395,7 @@
// 计算属性
const filteredList = computed(() => {
  let list = quotationList.value
  console.log(list)
  return list
})
@@ -400,19 +419,38 @@
  handleSearch()
}
const loadCustomerOptions = async () => {
  const res = await listCustomerPrivatePool({ current: -1, size: -1 });
  customerOption.value = res?.data?.records || [];
}
const handleAdd = async () => {
  dialogTitle.value = '新增报价'
  isEdit.value = false
  resetForm()
  dialogVisible.value = true
  getProductOptions();
  loadCustomerOptions();
  fetchCustomerOptions()
}
const fetchCustomerOptions = () => {
  if (customerOption.value.length > 0) return
  listCustomer({current: -1,size:-1, type: 0}).then((res) => {
    customerOption.value = res.data.records;
  });
}
// 客户选择变化时,加载该客户的合同列表
const handleCustomerChange = async (customerId) => {
  form.masterContractNo = '';
  contractList.value = [];
  if (!customerId) return;
  try {
    const res = await getCustomerContractList(customerId);
    if (res.code === 200) {
      contractList.value = res.data || [];
    }
  } catch (error) {
    console.error('获取客户合同列表失败:', error);
  }
};
const getProductOptions = () => {
   // 返回 Promise,便于编辑时 await 确保能反显
   return productTreeList().then((res) => {
@@ -430,7 +468,7 @@
      if (children && children.length > 0) {
         newItem.children = convertIdToValue(children);
      }
      return newItem;
   });
}
@@ -454,7 +492,7 @@
      row.productId = '';
      row.product = '';
      row.modelOptions = [];
      row.specificationId = '';
      row.productModelId = '';
      row.specification = '';
      row.unit = '';
      return;
@@ -475,13 +513,13 @@
   if (!row) return;
   // 如果清空选择,则清空相关字段
   if (!value) {
      row.specificationId = '';
      row.productModelId = '';
      row.specification = '';
      row.unit = '';
      return;
   }
   // 更新 specificationId(v-model 已经自动更新,这里确保一致性)
   row.specificationId = value;
   // 更新 productModelId(v-model 已经自动更新,这里确保一致性)
   row.productModelId = value;
   const modelOptions = row.modelOptions || [];
   const index = modelOptions.findIndex((item) => item.id === value);
   if (index !== -1) {
@@ -515,12 +553,13 @@
    quotationDate: row.quotationDate || '',
    validDate: row.validDate || '',
    paymentMethod: row.paymentMethod || '',
    masterContractNo: row.masterContractNo || '',
    status: row.status || '',
    remark: row.remark || '',
    products: row.products ? row.products.map(product => ({
      productId: product.productId || '',
      product: product.product || product.productName || '',
      specificationId: product.specificationId || '',
      productModelId: product.productModelId || '',
      specification: product.specification || '',
      quantity: product.quantity || 0,
      unit: product.unit || '',
@@ -539,48 +578,66 @@
  form.id = row.id || form.id || null
  // 先加载产品树数据,否则 el-tree-select 无法反显产品名称
  await getProductOptions()
  await fetchCustomerOptions()
  // 只复制需要的字段,避免将组件引用放入响应式对象
  form.quotationNo = row.quotationNo || ''
  form.customer = row.customer || ''
  form.customerId = row.customerId || undefined
  form.salesperson = row.salesperson || ''
  form.quotationDate = row.quotationDate || ''
  form.validDate = row.validDate || ''
  form.paymentMethod = row.paymentMethod || ''
  form.status = row.status || '草稿'
  form.remark = row.remark || ''
  // 编辑时加载客户的合同列表以便回显总合同号
  if (row.customerId) {
    try {
      const contractRes = await getCustomerContractList(row.customerId);
      if (contractRes.code === 200) {
        contractList.value = contractRes.data || [];
      }
    } catch (error) {
      console.error('获取客户合同列表失败:', error);
    }
  }
  // 兼容后端返回的 masterContractNo 或 master_contract_no,放在合同列表加载后设置
  form.masterContractNo = row.masterContractNo || row.master_contract_no || ''
  form.products = row.products ? await Promise.all(row.products.map(async (product) => {
    const productName = product.product || product.productName || ''
    // 优先用 productId;如果只有名称,尝试反查 id 以便树选择器反显
    const resolvedProductId = product.productId
      ? Number(product.productId)
      : findNodeIdByLabel(productOptions.value, productName) || ''
    // 如果有产品ID,加载对应的规格型号列表
    let modelOptions = [];
    let resolvedSpecificationId = product.specificationId || '';
    let resolvedProductModelId = product.productModelId || '';
    if (resolvedProductId) {
      try {
        const res = await modelList({ id: resolvedProductId });
        modelOptions = res || [];
        // 如果返回的数据没有 specificationId,但有 specification 名称,根据名称查找 ID
        if (!resolvedSpecificationId && product.specification) {
        // 如果返回的数据没有 productModelId,但有 specification 名称,根据名称查找 ID
        if (!resolvedProductModelId && product.specification) {
          const foundModel = modelOptions.find(item => item.model === product.specification);
          if (foundModel) {
            resolvedSpecificationId = foundModel.id;
            resolvedProductModelId = foundModel.id;
          }
        }
      } catch (error) {
        console.error('加载规格型号失败:', error);
      }
    }
    return {
      productId: resolvedProductId,
      product: productName,
      specificationId: resolvedSpecificationId,
      productModelId: resolvedProductModelId,
      specification: product.specification || '',
      quantity: product.quantity || 0,
      unit: product.unit || '',
@@ -590,10 +647,6 @@
    }
  })) : []
  form.subtotal = row.subtotal || 0
  form.freight = row.freight || 0
  form.otherFee = row.otherFee || 0
  form.discountRate = row.discountRate || 0
  form.discountAmount = row.discountAmount || 0
  form.totalAmount = row.totalAmount || 0
  dialogVisible.value = true
@@ -630,13 +683,13 @@
  form.paymentMethod = ''
  form.status = '草稿'
  form.remark = ''
  form.masterContractNo = ''
  form.products = []
  form.subtotal = 0
  form.freight = 0
  form.otherFee = 0
  form.discountRate = 0
  form.discountAmount = 0
  form.totalAmount = 0
  contractList.value = []
}
const addProduct = () => {
@@ -644,8 +697,7 @@
    productId: '',
    product: '',
    productName: '',
    specificationId: '',
    specification: '',
    productModelId: '',
    quantity: 1,
    unit: '',
    unitPrice: 0,
@@ -688,11 +740,20 @@
        return sum + price
      }, 0)
      form.customer = customerOption.value.find(item => item.id === form.customerId)?.customerName || ''
      // 剔除 products 中的 modelOptions 字段,以及 form 中的 subtotal、freight 字段
      const { subtotal, freight, ...formWithoutSubtotal } = form
      const submitData = {
        ...formWithoutSubtotal,
        products: form.products.map(({ modelOptions, ...rest }) => rest)
      }
      if (isEdit.value) {
        // 编辑
        const index = quotationList.value.findIndex(item => item.id === editId.value)
        if (index > -1) {
          updateQuotation(form).then(res=>{
          updateQuotation(submitData).then(res=>{
            // console.log(res)
            if(res.code===200){
              ElMessage.success('编辑成功')
@@ -703,7 +764,7 @@
        }
      } else {
        // 新增
        addQuotation(form).then(res=>{
        addQuotation(submitData).then(res=>{
          if(res.code===200){
            ElMessage.success('新增成功')
            dialogVisible.value = false
@@ -737,7 +798,9 @@
        id: item.id,
        quotationNo: item.quotationNo || '',
        customer: item.customer || '',
        customerId: item.customerId || undefined,
        salesperson: item.salesperson || '',
        masterContractNo: item.masterContractNo || '',
        quotationDate: item.quotationDate || '',
        validDate: item.validDate || '',
        paymentMethod: item.paymentMethod || '',
@@ -748,7 +811,7 @@
        products: item.products ? item.products.map(product => ({
          productId: product.productId || '',
          product: product.product || product.productName || '',
          specificationId: product.specificationId || '',
          productModelId: product.productModelId || '',
          specification: product.specification || '',
          quantity: product.quantity || 0,
          unit: product.unit || '',
@@ -757,9 +820,6 @@
        })) : [],
        subtotal: item.subtotal || 0,
        freight: item.freight || 0,
        otherFee: item.otherFee || 0,
        discountRate: item.discountRate || 0,
        discountAmount: item.discountAmount || 0,
        totalAmount: item.totalAmount || 0
      }))
      pagination.total = res.data.total
@@ -782,8 +842,8 @@
onMounted(()=>{
  getUserList()
  loadCustomerOptions()
  handleSearch()
  fetchCustomerOptions()
})
</script>
@@ -796,16 +856,16 @@
  padding: 10px 0;
  max-height: calc(100vh - 200px);
  overflow-y: auto;
  &::-webkit-scrollbar {
    width: 6px;
    height: 6px;
  }
  &::-webkit-scrollbar-thumb {
    background: #c1c1c1;
    border-radius: 3px;
    &:hover {
      background: #a8a8a8;
    }
@@ -822,17 +882,17 @@
  margin-bottom: 24px;
  border-radius: 8px;
  transition: all 0.3s ease;
  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
  }
  :deep(.el-card__header) {
    padding: 16px 20px;
    background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
    border-bottom: 1px solid #ebeef5;
  }
  :deep(.el-card__body) {
    padding: 20px;
  }
@@ -842,19 +902,19 @@
  display: flex;
  align-items: center;
  gap: 8px;
  .card-icon {
    font-size: 18px;
    color: #409eff;
  }
  .card-title {
    font-weight: 600;
    font-size: 16px;
    color: #303133;
    flex: 1;
  }
  .header-btn {
    margin-left: auto;
  }
@@ -878,20 +938,20 @@
.product-table {
  :deep(.el-table__header) {
    background-color: #f5f7fa;
    th {
      background-color: #f5f7fa !important;
      color: #606266;
      font-weight: 600;
    }
  }
  :deep(.el-table__row) {
    &:hover {
      background-color: #f5f7fa;
    }
  }
  :deep(.el-table__cell) {
    padding: 12px 0;
  }