Update grafico nuova gestione pagina
This commit is contained in:
@@ -1,37 +1,23 @@
|
||||
@inherits LayoutComponentBase
|
||||
@using System.Security.Claims
|
||||
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu />
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div class="top-row px-4">
|
||||
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
|
||||
@if (CurrentContext?.User?.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
var name = CurrentContext.User.FindFirst(ClaimTypes.Name)?.Value;
|
||||
<span class="ms-auto me-3">@name [Sign Out]</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a href="/login" class="ms-auto me-3">Sign In</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<article class="content px-4">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<NavMenu />
|
||||
<main class="main-content">
|
||||
<article class="content px-4 pt-5">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Inject] private HttpContext CurrentContext { get; set; } = default!;
|
||||
}
|
||||
<style>
|
||||
body {
|
||||
margin-top: 56px;
|
||||
background-color: #f1f5f9;
|
||||
}
|
||||
.main-content {
|
||||
min-height: calc(100vh - 56px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,29 +1,170 @@
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
@inject PLCService PlcService
|
||||
@inject NavigationManager NavManager
|
||||
@implements IDisposable
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using System.Security.Claims
|
||||
|
||||
<nav class="navbar-custom">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="">PlcVanguard</a>
|
||||
<div class="nav-brand d-flex align-items-center">
|
||||
<NavLink Match="NavLinkMatch.All" href="/" class="d-flex align-items-center text-decoration-none">
|
||||
<span class="logo-p rounded-circle me-2">P</span>
|
||||
<span class="brand-text fw-semibold">PlcVanguard</span>
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-links d-flex align-items-center">
|
||||
<NavLink href="/counter" class="nav-link-custom" Match="NavLinkMatch.Prefix">
|
||||
History
|
||||
</NavLink>
|
||||
<NavLink href="/comparison" class="nav-link-custom" Match="NavLinkMatch.Prefix">
|
||||
Comparison
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-right d-flex align-items-center gap-3">
|
||||
<div class="connection-status d-flex align-items-center gap-2">
|
||||
<span class="pulse-dot @(state.IsConnected ? "pulse-active" : "pulse-inactive")"></span>
|
||||
<span class="status-text @(state.IsConnected ? "text-success" : "text-danger")">
|
||||
@if (state.IsConnected)
|
||||
{
|
||||
<text>Connected: @state.IpAddress (@state.CpuType)</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>Disconnected</text>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
@if (CurrentContext?.User?.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
var name = CurrentContext.User.Claims.Where(c => c.Type == ClaimTypes.Name).Select(c => c.Value).FirstOrDefault();
|
||||
<a href="/logout" class="text-white-50 small">Sign Out @name</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a href="/login" class="text-white-50 small">Sign In</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
|
||||
@code {
|
||||
[Inject] private HttpContext? CurrentContext { get; set; }
|
||||
|
||||
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
|
||||
<nav class="flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-house-door-fill" aria-hidden="true"></span> Dashboard
|
||||
</NavLink>
|
||||
</div>
|
||||
private PlcState state = new();
|
||||
private Timer? _pollTimer;
|
||||
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="counter">
|
||||
<span class="bi bi-plus-square" aria-hidden="true"></span> Historical
|
||||
</NavLink>
|
||||
</div>
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
state = PlcService.GetState();
|
||||
_pollTimer = new Timer(_ =>
|
||||
{
|
||||
InvokeAsync(() =>
|
||||
{
|
||||
state = PlcService.GetState();
|
||||
StateHasChanged();
|
||||
});
|
||||
}, null, 1000, 1000);
|
||||
}
|
||||
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="comparison">
|
||||
<span class="bi bi-graph-up" aria-hidden="true"></span> Comparison
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
public void Dispose()
|
||||
{
|
||||
_pollTimer?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
<style>
|
||||
.navbar-custom {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #0f172a;
|
||||
height: 56px;
|
||||
border-bottom: 1px solid #1e293b;
|
||||
z-index: 1040;
|
||||
}
|
||||
|
||||
.navbar-custom .container-fluid {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.brand-link {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
color: #f8fafc;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.logo-p {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background-color: #3b82f6;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-link-custom {
|
||||
color: #94a3b8 !important;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.nav-link-custom:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05) !important;
|
||||
color: #f8fafc !important;
|
||||
}
|
||||
|
||||
.nav-link-custom.active {
|
||||
background-color: rgba(59, 130, 246, 0.2) !important;
|
||||
color: #60a5fa !important;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pulse-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #64748b;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pulse-active {
|
||||
background-color: #22c55e;
|
||||
box-shadow: 0 0 6px rgba(34, 197, 94, 0.6);
|
||||
animation: _pv_pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.pulse-inactive {
|
||||
background-color: #dc2626;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using PlcVanguard.Web.Components.Shared
|
||||
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<h2 class="font-semibold text-lg mb-4">Recording Sessions</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left text-xs">
|
||||
<thead class="bg-gray-50">
|
||||
<tr class="border-b">
|
||||
<th class="p-2">Session ID</th>
|
||||
<th class="p-2">Timestamp</th>
|
||||
<th class="p-2">Status</th>
|
||||
<th class="p-2">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (Sessions.Any())
|
||||
{
|
||||
@foreach (var s in Sessions)
|
||||
{
|
||||
<tr class="border-b hover:bg-gray-50 transition">
|
||||
<td class="p-2 font-mono font-semibold text-blue-600">@s.Id</td>
|
||||
<td class="p-2">@s.Timestamp.ToString("yyyy-MM-dd HH:mm:ss")</td>
|
||||
<td class="p-2">
|
||||
@if (s.Status == "Success")
|
||||
{
|
||||
<span class="px-2 py-1 rounded-full text-[10px] font-bold bg-green-100 text-green-700">@s.Status</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="px-2 py-1 rounded-full text-[10px] font-bold bg-gray-100 text-gray-700">@s.Status</span>
|
||||
}
|
||||
</td>
|
||||
<td class="p-2">
|
||||
<button class="text-blue-600 hover:underline font-semibold"
|
||||
@onclick="() => SelectedSession.InvokeAsync(s.Id)">
|
||||
Load
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<tr>
|
||||
<td colspan="4" class="p-4 text-center text-gray-400 italic">No recording sessions found.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (Sessions.Any())
|
||||
{
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<button class="px-3 py-1 bg-gray-200 hover:bg-gray-300 rounded text-xs font-semibold transition"
|
||||
@onclick="PrevPage"
|
||||
disabled="@(Page == 1)">
|
||||
Prev
|
||||
</button>
|
||||
<span class="text-xs text-gray-600">Page @Page of @TotalPages</span>
|
||||
<button class="px-3 py-1 bg-gray-200 hover:bg-gray-300 rounded text-xs font-semibold transition"
|
||||
@onclick="NextPage"
|
||||
disabled="@(Page >= TotalPages)">
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public List<SessionInfo> Sessions { get; set; } = new();
|
||||
[Parameter] public EventCallback<string> SelectedSession { get; set; }
|
||||
[Parameter] public int Page { get; set; } = 1;
|
||||
[Parameter] public int PageSize { get; set; } = 10;
|
||||
|
||||
private int TotalPages => (int)Math.Ceiling(Sessions.Count / (double)PageSize);
|
||||
|
||||
private void PrevPage()
|
||||
{
|
||||
if (Page > 1)
|
||||
SelectedSession.InvokeAsync(Sessions[Math.Max(0, (Page - 2) * PageSize % Sessions.Count)].Id);
|
||||
}
|
||||
|
||||
private void NextPage()
|
||||
{
|
||||
if (Page < TotalPages)
|
||||
SelectedSession.InvokeAsync(Sessions[Math.Min(Sessions.Count - 1, Page * PageSize % Sessions.Count)].Id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
@using Microsoft.JSInterop
|
||||
@using PlcVanguard.Web.Components.Shared
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-6 shadow-sm h-full flex flex-col">
|
||||
<div class="flex justify-between items-center mb-4 flex-shrink-0">
|
||||
<h2 class="font-semibold text-lg">@Title</h2>
|
||||
<span class="text-sm text-gray-500">@subtitle</span>
|
||||
</div>
|
||||
<div class="flex-1 min-h-0 relative">
|
||||
<canvas id="@ChartId"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Title { get; set; } = "Live Chart";
|
||||
[Parameter] public string subtitle { get; set; } = "";
|
||||
[Parameter] public List<DataLabel> Data { get; set; } = new();
|
||||
[Parameter] public string ChartId { get; set; } = "liveChart";
|
||||
[Parameter] public EventCallback OnChartReady { get; set; }
|
||||
|
||||
[Inject] private IJSRuntime JS { get; set; } = default!;
|
||||
private IJSObjectReference? _jsModule;
|
||||
private bool _initialized;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender && Data.Count > 1)
|
||||
{
|
||||
var ok = await EnsureChartAsync();
|
||||
if (ok) await OnChartReady.InvokeAsync();
|
||||
}
|
||||
else if (_initialized && Data.Count > 1)
|
||||
{
|
||||
await UpdateChartAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> EnsureChartAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_jsModule = await JS.InvokeAsync<IJSObjectReference>("import", "/js/chartHelper.js");
|
||||
await _jsModule.InvokeVoidAsync("initChart", ChartId, Data);
|
||||
_initialized = true;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Chart init failed: " + ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> UpdateChartAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_jsModule == null)
|
||||
_jsModule = await JS.InvokeAsync<IJSObjectReference>("import", "/js/chartHelper.js");
|
||||
await _jsModule.InvokeVoidAsync("updateLiveDataset", ChartId, Data);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Chart update failed: " + ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_jsModule != null)
|
||||
await _jsModule.DisposeAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using PlcVanguard.Web.Components.Shared
|
||||
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<h2 class="font-semibold text-lg mb-4">Control Panel</h2>
|
||||
<div class="space-y-4">
|
||||
@if (IsAcquiring)
|
||||
{
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5">
|
||||
<div class="bg-blue-600 h-2.5 rounded-full transition-all duration-300" style="width: @(ProgressPercent.ToString("F0"))%"></div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-2">Acquisition in progress... @($"{ProgressPercent:F0}")%</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (IsConnected)
|
||||
{
|
||||
<p class="text-sm text-gray-500">PLC Connected<br>@($"{plcIp} | {plcType}")</p>
|
||||
<button class="w-full bg-green-600 hover:bg-green-700 text-white font-bold py-3 rounded-lg transition shadow-md"
|
||||
@onclick="StartAcquisition">
|
||||
Start Acquisition (@SampleRate Hz)
|
||||
</button>
|
||||
<button class="w-full bg-red-600 hover:bg-red-700 text-white font-bold py-2 rounded-lg transition"
|
||||
@onclick="Disconnect">
|
||||
Disconnect
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-sm text-gray-500">Target: @($"{plcIp} ({plcType})")</p>
|
||||
<button class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-lg transition shadow-md"
|
||||
@onclick="Connect">
|
||||
Connect to PLC
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (IsConnected)
|
||||
{
|
||||
<div class="rounded-xl border border-gray-200 bg-slate-900 text-white p-6 shadow-lg mt-6">
|
||||
<h2 class="font-semibold text-lg mb-2">System Overview</h2>
|
||||
<ul class="text-sm space-y-2 opacity-90">
|
||||
<li>PLC: @plcType</li>
|
||||
<li>Area: DB100 (Continuous)</li>
|
||||
<li>Mode: @($"{SampleRate}Hz Polling")</li>
|
||||
<li>Persistence: File + MariaDB</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public bool IsConnected { get; set; }
|
||||
[Parameter] public string plcIp { get; set; } = "192.168.0.102";
|
||||
[Parameter] public string plcType { get; set; } = "S7-1500";
|
||||
[Parameter] public bool IsAcquiring { get; set; }
|
||||
[Parameter] public double ProgressPercent { get; set; }
|
||||
[Parameter] public int SampleRate { get; set; } = 5;
|
||||
|
||||
[Parameter] public EventCallback OnConnect { get; set; }
|
||||
[Parameter] public EventCallback OnDisconnect { get; set; }
|
||||
[Parameter] public EventCallback OnStartAcquisition { get; set; }
|
||||
|
||||
private async Task Connect() => await OnConnect.InvokeAsync();
|
||||
private async Task Disconnect() => await OnDisconnect.InvokeAsync();
|
||||
private async Task StartAcquisition() => await OnStartAcquisition.InvokeAsync();
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace PlcVanguard.Web.Components.Shared;
|
||||
|
||||
public class DataLabel
|
||||
{
|
||||
public double X { get; set; }
|
||||
public double Y { get; set; }
|
||||
}
|
||||
|
||||
public class PointData
|
||||
{
|
||||
public int Index { get; set; }
|
||||
public DateTime Timestamp { get; set; }
|
||||
public double Value { get; set; }
|
||||
public bool IsError { get; set; }
|
||||
public bool IsWarning { get; set; }
|
||||
}
|
||||
|
||||
public class SessionInfo
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public DateTime Timestamp { get; set; }
|
||||
public string Status { get; set; } = "Success";
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
@using PlcVanguard.Web.Components.Shared
|
||||
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||
@if (SessionId == null)
|
||||
{
|
||||
<h2 class="font-semibold text-lg mb-4">
|
||||
<span class="text-gray-400 italic">Click a session in the left table to load data</span>
|
||||
</h2>
|
||||
<div class="text-gray-400 italic text-center py-20">
|
||||
Select a recording session to view details
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h2 class="font-semibold text-lg mb-4">
|
||||
Session @SessionId
|
||||
</h2>
|
||||
|
||||
<div class="p-3 bg-gray-50 rounded-lg border border-gray-100 mb-4">
|
||||
<h3 class="text-xs uppercase font-bold text-gray-400 mb-2">Session Summary</h3>
|
||||
<table class="w-full text-left text-xs">
|
||||
<thead class="bg-gray-50 border-b">
|
||||
<tr>
|
||||
<th class="p-2">Index</th>
|
||||
<th class="p-2">Timestamp</th>
|
||||
<th class="p-2">Value</th>
|
||||
<th class="p-2">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var p in DisplayPoints)
|
||||
{
|
||||
<tr class="border-b hover:bg-blue-50 transition cursor-pointer"
|
||||
@onclick="() => { SelectedPoint = p; StateHasChanged(); }">
|
||||
<td class="p-2">@p.Index</td>
|
||||
<td class="p-2">@p.Timestamp.ToString("HH:mm:ss.fff")</td>
|
||||
<td class="p-2 font-mono font-bold">@p.Value.ToString("F3")</td>
|
||||
<td class="p-2">
|
||||
@if (p.IsError)
|
||||
{
|
||||
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-red-100 text-red-700">Error</span>
|
||||
}
|
||||
else if (p.IsWarning)
|
||||
{
|
||||
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-yellow-100 text-yellow-700">Warning</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-green-100 text-green-700">OK</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<button class="px-3 py-1 bg-gray-200 hover:bg-gray-300 rounded text-xs font-semibold transition"
|
||||
@onclick="PrevPage"
|
||||
disabled="@(CurrentPage == 1)">
|
||||
Prev
|
||||
</button>
|
||||
<span class="text-xs text-gray-600">Page @CurrentPage of @TotalPages</span>
|
||||
<button class="px-3 py-1 bg-gray-200 hover:bg-gray-300 rounded text-xs font-semibold transition"
|
||||
@onclick="NextPage"
|
||||
disabled="@(CurrentPage >= TotalPages)">
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (SelectedPoint != null)
|
||||
{
|
||||
<div class="mt-4 p-3 bg-blue-50 border border-blue-200 rounded">
|
||||
<h4 class="text-xs font-bold text-blue-800 mb-1">Selected Point</h4>
|
||||
<p class="text-xs font-mono">Idx = @(SelectedPoint.Index), Val = @(SelectedPoint.Value.ToString("F3")), Time = @(SelectedPoint.Timestamp.ToString("HH:mm:ss.fff"))</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string SessionId { get; set; }
|
||||
[Parameter] public List<PointData> AllPoints { get; set; } = new();
|
||||
[Parameter] public int WarningLimit { get; set; } = 2;
|
||||
[Parameter] public int ErrorLimit { get; set; } = 3;
|
||||
[Parameter] public double TargetMean { get; set; } = 210;
|
||||
|
||||
private int CurrentPage { get; set; } = 1;
|
||||
private const int PageSize = 10;
|
||||
private int TotalPages => (int)Math.Ceiling(AllPoints.Count / (double)PageSize);
|
||||
private PointData? SelectedPoint { get; set; }
|
||||
|
||||
private List<PointData> DisplayPoints => AllPoints
|
||||
.Skip((CurrentPage - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.ToList();
|
||||
|
||||
private void NextPage()
|
||||
{
|
||||
if (CurrentPage < TotalPages)
|
||||
{
|
||||
CurrentPage++;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void PrevPage()
|
||||
{
|
||||
if (CurrentPage > 1)
|
||||
{
|
||||
CurrentPage--;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,3 +141,8 @@ body {
|
||||
.chart-polyline {
|
||||
animation: chart-draw 1s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes _pv_pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(1.3); }
|
||||
}
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PlcVanguard</title>
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<component type="typeof(HeadOutlet)" render-mode="HeadOutlet" />
|
||||
</head>
|
||||
<body>
|
||||
<component type="typeof(App)" render-mode="ServerPrerendered" />
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
<script src="_framework/blazor.server.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
let chartInstances = {};
|
||||
|
||||
window.initChart = function(canvasId, dataItems) {
|
||||
if (typeof Chart === 'undefined') return;
|
||||
|
||||
const ctx = document.getElementById(canvasId);
|
||||
if (!ctx) return;
|
||||
|
||||
const values = Array.isArray(dataItems[0])
|
||||
? dataItems[0]
|
||||
: dataItems;
|
||||
|
||||
// Ensure we have at least 2 points
|
||||
if (values.length < 2) return;
|
||||
|
||||
// Generate y-limits if not provided
|
||||
const minVal = Math.min(...values.map(v => v.Y));
|
||||
const maxVal = Math.max(...values.map(v => v.Y));
|
||||
const range = maxVal - minVal || 1;
|
||||
const padding = range * 0.15;
|
||||
|
||||
chartInstances[canvasId] = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: 'Value',
|
||||
data: values.map(v => ({ x: v.X, y: v.Y })),
|
||||
borderColor: '#3b82f6',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
fill: true,
|
||||
pointRadius: 1,
|
||||
borderWidth: 2,
|
||||
tension: 0.1
|
||||
},
|
||||
{
|
||||
label: 'Warning UCL',
|
||||
data: values.map(v => ({ x: v.X, y: minVal + range + padding })),
|
||||
borderColor: '#facc15',
|
||||
borderWidth: 1,
|
||||
pointRadius: 0,
|
||||
fill: false,
|
||||
borderDash: [5, 5]
|
||||
},
|
||||
{
|
||||
label: 'Warning LCL',
|
||||
data: values.map(v => ({ x: v.X, y: minVal - padding })),
|
||||
borderColor: '#facc15',
|
||||
borderWidth: 1,
|
||||
pointRadius: 0,
|
||||
fill: false,
|
||||
borderDash: [5, 5]
|
||||
},
|
||||
{
|
||||
label: 'Error UCL',
|
||||
data: values.map(v => ({ x: v.X, y: minVal + range * 2 + padding })),
|
||||
borderColor: '#f87171',
|
||||
borderWidth: 1,
|
||||
pointRadius: 0,
|
||||
fill: false,
|
||||
borderDash: [5, 5]
|
||||
},
|
||||
{
|
||||
label: 'Error LCL',
|
||||
data: values.map(v => ({ x: v.X, y: minVal - range * 2 - padding })),
|
||||
borderColor: '#f87171',
|
||||
borderWidth: 1,
|
||||
pointRadius: 0,
|
||||
fill: false,
|
||||
borderDash: [5, 5]
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: { duration: 300 },
|
||||
scales: {
|
||||
x: { type: 'linear', title: { display: true, text: 'Acquisition Time' } },
|
||||
y: { min: minVal - padding, max: maxVal + padding, title: { display: true, text: 'Value' } }
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: { enabled: true, mode: 'index', intersect: false }
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.updateLiveDataset = function(canvasId, dataItems) {
|
||||
const chart = chartInstances[canvasId];
|
||||
if (!chart) return;
|
||||
if (!dataItems || !dataItems.length) return;
|
||||
|
||||
const newData = Array.isArray(dataItems[0])
|
||||
? dataItems[0]
|
||||
: dataItems;
|
||||
|
||||
if (newData.length < 2) return;
|
||||
|
||||
chart.data.datasets[0].data = newData.map(v => ({ x: v.X, y: v.Y }));
|
||||
|
||||
const minVal = Math.min(...newData.map(v => v.Y));
|
||||
const maxVal = Math.max(...newData.map(v => v.Y));
|
||||
const range = maxVal - minVal || 1;
|
||||
const padding = range * 0.15;
|
||||
|
||||
chart.data.datasets[1].data = newData.map(v => ({ x: v.X, y: minVal + range + padding }));
|
||||
chart.data.datasets[2].data = newData.map(v => ({ x: v.X, y: minVal - padding }));
|
||||
chart.data.datasets[3].data = newData.map(v => ({ x: v.X, y: minVal + range * 2 + padding }));
|
||||
chart.data.datasets[4].data = newData.map(v => ({ x: v.X, y: minVal - range * 2 - padding }));
|
||||
|
||||
chart.options.scales.y.min = minVal - padding;
|
||||
chart.options.scales.y.max = maxVal + padding;
|
||||
|
||||
chart.update();
|
||||
};
|
||||
|
||||
window.updateFromSession = function(canvasId, dataItems) {
|
||||
const chart = chartInstances[canvasId];
|
||||
if (!chart) return;
|
||||
const newData = Array.isArray(dataItems[0]) ? dataItems[0] : dataItems;
|
||||
if (newData.length < 2) return;
|
||||
|
||||
chart.data.datasets[0].data = newData.map(v => ({ x: v.X, y: v.Y }));
|
||||
|
||||
const minVal = Math.min(...newData.map(v => v.Y));
|
||||
const maxVal = Math.max(...newData.map(v => v.Y));
|
||||
const range = maxVal - minVal || 1;
|
||||
const padding = range * 0.15;
|
||||
|
||||
chart.data.datasets[1].data = newData.map(v => ({ x: v.X, y: minVal + range + padding }));
|
||||
chart.data.datasets[2].data = newData.map(v => ({ x: v.X, y: minVal - padding }));
|
||||
chart.data.datasets[3].data = newData.map(v => ({ x: v.X, y: minVal + range * 2 + padding }));
|
||||
chart.data.datasets[4].data = newData.map(v => ({ x: v.X, y: minVal - range * 2 - padding }));
|
||||
|
||||
chart.options.scales.y.min = minVal - padding;
|
||||
chart.options.scales.y.max = maxVal + padding;
|
||||
|
||||
chart.update();
|
||||
};
|
||||
|
||||
window.destroyChart = function(canvasId) {
|
||||
if (chartInstances[canvasId]) {
|
||||
chartInstances[canvasId].destroy();
|
||||
chartInstances[canvasId] = null;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user