编辑 | blame | 历史 | 原始文档

首页仪表盘图表功能实现计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 在 WMS 前端首页添加仪表盘图表,展示出入库统计和库存数据

Architecture:
- 后端:新建 DashboardController,提供6个统计接口,使用 SqlSugar 直接查询 Dt_Task_Hty(已完成任务历史表)和 Dt_StockInfo 表
- 前端:重写 Home.vue,使用 ECharts 5.0.2 实现仪表盘布局,复用 bigdata.vue 中的 ECharts 使用模式
- 数据来源:Dt_Task_Hty.InsertTime(任务完成时间),TaskType 区分入库(500-599)/出库(100-199)

Tech Stack: ASP.NET Core 6.0, Vue 3, ECharts 5.0.2, SqlSugar


文件结构

后端新增:
- WIDESEA_WMSServer/Controllers/Dashboard/DashboardController.cs  (仪表盘控制器)

前端修改:
- WIDESEA_WMSClient/src/views/Home.vue  (重写为仪表盘页面)

实现任务

Task 1: 创建后端 DashboardController

文件:
- Create: WIDESEA_WMSServer/Controllers/Dashboard/DashboardController.cs

说明: 创建 DashboardController,包含6个 API 接口

  • [ ] Step 1: 创建控制器文件
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
using WIDESEA_Core;
using WIDESEA_Model.Models;

namespace WIDESEA_WMSServer.Controllers.Dashboard
{
    /// <summary>
    /// 仪表盘
    /// </summary>
    [Route("api/Dashboard")]
    [ApiController]
    public class DashboardController : ControllerBase
    {
        private readonly ISqlSugarClient _db;

        public DashboardController(ISqlSugarClient db)
        {
            _db = db;
        }

        /// <summary>
        /// 总览数据
        /// </summary>
        [HttpGet("Overview")]
        public async Task<WebResponseContent> Overview()
        {
            // 实现见 Step 2
        }

        /// <summary>
        /// 每日统计
        /// </summary>
        [HttpGet("DailyStats")]
        public async Task<WebResponseContent> DailyStats([FromQuery] int days = 30)
        {
            // 实现见 Step 3
        }

        /// <summary>
        /// 每周统计
        /// </summary>
        [HttpGet("WeeklyStats")]
        public async Task<WebResponseContent> WeeklyStats([FromQuery] int weeks = 12)
        {
            // 实现见 Step 4
        }

        /// <summary>
        /// 每月统计
        /// </summary>
        [HttpGet("MonthlyStats")]
        public async Task<WebResponseContent> MonthlyStats([FromQuery] int months = 12)
        {
            // 实现见 Step 5
        }

        /// <summary>
        /// 库存库龄分布
        /// </summary>
        [HttpGet("StockAgeDistribution")]
        public async Task<WebResponseContent> StockAgeDistribution()
        {
            // 实现见 Step 6
        }

        /// <summary>
        /// 各仓库库存分布
        /// </summary>
        [HttpGet("StockByWarehouse")]
        public async Task<WebResponseContent> StockByWarehouse()
        {
            // 实现见 Step 7
        }
    }
}
  • [ ] Step 2: 实现 Overview 接口

在 Overview 方法中实现:

