!63 build: 10.4.8

* 10.4.8
* 优化ui
* 调整规则引擎
This commit is contained in:
Diego
2025-04-07 15:54:03 +00:00
parent c88fc5ec09
commit 020f440fdd
51 changed files with 1777 additions and 579 deletions

View File

@@ -7,7 +7,7 @@
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks>net9.0;net8.0;</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

View File

@@ -16,7 +16,7 @@
</ItemGroup>
<PropertyGroup>
<TargetFrameworks>net9.0;net8.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Content Remove="Locales\*.json" />

View File

@@ -4,7 +4,7 @@
<PropertyGroup>
<TargetFrameworks>net9.0;net8.0;</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
</PropertyGroup>
<Import Project="Admin.targets" Condition=" '$(Configuration)' != 'Debug' " />

View File

@@ -4,7 +4,7 @@
<Import Project="$(SolutionDir)PackNuget.props" />
<PropertyGroup>
<TargetFrameworks>net9.0;net8.0;</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>

View File

@@ -40,36 +40,54 @@ public class TimeTick
/// <param name="currentTime">当前时间</param>
/// <returns>是否触发时间刻度</returns>
public bool IsTickHappen(DateTime currentTime)
{
var nextTime = GetNextTime(currentTime);
bool result = SetLastTime(currentTime, nextTime);
return result;
}
private bool SetLastTime(DateTime currentTime, DateTime nextTime)
{
var diffMilliseconds = (currentTime - nextTime).TotalMilliseconds;
var result = diffMilliseconds >= 0;
if (result)
{
if (diffMilliseconds > _intervalMilliseconds)
LastTime = currentTime;
else
LastTime = nextTime;
}
return result;
}
public DateTime GetNextTime(DateTime currentTime, bool setLastTime = true)
{
// 在没有 Cron 表达式的情况下,使用固定间隔
if (cron == null)
{
var nextTime = LastTime.AddMilliseconds(_intervalMilliseconds);
var diffMilliseconds = (currentTime - nextTime).TotalMilliseconds;
if (setLastTime)
SetLastTime(currentTime, nextTime);
var result = diffMilliseconds >= 0;
if (result)
{
if (diffMilliseconds > _intervalMilliseconds)
LastTime = currentTime;
else
LastTime = nextTime;
}
return result;
return nextTime;
}
// 使用 Cron 表达式
else
{
var nextTime = cron.GetNext(LastTime);
if (currentTime >= nextTime)
{
LastTime = nextTime;
return true;
}
return false;
if (setLastTime)
SetLastTime(currentTime, nextTime);
return nextTime;
}
}
public DateTime GetNextTime(bool setLastTime = true) => GetNextTime(DateTime.UtcNow, setLastTime);
/// <summary>
/// 是否到达设置的时间间隔
/// </summary>

View File

@@ -3,7 +3,7 @@
<Import Project="$(SolutionDir)PackNuget.props" />
<PropertyGroup>
<TargetFrameworks>net9.0;net8.0;</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

View File

@@ -1,8 +1,8 @@
<Project>
<PropertyGroup>
<PluginVersion>10.4.7</PluginVersion>
<ProPluginVersion>10.4.7</ProPluginVersion>
<PluginVersion>10.4.8</PluginVersion>
<ProPluginVersion>10.4.8</ProPluginVersion>
<AuthenticationVersion>2.1.7</AuthenticationVersion>
</PropertyGroup>

View File

@@ -83,16 +83,8 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
//await _connectLock.WaitAsync().ConfigureAwait(false);
if (Online)
{
await this.OnChannelEvent(Stoping).ConfigureAwait(false);
await base.CloseAsync(msg).ConfigureAwait(false);
if (!Online)
{
Logger?.Debug($"{ToString()} Closed{msg}");
await this.OnChannelEvent(Stoped).ConfigureAwait(false);
}
}
}
finally
@@ -114,11 +106,7 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
{
//await SetupAsync(Config.Clone()).ConfigureAwait(false);
await base.ConnectAsync(millisecondsTimeout, token).ConfigureAwait(false);
if (Online)
{
Logger?.Debug($"{ToString()} Connected");
await this.OnChannelEvent(Started).ConfigureAwait(false);
}
}
}
finally
@@ -153,22 +141,35 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
return base.ToString();
}
protected override async Task OnSerialClosed(ClosedEventArgs e)
{
await this.OnChannelEvent(Stoped).ConfigureAwait(false);
Logger?.Info($"{ToString()} Closed{(e.Message.IsNullOrEmpty() ? string.Empty : $" -{e.Message}")}");
await base.OnSerialClosed(e).ConfigureAwait(false);
}
/// <inheritdoc/>
protected override async Task OnSerialClosing(ClosingEventArgs e)
{
await this.OnChannelEvent(Stoping).ConfigureAwait(false);
Logger?.Debug($"{ToString()} Closing{(e.Message.IsNullOrEmpty() ? string.Empty : $" -{e.Message}")}");
Logger?.Info($"{ToString()} Closing{(e.Message.IsNullOrEmpty() ? string.Empty : $" -{e.Message}")}");
await base.OnSerialClosing(e).ConfigureAwait(false);
}
/// <inheritdoc/>
protected override async Task OnSerialConnecting(ConnectingEventArgs e)
{
Logger?.Debug($"{ToString()} Connecting{(e.Message.IsNullOrEmpty() ? string.Empty : $" -{e.Message}")}");
Logger?.Trace($"{ToString()} Connecting{(e.Message.IsNullOrEmpty() ? string.Empty : $" -{e.Message}")}");
await this.OnChannelEvent(Starting).ConfigureAwait(false);
await base.OnSerialConnecting(e).ConfigureAwait(false);
}
protected override async Task OnSerialConnected(ConnectedEventArgs e)
{
Logger?.Debug($"{ToString()} Connected");
await this.OnChannelEvent(Started).ConfigureAwait(false);
await base.OnSerialConnected(e).ConfigureAwait(false);
}
/// <inheritdoc/>
protected override async Task OnSerialReceived(ReceivedDataEventArgs e)
{

View File

@@ -82,11 +82,6 @@ public class TcpClientChannel : TcpClient, IClientChannel
if (Online)
{
await base.CloseAsync(msg).ConfigureAwait(false);
if (!Online)
{
Logger?.Debug($"{ToString()} Closed{msg}");
await this.OnChannelEvent(Stoped).ConfigureAwait(false);
}
}
}
finally
@@ -106,14 +101,7 @@ public class TcpClientChannel : TcpClient, IClientChannel
//await _connectLock.WaitAsync(token).ConfigureAwait(false);
if (!Online)
{
//await SetupAsync(Config.Clone()).ConfigureAwait(false);
await base.ConnectAsync(millisecondsTimeout, token).ConfigureAwait(false);
if (Online)
{
Logger?.Debug($"{ToString()} Connected");
await this.OnChannelEvent(Started).ConfigureAwait(false);
}
}
}
finally
@@ -136,11 +124,22 @@ public class TcpClientChannel : TcpClient, IClientChannel
return $"{IP}:{Port}";
}
protected override async Task OnTcpClosed(ClosedEventArgs e)
{
Logger?.Info($"{ToString()} Closed{(e.Message.IsNullOrEmpty() ? string.Empty : $" -{e.Message}")}");
await this.OnChannelEvent(Stoped).ConfigureAwait(false);
await base.OnTcpClosed(e).ConfigureAwait(false);
}
/// <inheritdoc/>
protected override async Task OnTcpClosing(ClosingEventArgs e)
{
await this.OnChannelEvent(Stoping).ConfigureAwait(false);
Logger?.Debug($"{ToString()} Closing{(e.Message.IsNullOrEmpty() ? string.Empty : $" -{e.Message}")}");
Logger?.Info($"{ToString()} Closing{(e.Message.IsNullOrEmpty() ? string.Empty : $" -{e.Message}")}");
await base.OnTcpClosing(e).ConfigureAwait(false);
}
@@ -148,11 +147,19 @@ public class TcpClientChannel : TcpClient, IClientChannel
/// <inheritdoc/>
protected override async Task OnTcpConnecting(ConnectingEventArgs e)
{
Logger?.Debug($"{ToString()} Connecting{(e.Message.IsNullOrEmpty() ? string.Empty : $"-{e.Message}")}");
Logger?.Trace($"{ToString()} Connecting{(e.Message.IsNullOrEmpty() ? string.Empty : $"-{e.Message}")}");
await this.OnChannelEvent(Starting).ConfigureAwait(false);
await base.OnTcpConnecting(e).ConfigureAwait(false);
}
protected override async Task OnTcpConnected(ConnectedEventArgs e)
{
Logger?.Info($"{ToString()} Connected");
await this.OnChannelEvent(Started).ConfigureAwait(false);
await base.OnTcpConnected(e).ConfigureAwait(false);
}
/// <inheritdoc/>
protected override async Task OnTcpReceived(ReceivedDataEventArgs e)
{

View File

@@ -129,28 +129,28 @@ public abstract class TcpServiceChannelBase<TClient> : TcpService<TClient>, ITcp
/// <inheritdoc/>
protected override Task OnTcpClosed(TClient socketClient, ClosedEventArgs e)
{
Logger?.Debug($"{socketClient} Closed{(e.Message.IsNullOrEmpty() ? string.Empty : $"-{e.Message}")}");
Logger?.Info($"{socketClient} Closed{(e.Message.IsNullOrEmpty() ? string.Empty : $"-{e.Message}")}");
return base.OnTcpClosed(socketClient, e);
}
/// <inheritdoc/>
protected override Task OnTcpClosing(TClient socketClient, ClosingEventArgs e)
{
Logger?.Debug($"{socketClient} Closing{(e.Message.IsNullOrEmpty() ? string.Empty : $"-{e.Message}")}");
Logger?.Info($"{socketClient} Closing{(e.Message.IsNullOrEmpty() ? string.Empty : $"-{e.Message}")}");
return base.OnTcpClosing(socketClient, e);
}
/// <inheritdoc/>
protected override Task OnTcpConnected(TClient socketClient, ConnectedEventArgs e)
{
Logger?.Debug($"{socketClient} Connected");
Logger?.Info($"{socketClient} Connected");
return base.OnTcpConnected(socketClient, e);
}
/// <inheritdoc/>
protected override Task OnTcpConnecting(TClient socketClient, ConnectingEventArgs e)
{
Logger?.Debug($"{socketClient} Connecting");
Logger?.Trace($"{socketClient} Connecting");
return base.OnTcpConnecting(socketClient, e);
}
}

View File

