开发数据看板的几种方式

1.走 ASP.NET Core 中间层:在 WPF 进程内嵌一个 ASP.NET Core 主机,看板页面请求这个本地服务,由本地服务再去调用 WMS/EMS 的 API。这样浏览器层面始终是同源(都是 http://localhost:某端口),跨域问题在服务端用 HttpClient 解决,不受浏览器限制。模式一:ASP.NET Core + WebView2 集成

2.用 WebView2 的宿主对象通信:通过 WebView2 的 AddHostObjectToScript 或 WebMessage 机制,让 HTML 页面把请求交给 WPF 的 C# 代码,由 C# 用 HttpClient 去调用外部系统。这样完全绕开浏览器跨域,但通信方式不再是纯 HTTP 请求,而是走宿主桥接。

示例:这套完整案例,核心通信机制就是 AddHostObjectToScript,同时也混用了 WebMessage 通道作为补充,实现双向通讯。

<Window x:Class="WebView2DashboardDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WebView2DashboardDemo"
        mc:Ignorable="d"
        xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
        Title="产线实时监控看板" Height="900" Width="1600"
        Background="#0a111f" WindowStartupLocation="CenterScreen">
    <Grid>
        <wv2:WebView2 x:Name="webView" />
        <Border x:Name="loadingMask" Background="#0a111f">
            <TextBlock Text="看板加载中..." Foreground="#22d3c5"
                       FontSize="18" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Border>
    </Grid>
</Window>
View Code
 /// <summary>
 /// MainWindow.xaml 的交互逻辑
 /// </summary>
 public partial class MainWindow : Window
 {
     private DashboardBridge _bridge;
     private bool _pageReady = false;
     private string _pendingPush = null;
     public MainWindow()
     {
         InitializeComponent();
         InitializeWebView();
     }
     private async void InitializeWebView()
     {
         await webView.EnsureCoreWebView2Async();

         // 1. 允许 file:// 页面访问宿主对象
         webView.CoreWebView2.Settings.IsWebMessageEnabled = true;
         webView.CoreWebView2.Settings.AreHostObjectsAllowed = true;

         // 2. 创建桥接对象并注册到 JS
         _bridge = new DashboardBridge
         {
             PushToJs = payload =>
             {
                 // 页面没准备好时缓存,等 NavigationCompleted 后补发
                 if (!_pageReady) { _pendingPush = payload; return; }
                 // 用 PostWebMessageAsJson 推送,比 ExecuteScriptAsync 更安全
                 webView.CoreWebView2.PostWebMessageAsJson(payload);
             }
         };
         webView.CoreWebView2.AddHostObjectToScript("bridge", _bridge);

         // 3. 订阅 JS 发来的消息(postMessage 通道)
         webView.CoreWebView2.WebMessageReceived += OnWebMessageReceived;

         // 4. 页面加载完成
         webView.NavigationCompleted += OnNavigationCompleted;

         // 5. 加载本地 HTML
         string htmlPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"Assets\dashboard.html");
         webView.Source = new Uri(htmlPath);
     }

     private async void OnNavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e)
     {
         if (!e.IsSuccess) return;
         _pageReady = true;
         loadingMask.Visibility = Visibility.Collapsed;

         // 补发缓存的数据
         if (_pendingPush != null)
         {
             webView.CoreWebView2.PostWebMessageAsJson(_pendingPush);
             _pendingPush = null;
         }

         // 推一次初始快照
         string init = _bridge.GetRealtimeData();
         webView.CoreWebView2.PostWebMessageAsJson(init);

         await System.Threading.Tasks.Task.CompletedTask;
     }

     private void OnWebMessageReceived(object sender, CoreWebView2WebMessageReceivedEventArgs e)
     {
         // JS 通过 window.chrome.webview.postMessage(...) 发来的消息
         string json = e.WebMessageAsJson;
         // 这里可以按业务解析,比如 JS 通知 C# 记录日志、写库等
         System.Diagnostics.Debug.WriteLine($"[JS→C#] {json}");
     }
 }
