示例
后端代码
Program.cs
csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using YourNamespace.Hubs;
using YourNamespace.Services;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// 1. 添加 SignalR 服务
builder.Services.AddSignalR();
// 2. 添加后台模拟服务
builder.Services.AddHostedService<MultiChartDataService>();
var app = builder.Build();
// 3.映射 Hub 路由
app.MapHub<MultiChartHub>("/multiChartHub");
app.Run();
MultiChartDataService.cs
csharp
// 1. Hub 类
using Google.Protobuf.WellKnownTypes;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting;
using System.Threading.Tasks;
using System.Threading;
using System;
public class MultiChartHub : Hub { }
// 2. 数据推送服务
public class MultiChartDataService : BackgroundService
{
private readonly IHubContext<MultiChartHub> _hubContext;
private readonly Random _random = new Random();
public MultiChartDataService(IHubContext<MultiChartHub> hubContext)
{
_hubContext = hubContext;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// 推送温度数据(单值时间序列格式)
await PushToChart(
chartId: "temperature-chart",
data: new
{
time = DateTime.Now.ToString("HH:mm:ss"),
value = Math.Round(50 + _random.NextDouble() * 30, 1)
}
);
// 推送销售数据(多系列格式)
await PushToChart(
chartId: "sales-chart",
data: new
{
today = new[] { 100 + _random.Next(50), 80 + _random.Next(40), 120 + _random.Next(60) },
yesterday = new[] { 90 + _random.Next(40), 70 + _random.Next(30), 110 + _random.Next(50) }
}
);
// 推送状态数据(饼图格式)
await PushToChart(
chartId: "status-chart",
data: new[] {
new { name = "运行中", value = 3 + _random.Next(2) },
new { name = "停机", value = 1 + _random.Next(1) },
new { name = "维护中", value = 0 + _random.Next(1) }
}
);
await Task.Delay(3000, stoppingToken); // 每3秒推送一次
}
}
// 通用推送方法(不关心数据格式,只负责传递)
private async Task PushToChart(string chartId, object data)
{
await _hubContext.Clients.All.SendAsync("UpdateChart", chartId, data);
}
}
前端代码
csharp
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>实时数据监控大屏</title>
<!-- 引入 ECharts -->
<script src="./echarts/echarts.js"></script>
<script src="./echarts/echarts.min.js"></script>
<!-- 引入 SignalR 客户端库 -->
<script src="./signalr/signalr.js"></script>
<script src="./signalr/signalr.min.js"></script>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f0f2f5;
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
h1 {
color: #333;
margin-bottom: 30px;
}
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
gap: 20px;
width: 100%;
max-width: 1400px;
}
.chart-container {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
padding: 20px;
height: 400px;
display: flex;
flex-direction: column;
}
.chart-container h2 {
margin: 0 0 15px 0;
font-size: 1.2em;
color: #555;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.chart {
flex-grow: 1;
width: 100%;
}
#connection-status {
position: fixed;
top: 10px;
right: 20px;
padding: 5px 10px;
border-radius: 4px;
font-size: 14px;
color: #fff;
background-color: #ccc;
}
.connected {
background-color: #52c41a;
}
.disconnected {
background-color: #ff4d4f;
}
</style>
</head>
<body>
<div id="connection-status">连接中...</div>
<h1>实时数据监控大屏</h1>
<div class="dashboard">
<div class="chart-container">
<h2>实时温度监控</h2>
<div id="temperature-chart" class="chart"></div>
</div>
<div class="chart-container">
<h2>销售额对比</h2>
<div id="sales-chart" class="chart"></div>
</div>
<div class="chart-container">
<h2>设备状态分布</h2>
<div id="status-chart" class="chart"></div>
</div>
</div>
<script>
// --- 1. 初始化 ECharts 实例 ---
const tempChart = echarts.init(document.getElementById('temperature-chart'));
const salesChart = echarts.init(document.getElementById('sales-chart'));
const statusChart = echarts.init(document.getElementById('status-chart'));
// --- 2. 定义图表配置 ---
// 温度图配置 (时间序列)
const tempOption = {
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: [], boundaryGap: false },
yAxis: { type: 'value', name: '温度 (°C)' },
series: [{
name: '温度',
type: 'line',
data: [],
smooth: true,
areaStyle: {},
itemStyle: { color: '#5470C6' }
}]
};
tempChart.setOption(tempOption);
// 销售图配置 (多系列柱状图)
const salesOption = {
tooltip: { trigger: 'axis' },
legend: { data: ['今日', '昨日'] },
xAxis: { type: 'category', data: ['品类A', '品类B', '品类C'] },
yAxis: { type: 'value', name: '销售额' },
series: [
{ name: '今日', type: 'bar', data: [], itemStyle: { color: '#91CC75' } },
{ name: '昨日', type: 'bar', data: [], itemStyle: { color: '#FAC858' } }
]
};
salesChart.setOption(salesOption);
// 状态图配置 (饼图)
const statusOption = {
tooltip: { trigger: 'item' },
legend: { orient: 'vertical', left: 'left' },
series: [{
name: '设备状态',
type: 'pie',
radius: '50%',
data: [],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
};
statusChart.setOption(statusOption);
// --- 3. 建立 SignalR 连接 ---
const connection = new signalR.HubConnectionBuilder()
.withUrl("/MultiChartHub") // 确保这里的 URL 与后端 MapHub 的路径一致
.withAutomaticReconnect()
.configureLogging(signalR.LogLevel.Information)
.build();
const statusDiv = document.getElementById('connection-status');
connection.onclose(async () => {
console.warn("SignalR 连接已关闭,正在尝试重连...");
statusDiv.textContent = "连接已断开";
statusDiv.className = "disconnected";
});
connection.onreconnected(() => {
console.log("SignalR 连接已恢复");
statusDiv.textContent = "已连接";
statusDiv.className = "connected";
});
// --- 4. 监听后端推送的 UpdateChart 方法 ---
connection.on("UpdateChart", (chartId, data) => {
console.log(`收到图表 ${chartId} 的数据:`, data);
switch (chartId) {
case 'temperature-chart':
// 处理温度数据 (单值时间序列)
const timeData = tempOption.xAxis.data;
const valueData = tempOption.series[0].data;
timeData.push(data.time);
valueData.push(data.value);
// 保持数据点数量,避免图表过长
if (timeData.length > 20) {
timeData.shift();
valueData.shift();
}
tempChart.setOption({
xAxis: { data: timeData },
series: [{ data: valueData }]
});
break;
case 'sales-chart':
// 处理销售数据 (多系列)
salesChart.setOption({
series: [
{ name: '今日', data: data.today },
{ name: '昨日', data: data.yesterday }
]
});
break;
case 'status-chart':
// 处理状态数据 (饼图)
statusChart.setOption({
series: [{ data: data }]
});
break;
}
});
// --- 5. 启动连接 ---
async function start() {
try {
await connection.start();
console.log("SignalR 连接成功");
statusDiv.textContent = "已连接";
statusDiv.className = "connected";
} catch (err) {
console.error(err);
statusDiv.textContent = "连接失败";
statusDiv.className = "disconnected";
setTimeout(start, 5000);
}
}
start();
</script>
</body>
</html>
前端代码1
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>产线设备实时监控</title>
<!-- 引入 SignalR 客户端库 -->
<script src="./signalr/signalr.min.js"></script>
<style>
/* --- 1. 页面整体风格:工业深蓝 --- */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #0b1120; /* 深蓝背景 */
color: #e2e8f0; /* 浅灰文字,护眼 */
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
h1 {
color: #fff;
margin-top: 30px;
margin-bottom: 40px;
font-weight: 300;
letter-spacing: 2px;
text-shadow: 0 0 10px rgba(59, 130, 246, 0.5);
}
/* 连接状态指示器 */
#connection-status {
position: fixed;
top: 20px;
right: 20px;
padding: 6px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
background-color: #334155;
color: #94a3b8;
z-index: 100;
}
#connection-status.connected {
background-color: #064e3b;
color: #6ee7b7;
box-shadow: 0 0 10px rgba(110, 231, 183, 0.3);
}
#connection-status.disconnected {
background-color: #7f1d1d;
color: #fca5a5;
box-shadow: 0 0 10px rgba(252, 165, 165, 0.3);
}
/* --- 2. 产线布局:自动换行 --- */
.production-line {
display: flex;
flex-wrap: wrap; /* 核心:自动换行 */
justify-content: center; /* 居中对齐 */
align-items: center;
gap: 20px; /* 卡片间距 */
width: 90%;
max-width: 1600px;
padding: 20px;
background-color: #111827; /* 比背景稍浅的容器底色 */
border-radius: 12px;
border: 1px solid #1f2937;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
}
/* --- 3. 设备卡片设计 --- */
.device-card {
position: relative;
width: 180px;
height: 100px;
background: linear-gradient(145deg, #1e293b, #0f172a);
border: 1px solid #334155;
border-left: 6px solid #64748b; /* 默认灰色左边框 */
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
padding-left: 20px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.5);
transition: all 0.3s ease;
color: #e2e8f0;
}
.device-name {
font-size: 18px;
font-weight: bold;
letter-spacing: 1px;
margin-bottom: 8px;
text-transform: uppercase;
}
.device-status {
font-size: 14px;
color: #94a3b8;
}
/* --- 4. 箭头连接符 --- */
.device-card::after {
content: '➔';
position: absolute;
right: -28px; /* 调整位置 */
top: 50%;
transform: translateY(-50%);
color: #475569;
font-size: 24px;
z-index: 2;
}
/* 去掉每行最后一个卡片的箭头(纯CSS很难完美实现,这里用JS辅助或接受视觉上的小瑕疵) */
/* 简单处理:让箭头颜色淡一点,不那么突兀 */
/* --- 5. 状态样式 --- */
/* 正常状态 */
.status-normal {
border-left-color: #10b981; /* 绿色 */
box-shadow: 0 0 15px rgba(16, 185, 129, 0.15);
}
.status-normal .device-status {
color: #6ee7b7;
}
/* 报警状态 */
.status-alarm {
border-left-color: #ef4444; /* 红色 */
background: linear-gradient(145deg, #451a1a, #280d0d);
animation: pulse-red 1.5s infinite;
}
.status-alarm .device-status {
color: #fca5a5;
font-weight: bold;
}
/* 呼叫状态 */
.status-call {
border-left-color: #f59e0b; /* 黄色 */
background: linear-gradient(145deg, #452e08, #281a05);
}
.status-call .device-status {
color: #fcd34d;
font-weight: bold;
}
/* 报警闪烁动画 */
@keyframes pulse-red {
0% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4);
}
70% {
box-shadow: 0 0 0 15px rgba(239, 68, 68, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0);
}
}
</style>
</head>
<body>
<div id="connection-status">连接中...</div>
<h1>SMT 产线实时监控看板</h1>
<!-- 产线容器 -->
<div class="production-line" id="lineContainer">
<!-- 卡片由 JS 生成 -->
</div>
<script>
const lineContainer = document.getElementById('lineContainer');
const statusDiv = document.getElementById('connection-status');
// --- SignalR 连接配置 ---
const connection = new signalR.HubConnectionBuilder()
.withUrl("/MultiChartHub") // 确保与后端路由一致
.withAutomaticReconnect()
.configureLogging(signalR.LogLevel.Information)
.build();
// --- 监听连接状态 ---
connection.onclose(() => {
statusDiv.textContent = "已断开";
statusDiv.className = "disconnected";
});
connection.onreconnected(() => {
statusDiv.textContent = "已连接";
statusDiv.className = "connected";
});
// --- 监听后端数据 ---
connection.on("UpdateChart", (chartId, data) => {
// 只处理产线数据
if (chartId === 'lineContainer') {
renderLine(data);
}
});
// --- 渲染函数 ---
function renderLine(devices) {
if (!Array.isArray(devices)) return;
// 排序
devices.sort((a, b) => (a.sortOrder ?? a.SortOrder ?? 0) - (b.sortOrder ?? b.SortOrder ?? 0));
lineContainer.innerHTML = '';
devices.forEach(device => {
const card = document.createElement('div');
// 兼容大小写字段
const status = device.status ?? device.Status;
const name = device.name ?? device.Name ?? "未知设备";
const statusText = device.statusText ?? device.StatusText ?? "状态未知";
let statusClass = 'status-normal';
if (status === 1 || status === "1") statusClass = 'status-alarm';
if (status === 2 || status === "2") statusClass = 'status-call';
card.className = `device-card ${statusClass}`;
card.innerHTML = `
<div class="device-name">${name}</div>
<div class="device-status">${statusText}</div>
`;
lineContainer.appendChild(card);
});
}
// --- 启动连接 ---
async function start() {
try {
await connection.start();
console.log("SignalR 连接成功");
statusDiv.textContent = "已连接";
statusDiv.className = "connected";
} catch (err) {
console.error(err);
statusDiv.textContent = "连接失败";
statusDiv.className = "disconnected";
setTimeout(start, 5000);
}
}
start();
</script>
</body>
</html>
bug
现象:前端显示不了数据
原因:原代码如下,device对象返回的属性名称和下面代码不符,用了大驼峰,而前端默认是小驼峰
csharp
function renderLine(devices) {
// 按照 SortOrder 从小到大排序(保证线头到线尾的顺序)
devices.sort((a, b) => a.SortOrder - b.SortOrder);
lineContainer.innerHTML = ''; // 清空当前显示
devices.forEach(device => {
const card = document.createElement('div');
// 根据 Status 决定 CSS 类名
let statusClass = 'status-normal';
if (device.Status === 1) statusClass = 'status-alarm';
if (device.Status === 2) statusClass = 'status-call';
card.className = `device-card ${statusClass}`;
card.innerHTML = `
<div class="device-name">${device.Name}</div>
<div class="device-status">${device.StatusText}</div>
`;
lineContainer.appendChild(card);
});
}
调试界面显示,可以看到devices里全部是小驼峰

