RobotApp/RobotApp.Client/Pages/RobotConfigManager.razor
Đăng Nguyễn 70e27da4a2 update
2025-11-03 10:29:18 +07:00

433 lines
17 KiB
Plaintext

@page "/robot-config"
@rendermode InteractiveWebAssemblyNoPrerender
@attribute [Authorize]
@inject HttpClient Http
@inject ISnackbar Snackbar
@using System.Net.Http.Json
@using RobotApp.Common.Shares.Dtos
<PageTitle>Robot Configuration</PageTitle>
<div class="d-flex w-100 h-100 p-2 overflow-hidden flex-column position-relative">
<div class="rcm-toolbar">
<div class="rcm-toolbar-left">
<label class="rcm-label" for="configType">Config Type</label>
<div class="rcm-select-wrapper">
<select id="configType" class="form-select rcm-select" value="@SelectedType" @onchange="OnTypeChanged">
@foreach (var type in Enum.GetValues<RobotConfigType>())
{
<option value="@type">@type</option>
}
</select>
<span class="rcm-select-icon" aria-hidden="true">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M7 10l5 5 5-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</span>
</div>
</div>
<div class="rcm-toolbar-right">
<div class="rcm-action-group">
<button type="button" class="btn rcm-icon-btn" data-tooltip="Add config" aria-label="Add" @onclick="OpenAddConfig">
<i class="mdi mdi-plus" aria-hidden="true"></i>
</button>
<button type="button" class="btn rcm-icon-btn" data-tooltip="Update config" aria-label="Update" @onclick="SaveConfig">
<i class="mdi mdi-content-save" aria-hidden="true"></i>
</button>
<button type="button" class="btn rcm-icon-btn rcm-danger" data-tooltip="Delete config" aria-label="Delete" @onclick="DeleteConfig">
<i class="mdi mdi-delete" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<div class="content rcm-content flex-grow-1 d-flex gap-3 mt-3">
<div class="card p-2 config-list rcm-config-list">
<div class="card-body list-body p-0">
@switch (SelectedType)
{
case RobotConfigType.VDA5050:
@RenderList(VdaConfigs, SelectVda, v => v.ConfigName, v => v.IsActive)
break;
case RobotConfigType.Safety:
@RenderList(SafetyConfigs, SelectSafety, s => s.ConfigName, s => s.IsActive)
break;
case RobotConfigType.Simulation:
@RenderList(SimulationConfigs, SelectSimulation, s => s.ConfigName, s => s.IsActive)
break;
case RobotConfigType.PLC:
@RenderList(PlcConfigs, SelectPlc, p => p.ConfigName, p => p.IsActive)
break;
case RobotConfigType.Core:
@RenderList(CoreConfigs, SelectCore, c => c.ConfigName, c => c.IsActive)
break;
default:
<div class="p-2">No configs.</div>
break;
}
</div>
</div>
<div class="card p-2 config-content rcm-config-content">
<div class="card-body">
@if (!HasSelection)
{
<div class="text-muted">Select a config from the list or click Add to create one.</div>
}
else
{
@switch (SelectedType)
{
case RobotConfigType.VDA5050:
<RobotApp.Client.Pages.Components.Config.RobotVDA5050Config @ref="@RobotVDA5050ConfigRef" @bind-Model="SelectedVda" />
break;
case RobotConfigType.Safety:
<RobotApp.Client.Pages.Components.Config.RobotSafetyConfig @bind-Model="SelectedSafety" />
break;
case RobotConfigType.Simulation:
<RobotApp.Client.Pages.Components.Config.RobotSimulationConfig @bind-Model="SelectedSimulation" />
break;
case RobotConfigType.PLC:
<RobotApp.Client.Pages.Components.Config.RobotPLCConfig @bind-Model="SelectedPlc" />
break;
case RobotConfigType.Core:
<RobotApp.Client.Pages.Components.Config.RobotConfig @bind-Model="SelectedCore" />
break;
}
}
</div>
</div>
</div>
@if (IsLoading)
{
<div class="rcm-overlay" role="status" aria-live="polite" aria-busy="true">
<div class="rcm-overlay-content" aria-hidden="false">
<div class="spinner-border text-light" role="status" aria-hidden="true"></div>
<div class="rcm-overlay-message ms-2">Loading…</div>
</div>
</div>
}
@if (IsAddingNew)
{
<div class="rcm-modal-overlay" role="dialog" aria-modal="true">
<div class="rcm-modal">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>Create @SelectedType Config</strong>
<button class="btn btn-sm btn-link text-muted" @onclick="CloseAddDialog" aria-label="Close">✕</button>
</div>
<div class="card-body">
<EditForm Model="addForm" OnValidSubmit="SaveNewConfigAsync">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="mb-2">
<label class="form-label">Config name</label>
<InputText class="form-control" @bind-Value="addForm.ConfigName" />
</div>
<div class="mb-2">
<label class="form-label">Description</label>
<InputText class="form-control" @bind-Value="addForm.Description" />
</div>
<div class="mb-2">
<label class="form-label">Copy parameters from existing (@SelectedType)</label>
<select class="form-select" @bind="SelectedTemplateIdString">
@foreach (var t in GetTemplatesForSelectedType())
{
<option value="@t.Id">@t.Name</option>
}
</select>
<div class="form-text">If you choose an existing config, its parameter fields will be copied into the new config; you can still change name/description.</div>
</div>
<div class="d-flex justify-content-end gap-2 mt-3">
<button type="button" class="btn btn-outline-secondary" @onclick="CloseAddDialog">Cancel</button>
<button type="submit" class="btn btn-primary">Create</button>
</div>
</EditForm>
</div>
</div>
</div>
</div>
}
@if (ShowDeleteConfirm)
{
<div class="rcm-modal-overlay" role="dialog" aria-modal="true">
<div class="rcm-modal">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>Confirm Delete</strong>
<button class="btn btn-sm btn-link text-muted" @onclick="CancelDelete" aria-label="Close">✕</button>
</div>
<div class="card-body">
<p>Are you sure you want to delete configuration "<strong>@DeletePendingName</strong>"?</p>
<div class="d-flex justify-content-end gap-2 mt-3">
<button class="btn btn-outline-secondary" @onclick="CancelDelete">Cancel</button>
<button class="btn btn-danger" @onclick="ConfirmDeleteAsync">Delete</button>
</div>
</div>
</div>
</div>
</div>
}
</div>
@code {
private List<RobotVDA5050ConfigDto> VdaConfigs { get; set; } = new();
private List<RobotSafetyConfigDto> SafetyConfigs { get; set; } = new();
private List<RobotSimulationConfigDto> SimulationConfigs { get; set; } = new();
private List<RobotPlcConfigDto> PlcConfigs { get; set; } = new();
private List<RobotConfigDto> CoreConfigs { get; set; } = new();
private RobotVDA5050ConfigDto? SelectedVda { get; set; }
private RobotSafetyConfigDto? SelectedSafety { get; set; }
private RobotSimulationConfigDto? SelectedSimulation { get; set; }
private RobotPlcConfigDto? SelectedPlc { get; set; }
private RobotConfigDto? SelectedCore { get; set; }
private CreateRobotVDA5050ConfigDto CreateVda = new();
private CreateRobotConfigDto CreateSafety = new();
private CreateRobotSafetyConfigDto CreateSimulation = new();
private CreateRobotSimulationConfigDto CreatePlc = new();
private CreateRobotPlcConfigDto CreateCore = new();
private RobotConfigType SelectedType = RobotConfigType.VDA5050;
private RobotApp.Client.Pages.Components.Config.RobotVDA5050Config RobotVDA5050ConfigRef = default!;
private int SelectedIndex = -1;
private bool HasSelection => SelectedIndex >= 0;
private bool IsLoading = false;
private bool IsAddingNew = false;
private bool ShowDeleteConfirm = false;
private Guid? DeletePendingId;
private string DeletePendingName = string.Empty;
private class AddFormModel
{
public string ConfigName { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
}
private AddFormModel addForm = new();
private string SelectedTemplateIdString = string.Empty;
private IEnumerable<(Guid Id, string Name, bool Active)> GetTemplatesForSelectedType()
{
return SelectedType switch
{
RobotConfigType.VDA5050 => VdaConfigs.Select(x => (x.Id, x.ConfigName ?? string.Empty, x.IsActive)),
RobotConfigType.Safety => SafetyConfigs.Select(x => (x.Id, x.ConfigName ?? string.Empty, x.IsActive)),
RobotConfigType.Simulation => SimulationConfigs.Select(x => (x.Id, x.ConfigName ?? string.Empty, x.IsActive)),
RobotConfigType.PLC => PlcConfigs.Select(x => (x.Id, x.ConfigName ?? string.Empty, x.IsActive)),
RobotConfigType.Core => CoreConfigs.Select(x => (x.Id, x.ConfigName ?? string.Empty, x.IsActive)),
_ => [],
};
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (!firstRender) return;
await LoadForTypeAsync(SelectedType);
}
private async Task OnTypeChanged(ChangeEventArgs e)
{
if (e?.Value is null) return;
if (!Enum.TryParse<RobotConfigType>(e.Value.ToString(), out var newType)) return;
if (newType == SelectedType) return;
SelectedType = newType;
await LoadForTypeAsync(newType);
}
private async Task LoadForTypeAsync(RobotConfigType type)
{
IsLoading = true;
StateHasChanged();
try
{
switch (type)
{
case RobotConfigType.VDA5050:
await LoadVDA5050Configs();
break;
case RobotConfigType.Safety:
await LoadRobotSafetyConfigs();
break;
case RobotConfigType.Simulation:
await LoadRobotSimulationConfigs();
break;
case RobotConfigType.PLC:
await LoadRobotPlcConfigs();
break;
case RobotConfigType.Core:
await LoadRobotConfigs();
break;
}
}
finally
{
IsLoading = false;
StateHasChanged();
}
}
RenderFragment RenderList<T>(List<T> list, Action<int> onSelect, Func<T, string> nameSelector, Func<T, bool>? isActiveSelector = null) where T : class
{
return builder =>
{
if (list is null || !list.Any())
{
builder.OpenElement(0, "div");
builder.AddAttribute(1, "class", "p-2 text-muted");
builder.AddContent(2, "No configs found.");
builder.CloseElement();
return;
}
builder.OpenElement(3, "ul");
builder.AddAttribute(4, "class", "list-group list-group-flush");
for (int i = 0; i < list.Count; i++)
{
var item = list[i];
var idx = i;
builder.OpenElement(10 + i * 6, "li");
builder.AddAttribute(11 + i * 6, "class", $"list-group-item {(SelectedIndex == idx ? "active" : "")}");
builder.AddAttribute(12 + i * 6, "style", "cursor:pointer;padding:0.75rem 1rem;");
builder.AddAttribute(13 + i * 6, "onclick", EventCallback.Factory.Create(this, () => onSelect(idx)));
string name;
try
{
name = nameSelector(item) ?? "Unnamed";
}
catch
{
name = "Unnamed";
}
builder.AddContent(14 + i * 6, name);
bool isActive = false;
if (isActiveSelector is not null)
{
try
{
isActive = isActiveSelector(item);
}
catch
{
isActive = false;
}
}
if (isActive)
{
builder.OpenElement(15 + i * 6, "span");
builder.AddAttribute(16 + i * 6, "class", "badge bg-success ms-2 float-end");
builder.AddContent(17 + i * 6, "Active");
builder.CloseElement();
}
builder.CloseElement();
}
builder.CloseElement();
};
}
private Action<int> SelectVda => idx =>
{
SelectedIndex = idx;
SelectedVda = idx >= 0 && idx < VdaConfigs.Count ? VdaConfigs[idx] with { } : null;
StateHasChanged();
};
private Action<int> SelectSafety => idx =>
{
SelectedIndex = idx;
SelectedSafety = idx >= 0 && idx < SafetyConfigs.Count ? SafetyConfigs[idx] with { } : null;
StateHasChanged();
};
private Action<int> SelectSimulation => idx =>
{
SelectedIndex = idx;
SelectedSimulation = idx >= 0 && idx < SimulationConfigs.Count ? SimulationConfigs[idx] with { } : null;
StateHasChanged();
};
private Action<int> SelectPlc => idx =>
{
SelectedIndex = idx;
SelectedPlc = idx >= 0 && idx < PlcConfigs.Count ? PlcConfigs[idx] with { } : null;
StateHasChanged();
};
private Action<int> SelectCore => idx =>
{
SelectedIndex = idx;
SelectedCore = idx >= 0 && idx < CoreConfigs.Count ? CoreConfigs[idx] with { } : null;
StateHasChanged();
};
private void OpenAddConfig()
{
addForm = new AddFormModel();
var model = GetTemplatesForSelectedType();
var modelActive = model.Where(x => x.Active).ToList();
SelectedTemplateIdString = modelActive.Count > 0 ? modelActive.First().Id.ToString() : model.Any() ? model.First().Id.ToString() : string.Empty;
IsAddingNew = true;
}
private void CloseAddDialog()
{
IsAddingNew = false;
}
private void DeleteConfig()
{
var tuple = SelectedType switch
{
RobotConfigType.VDA5050 => (Id: SelectedVda?.Id, Name: SelectedVda?.ConfigName),
RobotConfigType.Safety => (Id: SelectedSafety?.Id, Name: SelectedSafety?.ConfigName),
RobotConfigType.Simulation => (Id: SelectedSimulation?.Id, Name: SelectedSimulation?.ConfigName),
RobotConfigType.PLC => (Id: SelectedPlc?.Id, Name: SelectedPlc?.ConfigName),
RobotConfigType.Core => (Id: SelectedCore?.Id, Name: SelectedCore?.ConfigName),
_ => (Id: (Guid?)null, Name: (string?)null)
};
if (tuple.Id is null || tuple.Id == Guid.Empty)
{
Snackbar.Add("No config selected to delete.", Severity.Warning);
return;
}
DeletePendingId = tuple.Id;
DeletePendingName = tuple.Name ?? string.Empty;
ShowDeleteConfirm = true;
}
private void CancelDelete()
{
ShowDeleteConfirm = false;
DeletePendingId = null;
DeletePendingName = string.Empty;
}
}