@@ -1,144 +1,289 @@
< template >
< div class = "app-container" >
< el-form :model = "queryParams" size = "small" :inline = "true" ref = "queryForm" >
< el-form-item label = "状态 " prop = "status " >
< el-select v-model = "queryParams.status" placeholder="全部" clearable style="width:120px" >
< el -option label = "草稿" value = "draft " / >
< el-option label = "已提交" value = "submitted" / >
< el-option label = "已采纳" value = "accepted" / >
< el-option label = "已拒绝" value = "rejected" / >
< / el-select >
< / el-form-item >
< el-form-item >
< el-button type = "primary" icon = "el-icon-search" size = "mini" @click ="handleQuery" > 搜索 < / el -button >
< el-button icon = "el-icon-refresh" size = "mini" @click ="resetQuery" > 重置 < /el -button >
< / el-form-item >
< / el-form >
< el-row :gutter = "10" class = "mb8" >
< el-col :span = "1.5 " >
< el-button type = "primary" p lain icon = "el-icon-plus" size = "mini" @click ="handleAdd" > 新增报价 < / el -button >
< div class = "app-container quotation-page " >
<!-- ─ ─ 顶部统计卡片 ─ ─ -- >
< el-row :gutter = "14 " class = "stat-row " >
< el-col :span = "6" >
< div c lass = "stat-card stat-all " >
< div c lass = "stat-num" > { { stats . total } } < / div >
< div c lass = "stat-lbl" > 全部报价 < / div >
< i c lass = "el-icon-document stat-icon" > < / i >
< / div >
< / el-col >
< el-col :span = "6" >
< div class = "stat-card stat-draft" >
< div class = "stat-num" > { { stats . draft } } < /div >
< div class = "stat-lbl" > 草稿 < / div >
< i class = "el-icon-edit-outline stat-icon" > < / i >
< / div >
< / el-col >
< el-col :span = "6 " >
< div c lass = "stat-card stat-submitted" >
< div class = "stat-num" > { { stats . submitted } } < / div >
< div class = "stat-lbl" > 待处理 < / div >
< i class = "el-icon-time stat-icon" > < / i >
< / div >
< / el-col >
< el-col :span = "6" >
< div class = "stat-card stat-accepted" >
< div class = "stat-num" > { { stats . accepted } } < / div >
< div class = "stat-lbl" > 已采纳 < / div >
< i class = "el-icon-circle-check stat-icon" > < / i >
< / div >
< / el-col >
< / el-row >
< el-table v-loading = "loading" :data="list" >
< el-table -column label = "报价单号" prop = "quoteNo" width = "140" / >
< el-table-column label = "RFQ编号" prop = "rfqNo" width = "140" / >
< el-table-column label = "RFQ标题" prop = "rfqTitle" :show-overflow-tooltip = "true" / >
< el-table-column label = "供应商" prop = "supplierName" width = "150" / >
< el-table-column label = "总金额" prop = "totalAmount" width = "120" align = "right" >
< templa te slot -scope = " scope ">
< span style = "color:#409EFF;font-weight:bold" > ¥ { { scope . row . totalAmount | numFormat } } < / span >
<!-- ─ ─ 搜索栏 ─ ─ -- >
< el-card shadow = "never" class = "search-card" >
< el-form :model = "queryParams" ref = "queryForm" size = "small" :inline = "true" >
< el-form-item label = "RFQ单号" >
< el-input v-model = "queryParams.rfqNo" placeholder="询价单号" clearable style="width:150px" @keyup.enter.native="handleQuery" / >
< / el-form-item >
< el-form-i tem label = "供应商 ">
< el-input v-model = "queryParams.supplierName" placeholder="供应商名称" clearable style="width:150px" @keyup.enter.native="handleQuery" / >
< / el-form-item >
< el-form-item label = "状态" >
< el-select v-model = "queryParams.status" placeholder="全部" clearable style="width:110px" >
< el -option label = "草稿" value = "draft" / >
< el-option label = "已提交" value = "submitted" / >
< el-option label = "已采纳" value = "accepted" / >
< el-option label = "已拒绝" value = "rejected" / >
< / el-select >
< / el-form-item >
< el-form-item >
< el-button type = "primary" icon = "el-icon-search" size = "mini" @click ="handleQuery" > 搜索 < / el -button >
< el-button icon = "el-icon-refresh" size = "mini" @click ="resetQuery" > 重置 < / el -button >
< / el-form-item >
< / el-form >
< / el-card >
<!-- ─ ─ 工具栏 ─ ─ -- >
< el-row :gutter = "10" class = "mb8" style = "margin-top:12px" >
< el-col :span = "1.5" >
< el-button type = "primary" icon = "el-icon-plus" size = "mini" @click ="handleAdd" > 新建报价 < / el -button >
< / el-col >
< / el-row >
<!-- ─ ─ 报价列表 ─ ─ -- >
< el-table v-loading = "loading" :data="list" border >
< el-table-column label = "报价单号" prop = "quoteNo" width = "155" / >
< el-table-column label = "关联询价单" width = "200" >
< template slot -scope = " s " >
< div style = "font-weight:600;color:#303133" > { { s . row . rfqNo } } < / div >
< div style = "font-size:12px;color:#909399;margin-top:2px" v-if = "s.row.rfqTitle" > {{ s.row.rfqTitle }} < / div >
< / template >
< / el -table -column >
< el-table-column label = "交期(天) " prop = "de liv eryDays" width ="90" align = "center" / >
< el-table-column label = "状态" width = "100 ">
< template slot -scop e =" scope ">
< el-tag :typ e = "statusType(scope.row.status) " > { { statusLabel ( scope . row . status ) } } < / el-tag >
< el-table-column label = "供应商 " prop = "supp lierName" min - width =" 150 " >
< template slot -scope = " s ">
< div styl e ="display:flex;align-items:center;gap:6px ">
< el-ava tar :siz e = "28" style = "background:#1171c4;flex-shrink:0 " > { { ( s . row . supplierName || '?' ) . charAt ( 0 ) } } < / el-ava tar >
< span > { { s . row . supplierName } } < / span >
< / div >
< / template >
< / el-table-column >
< el-table-column label = "提交时间" prop = "submitTime" width = "160" / >
< el-table-column label = "操作" align = "center" width = "220 ">
< template slot -scope = " scope " >
< el-button size = "mini" type = "text" icon = "el-icon-edit" @click ="handleUpdate(scope.row)" v-if = "scope.row.status==='draft'" > 编辑 < / el -button >
< el-button size = "mini" type = "text" @click ="handleSubmit(scope.row)" v-if = "scope.row.status==='draft'" style="color:#67C23A" > 提交 < / el -button >
< el-button size = "mini" type = "text" style = "color:#67C23A" @click ="handleAccept(scope.row)" v-if = "scope.row.status==='submitted'" > 采纳 < / el -button >
< el-button siz e ="mini" type = "text" style = "color:#F56C6C" @click ="handleReject(scope.row)" v-if = "scope.row.status==='submitted'" > 拒绝 < /el -button >
< el-button size = "mini" type = "text" icon = "el-icon-view" @click ="handleView(scope.row)" > 查看 < / el -button >
< el-table-column label = "金额" width = "120" align = "right" >
< template slot -scope = " s ">
< strong style = "color:#409EFF;font-size:15px" > ¥ { { s . row . totalAmount | money } } < / strong >
< / template >
< / el-table-column >
< el-table-column label = "交期" prop = "deliveryDays" width = "80" align = "center" >
< template slot -scop e =" s " > { { s . row . deliveryDays || '-' } } 天 < /template >
< / el-table-column >
< el-table-column label = "状态" width = "105" align = "center" >
< template slot -scope = " s " >
< div class = "status-chip" : class = "'status-' + s.row.status" >
< i :class = "statusIcon(s.row.status)" > < / i >
{ { statusLabel ( s . row . status ) } }
< / div >
< / template >
< / el-table-column >
< el-table-column label = "提交时间" prop = "submitTime" width = "155" align = "center" >
< template slot -scope = " s " >
< span v-if = "s.row.submitTime" style="font-size:12px" > {{ s.row.submitTime }} < / span >
< span v-else style = "color:#c0c4cc;font-size:12px" > 未提交 < / span >
< / template >
< / el-table-column >
< el-table-column label = "操作" align = "center" width = "220" fixed = "right" >
< template slot -scope = " s " >
< el-button size = "mini" type = "text" icon = "el-icon-view" @click ="handleView(s.row)" > 查看 < / el -button >
< el-button size = "mini" type = "text" icon = "el-icon-edit" @click ="handleUpdate(s.row)"
v-if = "s.row.status==='draft'" > 编辑 < / el -button >
< el-button size = "mini" type = "text" style = "color:#67C23A" icon = "el-icon-upload2"
@click ="handleSubmit(s.row)" v-if = "s.row.status==='draft'" > 提交 < / el -button >
< el-button size = "mini" type = "text" style = "color:#67C23A" icon = "el-icon-check"
@click ="handleAccept(s.row)" v-if = "s.row.status==='submitted'" > 采纳 < / el -button >
< el-button size = "mini" type = "text" style = "color:#F56C6C" icon = "el-icon-close"
@click ="handleReject(s.row)" v-if = "s.row.status==='submitted'" > 拒绝 < / el -button >
< el-button size = "mini" type = "text" icon = "el-icon-delete" style = "color:#f56c6c"
@click ="handleDelete(s.row)" v-if = "s.row.status==='draft'" > 删除 < / el -button >
< / template >
< / el-table-column >
< / el-table >
< pagination v-show = "total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" / >
< pagination v-show = "total>0" :total="total" :page.sync="queryParams.pageNum"
:limit.sync = "queryParams.pageSize" @pagination ="getList" / >
<!-- Edit / Create Dialog -- >
< el-dialog :title = "t itle" :visible.sync = "o pen" width = "96 0px" append -to -body >
< el-form ref = "form" :model = "form" :rules = "rules" label -width = " 10 0px" >
< el-row >
<!-- ─ ─ 创建 / 编辑对话框 ─ ─ -- >
< el-dialog :title = "dialogT itle" :visible.sync = "dialogO pen" width = "100 0px" append -to -body :close-on-click-modal = "false" >
< el-form ref = "form" :model = "form" :rules = "rules" label -width = " 9 0px" size = "small" >
< el-row :gutter = "16" >
< el-col :span = "12" >
< el-form-item label = "关联RFQ" prop = "rfqId" >
< el-select v-model = "form.rfqId" placeholder="选择RFQ " filterable style="width:100%" >
< el -option v-for = "r in rfqOptions" :key="r.rfqId" :label="r.rfqNo+' - '+r.rfqTitle" :value="r.rfqId" / >
< el-select v-model = "form.rfqId" placeholder="选择询价单 " filterable style="width:100%" @change="onRfqChange" >
< el -option v-for = "r in rfqOptions" :key="r.rfqId"
: label = "r.rfqNo + ' · ' + r.rfqTitle" :value = "r.rfqId" >
< div style = "display:flex;justify-content:space-between" >
< span style = "font-weight:600" > { { r . rfqNo } } < / span >
< span style = "color:#909399;font-size:12px;margin-left:8px" > { { r . rfqTitle } } < / span >
< / div >
< / el-option >
< / el-select >
< / el-form-item >
< / el-col >
< el-col :span = "12" >
< el-form-item label = "供应商" prop = "supplierId" >
< el-select v-model = "form.supplierId" placeholder="选择供应商" filterable style="width:100%" >
< el -option v-for = "s in supplierOptions" :key="s.supplierId" :label="s.supplierName" :value="s.supplierId" / >
< el -option v-for = "s in supplierOptions" :key="s.supplierId"
:label = "s.supplierName" :value = "s.supplierId" / >
< / el-select >
< / el-form-item >
< / el-col >
< / el-row >
< el-row >
< el-row :gutter = "16" >
< el-col :span = "8" >
< el-form-item label = "有效期(天)" prop = "validDays " >
< el-input-number v-model = "form.validDays" :min="1" style="width:100%" / >
< el-form-item label = "报价 有效期" >
< el-input-number v-model = "form.validDays" :min="1" :max="365" style="width:100%" / >
< span style = "margin-left:4px;color:#909399;font-size:12px" > 天 < / span >
< / el-form-item >
< / el-col >
< el-col :span = "8" >
< el-form-item label = "交货期(天)" prop = "deliveryDays " >
< el-form-item label = "整体交期 " >
< el-input-number v-model = "form.deliveryDays" :min="0" style="width:100%" / >
< span style = "margin-left:4px;color:#909399;font-size:12px" > 天 < / span >
< / el-form-item >
< / el-col >
< el-col :span = "8" >
< el-form-item label = "币种" prop = "currency" >
< el-form-item label = "币种" >
< el-select v-model = "form.currency" style="width:100%" >
< el -option label = "人民币 CNY" value = "CNY" / >
< el-option label = "美元 USD" value = "USD" / >
< el -option label = "人民币 CNY" value = "CNY" / >
< el-option label = "美元 USD" value = "USD" / >
< el-option label = "欧元 EUR" value = "EUR" / >
< / el-select >
< / el-form-item >
< / el-col >
< / el-row >
< el-divider content -position = " left " > 报价明细 < / el-divider >
< el-table : data = "form.items || []" border size = "small " >
< el-table-column label = "物料名称" min -width = " 150 " >
< template slot -scope = " scope " > < el-input v-model = "scope.row.materialName" size="small" / > < / template >
< / el-table-column >
< el-table-column label = "规格" min -width = " 120 " >
< template slot -scope = " scope " > < el-input v-model = "scope.row.spec" size="small" / > < / template >
< / el-table-column >
< el-table-column label = "单位" width = "80" >
< template slot -scope = " scope " > < el-input v-model = "scope.row.unit" size="small" / > < / template >
< / el-table-column >
< el-table-column label = "数量" width = "100" >
< template slot -scop e =" scope ">
< el-input-number v-model = "scope.row.quantity" :min="0" :precision="2" size="small" style="width:90px" / >
< el-divider content -position = " left " >
< span style = "font-weight:700;color:#1a2c4e" > 报价明细 < / span >
< el-tooltip content = "选择RFQ后可自动加载物料需求" placement = "top" >
< i class = "el-icon-question" style = "margin-left:6px;color:#909399;cursor:pointer" > < / i >
< / el-tooltip >
< / el-divider >
< div v-if = "rfqItemsLoading" style="text-align:center;padding:20px;color:#909399" >
< i class = "el-icon-loading" > < / i > 正在加载RFQ物料 …
< / div >
< el-table v-else :data = "form.items" border siz e ="small" class = "items-table ">
< el-table-column type = "index" width = "44" label = "#" / >
< el-table-column label = "物料名称" min -width = " 140 " >
< template slot -scope = " s " >
< el-input v-model = "s.row.materialName" size="mini" placeholder="物料名称" / >
< / template >
< / el-table-column >
< el-table-column label = "单价 " width = "13 0" >
< template slot -scope = " scope " >
< el-input-number v-model = "scope .row.unitPrice" :min="0" :precision="4" size="small" style="width:120px " / >
< el-table-column label = "规格 " width = "12 0" >
< template slot -scope = " s " >
< el-input v-model = "s.row.spec" size="mini" placeholder="规格 " / >
< / template >
< / el-table-column >
< el-table-column label = "金额 " width = "120" align = "right" >
< template slot -scope = " scope " >
< span style = "color:#409EFF" > { { ( ( scope . row . quantity || 0 ) * ( scope . row . unitPrice || 0 ) ) . toFixed ( 2 ) } } < / span >
< el-table-column label = "型号 " width = "120" >
< template slot -scope = " s " >
< el-input v-model = "s.row.modelNo" size="mini" placeholder="型号" / >
< / template >
< / el-table-column >
< el-table-column label = "操作 " width = "60" align = "center " >
< template slot -scope = " scope " >
< el-button type = "text" icon = "el-icon-delete" @click ="form.items.splice(scope.$index,1)" style = "color:#f56c6c" / >
< el-table-column label = "单位 " width = "72 " >
< template slot -scope = " s " >
< el-input v-model = "s.row.unit" size="mini" / >
< / template >
< / el-table-column >
< el-table-column label = "需求数量" width = "90" align = "right" >
< template slot -scope = " s " >
< span style = "color:#909399;font-size:13px" > { { s . row . quantity } } < / span >
< / template >
< / el-table-column >
< el-table-column label = "报价(元)" width = "120" >
< template slot -scope = " s " >
< el-input-number v-model = "s.row.unitPrice" :min="0" :precision="4"
size = "mini" controls -position = " right " style = "width:100%" @change ="calcItem(s.row)" / >
< / template >
< / el-table-column >
< el-table-column label = "金额(元)" width = "110" align = "right" >
< template slot -scope = " s " >
< strong style = "color:#409EFF" > ¥ { { itemTotal ( s . row ) } } < / strong >
< / template >
< / el-table-column >
< el-table-column label = "交期(天)" width = "85" >
< template slot -scope = " s " >
< el-input-number v-model = "s.row.deliveryDays" :min="0"
size = "mini" controls -position = " right " style = "width:100%" / >
< / template >
< / el-table-column >
< el-table-column label = "备注" min -width = " 100 " >
< template slot -scope = " s " >
< el-input v-model = "s.row.remark" size="mini" placeholder="备注" / >
< / template >
< / el-table-column >
< el-table-column width = "46" align = "center" >
< template slot = "header" >
< el-button type = "text" icon = "el-icon-plus" @click ="addItem" / >
< / template >
< template slot -scope = " s " >
< el-button type = "text" icon = "el-icon-delete" style = "color:#f56c6c"
@click ="form.items.splice(s.$index, 1)" / >
< / template >
< / el-table-column >
< / el-table >
< el-button size = "small" style = "margin-top:8px" @click ="form.items.push({materialName:'',spec:'',unit:'',quantity:1,unitPrice:0})" icon = "el-icon-plus" > 添加行 < / el-button >
< el-form-item label = "备注" style = "margin-top:16px" > < el-input v-model = "form.note" type="textarea" rows="2" / > < / el-form-item >
< div c lass = "form-total-bar" >
合计报价 : < strong > ¥ { { formTotal } } < / strong >
< span style = "margin-left:16px;color:#909399;font-size:12px" > { { form . items . length } } 项物料 < / span >
< / div >
< el-form-item label = "备注" style = "margin-top:12px" >
< el-input v-model = "form.note" type="textarea" :rows="2" placeholder="整体备注说明" / >
< / el-form-item >
< / el-form >
< div slot = "footer" >
< el-button @click ="open= false" > 取消 < / el -button >
< el-button type = "primary " @click ="submitForm" > 保存 < / el -button >
< el-button @click ="dialogOpen = false" > 取消 < / el -button >
< el-button type = "success " @click ="submitForm('draft')" :loading = "saving" > 保存草稿 < / el-button>
< el-button type = "primary" @click ="submitForm('submit')" :loading = "submitting" > 保存并提交 < / el-button >
< / div >
< / el-dialog >
<!-- Detail Dialog -- >
< el-dialog title = "报价单详情" :visible.sync = "detailOpen" width = "82 0px" append -to -body >
<!-- ─ ─ 详情对话框 ─ ─ -- >
< el-dialog title = "报价单详情" :visible.sync = "detailOpen" width = "86 0px" append -to -body >
< div v-if = "detailData" >
< ! - - 状态流程条 - - >
< div class = "detail-steps" >
< div class = "step-item" : class = "{ active: ['draft','submitted','accepted','rejected'].includes(detailData.status) }" >
< i class = "el-icon-edit-outline" > < / i > < span > 草稿 < / span >
< / div >
< div class = "step-line" : class = "{ active: ['submitted','accepted','rejected'].includes(detailData.status) }" > < / div >
< div class = "step-item" : class = "{ active: ['submitted','accepted','rejected'].includes(detailData.status) }" >
< i class = "el-icon-upload2" > < / i > < span > 已提交 < / span >
< / div >
< div class = "step-line" : class = "{ active: ['accepted','rejected'].includes(detailData.status) }" > < / div >
< div class = "step-item" : class = "{ active: detailData.status === 'accepted', rejected: detailData.status === 'rejected' }" >
< i : class = "detailData.status === 'rejected' ? 'el-icon-circle-close' : 'el-icon-circle-check'" > < / i >
< span > { { detailData . status === 'rejected' ? '已拒绝' : '已采纳' } } < / span >
< / div >
< / div >
<!-- PDF 内容区 -- >
< div id = "quote-pdf-area" class = "pdf-area" >
< div class = "pdf-header" >
< img :src = "logoSrc" class = "pdf-logo" / >
< div class = "pdf-header-text" >
< div class = "pdf-company" > 福安德综合报价系统 < / div >
< div class = "pdf-doc-type" > 报价单 < / div >
< div class = "pdf-doc-type" > 供应商 报价单< / div >
< / div >
< div class = "pdf-header-no" > { { detailData . quoteNo } } < / div >
< / div >
@@ -146,15 +291,20 @@
< table class = "pdf-meta-table" >
< tr >
< td class = "meta-label" > 报价单号 < / td > < td class = "meta-val" > { { detailData . quoteNo } } < / td >
< td class = "meta-label" > 供应商 < / td > < td class = "meta-val" > { { detailData . supplierName } } < / td >
< td class = "meta-label" > 供应商 < / td > < td class = "meta-val" > < strong > {{ detailData . supplierName } } < / strong > < / td>
< / tr >
< tr >
< td class = "meta-label" > 关联RFQ < / td > < td class = "meta-val" > { { detailData . rfqNo } } < / td >
< td class = "meta-label" > 状态 < / td > < td class = "meta-val" > < el-tag :type = "statusType(detailData.status)" size = "small" > { { statusLabel ( detailData . status ) } } < / el-tag > < / td >
< td class = "meta-label" > 关联询价 < / td > < td class = "meta-val" > { { detailData . rfqNo } } < / td >
< td class = "meta-label" > 状态 < / td >
< td class = "meta-val" >
< el-tag :type = "statusType(detailData.status)" size = "small" > { { statusLabel ( detailData . status ) } } < / el-tag >
< / td >
< / tr >
< tr >
< td class = "meta-label" > 总金 额 < / td > < td class = "meta-val amount" > ¥ { { detailData . totalAmount } } < / td >
< td class = "meta-label" > 交货期 < / td > < td class = "meta-val " > { { detailData . deliveryDays } } 天 < / td >
< td class = "meta-label" > 报价 总额< / td >
< td class = "meta-val amount " > ¥ { { detailData . totalAmount | money } } < / td >
< td class = "meta-label" > 整体交期 < / td >
< td class = "meta-val" > { { detailData . deliveryDays || '-' } } 天 < / td >
< / tr >
< tr >
< td class = "meta-label" > 有效期 < / td > < td class = "meta-val" > { { detailData . validDays } } 天 < / td >
@@ -167,22 +317,28 @@
< div class = "pdf-section-title" > 报价明细 < / div >
< table class = "pdf-items-table" >
< thead >
< tr > < th > 物料名称 < / th > < th > 规格型号 < / th > < th > 单位 < / th > < th > 数量 < / th > < th > 单价 ( 元 ) < / th > < th > 金额 ( 元 ) < / th > < / tr >
< tr >
< th > # < / th > < th > 物料名称 < / th > < th > 规格 < / th > < th > 型号 < / th >
< th > 单位 < / th > < th > 数量 < / th > < th > 单价 ( 元 ) < / th > < th > 金额 ( 元 ) < / th > < th > 交期 ( 天 ) < / th >
< / tr >
< / thead >
< tbody >
< tr v-for = "(item, i) in detailData.items" :key="i" >
< td > { { item . materialName } } < / td >
< td > { { item . spec } } < / td >
< td > { { i + 1 } } < / td >
< td style = "text-align:left;font-weight:500" > { { item . materialName } } < / td >
< td > { { item . spec || '-' } } < / td >
< td > { { item . modelNo || item . spec || '-' } } < / td >
< td > { { item . unit } } < / td >
< td > { { item . quantity } } < / td >
< td > { { item . unitPrice } } < / td >
< td class = "amount-cell" > { { item . totalPrice || ( ( item . quantity || 0 ) * ( item . unitPrice || 0 ) ) . toFixed ( 2 ) } } < / td >
< td class = "amount-cell" > { { item . totalPrice || itemTotal ( item ) } } < / td >
< td > { { item . deliveryDays || '-' } } < / td >
< / tr >
< / tbody >
< tfoot >
< tr >
< td colspan = "5 " style = "text-align:right;font-weight:bold ;padding:8 px" > 合计金额 < / td >
< td class = "amount-cell total-cell" > ¥ { { detailData . totalAmount } } < / td >
< td colspan = "7 " style = "text-align:right;font-weight:700 ;padding:10 px" > 合计金额 < / td >
< td class = "amount-cell total-cell" colspan = "2" > ¥ { { detailData . totalAmount | money } } < / td >
< / tr >
< / tfoot >
< / table >
@@ -190,7 +346,7 @@
< / div >
< / div >
< div slot = "footer" >
< el-button @click ="detailOpen= false" > 关闭 < / el -button >
< el-button @click ="detailOpen = false" > 关闭 < / el -button >
< el-button type = "primary" icon = "el-icon-download" :loading = "pdfLoading" @click ="exportPdf" > 导出 PDF < / el -button >
< / div >
< / el-dialog >
@@ -198,8 +354,9 @@
< / template >
< script >
import { listQuotation , getQuotation , addQuotation , updateQuotation , submitQuotation , acceptQuotation , rejectQuotation } from "@/api/bid/quotation" ;
import { listRfq } from "@/api/bid/rfq " ;
import { listQuotation , getQuotation , addQuotation , updateQuotation ,
submitQuotation , acceptQuotation , rejectQuotation , delQuotation } from "@/api/bid/quotation " ;
import { listRfq , getRfqItems } from "@/api/bid/rfq" ;
import { listSupplier } from "@/api/bid/supplier" ;
import logoImg from "@/assets/logo/logo.png" ;
import html2canvas from "html2canvas" ;
@@ -207,52 +364,143 @@ import jsPDF from "jspdf";
export default {
name : "Quotation" ,
filters : { numFormat : v => v ? Number ( v ) . toFixed ( 2 ) : "0.00" } ,
filters : { money : v => v ? Number ( v ) . toFixed ( 2 ) : "0.00" } ,
data ( ) {
return {
loading : false , total : 0 , list : [ ] ,
open : false , title : "" ,
detailOpen : false , detailData : null , pdfLoading : false ,
logoSrc : logoImg ,
loading : false , saving : false , submitting : false , pdfLoading : false ,
total : 0 , list : [ ] ,
stats : { total : 0 , draft : 0 , submitted : 0 , accepted : 0 } ,
dialogOpen : false , dialogTitle : "" ,
detailOpen : false , detailData : null ,
rfqItemsLoading : false ,
rfqOptions : [ ] , supplierOptions : [ ] ,
queryParams : { pageNum : 1 , pageSize : 10 , status : null } ,
form : { items : [ ] , currency : "CNY" , validDays : 30 } ,
logoSrc : logoImg ,
queryParams : { pageNum : 1 , pageSize : 10 , status : null , rfqNo : null , supplierName : null } ,
form : { items : [ ] , currency : "CNY" , validDays : 30 , deliveryDays : null } ,
rules : {
rfqId : [ { required : true , message : "请选择RFQ " , trigger : "change" } ] ,
supplierId : [ { required : true , message : "请选择供应商" , trigger : "change" } ]
rfqId : [ { required : true , message : "请选择询价单 " , trigger : "change" } ] ,
supplierId : [ { required : true , message : "请选择供应商" , trigger : "change" } ] ,
}
} ;
} ,
created ( ) { this . getList ( ) ; this . loadOptions ( ) ; } ,
computed : {
formTotal ( ) {
return ( this . form . items || [ ] ) . reduce ( ( s , i ) => s + ( parseFloat ( i . quantity || 0 ) * parseFloat ( i . unitPrice || 0 ) ) , 0 ) . toFixed ( 2 ) ;
}
} ,
created ( ) {
this . getList ( ) ;
listRfq ( { pageSize : 200 } ) . then ( r => { this . rfqOptions = r . rows || [ ] ; } ) ;
listSupplier ( { pageSize : 200 } ) . then ( r => { this . supplierOptions = r . rows || [ ] ; } ) ;
} ,
methods : {
getList ( ) {
this . loading = true ;
listQuotation ( this . queryParams ) . then ( r => { this . list = r . rows ; this . total = r . total ; this . loading = false ; } ) ;
} ,
loadOptions ( ) {
listRfq ( { pageSize : 200 } ) . then ( r => { this . rfqOptions = r . rows || [ ] ; } ) ;
listSupplier ( { pageSize : 200 } ) . then ( r => { this . supplierOptions = r . rows || [ ] ; } ) ;
listQuotation ( this . queryParams ) . then ( r => {
this . list = r . rows || [ ] ;
this . total = r . total || 0 ;
// Compute stats
this . stats . total = this . total ;
this . stats . draft = ( r . rows || [ ] ) . filter ( x => x . status === "draft" ) . length ;
this . stats . submitted = ( r . rows || [ ] ) . filter ( x => x . status === "submitted" ) . length ;
this . stats . accepted = ( r . rows || [ ] ) . filter ( x => x . status === "accepted" ) . length ;
this . loading = false ;
} ) . catch ( ( ) => { this . loading = false ; } ) ;
} ,
handleQuery ( ) { this . queryParams . pageNum = 1 ; this . getList ( ) ; } ,
resetQuery ( ) { this . resetForm ( "queryForm" ) ; this . handleQuery ( ) ; } ,
handleAdd ( ) { this . form = { items : [ ] , currency : "CNY" , validDays : 30 , status : "draft" } ; this . open = true ; this . title = "新增报价" ; } ,
handleUpdate ( row ) { getQuotation ( row . quotationId ) . then ( r => { this . form = r . data ; this . open = true ; this . title = "编辑报价" ; } ) ; } ,
handleView ( row ) { getQuotation ( row . quotationId ) . then ( r => { this . detailData = r . data ; this . detailOpen = true ; } ) ; } ,
handleAdd ( ) {
this . form = { items : [ ] , currency : "CNY" , validDays : 30 , deliveryDays : null , status : "draft" } ;
this . dialogTitle = "新建报价单" ;
this . dialogOpen = true ;
} ,
handleUpdate ( row ) {
getQuotation ( row . quotationId ) . then ( r => {
this . form = { ... r . data , items : r . data . items || [ ] } ;
this . dialogTitle = "编辑报价单" ;
this . dialogOpen = true ;
} ) ;
} ,
handleView ( row ) {
getQuotation ( row . quotationId ) . then ( r => {
this . detailData = r . data ;
this . detailOpen = true ;
} ) ;
} ,
async onRfqChange ( rfqId ) {
if ( ! rfqId ) return ;
this . rfqItemsLoading = true ;
try {
const res = await getRfqItems ( rfqId ) ;
const items = res . data || [ ] ;
this . form . items = items . map ( i => ( {
materialName : i . materialName || "" ,
spec : i . spec || "" ,
modelNo : i . modelNo || "" ,
unit : i . unit || "件" ,
quantity : i . quantity || 1 ,
unitPrice : 0 ,
deliveryDays : null ,
remark : ""
} ) ) ;
if ( this . form . items . length > 0 ) {
this . $message . success ( ` 已加载 ${ this . form . items . length } 项物料需求 ` ) ;
}
} catch ( e ) {
// If items API fails, just leave empty
}
this . rfqItemsLoading = false ;
} ,
addItem ( ) {
this . form . items . push ( { materialName : "" , spec : "" , modelNo : "" , unit : "件" , quantity : 1 , unitPrice : 0 , deliveryDays : null , remark : "" } ) ;
} ,
calcItem ( row ) {
// trigger reactivity
this . $set ( row , "unitPrice" , row . unitPrice ) ;
} ,
itemTotal ( row ) {
return ( ( parseFloat ( row . quantity ) || 0 ) * ( parseFloat ( row . unitPrice ) || 0 ) ) . toFixed ( 2 ) ;
} ,
handleSubmit ( row ) {
this . $modal . confirm ( "确认提交报价?提交后不可修改" ) . then ( ( ) => submitQuotation ( row . quotationId ) )
. then ( ( ) => { this . $modal . msgSuccess ( "提交成功" ) ; this . getList ( ) ; } ) ;
} ,
handleAccept ( row ) { acceptQuotation ( row . quotationId ) . then ( ( ) => { this . $modal . msgSuccess ( "已采纳" ) ; this . getList ( ) ; } ) ; } ,
handleReject ( row ) { rejectQuotation ( row . quotationId ) . then ( ( ) => { this . $modal . msgSuccess ( "已拒绝" ) ; this . getList ( ) ; } ) ; } ,
submitForm ( ) {
this . $refs [ "form" ] . validate ( valid => {
handleAccept ( row ) {
this . $modal . confirm ( "确认采纳此报价?" ) . then ( ( ) => acceptQuotation ( row . quotationId ) )
. then ( ( ) => { this . $modal . msgSuccess ( "已采纳" ) ; this . getList ( ) ; } ) ;
} ,
handleReject ( row ) {
this . $modal . confirm ( "确认拒绝此报价?" ) . then ( ( ) => rejectQuotation ( row . quotationId ) )
. then ( ( ) => { this . $modal . msgSuccess ( "已拒绝" ) ; this . getList ( ) ; } ) ;
} ,
handleDelete ( row ) {
this . $modal . confirm ( "确认删除?" ) . then ( ( ) => delQuotation ( row . quotationId ) )
. then ( ( ) => { this . $modal . msgSuccess ( "删除成功" ) ; this . getList ( ) ; } ) ;
} ,
submitForm ( mode ) {
this . $refs . form . validate ( valid => {
if ( ! valid ) return ;
if ( mode === "submit" ) this . submitting = true ;
else this . saving = true ;
const action = this . form . quotationId ? updateQuotation : addQuotation ;
action ( this . form ) . then ( ( ) => { this . $modal . msgSuccess ( "保存成功" ) ; this . open = false ; this . getList ( ) ; } ) ;
action ( this . form ) . then ( res => {
const id = ( res . data && res . data . quotationId ) || this . form . quotationId ;
if ( mode === "submit" && id ) {
return submitQuotation ( id ) . then ( ( ) => {
this . $modal . msgSuccess ( "提交成功" ) ;
this . dialogOpen = false ;
this . getList ( ) ;
} ) ;
}
this . $modal . msgSuccess ( "保存成功" ) ;
this . dialogOpen = false ;
this . getList ( ) ;
} ) . finally ( ( ) => { this . saving = false ; this . submitting = false ; } ) ;
} ) ;
} ,
async exportPdf ( ) {
this . pdfLoading = true ;
await this . $nextTick ( ) ;
try {
const el = document . getElementById ( "quote-pdf-area" ) ;
const canvas = await html2canvas ( el , { scale : 2 , useCORS : true , backgroundColor : "#ffffff" } ) ;
@@ -261,116 +509,102 @@ export default {
const pageW = pdf . internal . pageSize . getWidth ( ) ;
const pageH = pdf . internal . pageSize . getHeight ( ) ;
const imgH = ( canvas . height * pageW ) / canvas . width ;
let y = 0 ;
let remain = imgH ;
while ( remain > 0 ) {
let y = 0 , rem = imgH , first = true ;
whi le ( rem > 0 ) {
if ( ! first ) pdf . addPage ( ) ;
pdf . addImage ( imgData , "PNG" , 0 , y , pageW , imgH ) ;
remain -= pageH ;
if ( remain > 0 ) { pdf . addPage ( ) ; y -= pageH ; }
y -= pageH ; rem -= pageH ; first = false ;
}
pdf . save ( "报价单_" + ( this . detailData . quoteNo || "export" ) + ".pdf" ) ;
} finally {
this . pdfLoading = false ;
}
pdf . save ( "报价单_" + this . detailData . quoteNo + ".pdf" ) ;
this . $message . success ( "PDF已导出" ) ;
} catch ( e ) { this . $message . error ( "导出失败:" + e . message ) ; }
finally { this . pdfLoading = false ; }
} ,
statusType ( s ) { return { draft : "info" , submitted : "warning" , accepted : "success" , rejected : "danger" } [ s ] || "" ; } ,
statusLabel ( s ) { return { draft : "草稿" , submitted : "已提交" , accepted : "已采纳" , rejected : "已拒绝" } [ s ] || s ; }
statusType ( s ) { return { draft : "info" , submitted : "warning" , accepted : "success" , rejected : "danger" } [ s ] || "" ; } ,
statusLabel ( s ) { return { draft : "草稿" , submitted : "已提交" , accepted : "已采纳" , rejected : "已拒绝" } [ s ] || s ; } ,
statusIcon ( s ) { return { draft : "el-icon-edit-outline" , submitted : "el-icon-time" , accepted : "el-icon-circle-check" , rejected : "el-icon-circle-close" } [ s ] || "el-icon-document" ; }
}
} ;
< / script >
< style scoped >
. pdf - area {
padding : 24 px ;
background : # fff ;
font - family : "Microsoft YaHei" , "Noto Sans SC" , Arial , sans - serif ;
font - size : 13 px ;
color : # 222 ;
< style lang = "scss" scoped>
. quotation - page { padding - bottom : 30 px ; }
/* ── 顶部统计卡片 ── */
. stat - row { margin - bottom : 16 px ; }
. stat - card {
border - radius : 10 px ; padding : 18 px 20 px ; position : relative ;
overflow : hidden ; color : # fff ; cursor : default ;
}
. pdf - header {
display : flex ;
align - items : center ;
padding - bottom : 14 px ;
. stat - num { font - size : 32 px ; font - weight : 700 ; line - height : 1 ; }
. stat - lbl { font - size : 13 px ; margin - top : 6 px ; opacity : 0.9 ; }
. stat - icon { position : absolute ; right : 16 px ; top : 50 % ; transform : translateY ( - 50 % ) ; font - size : 48 px ; opacity : 0.2 ; }
. stat - all { background : linear - gradient ( 135 deg , # 1171 c4 , # 22 a4ff ) ; }
. stat - draft { background : linear - gradient ( 135 deg , # 909399 , # b0b3b8 ) ; }
. stat - submitted { background : linear - gradient ( 135 deg , # e6a23c , # f0c040 ) ; }
. stat - accepted { background : linear - gradient ( 135 deg , # 67 c23a , # 85 ce61 ) ; }
/* ── 搜索 ── */
. search - card { : : v - deep . el - card _ _body { padding : 16 px 20 px 8 px ; } }
/* ── 状态芯片 ── */
. status - chip {
display : inline - flex ; align - items : center ; gap : 4 px ; padding : 3 px 10 px ;
border - radius : 12 px ; font - size : 12 px ; font - weight : 600 ;
i { font - size : 12 px ; }
}
. pdf - logo {
width : 48 px ;
height : 48 px ;
object - fit : contain ;
margin - right : 16 px ;
. status - draft { background : # f4f4f5 ; color : # 909399 ; }
. status - submitted { background : # fdf6ec ; color : # e6a23c ; border : 1 px solid # faecd8 ; }
. status - accepted { background : # f0f9eb ; color : # 67 c23a ; border : 1 px solid # c2e7b0 ; }
. status - rejected { background : # fef0f0 ; color : # f56c6c ; border : 1 px solid # fbc4c4 ; }
/* ── 表单合计 ── */
. items - table { margin - bottom : 0 ; }
. form - total - bar {
text - align : right ; padding : 10 px 16 px ;
background : linear - gradient ( 90 deg , # f9fbff , # f0f7ff ) ;
border : 1 px solid # e4e7ed ; border - top : none ; border - radius : 0 0 4 px 4 px ;
font - size : 14 px ; color : # 606266 ;
strong { font - size : 20 px ; color : # 409 EFF ; margin - left : 6 px ; }
}
/* ── 详情 - 状态流程 ── */
. detail - steps {
display : flex ; align - items : center ; justify - content : center ;
padding : 16 px 0 20 px ; gap : 0 ;
}
. step - item {
display : flex ; flex - direction : column ; align - items : center ; gap : 4 px ;
color : # c0c4cc ; font - size : 12 px ;
i { font - size : 22 px ; }
& . active { color : # 1171 c4 ; }
& . rejected { color : # f56c6c ; }
}
. step - line {
flex : 1 ; max - width : 80 px ; height : 2 px ; background : # e4e7ed ; margin : 0 8 px ; margin - top : - 12 px ;
& . active { background : # 1171 c4 ; }
}
/* ── PDF ── */
. pdf - area { padding : 24 px ; background : # fff ; font - family : "Microsoft YaHei" , "Noto Sans SC" , Arial , sans - serif ; font - size : 13 px ; color : # 222 ; }
. pdf - header { display : flex ; align - items : center ; padding - bottom : 14 px ; }
. pdf - logo { width : 48 px ; height : 48 px ; object - fit : contain ; margin - right : 16 px ; }
. pdf - header - text { flex : 1 ; }
. pdf - company {
font - size : 20 px ;
font - weight : 700 ;
color : # 1171 c4 ;
letter - spacing : 1 px ;
}
. pdf - doc - type {
font - size : 13 px ;
color : # 666 ;
margin - top : 2 px ;
}
. pdf - header - no {
font - size : 13 px ;
color : # 888 ;
}
. pdf - divider {
border - top : 2 px solid # 1171 c4 ;
margin - bottom : 16 px ;
}
. pdf - meta - table {
width : 100 % ;
border - collapse : collapse ;
margin - bottom : 20 px ;
}
. pdf - meta - table td {
padding : 7 px 10 px ;
border : 1 px solid # e4e7ed ;
}
. meta - label {
background : # f5f7fa ;
color : # 606266 ;
font - weight : 600 ;
width : 90 px ;
}
. pdf - company { font - size : 20 px ; font - weight : 700 ; color : # 1171 c4 ; letter - spacing : 1 px ; }
. pdf - doc - type { font - size : 13 px ; color : # 666 ; margin - top : 2 px ; }
. pdf - header - no { font - size : 13 px ; color : # 888 ; }
. pdf - divider { border - top : 2 px solid # 1171 c4 ; margin - bottom : 16 px ; }
. pdf - meta - table { width : 100 % ; border - collapse : collapse ; margin - bottom : 20 px ; td { padding : 7 px 10 px ; border : 1 px solid # e4e7ed ; } }
. meta - label { background : # f5f7fa ; color : # 606266 ; font - weight : 600 ; width : 90 px ; }
. meta - val { color : # 303133 ; }
. amount {
color : # 409 EFF ;
font - weight : 7 00;
font - size : 15 px ;
. amount { color : # 409 EFF ; font - weight : 700 ; font - size : 15 px ; }
. pdf - section - title { font - size : 14 px ; font - weight : 700 ; color : # 1 a2c4e ; margin : 0 0 10 px ; padding - left : 8 px ; border - left : 4 px solid # 1171 c4 ; }
. pdf - items - table { width : 1 00% ; border - collapse : collapse ; margin - bottom : 20 px ;
th { background : # 1171 c4 ; color : # fff ; padding : 8 px ; text - align : center ; font - weight : 600 ; font- size : 12 px ; }
td { border : 1 px solid # e4e7ed ; padding : 7 px 8 px ; text - align : center ; font - size : 12 px ; }
tbody tr : nth - child ( even ) td { background : # f9fbff ; }
}
. pdf - section - title {
font - size : 14 px ;
font - weight : 700 ;
color : # 1 a2c4e ;
margin : 0 0 10 px ;
padding - left : 8 px ;
border - left : 4 px solid # 1171 c4 ;
}
. pdf - items - table {
width : 100 % ;
border - collapse : collapse ;
margin - bottom : 20 px ;
}
. pdf - items - table th {
background : # 1171 c4 ;
color : # fff ;
padding : 8 px 10 px ;
text - align : center ;
font - weight : 600 ;
}
. pdf - items - table td {
border : 1 px solid # e4e7ed ;
padding : 7 px 10 px ;
text - align : center ;
}
. pdf - items - table tbody tr : nth - child ( even ) td { background : # f9fbff ; }
. amount - cell { color : # 409 EFF ; font - weight : 600 ; }
. total - cell { font - size : 15 px ; background : # f0f7ff ! important ; }
. pdf - footer {
text - align : right ;
font - size : 11 px ;
color : # aaa ;
margin - top : 10 px ;
}
. total - cell { font - size : 15 px ; background : # f0f7ff ! important ; font - weight : 700 ; }
. pdf - footer { text - align : right ; font - size : 11 px ; color : # aaa ; margin - top : 10 px ; border - top : 1 px solid # f0f2f5 ; padding - top : 8 px ; }
< / style >