View Code
/// <summary>
/// 暴露给 JavaScript 的桥接对象
/// JS 通过 window.chrome.webview.hostObjects.bridge 调用这里的方法
/// </summary>
/// 
[ClassInterface(ClassInterfaceType.AutoDual)]
[ComVisible(true)]
public class DashboardBridge
{
    private readonly Random _rand = new Random();
    private readonly DispatcherTimer _timer;
    private string _currentLine = "A";

    /// <summary>
    /// C# 主动向 JS 推送数据的委托(由 MainWindow 注入)
    /// </summary>
    public Action<string> PushToJs { get; set; }

    public DashboardBridge()
    {
        // 每 2 秒主动推送一次模拟数据
        _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
        _timer.Tick += (s, e) => PushToJs?.Invoke(BuildPayload());
        _timer.Start();
    }

    /// <summary>
    /// JS 主动调用:获取当前产线快照
    /// </summary>
    public string GetRealtimeData()
    {
        return BuildPayload();
    }

    /// <summary>
    /// JS 主动调用:切换产线,返回切换后的快照
    /// </summary>
    public string SwitchLine(string line)
    {
        _currentLine = line;
        return BuildPayload();
    }

    /// <summary>
    /// JS 主动调用:获取历史趋势(示例)
    /// </summary>
    public string GetTrend(int points)
    {
        var list = new List<object>();
        for (int i = 0; i < points; i++)
        {
            list.Add(new
            {
                time = DateTime.Now.AddSeconds(-2 * (points - i)).ToString("HH:mm:ss"),
                value = _rand.Next(800, 1500)
            });
        }
        return JsonSerializer.Serialize(list);
    }

    /// <summary>
    /// JS 主动调用:确认告警(示例业务动作)
    /// </summary>
    public string AcknowledgeAlarm(int alarmId)
    {
        // 真实场景:写数据库 / 调 MES 接口
        return JsonSerializer.Serialize(new { success = true, alarmId, time = DateTime.Now });
    }

