Files
erp-next/doc/智慧报价系统-统计报表模块开发方案.md
王文昊 ba74618bea feat(bid): 新增投标报表统计分析模块
本次提交新增了完整的投标报表统计分析功能,包括:

添加用于数据检查与菜单初始化的 SQL 脚本

实现采购概览仪表板、采购成本分析及供应商绩效报告的后端服务、Mapper、Controller 及 VO 类

添加前端 API、路由配置以及使用 ECharts 可视化图表的页面组件

为仪表板添加通用的 KPI 卡片组件
2026-06-03 14:26:25 +08:00

39 KiB
Raw Blame History

智慧报价系统 — 统计报表模块开发方案

基于 RuoYi-Vue v3.9.2 (Spring Boot 4.0.3 + JDK 17 + Vue 2 + Element UI + ECharts 5.4.0) 对接现有"智慧报价"模块 12 张业务表 & 真实生产数据


目录


一、设计理念

1.1 目标

老板想看的不是原始数据的堆砌,而是一眼能抓住的数据洞察。因此本报表模块的核心原则:

原则 说明
KPI 优先 每个页面顶部展示关键指标卡片(总计/环比/趋势),让老板一眼看到业务状况
图文并茂 ECharts 图表为主,表格为辅,避免纯表格枯燥
层层下钻 从汇总趋势 → 点击进入明细 → 再进详情,支持交互式探索
聚焦决策 报表要能回答老板的问题:花了多少钱?省了多少钱?哪家供应商最好?

1.2 技术选型理由

技术 为什么选
ECharts 5.4.0 项目已有依赖(package.jsonecharts: 5.4.0),无需额外安装
Vue 2 + Element UI 项目现有技术栈,风格统一
MyBatis 手写 SQL 报表查询需要多表聚合,手写 SQL 最灵活
RuoYi BaseController 延续 startPage() + getDataTable() 分页模式,与现有代码一致

二、报表总览

2.1 报表菜单结构

智慧报价菜单下新增一个分组 统计分析,包含 4 个子页面:

智慧报价 (menu_id=2000)
├── 物料管理          (2001)
├── 供应商管理        (2002)
├── 报价请求          (2003)
├── 供应商报价        (2004)
├── 智慧比价          (2005)
├── 采购单            (2006)
├── 供应商评价        (2007)
├── 订单异议          (2008)
├── 交易记录          (2009)
├── 租户管理          (2010)
├── ═══════════════════════════════
├── 📊 统计分析        (2011)  ← 新增目录菜单(父节点)
│   ├── 📈 采购总览看板   (2012)  ← 仪表盘首页
│   ├── 💰 采购成本分析   (2013)  ← 成本趋势与对比
│   └── 🏆 供应商绩效     (2014)  ← 供应商评分与排名

2.2 四个报表页面说明

页面 路由 path 定位 老板能回答什么问题
采购总览看板 /bid/report/dashboard 仪表盘首页,一屏总览 本月花了多少钱?采购趋势怎么样?报价情况如何?
采购成本分析 /bid/report/cost 成本深度分析 比预算省了多少?各品类花了多少?同物料价格对比?
供应商绩效 /bid/report/supplier 供应商评分与排名 哪家供应商最好?哪家经常出问题?交期/质量/服务排名?

三、页面架构与路由设计

3.1 前端路由注册

ruoyi-ui/src/router/index.jsdynamicRoutes 中添加:

// ── 统计分析 路由 ──
{
  path: '/bid/report/dashboard',
  component: Layout,
  hidden: true,
  permissions: ['bid:report:dashboard'],
  children: [{
    path: '',
    component: () => import('@/views/bid/report/dashboard'),
    name: 'ReportDashboard',
    meta: { title: '采购总览看板', activeMenu: '/bid/report' }
  }]
},
{
  path: '/bid/report/cost',
  component: Layout,
  hidden: true,
  permissions: ['bid:report:cost'],
  children: [{
    path: '',
    component: () => import('@/views/bid/report/cost'),
    name: 'ReportCost',
    meta: { title: '采购成本分析', activeMenu: '/bid/report' }
  }]
},
{
  path: '/bid/report/supplier',
  component: Layout,
  hidden: true,
  permissions: ['bid:report:supplier'],
  children: [{
    path: '',
    component: () => import('@/views/bid/report/supplier'),
    name: 'ReportSupplier',
    meta: { title: '供应商绩效', activeMenu: '/bid/report' }
  }]
}

3.2 菜单 SQL

