feat: 增加一个变量读写表达式常用转换的友好编辑界面

This commit is contained in:
Diego
2025-06-24 16:14:35 +08:00
parent 920e407d05
commit 6510c3e289
11 changed files with 406 additions and 7 deletions

View File

@@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BootstrapBlazor.FontAwesome" Version="9.0.2" />
<PackageReference Include="BootstrapBlazor" Version="9.7.4-beta09" />
<PackageReference Include="BootstrapBlazor" Version="9.7.4" />
<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
</ItemGroup>

View File

@@ -1,8 +1,8 @@
<Project>
<PropertyGroup>
<PluginVersion>10.8.22</PluginVersion>
<ProPluginVersion>10.8.22</ProPluginVersion>
<PluginVersion>10.8.23</PluginVersion>
<ProPluginVersion>10.8.23</ProPluginVersion>
<AuthenticationVersion>2.8.4</AuthenticationVersion>
<SourceGeneratorVersion>10.8.6</SourceGeneratorVersion>
<NET8Version>8.0.17</NET8Version>

View File

@@ -1,4 +1,22 @@
{
"ThingsGateway.Gateway.Razor.ValueTransformType": {
"None": "None",
"Linear": "Linear",
"Sqrt": "Sqrt"
},
"ThingsGateway.Gateway.Razor.ValueTransformConfig": {
"TransformType": "TransformType",
"MinMax": "MinMax",
"ClampToRawRange": "ClampToRawRange",
"DecimalPlaces": "DecimalPlaces",
"RawMin": "RawMin",
"RawMax": "RawMax",
"ActualMin": "ActualMin",
"ActualMax": "ActualMax"
},
"ThingsGateway.Management.Authentication": {
"AuthName": "AuthName",
"Authorized": "Authorized",

View File

@@ -1,4 +1,20 @@
{
"ThingsGateway.Gateway.Razor.ValueTransformType": {
"None": "无",
"Linear": "线性",
"Sqrt": "开方"
},
"ThingsGateway.Gateway.Razor.ValueTransformConfig": {
"TransformType": "转换方式",
"MinMax": "最小最大值",
"ClampToRawRange": "限制范围",
"DecimalPlaces": "保留小数位",
"RawMin": "原始最小值",
"RawMax": "原始最大值",
"ActualMin": "实际最小值",
"ActualMax": "实际最大值"
},
"ThingsGateway.Management.Authentication": {
"AuthName": "公司名称",
"Authorized": "已授权",

View File

@@ -0,0 +1,62 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
namespace ThingsGateway.Gateway.Razor;
public class ValueTransformConfig
{
/// <summary>
/// 保留小数位
/// </summary>
public int DecimalPlaces { get; set; } = 2;
/// <summary>
/// 限制范围
/// </summary>
public bool ClampToRawRange { get; set; }
public ValueTransformType TransformType { get; set; }
/// <summary>
/// 原始低
/// </summary>
public decimal RawMin { get; set; }
/// <summary>
/// 原始高
/// </summary>
public decimal RawMax { get; set; }
/// <summary>
/// 实际低
/// </summary>
public decimal ActualMin { get; set; }
/// <summary>
/// 实际高
/// </summary>
public decimal ActualMax { get; set; }
}
public enum ValueTransformType
{
/// <summary>
/// 不转换,仅保留小数位
/// </summary>
None,
/// <summary>
/// 线性转换
/// </summary>
Linear,
/// <summary>
/// 开方转换
/// </summary>
Sqrt
}

View File

@@ -0,0 +1,31 @@
@namespace ThingsGateway.Gateway.Razor
@using ThingsGateway.Admin.Application
@using ThingsGateway.Admin.Razor
@using ThingsGateway.Foundation
@using ThingsGateway.Gateway.Application
@inherits ComponentDefault
<ValidateForm class="p-4 h-100" Model="@ValueTransformConfig" OnValidSubmit="OnSave">
<EditorForm AutoGenerateAllItem="false" RowType=RowType.Inline ItemsPerRow=1 LabelWidth=150 Model="ValueTransformConfig">
<FieldItems>
<EditorItem @bind-Field="@context.TransformType" GroupName=@(ValueTransformConfigLocalizer["TransformType"]) Cols="4" />
<EditorItem @bind-Field="@context.ClampToRawRange" GroupName=@(ValueTransformConfigLocalizer["TransformType"]) Cols="4" />
<EditorItem @bind-Field="@context.DecimalPlaces" GroupName=@(ValueTransformConfigLocalizer["TransformType"]) Cols="4" />
<EditorItem @bind-Field="@context.RawMin" GroupName=@(ValueTransformConfigLocalizer["MinMax"]) GroupOrder=2 Cols="6" />
<EditorItem @bind-Field="@context.RawMax" GroupName=@(ValueTransformConfigLocalizer["MinMax"]) GroupOrder=2 Cols="6" />
<EditorItem @bind-Field="@context.ActualMin" GroupName=@(ValueTransformConfigLocalizer["MinMax"]) GroupOrder=2 Cols="6" />
<EditorItem @bind-Field="@context.ActualMax" GroupName=@(ValueTransformConfigLocalizer["MinMax"]) GroupOrder=2 Cols="6" />
</FieldItems>
<Buttons>
<Button ButtonType="ButtonType.Submit" Icon="fa-solid fa-floppy-disk" IsAsync Text=@RazorLocalizer["Save"] />
</Buttons>
</EditorForm>
</ValidateForm>

View File

@@ -0,0 +1,215 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
using Microsoft.AspNetCore.Components.Forms;
using System.Globalization;
using System.Text.RegularExpressions;
using ThingsGateway.NewLife.Extension;
namespace ThingsGateway.Gateway.Razor;
public partial class ValueTransformConfigPage
{
private ValueTransformConfig ValueTransformConfig = new();
[Inject]
[NotNull]
private IStringLocalizer<ValueTransformConfig>? ValueTransformConfigLocalizer { get; set; }
[Parameter]
public string Expressions { get; set; }
[Parameter]
public EventCallback<string> ExpressionsChanged { get; set; }
public static bool TryParseLinearFormula(string formula, out ValueTransformConfig config)
{
config = new ValueTransformConfig();
var dec = @"[\d]+(?:\.[\d]+)?";
try
{
// None + clamp actual
var m = Regex.Match(formula, $@"^Math\.Round\(\s*
Math\.Min\(\s*
Math\.Max\(\s*
raw\.ToDecimal\(\)\s*,\s*({dec})\s*\)\s*,\s*({dec})\s*\)\s*,\s*(\d+)\s*\)$", RegexOptions.IgnorePatternWhitespace);
if (m.Success)
{
config.TransformType = ValueTransformType.None;
config.ClampToRawRange = true;
config.ActualMin = decimal.Parse(m.Groups[1].Value);
config.ActualMax = decimal.Parse(m.Groups[2].Value);
config.DecimalPlaces = int.Parse(m.Groups[3].Value);
return true;
}
// None pure
m = Regex.Match(formula, $@"^Math\.Round\(\s*raw\.ToDecimal\(\)\s*,\s*(\d+)\s*\)$");
if (m.Success)
{
config.TransformType = ValueTransformType.None;
config.ClampToRawRange = false;
config.DecimalPlaces = int.Parse(m.Groups[1].Value);
return true;
}
// Linear + clamp actual
m = Regex.Match(formula, $@"^Math\.Round\(\s*
Math\.Min\(\s*
Math\.Max\(\s*
\(\(raw\.ToDecimal\(\)\s*-\s*({dec})\)\s*/\s*
\(({dec})\s*-\s*({dec})\)\s*\*\s*
\(({dec})\s*-\s*({dec})\)\s*\+\s*
({dec})\)\s*,\s*({dec})\)\s*,\s*({dec})\)\s*,\s*(\d+)\s*\)$", RegexOptions.IgnorePatternWhitespace);
if (m.Success)
{
config.TransformType = ValueTransformType.Linear;
config.ClampToRawRange = true;
config.RawMin = decimal.Parse(m.Groups[1].Value);
config.RawMax = decimal.Parse(m.Groups[2].Value);
config.ActualMax = decimal.Parse(m.Groups[4].Value);
config.ActualMin = decimal.Parse(m.Groups[5].Value);
config.DecimalPlaces = int.Parse(m.Groups[9].Value);
return true;
}
// Linear pure
m = Regex.Match(formula, $@"^Math\.Round\(\s*
\(\(raw\.ToDecimal\(\)\s*-\s*({dec})\)\s*/\s*
\(({dec})\s*-\s*({dec})\)\s*\*\s*
\(({dec})\s*-\s*({dec})\)\s*\+\s*({dec})\)\s*,\s*(\d+)\s*\)$", RegexOptions.IgnorePatternWhitespace);
if (m.Success)
{
config.TransformType = ValueTransformType.Linear;
config.ClampToRawRange = false;
config.RawMin = decimal.Parse(m.Groups[1].Value); // raw减数0
config.RawMax = decimal.Parse(m.Groups[2].Value); // 分母第一个数10
config.ActualMax = decimal.Parse(m.Groups[4].Value); // 乘数第一个数1
config.ActualMin = decimal.Parse(m.Groups[6].Value); // 加数0
config.DecimalPlaces = int.Parse(m.Groups[7].Value); // 小数位2
return true;
}
// Sqrt + clamp actual
m = Regex.Match(formula, $@"^Math\.Round\(\s*
Math\.Min\(\s*
Math\.Max\(\s*
Math\.Sqrt\(Math\.Max\(raw\.ToDecimal\(\),\s*0\)\)\s*\*\s*({dec})\s*,\s*({dec})\)\s*,\s*({dec})\)\s*,\s*(\d+)\s*\)$", RegexOptions.IgnorePatternWhitespace);
if (m.Success)
{
config.TransformType = ValueTransformType.Sqrt;
config.ClampToRawRange = true;
config.ActualMax = decimal.Parse(m.Groups[1].Value);
config.ActualMin = decimal.Parse(m.Groups[2].Value);
config.DecimalPlaces = int.Parse(m.Groups[4].Value);
return true;
}
// Sqrt pure
m = Regex.Match(formula, $@"^Math\.Round\(\s*
Math\.Sqrt\(Math\.Max\(raw\.ToDecimal\(\),\s*0\)\)\s*\*\s*({dec})\s*,\s*(\d+)\s*\)$", RegexOptions.IgnorePatternWhitespace);
if (m.Success)
{
config.TransformType = ValueTransformType.Sqrt;
config.ClampToRawRange = false;
config.DecimalPlaces = int.Parse(m.Groups[2].Value);
return true;
}
}
catch
{
// ignore
}
return false;
}
public static string GenerateFormula(ValueTransformConfig config)
{
// 只有 ClampToRawRange 为 true 时才包裹实际值范围限制
string clampActual(string expr)
{
if (config.ClampToRawRange)
return $"Math.Min(Math.Max({expr}, {config.ActualMin}), {config.ActualMax})";
else
return expr;
}
string rawExpr = "raw.ToDecimal()"; // 这里不做 raw clamp
switch (config.TransformType)
{
case ValueTransformType.None:
return $"Math.Round({clampActual(rawExpr)}, {config.DecimalPlaces})";
case ValueTransformType.Linear:
var linearExpr = $"(({rawExpr} - {config.RawMin}) / ({config.RawMax} - {config.RawMin}) * ({config.ActualMax} - {config.ActualMin}) + {config.ActualMin})";
return $"Math.Round({clampActual(linearExpr)}, {config.DecimalPlaces})";
case ValueTransformType.Sqrt:
var sqrtExpr = $"Math.Sqrt(Math.Max({rawExpr}, 0)) * {config.ActualMax}";
return $"Math.Round({clampActual(sqrtExpr)}, {config.DecimalPlaces})";
default:
throw new NotSupportedException($"Unsupported transform type: {config.TransformType}");
}
}
protected override void OnParametersSet()
{
if (!Expressions.IsNullOrWhiteSpace())
{
if (TryParseLinearFormula(Expressions, out var config))
{
ValueTransformConfig = config;
}
}
base.OnParametersSet();
}
#region
[CascadingParameter]
private Func<Task>? OnCloseAsync { get; set; }
private async Task OnSave(EditContext editContext)
{
try
{
var result = GenerateFormula(ValueTransformConfig);
Expressions = result;
if (ExpressionsChanged.HasDelegate)
{
await ExpressionsChanged.InvokeAsync(result);
}
else
{
Expressions = result;
}
if (OnCloseAsync != null)
await OnCloseAsync();
}
catch (Exception ex)
{
await ToastService.Warn(ex);
}
}
#endregion
}

View File

@@ -0,0 +1,7 @@
.appconfig ::deep .tabs-body-content {
height: 100% !important;
}
.appconfig {
height: 100% !important;
}

View File

@@ -113,8 +113,28 @@
</EditorItem>
<EditorItem @bind-Field="@context.ArrayLength" />
<EditorItem @bind-Field="@context.ReadExpressions" Rows="1" />
<EditorItem @bind-Field="@context.WriteExpressions" Rows="1" />
<EditorItem @bind-Field="@context.ReadExpressions">
<EditTemplate Context="value">
<div class="col-12">
<BootstrapInputGroup>
<Textarea rows="1" @bind-Value="value.ReadExpressions" ShowLabel="true"></Textarea>
<Button Icon="fa-solid fa-bars" OnClick="() => ShowExpressionsUI(true)"></Button>
</BootstrapInputGroup>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.WriteExpressions">
<EditTemplate Context="value">
<div class="col-12">
<BootstrapInputGroup>
<Textarea rows="1" @bind-Value="value.WriteExpressions" ShowLabel="true"></Textarea>
<Button Icon="fa-solid fa-bars" OnClick="() => ShowExpressionsUI(false)"></Button>
</BootstrapInputGroup>
</div>
</EditTemplate>
</EditorItem>
<EditorItem TValue="string" TModel="Variable" @bind-Field="@context.Description">

View File

@@ -163,7 +163,37 @@ public partial class VariableEditComponent
op.Component = AddressDynamicComponent;
await DialogService.Show(op);
}
[Inject]
IStringLocalizer<Variable> VariableLocalizer { get; set; }
private async Task ShowExpressionsUI(bool read)
{
var op = new DialogOption()
{
IsScrolling = false,
ShowMaximizeButton = true,
Size = Size.Large,
Title = $"{Model.Name} {(read ? VariableLocalizer[nameof(Variable.ReadExpressions)] : VariableLocalizer[nameof(Variable.WriteExpressions)])}",
ShowFooter = false,
ShowCloseButton = false,
BodyTemplate = BootstrapDynamicComponent.CreateComponent<ValueTransformConfigPage>(new Dictionary<string, object?>
{
{nameof(ValueTransformConfigPage.ExpressionsChanged), EventCallback.Factory.Create<string>(this,a =>
{
if(read)
{
Model.ReadExpressions = a;
}
else
{
Model.WriteExpressions=a;
}
})
},
{nameof(ValueTransformConfigPage.Expressions),read?Model.ReadExpressions:Model.WriteExpressions },
}).Render(),
};
await DialogService.Show(op);
}
[Inject]
private IStringLocalizer<Device> DeviceLocalizer { get; set; }

View File

@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>10.8.22</Version>
<Version>10.8.23</Version>
</PropertyGroup>
<ItemGroup>