    private string BuildPayload()
    {
        // 真实场景:调 WMS 接口拿库存,调 MES 拿工单
        //var data = _httpClient.GetStringAsync("http://wms-server/api/line/A").Result;
        //return data;
        var payload = new
        {
            line = _currentLine,
            output = _rand.Next(800, 1500),
            yield = Math.Round(_rand.NextDouble() * 5 + 95, 2),
            alarm = _rand.Next(0, 6),
            oee = Math.Round(_rand.NextDouble() * 10 + 82, 2),
            stations = new[] { _rand.Next(250, 450), _rand.Next(250, 450), _rand.Next(250, 450), _rand.Next(250, 450) },
            bad = _rand.Next(5, 30),
            pending = _rand.Next(3, 15),
            devices = new[] { _rand.Next(0, 2) == 1, _rand.Next(0, 2) == 1, _rand.Next(0, 2) == 1 },
            alarms = new[]
            {
                new { msg = "上料机械手扭矩波动", level = "warn",  time = DateTime.Now.AddMinutes(-1).ToString("HH:mm:ss") },
                new { msg = "2号工位视觉定位偏差", level = "error", time = DateTime.Now.AddMinutes(-3).ToString("HH:mm:ss") }
            }
        };
        return JsonSerializer.Serialize(payload);
    }
}
View Code
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>产线实时监控看板</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.6.0/dist/echarts.min.js"></script>
<style>
  :root{
    --bg:#0a111f; --panel:#111b2d; --panel-2:#0e1728; --line:#1e2d47;
    --text:#e8f0fc; --dim:#8aa0c0; --cyan:#22d3c5; --blue:#5aa0ff;
    --green:#34d399; --red:#f87171; --amber:#fbbf24;
  }
  *{margin:0;padding:0;box-sizing:border-box}
  html,body{height:100%}
  body{font-family:"PingFang SC","Microsoft YaHei",system-ui,sans-serif;
       background:var(--bg);color:var(--text);padding:16px;min-height:100vh}
  .topbar{display:flex;align-items:center;justify-content:space-between;
          padding:10px 18px;margin-bottom:14px;background:var(--panel);
          border:1px solid var(--line);position:relative}
  .topbar::before,.topbar::after{content:"";position:absolute;width:14px;height:14px;border:1px solid var(--cyan)}
  .topbar::before{top:-1px;left:-1px;border-right:none;border-bottom:none}
  .topbar::after{bottom:-1px;right:-1px;border-left:none;border-top:none}
  .title h1{font-size:22px;font-weight:700;letter-spacing:2px}
  .title p{font-size:11px;color:var(--dim);margin-top:3px;letter-spacing:1px}
  .tabs{display:flex;gap:8px}
  .tab{padding:7px 18px;cursor:pointer;font-size:13px;color:var(--dim);
       border:1px solid var(--line);background:transparent;transition:all .2s}
  .tab:hover{color:var(--text);border-color:var(--cyan)}
  .tab.active{color:#062a26;background:var(--cyan);border-color:var(--cyan);font-weight:600}
  .clock{font-size:14px;color:var(--cyan);font-variant-numeric:tabular-nums;text-align:right}
  .clock span{display:block;font-size:11px;color:var(--dim);margin-top:2px}
  .kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-bottom:14px}
  .kpi{background:var(--panel);border:1px solid var(--line);padding:14px 16px;position:relative;overflow:hidden}
  .kpi .label{font-size:12px;color:var(--dim)}
  .kpi .value{font-size:30px;font-weight:700;margin-top:8px;font-variant-numeric:tabular-nums;line-height:1}
  .kpi .sub{font-size:11px;color:var(--dim);margin-top:8px}
  .kpi .tick{position:absolute;top:0;left:0;width:3px;height:100%;background:var(--cyan)}
  .kpi.alert .tick{background:var(--red)}
  .kpi.alert .value{color:var(--red)}
  .grid{display:grid;grid-template-columns:1.9fr 1fr;gap:12px;margin-bottom:12px}
  .grid-bottom{display:grid;grid-template-columns:1.4fr 1fr 1fr;gap:12px}
  .panel{background:var(--panel);border:1px solid var(--line);padding:14px 16px;position:relative}
  .panel h2{font-size:14px;font-weight:600;margin-bottom:4px;display:flex;align-items:center;gap:8px}
  .panel h2::before{content:"";width:3px;height:14px;background:var(--cyan);display:inline-block}
  .panel .note{font-size:11px;color:var(--dim);margin-bottom:8px}
  .chart{width:100%;min-height:300px}
  .alarms{display:flex;flex-direction:column;gap:8px;overflow:hidden}
  .alarm-row{display:flex;align-items:center;gap:10px;padding:9px 10px;
             background:var(--panel-2);border-left:3px solid var(--amber);font-size:12px}
  .alarm-row.error{border-left-color:var(--red)}
  .alarm-row .time{color:var(--dim);font-variant-numeric:tabular-nums;white-space:nowrap}
  .alarm-row .msg{color:var(--text);flex:1}
  .alarm-row .level{font-size:10px;padding:2px 6px;white-space:nowrap}
  .alarm-row .level.warn{background:rgba(251,191,36,.15);color:var(--amber)}
  .alarm-row .level.error{background:rgba(248,113,113,.15);color:var(--red)}
  .alarm-row .ack{cursor:pointer;font-size:10px;color:var(--cyan);
                  border:1px solid var(--cyan);padding:1px 6px;background:transparent}
  .alarm-row .ack:hover{background:var(--cyan);color:#062a26}
  .mock-tag{font-size:10px;color:var(--amber);border:1px dashed var(--amber);
            padding:1px 6px;letter-spacing:1px}
  #bridgeStatus{font-size:11px;color:var(--dim);margin-left:8px}
</style>
</head>
<body>

<div class="topbar">
  <div class="title">
    <h1>产线实时监控中心</h1>
    <p>INDUSTRIAL LINE MONITOR <span class="mock-tag">BRIDGE · DEMO</span>
       <span id="bridgeStatus">桥接状态:检测中…</span></p>
  </div>
  <div class="tabs" id="lineTabs">
    <button class="tab active" data-line="A">产线 A</button>
    <button class="tab" data-line="B">产线 B</button>
    <button class="tab" data-line="C">产线 C</button>
  </div>
  <div class="clock">
    <span id="clockDate"></span>
    <span id="clockTime"></span>
  </div>
</div>

<div class="kpis">
  <div class="kpi"><div class="tick"></div>
    <div class="label">今日产量</div>
    <div class="value" id="kpiOutput">--</div>
    <div class="sub" id="kpiOutputSub">班次目标完成率 --</div>
  </div>
  <div class="kpi"><div class="tick"></div>
    <div class="label">综合良率</div>
    <div class="value" id="kpiYield">--</div>
    <div class="sub" id="kpiYieldSub">不良数 --</div>
  </div>
  <div class="kpi alert"><div class="tick"></div>
    <div class="label">在产告警</div>
    <div class="value" id="kpiAlarm">--</div>
    <div class="sub" id="kpiAlarmSub">其中严重 --</div>
  </div>
  <div class="kpi"><div class="tick"></div>
    <div class="label">设备 OEE</div>
    <div class="value" id="kpiOee">--</div>
    <div class="sub">稼动率 --</div>
  </div>
</div>

<div class="grid">
  <div class="panel">
    <h2>实时产量趋势</h2>
    <div class="note">最近 30 个采样点 · 每 2 秒刷新</div>
    <div id="trendChart" class="chart"></div>
  </div>
  <div class="panel">
    <h2>实时告警列表 <button id="btnAckAll" style="float:right;font-size:11px;color:var(--cyan);
        border:1px solid var(--cyan);background:transparent;padding:2px 8px;cursor:pointer">全部确认</button></h2>
    <div class="note" id="alarmNote">点击“确认”通过桥接调用 C#</div>
    <div class="alarms" id="alarmList"></div>
  </div>
</div>

<div class="grid-bottom">
  <div class="panel"><h2>工位产量对比</h2><div class="note">各工位当日产量</div>
    <div id="barChart" class="chart"></div></div>
  <div class="panel"><h2>质量构成</h2><div class="note">良品 / 不良 / 待检</div>
    <div id="pieChart" class="chart"></div></div>
  <div class="panel"><h2>设备状态</h2><div class="note">关键设备运行状态</div>
    <div id="devicePanel"></div></div>
</div>

<script>
/* ==================== 桥接对象获取 ==================== */
let bridge = null;

function initBridge() {
  return new Promise((resolve) => {
    const tryGet = () => {
      if (window.chrome?.webview?.hostObjects?.bridge) {
        bridge = window.chrome.webview.hostObjects.bridge;
        document.getElementById('bridgeStatus').textContent = '桥接状态:已连接 ✅';
        resolve(bridge);
      } else {
        setTimeout(tryGet, 100);
      }
    };
    tryGet();
  });
}

/* ==================== 图表初始化 ==================== */
let trendChart, barChart, pieChart;
const MAX_POINTS = 30;
let trendData = [];

function initCharts() {
  trendChart = echarts.init(document.getElementById('trendChart'));
  trendChart.setOption({
    grid:{left:45,right:20,top:20,bottom:30},
    tooltip:{trigger:'axis',backgroundColor:'#111b2d',borderColor:'#1e2d47',textStyle:{color:'#e8f0fc'}},
    xAxis:{type:'category',boundaryGap:false,data:[],axisLine:{lineStyle:{color:'#1e2d47'}},axisLabel:{color:'#8aa0c0',fontSize:10}},
    yAxis:{type:'value',splitLine:{lineStyle:{color:'#1e2d47',type:'dashed'}},axisLabel:{color:'#8aa0c0',fontSize:10}},
    series:[{type:'line',smooth:true,symbol:'circle',symbolSize:5,data:[],
      lineStyle:{width:2,color:'#22d3c5'},itemStyle:{color:'#22d3c5'},
      areaStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[
        {offset:0,color:'rgba(34,211,197,0.35)'},{offset:1,color:'rgba(34,211,197,0.02)'}])}}]
  });

  barChart = echarts.init(document.getElementById('barChart'));
  barChart.setOption({
    grid:{left:40,right:15,top:20,bottom:30},
    tooltip:{trigger:'axis',backgroundColor:'#111b2d',borderColor:'#1e2d47',textStyle:{color:'#e8f0fc'}},
    xAxis:{type:'category',data:['工位1','工位2','工位3','工位4'],axisLine:{lineStyle:{color:'#1e2d47'}},axisLabel:{color:'#8aa0c0',fontSize:10}},
    yAxis:{type:'value',splitLine:{lineStyle:{color:'#1e2d47',type:'dashed'}},axisLabel:{color:'#8aa0c0',fontSize:10}},
    series:[{type:'bar',data:[],barWidth:'45%',
      itemStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[
        {offset:0,color:'#5aa0ff'},{offset:1,color:'#22d3c5'}]),borderRadius:[3,3,0,0]},
      label:{show:true,position:'top',color:'#8aa0c0',fontSize:10}}]
  });

  pieChart = echarts.init(document.getElementById('pieChart'));
  pieChart.setOption({
    tooltip:{trigger:'item',backgroundColor:'#111b2d',borderColor:'#1e2d47',textStyle:{color:'#e8f0fc'}},
    legend:{bottom:0,textStyle:{color:'#8aa0c0',fontSize:11}},
    series:[{type:'pie',radius:['48%','70%'],center:['50%','45%'],
      itemStyle:{borderColor:'#111b2d',borderWidth:2},
      label:{color:'#e8f0fc',fontSize:11,formatter:'{b}\n{d}%'},
      labelLine:{lineStyle:{color:'#1e2d47'}},
      data:[{value:0,name:'良品',itemStyle:{color:'#34d399'}},
            {value:0,name:'不良',itemStyle:{color:'#f87171'}},
            {value:0,name:'待检',itemStyle:{color:'#fbbf24'}}]}]
  });

  window.addEventListener('resize', () => {
    trendChart.resize(); barChart.resize(); pieChart.resize();
  });
}

