652 lines
32 KiB
Plaintext
652 lines
32 KiB
Plaintext
@page "/"
|
|
@using System.Text.Json
|
|
@using System.Text.Json.Serialization
|
|
@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">
|
|
|
|
<!-- ===== 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">
|
|
<MudGrid Spacing="3">
|
|
<MudItem xs="12"><MudTextField @bind-Value="edge.EdgeId" Label="Edge ID" /></MudItem>
|
|
<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>
|
|
|
|
<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>
|
|
|
|
<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>
|
|
</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();
|
|
|
|
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()
|
|
});
|
|
}
|
|
|
|
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 VDA5050.Order.Edge
|
|
{
|
|
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(VDA5050.Order.Edge edge)
|
|
{
|
|
Order.Edges.Remove(edge);
|
|
}
|
|
|
|
string OrderJson =>
|
|
JsonSerializer.Serialize(
|
|
Order.ToSchemaObject(),
|
|
new JsonSerializerOptions
|
|
{
|
|
WriteIndented = true,
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
|
});
|
|
|
|
|
|
public class UiActionParameter : ActionParameter
|
|
{
|
|
[JsonIgnore]
|
|
public string ValueString
|
|
{
|
|
get => Value?.ToString() ?? "";
|
|
set => Value = value;
|
|
}
|
|
}
|
|
|
|
public class OrderMessage
|
|
{
|
|
public int HeaderId { get; set; }
|
|
public string Timestamp { get; set; } = "";
|
|
public string Version { get; set; } = "v1";
|
|
public string Manufacturer { get; set; } = "PNKX";
|
|
public string SerialNumber { get; set; } = "AMR-01";
|
|
public string OrderId { get; set; } = Guid.NewGuid().ToString();
|
|
public int OrderUpdateId { get; set; }
|
|
public string? ZoneSetId { get; set; }
|
|
public List<Node> Nodes { get; set; } = new();
|
|
public List<VDA5050.Order.Edge> Edges { get; set; } = new();
|
|
|
|
public object ToSchemaObject()
|
|
{
|
|
int seq = 0;
|
|
|
|
return new
|
|
{
|
|
headerId = HeaderId++,
|
|
|
|
timestamp = string.IsNullOrWhiteSpace(Timestamp)
|
|
? DateTime.UtcNow.ToString("O")
|
|
: Timestamp,
|
|
|
|
version = Version,
|
|
manufacturer = Manufacturer,
|
|
serialNumber = SerialNumber,
|
|
orderId = OrderId,
|
|
orderUpdateId = OrderUpdateId,
|
|
|
|
zoneSetId = string.IsNullOrWhiteSpace(ZoneSetId)
|
|
? null
|
|
: ZoneSetId,
|
|
|
|
// ================= NODES =================
|
|
nodes = Nodes
|
|
.Select(n => new
|
|
{
|
|
nodeId = n.NodeId,
|
|
sequenceId = seq++,
|
|
released = n.Released,
|
|
|
|
nodePosition = new
|
|
{
|
|
x = n.NodePosition.X,
|
|
y = n.NodePosition.Y,
|
|
theta = n.NodePosition.Theta,
|
|
|
|
allowedDeviationXY = n.NodePosition.AllowedDeviationXY,
|
|
allowedDeviationTheta = n.NodePosition.AllowedDeviationTheta,
|
|
|
|
mapId = string.IsNullOrWhiteSpace(n.NodePosition.MapId)
|
|
? "MAP_01"
|
|
: n.NodePosition.MapId
|
|
},
|
|
|
|
actions = n.Actions
|
|
.Select(a => new
|
|
{
|
|
actionId = a.ActionId,
|
|
actionType = a.ActionType,
|
|
blockingType = a.BlockingType,
|
|
|
|
actionParameters = a.ActionParameters != null
|
|
? a.ActionParameters
|
|
.Select(p => new
|
|
{
|
|
key = p.Key,
|
|
value = p.Value
|
|
})
|
|
.ToArray()
|
|
: Array.Empty<object>()
|
|
})
|
|
.ToArray()
|
|
})
|
|
.ToArray(), // ✅ QUAN TRỌNG
|
|
|
|
// ================= EDGES =================
|
|
edges = Edges
|
|
.Select(e => new
|
|
{
|
|
edgeId = e.EdgeId,
|
|
sequenceId = seq++,
|
|
released = true,
|
|
|
|
startNodeId = e.StartNodeId,
|
|
endNodeId = e.EndNodeId,
|
|
|
|
actions = Array.Empty<object>()
|
|
})
|
|
.ToArray() // ✅ QUAN TRỌNG
|
|
};
|
|
}
|
|
|
|
}
|
|
|
|
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();
|
|
}
|
|
} |