bid_tables.sql 或系统菜单表中执行以下 SQL先查询最大 menu_id 再插入):

-- 父菜单:统计分析 (2011)
INSERT IGNORE INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES(2011, '统计分析', 2000, 11, 'report', NULL, 1, 0, 'M', '0', '0', 'bid:report:list', 'chart', 'admin', NOW());

-- 子菜单:采购总览看板 (2012)
INSERT IGNORE INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES(2012, '采购总览看板', 2011, 1, 'dashboard', 'bid/report/dashboard', 1, 0, 'C', '0', '0', 'bid:report:dashboard', 'dashboard', 'admin', NOW());

-- 子菜单:采购成本分析 (2013)
INSERT IGNORE INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES(2013, '采购成本分析', 2011, 2, 'cost', 'bid/report/cost', 1, 0, 'C', '0', '0', 'bid:report:cost', 'money', 'admin', NOW());

-- 子菜单:供应商绩效 (2014)
INSERT IGNORE INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES(2014, '供应商绩效', 2011, 3, 'supplier', 'bid/report/supplier', 1, 0, 'C', '0', '0', 'bid:report:supplier', 'star', 'admin', NOW());

四、后端详细设计

4.1 包结构

ruoyi-admin/src/main/java/com/ruoyi/web/controller/bid/
  └── BizReportController.java              ← 报表统计 Controller

ruoyi-system/src/main/java/com/ruoyi/system/
  ├── domain/bid/
  │   ├── ReportDashboardVO.java            ← 看板数据 VO
  │   ├── ReportCostVO.java                 ← 成本分析 VO
  │   └── ReportSupplierVO.java             ← 供应商绩效 VO
  ├── mapper/bid/
  │   └── BizReportMapper.java              ← 报表 Mapper 接口
  ├── service/bid/
  │   ├── IBizReportService.java            ← 报表 Service 接口
  │   └── impl/
  │       └── BizReportServiceImpl.java     ← 报表 Service 实现

ruoyi-system/src/main/resources/mapper/bid/
  └── BizReportMapper.xml                   ← 报表 SQL 映射

4.2 Domain / VO 设计

ReportDashboardVO.java — 看板数据

public class ReportDashboardVO {
    // ====== 顶部 KPI 卡片 ======
    private KpiCard totalPurchaseAmount;    // 采购总额(含环比)
    private KpiCard totalRfqCount;          // RFQ总数
    private KpiCard totalPoCount;           // 采购单数
    private KpiCard activeSupplierCount;    // 活跃供应商数

    // ====== 图表数据 ======
    private List<MonthTrend> monthlyTrend;          // 月度采购趋势
    private List<StatusDist> rfqStatusDist;         // RFQ状态分布
    private List<SupplierRank> topSuppliers;        // Top供应商排名
    private List<RecentActivity> recentActivities;  // 最近动态

    // ====== 内部类 ======
    @Data
    public static class KpiCard {
        private String label;       // 指标名
        private BigDecimal value;   // 当前值
        private double changeRate;  // 环比(%)
        private String unit;        // 单位
        private String trend;       // up/down
    }

    @Data
    public static class MonthTrend {
        private String month;       // "2026-01"
        private BigDecimal amount;  // 金额
        private int count;          // 单数
    }

    @Data
    public static class StatusDist {
        private String status;
        private String statusLabel;
        private int count;
        private double percent;
    }

    @Data
    public static class SupplierRank {
        private Long supplierId;
        private String supplierName;
        private BigDecimal totalAmount;     // 采购金额
        private int poCount;                // 采购次数
        private double avgScore;            // 评价均分
    }

    @Data
    public static class RecentActivity {
        private String time;
        private String type;       // PO / QUOTE / EVAL / OBJECTION
        private String desc;
        private String linkId;
    }
}

ReportCostVO.java — 成本分析

public class ReportCostVO {
    // ====== 顶部汇总 ======
    private CostSummary summary;

    @Data
    public static class CostSummary {
        private BigDecimal totalExpected;   // 预算总额RFQ期望价
        private BigDecimal totalActual;     // 实际采购总额
        private BigDecimal savedAmount;     // 节省金额
        private double savedRate;           // 节省比例
    }

    // ====== 月度成本趋势 ======
    private List<CostTrend> costTrend;

    @Data
    public static class CostTrend {
        private String month;
        private BigDecimal expectedAmount;
        private BigDecimal actualAmount;
        private BigDecimal savedAmount;
    }

