gaoluyang
2026-06-12 c9ed3d1958a2489460592b3b17e386d9d515d7ea
src/views/salesManagement/salesQuotation/index.vue
@@ -5,57 +5,64 @@
      <el-row :gutter="20" class="search-row">
        <el-col :span="8">
          <el-input
            v-model="searchForm.quotationNo"
            placeholder="请输入报价单号"
            clearable
            @keyup.enter="handleSearch"
              v-model="searchForm.quotationNo"
              placeholder="请输入报价单号"
              clearable
              @keyup.enter="handleSearch"
          >
            <template #prefix>
              <el-icon><Search /></el-icon>
              <el-icon>
                <Search/>
              </el-icon>
            </template>
          </el-input>
        </el-col>
        <el-col :span="8">
          <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
                     }}
                  </el-option>
          <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="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-button @click="resetSearch">重置</el-button>
          <el-button style="float: right;" type="primary" @click="handleAdd">
            新增报价
          </el-button>
        </el-col>
      </el-row>
      <el-row :gutter="20" style="margin-bottom: 20px;">
        <el-col :span="24">
          <el-button type="primary" @click="handleAdd">新增报价</el-button>
          <el-button type="primary" @click="handleImport">导入销售报价</el-button>
          <el-button type="success" @click="handleShowImportLog">导入记录</el-button>
        </el-col>
      </el-row>
      <!-- 报价列表 -->
      <el-table
        :data="filteredList"
        style="width: 100%"
        v-loading="loading"
        border
        stripe
        height="calc(100vh - 22em)"
          :data="filteredList"
          style="width: 100%"
          v-loading="loading"
          border
          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 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>
