# 首页仪表盘图表功能实现计划 > **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 { /// /// 仪表盘 /// [Route("api/Dashboard")] [ApiController] public class DashboardController : ControllerBase { private readonly ISqlSugarClient _db; public DashboardController(ISqlSugarClient db) { _db = db; } /// /// 总览数据 /// [HttpGet("Overview")] public async Task Overview() { // 实现见 Step 2 } /// /// 每日统计 /// [HttpGet("DailyStats")] public async Task DailyStats([FromQuery] int days = 30) { // 实现见 Step 3 } /// /// 每周统计 /// [HttpGet("WeeklyStats")] public async Task WeeklyStats([FromQuery] int weeks = 12) { // 实现见 Step 4 } /// /// 每月统计 /// [HttpGet("MonthlyStats")] public async Task MonthlyStats([FromQuery] int months = 12) { // 实现见 Step 5 } /// /// 库存库龄分布 /// [HttpGet("StockAgeDistribution")] public async Task StockAgeDistribution() { // 实现见 Step 6 } /// /// 各仓库库存分布 /// [HttpGet("StockByWarehouse")] public async Task StockByWarehouse() { // 实现见 Step 7 } } } ``` - [ ] **Step 2: 实现 Overview 接口** 在 Overview 方法中实现: ```csharp public async Task Overview() { var today = DateTime.Today; var firstDayOfMonth = new DateTime(today.Year, today.Month, 1); // 今日入库数 var todayInbound = await _db.Queryable() .Where(t => t.InsertTime >= today && t.TaskType >= 500 && t.TaskType < 600) .CountAsync(); // 今日出库数 var todayOutbound = await _db.Queryable() .Where(t => t.InsertTime >= today && t.TaskType >= 100 && t.TaskType < 200) .CountAsync(); // 本月入库数 var monthInbound = await _db.Queryable() .Where(t => t.InsertTime >= firstDayOfMonth && t.TaskType >= 500 && t.TaskType < 600) .CountAsync(); // 本月出库数 var monthOutbound = await _db.Queryable() .Where(t => t.InsertTime >= firstDayOfMonth && t.TaskType >= 100 && t.TaskType < 200) .CountAsync(); // 当前总库存 var totalStock = await _db.Queryable().CountAsync(); return WebResponseContent.Instance.OK(null, new { TodayInbound = todayInbound, TodayOutbound = todayOutbound, MonthInbound = monthInbound, MonthOutbound = monthOutbound, TotalStock = totalStock }); } ``` - [ ] **Step 3: 实现 DailyStats 接口** ```csharp public async Task 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() .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 WeeklyStats([FromQuery] int weeks = 12) { if (weeks <= 0) weeks = 12; var startDate = DateTime.Today.AddDays(-weeks * 7); var query = await _db.Queryable() .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) { // 获取周一开始的周 (ISO 8601) var diff = (7 + (date.DayOfWeek - DayOfWeek.Monday)) % 7; var monday = date.AddDays(-diff); var weekNum = System.Globalization.CultureInfo.InvariantCulture .Calendar.GetWeekOfYear(monday, System.Globalization.CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday); return $"{monday.Year}-W{weekNum:D2}"; } ``` - [ ] **Step 5: 实现 MonthlyStats 接口** ```csharp public async Task 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() .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 StockAgeDistribution() { var now = DateTime.Now; // 使用 SQL 直接分组统计,避免加载所有数据到内存 var result = new[] { new { Range = "7天内", Count = await _db.Queryable().Where(s => EF.Functions.DateDiffDay(s.CreateDate, now) <= 7).CountAsync() }, new { Range = "7-30天", Count = await _db.Queryable().Where(s => EF.Functions.DateDiffDay(s.CreateDate, now) > 7 && EF.Functions.DateDiffDay(s.CreateDate, now) <= 30).CountAsync() }, new { Range = "30-90天", Count = await _db.Queryable().Where(s => EF.Functions.DateDiffDay(s.CreateDate, now) > 30 && EF.Functions.DateDiffDay(s.CreateDate, now) <= 90).CountAsync() }, new { Range = "90天以上", Count = await _db.Queryable().Where(s => EF.Functions.DateDiffDay(s.CreateDate, now) > 90).CountAsync() } }; return WebResponseContent.Instance.OK(null, result); } ``` - [ ] **Step 7: 实现 StockByWarehouse 接口** ```csharp public async Task StockByWarehouse() { var result = await _db.Queryable() .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() .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 本月出入库趋势 今日出入库对比 本周出入库对比 本月出入库对比 当前库存总量 {{ overviewData.TotalStock || 0 }} 托盘 库存库龄分布 各仓库库存分布 ``` - [ ] **Step 2: 重写脚本部分** ```javascript ``` - [ ] **Step 3: 添加样式** ```vue ``` - [ ] **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 | 柱状图 |