    // ====== 品类采购分布 ======
    private List<CategoryDist> categoryDist;

    @Data
    public static class CategoryDist {
        private String categoryName;
        private BigDecimal amount;
        private int materialCount;
        private double percent;
    }

    // ====== 单次RFQ比价详情 ======
    private List<RfqCompareDetail> rfqDetails;

    @Data
    public static class RfqCompareDetail {
        private Long rfqId;
        private String rfqNo;
        private String rfqTitle;
        private BigDecimal expectedTotal;
        private BigDecimal lowestQuote;
        private BigDecimal acceptedQuote;
        private BigDecimal savedAmount;
        private int supplierCount;
    }
}

ReportSupplierVO.java — 供应商绩效

public class ReportSupplierVO {
    // ====== 评分排名 ======
    private List<SupplierScore> rankings;

    @Data
    public static class SupplierScore {
        private Long supplierId;
        private String supplierName;
        private int evalCount;            // 评价次数
        private double qualityAvg;        // 质量均分
        private double deliveryAvg;       // 交期均分
        private double serviceAvg;        // 服务均分
        private double priceAvg;          // 价格均分
        private double totalAvg;          // 综合均分
        private int poCount;              // 采购次数
        private BigDecimal poAmount;      // 采购金额
        private double winRate;           // 中标率 %
    }

    // ====== 供应商评价雷达图数据 ======
    private List<RadarData> radarData;

    @Data
    public static class RadarData {
        private String supplierName;
        private double quality;
        private double delivery;
        private double service;
        private double price;
    }

    // ====== 月度评分趋势 ======
    private List<ScoreTrend> scoreTrends;

    @Data
    public static class ScoreTrend {
        private String month;
        private Long supplierId;
        private String supplierName;
        private double score;
    }

    // ====== 异议统计 ======
    private List<ObjectionStat> objectionStats;

    @Data
    public static class ObjectionStat {
        private Long supplierId;
        private String supplierName;
        private int objectionCount;
        private int resolvedCount;
        private String topReason;
    }
}

4.3 Controller 设计

@RestController
@RequestMapping("/bid/report")
public class BizReportController extends BaseController {

    @Autowired private IBizReportService reportService;

    /** 采购总览看板数据 */
    @PreAuthorize("@ss.hasPermi('bid:report:dashboard')")
    @GetMapping("/dashboard")
    public AjaxResult dashboard() {
        return success(reportService.getDashboard());
    }

    /** 采购成本分析数据 */
    @PreAuthorize("@ss.hasPermi('bid:report:cost')")
    @GetMapping("/cost")
    public AjaxResult costAnalysis(
            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM") String startMonth,
            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM") String endMonth) {
        return success(reportService.getCostAnalysis(startMonth, endMonth));
    }

    /** 供应商绩效数据 */
    @PreAuthorize("@ss.hasPermi('bid:report:supplier')")
    @GetMapping("/supplier")
    public AjaxResult supplierPerformance() {
        return success(reportService.getSupplierPerformance());
    }

    /** 导出报表 Excel */
    @PreAuthorize("@ss.hasPermi('bid:report:list')")
    @Log(title = "报表导出", businessType = BusinessType.EXPORT)
    @PostMapping("/export/{type}")
    public void export(HttpServletResponse response, @PathVariable String type) {
        // 使用 RuoYi ExcelUtil 导出type=dashboard/cost/supplier
    }
}

4.4 Service 实现概要

@Service
public class BizReportServiceImpl implements IBizReportService {

    @Autowired private BizReportMapper mapper;

    @Override
    public ReportDashboardVO getDashboard() {
        ReportDashboardVO vo = new ReportDashboardVO();

        // 1. KPI 卡片:本月数据 vs 上月
        vo.setTotalPurchaseAmount(calcKpi("PO", "totalAmount"));
        vo.setTotalRfqCount(calcKpi("RFQ", "count"));
        vo.setTotalPoCount(calcKpi("PO", "count"));
        vo.setActiveSupplierCount(calcKpi("SUPPLIER", "activeCount"));

        // 2. 月度趋势近12个月
        vo.setMonthlyTrend(mapper.selectMonthlyTrend());

        // 3. RFQ 状态分布
        vo.setRfqStatusDist(mapper.selectRfqStatusDist());

        // 4. Top 5 供应商排名
        vo.setTopSuppliers(mapper.selectTopSuppliers());

        // 5. 最近10条动态
        vo.setRecentActivities(mapper.selectRecentActivities());

        return vo;
    }

    // ... 其他方法
}

