SIGNALR

示例

后端代码

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);
    });
}
相关推荐
铁皮饭盒1 小时前
Bun 处理 IO 密集型比 Java 强多少?
javascript
罗超驿1 小时前
6.Java LinkedList深度解析:从链表原理到源码实践
java·开发语言·链表
anod1 小时前
Amazon反爬虫:一场从自动化到Chrome插件的折腾之旅
javascript·chrome·amazon·插件
2501_948106911 小时前
计算机毕业设计之jsp物业管理系统
java·大数据·开发语言·汽车·课程设计
逝水无殇2 小时前
C# 反射详解
开发语言·后端·c#
xlxxy_2 小时前
sap获取批次特性报表
java·linux·开发语言·前端·数据库·abap·mm
北冥you鱼2 小时前
Go语言常用包使用指南:从标准库到第三方利器
开发语言·microsoft·golang
俺不中嘞2 小时前
python常用函数
开发语言·python
欢喜躲在眉梢里2 小时前
用飞算Java做了一套新能源充电运营管理系统:把站点、设备、订单拆成可运营的微服务
java·开发语言·微服务·ai编程·技术分享·java开发·飞算javaai炫技赛
weedsfly2 小时前
前端开发中的代理模式——从 Vue 3 响应式到日常开发
前端·javascript·面试