/* ==================== 时钟 ==================== */
function updateClock() {
  const now = new Date();
  const week = ['日','一','二','三','四','五','六'][now.getDay()];
  document.getElementById('clockDate').textContent =
    `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')} 周${week}`;
  document.getElementById('clockTime').textContent =
    `${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`;
}
setInterval(updateClock, 1000);

/* ==================== 渲染函数 ==================== */
function renderAll(d) {
  document.getElementById('kpiOutput').textContent = d.output ?? '--';
  document.getElementById('kpiOutputSub').textContent =
    `班次目标完成率 ${d.output ? ((d.output/1500)*100).toFixed(1) : '--'}%`;
  document.getElementById('kpiYield').textContent = d.yield != null ? d.yield + '%' : '--';
  document.getElementById('kpiYieldSub').textContent = `不良数 ${d.bad ?? '--'}`;
  document.getElementById('kpiAlarm').textContent = d.alarm ?? '--';
  const severe = (d.alarms||[]).filter(a=>a.level==='error').length;
  document.getElementById('kpiAlarmSub').textContent = `其中严重 ${severe}`;
  document.getElementById('kpiOee').textContent = d.oee != null ? d.oee + '%' : '--';

  if (d.output != null) {
    const now = new Date();
    trendData.push({
      label:`${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`,
      value:d.output
    });
    if (trendData.length > MAX_POINTS) trendData.shift();
    trendChart.setOption({
      xAxis:{data:trendData.map(p=>p.label)},
      series:[{data:trendData.map(p=>p.value)}]
    });
  }

  if (d.stations) barChart.setOption({ series:[{data:d.stations}] });

  if (d.output != null) {
    const good = Math.max(d.output - (d.bad||0) - (d.pending||0), 0);
    pieChart.setOption({ series:[{data:[
      {value:good,name:'良品',itemStyle:{color:'#34d399'}},
      {value:d.bad||0,name:'不良',itemStyle:{color:'#f87171'}},
      {value:d.pending||0,name:'待检',itemStyle:{color:'#fbbf24'}}]}]});
  }

  if (d.devices) {
    const names = ['贴片机','回流焊','AOI'];
    document.getElementById('devicePanel').innerHTML = d.devices.map((ok,i)=>`
      <div style="display:flex;align-items:center;gap:10px;padding:10px;margin-bottom:8px;
                  background:var(--panel-2);border-left:3px solid ${ok?'var(--green)':'var(--red)'}">
        <span style="width:8px;height:8px;border-radius:50%;background:${ok?'var(--green)':'var(--red)'};
                     box-shadow:0 0 8px ${ok?'var(--green)':'var(--red)'}"></span>
        <span style="flex:1;font-size:13px">${names[i]||'设备'+(i+1)}</span>
        <span style="font-size:12px;color:${ok?'var(--green)':'var(--red)'}">${ok?'运行中':'停机'}</span>
      </div>`).join('');
  }

  if (d.alarms) renderAlarms(d.alarms);
  if (d.line) document.getElementById('alarmNote').textContent = `产线 ${d.line} · 点击“确认”调用 C#`;
}

