gaoluyang
2026-05-21 a2c33664f683aee11fdc62391c8d1e21cc74f5b2
src/views/salesManagement/salesQuotation/index.vue
@@ -5,35 +5,18 @@
      <el-row :gutter="20" class="search-row">
        <el-col :span="8">
          <el-input
            v-model="searchForm.quotationNo"
            placeholder="请输入报价单号"
            v-model="searchForm.product"
            placeholder="请输入产品名称"
            clearable
            @keyup.enter="handleSearch"
            @keyup.enter="handleSearch(true)"
          >
            <template #prefix>
              <el-icon><Search /></el-icon>
            </template>
          </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">
                     {{
                        item.customerName + "——" + item.taxpayerIdentificationNumber
                     }}
                  </el-option>
          </el-select>
        </el-col>
<!--        <el-col :span="6">-->
<!--          <el-select v-model="searchForm.status" placeholder="请选择报价状态" clearable>-->
<!--            <el-option label="草稿" value="草稿"></el-option>-->
<!--            <el-option label="已发送" value="已发送"></el-option>-->
<!--            <el-option label="客户确认" value="客户确认"></el-option>-->
<!--            <el-option label="已过期" value="已过期"></el-option>-->
<!--          </el-select>-->
<!--        </el-col>-->
        <el-col :span="8">
          <el-button type="primary" @click="handleSearch">搜索</el-button>
        <el-col :span="4">
          <el-button type="primary" @click="handleSearch(true)">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
          <el-button style="float: right;" type="primary" @click="handleAdd">
            新增报价
@@ -50,28 +33,28 @@
        stripe
        height="calc(100vh - 22em)"
      >
            <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column prop="quotationNo" label="报价单号" />
        <el-table-column prop="customer" label="客户名称" />
        <el-table-column prop="salesperson" label="业务员" width="100" />
        <el-table-column prop="quotationDate" label="报价日期" width="120" />
        <el-table-column prop="validDate" label="有效期至" width="120" />
        <el-table-column prop="status" label="审批状态" width="120" align="center">
          <template #default="{ row }">
            <el-tag :type="getStatusType(row.status)" disable-transitions>
              {{ row.status || '--' }}
            </el-tag>
          </template>
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column prop="product" label="产品名称" min-width="180" show-overflow-tooltip />
        <el-table-column prop="specification" label="规格型号" min-width="140" show-overflow-tooltip />
        <el-table-column prop="paper" label="纸张" min-width="120" show-overflow-tooltip />
        <el-table-column prop="paperWeight" label="定量" min-width="100" show-overflow-tooltip />
        <el-table-column prop="unitPrice" label="单价" width="120" align="right">
          <template #default="{ row }">{{ formatMoney(row.unitPrice) }}</template>
        </el-table-column>
        <el-table-column prop="totalAmount" label="报价金额" width="120">
          <template #default="scope">
            ¥{{ scope.row.totalAmount.toFixed(2) }}
          </template>
        <el-table-column prop="printingFee" label="印版费" width="120" align="right">
          <template #default="{ row }">{{ formatMoney(row.printingFee) }}</template>
        </el-table-column>
        <el-table-column prop="dieCuttingFee" label="刀版费" width="120" align="right">
          <template #default="{ row }">{{ formatMoney(row.dieCuttingFee) }}</template>
        </el-table-column>
        <el-table-column prop="grindingFee" label="磨具费" width="120" align="right">
          <template #default="{ row }">{{ formatMoney(row.grindingFee) }}</template>
        </el-table-column>
        <el-table-column prop="quantity" label="数量" width="90" align="right" />
        <el-table-column label="操作" width="200" fixed="right" align="center">
          <template #default="scope">
            <el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
            <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>
          </template>
        </el-table-column>