public async Task<WebResponseContent> Overview()
{
    var today = DateTime.Today;
    var firstDayOfMonth = new DateTime(today.Year, today.Month, 1);

    // 今日入库数
    var todayInbound = await _db.Queryable<Dt_Task_Hty>()
        .Where(t => t.InsertTime >= today && t.TaskType >= 500 && t.TaskType < 600)
        .CountAsync();

    // 今日出库数
    var todayOutbound = await _db.Queryable<Dt_Task_Hty>()
        .Where(t => t.InsertTime >= today && t.TaskType >= 100 && t.TaskType < 200)
        .CountAsync();

    // 本月入库数
    var monthInbound = await _db.Queryable<Dt_Task_Hty>()
        .Where(t => t.InsertTime >= firstDayOfMonth && t.TaskType >= 500 && t.TaskType < 600)
        .CountAsync();

    // 本月出库数
    var monthOutbound = await _db.Queryable<Dt_Task_Hty>()
        .Where(t => t.InsertTime >= firstDayOfMonth && t.TaskType >= 100 && t.TaskType < 200)
        .CountAsync();

    // 当前总库存
    var totalStock = await _db.Queryable<Dt_StockInfo>().CountAsync();

    return WebResponseContent.Instance.OK(null, new
    {
        TodayInbound = todayInbound,
        TodayOutbound = todayOutbound,
        MonthInbound = monthInbound,
        MonthOutbound = monthOutbound,
        TotalStock = totalStock
    });
}
  • [ ] Step 3: 实现 DailyStats 接口
public async Task<WebResponseContent> DailyStats([FromQuery] int days = 30)
{
    if (days <= 0) days = 30;
    if (days > 365) days = 365;

    var startDate = DateTime.Today.AddDays(-days + 1);

    var query = await _db.Queryable<Dt_Task_Hty>()
        .Where(t => t.InsertTime >= startDate)
        .Select(t => new { t.InsertTime, t.TaskType })
        .ToListAsync();

    var result = query
        .GroupBy(t => t.InsertTime.Date)
        .Select(g => new
        {
            Date = g.Key.ToString("yyyy-MM-dd"),
            Inbound = g.Count(t => t.TaskType >= 500 && t.TaskType < 600),
            Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
        })
        .OrderBy(x => x.Date)
        .ToList();

    return WebResponseContent.Instance.OK(null, result);
}
  • [ ] Step 4: 实现 WeeklyStats 接口
public async Task<WebResponseContent> WeeklyStats([FromQuery] int weeks = 12)
{
    if (weeks <= 0) weeks = 12;

    var startDate = DateTime.Today.AddDays(-weeks * 7);

    var query = await _db.Queryable<Dt_Task_Hty>()
        .Where(t => t.InsertTime >= startDate)
        .Select(t => new { t.InsertTime, t.TaskType })
        .ToListAsync();

    var result = query
        .GroupBy(t => GetWeekKey(t.InsertTime))
        .Select(g => new
        {
            Week = g.Key,
            Inbound = g.Count(t => t.TaskType >= 500 && t.TaskType < 600),
            Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
        })
        .OrderBy(x => x.Week)
        .ToList();

    return WebResponseContent.Instance.OK(null, result);
}

private string GetWeekKey(DateTime date)
{
    // 获取周一开始的周
    var diff = (7 + (date.DayOfWeek - DayOfWeek.Monday)) % 7;
    var monday = date.AddDays(-diff);
    return monday.ToString("yyyy-Www");
}
  • [ ] Step 5: 实现 MonthlyStats 接口
public async Task<WebResponseContent> MonthlyStats([FromQuery] int months = 12)
{
    if (months <= 0) months = 12;

    var startDate = DateTime.Today.AddMonths(-months + 1);
    startDate = new DateTime(startDate.Year, startDate.Month, 1);

    var query = await _db.Queryable<Dt_Task_Hty>()
        .Where(t => t.InsertTime >= startDate)
        .Select(t => new { t.InsertTime, t.TaskType })
        .ToListAsync();

    var result = query
        .GroupBy(t => new { t.InsertTime.Year, t.InsertTime.Month })
        .Select(g => new
        {
            Month = $"{g.Key.Year}-{g.Key.Month:D2}",
            Inbound = g.Count(t => t.TaskType >= 500 && t.TaskType < 600),
            Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
        })
        .OrderBy(x => x.Month)
        .ToList();

    return WebResponseContent.Instance.OK(null, result);
}
  • [ ] Step 6: 实现 StockAgeDistribution 接口
