3.1 供货商管理页面 - 移除了右侧面板的"供货清单"Tab - 报价历史板块新增搜索功能(物料名称/报价单号/状态/日期范围) - 后端 Mapper 改造支持动态 SQL 过滤 3.2 报价请求与供应商报价关联 - 新增"供应商报价汇总"弹窗,展示 RFQ 下所有供应商的报价对比 - 报价单号改为可点击链接,跳转到供应商报价列表并按单号搜索 3.3 智慧比价详情页 - 修复了比价详情页路由(在 router/index.js 中补充) - 移除了评分维度展示(价格/交期/质量/服务评分条、综合分标签) - 精简为纯粹的供应商价格对比视图 3.4 其他修复 - 首页快捷操作路径修正(/bid/xxx → /xxx) - 停用 bid 目录后受影响的 router.push 路径全部修复 - biz_tenant 表缺失修复(创建建表 SQL 并执行) - 比价详情页路由注册补充 - goCompare 跳转路径修正
695 lines
30 KiB
Vue
695 lines
30 KiB
Vue
<template>
|
||
<div class="app-container clientquote-page">
|
||
<!-- ── 顶部统计卡片 ── -->
|
||
<el-row :gutter="14" class="stat-row">
|
||
<el-col :span="6">
|
||
<div class="stat-card stat-all">
|
||
<div class="stat-num">{{ stats.total_count || 0 }}</div>
|
||
<div class="stat-lbl">报价单总数</div>
|
||
<i class="el-icon-document-copy stat-icon"></i>
|
||
</div>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<div class="stat-card stat-client">
|
||
<div class="stat-num">{{ stats.client_count || 0 }}</div>
|
||
<div class="stat-lbl">客户数量</div>
|
||
<i class="el-icon-user stat-icon"></i>
|
||
</div>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<div class="stat-card stat-amount">
|
||
<div class="stat-num">¥{{ (stats.total_amount_sum || 0) | money }}</div>
|
||
<div class="stat-lbl">报价总金额</div>
|
||
<i class="el-icon-money stat-icon"></i>
|
||
</div>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<div class="stat-card stat-avg">
|
||
<div class="stat-num">¥{{ (stats.avg_amount || 0) | money }}</div>
|
||
<div class="stat-lbl">平均金额</div>
|
||
<i class="el-icon-s-data stat-icon"></i>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<!-- ── 搜索栏 ── -->
|
||
<el-card shadow="never" class="search-card">
|
||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true">
|
||
<el-form-item label="客户名称">
|
||
<el-input v-model="queryParams.clientName" placeholder="客户名称" clearable style="width:150px" @keyup.enter.native="handleQuery" />
|
||
</el-form-item>
|
||
<el-form-item label="报价单号">
|
||
<el-input v-model="queryParams.quoteNo" 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="sent" />
|
||
<el-option label="已确认" value="confirmed" />
|
||
<el-option label="已拒绝" value="rejected" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="创建日期">
|
||
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始"
|
||
end-placeholder="结束" value-format="yyyy-MM-dd" style="width:220px" clearable />
|
||
</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-col :span="1.5">
|
||
<el-button icon="el-icon-refresh" size="mini" @click="getList">刷新</el-button>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<!-- ── 报价单列表 ── -->
|
||
<el-table v-loading="loading" :data="list" border stripe highlight-current-row>
|
||
<el-table-column label="报价单号" prop="quoteNo" width="170" fixed>
|
||
<template slot-scope="s">
|
||
<span style="font-weight:600;color:#303133;cursor:pointer" @click="handleView(s.row)">{{ s.row.quoteNo }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="客户名称" prop="clientName" min-width="140" show-overflow-tooltip />
|
||
<el-table-column label="总金额" width="130" 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="currency" width="70" align="center" />
|
||
<el-table-column label="有效期" width="110" align="center">
|
||
<template slot-scope="s">{{ s.row.validityDate | dateFmt }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="90" 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="createBy" width="100" align="center" />
|
||
<el-table-column label="创建时间" prop="createTime" width="160" align="center" />
|
||
<el-table-column label="操作" align="center" width="280" 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)">编辑</el-button>
|
||
<el-button size="mini" type="text" icon="el-icon-document-copy" @click="handleQuickCreate(s.row)">快速新建</el-button>
|
||
<el-button size="mini" type="text" icon="el-icon-s-promotion" style="color:#67C23A" @click="handleCreateRfq(s.row)">生成RFQ</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" />
|
||
|
||
<!-- ── 创建/编辑对话框 ── -->
|
||
<el-dialog :title="dialogTitle" :visible.sync="dialogOpen" width="95%" append-to-body :close-on-click-modal="false" class="cq-edit-dialog">
|
||
<el-form ref="form" :model="form" :rules="rules" label-width="90px" size="small">
|
||
<el-row :gutter="16">
|
||
<el-col :span="12">
|
||
<el-form-item label="客户名称" prop="clientName">
|
||
<el-input v-model="form.clientName" placeholder="请输入客户/甲方名称" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label=" ">
|
||
<span style="color:#909399;font-size:12px">RFQ 通过「生成RFQ」按钮创建,自动关联此报价单</span>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="16">
|
||
<el-col :span="8">
|
||
<el-form-item label="有效期至">
|
||
<el-date-picker v-model="form.validityDate" type="date" value-format="yyyy-MM-dd HH:mm:ss"
|
||
placeholder="选择有效期" style="width:100%" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<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="欧元 EUR" value="EUR" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="状态">
|
||
<el-select v-model="form.status" style="width:100%">
|
||
<el-option label="草稿" value="draft" />
|
||
<el-option label="已发送" value="sent" />
|
||
<el-option label="已确认" value="confirmed" />
|
||
<el-option label="已拒绝" value="rejected" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-divider content-position="left">
|
||
<span style="font-weight:700;color:#1a2c4e">报价明细</span>
|
||
<el-button type="text" icon="el-icon-plus" @click="addItem" style="margin-left:12px">添加行</el-button>
|
||
</el-divider>
|
||
|
||
<div class="items-table-wrap">
|
||
<el-table :data="form.items" border size="small" class="items-table">
|
||
<el-table-column type="index" width="40" label="#" />
|
||
<el-table-column label="物料名称" width="170">
|
||
<template slot-scope="s">
|
||
<el-autocomplete
|
||
v-model="s.row.materialName"
|
||
:fetch-suggestions="queryMaterialSearch"
|
||
placeholder="搜索选择物料"
|
||
size="mini"
|
||
style="width:100%"
|
||
popper-class="material-popper"
|
||
:popper-append-to-body="true"
|
||
@select="(item) => onMaterialSelect(s.row, item)"
|
||
>
|
||
<template slot-scope="{ item }">
|
||
<div class="material-suggestion">
|
||
<div class="ms-top">
|
||
<span class="ms-name">{{ item.materialName }}</span>
|
||
<span class="ms-code" v-if="item.materialCode">{{ item.materialCode }}</span>
|
||
</div>
|
||
<div class="ms-detail">
|
||
<span v-if="item.spec" class="ms-tag">规格:{{ item.spec }}</span>
|
||
<span v-if="item.brand" class="ms-tag ms-brand-tag">品牌:{{ item.brand }}</span>
|
||
<span v-if="item.unit" class="ms-tag">单位:{{ item.unit }}</span>
|
||
<span v-if="item.categoryName" class="ms-tag">分类:{{ item.categoryName }}</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-autocomplete>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="规格型号" width="130">
|
||
<template slot-scope="s">
|
||
<el-input v-model="s.row.spec" size="mini" placeholder="规格型号" @change="s.row.modelNo = s.row.spec" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="单位" width="50">
|
||
<template slot-scope="s">
|
||
<el-input v-model="s.row.unit" size="mini" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="数量" width="70">
|
||
<template slot-scope="s">
|
||
<el-input v-model="s.row.quantity" size="mini" placeholder="0" @input="calcRow(s.row)" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="成本价" width="80">
|
||
<template slot-scope="s">
|
||
<el-input v-model="s.row.costPrice" size="mini" placeholder="0.00" @input="calcRow(s.row)" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="报价" width="80">
|
||
<template slot-scope="s">
|
||
<el-input v-model="s.row.unitPrice" size="mini" placeholder="0.00" @input="calcRow(s.row)" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="金额" width="75" align="right">
|
||
<template slot-scope="s">
|
||
<strong style="color:#409EFF">¥{{ itemTotal(s.row) }}</strong>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="毛利率" width="60" align="center">
|
||
<template slot-scope="s">
|
||
<span :style="{ color: marginColor(s.row) }">{{ calcMargin(s.row) }}%</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="交期" width="95">
|
||
<template slot-scope="s">
|
||
<el-input v-model="s.row.deliveryDays" size="mini" placeholder="0" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="55" align="center">
|
||
<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>
|
||
</div>
|
||
|
||
<div class="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.remark" type="textarea" :rows="2" placeholder="备注说明" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<div slot="footer">
|
||
<el-button @click="dialogOpen = false">取消</el-button>
|
||
<el-button type="primary" @click="submitForm" :loading="saving">保存</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<!-- ── 详情对话框 ── -->
|
||
<el-dialog title="报价单详情" :visible.sync="detailOpen" width="860px" append-to-body top="5vh">
|
||
<div v-if="detailData">
|
||
<!-- 状态流程条 -->
|
||
<div class="detail-steps">
|
||
<div class="step-item" :class="{ active: ['draft','sent','confirmed','rejected'].includes(detailData.status) }">
|
||
<i class="el-icon-edit-outline"></i><span>草稿</span>
|
||
</div>
|
||
<div class="step-line" :class="{ active: ['sent','confirmed','rejected'].includes(detailData.status) }"></div>
|
||
<div class="step-item" :class="{ active: ['sent','confirmed','rejected'].includes(detailData.status) }">
|
||
<i class="el-icon-upload2"></i><span>已发送</span>
|
||
</div>
|
||
<div class="step-line" :class="{ active: ['confirmed','rejected'].includes(detailData.status) }"></div>
|
||
<div class="step-item" :class="{ active: detailData.status === 'confirmed', 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>
|
||
|
||
<!-- 基本信息 -->
|
||
<el-descriptions :column="3" border size="small" style="margin-bottom:16px">
|
||
<el-descriptions-item label="报价单号">{{ detailData.quoteNo }}</el-descriptions-item>
|
||
<el-descriptions-item label="客户名称">{{ detailData.clientName }}</el-descriptions-item>
|
||
<el-descriptions-item label="状态">
|
||
<el-tag :type="statusType(detailData.status)" size="mini" effect="dark">{{ statusLabel(detailData.status) }}</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="币种">{{ detailData.currency || 'CNY' }}</el-descriptions-item>
|
||
<el-descriptions-item label="有效期">{{ detailData.validityDate | dateFmt }}</el-descriptions-item>
|
||
<el-descriptions-item label="RFQ数量">
|
||
<span v-if="detailRfqList.length > 0" style="color:#409eff;font-weight:600">{{ detailRfqList.length }} 个</span>
|
||
<span v-else style="color:#c0c4cc">-</span>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="总金额" :span="3">
|
||
<strong style="color:#409EFF;font-size:18px">¥{{ detailData.totalAmount | money }}</strong>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="备注" :span="3">{{ detailData.remark || '-' }}</el-descriptions-item>
|
||
</el-descriptions>
|
||
|
||
<div class="section-title">报价明细</div>
|
||
<el-table :data="detailData.items || []" border size="small" style="margin-top:12px">
|
||
<el-table-column type="index" width="46" label="#" />
|
||
<el-table-column label="物料名称" prop="materialName" min-width="150" />
|
||
<el-table-column label="规格型号" prop="spec" width="130" />
|
||
<el-table-column label="单位" prop="unit" width="55" align="center" />
|
||
<el-table-column label="数量" prop="quantity" width="70" align="right" />
|
||
<el-table-column label="成本价" width="90" align="right">
|
||
<template slot-scope="s">¥{{ s.row.costPrice | money }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="单价" width="90" align="right">
|
||
<template slot-scope="s">¥{{ s.row.unitPrice | money }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="金额" width="90" align="right">
|
||
<template slot-scope="s"><strong style="color:#409EFF">¥{{ s.row.totalPrice | money }}</strong></template>
|
||
</el-table-column>
|
||
<el-table-column label="交期(天)" prop="deliveryDays" width="65" align="center" />
|
||
</el-table>
|
||
|
||
<!-- ── 关联的RFQ列表 ── -->
|
||
<div style="margin-top:20px">
|
||
<div class="section-title" style="margin-bottom:12px">
|
||
已生成的采购计划(RFQ)
|
||
<el-button size="mini" type="success" icon="el-icon-s-promotion" style="margin-left:12px"
|
||
@click="handleCreateRfq(detailData)">生成RFQ</el-button>
|
||
</div>
|
||
<el-table :data="detailRfqList" v-loading="detailRfqLoading" border size="small">
|
||
<el-table-column label="RFQ编号" prop="rfqNo" width="150" />
|
||
<el-table-column label="标题" prop="rfqTitle" min-width="160" show-overflow-tooltip />
|
||
<el-table-column label="状态" width="90" align="center">
|
||
<template slot-scope="s">
|
||
<el-tag :type="rfqStatusType(s.row.status)" size="small">{{ rfqStatusLabel(s.row.status) }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="创建时间" prop="createTime" width="160" align="center" />
|
||
<el-table-column label="操作" width="80" align="center">
|
||
<template slot-scope="s">
|
||
<el-button type="text" size="small" @click="viewRfqDetail(s.row)">查看</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div v-if="!detailRfqLoading && detailRfqList.length === 0" style="text-align:center;padding:16px;color:#c0c4cc;font-size:13px">
|
||
暂未生成采购计划,点击上方「生成RFQ」按钮创建
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div slot="footer">
|
||
<el-button @click="detailOpen = false">关闭</el-button>
|
||
<el-button type="primary" icon="el-icon-edit" @click="editFromDetail" v-if="detailData">编辑</el-button>
|
||
<el-button type="success" icon="el-icon-document-copy" @click="quickCreateFromDetail" v-if="detailData">快速新建</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { listClientQuote, getClientQuote, addClientQuote, updateClientQuote, delClientQuote,
|
||
getClientQuoteStatistics, quickCreateFromQuote } from "@/api/bid/clientquote";
|
||
import { listRfq, createRfqFromQuote } from "@/api/bid/rfq";
|
||
import { listMaterial } from "@/api/bid/material";
|
||
|
||
export default {
|
||
name: "ClientQuote",
|
||
filters: {
|
||
money: v => v ? Number(v).toFixed(2) : "0.00",
|
||
dateFmt: v => v ? v.substring(0, 10) : "-"
|
||
},
|
||
data() {
|
||
return {
|
||
// 列表
|
||
loading: false,
|
||
list: [],
|
||
total: 0,
|
||
stats: {},
|
||
dateRange: null,
|
||
queryParams: {
|
||
pageNum: 1, pageSize: 10,
|
||
clientName: null, quoteNo: null, status: null,
|
||
params: { beginTime: null, endTime: null }
|
||
},
|
||
// 创建/编辑
|
||
dialogOpen: false,
|
||
dialogTitle: "",
|
||
saving: false,
|
||
materialCache: [],
|
||
form: { items: [], currency: "CNY", status: "draft" },
|
||
rules: {
|
||
clientName: [{ required: true, message: "请输入客户名称", trigger: "blur" }]
|
||
},
|
||
// 详情
|
||
detailOpen: false,
|
||
detailData: null,
|
||
detailRfqList: [],
|
||
detailRfqLoading: false
|
||
};
|
||
},
|
||
computed: {
|
||
formTotal() {
|
||
return (this.form.items || []).reduce((s, i) => s + (parseFloat(i.quantity || 0) * parseFloat(i.unitPrice || 0)), 0).toFixed(2);
|
||
}
|
||
},
|
||
created() {
|
||
this.getList();
|
||
this.getStats();
|
||
},
|
||
methods: {
|
||
// ===== 列表 =====
|
||
getList() {
|
||
this.loading = true;
|
||
if (this.dateRange && this.dateRange.length === 2) {
|
||
this.queryParams.params.beginTime = this.dateRange[0] + ' 00:00:00';
|
||
this.queryParams.params.endTime = this.dateRange[1] + ' 23:59:59';
|
||
} else {
|
||
this.queryParams.params.beginTime = null;
|
||
this.queryParams.params.endTime = null;
|
||
}
|
||
listClientQuote(this.queryParams).then(r => {
|
||
this.list = r.rows || [];
|
||
this.total = r.total || 0;
|
||
this.loading = false;
|
||
}).catch(() => { this.loading = false; });
|
||
},
|
||
getStats() {
|
||
getClientQuoteStatistics(this.queryParams).then(r => {
|
||
this.stats = r.data || {};
|
||
});
|
||
},
|
||
handleQuery() { this.queryParams.pageNum = 1; this.getList(); this.getStats(); },
|
||
resetQuery() {
|
||
this.resetForm("queryForm");
|
||
this.dateRange = null;
|
||
this.queryParams.params = { beginTime: null, endTime: null };
|
||
this.handleQuery();
|
||
},
|
||
|
||
// ===== 新建 =====
|
||
handleAdd() {
|
||
// 跳转到 detail.vue 页面进行新增
|
||
this.$router.push('/bid/clientquote/detail');
|
||
},
|
||
|
||
// ===== 编辑 =====
|
||
handleUpdate(row) {
|
||
// 跳转到 detail.vue 页面进行编辑,传递 quoteId
|
||
this.$router.push({ path: '/bid/clientquote/detail', query: { quoteId: row.quoteId } });
|
||
},
|
||
|
||
// ===== 保存 =====
|
||
submitForm() {
|
||
this.$refs.form.validate(valid => {
|
||
if (!valid) return;
|
||
this.saving = true;
|
||
const action = this.form.quoteId ? updateClientQuote : addClientQuote;
|
||
action(this.form).then(() => {
|
||
this.$modal.msgSuccess("保存成功");
|
||
this.dialogOpen = false;
|
||
this.getList();
|
||
this.getStats();
|
||
}).finally(() => { this.saving = false; });
|
||
});
|
||
},
|
||
|
||
// ===== 详情查看 =====
|
||
handleView(row) {
|
||
getClientQuote(row.quoteId).then(r => {
|
||
this.detailData = r.data || {};
|
||
if (!this.detailData.items) this.detailData.items = [];
|
||
this.detailOpen = true;
|
||
this.loadRfqForDetail(row.quoteId);
|
||
});
|
||
},
|
||
editFromDetail() {
|
||
this.detailOpen = false;
|
||
if (this.detailData) {
|
||
this.handleUpdate(this.detailData);
|
||
}
|
||
},
|
||
quickCreateFromDetail() {
|
||
this.detailOpen = false;
|
||
if (this.detailData) this.handleQuickCreate(this.detailData);
|
||
},
|
||
|
||
// ===== 快速新建 =====
|
||
handleQuickCreate(row) {
|
||
this.$modal.confirm("确认基于报价单【" + row.quoteNo + "】快速新建?").then(() => {
|
||
return quickCreateFromQuote(row.quoteId);
|
||
}).then(res => {
|
||
this.$modal.msgSuccess("已创建新报价单草稿");
|
||
this.getList();
|
||
this.getStats();
|
||
// 跳转到 detail.vue 编辑新创建的报价单
|
||
if (res.data && res.data.quoteId) {
|
||
this.$router.push({ path: '/bid/clientquote/detail', query: { quoteId: res.data.quoteId } });
|
||
}
|
||
}).catch(() => {});
|
||
},
|
||
|
||
// ===== 生成RFQ =====
|
||
handleCreateRfq(row) {
|
||
this.$modal.confirm("确认基于报价单【" + row.quoteNo + "】生成采购询价(RFQ)?").then(() => {
|
||
return createRfqFromQuote(row.quoteId);
|
||
}).then(res => {
|
||
this.detailOpen = false;
|
||
this.$modal.msgSuccess("RFQ已创建");
|
||
this.$router.push({ path: '/bid/rfq/detail', query: { rfqId: res.data.rfqId, rfqNo: res.data.rfqNo, edit: '1' } });
|
||
}).catch(() => {});
|
||
},
|
||
|
||
// ===== 删除 =====
|
||
handleDelete(row) {
|
||
this.$modal.confirm("确认删除报价单【" + row.quoteNo + "】?").then(() => delClientQuote(row.quoteId))
|
||
.then(() => { this.$modal.msgSuccess("删除成功"); this.getList(); this.getStats(); });
|
||
},
|
||
|
||
// ===== RFQ列表(详情弹窗中展示) =====
|
||
loadRfqForDetail(quoteId) {
|
||
this.detailRfqLoading = true;
|
||
listRfq({ clientQuoteId: quoteId, pageSize: 50 }).then(r => {
|
||
this.detailRfqList = r.rows || [];
|
||
this.detailRfqLoading = false;
|
||
}).catch(() => { this.detailRfqLoading = false; });
|
||
},
|
||
viewRfqDetail(rfq) {
|
||
this.detailOpen = false;
|
||
this.$router.push({ path: '/bid/rfq/detail', query: { rfqId: rfq.rfqId } });
|
||
},
|
||
rfqStatusType(s) { return { draft:"info", published:"warning", closed:"", completed:"success" }[s] || ""; },
|
||
rfqStatusLabel(s) { return { draft:"草稿", published:"已发布", closed:"已关闭", completed:"已完成" }[s] || s; },
|
||
|
||
// ===== 物料搜索 =====
|
||
queryMaterialSearch(query, cb) {
|
||
if (!query || query.length < 1) {
|
||
cb(this.materialCache.slice(0, 20)); return;
|
||
}
|
||
listMaterial({ materialName: query, pageSize: 20 }).then(res => {
|
||
const list = res.rows || [];
|
||
this.materialCache = list.map(m => ({
|
||
...m, value: m.materialName + (m.spec ? ' (' + m.spec + ')' : '')
|
||
}));
|
||
cb(this.materialCache.slice(0, 20));
|
||
}).catch(() => cb([]));
|
||
},
|
||
onMaterialSelect(row, item) {
|
||
if (!item) return;
|
||
row.materialId = item.materialId;
|
||
row.materialName = item.materialName;
|
||
row.spec = item.spec || '';
|
||
row.modelNo = item.spec || '';
|
||
row.unit = item.unit || '件';
|
||
},
|
||
|
||
// ===== 明细行 =====
|
||
addItem() {
|
||
this.form.items.push({ materialId: null, materialName: "", spec: "", modelNo: "", unit: "件",
|
||
quantity: 1, costPrice: 0, unitPrice: 0, totalPrice: "0.00", deliveryDays: null });
|
||
},
|
||
calcRow(row) {
|
||
const q = parseFloat(row.quantity) || 0;
|
||
const p = parseFloat(row.unitPrice) || 0;
|
||
row.totalPrice = (q * p).toFixed(2);
|
||
},
|
||
itemTotal(row) {
|
||
return ((parseFloat(row.quantity) || 0) * (parseFloat(row.unitPrice) || 0)).toFixed(2);
|
||
},
|
||
calcMargin(row) {
|
||
const cost = parseFloat(row.costPrice) || 0;
|
||
const price = parseFloat(row.unitPrice) || 0;
|
||
if (!price) return "0.0";
|
||
return (((price - cost) / price) * 100).toFixed(1);
|
||
},
|
||
marginColor(row) {
|
||
const m = parseFloat(this.calcMargin(row));
|
||
if (m >= 20) return "#67c23a";
|
||
if (m >= 10) return "#e6a23c";
|
||
return "#f56c6c";
|
||
},
|
||
|
||
// ===== 状态辅助 =====
|
||
statusType(s) { return { draft: "info", sent: "primary", confirmed: "success", rejected: "danger" }[s] || ""; },
|
||
statusLabel(s) { return { draft: "草稿", sent: "已发送", confirmed: "已确认", rejected: "已拒绝" }[s] || s; },
|
||
statusIcon(s) { return { draft: "el-icon-edit-outline", sent: "el-icon-upload2", confirmed: "el-icon-circle-check", rejected: "el-icon-circle-close" }[s] || "el-icon-document"; }
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.clientquote-page { padding-bottom: 30px; }
|
||
|
||
/* ── 统计卡片 ── */
|
||
.stat-row { margin-bottom: 16px; }
|
||
.stat-card {
|
||
border-radius: 10px; padding: 18px 20px; position: relative;
|
||
overflow: hidden; color: #fff; cursor: default;
|
||
}
|
||
.stat-num { font-size: 26px; font-weight: 700; line-height: 1; }
|
||
.stat-lbl { font-size: 13px; margin-top: 6px; opacity: 0.9; }
|
||
.stat-icon { position: absolute; right: 16px; top: 50%; transform: translateY(-50%); font-size: 48px; opacity: 0.2; }
|
||
.stat-all { background: linear-gradient(135deg, #1171c4, #22a4ff); }
|
||
.stat-client { background: linear-gradient(135deg, #67c23a, #85ce61); }
|
||
.stat-amount { background: linear-gradient(135deg, #e6a23c, #f0c040); }
|
||
.stat-avg { background: linear-gradient(135deg, #909399, #b0b3b8); }
|
||
|
||
/* ── 搜索 ── */
|
||
.search-card { ::v-deep .el-card__body { padding: 16px 20px 8px; } }
|
||
|
||
/* ── 状态芯片 ── */
|
||
.status-chip {
|
||
display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px;
|
||
border-radius: 12px; font-size: 12px; font-weight: 600;
|
||
i { font-size: 12px; }
|
||
}
|
||
.status-draft { background: #f4f4f5; color: #909399; }
|
||
.status-sent { background: #e6f1ff; color: #409eff; border: 1px solid #b3d8ff; }
|
||
.status-confirmed { background: #f0f9eb; color: #67c23a; border: 1px solid #c2e7b0; }
|
||
.status-rejected { background: #fef0f0; color: #f56c6c; border: 1px solid #fbc4c4; }
|
||
|
||
/* ── 表单合计 ── */
|
||
.items-table { margin-bottom: 0; }
|
||
.form-total-bar {
|
||
text-align: right; padding: 10px 16px;
|
||
background: linear-gradient(90deg, #f9fbff, #f0f7ff);
|
||
border: 1px solid #e4e7ed; border-top: none; border-radius: 0 0 4px 4px;
|
||
font-size: 14px; color: #606266;
|
||
strong { font-size: 20px; color: #409eff; margin-left: 6px; }
|
||
}
|
||
|
||
/* ── 详情 - 状态流程 ── */
|
||
.detail-steps {
|
||
display: flex; align-items: center; justify-content: center;
|
||
padding: 16px 0 20px; gap: 0;
|
||
}
|
||
.step-item {
|
||
display: flex; flex-direction: column; align-items: center; gap: 4px;
|
||
color: #c0c4cc; font-size: 12px;
|
||
i { font-size: 22px; }
|
||
&.active { color: #1171c4; }
|
||
&.rejected { color: #f56c6c; }
|
||
}
|
||
.step-line {
|
||
flex: 1; max-width: 80px; height: 2px; background: #e4e7ed; margin: 0 8px; margin-top: -12px;
|
||
&.active { background: #1171c4; }
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 14px; font-weight: 700; color: #1a2c4e;
|
||
padding-left: 8px; border-left: 4px solid #1171c4;
|
||
}
|
||
|
||
/* ── 物料搜索下拉建议 ── */
|
||
.material-suggestion {
|
||
display: flex; flex-direction: column; padding: 4px 0; line-height: 1.5;
|
||
}
|
||
.ms-top { display: flex; align-items: center; gap: 8px; }
|
||
.ms-name { font-size: 13px; font-weight: 600; color: #303133; }
|
||
.ms-code { font-size: 11px; color: #909399; }
|
||
.ms-detail { display: flex; flex-wrap: wrap; gap: 4px 8px; margin-top: 3px; }
|
||
.ms-tag {
|
||
display: inline-block; font-size: 11px; color: #606266;
|
||
background: #f5f7fa; padding: 0 6px; border-radius: 3px; line-height: 1.8;
|
||
}
|
||
.ms-brand-tag { color: #409EFF; background: #ecf5ff; }
|
||
</style>
|
||
|
||
<!-- ── 全局样式:修复 autocomplete 下拉框被遮挡 ── -->
|
||
<style lang="scss">
|
||
.material-popper {
|
||
z-index: 9999 !important;
|
||
}
|
||
.material-popper .el-autocomplete-suggestion {
|
||
width: 420px !important;
|
||
}
|
||
.material-popper .el-autocomplete-suggestion li {
|
||
padding: 6px 12px !important;
|
||
border-bottom: 1px solid #f0f2f5;
|
||
}
|
||
.material-popper .el-autocomplete-suggestion li:last-child {
|
||
border-bottom: none;
|
||
}
|
||
.material-popper .el-autocomplete-suggestion li:hover {
|
||
background: #f0f7ff !important;
|
||
}
|
||
|
||
/* 编辑对话框中的表格容器,防止表格溢出 */
|
||
.cq-edit-dialog .el-dialog__body {
|
||
padding: 16px 20px;
|
||
max-height: 70vh;
|
||
overflow-y: auto;
|
||
}
|
||
.items-table-wrap {
|
||
overflow-x: auto;
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 4px;
|
||
}
|
||
.items-table-wrap .el-table {
|
||
border: none !important;
|
||
}
|
||
.items-table-wrap .el-table::before {
|
||
display: none;
|
||
}
|
||
</style>
|