4.5 Mapper XML 核心 SQL

BizReportMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.system.mapper.bid.BizReportMapper">

    <!-- ═══════════════════ 看板 ═══════════════════ -->

    <!-- 月度采购趋势近12个月按月汇总 -->
    <select id="selectMonthlyTrend" resultType="com.ruoyi.system.domain.bid.ReportDashboardVO$MonthTrend">
        SELECT DATE_FORMAT(create_time, '%Y-%m') AS month,
               SUM(total_amount) AS amount,
               COUNT(*) AS count
        FROM biz_purchase_order
        WHERE status IN ('confirmed', 'closed', 'delivered')
          AND create_time >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
        GROUP BY DATE_FORMAT(create_time, '%Y-%m')
        ORDER BY month ASC
    </select>

    <!-- RFQ 状态分布 -->
    <select id="selectRfqStatusDist" resultType="com.ruoyi.system.domain.bid.ReportDashboardVO$StatusDist">
        SELECT status, COUNT(*) AS count
        FROM biz_rfq
        GROUP BY status
    </select>

    <!-- Top 5 供应商排名(按采购金额) -->
    <select id="selectTopSuppliers" resultType="com.ruoyi.system.domain.bid.ReportDashboardVO$SupplierRank">
        SELECT p.supplier_id, s.supplier_name,
               SUM(p.total_amount) AS totalAmount,
               COUNT(*) AS poCount,
               COALESCE(AVG(e.total_score), 0) AS avgScore
        FROM biz_purchase_order p
        LEFT JOIN biz_supplier s ON p.supplier_id = s.supplier_id
        LEFT JOIN biz_supplier_evaluation e ON p.po_id = e.po_id
        WHERE p.status IN ('confirmed', 'closed', 'delivered')
        GROUP BY p.supplier_id, s.supplier_name
        ORDER BY totalAmount DESC
        LIMIT 5
    </select>

    <!-- 最近动态 -->
    <select id="selectRecentActivities" resultType="com.ruoyi.system.domain.bid.ReportDashboardVO$RecentActivity">
        SELECT 'PO' AS type, po_no AS typeNo, total_amount AS amount,
               CONCAT('创建采购单 ', po_no) AS description,
               create_time
        FROM biz_purchase_order
        UNION ALL
        SELECT 'QUOTE', quote_no, total_amount,
               CONCAT('供应商报价 ', quote_no),
               create_time
        FROM biz_quotation WHERE status IN ('submitted', 'accepted')
        UNION ALL
        SELECT 'EVAL', CONCAT('EVAL-', eval_id), NULL,
               CONCAT('评价供应商:', comment),
               eval_time
        FROM biz_supplier_evaluation
        ORDER BY create_time DESC
        LIMIT 10
    </select>

    <!-- ═══════════════════ 成本分析 ═══════════════════ -->

    <!-- 月度预算 vs 实际成本对比 -->
    <select id="selectCostTrend" resultType="com.ruoyi.system.domain.bid.ReportCostVO$CostTrend">
        SELECT t.month,
               COALESCE(SUM(e.total_expected), 0) AS expectedAmount,
               COALESCE(SUM(a.total_actual), 0) AS actualAmount
        FROM (
            SELECT DATE_FORMAT(r.create_time, '%Y-%m') AS month,
                   SUM(ri.quantity * ri.expected_price) AS total_expected,
                   0 AS total_actual
            FROM biz_rfq r
            JOIN biz_rfq_item ri ON r.rfq_id = ri.rfq_id
            WHERE r.status = 'completed'
            GROUP BY DATE_FORMAT(r.create_time, '%Y-%m')
        ) e
        LEFT JOIN (
            SELECT DATE_FORMAT(p.create_time, '%Y-%m') AS month,
                   SUM(p.total_amount) AS total_actual
            FROM biz_purchase_order p
            WHERE p.status IN ('confirmed', 'closed', 'delivered')
            GROUP BY DATE_FORMAT(p.create_time, '%Y-%m')
        ) a ON e.month = a.month
        GROUP BY t.month
        ORDER BY month
    </select>

    <!-- 品类采购分布 -->
    <select id="selectCategoryDist" resultType="com.ruoyi.system.domain.bid.ReportCostVO$CategoryDist">
        SELECT COALESCE(c.category_name, '未分类') AS categoryName,
               SUM(pi.total_price) AS amount,
               COUNT(DISTINCT pi.material_id) AS materialCount
        FROM biz_purchase_order_item pi
        JOIN biz_purchase_order p ON pi.po_id = p.po_id
        LEFT JOIN biz_material m ON pi.material_id = m.material_id
        LEFT JOIN biz_material_category c ON m.category_id = c.category_id
        WHERE p.status IN ('confirmed', 'closed', 'delivered')
        GROUP BY c.category_name
        ORDER BY amount DESC
    </select>

    <!-- ═══════════════════ 供应商绩效 ═══════════════════ -->

    <!-- 供应商评分排名 -->
    <select id="selectSupplierScores" resultType="com.ruoyi.system.domain.bid.ReportSupplierVO$SupplierScore">
        SELECT s.supplier_id, s.supplier_name,
               COUNT(e.eval_id) AS evalCount,
               COALESCE(AVG(e.quality_score), 0) AS qualityAvg,
               COALESCE(AVG(e.delivery_score), 0) AS deliveryAvg,
               COALESCE(AVG(e.service_score), 0) AS serviceAvg,
               COALESCE(AVG(e.price_score), 0) AS priceAvg,
               COALESCE(AVG(e.total_score), 0) AS totalAvg,
               COALESCE(po.poCount, 0) AS poCount,
               COALESCE(po.poAmount, 0) AS poAmount
        FROM biz_supplier s
        LEFT JOIN biz_supplier_evaluation e ON s.supplier_id = e.supplier_id
        LEFT JOIN (
            SELECT supplier_id, COUNT(*) AS poCount, SUM(total_amount) AS poAmount
            FROM biz_purchase_order
            WHERE status IN ('confirmed', 'closed', 'delivered')
            GROUP BY supplier_id
        ) po ON s.supplier_id = po.supplier_id
        GROUP BY s.supplier_id, s.supplier_name
        ORDER BY totalAvg DESC
    </select>

    <!-- 供应商中标率 -->
    <select id="selectWinRate" resultType="com.ruoyi.system.domain.bid.ReportSupplierVO$SupplierScore">
        SELECT q.supplier_id, s.supplier_name,
               COUNT(*) AS totalQuotes,
               SUM(CASE WHEN q.status = 'accepted' THEN 1 ELSE 0 END) AS winCount,
               ROUND(SUM(CASE WHEN q.status = 'accepted' THEN 1 ELSE 0 END) / COUNT(*) * 100, 1) AS winRate
        FROM biz_quotation q
        JOIN biz_supplier s ON q.supplier_id = s.supplier_id
        WHERE q.status IN ('accepted', 'rejected', 'submitted')
        GROUP BY q.supplier_id, s.supplier_name
        ORDER BY winRate DESC
    </select>

    <!-- 异议统计 -->
    <select id="selectObjectionStats" resultType="com.ruoyi.system.domain.bid.ReportSupplierVO$ObjectionStat">
        SELECT o.supplier_id, s.supplier_name,
               COUNT(*) AS objectionCount,
               SUM(CASE WHEN o.status = 'resolved' THEN 1 ELSE 0 END) AS resolvedCount
        FROM biz_order_objection o
        JOIN biz_supplier s ON o.supplier_id = s.supplier_id
        GROUP BY o.supplier_id, s.supplier_name
        ORDER BY objectionCount DESC
    </select>