而实际,后端代码是大驼峰,也就是说,是后端自动转为小驼峰了。
csharp
_devices = new List<LineDeviceModel>
{
new LineDeviceModel { Id = 1, Name = "上料机", SortOrder = 1, Status = 0 },
new LineDeviceModel { Id = 2, Name = "贴片机", SortOrder = 2, Status = 0 },
new LineDeviceModel { Id = 3, Name = "回流焊", SortOrder = 3, Status = 0 },
new LineDeviceModel { Id = 4, Name = "AOI检测", SortOrder = 4, Status = 0 },
new LineDeviceModel { Id = 5, Name = "分板机", SortOrder = 5, Status = 0 },
new LineDeviceModel { Id = 6, Name = "下料机", SortOrder = 6, Status = 0 },
};
解决:前端属性改为小驼峰
csharp
function renderLine(devices) {
const lineContainer = document.getElementById('lineContainer');
// 按照 SortOrder 从小到大排序(保证线头到线尾的顺序)
devices.sort((a, b) => a.sortOrder - b.sortOrder);
lineContainer.innerHTML = ''; // 清空当前显示
devices.forEach(
device => {
const card = document.createElement('div');
// 根据 Status 决定 CSS 类名
let statusClass = 'status-normal';
if (device.status === 1) statusClass = 'status-alarm';
if (device.status === 2) statusClass = 'status-call';
card.className = `device-card ${statusClass}`;
card.innerHTML = `
<div class="device-name">${device.name}</div>
<div class="device-status">${device.statusText}</div>
`;
lineContainer.appendChild(card);
});
}