function renderAlarms(alarms) {
  const box = document.getElementById('alarmList');
  if (!alarms || alarms.length === 0) {
    box.innerHTML = `<div style="color:var(--dim);font-size:12px;padding:12px 4px">当前无告警</div>`;
    return;
  }
  box.innerHTML = alarms.map((a,i)=>`
    <div class="alarm-row ${a.level==='error'?'error':''}">
      <span class="time">${a.time||'--:--:--'}</span>
      <span class="msg">${a.msg||''}</span>
      <span class="level ${a.level==='error'?'error':'warn'}">${a.level==='error'?'严重':'警告'}</span>
      <button class="ack" data-idx="${i}">确认</button>
    </div>`).join('');

  box.querySelectorAll('.ack').forEach(btn=>{
    btn.onclick = async () => {
      if (!bridge) return;
      // ★ 关键:JS 通过桥接主动调用 C# 方法
      const res = await bridge.AcknowledgeAlarm(parseInt(btn.dataset.idx));
      btn.textContent = '已确认';
      btn.disabled = true;
      btn.style.opacity = 0.5;
      console.log('[JS→C#] AcknowledgeAlarm 返回:', res);
    };
  });
}

/* ==================== C# 推送数据接收(PostWebMessageAsJson) ==================== */
window.chrome?.webview?.addEventListener('message', (e) => {
  // e.data 就是 C# PostWebMessageAsJson 推送的 JSON 对象
  renderAll(e.data);
});