</mapper>

五、前端详细设计

5.1 文件结构

ruoyi-ui/src/views/bid/report/
  ├── dashboard.vue              ← 采购总览看板
  ├── cost.vue                   ← 采购成本分析
  ├── supplier.vue               ← 供应商绩效
  └── components/                ← 可复用组件
      ├── KpiCard.vue            ← 指标卡片
      └── TrendChart.vue         ← 趋势图表封装

ruoyi-ui/src/api/bid/
  └── report.js                  ← API 封装

5.2 API 封装report.js

import request from '@/utils/request'
const baseUrl = '/bid/report'

export const getDashboard = () => request({ url: baseUrl + '/dashboard', method: 'get' })
export const getCostAnalysis = (params) => request({ url: baseUrl + '/cost', method: 'get', params })
export const getSupplierPerformance = () => request({ url: baseUrl + '/supplier', method: 'get' })
export const exportReport = (type, data) => request({
  url: baseUrl + '/export/' + type,
  method: 'post',
  data,
  responseType: 'blob'
})

5.3 页面一采购总览看板dashboard.vue

布局方案:

┌─────────────────────────────────────────────────────────┐
│  📊 采购总览看板                                         │
├────────────┬────────────┬────────────┬──────────────────┤
│  采购总额   │  RFQ总数   │  采购单数   │  活跃供应商     │
│  ¥207,820  │    7 单    │   3 单     │    5 家         │
│  ↑ 12.5%   │  ↑ 40%    │  持平      │  ↑ 25%          │
├────────────┴────────────┴────────────┴──────────────────┤
│                                                         │
│  ┌───────────────────┐  ┌─────────────────────────┐     │
│  │  月度采购趋势       │  │  RFQ 状态分布            │     │
│  │  (折线图+柱状图)    │  │  (饼图)                  │     │
│  │                   │  │                         │     │
│  └───────────────────┘  └─────────────────────────┘     │
│                                                         │
│  ┌───────────────────┐  ┌─────────────────────────┐     │
│  │  供应商排名 Top5    │  │  最新动态                 │     │
│  │  (横向柱状图)       │  │  (时间线列表)             │     │
│  │                   │  │                         │     │
│  └───────────────────┘  └─────────────────────────┘     │
└─────────────────────────────────────────────────────────┘

