gaoluyang
2026-04-03 3145a4847f8dbd378c932e9bacb0376fd3fe1e54
src/views/salesManagement/salesQuotation/index.vue
@@ -229,45 +229,28 @@
          </template>
          <div class="form-content">
            <el-table :data="form.products" border style="width: 100%" class="product-table" v-if="form.products.length > 0">
            <el-table-column prop="product" label="产品名称" width="200">
            <el-table-column prop="product" label="产品名称" width="220">
              <template #default="scope">
                        <el-tree-select
                           v-model="scope.row.productId"
                           placeholder="请选择"
                           clearable
                           check-strictly
                           @change="getModels($event, scope.row)"
                           :data="productOptions"
                           :render-after-expand="false"
                           style="width: 100%"
                        />
                <span>{{ scope.row.product || scope.row.productName || '--' }}</span>
              </template>
            </el-table-column>
            <el-table-column prop="specification" label="规格型号" width="150">
            <el-table-column prop="specification" label="图纸编号" width="220">
              <template #default="scope">
                        <el-select
                           v-model="scope.row.specificationId"
                           placeholder="请选择"
                           clearable
                           @change="getProductModel($event, scope.row)"
                        >
                           <el-option
                              v-for="item in scope.row.modelOptions || []"
                              :key="item.id"
                              :label="item.model"
                              :value="item.id"
                           />
                        </el-select>
                <span>{{ scope.row.specification || '--' }}</span>
              </template>
            </el-table-column>
            <el-table-column prop="unit" label="单位">
              <template #default="scope">
                <el-input v-model="scope.row.unit" placeholder="单位" />
                <el-form-item :prop="`products.${scope.$index}.unit`" class="product-table-form-item">
                  <el-input v-model="scope.row.unit" placeholder="单位" clearable/>
                </el-form-item>
              </template>
            </el-table-column>
            <el-table-column prop="unitPrice" label="单价">
              <template #default="scope">
                <el-input-number v-model="scope.row.unitPrice" :min="0" :precision="2" style="width: 100%" />
                <el-form-item :prop="`products.${scope.$index}.unitPrice`" class="product-table-form-item">
                  <el-input-number v-model="scope.row.unitPrice" :min="0.01" :precision="2" :step="0.01" style="width: 100%" />
                </el-form-item>
              </template>
            </el-table-column>
            <el-table-column label="操作" width="80" align="center">
@@ -305,6 +288,12 @@
      </div>
    </FormDialog>
    <ProductSelectDialog
      v-model="productSelectVisible"
      :single="true"
      @confirm="handleProductSelectConfirm"
    />
    <!-- 查看详情对话框 -->
    <el-dialog v-model="viewDialogVisible" title="报价详情" width="800px">
      <el-descriptions :column="2" border>
@@ -326,7 +315,7 @@
        <h4>产品明细</h4>
        <el-table :data="currentQuotation.products" border style="width: 100%">
          <el-table-column prop="product" label="产品名称" />
          <el-table-column prop="specification" label="规格型号" />
          <el-table-column prop="specification" label="图纸编号" />
          <el-table-column prop="unit" label="单位" />
          <el-table-column prop="unitPrice" label="单价">
            <template #default="scope">