@@ -166,6 +166,7 @@ public class UdpSessionChannel : UdpSession, IClientChannel
return $"{ChannelOptions.BindUrl} {ChannelOptions.RemoteUrl}";
}
/// <inheritdoc/>
protected override async Task OnUdpReceived(UdpReceivedDataEventArgs e)
{

View File

@@ -10,8 +10,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="9.0.3" />
<PackageReference Include="TouchSocket" Version="3.0.23" />
<PackageReference Include="TouchSocket.SerialPorts" Version="3.0.23" />
<PackageReference Include="TouchSocket" Version="3.0.24" />
<PackageReference Include="TouchSocket.SerialPorts" Version="3.0.24" />
</ItemGroup>
<ItemGroup>

View File

@@ -3,7 +3,7 @@
<Import Project="$(SolutionDir)PackNuget.props" />
<PropertyGroup>
<TargetFrameworks>net9.0;net8.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

View File

@@ -136,28 +136,11 @@ public abstract class BusinessBase : DriverBase
// 获取设备连接状态并更新设备活动时间
if (IsConnected())
{
// 如果不是采集设备,则直接更新设备状态为当前时间
if (IsCollectDevice == false)
{
CurrentDevice.SetDeviceStatus(TimerX.Now, false);
}
else
{
// 否则,更新设备活动时间
CurrentDevice.SetDeviceStatus(TimerX.Now);
}
CurrentDevice.SetDeviceStatus(TimerX.Now, false);
}
else
{
// 如果设备未连接,则更新设备状态为断开
if (!IsConnected())
{
// 如果不是采集设备,则直接更新设备状态为当前时间
if (IsCollectDevice == false)
{
CurrentDevice.SetDeviceStatus(TimerX.Now, true);
}
}
CurrentDevice.SetDeviceStatus(TimerX.Now, true);
}
// 再次检查取消操作是否被请求

View File

@@ -168,7 +168,76 @@ public abstract class CollectBase : DriverBase
{
return string.Empty;
}
/// <summary>
/// 循环任务
/// </summary>
/// <param name="cancellationToken">取消操作的令牌。</param>
/// <returns>表示异步操作结果的枚举。</returns>
internal override async ValueTask<ThreadRunReturnTypeEnum> ExecuteAsync(CancellationToken cancellationToken)
{
try
{
// 如果取消操作被请求,则返回中断状态
if (cancellationToken.IsCancellationRequested)
{
return ThreadRunReturnTypeEnum.Break;
}
// 如果标志为停止,则暂停执行
if (Pause)
{
// 暂停
return ThreadRunReturnTypeEnum.Continue;
}
// 再次检查取消操作是否被请求
if (cancellationToken.IsCancellationRequested)
{
return ThreadRunReturnTypeEnum.Break;
}
// 获取设备连接状态并更新设备活动时间
if (IsConnected())
{
CurrentDevice.SetDeviceStatus(TimerX.Now);
}
// 再次检查取消操作是否被请求
if (cancellationToken.IsCancellationRequested)
{
return ThreadRunReturnTypeEnum.Break;
}
// 执行任务操作
await ProtectedExecuteAsync(cancellationToken).ConfigureAwait(false);
// 再次检查取消操作是否被请求
if (cancellationToken.IsCancellationRequested)
{
return ThreadRunReturnTypeEnum.Break;
}
// 正常返回None状态
return ThreadRunReturnTypeEnum.None;
}
catch (OperationCanceledException)
{
return ThreadRunReturnTypeEnum.Break;
}
catch (ObjectDisposedException)
{
return ThreadRunReturnTypeEnum.Break;
}
catch (Exception ex)
{
if (cancellationToken.IsCancellationRequested)
return ThreadRunReturnTypeEnum.Break;
// 记录异常信息,并更新设备状态为异常
LogMessage?.LogError(ex, "Execute");
CurrentDevice.SetDeviceStatus(TimerX.Now, true, ex.Message);
return ThreadRunReturnTypeEnum.None;
}
}
/// <summary>
/// 执行读取等方法,如果插件不支持读取,而是自更新值的话,需重写此方法
/// </summary>

View File

@@ -26,7 +26,7 @@ public abstract class CollectPropertyBase : DriverPropertyBase
/// 离线后恢复运行的间隔时间
/// </summary>
[DynamicProperty]
public virtual int ReIntervalTime { get; set; } = 30000;
public virtual int ReIntervalTime { get; set; } = 0;
/// <summary>
/// 失败重试次数默认3

View File

@@ -310,92 +310,7 @@ public abstract class DriverBase : DisposableObject, IDriver
/// </summary>
/// <param name="cancellationToken">取消操作的令牌。</param>
/// <returns>表示异步操作结果的枚举。</returns>
internal virtual async ValueTask<ThreadRunReturnTypeEnum> ExecuteAsync(CancellationToken cancellationToken)
{
try
{
// 如果取消操作被请求,则返回中断状态
if (cancellationToken.IsCancellationRequested)
{
return ThreadRunReturnTypeEnum.Break;
}
// 如果标志为停止,则暂停执行
if (Pause)
{
// 暂停
return ThreadRunReturnTypeEnum.Continue;
}
// 再次检查取消操作是否被请求
if (cancellationToken.IsCancellationRequested)
{
return ThreadRunReturnTypeEnum.Break;
}
// 获取设备连接状态并更新设备活动时间
if (IsConnected())
{
// 如果不是采集设备,则直接更新设备状态为当前时间
if (IsCollectDevice == false)
{
CurrentDevice.SetDeviceStatus(TimerX.Now, false);
}
else
{
// 否则,更新设备活动时间
CurrentDevice.SetDeviceStatus(TimerX.Now);
}
}
else
{
// 如果设备未连接,则更新设备状态为断开
if (!IsConnected())
{
// 如果不是采集设备,则直接更新设备状态为当前时间
if (IsCollectDevice == false)
{
CurrentDevice.SetDeviceStatus(TimerX.Now, true);
}
}
}
// 再次检查取消操作是否被请求
if (cancellationToken.IsCancellationRequested)
{
return ThreadRunReturnTypeEnum.Break;
}
// 执行任务操作
await ProtectedExecuteAsync(cancellationToken).ConfigureAwait(false);
// 再次检查取消操作是否被请求
if (cancellationToken.IsCancellationRequested)
{
return ThreadRunReturnTypeEnum.Break;
}
// 正常返回None状态
return ThreadRunReturnTypeEnum.None;
}
catch (OperationCanceledException)
{
return ThreadRunReturnTypeEnum.Break;
}
catch (ObjectDisposedException)
{
return ThreadRunReturnTypeEnum.Break;
}
catch (Exception ex)
{
if (cancellationToken.IsCancellationRequested)
return ThreadRunReturnTypeEnum.Break;
// 记录异常信息,并更新设备状态为异常
LogMessage?.LogError(ex, "Execute");
CurrentDevice.SetDeviceStatus(TimerX.Now, true, ex.Message);
return ThreadRunReturnTypeEnum.None;
}
}
internal abstract ValueTask<ThreadRunReturnTypeEnum> ExecuteAsync(CancellationToken cancellationToken);
/// <summary>
/// 已停止循环任务,释放插件

View File

@@ -54,7 +54,7 @@ public class Device : BaseDataEntity, IValidatableObject
/// 通道
/// </summary>
[SugarColumn(ColumnDescription = "通道", Length = 200)]
[AutoGenerateColumn(Visible = true, Filterable = false, Sortable = false)]
[AutoGenerateColumn(Ignore = true)]
[IgnoreExcel]
[MinValue(1)]
[Required]

View File

@@ -31,6 +31,7 @@ public class ChannelRuntime : Channel, IChannelOptions, IDisposable
[System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
[AdaptIgnore]
[AutoGenerateColumn(Ignore = true)]
public PluginInfo? PluginInfo { get; set; }
/// <summary>
@@ -41,11 +42,13 @@ public class ChannelRuntime : Channel, IChannelOptions, IDisposable
/// <summary>
/// 是否采集
/// </summary>
[AutoGenerateColumn(Ignore = true)]
public bool? IsCollect => PluginInfo == null ? null : PluginInfo?.PluginType == PluginTypeEnum.Collect;
[System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
[AdaptIgnore]
[AutoGenerateColumn(Ignore = true)]
public WaitLock WaitLock { get; private set; } = new WaitLock();
/// <inheritdoc/>
@@ -77,6 +80,7 @@ public class ChannelRuntime : Channel, IChannelOptions, IDisposable
[System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
[AdaptIgnore]
[AutoGenerateColumn(Ignore = true)]
public TouchSocketConfig Config { get; set; } = new();
[System.Text.Json.Serialization.JsonIgnore]
@@ -104,8 +108,10 @@ public class ChannelRuntime : Channel, IChannelOptions, IDisposable
[System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
[AdaptIgnore]
[AutoGenerateColumn(Ignore = true)]
public IDeviceThreadManage? DeviceThreadManage { get; internal set; }
[AutoGenerateColumn(Ignore = true)]
public string LogPath => Name.GetChannelLogPath();

View File

@@ -42,11 +42,13 @@ public class DeviceRuntime : Device, IDisposable
/// <summary>
/// 插件名称
/// </summary>
[AutoGenerateColumn(Ignore = true)]
public virtual PluginTypeEnum? PluginType => ChannelRuntime?.PluginInfo?.PluginType;
/// <summary>
/// 是否采集
/// </summary>
[AutoGenerateColumn(Ignore = true)]
public bool? IsCollect => PluginType == null ? null : PluginType == PluginTypeEnum.Collect;
/// <summary>
@@ -55,12 +57,14 @@ public class DeviceRuntime : Device, IDisposable
[System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
[AdaptIgnore]
[AutoGenerateColumn(Ignore = true)]
public ChannelRuntime? ChannelRuntime { get; set; }
/// <summary>
/// 通道名称
/// </summary>
public string? ChannelName => ChannelRuntime?.Name;
[AutoGenerateColumn(Ignore = true)]
public string LogPath => Name.GetDeviceLogPath();
/// <summary>
@@ -113,6 +117,7 @@ public class DeviceRuntime : Device, IDisposable
/// <summary>
/// 设备属性数量
/// </summary>
[AutoGenerateColumn(Ignore = true)]
public int PropertysCount { get => DevicePropertys == null ? 0 : DevicePropertys.Count; }
/// <summary>
@@ -158,6 +163,7 @@ public class DeviceRuntime : Device, IDisposable
[System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
[AdaptIgnore]
[AutoGenerateColumn(Ignore = true)]
public List<VariableMethod>? ReadVariableMethods { get; set; }
/// <summary>
@@ -171,6 +177,7 @@ public class DeviceRuntime : Device, IDisposable
[System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
[AdaptIgnore]
[AutoGenerateColumn(Ignore = true)]
public List<VariableSourceRead>? VariableSourceReads { get; set; }
/// <summary>
@@ -179,6 +186,7 @@ public class DeviceRuntime : Device, IDisposable
[System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
[AdaptIgnore]
[AutoGenerateColumn(Ignore = true)]
public List<VariableScriptRead>? VariableScriptReads { get; set; }
@@ -213,6 +221,7 @@ public class DeviceRuntime : Device, IDisposable
[System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
[AdaptIgnore]
[AutoGenerateColumn(Ignore = true)]
public IDriver? Driver { get; internal set; }

View File

@@ -589,13 +589,19 @@ internal sealed class DeviceThreadManage : IAsyncDisposable, IDeviceThreadManage
// 如果驱动处于离线状态且为采集驱动,则根据配置的间隔时间进行延迟
if (driver.CurrentDevice.DeviceStatus == DeviceStatusEnum.OffLine && IsCollectChannel == true)
{
driver.CurrentDevice.CheckEnable = false;
await Task.Delay(Math.Max(Math.Min(((CollectBase)driver).CollectProperties.ReIntervalTime, ManageHelper.ChannelThreadOptions.CheckInterval / 2) - CycleInterval, 3000), token).ConfigureAwait(false);
driver.CurrentDevice.CheckEnable = true;
var collectBase = (CollectBase)driver;
if (collectBase.CollectProperties.ReIntervalTime > 0)
{
await Task.Delay(Math.Max(Math.Min(collectBase.CollectProperties.ReIntervalTime, ManageHelper.ChannelThreadOptions.CheckInterval / 2) - CycleInterval, 3000), token).ConfigureAwait(false);
}
else
{
await Task.Delay(CycleInterval, token).ConfigureAwait(false);
}
}
else
{
await Task.Delay(CycleInterval, token).ConfigureAwait(false); // 默认延迟一段时间后再继续执行
await Task.Delay(CycleInterval, token).ConfigureAwait(false);
}
}
else if (result == ThreadRunReturnTypeEnum.Continue)
@@ -621,7 +627,6 @@ internal sealed class DeviceThreadManage : IAsyncDisposable, IDeviceThreadManage
return;
}
}
@@ -857,7 +862,7 @@ internal sealed class DeviceThreadManage : IAsyncDisposable, IDeviceThreadManage
if (driver.CurrentDevice != null)
{
//线程卡死/初始化失败检测
if (((driver.IsStarted && driver.CurrentDevice.ActiveTime != DateTime.UnixEpoch.ToLocalTime() && driver.CurrentDevice.ActiveTime.AddMinutes(ManageHelper.ChannelThreadOptions.CheckInterval) <= DateTime.Now)
if (((driver.IsStarted && driver.CurrentDevice.ActiveTime != DateTime.UnixEpoch.ToLocalTime() && driver.CurrentDevice.ActiveTime.AddMilliseconds(ManageHelper.ChannelThreadOptions.CheckInterval) <= DateTime.Now)
|| (driver.IsInitSuccess == false)) && !driver.DisposedValue)
{
//如果线程处于暂停状态,跳过

View File

@@ -9,8 +9,8 @@
<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
<PackageReference Include="SqlSugar.TDengineCore" Version="4.18.6" />
<PackageReference Include="Rougamo.Fody" Version="5.0.0" />
<PackageReference Include="TouchSocket.Dmtp" Version="3.0.23" />
<PackageReference Include="TouchSocket.WebApi.Swagger" Version="3.0.23" />
<PackageReference Include="TouchSocket.Dmtp" Version="3.0.24" />
<PackageReference Include="TouchSocket.WebApi.Swagger" Version="3.0.24" />
<PackageReference Include="ThingsGateway.Authentication" Version="$(AuthenticationVersion)" />
</ItemGroup>

View File

@@ -67,8 +67,7 @@
"ThingsGateway.Gateway.Razor.VariableRuntimeInfo": {
"WriteVariable": "WriteVariable",
"WriteValue": "WriteValue",
"ExcelVariable": "ExcelVariable",
"ImportExcel": "ImportExcel",
"TestVariableCount": "TestVariableCount",
"TestDeviceCount": "TestDeviceCount",
"SlaveUrl": "SlaveUrlUrl",
@@ -94,8 +93,10 @@
},
"ThingsGateway.Gateway.Razor.ShowTypeEnum": {
"Variable": "Variable",
"LogInfo": "Info"
"VariableTable": "VariableTable",
"LogInfo": "LogInfo",
"ChannelTable": "ChannelTable",
"DeviceTable": "DeviceTable"
},
"ThingsGateway.Gateway.Razor.VariableEditComponent": {
@@ -105,6 +106,10 @@
"ThingsGateway.Gateway.Razor._Imports": {
"ExcelVariable": "ExcelVariable",
"ImportExcel": "ImportExcel",
"CopyVariableNamePrefix": "CopyVariableNamePrefix",
"CopyVariableNameSuffixNumber": "CopyVariableNameSuffixNumber",
"CopyChannelNamePrefix": "CopyChannelNamePrefix",

View File

@@ -26,8 +26,6 @@
"ThingsGateway.Gateway.Razor.VariableRuntimeInfo": {
"WriteVariable": "写入",
"WriteValue": "写入值",
"ImportExcel": "导入变量",
"ExcelVariable": "在线excel编辑变量",
"TestVariableCount": "变量数量",
"TestDeviceCount": "采集设备数量",
"SlaveUrl": "服务端Url",
@@ -73,8 +71,10 @@
},
"ThingsGateway.Gateway.Razor.ShowTypeEnum": {
"Variable": "变量页面",
"LogInfo": "日志页面"
"VariableTable": "变量页面",
"LogInfo": "日志页面",
"ChannelTable": "通道页面",
"DeviceTable": "设备页面"
},
"ThingsGateway.Gateway.Razor.SavePlugin": {
@@ -98,6 +98,9 @@
},
"ThingsGateway.Gateway.Razor._Imports": {
"ExcelVariable": "在线excel编辑变量",
"ImportExcel": "导入变量",
"CopyVariableNamePrefix": "变量名称前缀",
"CopyVariableNameSuffixNumber": "变量名称后缀开始序号",
"CopyChannelNamePrefix": "通道名称前缀",

View File

@@ -11,112 +11,124 @@
{
<ValidateForm Model="Model" OnValidSubmit="ValidSubmit">
@renderFragment
<EditorForm class="p-2" AutoGenerateAllItem="false" RowType=RowType.Inline ItemsPerRow=2 LabelWidth=200 Model="Model">
<div class="form-footer">
<FieldItems>
<EditorItem TValue="string" TModel="Channel" @bind-Field="@context.Name">
<EditTemplate Context="value">
<div class="col-12">
<h6>@GatewayLocalizer["BasicInformation"]</h6>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.Name" Readonly=BatchEditEnable />
<EditorItem @bind-Field="@context.PluginName">
<EditTemplate Context="value">
<div class="col-12 col-md-6">
<Select @bind-Value="@value.PluginName"
Items="@PluginNames" IsDisabled=BatchEditEnable
ShowSearch="true">
<ItemTemplate Context="name">
@if (PluginDcit.TryGetValue(name.Value, out var pluginOutput))
{
if (pluginOutput.EducationPlugin)
{
<div class="d-flex">
<span>@name.Text</span>
<div style="flex-grow: 1;"></div>
<Tag Color="Color.Primary">PRO</Tag>
</div>
}
else
{
<span>@name.Text</span>
}
}
else
{
<span>@name.Value</span>
}
</ItemTemplate>
</Select>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.Enable" />
<EditorItem @bind-Field="@context.LogEnable" />
<EditorItem @bind-Field="@context.LogLevel" />
<EditorItem TValue="string" TModel="Channel" @bind-Field="@context.Name">
<EditTemplate Context="value">
<div class="col-12">
<h6>@GatewayLocalizer["Connection"]</h6>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.ChannelType">
<EditTemplate Context="value">
<div class="col-12 col-sm-6 col-md-6">
<Select SkipValidate="true" @bind-Value="@value.ChannelType" OnSelectedItemChanged=@((a)=>
{
return InvokeAsync(StateHasChanged);
}) />
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.RemoteUrl" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpClient&&context.ChannelType!=ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.BindUrl" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpClient&&context.ChannelType!=ChannelTypeEnum.UdpSession&&context.ChannelType!=ChannelTypeEnum.TcpService) />
<EditorItem @bind-Field="@context.PortName" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.BaudRate" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.DataBits" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.Parity" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.StopBits" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.DtrEnable" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.RtsEnable" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.CacheTimeout" Ignore=@(context.ChannelType==ChannelTypeEnum.UdpSession||context.ChannelType==ChannelTypeEnum.Other) />
<EditorItem @bind-Field="@context.ConnectTimeout" Ignore=@(context.ChannelType==ChannelTypeEnum.UdpSession||context.ChannelType==ChannelTypeEnum.TcpService||context.ChannelType==ChannelTypeEnum.Other) />
<EditorItem @bind-Field="@context.MaxConcurrentCount" Ignore=@(context.ChannelType==ChannelTypeEnum.Other) />
<EditorItem @bind-Field="@context.MaxClientCount" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpService) />
<EditorItem @bind-Field="@context.CheckClearTime" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpService) />
<EditorItem @bind-Field="@context.Heartbeat" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpService&&context.ChannelType!=ChannelTypeEnum.TcpClient&&context.ChannelType!=ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.HeartbeatTime" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpClient&&context.ChannelType!=ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.DtuId" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpClient&&context.ChannelType!=ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.DtuSeviceType" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpService&&context.ChannelType!=ChannelTypeEnum.UdpSession) />
</FieldItems>
<Buttons>
<Button ButtonType="ButtonType.Submit" Icon="fa-solid fa-floppy-disk" IsAsync Text=@RazorLocalizer["Save"] />
</Buttons>
</EditorForm>
<Button ButtonType="ButtonType.Submit" Icon="fa-solid fa-floppy-disk" IsAsync Text=@RazorLocalizer["Save"] />
</div>
</ValidateForm>
}
</div>
else
{
@renderFragment
}
</div>
@code {
RenderFragment renderFragment =>
@<EditorForm class="p-2" AutoGenerateAllItem="false" RowType=RowType.Inline ItemsPerRow=2 LabelWidth=200 Model="Model">
<FieldItems>
<EditorItem TValue="string" TModel="Channel" @bind-Field="@context.Name">
<EditTemplate Context="value">
<div class="col-12">
<h6>@GatewayLocalizer["BasicInformation"]</h6>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.Name" Readonly=BatchEditEnable />
<EditorItem @bind-Field="@context.PluginName">
<EditTemplate Context="value">
<div class="col-12 col-md-6">
<Select @bind-Value="@value.PluginName"
Items="@PluginNames" IsDisabled=BatchEditEnable
ShowSearch="true">
<ItemTemplate Context="name">
@if (PluginDcit.TryGetValue(name.Value, out var pluginOutput))
{
if (pluginOutput.EducationPlugin)
{
<div class="d-flex">
<span>@name.Text</span>
<div style="flex-grow: 1;"></div>
<Tag Color="Color.Primary">PRO</Tag>
</div>
}
else
{
<span>@name.Text</span>
}
}
else
{
<span>@name.Value</span>
}
</ItemTemplate>
</Select>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.Enable" />
<EditorItem @bind-Field="@context.LogEnable" />
<EditorItem @bind-Field="@context.LogLevel" />
<EditorItem TValue="string" TModel="Channel" @bind-Field="@context.Name">
<EditTemplate Context="value">
<div class="col-12">
<h6>@GatewayLocalizer["Connection"]</h6>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.ChannelType">
<EditTemplate Context="value">
<div class="col-12 col-sm-6 col-md-6">
<Select SkipValidate="true" @bind-Value="@value.ChannelType" OnSelectedItemChanged=@((a)=>
{
return InvokeAsync(StateHasChanged);
}) />
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.RemoteUrl" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpClient&&context.ChannelType!=ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.BindUrl" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpClient&&context.ChannelType!=ChannelTypeEnum.UdpSession&&context.ChannelType!=ChannelTypeEnum.TcpService) />
<EditorItem @bind-Field="@context.PortName" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.BaudRate" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.DataBits" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.Parity" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.StopBits" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.DtrEnable" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.RtsEnable" Ignore=@(context.ChannelType!=ChannelTypeEnum.SerialPort) />
<EditorItem @bind-Field="@context.CacheTimeout" Ignore=@(context.ChannelType==ChannelTypeEnum.UdpSession||context.ChannelType==ChannelTypeEnum.Other) />
<EditorItem @bind-Field="@context.ConnectTimeout" Ignore=@(context.ChannelType==ChannelTypeEnum.UdpSession||context.ChannelType==ChannelTypeEnum.TcpService||context.ChannelType==ChannelTypeEnum.Other) />
<EditorItem @bind-Field="@context.MaxConcurrentCount" Ignore=@(context.ChannelType==ChannelTypeEnum.Other) />
<EditorItem @bind-Field="@context.MaxClientCount" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpService) />
<EditorItem @bind-Field="@context.CheckClearTime" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpService) />
<EditorItem @bind-Field="@context.Heartbeat" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpService&&context.ChannelType!=ChannelTypeEnum.TcpClient&&context.ChannelType!=ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.HeartbeatTime" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpClient&&context.ChannelType!=ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.DtuId" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpClient&&context.ChannelType!=ChannelTypeEnum.UdpSession) />
<EditorItem @bind-Field="@context.DtuSeviceType" Ignore=@(context.ChannelType!=ChannelTypeEnum.TcpService&&context.ChannelType!=ChannelTypeEnum.UdpSession) />
</FieldItems>
</EditorForm>;
}

View File

@@ -0,0 +1,107 @@
@namespace ThingsGateway.Gateway.Razor
@using ThingsGateway.Admin.Application
@using ThingsGateway.Admin.Razor
@using ThingsGateway.Debug
@using ThingsGateway.Gateway.Application
@inherits ComponentDefault
<AdminTable @ref=table
TItem="ChannelRuntime"
EditDialogSize="Size.ExtraLarge"
AutoGenerateColumns="true"
ShowAdvancedSearch=false
ScrollingDialogContent=false
AllowResizing="true"
OnAdd="OnAdd"
IsFixedHeader=true
IsMultipleSelect=true
SearchMode=SearchMode.Top
ShowExtendButtons=true
ShowToolbar="true"
ShowExportButton
ShowDefaultButtons=true
ShowSearch=false
ShowExtendEditButton="true"
ShowExtendDeleteButton="true"
ExtendButtonColumnWidth=220
OnSaveAsync="Save"
OnDeleteAsync="Delete"
OnQueryAsync="OnQueryAsync"
IsPagination=true>
<TableColumns>
<TableColumn @bind-Field="@context.Name" ShowTips=true Filterable=true Sortable=true Visible=true>
<Template Context="value">
@value.Row?.ToString()
</Template>
</TableColumn>
<TableColumn @bind-Field="@context.PluginName" ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn @bind-Field="@context.Enable" Filterable=true Sortable=true Visible="true" />
<TableColumn @bind-Field="@context.LogEnable" Filterable=true Sortable=true Visible="false" />
<TableColumn @bind-Field="@context.LogLevel" Filterable=true Sortable=true Visible="false" />
<TableColumn @bind-Field="@context.ChannelType" Filterable=true Sortable=true Visible="false" />
<TableColumn Field="@context.CacheTimeout" FieldExpression=@(()=>context.CacheTimeout) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.CheckClearTime" FieldExpression=@(()=>context.CheckClearTime) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.ConnectTimeout" FieldExpression=@(()=>context.ConnectTimeout) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.Heartbeat" FieldExpression=@(()=>context.Heartbeat) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.HeartbeatTime" FieldExpression=@(()=>context.HeartbeatTime) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.DtuSeviceType" FieldExpression=@(()=>context.DtuSeviceType) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.DtuId" FieldExpression=@(()=>context.DtuId) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.PluginType" FieldExpression=@(()=>context.PluginType) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.PortName" FieldExpression=@(()=>context.PortName) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.RemoteUrl" FieldExpression=@(()=>context.RemoteUrl) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.BindUrl" FieldExpression=@(()=>context.BindUrl) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.MaxClientCount" FieldExpression=@(()=>context.MaxClientCount) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.MaxConcurrentCount" FieldExpression=@(()=>context.MaxConcurrentCount) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn @bind-Field="@context.Id" Filterable=true Sortable=true Visible="false" DefaultSort=true DefaultSortOrder="SortOrder.Asc" />
</TableColumns>
<EditTemplate Context="context">
<ChannelEditComponent Model=@(context) ValidateEnable=false BatchEditEnable=false PluginType="ChannelDeviceHelpers.GetPluginType( SelectModel)"></ChannelEditComponent>
</EditTemplate>
<ExportButtonDropdownTemplate Context="ExportContext">
<Button class="dropdown-item" OnClick="() => ExcelExportAsync(ExportContext,true)" IsDisabled=@(!AuthorizeButton("导出"))>
<i class="fas fa-file-export"></i>
<span>@RazorLocalizer["ExportAll"]</span>
</Button>
<Button class="dropdown-item" OnClick="() => ExcelExportAsync(ExportContext)" IsDisabled=@(!AuthorizeButton("导出"))>
<i class="fas fa-file-export"></i>
<span>@RazorLocalizer["TablesExportButtonExcelText"]</span>
</Button>
<Button class="dropdown-item" OnClick="() => ExcelImportAsync(ExportContext)" IsDisabled=@(!AuthorizeButton("导入"))>
<i class="fas fa-file-import"></i>
<span>@RazorLocalizer["TablesImportButtonExcelText"]</span>
</Button>
<Button class="dropdown-item" OnClick="() => ExcelChannelAsync(ExportContext)" IsDisabled=@(!AuthorizeButton("导入"))>
<i class="fas fa-file-import"></i>
<span>@GatewayLocalizer["ExcelChannel"]</span>
</Button>
</ExportButtonDropdownTemplate>
<TableToolbarTemplate>
<TableToolbarButton TItem="ChannelRuntime" IsDisabled=@(!AuthorizeButton(AdminOperConst.Add))
Color=Color.Success Text="@RazorLocalizer["Copy"]"
OnClickCallback=@(Copy) />
<TableToolbarButton TItem="ChannelRuntime" IsDisabled=@(!AuthorizeButton(AdminOperConst.Edit))
Color=Color.Info Text="@RazorLocalizer["BatchEdit"]"
OnClickCallback=@(BatchEdit) />
<TableToolbarPopConfirmButton TItem="ChannelRuntime"
Color=Color.Warning Text="@RazorLocalizer["Clear"]" IsDisabled=@(!AuthorizeButton(AdminOperConst.Delete))
IsAsync OnConfirm=@(ClearAsync) />
</TableToolbarTemplate>
</AdminTable>
@code {
AdminTable<ChannelRuntime> table;
}

View File

@@ -0,0 +1,370 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
using Mapster;
using Microsoft.AspNetCore.Components.Forms;
using ThingsGateway.Admin.Application;
using ThingsGateway.Extension.Generic;
using ThingsGateway.Gateway.Application;
using ThingsGateway.NewLife.Extension;
namespace ThingsGateway.Gateway.Razor;
public partial class ChannelTable : IDisposable
{
public bool Disposed { get; set; }
[Parameter]
public IEnumerable<ChannelRuntime>? Items { get; set; } = Enumerable.Empty<ChannelRuntime>();
public void Dispose()
{
Disposed = true;
GC.SuppressFinalize(this);
}
protected override void OnInitialized()
{
_ = RunTimerAsync();
base.OnInitialized();
}
private async Task RunTimerAsync()
{
while (!Disposed)
{
try
{
if (table != null)
await table.QueryAsync();
}
catch (Exception ex)
{
NewLife.Log.XTrace.WriteException(ex);
}
finally
{
await Task.Delay(1000);
}
}
}
#region
private QueryPageOptions _option = new();
private Task<QueryData<ChannelRuntime>> OnQueryAsync(QueryPageOptions options)
{
var data = Items
.WhereIf(!options.SearchText.IsNullOrWhiteSpace(), a => a.Name.Contains(options.SearchText))
.GetQueryData(options);
_option = options;
return Task.FromResult(data);
}
#endregion
#region
#region
private async Task Copy(IEnumerable<ChannelRuntime> channels)
{
if (!channels.Any())
{
await ToastService.Warning(null, RazorLocalizer["PleaseSelect"]);
return;
}
Channel oneModel = null;
Dictionary<Device, List<Variable>> deviceDict = new();
var channelRuntime = channels.FirstOrDefault();
oneModel = channelRuntime.Adapt<Channel>();
oneModel.Id = 0;
deviceDict = channelRuntime.ReadDeviceRuntimes.ToDictionary(a => a.Value.Adapt<Device>(), a => a.Value.ReadOnlyVariableRuntimes.Select(a => a.Value).Adapt<List<Variable>>());
var op = new DialogOption()
{
IsScrolling = false,
ShowMaximizeButton = true,
Size = Size.ExtraLarge,
Title = RazorLocalizer["Copy"],
ShowFooter = false,
ShowCloseButton = false,
};
op.Component = BootstrapDynamicComponent.CreateComponent<ChannelCopyComponent>(new Dictionary<string, object?>
{
{nameof(ChannelCopyComponent.OnSave), async (List<Channel> channels,Dictionary<Device,List<Variable>> devices) =>
{
await Task.Run(() =>GlobalData.ChannelRuntimeService.CopyAsync(channels,devices,AutoRestartThread, default));
await table.QueryAsync();
}},
{nameof(ChannelCopyComponent.Model),oneModel },
{nameof(ChannelCopyComponent.Devices),deviceDict },
});
await DialogService.Show(op);
}
private async Task BatchEdit(IEnumerable<Channel> changedModels)
{
var oldModel = changedModels.FirstOrDefault();//默认值显示第一个
if (oldModel == null)
{
await ToastService.Warning(null, RazorLocalizer["PleaseSelect"]);
return;
}
var oneModel = oldModel.Adapt<Channel>();//默认值显示第一个
var op = new DialogOption()
{
IsScrolling = false,
ShowMaximizeButton = true,
Size = Size.ExtraLarge,
Title = RazorLocalizer["BatchEdit"],
ShowFooter = false,
ShowCloseButton = false,
};
op.Component = BootstrapDynamicComponent.CreateComponent<ChannelEditComponent>(new Dictionary<string, object?>
{
{nameof(ChannelEditComponent.OnValidSubmit), async () =>
{
await Task.Run(() => GlobalData.ChannelRuntimeService.BatchEditAsync(changedModels, oldModel, oneModel,AutoRestartThread));
await InvokeAsync(table.QueryAsync);
} },
{nameof(ChannelEditComponent.Model),oneModel },
{nameof(ChannelEditComponent.ValidateEnable),true },
{nameof(ChannelEditComponent.BatchEditEnable),true },
});
await DialogService.Show(op);
}
private async Task<bool> Delete(IEnumerable<Channel> channels)
{
try
{
return await Task.Run(async () =>
{
return await GlobalData.ChannelRuntimeService.DeleteChannelAsync(channels.Select(a => a.Id), AutoRestartThread, default);
});
}
catch (Exception ex)
{
await InvokeAsync(async () =>
{
await ToastService.Warn(ex);
});
return false;
}
}
private async Task<bool> Save(Channel channel, ItemChangedType itemChangedType)
{
try
{
return await Task.Run(() => GlobalData.ChannelRuntimeService.SaveChannelAsync(channel, itemChangedType, AutoRestartThread));
}
catch (Exception ex)
{
await ToastService.Warn(ex);
return false;
}
}
#endregion
private Task<ChannelRuntime> OnAdd()
{
return Task.FromResult(ChannelDeviceHelpers.GetChannelModel(ItemChangedType.Add, SelectModel).Adapt<ChannelRuntime>());
}
#region
[Inject]
[NotNull]
private IGatewayExportService? GatewayExportService { get; set; }
private async Task ExcelExportAsync(ITableExportContext<ChannelRuntime> tableExportContext, bool all = false)
{
if (all)
{
await GatewayExportService.OnChannelExport(new() { QueryPageOptions = new() });
}
else
{
switch (SelectModel.ChannelDevicePluginType)
{
case ChannelDevicePluginTypeEnum.PluginName:
await GatewayExportService.OnChannelExport(new() { QueryPageOptions = new(), PluginName = SelectModel.PluginName });
break;
case ChannelDevicePluginTypeEnum.Channel:
await GatewayExportService.OnChannelExport(new() { QueryPageOptions = new(), ChannelId = SelectModel.ChannelRuntime.Id });
break;
case ChannelDevicePluginTypeEnum.Device:
await GatewayExportService.OnChannelExport(new() { QueryPageOptions = new(), DeviceId = SelectModel.DeviceRuntime.Id, PluginType = SelectModel.DeviceRuntime.PluginType });
break;
default:
await GatewayExportService.OnChannelExport(new() { QueryPageOptions = new() });
break;
}
}
// 返回 true 时自动弹出提示框
await ToastService.Default();
}
async Task ExcelChannelAsync(ITableExportContext<ChannelRuntime> tableExportContext)
{
var op = new DialogOption()
{
IsScrolling = false,
ShowMaximizeButton = true,
Size = Size.ExtraExtraLarge,
Title = GatewayLocalizer["ExcelChannel"],
ShowFooter = false,
ShowCloseButton = false,
};
var option = _option;
option.IsPage = false;
var models = Items
.WhereIf(!option.SearchText.IsNullOrWhiteSpace(), a => a.Name.Contains(option.SearchText)).GetData(option, out var total).ToList();
if (models.Count > 50000)
{
await ToastService.Warning("online Excel max data count 50000");
return;
}
var uSheetDatas = ChannelServiceHelpers.ExportChannel(models);
op.Component = BootstrapDynamicComponent.CreateComponent<USheet>(new Dictionary<string, object?>
{
{nameof(USheet.OnSave), async (USheetDatas data) =>
{
try
{
await Task.Run(async ()=>
{
var importData=await ChannelServiceHelpers.ImportAsync(data);
await GlobalData.ChannelRuntimeService.ImportChannelAsync(importData,AutoRestartThread);
})
;
}
finally
{
await InvokeAsync( async ()=>
{
await table.QueryAsync();
StateHasChanged();
});
}
}},
{nameof(USheet.Model),uSheetDatas },
});
await DialogService.Show(op);
}
private async Task ExcelImportAsync(ITableExportContext<ChannelRuntime> tableExportContext)
{
var op = new DialogOption()
{
IsScrolling = true,
ShowMaximizeButton = true,
Size = Size.ExtraLarge,
Title = GatewayLocalizer["ImportChannel"],
ShowFooter = false,
ShowCloseButton = false,
OnCloseAsync = async () =>
{
await InvokeAsync(table.QueryAsync);
},
};
Func<IBrowserFile, Task<Dictionary<string, ImportPreviewOutputBase>>> preview = (a => GlobalData.ChannelRuntimeService.PreviewAsync(a));
Func<Dictionary<string, ImportPreviewOutputBase>, Task> import = (value => GlobalData.ChannelRuntimeService.ImportChannelAsync(value, AutoRestartThread));
op.Component = BootstrapDynamicComponent.CreateComponent<ImportExcel>(new Dictionary<string, object?>
{
{nameof(ImportExcel.Import),import },
{nameof(ImportExcel.Preview),preview },
});
await DialogService.Show(op);
}
#endregion
#region
private async Task ClearAsync()
{
try
{
await Task.Run(async () =>
{
await GlobalData.ChannelRuntimeService.DeleteChannelAsync(Items.Select(a => a.Id), AutoRestartThread, default);
await InvokeAsync(async () =>
{
await ToastService.Default();
await InvokeAsync(table.QueryAsync);
});
});
}
catch (Exception ex)
{
await InvokeAsync(async () =>
{
await ToastService.Warn(ex);
});
}
}
#endregion
[Parameter]
public bool AutoRestartThread { get; set; }
[Parameter]
public ChannelDeviceTreeItem SelectModel { get; set; }
[Inject]
[NotNull]
public IStringLocalizer<ThingsGateway.Gateway.Razor._Imports>? GatewayLocalizer { get; set; }
#endregion
}

View File

@@ -0,0 +1,19 @@
.text-h6 {
/* Headline 6 */
font-family: Roboto !important;
font-style: normal !important;
font-weight: bold !important;
font-size: 1rem !important;
line-height: 1.875rem !important;
/* identical to box height */
letter-spacing: 0.01em !important;
}
.text-caption {
/* Caption-说明 */
font-family: Roboto !important;
font-style: normal !important;
font-weight: 500 !important;
font-size: 0.75rem !important;
line-height: 1.125rem !important;
}

View File

@@ -0,0 +1,124 @@
using Mapster;
using ThingsGateway.Gateway.Application;
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
namespace ThingsGateway.Gateway.Razor
{
internal static class ChannelDeviceHelpers
{
internal static Channel GetChannelModel(ItemChangedType itemChangedType, ChannelDeviceTreeItem channelDeviceTreeItem)
{
Channel oneModel = null;
if (channelDeviceTreeItem.TryGetChannelRuntime(out var channelRuntime))
{
oneModel = channelRuntime.Adapt<Channel>();
if (itemChangedType == ItemChangedType.Add)
{
oneModel.Id = 0;
oneModel.Name = $"{oneModel.Name}-Copy";
}
}
else if (channelDeviceTreeItem.TryGetDeviceRuntime(out var deviceRuntime))
{
oneModel = deviceRuntime.ChannelRuntime?.Adapt<Channel>() ?? new();
if (itemChangedType == ItemChangedType.Add)
{
oneModel.Id = 0;
oneModel.Name = $"{oneModel.Name}-Copy";
}
}
else if (channelDeviceTreeItem.TryGetPluginName(out var pluginName))
{
oneModel = new();
oneModel.PluginName = pluginName;
}
else if (channelDeviceTreeItem.TryGetPluginType(out var pluginType))
{
oneModel = new();
}
return oneModel;
}
internal static Device GetDeviceModel(ItemChangedType itemChangedType, ChannelDeviceTreeItem channelDeviceTreeItem)
{
Device oneModel = null;
if (channelDeviceTreeItem.TryGetDeviceRuntime(out var deviceRuntime))
{
oneModel = deviceRuntime.Adapt<Device>();
if (itemChangedType == ItemChangedType.Add)
{
oneModel.Id = 0;
oneModel.Name = $"{oneModel.Name}-Copy";
}
}
else if (channelDeviceTreeItem.TryGetChannelRuntime(out var channelRuntime))
{
oneModel = new();
oneModel.Id = 0;
oneModel.ChannelId = channelRuntime.Id;
}
else
{
oneModel = new();
oneModel.Id = 0;
}
return oneModel;
}
internal static PluginTypeEnum? GetPluginType(ChannelDeviceTreeItem channelDeviceTreeItem)
{
PluginTypeEnum? pluginTypeEnum = null;
if (channelDeviceTreeItem.TryGetPluginType(out var pluginType))
{
pluginTypeEnum = pluginType;
}
return pluginTypeEnum;
}
internal static async Task ShowCopy(Channel oneModel, Dictionary<Device, List<Variable>> deviceDict, string text, bool autoRestart, Func<Task> onsave, DialogService dialogService)
{
var op = new DialogOption()
{
IsScrolling = false,
ShowMaximizeButton = true,
Size = Size.ExtraLarge,
Title = text,
ShowFooter = false,
ShowCloseButton = false,
};
op.Component = BootstrapDynamicComponent.CreateComponent<ChannelCopyComponent>(new Dictionary<string, object?>
{
{nameof(ChannelCopyComponent.OnSave), async (List<Channel> channels,Dictionary<Device,List<Variable>> devices) =>
{
await Task.Run(() =>GlobalData.ChannelRuntimeService.CopyAsync(channels,devices,autoRestart, default));
if(onsave!=null)
await onsave();
}},
{nameof(ChannelCopyComponent.Model),oneModel },
{nameof(ChannelCopyComponent.Devices),deviceDict },
});
await dialogService.Show(op);
}
}
}

View File

@@ -1,4 +1,5 @@
@inherits ThingsGatewayModuleComponentBase
@attribute [JSModuleAutoLoader("Pages/GatewayMonitorPage/ChannelDeviceTree.razor.js")]
@namespace ThingsGateway.Gateway.Razor
@using ThingsGateway.Admin.Application
@using ThingsGateway.Admin.Razor

View File

@@ -117,39 +117,17 @@ public partial class ChannelDeviceTree
ShowFooter = false,
ShowCloseButton = false,
};
PluginTypeEnum? pluginTypeEnum = null;
Channel oneModel = null;
if (value is not ChannelDeviceTreeItem channelDeviceTreeItem) return;
if (channelDeviceTreeItem.TryGetChannelRuntime(out var channelRuntime))
{
oneModel = channelRuntime.Adapt<Channel>();
if (itemChangedType == ItemChangedType.Add)
{
oneModel.Id = 0;
oneModel.Name = $"{oneModel.Name}-Copy";
}
}
else if (channelDeviceTreeItem.TryGetPluginName(out var pluginName))
{
oneModel = new();
oneModel.PluginName = pluginName;
}
else if (channelDeviceTreeItem.TryGetPluginType(out var pluginType))
{
oneModel = new();
pluginTypeEnum = pluginType;
}
else
{
return;
}
if (value is not ChannelDeviceTreeItem channelDeviceTreeItem) return;
PluginTypeEnum? pluginTypeEnum = ChannelDeviceHelpers.GetPluginType(channelDeviceTreeItem);
var oneModel = ChannelDeviceHelpers.GetChannelModel(itemChangedType, channelDeviceTreeItem);
op.Component = BootstrapDynamicComponent.CreateComponent<ChannelEditComponent>(new Dictionary<string, object?>
{
{nameof(ChannelEditComponent.OnValidSubmit), async () =>
{
await Task.Run(() =>GlobalData.ChannelRuntimeService.SaveChannelAsync(oneModel,itemChangedType,AutoRestartThread));
await Notify();
}},
{nameof(ChannelEditComponent.Model),oneModel },
{nameof(ChannelEditComponent.ValidateEnable),true },
@@ -163,15 +141,7 @@ public partial class ChannelDeviceTree
async Task CopyChannel(ContextMenuItem item, object value)
{
var op = new DialogOption()
{
IsScrolling = false,
ShowMaximizeButton = true,
Size = Size.ExtraLarge,
Title = item.Text,
ShowFooter = false,
ShowCloseButton = false,
};
Channel oneModel = null;
Dictionary<Device, List<Variable>> deviceDict = new();
if (value is not ChannelDeviceTreeItem channelDeviceTreeItem) return;
@@ -188,25 +158,6 @@ public partial class ChannelDeviceTree
return;
}
op.Component = BootstrapDynamicComponent.CreateComponent<ChannelCopyComponent>(new Dictionary<string, object?>
{
{nameof(ChannelCopyComponent.OnSave), async (List<Channel> channels,Dictionary<Device,List<Variable>> devices) =>
{
await Task.Run(() =>GlobalData.ChannelRuntimeService.CopyAsync(channels,devices,AutoRestartThread, default));
}},
{nameof(ChannelCopyComponent.Model),oneModel },
{nameof(ChannelCopyComponent.Devices),deviceDict },
});
await DialogService.Show(op);
}
async Task BatchEditChannel(ContextMenuItem item, object value)
{
var op = new DialogOption()
{
@@ -218,6 +169,30 @@ public partial class ChannelDeviceTree
ShowCloseButton = false,
};
op.Component = BootstrapDynamicComponent.CreateComponent<ChannelCopyComponent>(new Dictionary<string, object?>
{
{nameof(ChannelCopyComponent.OnSave), async (List<Channel> channels,Dictionary<Device,List<Variable>> devices) =>
{
await Task.Run(() =>GlobalData.ChannelRuntimeService.CopyAsync(channels,devices,AutoRestartThread, default));
await Notify();
}},
{nameof(ChannelCopyComponent.Model),oneModel },
{nameof(ChannelCopyComponent.Devices),deviceDict },
});
await DialogService.Show(op);
}
async Task BatchEditChannel(ContextMenuItem item, object value)
{
Channel oldModel = null;
Channel oneModel = null;
IEnumerable<Channel>? changedModels = null;
@@ -257,21 +232,31 @@ public partial class ChannelDeviceTree
{
return;
}
var op = new DialogOption()
{
IsScrolling = false,
ShowMaximizeButton = true,
Size = Size.ExtraLarge,
Title = item.Text,
ShowFooter = false,
ShowCloseButton = false,
};
op.Component = BootstrapDynamicComponent.CreateComponent<ChannelEditComponent>(new Dictionary<string, object?>
{
{nameof(ChannelEditComponent.OnValidSubmit), async () =>
{
Spinner.SetRun(true);
Spinner.SetRun(true);
await Task.Run(() => GlobalData.ChannelRuntimeService.BatchEditAsync(changedModels, oldModel, oneModel,AutoRestartThread));
await InvokeAsync( ()=>
{
Spinner.SetRun(false);
StateHasChanged();
await Notify();
await InvokeAsync(() =>
{
Spinner.SetRun(false);
});
}},
} },
{nameof(ChannelEditComponent.Model),oneModel },
{nameof(ChannelEditComponent.ValidateEnable),true },
{nameof(ChannelEditComponent.BatchEditEnable),true },
@@ -279,6 +264,7 @@ public partial class ChannelDeviceTree
await DialogService.Show(op);
}
@@ -315,11 +301,11 @@ public partial class ChannelDeviceTree
}
finally
{
await InvokeAsync( ()=>
await Notify();
await InvokeAsync( ()=>
{
Spinner.SetRun(false);
StateHasChanged();
});
}
@@ -433,10 +419,10 @@ finally
Spinner.SetRun(true);
await Task.Run(() => GlobalData.ChannelRuntimeService.DeleteChannelAsync(modelIds.Select(a => a.Id), AutoRestartThread, default));
await Notify();
await InvokeAsync(() =>
{
Spinner.SetRun(false);
StateHasChanged();
});
}
@@ -477,10 +463,10 @@ finally
var key = await GlobalData.GetCurrentUserChannels().ConfigureAwait(false);
await Task.Run(() => GlobalData.ChannelRuntimeService.DeleteChannelAsync(key.Select(a => a.Id), AutoRestartThread, default));
await Notify();
await InvokeAsync(() =>
{
Spinner.SetRun(false);
StateHasChanged();
});
}
@@ -599,10 +585,10 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
});
await Task.Run(() => GlobalData.ChannelRuntimeService.ImportChannelAsync(value, AutoRestartThread));
await Notify();
await InvokeAsync(() =>
{
Spinner.SetRun(false);
StateHasChanged();
});
});
@@ -654,6 +640,7 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
{
await Task.Run(() =>GlobalData.DeviceRuntimeService.CopyAsync(devices,AutoRestartThread, default));
await Notify();
}},
{nameof(DeviceCopyComponent.Model),oneModel },
@@ -703,6 +690,7 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
{nameof(DeviceEditComponent.OnValidSubmit), async () =>
{
await Task.Run(() =>GlobalData.DeviceRuntimeService.SaveDeviceAsync(oneModel,itemChangedType, AutoRestartThread));
await Notify();
}},
{nameof(DeviceEditComponent.Model),oneModel },
{nameof(DeviceEditComponent.AutoRestartThread),AutoRestartThread },
@@ -836,11 +824,11 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
}
finally
{
await Notify();
await InvokeAsync( ()=>
{
Spinner.SetRun(false);
StateHasChanged();
});
}
@@ -1139,7 +1127,6 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
await Notify();
await InvokeAsync(() =>
{
Spinner.SetRun(false);
});
@@ -1157,13 +1144,13 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
#endregion
[Inject]
SwalService SwalService { get; set; }
[Inject]
ToastService ToastService { get; set; }
[Parameter]
[NotNull]
public ChannelDeviceTreeItem Value { get; set; }
@@ -1309,6 +1296,10 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
{
if (Disposed) return;
await OnClickSearch(SearchText);
if (ChannelDeviceChanged != null)
{
await ChannelDeviceChanged.Invoke(Value);
}
await InvokeAsync(StateHasChanged);
}
finally

View File

@@ -0,0 +1,9 @@

export function getShowType() {
return JSON.parse(localStorage.getItem('showType'))??0;
}
export function saveShowType(showType) {
if (localStorage) {
localStorage.setItem('showType', JSON.stringify(showType));
}
}

View File

@@ -10,125 +10,7 @@
@if (ValidateEnable)
{
<ValidateForm Model="Model" OnValidSubmit="ValidSubmit">
<Tab>
<TabItem Text=@GatewayLocalizer["DeviceInformation"]>
<EditorForm class="p-2" AutoGenerateAllItem="false" RowType=RowType.Inline ItemsPerRow=2 LabelWidth=200 Model="Model">
<FieldItems>
<EditorItem TValue="string" TModel="Device" @bind-Field="@context.Name">
<EditTemplate Context="value">
<div class="col-12">
<h6>@GatewayLocalizer["BasicInformation"]</h6>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.Name" Readonly=BatchEditEnable />
<EditorItem @bind-Field="@context.Description" />
<EditorItem @bind-Field="@context.Enable" />
<EditorItem @bind-Field="@context.LogEnable" />
<EditorItem @bind-Field="@context.LogLevel" />
<EditorItem TValue="string" TModel="Device" @bind-Field="@context.Name">
<EditTemplate Context="value">
<div class="col-12">
<h6>@GatewayLocalizer["Connection"]</h6>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.ChannelId">
<EditTemplate Context="value">
<div class="col-12 col-md-6 ">
<BootstrapInputGroup>
<Select IsVirtualize DefaultVirtualizeItemText=@(GlobalData.ReadOnlyChannels.TryGetValue(value.ChannelId,out var channelRuntime)?channelRuntime.Name:string.Empty) @bind-Value="@value.ChannelId" IsDisabled=BatchEditEnable Items="@_channelItems" OnSelectedItemChanged=OnChannelChanged ShowSearch="true" ShowLabel="true" />
<Button IsDisabled=BatchEditEnable class="text-end" Icon="fa-solid fa-plus" OnClick="AddChannel"></Button>
</BootstrapInputGroup>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.IntervalTime" />
<EditorItem TValue="string" TModel="Device" @bind-Field="@context.Description">
<EditTemplate Context="value">
<div class="col-12">
<h6>@GatewayLocalizer["Redundant"]</h6>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.RedundantEnable" Readonly=BatchEditEnable />
<EditorItem @bind-Field="@context.RedundantDeviceId">
<EditTemplate Context="value">
<div class="col-12 col-md-6">
<Select IsVirtualize DefaultVirtualizeItemText=@(GlobalData.ReadOnlyIdDevices.TryGetValue(value.RedundantDeviceId??0,out var deviceRuntime)?deviceRuntime.Name:string.Empty) @bind-Value="@value.RedundantDeviceId" class="w-100"
OnQueryAsync="(a)=>OnRedundantDevicesQuery(a,value)" IsDisabled="BatchEditEnable"
ShowSearch="true" IsClearable OnClearAsync=@(()=>
{
value.RedundantDeviceId = default;
return Task.CompletedTask;
})>
<DisplayTemplate Context="display">
@{
string device = "none";
if (value.RedundantDeviceId != null)
{
if (value.RedundantDeviceId.HasValue)
if (GlobalData.ReadOnlyIdDevices.TryGetValue(value.RedundantDeviceId.Value, out var deviceRuntime))
device = deviceRuntime?.Name ?? device;
}
@device
}
</DisplayTemplate>
</Select>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.RedundantSwitchType" />
<EditorItem @bind-Field="@context.RedundantScanIntervalTime" />
<EditorItem @bind-Field="@context.RedundantScript" Rows="1" />
<EditorItem TValue="string" TModel="Device" @bind-Field="@context.Description">
<EditTemplate Context="value">
<div class="col-12">
<h6>@GatewayLocalizer["Remark"]</h6>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.Remark1" />
<EditorItem @bind-Field="@context.Remark2" />
<EditorItem @bind-Field="@context.Remark3" />
<EditorItem @bind-Field="@context.Remark4" />
<EditorItem @bind-Field="@context.Remark5" />
</FieldItems>
</EditorForm>
</TabItem>
@if (!BatchEditEnable)
{
<TabItem Text=@GatewayLocalizer["PluginInformation"]>
@if (PluginPropertyModel != null && PluginPropertyEditorItems != null)
{
if (PluginPropertyRenderFragment == null)
{
<PropertyComponent Model="PluginPropertyModel" PluginPropertyEditorItems="PluginPropertyEditorItems" Id=@(Model.Id.ToString()) CanWrite="true" />
}
else
{
@PluginPropertyRenderFragment
}
}
</TabItem>
}
</Tab>
@renderFragment
<div class="form-footer">
@@ -137,4 +19,132 @@
</ValidateForm>
}
</div>
else
{
@renderFragment
}
</div>
@code {
RenderFragment renderFragment =>
@<Tab>
<TabItem Text=@GatewayLocalizer["DeviceInformation"]>
<EditorForm class="p-2" AutoGenerateAllItem="false" RowType=RowType.Inline ItemsPerRow=2 LabelWidth=200 Model="Model">
<FieldItems>
<EditorItem TValue="string" TModel="Device" @bind-Field="@context.Name">
<EditTemplate Context="value">
<div class="col-12">
<h6>@GatewayLocalizer["BasicInformation"]</h6>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.Name" Readonly=BatchEditEnable />
<EditorItem @bind-Field="@context.Description" />
<EditorItem @bind-Field="@context.Enable" />
<EditorItem @bind-Field="@context.LogEnable" />
<EditorItem @bind-Field="@context.LogLevel" />
<EditorItem TValue="string" TModel="Device" @bind-Field="@context.Name">
<EditTemplate Context="value">
<div class="col-12">
<h6>@GatewayLocalizer["Connection"]</h6>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.ChannelId">
<EditTemplate Context="value">
<div class="col-12 col-md-6 ">
<BootstrapInputGroup>
<Select IsVirtualize DefaultVirtualizeItemText=@(GlobalData.ReadOnlyChannels.TryGetValue(value.ChannelId,out var channelRuntime)?channelRuntime.Name:string.Empty) @bind-Value="@value.ChannelId" IsDisabled=BatchEditEnable Items="@_channelItems" OnSelectedItemChanged=OnChannelChanged ShowSearch="true" ShowLabel="true" />
<Button IsDisabled=BatchEditEnable class="text-end" Icon="fa-solid fa-plus" OnClick="AddChannel"></Button>
</BootstrapInputGroup>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.IntervalTime" />
<EditorItem TValue="string" TModel="Device" @bind-Field="@context.Description">
<EditTemplate Context="value">
<div class="col-12">
<h6>@GatewayLocalizer["Redundant"]</h6>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.RedundantEnable" Readonly=BatchEditEnable />
<EditorItem @bind-Field="@context.RedundantDeviceId">
<EditTemplate Context="value">
<div class="col-12 col-md-6">
<Select IsVirtualize DefaultVirtualizeItemText=@(GlobalData.ReadOnlyIdDevices.TryGetValue(value.RedundantDeviceId??0,out var deviceRuntime)?deviceRuntime.Name:string.Empty) @bind-Value="@value.RedundantDeviceId" class="w-100"
OnQueryAsync="(a)=>OnRedundantDevicesQuery(a,value)" IsDisabled="BatchEditEnable"
ShowSearch="true" IsClearable OnClearAsync=@(()=>
{
value.RedundantDeviceId = default;
return Task.CompletedTask;
})>
<DisplayTemplate Context="display">
@{
string device = "none";
if (value.RedundantDeviceId != null)
{
if (value.RedundantDeviceId.HasValue)
if (GlobalData.ReadOnlyIdDevices.TryGetValue(value.RedundantDeviceId.Value, out var deviceRuntime))
device = deviceRuntime?.Name ?? device;
}
@device
}
</DisplayTemplate>
</Select>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.RedundantSwitchType" />
<EditorItem @bind-Field="@context.RedundantScanIntervalTime" />
<EditorItem @bind-Field="@context.RedundantScript" Rows="1" />
<EditorItem TValue="string" TModel="Device" @bind-Field="@context.Description">
<EditTemplate Context="value">
<div class="col-12">
<h6>@GatewayLocalizer["Remark"]</h6>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.Remark1" />
<EditorItem @bind-Field="@context.Remark2" />
<EditorItem @bind-Field="@context.Remark3" />
<EditorItem @bind-Field="@context.Remark4" />
<EditorItem @bind-Field="@context.Remark5" />
</FieldItems>
</EditorForm>
</TabItem>
@if (!BatchEditEnable)
{
<TabItem Text=@GatewayLocalizer["PluginInformation"]>
@if (PluginPropertyModel != null && PluginPropertyEditorItems != null)
{
if (PluginPropertyRenderFragment == null)
{
<PropertyComponent Model="PluginPropertyModel" PluginPropertyEditorItems="PluginPropertyEditorItems" Id=@(Model.Id.ToString()) CanWrite="true" />
}
else
{
@PluginPropertyRenderFragment
}
}
</TabItem>
}
</Tab>;
}

View File

@@ -132,7 +132,7 @@ public partial class DeviceEditComponent
try
{
var pluginName = GlobalData.ReadOnlyChannels.TryGetValue(selectedItem.Value.ToLong(), out var channel) ? channel.PluginName : string.Empty;
if (pluginName.IsNullOrEmpty()) return;
var data = GlobalData.PluginService.GetDriverPropertyTypes(pluginName);
PluginPropertyModel = new ModelValueValidateForm() { Value = data.Model };
PluginPropertyEditorItems = data.EditorItems;

View File

@@ -0,0 +1,114 @@
@namespace ThingsGateway.Gateway.Razor
@using ThingsGateway.Admin.Application
@using ThingsGateway.Admin.Razor
@using ThingsGateway.Debug
@using ThingsGateway.Gateway.Application
@inherits ComponentDefault
<AdminTable @ref=table
TItem="DeviceRuntime"
EditDialogSize="Size.ExtraLarge"
AutoGenerateColumns="false"
ShowAdvancedSearch=false
ScrollingDialogContent=false
AllowResizing="true"
OnAdd="OnAdd"
IsFixedHeader=true
IsMultipleSelect=true
SearchMode=SearchMode.Top
ShowExtendButtons=true
ShowToolbar="true"
ShowExportButton
ShowDefaultButtons=true
ShowSearch=false
ShowExtendEditButton="true"
ShowExtendDeleteButton="true"
ExtendButtonColumnWidth=220
OnSaveAsync="Save"
OnDeleteAsync="Delete"
OnQueryAsync="OnQueryAsync"
IsPagination=true>
<TableColumns>
<TableColumn Field="@context.Name" FieldExpression=@(()=>context.Name) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.Description" FieldExpression=@(()=>context.Description) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.IntervalTime" FieldExpression=@(()=>context.IntervalTime) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.ChannelName" FieldExpression=@(()=>context.ChannelName) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn @bind-Field="@context.Enable" Filterable=true Sortable=true Visible="true" />
<TableColumn @bind-Field="@context.LogEnable" Filterable=true Sortable=true Visible="false" />
<TableColumn @bind-Field="@context.LogLevel" Filterable=true Sortable=true Visible="false" />
<TableColumn @bind-Field="@context.Remark1" Filterable=true Sortable=true Visible="false" />
<TableColumn @bind-Field="@context.Remark2" Filterable=true Sortable=true Visible="false" />
<TableColumn @bind-Field="@context.Remark3" Filterable=true Sortable=true Visible="false" />
<TableColumn @bind-Field="@context.Remark4" Filterable=true Sortable=true Visible="false" />
<TableColumn @bind-Field="@context.Remark5" Filterable=true Sortable=true Visible="false" />
<TableColumn Field="@context.ActiveTime" FieldExpression=@(()=>context.ActiveTime) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.DeviceStatus" FieldExpression=@(()=>context.DeviceStatus) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.Pause" FieldExpression=@(()=>context.Pause) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.LastErrorMessage" FieldExpression=@(()=>context.LastErrorMessage) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.PluginName" FieldExpression=@(()=>context.PluginName) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.DeviceVariableCount" FieldExpression=@(()=>context.DeviceVariableCount) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.MethodVariableCount" FieldExpression=@(()=>context.MethodVariableCount) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.SourceVariableCount" FieldExpression=@(()=>context.SourceVariableCount) ShowTips=true Filterable=true Sortable=true Visible=true />
<TableColumn Field="@context.ChannelId" FieldExpression=@(()=>context.ChannelId) ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.RedundantEnable" FieldExpression=@(()=>context.RedundantEnable) Ignore="true" ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.RedundantType" FieldExpression=@(()=>context.RedundantType) Ignore="true" ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.RedundantDeviceId" FieldExpression=@(()=>context.RedundantDeviceId) Ignore="true" ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.RedundantScanIntervalTime" FieldExpression=@(()=>context.RedundantScanIntervalTime) Ignore="true" ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.RedundantScript" FieldExpression=@(()=>context.RedundantScript) Ignore="true" ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn Field="@context.RedundantSwitchType" FieldExpression=@(()=>context.RedundantSwitchType) Ignore="true" ShowTips=true Filterable=true Sortable=true Visible=false />
<TableColumn @bind-Field="@context.Id" Filterable=true Sortable=true Visible="false" DefaultSort=true DefaultSortOrder="SortOrder.Asc" />
</TableColumns>
<EditTemplate Context="context">
<DeviceEditComponent Model=@(context) ValidateEnable=false BatchEditEnable=false AutoRestartThread=AutoRestartThread ></DeviceEditComponent>
</EditTemplate>
<ExportButtonDropdownTemplate Context="ExportContext">
<Button class="dropdown-item" OnClick="() => ExcelExportAsync(ExportContext,true)" IsDisabled=@(!AuthorizeButton("导出"))>
<i class="fas fa-file-export"></i>
<span>@RazorLocalizer["ExportAll"]</span>
</Button>
<Button class="dropdown-item" OnClick="() => ExcelExportAsync(ExportContext)" IsDisabled=@(!AuthorizeButton("导出"))>
<i class="fas fa-file-export"></i>
<span>@RazorLocalizer["TablesExportButtonExcelText"]</span>
</Button>
<Button class="dropdown-item" OnClick="() => ExcelImportAsync(ExportContext)" IsDisabled=@(!AuthorizeButton("导入"))>
<i class="fas fa-file-import"></i>
<span>@RazorLocalizer["TablesImportButtonExcelText"]</span>
</Button>
<Button class="dropdown-item" OnClick="() => ExcelDeviceAsync(ExportContext)" IsDisabled=@(!AuthorizeButton("导入"))>
<i class="fas fa-file-import"></i>
<span>@GatewayLocalizer["ExcelDevice"]</span>
</Button>
</ExportButtonDropdownTemplate>
<TableToolbarTemplate>
<TableToolbarButton TItem="DeviceRuntime" IsDisabled=@(!AuthorizeButton(AdminOperConst.Add))
Color=Color.Success Text="@RazorLocalizer["Copy"]"
OnClickCallback=@(Copy) />
<TableToolbarButton TItem="DeviceRuntime" IsDisabled=@(!AuthorizeButton(AdminOperConst.Edit))
Color=Color.Info Text="@RazorLocalizer["BatchEdit"]"
OnClickCallback=@(BatchEdit) />
<TableToolbarPopConfirmButton TItem="DeviceRuntime"
Color=Color.Warning Text="@RazorLocalizer["Clear"]" IsDisabled=@(!AuthorizeButton(AdminOperConst.Delete))
IsAsync OnConfirm=@(ClearAsync) />
</TableToolbarTemplate>
</AdminTable>
@code {
AdminTable<DeviceRuntime> table;
}

View File

@@ -0,0 +1,371 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
using Mapster;
using Microsoft.AspNetCore.Components.Forms;
using ThingsGateway.Admin.Application;
using ThingsGateway.Extension.Generic;
using ThingsGateway.Gateway.Application;
using ThingsGateway.NewLife.Extension;
namespace ThingsGateway.Gateway.Razor;
public partial class DeviceTable : IDisposable
{
public bool Disposed { get; set; }
[Parameter]
public IEnumerable<DeviceRuntime>? Items { get; set; } = Enumerable.Empty<DeviceRuntime>();
public void Dispose()
{
Disposed = true;
GC.SuppressFinalize(this);
}
protected override void OnInitialized()
{
_ = RunTimerAsync();
base.OnInitialized();
}
private async Task RunTimerAsync()
{
while (!Disposed)
{
try
{
if (table != null)
await table.QueryAsync();
}
catch (Exception ex)
{
NewLife.Log.XTrace.WriteException(ex);
}
finally
{
await Task.Delay(1000);
}
}
}
#region
private QueryPageOptions _option = new();
private Task<QueryData<DeviceRuntime>> OnQueryAsync(QueryPageOptions options)
{
var data = Items
.WhereIf(!options.SearchText.IsNullOrWhiteSpace(), a => a.Name.Contains(options.SearchText))
.GetQueryData(options);
_option = options;
return Task.FromResult(data);
}
#endregion
#region
#region
private async Task Copy(IEnumerable<DeviceRuntime> devices)
{
if (!devices.Any())
{
await ToastService.Warning(null, RazorLocalizer["PleaseSelect"]);
return;
}
Device oneModel = null;
List<Variable> variables = new();
var deviceRuntime = devices.FirstOrDefault();
oneModel = deviceRuntime.Adapt<Device>();
oneModel.Id = 0;
variables = deviceRuntime.ReadOnlyVariableRuntimes.Select(a => a.Value).Adapt<List<Variable>>();
var op = new DialogOption()
{
IsScrolling = false,
ShowMaximizeButton = true,
Size = Size.ExtraLarge,
Title = RazorLocalizer["Copy"],
ShowFooter = false,
ShowCloseButton = false,
};
op.Component = BootstrapDynamicComponent.CreateComponent<DeviceCopyComponent>(new Dictionary<string, object?>
{
{nameof(DeviceCopyComponent.OnSave), async (Dictionary<Device,List<Variable>> devices) =>
{
await Task.Run(() =>GlobalData.DeviceRuntimeService.CopyAsync(devices,AutoRestartThread, default));
await table.QueryAsync();
}},
{nameof(DeviceCopyComponent.Model),oneModel },
{nameof(DeviceCopyComponent.Variables),variables },
});
await DialogService.Show(op);
}
private async Task BatchEdit(IEnumerable<Device> changedModels)
{
var oldModel = changedModels.FirstOrDefault();//默认值显示第一个
if (oldModel == null)
{
await ToastService.Warning(null, RazorLocalizer["PleaseSelect"]);
return;
}
var oneModel = oldModel.Adapt<Device>();//默认值显示第一个
var op = new DialogOption()
{
IsScrolling = false,
ShowMaximizeButton = true,
Size = Size.ExtraLarge,
Title = RazorLocalizer["BatchEdit"],
ShowFooter = false,
ShowCloseButton = false,
};
op.Component = BootstrapDynamicComponent.CreateComponent<DeviceEditComponent>(new Dictionary<string, object?>
{
{nameof(DeviceEditComponent.OnValidSubmit), async () =>
{
await Task.Run(() => GlobalData.DeviceRuntimeService.BatchEditAsync(changedModels, oldModel, oneModel,AutoRestartThread));
await InvokeAsync(table.QueryAsync);
} },
{nameof(DeviceEditComponent.Model),oneModel },
{nameof(DeviceEditComponent.ValidateEnable),true },
{nameof(DeviceEditComponent.BatchEditEnable),true },
});
await DialogService.Show(op);
}
private async Task<bool> Delete(IEnumerable<Device> devices)
{
try
{
return await Task.Run(async () =>
{
return await GlobalData.DeviceRuntimeService.DeleteDeviceAsync(devices.Select(a => a.Id), AutoRestartThread, default);
});
}
catch (Exception ex)
{
await InvokeAsync(async () =>
{
await ToastService.Warn(ex);
});
return false;
}
}
private async Task<bool> Save(Device device, ItemChangedType itemChangedType)
{
try
{
return await Task.Run(() => GlobalData.DeviceRuntimeService.SaveDeviceAsync(device, itemChangedType, AutoRestartThread));
}
catch (Exception ex)
{
await ToastService.Warn(ex);
return false;
}
}
#endregion
private Task<DeviceRuntime> OnAdd()
{
return Task.FromResult(ChannelDeviceHelpers.GetDeviceModel(ItemChangedType.Add, SelectModel).Adapt<DeviceRuntime>());
}
#region
[Inject]
[NotNull]
private IGatewayExportService? GatewayExportService { get; set; }
private async Task ExcelExportAsync(ITableExportContext<DeviceRuntime> tableExportContext, bool all = false)
{
if (all)
{
await GatewayExportService.OnDeviceExport(new() { QueryPageOptions = new() });
}
else
{
switch (SelectModel.ChannelDevicePluginType)
{
case ChannelDevicePluginTypeEnum.PluginName:
await GatewayExportService.OnDeviceExport(new() { QueryPageOptions = new(), PluginName = SelectModel.PluginName });
break;
case ChannelDevicePluginTypeEnum.Channel:
await GatewayExportService.OnDeviceExport(new() { QueryPageOptions = new(), ChannelId = SelectModel.ChannelRuntime.Id });
break;
case ChannelDevicePluginTypeEnum.Device:
await GatewayExportService.OnDeviceExport(new() { QueryPageOptions = new(), DeviceId = SelectModel.DeviceRuntime.Id, PluginType = SelectModel.DeviceRuntime.PluginType });
break;
default:
await GatewayExportService.OnDeviceExport(new() { QueryPageOptions = new() });
break;
}
}
// 返回 true 时自动弹出提示框
await ToastService.Default();
}
async Task ExcelDeviceAsync(ITableExportContext<DeviceRuntime> tableExportContext)
{
var op = new DialogOption()
{
IsScrolling = false,
ShowMaximizeButton = true,
Size = Size.ExtraExtraLarge,
Title = GatewayLocalizer["ExcelDevice"],
ShowFooter = false,
ShowCloseButton = false,
};
var option = _option;
option.IsPage = false;
var models = Items
.WhereIf(!option.SearchText.IsNullOrWhiteSpace(), a => a.Name.Contains(option.SearchText)).GetData(option, out var total).ToList();
if (models.Count > 50000)
{
await ToastService.Warning("online Excel max data count 50000");
return;
}
var uSheetDatas = await DeviceServiceHelpers.ExportDeviceAsync(models);
op.Component = BootstrapDynamicComponent.CreateComponent<USheet>(new Dictionary<string, object?>
{
{nameof(USheet.OnSave), async (USheetDatas data) =>
{
try
{
await Task.Run(async ()=>
{
var importData=await DeviceServiceHelpers.ImportAsync(data);
await GlobalData.DeviceRuntimeService.ImportDeviceAsync(importData,AutoRestartThread);
})
;
}
finally
{
await InvokeAsync( async ()=>
{
await table.QueryAsync();
StateHasChanged();
});
}
}},
{nameof(USheet.Model),uSheetDatas },
});
await DialogService.Show(op);
}
private async Task ExcelImportAsync(ITableExportContext<DeviceRuntime> tableExportContext)
{
var op = new DialogOption()
{
IsScrolling = true,
ShowMaximizeButton = true,
Size = Size.ExtraLarge,
Title = GatewayLocalizer["ImportDevice"],
ShowFooter = false,
ShowCloseButton = false,
OnCloseAsync = async () =>
{
await InvokeAsync(table.QueryAsync);
},
};
Func<IBrowserFile, Task<Dictionary<string, ImportPreviewOutputBase>>> preview = (a => GlobalData.DeviceRuntimeService.PreviewAsync(a));
Func<Dictionary<string, ImportPreviewOutputBase>, Task> import = (value => GlobalData.DeviceRuntimeService.ImportDeviceAsync(value, AutoRestartThread));
op.Component = BootstrapDynamicComponent.CreateComponent<ImportExcel>(new Dictionary<string, object?>
{
{nameof(ImportExcel.Import),import },
{nameof(ImportExcel.Preview),preview },
});
await DialogService.Show(op);
}
#endregion
#region
private async Task ClearAsync()
{
try
{
await Task.Run(async () =>
{
await GlobalData.DeviceRuntimeService.DeleteDeviceAsync(Items.Select(a => a.Id), AutoRestartThread, default);
await InvokeAsync(async () =>
{
await ToastService.Default();
await InvokeAsync(table.QueryAsync);
});
});
}
catch (Exception ex)
{
await InvokeAsync(async () =>
{
await ToastService.Warn(ex);
});
}
}
#endregion
[Parameter]
public bool AutoRestartThread { get; set; }
[Parameter]
public ChannelDeviceTreeItem SelectModel { get; set; }
[Inject]
[NotNull]
public IStringLocalizer<ThingsGateway.Gateway.Razor._Imports>? GatewayLocalizer { get; set; }
#endregion
}

View File

@@ -0,0 +1,19 @@
.text-h6 {
/* Headline 6 */
font-family: Roboto !important;
font-style: normal !important;
font-weight: bold !important;
font-size: 1rem !important;
line-height: 1.875rem !important;
/* identical to box height */
letter-spacing: 0.01em !important;
}
.text-caption {
/* Caption-说明 */
font-family: Roboto !important;
font-style: normal !important;
font-weight: 500 !important;
font-size: 0.75rem !important;
line-height: 1.125rem !important;
}

View File

@@ -5,11 +5,11 @@
@namespace ThingsGateway.Gateway.Razor
@if (ShowType == ShowTypeEnum.Variable)
@if (ShowType == ShowTypeEnum.VariableTable)
{
<VariableRuntimeInfo Items="VariableRuntimes" SelectModel="SelectModel" AutoRestartThread="AutoRestartThread" />
}
else
else if (ShowType == ShowTypeEnum.LogInfo)
{
if (GlobalData.ReadOnlyIdDevices.TryGetValue(ShowDeviceRuntime, out var device))
{
@@ -20,3 +20,12 @@ else
<ChannelRuntimeInfo ChannelRuntime="channel" />
}
}
else if (ShowType == ShowTypeEnum.ChannelTable)
{
<ChannelTable SelectModel="SelectModel" Items="ChannelRuntimes" AutoRestartThread=AutoRestartThread />
}
else if (ShowType == ShowTypeEnum.DeviceTable)
{
<DeviceTable SelectModel="SelectModel" Items="DeviceRuntimes" AutoRestartThread=AutoRestartThread />
}

View File

@@ -15,11 +15,16 @@ namespace ThingsGateway.Gateway.Razor;
public partial class GatewayInfo
{
[Parameter]
public ChannelDeviceTreeItem SelectModel { get; set; } = new() { ChannelDevicePluginType = ChannelDevicePluginTypeEnum.PluginType, PluginType = PluginTypeEnum.Collect };
public ChannelDeviceTreeItem SelectModel { get; set; } = new() { ChannelDevicePluginType = ChannelDevicePluginTypeEnum.PluginType, PluginType = null };
[Parameter]
public IEnumerable<VariableRuntime> VariableRuntimes { get; set; } = Enumerable.Empty<VariableRuntime>();
[Parameter]
public IEnumerable<ChannelRuntime> ChannelRuntimes { get; set; } = Enumerable.Empty<ChannelRuntime>();
[Parameter]
public IEnumerable<DeviceRuntime> DeviceRuntimes { get; set; } = Enumerable.Empty<DeviceRuntime>();
[Parameter]
public long ShowChannelRuntime { get; set; }
[Parameter]

View File

@@ -13,9 +13,10 @@
<FirstPaneTemplate>
<Card IsShadow=true class="h-100" Color="Color.Primary">
<Card IsShadow=true class="h-100 me-1" Color="Color.Primary">
<BodyTemplate>
<ChannelDeviceTree @bind-ShowType=ShowType AutoRestartThread="AutoRestartThread" ChannelDeviceChanged="TreeChangedAsync" Value="SelectModel"></ChannelDeviceTree>
<ChannelDeviceTree @bind-ShowType=ShowType AutoRestartThread="AutoRestartThread"
ChannelDeviceChanged="TreeChangedAsync" Value="SelectModel"></ChannelDeviceTree>
</BodyTemplate>
</Card>
@@ -23,7 +24,9 @@
</FirstPaneTemplate>
<SecondPaneTemplate>
<GatewayInfo AutoRestartThread=AutoRestartThread SelectModel=SelectModel ShowChannelRuntime=ShowChannelRuntime ShowDeviceRuntime=ShowDeviceRuntime ShowType=ShowType VariableRuntimes=VariableRuntimes />
<div class="h-100 ms-1">
<GatewayInfo AutoRestartThread=AutoRestartThread SelectModel=SelectModel ShowChannelRuntime=ShowChannelRuntime ShowDeviceRuntime=ShowDeviceRuntime ShowType=ShowType VariableRuntimes=VariableRuntimes ChannelRuntimes="ChannelRuntimes" DeviceRuntimes="DeviceRuntimes" />
</div>
</SecondPaneTemplate>
</Split>

View File

@@ -14,7 +14,7 @@ namespace ThingsGateway.Gateway.Razor;
public partial class GatewayMonitorPage
{
private ChannelDeviceTreeItem SelectModel { get; set; } = new() { ChannelDevicePluginType = ChannelDevicePluginTypeEnum.PluginType, PluginType = PluginTypeEnum.Collect };
private ChannelDeviceTreeItem SelectModel { get; set; } = new() { ChannelDevicePluginType = ChannelDevicePluginTypeEnum.PluginType, PluginType = null };
#region
@@ -24,9 +24,13 @@ public partial class GatewayMonitorPage
ShowChannelRuntime = 0;
ShowDeviceRuntime = 0;
SelectModel = channelDeviceTreeItem;
var variables = await GlobalData.GetCurrentUserIdVariables().ConfigureAwait(false);
var channels = await GlobalData.GetCurrentUserChannels().ConfigureAwait(false);
var devices = await GlobalData.GetCurrentUserDevices().ConfigureAwait(false);
if (channelDeviceTreeItem.TryGetChannelRuntime(out var channelRuntime))
{
ShowChannelRuntime = channelRuntime.Id;
if (channelRuntime.IsCollect == true)
{
VariableRuntimes = channelRuntime.ReadDeviceRuntimes.SelectMany(a => a.Value.ReadOnlyVariableRuntimes.Select(a => a.Value).Where(a => a != null));
@@ -35,7 +39,8 @@ public partial class GatewayMonitorPage
{
VariableRuntimes = channelRuntime.ReadDeviceRuntimes.Where(a => a.Value?.Driver?.IdVariableRuntimes != null).SelectMany(a => a.Value?.Driver?.IdVariableRuntimes?.Where(a => a.Value != null)?.Select(a => a.Value)).Where(a => a != null);
}
ChannelRuntimes = Enumerable.Repeat(channelRuntime, 1);
DeviceRuntimes = channelRuntime.ReadDeviceRuntimes.Select(a => a.Value);
}
else if (channelDeviceTreeItem.TryGetDeviceRuntime(out var deviceRuntime))
{
@@ -49,26 +54,43 @@ public partial class GatewayMonitorPage
VariableRuntimes = deviceRuntime.Driver?.IdVariableRuntimes?.Where(a => a.Value != null)
.Select(a => a.Value) ?? Enumerable.Empty<VariableRuntime>();
}
ChannelRuntimes = Enumerable.Repeat(deviceRuntime.ChannelRuntime, 1);
DeviceRuntimes = Enumerable.Repeat(deviceRuntime, 1);
}
else if (channelDeviceTreeItem.TryGetPluginName(out var pluginName))
{
var pluginType = GlobalData.PluginService.GetList().FirstOrDefault(a => a.FullName == pluginName)?.PluginType;
if (pluginType == PluginTypeEnum.Collect)
{
var channels = await GlobalData.GetCurrentUserChannels().ConfigureAwait(false);
VariableRuntimes = channels.Where(a => a.PluginName == pluginName).SelectMany(a => a.ReadDeviceRuntimes).SelectMany(a => a.Value.ReadOnlyVariableRuntimes).Select(a => a.Value).Where(a => a != null);
}
else
{
var channels = await GlobalData.GetCurrentUserChannels().ConfigureAwait(false);
VariableRuntimes = channels.Where(a => a.PluginName == pluginName).SelectMany(a => a.ReadDeviceRuntimes).Where(a => a.Value.Driver?.IdVariableRuntimes != null).SelectMany(a => a.Value.Driver?.IdVariableRuntimes).Select(a => a.Value);
}
ChannelRuntimes = channels.Where(a => a.PluginName == pluginName);
DeviceRuntimes = devices.Where(a => a.PluginName == pluginName);
}
else
{
var variables = await GlobalData.GetCurrentUserIdVariables().ConfigureAwait(false);
VariableRuntimes = variables.Where(a => a != null);
if (channelDeviceTreeItem.TryGetPluginType(out var pluginTypeEnum))
{
if (pluginTypeEnum != null)
{
ChannelRuntimes = channels.Where(a => a.PluginType == pluginTypeEnum);
DeviceRuntimes = devices.Where(a => a.PluginType == pluginTypeEnum);
}
else
{
ChannelRuntimes = channels;
DeviceRuntimes = devices;
}
}
}
await InvokeAsync(StateHasChanged);
}
@@ -82,6 +104,10 @@ public partial class GatewayMonitorPage
}
public IEnumerable<VariableRuntime> VariableRuntimes { get; set; } = Enumerable.Empty<VariableRuntime>();
public IEnumerable<ChannelRuntime> ChannelRuntimes { get; set; } = Enumerable.Empty<ChannelRuntime>();
public IEnumerable<DeviceRuntime> DeviceRuntimes { get; set; } = Enumerable.Empty<DeviceRuntime>();
private long ShowChannelRuntime { get; set; }
private long ShowDeviceRuntime { get; set; }
public ShowTypeEnum? ShowType { get; set; }

View File

@@ -12,6 +12,8 @@ namespace ThingsGateway.Gateway.Razor;
public enum ShowTypeEnum
{
Variable,
LogInfo
LogInfo,
ChannelTable,
DeviceTable,
VariableTable,
}

View File

@@ -60,7 +60,7 @@
<TableColumn @bind-Field="@context.Index" Filterable=true Sortable=true Visible="false" />
<TableColumn @bind-Field="@context.Id" Filterable=true Sortable=true Visible="false" DefaultSort=true DefaultSortOrder="SortOrder.Asc" />
</TableColumns>
<RowButtonTemplate>
@@ -94,12 +94,12 @@
</Button>
<Button class="dropdown-item" OnClick="() => ExcelVariableAsync(ExportContext)" IsDisabled=@(!AuthorizeButton("导入"))>
<i class="fas fa-file-import"></i>
<span>@Localizer["ExcelVariable"]</span>
<span>@GatewayLocalizer["ExcelVariable"]</span>
</Button>
</ExportButtonDropdownTemplate>
<TableToolbarTemplate>
<TableToolbarButton TItem="VariableRuntime" IsDisabled=@(!AuthorizeButton(AdminOperConst.Add))
<TableToolbarButton TItem="VariableRuntime" IsDisabled=@(!AuthorizeButton(AdminOperConst.Add))
Color=Color.Success Text="@RazorLocalizer["Copy"]"
OnClickCallback=@(Copy) />
@@ -108,19 +108,22 @@
OnClickCallback=@(BatchEdit) />
<TableToolbarPopConfirmButton TItem="VariableRuntime"
Color=Color.Warning Text="@RazorLocalizer["Clear"]" IsDisabled=@(!AuthorizeButton("清空"))
IsAsync OnConfirm=@(ClearVariableAsync) />
Color=Color.Warning Text="@RazorLocalizer["Clear"]" IsDisabled=@(!AuthorizeButton(AdminOperConst.Delete))
IsAsync OnConfirm=@(ClearAsync) />
<PopConfirmButton Color=Color.Warning Text="@Localizer["Test"]" IsDisabled=@(!AuthorizeButton(AdminOperConst.Add))
IsAsync OnConfirm=@(InsertTestDataAsync)>
@if (WebsiteOption.Value.Demo)
{
<PopConfirmButton Color=Color.Warning Text="@Localizer["Test"]" IsDisabled=@(!AuthorizeButton(AdminOperConst.Add))
IsAsync OnConfirm=@(InsertTestDataAsync)>
<BodyTemplate>
<BootstrapInput @bind-Value=TestVariableCount ShowLabel="true" ShowLabelTooltip="true" />
<BootstrapInput @bind-Value=TestDeviceCount ShowLabel="true" ShowLabelTooltip="true" />
<BootstrapInput @bind-Value=SlaveUrl ShowLabel="true" ShowLabelTooltip="true" />
</BodyTemplate>
<BodyTemplate>
<BootstrapInput @bind-Value=TestVariableCount ShowLabel="true" ShowLabelTooltip="true" />
<BootstrapInput @bind-Value=TestDeviceCount ShowLabel="true" ShowLabelTooltip="true" />
<BootstrapInput @bind-Value=SlaveUrl ShowLabel="true" ShowLabelTooltip="true" />
</BodyTemplate>
</PopConfirmButton>
</PopConfirmButton>
}
</TableToolbarTemplate>
</AdminTable>

View File

@@ -11,6 +11,7 @@
using Mapster;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.Extensions.Options;
using ThingsGateway.Admin.Application;
using ThingsGateway.Extension.Generic;
@@ -22,6 +23,8 @@ namespace ThingsGateway.Gateway.Razor;
public partial class VariableRuntimeInfo : IDisposable
{
[Inject]
private IOptions<WebsiteOptions>? WebsiteOption { get; set; }
public bool Disposed { get; set; }
[Parameter]
@@ -169,19 +172,19 @@ public partial class VariableRuntimeInfo : IDisposable
ShowFooter = false,
ShowCloseButton = false,
};
var oldmodel = variables.FirstOrDefault();//默认值显示第一个
if (oldmodel == null)
var oldModel = variables.FirstOrDefault();//默认值显示第一个
if (oldModel == null)
{
await ToastService.Warning(null, RazorLocalizer["PleaseSelect"]);
return;
}
var model = oldmodel.Adapt<Variable>();//默认值显示第一个
var model = oldModel.Adapt<Variable>();//默认值显示第一个
op.Component = BootstrapDynamicComponent.CreateComponent<VariableEditComponent>(new Dictionary<string, object?>
{
{nameof(VariableEditComponent.OnValidSubmit), async () =>
{
await Task.Run(()=> GlobalData. VariableRuntimeService.BatchEditAsync(variables,oldmodel,model, AutoRestartThread,default));
await Task.Run(()=> GlobalData. VariableRuntimeService.BatchEditAsync(variables,oldModel,model, AutoRestartThread,default));
await InvokeAsync(table.QueryAsync);
}},
@@ -194,13 +197,13 @@ public partial class VariableRuntimeInfo : IDisposable
}
private async Task<bool> Delete(IEnumerable<Variable> devices)
private async Task<bool> Delete(IEnumerable<Variable> variables)
{
try
{
return await Task.Run(async () =>
{
return await GlobalData.VariableRuntimeService.DeleteVariableAsync(devices.Select(a => a.Id), AutoRestartThread, default);
return await GlobalData.VariableRuntimeService.DeleteVariableAsync(variables.Select(a => a.Id), AutoRestartThread, default);
});
}
@@ -294,7 +297,7 @@ public partial class VariableRuntimeInfo : IDisposable
IsScrolling = false,
ShowMaximizeButton = true,
Size = Size.ExtraExtraLarge,
Title = Localizer["ExcelVariable"],
Title = GatewayLocalizer["ExcelVariable"],
ShowFooter = false,
ShowCloseButton = false,
};
@@ -348,7 +351,7 @@ finally
IsScrolling = true,
ShowMaximizeButton = true,
Size = Size.ExtraLarge,
Title = Localizer["ImportExcel"],
Title = GatewayLocalizer["ImportVariable"],
ShowFooter = false,
ShowCloseButton = false,
OnCloseAsync = async () =>
@@ -373,7 +376,7 @@ finally
#region
private async Task ClearVariableAsync()
private async Task ClearAsync()
{
try
{
@@ -398,8 +401,7 @@ finally
}
#endregion
[Inject]
MaskService MaskService { get; set; }
private async Task InsertTestDataAsync()
{
try
@@ -434,6 +436,8 @@ finally
public bool AutoRestartThread { get; set; }
[Parameter]
public ChannelDeviceTreeItem SelectModel { get; set; }
[Inject]
[NotNull]
public IStringLocalizer<ThingsGateway.Gateway.Razor._Imports>? GatewayLocalizer { get; set; }
#endregion
}

View File

@@ -34,14 +34,7 @@ public interface IRulesService
/// 从缓存/数据库获取全部信息
/// </summary>
/// <returns>规则列表</returns>
List<Rules> GetAll();
/// <summary>
/// 通过ID获取规则
/// </summary>
/// <param name="id">规则ID</param>
/// <returns>规则对象</returns>
Rules? GetRulesById(long id);
Task<List<Rules>> GetAllAsync();
/// <summary>
/// 报表查询

View File

@@ -58,7 +58,7 @@ internal sealed class RulesEngineHostedService : BackgroundService, IRulesEngine
if (rules.Status)
{
var data = Init(rules);
await Start(data.rulesLog, data.blazorDiagram, default).ConfigureAwait(false);
await Start(data.rulesLog, data.blazorDiagram, TokenSource.Token).ConfigureAwait(false);
var service = App.GetService<IDispatchService<Rules>>();
service.Dispatch(new());
}
@@ -108,23 +108,15 @@ internal sealed class RulesEngineHostedService : BackgroundService, IRulesEngine
return result;
}
private static async Task Start(RulesLog rulesLog, BlazorDiagram item, CancellationToken cancellationToken)
private static Task Start(RulesLog rulesLog, BlazorDiagram item, CancellationToken cancellationToken)
{
rulesLog.Log.Trace("Start");
var startNodes = item.Nodes.Where(a => a is StartNode);
startNodes.ForEach(a =>
{
if (a is INode node)
{
node.Logger = rulesLog.Log;
node.RulesEngineName = rulesLog.Rules.Name;
}
}
);
foreach (var link in startNodes.SelectMany(a => a.PortLinks))
{
rulesLog.Log.Trace("Start");
await Analysis((link.Target.Model as PortModel)?.Parent, new NodeInput(), rulesLog, cancellationToken).ConfigureAwait(false);
_ = Analysis((link.Target.Model as PortModel)?.Parent, new NodeInput(), rulesLog, cancellationToken);
}
return Task.CompletedTask;
}
private static async Task Analysis(NodeModel targetNode, NodeInput input, RulesLog rulesLog, CancellationToken cancellationToken)
@@ -192,11 +184,11 @@ internal sealed class RulesEngineHostedService : BackgroundService, IRulesEngine
#region worker服务
private async Task BefortStart(CancellationToken cancellationToken)
private async Task StartAll(CancellationToken cancellationToken)
{
AfterStop();
Clear();
Rules = App.GetService<IRulesService>().GetAll();
Rules = await App.GetService<IRulesService>().GetAllAsync().ConfigureAwait(false);
BlazorDiagrams = new();
foreach (var rules in Rules.Where(a => a.Status))
{
@@ -223,29 +215,10 @@ internal sealed class RulesEngineHostedService : BackgroundService, IRulesEngine
}
private void AfterStop()
{
foreach (var item in BlazorDiagrams.Values)
{
foreach (var nodeModel in item.Nodes)
{
nodeModel.TryDispose();
}
}
BlazorDiagrams.Clear();
}
private CancellationTokenSource? TokenSource { get; set; }
private void Cancel()
{
if (TokenSource != null)
{
TokenSource.Cancel();
TokenSource.Dispose();
TokenSource = null;
}
}
internal async Task StartAsync()
{
@@ -253,7 +226,7 @@ internal sealed class RulesEngineHostedService : BackgroundService, IRulesEngine
{
await RestartLock.WaitAsync().ConfigureAwait(false); // 等待获取锁,以确保只有一个线程可以执行以下代码
TokenSource ??= new CancellationTokenSource();
await BefortStart(TokenSource.Token).ConfigureAwait(false);
await StartAll(TokenSource.Token).ConfigureAwait(false);
_logger.LogInformation(Localizer["RulesEngineTaskStart"]);
}
catch (Exception ex)
@@ -272,7 +245,7 @@ internal sealed class RulesEngineHostedService : BackgroundService, IRulesEngine
{
await RestartLock.WaitAsync().ConfigureAwait(false); // 等待获取锁,以确保只有一个线程可以执行以下代码
Cancel();
AfterStop();
Clear();
}
catch (Exception ex)
{
@@ -284,6 +257,28 @@ internal sealed class RulesEngineHostedService : BackgroundService, IRulesEngine
}
}
private void Cancel()
{
if (TokenSource != null)
{
TokenSource.Cancel();
TokenSource.Dispose();
TokenSource = null;
}
}
private void Clear()
{
foreach (var item in BlazorDiagrams.Values)
{
foreach (var nodeModel in item.Nodes)
{
nodeModel.TryDispose();
}
}
BlazorDiagrams.Clear();
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
return Task.CompletedTask;

View File

@@ -42,7 +42,7 @@ internal sealed class RulesService : BaseService<Rules>, IRulesService
using var db = GetDB();
var data = GetAll()
var data = (await GetAllAsync().ConfigureAwait(false))
.WhereIf(dataScope != null && dataScope?.Count > 0, u => dataScope.Contains(u.CreateOrgId))//在指定机构列表查询
.WhereIf(dataScope?.Count == 0, u => u.CreateUserId == UserManager.UserId)
.Select(a => a.Id).ToList();
@@ -80,25 +80,19 @@ internal sealed class RulesService : BaseService<Rules>, IRulesService
/// 从缓存/数据库获取全部信息
/// </summary>
/// <returns>列表</returns>
public List<Rules> GetAll()
public async Task<List<Rules>> GetAllAsync()
{
var key = Cache_Rules;
var channels = App.CacheService.Get<List<Rules>>(key);
if (channels == null)
{
using var db = GetDB();
channels = db.Queryable<Rules>().ToList();
channels = await db.Queryable<Rules>().ToListAsync().ConfigureAwait(false);
App.CacheService.Set(key, channels);
}
return channels;
}
public Rules? GetRulesById(long id)
{
var data = GetAll();
return data?.FirstOrDefault(x => x.Id == id);
}
/// <summary>
/// 报表查询
/// </summary>
@@ -142,10 +136,5 @@ internal sealed class RulesService : BaseService<Rules>, IRulesService
}
}

View File

@@ -44,7 +44,7 @@
<!--打包复制-->
<Import Project="..\ThingsGateway.Server\targets\PluginPublish.targets" />
<PropertyGroup>
<TargetFrameworks>net9.0;net8.0;</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
<CustomTargetFramework>$(TargetFramework)</CustomTargetFramework>
<OutputType>WinExe</OutputType>
<OpenApiGenerateDocuments>false</OpenApiGenerateDocuments>

View File

@@ -42,7 +42,7 @@
<!--打包复制-->
<Import Project="targets\PluginPublish.targets" />
<PropertyGroup>
<TargetFrameworks>net9.0;net8.0;</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
<CustomTargetFramework>$(TargetFramework)</CustomTargetFramework>
<OpenApiGenerateDocuments>false</OpenApiGenerateDocuments>
<SatelliteResourceLanguages>zh-Hans;en-US</SatelliteResourceLanguages>

View File

@@ -16,7 +16,7 @@
<ItemGroup>
<PackageReference Include="TouchSocket.Dmtp" Version="3.0.23" />
<PackageReference Include="TouchSocket.Dmtp" Version="3.0.24" />
</ItemGroup>

View File

@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>10.4.7</Version>
<Version>10.4.8</Version>
</PropertyGroup>
<ItemGroup>