关键代码片段:

ECharts 图表配置示例(月度趋势)

// 在 dashboard.vue 的 methods 中
initTrendChart(data) {
  const chart = echarts.init(document.getElementById('trendChart'))
  const months = data.map(d => d.month)
  const amounts = data.map(d => d.amount)
  const counts = data.map(d => d.count)

  chart.setOption({
    tooltip: { trigger: 'axis' },
    legend: { data: ['采购金额', '采购单数'] },
    grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
    xAxis: { type: 'category', data: months },
    yAxis: [
      { type: 'value', name: '金额(元)' },
      { type: 'value', name: '单数' }
    ],
    series: [
      {
        name: '采购金额',
        type: 'bar',
        data: amounts,
        itemStyle: { color: '#409EFF' },
        label: { show: true, formatter: '¥{c}', position: 'top' }
      },
      {
        name: '采购单数',
        type: 'line',
        yAxisIndex: 1,
        data: counts,
        itemStyle: { color: '#67C23A' }
      }
    ]
  })
}

KPI 卡片组件KpiCard.vue

<template>
  <el-card shadow="hover" class="kpi-card" :style="cardStyle">
    <div class="kpi-label">{{ label }}</div>
    <div class="kpi-value">
      <span class="kpi-number">{{ displayValue }}</span>
      <span class="kpi-unit">{{ unit }}</span>
    </div>
    <div class="kpi-trend" :class="trendClass">
      <i :class="trendIcon"></i>
      {{ changeRateText }}
      <span class="trend-label">环比上月</span>
    </div>
  </el-card>
</template>

<script>
export default {
  props: {
    label: String, value: [Number, String], unit: { type: String, default: '' },
    changeRate: { type: Number, default: 0 }
  },
  computed: {
    displayValue() {
      if (typeof this.value === 'number' && this.value >= 10000 && this.unit === '') {
        return '¥' + this.value.toLocaleString()
      }
      return this.value
    },
    trendClass() { return this.changeRate >= 0 ? 'trend-up' : 'trend-down' },
    trendIcon() { return this.changeRate >= 0 ? 'el-icon-top' : 'el-icon-bottom' },
    changeRateText() { return Math.abs(this.changeRate).toFixed(1) + '%' },
    cardStyle() {
      const colors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C']
      const idx = ['采购总额', 'RFQ总数', '采购单数'].indexOf(this.label)
      return { '--kpi-accent': colors[Math.max(idx, 0) % colors.length] }
    }
  }
}
</script>