@@ -350,6 +339,7 @@
import { Search, Document, UserFilled, Box, EditPen, Plus, ArrowRight, Delete } from '@element-plus/icons-vue'
import Pagination from '@/components/PIMTable/Pagination.vue'
import FormDialog from '@/components/Dialog/FormDialog.vue'
import ProductSelectDialog from '@/views/basicData/product/ProductSelectDialog.vue'
import {getQuotationList,addQuotation,updateQuotation,deleteQuotation} from '@/api/salesManagement/salesQuotation.js'
import {userListNoPage} from "@/api/system/user.js";
import {customerList} from "@/api/salesManagement/salesLedger.js";
@@ -365,7 +355,6 @@
const quotationList = ref([])
const productOptions = ref([]);
const modelOptions = ref([]);
const pagination = reactive({
  total: 3,
  currentPage: 1,
@@ -374,6 +363,8 @@
const dialogVisible = ref(false)
const viewDialogVisible = ref(false)
const productSelectVisible = ref(false)
const activeProductIndex = ref(-1)
const dialogTitle = ref('新增报价')
const form = reactive({
  quotationNo: '',
@@ -393,13 +384,30 @@
  totalAmount: 0
})
const rules = {
const baseRules = {
  customer: [{ required: true, message: '请选择客户', trigger: 'change' }],
  salesperson: [{ required: true, message: '请选择业务员', trigger: 'change' }],
  quotationDate: [{ required: true, message: '请选择报价日期', trigger: 'change' }],
  validDate: [{ required: true, message: '请选择有效期', trigger: 'change' }],
  paymentMethod: [{ required: true, message: '请输入付款方式', trigger: 'blur' }]
}
const productRowRules = {
  productId: [{ required: true, message: '请选择产品名称', trigger: 'change' }],
  specificationId: [{ required: true, message: '请选择规格型号', trigger: 'change' }],
  unit: [{ required: true, message: '请填写单位', trigger: 'blur' }],
  unitPrice: [{ required: true, message: '请填写单价', trigger: 'change' }]
}
const rules = computed(() => {
  const r = { ...baseRules }
  ;(form.products || []).forEach((_, i) => {
    r[`products.${i}.productId`] = productRowRules.productId
    r[`products.${i}.specificationId`] = productRowRules.specificationId
    r[`products.${i}.unit`] = productRowRules.unit
    r[`products.${i}.unitPrice`] = productRowRules.unitPrice
  })
  return r
})
const userList = ref([]);
const customerOption = ref([]);
@@ -515,65 +523,6 @@
   }
   return null;
}
const getModels = (value, row) => {
   if (!row) return;
   // 如果清空选择,则清空相关字段
   if (!value) {
      row.productId = '';
      row.product = '';
      row.modelOptions = [];
      row.specificationId = '';
      row.specification = '';
      row.unit = '';
      return;
   }
   // 更新 productId(v-model 已经自动更新,这里确保一致性)
   row.productId = value;
   // 找到对应的 label 并赋值给 row.product
   const label = findNodeById(productOptions.value, value);
   if (label) {
      row.product = label;
   }
   // 获取规格型号列表,设置到当前行的 modelOptions
   modelList({ id: value }).then((res) => {
      row.modelOptions = res || [];
   });
};
const getProductModel = (value, row) => {
   if (!row) return;
   // 如果清空选择,则清空相关字段
   if (!value) {
      row.specificationId = '';
      row.specification = '';
      row.unit = '';
      return;
   }
   // 更新 specificationId(v-model 已经自动更新,这里确保一致性)
   row.specificationId = value;
   const modelOptions = row.modelOptions || [];
   const index = modelOptions.findIndex((item) => item.id === value);
   if (index !== -1) {
      row.specification = modelOptions[index].model;
      row.unit = modelOptions[index].unit;
   } else {
      row.specification = '';
      row.unit = '';
   }
};
const findNodeById = (nodes, productId) => {
   for (let i = 0; i < nodes.length; i++) {
      if (nodes[i].value === productId) {
         return nodes[i].label; // 找到节点,返回 label
      }
      if (nodes[i].children && nodes[i].children.length > 0) {
         const foundLabel = findNodeById(nodes[i].children, productId);
         if (foundLabel) {
            return foundLabel; // 在子节点中找到,返回 label
         }
      }
   }
   return null; // 没有找到节点,返回null
};
const handleView = (row) => {
  // 只复制需要的字段,避免将组件引用放入响应式对象
  currentQuotation.value = {
@@ -729,23 +678,51 @@
}
const addProduct = () => {
  form.products.push({
    productId: '',
    product: '',
    productName: '',
    specificationId: '',
    specification: '',
    quantity: 1,
    unit: '',
    unitPrice: 0,
    amount: 0,
    modelOptions: [] // 为每行添加独立的规格型号列表
  })
  activeProductIndex.value = -1
  productSelectVisible.value = true
}
const removeProduct = (index) => {
  form.products.splice(index, 1)
  calculateSubtotal()
}
const handleProductSelectConfirm = (rows) => {
  if (!rows || rows.length === 0 || activeProductIndex.value < 0) {
    if (!rows || rows.length === 0) {
      return
    }
    const selected = rows[0]
    form.products.push({
      productId: selected.id,
      product: selected.productName || '',
      productName: selected.productName || '',
      specificationId: selected.id,
      specification: selected.model || '',
      quantity: 1,
      unit: selected.unit || '',
    unitPrice: 0.01,
      amount: 0,
      modelOptions: [],
    })
    calculateSubtotal()
    activeProductIndex.value = -1
    return
  }
  const row = form.products[activeProductIndex.value]
  if (!row) {
    return
  }
  const selected = rows[0]
  row.productId = selected.id
  row.product = selected.productName || ''
  row.productName = selected.productName || ''
  row.specificationId = selected.id
  row.specification = selected.model || ''
  row.unit = selected.unit || row.unit || ''
  calculateAmount(row)
}
const calculateAmount = (product) => {
@@ -774,16 +751,18 @@
        ElMessage.warning('请至少添加一个产品')
        return
      }
      // 审批人必填校验
      const hasEmptyApprover = approverNodes.value.some(node => !node.userId)
      if (hasEmptyApprover) {
        ElMessage.error('请为所有审批节点选择审批人!')
      const hasInvalidPrice = form.products.some(product => Number(product.unitPrice) <= 0)
      if (hasInvalidPrice) {
        ElMessage.error('单价必须大于0')
        return
      }
      // 收集所有节点的审批人id
      form.approveUserIds = approverNodes.value.map(node => node.userId).join(',')
      // 收集所有节点的审批人id,允许为空
      form.approveUserIds = approverNodes.value
        .map(node => node.userId)
        .filter(userId => userId !== null && userId !== undefined && userId !== '')
        .join(',')
      
      // 计算所有产品的单价总和
      form.totalAmount = form.products.reduce((sum, product) => {
@@ -956,6 +935,17 @@
  padding: 8px 0;
}
.product-table-form-item {
  margin-bottom: 0;
  :deep(.el-form-item__content) {
    margin-left: 0 !important;
  }
  :deep(.el-form-item__label) {
    width: auto;
    min-width: auto;
  }
}
.approver-nodes-container {
  display: flex;
  flex-wrap: wrap;