622 lines
33 KiB
Plaintext
622 lines
33 KiB
Plaintext
@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();
|
||
}
|
||
}
|