wanshenmean
2026-03-19 c493779a8504fe1eb548c865ff268a7f7436ec01
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 文件)。
已添加4个文件
已修改21个文件
2448 ■■■■ 文件已修改
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Controllers/RobotClientsController.cs 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Program.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Services/RobotClientManager.cs 588 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/App.vue 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/api/index.ts 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/router/index.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/style.css 145 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/types/index.ts 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/CreateView.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/DetailsView.vue 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/EditView.vue 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/HomeView.vue 334 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/ProtocolTemplatesView.vue 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/RobotClientsView.vue 452 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/.vs/WIDESEAWCS_Server/v18/DocumentLayout.json 146 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_DTO/TaskInfo/WMSTaskDTO.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Model/Models/TaskInfo/Dt_RobotTask.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/appsettings.json 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoService/RobotTaskService.cs 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoService/TaskService.cs 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotWorkflowOrchestrator.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/v18/DocumentLayout.backup.json 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/v18/DocumentLayout.json 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockSerivce.cs 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目资料/设备协议/机械手协议/~$交互流程表(1).xlsx 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Controllers/RobotClientsController.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,126 @@
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 = "清空接收消息失败" });
        }
    }
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Program.cs
@@ -46,6 +46,7 @@
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>();
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Services/RobotClientManager.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,588 @@
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;
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/App.vue
@@ -1,94 +1,169 @@
<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-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-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="app-main">
      <el-main class="admin-main">
        <div class="content-shell">
      <router-view />
        </div>
    </el-main>
    <el-footer class="app-footer">
      <span>&copy; 2026 - S7 PLC Simulator Management UI</span>
    </el-footer>
    </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>
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/api/index.ts
@@ -1,9 +1,12 @@
import axios from 'axios'
import axios from 'axios'
import type {
  InstanceListItem,
  InstanceState,
  InstanceConfig,
  ProtocolTemplate
  ProtocolTemplate,
  RobotClientStartRequest,
  RobotClientSendRequest,
  RobotClientStatusResponse
} from '../types'
const api = axios.create({
@@ -19,7 +22,7 @@
  return response.data
}
// èŽ·å–æŒ‡å®šå®žä¾‹çŠ¶æ€
// èŽ·å–å®žä¾‹çŠ¶æ€
export async function getInstance(id: string): Promise<InstanceState | null> {
  try {
    const response = await api.get<InstanceState>('/SimulatorInstances/GetInstance', {
@@ -175,4 +178,35 @@
  }
}
// èŽ·å–æœºæ¢°æ‰‹æœåŠ¡ç«¯è¿è¡ŒçŠ¶æ€ï¼ˆåŒ…å«å¤šå®žä¾‹å’ŒæŽ¥æ”¶æ¶ˆæ¯æ—¥å¿—ï¼‰
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
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/router/index.ts
@@ -28,6 +28,11 @@
    path: '/protocol-templates',
    name: 'protocolTemplates',
    component: () => import('../views/ProtocolTemplatesView.vue')
  },
  {
    path: '/robot-clients',
    name: 'robotClients',
    component: () => import('../views/RobotClientsView.vue')
  }
]
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/style.css
@@ -1,50 +1,135 @@
/* 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;
}
@@ -58,30 +143,32 @@
  }
}
/* 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;
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/types/index.ts
@@ -112,3 +112,58 @@
  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[]
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/CreateView.vue
@@ -1,5 +1,5 @@
<template>
  <div>
  <div class="admin-page">
    <div class="page-header">
      <div class="header-left">
        <h2>
@@ -16,7 +16,7 @@
    <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">
@@ -315,27 +315,6 @@
</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;
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/DetailsView.vue
@@ -1,5 +1,6 @@
<template>
  <div
    class="admin-page"
    v-loading.fullscreen.lock="startActionLoading"
    element-loading-text="正在启动实例,请稍候..."
  >
@@ -31,9 +32,17 @@
        </el-button>
      </div>
      <el-row :gutter="20" class="status-cards">
      <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">
              <el-card shadow="hover" class="status-card panel-card">
            <el-statistic title="状态">
              <template #default>
                <el-tag :type="getStatusTagType(instance.status)" size="large">
@@ -44,7 +53,7 @@
          </el-card>
        </el-col>
        <el-col :xs="12" :sm="6">
          <el-card shadow="hover" class="status-card">
              <el-card shadow="hover" class="status-card panel-card">
            <el-statistic title="连接客户端" :value="instance.clientCount">
              <template #suffix>
                <el-icon><User /></el-icon>
@@ -53,22 +62,34 @@
          </el-card>
        </el-col>
        <el-col :xs="12" :sm="6">
          <el-card shadow="hover" class="status-card">
              <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">
              <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>
@@ -79,17 +100,18 @@
          <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>
@@ -98,6 +120,7 @@
          <el-button
            v-if="instance.status === 'Running'"
            type="warning"
            @click="handleStop"
          >
            <el-icon><VideoPause /></el-icon>
@@ -114,13 +137,15 @@
        </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>
@@ -149,25 +174,27 @@
                        :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'"
@@ -181,17 +208,18 @@
                                @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"
@@ -205,26 +233,29 @@
                            </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'"
@@ -238,17 +269,18 @@
                            @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"
@@ -262,6 +294,8 @@
                        </template>
                      </el-table-column>
                    </el-table>
                    </div>
                    </template>
                    <div v-else class="text-muted">当前DB块无字段映射</div>
                    <div class="card-header-title field-title">原始数据</div>
@@ -300,6 +334,10 @@
          />
        </div>
      </el-card>
        </el-col>
      </el-row>
        </div>
      </section>
    </div>
  </div>
</template>
@@ -1065,48 +1103,8 @@
</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 {
@@ -1119,13 +1117,17 @@
}
.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 {
@@ -1142,7 +1144,7 @@
.db-content {
  margin: 0;
  max-height: 180px;
  max-height: 168px;
  overflow: auto;
  white-space: pre-wrap;
  font-family: Consolas, Monaco, 'Courier New', monospace;
@@ -1168,11 +1170,11 @@
}
.db-tabs {
  margin-top: 8px;
  margin-top: 10px;
}
.data-view-tabs {
  margin-top: 8px;
  margin-top: 10px;
}
.db-raw-toolbar {
@@ -1189,4 +1191,35 @@
  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>
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/EditView.vue
@@ -1,5 +1,5 @@
<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>
@@ -40,7 +40,7 @@
      <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">
@@ -385,46 +385,6 @@
</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;
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/HomeView.vue
@@ -1,5 +1,6 @@
<template>
  <div
    class="admin-page"
    v-loading.fullscreen.lock="startActionLoading"
    element-loading-text="正在启动实例,请稍候..."
  >
@@ -12,16 +13,43 @@
        <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">
@@ -35,31 +63,62 @@
    </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">
    <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>
              <el-tag :type="getStatusTagType(instance.status)">
                <span class="instance-name">{{ instance.name || '未命名实例' }}</span>
                <span class="instance-sub">PLC</span>
              </div>
            </div>
            <el-tag :type="getStatusTagType(instance.status)" effect="dark" round>
                {{ getStatusText(instance.status) }}
              </el-tag>
            </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="客户端">
          <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 }}
              </el-descriptions-item>
              <el-descriptions-item v-if="instance.startTime" label="启动时间" :span="2">
                {{ formatDate(instance.startTime) }}
              </el-descriptions-item>
            </el-descriptions>
              </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"
@@ -70,10 +129,9 @@
            >
              {{ instance.errorMessage }}
            </el-alert>
          </div>
          <template #footer>
            <div class="card-footer">
            <div class="main-actions">
              <el-button
                v-if="instance.status === 'Running'"
                type="warning"
@@ -98,14 +156,16 @@
                <el-icon><Edit /></el-icon>
                ç¼–辑
              </el-button>
              <el-button type="danger" @click="handleDelete(instance.instanceId)">
            </div>
            <el-button class="delete-btn" type="danger" @click="handleDelete(instance.instanceId)">
                <el-icon><Delete /></el-icon>
              </el-button>
            </div>
          </template>
        </el-card>
      </el-col>
    </el-row>
      </div>
    </section>
  </div>
</template>
@@ -246,106 +306,220 @@
</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);
.summary-title {
  color: #64748b;
  font-size: 13px;
  margin-bottom: 6px;
  }
  to {
    transform: rotate(360deg);
.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>
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/ProtocolTemplatesView.vue
@@ -1,5 +1,5 @@
<template>
  <div>
  <div class="admin-page">
    <div class="page-header">
      <div>
        <h2>协议模板管理</h2>
@@ -8,6 +8,7 @@
      <el-button type="primary" @click="openNewTemplate">新建模板</el-button>
    </div>
    <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" />
@@ -22,6 +23,7 @@
        </template>
      </el-table-column>
    </el-table>
    </el-card>
    <el-dialog v-model="dialogVisible" title="协议模板" width="96vw" top="2vh" class="protocol-dialog">
      <el-form :model="editing" label-width="100px">
@@ -187,18 +189,6 @@
</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;
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/RobotClientsView.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,452 @@
<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>
Code/WCS/WIDESEAWCS_Server/.vs/WIDESEAWCS_Server/v18/DocumentLayout.json
@@ -3,40 +3,16 @@
  "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": [
@@ -46,7 +22,7 @@
      "DocumentGroups": [
        {
          "DockedWidth": 200,
          "SelectedChildIndex": 11,
          "SelectedChildIndex": 3,
          "Children": [
            {
              "$type": "Bookmark",
@@ -62,116 +38,40 @@
            },
            {
              "$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"
            }
          ]
        }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_DTO/TaskInfo/WMSTaskDTO.cs
@@ -36,7 +36,7 @@
        /// <summary>
        /// ä»»åŠ¡çŠ¶æ€
        /// </summary>
        public int TaskState { get; set; }
        public int TaskStatus { get; set; }
        /// <summary>
        /// èµ·ç‚¹
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Model/Models/TaskInfo/Dt_RobotTask.cs
@@ -18,7 +18,7 @@
        /// <summary>
        /// æœºå™¨äººä»»åŠ¡ID
        /// </summary>
        [SugarColumn(IsPrimaryKey = true, ColumnDescription = "机器人任务ID")]
        [SugarColumn(IsPrimaryKey = true, IsIdentity = true, ColumnDescription = "机器人任务ID")]
        public int RobotTaskId { get; set; }
        /// <summary>
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/appsettings.json
@@ -72,11 +72,11 @@
      "110681": "Split"
    },
    "AddressRoadwayMap": { // å¯¹åº”设备地址映射
      "11001": "HPRB1",
      "11010": "HPRB1",
      "11068": "ZYRB1",
      "10010": "HPRB1",
      "10030": "HPRB1"
      "11001": "换盘机械手",
      "11010": "换盘机械手",
      "11068": "注液组盘机械手",
      "10010": "换盘机械手",
      "10030": "换盘机械手"
    },
    "AddressSourceLineNoMap": { // å¯¹åº”输送线编号地址映射
      "11001": "10010",
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoService/RobotTaskService.cs
@@ -87,9 +87,10 @@
                    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);
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoService/TaskService.cs
@@ -144,7 +144,7 @@
                    Roadway = "SC01",
                    SourceAddress = sourceAddress,
                    TargetAddress = "SC01",
                    TaskState = (int)TaskInStatusEnum.InNew,
                    TaskStatus = (int)TaskInStatusEnum.InNew,
                    Id = 0,
                    TaskType = (int)TaskInboundTypeEnum.Inbound
                };
@@ -399,7 +399,7 @@
            task.Modifier = "System";
            if (task.TaskStatus == (int)TaskOutStatusEnum.Line_OutFinish)
            {
                BaseDal.DeleteAndMoveIntoHty(task, OperateTypeEnum.自动删除);
                BaseDal.DeleteAndMoveIntoHty(task, OperateTypeEnum.自动完成);
            }
            else
            {
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotWorkflowOrchestrator.cs
@@ -39,7 +39,7 @@
                {
                    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()
Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/v18/DocumentLayout.backup.json
@@ -7,12 +7,12 @@
      "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}",
@@ -86,15 +86,16 @@
            },
            {
              "$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",
@@ -134,13 +135,13 @@
            },
            {
              "$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": ""
@@ -262,7 +263,7 @@
              "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": ""
Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/v18/DocumentLayout.json
@@ -7,12 +7,12 @@
      "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}",
@@ -86,13 +86,13 @@
            },
            {
              "$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"
            },
@@ -134,16 +134,15 @@
            },
            {
              "$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",
@@ -215,8 +214,7 @@
              "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",
@@ -262,7 +260,7 @@
              "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": ""
Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockSerivce.cs
@@ -101,6 +101,7 @@
        /// </summary>
        public async Task<WebResponseContent> ChangePalletAsync(StockDTO stock)
        {
            WebResponseContent content = new WebResponseContent();
            if (stock == null ||
                string.IsNullOrWhiteSpace(stock.TargetPalletNo) ||
@@ -110,7 +111,7 @@
                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);
@@ -131,11 +132,11 @@
                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)
@@ -253,7 +254,9 @@
                Creater = s.Creater,
                CreateDate = s.CreateDate,
                Modifier = s.Modifier,
                ModifyDate = s.ModifyDate
                ModifyDate = s.ModifyDate,
                LocationId = s.LocationId,
                OutboundDate = s.OutboundDate
            }).ToList();
        }
    }
ÏîÄ¿×ÊÁÏ/É豸ЭÒé/»úеÊÖЭÒé/~$½»»¥Á÷³Ì±í(1).xlsx
Binary files differ