@@ -91,137 +74,13 @@
    <FormDialog v-model="dialogVisible" :title="dialogTitle" width="85%" :close-on-click-modal="false" @close="dialogVisible = false" @confirm="handleSubmit" @cancel="dialogVisible = false">
      <div class="quotation-form-container">
        <el-form :model="form" :rules="rules" ref="formRef" label-width="120px" class="quotation-form">
        <!-- 基本信息 -->
        <el-card class="form-card" shadow="hover">
          <template #header>
            <div class="card-header-wrapper">
              <el-icon class="card-icon"><Document /></el-icon>
              <span class="card-title">基本信息</span>
            </div>
          </template>
          <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%" @change="handleCustomerChange" clearable>
                    <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName" :value="item.customerName">
                      {{
                        item.customerName + "——" + item.taxpayerIdentificationNumber
                      }}
                    </el-option>
                  </el-select>
                </el-form-item>
              </el-col>
              <el-col :span="12">
                <el-form-item label="业务员" prop="salesperson">
                  <el-select v-model="form.salesperson" placeholder="请选择业务员" style="width: 100%" clearable>
                    <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName"
                      :value="item.nickName" />
                  </el-select>
                </el-form-item>
              </el-col>
            </el-row>
            <el-row :gutter="24">
              <el-col :span="12">
                <el-form-item label="报价日期" prop="quotationDate">
                  <el-date-picker
                    v-model="form.quotationDate"
                    type="date"
                    placeholder="选择报价日期"
                    style="width: 100%"
                    format="YYYY-MM-DD"
                    value-format="YYYY-MM-DD"
                    clearable
                  />
                </el-form-item>
              </el-col>
              <el-col :span="12">
                <el-form-item label="有效期至" prop="validDate">
                  <el-date-picker
                    v-model="form.validDate"
                    type="date"
                    placeholder="选择有效期"
                    style="width: 100%"
                    format="YYYY-MM-DD"
                    value-format="YYYY-MM-DD"
                    clearable
                  />
                </el-form-item>
              </el-col>
            </el-row>
            <el-row :gutter="24">
              <el-col :span="12">
                <el-form-item label="付款方式" prop="paymentMethod">
                  <el-input v-model="form.paymentMethod" placeholder="请输入付款方式" clearable />
                </el-form-item>
              </el-col>
            </el-row>
          </div>
        </el-card>
        <!-- 审批人信息 -->
        <el-card class="form-card" shadow="hover">
          <template #header>
            <div class="card-header-wrapper">
              <el-icon class="card-icon"><UserFilled /></el-icon>
              <span class="card-title">审批人选择</span>
              <el-button type="primary" size="small" @click="addApproverNode" class="header-btn">
                <el-icon><Plus /></el-icon>
                新增节点
              </el-button>
            </div>
          </template>
          <div class="form-content">
            <el-row>
              <el-col :span="24">
                <el-form-item>
                  <div class="approver-nodes-container">
                    <div
                      v-for="(node, index) in approverNodes"
                      :key="node.id"
                      class="approver-node-item"
                    >
                      <div class="approver-node-label">
                        <span class="node-step">{{ index + 1 }}</span>
                        <span class="node-text">审批人</span>
                        <el-icon class="arrow-icon"><ArrowRight /></el-icon>
                      </div>
                      <el-select
                        v-model="node.userId"
                        placeholder="选择人员"
                        class="approver-select"
                        clearable
                      >
                        <el-option
                          v-for="user in userList"
                          :key="user.userId"
                          :label="user.nickName"
                          :value="user.userId"
                        />
                      </el-select>
                      <el-button
                        type="danger"
                        size="small"
                        :icon="Delete"
                        @click="removeApproverNode(index)"
                        v-if="approverNodes.length > 1"
                        class="remove-btn"
                      >删除</el-button>
                    </div>
                  </div>
                </el-form-item>
              </el-col>
            </el-row>
          </div>
        </el-card>
        <!-- 产品信息 -->
        <el-card class="form-card" shadow="hover">
          <template #header>
            <div class="card-header-wrapper">
              <el-icon class="card-icon"><Box /></el-icon>
              <span class="card-title">产品信息</span>
              <el-button type="primary" size="small" @click="addProduct" class="header-btn">
              <el-button type="primary" size="small" @click="addProduct" class="header-btn" :disabled="isEdit">
                <el-icon><Plus /></el-icon>
                添加产品
              </el-button>