/* ==================== 产线切换:通过桥接调用 C# ==================== */
document.getElementById('lineTabs').addEventListener('click', async (e) => {
  const btn = e.target.closest('.tab');
  if (!btn) return;
  document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
  btn.classList.add('active');
  const line = btn.dataset.line;

  trendData = [];
  trendChart.setOption({ xAxis:{data:[]}, series:[{data:[]}] });

  if (bridge) {
    // ★ JS 主动调用 C# 的 SwitchLine,返回该产线快照
    const json = await bridge.SwitchLine(line);
    renderAll(JSON.parse(json));
    // 同时通过 postMessage 通知 C#(走 WebMessageReceived 通道)
    window.chrome.webview.postMessage({ type:'switchLine', line });
  }
});

/* ==================== 全部确认 ==================== */
document.getElementById('btnAckAll').onclick = async () => {
  if (!bridge) return;
  const res = await bridge.AcknowledgeAlarm(-1);
  console.log('全部确认返回:', res);
  document.querySelectorAll('.ack').forEach(b=>{ b.textContent='已确认'; b.disabled=true; });
};

/* ==================== 启动 ==================== */
(async function boot() {
  initCharts();
  updateClock();
  await initBridge();          // 等待桥接就绪
  if (bridge) {
    // 首屏主动拉一次数据
    const json = await bridge.GetRealtimeData();
    renderAll(JSON.parse(json));
  }
})();
</script>
</body>
</html>
View Code

通道一:AddHostObjectToScript(宿主对象桥接)这条通道负责 JS → C# 的“请求-响应”,

//CSharp
webView.CoreWebView2.AddHostObjectToScript("bridge", _bridge);
//js
const bridge = window.chrome.webview.hostObjects.bridge;
const json = await bridge.GetRealtimeData();   // JS 主动调 C#
const snapshot = await bridge.SwitchLine('B'); // 带参数、有返回值
await bridge.AcknowledgeAlarm(3);              // 触发业务动作
View Code

通道二:WebMessage(消息通道)负责 C# → JS 的定时推送,以及 JS → C# 的轻量通知。

//csharp
webView.CoreWebView2.PostWebMessageAsJson(payload);        // C# → JS
webView.CoreWebView2.WebMessageReceived += OnWebMessageReceived; // JS → C#