@@ -68,191 +75,277 @@
            ¥{{ scope.row.totalAmount.toFixed(2) }}
          </template>
        </el-table-column>
        <el-table-column label="操作" width="200" fixed="right" align="center">
        <el-table-column label="操作" width="300" 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="info" @click="handleShowPriceHistory(scope.row)">降价历史</el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- 分页 -->
      <pagination
        :total="pagination.total"
        layout="total, sizes, prev, pager, next, jumper"
        :page="pagination.currentPage"
        :limit="pagination.pageSize"
        @pagination="handleCurrentChange"
          :total="pagination.total"
          layout="total, sizes, prev, pager, next, jumper"
          :page="pagination.currentPage"
          :limit="pagination.pageSize"
          @pagination="handleCurrentChange"
      />
    </el-card>
    <!-- 新增/编辑对话框 -->
    <FormDialog v-model="dialogVisible" :title="dialogTitle" width="85%" :close-on-click-modal="false" @close="dialogVisible = false" @confirm="handleSubmit" @cancel="dialogVisible = false">
    <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="customerId">
                  <el-select v-model="form.customerId" placeholder="请选择客户" style="width: 100%" clearable filterable>
                    <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="业务员" prop="salesperson">
                  <el-select v-model="form.salesperson" placeholder="请选择业务员" style="width: 100%" clearable filterable>
                    <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"><Box /></el-icon>
              <span class="card-title">产品信息</span>
              <el-button type="primary" size="small" @click="addProduct" class="header-btn">
                <el-icon><Plus /></el-icon>
                添加产品
              </el-button>
            </div>
          </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">
              <template #default="scope">
                <el-form-item :prop="`products.${scope.$index}.productId`" class="product-table-form-item">
                  <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%"
                  />
                </el-form-item>
              </template>
            </el-table-column>
            <el-table-column prop="specification" label="规格型号" width="200">
              <template #default="scope">
                <el-form-item :prop="`products.${scope.$index}.productModelId`" class="product-table-form-item">
                  <el-select
                    v-model="scope.row.productModelId"
                    placeholder="请选择"
                    clearable
                    @change="getProductModel($event, scope.row)"
                    style="width: 100%"
                  >
                    <el-option
                      v-for="item in scope.row.modelOptions || []"
                      :key="item.id"
                      :label="item.model"
                      :value="item.id"
          <!-- 基本信息 -->
          <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-select>
                </el-form-item>
              </template>
            </el-table-column>
            <el-table-column prop="unit" label="单位">
              <template #default="scope">
                <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-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 label="操作" width="80" align="center">
              <template #default="scope">
                <el-button link type="danger" @click="removeProduct(scope.$index)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
          <el-empty v-else description="暂无产品,请点击添加产品" :image-size="80" />
          </div>
        </el-card>
        <!-- 备注信息 -->
        <el-card class="form-card" shadow="hover">
          <template #header>
            <div class="card-header-wrapper">
              <el-icon class="card-icon"><EditPen /></el-icon>
              <span class="card-title">备注信息</span>
                  </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>
          </template>
          <div class="form-content">
            <el-form-item label="备注" prop="remark">
              <el-input
                type="textarea"
                v-model="form.remark"
                placeholder="请输入备注信息(选填)"
                :rows="4"
                maxlength="500"
                show-word-limit
              ></el-input>
            </el-form-item>
          </div>
        </el-card>
      </el-form>
          </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-icon>
                    <Plus/>
                  </el-icon>
                  添加产品
                </el-button>
              </div>
            </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">
                  <template #default="scope">
                    <el-form-item :prop="`products.${scope.$index}.productId`" class="product-table-form-item">
                      <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%"
                      />
                    </el-form-item>
                  </template>
                </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-select
                          v-model="scope.row.specificationId"
                          placeholder="请选择"
                          clearable
                          @change="getProductModel($event, scope.row)"
                          style="width: 100%"
                      >
                        <el-option
                            v-for="item in scope.row.modelOptions || []"
                            :key="item.id"
                            :label="item.model"
                            :value="item.id"
                        />
                      </el-select>
                    </el-form-item>
                  </template>
                </el-table-column>
                <el-table-column prop="unit" label="单位">
                  <template #default="scope">
                    <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-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 label="操作" width="80" align="center">
                  <template #default="scope">
                    <el-button link type="danger" @click="removeProduct(scope.$index)">删除</el-button>
                  </template>
                </el-table-column>
              </el-table>
              <el-empty v-else description="暂无产品,请点击添加产品" :image-size="80"/>
            </div>
          </el-card>
          <!-- 备注信息 -->
          <el-card class="form-card" shadow="hover">
            <template #header>
              <div class="card-header-wrapper">
                <el-icon class="card-icon">
                  <EditPen/>
                </el-icon>
                <span class="card-title">备注信息</span>
              </div>
            </template>
            <div class="form-content">
              <el-form-item label="备注" prop="remark">
                <el-input
                    type="textarea"
                    v-model="form.remark"
                    placeholder="请输入备注信息(选填)"
                    :rows="4"
                    maxlength="500"
                    show-word-limit
                ></el-input>
              </el-form-item>
            </div>
          </el-card>
        </el-form>
      </div>
    </FormDialog>
    <ImportDialog ref="importDialogRef"
                  v-model="importDialogVisible"
                  title="导入报价单"
                  :action="importAction"
                  :headers="importHeaders"
                  :auto-upload="false"
                  :on-success="handleImportSuccess"
                  :on-error="handleImportError"
                  @confirm="handleImportConfirm"
                  @download-template="handleDownloadTemplate"
                  @close="handleImportClose" />
    <!-- 导入记录对话框 -->
    <el-dialog v-model="importLogDialogVisible" title="导入记录" width="900px">
      <el-table :data="importLogList" border stripe v-loading="importLogLoading" height="400">
        <el-table-column align="center" label="序号" type="index" width="60"/>
        <el-table-column prop="batchNo" label="批次号" min-width="180"/>
        <el-table-column prop="fileName" label="文件名" min-width="160"/>
        <el-table-column prop="totalCount" label="总数" width="80" align="center"/>
        <el-table-column prop="successCount" label="成功" width="80" align="center"/>
        <el-table-column prop="failCount" label="失败" width="80" align="center"/>
        <el-table-column prop="status" label="状态" width="100" align="center">
          <template #default="{ row }">
            <el-tag :type="row.status === 'completed' ? 'success' : 'danger'" disable-transitions>
              {{ row.status === 'completed' ? '完成' : row.status }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="createUserName" label="操作人" width="100"/>
        <el-table-column prop="createTime" label="导入时间" width="160"/>
      </el-table>
      <pagination
          v-if="importLogTotal > 0"
          :total="importLogTotal"
          layout="total, prev, pager, next"
          :page="importLogPage.current"
          :limit="importLogPage.size"
          @pagination="handleImportLogPageChange"
      />
    </el-dialog>
    <!-- 降价历史对话框 -->
    <el-dialog v-model="priceHistoryDialogVisible" title="降价历史" width="900px">
      <el-table :data="priceHistoryList" border stripe v-loading="priceHistoryLoading" height="400">
        <el-table-column align="center" label="序号" type="index" width="60"/>
        <el-table-column prop="productName" label="产品名称" min-width="140"/>
        <el-table-column prop="specification" label="规格型号" min-width="120"/>
        <el-table-column prop="oldPrice" label="原价" width="100" align="center">
          <template #default="{ row }">¥{{ row.oldPrice?.toFixed(2) }}</template>
        </el-table-column>
        <el-table-column prop="newPrice" label="新价" width="100" align="center">
          <template #default="{ row }">¥{{ row.newPrice?.toFixed(2) }}</template>
        </el-table-column>
        <el-table-column prop="priceChange" label="变动" width="100" align="center">
          <template #default="{ row }">
            <span :style="{ color: row.priceChange < 0 ? '#67C23A' : '#F56C6C' }">
              {{ row.priceChange?.toFixed(2) }}
            </span>
          </template>
        </el-table-column>
        <el-table-column prop="changeReason" label="原因" width="100"/>
        <el-table-column prop="importBatch" label="导入批次" min-width="180"/>
        <el-table-column prop="importTime" label="导入时间" width="160"/>
        <el-table-column prop="createUserName" label="操作人" width="100"/>
      </el-table>
    </el-dialog>
    <!-- 查看详情对话框 -->
    <el-dialog v-model="viewDialogVisible" title="报价详情" width="800px">
@@ -263,20 +356,22 @@
        <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="报价状态">-->
        <!--          <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>
          <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="product" label="产品名称"/>
          <el-table-column prop="specification" label="规格型号"/>
          <el-table-column prop="unit" label="单位"/>
          <el-table-column prop="unitPrice" label="单价">
            <template #default="scope">
              ¥{{ scope.row.unitPrice.toFixed(2) }}
@@ -294,28 +389,48 @@
</template>
<script setup>
import { ref, reactive, computed, onMounted, markRaw, shallowRef } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Document, Box, EditPen, Plus } from '@element-plus/icons-vue'
import {ref, reactive, computed, onMounted, nextTick, getCurrentInstance} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus'
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 {getQuotationList,addQuotation,updateQuotation,deleteQuotation} from '@/api/salesManagement/salesQuotation.js'
import ImportDialog from '@/components/Dialog/ImportDialog.vue'
import {
  getQuotationList,
  addQuotation,
  updateQuotation,
  deleteQuotation,
  downloadQuotationTemplate,
  getImportLogList,
  getPriceHistoryList
} from '@/api/salesManagement/salesQuotation.js'
import {userListNoPage} from "@/api/system/user.js";
import {customerList} from "@/api/salesManagement/salesLedger.js";
import {modelList, productTreeList} from "@/api/basicData/product.js";
import {listCustomer} from "@/api/basicData/customer.js";
import { userListNoPage } from "@/api/system/user.js";
import {getToken} from "@/utils/auth";
const {proxy} = getCurrentInstance();
// 响应式数据
const loading = ref(false)
const searchForm = reactive({
  quotationNo: '',
  customerId: '',
  customer: '',
  status: ''
})
const quotationList = ref([])
const userList = ref([])
const productOptions = ref([]);
const modelOptions  = ref([]);
const modelOptions = ref([]);
const pagination = reactive({
  total: 3,
  currentPage: 1,
@@ -323,11 +438,27 @@
})
const dialogVisible = ref(false)
const importDialogVisible = ref(false)
const importLogDialogVisible = ref(false)
const priceHistoryDialogVisible = ref(false)
const viewDialogVisible = ref(false)
const dialogTitle = ref('新增报价')
const importDialogRef = ref(null)
const importAction = import.meta.env.VITE_APP_BASE_API + "/sales/quotation/import"
const importHeaders = ref({
  Authorization: `Bearer ${getToken()}`,
})
const importLogList = ref([])
const importLogLoading = ref(false)
const importLogTotal = ref(0)
const importLogPage = reactive({ current: 1, size: 10 })
const priceHistoryList = ref([])
const priceHistoryLoading = ref(false)
const currentQuotationForLog = ref(null)
const currentQuotationForPriceHistory = ref(null)
const dialogTitle= ref('新增报价')
const form = reactive({
  quotationNo: '',
  customerId: undefined,
  customer: '',
  salesperson: '',
  quotationDate: '',
@@ -345,35 +476,126 @@
})
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' }]
  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' }],
  productModelId: [{ required: true, message: '请选择规格型号', trigger: 'change' }],
  unit: [{ required: true, message: '请填写单位', trigger: 'blur' }],
  unitPrice: [{ required: true, message: '请填写单价', trigger: 'change' }]
  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 }
  const r = {...baseRules}
  ;(form.products || []).forEach((_, i) => {
    r[`products.${i}.productId`] = productRowRules.productId
    r[`products.${i}.productModelId`] = productRowRules.productModelId
    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([]);
const isEdit = ref(false)
const editId = ref(null)
const currentQuotation = ref({})
const formRef = ref()
// 导入成功
const handleImportSuccess = (response) => {
  if (response.code === 200) {
    ElMessage.success("导入成功")
    importDialogVisible.value = false
    if (importDialogRef.value) {
      importDialogRef.value.clearFiles()
    }
    handleSearch()
  } else {
    ElMessage.error(response.msg || "导入失败")
  }
}
// 导入失败
const handleImportError = () => {
  ElMessage.error("导入失败,请检查文件格式是否正确")
}
// 确认导入
const handleImportConfirm = () => {
  if (importDialogRef.value) {
    importDialogRef.value.submit()
  }
}
// 下载模板
const handleDownloadTemplate = () => {
  downloadQuotationTemplate().then(blob => {
    const url = window.URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = '报价单导入模板.xlsx'
    a.click()
    window.URL.revokeObjectURL(url)
  })
}
// 关闭导入弹窗
const handleImportClose = () => {
  importDialogVisible.value = false
  if (importDialogRef.value) {
    importDialogRef.value.clearFiles()
  }
}
// 导入记录
const handleShowImportLog = () => {
  importLogPage.current = 1
  importLogDialogVisible.value = true
  fetchImportLogList()
}
const fetchImportLogList = () => {
  importLogLoading.value = true
  getImportLogList({ pageNum: importLogPage.current, pageSize: importLogPage.size }).then(res => {
    if (res.code === 200) {
      importLogList.value = res.data.records || []
      importLogTotal.value = res.data.total || 0
    }
  }).finally(() => {
    importLogLoading.value = false
  })
}
const handleImportLogPageChange = (val) => {
  importLogPage.current = val.page
  importLogPage.size = val.limit
  fetchImportLogList()
}
// 降价历史
const handleShowPriceHistory = (row) => {
  currentQuotationForPriceHistory.value = row
  priceHistoryDialogVisible.value = true
  priceHistoryList.value = []
  fetchPriceHistoryList(row)
}
const fetchPriceHistoryList = (row) => {
  priceHistoryLoading.value = true
  getPriceHistoryList({ quotationProductId: row.id }).then(res => {
    priceHistoryList.value = res.data
  }).finally(() => {
    priceHistoryLoading.value = false
  })
}
const handlePriceHistoryPageChange = () => {}
// 计算属性
const filteredList = computed(() => {
@@ -401,113 +623,133 @@
  handleSearch()
}
// 导入文件
const handleImport = () => {
  importDialogVisible.value = true
  nextTick(() => {
    if (importDialogRef.value) {
      importDialogRef.value.clearFiles()
    }
  })
}
const handleAdd = async () => {
  dialogTitle.value = '新增报价'
  isEdit.value = false
  resetForm()
  dialogVisible.value = true
  let userLists = await userListNoPage();
  // 只复制需要的字段,避免将组件引用放入响应式对象
  userList.value = (userLists.data || []).map(item => ({
    userId: item.userId,
    nickName: item.nickName || '',
    userName: item.userName || ''
  }));
  getProductOptions();
  fetchCustomerOptions()
}
const fetchCustomerOptions = () => {
  if (customerOption.value.length > 0) return
  listCustomer({current: -1,size:-1, type: 0}).then((res) => {
    customerOption.value = res.data.records;
  customerList().then((res) => {
    // 只复制需要的字段,避免将组件引用放入响应式对象
    customerOption.value = (Array.isArray(res) ? res : []).map(item => ({
      id: item.id,
      customerName: item.customerName || '',
      taxpayerIdentificationNumber: item.taxpayerIdentificationNumber || ''
    }))
  });
}
const getProductOptions = () => {
   // 返回 Promise,便于编辑时 await 确保能反显
   return productTreeList().then((res) => {
      productOptions.value = convertIdToValue(res);
      return productOptions.value
   });
  // 返回 Promise,便于编辑时 await 确保能反显
  return productTreeList().then((res) => {
    productOptions.value = convertIdToValue(res);
    return productOptions.value
  });
};
function convertIdToValue(data) {
   return data.map((item) => {
      const { id, children, ...rest } = item;
      const newItem = {
         ...rest,
         value: id, // 将 id 改为 value
      };
      if (children && children.length > 0) {
         newItem.children = convertIdToValue(children);
      }
      return newItem;
   });
function convertIdToValue(data) {
  return data.map((item) => {
    const {id, children, ...rest} = item;
    const newItem = {
      ...rest,
      value: id, // 将 id 改为 value
    };
    if (children && children.length > 0) {
      newItem.children = convertIdToValue(children);
    }
    return newItem;
  });
}
// 根据名称反查节点 id,便于仅存名称时的反显
function findNodeIdByLabel(nodes, label) {
   if (!label) return null;
   for (let i = 0; i < nodes.length; i++) {
      const node = nodes[i];
      if (node.label === label) return node.value;
      if (node.children && node.children.length > 0) {
         const found = findNodeIdByLabel(node.children, label);
         if (found !== null && found !== undefined) return found;
      }
   }
   return null;
  if (!label) return null;
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i];
    if (node.label === label) return node.value;
    if (node.children && node.children.length > 0) {
      const found = findNodeIdByLabel(node.children, label);
      if (found !== null && found !== undefined) return found;
    }
  }
  return null;
}
const getModels = (value, row) => {
   if (!row) return;
   // 如果清空选择,则清空相关字段
   if (!value) {
      row.productId = '';
      row.product = '';
      row.modelOptions = [];
      row.productModelId = '';
      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 || [];
   });
  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.productModelId = '';
      row.specification = '';
      row.unit = '';
      return;
   }
   // 更新 productModelId(v-model 已经自动更新,这里确保一致性)
   row.productModelId = 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 = '';
   }
  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
  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) => {
  // 只复制需要的字段,避免将组件引用放入响应式对象
@@ -523,7 +765,7 @@
    products: row.products ? row.products.map(product => ({
      productId: product.productId || '',
      product: product.product || product.productName || '',
      productModelId: product.productModelId || '',
      specificationId: product.specificationId || '',
      specification: product.specification || '',
      quantity: product.quantity || 0,
      unit: product.unit || '',
@@ -542,12 +784,10 @@
  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 || ''
@@ -558,23 +798,23 @@
    const productName = product.product || product.productName || ''
    // 优先用 productId;如果只有名称,尝试反查 id 以便树选择器反显
    const resolvedProductId = product.productId
      ? Number(product.productId)
      : findNodeIdByLabel(productOptions.value, productName) || ''
        ? Number(product.productId)
        : findNodeIdByLabel(productOptions.value, productName) || ''
    // 如果有产品ID,加载对应的规格型号列表
    let modelOptions = [];
    let resolvedProductModelId = product.productModelId || '';
    let resolvedSpecificationId = product.specificationId || '';
    if (resolvedProductId) {
      try {
        const res = await modelList({ id: resolvedProductId });
        const res = await modelList({id: resolvedProductId});
        modelOptions = res || [];
        // 如果返回的数据没有 productModelId,但有 specification 名称,根据名称查找 ID
        if (!resolvedProductModelId && product.specification) {
        // 如果返回的数据没有 specificationId,但有 specification 名称,根据名称查找 ID
        if (!resolvedSpecificationId && product.specification) {
          const foundModel = modelOptions.find(item => item.model === product.specification);
          if (foundModel) {
            resolvedProductModelId = foundModel.id;
            resolvedSpecificationId = foundModel.id;
          }
        }
      } catch (error) {
@@ -585,7 +825,7 @@
    return {
      productId: resolvedProductId,
      product: productName,
      productModelId: resolvedProductModelId,
      specificationId: resolvedSpecificationId,
      specification: product.specification || '',
      quantity: product.quantity || 0,
      unit: product.unit || '',
@@ -601,6 +841,14 @@
  form.discountAmount = row.discountAmount || 0
  form.totalAmount = row.totalAmount || 0
  // 加载用户列表
  let userLists = await userListNoPage();
  userList.value = (userLists.data || []).map(item => ({
    userId: item.userId,
    nickName: item.nickName || '',
    userName: item.userName || ''
  }));
  dialogVisible.value = true
}
@@ -613,9 +861,9 @@
  }).then(() => {
    const index = quotationList.value.findIndex(item => item.id === row.id)
    if (index > -1) {
      deleteQuotation(row.id).then(res=>{
      deleteQuotation(row.id).then(res => {
        // console.log(res)
        if(res.code===200){
        if (res.code === 200) {
          ElMessage.success('删除成功')
          handleSearch()
        }
@@ -649,7 +897,8 @@
    productId: '',
    product: '',
    productName: '',
    productModelId: '',
    specificationId: '',
    specification: '',
    quantity: 1,
    unit: '',
    unitPrice: 0,
@@ -678,6 +927,10 @@
  form.totalAmount = form.subtotal + form.freight + form.otherFee - form.discountAmount
}
const handleCustomerChange = () => {
  // 可以根据客户信息自动填充一些默认值
}
const handleSubmit = () => {
  formRef.value.validate((valid) => {
    if (valid) {
@@ -692,14 +945,13 @@
        return sum + price
      }, 0)
      form.customer = customerOption.value.find(item => item.id === form.customerId)?.customerName || ''
      if (isEdit.value) {
        // 编辑
        const index = quotationList.value.findIndex(item => item.id === editId.value)
        if (index > -1) {
          updateQuotation(form).then(res=>{
          updateQuotation(form).then(res => {
            // console.log(res)
            if(res.code===200){
            if (res.code === 200) {
              ElMessage.success('编辑成功')
              dialogVisible.value = false
              handleSearch()
@@ -708,8 +960,8 @@
        }
      } else {
        // 新增
        addQuotation(form).then(res=>{
          if(res.code===200){
        addQuotation(form).then(res => {
          if (res.code === 200) {
            ElMessage.success('新增成功')
            dialogVisible.value = false
            handleSearch()
@@ -721,40 +973,41 @@
  })
}
const downloadImportTemplate = () => {
  proxy.download("/sales/quotation/downloadTemplate", {}, "报价单导入模板.xlsx");
}
const handleCurrentChange = (val) => {
  pagination.currentPage = val.page
  pagination.pageSize = val.limit
  // 分页变化时重新查询列表
  handleSearch()
}
const handleSearch = ()=>{
const handleSearch = () => {
  const params = {
    // 后端分页参数:current / size
    current: pagination.currentPage,
    size: pagination.pageSize,
    ...searchForm
  }
  getQuotationList(params).then(res=>{
  getQuotationList(params).then(res => {
    // console.log(res)
    if(res.code===200){
    if (res.code === 200) {
      // 只复制需要的字段,避免将组件引用或其他对象放入响应式对象
      quotationList.value = (res.data.records || []).map(item => ({
        id: item.id,
        quotationNo: item.quotationNo || '',
        customer: item.customer || '',
        customerId: item.customerId || undefined,
        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 || '',
          productModelId: product.productModelId || '',
          specificationId: product.specificationId || '',
          specification: product.specification || '',
          quantity: product.quantity || 0,
          unit: product.unit || '',
@@ -771,25 +1024,18 @@
      pagination.total = res.data.total
    }
  })
   // customerList().then((res) => {
   //    customerOption.value = res;
   // });
  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()
onMounted(() => {
  handleSearch()
  fetchCustomerOptions()
})
</script>
@@ -872,13 +1118,80 @@
.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;
  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 {
@@ -907,4 +1220,14 @@
  text-align: right;
}
// 响应式优化
@media (max-width: 1200px) {
  .approver-nodes-container {
    gap: 16px;
  }
  .approver-node-item {
    min-width: 160px;
  }
}
</style>