You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
haoliang-net/src/CncSimulator/Admin/AdminHandler.cs

275 lines
22 KiB
C#

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

using System.Text;
using CncSimulator.Core;
namespace CncSimulator.Admin
{
/// <summary>
/// 管理页面HTML生成器。
/// 生成总管理页面和单地址管理页面的完整HTML+CSS+JS。
/// </summary>
public class AdminHandler
{
/// <summary>生成总管理页面(网关页面)</summary>
public string BuildGatewayPage(SimulatorEngine engine)
{
var sb = new StringBuilder();
sb.AppendLine("<!DOCTYPE html>");
sb.AppendLine("<html lang='zh-CN'><head><meta charset='utf-8'>");
sb.AppendLine("<meta name='viewport' content='width=device-width, initial-scale=1'>");
sb.AppendLine("<title>CNC 模拟采集服务 - 总管理</title>");
sb.AppendLine("<style>");
sb.AppendLine("* { margin:0; padding:0; box-sizing:border-box; }");
sb.AppendLine("body { font-family:'Microsoft YaHei',sans-serif; background:#f0f2f5; color:#333; }");
sb.AppendLine(".header { background:#1890ff; color:#fff; padding:16px 24px; display:flex; justify-content:space-between; align-items:center; }");
sb.AppendLine(".header h1 { font-size:20px; font-weight:600; }");
sb.AppendLine(".header-actions button { margin-left:8px; padding:6px 16px; border:none; border-radius:4px; cursor:pointer; font-size:14px; }");
sb.AppendLine(".btn-start { background:#52c41a; color:#fff; }");
sb.AppendLine(".btn-stop { background:#ff4d4f; color:#fff; }");
sb.AppendLine(".container { max-width:1200px; margin:20px auto; padding:0 20px; }");
sb.AppendLine(".section { background:#fff; border-radius:8px; padding:20px; margin-bottom:16px; box-shadow:0 1px 4px rgba(0,0,0,0.1); }");
sb.AppendLine(".section h2 { font-size:16px; margin-bottom:12px; padding-bottom:8px; border-bottom:1px solid #e8e8e8; }");
sb.AppendLine("table { width:100%; border-collapse:collapse; }");
sb.AppendLine("th,td { padding:10px 12px; text-align:left; border-bottom:1px solid #f0f0f0; font-size:14px; }");
sb.AppendLine("th { background:#fafafa; font-weight:600; color:#666; }");
sb.AppendLine(".status-dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:6px; }");
sb.AppendLine(".status-running { background:#52c41a; }");
sb.AppendLine(".status-stopped { background:#d9d9d9; }");
sb.AppendLine(".btn-link { color:#1890ff; cursor:pointer; text-decoration:none; border:none; background:none; font-size:14px; }");
sb.AppendLine(".log-area { max-height:300px; overflow-y:auto; font-family:Consolas,monospace; font-size:13px; line-height:1.8; color:#666; }");
sb.AppendLine("</style></head><body>");
sb.AppendLine("<div class='header'>");
sb.AppendLine(" <h1>CNC 模拟采集服务</h1>");
sb.AppendLine(" <div class='header-actions'>");
sb.AppendLine(" <button class='btn-start' onclick='startAll()'>全部启动</button>");
sb.AppendLine(" <button class='btn-stop' onclick='stopAll()'>全部停止</button>");
sb.AppendLine(" </div>");
sb.AppendLine("</div>");
sb.AppendLine("<div class='container'>");
sb.AppendLine(" <div class='section'><h2>地址列表</h2>");
sb.AppendLine(" <table><thead><tr><th>名称</th><th>端口</th><th>状态</th><th>设备数</th><th>数据频率</th><th>请求次数</th><th>操作</th></tr></thead>");
sb.AppendLine(" <tbody id='addressTable'></tbody></table>");
sb.AppendLine(" </div>");
sb.AppendLine(" <div class='section'><h2>控制台日志</h2>");
sb.AppendLine(" <div class='log-area' id='logArea'>加载中...</div>");
sb.AppendLine(" </div>");
sb.AppendLine("</div>");
sb.AppendLine("<script>");
sb.AppendLine("var logLines=[];var maxLogs=50;");
sb.AppendLine("function refresh(){");
sb.AppendLine(" fetch('/admin/api/status').then(r=>r.json()).then(data=>{");
sb.AppendLine(" var tb=document.getElementById('addressTable');tb.innerHTML='';");
sb.AppendLine(" data.forEach(function(a){");
sb.AppendLine(" var st=a.isRunning?'<span class=\"status-dot status-running\"></span>运行中':'<span class=\"status-dot status-stopped\"></span>已停止';");
sb.AppendLine(" tb.innerHTML+='<tr><td>'+a.name+'</td><td>'+a.port+'</td><td>'+st+'</td><td>'+a.onlineDevices+'/'+a.totalDevices+'台</td><td>'+a.dataChangeInterval+'秒</td><td>'+a.requestCount+'</td><td><a class=\"btn-link\" href=\"http://localhost:'+a.port+'/admin\" target=\"_blank\">管理</a></td></tr>';");
sb.AppendLine(" });");
sb.AppendLine(" }).catch(function(){});");
sb.AppendLine("}");
sb.AppendLine("function addLog(msg){var now=new Date();var ts=now.toTimeString().substr(0,8);logLines.unshift(ts+' '+msg);if(logLines.length>maxLogs)logLines.length=maxLogs;document.getElementById('logArea').innerHTML=logLines.join('<br>');}");
sb.AppendLine("function startAll(){fetch('/admin/api/start',{method:'POST'}).then(function(){addLog('全部启动');refresh()});}");
sb.AppendLine("function stopAll(){fetch('/admin/api/stop',{method:'POST'}).then(function(){addLog('全部停止');refresh()});}");
sb.AppendLine("setInterval(refresh,2000);refresh();");
sb.AppendLine("</script></body></html>");
return sb.ToString();
}
/// <summary>生成单地址管理页面</summary>
public string BuildSingleAddressPage(SimulatorServer server)
{
var sb = new StringBuilder();
sb.AppendLine("<!DOCTYPE html>");
sb.AppendLine("<html lang='zh-CN'><head><meta charset='utf-8'>");
sb.AppendLine("<meta name='viewport' content='width=device-width, initial-scale=1'>");
sb.AppendLine("<title>" + server.Name + " - 模拟管理</title>");
sb.AppendLine("<style>");
sb.AppendLine("* { margin:0; padding:0; box-sizing:border-box; }");
sb.AppendLine("body { font-family:'Microsoft YaHei',sans-serif; background:#f0f2f5; color:#333; }");
sb.AppendLine(".header { background:#1890ff; color:#fff; padding:16px 24px; display:flex; justify-content:space-between; align-items:center; }");
sb.AppendLine(".header h1 { font-size:18px; font-weight:600; }");
sb.AppendLine(".header-actions button { margin-left:8px; padding:6px 16px; border:none; border-radius:4px; cursor:pointer; font-size:14px; }");
sb.AppendLine(".btn-start { background:#52c41a; color:#fff; }");
sb.AppendLine(".btn-stop { background:#ff4d4f; color:#fff; }");
sb.AppendLine(".container { max-width:1200px; margin:20px auto; padding:0 20px; }");
sb.AppendLine(".section { background:#fff; border-radius:8px; padding:20px; margin-bottom:16px; box-shadow:0 1px 4px rgba(0,0,0,0.1); }");
sb.AppendLine(".section h2 { font-size:16px; margin-bottom:12px; padding-bottom:8px; border-bottom:1px solid #e8e8e8; }");
sb.AppendLine(".settings-row { display:flex; gap:24px; align-items:center; flex-wrap:wrap; margin-bottom:8px; }");
sb.AppendLine(".settings-row label { font-size:14px; color:#666; min-width:100px; }");
sb.AppendLine(".settings-row input[type=number] { width:80px; padding:4px 8px; border:1px solid #d9d9d9; border-radius:4px; }");
sb.AppendLine(".settings-row select { padding:4px 8px; border:1px solid #d9d9d9; border-radius:4px; }");
sb.AppendLine(".settings-row input[type=radio] { margin-right:4px; }");
sb.AppendLine(".device-cards { display:grid; grid-template-columns:repeat(auto-fill,minmax(320px,1fr)); gap:12px; }");
sb.AppendLine(".device-card { border:1px solid #e8e8e8; border-radius:6px; padding:12px; }");
sb.AppendLine(".device-card.offline { opacity:0.5; background:#fafafa; }");
sb.AppendLine(".device-card h3 { font-size:14px; margin-bottom:8px; }");
sb.AppendLine(".device-card .info { font-size:13px; color:#666; line-height:1.8; }");
sb.AppendLine(".device-card .actions { margin-top:8px; display:flex; flex-wrap:wrap; gap:4px; }");
sb.AppendLine(".device-card .actions button { padding:2px 8px; font-size:12px; border:1px solid #d9d9d9; border-radius:3px; background:#fff; cursor:pointer; }");
sb.AppendLine(".device-card .actions button:hover { border-color:#1890ff; color:#1890ff; }");
sb.AppendLine(".json-preview { max-height:200px; overflow:auto; background:#fafafa; padding:12px; border-radius:4px; font-family:Consolas,monospace; font-size:12px; white-space:pre-wrap; word-break:break-all; }");
sb.AppendLine(".log-table { width:100%; border-collapse:collapse; font-size:13px; }");
sb.AppendLine(".log-table th,.log-table td { padding:6px 8px; text-align:left; border-bottom:1px solid #f0f0f0; }");
sb.AppendLine(".log-table th { background:#fafafa; color:#666; font-weight:600; }");
sb.AppendLine(".stats-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); gap:12px; }");
sb.AppendLine(".stat-item { text-align:center; padding:12px; background:#fafafa; border-radius:4px; }");
sb.AppendLine(".stat-item .value { font-size:24px; font-weight:600; color:#1890ff; }");
sb.AppendLine(".stat-item .label { font-size:13px; color:#999; margin-top:4px; }");
sb.AppendLine(".btn-apply { padding:4px 12px; border:1px solid #1890ff; border-radius:4px; background:#fff; color:#1890ff; cursor:pointer; }");
sb.AppendLine("</style></head><body>");
sb.AppendLine("<div class='header'>");
sb.AppendLine(" <h1>" + server.Name + " (端口 " + server.Port + ")</h1>");
sb.AppendLine(" <div class='header-actions'>");
sb.AppendLine(" <a href='http://localhost:" + server.Port + "/' target='_blank' style='color:#fff;margin-right:8px;font-size:14px;'>数据接口</a>");
sb.AppendLine(" <button class='btn-start' onclick='startSim()'>启动</button>");
sb.AppendLine(" <button class='btn-stop' onclick='stopSim()'>停止</button>");
sb.AppendLine(" </div>");
sb.AppendLine("</div>");
sb.AppendLine("<div class='container'>");
// 全局设置
sb.AppendLine(" <div class='section'><h2>全局设置</h2>");
sb.AppendLine(" <div class='settings-row'>");
sb.AppendLine(" <label>数据变化频率:</label>");
sb.AppendLine(" <input type='number' id='intervalInput' min='1' max='300' value='" + server.Config.DataChangeInterval + "'> 秒");
sb.AppendLine(" <button class='btn-apply' onclick='setInterval2()'>应用</button>");
sb.AppendLine(" </div>");
sb.AppendLine(" <div class='settings-row'>");
sb.AppendLine(" <label>剧本模式:</label>");
sb.AppendLine(" <label><input type='radio' name='mode' value='auto'" + (server.Config.ScenarioMode == "auto" ? " checked" : "") + "> 自动循环</label>");
sb.AppendLine(" <label><input type='radio' name='mode' value='manual'" + (server.Config.ScenarioMode == "manual" ? " checked" : "") + "> 手动触发</label>");
sb.AppendLine(" </div>");
sb.AppendLine(" <div class='settings-row'>");
sb.AppendLine(" <label>网络异常模拟:</label>");
sb.AppendLine(" <select id='networkSelect' onchange='setNetwork(this.value)'>");
sb.AppendLine(" <option value='normal'>正常</option>");
sb.AppendLine(" <option value='http500'>HTTP 500</option>");
sb.AppendLine(" <option value='timeout'>连接超时(60s)</option>");
sb.AppendLine(" <option value='refuse'>拒绝连接</option>");
sb.AppendLine(" <option value='empty'>返回空数组</option>");
sb.AppendLine(" <option value='malformed'>畸形JSON</option>");
sb.AppendLine(" </select>");
sb.AppendLine(" </div>");
sb.AppendLine(" </div>");
// 设备状态卡片
sb.AppendLine(" <div class='section'><h2>设备状态卡片</h2>");
sb.AppendLine(" <div class='device-cards' id='deviceCards'>加载中...</div>");
sb.AppendLine(" </div>");
// JSON预览
sb.AppendLine(" <div class='section'><h2>当前返回JSON预览</h2>");
sb.AppendLine(" <button class='btn-apply' onclick='refreshJson()' style='margin-bottom:8px;'>刷新</button>");
sb.AppendLine(" <div class='json-preview' id='jsonPreview'>加载中...</div>");
sb.AppendLine(" </div>");
// 日志
sb.AppendLine(" <div class='section'><h2>返回数据日志最近100条</h2>");
sb.AppendLine(" <table class='log-table'>");
sb.AppendLine(" <thead><tr><th>#</th><th>时间</th><th>设备数</th><th>关键数据</th><th>耗时</th><th>操作</th></tr></thead>");
sb.AppendLine(" <tbody id='logTable'></tbody>");
sb.AppendLine(" </table>");
sb.AppendLine(" </div>");
// 统计
sb.AppendLine(" <div class='section'><h2>统计</h2>");
sb.AppendLine(" <div class='stats-grid' id='statsGrid'>加载中...</div>");
sb.AppendLine(" </div>");
sb.AppendLine("</div>");
// JavaScript
sb.AppendLine("<script>");
sb.AppendLine("var scenarioNames={'machining':'正常加工','same_part':'同一零件','idle':'待机空闲','program_change':'换零件','manual_reset':'手动清零','power_off':'断电','power_on':'恢复开机','pause':'暂停加工'};");
sb.AppendLine("var runNames={0:'待机',1:'运行',3:'加工中'};");
sb.AppendLine("function refresh(){");
sb.AppendLine(" fetch('/admin/api/status').then(r=>r.json()).then(data=>{");
sb.AppendLine(" var cards=document.getElementById('deviceCards');cards.innerHTML='';");
sb.AppendLine(" data.devices.forEach(function(d){");
sb.AppendLine(" var cls=d.isOnline?'':'offline';");
sb.AppendLine(" var scName=scenarioNames[d.scenario]||d.scenario;");
sb.AppendLine(" var h='<div class=\"device-card '+cls+'\">';");
sb.AppendLine(" h+='<h3>'+d.deviceCode+' '+d.desc+(d.isOnline?'':' (离线)')+'</h3>';");
sb.AppendLine(" h+='<div class=\"info\">';");
sb.AppendLine(" h+='场景: '+scName+' ('+d.scenarioTick+'/'+d.scenarioDuration+')<br>';");
sb.AppendLine(" if(d.isOnline){");
sb.AppendLine(" h+='程序: '+d.programName+'<br>';");
sb.AppendLine(" h+='零件数: '+d.partCount+'<br>';");
sb.AppendLine(" h+='运行状态: '+d.runStatus+' ('+runNames[d.runStatus]+')<br>';");
sb.AppendLine(" h+='主轴: '+d.spindleSpeedActual+'/'+d.spindleSpeedSet+' 负载'+d.spindleLoad+'%<br>';");
sb.AppendLine(" h+='进给: '+d.feedSpeedActual+'/'+d.feedSpeedSet+'<br>';");
sb.AppendLine(" h+='加工: '+d.machiningStatus;");
sb.AppendLine(" } else { h+='(设备离线,不返回数据)'; }");
sb.AppendLine(" h+='</div>';");
// 按钮使用data属性避免转义问题
sb.AppendLine(" h+='<div class=\"actions\" data-device=\"'+d.deviceCode+'\">';");
sb.AppendLine(" h+='<button data-event=\"program_change\">换零件</button>';");
sb.AppendLine(" h+='<button data-event=\"manual_reset\">手动清零</button>';");
sb.AppendLine(" h+='<button data-event=\"power_off\">断电</button>';");
sb.AppendLine(" h+='<button data-event=\"pause\">暂停</button>';");
sb.AppendLine(" h+='<button data-event=\"power_on\">恢复</button>';");
sb.AppendLine(" h+='<button data-event=\"machining\">加工</button>';");
sb.AppendLine(" h+='<button data-event=\"idle\">待机</button>';");
sb.AppendLine(" h+='</div></div>';");
sb.AppendLine(" cards.innerHTML+=h;");
sb.AppendLine(" });");
// 事件委托处理按钮点击
sb.AppendLine(" document.querySelectorAll('.device-card .actions button').forEach(function(btn){");
sb.AppendLine(" btn.onclick=function(){");
sb.AppendLine(" var dev=this.parentElement.getAttribute('data-device');");
sb.AppendLine(" var evt=this.getAttribute('data-event');");
sb.AppendLine(" triggerEvent(dev,evt);");
sb.AppendLine(" };");
sb.AppendLine(" });");
// 统计
sb.AppendLine(" var sg=document.getElementById('statsGrid');");
sb.AppendLine(" sg.innerHTML='<div class=\"stat-item\"><div class=\"value\">'+data.requestCount+'</div><div class=\"label\">总请求次数</div></div>';");
sb.AppendLine(" sg.innerHTML+='<div class=\"stat-item\"><div class=\"value\">'+data.successCount+'</div><div class=\"label\">成功次数</div></div>';");
sb.AppendLine(" sg.innerHTML+='<div class=\"stat-item\"><div class=\"value\">'+data.failCount+'</div><div class=\"label\">失败次数</div></div>';");
sb.AppendLine(" sg.innerHTML+='<div class=\"stat-item\"><div class=\"value\">'+data.onlineDevices+'/'+data.totalDevices+'</div><div class=\"label\">在线/总设备</div></div>';");
sb.AppendLine(" sg.innerHTML+='<div class=\"stat-item\"><div class=\"value\">'+data.uptime+'</div><div class=\"label\">运行时长</div></div>';");
sb.AppendLine(" sg.innerHTML+='<div class=\"stat-item\"><div class=\"value\">'+data.dataChangeInterval+'s</div><div class=\"label\">数据频率</div></div>';");
sb.AppendLine(" }).catch(function(){});");
sb.AppendLine("}");
sb.AppendLine("function refreshJson(){");
sb.AppendLine(" fetch('/data').then(r=>r.text()).then(json=>{");
sb.AppendLine(" try{document.getElementById('jsonPreview').textContent=JSON.stringify(JSON.parse(json),null,2);}");
sb.AppendLine(" catch(e){document.getElementById('jsonPreview').textContent=json;}");
sb.AppendLine(" }).catch(function(e){document.getElementById('jsonPreview').textContent='请求失败: '+e.message;});");
sb.AppendLine("}");
sb.AppendLine("var allLogs=[];");
sb.AppendLine("function refreshLogs(){");
sb.AppendLine(" fetch('/admin/api/logs').then(r=>r.json()).then(logs=>{");
sb.AppendLine(" allLogs=logs;var tb=document.getElementById('logTable');tb.innerHTML='';");
sb.AppendLine(" logs.forEach(function(l){");
sb.AppendLine(" var tr=document.createElement('tr');");
sb.AppendLine(" tr.innerHTML='<td>'+l.index+'</td><td>'+l.timestamp+'</td><td>'+l.deviceCount+'</td><td>'+l.keyData+'</td><td>'+l.duration+'ms</td><td></td>';");
sb.AppendLine(" var btn=document.createElement('button');");
sb.AppendLine(" btn.textContent='完整JSON';btn.style.cssText='font-size:12px;padding:2px 6px;cursor:pointer;';");
sb.AppendLine(" btn.onclick=(function(idx){return function(){showJson(idx);};})(l.index);");
sb.AppendLine(" tr.cells[5].appendChild(btn);");
sb.AppendLine(" tb.appendChild(tr);");
sb.AppendLine(" });");
sb.AppendLine(" }).catch(function(){});");
sb.AppendLine("}");
sb.AppendLine("function showJson(idx){");
sb.AppendLine(" var l=allLogs.find(function(x){return x.index===idx;});");
sb.AppendLine(" if(l){try{document.getElementById('jsonPreview').textContent=JSON.stringify(JSON.parse(l.fullJson),null,2);}catch(e){document.getElementById('jsonPreview').textContent=l.fullJson;}}");
sb.AppendLine("}");
sb.AppendLine("function startSim(){fetch('/admin/api/start',{method:'POST'}).then(function(){refresh();});}");
sb.AppendLine("function stopSim(){fetch('/admin/api/stop',{method:'POST'}).then(function(){refresh();});}");
sb.AppendLine("function setInterval2(){var v=document.getElementById('intervalInput').value;fetch('/admin/api/interval',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({value:parseInt(v)})}).then(function(){refresh();});}");
sb.AppendLine("function setNetwork(type){fetch('/admin/api/network',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type:type})}).then(function(){});}");
sb.AppendLine("function triggerEvent(deviceId,eventType){fetch('/admin/api/event',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({deviceId:deviceId,eventType:eventType})}).then(function(){setTimeout(refresh,500);});}");
sb.AppendLine("document.querySelectorAll('input[name=mode]').forEach(function(r){");
sb.AppendLine(" r.addEventListener('change',function(){");
sb.AppendLine(" fetch('/admin/api/mode',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({mode:this.value})}).then(function(){});");
sb.AppendLine(" });");
sb.AppendLine("});");
sb.AppendLine("setInterval(function(){refresh();refreshJson();refreshLogs();},2000);");
sb.AppendLine("refresh();refreshJson();refreshLogs();");
sb.AppendLine("</script></body></html>");
return sb.ToString();
}
}
}