@@ -272,10 +131,45 @@
                </el-form-item>
              </template>
            </el-table-column>
            <el-table-column prop="paper" label="纸张" width="180">
              <template #default="scope">
                <el-form-item :prop="`products.${scope.$index}.paper`" class="product-table-form-item">
                  <el-input v-model="scope.row.paper" placeholder="纸张" clearable />
                </el-form-item>
              </template>
            </el-table-column>
            <el-table-column prop="paperWeight" label="定量" width="140">
              <template #default="scope">
                <el-form-item :prop="`products.${scope.$index}.paperWeight`" class="product-table-form-item">
                  <el-input v-model="scope.row.paperWeight" placeholder="定量" clearable />
                </el-form-item>
              </template>
            </el-table-column>
            <el-table-column prop="unitPrice" label="单价">
              <template #default="scope">
                <el-form-item :prop="`products.${scope.$index}.unitPrice`" class="product-table-form-item">
                  <el-input-number v-model="scope.row.unitPrice" :min="0" :precision="2" style="width: 100%" />
                </el-form-item>
              </template>
            </el-table-column>
            <el-table-column prop="printingFee" label="印版费" width="140">
              <template #default="scope">
                <el-form-item :prop="`products.${scope.$index}.printingFee`" class="product-table-form-item">
                  <el-input-number v-model="scope.row.printingFee" :min="0" :precision="2" style="width: 100%" />
                </el-form-item>
              </template>
            </el-table-column>
            <el-table-column prop="dieCuttingFee" label="刀版费" width="140">
              <template #default="scope">
                <el-form-item :prop="`products.${scope.$index}.dieCuttingFee`" class="product-table-form-item">
                  <el-input-number v-model="scope.row.dieCuttingFee" :min="0" :precision="2" style="width: 100%" />
                </el-form-item>
              </template>
            </el-table-column>
            <el-table-column prop="grindingFee" label="磨具费" width="140">
              <template #default="scope">
                <el-form-item :prop="`products.${scope.$index}.grindingFee`" class="product-table-form-item">
                  <el-input-number v-model="scope.row.grindingFee" :min="0" :precision="2" style="width: 100%" />
                </el-form-item>
              </template>
            </el-table-column>
@@ -316,36 +210,37 @@
    <!-- 查看详情对话框 -->
    <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.customer }}</el-descriptions-item>
        <el-descriptions-item label="业务员">{{ currentQuotation.salesperson }}</el-descriptions-item>
        <el-descriptions-item label="报价日期">{{ currentQuotation.quotationDate }}</el-descriptions-item>
        <el-descriptions-item label="有效期至">{{ currentQuotation.validDate }}</el-descriptions-item>
        <el-descriptions-item label="付款方式">{{ currentQuotation.paymentMethod }}</el-descriptions-item>
<!--        <el-descriptions-item label="报价状态">-->
<!--          <el-tag :type="getStatusType(currentQuotation.status)">{{ currentQuotation.status }}</el-tag>-->
<!--        </el-descriptions-item>-->
        <el-descriptions-item label="报价总额" :span="2">
          <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%">
          <el-table-column prop="product" label="产品名称" />
          <el-table-column prop="specification" label="规格型号" />
          <el-table-column prop="unit" label="单位" />
          <el-table-column prop="paper" label="纸张" />
          <el-table-column prop="paperWeight" label="定量" />
          <el-table-column prop="unitPrice" label="单价">
            <template #default="scope">
              ¥{{ scope.row.unitPrice.toFixed(2) }}
            </template>
          </el-table-column>
          <el-table-column prop="printingFee" label="印版费">
            <template #default="scope">
              ¥{{ Number(scope.row.printingFee || 0).toFixed(2) }}
            </template>
          </el-table-column>
          <el-table-column prop="dieCuttingFee" label="刀版费">
            <template #default="scope">
              ¥{{ Number(scope.row.dieCuttingFee || 0).toFixed(2) }}
            </template>
          </el-table-column>
          <el-table-column prop="grindingFee" label="磨具费">
            <template #default="scope">
              ¥{{ Number(scope.row.grindingFee || 0).toFixed(2) }}
            </template>
          </el-table-column>
        </el-table>
      </div>
      <div v-if="currentQuotation.remark" style="margin-top: 20px;">
      <div v-if="currentQuotation.remark" style="margin-top: 12px;">
        <h4>备注</h4>
        <p>{{ currentQuotation.remark }}</p>
      </div>