//js
window.chrome.webview.addEventListener('message', e => renderAll(e.data)); // 收推送
window.chrome.webview.postMessage({ type:'switchLine', line });            // 发通知,无需数据返回
View Code

 

解析:

DashboardBridge 类是C#和JavaScript之间专门建立的一个通信中转对象,他让HTML页面中的JS代码能够像调用本地函数一样直接调用C#方法。

DashboardBridge 被注册到 WebView2 后,JS 侧就能通过一个固定的入口访问它:

//javascript中的固定入口
const bridge = window.chrome.webview.hostObjects.bridge;

//bridge 这个名字,就是你调用 AddHostObjectToScript("bridge", _bridge) 时起的
//js中可以执行操作让C#执行
// JS 主动调用 C# 方法,拿返回值
const json = await bridge.GetRealtimeData();
renderAll(JSON.parse(json));

// JS 触发一个业务动作,让 C# 去处理
await bridge.AcknowledgeAlarm(3);

// JS 告诉 C# 切换产线
const snapshot = await bridge.SwitchLine('B');

//对应到 C# 侧,就是 DashboardBridge 类里的这几个方法
public string GetRealtimeData() { ... }
public string SwitchLine(string line) { ... }
public string AcknowledgeAlarm(int alarmId) { ... }
View Code

他解决了什么问题?

1. 让前端能“主动拉数据”

看板刚打开、或者用户手动刷新时,JS 不需要等 C# 的定时器,直接 await bridge.GetRealtimeData() 就能拿到最新快照。

2. 让前端能“触发业务动作”

告警列表里每一条后面的“确认”按钮,点击后 JS 调用 bridge.AcknowledgeAlarm(idx),C# 收到后可以去写数据库、调 MES 接口、记日志。这些操作 JS 自己做不了,必须交给 C#。

3. 把外部系统调用封装在 C# 侧

这是最关键的一点。假设你要对接 WMS 或 EMS,真实的数据来自它们的 HTTP API。你可以在 DashboardBridge 里用 HttpClient 去调这些接口:

public string GetRealtimeData()
{
    // 真实场景:调 WMS 接口拿库存,调 MES 拿工单
    var data = _httpClient.GetStringAsync("http://wms-server/api/line/A").Result;
    return data;
}

JS 侧完全不用关心 WMS 的地址、认证、跨域问题,它只管 await bridge.GetRealtimeData()。桥接层把“前端展示”和“后端集成”彻底隔离开了。

HTML如何接收到C#的数据呢?

利用PostWebMessageAsJson 推送的数据,在 dashboard.html 里是通过 window.chrome.webview 的 message 事件接收的。对应你 HTML 里这一段:

/* ==================== C# 推送数据接收(PostWebMessageAsJson)addEventListener 要尽早注册,否则 C# 在页面还没注册监听时就推送,消息会丢失 ==================== */
window.chrome?.webview?.addEventListener('message', (e) => {
  // e.data 就是 C# PostWebMessageAsJson 推送的 JSON 对象
  renderAll(e.data);//e.data是对象不是字符串
});

 

boot()的作用?

dashboard.html 里,boot() 是整个页面的启动入口函数。它负责在页面加载完成后,按正确的顺序把看板“点燃”——初始化图表、启动时钟、建立桥接、拉取首屏数据

 

dashboard.html 中主要的交互:

  • addEventListener('message', ...) 接收 C# 推送

  • await bridge.SwitchLine(line) 调用 C# 方法

  • window.chrome.webview.postMessage({...}) 通知 C#

 PostWebMessageAsJson 和 postMessage 的区别?

image

 

window.chrome?.webview?.addEventListener('message', (e) => {
  const data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;
  renderAll(data);
});js中可以加兼容

 

区别:

ASP.NET Core 中间层是把“Web 开发”的完整能力搬进了 WPF,适合逻辑复杂、需要复用 Web 生态的场景;而 WebView2 宿主对象则是把“C# 能力”直接嫁接给了 JavaScript,适合轻量、直接、追求极致性能的本地交互。

 

posted @ 2026-09-17 22:26  HelloWorld庄先生  阅读(18)  评论(0)    收藏  举报