public async Task<WebResponseContent> StockAgeDistribution()
{
    var now = DateTime.Now;
    var stocks = await _db.Queryable<Dt_StockInfo>()
        .Select(s => s.CreateDate)
        .ToListAsync();

    var result = new[]
    {
        new { Range = "7天内", Count = stocks.Count(s => (now - s).TotalDays <= 7) },
        new { Range = "7-30天", Count = stocks.Count(s => (now - s).TotalDays > 7 && (now - s).TotalDays <= 30) },
        new { Range = "30-90天", Count = stocks.Count(s => (now - s).TotalDays > 30 && (now - s).TotalDays <= 90) },
        new { Range = "90天以上", Count = stocks.Count(s => (now - s).TotalDays > 90) }
    };

    return WebResponseContent.Instance.OK(null, result);
}
  • [ ] Step 7: 实现 StockByWarehouse 接口
public async Task<WebResponseContent> StockByWarehouse()
{
    var result = await _db.Queryable<Dt_StockInfo>()
        .GroupBy(s => s.WarehouseId)
        .Select(g => new
        {
            WarehouseId = g.Key,
            Count = g.Count()
        })
        .ToListAsync();

    // 联查仓库名称
    var warehouseIds = result.Select(x => x.WarehouseId).ToList();
    var warehouses = await _db.Queryable<Dt_Warehouse>()
        .Where(w => warehouseIds.Contains(w.WarehouseId))
        .Select(w => new { w.WarehouseId, w.WarehouseName })
        .ToListAsync();

    var finalResult = result.Select(r =>
    {
        var wh = warehouses.FirstOrDefault(w => w.WarehouseId == r.WarehouseId);
        return new
        {
            Warehouse = wh?.WarehouseName ?? $"仓库{r.WarehouseId}",
            Count = r.Count
        };
    }).ToList();

    return WebResponseContent.Instance.OK(null, finalResult);
}
  • [ ] Step 8: 提交代码
git add WIDESEA_WMSServer/Controllers/Dashboard/DashboardController.cs
git commit -m "feat(Dashboard): 添加仪表盘控制器,包含6个统计接口"

Task 2: 重写前端 Home.vue 实现仪表盘

文件:
- Modify: WIDESEA_WMSClient/src/views/Home.vue

说明: 重写为空白的首页,实现仪表盘图表布局

  • [ ] Step 1: 重写 Home.vue 模板部分
<template>
  <div class="dashboard-container">
    <!-- 顶部:本月出入库趋势 (全宽) -->
    <div class="chart-row full-width">
      <div class="chart-card">
        <div class="card-title">本月出入库趋势</div>
        <div id="chart-monthly-trend" class="chart-content"></div>
      </div>
    </div>

    <!-- 第二行:今日/本周出入库对比 -->
    <div class="chart-row">
      <div class="chart-card">
        <div class="card-title">今日出入库对比</div>
        <div id="chart-today" class="chart-content"></div>
      </div>
      <div class="chart-card">
        <div class="card-title">本周出入库对比</div>
        <div id="chart-week" class="chart-content"></div>
      </div>
    </div>

    <!-- 第三行:本月对比/库存总量 -->
    <div class="chart-row">
      <div class="chart-card">
        <div class="card-title">本月出入库对比</div>
        <div id="chart-month" class="chart-content"></div>
      </div>
      <div class="chart-card">
        <div class="card-title">当前库存总量</div>
        <div class="stock-total">
          <div class="total-number">{{ overviewData.TotalStock || 0 }}</div>
          <div class="total-label">托盘</div>
        </div>
      </div>
    </div>

    <!-- 第四行:库龄分布/仓库分布 -->
    <div class="chart-row">
      <div class="chart-card">
        <div class="card-title">库存库龄分布</div>
        <div id="chart-stock-age" class="chart-content"></div>
      </div>
      <div class="chart-card">
        <div class="card-title">各仓库库存分布</div>
        <div id="chart-warehouse" class="chart-content"></div>
      </div>
    </div>
  </div>
