wanshenmean
2026-03-30 42c82b034ec6aba3664436c5dcf63e9b3511d7a5
docs: 添加首页仪表盘实现计划

- 后端 DashboardController 6个接口
- 前端 Home.vue 仪表盘页面
- 包含完整代码和验证步骤

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
已添加1个文件
866 ■■■■■ 文件已修改
Code/WMS/docs/superpowers/plans/2026-03-30-dashboard-chart-plan.md 866 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/docs/superpowers/plans/2026-03-30-dashboard-chart-plan.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,866 @@
# é¦–页仪表盘图表功能实现计划
> **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: åˆ›å»ºæŽ§åˆ¶å™¨æ–‡ä»¶**
```csharp
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 æ–¹æ³•中实现:
```csharp
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 æŽ¥å£**
```csharp
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 æŽ¥å£**
```csharp
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 æŽ¥å£**
```csharp
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 æŽ¥å£**
```csharp
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 æŽ¥å£**
```csharp
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: æäº¤ä»£ç **
```bash
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 æ¨¡æ¿éƒ¨åˆ†**
```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: é‡å†™è„šæœ¬éƒ¨åˆ†**
```javascript
<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: æ·»åŠ æ ·å¼**
```vue
<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: æäº¤ä»£ç **
```bash
git add WIDESEA_WMSClient/src/views/Home.vue
git commit -m "feat(Home): é‡å†™é¦–页为仪表盘图表页面"
```
---
### Task 3: éªŒè¯å®žçް
- [ ] **Step 1: æž„建后端**
```bash
cd WIDESEA_WMSServer
dotnet build WIDESEA_WMSServer.sln
```
预期:构建成功,无错误
- [ ] **Step 2: æž„建前端**
```bash
cd WIDESEA_WMSClient
yarn build
```
预期:构建成功,无错误
- [ ] **Step 3: å¯åŠ¨åŽç«¯æµ‹è¯• API**
```bash
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 | æŸ±çж图 |