feat: multi-dim comparison + clustering + per-supplier PDF export
Backend: - BizComparisonVO: add composite score fields (priceScore, deliveryScore, qualityScore, serviceScore, compositeScore, rankBadge) + SupplierPlan cluster VO - Mapper: join biz_supplier_evaluation for quality/service history scores - Service: weighted scoring (price 40%/delivery 25%/quality 20%/service 15%), greedy clustering assigns each item to best-score supplier, groups into plans - Controller: returns CompareResult with items + recommendedPlans Frontend: - Tab 1 (多维度比价): supplier rank cards with 4-dim progress bars - Tab 2 (智能推荐方案): per-supplier cluster cards with explanation + PDF export - PDF: logo header, score legend, items table, cluster reason per supplier
This commit is contained in:
@@ -1,77 +1,495 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="filter-bar">
|
||||
<div class="app-container comparison-page">
|
||||
<!-- ── 顶部选择栏 ── -->
|
||||
<el-card class="filter-card" shadow="never">
|
||||
<el-form :inline="true" size="small">
|
||||
<el-form-item label="选择RFQ">
|
||||
<el-select v-model="selectedRfqId" placeholder="请选择要比价的RFQ" filterable style="width:320px">
|
||||
<el-option v-for="r in rfqOptions" :key="r.rfqId" :label="r.rfqNo+' — '+r.rfqTitle" :value="r.rfqId" />
|
||||
<el-form-item label="选择询价单">
|
||||
<el-select v-model="selectedRfqId" placeholder="请选择要比价的RFQ" filterable style="width:360px" @change="resetResult">
|
||||
<el-option v-for="r in rfqOptions" :key="r.rfqId" :label="r.rfqNo + ' — ' + r.rfqTitle" :value="r.rfqId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-data-analysis" @click="doCompare" :loading="loading">开始比价</el-button>
|
||||
<el-button type="primary" icon="el-icon-data-analysis" @click="doCompare" :loading="loading">开始智慧比价</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- ── 空状态 ── -->
|
||||
<div v-if="!result && !loading" class="empty-state">
|
||||
<i class="el-icon-data-analysis"></i>
|
||||
<p>选择询价单,点击「开始智慧比价」查看多维度分析与推荐方案</p>
|
||||
</div>
|
||||
|
||||
<div v-if="result.length === 0 && !loading" class="empty-tip">
|
||||
<i class="el-icon-data-analysis" style="font-size:48px;color:#ddd"></i>
|
||||
<p>请选择RFQ并点击"开始比价"</p>
|
||||
</div>
|
||||
<div v-if="result">
|
||||
<el-tabs v-model="activeTab" type="border-card" class="main-tabs">
|
||||
|
||||
<div v-for="(item, idx) in result" :key="item.rfqItemId" style="margin-bottom:24px">
|
||||
<div class="compare-header">
|
||||
<span class="item-no">物料 {{ idx+1 }}</span>
|
||||
<strong>{{ item.materialName }}</strong>
|
||||
<span v-if="item.spec" style="color:#909399;margin-left:8px">{{ item.spec }}</span>
|
||||
<span style="margin-left:16px;color:#606266">数量: {{ item.quantity }} {{ item.unit }}</span>
|
||||
</div>
|
||||
<el-table :data="item.prices || []" border size="small">
|
||||
<el-table-column label="供应商" prop="supplierName" min-width="140">
|
||||
<template slot-scope="scope">
|
||||
<i class="el-icon-star-on" style="color:#f7ba2a;margin-right:4px" v-if="scope.row.lowestPrice"></i>
|
||||
{{ scope.row.supplierName }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报价单号" prop="quoteNo" width="140" />
|
||||
<el-table-column label="单价" prop="unitPrice" width="120" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span :style="scope.row.lowestPrice ? 'color:#67C23A;font-weight:bold' : ''">
|
||||
¥{{ scope.row.unitPrice }}
|
||||
</span>
|
||||
<el-tag v-if="scope.row.lowestPrice" type="success" size="mini" style="margin-left:4px">最低</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="总价" prop="totalPrice" width="130" align="right">
|
||||
<template slot-scope="scope">¥{{ scope.row.totalPrice }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="交货期(天)" prop="deliveryDays" width="100" align="center" />
|
||||
</el-table>
|
||||
<div v-if="!item.prices || item.prices.length === 0" style="padding:12px;color:#909399;background:#fafafa;border:1px solid #eee">暂无供应商报价</div>
|
||||
<!-- ══ Tab 1: 维度比价 ══ -->
|
||||
<el-tab-pane label="📊 多维度比价" name="compare">
|
||||
<div v-for="(item, idx) in result.items" :key="item.rfqItemId" class="item-block">
|
||||
<div class="item-header">
|
||||
<span class="item-badge">物料 {{ idx + 1 }}</span>
|
||||
<strong class="item-name">{{ item.materialName }}</strong>
|
||||
<span class="item-spec" v-if="item.spec">{{ item.spec }}</span>
|
||||
<span class="item-qty">需求数量:{{ item.quantity }} {{ item.unit }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!item.prices || !item.prices.length" class="no-quote">暂无供应商报价</div>
|
||||
<div v-else>
|
||||
<!-- 综合排名卡片 -->
|
||||
<div class="supplier-cards">
|
||||
<div v-for="(sp, si) in item.prices" :key="sp.supplierId"
|
||||
class="supplier-card" :class="{ 'best-card': si === 0 }">
|
||||
<div class="card-rank">
|
||||
<span class="rank-num" :class="'rank-' + (si+1)">{{ si + 1 }}</span>
|
||||
<el-tag v-if="sp.rankBadge" size="mini" :type="badgeType(sp.rankBadge)" class="rank-badge">{{ sp.rankBadge }}</el-tag>
|
||||
</div>
|
||||
<div class="card-supplier">{{ sp.supplierName }}</div>
|
||||
<div class="card-score-big" :class="scoreClass(sp.compositeScore)">
|
||||
{{ sp.compositeScore.toFixed(1) }}
|
||||
<span class="score-unit">分</span>
|
||||
</div>
|
||||
<div class="card-price">¥{{ sp.unitPrice }} / {{ item.unit }}</div>
|
||||
|
||||
<!-- 四维进度条 -->
|
||||
<div class="dim-bars">
|
||||
<div class="dim-row">
|
||||
<span class="dim-label">价格</span>
|
||||
<el-progress :percentage="sp.priceScore" :stroke-width="6" :color="dimColor(sp.priceScore)" :show-text="false" class="dim-bar" />
|
||||
<span class="dim-val">{{ sp.priceScore.toFixed(0) }}</span>
|
||||
</div>
|
||||
<div class="dim-row">
|
||||
<span class="dim-label">交期</span>
|
||||
<el-progress :percentage="sp.deliveryScore" :stroke-width="6" :color="dimColor(sp.deliveryScore)" :show-text="false" class="dim-bar" />
|
||||
<span class="dim-val">{{ sp.deliveryScore.toFixed(0) }}</span>
|
||||
</div>
|
||||
<div class="dim-row">
|
||||
<span class="dim-label">质量</span>
|
||||
<el-progress :percentage="sp.qualityScore" :stroke-width="6" :color="dimColor(sp.qualityScore)" :show-text="false" class="dim-bar" />
|
||||
<span class="dim-val">{{ sp.qualityScore.toFixed(0) }}</span>
|
||||
</div>
|
||||
<div class="dim-row">
|
||||
<span class="dim-label">服务</span>
|
||||
<el-progress :percentage="sp.serviceScore" :stroke-width="6" :color="dimColor(sp.serviceScore)" :show-text="false" class="dim-bar" />
|
||||
<span class="dim-val">{{ sp.serviceScore.toFixed(0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-meta">
|
||||
交货 {{ sp.deliveryDays }} 天
|
||||
<span v-if="sp.historyCount > 0" style="margin-left:8px">· 历史 {{ sp.historyCount }} 次</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 明细对比表 -->
|
||||
<el-table :data="item.prices" border size="small" class="detail-table">
|
||||
<el-table-column label="供应商" prop="supplierName" min-width="140" />
|
||||
<el-table-column label="报价单号" prop="quoteNo" width="130" />
|
||||
<el-table-column label="单价(元)" prop="unitPrice" width="110" align="right">
|
||||
<template slot-scope="s">
|
||||
<span :class="s.row.lowestPrice ? 'price-low' : ''">¥{{ s.row.unitPrice }}</span>
|
||||
<el-tag v-if="s.row.lowestPrice" type="success" size="mini" style="margin-left:4px">最低</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="总价(元)" width="120" align="right">
|
||||
<template slot-scope="s">¥{{ s.row.totalPrice }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="交期(天)" prop="deliveryDays" width="85" align="center" />
|
||||
<el-table-column label="价格分" width="80" align="center">
|
||||
<template slot-scope="s"><span :class="scoreClass(s.row.priceScore)">{{ s.row.priceScore }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="交期分" width="80" align="center">
|
||||
<template slot-scope="s"><span :class="scoreClass(s.row.deliveryScore)">{{ s.row.deliveryScore }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="质量分" width="80" align="center">
|
||||
<template slot-scope="s"><span :class="scoreClass(s.row.qualityScore)">{{ s.row.qualityScore }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="服务分" width="80" align="center">
|
||||
<template slot-scope="s"><span :class="scoreClass(s.row.serviceScore)">{{ s.row.serviceScore }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="综合分" width="90" align="center">
|
||||
<template slot-scope="s">
|
||||
<strong :class="scoreClass(s.row.compositeScore)">{{ s.row.compositeScore }}</strong>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ══ Tab 2: 推荐采购方案 ══ -->
|
||||
<el-tab-pane name="plan">
|
||||
<span slot="label">🎯 智能推荐方案 <el-badge :value="result.recommendedPlans.length" type="primary" /></span>
|
||||
|
||||
<el-alert type="info" :closable="false" show-icon style="margin-bottom:20px">
|
||||
<template slot="title">
|
||||
算法说明:综合评分 = 价格(40%) + 交期(25%) + 历史质量(20%) + 历史服务(15%)
|
||||
,对每个物料选出综合分最高的供应商,相同供应商的物料合并为一份采购方案。
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<div v-if="!result.recommendedPlans.length" class="no-quote" style="margin:40px auto">
|
||||
暂无可用报价,无法生成推荐方案
|
||||
</div>
|
||||
|
||||
<div v-for="(plan, pi) in result.recommendedPlans" :key="plan.supplierId" class="plan-card">
|
||||
<div class="plan-header">
|
||||
<div class="plan-title">
|
||||
<span class="plan-rank">方案 {{ pi + 1 }}</span>
|
||||
<strong class="plan-supplier">{{ plan.supplierName }}</strong>
|
||||
<el-tag type="success" size="small" style="margin-left:12px">
|
||||
综合评分 {{ plan.avgCompositeScore.toFixed(1) }} 分
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="plan-actions">
|
||||
<span class="plan-total">合计:<strong>¥{{ plan.totalAmount.toFixed ? plan.totalAmount.toFixed(2) : plan.totalAmount }}</strong></span>
|
||||
<el-button type="primary" size="small" icon="el-icon-download"
|
||||
:loading="pdfLoading[plan.supplierId]" @click="exportPlanPdf(plan, pi)">
|
||||
导出 PDF
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="plan-reason">
|
||||
<i class="el-icon-info" style="color:#409EFF;margin-right:4px"></i>{{ plan.clusterReason }}
|
||||
</div>
|
||||
|
||||
<!-- 方案物料表 -->
|
||||
<el-table :data="plan.items" border size="small" class="plan-table">
|
||||
<el-table-column type="index" width="46" label="#" />
|
||||
<el-table-column label="物料名称" prop="materialName" min-width="140" />
|
||||
<el-table-column label="规格" prop="spec" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="单位" prop="unit" width="70" align="center" />
|
||||
<el-table-column label="数量" prop="quantity" width="80" align="right" />
|
||||
<el-table-column label="单价(元)" prop="unitPrice" width="110" align="right">
|
||||
<template slot-scope="s">¥{{ s.row.unitPrice }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额(元)" width="120" align="right">
|
||||
<template slot-scope="s">
|
||||
<strong style="color:#409EFF">¥{{ s.row.totalPrice }}</strong>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="交期(天)" prop="deliveryDays" width="85" align="center" />
|
||||
<el-table-column label="综合分" width="80" align="center">
|
||||
<template slot-scope="s">
|
||||
<strong :class="scoreClass(s.row.compositeScore)">{{ s.row.compositeScore }}</strong>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="推荐标签" width="90" align="center">
|
||||
<template slot-scope="s">
|
||||
<el-tag v-if="s.row.rankBadge" size="mini" :type="badgeType(s.row.rankBadge)">{{ s.row.rankBadge }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 隐藏 PDF 渲染区 -->
|
||||
<div :id="'plan-pdf-' + plan.supplierId" class="pdf-area" style="position:absolute;left:-9999px;top:0;width:794px">
|
||||
<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>
|
||||
<div class="pdf-header-meta">
|
||||
<div>方案编号:方案{{ pi + 1 }}</div>
|
||||
<div>生成日期:{{ today }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pdf-divider"></div>
|
||||
|
||||
<table class="pdf-meta-table">
|
||||
<tr>
|
||||
<td class="meta-label">询价单</td>
|
||||
<td class="meta-val" colspan="3">{{ result.rfqNo }} — {{ result.rfqTitle }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="meta-label">推荐供应商</td><td class="meta-val">{{ plan.supplierName }}</td>
|
||||
<td class="meta-label">综合评分</td><td class="meta-val score-good">{{ plan.avgCompositeScore.toFixed(1) }} / 100</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="meta-label">合计金额</td>
|
||||
<td class="meta-val amount-big" colspan="3">¥{{ plan.totalAmount.toFixed ? plan.totalAmount.toFixed(2) : plan.totalAmount }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="meta-label">推荐依据</td>
|
||||
<td class="meta-val" colspan="3">{{ plan.clusterReason }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="pdf-score-legend">
|
||||
<strong>评分权重说明:</strong>
|
||||
价格占比 40% | 交期占比 25% | 历史质量占比 20% | 服务水平占比 15%
|
||||
</div>
|
||||
|
||||
<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><th>金额(元)</th><th>交货期(天)</th><th>综合评分</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, ii) in plan.items" :key="ii">
|
||||
<td>{{ ii + 1 }}</td>
|
||||
<td>{{ item.materialName }}</td>
|
||||
<td>{{ item.spec || '-' }}</td>
|
||||
<td>{{ item.unit }}</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
<td>{{ item.unitPrice }}</td>
|
||||
<td class="amount-cell">{{ item.totalPrice }}</td>
|
||||
<td>{{ item.deliveryDays }}</td>
|
||||
<td :class="'score-cell ' + scorePdfClass(item.compositeScore)">{{ item.compositeScore }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="6" style="text-align:right;font-weight:bold;padding:10px 8px">合计</td>
|
||||
<td class="amount-cell total-cell" colspan="3">¥{{ plan.totalAmount.toFixed ? plan.totalAmount.toFixed(2) : plan.totalAmount }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<div class="pdf-footer">本方案由福安德智慧报价系统自动生成,仅供参考 · 生成时间:{{ new Date().toLocaleString('zh-CN') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { compareRfq } from "@/api/bid/comparison";
|
||||
import { listRfq } from "@/api/bid/rfq";
|
||||
import logoImg from "@/assets/logo/logo.png";
|
||||
import html2canvas from "html2canvas";
|
||||
import jsPDF from "jspdf";
|
||||
|
||||
export default {
|
||||
name: "Comparison",
|
||||
data() {
|
||||
return { selectedRfqId: null, loading: false, result: [], rfqOptions: [] };
|
||||
return {
|
||||
selectedRfqId: null,
|
||||
loading: false,
|
||||
result: null,
|
||||
activeTab: "compare",
|
||||
rfqOptions: [],
|
||||
logoSrc: logoImg,
|
||||
pdfLoading: {},
|
||||
today: new Date().toLocaleDateString("zh-CN")
|
||||
};
|
||||
},
|
||||
created() {
|
||||
listRfq({ pageSize: 200, status: "published" }).then(r => { this.rfqOptions = r.rows || []; });
|
||||
listRfq({ pageSize: 200 }).then(r => { this.rfqOptions = r.rows || []; });
|
||||
},
|
||||
methods: {
|
||||
resetResult() { this.result = null; this.activeTab = "compare"; },
|
||||
doCompare() {
|
||||
if (!this.selectedRfqId) { this.$message.warning("请先选择RFQ"); return; }
|
||||
this.loading = true;
|
||||
compareRfq(this.selectedRfqId).then(r => { this.result = r.data || []; this.loading = false; }).catch(() => { this.loading = false; });
|
||||
compareRfq(this.selectedRfqId).then(r => {
|
||||
this.result = r.data;
|
||||
this.loading = false;
|
||||
this.activeTab = "compare";
|
||||
}).catch(() => { this.loading = false; });
|
||||
},
|
||||
async exportPlanPdf(plan, idx) {
|
||||
this.$set(this.pdfLoading, plan.supplierId, true);
|
||||
// wait a tick so DOM updates
|
||||
await this.$nextTick();
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
try {
|
||||
const el = document.getElementById("plan-pdf-" + plan.supplierId);
|
||||
// temporarily show it
|
||||
const origStyle = el.style.cssText;
|
||||
el.style.cssText = "position:absolute;left:-9999px;top:0;width:794px;background:#fff";
|
||||
const canvas = await html2canvas(el, { scale: 2, useCORS: true, backgroundColor: "#ffffff", width: 794 });
|
||||
el.style.cssText = origStyle;
|
||||
|
||||
const imgData = canvas.toDataURL("image/png");
|
||||
const pdf = new jsPDF({ orientation: "p", unit: "mm", format: "a4" });
|
||||
const pageW = pdf.internal.pageSize.getWidth();
|
||||
const pageH = pdf.internal.pageSize.getHeight();
|
||||
const imgH = (canvas.height * pageW) / canvas.width;
|
||||
|
||||
let yPos = 0;
|
||||
let remaining = imgH;
|
||||
let firstPage = true;
|
||||
while (remaining > 0) {
|
||||
if (!firstPage) pdf.addPage();
|
||||
pdf.addImage(imgData, "PNG", 0, yPos, pageW, imgH);
|
||||
yPos -= pageH;
|
||||
remaining -= pageH;
|
||||
firstPage = false;
|
||||
}
|
||||
const filename = "推荐方案_" + plan.supplierName + "_方案" + (idx + 1) + ".pdf";
|
||||
pdf.save(filename);
|
||||
this.$message.success("PDF 已导出:" + filename);
|
||||
} catch(e) {
|
||||
this.$message.error("PDF 导出失败:" + e.message);
|
||||
} finally {
|
||||
this.$set(this.pdfLoading, plan.supplierId, false);
|
||||
}
|
||||
},
|
||||
badgeType(badge) {
|
||||
if (badge === "综合最优") return "success";
|
||||
if (badge === "价格最低") return "warning";
|
||||
if (badge === "交期最快") return "primary";
|
||||
return "info";
|
||||
},
|
||||
scoreClass(score) {
|
||||
if (score >= 80) return "score-high";
|
||||
if (score >= 60) return "score-mid";
|
||||
return "score-low";
|
||||
},
|
||||
scorePdfClass(score) {
|
||||
if (score >= 80) return "score-good";
|
||||
if (score >= 60) return "score-ok";
|
||||
return "score-weak";
|
||||
},
|
||||
dimColor(v) {
|
||||
if (v >= 80) return "#67c23a";
|
||||
if (v >= 60) return "#e6a23c";
|
||||
return "#f56c6c";
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.compare-header { background:#f5f7fa; padding:10px 16px; border-left:4px solid #409EFF; margin-bottom:8px; border-radius:2px; }
|
||||
.item-no { background:#409EFF; color:#fff; padding:2px 8px; border-radius:10px; font-size:12px; margin-right:8px; }
|
||||
.empty-tip { text-align:center; padding:80px; color:#909399; }
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.comparison-page { padding: 0; }
|
||||
.filter-card { margin-bottom: 20px; ::v-deep .el-card__body { padding: 16px 20px; } }
|
||||
|
||||
.empty-state {
|
||||
text-align: center; padding: 100px 40px; color: #c0c4cc;
|
||||
i { font-size: 56px; display: block; margin-bottom: 16px; }
|
||||
p { font-size: 14px; }
|
||||
}
|
||||
|
||||
.main-tabs { ::v-deep .el-tabs__content { padding: 20px; } }
|
||||
|
||||
/* ── 物料块 ── */
|
||||
.item-block { margin-bottom: 32px; }
|
||||
.item-header {
|
||||
background: linear-gradient(90deg, #1171c4, #22a4ff);
|
||||
color: #fff; padding: 10px 16px; border-radius: 6px 6px 0 0;
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
}
|
||||
.item-badge {
|
||||
background: rgba(255,255,255,0.25); padding: 2px 10px;
|
||||
border-radius: 12px; font-size: 12px;
|
||||
}
|
||||
.item-name { font-size: 15px; font-weight: 700; }
|
||||
.item-spec { color: rgba(255,255,255,0.8); font-size: 13px; }
|
||||
.item-qty { margin-left: auto; font-size: 12px; color: rgba(255,255,255,0.8); }
|
||||
|
||||
.no-quote {
|
||||
padding: 20px; color: #909399; background: #fafafa;
|
||||
border: 1px dashed #e4e7ed; border-radius: 4px; text-align: center;
|
||||
}
|
||||
|
||||
/* ── 供应商排名卡片 ── */
|
||||
.supplier-cards {
|
||||
display: flex; gap: 12px; padding: 16px 0; overflow-x: auto;
|
||||
}
|
||||
.supplier-card {
|
||||
flex: 0 0 200px; border: 1px solid #e4e7ed; border-radius: 8px;
|
||||
padding: 14px; background: #fff; transition: box-shadow 0.2s;
|
||||
&:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.1); }
|
||||
&.best-card { border-color: #67c23a; box-shadow: 0 2px 8px rgba(103,194,58,0.2); }
|
||||
}
|
||||
.card-rank { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
|
||||
.rank-num {
|
||||
width: 22px; height: 22px; border-radius: 50%; display: flex;
|
||||
align-items: center; justify-content: center; font-size: 12px; font-weight: 700;
|
||||
&.rank-1 { background: #f7ba2a; color: #fff; }
|
||||
&.rank-2 { background: #c0c4cc; color: #fff; }
|
||||
&.rank-3 { background: #a57c52; color: #fff; }
|
||||
}
|
||||
.rank-badge { flex-shrink: 0; }
|
||||
.card-supplier { font-size: 13px; font-weight: 600; color: #303133; margin-bottom: 6px; }
|
||||
.card-score-big {
|
||||
font-size: 28px; font-weight: 700; line-height: 1; margin: 8px 0;
|
||||
&.score-high { color: #67c23a; }
|
||||
&.score-mid { color: #e6a23c; }
|
||||
&.score-low { color: #f56c6c; }
|
||||
.score-unit { font-size: 13px; font-weight: 400; margin-left: 2px; }
|
||||
}
|
||||
.card-price { font-size: 13px; color: #409EFF; font-weight: 600; margin-bottom: 10px; }
|
||||
.dim-bars { display: flex; flex-direction: column; gap: 4px; }
|
||||
.dim-row { display: flex; align-items: center; gap: 6px; }
|
||||
.dim-label { font-size: 11px; color: #909399; width: 24px; flex-shrink: 0; }
|
||||
.dim-bar { flex: 1; }
|
||||
.dim-val { font-size: 11px; color: #606266; width: 24px; text-align: right; flex-shrink: 0; }
|
||||
.card-meta { font-size: 11px; color: #c0c4cc; margin-top: 8px; }
|
||||
.detail-table { margin-top: 8px; }
|
||||
|
||||
/* ── 分数颜色 ── */
|
||||
.score-high { color: #67c23a; font-weight: 700; }
|
||||
.score-mid { color: #e6a23c; font-weight: 600; }
|
||||
.score-low { color: #f56c6c; }
|
||||
.price-low { color: #67c23a; font-weight: 700; }
|
||||
|
||||
/* ── 推荐方案卡片 ── */
|
||||
.plan-card {
|
||||
border: 1px solid #e4e7ed; border-radius: 8px; margin-bottom: 24px;
|
||||
overflow: hidden; background: #fff; position: relative;
|
||||
}
|
||||
.plan-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 14px 20px; background: linear-gradient(90deg, #f0f7ff, #fff);
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
.plan-title { display: flex; align-items: center; gap: 10px; }
|
||||
.plan-rank {
|
||||
background: #1171c4; color: #fff; padding: 3px 10px;
|
||||
border-radius: 12px; font-size: 12px;
|
||||
}
|
||||
.plan-supplier { font-size: 16px; font-weight: 700; color: #1a2c4e; }
|
||||
.plan-actions { display: flex; align-items: center; gap: 16px; }
|
||||
.plan-total { font-size: 14px; color: #606266; strong { font-size: 18px; color: #409EFF; } }
|
||||
.plan-reason { padding: 8px 20px; font-size: 13px; color: #606266; background: #f9fbff; border-bottom: 1px solid #f0f2f5; }
|
||||
.plan-table { border-radius: 0; }
|
||||
|
||||
/* ── PDF 渲染区域 ── */
|
||||
.pdf-area {
|
||||
padding: 28px;
|
||||
background: #fff;
|
||||
font-family: "Microsoft YaHei", "Noto Sans SC", Arial, sans-serif;
|
||||
font-size: 13px; color: #222;
|
||||
}
|
||||
.pdf-header { display: flex; align-items: flex-start; padding-bottom: 12px; }
|
||||
.pdf-logo { width: 52px; height: 52px; object-fit: contain; margin-right: 14px; }
|
||||
.pdf-header-text { flex: 1; }
|
||||
.pdf-company { font-size: 22px; font-weight: 700; color: #1171c4; letter-spacing: 1px; }
|
||||
.pdf-doc-type { font-size: 13px; color: #666; margin-top: 2px; }
|
||||
.pdf-header-meta { font-size: 12px; color: #888; text-align: right; }
|
||||
.pdf-divider { border-top: 2px solid #1171c4; margin: 0 0 16px; }
|
||||
.pdf-meta-table {
|
||||
width: 100%; border-collapse: collapse; margin-bottom: 16px;
|
||||
td { padding: 7px 10px; border: 1px solid #e4e7ed; }
|
||||
}
|
||||
.meta-label { background: #f5f7fa; color: #606266; font-weight: 600; width: 90px; }
|
||||
.meta-val { color: #303133; }
|
||||
.amount-big { color: #409EFF; font-weight: 700; font-size: 16px; }
|
||||
.score-good { color: #67c23a; font-weight: 700; }
|
||||
.score-ok { color: #e6a23c; font-weight: 600; }
|
||||
.score-weak { color: #f56c6c; }
|
||||
.pdf-score-legend {
|
||||
background: #f0f7ff; border: 1px solid #c6e2ff; border-radius: 4px;
|
||||
padding: 8px 14px; font-size: 12px; color: #606266; margin-bottom: 16px;
|
||||
}
|
||||
.pdf-section-title {
|
||||
font-size: 14px; font-weight: 700; color: #1a2c4e;
|
||||
margin: 0 0 10px; padding-left: 8px; border-left: 4px solid #1171c4;
|
||||
}
|
||||
.pdf-items-table {
|
||||
width: 100%; border-collapse: collapse; margin-bottom: 20px;
|
||||
th { background: #1171c4; color: #fff; padding: 8px 8px; text-align: center; font-weight: 600; font-size: 12px; }
|
||||
td { border: 1px solid #e4e7ed; padding: 7px 8px; text-align: center; font-size: 12px; }
|
||||
tbody tr:nth-child(even) td { background: #f9fbff; }
|
||||
}
|
||||
.amount-cell { color: #409EFF; font-weight: 600; }
|
||||
.total-cell { font-size: 14px; background: #f0f7ff !important; font-weight: 700; }
|
||||
.score-cell { font-weight: 700; }
|
||||
.pdf-footer { text-align: center; font-size: 11px; color: #aaa; margin-top: 16px; border-top: 1px solid #f0f2f5; padding-top: 12px; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user