</template>
  • [ ] Step 2: 重写脚本部分
<script>
import * as echarts from "echarts";

export default {
  name: "Home",
  data() {
    return {
      charts: {},
      overviewData: {
        TodayInbound: 0,
        TodayOutbound: 0,
        MonthInbound: 0,
        MonthOutbound: 0,
        TotalStock: 0
      },
      dailyData: [],
      weeklyData: [],
      monthlyData: [],
      stockAgeData: [],
      warehouseData: []
    };
  },
  mounted() {
    this.initCharts();
    this.loadData();
  },
  beforeUnmount() {
    Object.values(this.charts).forEach(chart => chart.dispose());
  },
  methods: {
    initCharts() {
      this.charts.monthlyTrend = echarts.init(document.getElementById("chart-monthly-trend"));
      this.charts.today = echarts.init(document.getElementById("chart-today"));
      this.charts.week = echarts.init(document.getElementById("chart-week"));
      this.charts.month = echarts.init(document.getElementById("chart-month"));
      this.charts.stockAge = echarts.init(document.getElementById("chart-stock-age"));
      this.charts.warehouse = echarts.init(document.getElementById("chart-warehouse"));
    },

    async loadData() {
      await this.loadOverview();
      await this.loadDailyStats();
      await this.loadWeeklyStats();
      await this.loadMonthlyStats();
      await this.loadStockAgeDistribution();
      await this.loadStockByWarehouse();
    },

    async loadOverview() {
      try {
        const res = await this.http.get("/api/Dashboard/Overview");
        if (res.Status && res.Data) {
          this.overviewData = res.Data;
          this.updateTodayChart();
          this.updateWeekChart();
          this.updateMonthChart();
        }
      } catch (e) {
        console.error("加载总览数据失败", e);
      }
    },

    async loadDailyStats() {
      try {
        const res = await this.http.get("/api/Dashboard/DailyStats", { days: 30 });
        if (res.Status && res.Data) {
          this.dailyData = res.Data;
        }
      } catch (e) {
        console.error("加载每日统计失败", e);
      }
    },

    async loadWeeklyStats() {
      try {
        const res = await this.http.get("/api/Dashboard/WeeklyStats", { weeks: 12 });
        if (res.Status && res.Data) {
          this.weeklyData = res.Data;
          this.updateWeeklyTrendChart();
        }
      } catch (e) {
        console.error("加载每周统计失败", e);
      }
    },

    async loadMonthlyStats() {
      try {
        const res = await this.http.get("/api/Dashboard/MonthlyStats", { months: 12 });
        if (res.Status && res.Data) {
          this.monthlyData = res.Data;
          this.updateMonthlyTrendChart();
        }
      } catch (e) {
        console.error("加载每月统计失败", e);
      }
    },

    async loadStockAgeDistribution() {
      try {
        const res = await this.http.get("/api/Dashboard/StockAgeDistribution");
        if (res.Status && res.Data) {
          this.stockAgeData = res.Data;
          this.updateStockAgeChart();
        }
      } catch (e) {
        console.error("加载库龄分布失败", e);
      }
    },

    async loadStockByWarehouse() {
      try {
        const res = await this.http.get("/api/Dashboard/StockByWarehouse");
        if (res.Status && res.Data) {
          this.warehouseData = res.Data;
          this.updateWarehouseChart();
        }
      } catch (e) {
        console.error("加载仓库分布失败", e);
      }
    },

    // 更新今日对比图表
    updateTodayChart() {
      const option = {
        tooltip: { trigger: "axis" },
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: ["今日"],
          axisLabel: { color: "#fff" }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
        },
        series: [
          { name: "入库", type: "bar", data: [this.overviewData.TodayInbound], itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "bar", data: [this.overviewData.TodayOutbound], itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.today.setOption(option, true);
    },

    // 更新本周对比图表
    updateWeekChart() {
      // 本周数据从 weeklyData 中计算当周数据
      const thisWeek = this.getThisWeekData(this.weeklyData);
      const option = {
        tooltip: { trigger: "axis" },
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: ["本周"],
          axisLabel: { color: "#fff" }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
        },
        series: [
          { name: "入库", type: "bar", data: [thisWeek.Inbound], itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "bar", data: [thisWeek.Outbound], itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.week.setOption(option, true);
    },

    getThisWeekData(weeklyData) {
      if (!weeklyData || weeklyData.length === 0) return { Inbound: 0, Outbound: 0 };
      const thisWeekKey = this.getCurrentWeekKey();
      const thisWeek = weeklyData.find(w => w.Week === thisWeekKey);
      return thisWeek || { Inbound: 0, Outbound: 0 };
    },

    getCurrentWeekKey() {
      const now = new Date();
      const diff = (7 + (now.getDay() - 1)) % 7;
      const monday = new Date(now);
      monday.setDate(now.getDate() - diff);
      const year = monday.getFullYear();
      const month = monday.getMonth() + 1;
      const day = monday.getDate();
      // ISO week start (Monday)
      const jan1 = new Date(year, 0, 1);
      const weekNum = Math.ceil(((monday - jan1) / 86400000 + jan1.getDay() + 1) / 7);
      return `${year}-W${String(weekNum).padStart(2, "0")}`;
    },

    // 更新本月对比图表
    updateMonthChart() {
      const option = {
        tooltip: { trigger: "axis" },
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: ["本月"],
          axisLabel: { color: "#fff" }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
        },
        series: [
          { name: "入库", type: "bar", data: [this.overviewData.MonthInbound], itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "bar", data: [this.overviewData.MonthOutbound], itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.month.setOption(option, true);
    },

    // 更新月度趋势图表(折线+柱状组合)
    updateMonthlyTrendChart() {
      const option = {
        tooltip: { trigger: "axis" },
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: this.monthlyData.map(m => m.Month),
          axisLabel: { color: "#fff", rotate: 45 }
        },
        yAxis: [
          {
            type: "value",
            name: "数量",
            axisLabel: { color: "#fff" }
          }
        ],
        series: [
          { name: "入库", type: "line", data: this.monthlyData.map(m => m.Inbound), itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "line", data: this.monthlyData.map(m => m.Outbound), itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.monthlyTrend.setOption(option, true);
    },

    // 更新周趋势图表
    updateWeeklyTrendChart() {
      const option = {
        tooltip: { trigger: "axis" },
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: this.weeklyData.map(w => w.Week),
          axisLabel: { color: "#fff", rotate: 45 }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
        },
        series: [
          { name: "入库", type: "bar", data: this.weeklyData.map(w => w.Inbound), itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "bar", data: this.weeklyData.map(w => w.Outbound), itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.monthlyTrend.setOption(option, true);
    },

    // 更新库龄分布图表
    updateStockAgeChart() {
      const option = {
        tooltip: { trigger: "item" },
        legend: { data: this.stockAgeData.map(s => s.Range), textStyle: { color: "#fff" } },
        series: [
          {
            type: "pie",
            radius: "60%",
            data: this.stockAgeData.map((s, i) => ({
              name: s.Range,
              value: s.Count
            })),
            emphasis: {
              itemStyle: {
                shadowBlur: 10,
                shadowOffsetX: 0,
                shadowColor: "rgba(0, 0, 0, 0.5)"
              }
            }
          }
        ]
      };
      this.charts.stockAge.setOption(option, true);
    },

    // 更新仓库分布图表
    updateWarehouseChart() {
      const option = {
        tooltip: { trigger: "axis" },
        xAxis: {
          type: "category",
          data: this.warehouseData.map(w => w.Warehouse),
          axisLabel: { color: "#fff", rotate: 30 }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
        },
        series: [
          {
            type: "bar",
            data: this.warehouseData.map(w => w.Count),
            itemStyle: { color: "#5470c6" }
          }
        ]
      };
      this.charts.warehouse.setOption(option, true);
    }
  }
};
</script>
  • [ ] Step 3: 添加样式
<style scoped>
.dashboard-container {
  padding: 20px;
  background-color: #0e1a2b;
  min-height: calc(100vh - 60px);
}

.chart-row {
  display: flex;
  gap: 20px;
  margin-bottom: 20px;
}

.chart-row.full-width {
  width: 100%;
}

.chart-card {
  flex: 1;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(25, 186, 139, 0.17);
  border-radius: 4px;
  padding: 15px;
  position: relative;
}

.chart-card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 10px;
  height: 10px;
  border-top: 2px solid #02a6b5;
  border-left: 2px solid #02a6b5;
}

.chart-card::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  width: 10px;
  height: 10px;
  border-top: 2px solid #02a6b5;
  border-right: 2px solid #02a6b5;
}

.card-title {
  color: #fff;
  font-size: 16px;
  text-align: center;
  margin-bottom: 10px;
}

.chart-content {
  height: 280px;
  width: 100%;
}

.stock-total {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 280px;
}

.total-number {
  font-size: 64px;
  font-weight: bold;
  color: #67caca;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif;
}

.total-label {
  font-size: 18px;
  color: #fcf0d8;
  margin-top: 10px;
}

/* 全宽图表 */
.full-width .chart-card {
  flex: none;
  width: 100%;
}

.full-width .chart-content {
  height: 350px;
}
</style>
  • [ ] Step 4: 提交代码
git add WIDESEA_WMSClient/src/views/Home.vue
git commit -m "feat(Home): 重写首页为仪表盘图表页面"

Task 3: 验证实现

  • [ ] Step 1: 构建后端
cd WIDESEA_WMSServer
dotnet build WIDESEA_WMSServer.sln

预期:构建成功,无错误

  • [ ] Step 2: 构建前端
cd WIDESEA_WMSClient
yarn build

预期:构建成功,无错误

  • [ ] Step 3: 启动后端测试 API
cd WIDESEA_WMSServer/WIDESEA_WMSServer
dotnet run

使用浏览器或 Postman 测试:
- GET http://localhost:9291/api/Dashboard/Overview
- GET http://localhost:9291/api/Dashboard/DailyStats?days=30
- GET http://localhost:9291/api/Dashboard/WeeklyStats?weeks=12
- GET http://localhost:9291/api/Dashboard/MonthlyStats?months=12
- GET http://localhost:9291/api/Dashboard/StockAgeDistribution
- GET http://localhost:9291/api/Dashboard/StockByWarehouse

预期:各接口返回 JSON 数据,格式符合设计文档


总结

后端(DashboardController)

接口 路由 说明
Overview GET /api/Dashboard/Overview 总览数据
DailyStats GET /api/Dashboard/DailyStats?days=30 每日统计
WeeklyStats GET /api/Dashboard/WeeklyStats?weeks=12 每周统计
MonthlyStats GET /api/Dashboard/MonthlyStats?months=12 每月统计
StockAgeDistribution GET /api/Dashboard/StockAgeDistribution 库龄分布
StockByWarehouse GET /api/Dashboard/StockByWarehouse 仓库分布

前端(Home.vue)

图表 组件 ID 图表类型
本月出入库趋势 chart-monthly-trend 折线图
今日出入库对比 chart-today 柱状图
本周出入库对比 chart-week 柱状图
本月出入库对比 chart-month 柱状图

| 当前库存总量 | (数字卡片) | - |
| 库存库龄分布 | chart-stock-age | 饼图 |
| 各仓库库存分布 | chart-warehouse | 柱状图 |