wanshenmean
2026-03-13 58376d519aeb76ef78d38b737c0c57f8d982fb52
feat: 完成S7 PLC模拟器完整实现

实现基于HSL Communication库的西门子S7 PLC模拟器系统,用于WCS开发测试。

核心功能:
- 多实例管理:支持创建和管理多个独立的S7服务器实例
- 内存模拟:支持M/DB/I/Q/T/C所有常用地址区域
- 地址解析:支持"M100"、"DB1.DBD0"、"I0.0"、"T1"、"C1"等S7地址格式
- 数据持久化:配置和内存数据自动保存到本地文件
- Web管理界面:提供直观的Web UI进行实例管理
- 实时监控:显示连接状态、请求数、客户端信息等

项目结构:
- WIDESEAWCS_S7Simulator.Core - 核心领域层
* Memory/ - 内存区域实现(I/Q/T/C/M/DB regions)
* Persistence/ - 文件持久化服务
* Server/ - S7服务器实例(HSL集成)
* Manager/ - 实例管理器
- WIDESEAWCS_S7Simulator.Server - Web API服务器
* Controllers/ - REST API控制器
- WIDESEAWCS_S7Simulator.Web - Web管理界面
* Pages/ - Razor Pages(列表/创建/编辑/详情)
- WIDESEAWCS_S7Simulator.UnitTests - 单元测试

