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 (重写为仪表盘页面)
文件:
- Create: WIDESEA_WMSServer/Controllers/Dashboard/DashboardController.cs
说明: 创建 DashboardController,包含6个 API 接口
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
}
}
}
在 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
});
}
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);
}
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)
{
// 获取周一开始的周 (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}";
}
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);
}
public async Task<WebResponseContent> StockAgeDistribution()
{
var now = DateTime.Now;
// 使用 SQL 直接分组统计,避免加载所有数据到内存
var result = new[]
{
new { Range = "7天内", Count = await _db.Queryable<Dt_StockInfo>().Where(s => EF.Functions.DateDiffDay(s.CreateDate, now) <= 7).CountAsync() },
new { Range = "7-30天", Count = await _db.Queryable<Dt_StockInfo>().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<Dt_StockInfo>().Where(s => EF.Functions.DateDiffDay(s.CreateDate, now) > 30 && EF.Functions.DateDiffDay(s.CreateDate, now) <= 90).CountAsync() },
new { Range = "90天以上", Count = await _db.Queryable<Dt_StockInfo>().Where(s => EF.Functions.DateDiffDay(s.CreateDate, now) > 90).CountAsync() }
};
return WebResponseContent.Instance.OK(null, result);
}
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);
}
git add WIDESEA_WMSServer/Controllers/Dashboard/DashboardController.cs
git commit -m "feat(Dashboard): 添加仪表盘控制器,包含6个统计接口"
文件:
- Modify: WIDESEA_WMSClient/src/views/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>
<script>
import * as echarts from "echarts";
export default {
name: "Home",
data() {
return {
charts: {},
overviewData: {
TodayInbound: 0,
TodayOutbound: 0,
MonthInbound: 0,
MonthOutbound: 0,
TotalStock: 0
},
weeklyData: [],
monthlyData: [],
stockAgeData: [],
warehouseData: []
};
},
mounted() {
this.initCharts();
this.loadData();
window.addEventListener("resize", this.handleResize);
},
beforeUnmount() {
window.removeEventListener("resize", this.handleResize);
Object.values(this.charts).forEach(chart => chart.dispose());
},
methods: {
handleResize() {
Object.values(this.charts).forEach(chart => chart.resize());
},
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.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 loadWeeklyStats() {
try {
const res = await this.http.get("/api/Dashboard/WeeklyStats", { weeks: 12 });
if (res.Status && res.Data) {
this.weeklyData = res.Data;
this.updateWeekChart();
}
} 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);
},
// 更新库龄分布图表
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>
<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>
git add WIDESEA_WMSClient/src/views/Home.vue
git commit -m "feat(Home): 重写首页为仪表盘图表页面"
cd WIDESEA_WMSServer
dotnet build WIDESEA_WMSServer.sln
预期:构建成功,无错误
cd WIDESEA_WMSClient
yarn build
预期:构建成功,无错误
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 数据,格式符合设计文档
| 接口 | 路由 | 说明 |
|---|---|---|
| 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 | 仓库分布 |
| 图表 | 组件 ID | 图表类型 |
|---|---|---|
| 本月出入库趋势 | chart-monthly-trend | 折线图 |
| 今日出入库对比 | chart-today | 柱状图 |
| 本周出入库对比 | chart-week | 柱状图 |
| 本月出入库对比 | chart-month | 柱状图 |
| 当前库存总量 | (数字卡片) | - |
| 库存库龄分布 | chart-stock-age | 饼图 |
| 各仓库库存分布 | chart-warehouse | 柱状图 |