RobotApp/RobotApp.Client/Pages/Home.razor
2025-12-21 11:33:11 +07:00

622 lines
33 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@page "/"
@using System.Text.Json
@using System.Text.Json.Serialization
@using RobotApp.Client.Services
@using RobotApp.VDA5050.InstantAction
@using RobotApp.VDA5050.Order
@using RobotApp.VDA5050.Type
@using System.ComponentModel.DataAnnotations
@attribute [Authorize]
@rendermode InteractiveWebAssemblyNoPrerender
@inject IJSRuntime JS
@inject IDialogService DialogService
<MudMainContent Class="pa-0 ma-0">
<div style="height:100vh; overflow:hidden;">
<MudContainer MaxWidth="MaxWidth.False" Class="pa-4" Style="max-width: 100%; height:100%; display:flex; flex-direction:column;">
<!-- TIÊU ĐỀ -->
<MudText Typo="Typo.h4" Align="Align.Center" Class="mb-6 flex-shrink-0">
🧾 VDA5050 Order Editor
</MudText>
<MudGrid Spacing="4" Class="flex-grow-1" Style="overflow:hidden;">
<!-- ================= CỘT TRÁI (50%) ================= -->
<MudItem xs="12" md="7" Class="d-flex flex-column h-100" Style="gap:16px;">
<!-- Nodes và Edges chia 1x2, mỗi phần có scroll nội bộ -->
<MudGrid Spacing="4" Class="flex-grow-1" Style="overflow:hidden;">
<!-- Nodes (trái) -->
<MudItem xs="12" md="6" Class="h-100">
<MudPaper Class="pa-4 h-100 d-flex flex-column" Elevation="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-4 flex-shrink-0">
<MudText Typo="Typo.h6">📍 Nodes</MudText>
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" OnClick="AddNode">
Add Node
</MudButton>
</MudStack>
<!-- Cuộn nội bộ chỉ cho phần danh sách Nodes -->
<div class="flex-grow-1" style="overflow:auto;">
<MudExpansionPanels MultiExpansion="true">
@foreach (var node in Order.Nodes)
{
<MudExpansionPanel @key="node.NodeId">
<!-- ===== HEADER ===== -->
<TitleContent>
<div class="d-flex align-center justify-space-between w-100">
<MudText Typo="Typo.subtitle1" Class="fw-bold">
@node.NodeId
</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Color="Color.Primary"
Size="Size.Small"
OnClick="@(() => OpenEditNodeDialog(node))"
StopPropagation="true" />
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
Size="Size.Small"
OnClick="@(() => RemoveNode(node))"
StopPropagation="true" />
</div>
</TitleContent>
<!-- ===== BODY (BẮT BUỘC ChildContent) ===== -->
<ChildContent>
<MudGrid Spacing="3">
<MudItem xs="12">
<MudTextField @bind-Value="node.NodeId" Label="Node ID" />
</MudItem>
<MudItem xs="12">
<MudNumericField T="int"
@bind-Value="node.SequenceId"
Label="Sequence ID" />
</MudItem>
<MudItem xs="12">
<MudSwitch T="bool"
@bind-Checked="node.Released"
Label="Released" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="double"
@bind-Value="node.NodePosition.X"
Label="X" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="double"
@bind-Value="node.NodePosition.Y"
Label="Y" />
</MudItem>
<MudItem xs="12">
<MudTextField @bind-Value="node.NodePosition.MapId"
Label="Map ID" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="double"
@bind-Value="node.NodePosition.Theta"
Label="Theta (rad)" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="double"
@bind-Value="node.NodePosition.AllowedDeviationXY"
Label="Allowed Dev XY" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="double"
@bind-Value="node.NodePosition.AllowedDeviationTheta"
Label="Allowed Dev Theta" />
</MudItem>
<MudItem xs="12">
<MudDivider Class="my-4" />
<MudText Typo="Typo.subtitle1" Class="mb-3">
Actions
</MudText>
@foreach (var act in node.Actions)
{
<MudPaper Class="pa-3 mb-3" Outlined>
<MudGrid Spacing="3">
<MudItem xs="12">
<MudSelect T="string"
Label="Action Type"
@bind-Value="act.ActionType"
Dense="true"
Required="true">
@foreach (var at in Enum.GetValues<ActionType>())
{
<MudSelectItem Value="@at.ToString()">
@at
</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudSelect T="string"
@bind-Value="act.BlockingType"
Label="Blocking Type">
<MudSelectItem Value="@("NONE")">NONE</MudSelectItem>
<MudSelectItem Value="@("SOFT")">SOFT</MudSelectItem>
<MudSelectItem Value="@("HARD")">HARD</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudTextField @bind-Value="act.ActionId"
Label="Action ID" />
</MudItem>
</MudGrid>
<MudText Typo="Typo.caption" Class="mt-3 mb-2">
Action Parameters
</MudText>
@foreach (var p in act.ActionParameters.Cast<UiActionParameter>())
{
<MudGrid Class="mt-1">
<MudItem xs="6">
<MudTextField @bind-Value="p.Key" Label="Key" />
</MudItem>
<MudItem xs="6">
<MudTextField @bind-Value="p.ValueString" Label="Value" />
</MudItem>
<MudItem xs="2">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
OnClick="@(() => RemoveActionParameter(act, p))" />
</MudItem>
</MudGrid>
}
<MudButton Size="Size.Small"
StartIcon="@Icons.Material.Filled.Add"
Class="mt-3"
OnClick="@(() => AddActionParameter(act))">
Add Parameter
</MudButton>
<MudDivider Class="my-3" />
<MudButton Size="Size.Small"
Color="Color.Error"
Variant="Variant.Text"
StartIcon="@Icons.Material.Filled.Delete"
OnClick="@(() => RemoveAction(node, act))">
Remove Action
</MudButton>
</MudPaper>
}
<MudButton Size="Size.Small"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@(() => AddAction(node))">
Add Action
</MudButton>
</MudItem>
</MudGrid>
</ChildContent>
</MudExpansionPanel>
}
</MudExpansionPanels>
</div>
</MudPaper>
</MudItem>
<!-- Edges (phải) -->
<MudItem xs="12" md="6" Class="h-100">
<MudPaper Class="pa-4 h-100 d-flex flex-column" Elevation="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-4 flex-shrink-0">
<MudText Typo="Typo.h6">🔗 Edges</MudText>
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" OnClick="AddEdge">
Add Edge
</MudButton>
</MudStack>
<!-- Cuộn nội bộ chỉ cho phần danh sách Edges -->
<div class="flex-grow-1" style="overflow:auto;">
<MudExpansionPanels MultiExpansion="true">
@foreach (var edge in Order.Edges)
{
<MudExpansionPanel Text="@($"{edge.EdgeId} ({edge.StartNodeId} → {edge.EndNodeId})")"
@key="edge.EdgeId">
<ChildContent>
<MudGrid Spacing="3">
<!-- Edge ID -->
<MudItem xs="12">
<MudTextField @bind-Value="edge.EdgeId"
Label="Edge ID" />
</MudItem>
<!-- Start Node -->
<MudItem xs="12">
<MudSelect T="string"
Label="Start Node"
Dense="true"
Required="true"
@bind-Value="edge.StartNodeId">
@foreach (var node in Order.Nodes)
{
<MudSelectItem Value="@node.NodeId">
@node.NodeId
</MudSelectItem>
}
</MudSelect>
</MudItem>
<!-- End Node -->
<MudItem xs="12">
<MudSelect T="string"
Label="End Node"
Dense="true"
Required="true"
@bind-Value="edge.EndNodeId">
@foreach (var node in Order.Nodes)
{
<MudSelectItem Value="@node.NodeId"
Disabled="@(node.NodeId == edge.StartNodeId)">
@node.NodeId
</MudSelectItem>
}
</MudSelect>
</MudItem>
<!-- Radius: =0 → đường thẳng -->
<MudItem xs="6">
<MudNumericField T="double"
@bind-Value="edge.Radius"
Label="Bán kính (0 = đường thẳng)"
Min="0"
Step="0.1" />
</MudItem>
<!-- Quadrant chỉ hiện khi Radius > 0 -->
@if (edge.Radius > 0)
{
<MudItem xs="6">
<MudSelect T="Quadrant"
@bind-Value="edge.Quadrant"
Label="Góc phần tư">
<MudSelectItem Value="Quadrant.I">Góc I</MudSelectItem>
<MudSelectItem Value="Quadrant.II">Góc II</MudSelectItem>
<MudSelectItem Value="Quadrant.III">Góc III</MudSelectItem>
<MudSelectItem Value="Quadrant.IV">Góc IV</MudSelectItem>
</MudSelect>
</MudItem>
}
@if (edge.Radius > 0 && !edge.Expanded)
{
<MudItem xs="12">
<MudButton Color="Color.Primary"
Variant="Variant.Outlined"
StartIcon="@Icons.Material.Filled.Merge"
OnClick="@(() => ApplyCurve(edge))">
Apply Curve (tạo node)
</MudButton>
</MudItem>
}
<MudItem xs="12" Class="mt-4">
<MudButton Color="Color.Error"
Variant="Variant.Text"
StartIcon="@Icons.Material.Filled.Delete"
OnClick="@(() => RemoveEdge(edge))">
Remove Edge
</MudButton>
</MudItem>
</MudGrid>
</ChildContent>
</MudExpansionPanel>
}
</MudExpansionPanels>
</div>
</MudPaper>
</MudItem>
</MudGrid>
</MudItem>
<!-- ================= CỘT PHẢI (50%) - JSON Output ================= -->
<MudItem xs="12" md="5" Class="h-100">
<MudPaper Class="pa-4 h-100 d-flex flex-column" Elevation="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-4 flex-shrink-0">
<MudText Typo="Typo.h6">📄 JSON Output (/order)</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Success"
StartIcon="@Icons.Material.Filled.Send"
OnClick="SendOrderToServer">
Send
</MudButton>
@if (!string.IsNullOrEmpty(sendResult))
{
<MudAlert Severity="@(sendResult.StartsWith("✅") ? Severity.Success : Severity.Error)"
Class="mt-3">
@sendResult
</MudAlert>
}
<MudTooltip Text="@(copied ? "Copied!" : "Copy to clipboard")">
<MudButton Variant="Variant.Filled"
Color="@(copied ? Color.Success : Color.Primary)"
Size="Size.Small"
StartIcon="@(copied ? Icons.Material.Filled.Check : Icons.Material.Filled.ContentCopy)"
OnClick="CopyJsonToClipboard">
@if (copied)
{
<MudText Class="ml-2">Copied!</MudText>
}
else
{
<MudText Class="ml-2">Copy</MudText>
}
</MudButton>
</MudTooltip>
</MudStack>
<!-- Cuộn nội bộ cho JSON -->
<div class="flex-grow-1" style="overflow:auto;">
<MudTextField Value="@OrderJson"
ReadOnly="true"
Variant="Variant.Filled"
Lines="70"
Class="h-100"
Style="font-family: 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 0.875rem;
background-color: #1e1e1e;
color: #d4d4d4;" />
</div>
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer>
</div>
</MudMainContent>
<script>
window.copyText = async function (text) {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
return true;
}
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
document.execCommand("copy");
return true;
} catch (err) {
console.error("Copy failed", err);
return false;
} finally {
document.body.removeChild(textarea);
}
};
</script>
@code {
[Inject] HttpClient Http { get; set; } = default!;
bool sending;
string? sendResult;
bool IsEditNodeOpen;
Node? EditingNode;
OrderMessage Order = new();
void ApplyCurve(UiEdge edge)
{
if (edge.Radius <= 0 || edge.Expanded)
return;
var startNode = Order.Nodes.First(n => n.NodeId == edge.StartNodeId);
// 1⃣ Sinh node C (điểm kết thúc cung tròn)
var endNode = OrderMessage.CreateCurveNode(startNode, edge);
Order.Nodes.Add(endNode);
// 2⃣ Edge trở thành A → C (đường cong)
edge.EndNodeId = endNode.NodeId;
edge.Expanded = true;
// 3⃣ Resequence node (đúng chuẩn VDA5050)
for (int i = 0; i < Order.Nodes.Count; i++)
Order.Nodes[i].SequenceId = i;
}
async Task SendOrderToServer()
{
if (sending)
return;
sending = true;
sendResult = null;
StateHasChanged();
try
{
var response = await Http.PostAsJsonAsync(
"/api/order",
JsonSerializer.Deserialize<JsonElement>(OrderJson)
);
if (response.IsSuccessStatusCode)
{
sendResult = "✅ Done!";
}
else
{
sendResult = $"❌ Failed: {response.StatusCode}";
}
}
catch (Exception ex)
{
sendResult = $"❌ Error: {ex.Message}";
}
finally
{
sending = false;
StateHasChanged();
}
}
void AddNode()
{
Order.Nodes.Add(new Node
{
NodeId = $"NODE_{Order.Nodes.Count + 1}",
SequenceId = Order.Nodes.Count,
Released = true,
NodePosition = new NodePosition { MapId = "MAP_01" }
});
}
void RemoveNode(Node node)
{
Order.Nodes.Remove(node);
Order.Edges.RemoveAll(e =>
e.StartNodeId == node.NodeId ||
e.EndNodeId == node.NodeId);
for (int i = 0; i < Order.Nodes.Count; i++)
Order.Nodes[i].SequenceId = i;
}
void RemoveAction(Node node, VDA5050.InstantAction.Action act)
{
node.Actions = node.Actions
.Where(a => a != act)
.ToArray();
}
void AddAction(Node node)
{
node.Actions = node.Actions
.Append(new VDA5050.InstantAction.Action
{
ActionId = Guid.NewGuid().ToString(),
ActionType = ActionType.startPause.ToString(),
BlockingType = "NONE",
ActionParameters = Array.Empty<ActionParameter>()
})
.ToArray();
}
private void RemoveActionParameter(VDA5050.InstantAction.Action act, ActionParameter param)
{
if (act.ActionParameters == null) return;
act.ActionParameters = act.ActionParameters
.Where(p => p != param)
.ToArray();
}
void AddActionParameter(VDA5050.InstantAction.Action act)
{
var newList = (act.ActionParameters ?? Array.Empty<ActionParameter>()).ToList();
newList.Add(new UiActionParameter());
act.ActionParameters = newList.ToArray();
}
void AddEdge()
{
if (Order.Nodes.Count < 2) return;
Order.Edges.Add(new RobotApp.Client.Services.UiEdge
{
EdgeId = $"EDGE_{Order.Edges.Count + 1}",
StartNodeId = Order.Nodes[^2].NodeId,
EndNodeId = Order.Nodes[^1].NodeId
});
}
bool copied = false;
CancellationTokenSource? _copyCts;
async Task CopyJsonToClipboard()
{
_copyCts?.Cancel();
_copyCts = new();
var success = await JS.InvokeAsync<bool>(
"copyText",
OrderJson
);
if (!success)
return;
copied = true;
StateHasChanged();
try
{
await Task.Delay(1500, _copyCts.Token);
}
catch { }
copied = false;
StateHasChanged();
}
void RemoveEdge(RobotApp.Client.Services.UiEdge edge)
{
Order.Edges.Remove(edge);
}
string OrderJson =>
JsonSerializer.Serialize(
Order.ToSchemaObject(),
new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
private async Task OpenEditNodeDialog(Node node)
{
var parameters = new DialogParameters<EditNodeDialog>
{
{ x => x.Node, node } // Truyền trực tiếp reference gốc
};
var options = new DialogOptions
{
CloseButton = true,
MaxWidth = MaxWidth.Large,
FullWidth = true,
CloseOnEscapeKey = true
};
var dialog = await DialogService.ShowAsync<EditNodeDialog>($"Edit Node: {node.NodeId}", parameters, options);
await dialog.Result; // Đợi dialog đóng
StateHasChanged();
}
}