<style scoped>
.kpi-card { border-left: 4px solid var(--kpi-accent, #409EFF); }
.kpi-label { font-size: 14px; color: #909399; margin-bottom: 8px; }
.kpi-value { margin-bottom: 8px; }
.kpi-number { font-size: 28px; font-weight: 700; color: #303133; }
.kpi-unit { font-size: 14px; color: #909399; margin-left: 4px; }
.kpi-trend { font-size: 13px; }
.trend-up { color: #67C23A; }
.trend-down { color: #F56C6C; }
.trend-label { color: #C0C4CC; margin-left: 4px; font-size: 12px; }
</style>

5.4 页面二采购成本分析cost.vue

布局方案:

┌─────────────────────────────────────────────────────────┐
│  💰 采购成本分析                                          │
├─────────────────────────────────────────────────────────┤
│  预算总额: ¥XXX   实际采购: ¥XXX   节省: ¥XXX (XX%)     │
├──────────────────────┬──────────────────────────────────┤
│                      │                                  │
│  月度预算 vs 实际     │  品类采购分布                      │
│  (双柱状对比图)       │  (饼图/环形图)                    │
│                      │                                  │
├──────────────────────┴──────────────────────────────────┤
│                                                         │
│  RFQ 比价明细(表格)                                     │
│  ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┐    │
│  │询价单 │预算价 │最低价 │采纳价 │节省额 │比价数 │操作  │    │
│  ├──────┼──────┼──────┼──────┼──────┼──────┼──────┤    │
│  │ ...  │ ...  │ ...  │ ...  │ ...  │ ...  │ 详情 │    │
│  └──────┴──────┴──────┴──────┴──────┴──────┴──────┘    │
│                                                         │
└─────────────────────────────────────────────────────────┘

核心逻辑:

  1. 查询所有 completed 的 RFQ关联 RFQ Item 中的 expected_price 作为预算价
  2. 查询对应被采纳的报价(quotation.status = 'accepted')作为实际采购价
  3. 节省额 = 预算总额 - 实际采购总额
  4. 表格中每行是一个 RFQ点击"详情"跳转到比价详情页

5.5 页面三供应商绩效supplier.vue

布局方案:

┌─────────────────────────────────────────────────────────┐
│  🏆 供应商绩效                                           │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  ┌─────────────────────────────────────────┐             │
│  │  供应商综合评分排名(表格)                │             │
│  │  排名 │ 供应商 │ 质量 │ 交期│ 服务│ 价格│ 综合│ 操作│  │
│  │  1   │ 华顺达  │ 5.0 │ 4.0│ 5.0│ 4.0│ 4.5│ 详情│  │
│  │  2   │ 博远    │ 5.0 │ 3.0│ 5.0│ 5.0│ 4.5│ 详情│  │
│  │  ... │ ...     │ ... │ ...│ ...│ ...│ ...│ ... │  │
│  └─────────────────────────────────────────┘             │
│                                                         │
│  ┌─────────────────────┐  ┌────────────────────────┐    │
│  │  供应商评价雷达图     │  │  中标率分析             │    │
│  │  (可切换供应商)       │  │  (柱状图)              │    │
│  │                     │  │                        │    │
│  └─────────────────────┘  └────────────────────────┘    │
│                                                         │
│  ┌─────────────────────────────────────────┐             │
│  │  订单异议统计(表格)                      │             │
│  │  供应商 │ 异议数 │ 已解决 │ 解决率 │ 操作            │  │
│  └─────────────────────────────────────────┘             │
│                                                         │
└─────────────────────────────────────────────────────────┘

核心交互:

  1. 评分排名表格 - 可点击表头排序
  2. 雷达图 - 点击表格行可切换显示的供应商
  3. 中标率 - 柱状图展示每个供应商的报价次数 vs 中标次数
  4. 异议统计 - 展示各供应商的订单问题情况

六、数据库查询方案SQL 核心)

6.1 关键业务指标 SQL

月度采购金额与环比

-- 当月采购总额
SELECT COALESCE(SUM(total_amount), 0) AS currentMonthAmount
FROM biz_purchase_order
WHERE status IN ('confirmed', 'closed', 'delivered')
  AND DATE_FORMAT(create_time, '%Y-%m') = DATE_FORMAT(NOW(), '%Y-%m');

-- 上月采购总额(环比基准)
SELECT COALESCE(SUM(total_amount), 0) AS lastMonthAmount
FROM biz_purchase_order
WHERE status IN ('confirmed', 'closed', 'delivered')
  AND DATE_FORMAT(create_time, '%Y-%m') = DATE_FORMAT(DATE_SUB(NOW(), INTERVAL 1 MONTH), '%Y-%m');

-- 环比 = (当月 - 上月) / 上月 * 100

预算节省分析

-- 每个 RFQ 的预算 vs 实际采纳金额
SELECT
  r.rfq_id,
  r.rfq_no,
  r.rfq_title,
  ri.total_expected AS expectedTotal,
  COALESCE(q.total_amount, 0) AS acceptedQuote,
  (ri.total_expected - COALESCE(q.total_amount, 0)) AS savedAmount
FROM biz_rfq r
JOIN (
  SELECT rfq_id, SUM(quantity * expected_price) AS total_expected
  FROM biz_rfq_item GROUP BY rfq_id
) ri ON r.rfq_id = ri.rfq_id
LEFT JOIN biz_quotation q ON r.rfq_id = q.rfq_id AND q.status = 'accepted'
WHERE r.status = 'completed';

供应商中标率

SELECT
  q.supplier_id,
  s.supplier_name,
  COUNT(*) AS totalQuotes,
  SUM(CASE WHEN q.status = 'accepted' THEN 1 ELSE 0 END) AS winCount,
  ROUND(SUM(CASE WHEN q.status = 'accepted' THEN 1 ELSE 0 END) / COUNT(*) * 100, 1) AS winRate
FROM biz_quotation q
JOIN biz_supplier s ON q.supplier_id = s.supplier_id
WHERE q.status IN ('accepted', 'rejected')
GROUP BY q.supplier_id, s.supplier_name
ORDER BY winRate DESC;

6.2 性能优化建议

优化点 方案
大数据量 报表查询加时间范围过滤默认只查最近12个月
缓存 看板数据用 Redis 缓存 5 分钟(利用 RuoYi 已有 Redis 集成)
复杂统计 新建物化视图或定时任务汇总到统计表(后续优化)

七、权限与菜单设计

7.1 权限标识

权限标识 对应页面 说明
bid:report:list 统计分析目录 父菜单显示
bid:report:dashboard 采购总览看板 仪表盘页面
bid:report:cost 采购成本分析 成本页面
bid:report:supplier 供应商绩效 供应商页面

7.2 权限分配

在系统管理 → 角色管理中,为管理员角色勾选新增的菜单权限:

  • 统计分析 → 采购总览看板
  • 统计分析 → 采购成本分析
  • 统计分析 → 供应商绩效

八、实施步骤与里程碑

Phase 1后端基础预计 1 天)

步骤 内容 文件
1.1 创建 3 个 VO 类 ReportDashboardVO.java, ReportCostVO.java, ReportSupplierVO.java
1.2 创建 Mapper 接口 + XML BizReportMapper.java, BizReportMapper.xml
1.3 创建 Service 接口 + 实现 IBizReportService.java, BizReportServiceImpl.java
1.4 创建 Controller BizReportController.java
1.5 测试 API 使用 Swagger 或浏览器验证 JSON 返回

Phase 2前端页面预计 1.5 天)

步骤 内容 文件
2.1 创建 API 封装 src/api/bid/report.js
2.2 搭建看板页面 dashboard.vue + KpiCard.vue
2.3 搭建成本分析页面 cost.vue
2.4 搭建供应商绩效页面 supplier.vue
2.5 集成 ECharts 图表 在各页面中初始化

Phase 3集成上线预计 0.5 天)

步骤 内容
3.1 注册路由到 router/index.js
3.2 执行菜单 SQL授权 admin 角色
3.3 前后端联调测试
3.4 展示给老板看,收集反馈

总计工期:约 3 天


九、文件清单与代码量预估

后端7 个文件)

文件 路径 预估行数
BizReportController.java ruoyi-admin/.../controller/bid/ ~60 行
IBizReportService.java ruoyi-system/.../service/bid/ ~20 行
BizReportServiceImpl.java ruoyi-system/.../service/bid/impl/ ~120 行
BizReportMapper.java ruoyi-system/.../mapper/bid/ ~15 行
BizReportMapper.xml ruoyi-system/.../mapper/bid/ ~200 行
3 个 VO 类 ruoyi-system/.../domain/bid/ ~300 行
小计 ~715 行

前端5 个文件)

文件 路径 预估行数
report.js src/api/bid/ ~30 行
dashboard.vue src/views/bid/report/ ~350 行
cost.vue src/views/bid/report/ ~300 行
supplier.vue src/views/bid/report/ ~350 行
KpiCard.vue src/views/bid/report/components/ ~80 行
小计 ~1,110 行

SQL 变更1 个脚本)

文件 用途 行数
report_menu.sql 菜单 SQL ~20 行

总计:约 12 个文件~1,800 行代码


附录:与老板汇报话术参考

老板,我做了一个统计分析模块,分为三个页面:

  1. 采购总览看板 — 一打开就能看到本月花了多少钱、发了多少询价单、供应商活跃情况,还配了趋势图和排名。
  2. 采购成本分析 — 对比预算和实际花销,看哪些品类花钱多,每次比价节省了多少,一眼看清钱花在哪。
  3. 供应商绩效 — 给每个供应商打分排名(质量/交期/服务/价格),哪家好哪家差一目了然,还有异常统计。

数据都来自我们系统里已有的业务数据,不需要额外录入。预计 3 天可以开发完成。


文档版本: v1.0
编写日期: 2026-06-03
适用范围: 福安德智慧报价平台