@@ -356,23 +251,28 @@
<script setup>
import { ref, reactive, computed, onMounted, markRaw, shallowRef } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Document, UserFilled, Box, EditPen, Plus, ArrowRight, Delete } from '@element-plus/icons-vue'
import { Search, Box, EditPen, Plus } from '@element-plus/icons-vue'
import Pagination from '@/components/PIMTable/Pagination.vue'
import FormDialog from '@/components/Dialog/FormDialog.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";
import {
  quotationProductListPage,
  getQuotationProductInfo,
  addOrUpdateQuotationProduct,
  editQuotationProduct,
  deleteQuotationProduct
} from '@/api/salesManagement/salesQuotationProduct.js'
import {modelList, productTreeList} from "@/api/basicData/product.js";
// import {listCustomerPrivatePool} from "@/api/basicData/customerFile.js";
import { userListNoPage } from "@/api/system/user.js";
// 响应式数据
const loading = ref(false)
const searchForm = reactive({
  quotationNo: '',
  customer: '',
  status: ''
  product: ''
})
const quotationList = ref([])
const userList = ref([])
const productOptions = ref([]);
const modelOptions = ref([]);
const pagination = reactive({
@@ -402,13 +302,7 @@
  totalAmount: 0
})
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 baseRules = {}
const productRowRules = {
  productId: [{ required: true, message: '请选择产品名称', trigger: 'change' }],
@@ -426,51 +320,28 @@
  })
  return r
})
const userList = ref([]);
const customerOption = ref([]);
// 审批人节点相关
const approverNodes = ref([
  { id: 1, userId: null }
])
let nextApproverId = 2
const isEdit = ref(false)
const editId = ref(null)
const currentQuotation = ref({})
const formRef = ref()
// 添加审批人节点
function addApproverNode() {
  approverNodes.value.push({ id: nextApproverId++, userId: null })
}
// 删除审批人节点
function removeApproverNode(index) {
  approverNodes.value.splice(index, 1)
}
// 计算属性
const filteredList = computed(() => {
  let list = quotationList.value
  return list
  const keyword = String(searchForm.product || '').trim()
  if (!keyword) return quotationList.value
  return quotationList.value.filter((row) => {
    const p = String(row?.product || '').toLowerCase()
    return p.includes(keyword.toLowerCase())
  })
})
// 方法
const getStatusType = (status) => {
  const statusMap = {
    '待审批': 'info',
    '审核中': 'primary',
    '通过': 'success',
    '拒绝': 'danger'
  }
  return statusMap[status] || 'info'
}
const formatMoney = (value) => `¥${Number(value || 0).toFixed(2)}`
const resetSearch = () => {
  searchForm.quotationNo = ''
  searchForm.customer = ''
  searchForm.status = ''
  searchForm.product = ''
  // 重置到第一页并重新查询
  pagination.currentPage = 1
  handleSearch()
@@ -480,26 +351,11 @@
  dialogTitle.value = '新增报价'
  isEdit.value = false
  resetForm()
  // 重置审批人节点
  approverNodes.value = [{ id: 1, userId: null }]
  nextApproverId = 2
  dialogVisible.value = true
   let userLists = await userListNoPage();
   // 只复制需要的字段,避免将组件引用放入响应式对象
   userList.value = (userLists.data || []).map(item => ({
    userId: item.userId,
    nickName: item.nickName || '',
    userName: item.userName || ''
  }));
   getProductOptions();
   customerList().then((res) => {
      // 只复制需要的字段,避免将组件引用放入响应式对象
      customerOption.value = (Array.isArray(res) ? res : []).map(item => ({
      id: item.id,
      customerName: item.customerName || '',
      taxpayerIdentificationNumber: item.taxpayerIdentificationNumber || ''
    }))
   });
  getProductOptions();
  // listCustomerPrivatePool({current: -1,size:-1}).then((res) => {
  //   customerOption.value = res.data.records;
  // });
}
const getProductOptions = () => {
   // 返回 Promise,便于编辑时 await 确保能反显
@@ -594,29 +450,105 @@
   }
   return null; // 没有找到节点,返回null
};
const handleView = (row) => {
  // 只复制需要的字段,避免将组件引用放入响应式对象
  currentQuotation.value = {
    quotationNo: row.quotationNo || '',
    customer: row.customer || '',
    salesperson: row.salesperson || '',
    quotationDate: row.quotationDate || '',
    validDate: row.validDate || '',
    paymentMethod: row.paymentMethod || '',
    status: row.status || '',
    remark: row.remark || '',
    products: row.products ? row.products.map(product => ({
      productId: product.productId || '',
      product: product.product || product.productName || '',
      specificationId: product.specificationId || '',
      specification: product.specification || '',
      quantity: product.quantity || 0,
      unit: product.unit || '',
      unitPrice: product.unitPrice || 0,
      amount: product.amount || 0
    })) : [],
    totalAmount: row.totalAmount || 0
const mapProductItem = (product) => ({
  id: product?.id || '',
  salesQuotationId: product?.salesQuotationId || '',
  productId: product?.productId || '',
  product: product?.product || product?.productName || '',
  specificationId: product?.specificationId || '',
  specification: product?.specification || '',
  quantity: product?.quantity || 0,
  unit: product?.unit || '',
  paper: product?.paper || '',
  paperWeight: product?.paperWeight || '',
  unitPrice: product?.unitPrice || 0,
  printingFee: Number(product?.printingFee || 0),
  dieCuttingFee: Number(product?.dieCuttingFee || 0),
  grindingFee: Number(product?.grindingFee || 0),
  amount: product?.amount || 0
})
const mapQuotationItem = (item) => ({
  id: item?.id,
  salesQuotationId: item?.salesQuotationId || '',
  product: item?.product || '',
  specification: item?.specification || '',
  unit: item?.unit || '',
  paper: item?.paper || '',
  paperWeight: item?.paperWeight || '',
  unitPrice: Number(item?.unitPrice || 0),
  printingFee: Number(item?.printingFee || 0),
  dieCuttingFee: Number(item?.dieCuttingFee || 0),
  grindingFee: Number(item?.grindingFee || 0),
  quantity: Number(item?.quantity || 0),
  amount: Number(item?.amount || 0),
  createTime: item?.createTime || '',
  quotationNo: item?.quotationNo || '',
  customer: item?.customer || '',
  salesperson: item?.salesperson || '',
  quotationDate: item?.quotationDate || '',
  validDate: item?.validDate || '',
  paymentMethod: item?.paymentMethod || '',
  status: item?.status || '草稿',
  approveUserIds: item?.approveUserIds || '',
  remark: item?.remark || '',
  products: Array.isArray(item?.products) && item.products.length > 0
    ? item.products.map(mapProductItem)
    : [mapProductItem(item)],
  subtotal: item?.subtotal || 0,
  freight: item?.freight || 0,
  otherFee: item?.otherFee || 0,
  discountRate: item?.discountRate || 0,
  discountAmount: item?.discountAmount || 0,
  totalAmount: item?.totalAmount || 0
})
const calcTotalAmountFromProducts = (products) => {
  return (products || []).reduce((sum, product) => {
    const price = Number(product.unitPrice) || 0
    const printingFee = Number(product.printingFee) || 0
    const dieCuttingFee = Number(product.dieCuttingFee) || 0
    const grindingFee = Number(product.grindingFee) || 0
    return sum + price + printingFee + dieCuttingFee + grindingFee
  }, 0)
}
const buildProductPayload = (product) => {
  const quantity = Number(product?.quantity || 0)
  const unitPrice = Number(product?.unitPrice || 0)
  const printingFee = Number(product?.printingFee || 0)
  const dieCuttingFee = Number(product?.dieCuttingFee || 0)
  const grindingFee = Number(product?.grindingFee || 0)
  return {
    id: product?.id || undefined,
    salesQuotationId: product?.salesQuotationId || form.salesQuotationId || null,
    product: product?.product || '',
    specification: product?.specification || '',
    unit: product?.unit || '',
    paper: product?.paper || '',
    paperWeight: product?.paperWeight || '',
    unitPrice,
    printingFee,
    dieCuttingFee,
    grindingFee,
    quantity,
    amount: Number(product?.amount ?? quantity * unitPrice),
    remark: product?.remark ?? form.remark ?? ''
  }
}
const handleView = async (row) => {
  let source = row
  if (row?.id) {
    try {
      const res = await getQuotationProductInfo(row.id)
      source = res?.data || row
    } catch (error) {
      source = row
    }
  }
  source.totalAmount = source.totalAmount || calcTotalAmountFromProducts(source?.products || [source])
  currentQuotation.value = mapQuotationItem(source)
  viewDialogVisible.value = true
}
@@ -627,17 +559,31 @@
  form.id = row.id || form.id || null
  // 先加载产品树数据,否则 el-tree-select 无法反显产品名称
  await getProductOptions()
  let source = row
  if (row?.id) {
    try {
      const res = await getQuotationProductInfo(row.id)
      source = res?.data || row
    } catch (error) {
      source = row
    }
  }
  source.totalAmount = source.totalAmount || calcTotalAmountFromProducts(source?.products || [source])
  const sourceProducts = Array.isArray(source?.products) && source.products.length > 0
    ? source.products
    : [source]
  // 只复制需要的字段,避免将组件引用放入响应式对象
  form.quotationNo = row.quotationNo || ''
  form.customer = row.customer || ''
  form.salesperson = row.salesperson || ''
  form.quotationDate = row.quotationDate || ''
  form.validDate = row.validDate || ''
  form.paymentMethod = row.paymentMethod || ''
  form.status = row.status || '草稿'
  form.remark = row.remark || ''
  form.products = row.products ? await Promise.all(row.products.map(async (product) => {
  form.quotationNo = source.quotationNo || ''
  form.customer = source.customer || ''
  form.salesperson = source.salesperson || ''
  form.quotationDate = source.quotationDate || ''
  form.validDate = source.validDate || ''
  form.paymentMethod = source.paymentMethod || ''
  form.status = source.status || '草稿'
  form.remark = source.remark || ''
  form.products = await Promise.all(sourceProducts.map(async (product) => {
    const productName = product.product || product.productName || ''
    // 优先用 productId;如果只有名称,尝试反查 id 以便树选择器反显
    const resolvedProductId = product.productId
@@ -672,39 +618,23 @@
      specification: product.specification || '',
      quantity: product.quantity || 0,
      unit: product.unit || '',
      paper: product.paper || '',
      paperWeight: product.paperWeight || '',
      unitPrice: product.unitPrice || 0,
      printingFee: Number(product.printingFee || 0),
      dieCuttingFee: Number(product.dieCuttingFee || 0),
      grindingFee: Number(product.grindingFee || 0),
      amount: product.amount || 0,
      modelOptions: modelOptions // 为每行添加独立的规格型号列表
    }
  })) : []
  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
  // 反显审批人
  if (row.approveUserIds) {
    const userIds = row.approveUserIds.split(',')
    approverNodes.value = userIds.map((userId, idx) => ({
      id: idx + 1,
      userId: parseInt(userId.trim())
    }))
    nextApproverId = userIds.length + 1
  } else {
    approverNodes.value = [{ id: 1, userId: null }]
    nextApproverId = 2
  }
  // 加载用户列表
  let userLists = await userListNoPage();
  userList.value = (userLists.data || []).map(item => ({
    userId: item.userId,
    nickName: item.nickName || '',
    userName: item.userName || ''
  }));
  }))
  form.subtotal = source.subtotal || 0
  form.freight = source.freight || 0
  form.otherFee = source.otherFee || 0
  form.discountRate = source.discountRate || 0
  form.discountAmount = source.discountAmount || 0
  form.totalAmount = source.totalAmount || 0
  dialogVisible.value = true
}
@@ -715,23 +645,21 @@
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    const index = quotationList.value.findIndex(item => item.id === row.id)
    if (index > -1) {
      deleteQuotation(row.id).then(res=>{
        // console.log(res)
        if(res.code===200){
          ElMessage.success('删除成功')
          handleSearch()
        }
      })
      // quotationList.value.splice(index, 1)
      // pagination.total--
      // ElMessage.success('删除成功')
    if (!row?.id) {
      ElMessage.warning('未获取到可删除的产品ID')
      return
    }
    deleteQuotationProduct([row.id]).then(res=>{
      if(res.code===200){
        ElMessage.success('删除成功')
        handleSearch()
      }
    })
  })
}
const resetForm = () => {
  form.id = null
  form.customer = ''
  form.salesperson = ''
  form.quotationDate = ''
@@ -749,6 +677,10 @@
}
const addProduct = () => {
  if (isEdit.value) {
    ElMessage.warning('编辑时不允许新增产品')
    return
  }
  form.products.push({
    productId: '',
    product: '',
@@ -757,7 +689,12 @@
    specification: '',
    quantity: 1,
    unit: '',
    paper: '',
    paperWeight: '',
    unitPrice: 0,
    printingFee: 0,
    dieCuttingFee: 0,
    grindingFee: 0,
    amount: 0,
    modelOptions: [] // 为每行添加独立的规格型号列表
  })
@@ -783,10 +720,6 @@
  form.totalAmount = form.subtotal + form.freight + form.otherFee - form.discountAmount
}
const handleCustomerChange = () => {
  // 可以根据客户信息自动填充一些默认值
}
const handleSubmit = () => {
  formRef.value.validate((valid) => {
    if (valid) {
@@ -795,46 +728,38 @@
        return
      }
      // 审批人必填校验
      const hasEmptyApprover = approverNodes.value.some(node => !node.userId)
      if (hasEmptyApprover) {
        ElMessage.error('请为所有审批节点选择审批人!')
        return
      }
      // 收集所有节点的审批人id
      form.approveUserIds = approverNodes.value.map(node => node.userId).join(',')
      // 计算所有产品的单价总和
      form.totalAmount = form.products.reduce((sum, product) => {
        const price = Number(product.unitPrice) || 0
        return sum + price
        const printingFee = Number(product.printingFee) || 0
        const dieCuttingFee = Number(product.dieCuttingFee) || 0
        const grindingFee = Number(product.grindingFee) || 0
        return sum + price + printingFee + dieCuttingFee + grindingFee
      }, 0)
      if (isEdit.value) {
        // 编辑
        const index = quotationList.value.findIndex(item => item.id === editId.value)
        if (index > -1) {
          updateQuotation(form).then(res=>{
            // console.log(res)
            if(res.code===200){
              ElMessage.success('编辑成功')
              dialogVisible.value = false
              handleSearch()
            }
          })
        }
        const editingItem = form.products[0] || {}
        const payload = buildProductPayload({
          ...editingItem,
          id: editingItem.id || form.id
        })
        editQuotationProduct(payload).then((res) => {
          if (res?.code === 200) {
            ElMessage.success('编辑成功')
            dialogVisible.value = false
            handleSearch()
          }
        })
      } else {
        // 新增
        addQuotation(form).then(res=>{
          if(res.code===200){
        const payloadList = form.products.map((item) => buildProductPayload(item))
        addOrUpdateQuotationProduct(payloadList).then((res) => {
          if (res?.code === 200) {
            ElMessage.success('新增成功')
            dialogVisible.value = false
            handleSearch()
          }
        })
      }
    }
  })
}
@@ -845,60 +770,41 @@
  // 分页变化时重新查询列表
  handleSearch()
}
const handleSearch = ()=>{
const handleSearch = (resetPage = false)=>{
  if (resetPage) {
    pagination.currentPage = 1
  }
  const params = {
    // 后端分页参数:current / size
    current: pagination.currentPage,
    size: pagination.pageSize,
    ...searchForm
  }
  getQuotationList(params).then(res=>{
    // console.log(res)
  quotationProductListPage(params).then(res=>{
    if(res.code===200){
      // 只复制需要的字段,避免将组件引用或其他对象放入响应式对象
      quotationList.value = (res.data.records || []).map(item => ({
        id: item.id,
        quotationNo: item.quotationNo || '',
        customer: item.customer || '',
        salesperson: item.salesperson || '',
        quotationDate: item.quotationDate || '',
        validDate: item.validDate || '',
        paymentMethod: item.paymentMethod || '',
        status: item.status || '草稿',
        // 审批人(用于编辑时反显)
        approveUserIds: item.approveUserIds || '',
        remark: item.remark || '',
        products: item.products ? item.products.map(product => ({
          productId: product.productId || '',
          product: product.product || product.productName || '',
          specificationId: product.specificationId || '',
          specification: product.specification || '',
          quantity: product.quantity || 0,
          unit: product.unit || '',
          unitPrice: product.unitPrice || 0,
          amount: product.amount || 0
        })) : [],
        subtotal: item.subtotal || 0,
        freight: item.freight || 0,
        otherFee: item.otherFee || 0,
        discountRate: item.discountRate || 0,
        discountAmount: item.discountAmount || 0,
        totalAmount: item.totalAmount || 0
      }))
      const records = res.data.records || []
      quotationList.value = records.map((item) => {
        const mapped = mapQuotationItem(item)
        mapped.totalAmount = calcTotalAmountFromProducts(mapped.products)
        return mapped
      })
      pagination.total = res.data.total
    }
  })
   customerList().then((res) => {
      // 只复制需要的字段,避免将组件引用放入响应式对象
      customerOption.value = (Array.isArray(res) ? res : []).map(item => ({
      id: item.id,
      customerName: item.customerName || '',
      taxpayerIdentificationNumber: item.taxpayerIdentificationNumber || ''
    }))
   });
}
const getUserList = async () => {
  try {
    const res = await userListNoPage()
    userList.value = Array.isArray(res?.data) ? res.data : []
  } catch (error) {
    userList.value = []
    ElMessage.error('加载业务员列表失败')
  }
}
onMounted(()=>{
  getUserList()
  handleSearch()
})
</script>
@@ -991,71 +897,6 @@
  }
}
.approver-nodes-container {
  display: flex;
  flex-wrap: wrap;
  gap: 24px;
  padding: 12px 0;
}
.approver-node-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 16px;
  background: #f8f9fa;
  border-radius: 8px;
  border: 1px solid #e4e7ed;
  transition: all 0.3s ease;
  min-width: 180px;
  &:hover {
    border-color: #409eff;
    background: #f0f7ff;
    box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
  }
}
.approver-node-label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  color: #606266;
  .node-step {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    background: #409eff;
    color: #fff;
    border-radius: 50%;
    font-size: 12px;
    font-weight: 600;
  }
  .node-text {
    font-weight: 500;
  }
  .arrow-icon {
    color: #909399;
    font-size: 14px;
  }
}
.approver-select {
  width: 100%;
  min-width: 150px;
}
.remove-btn {
  margin-top: 4px;
}
.product-table {
  :deep(.el-table__header) {
    background-color: #f5f7fa;
@@ -1082,14 +923,4 @@
  text-align: right;
}
// 响应式优化
@media (max-width: 1200px) {
  .approver-nodes-container {
    gap: 16px;
  }
  .approver-node-item {
    min-width: 160px;
  }
}
</style>