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