| | |
| | | <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> |
| | |
| | | ¥{{ 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"> |
| | |
| | | <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) }} |
| | |
| | | </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, |
| | |
| | | }) |
| | | |
| | | 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: '', |
| | |
| | | }) |
| | | |
| | | 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(() => { |
| | |
| | | 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) => { |
| | | // 只复制需要的字段,避免将组件引用放入响应式对象 |
| | |
| | | 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 || '', |
| | |
| | | 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 || '' |
| | |
| | | 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) { |
| | |
| | | return { |
| | | productId: resolvedProductId, |
| | | product: productName, |
| | | productModelId: resolvedProductModelId, |
| | | specificationId: resolvedSpecificationId, |
| | | specification: product.specification || '', |
| | | quantity: product.quantity || 0, |
| | | unit: product.unit || '', |
| | |
| | | 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 |
| | | } |
| | | |
| | |
| | | }).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() |
| | | } |
| | |
| | | productId: '', |
| | | product: '', |
| | | productName: '', |
| | | productModelId: '', |
| | | specificationId: '', |
| | | specification: '', |
| | | quantity: 1, |
| | | unit: '', |
| | | unitPrice: 0, |
| | |
| | | form.totalAmount = form.subtotal + form.freight + form.otherFee - form.discountAmount |
| | | } |
| | | |
| | | const handleCustomerChange = () => { |
| | | // 可以根据客户信息自动填充一些默认值 |
| | | } |
| | | |
| | | const handleSubmit = () => { |
| | | formRef.value.validate((valid) => { |
| | | if (valid) { |
| | |
| | | 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() |
| | |
| | | } |
| | | } else { |
| | | // 新增 |
| | | addQuotation(form).then(res=>{ |
| | | if(res.code===200){ |
| | | addQuotation(form).then(res => { |
| | | if (res.code === 200) { |
| | | ElMessage.success('新增成功') |
| | | dialogVisible.value = false |
| | | handleSearch() |
| | |
| | | }) |
| | | } |
| | | |
| | | 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 || '', |
| | |
| | | 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> |
| | | |
| | |
| | | |
| | | .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 { |
| | |
| | | text-align: right; |
| | | } |
| | | |
| | | // 响应式优化 |
| | | @media (max-width: 1200px) { |
| | | .approver-nodes-container { |
| | | gap: 16px; |
| | | } |
| | | |
| | | .approver-node-item { |
| | | min-width: 160px; |
| | | } |
| | | } |
| | | </style> |