From 58376d519aeb76ef78d38b737c0c57f8d982fb52 Mon Sep 17 00:00:00 2001
From: wanshenmean <cathay_xy@163.com>
Date: 星期五, 13 三月 2026 14:29:01 +0800
Subject: [PATCH] feat: 完成S7 PLC模拟器完整实现
---
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Create.cshtml | 160 ++
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Interfaces/ISimulatorInstanceManager.cs | 133 ++
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Manager/SimulatorInstanceManager.cs | 474 +++++++
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/wwwroot/css/site.css | 128 +
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Controllers/SimulatorInstancesController.cs | 375 ++++++
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Details.cshtml.cs | 61 +
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Edit.cshtml.cs | 182 +++
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Controllers/MemoryController.cs | 219 +++
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/wwwroot/js/site.js | 190 +++
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Index.cshtml | 224 +++
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Shared/_Layout.cshtml | 51
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Interfaces/IS7ServerInstance.cs | 66 +
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Server/S7ServerInstance.cs | 444 +++++++
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Controllers/ClientsController.cs | 110 +
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Create.cshtml.cs | 141 ++
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Index.cshtml.cs | 8
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Details.cshtml | 338 +++++
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/_ViewImports.cshtml | 2
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Edit.cshtml | 245 ++++
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Shared/_ValidationScriptsPartial.cshtml | 16
20 files changed, 3,528 insertions(+), 39 deletions(-)
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Interfaces/IS7ServerInstance.cs b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Interfaces/IS7ServerInstance.cs
new file mode 100644
index 0000000..8cf9681
--- /dev/null
+++ b/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);
+ }
+}
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Interfaces/ISimulatorInstanceManager.cs b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Interfaces/ISimulatorInstanceManager.cs
new file mode 100644
index 0000000..db97b5c
--- /dev/null
+++ b/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>瀹炰緥锛屼笉瀛樺湪鍒欒繑鍥瀗ull</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">鏄惁鑷姩鍚姩鏍囪涓篈utoStart鐨勫疄渚�</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();
+ }
+}
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Manager/SimulatorInstanceManager.cs b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Manager/SimulatorInstanceManager.cs
new file mode 100644
index 0000000..93575e4
--- /dev/null
+++ b/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锛岀敓鎴愭柊鐨凣UID
+ if (string.IsNullOrWhiteSpace(config.Id))
+ {
+ config.Id = Guid.NewGuid().ToString("N");
+ _logger.LogDebug("涓哄疄渚嬬敓鎴愭柊ID: {InstanceId}", config.Id);
+ }
+
+ // 妫�鏌D鏄惁宸插瓨鍦�
+ 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("灏濊瘯鍚姩瀹炰緥鏃舵彁渚涗簡绌篒D");
+ 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("灏濊瘯鍋滄瀹炰緥鏃舵彁渚涗簡绌篒D");
+ 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("灏濊瘯閲嶅惎瀹炰緥鏃舵彁渚涗簡绌篒D");
+ 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("灏濊瘯鍒犻櫎瀹炰緥鏃舵彁渚涗簡绌篒D");
+ 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);
+ }
+ }
+}
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Server/S7ServerInstance.cs b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Server/S7ServerInstance.cs
new file mode 100644
index 0000000..f29afa6
--- /dev/null
+++ b/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搴撳疄鐜癝7 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("宸茶缃縺娲荤爜");
+ }
+
+ // 鍒濆鍖朌B鍧楋紙鏍规嵁閰嶇疆锛�
+ InitializeDbBlocks();
+
+ // 浠嶮emoryStore鍚屾鍒濆鏁版嵁鍒版湇鍔″櫒
+ 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)
+ {
+ // 鍋滄鍓嶅悓姝ユ湇鍔″櫒鏁版嵁鍒癕emoryStore
+ 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)
+ {
+ // 鍏堝悓姝ユ湇鍔″櫒鏁版嵁鍒癕emoryStore
+ 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>
+ /// 鍒濆鍖朌B鍧�
+ /// </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("宸叉坊鍔燚B鍧�: DB{DbNumber}, 澶у皬: {Size}", i, Config.MemoryConfig.DBBlockSize);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "鍒濆鍖朌B鍧楁椂鍙戠敓璀﹀憡");
+ }
+ }
+
+ /// <summary>
+ /// 浠嶮emoryStore鍚屾鏁版嵁鍒版湇鍔″櫒
+ /// </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鏁版嵁鍚屾鍒癝7鏈嶅姟鍣�");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "鍚屾鏁版嵁鍒版湇鍔″櫒鏃跺彂鐢熻鍛�");
+ }
+ }
+
+ /// <summary>
+ /// 浠庢湇鍔″櫒鍚屾鏁版嵁鍒癕emoryStore
+ /// </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();
+
+ // 瀵煎叆鍒癕emoryStore
+ 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">鏄惁姝e湪閲婃斁鎵樼璧勬簮</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ Stop();
+ MemoryStore?.Dispose();
+ }
+ _disposed = true;
+ }
+ }
+ }
+}
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Controllers/ClientsController.cs b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Controllers/ClientsController.cs
new file mode 100644
index 0000000..5e065d4
--- /dev/null
+++ b/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" });
+ }
+ }
+ }
+}
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Controllers/MemoryController.cs b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Controllers/MemoryController.cs
new file mode 100644
index 0000000..8fe944d
--- /dev/null
+++ b/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" });
+ }
+ }
+ }
+}
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Controllers/SimulatorInstancesController.cs b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Server/Controllers/SimulatorInstancesController.cs
new file mode 100644
index 0000000..951d0ab
--- /dev/null
+++ b/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" });
+ }
+ }
+ }
+}
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Create.cshtml b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Create.cshtml
new file mode 100644
index 0000000..8dc2f23
--- /dev/null
+++ b/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鍙稰LC浠跨湡鍣�" />
+ <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>
+}
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Create.cshtml.cs b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Create.cshtml.cs
new file mode 100644
index 0000000..928a997
--- /dev/null
+++ b/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宸插瓨鍦紝璇蜂娇鐢ㄥ叾浠朓D");
+ }
+ 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; }
+ }
+}
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Details.cshtml b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Details.cshtml
new file mode 100644
index 0000000..95b4ece
--- /dev/null
+++ b/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>瀹㈡埛绔疘D</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>
+}
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Details.cshtml.cs b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Details.cshtml.cs
new file mode 100644
index 0000000..bcbd46f
--- /dev/null
+++ b/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();
+ }
+}
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Edit.cshtml b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Edit.cshtml
new file mode 100644
index 0000000..dc89e76
--- /dev/null
+++ b/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> 瀹炰緥姝e湪杩愯涓�傛煇浜涢厤缃慨鏀瑰彲鑳介渶瑕侀噸鍚疄渚嬫墠鑳界敓鏁堛��
+ </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>
+}
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Edit.cshtml.cs b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Edit.cshtml.cs
new file mode 100644
index 0000000..d4b125d
--- /dev/null
+++ b/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; }
+ }
+}
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Index.cshtml b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Index.cshtml
index b5f0c15..e86b444 100644
--- a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Index.cshtml
+++ b/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">姝e湪鍔犺浇瀹炰緥鍒楄〃...</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>
+}
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Index.cshtml.cs b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Index.cshtml.cs
index 76a2461..05e677f 100644
--- a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Index.cshtml.cs
+++ b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Index.cshtml.cs
@@ -1,8 +1,10 @@
-锘縰sing Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.RazorPages;
+锘縰sing 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");
}
}
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Shared/_Layout.cshtml b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Shared/_Layout.cshtml
index 09f6909..911de9c 100644
--- a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Shared/_Layout.cshtml
+++ b/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">
- © 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>© 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>
\ No newline at end of file
+</html>
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Shared/_ValidationScriptsPartial.cshtml b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Shared/_ValidationScriptsPartial.cshtml
index 5a16d80..b94c97f 100644
--- a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/Shared/_ValidationScriptsPartial.cshtml
+++ b/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>
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/_ViewImports.cshtml b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/_ViewImports.cshtml
index 676b6dd..bac26ae 100644
--- a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/Pages/_ViewImports.cshtml
+++ b/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
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/wwwroot/css/site.css b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/wwwroot/css/site.css
index f27e5ad..e43a1f9 100644
--- a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/wwwroot/css/site.css
+++ b/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;
}
\ No newline at end of file
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/wwwroot/js/site.js b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/wwwroot/js/site.js
index ac49c18..4754930 100644
--- a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/wwwroot/js/site.js
+++ b/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();
+});
--
Gitblit v1.9.3