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