RobotNet/RobotNet.WebApp/Maps/Components/Editor/Node/MapNode.razor
2025-10-15 15:15:53 +07:00

490 lines
21 KiB
Plaintext

@using System.Text.Json
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using RobotNet.WebApp.Maps.Components.Editor.Element
@inject ISnackbar Snackbar
@inject IHttpClientFactory HttpClientFactory
@inject IDialogService Dialog
@foreach (var node in Models)
{
<Node Model="node" ShowName="@ShowName" OnClick="NodeClickChanged" DoubleClick="OnNodeDoubleClick" EditorState="@EditorState" />
}
@foreach (var element in Elements)
{
<Element Model="@element" ShowName="@ShowName" EditorState="EditorState" DoubleClick="ElementDoubleClick" />
}
<MudDialog @bind-Visible="updateNodeVisible">
<TitleContent>
<MudText Typo="Typo.h6">
Update node @UpdateModel.Id
</MudText>
</TitleContent>
<DialogContent>
<div class="d-flex flex-row">
<div class="d-flex flex-column">
<div class="mb-2">
<MudTextField @bind-Value="UpdateModel.Name" Label="Name" ShrinkLabel Variant="Variant.Outlined" Margin="Margin.Dense" ReadOnly=@(MapIsActive) />
</div>
<div class="mb-2">
<MudNumericField @bind-Value="UpdateModel.X" Label="X" ShrinkLabel Variant="Variant.Outlined" Margin="Margin.Dense" ReadOnly=@(MapIsActive) />
</div>
<div class="mb-2">
<MudNumericField @bind-Value="UpdateModel.Y" Label="Y" ShrinkLabel Variant="Variant.Outlined" Margin="Margin.Dense" ReadOnly=@(MapIsActive) />
</div>
<div class="mb-2">
<MudNumericField @bind-Value="UpdateModel.Theta" Label="Theta" AdornmentText="Degree" Max="180" Min="-180" ShrinkLabel Variant="Variant.Outlined" Margin="Margin.Dense" ReadOnly=@(MapIsActive) />
</div>
<div class="mb-2">
<MudNumericField @bind-Value="UpdateModel.AllowedDeviationXy" Label="AllowedDeviationXy" Step="0.01" Min="0" ShrinkLabel Variant="Variant.Outlined" Margin="Margin.Dense" ReadOnly=@(MapIsActive) />
</div>
<div>
<MudNumericField @bind-Value="UpdateModel.AllowedDeviationTheta" Label="AllowedDeviationTheta" Step="1" Min="0" Max="180" ShrinkLabel Variant="Variant.Outlined" Margin="Margin.Dense" ReadOnly=@(MapIsActive) />
</div>
</div>
<div class="d-flex flex-column ms-2 flex-grow-1">
<div class="d-flex flex-row mb-5">
<MudSelect @bind-Value=ActionSelected Margin="Margin.Dense" T="ActionDto" Label="Action" Variant="Variant.Outlined" Class="w-100" AnchorOrigin="Origin.BottomLeft">
@foreach (var action in Actions)
{
<MudSelectItem Value="@action">@action.Name</MudSelectItem>
}
</MudSelect>
<MudFab Class="ms-2" Color="Color.Secondary" StartIcon="@Icons.Material.Filled.Add" Size="Size.Medium" OnClick="AddAction" />
</div>
<div class="paper-action">
@foreach (var actionId in UpdateModel.Actions)
{
<div class="m-1" style="height: fit-content">
<MudButton Variant="Variant.Filled" Color="Color.Info" Style="text-transform:none" OnClick="(() => DeleteAction(actionId))">
@(Actions.FirstOrDefault(ac => ac.Id == actionId)?.Name)
</MudButton>
</div>
}
</div>
</div>
</div>
</DialogContent>
<DialogActions>
<MudButton OnClick="(() => updateNodeVisible = false)" Class="px-10" Variant="Variant.Filled">Cancel</MudButton>
<MudButton OnClick="UpdateNode" Variant="Variant.Filled" Color="Color.Primary" Class="px-10 me-4" Disabled=@(MapIsActive)>Update</MudButton>
</DialogActions>
</MudDialog>
<MudDialog @bind-Visible="createElementVisible">
<TitleContent>
<MudText Typo="Typo.h6">
Create Element Node @ElementCreateModel.NodeId
</MudText>
</TitleContent>
<DialogContent>
<div class="d-flex flex-column" style="max-height: 700px; overflow-y: auto">
<MudTextField Class="my-2"
@bind-Value="ElementCreateModel.Name" T="string"
Label="Name" Required="true" RequiredError="Name is required!"
Counter="18" Variant="Variant.Outlined"
Validation="@(new Func<string, IEnumerable<string>>(MapEditorHelper.NameValidation))" />
<div class="d-flex flex-row mb-2">
<MudNumericField Class="me-2" @bind-Value="ElementCreateModel.OffsetX" Label="Offset X" Step="0.1" Variant="Variant.Outlined" />
<MudNumericField Class="ms-2" @bind-Value="ElementCreateModel.OffsetY" Label="Offset Y" Step="0.1" Variant="Variant.Outlined" />
</div>
<MudSelect Class="mb-2" T="ElementModelDto" Variant="Variant.Outlined" Label="Type" Required="true" RequiredError="Type is required!"
@bind-Value="@ElementModelSelected">
@foreach (var model in ElementModels)
{
<MudSelectItem Value="@model">@model.Name</MudSelectItem>
}
</MudSelect>
</div>
</DialogContent>
<DialogActions>
<MudButton OnClick="(() => createElementVisible = false)" Class="px-10" Variant="Variant.Filled">Cancel</MudButton>
<MudButton OnClick="CreateElement" Variant="Variant.Filled" Color="Color.Primary" Class="px-10" Disabled=@(MapIsActive)>Create</MudButton>
</DialogActions>
</MudDialog>
<MudDialog @bind-Visible="updateElementVisble">
<TitleContent>
<MudText Typo="Typo.h6">
Update Element @ElementUpdateModel.Name
</MudText>
</TitleContent>
<DialogContent>
<div class="d-flex flex-row" style="max-height: 700px; overflow-y: auto; width: 600px">
<div class="d-flex flex-column h-100 me-2" style="width: 47%">
<MudTextField T="string" Label="Name" @bind-value="ElementUpdateModel.Name" Variant="Variant.Outlined" ReadOnly="@(MapIsActive)"
Required="true" RequiredError="Name is required!"
Validation="@(new Func<string, IEnumerable<string>>(MapEditorHelper.NameValidation))" />
<MudNumericField T="double" Label="Offset X" @bind-value="ElementUpdateModel.OffsetX" Variant="Variant.Outlined" Step="0.1" ReadOnly="@(MapIsActive)" />
<MudNumericField T="double" Label="Offset Y" @bind-value="ElementUpdateModel.OffsetY" Variant="Variant.Outlined" Step="0.1" ReadOnly="@(MapIsActive)" />
@* <MudCheckBox Size="Size.Small" T="bool" LabelPlacement="Placement.Start" Label="IsOpen" @bind-value="ElementUpdateModel.IsOpen" Color="Color.Success"
UncheckedColor="Color.Default" ReadOnly /> *@
</div>
<div class="paper-property-container">
<div class="paper-property ms-2">
@foreach (var property in ElementProperties)
{
@if (property.Type == typeof(int).ToString())
{
if (int.TryParse(property.DefaultValue, out int value))
{
<MudNumericField T="int" Margin="Margin.Dense" Label="@property.Name" Value="value" Variant="Variant.Outlined" ReadOnly="@(MapIsActive)"
ValueChanged="((data) => DefaultIntValueChanged(property.Id, data))" />
}
}
else if (property.Type == typeof(double).ToString())
{
if (double.TryParse(property.DefaultValue, out double value))
{
<MudNumericField T="double" Margin="Margin.Dense" Label="@property.Name" Value="value" Variant="Variant.Outlined" Step="0.1" ReadOnly="@(MapIsActive)"
ValueChanged="((data) => DefaultDoubleValueChanged(property.Id, data))" />
}
}
else if (property.Type == typeof(bool).ToString())
{
if (bool.TryParse(property.DefaultValue, out bool value))
{
<MudCheckBox Class="m-0" Size="Size.Small" T="bool" LabelPlacement="Placement.Start" Label="@property.Name" Value="value" Color="Color.Success"
UncheckedColor="Color.Default" ReadOnly="@(MapIsActive)" ValueChanged="((data) => DefaultBooleanValueChanged(property.Id, data))" />
}
}
else if (property.Type == typeof(string).ToString())
{
<MudTextField T="string" Label="@property.Name" Value="@property.DefaultValue" Variant="Variant.Outlined" Margin="Margin.Dense" ReadOnly="@(MapIsActive)"
ValueChanged="((data) => property.DefaultValue = data)" />
}
}
</div>
<span class="paper-title">Properties</span>
</div>
</div>
</DialogContent>
<DialogActions>
<MudButton OnClick="Delete" Variant="Variant.Filled" Color="Color.Secondary" Class="px-10" Disabled=@(MapIsActive)>Delete</MudButton>
<MudButton OnClick="(() => updateElementVisble = false)" Class="px-10" Variant="Variant.Filled">Cancel</MudButton>
<MudButton OnClick="UpdateElement" Variant="Variant.Filled" Color="Color.Primary" Class="px-10" Disabled=@(MapIsActive)>Update</MudButton>
</DialogActions>
</MudDialog>
@code {
[Parameter, EditorRequired]
public MapNodeModel Models { get; set; } = null!;
[Parameter]
public MapElementModel Elements { get; set; } = [];
[CascadingParameter]
protected bool MapIsActive { get; set; }
[Parameter]
public bool ShowName { get; set; }
[Parameter]
public EditorState EditorState { get; set; }
private NodeUpdateModel UpdateModel = new();
private bool updateNodeVisible;
private List<ActionDto> Actions = [];
private ActionDto? ActionSelected = null;
public List<ElementModelDto> ElementModels { get; set; } = [];
private bool createElementVisible;
private ElementCreateModel ElementCreateModel = new();
private ElementModelDto? ElementModelSelected = null;
private NodeModel? NodeModelDoubleClick = null;
private bool updateElementVisble;
private ElementUpdateModel ElementUpdateModel = new();
private List<ElementProperty> ElementProperties = [];
private HttpClient Http = default!;
protected override void OnAfterRender(bool firstRender)
{
base.OnAfterRender(firstRender);
if (!firstRender) return;
Http = HttpClientFactory.CreateClient("MapManagerAPI");
Models.Changed += StateHasChanged;
Elements.Changed += StateHasChanged;
}
private void NodeClickChanged(NodeModel? node) => Models.SelectedNode = node;
private async Task LoadActionAsync(Guid mapId)
{
var result = await Http.GetFromJsonAsync<IEnumerable<ActionDto>>($"api/Actions/{mapId}");
if (result is not null && result.Any())
{
Actions.Clear();
Actions.AddRange(result);
}
if (Actions.Any()) ActionSelected = Actions.First();
StateHasChanged();
}
private async Task LoadElementModels(Guid mapId)
{
ElementModels.Clear();
var elModels = await Http.GetFromJsonAsync<MessageResult<IEnumerable<ElementModelDto>>>($"api/ElementModels/map/{mapId}");
if (elModels is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error);
else if (!elModels.IsSuccess) Snackbar.Add($"Có lỗi xảy ra: {elModels.Message}", Severity.Error);
else if(elModels.Data is null || !elModels.Data.Any())
{
Snackbar.Add("Không có Element model nào trong bản đồ này", Severity.Warning);
ElementModelSelected = null;
}
else
{
ElementModels.AddRange(elModels.Data.OrderBy(el => el.Name));
if (ElementModels.Count > 0)
{
ElementModelSelected = ElementModelSelected is null ? ElementModels.First() : ElementModels.FirstOrDefault(em => em.Id == ElementModelSelected.Id) ?? ElementModels.First();
}
}
StateHasChanged();
}
private void AddAction()
{
if (ActionSelected is null) return;
if (UpdateModel.Actions.Any(action => action == ActionSelected.Id))
{
Snackbar.Add("Action đã tồn tại", Severity.Warning);
return;
}
UpdateModel.Actions = [.. UpdateModel.Actions, ActionSelected.Id];
StateHasChanged();
}
private void DeleteAction(Guid actionId)
{
UpdateModel.Actions = UpdateModel.Actions.Where(a => actionId != a).ToArray();
StateHasChanged();
}
private async Task OnNodeDoubleClick(NodeModel model)
{
if (model == null || (EditorState != EditorState.NavigationEdit && EditorState != EditorState.View)) return;
NodeModelDoubleClick = model;
if (EditorState == EditorState.NavigationEdit)
{
await LoadActionAsync(model.MapId);
UpdateModel.Id = model.Id;
UpdateModel.Name = model.Name;
UpdateModel.X = model.X;
UpdateModel.Y = model.Y;
UpdateModel.Theta = model.Theta;
UpdateModel.AllowedDeviationXy = model.AllowedDeviationXy;
UpdateModel.AllowedDeviationTheta = model.AllowedDeviationTheta;
UpdateModel.Actions = [];
var actions = JsonSerializer.Deserialize<Guid[]>(model.Actions ?? "");
if (actions is not null && actions.Length > 0)
{
UpdateModel.Actions = [.. actions];
}
updateNodeVisible = true;
}
else if (EditorState == EditorState.View)
{
await LoadElementModels(model.MapId);
ElementCreateModel.Name = "";
ElementCreateModel.MapId = model.MapId;
ElementCreateModel.NodeId = model.Id;
createElementVisible = true;
}
StateHasChanged();
}
private async Task UpdateNode()
{
var selectedModel = Models.FirstOrDefault(e => e.Id == UpdateModel.Id);
if (selectedModel == null)
{
updateNodeVisible = false;
StateHasChanged();
return;
}
var result = await (await Http.PutAsJsonAsync($"api/Nodes", UpdateModel)).Content.ReadFromJsonAsync<MessageResult>();
if (result is null)
{
Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error);
return;
}
else if (!result.IsSuccess)
{
Snackbar.Add(result.Message, Severity.Error);
return;
}
selectedModel.UpdateData(UpdateModel);
updateNodeVisible = false;
Snackbar.Add("Cập nhật thành công", Severity.Success);
StateHasChanged();
}
private async Task CreateElement()
{
try
{
if (NodeModelDoubleClick is null) return;
if (ElementModelSelected is null)
{
Snackbar.Add("Hãy chọn Element model!", Severity.Warning);
return;
}
if (string.IsNullOrEmpty(ElementCreateModel.Name))
{
Snackbar.Add("Name không được để trống.", Severity.Warning);
return;
}
var nameInvalid = MapEditorHelper.NameChecking(ElementCreateModel.Name);
if (!nameInvalid.IsSuccess)
{
Snackbar.Add(nameInvalid.returnStr, Severity.Warning);
return;
}
ElementCreateModel.ModelId = ElementModelSelected.Id;
var create = await (await Http.PostAsJsonAsync("api/Elements", ElementCreateModel)).Content.ReadFromJsonAsync<MessageResult<ElementDto>>();
if (create is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error);
else if (!create.IsSuccess) Snackbar.Add($"Có lỗi xảy ra: {create.Message}", Severity.Error);
else if (create.Data is null) Snackbar.Add("Tạo Element không thành công", Severity.Error);
else
{
Elements.Add(new(create.Data, NodeModelDoubleClick));
createElementVisible = false;
Snackbar.Add("Tạo Element thành công!", Severity.Success);
}
StateHasChanged();
}
catch (AccessTokenNotAvailableException ex)
{
ex.Redirect();
return;
}
}
private void ElementDoubleClick(ElementModel model)
{
ElementUpdateModel.Id = model.Id;
ElementUpdateModel.Name = model.Name;
ElementUpdateModel.IsOpen = model.IsOpen;
ElementUpdateModel.OffsetX = model.OffsetX;
ElementUpdateModel.OffsetY = model.OffsetY;
ElementUpdateModel.Content = model.Content;
if (model is not null && !string.IsNullOrEmpty(model.Content))
{
var properties = JsonSerializer.Deserialize<List<ElementProperty>>(model.Content, JsonOptionExtends.Read);
if (properties is not null) ElementProperties = [.. properties];
}
updateElementVisble = true;
StateHasChanged();
}
private async Task Delete()
{
var parameters = new DialogParameters<ConfirmDialog>
{
{ x => x.Content, $"Bạn có chắc chắn muốn xóa element {ElementUpdateModel.Name} đi không?" },
{ x => x.ConfirmText, "Delete" },
{ x => x.Color, Color.Secondary }
};
var ConfirmDelete = await Dialog.ShowAsync<ConfirmDialog>("Xoá Element", parameters);
var result = await ConfirmDelete.Result;
if (result is not null && result.Data is not null && bool.TryParse(result.Data.ToString(), out bool data) && data)
{
var response = await Http.DeleteFromJsonAsync<MessageResult>($"api/Elements/{ElementUpdateModel.Id}");
if (response is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error);
else if (!response.IsSuccess) Snackbar.Add(response.Message ?? "Lỗi chưa xác định.", Severity.Error);
else
{
var elementData = Elements.FirstOrDefault(m => m.Id == ElementUpdateModel.Id);
if (elementData is not null)
{
Elements.Remove(elementData);
Snackbar.Add($"Xóa element thành công.", Severity.Success);
}
else Snackbar.Add($"Element không tồn tại.", Severity.Warning);
}
updateElementVisble = false;
StateHasChanged();
}
}
private void DefaultIntValueChanged(Guid id, int value)
{
var property = ElementProperties.FirstOrDefault(p => p.Id == id);
if (property is null) return;
property.DefaultValue = value.ToString();
}
private void DefaultDoubleValueChanged(Guid id, double value)
{
var property = ElementProperties.FirstOrDefault(p => p.Id == id);
if (property is null) return;
property.DefaultValue = value.ToString();
}
private void DefaultBooleanValueChanged(Guid id, bool value)
{
var property = ElementProperties.FirstOrDefault(p => p.Id == id);
if (property is null) return;
property.DefaultValue = value.ToString();
}
private async Task UpdateElement()
{
var selectedElement = Elements.FirstOrDefault(e => e.Id == ElementUpdateModel.Id);
if (selectedElement is null) return;
var result = await (await Http.PutAsJsonAsync($"api/Elements", new ElementUpdateModel()
{
Id = ElementUpdateModel.Id,
Name = ElementUpdateModel.Name,
IsOpen = ElementUpdateModel.IsOpen,
OffsetX = ElementUpdateModel.OffsetX,
OffsetY = ElementUpdateModel.OffsetY,
Content = JsonSerializer.Serialize(ElementProperties, JsonOptionExtends.Write),
})).Content.ReadFromJsonAsync<MessageResult<ElementDto>>();
if (result == null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error);
else if (!result.IsSuccess) Snackbar.Add(result.Message, Severity.Error);
else if (result.Data is null) Snackbar.Add("Lỗi dữ liệu", Severity.Error);
else
{
selectedElement.UpdateElement(result.Data);
Snackbar.Add("Cập nhật thành công", Severity.Success);
}
updateElementVisble = false;
StateHasChanged();
}
public void Dispose()
{
Models.Changed -= StateHasChanged;
Elements.Changed -= StateHasChanged;
GC.SuppressFinalize(this);
}
}