feat: 集成机械手客户端并重构模拟器前端工作台
本次提交按要求包含当前仓库所有已修改/新增文件,覆盖 S7Simulator、WIDESEAWCS_Server、WMS 三个工程域。
一、S7Simulator 后端能力扩展
1. 新增 RobotClientsController,补齐机械手客户端管理 API(实例生命周期、消息交互、状态查询)。
2. 新增 RobotClientManager,集中处理客户端连接、端口管理、收发消息与运行状态维护。
3. Program 启动注册与依赖注入同步调整,接入机器人客户端管理服务。
二、S7Simulator 前端能力扩展与页面重构
1. 新增 RobotClientsView 页面,并接入 App/router/api/types,形成前后端闭环。
2. 实例管理页与详情页改造为统一后台骨架(标题区/信息区/操作区),统一交互节奏。
3. 实例详情重排为左侧信息 + 右侧实时数据布局,优化密度与阅读顺序。
4. 协议模板、创建、编辑等页面同步调整,保持 UI 与数据结构一致。
三、字段解释与实时数据展示优化
1. 修复字段解释表在条件分支下的渲染结构问题(v-if/v-else-if 相邻关系)。
2. 字段表改为按内容自适应列宽(table-layout=auto),降低长字段被截断概率。
3. 操作列固定右侧,横向滚动时保持可见,保证写入动作可达。
4. 输入控件与表格滚动样式联动优化,缓解修改值列显示不全问题。
四、相关服务工程联动更新
1. WIDESEAWCS_Server:任务 DTO、模型、任务服务、编排流程及配置文件同步调整。
2. WIDESEA_WMSServer:库存服务与工程布局文件更新。
3. 本次提交包含工作区内生成/修改的辅助文件(含 .vs 布局与临时 Excel 文件)。
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | using Microsoft.AspNetCore.Mvc; |
| | | using WIDESEAWCS_S7Simulator.Server.Services; |
| | | |
| | | namespace WIDESEAWCS_S7Simulator.Server.Controllers; |
| | | |
| | | /// <summary> |
| | | /// æºæ¢°æå®¢æ·ç«¯ç®¡çæ¥å£ï¼ä¸»å¨è¿æ¥æ¨¡å¼ï¼ã |
| | | /// </summary> |
| | | [ApiController] |
| | | [Route("api/[controller]")] |
| | | public class RobotClientsController : ControllerBase |
| | | { |
| | | private readonly IRobotClientManager _robotClientManager; |
| | | private readonly ILogger<RobotClientsController> _logger; |
| | | |
| | | public RobotClientsController(IRobotClientManager robotClientManager, ILogger<RobotClientsController> logger) |
| | | { |
| | | _robotClientManager = robotClientManager; |
| | | _logger = logger; |
| | | } |
| | | |
| | | [HttpGet("status")] |
| | | [ProducesResponseType(typeof(RobotServerCollectionStatusResponse), StatusCodes.Status200OK)] |
| | | public async Task<ActionResult<RobotServerCollectionStatusResponse>> GetStatus() |
| | | { |
| | | var status = await _robotClientManager.GetStatusAsync(); |
| | | return Ok(status); |
| | | } |
| | | |
| | | [HttpPost("start")] |
| | | [ProducesResponseType(typeof(RobotServerCollectionStatusResponse), StatusCodes.Status200OK)] |
| | | [ProducesResponseType(StatusCodes.Status400BadRequest)] |
| | | public async Task<ActionResult<RobotServerCollectionStatusResponse>> Start([FromBody] RobotServerStartRequest request) |
| | | { |
| | | try |
| | | { |
| | | var status = await _robotClientManager.StartAsync(request, HttpContext.RequestAborted); |
| | | return Ok(status); |
| | | } |
| | | catch (ArgumentException ex) |
| | | { |
| | | return BadRequest(new { error = ex.Message }); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "å¯å¨æºæ¢°æå®¢æ·ç«¯å®ä¾å¤±è´¥"); |
| | | return StatusCode(StatusCodes.Status500InternalServerError, new { error = "å¯å¨æºæ¢°æå®¢æ·ç«¯å®ä¾å¤±è´¥" }); |
| | | } |
| | | } |
| | | |
| | | [HttpPost("stop")] |
| | | [ProducesResponseType(StatusCodes.Status200OK)] |
| | | public async Task<ActionResult> Stop([FromQuery] string? serverId = null) |
| | | { |
| | | await _robotClientManager.StopAsync(serverId); |
| | | return Ok(new { message = string.IsNullOrWhiteSpace(serverId) ? "æºæ¢°æå®¢æ·ç«¯å·²å
¨é¨åæ¢" : $"æºæ¢°æå®¢æ·ç«¯ {serverId} 已忢" }); |
| | | } |
| | | |
| | | [HttpPost("send")] |
| | | [ProducesResponseType(StatusCodes.Status200OK)] |
| | | [ProducesResponseType(StatusCodes.Status400BadRequest)] |
| | | public async Task<ActionResult> Send([FromBody] RobotServerSendRequest request) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(request.Message)) |
| | | { |
| | | return BadRequest(new { error = "åéå
容ä¸è½ä¸ºç©º" }); |
| | | } |
| | | |
| | | try |
| | | { |
| | | if (string.IsNullOrWhiteSpace(request.ServerId)) |
| | | { |
| | | return BadRequest(new { error = "ServerId ä¸è½ä¸ºç©º" }); |
| | | } |
| | | |
| | | if (request.ClientId.HasValue) |
| | | { |
| | | await _robotClientManager.SendToClientAsync(request.ServerId, request.ClientId.Value, request.Message); |
| | | } |
| | | else |
| | | { |
| | | await _robotClientManager.SendToAllAsync(request.ServerId, request.Message); |
| | | } |
| | | |
| | | return Ok(new { message = "åéæå" }); |
| | | } |
| | | catch (InvalidOperationException ex) |
| | | { |
| | | return BadRequest(new { error = ex.Message }); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "åéæºæ¢°æå®¢æ·ç«¯æ¶æ¯å¤±è´¥"); |
| | | return StatusCode(StatusCodes.Status500InternalServerError, new { error = "åéæºæ¢°æå®¢æ·ç«¯æ¶æ¯å¤±è´¥" }); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// æ¸
空æå®å®¢æ·ç«¯å®ä¾çæ¶æ¯æ¥å¿ã |
| | | /// </summary> |
| | | [HttpPost("clear-received")] |
| | | [ProducesResponseType(StatusCodes.Status200OK)] |
| | | [ProducesResponseType(StatusCodes.Status400BadRequest)] |
| | | public async Task<ActionResult> ClearReceived([FromQuery] string serverId) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(serverId)) |
| | | { |
| | | return BadRequest(new { error = "ServerId ä¸è½ä¸ºç©º" }); |
| | | } |
| | | |
| | | try |
| | | { |
| | | await _robotClientManager.ClearReceivedMessagesAsync(serverId); |
| | | return Ok(new { message = $"å®ä¾ {serverId} çæ¥æ¶æ¶æ¯å·²æ¸
空" }); |
| | | } |
| | | catch (InvalidOperationException ex) |
| | | { |
| | | return BadRequest(new { error = ex.Message }); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "æ¸
ç©ºæ¥æ¶æ¶æ¯å¤±è´¥"); |
| | | return StatusCode(StatusCodes.Status500InternalServerError, new { error = "æ¸
ç©ºæ¥æ¶æ¶æ¯å¤±è´¥" }); |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | builder.Services.AddSingleton<IPersistenceService>(sp => new FilePersistenceService(dataPath)); |
| | | builder.Services.AddSingleton<IProtocolTemplateService>(sp => new FileProtocolTemplateService(dataPath)); |
| | | builder.Services.AddSingleton<IRobotClientManager, RobotClientManager>(); |
| | | builder.Services.Configure<ProtocolMonitoringOptions>(builder.Configuration.GetSection("ProtocolMonitoring")); |
| | | builder.Services.AddSingleton<MirrorAckProtocolHandler>(); |
| | | builder.Services.AddSingleton<IDeviceProtocolHandler, WcsLineProtocolHandler>(); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | using System.Collections.Concurrent; |
| | | using System.Net; |
| | | using System.Net.Sockets; |
| | | using System.Text; |
| | | |
| | | namespace WIDESEAWCS_S7Simulator.Server.Services; |
| | | |
| | | public interface IRobotClientManager |
| | | { |
| | | Task<RobotServerCollectionStatusResponse> StartAsync(RobotServerStartRequest request, CancellationToken cancellationToken = default); |
| | | Task StopAsync(string? serverId = null); |
| | | Task<RobotServerCollectionStatusResponse> GetStatusAsync(); |
| | | Task SendToClientAsync(string serverId, int clientId, string message); |
| | | Task SendToAllAsync(string serverId, string message); |
| | | Task ClearReceivedMessagesAsync(string serverId); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// æºæ¢°æ TCP 客æ·ç«¯å¤å®ä¾ç®¡çå¨ã |
| | | /// ä¸ä¸ª ServerId 对åºä¸ä¸ª TcpClientï¼ä¸»å¨è¿æ¥ç®æ æå¡ç«¯ã |
| | | /// </summary> |
| | | public sealed class RobotClientManager : IRobotClientManager, IDisposable |
| | | { |
| | | private readonly ILogger<RobotClientManager> _logger; |
| | | private readonly ConcurrentDictionary<string, RobotClientRuntime> _clients = new(StringComparer.OrdinalIgnoreCase); |
| | | private bool _disposed; |
| | | |
| | | public RobotClientManager(ILogger<RobotClientManager> logger) |
| | | { |
| | | _logger = logger; |
| | | } |
| | | |
| | | public async Task<RobotServerCollectionStatusResponse> StartAsync(RobotServerStartRequest request, CancellationToken cancellationToken = default) |
| | | { |
| | | ValidateStartRequest(request); |
| | | EnsureLocalPortAvailable(request.LocalPort); |
| | | |
| | | var key = request.ServerId.Trim(); |
| | | if (_clients.ContainsKey(key)) |
| | | { |
| | | throw new InvalidOperationException($"客æ·ç«¯å®ä¾ '{key}' å·²åå¨"); |
| | | } |
| | | |
| | | var runtime = new RobotClientRuntime |
| | | { |
| | | ServerId = key, |
| | | RemoteIp = request.ListenIp, |
| | | RemotePort = request.ListenPort, |
| | | LocalPort = request.LocalPort, |
| | | Cancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken), |
| | | Connected = false |
| | | }; |
| | | |
| | | runtime.ConnectionLoopTask = Task.Run(() => ConnectionLoopAsync(runtime), runtime.Cancellation.Token); |
| | | |
| | | if (!_clients.TryAdd(key, runtime)) |
| | | { |
| | | await StopRuntimeAsync(runtime); |
| | | throw new InvalidOperationException($"客æ·ç«¯å®ä¾ '{key}' å建失败"); |
| | | } |
| | | |
| | | _logger.LogInformation("æºæ¢°æå®¢æ·ç«¯å·²å¯å¨: {ServerId} æ¬å°:{LocalPort} -> è¿ç«¯:{Ip}:{Port}", |
| | | key, |
| | | request.LocalPort, |
| | | request.ListenIp, |
| | | request.ListenPort); |
| | | |
| | | return await GetStatusAsync(); |
| | | } |
| | | |
| | | public async Task StopAsync(string? serverId = null) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(serverId)) |
| | | { |
| | | var all = _clients.Values.ToArray(); |
| | | _clients.Clear(); |
| | | await Task.WhenAll(all.Select(StopRuntimeAsync)); |
| | | return; |
| | | } |
| | | |
| | | if (_clients.TryRemove(serverId.Trim(), out var runtime)) |
| | | { |
| | | await StopRuntimeAsync(runtime); |
| | | } |
| | | } |
| | | |
| | | public Task<RobotServerCollectionStatusResponse> GetStatusAsync() |
| | | { |
| | | var servers = _clients.Values |
| | | .OrderBy(x => x.ServerId, StringComparer.OrdinalIgnoreCase) |
| | | .Select(ToStatusItem) |
| | | .ToArray(); |
| | | |
| | | return Task.FromResult(new RobotServerCollectionStatusResponse |
| | | { |
| | | RunningServerCount = servers.Count(x => x.Running), |
| | | Servers = servers |
| | | }); |
| | | } |
| | | |
| | | public async Task SendToClientAsync(string serverId, int clientId, string message) |
| | | { |
| | | var runtime = GetClientOrThrow(serverId); |
| | | if (clientId != 1) |
| | | { |
| | | throw new InvalidOperationException($"客æ·ç«¯å®ä¾ '{runtime.ServerId}' ä»
æ¯æ ClientId=1"); |
| | | } |
| | | |
| | | await SendFrameAsync(runtime, message, CancellationToken.None); |
| | | } |
| | | |
| | | public async Task SendToAllAsync(string serverId, string message) |
| | | { |
| | | var runtime = GetClientOrThrow(serverId); |
| | | await SendFrameAsync(runtime, message, CancellationToken.None); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// æ¸
空æå®å®¢æ·ç«¯å®ä¾çæ¶åæ¶æ¯æ¥å¿ã |
| | | /// </summary> |
| | | public Task ClearReceivedMessagesAsync(string serverId) |
| | | { |
| | | var runtime = GetClientOrThrow(serverId); |
| | | |
| | | while (runtime.ReceivedMessages.TryDequeue(out _)) |
| | | { |
| | | } |
| | | |
| | | while (runtime.SentMessages.TryDequeue(out _)) |
| | | { |
| | | } |
| | | |
| | | return Task.CompletedTask; |
| | | } |
| | | |
| | | private RobotClientRuntime GetClientOrThrow(string serverId) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(serverId)) |
| | | { |
| | | throw new ArgumentException("ServerId ä¸è½ä¸ºç©º"); |
| | | } |
| | | |
| | | if (!_clients.TryGetValue(serverId.Trim(), out var runtime)) |
| | | { |
| | | throw new InvalidOperationException($"客æ·ç«¯å®ä¾ '{serverId}' ä¸åå¨"); |
| | | } |
| | | |
| | | return runtime; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 客æ·ç«¯è¿æ¥å®æ¤å¾ªç¯ï¼æå¼åèªå¨éè¿ã |
| | | /// </summary> |
| | | private async Task ConnectionLoopAsync(RobotClientRuntime runtime) |
| | | { |
| | | var token = runtime.Cancellation.Token; |
| | | |
| | | while (!token.IsCancellationRequested) |
| | | { |
| | | if (!runtime.Connected) |
| | | { |
| | | try |
| | | { |
| | | await ConnectOnceAsync(runtime, token); |
| | | } |
| | | catch (OperationCanceledException) |
| | | { |
| | | break; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | runtime.LastError = ex.Message; |
| | | _logger.LogWarning(ex, "[{ServerId}] è¿æ¥å¤±è´¥ï¼å°å¨ 2 ç§åéè¯", runtime.ServerId); |
| | | await DelayForReconnect(token); |
| | | continue; |
| | | } |
| | | } |
| | | |
| | | var tcpClient = runtime.TcpClient; |
| | | if (tcpClient == null) |
| | | { |
| | | await DelayForReconnect(token); |
| | | continue; |
| | | } |
| | | |
| | | await ReceiveLoopAsync(runtime, tcpClient); |
| | | await DelayForReconnect(token); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 忬¡è¿æ¥å¨ä½ãæååæ´æ°è¿è¡æ¶è¿æ¥ç¶æã |
| | | /// </summary> |
| | | private async Task ConnectOnceAsync(RobotClientRuntime runtime, CancellationToken token) |
| | | { |
| | | var tcpClient = new TcpClient(AddressFamily.InterNetwork); |
| | | try |
| | | { |
| | | // æ¯æ¬¡éè¿é½åºå®ç»å®å䏿¬å°ç«¯å£ã |
| | | tcpClient.Client.Bind(new IPEndPoint(IPAddress.Any, runtime.LocalPort)); |
| | | await tcpClient.ConnectAsync(runtime.RemoteIp, runtime.RemotePort, token); |
| | | } |
| | | catch |
| | | { |
| | | tcpClient.Dispose(); |
| | | throw; |
| | | } |
| | | |
| | | runtime.TcpClient = tcpClient; |
| | | runtime.Connected = true; |
| | | runtime.ConnectedAt = DateTimeOffset.Now; |
| | | runtime.LastError = null; |
| | | |
| | | _logger.LogInformation("[{ServerId}] å·²è¿æ¥ æ¬å°:{LocalPort} -> è¿ç«¯:{Ip}:{Port}", |
| | | runtime.ServerId, |
| | | runtime.LocalPort, |
| | | runtime.RemoteIp, |
| | | runtime.RemotePort); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// å个客æ·ç«¯æ¶å
循ç¯ï¼æ¯æå¸§åè®®åæ®éææ¬ä¸¤ç§è¾å
¥ã |
| | | /// </summary> |
| | | private async Task ReceiveLoopAsync(RobotClientRuntime runtime, TcpClient tcpClient) |
| | | { |
| | | var token = runtime.Cancellation.Token; |
| | | var buffer = new byte[2048]; |
| | | var cache = new StringBuilder(); |
| | | |
| | | try |
| | | { |
| | | var stream = tcpClient.GetStream(); |
| | | while (!token.IsCancellationRequested) |
| | | { |
| | | var length = await stream.ReadAsync(buffer, 0, buffer.Length, token); |
| | | if (length <= 0) |
| | | { |
| | | break; |
| | | } |
| | | |
| | | runtime.LastReceivedAt = DateTimeOffset.Now; |
| | | cache.Append(Encoding.UTF8.GetString(buffer, 0, length)); |
| | | TryReadMessages(runtime, cache); |
| | | } |
| | | } |
| | | catch (OperationCanceledException) |
| | | { |
| | | // æ£å¸¸åæ¢ã |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | runtime.LastError = ex.Message; |
| | | _logger.LogWarning(ex, "[{ServerId}] 客æ·ç«¯æ¶å
å¼å¸¸", runtime.ServerId); |
| | | } |
| | | finally |
| | | { |
| | | runtime.Connected = false; |
| | | await CloseRuntimeSocketAsync(runtime); |
| | | _logger.LogWarning("[{ServerId}] 客æ·ç«¯è¿æ¥å·²æå¼ï¼åå¤èªå¨éè¿", runtime.ServerId); |
| | | } |
| | | } |
| | | |
| | | private void TryReadMessages(RobotClientRuntime runtime, StringBuilder cache) |
| | | { |
| | | const string start = "<START>"; |
| | | const string end = "<END>"; |
| | | |
| | | while (true) |
| | | { |
| | | var text = cache.ToString(); |
| | | var startIndex = text.IndexOf(start, StringComparison.Ordinal); |
| | | var endIndex = text.IndexOf(end, StringComparison.Ordinal); |
| | | |
| | | // æªå½ä¸å¸§åè®®æ¶ï¼ææ´æ®µææ¬è®°å½ï¼é¿å
æ¶æ¯ä¸¢å¤±ã |
| | | if (startIndex < 0 || endIndex < 0 || endIndex <= startIndex) |
| | | { |
| | | if (cache.Length > 0 && cache.Length < 2048 && !text.Contains("<START>", StringComparison.Ordinal)) |
| | | { |
| | | AppendReceived(runtime, text.Trim()); |
| | | cache.Clear(); |
| | | } |
| | | |
| | | if (cache.Length > 10240) |
| | | { |
| | | cache.Clear(); |
| | | } |
| | | |
| | | return; |
| | | } |
| | | |
| | | var contentStart = startIndex + start.Length; |
| | | var contentLength = endIndex - contentStart; |
| | | var content = text.Substring(contentStart, contentLength); |
| | | cache.Remove(0, endIndex + end.Length); |
| | | AppendReceived(runtime, content); |
| | | } |
| | | } |
| | | |
| | | private void AppendReceived(RobotClientRuntime runtime, string message) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(message)) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | runtime.LastReceivedMessage = message; |
| | | runtime.ReceivedMessages.Enqueue(new RobotServerReceivedMessageItem |
| | | { |
| | | ReceivedAt = DateTimeOffset.Now, |
| | | ClientId = 1, |
| | | RemoteEndPoint = $"{runtime.RemoteIp}:{runtime.RemotePort}", |
| | | Message = message |
| | | }); |
| | | |
| | | while (runtime.ReceivedMessages.Count > 500) |
| | | { |
| | | runtime.ReceivedMessages.TryDequeue(out _); |
| | | } |
| | | |
| | | _logger.LogInformation("[{ServerId}] æ¶å°: {Message}", runtime.ServerId, message); |
| | | } |
| | | |
| | | private async Task SendFrameAsync(RobotClientRuntime runtime, string message, CancellationToken token) |
| | | { |
| | | if (!runtime.Connected) |
| | | { |
| | | throw new InvalidOperationException($"客æ·ç«¯å®ä¾ '{runtime.ServerId}' æªè¿æ¥"); |
| | | } |
| | | |
| | | var frame = $"<START>{message}<END>"; |
| | | var bytes = Encoding.UTF8.GetBytes(frame); |
| | | |
| | | await runtime.SendLock.WaitAsync(token); |
| | | try |
| | | { |
| | | if (runtime.TcpClient == null) |
| | | { |
| | | throw new InvalidOperationException($"客æ·ç«¯å®ä¾ '{runtime.ServerId}' æªè¿æ¥"); |
| | | } |
| | | |
| | | var stream = runtime.TcpClient.GetStream(); |
| | | await stream.WriteAsync(bytes, 0, bytes.Length, token); |
| | | await stream.FlushAsync(token); |
| | | runtime.LastSentAt = DateTimeOffset.Now; |
| | | |
| | | runtime.SentMessages.Enqueue(new RobotServerSentMessageItem |
| | | { |
| | | SentAt = DateTimeOffset.Now, |
| | | ClientId = 1, |
| | | RemoteEndPoint = $"{runtime.RemoteIp}:{runtime.RemotePort}", |
| | | Message = message |
| | | }); |
| | | |
| | | while (runtime.SentMessages.Count > 500) |
| | | { |
| | | runtime.SentMessages.TryDequeue(out _); |
| | | } |
| | | |
| | | _logger.LogInformation("[{ServerId}] åé: {Frame}", runtime.ServerId, frame); |
| | | } |
| | | finally |
| | | { |
| | | runtime.SendLock.Release(); |
| | | } |
| | | } |
| | | |
| | | private static async Task CloseRuntimeSocketAsync(RobotClientRuntime runtime) |
| | | { |
| | | await runtime.SendLock.WaitAsync(); |
| | | try |
| | | { |
| | | if (runtime.TcpClient != null) |
| | | { |
| | | try { runtime.TcpClient.Close(); } catch { } |
| | | runtime.TcpClient.Dispose(); |
| | | runtime.TcpClient = null; |
| | | } |
| | | runtime.Connected = false; |
| | | } |
| | | finally |
| | | { |
| | | runtime.SendLock.Release(); |
| | | } |
| | | } |
| | | |
| | | private async Task StopRuntimeAsync(RobotClientRuntime runtime) |
| | | { |
| | | try { runtime.Cancellation.Cancel(); } catch { } |
| | | |
| | | if (runtime.ConnectionLoopTask != null) |
| | | { |
| | | try { await runtime.ConnectionLoopTask; } catch { } |
| | | } |
| | | |
| | | await CloseRuntimeSocketAsync(runtime); |
| | | runtime.Cancellation.Dispose(); |
| | | _logger.LogInformation("æºæ¢°æå®¢æ·ç«¯å·²åæ¢: {ServerId}", runtime.ServerId); |
| | | } |
| | | |
| | | private static RobotServerStatusItem ToStatusItem(RobotClientRuntime runtime) |
| | | { |
| | | var clients = new[] |
| | | { |
| | | new RobotServerClientStatusItem |
| | | { |
| | | ClientId = 1, |
| | | RemoteEndPoint = $"{runtime.RemoteIp}:{runtime.RemotePort}", |
| | | Connected = runtime.Connected, |
| | | ConnectedAt = runtime.ConnectedAt, |
| | | LastReceivedAt = runtime.LastReceivedAt, |
| | | LastSentAt = runtime.LastSentAt, |
| | | LastReceivedMessage = runtime.LastReceivedMessage, |
| | | LastError = runtime.LastError |
| | | } |
| | | }; |
| | | |
| | | var receivedMessages = runtime.ReceivedMessages |
| | | .Reverse() |
| | | .Take(200) |
| | | .ToArray(); |
| | | |
| | | var sentMessages = runtime.SentMessages |
| | | .Reverse() |
| | | .Take(200) |
| | | .ToArray(); |
| | | |
| | | return new RobotServerStatusItem |
| | | { |
| | | ServerId = runtime.ServerId, |
| | | Running = !runtime.Cancellation.IsCancellationRequested, |
| | | ListenIp = runtime.RemoteIp, |
| | | ListenPort = runtime.RemotePort, |
| | | LocalPort = runtime.LocalPort, |
| | | ConnectedCount = runtime.Connected ? 1 : 0, |
| | | Clients = clients, |
| | | ReceivedMessages = receivedMessages, |
| | | SentMessages = sentMessages |
| | | }; |
| | | } |
| | | |
| | | private static void ValidateStartRequest(RobotServerStartRequest request) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(request.ServerId)) |
| | | { |
| | | throw new ArgumentException("ServerId ä¸è½ä¸ºç©º"); |
| | | } |
| | | |
| | | if (string.IsNullOrWhiteSpace(request.ListenIp)) |
| | | { |
| | | throw new ArgumentException("ç®æ æå¡ç«¯å°åä¸è½ä¸ºç©º"); |
| | | } |
| | | |
| | | if (request.ListenPort <= 0 || request.ListenPort > 65535) |
| | | { |
| | | throw new ArgumentException("ç®æ æå¡ç«¯ç«¯å£å¿
é¡»å¨ 1-65535 èå´å
"); |
| | | } |
| | | |
| | | if (request.LocalPort <= 0 || request.LocalPort > 65535) |
| | | { |
| | | throw new ArgumentException("客æ·ç«¯æ¬å°ç«¯å£å¿
é¡»å¨ 1-65535 èå´å
"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// å¯å¨åæ£æ¥æ¬å°ç«¯å£æ¯å¦å¯ç¨ï¼é¿å
å®ä¾è¿å
¥æ æéè¿ç¶æã |
| | | /// </summary> |
| | | private static void EnsureLocalPortAvailable(int localPort) |
| | | { |
| | | Socket? probe = null; |
| | | try |
| | | { |
| | | probe = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); |
| | | probe.Bind(new IPEndPoint(IPAddress.Any, localPort)); |
| | | } |
| | | catch (SocketException ex) |
| | | { |
| | | throw new InvalidOperationException($"æ¬å°ç«¯å£ {localPort} 已被å ç¨æä¸å¯ç¨", ex); |
| | | } |
| | | finally |
| | | { |
| | | try { probe?.Dispose(); } catch { } |
| | | } |
| | | } |
| | | |
| | | private static async Task DelayForReconnect(CancellationToken token) |
| | | { |
| | | try |
| | | { |
| | | await Task.Delay(TimeSpan.FromSeconds(2), token); |
| | | } |
| | | catch (OperationCanceledException) |
| | | { |
| | | } |
| | | } |
| | | |
| | | public void Dispose() |
| | | { |
| | | if (_disposed) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | _disposed = true; |
| | | StopAsync().GetAwaiter().GetResult(); |
| | | } |
| | | |
| | | private sealed class RobotClientRuntime |
| | | { |
| | | public string ServerId { get; set; } = string.Empty; |
| | | public string RemoteIp { get; set; } = string.Empty; |
| | | public int RemotePort { get; set; } |
| | | public int LocalPort { get; set; } |
| | | public TcpClient? TcpClient { get; set; } |
| | | public CancellationTokenSource Cancellation { get; set; } = default!; |
| | | public Task? ConnectionLoopTask { get; set; } |
| | | public SemaphoreSlim SendLock { get; } = new(1, 1); |
| | | public bool Connected { get; set; } |
| | | public DateTimeOffset? ConnectedAt { get; set; } |
| | | public DateTimeOffset? LastReceivedAt { get; set; } |
| | | public DateTimeOffset? LastSentAt { get; set; } |
| | | public string? LastReceivedMessage { get; set; } |
| | | public string? LastError { get; set; } |
| | | public ConcurrentQueue<RobotServerReceivedMessageItem> ReceivedMessages { get; } = new(); |
| | | public ConcurrentQueue<RobotServerSentMessageItem> SentMessages { get; } = new(); |
| | | } |
| | | } |
| | | |
| | | public sealed class RobotServerStartRequest |
| | | { |
| | | public string ServerId { get; set; } = "default"; |
| | | public string ListenIp { get; set; } = "127.0.0.1"; |
| | | public int ListenPort { get; set; } = 2000; |
| | | public int LocalPort { get; set; } = 2001; |
| | | } |
| | | |
| | | public sealed class RobotServerSendRequest |
| | | { |
| | | public string ServerId { get; set; } = string.Empty; |
| | | public int? ClientId { get; set; } |
| | | public string Message { get; set; } = string.Empty; |
| | | } |
| | | |
| | | public sealed class RobotServerCollectionStatusResponse |
| | | { |
| | | public int RunningServerCount { get; set; } |
| | | public IReadOnlyList<RobotServerStatusItem> Servers { get; set; } = Array.Empty<RobotServerStatusItem>(); |
| | | } |
| | | |
| | | public sealed class RobotServerStatusItem |
| | | { |
| | | public string ServerId { get; set; } = string.Empty; |
| | | public bool Running { get; set; } |
| | | public string ListenIp { get; set; } = string.Empty; |
| | | public int ListenPort { get; set; } |
| | | public int LocalPort { get; set; } |
| | | public int ConnectedCount { get; set; } |
| | | public IReadOnlyList<RobotServerClientStatusItem> Clients { get; set; } = Array.Empty<RobotServerClientStatusItem>(); |
| | | public IReadOnlyList<RobotServerReceivedMessageItem> ReceivedMessages { get; set; } = Array.Empty<RobotServerReceivedMessageItem>(); |
| | | public IReadOnlyList<RobotServerSentMessageItem> SentMessages { get; set; } = Array.Empty<RobotServerSentMessageItem>(); |
| | | } |
| | | |
| | | public sealed class RobotServerClientStatusItem |
| | | { |
| | | public int ClientId { get; set; } |
| | | public string? RemoteEndPoint { get; set; } |
| | | public bool Connected { get; set; } |
| | | public DateTimeOffset? ConnectedAt { get; set; } |
| | | public DateTimeOffset? LastReceivedAt { get; set; } |
| | | public DateTimeOffset? LastSentAt { get; set; } |
| | | public string? LastReceivedMessage { get; set; } |
| | | public string? LastError { get; set; } |
| | | } |
| | | |
| | | public sealed class RobotServerReceivedMessageItem |
| | | { |
| | | public DateTimeOffset ReceivedAt { get; set; } |
| | | public int ClientId { get; set; } |
| | | public string? RemoteEndPoint { get; set; } |
| | | public string Message { get; set; } = string.Empty; |
| | | } |
| | | |
| | | public sealed class RobotServerSentMessageItem |
| | | { |
| | | public DateTimeOffset SentAt { get; set; } |
| | | public int ClientId { get; set; } |
| | | public string? RemoteEndPoint { get; set; } |
| | | public string Message { get; set; } = string.Empty; |
| | | } |
| | |
| | | <template> |
| | | <el-container class="app-container"> |
| | | <el-header class="app-header"> |
| | | <div class="header-content"> |
| | | <router-link to="/" class="logo"> |
| | | <el-icon :size="24"><Cpu /></el-icon> |
| | | <span>S7 PLC Simulator</span> |
| | | </router-link> |
| | | <div class="header-nav"> |
| | | <router-link to="/protocol-templates" class="nav-link">å议模æ¿</router-link> |
| | | <template> |
| | | <el-container class="admin-layout"> |
| | | <el-aside class="admin-aside" width="220px"> |
| | | <div class="brand-area"> |
| | | <el-icon :size="24" class="brand-icon"><Cpu /></el-icon> |
| | | <div class="brand-text"> |
| | | <div class="brand-title">WCS 模æå¹³å°</div> |
| | | <div class="brand-subtitle">S7 Simulator</div> |
| | | </div> |
| | | </div> |
| | | </el-header> |
| | | |
| | | <el-main class="app-main"> |
| | | <router-view /> |
| | | </el-main> |
| | | <el-menu |
| | | class="aside-menu" |
| | | :default-active="activeMenu" |
| | | background-color="#1f2a37" |
| | | text-color="#c7d2fe" |
| | | active-text-color="#ffffff" |
| | | router |
| | | > |
| | | <el-menu-item index="/"> |
| | | <el-icon><House /></el-icon> |
| | | <span>å®ä¾ç®¡ç</span> |
| | | </el-menu-item> |
| | | <el-menu-item index="/protocol-templates"> |
| | | <el-icon><Files /></el-icon> |
| | | <span>å议模æ¿</span> |
| | | </el-menu-item> |
| | | <el-menu-item index="/robot-clients"> |
| | | <el-icon><Connection /></el-icon> |
| | | <span>æºæ¢°æå®¢æ·ç«¯</span> |
| | | </el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | |
| | | <el-footer class="app-footer"> |
| | | <span>© 2026 - S7 PLC Simulator Management UI</span> |
| | | </el-footer> |
| | | <el-container> |
| | | <el-header class="admin-header"> |
| | | <div class="header-left"> |
| | | <div class="page-title">{{ pageTitle }}</div> |
| | | <div class="page-path">{{ route.path }}</div> |
| | | </div> |
| | | </el-header> |
| | | |
| | | <el-main class="admin-main"> |
| | | <div class="content-shell"> |
| | | <router-view /> |
| | | </div> |
| | | </el-main> |
| | | </el-container> |
| | | </el-container> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { Cpu } from '@element-plus/icons-vue' |
| | | import { computed } from 'vue' |
| | | import { useRoute } from 'vue-router' |
| | | import { Connection, Cpu, Files, House } from '@element-plus/icons-vue' |
| | | |
| | | const route = useRoute() |
| | | |
| | | const activeMenu = computed(() => { |
| | | if (route.path.startsWith('/protocol-templates')) return '/protocol-templates' |
| | | if (route.path.startsWith('/robot-clients')) return '/robot-clients' |
| | | return '/' |
| | | }) |
| | | |
| | | const pageTitle = computed(() => { |
| | | if (route.path.startsWith('/create')) return 'å建å®ä¾' |
| | | if (route.path.startsWith('/edit')) return 'ç¼è¾å®ä¾' |
| | | if (route.path.startsWith('/details')) return 'å®ä¾è¯¦æ
' |
| | | if (route.path.startsWith('/protocol-templates')) return 'å议模æ¿' |
| | | if (route.path.startsWith('/robot-clients')) return 'æºæ¢°æå®¢æ·ç«¯' |
| | | return 'å®ä¾ç®¡ç' |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .app-container { |
| | | min-height: 100vh; |
| | | .admin-layout { |
| | | height: 100vh; |
| | | } |
| | | |
| | | .app-header { |
| | | background: #409eff; |
| | | color: white; |
| | | padding: 0 20px; |
| | | .admin-aside { |
| | | background: linear-gradient(180deg, #1f2a37 0%, #111827 100%); |
| | | border-right: 1px solid #263445; |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .brand-area { |
| | | height: 64px; |
| | | padding: 0 14px; |
| | | display: flex; |
| | | align-items: center; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
| | | border-bottom: 1px solid #2b3a4f; |
| | | color: #e5e7eb; |
| | | } |
| | | |
| | | .header-content { |
| | | width: 100%; |
| | | max-width: 1400px; |
| | | margin: 0 auto; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | .brand-icon { |
| | | margin-right: 10px; |
| | | } |
| | | |
| | | .logo { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | color: white; |
| | | text-decoration: none; |
| | | font-size: 18px; |
| | | font-weight: 500; |
| | | .brand-title { |
| | | font-size: 15px; |
| | | font-weight: 700; |
| | | line-height: 1.2; |
| | | } |
| | | |
| | | .logo:hover { |
| | | color: white; |
| | | .brand-subtitle { |
| | | font-size: 12px; |
| | | color: #94a3b8; |
| | | line-height: 1.2; |
| | | } |
| | | |
| | | .header-nav { |
| | | display: flex; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .nav-link { |
| | | color: #fff; |
| | | text-decoration: none; |
| | | } |
| | | |
| | | .app-main { |
| | | .aside-menu { |
| | | border-right: none; |
| | | flex: 1; |
| | | padding: 20px; |
| | | max-width: 1400px; |
| | | } |
| | | |
| | | .admin-header { |
| | | height: 64px; |
| | | background: #ffffff; |
| | | border-bottom: 1px solid #e5e7eb; |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 0 20px; |
| | | } |
| | | |
| | | .header-left { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 2px; |
| | | } |
| | | |
| | | .page-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #0f172a; |
| | | } |
| | | |
| | | .page-path { |
| | | font-size: 12px; |
| | | color: #64748b; |
| | | } |
| | | |
| | | .admin-main { |
| | | background: #f1f5f9; |
| | | padding: 14px; |
| | | } |
| | | |
| | | .content-shell { |
| | | min-height: calc(100vh - 64px - 28px); |
| | | width: 100%; |
| | | max-width: 1680px; |
| | | margin: 0 auto; |
| | | padding: 2px; |
| | | } |
| | | |
| | | .app-footer { |
| | | text-align: center; |
| | | color: #909399; |
| | | border-top: 1px solid #dcdfe6; |
| | | padding: 20px; |
| | | } |
| | | @media (max-width: 960px) { |
| | | .admin-aside { |
| | | width: 64px !important; |
| | | } |
| | | |
| | | a.router-link-active { |
| | | font-weight: bold; |
| | | .brand-text { |
| | | display: none; |
| | | } |
| | | |
| | | .admin-header { |
| | | padding: 0 12px; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | import axios from 'axios' |
| | | import axios from 'axios' |
| | | import type { |
| | | InstanceListItem, |
| | | InstanceState, |
| | | InstanceConfig, |
| | | ProtocolTemplate |
| | | ProtocolTemplate, |
| | | RobotClientStartRequest, |
| | | RobotClientSendRequest, |
| | | RobotClientStatusResponse |
| | | } from '../types' |
| | | |
| | | const api = axios.create({ |
| | |
| | | return response.data |
| | | } |
| | | |
| | | // è·åæå®å®ä¾ç¶æ |
| | | // è·åå®ä¾ç¶æ |
| | | export async function getInstance(id: string): Promise<InstanceState | null> { |
| | | try { |
| | | const response = await api.get<InstanceState>('/SimulatorInstances/GetInstance', { |
| | |
| | | // å建å®ä¾ |
| | | export async function createInstance(config: InstanceConfig): Promise<InstanceState | null> { |
| | | try { |
| | | const response = await api.post<InstanceState>('/SimulatorInstances/Create', config ) |
| | | const response = await api.post<InstanceState>('/SimulatorInstances/Create', config) |
| | | return response.data |
| | | } catch (error) { |
| | | console.error('å建å®ä¾å¤±è´¥:', error) |
| | |
| | | } |
| | | } |
| | | |
| | | // è·åæºæ¢°ææå¡ç«¯è¿è¡ç¶æï¼å
å«å¤å®ä¾åæ¥æ¶æ¶æ¯æ¥å¿ï¼ |
| | | export async function getRobotClientStatus(): Promise<RobotClientStatusResponse> { |
| | | const response = await api.get<RobotClientStatusResponse>('/RobotClients/status') |
| | | return response.data |
| | | } |
| | | |
| | | // å¯å¨ä¸ä¸ªæºæ¢°ææå¡ç«¯å®ä¾ |
| | | export async function startRobotClients(request: RobotClientStartRequest): Promise<RobotClientStatusResponse> { |
| | | const response = await api.post<RobotClientStatusResponse>('/RobotClients/start', request) |
| | | return response.data |
| | | } |
| | | |
| | | // åæ¢æºæ¢°ææå¡ç«¯ï¼serverId 为空æ¶åæ¢å
¨é¨ |
| | | export async function stopRobotClients(serverId?: string): Promise<void> { |
| | | await api.post('/RobotClients/stop', null, { |
| | | params: { serverId } |
| | | }) |
| | | } |
| | | |
| | | // åéæºæ¢°ææ¶æ¯ï¼ææå¡ç«¯å®ä¾å¹¿ææååï¼ |
| | | export async function sendRobotClientMessage(request: RobotClientSendRequest): Promise<void> { |
| | | await api.post('/RobotClients/send', request) |
| | | } |
| | | |
| | | // æ¸
空æå®æå¡ç«¯å®ä¾çæ¥æ¶æ¶æ¯æ¥å¿ |
| | | export async function clearRobotClientReceivedMessages(serverId: string): Promise<void> { |
| | | await api.post('/RobotClients/clear-received', null, { |
| | | params: { serverId } |
| | | }) |
| | | } |
| | | |
| | | export default api |
| | |
| | | path: '/protocol-templates', |
| | | name: 'protocolTemplates', |
| | | component: () => import('../views/ProtocolTemplatesView.vue') |
| | | }, |
| | | { |
| | | path: '/robot-clients', |
| | | name: 'robotClients', |
| | | component: () => import('../views/RobotClientsView.vue') |
| | | } |
| | | ] |
| | | |
| | |
| | | /* Global Styles */ |
| | | :root { |
| | | font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; |
| | | --bg-page: #edf2f7; |
| | | --bg-panel: #ffffff; |
| | | --bg-soft: #f8fafc; |
| | | --text-main: #0f172a; |
| | | --text-sub: #64748b; |
| | | --border-main: #dbe2ea; |
| | | --brand-main: #0b5cab; |
| | | --brand-soft: #e8f2ff; |
| | | --ok: #16a34a; |
| | | --warn: #d97706; |
| | | --danger: #dc2626; |
| | | font-family: 'Noto Sans SC', 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', sans-serif; |
| | | } |
| | | |
| | | * { |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | body { |
| | | margin: 0; |
| | | min-height: 100vh; |
| | | background: #f5f7fa; |
| | | color: var(--text-main); |
| | | background: |
| | | radial-gradient(circle at 0% 0%, #f6faff 0%, transparent 35%), |
| | | radial-gradient(circle at 100% 0%, #eef6ff 0%, transparent 38%), |
| | | var(--bg-page); |
| | | } |
| | | |
| | | /* Status Colors - Element Plus */ |
| | | #app { |
| | | min-height: 100vh; |
| | | } |
| | | |
| | | .admin-page { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 14px; |
| | | } |
| | | |
| | | .page-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: flex-start; |
| | | flex-wrap: wrap; |
| | | gap: 12px; |
| | | padding: 16px 18px; |
| | | border: 1px solid var(--border-main); |
| | | border-radius: 12px; |
| | | background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%); |
| | | } |
| | | |
| | | .page-header h2 { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | margin: 0; |
| | | font-size: 20px; |
| | | } |
| | | |
| | | .text-muted { |
| | | margin: 4px 0 0 0; |
| | | color: var(--text-sub); |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .panel-card { |
| | | border-radius: 12px; |
| | | border: 1px solid var(--border-main); |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .section-block { |
| | | border: 1px solid var(--border-main); |
| | | border-radius: 12px; |
| | | background: var(--bg-panel); |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .section-head { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | gap: 10px; |
| | | padding: 12px 16px; |
| | | border-bottom: 1px solid #e6edf4; |
| | | background: linear-gradient(180deg, #ffffff 0%, #f9fbfe 100%); |
| | | } |
| | | |
| | | .section-title { |
| | | margin: 0; |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #0f172a; |
| | | } |
| | | |
| | | .section-desc { |
| | | margin: 2px 0 0; |
| | | font-size: 12px; |
| | | color: var(--text-sub); |
| | | } |
| | | |
| | | .section-body { |
| | | padding: 12px 14px; |
| | | } |
| | | |
| | | .status-stopped { |
| | | border-left: 4px solid #909399; |
| | | border-left: 4px solid #94a3b8; |
| | | } |
| | | |
| | | .status-starting { |
| | | border-left: 4px solid #409eff; |
| | | border-left: 4px solid var(--brand-main); |
| | | } |
| | | |
| | | .status-running { |
| | | border-left: 4px solid #67c23a; |
| | | border-left: 4px solid var(--ok); |
| | | } |
| | | |
| | | .status-stopping { |
| | | border-left: 4px solid #e6a23c; |
| | | border-left: 4px solid var(--warn); |
| | | } |
| | | |
| | | .status-error { |
| | | border-left: 4px solid #f56c6c; |
| | | border-left: 4px solid var(--danger); |
| | | } |
| | | |
| | | /* Loading Spinner Overlay */ |
| | | .spinner-overlay { |
| | | position: fixed; |
| | | top: 0; |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | background-color: rgba(0, 0, 0, 0.5); |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | z-index: 9999; |
| | | .loading-container { |
| | | text-align: center; |
| | | padding: 60px 0; |
| | | color: var(--text-sub); |
| | | } |
| | | |
| | | /* Spin icon animation */ |
| | | .loading-icon, |
| | | .spin-icon { |
| | | animation: spin 1s linear infinite; |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | /* Card hover effect */ |
| | | .el-card { |
| | | transition: all 0.3s ease; |
| | | transition: all 0.2s ease; |
| | | } |
| | | |
| | | .el-card:hover { |
| | | transform: translateY(-2px); |
| | | transform: translateY(-1px); |
| | | } |
| | | |
| | | /* Custom scrollbar */ |
| | | .el-table { |
| | | --el-table-header-bg-color: #f3f7fb; |
| | | } |
| | | |
| | | ::-webkit-scrollbar { |
| | | width: 8px; |
| | | height: 8px; |
| | | } |
| | | |
| | | ::-webkit-scrollbar-track { |
| | | background: #f1f1f1; |
| | | background: #eef2f6; |
| | | } |
| | | |
| | | ::-webkit-scrollbar-thumb { |
| | | background: #c0c4cc; |
| | | background: #b4beca; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | ::-webkit-scrollbar-thumb:hover { |
| | | background: #909399; |
| | | background: #8f9cab; |
| | | } |
| | |
| | | version: string |
| | | fields: ProtocolFieldMapping[] |
| | | } |
| | | |
| | | export interface RobotClientStartRequest { |
| | | serverId: string |
| | | listenIp: string |
| | | listenPort: number |
| | | localPort: number |
| | | } |
| | | |
| | | export interface RobotClientSendRequest { |
| | | serverId: string |
| | | clientId?: number | null |
| | | message: string |
| | | } |
| | | |
| | | export interface RobotClientStatusItem { |
| | | clientId: number |
| | | remoteEndPoint: string | null |
| | | connected: boolean |
| | | connectedAt: string | null |
| | | lastReceivedMessage: string | null |
| | | lastError: string | null |
| | | lastReceivedAt: string | null |
| | | lastSentAt: string | null |
| | | } |
| | | |
| | | export interface RobotServerReceivedMessageItem { |
| | | receivedAt: string |
| | | clientId: number |
| | | remoteEndPoint: string | null |
| | | message: string |
| | | } |
| | | |
| | | export interface RobotServerSentMessageItem { |
| | | sentAt: string |
| | | clientId: number |
| | | remoteEndPoint: string | null |
| | | message: string |
| | | } |
| | | |
| | | export interface RobotServerStatusItem { |
| | | serverId: string |
| | | running: boolean |
| | | listenIp: string |
| | | listenPort: number |
| | | localPort: number |
| | | connectedCount: number |
| | | clients: RobotClientStatusItem[] |
| | | receivedMessages: RobotServerReceivedMessageItem[] |
| | | sentMessages: RobotServerSentMessageItem[] |
| | | } |
| | | |
| | | export interface RobotClientStatusResponse { |
| | | runningServerCount: number |
| | | servers: RobotServerStatusItem[] |
| | | } |
| | |
| | | <template> |
| | | <div> |
| | | <div class="admin-page"> |
| | | <div class="page-header"> |
| | | <div class="header-left"> |
| | | <h2> |
| | |
| | | |
| | | <el-row justify="center"> |
| | | <el-col :lg="24"> |
| | | <el-card shadow="never"> |
| | | <el-card shadow="never" class="panel-card"> |
| | | <el-form :model="form" :rules="rules" ref="formRef" label-width="120px"> |
| | | <!-- åºæ¬ä¿¡æ¯ --> |
| | | <el-divider content-position="left"> |
| | |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .page-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: flex-start; |
| | | margin-bottom: 20px; |
| | | flex-wrap: wrap; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .header-left h2 { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | margin: 0 0 8px 0; |
| | | } |
| | | |
| | | .text-muted { |
| | | color: #909399; |
| | | margin: 0; |
| | | } |
| | | |
| | | .el-divider h3 { |
| | | margin: 0; |
| | | font-size: 16px; |
| | |
| | | <template> |
| | | <div |
| | | class="admin-page" |
| | | v-loading.fullscreen.lock="startActionLoading" |
| | | element-loading-text="æ£å¨å¯å¨å®ä¾ï¼è¯·ç¨å..." |
| | | > |
| | |
| | | </el-button> |
| | | </div> |
| | | |
| | | <el-row :gutter="20" class="status-cards"> |
| | | <el-col :xs="12" :sm="6"> |
| | | <el-card shadow="hover" class="status-card"> |
| | | <el-statistic title="ç¶æ"> |
| | | <template #default> |
| | | <el-tag :type="getStatusTagType(instance.status)" size="large"> |
| | | {{ getStatusText(instance.status) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-statistic> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="12" :sm="6"> |
| | | <el-card shadow="hover" class="status-card"> |
| | | <el-statistic title="è¿æ¥å®¢æ·ç«¯" :value="instance.clientCount"> |
| | | <template #suffix> |
| | | <el-icon><User /></el-icon> |
| | | </template> |
| | | </el-statistic> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="12" :sm="6"> |
| | | <el-card shadow="hover" class="status-card"> |
| | | <el-statistic title="æ»è¯·æ±æ°" :value="instance.totalRequests" /> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="12" :sm="6"> |
| | | <el-card shadow="hover" class="status-card"> |
| | | <el-statistic title="端å£" :value="instance.port" /> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | <section class="section-block"> |
| | | <div class="section-head"> |
| | | <div> |
| | | <h3 class="section-title">ä¿¡æ¯åº</h3> |
| | | <p class="section-desc">å®ä¾è¿è¡ç¶æä¸è¿æ¥æ¦è§</p> |
| | | </div> |
| | | </div> |
| | | <div class="section-body"> |
| | | <el-row :gutter="12" class="status-cards"> |
| | | <el-col :xs="12" :sm="6"> |
| | | <el-card shadow="hover" class="status-card panel-card"> |
| | | <el-statistic title="ç¶æ"> |
| | | <template #default> |
| | | <el-tag :type="getStatusTagType(instance.status)" size="large"> |
| | | {{ getStatusText(instance.status) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-statistic> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="12" :sm="6"> |
| | | <el-card shadow="hover" class="status-card panel-card"> |
| | | <el-statistic title="è¿æ¥å®¢æ·ç«¯" :value="instance.clientCount"> |
| | | <template #suffix> |
| | | <el-icon><User /></el-icon> |
| | | </template> |
| | | </el-statistic> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="12" :sm="6"> |
| | | <el-card shadow="hover" class="status-card panel-card"> |
| | | <el-statistic title="æ»è¯·æ±æ°" :value="instance.totalRequests" /> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="12" :sm="6"> |
| | | <el-card shadow="hover" class="status-card panel-card"> |
| | | <el-statistic title="端å£" :value="instance.port" /> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </section> |
| | | |
| | | <el-card class="mt-4" shadow="never"> |
| | | <section class="section-block detail-section"> |
| | | <div class="section-head"> |
| | | <div> |
| | | <h3 class="section-title">æä½åº</h3> |
| | | <p class="section-desc">左侧å®ä¾ä¿¡æ¯ä¸æä½ï¼å³ä¾§å®æ¶ DB æ°æ®</p> |
| | | </div> |
| | | </div> |
| | | <div class="section-body"> |
| | | <el-row :gutter="16" class="detail-main"> |
| | | <el-col :xs="24" :lg="8"> |
| | | <el-card class="panel-card" shadow="never"> |
| | | <template #header> |
| | | <span class="card-header-title">åºæ¬ä¿¡æ¯</span> |
| | | </template> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions :column="1" border> |
| | | <el-descriptions-item label="å®ä¾ID">{{ instance.instanceId }}</el-descriptions-item> |
| | | <el-descriptions-item label="å®ä¾åç§°">{{ instance.name }}</el-descriptions-item> |
| | | <el-descriptions-item label="PLCåå·">{{ getPlcTypeText(instance.plcType) }}</el-descriptions-item> |
| | |
| | | <el-descriptions-item v-if="instance.lastActivityTime" label="æåæ´»å¨æ¶é´"> |
| | | {{ formatDate(instance.lastActivityTime) }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item v-if="instance.errorMessage" label="é误信æ¯" :span="2"> |
| | | <el-descriptions-item v-if="instance.errorMessage" label="é误信æ¯"> |
| | | <el-text type="danger">{{ instance.errorMessage }}</el-text> |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-card> |
| | | |
| | | <el-card class="mt-4" shadow="never"> |
| | | <el-card class="panel-card left-actions" shadow="never"> |
| | | <div class="action-buttons"> |
| | | <el-button |
| | | v-if="instance.status === 'Stopped' || instance.status === 'Error'" |
| | | type="success" |
| | | |
| | | @click="handleStart" |
| | | > |
| | | <el-icon><VideoPlay /></el-icon> |
| | |
| | | <el-button |
| | | v-if="instance.status === 'Running'" |
| | | type="warning" |
| | | |
| | | @click="handleStop" |
| | | > |
| | | <el-icon><VideoPause /></el-icon> |
| | |
| | | </div> |
| | | </el-card> |
| | | |
| | | <el-card class="mt-4" shadow="never"> |
| | | </el-col> |
| | | <el-col :xs="24" :lg="16"> |
| | | <el-card class="panel-card" shadow="never"> |
| | | <template #header> |
| | | <div class="db-header"> |
| | | <span class="card-header-title">DBå宿¶æ°æ®</span> |
| | | <div class="db-toolbar"> |
| | | <el-switch v-model="autoRefreshDb" active-text="èªå¨å·æ°" /> |
| | | <el-button size="small" @click="loadMemoryData(true)">æå¨å·æ°</el-button> |
| | | <el-button @click="loadMemoryData(true)">æå¨å·æ°</el-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | <template #default> |
| | | <div v-if="deviceDbViews.length === 0" class="text-muted">å½åè®¾å¤æ¨¡æ¿æªå¹é
å°å¯æ¾ç¤ºçDBå</div> |
| | | <div v-else> |
| | | <el-tabs type="border-card" class="db-tabs"> |
| | | <el-tabs type="border-card" class="db-tabs"> |
| | | <el-tab-pane |
| | | v-for="view in deviceDbViews" |
| | | :key="view.templateDbNumber" |
| | |
| | | :key="`${view.templateDbNumber}-${group.key}`" |
| | | > |
| | | <template #label> |
| | | <el-tag :type="getFieldGroupTagType(group.key)" size="small">{{ group.key }}</el-tag> |
| | | <el-tag :type="getFieldGroupTagType(group.key)">{{ group.key }}</el-tag> |
| | | </template> |
| | | <div class="field-table-wrap"> |
| | | <el-table |
| | | :data="group.fields" |
| | | border |
| | | size="small" |
| | | class="field-table" |
| | | table-layout="auto" |
| | | empty-text="å½ååç»æ åæ®µæ å°" |
| | | > |
| | | <el-table-column prop="fieldKey" label="åæ®µ" min-width="140" /> |
| | | <el-table-column prop="address" label="å°å" width="130" /> |
| | | <el-table-column prop="mappedDb" label="æ å°å" width="120"> |
| | | <el-table-column prop="fieldKey" label="åæ®µ" /> |
| | | <el-table-column prop="address" label="å°å" /> |
| | | <el-table-column prop="mappedDb" label="æ å°å"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="getDbTagType(row.mappedDb)" size="small">{{ row.mappedDb }}</el-tag> |
| | | <el-tag :type="getDbTagType(row.mappedDb)">{{ row.mappedDb }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="dataType" label="ç±»å" width="90" /> |
| | | <el-table-column prop="direction" label="æ¹å" width="130" /> |
| | | <el-table-column prop="value" label="å½åå¼" min-width="220" /> |
| | | <el-table-column label="ä¿®æ¹å¼" min-width="220"> |
| | | <el-table-column prop="dataType" label="ç±»å" /> |
| | | <el-table-column prop="direction" label="æ¹å" /> |
| | | <el-table-column prop="value" label="å½åå¼" /> |
| | | <el-table-column label="ä¿®æ¹å¼"> |
| | | <template #default="{ row }"> |
| | | <el-switch |
| | | v-if="row.dataType === 'Bool'" |
| | |
| | | @change="markFieldDirty(row)" |
| | | :disabled="!isFieldWritable(row)" |
| | | :controls="false" |
| | | style="width: 100%" |
| | | class="editable-control" |
| | | /> |
| | | <el-input |
| | | v-else |
| | | v-model="fieldEditValues[getFieldEditKey(row)]" |
| | | @input="markFieldDirty(row)" |
| | | :disabled="!isFieldWritable(row)" |
| | | class="editable-control" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="90" fixed="right"> |
| | | <el-table-column label="æä½" width="88" fixed="right"> |
| | | <template #default="{ row }"> |
| | | <el-button |
| | | type="primary" |
| | |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | <template v-else-if="view.fields.length > 0"> |
| | | <div class="field-table-wrap"> |
| | | <el-table |
| | | v-else-if="view.fields.length > 0" |
| | | :data="view.fields" |
| | | border |
| | | size="small" |
| | | class="field-table" |
| | | table-layout="auto" |
| | | empty-text="å½åDBåæ åæ®µæ å°" |
| | | > |
| | | <el-table-column prop="fieldKey" label="åæ®µ" min-width="140" /> |
| | | <el-table-column prop="address" label="å°å" width="130" /> |
| | | <el-table-column prop="mappedDb" label="æ å°å" width="120"> |
| | | <el-table-column prop="fieldKey" label="åæ®µ" /> |
| | | <el-table-column prop="address" label="å°å" /> |
| | | <el-table-column prop="mappedDb" label="æ å°å"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="getDbTagType(row.mappedDb)" size="small">{{ row.mappedDb }}</el-tag> |
| | | <el-tag :type="getDbTagType(row.mappedDb)">{{ row.mappedDb }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="dataType" label="ç±»å" width="90" /> |
| | | <el-table-column prop="direction" label="æ¹å" width="130" /> |
| | | <el-table-column prop="value" label="å½åå¼" min-width="220" /> |
| | | <el-table-column label="ä¿®æ¹å¼" min-width="220"> |
| | | <el-table-column prop="dataType" label="ç±»å" /> |
| | | <el-table-column prop="direction" label="æ¹å" /> |
| | | <el-table-column prop="value" label="å½åå¼" /> |
| | | <el-table-column label="ä¿®æ¹å¼"> |
| | | <template #default="{ row }"> |
| | | <el-switch |
| | | v-if="row.dataType === 'Bool'" |
| | |
| | | @change="markFieldDirty(row)" |
| | | :disabled="!isFieldWritable(row)" |
| | | :controls="false" |
| | | style="width: 100%" |
| | | class="editable-control" |
| | | /> |
| | | <el-input |
| | | v-else |
| | | v-model="fieldEditValues[getFieldEditKey(row)]" |
| | | @input="markFieldDirty(row)" |
| | | :disabled="!isFieldWritable(row)" |
| | | class="editable-control" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="90" fixed="right"> |
| | | <el-table-column label="æä½" width="88" fixed="right"> |
| | | <template #default="{ row }"> |
| | | <el-button |
| | | type="primary" |
| | |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </template> |
| | | <div v-else class="text-muted">å½åDBåæ åæ®µæ å°</div> |
| | | |
| | | <div class="card-header-title field-title">åå§æ°æ®</div> |
| | |
| | | /> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </section> |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .page-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: flex-start; |
| | | margin-bottom: 20px; |
| | | flex-wrap: wrap; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .header-left h2 { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | margin: 0 0 8px 0; |
| | | } |
| | | |
| | | .text-muted { |
| | | color: #909399; |
| | | margin: 0; |
| | | } |
| | | |
| | | .loading-container { |
| | | text-align: center; |
| | | padding: 60px 0; |
| | | color: #909399; |
| | | } |
| | | |
| | | .loading-icon { |
| | | animation: spin 1s linear infinite; |
| | | } |
| | | |
| | | @keyframes spin { |
| | | from { |
| | | transform: rotate(0deg); |
| | | } |
| | | to { |
| | | transform: rotate(360deg); |
| | | } |
| | | } |
| | | |
| | | .status-cards { |
| | | margin-bottom: 20px; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .status-card { |
| | |
| | | } |
| | | |
| | | .mt-4 { |
| | | margin-top: 16px; |
| | | margin-top: 14px; |
| | | } |
| | | |
| | | .action-buttons { |
| | | display: flex; |
| | | gap: 12px; |
| | | gap: 10px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .left-actions { |
| | | margin-top: 14px; |
| | | } |
| | | |
| | | .db-header { |
| | |
| | | |
| | | .db-content { |
| | | margin: 0; |
| | | max-height: 180px; |
| | | max-height: 168px; |
| | | overflow: auto; |
| | | white-space: pre-wrap; |
| | | font-family: Consolas, Monaco, 'Courier New', monospace; |
| | |
| | | } |
| | | |
| | | .db-tabs { |
| | | margin-top: 8px; |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .data-view-tabs { |
| | | margin-top: 8px; |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .db-raw-toolbar { |
| | |
| | | margin-top: 12px; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .field-table-wrap { |
| | | width: 100%; |
| | | overflow-x: auto; |
| | | } |
| | | |
| | | :deep(.field-table) { |
| | | width: max-content; |
| | | min-width: 100%; |
| | | } |
| | | |
| | | :deep(.editable-control) { |
| | | width: 100%; |
| | | } |
| | | |
| | | :deep(.status-card .el-card__body) { |
| | | padding: 14px 16px; |
| | | } |
| | | |
| | | :deep(.action-buttons .el-button) { |
| | | min-width: 82px; |
| | | } |
| | | |
| | | :deep(.panel-card > .el-card__header) { |
| | | padding: 14px 18px; |
| | | } |
| | | |
| | | :deep(.panel-card > .el-card__body) { |
| | | padding: 14px 18px; |
| | | } |
| | | </style> |
| | | |
| | |
| | | <template> |
| | | <div> |
| | | <div class="admin-page"> |
| | | <div v-if="loading" class="loading-container"> |
| | | <el-icon class="loading-icon" :size="40"><Loading /></el-icon> |
| | | <p>å è½½ä¸...</p> |
| | |
| | | |
| | | <el-row justify="center"> |
| | | <el-col :lg="24"> |
| | | <el-card shadow="never"> |
| | | <el-card shadow="never" class="panel-card"> |
| | | <el-form :model="form" :rules="rules" ref="formRef" label-width="120px"> |
| | | <!-- åºæ¬ä¿¡æ¯ --> |
| | | <el-divider content-position="left"> |
| | |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .page-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: flex-start; |
| | | margin-bottom: 20px; |
| | | flex-wrap: wrap; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .header-left h2 { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | margin: 0 0 8px 0; |
| | | } |
| | | |
| | | .text-muted { |
| | | color: #909399; |
| | | margin: 0; |
| | | } |
| | | |
| | | .loading-container { |
| | | text-align: center; |
| | | padding: 60px 0; |
| | | color: #909399; |
| | | } |
| | | |
| | | .loading-icon { |
| | | animation: spin 1s linear infinite; |
| | | } |
| | | |
| | | @keyframes spin { |
| | | from { |
| | | transform: rotate(0deg); |
| | | } |
| | | to { |
| | | transform: rotate(360deg); |
| | | } |
| | | } |
| | | |
| | | .form-tip { |
| | | font-size: 12px; |
| | | color: #909399; |
| | |
| | | <template> |
| | | <div |
| | | class="admin-page" |
| | | v-loading.fullscreen.lock="startActionLoading" |
| | | element-loading-text="æ£å¨å¯å¨å®ä¾ï¼è¯·ç¨å..." |
| | | > |
| | |
| | | <p class="text-muted">管çåçæ§ S7 PLC 模æå¨å®ä¾</p> |
| | | </div> |
| | | <div class="header-right"> |
| | | <div class="stats"> |
| | | <span>è¿è¡ä¸: {{ runningCount }} | 已忢: {{ stoppedCount }}</span> |
| | | <span v-if="errorCount > 0" class="error-text">| é误: {{ errorCount }}</span> |
| | | </div> |
| | | <el-button type="primary" @click="$router.push('/create')"> |
| | | <el-button type="primary" class="create-btn" @click="$router.push('/create')"> |
| | | <el-icon><Plus /></el-icon> |
| | | å建å®ä¾ |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <section class="section-block"> |
| | | <div class="section-head"> |
| | | <div> |
| | | <h3 class="section-title">ä¿¡æ¯åº</h3> |
| | | <p class="section-desc">å®ä¾è¿è¡ç¶ææ»è§</p> |
| | | </div> |
| | | </div> |
| | | <div class="section-body"> |
| | | <el-row :gutter="12" class="summary-row"> |
| | | <el-col :xs="24" :sm="8"> |
| | | <el-card shadow="hover" class="summary-card running-card"> |
| | | <div class="summary-title">è¿è¡ä¸</div> |
| | | <div class="summary-value">{{ runningCount }}</div> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="24" :sm="8"> |
| | | <el-card shadow="hover" class="summary-card stopped-card"> |
| | | <div class="summary-title">已忢</div> |
| | | <div class="summary-value">{{ stoppedCount }}</div> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="24" :sm="8"> |
| | | <el-card shadow="hover" class="summary-card error-card"> |
| | | <div class="summary-title">é误å®ä¾</div> |
| | | <div class="summary-value">{{ errorCount }}</div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </section> |
| | | |
| | | <!-- Loading state --> |
| | | <div v-if="loading && instances.length === 0" class="loading-container"> |
| | |
| | | </el-empty> |
| | | |
| | | <!-- Instances grid --> |
| | | <el-row v-else :gutter="20"> |
| | | <el-col v-for="instance in instances" :key="instance.instanceId" :xs="24" :sm="12" :xl="8"> |
| | | <el-card class="instance-card" :class="getStatusClass(instance.status)" shadow="hover"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span class="instance-id">{{ instance.instanceId }}</span> |
| | | <el-tag :type="getStatusTagType(instance.status)"> |
| | | {{ getStatusText(instance.status) }} |
| | | </el-tag> |
| | | <section v-else class="section-block"> |
| | | <div class="section-head"> |
| | | <div> |
| | | <h3 class="section-title">æä½åº</h3> |
| | | <p class="section-desc">å®ä¾å¯å¨ã忢ãç¼è¾ä¸è¯¦æ
å
¥å£</p> |
| | | </div> |
| | | </div> |
| | | <div class="section-body"> |
| | | <el-row :gutter="14" class="instances-grid"> |
| | | <el-col |
| | | v-for="instance in instances" |
| | | :key="instance.instanceId" |
| | | :xs="24" |
| | | :sm="12" |
| | | :md="12" |
| | | :lg="8" |
| | | :xl="6" |
| | | > |
| | | <el-card class="instance-card panel-card" :class="getStatusClass(instance.status)" shadow="hover"> |
| | | <div class="card-glow"></div> |
| | | |
| | | <div class="card-top"> |
| | | <div class="card-title"> |
| | | <div class="instance-id-line"> |
| | | <span class="instance-id">{{ instance.instanceId }}</span> |
| | | <span class="instance-name">{{ instance.name || 'æªå½åå®ä¾' }}</span> |
| | | <span class="instance-sub">PLC</span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="instance-info"> |
| | | <el-descriptions :column="2" size="small"> |
| | | <el-descriptions-item label="åç§°">{{ instance.name || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="PLCåå·">{{ getPlcTypeText(instance.plcType) }}</el-descriptions-item> |
| | | <el-descriptions-item label="端å£">{{ instance.port || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="客æ·ç«¯"> |
| | | <el-icon><User /></el-icon> |
| | | {{ instance.clientCount || 0 }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item v-if="instance.startTime" label="å¯å¨æ¶é´" :span="2"> |
| | | {{ formatDate(instance.startTime) }} |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <el-alert |
| | | v-if="instance.errorMessage" |
| | | type="error" |
| | | :closable="false" |
| | | class="mt-2" |
| | | show-icon |
| | | > |
| | | {{ instance.errorMessage }} |
| | | </el-alert> |
| | | <el-tag :type="getStatusTagType(instance.status)" effect="dark" round> |
| | | {{ getStatusText(instance.status) }} |
| | | </el-tag> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="card-footer"> |
| | | <div class="meta-row"> |
| | | <div class="meta-chip"> |
| | | <span class="chip-label">PLC</span> |
| | | <span class="chip-value">{{ getPlcTypeText(instance.plcType) }}</span> |
| | | </div> |
| | | <div class="meta-chip"> |
| | | <span class="chip-label">端å£</span> |
| | | <span class="chip-value">{{ instance.port || '-' }}</span> |
| | | </div> |
| | | <div class="meta-chip"> |
| | | <span class="chip-label">客æ·ç«¯</span> |
| | | <span class="chip-value"> |
| | | <el-icon><User /></el-icon> |
| | | {{ instance.clientCount || 0 }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="time-row"> |
| | | <span class="time-label">å¯å¨æ¶é´</span> |
| | | <span class="time-value">{{ instance.startTime ? formatDate(instance.startTime) : '-' }}</span> |
| | | </div> |
| | | |
| | | <el-alert |
| | | v-if="instance.errorMessage" |
| | | type="error" |
| | | :closable="false" |
| | | class="mt-2" |
| | | show-icon |
| | | > |
| | | {{ instance.errorMessage }} |
| | | </el-alert> |
| | | |
| | | <div class="card-footer"> |
| | | <div class="main-actions"> |
| | | <el-button |
| | | v-if="instance.status === 'Running'" |
| | | type="warning" |
| | |
| | | <el-icon><Edit /></el-icon> |
| | | ç¼è¾ |
| | | </el-button> |
| | | <el-button type="danger" @click="handleDelete(instance.instanceId)"> |
| | | <el-icon><Delete /></el-icon> |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | <el-button class="delete-btn" type="danger" @click="handleDelete(instance.instanceId)"> |
| | | <el-icon><Delete /></el-icon> |
| | | </el-button> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </section> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .page-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: flex-start; |
| | | margin-bottom: 20px; |
| | | flex-wrap: wrap; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .header-left h2 { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | margin: 0 0 8px 0; |
| | | } |
| | | |
| | | .text-muted { |
| | | color: #909399; |
| | | margin: 0; |
| | | } |
| | | |
| | | .header-right { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 16px; |
| | | gap: 10px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .stats { |
| | | color: #606266; |
| | | font-size: 14px; |
| | | .create-btn { |
| | | padding-inline: 18px; |
| | | } |
| | | |
| | | .error-text { |
| | | color: #f56c6c; |
| | | .summary-row { |
| | | margin-top: -2px; |
| | | margin-bottom: 2px; |
| | | } |
| | | |
| | | .loading-container { |
| | | text-align: center; |
| | | padding: 60px 0; |
| | | color: #909399; |
| | | .instances-grid { |
| | | max-width: 1500px; |
| | | } |
| | | |
| | | .loading-icon { |
| | | animation: spin 1s linear infinite; |
| | | .summary-card { |
| | | border-radius: 12px; |
| | | border: 1px solid #dbe2ea; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | @keyframes spin { |
| | | from { |
| | | transform: rotate(0deg); |
| | | } |
| | | to { |
| | | transform: rotate(360deg); |
| | | } |
| | | .summary-title { |
| | | color: #64748b; |
| | | font-size: 13px; |
| | | margin-bottom: 6px; |
| | | } |
| | | |
| | | .summary-value { |
| | | font-size: 28px; |
| | | font-weight: 700; |
| | | line-height: 1; |
| | | } |
| | | |
| | | .running-card { |
| | | background: linear-gradient(180deg, #f0fdf4 0%, #ffffff 100%); |
| | | } |
| | | |
| | | .running-card .summary-value { |
| | | color: #15803d; |
| | | } |
| | | |
| | | .stopped-card { |
| | | background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%); |
| | | } |
| | | |
| | | .stopped-card .summary-value { |
| | | color: #334155; |
| | | } |
| | | |
| | | .error-card { |
| | | background: linear-gradient(180deg, #fef2f2 0%, #ffffff 100%); |
| | | } |
| | | |
| | | .error-card .summary-value { |
| | | color: #b91c1c; |
| | | } |
| | | |
| | | .instance-card { |
| | | margin-bottom: 20px; |
| | | transition: all 0.3s; |
| | | margin-bottom: 10px; |
| | | position: relative; |
| | | border-radius: 12px; |
| | | border: 1px solid #dbe2ea; |
| | | transition: all 0.25s; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .instance-card:hover { |
| | | transform: translateY(-4px); |
| | | transform: translateY(-3px); |
| | | box-shadow: 0 14px 26px rgba(15, 23, 42, 0.1); |
| | | } |
| | | |
| | | .card-header { |
| | | .card-glow { |
| | | position: absolute; |
| | | top: -56px; |
| | | right: -56px; |
| | | width: 100px; |
| | | height: 100px; |
| | | border-radius: 50%; |
| | | background: radial-gradient(circle, rgba(14, 116, 244, 0.16), transparent 70%); |
| | | pointer-events: none; |
| | | } |
| | | |
| | | .card-title { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | flex-direction: column; |
| | | gap: 1px; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .instance-id { |
| | | font-weight: 600; |
| | | font-size: 16px; |
| | | font-size: 15px; |
| | | color: #0f172a; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .instance-info { |
| | | margin-bottom: 16px; |
| | | .instance-name { |
| | | color: #64748b; |
| | | font-size: 12px; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .card-footer { |
| | | .instance-id-line { |
| | | display: flex; |
| | | align-items: baseline; |
| | | gap: 6px; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .instance-sub { |
| | | color: #94a3b8; |
| | | font-size: 10px; |
| | | } |
| | | |
| | | .card-top { |
| | | margin-top: 2px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | align-items: flex-start; |
| | | } |
| | | |
| | | .meta-row { |
| | | margin-top: 8px; |
| | | display: flex; |
| | | gap: 6px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .card-footer .el-button { |
| | | flex: 1; |
| | | min-width: 60px; |
| | | .meta-chip { |
| | | border: 1px solid #e2e8f0; |
| | | border-radius: 999px; |
| | | padding: 4px 8px; |
| | | background: #f8fafc; |
| | | display: inline-flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | } |
| | | |
| | | .status-stopped { border-left: 4px solid #909399; } |
| | | .status-starting { border-left: 4px solid #409eff; } |
| | | .status-running { border-left: 4px solid #67c23a; } |
| | | .status-stopping { border-left: 4px solid #e6a23c; } |
| | | .status-error { border-left: 4px solid #f56c6c; } |
| | | .chip-label { |
| | | font-size: 10px; |
| | | line-height: 1; |
| | | color: #64748b; |
| | | padding: 1px 5px; |
| | | border-radius: 999px; |
| | | background: #e2e8f0; |
| | | } |
| | | |
| | | .chip-value { |
| | | font-size: 12px; |
| | | line-height: 1; |
| | | color: #0f172a; |
| | | display: inline-flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .time-row { |
| | | margin-top: 7px; |
| | | margin-bottom: 8px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | font-size: 11px; |
| | | } |
| | | |
| | | .time-label { |
| | | color: #64748b; |
| | | } |
| | | |
| | | .time-value { |
| | | color: #0f172a; |
| | | } |
| | | |
| | | .card-footer { |
| | | border-top: 1px dashed #dbe2ea; |
| | | padding-top: 8px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .main-actions { |
| | | display: flex; |
| | | gap: 6px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .main-actions .el-button { |
| | | border-radius: 8px; |
| | | height: 28px; |
| | | padding: 0 10px; |
| | | } |
| | | |
| | | .delete-btn { |
| | | border-radius: 8px; |
| | | height: 28px; |
| | | padding: 0 10px; |
| | | } |
| | | |
| | | .mt-2 { |
| | | margin-top: 8px; |
| | | } |
| | | </style> |
| | | |
| | | |
| | |
| | | <template> |
| | | <div> |
| | | <div class="admin-page"> |
| | | <div class="page-header"> |
| | | <div> |
| | | <h2>å议模æ¿ç®¡ç</h2> |
| | |
| | | <el-button type="primary" @click="openNewTemplate">æ°å»ºæ¨¡æ¿</el-button> |
| | | </div> |
| | | |
| | | <el-table :data="templates" border> |
| | | <el-card shadow="never" class="panel-card"> |
| | | <el-table :data="templates" border> |
| | | <el-table-column prop="id" label="模æ¿ID" width="240" /> |
| | | <el-table-column prop="name" label="åç§°" min-width="240" /> |
| | | <el-table-column prop="version" label="çæ¬" width="140" /> |
| | |
| | | <el-button size="small" type="danger" @click="removeTemplate(row.id)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-table> |
| | | </el-card> |
| | | |
| | | <el-dialog v-model="dialogVisible" title="å议模æ¿" width="96vw" top="2vh" class="protocol-dialog"> |
| | | <el-form :model="editing" label-width="100px"> |
| | |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .page-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: flex-start; |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .text-muted { |
| | | color: #909399; |
| | | margin: 4px 0 0 0; |
| | | } |
| | | |
| | | .mt-2 { |
| | | margin-top: 12px; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="admin-page robot-admin-page"> |
| | | <div class="page-header"> |
| | | <div> |
| | | <h2>æºæ¢°æå®¢æ·ç«¯ç®¡çå°</h2> |
| | | <p class="text-muted">å¤å®ä¾è¿æ¥ç®æ æå¡ç«¯ãæ¶æ¯æ¶åçæ§</p> |
| | | </div> |
| | | <div class="toolbar-actions"> |
| | | <el-button :loading="refreshing" @click="loadStatus">å·æ°</el-button> |
| | | <el-button type="danger" :loading="stoppingAll" @click="handleStopAll">忢å
¨é¨</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <el-row :gutter="12" class="stats-row"> |
| | | <el-col :xs="24" :sm="8"> |
| | | <el-card shadow="hover" class="stat-card"> |
| | | <el-statistic title="è¿è¡å®ä¾" :value="status?.runningServerCount ?? 0" /> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="24" :sm="8"> |
| | | <el-card shadow="hover" class="stat-card"> |
| | | <el-statistic title="å®ä¾æ»æ°" :value="status?.servers.length ?? 0" /> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="24" :sm="8"> |
| | | <el-card shadow="hover" class="stat-card"> |
| | | <el-statistic title="å¨çº¿è¿æ¥æ»æ°" :value="totalConnectedCount" /> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-card shadow="never" class="create-card"> |
| | | <template #header> |
| | | <span>æ°å¢å®¢æ·ç«¯å®ä¾</span> |
| | | </template> |
| | | <el-form :inline="true" :model="startForm" class="create-form"> |
| | | <el-form-item label="å®ä¾ID"> |
| | | <el-input v-model="startForm.serverId" placeholder="robot-client-1" style="width: 180px" /> |
| | | </el-form-item> |
| | | <el-form-item label="æå¡ç«¯å°å"> |
| | | <el-input v-model="startForm.listenIp" placeholder="127.0.0.1" style="width: 160px" /> |
| | | </el-form-item> |
| | | <el-form-item label="æå¡ç«¯ç«¯å£"> |
| | | <el-input-number v-model="startForm.listenPort" :min="1" :max="65535" /> |
| | | </el-form-item> |
| | | <el-form-item label="æ¬å°ç«¯å£"> |
| | | <el-input-number v-model="startForm.localPort" :min="1" :max="65535" /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" :loading="starting" @click="handleStart">è¿æ¥å®ä¾</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </el-card> |
| | | |
| | | <el-row :gutter="12" class="main-row"> |
| | | <el-col :xs="24" :lg="8"> |
| | | <el-card shadow="never" class="panel-card"> |
| | | <template #header> |
| | | <span>客æ·ç«¯å®ä¾å表</span> |
| | | </template> |
| | | <el-table |
| | | :data="status?.servers || []" |
| | | border |
| | | size="small" |
| | | highlight-current-row |
| | | :row-class-name="serverRowClassName" |
| | | @row-click="onServerRowClick" |
| | | > |
| | | <el-table-column prop="serverId" label="å®ä¾ID" min-width="120" /> |
| | | <el-table-column label="ç®æ æå¡ç«¯" min-width="130"> |
| | | <template #default="{ row }">{{ row.listenIp }}:{{ row.listenPort }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="localPort" label="æ¬å°ç«¯å£" width="100" /> |
| | | <el-table-column prop="connectedCount" label="è¿æ¥" width="70" /> |
| | | <el-table-column label="æä½" width="90"> |
| | | <template #default="{ row }"> |
| | | <el-button link type="danger" @click.stop="handleStopOne(row.serverId)">忢</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | </el-col> |
| | | |
| | | <el-col :xs="24" :lg="16"> |
| | | <el-card v-if="selectedServer" shadow="never" class="panel-card"> |
| | | <template #header> |
| | | <div class="panel-header"> |
| | | <span>å®ä¾è¯¦æ
ï¼{{ selectedServer.serverId }}</span> |
| | | <el-tag :type="selectedServer.running ? 'success' : 'info'"> |
| | | {{ selectedServer.running ? 'è¿è¡ä¸' : '已忢' }} |
| | | </el-tag> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-descriptions :column="4" border class="desc-block"> |
| | | <el-descriptions-item label="æå¡ç«¯å°å">{{ selectedServer.listenIp }}</el-descriptions-item> |
| | | <el-descriptions-item label="æå¡ç«¯ç«¯å£">{{ selectedServer.listenPort }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ¬å°ç«¯å£">{{ selectedServer.localPort }}</el-descriptions-item> |
| | | <el-descriptions-item label="è¿æ¥ç¶æ">{{ selectedServer.connectedCount > 0 ? 'å·²è¿æ¥' : 'æªè¿æ¥' }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <div class="message-actions"> |
| | | <el-button type="primary" @click="openMessageCenter"> |
| | | æ¶æ¯ä¸å¿ |
| | | </el-button> |
| | | </div> |
| | | |
| | | <el-card shadow="never" class="connection-card"> |
| | | <template #header> |
| | | <span>è¿æ¥å表</span> |
| | | </template> |
| | | <el-table :data="selectedServer.clients || []" border size="small"> |
| | | <el-table-column prop="clientId" label="è¿æ¥ID" width="100" /> |
| | | <el-table-column prop="remoteEndPoint" label="è¿ç«¯å°å" min-width="170" /> |
| | | <el-table-column label="ç¶æ" width="100"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="row.connected ? 'success' : 'danger'">{{ row.connected ? 'å¨çº¿' : '离线' }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="connectedAt" label="è¿æ¥æ¶é´" min-width="170" /> |
| | | <el-table-column prop="lastReceivedAt" label="æè¿æ¥æ¶" min-width="170" /> |
| | | <el-table-column prop="lastSentAt" label="æè¿åé" min-width="170" /> |
| | | <el-table-column prop="lastError" label="æåé误" min-width="190" /> |
| | | </el-table> |
| | | </el-card> |
| | | </el-card> |
| | | |
| | | <el-empty v-else description="è¯·éæ©å·¦ä¾§å®¢æ·ç«¯å®ä¾æ¥ç详æ
" /> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-dialog |
| | | v-model="messageCenterVisible" |
| | | title="æ¶æ¯ä¸å¿" |
| | | width="78vw" |
| | | destroy-on-close |
| | | > |
| | | <el-card shadow="never" class="message-send-card"> |
| | | <template #header> |
| | | <span>åéæä»¤</span> |
| | | </template> |
| | | <div class="send-row"> |
| | | <el-input v-model="sendMessage" placeholder="ä¾å¦ Pickbattery,1" /> |
| | | <el-button type="primary" :loading="sending" @click="handleSend">åé</el-button> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <el-tabs v-model="messageTab"> |
| | | <el-tab-pane |
| | | :label="`æ¥æ¶æ¶æ¯ (${selectedServer?.receivedMessages?.length || 0})`" |
| | | name="received" |
| | | > |
| | | <div class="message-toolbar"> |
| | | <el-button |
| | | link |
| | | type="danger" |
| | | :loading="clearingMessages" |
| | | @click="handleClearMessages" |
| | | > |
| | | æ¸
ç©ºæ¶æ¯ |
| | | </el-button> |
| | | </div> |
| | | <el-table :data="selectedServer?.receivedMessages || []" border size="small" max-height="430"> |
| | | <el-table-column prop="receivedAt" label="æ¥æ¶æ¶é´" min-width="170" /> |
| | | <el-table-column prop="clientId" label="è¿æ¥ID" width="100" /> |
| | | <el-table-column prop="remoteEndPoint" label="è¿ç«¯å°å" min-width="170" /> |
| | | <el-table-column prop="message" label="æ¶æ¯å
容" min-width="250" /> |
| | | </el-table> |
| | | </el-tab-pane> |
| | | <el-tab-pane |
| | | :label="`åéæ¶æ¯ (${selectedServer?.sentMessages?.length || 0})`" |
| | | name="sent" |
| | | > |
| | | <el-table :data="selectedServer?.sentMessages || []" border size="small" max-height="430"> |
| | | <el-table-column prop="sentAt" label="åéæ¶é´" min-width="170" /> |
| | | <el-table-column prop="clientId" label="è¿æ¥ID" width="100" /> |
| | | <el-table-column prop="remoteEndPoint" label="è¿ç«¯å°å" min-width="170" /> |
| | | <el-table-column prop="message" label="æ¶æ¯å
容" min-width="250" /> |
| | | </el-table> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { computed, onMounted, onUnmounted, ref } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { |
| | | clearRobotClientReceivedMessages, |
| | | getRobotClientStatus, |
| | | sendRobotClientMessage, |
| | | startRobotClients, |
| | | stopRobotClients |
| | | } from '../api' |
| | | import type { |
| | | RobotClientStartRequest, |
| | | RobotClientStatusResponse, |
| | | RobotServerStatusItem |
| | | } from '../types' |
| | | |
| | | const startForm = ref<RobotClientStartRequest>({ |
| | | serverId: 'robot-client-1', |
| | | listenIp: '127.0.0.1', |
| | | listenPort: 2000, |
| | | localPort: 2001 |
| | | }) |
| | | |
| | | const status = ref<RobotClientStatusResponse | null>(null) |
| | | const selectedServerId = ref('') |
| | | |
| | | const refreshing = ref(false) |
| | | const starting = ref(false) |
| | | const stoppingAll = ref(false) |
| | | const sending = ref(false) |
| | | const clearingMessages = ref(false) |
| | | const messageCenterVisible = ref(false) |
| | | const messageTab = ref<'received' | 'sent'>('received') |
| | | |
| | | const sendMessage = ref('') |
| | | |
| | | let timer: number | null = null |
| | | |
| | | const totalConnectedCount = computed(() => |
| | | (status.value?.servers || []).reduce((sum, x) => sum + (x.connectedCount || 0), 0) |
| | | ) |
| | | |
| | | const selectedServer = computed<RobotServerStatusItem | null>(() => { |
| | | if (!status.value) return null |
| | | return status.value.servers.find(x => x.serverId === selectedServerId.value) || null |
| | | }) |
| | | |
| | | async function loadStatus() { |
| | | refreshing.value = true |
| | | try { |
| | | status.value = await getRobotClientStatus() |
| | | if ((status.value.servers || []).length > 0) { |
| | | const exists = status.value.servers.some(x => x.serverId === selectedServerId.value) |
| | | if (!exists) { |
| | | selectedServerId.value = status.value.servers[0].serverId |
| | | } |
| | | } else { |
| | | selectedServerId.value = '' |
| | | } |
| | | } catch (error) { |
| | | console.error(error) |
| | | } finally { |
| | | refreshing.value = false |
| | | } |
| | | } |
| | | |
| | | function onServerRowClick(row: RobotServerStatusItem) { |
| | | selectedServerId.value = row.serverId |
| | | } |
| | | |
| | | function serverRowClassName(args: { row: RobotServerStatusItem }) { |
| | | return args.row.serverId === selectedServerId.value ? 'is-selected-row' : '' |
| | | } |
| | | |
| | | function openMessageCenter() { |
| | | if (!selectedServer.value) { |
| | | ElMessage.warning('请å
éæ©ä¸ä¸ªå®¢æ·ç«¯å®ä¾') |
| | | return |
| | | } |
| | | |
| | | messageTab.value = 'received' |
| | | messageCenterVisible.value = true |
| | | } |
| | | |
| | | async function handleStart() { |
| | | if (!startForm.value.serverId.trim()) { |
| | | ElMessage.warning('请è¾å
¥å®ä¾ID') |
| | | return |
| | | } |
| | | |
| | | starting.value = true |
| | | try { |
| | | status.value = await startRobotClients({ |
| | | ...startForm.value, |
| | | serverId: startForm.value.serverId.trim() |
| | | }) |
| | | selectedServerId.value = startForm.value.serverId.trim() |
| | | ElMessage.success('客æ·ç«¯å®ä¾è¿æ¥æå') |
| | | } catch (error) { |
| | | console.error(error) |
| | | ElMessage.error('è¿æ¥å¤±è´¥ï¼è¯·æ£æ¥æå¡ç«¯å°åå端å£') |
| | | } finally { |
| | | starting.value = false |
| | | } |
| | | } |
| | | |
| | | async function handleStopAll() { |
| | | stoppingAll.value = true |
| | | try { |
| | | await stopRobotClients() |
| | | await loadStatus() |
| | | ElMessage.success('å
¨é¨å®¢æ·ç«¯å®ä¾å·²åæ¢') |
| | | } catch (error) { |
| | | console.error(error) |
| | | ElMessage.error('åæ¢å¤±è´¥ï¼è¯·æ¥çæ¥å¿') |
| | | } finally { |
| | | stoppingAll.value = false |
| | | } |
| | | } |
| | | |
| | | async function handleStopOne(serverId: string) { |
| | | try { |
| | | await stopRobotClients(serverId) |
| | | await loadStatus() |
| | | ElMessage.success(`å®ä¾ ${serverId} 已忢`) |
| | | } catch (error) { |
| | | console.error(error) |
| | | ElMessage.error('忢å®ä¾å¤±è´¥') |
| | | } |
| | | } |
| | | |
| | | async function handleSend() { |
| | | if (!selectedServer.value) { |
| | | ElMessage.warning('请å
éæ©ä¸ä¸ªå®¢æ·ç«¯å®ä¾') |
| | | return |
| | | } |
| | | |
| | | if (!sendMessage.value.trim()) { |
| | | ElMessage.warning('请è¾å
¥åéå
容') |
| | | return |
| | | } |
| | | |
| | | sending.value = true |
| | | try { |
| | | await sendRobotClientMessage({ |
| | | serverId: selectedServer.value.serverId, |
| | | clientId: null, |
| | | message: sendMessage.value.trim() |
| | | }) |
| | | ElMessage.success('åéæå') |
| | | } catch (error) { |
| | | console.error(error) |
| | | ElMessage.error('åé失败ï¼è¯·æ£æ¥è¿æ¥ç¶æ') |
| | | } finally { |
| | | sending.value = false |
| | | } |
| | | } |
| | | |
| | | async function handleClearMessages() { |
| | | if (!selectedServer.value) { |
| | | ElMessage.warning('请å
éæ©ä¸ä¸ªå®¢æ·ç«¯å®ä¾') |
| | | return |
| | | } |
| | | |
| | | clearingMessages.value = true |
| | | try { |
| | | await clearRobotClientReceivedMessages(selectedServer.value.serverId) |
| | | await loadStatus() |
| | | ElMessage.success('æ¥æ¶æ¶æ¯å·²æ¸
空') |
| | | } catch (error) { |
| | | console.error(error) |
| | | ElMessage.error('æ¸
空失败ï¼è¯·æ¥çæ¥å¿') |
| | | } finally { |
| | | clearingMessages.value = false |
| | | } |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | await loadStatus() |
| | | timer = window.setInterval(() => { |
| | | loadStatus() |
| | | }, 2000) |
| | | }) |
| | | |
| | | onUnmounted(() => { |
| | | if (timer !== null) { |
| | | clearInterval(timer) |
| | | } |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .robot-admin-page { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 14px; |
| | | } |
| | | |
| | | .toolbar-actions { |
| | | display: flex; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .stats-row { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .stat-card { |
| | | min-height: 92px; |
| | | } |
| | | |
| | | .create-card, |
| | | .panel-card { |
| | | border-radius: 8px; |
| | | } |
| | | |
| | | .create-form { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8px 12px; |
| | | } |
| | | |
| | | .main-row { |
| | | margin-top: 0; |
| | | } |
| | | |
| | | .panel-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .desc-block { |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .connection-card { |
| | | margin-top: 8px; |
| | | } |
| | | |
| | | .message-send-card { |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .send-row { |
| | | display: flex; |
| | | gap: 8px; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .message-actions { |
| | | display: flex; |
| | | gap: 10px; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .message-toolbar { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | margin-bottom: 6px; |
| | | } |
| | | |
| | | :deep(.is-selected-row td) { |
| | | background-color: #ecf5ff !important; |
| | | } |
| | | </style> |
| | |
| | | "WorkspaceRootPath": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\", |
| | | "Documents": [ |
| | | { |
| | | "AbsoluteMoniker": "D:0:0:{83F18A31-5983-4587-A0B2-414BF70E50B5}|WIDESEAWCS_TaskInfoService\\WIDESEAWCS_TaskInfoService.csproj|d:\\git\\shanmeixinnengyuan\\code\\wcs\\wideseawcs_server\\wideseawcs_taskinfoservice\\taskservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", |
| | | "RelativeMoniker": "D:0:0:{83F18A31-5983-4587-A0B2-414BF70E50B5}|WIDESEAWCS_TaskInfoService\\WIDESEAWCS_TaskInfoService.csproj|solutionrelative:wideseawcs_taskinfoservice\\taskservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" |
| | | }, |
| | | { |
| | | "AbsoluteMoniker": "D:0:0:{294E4915-0241-4C8C-BA99-7588B945863A}|WIDESEAWCS_Tasks\\WIDESEAWCS_Tasks.csproj|d:\\git\\shanmeixinnengyuan\\code\\wcs\\wideseawcs_server\\wideseawcs_tasks\\conveyorlinenewjob\\commonconveyorlinenewjob.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", |
| | | "RelativeMoniker": "D:0:0:{294E4915-0241-4C8C-BA99-7588B945863A}|WIDESEAWCS_Tasks\\WIDESEAWCS_Tasks.csproj|solutionrelative:wideseawcs_tasks\\conveyorlinenewjob\\commonconveyorlinenewjob.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" |
| | | }, |
| | | { |
| | | "AbsoluteMoniker": "D:0:0:{861C4D0B-A478-48DB-A0FA-AE70F5BA210A}|WIDESEAWCS_Communicator\\WIDESEAWCS_Communicator.csproj|d:\\git\\shanmeixinnengyuan\\code\\wcs\\wideseawcs_server\\wideseawcs_communicator\\siemens\\siemenss7communicator.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", |
| | | "RelativeMoniker": "D:0:0:{861C4D0B-A478-48DB-A0FA-AE70F5BA210A}|WIDESEAWCS_Communicator\\WIDESEAWCS_Communicator.csproj|solutionrelative:wideseawcs_communicator\\siemens\\siemenss7communicator.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" |
| | | }, |
| | | { |
| | | "AbsoluteMoniker": "D:0:0:{294E4915-0241-4C8C-BA99-7588B945863A}|WIDESEAWCS_Tasks\\WIDESEAWCS_Tasks.csproj|d:\\git\\shanmeixinnengyuan\\code\\wcs\\wideseawcs_server\\wideseawcs_tasks\\conveyorlinenewjob\\conveyorlinedispatchhandler.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", |
| | | "RelativeMoniker": "D:0:0:{294E4915-0241-4C8C-BA99-7588B945863A}|WIDESEAWCS_Tasks\\WIDESEAWCS_Tasks.csproj|solutionrelative:wideseawcs_tasks\\conveyorlinenewjob\\conveyorlinedispatchhandler.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" |
| | | }, |
| | | { |
| | | "AbsoluteMoniker": "D:0:0:{6236BFFF-173D-44A8-9FC3-7C001EA30347}|WIDESEAWCS_QuartzJob\\WIDESEAWCS_QuartzJob.csproj|d:\\git\\shanmeixinnengyuan\\code\\wcs\\wideseawcs_server\\wideseawcs_quartzjob\\service\\routerservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", |
| | | "RelativeMoniker": "D:0:0:{6236BFFF-173D-44A8-9FC3-7C001EA30347}|WIDESEAWCS_QuartzJob\\WIDESEAWCS_QuartzJob.csproj|solutionrelative:wideseawcs_quartzjob\\service\\routerservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" |
| | | }, |
| | | { |
| | | "AbsoluteMoniker": "D:0:0:{9FBC654C-51DE-422D-9E1E-6A38268DE1E2}|WIDESEAWCS_Common\\WIDESEAWCS_Common.csproj|d:\\git\\shanmeixinnengyuan\\code\\wcs\\wideseawcs_server\\wideseawcs_common\\httpenum\\configkey.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", |
| | | "RelativeMoniker": "D:0:0:{9FBC654C-51DE-422D-9E1E-6A38268DE1E2}|WIDESEAWCS_Common\\WIDESEAWCS_Common.csproj|solutionrelative:wideseawcs_common\\httpenum\\configkey.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" |
| | | }, |
| | | { |
| | | "AbsoluteMoniker": "D:0:0:{BFFDD936-2E61-4D3A-ABFE-7CF77FE0B184}|WIDESEAWCS_Core\\WIDESEAWCS_Core.csproj|d:\\git\\shanmeixinnengyuan\\code\\wcs\\wideseawcs_server\\wideseawcs_core\\http\\http\\httpclienthelper.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", |
| | | "RelativeMoniker": "D:0:0:{BFFDD936-2E61-4D3A-ABFE-7CF77FE0B184}|WIDESEAWCS_Core\\WIDESEAWCS_Core.csproj|solutionrelative:wideseawcs_core\\http\\http\\httpclienthelper.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" |
| | | "AbsoluteMoniker": "D:0:0:{294E4915-0241-4C8C-BA99-7588B945863A}|WIDESEAWCS_Tasks\\WIDESEAWCS_Tasks.csproj|d:\\git\\shanmeixinnengyuan\\code\\wcs\\wideseawcs_server\\wideseawcs_tasks\\robotjob\\workflow\\robotworkfloworchestrator.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", |
| | | "RelativeMoniker": "D:0:0:{294E4915-0241-4C8C-BA99-7588B945863A}|WIDESEAWCS_Tasks\\WIDESEAWCS_Tasks.csproj|solutionrelative:wideseawcs_tasks\\robotjob\\workflow\\robotworkfloworchestrator.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" |
| | | }, |
| | | { |
| | | "AbsoluteMoniker": "D:0:0:{294E4915-0241-4C8C-BA99-7588B945863A}|WIDESEAWCS_Tasks\\WIDESEAWCS_Tasks.csproj|d:\\git\\shanmeixinnengyuan\\code\\wcs\\wideseawcs_server\\wideseawcs_tasks\\robotjob\\robotjob.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", |
| | | "RelativeMoniker": "D:0:0:{294E4915-0241-4C8C-BA99-7588B945863A}|WIDESEAWCS_Tasks\\WIDESEAWCS_Tasks.csproj|solutionrelative:wideseawcs_tasks\\robotjob\\robotjob.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" |
| | | }, |
| | | { |
| | | "AbsoluteMoniker": "D:0:0:{7F200FE8-CAF6-4131-BD25-8D438FE0ABAC}|WIDESEAWCS_Model\\WIDESEAWCS_Model.csproj|d:\\git\\shanmeixinnengyuan\\code\\wcs\\wideseawcs_server\\wideseawcs_model\\models\\taskinfo\\dt_task_hty.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", |
| | | "RelativeMoniker": "D:0:0:{7F200FE8-CAF6-4131-BD25-8D438FE0ABAC}|WIDESEAWCS_Model\\WIDESEAWCS_Model.csproj|solutionrelative:wideseawcs_model\\models\\taskinfo\\dt_task_hty.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" |
| | | "AbsoluteMoniker": "D:0:0:{294E4915-0241-4C8C-BA99-7588B945863A}|WIDESEAWCS_Tasks\\WIDESEAWCS_Tasks.csproj|d:\\git\\shanmeixinnengyuan\\code\\wcs\\wideseawcs_server\\wideseawcs_tasks\\robotjob\\robotmessagehandler.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", |
| | | "RelativeMoniker": "D:0:0:{294E4915-0241-4C8C-BA99-7588B945863A}|WIDESEAWCS_Tasks\\WIDESEAWCS_Tasks.csproj|solutionrelative:wideseawcs_tasks\\robotjob\\robotmessagehandler.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" |
| | | } |
| | | ], |
| | | "DocumentGroupContainers": [ |
| | |
| | | "DocumentGroups": [ |
| | | { |
| | | "DockedWidth": 200, |
| | | "SelectedChildIndex": 11, |
| | | "SelectedChildIndex": 3, |
| | | "Children": [ |
| | | { |
| | | "$type": "Bookmark", |
| | |
| | | }, |
| | | { |
| | | "$type": "Document", |
| | | "DocumentIndex": 3, |
| | | "Title": "ConveyorLineDispatchHandler.cs", |
| | | "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Tasks\\ConveyorLineNewJob\\ConveyorLineDispatchHandler.cs", |
| | | "RelativeDocumentMoniker": "WIDESEAWCS_Tasks\\ConveyorLineNewJob\\ConveyorLineDispatchHandler.cs", |
| | | "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Tasks\\ConveyorLineNewJob\\ConveyorLineDispatchHandler.cs", |
| | | "RelativeToolTip": "WIDESEAWCS_Tasks\\ConveyorLineNewJob\\ConveyorLineDispatchHandler.cs", |
| | | "ViewState": "AgIAAIgAAAAAAAAAAAAlwKAAAAAQAAAAAAAAAA==", |
| | | "DocumentIndex": 0, |
| | | "Title": "RobotWorkflowOrchestrator.cs", |
| | | "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Tasks\\RobotJob\\Workflow\\RobotWorkflowOrchestrator.cs", |
| | | "RelativeDocumentMoniker": "WIDESEAWCS_Tasks\\RobotJob\\Workflow\\RobotWorkflowOrchestrator.cs", |
| | | "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Tasks\\RobotJob\\Workflow\\RobotWorkflowOrchestrator.cs*", |
| | | "RelativeToolTip": "WIDESEAWCS_Tasks\\RobotJob\\Workflow\\RobotWorkflowOrchestrator.cs*", |
| | | "ViewState": "AgIAAAAAAAAAAAAAAAAAACMAAABzAAAAAAAAAA==", |
| | | "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", |
| | | "WhenOpened": "2026-03-18T03:31:16.145Z", |
| | | "WhenOpened": "2026-03-18T09:21:38.852Z", |
| | | "EditorCaption": "" |
| | | }, |
| | | { |
| | | "$type": "Document", |
| | | "DocumentIndex": 2, |
| | | "Title": "SiemensS7Communicator.cs", |
| | | "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Communicator\\Siemens\\SiemensS7Communicator.cs", |
| | | "RelativeDocumentMoniker": "WIDESEAWCS_Communicator\\Siemens\\SiemensS7Communicator.cs", |
| | | "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Communicator\\Siemens\\SiemensS7Communicator.cs", |
| | | "RelativeToolTip": "WIDESEAWCS_Communicator\\Siemens\\SiemensS7Communicator.cs", |
| | | "ViewState": "AgIAANkBAAAAAAAAAAAewOsBAAAQAAAAAAAAAA==", |
| | | "Title": "RobotMessageHandler.cs", |
| | | "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Tasks\\RobotJob\\RobotMessageHandler.cs", |
| | | "RelativeDocumentMoniker": "WIDESEAWCS_Tasks\\RobotJob\\RobotMessageHandler.cs", |
| | | "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Tasks\\RobotJob\\RobotMessageHandler.cs", |
| | | "RelativeToolTip": "WIDESEAWCS_Tasks\\RobotJob\\RobotMessageHandler.cs", |
| | | "ViewState": "AgIAABMAAAAAAAAAAAAAwDIAAAAQAAAAAAAAAA==", |
| | | "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", |
| | | "WhenOpened": "2026-03-18T03:25:37.718Z", |
| | | "EditorCaption": "" |
| | | "WhenOpened": "2026-03-18T09:10:02.533Z" |
| | | }, |
| | | { |
| | | "$type": "Document", |
| | | "DocumentIndex": 4, |
| | | "Title": "RouterService.cs", |
| | | "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_QuartzJob\\Service\\RouterService.cs", |
| | | "RelativeDocumentMoniker": "WIDESEAWCS_QuartzJob\\Service\\RouterService.cs", |
| | | "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_QuartzJob\\Service\\RouterService.cs", |
| | | "RelativeToolTip": "WIDESEAWCS_QuartzJob\\Service\\RouterService.cs", |
| | | "ViewState": "AgIAACYBAAAAAAAAAAAlwDkBAAAIAAAAAAAAAA==", |
| | | "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", |
| | | "WhenOpened": "2026-03-17T07:55:50.917Z", |
| | | "EditorCaption": "" |
| | | }, |
| | | { |
| | | "$type": "Document", |
| | | "DocumentIndex": 7, |
| | | "DocumentIndex": 1, |
| | | "Title": "RobotJob.cs", |
| | | "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Tasks\\RobotJob\\RobotJob.cs", |
| | | "RelativeDocumentMoniker": "WIDESEAWCS_Tasks\\RobotJob\\RobotJob.cs", |
| | | "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Tasks\\RobotJob\\RobotJob.cs", |
| | | "RelativeToolTip": "WIDESEAWCS_Tasks\\RobotJob\\RobotJob.cs", |
| | | "ViewState": "AgIAADwAAAAAAAAAAADwv1UAAAAxAAAAAAAAAA==", |
| | | "ViewState": "AgIAAEoAAAAAAAAAAAAAwGAAAAA7AAAAAAAAAA==", |
| | | "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", |
| | | "WhenOpened": "2026-03-17T02:11:28.683Z" |
| | | }, |
| | | { |
| | | "$type": "Document", |
| | | "DocumentIndex": 8, |
| | | "Title": "Dt_Task_Hty.cs", |
| | | "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Model\\Models\\TaskInfo\\Dt_Task_Hty.cs", |
| | | "RelativeDocumentMoniker": "WIDESEAWCS_Model\\Models\\TaskInfo\\Dt_Task_Hty.cs", |
| | | "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Model\\Models\\TaskInfo\\Dt_Task_Hty.cs", |
| | | "RelativeToolTip": "WIDESEAWCS_Model\\Models\\TaskInfo\\Dt_Task_Hty.cs", |
| | | "ViewState": "AgIAAAsAAAAAAAAAAAAwwAsAAAAmAAAAAAAAAA==", |
| | | "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", |
| | | "WhenOpened": "2026-03-13T02:59:26.007Z" |
| | | }, |
| | | { |
| | | "$type": "Document", |
| | | "DocumentIndex": 5, |
| | | "Title": "ConfigKey.cs", |
| | | "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Common\\HttpEnum\\ConfigKey.cs", |
| | | "RelativeDocumentMoniker": "WIDESEAWCS_Common\\HttpEnum\\ConfigKey.cs", |
| | | "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Common\\HttpEnum\\ConfigKey.cs", |
| | | "RelativeToolTip": "WIDESEAWCS_Common\\HttpEnum\\ConfigKey.cs", |
| | | "ViewState": "AgIAABkAAAAAAAAAAAAawCwAAAAIAAAAAAAAAA==", |
| | | "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", |
| | | "WhenOpened": "2026-03-12T01:55:15.084Z" |
| | | }, |
| | | { |
| | | "$type": "Document", |
| | | "DocumentIndex": 6, |
| | | "Title": "HttpClientHelper.cs", |
| | | "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Core\\Http\\HTTP\\HttpClientHelper.cs", |
| | | "RelativeDocumentMoniker": "WIDESEAWCS_Core\\Http\\HTTP\\HttpClientHelper.cs", |
| | | "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Core\\Http\\HTTP\\HttpClientHelper.cs", |
| | | "RelativeToolTip": "WIDESEAWCS_Core\\Http\\HTTP\\HttpClientHelper.cs", |
| | | "ViewState": "AgIAABAAAAAAAAAAAAAAAB8AAAASAAAAAAAAAA==", |
| | | "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", |
| | | "WhenOpened": "2026-03-12T01:54:05.934Z" |
| | | }, |
| | | { |
| | | "$type": "Document", |
| | | "DocumentIndex": 1, |
| | | "Title": "CommonConveyorLineNewJob.cs", |
| | | "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Tasks\\ConveyorLineNewJob\\CommonConveyorLineNewJob.cs", |
| | | "RelativeDocumentMoniker": "WIDESEAWCS_Tasks\\ConveyorLineNewJob\\CommonConveyorLineNewJob.cs", |
| | | "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Tasks\\ConveyorLineNewJob\\CommonConveyorLineNewJob.cs", |
| | | "RelativeToolTip": "WIDESEAWCS_Tasks\\ConveyorLineNewJob\\CommonConveyorLineNewJob.cs", |
| | | "ViewState": "AgIAADwAAAAAAAAAAAApwFEAAAAcAAAAAAAAAA==", |
| | | "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", |
| | | "WhenOpened": "2026-03-11T09:29:57.419Z", |
| | | "EditorCaption": "" |
| | | }, |
| | | { |
| | | "$type": "Document", |
| | | "DocumentIndex": 0, |
| | | "Title": "TaskService.cs", |
| | | "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_TaskInfoService\\TaskService.cs", |
| | | "RelativeDocumentMoniker": "WIDESEAWCS_TaskInfoService\\TaskService.cs", |
| | | "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_TaskInfoService\\TaskService.cs", |
| | | "RelativeToolTip": "WIDESEAWCS_TaskInfoService\\TaskService.cs", |
| | | "ViewState": "AgIAAG4CAAAAAAAAAAAAAIICAAB9AAAAAAAAAA==", |
| | | "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", |
| | | "WhenOpened": "2026-03-11T09:01:01.549Z", |
| | | "EditorCaption": "" |
| | | "WhenOpened": "2026-03-18T08:56:41.452Z" |
| | | } |
| | | ] |
| | | } |
| | |
| | | /// <summary> |
| | | /// ä»»å¡ç¶æ |
| | | /// </summary> |
| | | public int TaskState { get; set; } |
| | | public int TaskStatus { get; set; } |
| | | |
| | | /// <summary> |
| | | /// èµ·ç¹ |
| | |
| | | /// <summary> |
| | | /// æºå¨äººä»»å¡ID |
| | | /// </summary> |
| | | [SugarColumn(IsPrimaryKey = true, ColumnDescription = "æºå¨äººä»»å¡ID")] |
| | | [SugarColumn(IsPrimaryKey = true, IsIdentity = true, ColumnDescription = "æºå¨äººä»»å¡ID")] |
| | | public int RobotTaskId { get; set; } |
| | | |
| | | /// <summary> |
| | |
| | | "110681": "Split" |
| | | }, |
| | | "AddressRoadwayMap": { // 对åºè®¾å¤å°åæ å° |
| | | "11001": "HPRB1", |
| | | "11010": "HPRB1", |
| | | "11068": "ZYRB1", |
| | | "10010": "HPRB1", |
| | | "10030": "HPRB1" |
| | | "11001": "æ¢çæºæ¢°æ", |
| | | "11010": "æ¢çæºæ¢°æ", |
| | | "11068": "注液ç»çæºæ¢°æ", |
| | | "10010": "æ¢çæºæ¢°æ", |
| | | "10030": "æ¢çæºæ¢°æ" |
| | | }, |
| | | "AddressSourceLineNoMap": { // 对åºè¾é线ç¼å·å°åæ å° |
| | | "11001": "10010", |
| | |
| | | RobotSourceAddressPalletCode = stockDTO.SourcePalletNo, |
| | | RobotTargetAddressPalletCode = stockDTO.TargetPalletNo, |
| | | RobotTaskType = taskDTO.TaskType, |
| | | RobotTaskState = taskDTO.TaskState, |
| | | RobotTaskState = taskDTO.TaskStatus, |
| | | RobotGrade = taskDTO.Grade, |
| | | Creater = "WMS" |
| | | Creater = "WMS", |
| | | RobotTaskTotalNum = 0, |
| | | }; |
| | | |
| | | BaseDal.AddData(task); |
| | |
| | | Roadway = "SC01", |
| | | SourceAddress = sourceAddress, |
| | | TargetAddress = "SC01", |
| | | TaskState = (int)TaskInStatusEnum.InNew, |
| | | TaskStatus = (int)TaskInStatusEnum.InNew, |
| | | Id = 0, |
| | | TaskType = (int)TaskInboundTypeEnum.Inbound |
| | | }; |
| | |
| | | task.Modifier = "System"; |
| | | if (task.TaskStatus == (int)TaskOutStatusEnum.Line_OutFinish) |
| | | { |
| | | BaseDal.DeleteAndMoveIntoHty(task, OperateTypeEnum.èªå¨å é¤); |
| | | BaseDal.DeleteAndMoveIntoHty(task, OperateTypeEnum.èªå¨å®æ); |
| | | } |
| | | else |
| | | { |
| | |
| | | { |
| | | await HandlePickFinishedStateAsync(task, ipAddress); |
| | | } |
| | | else if ((latestState.CurrentAction == "PutFinished" || latestState.CurrentAction == "AllPutFinished") |
| | | else if ((latestState.CurrentAction == "PutFinished" || latestState.CurrentAction == "AllPutFinished" || latestState.CurrentAction == null) |
| | | && latestState.OperStatus == "Homed" |
| | | && latestState.RobotArmObject == 0 |
| | | && (task.RobotTaskState == TaskRobotStatusEnum.RobotPutFinish.GetHashCode() |
| | |
| | | "RelativeMoniker": "D:0:0:{7D7534D4-51D9-46DC-A6B7-6430042F4E12}|WIDESEA_TaskInfoService\\WIDESEA_TaskInfoService.csproj|solutionrelative:widesea_taskinfoservice\\taskservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" |
| | | }, |
| | | { |
| | | "AbsoluteMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_wmsserver\\controllers\\taskinfo\\taskcontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", |
| | | "RelativeMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|solutionrelative:widesea_wmsserver\\controllers\\taskinfo\\taskcontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" |
| | | }, |
| | | { |
| | | "AbsoluteMoniker": "D:0:0:{929DF936-042C-4EEC-8722-A831FC2F0AEA}|WIDESEA_DTO\\WIDESEA_DTO.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_dto\\task\\wmstaskdto.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", |
| | | "RelativeMoniker": "D:0:0:{929DF936-042C-4EEC-8722-A831FC2F0AEA}|WIDESEA_DTO\\WIDESEA_DTO.csproj|solutionrelative:widesea_dto\\task\\wmstaskdto.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" |
| | | }, |
| | | { |
| | | "AbsoluteMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_wmsserver\\controllers\\taskinfo\\taskcontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", |
| | | "RelativeMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|solutionrelative:widesea_wmsserver\\controllers\\taskinfo\\taskcontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" |
| | | }, |
| | | { |
| | | "AbsoluteMoniker": "D:0:0:{AF8F3D65-1D75-4B8F-AFD9-4150E591C44D}|WIDESEA_Common\\WIDESEA_Common.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_common\\taskenum\\tasktypeenum.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", |
| | |
| | | }, |
| | | { |
| | | "$type": "Document", |
| | | "DocumentIndex": 2, |
| | | "DocumentIndex": 1, |
| | | "Title": "WMSTaskDTO.cs", |
| | | "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_DTO\\Task\\WMSTaskDTO.cs", |
| | | "RelativeDocumentMoniker": "WIDESEA_DTO\\Task\\WMSTaskDTO.cs", |
| | | "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_DTO\\Task\\WMSTaskDTO.cs", |
| | | "RelativeToolTip": "WIDESEA_DTO\\Task\\WMSTaskDTO.cs", |
| | | "ViewState": "AgIAAAYAAAAAAAAAAAAuwB8AAAArAAAAAAAAAA==", |
| | | "ViewState": "AgIAABgAAAAAAAAAAAAIwCkAAAAdAAAAAAAAAA==", |
| | | "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", |
| | | "WhenOpened": "2026-03-17T07:42:09.48Z" |
| | | "WhenOpened": "2026-03-17T07:42:09.48Z", |
| | | "EditorCaption": "" |
| | | }, |
| | | { |
| | | "$type": "Document", |
| | |
| | | }, |
| | | { |
| | | "$type": "Document", |
| | | "DocumentIndex": 1, |
| | | "DocumentIndex": 2, |
| | | "Title": "TaskController.cs", |
| | | "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\Controllers\\TaskInfo\\TaskController.cs", |
| | | "RelativeDocumentMoniker": "WIDESEA_WMSServer\\Controllers\\TaskInfo\\TaskController.cs", |
| | | "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\Controllers\\TaskInfo\\TaskController.cs", |
| | | "RelativeToolTip": "WIDESEA_WMSServer\\Controllers\\TaskInfo\\TaskController.cs", |
| | | "ViewState": "AgIAAHgAAAAAAAAAAADgv5cAAAAjAAAAAAAAAA==", |
| | | "ViewState": "AgIAAIoAAAAAAAAAAAAhwJsAAAAIAAAAAAAAAA==", |
| | | "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", |
| | | "WhenOpened": "2026-03-13T02:00:31.089Z", |
| | | "EditorCaption": "" |
| | |
| | | "RelativeDocumentMoniker": "WIDESEA_TaskInfoService\\TaskService.cs", |
| | | "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_TaskInfoService\\TaskService.cs", |
| | | "RelativeToolTip": "WIDESEA_TaskInfoService\\TaskService.cs", |
| | | "ViewState": "AgIAAGkCAAAAAAAAAAAkwHoCAAAIAAAAAAAAAA==", |
| | | "ViewState": "AgIAAGkCAAAAAAAAAAAkwHgCAAAWAAAAAAAAAA==", |
| | | "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", |
| | | "WhenOpened": "2026-02-06T06:34:59.734Z", |
| | | "EditorCaption": "" |
| | |
| | | "RelativeMoniker": "D:0:0:{7D7534D4-51D9-46DC-A6B7-6430042F4E12}|WIDESEA_TaskInfoService\\WIDESEA_TaskInfoService.csproj|solutionrelative:widesea_taskinfoservice\\taskservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" |
| | | }, |
| | | { |
| | | "AbsoluteMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_wmsserver\\controllers\\taskinfo\\taskcontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", |
| | | "RelativeMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|solutionrelative:widesea_wmsserver\\controllers\\taskinfo\\taskcontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" |
| | | }, |
| | | { |
| | | "AbsoluteMoniker": "D:0:0:{929DF936-042C-4EEC-8722-A831FC2F0AEA}|WIDESEA_DTO\\WIDESEA_DTO.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_dto\\task\\wmstaskdto.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", |
| | | "RelativeMoniker": "D:0:0:{929DF936-042C-4EEC-8722-A831FC2F0AEA}|WIDESEA_DTO\\WIDESEA_DTO.csproj|solutionrelative:widesea_dto\\task\\wmstaskdto.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" |
| | | }, |
| | | { |
| | | "AbsoluteMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_wmsserver\\controllers\\taskinfo\\taskcontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", |
| | | "RelativeMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|solutionrelative:widesea_wmsserver\\controllers\\taskinfo\\taskcontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" |
| | | }, |
| | | { |
| | | "AbsoluteMoniker": "D:0:0:{AF8F3D65-1D75-4B8F-AFD9-4150E591C44D}|WIDESEA_Common\\WIDESEA_Common.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_common\\taskenum\\tasktypeenum.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", |
| | |
| | | }, |
| | | { |
| | | "$type": "Document", |
| | | "DocumentIndex": 2, |
| | | "DocumentIndex": 1, |
| | | "Title": "WMSTaskDTO.cs", |
| | | "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_DTO\\Task\\WMSTaskDTO.cs", |
| | | "RelativeDocumentMoniker": "WIDESEA_DTO\\Task\\WMSTaskDTO.cs", |
| | | "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_DTO\\Task\\WMSTaskDTO.cs", |
| | | "RelativeToolTip": "WIDESEA_DTO\\Task\\WMSTaskDTO.cs", |
| | | "ViewState": "AgIAAAYAAAAAAAAAAAAuwB8AAAArAAAAAAAAAA==", |
| | | "ViewState": "AgIAABgAAAAAAAAAAAAIwCkAAAAdAAAAAAAAAA==", |
| | | "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", |
| | | "WhenOpened": "2026-03-17T07:42:09.48Z" |
| | | }, |
| | |
| | | }, |
| | | { |
| | | "$type": "Document", |
| | | "DocumentIndex": 1, |
| | | "DocumentIndex": 2, |
| | | "Title": "TaskController.cs", |
| | | "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\Controllers\\TaskInfo\\TaskController.cs", |
| | | "RelativeDocumentMoniker": "WIDESEA_WMSServer\\Controllers\\TaskInfo\\TaskController.cs", |
| | | "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\Controllers\\TaskInfo\\TaskController.cs", |
| | | "RelativeToolTip": "WIDESEA_WMSServer\\Controllers\\TaskInfo\\TaskController.cs", |
| | | "ViewState": "AgIAAHgAAAAAAAAAAADgv5cAAAAjAAAAAAAAAA==", |
| | | "ViewState": "AgIAAIoAAAAAAAAAAAAhwJsAAAAIAAAAAAAAAA==", |
| | | "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", |
| | | "WhenOpened": "2026-03-13T02:00:31.089Z", |
| | | "EditorCaption": "" |
| | | "WhenOpened": "2026-03-13T02:00:31.089Z" |
| | | }, |
| | | { |
| | | "$type": "Document", |
| | |
| | | "RelativeToolTip": "WIDESEA_BasicService\\LocationInfoService.cs", |
| | | "ViewState": "AgIAAFcAAAAAAAAAAAAAwGgAAAARAAAAAAAAAA==", |
| | | "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", |
| | | "WhenOpened": "2026-03-12T02:05:14.224Z", |
| | | "EditorCaption": "" |
| | | "WhenOpened": "2026-03-12T02:05:14.224Z" |
| | | }, |
| | | { |
| | | "$type": "Document", |
| | |
| | | "RelativeDocumentMoniker": "WIDESEA_TaskInfoService\\TaskService.cs", |
| | | "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_TaskInfoService\\TaskService.cs", |
| | | "RelativeToolTip": "WIDESEA_TaskInfoService\\TaskService.cs", |
| | | "ViewState": "AgIAAKwCAAAAAAAAAAAgwL8CAAAAAAAAAAAAAA==", |
| | | "ViewState": "AgIAAAAAAAAAAAAAAAAAAHgCAAAWAAAAAAAAAA==", |
| | | "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", |
| | | "WhenOpened": "2026-02-06T06:34:59.734Z", |
| | | "EditorCaption": "" |
| | |
| | | /// </summary> |
| | | public async Task<WebResponseContent> ChangePalletAsync(StockDTO stock) |
| | | { |
| | | |
| | | WebResponseContent content = new WebResponseContent(); |
| | | if (stock == null || |
| | | string.IsNullOrWhiteSpace(stock.TargetPalletNo) || |
| | |
| | | return content.Error("æºæçå·ä¸ç®æ æçå·ç¸å"); |
| | | } |
| | | |
| | | var sourceStock = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.SourcePalletNo); |
| | | var sourceStock = await StockInfoService.Repository.QueryDataNavFirstAsync(s => s.PalletCode == stock.SourcePalletNo); |
| | | if (sourceStock == null) return content.Error("æºæçä¸åå¨"); |
| | | |
| | | var targetStock = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.TargetPalletNo); |
| | |
| | | targetStock.Id = newId; |
| | | } |
| | | |
| | | var serialNumbers = stock.Details.Select(d => d.CellBarcode).Distinct().ToList(); |
| | | var serialNumbers = stock.Details.Select(d => d.Channel).Distinct().ToList(); |
| | | if (!serialNumbers.Any()) return content.Error("æªæ¾å°ææçåºåå·"); |
| | | |
| | | var detailEntities = StockInfoDetailService.Repository.QueryData( |
| | | d => d.StockId == sourceStock.Id && serialNumbers.Contains(d.SerialNumber)); |
| | | d => d.StockId == sourceStock.Id && serialNumbers.Contains(d.InboundOrderRowNo)); |
| | | if (!detailEntities.Any()) return content.Error("æªæ¾å°ææçåºåæç»"); |
| | | |
| | | if (await StockInfoDetail_HtyService.Repository.AddDataAsync(CreateDetailHistory(detailEntities, "æ¢ç")) <= 0) |
| | |
| | | Creater = s.Creater, |
| | | CreateDate = s.CreateDate, |
| | | Modifier = s.Modifier, |
| | | ModifyDate = s.ModifyDate |
| | | ModifyDate = s.ModifyDate, |
| | | LocationId = s.LocationId, |
| | | OutboundDate = s.OutboundDate |
| | | }).ToList(); |
| | | } |
| | | } |