Files
erp-next/ruoyi-ui/src/views/bid/clientquote/index.vue
王文昊 c97fdf4c6f feat: 页面功能完善
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 跳转路径修正
2026-06-06 15:20:46 +08:00

695 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>