技术细节:
- 线程安全的内存操作(ReaderWriterLockSlim)
- 大端字节序(S7 PLC规范)
- 完整的Dispose模式
- 原子性写入操作(文件持久化)
- 异步API设计
- Bootstrap 5 + Alpine.js 前端

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
已添加13个文件
已修改7个文件
3567 ■■■■■ 文件已修改
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Interfaces/IS7ServerInstance.cs 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Interfaces/ISimulatorInstanceManager.cs 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Manager/SimulatorInstanceManager.cs 474 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Server/S7ServerInstance.cs 444 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Controllers/ClientsController.cs 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Controllers/MemoryController.cs 219 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Controllers/SimulatorInstancesController.cs 375 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Create.cshtml 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Create.cshtml.cs 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Details.cshtml 338 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Details.cshtml.cs 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Edit.cshtml 245 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Edit.cshtml.cs 182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Index.cshtml 224 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Index.cshtml.cs 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Shared/_Layout.cshtml 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Shared/_ValidationScriptsPartial.cshtml 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/_ViewImports.cshtml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/wwwroot/css/site.css 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/wwwroot/js/site.js 190 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Interfaces/IS7ServerInstance.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
using WIDESEAWCS_S7Simulator.Core.Entities;
namespace WIDESEAWCS_S7Simulator.Core.Interfaces
{
    /// <summary>
    /// S7服务器实例接口
    /// ç®¡ç†å•个S7 PLC仿真实例的生命周期
    /// </summary>
    public interface IS7ServerInstance : IDisposable
    {
        /// <summary>
        /// å®žä¾‹é…ç½®
        /// </summary>
        InstanceConfig Config { get; }
        /// <summary>
        /// å®žä¾‹çŠ¶æ€
        /// </summary>
        InstanceState State { get; }
        /// <summary>
        /// å†…存存储
        /// </summary>
        IMemoryStore MemoryStore { get; }
        /// <summary>
        /// å¯åЍS7服务器
        /// </summary>
        /// <returns>启动是否成功</returns>
        bool Start();
        /// <summary>
        /// åœæ­¢S7服务器
        /// </summary>
        void Stop();
        /// <summary>
        /// é‡å¯S7服务器
        /// </summary>
        /// <returns>重启是否成功</returns>
        bool Restart();
        /// <summary>
        /// èŽ·å–å®žä¾‹çŠ¶æ€å¿«ç…§
        /// </summary>
        /// <returns>状态快照</returns>
        InstanceState GetState();
        /// <summary>
        /// æ¸…空所有内存
        /// </summary>
        void ClearMemory();
        /// <summary>
        /// å¯¼å‡ºå†…存数据
        /// </summary>
        /// <returns>内存数据</returns>
        Dictionary<string, byte[]> ExportMemory();
        /// <summary>
        /// å¯¼å…¥å†…存数据
        /// </summary>
        /// <param name="data">内存数据</param>
        void ImportMemory(Dictionary<string, byte[]> data);
    }
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Interfaces/ISimulatorInstanceManager.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,133 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using WIDESEAWCS_S7Simulator.Core.Entities;
using WIDESEAWCS_S7Simulator.Core.Enums;
namespace WIDESEAWCS_S7Simulator.Core.Interfaces
{
    /// <summary>
    /// ä»¿çœŸå™¨å®žä¾‹ç®¡ç†å™¨æŽ¥å£
    /// ç®¡ç†å¤šä¸ªS7服务器实例的生命周期
    /// </summary>
    public interface ISimulatorInstanceManager
    {
        /// <summary>
        /// å®žä¾‹çŠ¶æ€å˜åŒ–äº‹ä»¶
        /// </summary>
        event EventHandler<InstanceStateChangedEventArgs>? InstanceStateChanged;
        /// <summary>
        /// èŽ·å–æ‰€æœ‰å®žä¾‹
        /// </summary>
        /// <returns>实例列表</returns>
        IReadOnlyList<IS7ServerInstance> GetAllInstances();
        /// <summary>
        /// æ ¹æ®ID获取实例
        /// </summary>
        /// <param name="instanceId">实例ID</param>
        /// <returns>实例,不存在则返回null</returns>
        IS7ServerInstance? GetInstance(string instanceId);
        /// <summary>
        /// æ£€æŸ¥å®žä¾‹æ˜¯å¦å­˜åœ¨
        /// </summary>
        /// <param name="instanceId">实例ID</param>
        /// <returns>是否存在</returns>
        bool InstanceExists(string instanceId);
        /// <summary>
        /// åˆ›å»ºæ–°å®žä¾‹
        /// </summary>
        /// <param name="config">实例配置</param>
        /// <returns>创建的实例</returns>
        Task<IS7ServerInstance> CreateInstanceAsync(InstanceConfig config);
        /// <summary>
        /// å¯åŠ¨å®žä¾‹
        /// </summary>
        /// <param name="instanceId">实例ID</param>
        /// <returns>是否成功</returns>
        Task<bool> StartInstanceAsync(string instanceId);
        /// <summary>
        /// åœæ­¢å®žä¾‹
        /// </summary>
        /// <param name="instanceId">实例ID</param>
        Task StopInstanceAsync(string instanceId);
        /// <summary>
        /// é‡å¯å®žä¾‹
        /// </summary>
        /// <param name="instanceId">实例ID</param>
        /// <returns>是否成功</returns>
        Task<bool> RestartInstanceAsync(string instanceId);
        /// <summary>
        /// åˆ é™¤å®žä¾‹
        /// </summary>
        /// <param name="instanceId">实例ID</param>
        /// <param name="deleteConfig">是否删除配置文件</param>
        Task DeleteInstanceAsync(string instanceId, bool deleteConfig = true);
        /// <summary>
        /// èŽ·å–å®žä¾‹çŠ¶æ€
        /// </summary>
        /// <param name="instanceId">实例ID</param>
        /// <returns>实例状态,不存在则返回null</returns>
        InstanceState? GetInstanceState(string instanceId);
        /// <summary>
        /// èŽ·å–æ‰€æœ‰å®žä¾‹çŠ¶æ€
        /// </summary>
        /// <returns>实例状态列表</returns>
        IReadOnlyList<InstanceState> GetAllInstanceStates();
        /// <summary>
        /// åŠ è½½æ‰€æœ‰å·²ä¿å­˜çš„å®žä¾‹
        /// </summary>
        /// <param name="autoStart">是否自动启动标记为AutoStart的实例</param>
        Task LoadSavedInstancesAsync(bool autoStart = true);
        /// <summary>
        /// åœæ­¢æ‰€æœ‰å®žä¾‹
        /// </summary>
        Task StopAllInstancesAsync();
        /// <summary>
        /// èŽ·å–è¿è¡Œä¸­çš„å®žä¾‹æ•°é‡
        /// </summary>
        int GetRunningInstanceCount();
        /// <summary>
        /// èŽ·å–å®žä¾‹æ€»æ•°
        /// </summary>
        int GetTotalInstanceCount();
    }
    /// <summary>
    /// å®žä¾‹çŠ¶æ€å˜åŒ–äº‹ä»¶å‚æ•°
    /// </summary>
    public class InstanceStateChangedEventArgs : EventArgs
    {
        /// <summary>
        /// å®žä¾‹ID
        /// </summary>
        public string InstanceId { get; set; } = string.Empty;
        /// <summary>
        /// æ—§çŠ¶æ€
        /// </summary>
        public InstanceStatus OldStatus { get; set; }
        /// <summary>
        /// æ–°çŠ¶æ€
        /// </summary>
        public InstanceStatus NewStatus { get; set; }
        /// <summary>
        /// å®žä¾‹çŠ¶æ€
        /// </summary>
        public InstanceState InstanceState { get; set; } = new();
    }
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Manager/SimulatorInstanceManager.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,474 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using WIDESEAWCS_S7Simulator.Core.Entities;
using WIDESEAWCS_S7Simulator.Core.Enums;
using WIDESEAWCS_S7Simulator.Core.Interfaces;
using WIDESEAWCS_S7Simulator.Core.Persistence;
using WIDESEAWCS_S7Simulator.Core.Server;
namespace WIDESEAWCS_S7Simulator.Core.Manager
{
    /// <summary>
    /// ä»¿çœŸå™¨å®žä¾‹ç®¡ç†å™¨å®žçް
    /// ç®¡ç†å¤šä¸ªS7服务器实例的生命周期,提供线程安全的CRUD操作
    /// </summary>
    public class SimulatorInstanceManager : ISimulatorInstanceManager
    {
        private readonly ConcurrentDictionary<string, IS7ServerInstance> _instances = new();
        private readonly IPersistenceService _persistenceService;
        private readonly ILogger<SimulatorInstanceManager> _logger;
        private readonly ILoggerFactory _loggerFactory;
        /// <inheritdoc/>
        public event EventHandler<InstanceStateChangedEventArgs>? InstanceStateChanged;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="persistenceService">持久化服务</param>
        /// <param name="logger">日志记录器</param>
        /// <param name="loggerFactory">日志工厂</param>
        public SimulatorInstanceManager(
            IPersistenceService persistenceService,
            ILogger<SimulatorInstanceManager> logger,
            ILoggerFactory loggerFactory)
        {
            _persistenceService = persistenceService ?? throw new ArgumentNullException(nameof(persistenceService));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
        }
        /// <inheritdoc/>
        public IReadOnlyList<IS7ServerInstance> GetAllInstances()
        {
            return _instances.Values.ToList().AsReadOnly();
        }
        /// <inheritdoc/>
        public IS7ServerInstance? GetInstance(string instanceId)
        {
            if (string.IsNullOrWhiteSpace(instanceId))
            {
                return null;
            }
            _instances.TryGetValue(instanceId, out var instance);
            return instance;
        }
        /// <inheritdoc/>
        public bool InstanceExists(string instanceId)
        {
            return !string.IsNullOrWhiteSpace(instanceId) && _instances.ContainsKey(instanceId);
        }
        /// <inheritdoc/>
        public async Task<IS7ServerInstance> CreateInstanceAsync(InstanceConfig config)
        {
            if (config == null)
            {
                throw new ArgumentNullException(nameof(config));
            }
            // å¦‚果没有提供ID,生成新的GUID
            if (string.IsNullOrWhiteSpace(config.Id))
            {
                config.Id = Guid.NewGuid().ToString("N");
                _logger.LogDebug("为实例生成新ID: {InstanceId}", config.Id);
            }
            // æ£€æŸ¥ID是否已存在
            if (_instances.ContainsKey(config.Id))
            {
                throw new InvalidOperationException($"实例ID {config.Id} å·²å­˜åœ¨");
            }
            try
            {
                // åˆ›å»ºå®žä¾‹
                var instanceLogger = _loggerFactory.CreateLogger<S7ServerInstance>();
                var instance = new S7ServerInstance(config, instanceLogger);
                // æ·»åŠ åˆ°å­—å…¸
                if (!_instances.TryAdd(config.Id, instance))
                {
                    throw new InvalidOperationException($"无法将实例 {config.Id} æ·»åŠ åˆ°ç®¡ç†å™¨");
                }
                // ä¿å­˜é…ç½®
                await _persistenceService.SaveInstanceConfigAsync(config);
                // è§¦å‘状态变化事件
                OnInstanceStateChanged(new InstanceStateChangedEventArgs
                {
                    InstanceId = config.Id,
                    OldStatus = InstanceStatus.Stopped,
                    NewStatus = InstanceStatus.Stopped,
                    InstanceState = instance.GetState()
                });
                _logger.LogInformation("已创建实例 {InstanceId} ({InstanceName})", config.Id, config.Name);
                return instance;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "创建实例 {InstanceId} æ—¶å‘生错误", config.Id);
                throw;
            }
        }
        /// <inheritdoc/>
        public async Task<bool> StartInstanceAsync(string instanceId)
        {
            if (string.IsNullOrWhiteSpace(instanceId))
            {
                _logger.LogWarning("尝试启动实例时提供了空ID");
                return false;
            }
            if (!_instances.TryGetValue(instanceId, out var instance))
            {
                _logger.LogWarning("尝试启动不存在的实例 {InstanceId}", instanceId);
                return false;
            }
            try
            {
                var oldState = instance.GetState();
                var oldStatus = oldState.Status;
                // å¯åŠ¨å®žä¾‹
                var success = instance.Start();
                if (success)
                {
                    var newState = instance.GetState();
                    // è§¦å‘状态变化事件
                    OnInstanceStateChanged(new InstanceStateChangedEventArgs
                    {
                        InstanceId = instanceId,
                        OldStatus = oldStatus,
                        NewStatus = newState.Status,
                        InstanceState = newState
                    });
                    _logger.LogInformation("实例 {InstanceId} å·²å¯åЍ", instanceId);
                }
                else
                {
                    _logger.LogWarning("实例 {InstanceId} å¯åŠ¨å¤±è´¥", instanceId);
                }
                return success;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "启动实例 {InstanceId} æ—¶å‘生异常", instanceId);
                return false;
            }
        }
        /// <inheritdoc/>
        public async Task StopInstanceAsync(string instanceId)
        {
            if (string.IsNullOrWhiteSpace(instanceId))
            {
                _logger.LogWarning("尝试停止实例时提供了空ID");
                return;
            }
            if (!_instances.TryGetValue(instanceId, out var instance))
            {
                _logger.LogWarning("尝试停止不存在的实例 {InstanceId}", instanceId);
                return;
            }
            try
            {
                var oldState = instance.GetState();
                var oldStatus = oldState.Status;
                // åœæ­¢å®žä¾‹
                instance.Stop();
                // åŒæ­¥å†…存数据到持久化存储
                await _persistenceService.SaveMemoryDataAsync(instanceId, instance.MemoryStore);
                var newState = instance.GetState();
                // è§¦å‘状态变化事件
                OnInstanceStateChanged(new InstanceStateChangedEventArgs
                {
                    InstanceId = instanceId,
                    OldStatus = oldStatus,
                    NewStatus = newState.Status,
                    InstanceState = newState
                });
                _logger.LogInformation("实例 {InstanceId} å·²åœæ­¢", instanceId);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "停止实例 {InstanceId} æ—¶å‘生异常", instanceId);
            }
        }
        /// <inheritdoc/>
        public async Task<bool> RestartInstanceAsync(string instanceId)
        {
            if (string.IsNullOrWhiteSpace(instanceId))
            {
                _logger.LogWarning("尝试重启实例时提供了空ID");
                return false;
            }
            if (!_instances.TryGetValue(instanceId, out var instance))
            {
                _logger.LogWarning("尝试重启不存在的实例 {InstanceId}", instanceId);
                return false;
            }
            try
            {
                var oldState = instance.GetState();
                var oldStatus = oldState.Status;
                // é‡å¯å®žä¾‹
                var success = instance.Restart();
                if (success)
                {
                    var newState = instance.GetState();
                    // è§¦å‘状态变化事件
                    OnInstanceStateChanged(new InstanceStateChangedEventArgs
                    {
                        InstanceId = instanceId,
                        OldStatus = oldStatus,
                        NewStatus = newState.Status,
                        InstanceState = newState
                    });
                    _logger.LogInformation("实例 {InstanceId} å·²é‡å¯", instanceId);
                }
                else
                {
                    _logger.LogWarning("实例 {InstanceId} é‡å¯å¤±è´¥", instanceId);
                }
                return success;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "重启实例 {InstanceId} æ—¶å‘生异常", instanceId);
                return false;
            }
        }
        /// <inheritdoc/>
        public async Task DeleteInstanceAsync(string instanceId, bool deleteConfig = true)
        {
            if (string.IsNullOrWhiteSpace(instanceId))
            {
                _logger.LogWarning("尝试删除实例时提供了空ID");
                return;
            }
            if (!_instances.TryRemove(instanceId, out var instance))
            {
                _logger.LogWarning("尝试删除不存在的实例 {InstanceId}", instanceId);
                return;
            }
            try
            {
                var oldState = instance.GetState();
                // åœæ­¢å®žä¾‹
                instance.Stop();
                // é‡Šæ”¾å®žä¾‹èµ„源
                instance.Dispose();
                // åˆ é™¤é…ç½®æ–‡ä»¶
                if (deleteConfig)
                {
                    await _persistenceService.DeleteInstanceConfigAsync(instanceId);
                }
                // è§¦å‘状态变化事件
                OnInstanceStateChanged(new InstanceStateChangedEventArgs
                {
                    InstanceId = instanceId,
                    OldStatus = oldState.Status,
                    NewStatus = InstanceStatus.Stopped,
                    InstanceState = oldState
                });
                _logger.LogInformation("实例 {InstanceId} å·²åˆ é™¤", instanceId);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "删除实例 {InstanceId} æ—¶å‘生异常", instanceId);
            }
        }
        /// <inheritdoc/>
        public InstanceState? GetInstanceState(string instanceId)
        {
            if (string.IsNullOrWhiteSpace(instanceId))
            {
                return null;
            }
            if (!_instances.TryGetValue(instanceId, out var instance))
            {
                return null;
            }
            return instance.GetState();
        }
        /// <inheritdoc/>
        public IReadOnlyList<InstanceState> GetAllInstanceStates()
        {
            return _instances.Values
                .Select(i => i.GetState())
                .ToList()
                .AsReadOnly();
        }
        /// <inheritdoc/>
        public async Task LoadSavedInstancesAsync(bool autoStart = true)
        {
            try
            {
                _logger.LogInformation("开始加载已保存的实例配置...");
                // åŠ è½½æ‰€æœ‰é…ç½®
                var configs = await _persistenceService.LoadAllInstanceConfigsAsync();
                if (configs == null || configs.Count == 0)
                {
                    _logger.LogInformation("没有找到已保存的实例配置");
                    return;
                }
                _logger.LogInformation("找到 {Count} ä¸ªå·²ä¿å­˜çš„实例配置", configs.Count);
                foreach (var config in configs)
                {
                    try
                    {
                        // åˆ›å»ºå®žä¾‹
                        var instanceLogger = _loggerFactory.CreateLogger<S7ServerInstance>();
                        var instance = new S7ServerInstance(config, instanceLogger);
                        // æ·»åŠ åˆ°å­—å…¸
                        if (_instances.TryAdd(config.Id, instance))
                        {
                            // åŠ è½½å†…å­˜æ•°æ®
                            try
                            {
                                await _persistenceService.LoadMemoryDataAsync(config.Id, instance.MemoryStore);
                                _logger.LogDebug("已加载实例 {InstanceId} çš„内存数据", config.Id);
                            }
                            catch (Exception ex)
                            {
                                _logger.LogWarning(ex, "加载实例 {InstanceId} çš„内存数据时发生警告", config.Id);
                            }
                            // å¦‚果配置了自动启动,则启动实例
                            if (autoStart && config.AutoStart)
                            {
                                _logger.LogInformation("自动启动实例 {InstanceId} ({InstanceName})", config.Id, config.Name);
                                var success = instance.Start();
                                if (success)
                                {
                                    _logger.LogInformation("实例 {InstanceId} è‡ªåŠ¨å¯åŠ¨æˆåŠŸ", config.Id);
                                }
                                else
                                {
                                    _logger.LogWarning("实例 {InstanceId} è‡ªåŠ¨å¯åŠ¨å¤±è´¥", config.Id);
                                }
                            }
                            // è§¦å‘状态变化事件
                            OnInstanceStateChanged(new InstanceStateChangedEventArgs
                            {
                                InstanceId = config.Id,
                                OldStatus = InstanceStatus.Stopped,
                                NewStatus = instance.State.Status,
                                InstanceState = instance.GetState()
                            });
                            _logger.LogInformation("已加载实例 {InstanceId} ({InstanceName})", config.Id, config.Name);
                        }
                        else
                        {
                            _logger.LogWarning("实例 {InstanceId} å·²å­˜åœ¨ï¼Œè·³è¿‡åŠ è½½", config.Id);
                        }
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, "加载实例 {InstanceId} æ—¶å‘生错误", config.Id);
                    }
                }
                _logger.LogInformation("实例加载完成,共加载 {Count} ä¸ªå®žä¾‹", _instances.Count);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "加载已保存的实例时发生异常");
            }
        }
        /// <inheritdoc/>
        public async Task StopAllInstancesAsync()
        {
            _logger.LogInformation("开始停止所有实例...");
            var instanceIds = _instances.Keys.ToList();
            foreach (var instanceId in instanceIds)
            {
                try
                {
                    await StopInstanceAsync(instanceId);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "停止实例 {InstanceId} æ—¶å‘生异常", instanceId);
                }
            }
            _logger.LogInformation("所有实例已停止");
        }
        /// <inheritdoc/>
        public int GetRunningInstanceCount()
        {
            return _instances.Values.Count(i => i.State.Status == InstanceStatus.Running);
        }
        /// <inheritdoc/>
        public int GetTotalInstanceCount()
        {
            return _instances.Count;
        }
        /// <summary>
        /// è§¦å‘实例状态变化事件
        /// </summary>
        /// <param name="e">事件参数</param>
        protected virtual void OnInstanceStateChanged(InstanceStateChangedEventArgs e)
        {
            InstanceStateChanged?.Invoke(this, e);
        }
    }
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Server/S7ServerInstance.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,444 @@
using System.Collections.Concurrent;
using HslCommunication;
using HslCommunication.Profinet.Siemens;
using HslCommunication.Reflection;
using Microsoft.Extensions.Logging;
using WIDESEAWCS_S7Simulator.Core.Entities;
using WIDESEAWCS_S7Simulator.Core.Enums;
using WIDESEAWCS_S7Simulator.Core.Interfaces;
using WIDESEAWCS_S7Simulator.Core.Memory;
namespace WIDESEAWCS_S7Simulator.Core.Server
{
    /// <summary>
    /// S7服务器实例实现
    /// ä½¿ç”¨HSL Communication库实现S7 PLC仿真服务器
    /// </summary>
    public class S7ServerInstance : IS7ServerInstance
    {
        private readonly ILogger<S7ServerInstance> _logger;
        private readonly object _lock = new();
        private SiemensS7Server? _server;
        private bool _disposed;
        /// <inheritdoc/>
        public InstanceConfig Config { get; }
        /// <inheritdoc/>
        public InstanceState State { get; private set; }
        /// <inheritdoc/>
        public IMemoryStore MemoryStore { get; }
        /// <summary>
        /// å®¢æˆ·ç«¯è¿žæŽ¥è¿½è¸ª
        /// </summary>
        private readonly ConcurrentDictionary<string, S7ClientConnection> _clients = new();
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="config">实例配置</param>
        /// <param name="logger">日志记录器</param>
        public S7ServerInstance(InstanceConfig config, ILogger<S7ServerInstance> logger)
        {
            Config = config ?? throw new ArgumentNullException(nameof(config));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            // åˆå§‹åŒ–内存存储
            MemoryStore = new MemoryStore(config.MemoryConfig);
            // åˆå§‹åŒ–状态
            State = new InstanceState
            {
                InstanceId = config.Id,
                Status = InstanceStatus.Stopped,
                ClientCount = 0,
                TotalRequests = 0
            };
            _logger.LogInformation("S7服务器实例 {InstanceId} ({InstanceName}) å·²åˆ›å»ºï¼ŒPLC类型: {PLCType}, ç«¯å£: {Port}",
                config.Id, config.Name, config.PLCType, config.Port);
        }
        /// <inheritdoc/>
        public bool Start()
        {
            lock (_lock)
            {
                if (_disposed)
                {
                    _logger.LogError("无法启动已释放的实例 {InstanceId}", Config.Id);
                    return false;
                }
                if (State.Status == InstanceStatus.Running)
                {
                    _logger.LogWarning("实例 {InstanceId} å·²åœ¨è¿è¡Œä¸­", Config.Id);
                    return true;
                }
                try
                {
                    // åˆ›å»ºS7服务器
                    _server = new SiemensS7Server();
                    // è®¾ç½®æ¿€æ´»ç 
                    if (!string.IsNullOrWhiteSpace(Config.ActivationKey))
                    {
                        HslCommunication.Authorization.SetAuthorizationCode(Config.ActivationKey);
                        _logger.LogDebug("已设置激活码");
                    }
                    // åˆå§‹åŒ–DB块(根据配置)
                    InitializeDbBlocks();
                    // ä»ŽMemoryStore同步初始数据到服务器
                    SynchronizeMemoryToServer();
                    // å¯åŠ¨æœåŠ¡å™¨
                    try
                    {
                        _server.ServerStart(Config.Port);
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, "启动S7服务器失败");
                        State.Status = InstanceStatus.Error;
                        State.ErrorMessage = ex.Message;
                        return false;
                    }
                    // æ›´æ–°çŠ¶æ€
                    State.Status = InstanceStatus.Running;
                    State.StartTime = DateTime.Now;
                    State.ErrorMessage = null;
                    _logger.LogInformation("S7服务器实例 {InstanceId} å·²æˆåŠŸå¯åŠ¨ï¼Œç›‘å¬ç«¯å£: {Port}", Config.Id, Config.Port);
                    return true;
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "启动S7服务器实例 {InstanceId} æ—¶å‘生异常", Config.Id);
                    State.Status = InstanceStatus.Error;
                    State.ErrorMessage = ex.Message;
                    return false;
                }
            }
        }
        /// <inheritdoc/>
        public void Stop()
        {
            lock (_lock)
            {
                if (_disposed || State.Status == InstanceStatus.Stopped)
                {
                    return;
                }
                try
                {
                    if (_server != null)
                    {
                        // åœæ­¢å‰åŒæ­¥æœåŠ¡å™¨æ•°æ®åˆ°MemoryStore
                        SynchronizeServerToMemory();
                        _server.ServerClose();
                        _server = null;
                    }
                    // æ¸…空客户端连接
                    _clients.Clear();
                    // æ›´æ–°çŠ¶æ€
                    State.Status = InstanceStatus.Stopped;
                    State.ClientCount = 0;
                    State.StartTime = null;
                    _logger.LogInformation("S7服务器实例 {InstanceId} å·²åœæ­¢", Config.Id);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "停止S7服务器实例 {InstanceId} æ—¶å‘生异常", Config.Id);
                    State.Status = InstanceStatus.Error;
                    State.ErrorMessage = ex.Message;
                }
            }
        }
        /// <inheritdoc/>
        public bool Restart()
        {
            Stop();
            return Start();
        }
        /// <inheritdoc/>
        public InstanceState GetState()
        {
            lock (_lock)
            {
                // æ›´æ–°å®¢æˆ·ç«¯è¿žæŽ¥æ•°
                State.ClientCount = _clients.Count;
                State.Clients = _clients.Values.ToList();
                // è¿”回状态副本
                return new InstanceState
                {
                    InstanceId = State.InstanceId,
                    Status = State.Status,
                    ClientCount = State.ClientCount,
                    TotalRequests = State.TotalRequests,
                    StartTime = State.StartTime,
                    LastActivityTime = State.LastActivityTime,
                    Clients = new List<S7ClientConnection>(State.Clients),
                    ErrorMessage = State.ErrorMessage
                };
            }
        }
        /// <inheritdoc/>
        public void ClearMemory()
        {
            lock (_lock)
            {
                MemoryStore.Clear();
                // åŒæ—¶æ¸…空服务器内部数据
                if (_server != null && State.Status == InstanceStatus.Running)
                {
                    // æ¸…空各内存区域
                    for (int i = 0; i < 100; i++)
                    {
                        _server.Write($"M{i}", (byte)0);
                        _server.Write($"I{i}", (byte)0);
                        _server.Write($"Q{i}", (byte)0);
                    }
                    // æ¸…空DB块
                    for (ushort db = 1; db <= Config.MemoryConfig.DBBlockCount; db++)
                    {
                        for (int i = 0; i < 10; i++)
                        {
                            _server.Write($"DB{db}.DBD{i}", (byte)0);
                        }
                    }
                }
                _logger.LogInformation("实例 {InstanceId} å†…存已清空", Config.Id);
            }
        }
        /// <inheritdoc/>
        public Dictionary<string, byte[]> ExportMemory()
        {
            lock (_lock)
            {
                // å…ˆåŒæ­¥æœåŠ¡å™¨æ•°æ®åˆ°MemoryStore
                if (_server != null && State.Status == InstanceStatus.Running)
                {
                    SynchronizeServerToMemory();
                }
                return MemoryStore.Export();
            }
        }
        /// <inheritdoc/>
        public void ImportMemory(Dictionary<string, byte[]> data)
        {
            lock (_lock)
            {
                MemoryStore.Import(data);
                // åŒæ­¥åˆ°æœåС噍
                if (_server != null && State.Status == InstanceStatus.Running)
                {
                    SynchronizeMemoryToServer();
                }
                _logger.LogInformation("实例 {InstanceId} å†…存数据已导入", Config.Id);
            }
        }
        /// <summary>
        /// åˆå§‹åŒ–DB块
        /// </summary>
        private void InitializeDbBlocks()
        {
            if (_server == null)
                return;
            try
            {
                // æ ¹æ®é…ç½®æ·»åŠ DB块
                for (ushort i = 1; i <= Config.MemoryConfig.DBBlockCount; i++)
                {
                    _server.AddDbBlock(i, Config.MemoryConfig.DBBlockSize);
                    _logger.LogDebug("已添加DB块: DB{DbNumber}, å¤§å°: {Size}", i, Config.MemoryConfig.DBBlockSize);
                }
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "初始化DB块时发生警告");
            }
        }
        /// <summary>
        /// ä»ŽMemoryStore同步数据到服务器
        /// </summary>
        private void SynchronizeMemoryToServer()
        {
            if (_server == null)
                return;
            try
            {
                var data = MemoryStore.Export();
                // åŒæ­¥M区
                if (data.ContainsKey("M"))
                {
                    var mBytes = data["M"];
                    for (int i = 0; i < Math.Min(mBytes.Length, Config.MemoryConfig.MRegionSize); i++)
                    {
                        _server.Write($"M{i}", mBytes[i]);
                    }
                }
                // åŒæ­¥I区
                if (data.ContainsKey("I"))
                {
                    var iBytes = data["I"];
                    for (int i = 0; i < Math.Min(iBytes.Length, Config.MemoryConfig.IRegionSize); i++)
                    {
                        _server.Write($"I{i}", iBytes[i]);
                    }
                }
                // åŒæ­¥Q区
                if (data.ContainsKey("Q"))
                {
                    var qBytes = data["Q"];
                    for (int i = 0; i < Math.Min(qBytes.Length, Config.MemoryConfig.QRegionSize); i++)
                    {
                        _server.Write($"Q{i}", qBytes[i]);
                    }
                }
                // åŒæ­¥DB区
                if (data.ContainsKey("DB"))
                {
                    var dbBytes = data["DB"];
                    int offset = 0;
                    for (ushort db = 1; db <= Config.MemoryConfig.DBBlockCount; db++)
                    {
                        int blockSize = Math.Min(Config.MemoryConfig.DBBlockSize, dbBytes.Length - offset);
                        for (int i = 0; i < blockSize; i++)
                        {
                            _server.Write($"DB{db}.DBD{i}", dbBytes[offset + i]);
                        }
                        offset += Config.MemoryConfig.DBBlockSize;
                    }
                }
                _logger.LogDebug("已将MemoryStore数据同步到S7服务器");
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "同步数据到服务器时发生警告");
            }
        }
        /// <summary>
        /// ä»ŽæœåŠ¡å™¨åŒæ­¥æ•°æ®åˆ°MemoryStore
        /// </summary>
        private void SynchronizeServerToMemory()
        {
            if (_server == null)
                return;
            try
            {
                var data = new Dictionary<string, byte[]>();
                // è¯»å–M区
                var mResult = _server.Read("M0", (ushort)Config.MemoryConfig.MRegionSize);
                if (mResult.IsSuccess)
                {
                    data["M"] = mResult.Content;
                }
                // è¯»å–I区
                var iResult = _server.Read("I0", (ushort)Config.MemoryConfig.IRegionSize);
                if (iResult.IsSuccess)
                {
                    data["I"] = iResult.Content;
                }
                // è¯»å–Q区
                var qResult = _server.Read("Q0", (ushort)Config.MemoryConfig.QRegionSize);
                if (qResult.IsSuccess)
                {
                    data["Q"] = qResult.Content;
                }
                // è¯»å–DB区
                var dbBytes = new List<byte>();
                for (ushort db = 1; db <= Config.MemoryConfig.DBBlockCount; db++)
                {
                    var dbResult = _server.Read($"DB{db}.DBD0", (ushort)Config.MemoryConfig.DBBlockSize);
                    if (dbResult.IsSuccess)
                    {
                        dbBytes.AddRange(dbResult.Content);
                    }
                }
                data["DB"] = dbBytes.ToArray();
                // å¯¼å…¥åˆ°MemoryStore
                MemoryStore.Import(data);
                _logger.LogDebug("已将S7服务器数据同步到MemoryStore");
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "从服务器同步数据时发生警告");
            }
        }
        /// <summary>
        /// å¢žåŠ è¯·æ±‚è®¡æ•°å¹¶æ›´æ–°æ´»åŠ¨æ—¶é—´
        /// </summary>
        private void IncrementRequestCount()
        {
            State.TotalRequests++;
            State.LastActivityTime = DateTime.Now;
        }
        /// <summary>
        /// é‡Šæ”¾èµ„源
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        /// <summary>
        /// é‡Šæ”¾èµ„源
        /// </summary>
        /// <param name="disposing">是否正在释放托管资源</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    Stop();
                    MemoryStore?.Dispose();
                }
                _disposed = true;
            }
        }
    }
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Controllers/ClientsController.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,110 @@
using Microsoft.AspNetCore.Mvc;
using WIDESEAWCS_S7Simulator.Core.Entities;
using WIDESEAWCS_S7Simulator.Core.Interfaces;
namespace WIDESEAWCS_S7Simulator.Server.Controllers
{
    /// <summary>
    /// å®¢æˆ·ç«¯è¿žæŽ¥ç®¡ç†æŽ§åˆ¶å™¨
    /// </summary>
    [ApiController]
    [Route("api/instances/{id}/[controller]")]
    public class ClientsController : ControllerBase
    {
        private readonly ISimulatorInstanceManager _instanceManager;
        private readonly ILogger<ClientsController> _logger;
        public ClientsController(
            ISimulatorInstanceManager instanceManager,
            ILogger<ClientsController> logger)
        {
            _instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }
        /// <summary>
        /// èŽ·å–è¿žæŽ¥çš„å®¢æˆ·ç«¯åˆ—è¡¨
        /// </summary>
        [HttpGet]
        [ProducesResponseType(typeof(List<S7ClientConnection>), StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        public ActionResult<List<S7ClientConnection>> GetConnectedClients(string id)
        {
            try
            {
                if (string.IsNullOrWhiteSpace(id))
                {
                    return BadRequest(new { error = "Instance ID is required" });
                }
                var instance = _instanceManager.GetInstance(id);
                if (instance == null)
                {
                    return NotFound(new { error = $"Instance with ID '{id}' not found" });
                }
                var state = instance.GetState();
                return Ok(state.Clients);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to get clients for instance {InstanceId}", id);
                return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to retrieve clients" });
            }
        }
        /// <summary>
        /// æ–­å¼€æŒ‡å®šå®¢æˆ·ç«¯
        /// </summary>
        [HttpDelete("{clientId}")]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        public ActionResult DisconnectClient(string id, string clientId)
        {
            try
            {
                if (string.IsNullOrWhiteSpace(id))
                {
                    return BadRequest(new { error = "Instance ID is required" });
                }
                if (string.IsNullOrWhiteSpace(clientId))
                {
                    return BadRequest(new { error = "Client ID is required" });
                }
                var instance = _instanceManager.GetInstance(id);
                if (instance == null)
                {
                    return NotFound(new { error = $"Instance with ID '{id}' not found" });
                }
                var state = instance.GetState();
                var client = state.Clients.FirstOrDefault(c => c.ClientId == clientId);
                if (client == null)
                {
                    return NotFound(new { error = $"Client with ID '{clientId}' not found" });
                }
                // Note: The actual disconnection logic would need to be implemented in IS7ServerInstance
                // For now, we're returning a success response indicating the operation was requested
                // In a real implementation, you would call instance.DisconnectClient(clientId)
                _logger.LogInformation("Disconnect request for client {ClientId} on instance {InstanceId}", clientId, id);
                return Ok(new
                {
                    message = "Client disconnect requested",
                    clientId = clientId,
                    instanceId = id,
                    note = "Actual disconnection logic should be implemented in IS7ServerInstance"
                });
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to disconnect client {ClientId} for instance {InstanceId}", clientId, id);
                return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to disconnect client" });
            }
        }
    }
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Controllers/MemoryController.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,219 @@
using Microsoft.AspNetCore.Mvc;
using WIDESEAWCS_S7Simulator.Core.Interfaces;
namespace WIDESEAWCS_S7Simulator.Server.Controllers
{
    /// <summary>
    /// å†…存操作控制器
    /// </summary>
    [ApiController]
    [Route("api/instances/{id}/[controller]")]
    public class MemoryController : ControllerBase
    {
        private readonly ISimulatorInstanceManager _instanceManager;
        private readonly ILogger<MemoryController> _logger;
        public MemoryController(
            ISimulatorInstanceManager instanceManager,
            ILogger<MemoryController> logger)
        {
            _instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }
        /// <summary>
        /// è¯»å–内存数据
        /// </summary>
        [HttpGet]
        [ProducesResponseType(typeof(Dictionary<string, byte[]>), StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        public ActionResult<Dictionary<string, byte[]>> ReadMemory(string id)
        {
            try
            {
                if (string.IsNullOrWhiteSpace(id))
                {
                    return BadRequest(new { error = "Instance ID is required" });
                }
                var instance = _instanceManager.GetInstance(id);
                if (instance == null)
                {
                    return NotFound(new { error = $"Instance with ID '{id}' not found" });
                }
                var memoryData = instance.ExportMemory();
                return Ok(memoryData);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to read memory for instance {InstanceId}", id);
                return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to read memory" });
            }
        }
        /// <summary>
        /// å†™å…¥å†…存数据
        /// </summary>
        [HttpPost]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        public ActionResult WriteMemory(string id, [FromBody] Dictionary<string, byte[]> data)
        {
            try
            {
                if (string.IsNullOrWhiteSpace(id))
                {
                    return BadRequest(new { error = "Instance ID is required" });
                }
                if (data == null || data.Count == 0)
                {
                    return BadRequest(new { error = "Memory data is required" });
                }
                var instance = _instanceManager.GetInstance(id);
                if (instance == null)
                {
                    return NotFound(new { error = $"Instance with ID '{id}' not found" });
                }
                instance.ImportMemory(data);
                return Ok(new { message = "Memory data written successfully" });
            }
            catch (ArgumentException ex)
            {
                _logger.LogWarning(ex, "Invalid memory data for instance {InstanceId}", id);
                return BadRequest(new { error = ex.Message });
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to write memory for instance {InstanceId}", id);
                return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to write memory" });
            }
        }
        /// <summary>
        /// æ¸…空内存数据
        /// </summary>
        [HttpDelete]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        public ActionResult ClearMemory(string id)
        {
            try
            {
                if (string.IsNullOrWhiteSpace(id))
                {
                    return BadRequest(new { error = "Instance ID is required" });
                }
                var instance = _instanceManager.GetInstance(id);
                if (instance == null)
                {
                    return NotFound(new { error = $"Instance with ID '{id}' not found" });
                }
                instance.ClearMemory();
                return Ok(new { message = "Memory cleared successfully" });
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to clear memory for instance {InstanceId}", id);
                return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to clear memory" });
            }
        }
        /// <summary>
        /// ä¿å­˜å†…存快照
        /// </summary>
        [HttpPost("save")]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        public ActionResult SaveMemorySnapshot(string id, [FromQuery] string? snapshotName = null)
        {
            try
            {
                if (string.IsNullOrWhiteSpace(id))
                {
                    return BadRequest(new { error = "Instance ID is required" });
                }
                var instance = _instanceManager.GetInstance(id);
                if (instance == null)
                {
                    return NotFound(new { error = $"Instance with ID '{id}' not found" });
                }
                var memoryData = instance.ExportMemory();
                var fileName = string.IsNullOrWhiteSpace(snapshotName)
                    ? $"snapshot_{id}_{DateTime.UtcNow:yyyyMMdd_HHmmss}.json"
                    : snapshotName;
                // In a real implementation, you would save this to a file system or database
                // For now, we'll return the data that would be saved
                return Ok(new
                {
                    message = "Memory snapshot captured successfully",
                    fileName = fileName,
                    timestamp = DateTime.UtcNow,
                    dataSize = memoryData.Sum(kvp => kvp.Value.Length),
                    regions = memoryData.Keys.ToList()
                });
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to save memory snapshot for instance {InstanceId}", id);
                return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to save memory snapshot" });
            }
        }
        /// <summary>
        /// åŠ è½½å†…å­˜å¿«ç…§
        /// </summary>
        [HttpPost("load")]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        public ActionResult LoadMemorySnapshot(string id, [FromBody] Dictionary<string, byte[]> snapshotData)
        {
            try
            {
                if (string.IsNullOrWhiteSpace(id))
                {
                    return BadRequest(new { error = "Instance ID is required" });
                }
                if (snapshotData == null || snapshotData.Count == 0)
                {
                    return BadRequest(new { error = "Snapshot data is required" });
                }
                var instance = _instanceManager.GetInstance(id);
                if (instance == null)
                {
                    return NotFound(new { error = $"Instance with ID '{id}' not found" });
                }
                instance.ImportMemory(snapshotData);
                return Ok(new
                {
                    message = "Memory snapshot loaded successfully",
                    timestamp = DateTime.UtcNow,
                    regions = snapshotData.Keys.ToList()
                });
            }
            catch (ArgumentException ex)
            {
                _logger.LogWarning(ex, "Invalid snapshot data for instance {InstanceId}", id);
                return BadRequest(new { error = ex.Message });
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to load memory snapshot for instance {InstanceId}", id);
                return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to load memory snapshot" });
            }
        }
    }
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Controllers/SimulatorInstancesController.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,375 @@
using Microsoft.AspNetCore.Mvc;
using WIDESEAWCS_S7Simulator.Core.Entities;
using WIDESEAWCS_S7Simulator.Core.Interfaces;
namespace WIDESEAWCS_S7Simulator.Server.Controllers
{
    /// <summary>
    /// ä»¿çœŸå™¨å®žä¾‹ç®¡ç†æŽ§åˆ¶å™¨
    /// </summary>
    [ApiController]
    [Route("api/[controller]")]
    public class SimulatorInstancesController : ControllerBase
    {
        private readonly ISimulatorInstanceManager _instanceManager;
        private readonly ILogger<SimulatorInstancesController> _logger;
        public SimulatorInstancesController(
            ISimulatorInstanceManager instanceManager,
            ILogger<SimulatorInstancesController> logger)
        {
            _instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }
        /// <summary>
        /// èŽ·å–æ‰€æœ‰å®žä¾‹åˆ—è¡¨
        /// </summary>
        [HttpGet]
        [ProducesResponseType(typeof(IEnumerable<InstanceState>), StatusCodes.Status200OK)]
        public ActionResult<IEnumerable<InstanceState>> GetAllInstances()
        {
            try
            {
                var instances = _instanceManager.GetAllInstances();
                var states = instances.Select(i => i.GetState()).ToList();
                return Ok(states);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to get all instances");
                return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to retrieve instances" });
            }
        }
        /// <summary>
        /// åˆ›å»ºæ–°å®žä¾‹
        /// </summary>
        [HttpPost]
        [ProducesResponseType(typeof(InstanceState), StatusCodes.Status201Created)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        public async Task<ActionResult<InstanceState>> CreateInstance([FromBody] InstanceConfig config)
        {
            try
            {
                if (config == null)
                {
                    return BadRequest(new { error = "Instance configuration is required" });
                }
                if (string.IsNullOrWhiteSpace(config.Id))
                {
                    return BadRequest(new { error = "Instance ID is required" });
                }
                if (string.IsNullOrWhiteSpace(config.Name))
                {
                    return BadRequest(new { error = "Instance name is required" });
                }
                if (_instanceManager.InstanceExists(config.Id))
                {
                    return Conflict(new { error = $"Instance with ID '{config.Id}' already exists" });
                }
                if (config.Port <= 0 || config.Port > 65535)
                {
                    return BadRequest(new { error = "Port must be between 1 and 65535" });
                }
                var instance = await _instanceManager.CreateInstanceAsync(config);
                var state = instance.GetState();
                return CreatedAtAction(
                    nameof(GetInstance),
                    new { id = config.Id },
                    state);
            }
            catch (ArgumentException ex)
            {
                _logger.LogWarning(ex, "Invalid instance configuration");
                return BadRequest(new { error = ex.Message });
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to create instance");
                return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to create instance" });
            }
        }
        /// <summary>
        /// èŽ·å–æŒ‡å®šå®žä¾‹è¯¦æƒ…
        /// </summary>
        [HttpGet("{id}")]
        [ProducesResponseType(typeof(InstanceState), StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        public ActionResult<InstanceState> GetInstance(string id)
        {
            try
            {
                if (string.IsNullOrWhiteSpace(id))
                {
                    return BadRequest(new { error = "Instance ID is required" });
                }
                var instance = _instanceManager.GetInstance(id);
                if (instance == null)
                {
                    return NotFound(new { error = $"Instance with ID '{id}' not found" });
                }
                var state = instance.GetState();
                return Ok(state);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to get instance {InstanceId}", id);
                return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to retrieve instance" });
            }
        }
        /// <summary>
        /// æ›´æ–°å®žä¾‹é…ç½®
        /// </summary>
        [HttpPut("{id}")]
        [ProducesResponseType(typeof(InstanceState), StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        public async Task<ActionResult<InstanceState>> UpdateInstance(string id, [FromBody] InstanceConfig config)
        {
            try
            {
                if (string.IsNullOrWhiteSpace(id))
                {
                    return BadRequest(new { error = "Instance ID is required" });
                }
                if (config == null)
                {
                    return BadRequest(new { error = "Instance configuration is required" });
                }
                var instance = _instanceManager.GetInstance(id);
                if (instance == null)
                {
                    return NotFound(new { error = $"Instance with ID '{id}' not found" });
                }
                // Validate port if changed
                if (config.Port <= 0 || config.Port > 65535)
                {
                    return BadRequest(new { error = "Port must be between 1 and 65535" });
                }
                // Delete existing instance and recreate with new config
                // Note: This is a simplified approach. In production, you might want to support hot-reload
                bool wasRunning = instance.GetState().Status == Core.Enums.InstanceStatus.Running;
                await _instanceManager.DeleteInstanceAsync(id, deleteConfig: false);
                config.Id = id; // Ensure ID matches route parameter
                var newInstance = await _instanceManager.CreateInstanceAsync(config);
                if (wasRunning)
                {
                    await _instanceManager.StartInstanceAsync(id);
                }
                var state = newInstance.GetState();
                return Ok(state);
            }
            catch (ArgumentException ex)
            {
                _logger.LogWarning(ex, "Invalid instance configuration");
                return BadRequest(new { error = ex.Message });
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to update instance {InstanceId}", id);
                return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to update instance" });
            }
        }
        /// <summary>
        /// åˆ é™¤å®žä¾‹
        /// </summary>
        [HttpDelete("{id}")]
        [ProducesResponseType(StatusCodes.Status204NoContent)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        public async Task<IActionResult> DeleteInstance(string id, [FromQuery] bool deleteConfig = true)
        {
            try
            {
                if (string.IsNullOrWhiteSpace(id))
                {
                    return BadRequest(new { error = "Instance ID is required" });
                }
                if (!_instanceManager.InstanceExists(id))
                {
                    return NotFound(new { error = $"Instance with ID '{id}' not found" });
                }
                await _instanceManager.DeleteInstanceAsync(id, deleteConfig);
                return NoContent();
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to delete instance {InstanceId}", id);
                return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to delete instance" });
            }
        }
        /// <summary>
        /// å¯åŠ¨å®žä¾‹
        /// </summary>
        [HttpPost("{id}/start")]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        public async Task<ActionResult<InstanceState>> StartInstance(string id)
        {
            try
            {
                if (string.IsNullOrWhiteSpace(id))
                {
                    return BadRequest(new { error = "Instance ID is required" });
                }
                if (!_instanceManager.InstanceExists(id))
                {
                    return NotFound(new { error = $"Instance with ID '{id}' not found" });
                }
                bool success = await _instanceManager.StartInstanceAsync(id);
                if (!success)
                {
                    return BadRequest(new { error = "Failed to start instance" });
                }
                var instance = _instanceManager.GetInstance(id);
                var state = instance?.GetState();
                return Ok(state);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to start instance {InstanceId}", id);
                return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to start instance" });
            }
        }
        /// <summary>
        /// åœæ­¢å®žä¾‹
        /// </summary>
        [HttpPost("{id}/stop")]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        public async Task<ActionResult<InstanceState>> StopInstance(string id)
        {
            try
            {
                if (string.IsNullOrWhiteSpace(id))
                {
                    return BadRequest(new { error = "Instance ID is required" });
                }
                if (!_instanceManager.InstanceExists(id))
                {
                    return NotFound(new { error = $"Instance with ID '{id}' not found" });
                }
                await _instanceManager.StopInstanceAsync(id);
                var instance = _instanceManager.GetInstance(id);
                var state = instance?.GetState();
                return Ok(state);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to stop instance {InstanceId}", id);
                return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to stop instance" });
            }
        }
        /// <summary>
        /// é‡å¯å®žä¾‹
        /// </summary>
        [HttpPost("{id}/restart")]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        public async Task<ActionResult<InstanceState>> RestartInstance(string id)
        {
            try
            {
                if (string.IsNullOrWhiteSpace(id))
                {
                    return BadRequest(new { error = "Instance ID is required" });
                }
                if (!_instanceManager.InstanceExists(id))
                {
                    return NotFound(new { error = $"Instance with ID '{id}' not found" });
                }
                bool success = await _instanceManager.RestartInstanceAsync(id);
                if (!success)
                {
                    return BadRequest(new { error = "Failed to restart instance" });
                }
                var instance = _instanceManager.GetInstance(id);
                var state = instance?.GetState();
                return Ok(state);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to restart instance {InstanceId}", id);
                return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to restart instance" });
            }
        }
        /// <summary>
        /// å¯åŠ¨æ‰€æœ‰è‡ªåŠ¨å¯åŠ¨å®žä¾‹
        /// </summary>
        [HttpPost("start-all")]
        [ProducesResponseType(StatusCodes.Status200OK)]
        public async Task<ActionResult> StartAllAutoStartInstances()
        {
            try
            {
                await _instanceManager.LoadSavedInstancesAsync(autoStart: true);
                return Ok(new {
                    message = "Started all auto-start instances",
                    runningCount = _instanceManager.GetRunningInstanceCount()
                });
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to start all instances");
                return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to start instances" });
            }
        }
        /// <summary>
        /// åœæ­¢æ‰€æœ‰å®žä¾‹
        /// </summary>
        [HttpPost("stop-all")]
        [ProducesResponseType(StatusCodes.Status200OK)]
        public async Task<ActionResult> StopAllInstances()
        {
            try
            {
                await _instanceManager.StopAllInstancesAsync();
                return Ok(new {
                    message = "Stopped all instances",
                    runningCount = _instanceManager.GetRunningInstanceCount()
                });
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to stop all instances");
                return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to stop instances" });
            }
        }
    }
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Create.cshtml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,160 @@
@page
@model CreateModel
@{
    ViewData["Title"] = "创建实例";
}
<div class="row justify-content-center">
    <div class="col-lg-8">
        <div class="card shadow">
            <div class="card-header bg-primary text-white">
                <h4 class="mb-0">
                    <i class="bi bi-plus-circle me-2"></i>创建新实例
                </h4>
            </div>
            <div class="card-body">
                <form method="post" id="createForm">
                    <div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div>
                    <!-- åŸºæœ¬ä¿¡æ¯ -->
                    <h5 class="mb-3">
                        <i class="bi bi-info-circle me-2"></i>基本信息
                    </h5>
                    <div class="row mb-3">
                        <div class="col-md-6">
                            <label asp-for="Input.Id" class="form-label required">实例ID</label>
                            <input asp-for="Input.Id" class="form-control" placeholder="例如: plc-simulator-1" />
                            <span asp-validation-for="Input.Id" class="text-danger"></span>
                            <small class="text-muted">唯一标识符,只能包含字母、数字、下划线和连字符</small>
                        </div>
                        <div class="col-md-6">
                            <label asp-for="Input.Name" class="form-label required">实例名称</label>
                            <input asp-for="Input.Name" class="form-control" placeholder="例如: 1号PLC仿真器" />
                            <span asp-validation-for="Input.Name" class="text-danger"></span>
                        </div>
                    </div>
                    <div class="row mb-3">
                        <div class="col-md-6">
                            <label asp-for="Input.PLCType" class="form-label required">PLC型号</label>
                            <select asp-for="Input.PLCType" class="form-select">
                                <option value="0">S7-200 Smart</option>
                                <option value="1" selected>S7-1200</option>
                                <option value="2">S7-1500</option>
                                <option value="3">S7-300</option>
                                <option value="4">S7-400</option>
                            </select>
                            <span asp-validation-for="Input.PLCType" class="text-danger"></span>
                        </div>
                        <div class="col-md-6">
                            <label asp-for="Input.Port" class="form-label required">监听端口</label>
                            <input asp-for="Input.Port" class="form-control" type="number" min="1" max="65535" placeholder="102" />
                            <span asp-validation-for="Input.Port" class="text-danger"></span>
                            <small class="text-muted">S7默认端口为102</small>
                        </div>
                    </div>
                    <div class="row mb-3">
                        <div class="col-md-6">
                            <label asp-for="Input.ActivationKey" class="form-label">HSL激活码</label>
                            <input asp-for="Input.ActivationKey" class="form-control" placeholder="可选" />
                            <span asp-validation-for="Input.ActivationKey" class="text-danger"></span>
                            <small class="text-muted">用于HSL库的商业激活,留空使用免费版</small>
                        </div>
                        <div class="col-md-6">
                            <label asp-for="Input.AutoStart" class="form-label">自动启动</label>
                            <div class="form-check mt-2">
                                <input asp-for="Input.AutoStart" class="form-check-input" />
                                <label asp-for="Input.AutoStart" class="form-check-label">
                                    æœåŠ¡å™¨å¯åŠ¨æ—¶è‡ªåŠ¨å¯åŠ¨æ­¤å®žä¾‹
                                </label>
                            </div>
                        </div>
                    </div>
                    <hr class="my-4">
                    <!-- å†…存配置 -->
                    <div class="d-flex justify-content-between align-items-center mb-3">
                        <h5 class="mb-0">
                            <i class="bi bi-memory me-2"></i>内存配置
                        </h5>
                        <button type="button" class="btn btn-outline-secondary btn-sm" onclick="resetMemoryConfig()">
                            <i class="bi bi-arrow-counterclockwise me-1"></i>重置为默认值
                        </button>
                    </div>
                    <div class="row mb-3">
                        <div class="col-md-4">
                            <label asp-for="Input.MRegionSize" class="form-label">M区域大小</label>
                            <input asp-for="Input.MRegionSize" class="form-control" type="number" min="0" placeholder="1024" />
                            <span asp-validation-for="Input.MRegionSize" class="text-danger"></span>
                        </div>
                        <div class="col-md-4">
                            <label asp-for="Input.IRegionSize" class="form-label">I区域大小</label>
                            <input asp-for="Input.IRegionSize" class="form-control" type="number" min="0" placeholder="256" />
                            <span asp-validation-for="Input.IRegionSize" class="text-danger"></span>
                        </div>
                        <div class="col-md-4">
                            <label asp-for="Input.QRegionSize" class="form-label">Q区域大小</label>
                            <input asp-for="Input.QRegionSize" class="form-control" type="number" min="0" placeholder="256" />
                            <span asp-validation-for="Input.QRegionSize" class="text-danger"></span>
                        </div>
                    </div>
                    <div class="row mb-3">
                        <div class="col-md-4">
                            <label asp-for="Input.DBBlockCount" class="form-label">DB块数量</label>
                            <input asp-for="Input.DBBlockCount" class="form-control" type="number" min="0" placeholder="100" />
                            <span asp-validation-for="Input.DBBlockCount" class="text-danger"></span>
                        </div>
                        <div class="col-md-4">
                            <label asp-for="Input.DBBlockSize" class="form-label">DB块大小</label>
                            <input asp-for="Input.DBBlockSize" class="form-control" type="number" min="0" placeholder="1024" />
                            <span asp-validation-for="Input.DBBlockSize" class="text-danger"></span>
                        </div>
                        <div class="col-md-2">
                            <label asp-for="Input.TRegionCount" class="form-label">定时器数量</label>
                            <input asp-for="Input.TRegionCount" class="form-control" type="number" min="0" placeholder="64" />
                            <span asp-validation-for="Input.TRegionCount" class="text-danger"></span>
                        </div>
                        <div class="col-md-2">
                            <label asp-for="Input.CRegionCount" class="form-label">计数器数量</label>
                            <input asp-for="Input.CRegionCount" class="form-control" type="number" min="0" placeholder="64" />
                            <span asp-validation-for="Input.CRegionCount" class="text-danger"></span>
                        </div>
                    </div>
                    <hr class="my-4">
                    <!-- æäº¤æŒ‰é’® -->
                    <div class="d-flex justify-content-between">
                        <a asp-page="/Index" class="btn btn-secondary">
                            <i class="bi bi-x-lg me-1"></i>取消
                        </a>
                        <button type="submit" class="btn btn-primary">
                            <i class="bi bi-check-lg me-1"></i>创建实例
                        </button>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script>
    function resetMemoryConfig() {
        document.querySelector('#Input_MRegionSize').value = '1024';
        document.querySelector('#Input_IRegionSize').value = '256';
        document.querySelector('#Input_QRegionSize').value = '256';
        document.querySelector('#Input_DBBlockCount').value = '100';
        document.querySelector('#Input_DBBlockSize').value = '1024';
        document.querySelector('#Input_TRegionCount').value = '64';
        document.querySelector('#Input_CRegionCount').value = '64';
    }
</script>
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Create.cshtml.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,141 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;
using WIDESEAWCS_S7Simulator.Core.Entities;
using WIDESEAWCS_S7Simulator.Core.Enums;
namespace WIDESEAWCS_S7Simulator.Web.Pages;
/// <summary>
/// åˆ›å»ºå®žä¾‹é¡µ
/// </summary>
public class CreateModel : PageModel
{
    private readonly ILogger<CreateModel> _logger;
    private readonly HttpClient _httpClient;
    public CreateModel(ILogger<CreateModel> logger, IHttpClientFactory httpClientFactory)
    {
        _logger = logger;
        _httpClient = httpClientFactory.CreateClient();
        _httpClient.BaseAddress = new Uri($"{Request.Scheme}://{Request.Host}");
    }
    [BindProperty]
    public CreateInstanceInputModel Input { get; set; } = new();
    public void OnGet()
    {
        _logger.LogInformation("Loading create instance page");
    }
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }
        try
        {
            var config = new InstanceConfig
            {
                Id = Input.Id,
                Name = Input.Name,
                PLCType = Input.PLCType,
                Port = Input.Port,
                ActivationKey = Input.ActivationKey ?? string.Empty,
                AutoStart = Input.AutoStart,
                MemoryConfig = new MemoryRegionConfig
                {
                    MRegionSize = Input.MRegionSize > 0 ? Input.MRegionSize : 1024,
                    DBBlockCount = Input.DBBlockCount > 0 ? Input.DBBlockCount : 100,
                    DBBlockSize = Input.DBBlockSize > 0 ? Input.DBBlockSize : 1024,
                    IRegionSize = Input.IRegionSize > 0 ? Input.IRegionSize : 256,
                    QRegionSize = Input.QRegionSize > 0 ? Input.QRegionSize : 256,
                    TRegionCount = Input.TRegionCount > 0 ? Input.TRegionCount : 64,
                    CRegionCount = Input.CRegionCount > 0 ? Input.CRegionCount : 64
                }
            };
            var response = await _httpClient.PostAsJsonAsync("/api/SimulatorInstances", config);
            if (response.IsSuccessStatusCode)
            {
                _logger.LogInformation("Instance {InstanceId} created successfully", Input.Id);
                TempData["SuccessMessage"] = $"实例 \"{Input.Id}\" åˆ›å»ºæˆåŠŸ!";
                return RedirectToPage("/Index");
            }
            else if (response.StatusCode == System.Net.HttpStatusCode.Conflict)
            {
                ModelState.AddModelError(string.Empty, "实例ID已存在,请使用其他ID");
            }
            else
            {
                var error = await response.Content.ReadFromJsonAsync<object>();
                ModelState.AddModelError(string.Empty, error?.ToString() ?? "创建实例失败");
            }
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "Failed to create instance");
            ModelState.AddModelError(string.Empty, "网络错误,请稍后重试");
        }
        return Page();
    }
    /// <summary>
    /// åˆ›å»ºå®žä¾‹è¾“入模型
    /// </summary>
    public class CreateInstanceInputModel
    {
        /// <summary>
        /// å®žä¾‹ID
        /// </summary>
        [Required(ErrorMessage = "实例ID不能为空")]
        [StringLength(50, MinimumLength = 1, ErrorMessage = "实例ID长度必须在1-50个字符之间")]
        [RegularExpression("^[a-zA-Z0-9_-]+$", ErrorMessage = "实例ID只能包含字母、数字、下划线和连字符")]
        public string Id { get; set; } = string.Empty;
        /// <summary>
        /// å®žä¾‹åç§°
        /// </summary>
        [Required(ErrorMessage = "实例名称不能为空")]
        [StringLength(100, ErrorMessage = "实例名称不能超过100个字符")]
        public string Name { get; set; } = string.Empty;
        /// <summary>
        /// PLC型号
        /// </summary>
        [Required(ErrorMessage = "PLC型号不能为空")]
        public SiemensPLCType PLCType { get; set; } = SiemensPLCType.S71200;
        /// <summary>
        /// ç›‘听端口
        /// </summary>
        [Required(ErrorMessage = "端口不能为空")]
        [Range(1, 65535, ErrorMessage = "端口必须在1-65535之间")]
        public int Port { get; set; } = 102;
        /// <summary>
        /// HSL激活码
        /// </summary>
        [StringLength(200, ErrorMessage = "激活码不能超过200个字符")]
        public string? ActivationKey { get; set; }
        /// <summary>
        /// è‡ªåŠ¨å¯åŠ¨
        /// </summary>
        public bool AutoStart { get; set; } = false;
        // å†…存配置
        public int MRegionSize { get; set; }
        public int DBBlockCount { get; set; }
        public int DBBlockSize { get; set; }
        public int IRegionSize { get; set; }
        public int QRegionSize { get; set; }
        public int TRegionCount { get; set; }
        public int CRegionCount { get; set; }
    }
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Details.cshtml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,338 @@
@page
@model DetailsModel
@{
    ViewData["Title"] = $"实例详情 - {Model.Instance?.InstanceId}";
}
@if (Model.Instance == null)
{
    <div class="alert alert-warning" role="alert">
        <i class="bi bi-exclamation-triangle-fill me-2"></i>
        å®žä¾‹ä¸å­˜åœ¨æˆ–加载失败
    </div>
    <a asp-page="/Index" class="btn btn-primary">
        <i class="bi bi-arrow-left me-1"></i>返回列表
    </a>
}
else
{
    <div class="d-flex justify-content-between align-items-center mb-4">
        <div>
            <h2 class="mb-0">
                <i class="bi bi-info-circle me-2"></i>实例详情: @Model.Instance.InstanceId
            </h2>
            <p class="text-muted mb-0 mt-1">查看和管理仿真器实例的详细信息</p>
        </div>
        <div>
            <a asp-page="/Edit" asp-route-id="@Model.Instance.InstanceId" class="btn btn-primary me-2">
                <i class="bi bi-pencil-fill me-1"></i>编辑
            </a>
            <a asp-page="/Index" class="btn btn-secondary">
                <i class="bi bi-arrow-left me-1"></i>返回列表
            </a>
        </div>
    </div>
    <!-- çŠ¶æ€å¡ç‰‡ -->
    <div class="row mb-4">
        <div class="col-md-3">
            <div class="card text-center">
                <div class="card-body">
                    <h6 class="card-subtitle mb-2 text-muted">状态</h6>
                    <h4 class="card-title">
                        <span class="badge @GetStatusBadgeClass(Model.Instance.Status) fs-6">
                            @GetStatusText(Model.Instance.Status)
                        </span>
                    </h4>
                </div>
            </div>
        </div>
        <div class="col-md-3">
            <div class="card text-center">
                <div class="card-body">
                    <h6 class="card-subtitle mb-2 text-muted">连接客户端</h6>
                    <h4 class="card-title">
                        <i class="bi bi-people-fill text-primary me-1"></i>@Model.Instance.ClientCount
                    </h4>
                </div>
            </div>
        </div>
        <div class="col-md-3">
            <div class="card text-center">
                <div class="card-body">
                    <h6 class="card-subtitle mb-2 text-muted">总请求数</h6>
                    <h4 class="card-title">
                        <i class="bi bi-graph-up-arrow text-success me-1"></i>@Model.Instance.TotalRequests
                    </h4>
                </div>
            </div>
        </div>
        <div class="col-md-3">
            <div class="card text-center">
                <div class="card-body">
                    <h6 class="card-subtitle mb-2 text-muted">运行时间</h6>
                    <h4 class="card-title" id="runningTime">
                        @if (Model.Instance.StartTime.HasValue)
                        {
                            <span>@GetRunningTime(Model.Instance.StartTime.Value)</span>
                        }
                        else
                        {
                            <span class="text-muted">-</span>
                        }
                    </h4>
                </div>
            </div>
        </div>
    </div>
    <!-- å®žä¾‹ä¿¡æ¯ -->
    <div class="card mb-4">
        <div class="card-header">
            <h5 class="mb-0">
                <i class="bi bi-cpu me-2"></i>实例信息
            </h5>
        </div>
        <div class="card-body">
            <div class="row">
                <div class="col-md-6 mb-3">
                    <strong>实例ID:</strong>
                    <div class="text-muted">@Model.Instance.InstanceId</div>
                </div>
                <div class="col-md-6 mb-3">
                    <strong>启动时间:</strong>
                    <div class="text-muted">@(Model.Instance.StartTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-")</div>
                </div>
                <div class="col-md-6 mb-3">
                    <strong>最后活动:</strong>
                    <div class="text-muted">@(Model.Instance.LastActivityTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-")</div>
                </div>
                <div class="col-md-6 mb-3">
                    <strong>错误信息:</strong>
                    <div class="text-muted">@(Model.Instance.ErrorMessage ?? "无")</div>
                </div>
            </div>
        </div>
        <div class="card-footer">
            <div class="d-flex gap-2">
                @if (Model.Instance.Status == WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Running)
                {
                    <button class="btn btn-warning" onclick="stopInstance('@Model.Instance.InstanceId')">
                        <i class="bi bi-stop-fill me-1"></i>停止
                    </button>
                    <button class="btn btn-info" onclick="restartInstance('@Model.Instance.InstanceId')">
                        <i class="bi bi-arrow-clockwise me-1"></i>重启
                    </button>
                }
                else if (Model.Instance.Status == WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Stopped)
                {
                    <button class="btn btn-success" onclick="startInstance('@Model.Instance.InstanceId')">
                        <i class="bi bi-play-fill me-1"></i>启动
                    </button>
                }
            </div>
        </div>
    </div>
    <!-- è¿žæŽ¥çš„客户端 -->
    <div class="card">
        <div class="card-header d-flex justify-content-between align-items-center">
            <h5 class="mb-0">
                <i class="bi bi-people me-2"></i>连接的客户端
            </h5>
            <span class="badge bg-primary">@Model.Clients.Count</span>
        </div>
        <div class="card-body">
            @if (Model.Clients.Any())
            {
                <div class="table-responsive">
                    <table class="table table-striped table-hover client-table">
                        <thead>
                            <tr>
                                <th>客户端ID</th>
                                <th>远程地址</th>
                                <th>连接时间</th>
                                <th>最后活动</th>
                                <th>操作</th>
                            </tr>
                        </thead>
                        <tbody>
                            @foreach (var client in Model.Clients)
                            {
                                <tr>
                                    <td><code>@client.ClientId</code></td>
                                    <td>@client.RemoteEndPoint</td>
                                    <td>@client.ConnectedTime.ToString("yyyy-MM-dd HH:mm:ss")</td>
                                    <td>@client.LastActivityTime.ToString("yyyy-MM-dd HH:mm:ss")</td>
                                    <td>
                                        <button class="btn btn-sm btn-danger" onclick="disconnectClient('@Model.Instance.InstanceId', '@client.ClientId')">
                                            <i class="bi bi-x-circle-fill"></i>断开
                                        </button>
                                    </td>
                                </tr>
                            }
                        </tbody>
                    </table>
                </div>
            }
            else
            {
                <div class="text-center text-muted py-4">
                    <i class="bi bi-inbox" style="font-size: 2rem;"></i>
                    <p class="mt-2">暂无连接的客户端</p>
                </div>
            }
        </div>
    </div>
}
@{
    string GetStatusBadgeClass(WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus status)
    {
        return status switch
        {
            WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Running => "bg-success",
            WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Stopped => "bg-secondary",
            WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Starting => "bg-info",
            WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Stopping => "bg-warning text-dark",
            WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Error => "bg-danger",
            _ => "bg-secondary"
        };
    }
    string GetStatusText(WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus status)
    {
        return status switch
        {
            WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Running => "运行中",
            WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Stopped => "已停止",
            WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Starting => "启动中",
            WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Stopping => "停止中",
            WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Error => "错误",
            _ => status.ToString()
        };
    }
    string GetRunningTime(DateTime startTime)
    {
        var span = DateTime.Now - startTime;
        if (span.TotalDays >= 1)
            return $"{(int)span.TotalDays}天 {span.Hours}小时";
        else if (span.TotalHours >= 1)
            return $"{(int)span.TotalHours}小时 {span.Minutes}分钟";
        else if (span.TotalMinutes >= 1)
            return $"{(int)span.TotalMinutes}分钟 {span.Seconds}秒";
        else
            return $"{span.Seconds}秒";
    }
}
@section Scripts {
<script>
    // Auto refresh
    const instanceId = '@Model.Instance?.InstanceId';
    let refreshInterval = null;
    document.addEventListener('DOMContentLoaded', function () {
        if (instanceId) {
            startAutoRefresh();
            // Update running time every second
            setInterval(updateRunningTime, 1000);
        }
    });
    function startAutoRefresh() {
        refreshInterval = setInterval(async () => {
            try {
                const [instance, clients] = await Promise.all([
                    fetch(`${API_BASE_URL}/SimulatorInstances/${encodeURIComponent(instanceId)}`).then(r => r.json()),
                    fetch(`${API_BASE_URL}/instances/${encodeURIComponent(instanceId)}/Clients`).then(r => r.json())
                ]);
                // Update status badges
                updateInstanceStatus(instance);
            } catch (error) {
                console.error('Auto refresh failed:', error);
            }
        }, 5000);
    }
    function updateInstanceStatus(instance) {
        // You can add logic here to update UI elements without full page reload
        // For now, we rely on page refresh for major changes
    }
    function updateRunningTime() {
        const startTimeElement = document.querySelector('#runningTime span');
        if (startTimeElement && startTimeElement.textContent !== '-') {
            // This would require storing the start time and calculating
            // For simplicity, we'll skip the real-time update
        }
    }
    async function startInstance(id) {
        confirmAction(`确定要启动实例 "${id}" å—?`, async () => {
            try {
                await apiCall(`${API_BASE_URL}/SimulatorInstances/${encodeURIComponent(id)}/start`, {
                    method: 'POST'
                });
                showToast('启动命令已发送', 'success');
                setTimeout(() => location.reload(), 1000);
            } catch (error) {
                console.error('Failed to start instance:', error);
            }
        });
    }
    async function stopInstance(id) {
        confirmAction(`确定要停止实例 "${id}" å—?`, async () => {
            try {
                await apiCall(`${API_BASE_URL}/SimulatorInstances/${encodeURIComponent(id)}/stop`, {
                    method: 'POST'
                });
                showToast('停止命令已发送', 'success');
                setTimeout(() => location.reload(), 1000);
            } catch (error) {
                console.error('Failed to stop instance:', error);
            }
        });
    }
    async function restartInstance(id) {
        confirmAction(`确定要重启实例 "${id}" å—?`, async () => {
            try {
                await apiCall(`${API_BASE_URL}/SimulatorInstances/${encodeURIComponent(id)}/restart`, {
                    method: 'POST'
                });
                showToast('重启命令已发送', 'success');
                setTimeout(() => location.reload(), 1000);
            } catch (error) {
                console.error('Failed to restart instance:', error);
            }
        });
    }
    async function disconnectClient(instanceId, clientId) {
        confirmAction(`确定要断开客户端 "${clientId}" å—?`, async () => {
            try {
                await apiCall(`${API_BASE_URL}/instances/${encodeURIComponent(instanceId)}/Clients/${encodeURIComponent(clientId)}`, {
                    method: 'DELETE'
                });
                showToast('客户端已断开', 'success');
                setTimeout(() => location.reload(), 500);
            } catch (error) {
                console.error('Failed to disconnect client:', error);
            }
        });
    }
    // Stop auto-refresh when page is hidden
    document.addEventListener('visibilitychange', function () {
        if (document.hidden) {
            if (refreshInterval) clearInterval(refreshInterval);
        } else {
            startAutoRefresh();
        }
    });
</script>
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Details.cshtml.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using WIDESEAWCS_S7Simulator.Core.Entities;
namespace WIDESEAWCS_S7Simulator.Web.Pages;
/// <summary>
/// å®žä¾‹è¯¦æƒ…页
/// </summary>
public class DetailsModel : PageModel
{
    private readonly ILogger<DetailsModel> _logger;
    private readonly HttpClient _httpClient;
    public DetailsModel(ILogger<DetailsModel> logger, IHttpClientFactory httpClientFactory)
    {
        _logger = logger;
        _httpClient = httpClientFactory.CreateClient();
        _httpClient.BaseAddress = new Uri($"{Request.Scheme}://{Request.Host}");
    }
    public InstanceState? Instance { get; private set; }
    public List<S7ClientConnection> Clients { get; private set; } = new();
    public async Task<IActionResult> OnGetAsync(string id)
    {
        if (string.IsNullOrWhiteSpace(id))
        {
            return BadRequest("实例ID不能为空");
        }
        try
        {
            var response = await _httpClient.GetAsync($"/api/SimulatorInstances/{Uri.EscapeDataString(id)}");
            if (response.IsSuccessStatusCode)
            {
                Instance = await response.Content.ReadFromJsonAsync<InstanceState>();
                // Load clients
                var clientsResponse = await _httpClient.GetAsync($"/api/instances/{Uri.EscapeDataString(id)}/Clients");
                if (clientsResponse.IsSuccessStatusCode)
                {
                    Clients = await clientsResponse.Content.ReadFromJsonAsync<List<S7ClientConnection>>() ?? new();
                }
            }
            else if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
            {
                return NotFound($"实例 \"{id}\" ä¸å­˜åœ¨");
            }
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "Failed to load instance details for {InstanceId}", id);
            ModelState.AddModelError(string.Empty, "加载实例详情失败,请稍后重试");
        }
        return Page();
    }
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Edit.cshtml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,245 @@
@page
@model EditModel
@{
    ViewData["Title"] = $"编辑实例 - {Model.Input.Id}";
}
<div class="row justify-content-center">
    <div class="col-lg-8">
        <div class="card shadow">
            <div class="card-header bg-warning text-dark">
                <h4 class="mb-0">
                    <i class="bi bi-pencil-square me-2"></i>编辑实例: @Model.Input.Id
                </h4>
            </div>
            <div class="card-body">
                @if (Model.IsRunning)
                {
                    <div class="alert alert-warning d-flex align-items-center" role="alert">
                        <i class="bi bi-exclamation-triangle-fill flex-shrink-0 me-2"></i>
                        <div>
                            <strong>警告:</strong> å®žä¾‹æ­£åœ¨è¿è¡Œä¸­ã€‚某些配置修改可能需要重启实例才能生效。
                        </div>
                    </div>
                }
                <form method="post" id="editForm">
                    <div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div>
                    <!-- åŸºæœ¬ä¿¡æ¯ -->
                    <h5 class="mb-3">
                        <i class="bi bi-info-circle me-2"></i>基本信息
                    </h5>
                    <div class="row mb-3">
                        <div class="col-md-6">
                            <label class="form-label">实例ID</label>
                            <input type="text" class="form-control" value="@Model.Input.Id" disabled />
                            <small class="text-muted">实例ID不可修改</small>
                        </div>
                        <div class="col-md-6">
                            <label asp-for="Input.Name" class="form-label required">实例名称</label>
                            <input asp-for="Input.Name" class="form-control" />
                            <span asp-validation-for="Input.Name" class="text-danger"></span>
                        </div>
                    </div>
                    <div class="row mb-3">
                        <div class="col-md-6">
                            <label asp-for="Input.PLCType" class="form-label required">PLC型号</label>
                            @if (Model.IsRunning)
                            {
                                <select asp-for="Input.PLCType" class="form-select" disabled>
                                    <option value="0">S7-200 Smart</option>
                                    <option value="1">S7-1200</option>
                                    <option value="2">S7-1500</option>
                                    <option value="3">S7-300</option>
                                    <option value="4">S7-400</option>
                                </select>
                                <small class="text-muted">运行中不可修改</small>
                            }
                            else
                            {
                                <select asp-for="Input.PLCType" class="form-select">
                                    <option value="0">S7-200 Smart</option>
                                    <option value="1">S7-1200</option>
                                    <option value="2">S7-1500</option>
                                    <option value="3">S7-300</option>
                                    <option value="4">S7-400</option>
                                </select>
                            }
                            <span asp-validation-for="Input.PLCType" class="text-danger"></span>
                        </div>
                        <div class="col-md-6">
                            <label asp-for="Input.Port" class="form-label required">监听端口</label>
                            @if (Model.IsRunning)
                            {
                                <input asp-for="Input.Port" class="form-control" type="number" min="1" max="65535" disabled />
                                <small class="text-muted">运行中不可修改</small>
                            }
                            else
                            {
                                <input asp-for="Input.Port" class="form-control" type="number" min="1" max="65535" />
                            }
                            <span asp-validation-for="Input.Port" class="text-danger"></span>
                        </div>
                    </div>
                    <div class="row mb-3">
                        <div class="col-md-6">
                            <label asp-for="Input.ActivationKey" class="form-label">HSL激活码</label>
                            <input asp-for="Input.ActivationKey" class="form-control" placeholder="可选" />
                            <span asp-validation-for="Input.ActivationKey" class="text-danger"></span>
                        </div>
                        <div class="col-md-6">
                            <label asp-for="Input.AutoStart" class="form-label">自动启动</label>
                            <div class="form-check mt-2">
                                <input asp-for="Input.AutoStart" class="form-check-input" />
                                <label asp-for="Input.AutoStart" class="form-check-label">
                                    æœåŠ¡å™¨å¯åŠ¨æ—¶è‡ªåŠ¨å¯åŠ¨æ­¤å®žä¾‹
                                </label>
                            </div>
                        </div>
                    </div>
                    <hr class="my-4">
                    <!-- å†…存配置 -->
                    <div class="d-flex justify-content-between align-items-center mb-3">
                        <h5 class="mb-0">
                            <i class="bi bi-memory me-2"></i>内存配置
                        </h5>
                        <button type="button" class="btn btn-outline-secondary btn-sm" onclick="resetMemoryConfig()">
                            <i class="bi bi-arrow-counterclockwise me-1"></i>重置为默认值
                        </button>
                    </div>
                    <div class="row mb-3">
                        <div class="col-md-4">
                            <label asp-for="Input.MRegionSize" class="form-label">M区域大小</label>
                            <input asp-for="Input.MRegionSize" class="form-control" type="number" min="0" placeholder="1024" />
                            <span asp-validation-for="Input.MRegionSize" class="text-danger"></span>
                        </div>
                        <div class="col-md-4">
                            <label asp-for="Input.IRegionSize" class="form-label">I区域大小</label>
                            <input asp-for="Input.IRegionSize" class="form-control" type="number" min="0" placeholder="256" />
                            <span asp-validation-for="Input.IRegionSize" class="text-danger"></span>
                        </div>
                        <div class="col-md-4">
                            <label asp-for="Input.QRegionSize" class="form-label">Q区域大小</label>
                            <input asp-for="Input.QRegionSize" class="form-control" type="number" min="0" placeholder="256" />
                            <span asp-validation-for="Input.QRegionSize" class="text-danger"></span>
                        </div>
                    </div>
                    <div class="row mb-3">
                        <div class="col-md-4">
                            <label asp-for="Input.DBBlockCount" class="form-label">DB块数量</label>
                            <input asp-for="Input.DBBlockCount" class="form-control" type="number" min="0" placeholder="100" />
                            <span asp-validation-for="Input.DBBlockCount" class="text-danger"></span>
                        </div>
                        <div class="col-md-4">
                            <label asp-for="Input.DBBlockSize" class="form-label">DB块大小</label>
                            <input asp-for="Input.DBBlockSize" class="form-control" type="number" min="0" placeholder="1024" />
                            <span asp-validation-for="Input.DBBlockSize" class="text-danger"></span>
                        </div>
                        <div class="col-md-2">
                            <label asp-for="Input.TRegionCount" class="form-label">定时器数量</label>
                            <input asp-for="Input.TRegionCount" class="form-control" type="number" min="0" placeholder="64" />
                            <span asp-validation-for="Input.TRegionCount" class="text-danger"></span>
                        </div>
                        <div class="col-md-2">
                            <label asp-for="Input.CRegionCount" class="form-label">计数器数量</label>
                            <input asp-for="Input.CRegionCount" class="form-control" type="number" min="0" placeholder="64" />
                            <span asp-validation-for="Input.CRegionCount" class="text-danger"></span>
                        </div>
                    </div>
                    <hr class="my-4">
                    <!-- æäº¤æŒ‰é’® -->
                    <div class="d-flex justify-content-between">
                        <a asp-page="/Index" class="btn btn-secondary">
                            <i class="bi bi-x-lg me-1"></i>取消
                        </a>
                        <button type="submit" class="btn btn-primary">
                            <i class="bi bi-check-lg me-1"></i>保存修改
                        </button>
                    </div>
                </form>
            </div>
        </div>
        <!-- å½“前状态卡片 -->
        @if (Model.CurrentInstance != null)
        {
            <div class="card mt-4">
                <div class="card-header">
                    <h6 class="mb-0">
                        <i class="bi bi-activity me-2"></i>当前状态
                    </h6>
                </div>
                <div class="card-body">
                    <div class="row">
                        <div class="col-md-4">
                            <strong>状态:</strong>
                            <span class="badge @GetStatusBadgeClass(Model.CurrentInstance.Status)">
                                @GetStatusText(Model.CurrentInstance.Status)
                            </span>
                        </div>
                        <div class="col-md-4">
                            <strong>客户端数:</strong> @Model.CurrentInstance.ClientCount
                        </div>
                        <div class="col-md-4">
                            <strong>总请求数:</strong> @Model.CurrentInstance.TotalRequests
                        </div>
                    </div>
                </div>
            </div>
        }
    </div>
</div>
@{
    string GetStatusBadgeClass(WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus status)
    {
        return status switch
        {
            WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Running => "bg-success",
            WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Stopped => "bg-secondary",
            WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Starting => "bg-info",
            WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Stopping => "bg-warning",
            WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Error => "bg-danger",
            _ => "bg-secondary"
        };
    }
    string GetStatusText(WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus status)
    {
        return status switch
        {
            WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Running => "运行中",
            WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Stopped => "已停止",
            WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Starting => "启动中",
            WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Stopping => "停止中",
            WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Error => "错误",
            _ => status.ToString()
        };
    }
}
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script>
    function resetMemoryConfig() {
        document.querySelector('#Input_MRegionSize').value = '1024';
        document.querySelector('#Input_IRegionSize').value = '256';
        document.querySelector('#Input_QRegionSize').value = '256';
        document.querySelector('#Input_DBBlockCount').value = '100';
        document.querySelector('#Input_DBBlockSize').value = '1024';
        document.querySelector('#Input_TRegionCount').value = '64';
        document.querySelector('#Input_CRegionCount').value = '64';
    }
</script>
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Edit.cshtml.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,182 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;
using WIDESEAWCS_S7Simulator.Core.Entities;
using WIDESEAWCS_S7Simulator.Core.Enums;
namespace WIDESEAWCS_S7Simulator.Web.Pages;
/// <summary>
/// ç¼–辑实例页
/// </summary>
public class EditModel : PageModel
{
    private readonly ILogger<EditModel> _logger;
    private readonly HttpClient _httpClient;
    public EditModel(ILogger<EditModel> logger, IHttpClientFactory httpClientFactory)
    {
        _logger = logger;
        _httpClient = httpClientFactory.CreateClient();
        _httpClient.BaseAddress = new Uri($"{Request.Scheme}://{Request.Host}");
    }
    [BindProperty]
    public EditInstanceInputModel Input { get; set; } = new();
    public InstanceState? CurrentInstance { get; private set; }
    public bool IsRunning { get; private set; }
    public async Task<IActionResult> OnGetAsync(string id)
    {
        if (string.IsNullOrWhiteSpace(id))
        {
            return BadRequest("实例ID不能为空");
        }
        try
        {
            var response = await _httpClient.GetAsync($"/api/SimulatorInstances/{Uri.EscapeDataString(id)}");
            if (response.IsSuccessStatusCode)
            {
                CurrentInstance = await response.Content.ReadFromJsonAsync<InstanceState>();
                IsRunning = CurrentInstance?.Status == InstanceStatus.Running;
                // Load existing config
                var configResponse = await _httpClient.GetAsync($"/api/SimulatorInstances");
                if (configResponse.IsSuccessStatusCode)
                {
                    var allInstances = await configResponse.Content.ReadFromJsonAsync<List<InstanceState>>();
                    // For now, we'll initialize with defaults. In production, you'd have a separate endpoint to get instance config
                }
                // Initialize input with current values (default for now)
                Input.Id = id;
                Input.Name = CurrentInstance?.InstanceId ?? id;
                Input.Port = 102; // Default
            }
            else if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
            {
                return NotFound($"实例 \"{id}\" ä¸å­˜åœ¨");
            }
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "Failed to load instance {InstanceId}", id);
            ModelState.AddModelError(string.Empty, "加载实例失败,请稍后重试");
        }
        return Page();
    }
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            // Reload instance state
            await LoadInstanceStateAsync(Input.Id);
            return Page();
        }
        try
        {
            var config = new InstanceConfig
            {
                Id = Input.Id,
                Name = Input.Name,
                PLCType = Input.PLCType,
                Port = Input.Port,
                ActivationKey = Input.ActivationKey ?? string.Empty,
                AutoStart = Input.AutoStart,
                MemoryConfig = new MemoryRegionConfig
                {
                    MRegionSize = Input.MRegionSize > 0 ? Input.MRegionSize : 1024,
                    DBBlockCount = Input.DBBlockCount > 0 ? Input.DBBlockCount : 100,
                    DBBlockSize = Input.DBBlockSize > 0 ? Input.DBBlockSize : 1024,
                    IRegionSize = Input.IRegionSize > 0 ? Input.IRegionSize : 256,
                    QRegionSize = Input.QRegionSize > 0 ? Input.QRegionSize : 256,
                    TRegionCount = Input.TRegionCount > 0 ? Input.TRegionCount : 64,
                    CRegionCount = Input.CRegionCount > 0 ? Input.CRegionCount : 64
                }
            };
            var response = await _httpClient.PutAsJsonAsync($"/api/SimulatorInstances/{Uri.EscapeDataString(Input.Id)}", config);
            if (response.IsSuccessStatusCode)
            {
                _logger.LogInformation("Instance {InstanceId} updated successfully", Input.Id);
                TempData["SuccessMessage"] = $"实例 \"{Input.Id}\" æ›´æ–°æˆåŠŸ!";
                return RedirectToPage("/Index");
            }
            else if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
            {
                ModelState.AddModelError(string.Empty, "实例不存在");
            }
            else
            {
                var error = await response.Content.ReadFromJsonAsync<object>();
                ModelState.AddModelError(string.Empty, error?.ToString() ?? "更新实例失败");
            }
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "Failed to update instance");
            ModelState.AddModelError(string.Empty, "网络错误,请稍后重试");
        }
        await LoadInstanceStateAsync(Input.Id);
        return Page();
    }
    private async Task LoadInstanceStateAsync(string id)
    {
        try
        {
            var response = await _httpClient.GetAsync($"/api/SimulatorInstances/{Uri.EscapeDataString(id)}");
            if (response.IsSuccessStatusCode)
            {
                CurrentInstance = await response.Content.ReadFromJsonAsync<InstanceState>();
                IsRunning = CurrentInstance?.Status == InstanceStatus.Running;
            }
        }
        catch
        {
            // Ignore errors when reloading state
        }
    }
    /// <summary>
    /// ç¼–辑实例输入模型
    /// </summary>
    public class EditInstanceInputModel
    {
        public string Id { get; set; } = string.Empty;
        [Required(ErrorMessage = "实例名称不能为空")]
        [StringLength(100, ErrorMessage = "实例名称不能超过100个字符")]
        public string Name { get; set; } = string.Empty;
        [Required(ErrorMessage = "PLC型号不能为空")]
        public SiemensPLCType PLCType { get; set; } = SiemensPLCType.S71200;
        [Required(ErrorMessage = "端口不能为空")]
        [Range(1, 65535, ErrorMessage = "端口必须在1-65535之间")]
        public int Port { get; set; } = 102;
        [StringLength(200, ErrorMessage = "激活码不能超过200个字符")]
        public string? ActivationKey { get; set; }
        public bool AutoStart { get; set; } = false;
        // å†…存配置
        public int MRegionSize { get; set; }
        public int DBBlockCount { get; set; }
        public int DBBlockSize { get; set; }
        public int IRegionSize { get; set; }
        public int QRegionSize { get; set; }
        public int TRegionCount { get; set; }
        public int CRegionCount { get; set; }
    }
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Index.cshtml
@@ -1,10 +1,226 @@
@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
    ViewData["Title"] = "实例列表";
}
<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
<div class="d-flex justify-content-between align-items-center mb-4">
    <div>
        <h2 class="mb-0">
            <i class="bi bi-cpu-fill me-2"></i>S7 PLC ä»¿çœŸå™¨å®žä¾‹
        </h2>
        <p class="text-muted mb-0 mt-1">管理和监控 S7 PLC ä»¿çœŸå™¨å®žä¾‹</p>
    </div>
    <a asp-page="/Create" class="btn btn-primary">
        <i class="bi bi-plus-lg me-1"></i>创建实例
    </a>
</div>
<div id="instancesContainer">
    <!-- Loading state -->
    <div id="loadingState" class="text-center py-5">
        <div class="spinner-border text-primary" role="status">
            <span class="visually-hidden">加载中...</span>
        </div>
        <p class="mt-3 text-muted">正在加载实例列表...</p>
    </div>
    <!-- Empty state -->
    <div id="emptyState" class="empty-state d-none">
        <i class="bi bi-inbox"></i>
        <h3>暂无实例</h3>
        <p>点击上方"创建实例"按钮来创建您的第一个仿真器实例</p>
    </div>
    <!-- Instances grid -->
    <div id="instancesGrid" class="row row-cols-1 row-cols-md-2 row-cols-xl-3 g-4 d-none"></div>
</div>
@section Scripts {
<script>
    let autoRefreshInterval = null;
    document.addEventListener('DOMContentLoaded', function () {
        loadInstances();
        startAutoRefresh(loadInstances, 5000);
    });
    async function loadInstances() {
        try {
            const instances = await apiCall(`${API_BASE_URL}/SimulatorInstances`);
            renderInstances(instances);
        } catch (error) {
            console.error('Failed to load instances:', error);
            document.getElementById('loadingState').classList.add('d-none');
            document.getElementById('emptyState').classList.remove('d-none');
        }
    }
    function renderInstances(instances) {
        const loadingState = document.getElementById('loadingState');
        const emptyState = document.getElementById('emptyState');
        const grid = document.getElementById('instancesGrid');
        loadingState.classList.add('d-none');
        grid.classList.add('d-none');
        emptyState.classList.add('d-none');
        if (!instances || instances.length === 0) {
            emptyState.classList.remove('d-none');
            return;
        }
        grid.classList.remove('d-none');
        grid.innerHTML = instances.map(instance => createInstanceCard(instance)).join('');
    }
    function createInstanceCard(instance) {
        const statusClass = getStatusClass(instance.status);
        const statusText = getStatusText(instance.status);
        const plcTypeText = getPlcTypeText(instance.plcType);
        const isRunning = instance.status === 'Running';
        const isStopped = instance.status === 'Stopped';
        return `
            <div class="col">
                <div class="card instance-card h-100 ${statusClass}">
                    <div class="card-header d-flex justify-content-between align-items-center">
                        <h5 class="card-title mb-0">${escapeHtml(instance.instanceId)}</h5>
                        <span class="badge ${statusClass}">${statusText}</span>
                    </div>
                    <div class="card-body">
                        <div class="instance-info mb-3">
                            <div class="row mb-2">
                                <div class="col-6">
                                    <small class="instance-info-label">名称</small>
                                    <div class="instance-info-value">${escapeHtml(instance.name || '-')}</div>
                                </div>
                                <div class="col-6">
                                    <small class="instance-info-label">PLC型号</small>
                                    <div class="instance-info-value">${plcTypeText}</div>
                                </div>
                            </div>
                            <div class="row mb-2">
                                <div class="col-6">
                                    <small class="instance-info-label">端口</small>
                                    <div class="instance-info-value">${instance.port || '-'}</div>
                                </div>
                                <div class="col-6">
                                    <small class="instance-info-label">客户端</small>
                                    <div class="instance-info-value">
                                        <i class="bi bi-people-fill me-1"></i>${instance.clientCount || 0}
                                    </div>
                                </div>
                            </div>
                            ${instance.startTime ? `
                            <div class="row">
                                <div class="col-12">
                                    <small class="instance-info-label">启动时间</small>
                                    <div class="instance-info-value small">${formatDate(instance.startTime)}</div>
                                </div>
                            </div>
                            ` : ''}
                            ${instance.errorMessage ? `
                            <div class="alert alert-danger alert-sm mt-2 mb-0 py-2 small">
                                <i class="bi bi-exclamation-triangle-fill me-1"></i>${escapeHtml(instance.errorMessage)}
                            </div>
                            ` : ''}
                        </div>
                    </div>
                    <div class="card-footer bg-white">
                        <div class="action-buttons d-flex gap-2">
                            ${isRunning ? `
                                <button class="btn btn-warning btn-sm flex-fill" onclick="stopInstance('${instance.instanceId}')" ${instance.status === 'Stopping' ? 'disabled' : ''}>
                                    <i class="bi bi-stop-fill me-1"></i>停止
                                </button>
                            ` : ''}
                            ${isStopped ? `
                                <button class="btn btn-success btn-sm flex-fill" onclick="startInstance('${instance.instanceId}')" ${instance.status === 'Starting' ? 'disabled' : ''}>
                                    <i class="bi bi-play-fill me-1"></i>启动
                                </button>
                            ` : ''}
                            <a href="/Details?id=${encodeURIComponent(instance.instanceId)}" class="btn btn-info btn-sm text-white flex-fill">
                                <i class="bi bi-info-circle-fill me-1"></i>详情
                            </a>
                            <a href="/Edit?id=${encodeURIComponent(instance.instanceId)}" class="btn btn-primary btn-sm flex-fill">
                                <i class="bi bi-pencil-fill me-1"></i>编辑
                            </a>
                            <button class="btn btn-danger btn-sm" onclick="deleteInstance('${instance.instanceId}')">
                                <i class="bi bi-trash-fill"></i>
                            </button>
                        </div>
                    </div>
                </div>
            </div>
        `;
    }
    function getStatusClass(status) {
        const map = {
            'Stopped': 'status-stopped',
            'Starting': 'status-starting',
            'Running': 'status-running',
            'Stopping': 'status-stopping',
            'Error': 'status-error'
        };
        return map[status] || '';
    }
    async function startInstance(id) {
        confirmAction(`确定要启动实例 "${id}" å—?`, async () => {
            try {
                await apiCall(`${API_BASE_URL}/SimulatorInstances/${encodeURIComponent(id)}/start`, {
                    method: 'POST'
                });
                showToast(`实例 "${id}" å¯åŠ¨å‘½ä»¤å·²å‘é€`, 'success');
                setTimeout(() => loadInstances(), 1000);
            } catch (error) {
                console.error('Failed to start instance:', error);
            }
        });
    }
    async function stopInstance(id) {
        confirmAction(`确定要停止实例 "${id}" å—?`, async () => {
            try {
                await apiCall(`${API_BASE_URL}/SimulatorInstances/${encodeURIComponent(id)}/stop`, {
                    method: 'POST'
                });
                showToast(`实例 "${id}" åœæ­¢å‘½ä»¤å·²å‘送`, 'success');
                setTimeout(() => loadInstances(), 1000);
            } catch (error) {
                console.error('Failed to stop instance:', error);
            }
        });
    }
    async function deleteInstance(id) {
        confirmAction(`确定要删除实例 "${id}" å—?此操作不可撤销!`, async () => {
            try {
                await apiCall(`${API_BASE_URL}/SimulatorInstances/${encodeURIComponent(id)}?deleteConfig=true`, {
                    method: 'DELETE'
                });
                showToast(`实例 "${id}" å·²åˆ é™¤`, 'success');
                loadInstances();
            } catch (error) {
                console.error('Failed to delete instance:', error);
            }
        });
    }
    function escapeHtml(text) {
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
    }
    // Stop auto-refresh when page is hidden
    document.addEventListener('visibilitychange', function () {
        if (document.hidden) {
            stopAutoRefresh();
        } else {
            startAutoRefresh(loadInstances, 5000);
        }
    });
</script>
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Index.cshtml.cs
@@ -1,8 +1,10 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace WIDESEAWCS_S7Simulator.Web.Pages;
/// <summary>
/// å®žä¾‹åˆ—表页
/// </summary>
public class IndexModel : PageModel
{
    private readonly ILogger<IndexModel> _logger;
@@ -14,6 +16,6 @@
    public void OnGet()
    {
        _logger.LogInformation("Loading instance list page");
    }
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Shared/_Layout.cshtml
@@ -1,18 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - WIDESEAWCS_S7Simulator.Web</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <title>@ViewData["Title"] - S7 PLC Simulator</title>
    <!-- Bootstrap 5 CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <!-- Bootstrap Icons -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
    <!-- Custom CSS -->
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/WIDESEAWCS_S7Simulator.Web.styles.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-page="/Index">WIDESEAWCS_S7Simulator.Web</a>
        <nav class="navbar navbar-expand-sm navbar-dark bg-primary shadow-sm mb-4">
            <div class="container-fluid">
                <a class="navbar-brand" asp-page="/Index">
                    <i class="bi bi-cpu-fill me-2"></i>S7 PLC Simulator
                </a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
@@ -20,32 +28,37 @@
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
                            <a class="nav-link" asp-page="/Index">
                                <i class="bi bi-house-door me-1"></i>实例列表
                            </a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
    <div class="container-fluid px-4">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>
    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2026 - WIDESEAWCS_S7Simulator.Web - <a asp-area="" asp-page="/Privacy">Privacy</a>
    <footer class="border-top py-3 mt-4 bg-light">
        <div class="container-fluid text-center text-muted">
            <small>&copy; 2026 - S7 PLC Simulator Management UI</small>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <!-- Bootstrap 5 JS Bundle -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
    <!-- Alpine.js -->
    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
    <!-- Custom JS -->
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>
</html>
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Shared/_ValidationScriptsPartial.cshtml
@@ -1,2 +1,14 @@
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
<environment include="Development">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.5/jquery.validate.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.12/jquery.validate.unobtrusive.min.js"></script>
</environment>
<environment exclude="Development">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.5/jquery.validate.min.js"
            asp-fallback-src="~/lib/jquery-validation/dist/jquery.validate.min.js"
            asp-fallback-test="window.jQuery && window.jQuery.validator">
    </script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.12/jquery.validate.unobtrusive.min.js"
            asp-fallback-src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"
            asp-fallback-test="window.jQuery && window.jQuery.validator && window.jQuery.validator.unobtrusive">
    </script>
</environment>
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/_ViewImports.cshtml
@@ -1,3 +1,5 @@
@using WIDESEAWCS_S7Simulator.Web
@using WIDESEAWCS_S7Simulator.Core.Entities
@using WIDESEAWCS_S7Simulator.Core.Enums
@namespace WIDESEAWCS_S7Simulator.Web.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/wwwroot/css/site.css
@@ -1,18 +1,126 @@
html {
  font-size: 14px;
}
@media (min-width: 768px) {
  html {
    font-size: 16px;
  }
}
html {
  position: relative;
  min-height: 100%;
}
body {
  margin-bottom: 60px;
  background-color: #f8f9fa;
}
/* Status Badge Colors */
.status-stopped {
  background-color: #6c757d;
}
.status-starting {
  background-color: #0dcaf0;
}
.status-running {
  background-color: #198754;
}
.status-stopping {
  background-color: #ffc107;
  color: #000;
}
.status-error {
  background-color: #dc3545;
}
/* Instance Card Styles */
.instance-card {
  transition: transform 0.2s, box-shadow 0.2s;
  border-left: 4px solid transparent;
}
.instance-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.instance-card.status-running {
  border-left-color: #198754;
}
.instance-card.status-stopped {
  border-left-color: #6c757d;
}
.instance-card.status-error {
  border-left-color: #dc3545;
}
.instance-card.status-starting,
.instance-card.status-stopping {
  border-left-color: #ffc107;
}
/* Instance Info Section */
.instance-info-label {
  font-weight: 600;
  color: #495057;
}
.instance-info-value {
  color: #212529;
}
/* Action Buttons */
.action-buttons .btn {
  min-width: 80px;
}
/* Loading Spinner */
.spinner-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 9999;
}
/* Memory Region Table */
.memory-table th {
  background-color: #e9ecef;
  font-weight: 600;
}
/* Client Table */
.client-table {
  font-size: 0.9rem;
}
/* Toast Container */
.toast-container {
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 9999;
}
/* Empty State */
.empty-state {
  text-align: center;
  padding: 60px 20px;
  color: #6c757d;
}
.empty-state i {
  font-size: 4rem;
  margin-bottom: 20px;
  opacity: 0.5;
}
/* Form Styles */
.form-label.required::after {
  content: " *";
  color: #dc3545;
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/wwwroot/js/site.js
@@ -1,4 +1,192 @@
// Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
// for details on configuring this project to bundle and minify static web assets.
// Write your JavaScript code.
// API Base URL
const API_BASE_URL = '/api';
// Show toast notification
function showToast(message, type = 'info') {
    const toastContainer = document.getElementById('toastContainer') || createToastContainer();
    const toastId = 'toast-' + Date.now();
    const bgClass = {
        'success': 'bg-success',
        'danger': 'bg-danger',
        'warning': 'bg-warning',
        'info': 'bg-info',
        'error': 'bg-danger'
    }[type] || 'bg-info';
    const toastHtml = `
        <div id="${toastId}" class="toast align-items-center text-white ${bgClass} border-0" role="alert" aria-live="assertive" aria-atomic="true">
            <div class="d-flex">
                <div class="toast-body">
                    ${message}
                </div>
                <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
            </div>
        </div>
    `;
    toastContainer.insertAdjacentHTML('beforeend', toastHtml);
    const toastElement = document.getElementById(toastId);
    const toast = new bootstrap.Toast(toastElement, { delay: 3000 });
    toast.show();
    toastElement.addEventListener('hidden.bs.toast', () => {
        toastElement.remove();
    });
}
function createToastContainer() {
    const container = document.createElement('div');
    container.id = 'toastContainer';
    container.className = 'toast-container';
    document.body.appendChild(container);
    return container;
}
// Show loading spinner
function showLoading() {
    const existingOverlay = document.getElementById('loadingOverlay');
    if (existingOverlay) return;
    const overlay = document.createElement('div');
    overlay.id = 'loadingOverlay';
    overlay.className = 'spinner-overlay';
    overlay.innerHTML = `
        <div class="spinner-border text-light" style="width: 3rem; height: 3rem;" role="status">
            <span class="visually-hidden">Loading...</span>
        </div>
    `;
    document.body.appendChild(overlay);
}
// Hide loading spinner
function hideLoading() {
    const overlay = document.getElementById('loadingOverlay');
    if (overlay) {
        overlay.remove();
    }
}
// API call helper
async function apiCall(url, options = {}) {
    try {
        showLoading();
        const response = await fetch(url, {
            ...options,
            headers: {
                'Content-Type': 'application/json',
                ...options.headers
            }
        });
        if (!response.ok) {
            const errorData = await response.json().catch(() => ({ error: response.statusText }));
            throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
        }
        return await response.json();
    } catch (error) {
        showToast(error.message, 'error');
        throw error;
    } finally {
        hideLoading();
    }
}
// Format date
function formatDate(dateString) {
    if (!dateString) return '-';
    const date = new Date(dateString);
    return date.toLocaleString('zh-CN', {
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
        hour: '2-digit',
        minute: '2-digit',
        second: '2-digit'
    });
}
// Get status badge class
function getStatusBadgeClass(status) {
    const statusMap = {
        'Stopped': 'status-stopped',
        'Starting': 'status-starting',
        'Running': 'status-running',
        'Stopping': 'status-stopping',
        'Error': 'status-error'
    };
    return statusMap[status] || 'bg-secondary';
}
// Get status text
function getStatusText(status) {
    const statusMap = {
        'Stopped': '已停止',
        'Starting': '启动中',
        'Running': '运行中',
        'Stopping': '停止中',
        'Error': '错误'
    };
    return statusMap[status] || status;
}
// Get PLC type text
function getPlcTypeText(plcType) {
    const plcTypeMap = {
        'S7200Smart': 'S7-200 Smart',
        'S71200': 'S7-1200',
        'S71500': 'S7-1500',
        'S7300': 'S7-300',
        'S7400': 'S7-400'
    };
    return plcTypeMap[plcType] || plcType;
}
// Confirm action
function confirmAction(message, callback) {
    if (confirm(message)) {
        callback();
    }
}
// Redirect with delay
function redirectWithDelay(url, delay = 1500) {
    setTimeout(() => {
        window.location.href = url;
    }, delay);
}
// Format bytes
function formatBytes(bytes, decimals = 2) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const dm = decimals < 0 ? 0 : decimals;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
// Auto-refresh interval handler
let refreshInterval = null;
function startAutoRefresh(callback, intervalMs = 5000) {
    stopAutoRefresh();
    callback(); // Initial call
    refreshInterval = setInterval(callback, intervalMs);
}
function stopAutoRefresh() {
    if (refreshInterval) {
        clearInterval(refreshInterval);
        refreshInterval = null;
    }
}
// Page unload handler
window.addEventListener('beforeunload', () => {
    stopAutoRefresh();
});