mirror of
				https://gitee.com/ThingsGateway/ThingsGateway.git
				synced 2025-11-04 17:43:58 +08:00 
			
		
		
		
	Compare commits
	
		
			11 Commits
		
	
	
		
			10.10.10.0
			...
			10.10.18.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					02ad494a26 | ||
| 
						 | 
					280366e1b2 | ||
| 
						 | 
					6660ce3e34 | ||
| 
						 | 
					7499162c1a | ||
| 
						 | 
					40208a5cd6 | ||
| 
						 | 
					fa347f4f68 | ||
| 
						 | 
					d7df6fc605 | ||
| 
						 | 
					eb4bb2fd48 | ||
| 
						 | 
					faa9858974 | ||
| 
						 | 
					1b3d2dda49 | ||
| 
						 | 
					a8a9453611 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -364,6 +364,7 @@ FodyWeavers.xsd
 | 
			
		||||
 | 
			
		||||
/src/*Pro*/
 | 
			
		||||
/src/*Pro*
 | 
			
		||||
/src/**/*Pro*
 | 
			
		||||
/src/*pro*
 | 
			
		||||
/src/*pro*/
 | 
			
		||||
/src/ThingsGateway.Server/Configuration/GiteeOAuthSettings.json
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@
 | 
			
		||||
		<IncludeBuildOutput>false</IncludeBuildOutput>
 | 
			
		||||
		<!-- 避免 DLL 被打包到 lib/ -->
 | 
			
		||||
		<EnableSourceGenerator>true</EnableSourceGenerator>
 | 
			
		||||
		
 | 
			
		||||
		<!-- 可选 -->
 | 
			
		||||
 | 
			
		||||
		
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@ public class FileController : ControllerBase
 | 
			
		||||
        var filePath = Path.Combine(wwwroot, fileName);
 | 
			
		||||
        // 防止路径穿越攻击
 | 
			
		||||
#pragma warning disable CA3003
 | 
			
		||||
        if (!filePath.StartsWith(wwwroot, StringComparison.OrdinalIgnoreCase) || !System.IO.File.Exists(filePath))
 | 
			
		||||
        if (filePath.Contains("..") || !System.IO.File.Exists(filePath))
 | 
			
		||||
        {
 | 
			
		||||
            return NotFound();
 | 
			
		||||
        }
 | 
			
		||||
@@ -49,6 +49,6 @@ public class FileController : ControllerBase
 | 
			
		||||
 | 
			
		||||
        Response.Headers.Append("Access-Control-Expose-Headers", "Content-Disposition");
 | 
			
		||||
 | 
			
		||||
        return File(fileStream, "application/octet-stream", (fileName.Replace('/', '_')));
 | 
			
		||||
        return File(fileStream, "application/octet-stream", (Path.GetFileName(filePath).Replace('/', '_')));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,16 +21,7 @@
 | 
			
		||||
    "UserNoModule": "This account has not been assigned a module. Please contact the administrator",
 | 
			
		||||
    "UserNull": "User {0} does not exist"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.BaseDataEntity": {
 | 
			
		||||
    "CreateOrgId": "CreateOrgId"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.BaseEntity": {
 | 
			
		||||
    "CreateTime": "CreateTime",
 | 
			
		||||
    "CreateUser": "CreateUser",
 | 
			
		||||
    "SortCode": "SortCode",
 | 
			
		||||
    "UpdateTime": "UpdateTime",
 | 
			
		||||
    "UpdateUser": "UpdateUser"
 | 
			
		||||
  },
 | 
			
		||||
  
 | 
			
		||||
  "ThingsGateway.Admin.Application.BlazorAuthenticationHandler": {
 | 
			
		||||
    "UserExpire": "User expired, please login again"
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -21,16 +21,7 @@
 | 
			
		||||
    "UserNoModule": "该账号未分配模块,请联系管理员",
 | 
			
		||||
    "UserNull": "用户 {0} 不存在"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.BaseDataEntity": {
 | 
			
		||||
    "CreateOrgId": "创建机构Id"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.BaseEntity": {
 | 
			
		||||
    "CreateTime": "创建时间",
 | 
			
		||||
    "CreateUser": "创建人",
 | 
			
		||||
    "SortCode": "排序",
 | 
			
		||||
    "UpdateTime": "更新时间",
 | 
			
		||||
    "UpdateUser": "更新人"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Admin.Application.BlazorAuthenticationHandler": {
 | 
			
		||||
    "UserExpire": "用户登录已过期,请重新登录"
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<GenerateDocumentationFile>True</GenerateDocumentationFile>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ public class BlazorAppContext
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 全部菜单
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IEnumerable<SysResource> AllMenus { get; private set; }
 | 
			
		||||
    public List<SysResource> AllMenus { get; private set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当前用户
 | 
			
		||||
@@ -42,22 +42,22 @@ public class BlazorAppContext
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户个人菜单
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IEnumerable<MenuItem> OwnMenuItems { get; private set; }
 | 
			
		||||
    public List<MenuItem> OwnMenuItems { get; private set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 不同模块的菜单
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IEnumerable<MenuItem> AllOwnMenuItems { get; private set; }
 | 
			
		||||
    public List<MenuItem> AllOwnMenuItems { get; private set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户个人菜单,多个模块
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IEnumerable<SysResource> OwnMenus { get; private set; }
 | 
			
		||||
    public List<SysResource> OwnMenus { get; private set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户个人菜单,非树形
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IEnumerable<MenuItem> OwnSameLevelMenuItems { get; private set; }
 | 
			
		||||
    public List<MenuItem> OwnSameLevelMenuItems { get; private set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 个人工作台
 | 
			
		||||
@@ -67,9 +67,9 @@ public class BlazorAppContext
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户个人快捷方式菜单
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IEnumerable<SysResource> UserWorkbenchOutputs { get; private set; }
 | 
			
		||||
    public List<SysResource> UserWorkbenchOutputs { get; private set; }
 | 
			
		||||
 | 
			
		||||
    public IEnumerable<SysResource> AllResource { get; private set; }
 | 
			
		||||
    public List<SysResource> AllResource { get; private set; }
 | 
			
		||||
 | 
			
		||||
    private ISysResourceService ResourceService { get; }
 | 
			
		||||
    private ISysUserService SysUserService { get; }
 | 
			
		||||
@@ -93,7 +93,7 @@ public class BlazorAppContext
 | 
			
		||||
            AllResource = sysResources;
 | 
			
		||||
            var ids = CurrentUser.ModuleList.Select(a => a.Id).ToHashSet();
 | 
			
		||||
            CurrentUser.ModuleList = AllResource.Where(a => ids.Contains(a.Id)).OrderBy(a => a.SortCode).ToList();
 | 
			
		||||
            AllMenus = AllResource.Where(a => a.Category == ResourceCategoryEnum.Menu);
 | 
			
		||||
            AllMenus = AllResource.Where(a => a.Category == ResourceCategoryEnum.Menu).ToList();
 | 
			
		||||
 | 
			
		||||
            if (moduleId == null)
 | 
			
		||||
            {
 | 
			
		||||
@@ -123,8 +123,8 @@ public class BlazorAppContext
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            var ownMenus = OwnMenus.Where(a => a.Module == CurrentModuleId);
 | 
			
		||||
            OwnMenuItems = ResourceUtil.BuildMenuTrees(ownMenus).ToList();
 | 
			
		||||
            AllOwnMenuItems = ResourceUtil.BuildMenuTrees(OwnMenus).ToList();
 | 
			
		||||
            OwnMenuItems = AdminResourceUtil.BuildMenuTrees(ownMenus).ToList();
 | 
			
		||||
            AllOwnMenuItems = AdminResourceUtil.BuildMenuTrees(OwnMenus).ToList();
 | 
			
		||||
            OwnSameLevelMenuItems = ownMenus.Where(a => !a.Href.IsNullOrWhiteSpace()).Select(item => new MenuItem()
 | 
			
		||||
            {
 | 
			
		||||
                Match = item.NavLinkMatch ?? Microsoft.AspNetCore.Components.Routing.NavLinkMatch.All,
 | 
			
		||||
@@ -132,8 +132,8 @@ public class BlazorAppContext
 | 
			
		||||
                Icon = item.Icon,
 | 
			
		||||
                Url = item.Href,
 | 
			
		||||
                Target = item.Target.ToString(),
 | 
			
		||||
            });
 | 
			
		||||
            UserWorkbenchOutputs = AllMenus.Where(it => UserWorkBench.Shortcuts.Contains(it.Id));
 | 
			
		||||
            }).ToList();
 | 
			
		||||
            UserWorkbenchOutputs = AllMenus.Where(it => UserWorkBench.Shortcuts.Contains(it.Id)).ToList();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ public partial class EditPagePolicy
 | 
			
		||||
 | 
			
		||||
    protected override Task OnParametersSetAsync()
 | 
			
		||||
    {
 | 
			
		||||
        ShortcutsTreeViewItems = ResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
 | 
			
		||||
        ShortcutsTreeViewItems = AdminResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
 | 
			
		||||
        return base.OnParametersSetAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -48,6 +48,6 @@ public partial class EditPagePolicy
 | 
			
		||||
    {
 | 
			
		||||
        await Task.CompletedTask;
 | 
			
		||||
        ShortcutsSearchText = searchText;
 | 
			
		||||
        return ResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
 | 
			
		||||
        return AdminResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,7 @@ public partial class MenuChoiceDialog
 | 
			
		||||
        var all = (await SysResourceService.GetAllAsync());
 | 
			
		||||
        var items = all.Where(a => a.Category == ResourceCategoryEnum.Menu && a.Module == ModuleId);
 | 
			
		||||
        ModuleTitle = all.FirstOrDefault(a => a.Id == ModuleId)?.Title;
 | 
			
		||||
        Items = ResourceUtil.BuildTreeItemList(items, new List<long> { Value }, RenderTreeItem);
 | 
			
		||||
        Items = AdminResourceUtil.BuildTreeItemList(items, new List<long> { Value }, RenderTreeItem);
 | 
			
		||||
        await base.OnParametersSetAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -39,8 +39,8 @@ public partial class SysResourcePage
 | 
			
		||||
 | 
			
		||||
    protected override async Task OnParametersSetAsync()
 | 
			
		||||
    {
 | 
			
		||||
        ModuleSelectedItems = ResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
 | 
			
		||||
        MenuItems = ResourceUtil.BuildMenuSelectList((await SysResourceService.GetAllAsync())).Concat(new List<SelectedItem>() { new("0", AdminLocalizer["Root"]) }).ToList();
 | 
			
		||||
        ModuleSelectedItems = AdminResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
 | 
			
		||||
        MenuItems = AdminResourceUtil.BuildMenuSelectList((await SysResourceService.GetAllAsync())).Concat(new List<SelectedItem>() { new("0", AdminLocalizer["Root"]) }).ToList();
 | 
			
		||||
 | 
			
		||||
        await base.OnParametersSetAsync();
 | 
			
		||||
    }
 | 
			
		||||
@@ -49,7 +49,7 @@ public partial class SysResourcePage
 | 
			
		||||
 | 
			
		||||
    private async Task<QueryData<SysResource>> OnQueryAsync(QueryPageOptions options)
 | 
			
		||||
    {
 | 
			
		||||
        MenuTreeItems = new List<TreeViewItem<SysResource>>() { new TreeViewItem<SysResource>(new SysResource()) { Text = AdminLocalizer["Root"] } }.Concat(ResourceUtil.BuildTreeItemList((await SysResourceService.GetAllAsync()).Where(a => a.Module == CustomerSearchModel.Module), new(), null)).ToList();
 | 
			
		||||
        MenuTreeItems = new List<TreeViewItem<SysResource>>() { new TreeViewItem<SysResource>(new SysResource()) { Text = AdminLocalizer["Root"] } }.Concat(AdminResourceUtil.BuildTreeItemList((await SysResourceService.GetAllAsync()).Where(a => a.Module == CustomerSearchModel.Module), new(), null)).ToList();
 | 
			
		||||
 | 
			
		||||
        var data = await SysResourceService.PageAsync(options, CustomerSearchModel);
 | 
			
		||||
        return data;
 | 
			
		||||
@@ -136,14 +136,14 @@ public partial class SysResourcePage
 | 
			
		||||
    private async Task<IEnumerable<TableTreeNode<SysResource>>> OnTreeExpand(SysResource menu)
 | 
			
		||||
    {
 | 
			
		||||
        var sysResources = await SysResourceService.GetAllAsync();
 | 
			
		||||
        var result = ResourceUtil.BuildTableTrees(sysResources, menu.Id);
 | 
			
		||||
        var result = AdminResourceUtil.BuildTableTrees(sysResources, menu.Id);
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static async Task<IEnumerable<TableTreeNode<SysResource>>> TreeNodeConverter(IEnumerable<SysResource> items)
 | 
			
		||||
    {
 | 
			
		||||
        await Task.CompletedTask;
 | 
			
		||||
        var result = ResourceUtil.BuildTableTrees(items, 0);
 | 
			
		||||
        var result = AdminResourceUtil.BuildTableTrees(items, 0);
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ public partial class GrantResourceDialog
 | 
			
		||||
    {
 | 
			
		||||
        var items = (await SysResourceService.GetAllAsync()).Where(a => a.Category != ResourceCategoryEnum.Module).OrderBy(a => a.Module).ThenBy(a => a.Id).ToList();
 | 
			
		||||
 | 
			
		||||
        Items = ResourceUtil.BuildTreeItemList(items, Value, RenderTreeItem);
 | 
			
		||||
        Items = AdminResourceUtil.BuildTreeItemList(items, Value, RenderTreeItem);
 | 
			
		||||
        ModuleList = (await SysResourceService.GetAllAsync()).Where(a => a.Category == ResourceCategoryEnum.Module).ToList();
 | 
			
		||||
        await base.OnInitializedAsync();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ public partial class SysUserEdit
 | 
			
		||||
        BoolItems = LocalizerUtil.GetBoolItems(Model.GetType(), nameof(Model.Status));
 | 
			
		||||
        var items = await SysPositionService.SelectorAsync(new PositionSelectorInput());
 | 
			
		||||
        Items = PositionUtil.BuildCascaderItemList(items);
 | 
			
		||||
        ModuleSelectedItems = ResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
 | 
			
		||||
        ModuleSelectedItems = AdminResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
 | 
			
		||||
        await InvokeAsync(StateHasChanged);
 | 
			
		||||
        await base.OnInitializedAsync();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
 | 
			
		||||
		
 | 
			
		||||
		<!--<UseRazorSourceGenerator>false</UseRazorSourceGenerator>-->
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ namespace ThingsGateway.Admin.Razor;
 | 
			
		||||
 | 
			
		||||
/// <inheritdoc/>
 | 
			
		||||
[ThingsGateway.DependencyInjection.SuppressSniffer]
 | 
			
		||||
public static class ResourceUtil
 | 
			
		||||
public static class AdminResourceUtil
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 构造选择项,ID/TITLE
 | 
			
		||||
@@ -5,7 +5,8 @@
 | 
			
		||||
#推送:docker push registry.cn-shenzhen.aliyuncs.com/thingsgateway/thingsgateway
 | 
			
		||||
 | 
			
		||||
#aspnetcore9.0环境
 | 
			
		||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
 | 
			
		||||
#FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
 | 
			
		||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble AS base
 | 
			
		||||
COPY .  /app
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
#默认web
 | 
			
		||||
@@ -13,6 +14,8 @@ EXPOSE 5000
 | 
			
		||||
 | 
			
		||||
# 添加时区环境变量,亚洲,上海
 | 
			
		||||
ENV TimeZone=Asia/Shanghai
 | 
			
		||||
# 转发头
 | 
			
		||||
ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
 | 
			
		||||
# 使用软连接,并且将时区配置覆盖/etc/timezone
 | 
			
		||||
RUN ln -snf /usr/share/zoneinfo/$TimeZone /etc/localtime && echo $TimeZone > /etc/timezone
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,8 @@
 | 
			
		||||
#推送:docker push registry.cn-shenzhen.aliyuncs.com/thingsgateway/thingsgateway_arm64
 | 
			
		||||
 | 
			
		||||
#aspnetcore9.0环境
 | 
			
		||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine-arm64v8  AS base
 | 
			
		||||
#FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine-arm64v8  AS base
 | 
			
		||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble-arm64v8 AS base
 | 
			
		||||
COPY .  /app
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
#默认web
 | 
			
		||||
@@ -13,6 +14,8 @@ EXPOSE 5000
 | 
			
		||||
 | 
			
		||||
# 添加时区环境变量,亚洲,上海
 | 
			
		||||
ENV TimeZone=Asia/Shanghai
 | 
			
		||||
# 转发头
 | 
			
		||||
ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
 | 
			
		||||
# 使用软连接,并且将时区配置覆盖/etc/timezone
 | 
			
		||||
RUN ln -snf /usr/share/zoneinfo/$TimeZone /etc/localtime && echo $TimeZone > /etc/timezone
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@
 | 
			
		||||
    <BlazorReconnector @rendermode="new InteractiveServerRenderMode(false)" />
 | 
			
		||||
 | 
			
		||||
    <script src=@($"_content/BootstrapBlazor/js/bootstrap.blazor.bundle.min.js?v={this.GetType().Assembly.GetName().Version}")></script>
 | 
			
		||||
    <script src=@($"{WebsiteConst.DefaultResourceUrl}js/culture.js?v={this.GetType().Assembly.GetName().Version}")></script>
 | 
			
		||||
    <script src=@($"{WebsiteConst.DefaultResourceUrl}js/localStorageUtil.js?v={this.GetType().Assembly.GetName().Version}")></script>
 | 
			
		||||
    <script src="_framework/blazor.web.js"></script>
 | 
			
		||||
    <!-- PWA Service Worker -->
 | 
			
		||||
    <script type="text/javascript">'serviceWorker' in navigator && navigator.serviceWorker.register('./service-worker.js')</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@
 | 
			
		||||
    </app>
 | 
			
		||||
    
 | 
			
		||||
    <script src="_content/BootstrapBlazor/js/bootstrap.blazor.bundle.min.js"></script>
 | 
			
		||||
    <script src=@($"{WebsiteConst.DefaultResourceUrl}js/culture.js")></script>
 | 
			
		||||
    <script src=@($"{WebsiteConst.DefaultResourceUrl}js/localStorageUtil.js")></script>
 | 
			
		||||
    <script src="_framework/blazor.server.js"></script>
 | 
			
		||||
 | 
			
		||||
       <!-- PWA Service Worker -->
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
	<!--<Import Project="Admin.targets" Condition=" '$(Configuration)' != 'Debug' " />-->
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<GenerateDocumentationFile>True</GenerateDocumentationFile>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
 | 
			
		||||
 
 | 
			
		||||
@@ -53,6 +53,8 @@ public static class QueryPageOptionsExtensions
 | 
			
		||||
        return datas;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public static IEnumerable<T> GetQuery<T>(this IEnumerable<T> query, QueryPageOptions option, Func<IEnumerable<T>, IEnumerable<T>>? queryFunc = null, FilterKeyValueAction where = null)
 | 
			
		||||
    {
 | 
			
		||||
        if (queryFunc != null)
 | 
			
		||||
@@ -123,7 +125,36 @@ public static class QueryPageOptionsExtensions
 | 
			
		||||
        };
 | 
			
		||||
        var items = datas.GetData(option, out var totalCount, where);
 | 
			
		||||
        ret.TotalCount = totalCount;
 | 
			
		||||
 | 
			
		||||
        if (totalCount > 0)
 | 
			
		||||
        {
 | 
			
		||||
            if (!items.Any() && option.PageIndex != 1)
 | 
			
		||||
            {
 | 
			
		||||
                option.PageIndex = 1;
 | 
			
		||||
                items = datas.GetData(option, out totalCount, where);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        ret.Items = items.ToList();
 | 
			
		||||
        return ret;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 根据查询条件返回QueryData
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static QueryData<SelectedItem> GetQueryData<T>(this IEnumerable<T> datas, VirtualizeQueryOption option, Func<IEnumerable<T>, IEnumerable<SelectedItem>> func, FilterKeyValueAction where = null)
 | 
			
		||||
    {
 | 
			
		||||
        var ret = new QueryData<SelectedItem>()
 | 
			
		||||
        {
 | 
			
		||||
            IsSorted = false,
 | 
			
		||||
            IsFiltered = false,
 | 
			
		||||
            IsAdvanceSearch = false,
 | 
			
		||||
            IsSearch = !option.SearchText.IsNullOrWhiteSpace()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        var items = datas.Skip((option.StartIndex)).Take(option.Count);
 | 
			
		||||
        ret.TotalCount = datas.Count();
 | 
			
		||||
 | 
			
		||||
        ret.Items = func(items).ToList();
 | 
			
		||||
        return ret;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								src/Admin/ThingsGateway.DB/Locales/en-US.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/Admin/ThingsGateway.DB/Locales/en-US.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
{
 | 
			
		||||
 
 | 
			
		||||
  "ThingsGateway.Admin.Application.BaseDataEntity": {
 | 
			
		||||
    "CreateOrgId": "CreateOrgId"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.BaseEntity": {
 | 
			
		||||
    "CreateTime": "CreateTime",
 | 
			
		||||
    "CreateUser": "CreateUser",
 | 
			
		||||
    "SortCode": "SortCode",
 | 
			
		||||
    "UpdateTime": "UpdateTime",
 | 
			
		||||
    "UpdateUser": "UpdateUser"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								src/Admin/ThingsGateway.DB/Locales/zh-CN.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/Admin/ThingsGateway.DB/Locales/zh-CN.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
{
 | 
			
		||||
  
 | 
			
		||||
  "ThingsGateway.DB.BaseDataEntity": {
 | 
			
		||||
    "CreateOrgId": "创建机构Id"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.DB.BaseEntity": {
 | 
			
		||||
    "CreateTime": "创建时间",
 | 
			
		||||
    "CreateUser": "创建人",
 | 
			
		||||
    "SortCode": "排序",
 | 
			
		||||
    "UpdateTime": "更新时间",
 | 
			
		||||
    "UpdateUser": "更新人"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<GenerateDocumentationFile>True</GenerateDocumentationFile>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
 | 
			
		||||
@@ -18,6 +19,12 @@
 | 
			
		||||
		<None Remove="..\..\..\README.zh-CN.md" Pack="false" PackagePath="\" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
	  <EmbeddedResource Include="Locales\en-US.json" />
 | 
			
		||||
	  <EmbeddedResource Include="Locales\zh-CN.json" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<!--<PackageReference Include="ThingsGateway.Razor" Version="$(SourceGeneratorVersion)" />-->
 | 
			
		||||
		<!--<ProjectReference Include="..\ThingsGateway.Razor\ThingsGateway.Razor.csproj" />-->
 | 
			
		||||
 
 | 
			
		||||
@@ -554,10 +554,9 @@ public static class App
 | 
			
		||||
        {
 | 
			
		||||
            types = ass.GetTypes();
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            XTrace.Log.Warn($"Error load `{ass.FullName}` assembly.");
 | 
			
		||||
            Console.WriteLine($"Error load `{ass.FullName}` assembly.");
 | 
			
		||||
            XTrace.Log.Warn($"Error load `{ass.FullName}` assembly. : {ex.Message}");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return types.Where(u => u.IsPublic && !u.IsDefined(typeof(SuppressSnifferAttribute), false));
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@
 | 
			
		||||
		<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
 | 
			
		||||
		<SignAssembly>True</SignAssembly>
 | 
			
		||||
		<AssemblyOriginatorKeyFile>newlife.snk</AssemblyOriginatorKeyFile>
 | 
			
		||||
		
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -306,8 +306,19 @@ public class TimerScheduler : ILogFeature
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var func = timer.Method.As<Func<Object?, Task>>(target);
 | 
			
		||||
            await func!(timer.State).ConfigureAwait(false);
 | 
			
		||||
#if NET6_0_OR_GREATER
 | 
			
		||||
            if (timer.IsValueTask)
 | 
			
		||||
            {
 | 
			
		||||
                var func = timer.Method.As<Func<Object?, ValueTask>>(target);
 | 
			
		||||
                await func!(timer.State).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
#endif
 | 
			
		||||
            {
 | 
			
		||||
                var func = timer.Method.As<Func<Object?, Task>>(target);
 | 
			
		||||
                await func!(timer.State).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        catch (ThreadAbortException) { throw; }
 | 
			
		||||
        catch (ThreadInterruptedException) { throw; }
 | 
			
		||||
 
 | 
			
		||||
@@ -87,6 +87,8 @@ public class TimerX : ITimer, ITimerx, IDisposable
 | 
			
		||||
 | 
			
		||||
    private DateTime _AbsolutelyNext;
 | 
			
		||||
    private readonly Cron[]? _crons;
 | 
			
		||||
 | 
			
		||||
    internal bool IsValueTask { get; }
 | 
			
		||||
    #endregion
 | 
			
		||||
 | 
			
		||||
    //    #region 静态
 | 
			
		||||
@@ -158,6 +160,29 @@ public class TimerX : ITimer, ITimerx, IDisposable
 | 
			
		||||
        Init(dueTime);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#if NET6_0_OR_GREATER
 | 
			
		||||
 | 
			
		||||
    /// <summary>实例化一个不可重入的定时器</summary>
 | 
			
		||||
    /// <param name="callback">委托</param>
 | 
			
		||||
    /// <param name="state">用户数据</param>
 | 
			
		||||
    /// <param name="dueTime">多久之后开始。毫秒</param>
 | 
			
		||||
    /// <param name="period">间隔周期。毫秒</param>
 | 
			
		||||
    /// <param name="scheduler">调度器</param>
 | 
			
		||||
    public TimerX(Func<Object, ValueTask> callback, Object? state, Int32 dueTime, Int32 period, String? scheduler = null) : this(callback.Target, callback.Method, state, scheduler)
 | 
			
		||||
    {
 | 
			
		||||
        IsValueTask = true;
 | 
			
		||||
        if (callback == null) throw new ArgumentNullException(nameof(callback));
 | 
			
		||||
        if (dueTime < 0) throw new ArgumentOutOfRangeException(nameof(dueTime));
 | 
			
		||||
 | 
			
		||||
        IsAsyncTask = true;
 | 
			
		||||
        Async = true;
 | 
			
		||||
        Period = period;
 | 
			
		||||
 | 
			
		||||
        Init(dueTime);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    /// <summary>实例化一个绝对定时器,指定时刻执行,跟当前时间和SetNext无关</summary>
 | 
			
		||||
    /// <param name="callback">委托</param>
 | 
			
		||||
    /// <param name="state">用户数据</param>
 | 
			
		||||
@@ -210,6 +235,37 @@ public class TimerX : ITimer, ITimerx, IDisposable
 | 
			
		||||
        Init(ms);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#if NET6_0_OR_GREATER
 | 
			
		||||
 | 
			
		||||
    /// <summary>实例化一个绝对定时器,指定时刻执行,跟当前时间和SetNext无关</summary>
 | 
			
		||||
    /// <param name="callback">委托</param>
 | 
			
		||||
    /// <param name="state">用户数据</param>
 | 
			
		||||
    /// <param name="startTime">绝对开始时间</param>
 | 
			
		||||
    /// <param name="period">间隔周期。毫秒</param>
 | 
			
		||||
    /// <param name="scheduler">调度器</param>
 | 
			
		||||
    public TimerX(Func<Object, ValueTask> callback, Object? state, DateTime startTime, Int32 period, String? scheduler = null) : this(callback.Target, callback.Method, state, scheduler)
 | 
			
		||||
    {
 | 
			
		||||
        IsValueTask = true;
 | 
			
		||||
        if (callback == null) throw new ArgumentNullException(nameof(callback));
 | 
			
		||||
        if (startTime <= DateTime.MinValue) throw new ArgumentOutOfRangeException(nameof(startTime));
 | 
			
		||||
        if (period <= 0) throw new ArgumentOutOfRangeException(nameof(period));
 | 
			
		||||
 | 
			
		||||
        IsAsyncTask = true;
 | 
			
		||||
        Async = true;
 | 
			
		||||
        Period = period;
 | 
			
		||||
        Absolutely = true;
 | 
			
		||||
 | 
			
		||||
        //var now = DateTime.Now;
 | 
			
		||||
        var now = Scheduler.GetNow();
 | 
			
		||||
        var next = startTime;
 | 
			
		||||
        while (next < now) next = next.AddMilliseconds(period);
 | 
			
		||||
 | 
			
		||||
        var ms = (Int64)(next - now).TotalMilliseconds;
 | 
			
		||||
        _AbsolutelyNext = next;
 | 
			
		||||
        Init(ms);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
    /// <summary>实例化一个Cron定时器</summary>
 | 
			
		||||
    /// <param name="callback">委托</param>
 | 
			
		||||
    /// <param name="state">用户数据</param>
 | 
			
		||||
@@ -274,6 +330,42 @@ public class TimerX : ITimer, ITimerx, IDisposable
 | 
			
		||||
        //Init(_AbsolutelyNext = _cron.GetNext(DateTime.Now));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#if NET6_0_OR_GREATER
 | 
			
		||||
    /// <summary>实例化一个Cron定时器</summary>
 | 
			
		||||
    /// <param name="callback">委托</param>
 | 
			
		||||
    /// <param name="state">用户数据</param>
 | 
			
		||||
    /// <param name="cronExpression">Cron表达式。支持多个表达式,分号分隔</param>
 | 
			
		||||
    /// <param name="scheduler">调度器</param>
 | 
			
		||||
    public TimerX(Func<Object, ValueTask> callback, Object? state, String cronExpression, String? scheduler = null) : this(callback.Target, callback.Method, state, scheduler)
 | 
			
		||||
    {
 | 
			
		||||
        IsValueTask = true;
 | 
			
		||||
        if (callback == null) throw new ArgumentNullException(nameof(callback));
 | 
			
		||||
        if (cronExpression.IsNullOrEmpty()) throw new ArgumentNullException(nameof(cronExpression));
 | 
			
		||||
 | 
			
		||||
        var list = new List<Cron>();
 | 
			
		||||
        foreach (var item in cronExpression.Split(";"))
 | 
			
		||||
        {
 | 
			
		||||
            var cron = new Cron();
 | 
			
		||||
            if (!cron.Parse(item)) throw new ArgumentException($"Invalid Cron expression[{item}]", nameof(cronExpression));
 | 
			
		||||
 | 
			
		||||
            list.Add(cron);
 | 
			
		||||
        }
 | 
			
		||||
        _crons = list.ToArray();
 | 
			
		||||
 | 
			
		||||
        IsAsyncTask = true;
 | 
			
		||||
        Async = true;
 | 
			
		||||
        Absolutely = true;
 | 
			
		||||
 | 
			
		||||
        //var now = DateTime.Now;
 | 
			
		||||
        var now = Scheduler.GetNow();
 | 
			
		||||
        var next = _crons.Min(e => e.GetNext(now));
 | 
			
		||||
        var ms = (Int64)(next - now).TotalMilliseconds;
 | 
			
		||||
        _AbsolutelyNext = next;
 | 
			
		||||
        Init(ms);
 | 
			
		||||
        //Init(_AbsolutelyNext = _cron.GetNext(DateTime.Now));
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    public bool Disposed { get; private set; }
 | 
			
		||||
    /// <summary>销毁定时器</summary>
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,8 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Common.Extension;
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
using ThingsGateway.Razor.Extension;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Razor;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
<Step @ref="@step" IsVertical="true">
 | 
			
		||||
    <StepItem Text=@Localizer["First"] Title=@Localizer["Upload"]>
 | 
			
		||||
        <InputUpload ShowDeleteButton="true" @bind-Value=_importFile Accept=".xlsx"></InputUpload>
 | 
			
		||||
        <Button class="mt-2" IsAsync OnClick="() => DeviceImport(_importFile)">@Localizer["Validate"]</Button>
 | 
			
		||||
        <PopConfirmButton IsAsync Color=Color.Warning class="mt-2" OnConfirm=@(SaveDeviceImport)>@Localizer["Import"]</PopConfirmButton>
 | 
			
		||||
    </StepItem>
 | 
			
		||||
    <StepItem Text=@Localizer["Second"] Title=@Localizer["ValidateText"]>
 | 
			
		||||
 | 
			
		||||
@@ -41,16 +41,12 @@
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            <PopConfirmButton IsAsync IsDisabled=@_importPreviews.Any(it => it.Value.HasError) Color=Color.Warning class="mt-2" OnConfirm=@(SaveDeviceImport)>@Localizer["Import"]</PopConfirmButton>
 | 
			
		||||
            <Button class="mt-2" IsAsync OnClick="() => DeviceImport()">@RazorLocalizer["Close"]</Button>
 | 
			
		||||
 | 
			
		||||
@* 
 | 
			
		||||
            <Button IsAsync class="mt-2" IsDisabled=@_importPreviews.Any(it => it.Value.HasError) OnClick="() => step.Next()">@Localizer["Next"]</Button> *@
 | 
			
		||||
 | 
			
		||||
        </div>
 | 
			
		||||
    </StepItem>
 | 
			
		||||
@*     <StepItem Text=@Localizer["Third"] Title=@Localizer["Import"]>
 | 
			
		||||
        <PopConfirmButton IsAsync Color=Color.Warning class="mt-2" OnConfirm=@(SaveDeviceImport)>@Localizer["Import"]</PopConfirmButton>
 | 
			
		||||
    </StepItem> *@
 | 
			
		||||
 | 
			
		||||
</Step>
 | 
			
		||||
@code {
 | 
			
		||||
    [NotNull]
 | 
			
		||||
 
 | 
			
		||||
@@ -24,18 +24,17 @@ public partial class ImportExcel
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    [EditorRequired]
 | 
			
		||||
    public Func<Dictionary<string, ImportPreviewOutputBase>, Task> Import { get; set; }
 | 
			
		||||
    public Func<IBrowserFile, Task<Dictionary<string, ImportPreviewOutputBase>>> Import { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Inject]
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    private IStringLocalizer<ImportExcel>? Localizer { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 预览
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    [EditorRequired]
 | 
			
		||||
    public Func<IBrowserFile, Task<Dictionary<string, ImportPreviewOutputBase>>> Preview { get; set; }
 | 
			
		||||
    [Inject]
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    private IStringLocalizer<ThingsGateway.Razor._Imports>? RazorLocalizer { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    [Inject]
 | 
			
		||||
    [NotNull]
 | 
			
		||||
@@ -47,13 +46,17 @@ public partial class ImportExcel
 | 
			
		||||
    [CascadingParameter]
 | 
			
		||||
    private Func<Task>? OnCloseAsync { get; set; }
 | 
			
		||||
 | 
			
		||||
    private async Task DeviceImport(IBrowserFile file)
 | 
			
		||||
    private async Task DeviceImport()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            _importPreviews.Clear();
 | 
			
		||||
            _importPreviews = await Preview.Invoke(file);
 | 
			
		||||
            await step.Next();
 | 
			
		||||
            await InvokeAsync(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                if (OnCloseAsync != null)
 | 
			
		||||
                    await OnCloseAsync();
 | 
			
		||||
                await ToastService.Default();
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
@@ -67,16 +70,12 @@ public partial class ImportExcel
 | 
			
		||||
        {
 | 
			
		||||
            await Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                await Import.Invoke(_importPreviews);
 | 
			
		||||
                _importPreviews = await Import.Invoke(_importFile);
 | 
			
		||||
                _importFile = null;
 | 
			
		||||
 | 
			
		||||
                await InvokeAsync(async () =>
 | 
			
		||||
                {
 | 
			
		||||
                    if (OnCloseAsync != null)
 | 
			
		||||
                        await OnCloseAsync();
 | 
			
		||||
                    await ToastService.Default();
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
            await step.Next();
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,58 @@
 | 
			
		||||
@using ThingsGateway.Extension
 | 
			
		||||
@namespace ThingsGateway.Razor
 | 
			
		||||
<Button OnClick="() => step.Reset()">@Localizer["Reset"]</Button>
 | 
			
		||||
<h6 class="my-3 green--text">@Localizer["Tip"] </h6>
 | 
			
		||||
<Step @ref="@step" IsVertical="true">
 | 
			
		||||
    <StepItem Text=@Localizer["First"] Title=@Localizer["Upload"]>
 | 
			
		||||
        <InputUpload ShowDeleteButton="true" @bind-Value=_importFile Accept=".xlsx"></InputUpload>
 | 
			
		||||
        <Button class="mt-2" IsAsync OnClick="() => DeviceImport(_importFile)">@Localizer["Validate"]</Button>
 | 
			
		||||
    </StepItem>
 | 
			
		||||
    <StepItem Text=@Localizer["Second"] Title=@Localizer["ValidateText"]>
 | 
			
		||||
 | 
			
		||||
        <div class="overflow-y-auto">
 | 
			
		||||
 | 
			
		||||
            @foreach (var item in _importPreviews)
 | 
			
		||||
            {
 | 
			
		||||
                <div class="mt-2">
 | 
			
		||||
                    @(
 | 
			
		||||
                                    Localizer["UploadCount", item.Key, item.Value.DataCount]
 | 
			
		||||
                                    )
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class=@((item.Value.HasError ? "my-2 red--text" : "my-2 green--text"))>
 | 
			
		||||
                    @(
 | 
			
		||||
                                    (item.Value.HasError ? "Error" : "Success")
 | 
			
		||||
                                    )
 | 
			
		||||
            </div>
 | 
			
		||||
                        if (item.Value.HasError)
 | 
			
		||||
                {
 | 
			
		||||
                    <div style="height:300px;" class="overflow-y-scroll">
 | 
			
		||||
                        <Virtualize Items="item.Value.Results.Where(a => !a.Success).OrderBy(a => a.Row).ToList()" Context="item1" ItemSize="60" OverscanCount=2>
 | 
			
		||||
                            <ItemContent>
 | 
			
		||||
                                <div class="row g-0">
 | 
			
		||||
                                    <span class="col mx-2">@item1.Row</span>
 | 
			
		||||
                                    <span class=@((item1.Success ? "green--text col-auto" : "red--text col-auto"))>
 | 
			
		||||
                                        <strong>@item1.ErrorMessage</strong>
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </ItemContent>
 | 
			
		||||
                        </Virtualize>
 | 
			
		||||
                    </div>
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            <PopConfirmButton IsAsync IsDisabled=@_importPreviews.Any(it => it.Value.HasError) Color=Color.Warning class="mt-2" OnConfirm=@(SaveDeviceImport)>@Localizer["Import"]</PopConfirmButton>
 | 
			
		||||
 | 
			
		||||
@* 
 | 
			
		||||
            <Button IsAsync class="mt-2" IsDisabled=@_importPreviews.Any(it => it.Value.HasError) OnClick="() => step.Next()">@Localizer["Next"]</Button> *@
 | 
			
		||||
 | 
			
		||||
        </div>
 | 
			
		||||
    </StepItem>
 | 
			
		||||
@*     <StepItem Text=@Localizer["Third"] Title=@Localizer["Import"]>
 | 
			
		||||
        <PopConfirmButton IsAsync Color=Color.Warning class="mt-2" OnConfirm=@(SaveDeviceImport)>@Localizer["Import"]</PopConfirmButton>
 | 
			
		||||
    </StepItem> *@
 | 
			
		||||
</Step>
 | 
			
		||||
@code {
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    Step? step { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,86 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人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.ComponentModel.DataAnnotations;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Razor;
 | 
			
		||||
 | 
			
		||||
/// <inheritdoc/>
 | 
			
		||||
public partial class ImportExcelConfirm
 | 
			
		||||
{
 | 
			
		||||
    private Dictionary<string, ImportPreviewOutputBase> _importPreviews = new();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 导入
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    [EditorRequired]
 | 
			
		||||
    public Func<Dictionary<string, ImportPreviewOutputBase>, Task> Import { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Inject]
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    private IStringLocalizer<ImportExcelConfirm>? Localizer { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 预览
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Parameter]
 | 
			
		||||
    [EditorRequired]
 | 
			
		||||
    public Func<IBrowserFile, Task<Dictionary<string, ImportPreviewOutputBase>>> Preview { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Inject]
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    private ToastService? ToastService { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Required]
 | 
			
		||||
    private IBrowserFile _importFile { get; set; }
 | 
			
		||||
 | 
			
		||||
    [CascadingParameter]
 | 
			
		||||
    private Func<Task>? OnCloseAsync { get; set; }
 | 
			
		||||
 | 
			
		||||
    private async Task DeviceImport(IBrowserFile file)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            _importPreviews.Clear();
 | 
			
		||||
            _importPreviews = await Preview.Invoke(file);
 | 
			
		||||
            await step.Next();
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            await ToastService.Warn(ex);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task SaveDeviceImport()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                await Import.Invoke(_importPreviews);
 | 
			
		||||
                _importFile = null;
 | 
			
		||||
 | 
			
		||||
                await InvokeAsync(async () =>
 | 
			
		||||
                {
 | 
			
		||||
                    if (OnCloseAsync != null)
 | 
			
		||||
                        await OnCloseAsync();
 | 
			
		||||
                    await ToastService.Default();
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            await InvokeAsync(async () => await ToastService.Warn(ex));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
::deep .avatar {
 | 
			
		||||
    border-radius: 1.5rem;
 | 
			
		||||
    width: 24px;
 | 
			
		||||
    height: 24px;
 | 
			
		||||
    background-color: var(--bs-red);
 | 
			
		||||
    color: #fff;
 | 
			
		||||
    flex: 0 0 auto;
 | 
			
		||||
    font-size: 1rem;
 | 
			
		||||
}
 | 
			
		||||
@@ -10,7 +10,7 @@
 | 
			
		||||
 | 
			
		||||
using Microsoft.JSInterop;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Common.Extension;
 | 
			
		||||
namespace ThingsGateway.Razor.Extension;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// JSRuntime扩展方法
 | 
			
		||||
@@ -49,4 +49,28 @@ public static class JSRuntimeExtensions
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public static async ValueTask<T> GetLocalStorage<T>(this IJSRuntime jsRuntime, string name)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            return await jsRuntime.InvokeAsync<T>("getLocalStorage", name).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        {
 | 
			
		||||
            return default;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static async ValueTask SetLocalStorage<T>(this IJSRuntime jsRuntime, string name, T data)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await jsRuntime.InvokeVoidAsync("setLocalStorage", name, data).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -25,7 +25,8 @@
 | 
			
		||||
    "Success": "Success",
 | 
			
		||||
    "TablesExportButtonExcelText": "Export Excel",
 | 
			
		||||
    "TablesImportButtonExcelText": "Import Excel",
 | 
			
		||||
    "True": "Yes"
 | 
			
		||||
    "True": "Yes",
 | 
			
		||||
    "Info": "Info"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Razor.About": {
 | 
			
		||||
    "Community": "Community",
 | 
			
		||||
@@ -59,6 +60,19 @@
 | 
			
		||||
    "SearchText": "Search Page"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Razor.ImportExcel": {
 | 
			
		||||
    "First": "Step 1",
 | 
			
		||||
    "Import": "If there are no errors during verification, it will be directly imported into the database",
 | 
			
		||||
    "Next": "Next",
 | 
			
		||||
    "Reset": "Reset",
 | 
			
		||||
    "Second": "Step 2",
 | 
			
		||||
    "Third": "Step 3",
 | 
			
		||||
    "Tip": "When the data volume is large (more than 200,000), the import may take more than 1 minute, please be patient",
 | 
			
		||||
    "Upload": "Upload File",
 | 
			
		||||
    "UploadCount": " Table {0}, import {1} records",
 | 
			
		||||
    "Validate": "Validate",
 | 
			
		||||
    "ValidateText": "Validation Content"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Razor.ImportExcelConfirm": {
 | 
			
		||||
    "First": "Step 1",
 | 
			
		||||
    "Import": "Import",
 | 
			
		||||
    "Next": "Next",
 | 
			
		||||
@@ -67,7 +81,7 @@
 | 
			
		||||
    "Third": "Step 3",
 | 
			
		||||
    "Tip": "When the data volume is large (more than 200,000), the import may take more than 1 minute, please be patient",
 | 
			
		||||
    "Upload": "Upload File",
 | 
			
		||||
    "UploadCount": " Table {0}, expecting to import {1} records",
 | 
			
		||||
    "UploadCount": " Table {0}, import {1} records",
 | 
			
		||||
    "Validate": "Validate",
 | 
			
		||||
    "ValidateText": "Validation Content"
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,8 @@
 | 
			
		||||
    "Success": "成功",
 | 
			
		||||
    "TablesExportButtonExcelText": "导出Excel",
 | 
			
		||||
    "TablesImportButtonExcelText": "导入Excel",
 | 
			
		||||
    "True": "是"
 | 
			
		||||
    "True": "是",
 | 
			
		||||
    "Info": "详情"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Razor.About": {
 | 
			
		||||
    "Community": "社区",
 | 
			
		||||
@@ -59,6 +60,19 @@
 | 
			
		||||
    "SearchText": "搜索页面"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Razor.ImportExcel": {
 | 
			
		||||
    "First": "第一步",
 | 
			
		||||
    "Import": "若验证无错误,将直接导入数据库",
 | 
			
		||||
    "Next": "下一步",
 | 
			
		||||
    "Reset": "重置",
 | 
			
		||||
    "Second": "第二步",
 | 
			
		||||
    "Third": "第三",
 | 
			
		||||
    "Tip": "数据量较大时(大于20万),所需导入时间可能超过1分钟,请耐心等待",
 | 
			
		||||
    "Upload": "上传文件",
 | 
			
		||||
    "UploadCount": " 表 {0},导入 {1} 条数据",
 | 
			
		||||
    "Validate": "验证",
 | 
			
		||||
    "ValidateText": "验证内容"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Razor.ImportExcelConfirm": {
 | 
			
		||||
    "First": "第一步",
 | 
			
		||||
    "Import": "导入",
 | 
			
		||||
    "Next": "下一步",
 | 
			
		||||
@@ -67,7 +81,7 @@
 | 
			
		||||
    "Third": "第三",
 | 
			
		||||
    "Tip": "数据量较大时(大于20万),所需导入时间可能超过1分钟,请耐心等待",
 | 
			
		||||
    "Upload": "上传文件",
 | 
			
		||||
    "UploadCount": " 表 {0},预计导入 {1} 条数据",
 | 
			
		||||
    "UploadCount": " 表 {0},导入 {1} 条数据",
 | 
			
		||||
    "Validate": "验证",
 | 
			
		||||
    "ValidateText": "验证内容"
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@
 | 
			
		||||
	<Import Project="..\..\PackNuget.props" />
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0</TargetFrameworks>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.FontAwesome" Version="9.1.0" />
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
// 设置 culture
 | 
			
		||||
function setCultureLocalStorage(culture) {
 | 
			
		||||
    localStorage.setItem("culture", culture);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取 culture
 | 
			
		||||
function getCultureLocalStorage() {
 | 
			
		||||
    return localStorage.getItem("culture");
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								src/Admin/ThingsGateway.Razor/wwwroot/js/localStorageUtil.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/Admin/ThingsGateway.Razor/wwwroot/js/localStorageUtil.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
// 设置 culture
 | 
			
		||||
function setCultureLocalStorage(culture) {
 | 
			
		||||
    localStorage.setItem("culture", culture);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取 culture
 | 
			
		||||
function getCultureLocalStorage() {
 | 
			
		||||
    return localStorage.getItem("culture");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 function getLocalStorage(name) {
 | 
			
		||||
    return JSON.parse(localStorage.getItem(name)) ?? 0;
 | 
			
		||||
}
 | 
			
		||||
 function setLocalStorage(name, data) {
 | 
			
		||||
    if (localStorage) {
 | 
			
		||||
        localStorage.setItem(name, JSON.stringify(data));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -51,9 +51,14 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
            // HTTP GET 请求执行SQL
 | 
			
		||||
            var result = string.Empty;
 | 
			
		||||
            var url = $"{this.url}/exec?query={HttpUtility.UrlEncode(sql)}";
 | 
			
		||||
 | 
			
		||||
            var request = new HttpRequestMessage(HttpMethod.Get, url);
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(authorization))
 | 
			
		||||
                client.DefaultRequestHeaders.Add("Authorization", authorization);
 | 
			
		||||
            var httpResponseMessage = await client.GetAsync(url).ConfigureAwait(false);
 | 
			
		||||
            {
 | 
			
		||||
                request.Headers.Authorization = AuthenticationHeaderValue.Parse(authorization);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            using var httpResponseMessage = await client.SendAsync(request).ConfigureAwait(false);
 | 
			
		||||
            result = await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
@@ -127,35 +132,43 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
            {
 | 
			
		||||
                // 准备多部分表单数据
 | 
			
		||||
                var boundary = "---------------" + DateTime.Now.Ticks.ToString("x");
 | 
			
		||||
                var list = new List<Hashtable>();
 | 
			
		||||
 | 
			
		||||
                tableName ??= db.EntityMaintenance.GetEntityInfo<T>().DbTableName;
 | 
			
		||||
 | 
			
		||||
                // 获取或创建列信息缓存
 | 
			
		||||
                var key = "QuestDbBulkCopy" + typeof(T).FullName + typeof(T).GetHashCode();
 | 
			
		||||
                var columns = ReflectionInoCacheService.Instance.GetOrCreate(key, () =>
 | 
			
		||||
                 db.CopyNew().DbMaintenance.GetColumnInfosByTableName(tableName));
 | 
			
		||||
 | 
			
		||||
                // 构建schema信息
 | 
			
		||||
                columns.ForEach(d =>
 | 
			
		||||
                var list = ReflectionInoCacheService.Instance.GetOrCreate($"{key}{dateFormat}List<Hashtable>", () =>
 | 
			
		||||
                {
 | 
			
		||||
                    if (d.DataType == "TIMESTAMP")
 | 
			
		||||
                    var list = new List<Hashtable>();
 | 
			
		||||
 | 
			
		||||
                    // 构建schema信息
 | 
			
		||||
                    columns.ForEach(d =>
 | 
			
		||||
                    {
 | 
			
		||||
                        list.Add(new Hashtable()
 | 
			
		||||
                        if (d.DataType == "TIMESTAMP")
 | 
			
		||||
                        {
 | 
			
		||||
                            list.Add(new Hashtable()
 | 
			
		||||
                        {
 | 
			
		||||
                            { "name", d.DbColumnName },
 | 
			
		||||
                            { "type", d.DataType },
 | 
			
		||||
                            { "pattern", dateFormat}
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        list.Add(new Hashtable()
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            list.Add(new Hashtable()
 | 
			
		||||
                        {
 | 
			
		||||
                            { "name", d.DbColumnName },
 | 
			
		||||
                            { "type", d.DataType }
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    return list;
 | 
			
		||||
                }
 | 
			
		||||
 );
 | 
			
		||||
 | 
			
		||||
                var schema = JsonConvert.SerializeObject(list);
 | 
			
		||||
 | 
			
		||||
                // 写入CSV文件
 | 
			
		||||
@@ -184,8 +197,8 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
                    "multipart/form-data; boundary=" + boundary);
 | 
			
		||||
 | 
			
		||||
                // 发送请求并处理响应
 | 
			
		||||
                var httpResponseMessage =
 | 
			
		||||
                    await Post(client, tableName, httpContent).ConfigureAwait(false);
 | 
			
		||||
                using var httpResponseMessage =
 | 
			
		||||
                      await Post(client, tableName, httpContent).ConfigureAwait(false);
 | 
			
		||||
                var readAsStringAsync = await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
 | 
			
		||||
                var splitByLine = QuestDbRestAPHelper.SplitByLine(readAsStringAsync);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,6 @@
 | 
			
		||||
        V Get<V>(string key);
 | 
			
		||||
        IEnumerable<string> GetAllKey<V>();
 | 
			
		||||
        void Remove<V>(string key);
 | 
			
		||||
        V GetOrCreate<V>(string cacheKey, Func<V> create, int cacheDurationInSeconds = int.MaxValue);
 | 
			
		||||
        V GetOrCreate<V>(string cacheKey, Func<V> create, int cacheDurationInSeconds = 3600);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -31,7 +31,7 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
            return ReflectionInoCore<V>.GetInstance().GetAllKey();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public V GetOrCreate<V>(string cacheKey, Func<V> create, int cacheDurationInSeconds = int.MaxValue)
 | 
			
		||||
        public V GetOrCreate<V>(string cacheKey, Func<V> create, int cacheDurationInSeconds = 3600)
 | 
			
		||||
        {
 | 
			
		||||
            return ReflectionInoCore<V>.GetInstance().GetOrCreate(cacheKey, create);
 | 
			
		||||
        }
 | 
			
		||||
@@ -43,10 +43,13 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
    }
 | 
			
		||||
    public class ReflectionInoCore<V>
 | 
			
		||||
    {
 | 
			
		||||
        private MemoryCache InstanceCache => new MemoryCache() { Expire = 60 };
 | 
			
		||||
        private MemoryCache InstanceCache = new MemoryCache() { Expire = 180 };
 | 
			
		||||
        private static ReflectionInoCore<V> _instance = null;
 | 
			
		||||
        private static readonly object _instanceLock = new object();
 | 
			
		||||
        private ReflectionInoCore() { }
 | 
			
		||||
        private ReflectionInoCore()
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public V this[string key]
 | 
			
		||||
        {
 | 
			
		||||
@@ -107,10 +110,10 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
            return this.InstanceCache.Keys;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public V GetOrCreate(string cacheKey, Func<V> create)
 | 
			
		||||
        public V GetOrCreate(string cacheKey, Func<V> create, int expire = 3600)
 | 
			
		||||
        {
 | 
			
		||||
            return InstanceCache.GetOrAdd<V>(cacheKey, (a) =>
 | 
			
		||||
            create());
 | 
			
		||||
            create(), expire);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    public static class ReflectionInoHelper
 | 
			
		||||
 
 | 
			
		||||
@@ -447,6 +447,28 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public override List<DbColumnInfo> GetColumnInfosByTableName(string tableName, bool isCache = true)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrEmpty(tableName)) return new List<DbColumnInfo>();
 | 
			
		||||
            string cacheKey = "QuestDB.GetColumnInfosByTableName." + this.SqlBuilder.GetNoTranslationColumnName(tableName).ToLower() + this.Context.CurrentConnectionConfig.ConfigId;
 | 
			
		||||
            cacheKey = GetCacheKey(cacheKey);
 | 
			
		||||
 | 
			
		||||
            if (isCache)
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
                return this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () =>
 | 
			
		||||
                {
 | 
			
		||||
                    return GetColInfo(tableName);
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return GetColInfo(tableName);
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private List<DbColumnInfo> GetColInfo(string tableName)
 | 
			
		||||
        {
 | 
			
		||||
            var sql = String.Format(GetColumnInfosByTableNameSql, tableName);
 | 
			
		||||
            List<DbColumnInfo> result = new List<DbColumnInfo>();
 | 
			
		||||
 
 | 
			
		||||
@@ -717,8 +717,32 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
        /// <returns>列信息列表</returns>
 | 
			
		||||
        public override List<DbColumnInfo> GetColumnInfosByTableName(string tableName, bool isCache = true)
 | 
			
		||||
        {
 | 
			
		||||
            var sql = $"select * from {this.SqlBuilder.GetTranslationColumnName(tableName)} where 1=2 ";
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrEmpty(tableName)) return new List<DbColumnInfo>();
 | 
			
		||||
            string cacheKey = "TDengine.GetColumnInfosByTableName." + this.SqlBuilder.GetNoTranslationColumnName(tableName).ToLower() + this.Context.CurrentConnectionConfig.ConfigId;
 | 
			
		||||
            cacheKey = GetCacheKey(cacheKey);
 | 
			
		||||
 | 
			
		||||
            if (isCache)
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
                return this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () =>
 | 
			
		||||
                     {
 | 
			
		||||
                         return GetColInfo(tableName);
 | 
			
		||||
                     });
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return GetColInfo(tableName);
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private List<DbColumnInfo> GetColInfo(string tableName)
 | 
			
		||||
        {
 | 
			
		||||
            List<DbColumnInfo> result = new List<DbColumnInfo>();
 | 
			
		||||
 | 
			
		||||
            var sql = $"select * from {this.SqlBuilder.GetTranslationColumnName(tableName)} where 1=2 ";
 | 
			
		||||
            DataTable dt = null;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<GenerateDocumentationFile>True</GenerateDocumentationFile>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
 | 
			
		||||
@@ -31,7 +32,7 @@
 | 
			
		||||
		<PackageReference Include="CsvHelper" Version="33.1.0" />
 | 
			
		||||
		<PackageReference Include="TDengine.Connector" Version="3.1.7" />
 | 
			
		||||
		<PackageReference Include="Oracle.ManagedDataAccess.Core" Version="23.9.1" />
 | 
			
		||||
		<PackageReference Include="Oscar.Data.SqlClient" Version="4.2.21" />
 | 
			
		||||
		<PackageReference Include="Oscar.Data.SqlClient" Version="4.2.22" />
 | 
			
		||||
		<PackageReference Include="System.Data.Common" Version="4.3.0" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.0" />
 | 
			
		||||
		<PackageReference Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
<Project>
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<PluginVersion>10.10.7</PluginVersion>
 | 
			
		||||
		<ProPluginVersion>10.10.6</ProPluginVersion>
 | 
			
		||||
		<DefaultVersion>10.10.10</DefaultVersion>
 | 
			
		||||
		<PluginVersion>10.10.18</PluginVersion>
 | 
			
		||||
		<ProPluginVersion>10.10.18</ProPluginVersion>
 | 
			
		||||
		<DefaultVersion>10.10.18</DefaultVersion>
 | 
			
		||||
		<AuthenticationVersion>10.10.1</AuthenticationVersion>
 | 
			
		||||
		<SourceGeneratorVersion>10.10.1</SourceGeneratorVersion>
 | 
			
		||||
		<NET8Version>8.0.19</NET8Version>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,10 +4,11 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>netstandard2.0;</TargetFrameworks>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="CS-Script" Version="4.10.1" />
 | 
			
		||||
		<PackageReference Include="CS-Script" Version="4.11.0" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@
 | 
			
		||||
            <EditorItem @bind-Field=Model.EncodingName>
 | 
			
		||||
                <EditTemplate Context="value">
 | 
			
		||||
                    <div class="col-12 col-sm-4">
 | 
			
		||||
                        <Select @bind-Value=value.EncodingName Items="EncodingItems" />
 | 
			
		||||
                        <Select @bind-Value=value.EncodingName Items="EncodingItems" IsClearable/>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </EditTemplate>
 | 
			
		||||
            </EditorItem>
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,6 @@ public partial class ConverterConfigComponent : ComponentBase
 | 
			
		||||
    protected override void OnInitialized()
 | 
			
		||||
    {
 | 
			
		||||
        BoolItems = LocalizerUtil.GetBoolItems(Model.GetType(), nameof(Model.VariableStringLength), true);
 | 
			
		||||
        EncodingItems = new List<SelectedItem>() { new SelectedItem("", "none") }.Concat(Encoding.GetEncodings().Select(a => new SelectedItem(a.CodePage.ToString(), a.DisplayName))).ToList();
 | 
			
		||||
        EncodingItems = Encoding.GetEncodings().Select(a => new SelectedItem(a.CodePage.ToString(), a.DisplayName)).ToList();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -95,7 +95,7 @@ public partial class LogConsole : IDisposable
 | 
			
		||||
 | 
			
		||||
            if (LogPath != null)
 | 
			
		||||
            {
 | 
			
		||||
                var files = await TextFileReadService.GetLogFiles(LogPath);
 | 
			
		||||
                var files = await TextFileReadService.GetLogFilesAsync(LogPath);
 | 
			
		||||
                if (!files.IsSuccess)
 | 
			
		||||
                {
 | 
			
		||||
                    Messages = new List<LogMessage>();
 | 
			
		||||
@@ -106,7 +106,7 @@ public partial class LogConsole : IDisposable
 | 
			
		||||
                    await Task.Run(async () =>
 | 
			
		||||
                    {
 | 
			
		||||
                        Stopwatch sw = Stopwatch.StartNew();
 | 
			
		||||
                        var result = await TextFileReadService.LastLogData(files.Content.FirstOrDefault());
 | 
			
		||||
                        var result = await TextFileReadService.LastLogDataAsync(files.Content.FirstOrDefault());
 | 
			
		||||
                        if (result.IsSuccess)
 | 
			
		||||
                        {
 | 
			
		||||
                            Messages = result.Content.Where(a => a.LogLevel >= LogLevel).Select(a => new LogMessage((int)a.LogLevel, $"{a.LogTime} - {a.Message}{(a.ExceptionString.IsNullOrWhiteSpace() ? null : $"{Environment.NewLine}{a.ExceptionString}")}")).ToList();
 | 
			
		||||
@@ -144,7 +144,7 @@ public partial class LogConsole : IDisposable
 | 
			
		||||
    {
 | 
			
		||||
        if (LogPath != null)
 | 
			
		||||
        {
 | 
			
		||||
            var files = await TextFileReadService.GetLogFiles(LogPath);
 | 
			
		||||
            var files = await TextFileReadService.GetLogFilesAsync(LogPath);
 | 
			
		||||
            if (files.IsSuccess)
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var item in files.Content)
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ public class PlatformService : IPlatformService
 | 
			
		||||
 | 
			
		||||
    public async Task OnLogExport(string logPath)
 | 
			
		||||
    {
 | 
			
		||||
        var files = TextFileReader.GetLogFiles(logPath);
 | 
			
		||||
        var files = TextFileReader.GetLogFilesAsync(logPath);
 | 
			
		||||
        if (!files.IsSuccess)
 | 
			
		||||
        {
 | 
			
		||||
            return;
 | 
			
		||||
@@ -40,7 +40,7 @@ public class PlatformService : IPlatformService
 | 
			
		||||
            await using var jSObject = await JSRuntime.InvokeAsync<IJSObjectReference>("import", $"{WebsiteConst.DefaultResourceUrl}js/downloadFile.js");
 | 
			
		||||
            var path = Path.GetRelativePath("wwwroot", item);
 | 
			
		||||
            string fileName = DateTime.Now.ToFileDateTimeFormat();
 | 
			
		||||
            await jSObject.InvokeVoidAsync("blazor_downloadFile", url, fileName, new { FileName = path });
 | 
			
		||||
            await jSObject.InvokeAsync<bool>("blazor_downloadFile", url, fileName, new { FileName = path });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
	<Import Project="..\..\PackNuget.props" />
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;</TargetFrameworks>
 | 
			
		||||
		
 | 
			
		||||
		<!--<UseRazorSourceGenerator>false</UseRazorSourceGenerator>-->
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
		<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
 | 
			
		||||
		<NoPackageAnalysis>true</NoPackageAnalysis>
 | 
			
		||||
		<SignAssembly>false</SignAssembly>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>netstandard2.0;</TargetFrameworks>
 | 
			
		||||
		<Version>$(SourceGeneratorVersion)</Version>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,7 @@ public static class TextFileReader
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="directoryPath">目录路径</param>
 | 
			
		||||
    /// <returns>包含文件信息的列表</returns>
 | 
			
		||||
    public static OperResult<List<string>> GetLogFiles(string directoryPath)
 | 
			
		||||
    public static OperResult<List<string>> GetLogFilesAsync(string directoryPath)
 | 
			
		||||
    {
 | 
			
		||||
        OperResult<List<string>> result = new(); // 初始化结果对象
 | 
			
		||||
        // 检查目录是否存在
 | 
			
		||||
@@ -91,7 +91,7 @@ public static class TextFileReader
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static OperResult<List<LogData>> LastLogData(string file, int lineCount = 200)
 | 
			
		||||
    public static OperResult<List<LogData>> LastLogDataAsync(string file, int lineCount = 200)
 | 
			
		||||
    {
 | 
			
		||||
        if (!File.Exists(file))
 | 
			
		||||
            return new OperResult<List<LogData>>("The file path is invalid");
 | 
			
		||||
@@ -104,7 +104,7 @@ public static class TextFileReader
 | 
			
		||||
            {
 | 
			
		||||
                var fileInfo = new FileInfo(file);
 | 
			
		||||
                var length = fileInfo.Length;
 | 
			
		||||
                var cacheKey = $"{nameof(TextFileReader)}_{nameof(LastLogData)}_{file})";
 | 
			
		||||
                var cacheKey = $"{nameof(TextFileReader)}_{nameof(LastLogDataAsync)}_{file})";
 | 
			
		||||
                if (_cache.TryGetValue<LogDataCache>(cacheKey, out var cachedData))
 | 
			
		||||
                {
 | 
			
		||||
                    if (cachedData != null && cachedData.Length == length)
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ public interface ITextFileReadService
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="directoryPath">目录路径</param>
 | 
			
		||||
    /// <returns>包含文件信息的列表</returns>
 | 
			
		||||
    public Task<OperResult<List<string>>> GetLogFiles(string directoryPath);
 | 
			
		||||
    public Task<OperResult<List<string>>> GetLogFilesAsync(string directoryPath);
 | 
			
		||||
 | 
			
		||||
    public Task<OperResult<List<LogData>>> LastLogData(string file, int lineCount = 200);
 | 
			
		||||
    public Task<OperResult<List<LogData>>> LastLogDataAsync(string file, int lineCount = 200);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ namespace ThingsGateway.Foundation;
 | 
			
		||||
 | 
			
		||||
public class TextFileReadService : ITextFileReadService
 | 
			
		||||
{
 | 
			
		||||
    public Task<OperResult<List<string>>> GetLogFiles(string directoryPath) => Task.FromResult(TextFileReader.GetLogFiles(directoryPath));
 | 
			
		||||
    public Task<OperResult<List<string>>> GetLogFilesAsync(string directoryPath) => Task.FromResult(TextFileReader.GetLogFilesAsync(directoryPath));
 | 
			
		||||
 | 
			
		||||
    public Task<OperResult<List<LogData>>> LastLogData(string file, int lineCount = 200) => Task.FromResult(TextFileReader.LastLogData(file, lineCount));
 | 
			
		||||
    public Task<OperResult<List<LogData>>> LastLogDataAsync(string file, int lineCount = 200) => Task.FromResult(TextFileReader.LastLogDataAsync(file, lineCount));
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<Description>工业设备通讯协议-基础类库</Description>
 | 
			
		||||
		<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
 | 
			
		||||
		
 | 
			
		||||
		<!--<UseRazorSourceGenerator>false</UseRazorSourceGenerator>-->
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,13 +12,15 @@ using TouchSocket.Core;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
public class AsyncReadWriteLock
 | 
			
		||||
public class AsyncReadWriteLock : IAsyncDisposable
 | 
			
		||||
{
 | 
			
		||||
    private readonly int _writeReadRatio = 3; // 写3次会允许1次读,但写入也不会被阻止,具体协议取决于插件协议实现
 | 
			
		||||
    public AsyncReadWriteLock(int writeReadRatio)
 | 
			
		||||
    public AsyncReadWriteLock(int writeReadRatio, bool writePriority)
 | 
			
		||||
    {
 | 
			
		||||
        _writeReadRatio = writeReadRatio;
 | 
			
		||||
        _writePriority = writePriority;
 | 
			
		||||
    }
 | 
			
		||||
    private bool _writePriority;
 | 
			
		||||
    private AsyncAutoResetEvent _readerLock = new AsyncAutoResetEvent(false); // 控制读计数
 | 
			
		||||
    private long _writerCount = 0; // 当前活跃的写线程数
 | 
			
		||||
    private long _readerCount = 0; // 当前被阻塞的读线程数
 | 
			
		||||
@@ -33,6 +35,8 @@ public class AsyncReadWriteLock
 | 
			
		||||
        {
 | 
			
		||||
            Interlocked.Increment(ref _readerCount);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            // 第一个读者需要获取写入锁,防止写操作
 | 
			
		||||
            await _readerLock.WaitOneAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
@@ -52,10 +56,13 @@ public class AsyncReadWriteLock
 | 
			
		||||
 | 
			
		||||
        if (Interlocked.Increment(ref _writerCount) == 1)
 | 
			
		||||
        {
 | 
			
		||||
            var cancellationTokenSource = _cancellationTokenSource;
 | 
			
		||||
            _cancellationTokenSource = new();
 | 
			
		||||
            await cancellationTokenSource.SafeCancelAsync().ConfigureAwait(false); // 取消读取
 | 
			
		||||
            cancellationTokenSource.SafeDispose();
 | 
			
		||||
            if (_writePriority)
 | 
			
		||||
            {
 | 
			
		||||
                var cancellationTokenSource = _cancellationTokenSource;
 | 
			
		||||
                _cancellationTokenSource = new();
 | 
			
		||||
                await cancellationTokenSource.SafeCancelAsync().ConfigureAwait(false); // 取消读取
 | 
			
		||||
                cancellationTokenSource.SafeDispose();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new Writer(this);
 | 
			
		||||
@@ -63,11 +70,16 @@ public class AsyncReadWriteLock
 | 
			
		||||
    private object lockObject = new();
 | 
			
		||||
    private void ReleaseWriter()
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        var writerCount = Interlocked.Decrement(ref _writerCount);
 | 
			
		||||
 | 
			
		||||
        // 每次释放写时,总是唤醒至少一个读
 | 
			
		||||
        _readerLock.Set();
 | 
			
		||||
 | 
			
		||||
        if (writerCount == 0)
 | 
			
		||||
        {
 | 
			
		||||
            var resetEvent = _readerLock;
 | 
			
		||||
            _readerLock = new(false);
 | 
			
		||||
            //_readerLock = new(false);
 | 
			
		||||
            Interlocked.Exchange(ref _writeSinceLastReadCount, 0);
 | 
			
		||||
            resetEvent.SetAll();
 | 
			
		||||
        }
 | 
			
		||||
@@ -83,18 +95,28 @@ public class AsyncReadWriteLock
 | 
			
		||||
                    if (count >= _writeReadRatio)
 | 
			
		||||
                    {
 | 
			
		||||
                        Interlocked.Exchange(ref _writeSinceLastReadCount, 0);
 | 
			
		||||
                        _readerLock.Set();
 | 
			
		||||
                        //_readerLock.Set();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    _readerLock.Set();
 | 
			
		||||
                    //_readerLock.Set();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async ValueTask DisposeAsync()
 | 
			
		||||
    {
 | 
			
		||||
        if (_cancellationTokenSource != null)
 | 
			
		||||
        {
 | 
			
		||||
            await _cancellationTokenSource.SafeCancelAsync().ConfigureAwait(false);
 | 
			
		||||
            _cancellationTokenSource.SafeDispose();
 | 
			
		||||
        }
 | 
			
		||||
        _readerLock.SetAll();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private int _writeSinceLastReadCount = 0;
 | 
			
		||||
    private struct Writer : IDisposable
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ namespace BootstrapBlazor.Components;
 | 
			
		||||
/// <param name="fieldName">字段名称</param>
 | 
			
		||||
/// <param name="fieldType">字段类型</param>
 | 
			
		||||
/// <param name="fieldText">显示文字</param>
 | 
			
		||||
internal sealed class InternalTableColumn(string fieldName, Type fieldType, string? fieldText = null) : IEditorItem, ITableColumn
 | 
			
		||||
public sealed class DefaultTableColumn(string fieldName, Type fieldType, string? fieldText = null) : IEditorItem, ITableColumn
 | 
			
		||||
{
 | 
			
		||||
    public IEnumerable<KeyValuePair<string, object>>? ComponentParameters { get; set; }
 | 
			
		||||
    public Type? ComponentType { get; set; }
 | 
			
		||||
@@ -12,7 +12,7 @@ using BootstrapBlazor.Components;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
public class ExportFilter
 | 
			
		||||
public class GatewayExportFilter
 | 
			
		||||
{
 | 
			
		||||
    public FilterKeyValueAction FilterKeyValueAction { get; set; }
 | 
			
		||||
    public string? PluginName { get; set; }
 | 
			
		||||
@@ -0,0 +1,57 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
public class LinkedCancellationTokenSourceCache : IDisposable
 | 
			
		||||
{
 | 
			
		||||
    private CancellationTokenSource? _cachedCts;
 | 
			
		||||
    private CancellationToken _token1;
 | 
			
		||||
    private CancellationToken _token2;
 | 
			
		||||
    private readonly object _lock = new();
 | 
			
		||||
    ~LinkedCancellationTokenSourceCache()
 | 
			
		||||
    {
 | 
			
		||||
        Dispose();
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取一个 CancellationTokenSource,它是由两个 token 链接而成的。
 | 
			
		||||
    /// 会尝试复用之前缓存的 CTS,前提是两个 token 仍然相同且未取消。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public CancellationTokenSource GetLinkedTokenSource(CancellationToken token1, CancellationToken token2)
 | 
			
		||||
    {
 | 
			
		||||
        lock (_lock)
 | 
			
		||||
        {
 | 
			
		||||
            // 如果缓存的 CTS 已经取消或 Dispose,或者 token 不同,重新创建
 | 
			
		||||
            if (_cachedCts?.IsCancellationRequested != false ||
 | 
			
		||||
                !_token1.Equals(token1) || !_token2.Equals(token2))
 | 
			
		||||
            {
 | 
			
		||||
                _cachedCts?.Dispose();
 | 
			
		||||
 | 
			
		||||
                _cachedCts = CancellationTokenSource.CreateLinkedTokenSource(token1, token2);
 | 
			
		||||
                _token1 = token1;
 | 
			
		||||
                _token2 = token2;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return _cachedCts;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
        lock (_lock)
 | 
			
		||||
        {
 | 
			
		||||
            _cachedCts?.Dispose();
 | 
			
		||||
            _cachedCts = null!;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -10,6 +10,7 @@ public class CronScheduledTask : DisposeBase, IScheduledTask
 | 
			
		||||
    private int _interval10MS = 10;
 | 
			
		||||
    private string _interval;
 | 
			
		||||
    private readonly Func<object?, CancellationToken, Task> _taskFunc;
 | 
			
		||||
    private readonly Func<object?, CancellationToken, ValueTask> _valueTaskFunc;
 | 
			
		||||
    private readonly Action<object?, CancellationToken> _taskAction;
 | 
			
		||||
    private readonly CancellationToken _token;
 | 
			
		||||
    private TimerX? _timer;
 | 
			
		||||
@@ -28,6 +29,17 @@ public class CronScheduledTask : DisposeBase, IScheduledTask
 | 
			
		||||
        _taskFunc = taskFunc;
 | 
			
		||||
        _token = token;
 | 
			
		||||
    }
 | 
			
		||||
    public CronScheduledTask(string interval, Func<object?, CancellationToken, ValueTask> taskFunc, object? state, ILog log, CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
        _interval = interval;
 | 
			
		||||
        LogMessage = log;
 | 
			
		||||
        _state = state;
 | 
			
		||||
        _valueTaskFunc = taskFunc;
 | 
			
		||||
        _token = token;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public CronScheduledTask(string interval, Action<object?, CancellationToken> taskAction, object? state, ILog log, CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
        _interval = interval;
 | 
			
		||||
@@ -51,14 +63,14 @@ public class CronScheduledTask : DisposeBase, IScheduledTask
 | 
			
		||||
        if (Check()) return;
 | 
			
		||||
        if (_taskAction != null)
 | 
			
		||||
            _timer = new TimerX(TimerCallback, _state, _interval, nameof(IScheduledTask)) { Async = true };
 | 
			
		||||
        else if (_taskFunc != null)
 | 
			
		||||
        else if (_taskFunc != null || _valueTaskFunc != null)
 | 
			
		||||
            _timer = new TimerX(TimerCallbackAsync, _state, _interval, nameof(IScheduledTask)) { Async = true };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task TimerCallbackAsync(object? state)
 | 
			
		||||
    private async ValueTask TimerCallbackAsync(object? state)
 | 
			
		||||
    {
 | 
			
		||||
        if (Check()) return;
 | 
			
		||||
        if (_taskFunc == null)
 | 
			
		||||
        if (_taskFunc == null && _valueTaskFunc == null)
 | 
			
		||||
        {
 | 
			
		||||
            Dispose();
 | 
			
		||||
            return;
 | 
			
		||||
@@ -74,7 +86,10 @@ public class CronScheduledTask : DisposeBase, IScheduledTask
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await _taskFunc(state, _token).ConfigureAwait(false);
 | 
			
		||||
            if (_taskFunc != null)
 | 
			
		||||
                await _taskFunc(state, _token).ConfigureAwait(false);
 | 
			
		||||
            else if (_valueTaskFunc != null)
 | 
			
		||||
                await _valueTaskFunc(state, _token).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
        catch (OperationCanceledException)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ public class ScheduledAsyncTask : DisposeBase, IScheduledTask, IScheduledIntInte
 | 
			
		||||
    private int _interval10MS = 10;
 | 
			
		||||
    public int IntervalMS { get; }
 | 
			
		||||
    private readonly Func<object?, CancellationToken, Task> _taskFunc;
 | 
			
		||||
    private readonly Func<object?, CancellationToken, ValueTask> _valueTaskFunc;
 | 
			
		||||
    private readonly CancellationToken _token;
 | 
			
		||||
    private TimerX? _timer;
 | 
			
		||||
    private object? _state;
 | 
			
		||||
@@ -26,6 +27,14 @@ public class ScheduledAsyncTask : DisposeBase, IScheduledTask, IScheduledIntInte
 | 
			
		||||
        _taskFunc = taskFunc;
 | 
			
		||||
        _token = token;
 | 
			
		||||
    }
 | 
			
		||||
    public ScheduledAsyncTask(int interval, Func<object?, CancellationToken, ValueTask> taskFunc, object? state, ILog log, CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
        IntervalMS = interval;
 | 
			
		||||
        LogMessage = log;
 | 
			
		||||
        _state = state;
 | 
			
		||||
        _valueTaskFunc = taskFunc;
 | 
			
		||||
        _token = token;
 | 
			
		||||
    }
 | 
			
		||||
    private bool Check()
 | 
			
		||||
    {
 | 
			
		||||
        if (_token.IsCancellationRequested)
 | 
			
		||||
@@ -42,11 +51,17 @@ public class ScheduledAsyncTask : DisposeBase, IScheduledTask, IScheduledIntInte
 | 
			
		||||
            _timer = new TimerX(DoAsync, _state, IntervalMS, IntervalMS, nameof(IScheduledTask)) { Async = true };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task DoAsync(object? state)
 | 
			
		||||
    private async ValueTask DoAsync(object? state)
 | 
			
		||||
    {
 | 
			
		||||
        if (Check())
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        if (_taskFunc == null && _valueTaskFunc == null)
 | 
			
		||||
        {
 | 
			
		||||
            Dispose();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Interlocked.Increment(ref _pendingTriggers);
 | 
			
		||||
 | 
			
		||||
        if (Interlocked.Exchange(ref _isRunning, 1) == 1)
 | 
			
		||||
@@ -55,9 +70,13 @@ public class ScheduledAsyncTask : DisposeBase, IScheduledTask, IScheduledIntInte
 | 
			
		||||
        // 减少一个触发次数
 | 
			
		||||
        Interlocked.Decrement(ref _pendingTriggers);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await _taskFunc(state, _token).ConfigureAwait(false);
 | 
			
		||||
            if (_taskFunc != null)
 | 
			
		||||
                await _taskFunc(state, _token).ConfigureAwait(false);
 | 
			
		||||
            else if (_valueTaskFunc != null)
 | 
			
		||||
                await _valueTaskFunc(state, _token).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
        catch (OperationCanceledException)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,18 @@ public static class ScheduledTaskHelper
 | 
			
		||||
            return new CronScheduledTask(interval, func, state, log, cancellationToken);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    public static IScheduledTask GetTask(string interval, Func<object?, CancellationToken, ValueTask> func, object? state, TouchSocket.Core.ILog log, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        if (int.TryParse(interval, out int intervalV))
 | 
			
		||||
        {
 | 
			
		||||
            var intervalMilliseconds = intervalV < 10 ? 10 : intervalV;
 | 
			
		||||
            return new ScheduledAsyncTask(intervalMilliseconds, func, state, log, cancellationToken);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            return new CronScheduledTask(interval, func, state, log, cancellationToken);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    public static IScheduledTask GetTask(string interval, Action<object?, CancellationToken> action, object? state, TouchSocket.Core.ILog log, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        if (int.TryParse(interval, out int intervalV))
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
public static class ExportString
 | 
			
		||||
public static class GatewayExportString
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 通道名称
 | 
			
		||||
@@ -35,13 +35,19 @@ public static class ExportString
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static string VariableName => Localizer["VariableName"];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 变量报警表名称
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static string AlarmName => Localizer["AlarmName"];
 | 
			
		||||
 | 
			
		||||
    public static IStringLocalizer localizer;
 | 
			
		||||
    public static IStringLocalizer Localizer
 | 
			
		||||
    {
 | 
			
		||||
        get
 | 
			
		||||
        {
 | 
			
		||||
            if (localizer == null)
 | 
			
		||||
                localizer = App.CreateLocalizerByType(typeof(ExportString));
 | 
			
		||||
                localizer = App.CreateLocalizerByType(typeof(GatewayExportString));
 | 
			
		||||
            return localizer;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -15,14 +15,10 @@ using Microsoft.AspNetCore.Http;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
 | 
			
		||||
using System.ComponentModel;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.IO.Ports;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.FriendlyException;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Core;
 | 
			
		||||
using TouchSocket.Rpc;
 | 
			
		||||
using TouchSocket.Sockets;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
@@ -156,9 +152,9 @@ public class ControlController : ControllerBase, IRpcServer
 | 
			
		||||
    [HttpPost("batchSaveChannel")]
 | 
			
		||||
    [DisplayName("保存通道")]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public Task<bool> BatchSaveChannelAsync([FromBody][TouchSocket.WebApi.FromBody] List<ChannelInput> channels, ItemChangedType type, bool restart = true)
 | 
			
		||||
    public Task<bool> BatchSaveChannelAsync([FromBody][TouchSocket.WebApi.FromBody] List<Channel> channels, ItemChangedType type, bool restart = true)
 | 
			
		||||
    {
 | 
			
		||||
        return GlobalData.ChannelRuntimeService.BatchSaveChannelAsync(channels.AdaptListChannel(), type, restart);
 | 
			
		||||
        return GlobalData.ChannelRuntimeService.BatchSaveChannelAsync(channels, type, restart);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -167,9 +163,9 @@ public class ControlController : ControllerBase, IRpcServer
 | 
			
		||||
    [HttpPost("batchSaveDevice")]
 | 
			
		||||
    [DisplayName("保存设备")]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public Task<bool> BatchSaveDeviceAsync([FromBody][TouchSocket.WebApi.FromBody] List<DeviceInput> devices, ItemChangedType type, bool restart = true)
 | 
			
		||||
    public Task<bool> BatchSaveDeviceAsync([FromBody][TouchSocket.WebApi.FromBody] List<Device> devices, ItemChangedType type, bool restart = true)
 | 
			
		||||
    {
 | 
			
		||||
        return GlobalData.DeviceRuntimeService.BatchSaveDeviceAsync(devices.AdaptListDevice(), type, restart);
 | 
			
		||||
        return GlobalData.DeviceRuntimeService.BatchSaveDeviceAsync(devices, type, restart);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -178,9 +174,9 @@ public class ControlController : ControllerBase, IRpcServer
 | 
			
		||||
    [HttpPost("batchSaveVariable")]
 | 
			
		||||
    [DisplayName("保存变量")]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public Task<bool> BatchSaveVariableAsync([FromBody][TouchSocket.WebApi.FromBody] List<VariableInput> variables, ItemChangedType type, bool restart = true)
 | 
			
		||||
    public Task<bool> BatchSaveVariableAsync([FromBody][TouchSocket.WebApi.FromBody] List<Variable> variables, ItemChangedType type, bool restart = true)
 | 
			
		||||
    {
 | 
			
		||||
        return GlobalData.VariableRuntimeService.BatchSaveVariableAsync(variables.AdaptListVariable(), type, restart, default);
 | 
			
		||||
        return GlobalData.VariableRuntimeService.BatchSaveVariableAsync(variables, type, restart);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -192,7 +188,7 @@ public class ControlController : ControllerBase, IRpcServer
 | 
			
		||||
    public Task<bool> DeleteChannelAsync([FromBody][TouchSocket.WebApi.FromBody] List<long> ids, bool restart = true)
 | 
			
		||||
    {
 | 
			
		||||
        if (ids == null || ids.Count == 0) ids = GlobalData.IdChannels.Keys.ToList();
 | 
			
		||||
        return GlobalData.ChannelRuntimeService.DeleteChannelAsync(ids, restart, default);
 | 
			
		||||
        return GlobalData.ChannelRuntimeService.DeleteChannelAsync(ids, restart);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -204,7 +200,7 @@ public class ControlController : ControllerBase, IRpcServer
 | 
			
		||||
    public Task<bool> DeleteDeviceAsync([FromBody][TouchSocket.WebApi.FromBody] List<long> ids, bool restart = true)
 | 
			
		||||
    {
 | 
			
		||||
        if (ids == null || ids.Count == 0) ids = GlobalData.IdDevices.Keys.ToList();
 | 
			
		||||
        return GlobalData.DeviceRuntimeService.DeleteDeviceAsync(ids, restart, default);
 | 
			
		||||
        return GlobalData.DeviceRuntimeService.DeleteDeviceAsync(ids, restart);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -216,7 +212,7 @@ public class ControlController : ControllerBase, IRpcServer
 | 
			
		||||
    public Task<bool> DeleteVariableAsync([FromBody][TouchSocket.WebApi.FromBody] List<long> ids, bool restart = true)
 | 
			
		||||
    {
 | 
			
		||||
        if (ids == null || ids.Count == 0) ids = GlobalData.IdVariables.Keys.ToList();
 | 
			
		||||
        return GlobalData.VariableRuntimeService.DeleteVariableAsync(ids, restart, default);
 | 
			
		||||
        return GlobalData.VariableRuntimeService.DeleteVariableAsync(ids, restart);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -227,7 +223,7 @@ public class ControlController : ControllerBase, IRpcServer
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public Task InsertTestDataAsync(int testVariableCount, int testDeviceCount, string slaveUrl, bool businessEnable, bool restart = true)
 | 
			
		||||
    {
 | 
			
		||||
        return GlobalData.VariableRuntimeService.InsertTestDataAsync(testVariableCount, testDeviceCount, slaveUrl, businessEnable, restart, default);
 | 
			
		||||
        return GlobalData.VariableRuntimeService.InsertTestDataAsync(testVariableCount, testDeviceCount, slaveUrl, businessEnable, restart);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -247,552 +243,3 @@ public class ControlController : ControllerBase, IRpcServer
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
public class ChannelInput
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 主键Id
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual long Id { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 通道名称
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Required]
 | 
			
		||||
    public virtual string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public virtual ChannelTypeEnum ChannelType { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 插件名称
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Required]
 | 
			
		||||
    public virtual string PluginName { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual bool Enable { get; set; } = true;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// LogLevel
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public LogLevel LogLevel { get; set; } = LogLevel.Info;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 远程地址,可由<see cref="IPHost"/> 与 <see cref="string"/> 相互转化
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [UriValidation]
 | 
			
		||||
    public virtual string RemoteUrl { get; set; } = "127.0.0.1:502";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 本地地址,可由<see cref="IPHost.IPHost(string)"/>与<see href="IPHost.ToString()"/>相互转化
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [UriValidation]
 | 
			
		||||
    public virtual string BindUrl { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// COM
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual string PortName { get; set; } = "COM1";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 波特率
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual int BaudRate { get; set; } = 9600;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 数据位
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual int DataBits { get; set; } = 8;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 校验位
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual Parity Parity { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 停止位
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual StopBits StopBits { get; set; } = StopBits.One;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// DtrEnable
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual bool DtrEnable { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// RtsEnable
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual bool RtsEnable { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// StreamAsync
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual bool StreamAsync { get; set; } = false;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 缓存超时
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [MinValue(100)]
 | 
			
		||||
    public virtual int CacheTimeout { get; set; } = 500;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 连接超时
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [MinValue(100)]
 | 
			
		||||
    public virtual ushort ConnectTimeout { get; set; } = 3000;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 最大并发数
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [MinValue(1)]
 | 
			
		||||
    public virtual int MaxConcurrentCount { get; set; } = 1;
 | 
			
		||||
 | 
			
		||||
    public virtual int MaxClientCount { get; set; } = 10000;
 | 
			
		||||
    public virtual int CheckClearTime { get; set; } = 120000;
 | 
			
		||||
    public virtual string Heartbeat { get; set; } = "Heartbeat";
 | 
			
		||||
 | 
			
		||||
    #region dtu终端
 | 
			
		||||
 | 
			
		||||
    public virtual int HeartbeatTime { get; set; } = 60000;
 | 
			
		||||
    public virtual string DtuId { get; set; }
 | 
			
		||||
 | 
			
		||||
    #endregion
 | 
			
		||||
 | 
			
		||||
    public virtual DtuSeviceType DtuSeviceType { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class DeviceInput : IValidatableObject
 | 
			
		||||
{
 | 
			
		||||
    public long Id { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 名称
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Required]
 | 
			
		||||
    [RegularExpression(@"^[^.]*$", ErrorMessage = "The field {0} cannot contain a dot ('.')")]
 | 
			
		||||
    public virtual string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 描述
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? Description { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 通道
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [MinValue(1)]
 | 
			
		||||
    [Required]
 | 
			
		||||
    public virtual long ChannelId { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 默认执行间隔,支持corn表达式
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual string IntervalTime { get; set; } = "1000";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 设备使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual bool Enable { get; set; } = true;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// LogLevel
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual TouchSocket.Core.LogLevel LogLevel { get; set; } = TouchSocket.Core.LogLevel.Info;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 设备属性Json
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public Dictionary<string, string>? DevicePropertys { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
    #region 冗余配置
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 启用冗余
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool RedundantEnable { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 冗余设备Id,只能选择相同驱动
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public long? RedundantDeviceId { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 冗余模式
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual RedundantSwitchTypeEnum RedundantSwitchType { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 冗余扫描间隔
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [MinValue(30000)]
 | 
			
		||||
    public virtual int RedundantScanIntervalTime { get; set; } = 30000;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 冗余切换判断脚本,返回true则切换冗余设备
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual string RedundantScript { get; set; }
 | 
			
		||||
 | 
			
		||||
    #endregion 冗余配置
 | 
			
		||||
 | 
			
		||||
    #region 备用字段
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? Remark1 { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? Remark2 { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? Remark3 { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? Remark4 { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? Remark5 { get; set; }
 | 
			
		||||
 | 
			
		||||
    #endregion 备用字段
 | 
			
		||||
 | 
			
		||||
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
 | 
			
		||||
    {
 | 
			
		||||
        if (RedundantEnable && RedundantDeviceId == null)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("When enable redundancy, you must select a redundant device.", new[] { nameof(RedundantEnable), nameof(RedundantDeviceId) });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class VariableInput : IValidatableObject
 | 
			
		||||
{
 | 
			
		||||
    public virtual long Id { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 设备
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Required]
 | 
			
		||||
    public virtual long DeviceId { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 变量名称
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Required]
 | 
			
		||||
    public virtual string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 描述
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? Description { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 单位
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual string? Unit { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 间隔时间
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual string? IntervalTime { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 变量地址,可能带有额外的信息,比如<see cref="DataFormatEnum"/> ,以;分割
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? RegisterAddress { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 数组长度
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public int? ArrayLength { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 其他方法,若不为空,此时RegisterAddress为方法参数
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? OtherMethod { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual bool Enable { get; set; } = true;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 读写权限
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual ProtectTypeEnum ProtectType { get; set; } = ProtectTypeEnum.ReadWrite;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 数据类型
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual DataTypeEnum DataType { get; set; } = DataTypeEnum.Int16;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 读取表达式
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual string? ReadExpressions { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 写入表达式
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual string? WriteExpressions { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否允许远程Rpc写入
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual bool RpcWriteEnable { get; set; } = true;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 初始值
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public object? InitValue
 | 
			
		||||
    {
 | 
			
		||||
        get
 | 
			
		||||
        {
 | 
			
		||||
            return _value;
 | 
			
		||||
        }
 | 
			
		||||
        set
 | 
			
		||||
        {
 | 
			
		||||
            if (value != null)
 | 
			
		||||
                _value = value?.ToString()?.GetJTokenFromString();
 | 
			
		||||
            else
 | 
			
		||||
                _value = null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    private object? _value;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 保存初始值
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual bool SaveValue { get; set; } = false;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 变量额外属性Json
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public Dictionary<long, Dictionary<string, string>>? VariablePropertys { get; set; }
 | 
			
		||||
 | 
			
		||||
    #region 报警
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 报警延时
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public int AlarmDelay { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 布尔开报警使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool BoolOpenAlarmEnable { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 布尔开报警约束
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? BoolOpenRestrainExpressions { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 布尔开报警文本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? BoolOpenAlarmText { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 布尔关报警使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool BoolCloseAlarmEnable { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 布尔关报警约束
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? BoolCloseRestrainExpressions { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 布尔关报警文本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? BoolCloseAlarmText { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高报使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool HAlarmEnable { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高报约束
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? HRestrainExpressions { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高报文本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? HAlarmText { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高限值
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public double? HAlarmCode { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高高报使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool HHAlarmEnable { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高高报约束
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? HHRestrainExpressions { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高高报文本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? HHAlarmText { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高高限值
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public double? HHAlarmCode { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低报使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool LAlarmEnable { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低报约束
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? LRestrainExpressions { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低报文本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? LAlarmText { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低限值
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public double? LAlarmCode { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低低报使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool LLAlarmEnable { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低低报约束
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? LLRestrainExpressions { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低低报文本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? LLAlarmText { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低低限值
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public double? LLAlarmCode { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义报警使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool CustomAlarmEnable { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义报警条件约束
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? CustomRestrainExpressions { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义文本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? CustomAlarmText { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义报警条件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? CustomAlarmCode { get; set; }
 | 
			
		||||
 | 
			
		||||
    #endregion 报警
 | 
			
		||||
 | 
			
		||||
    #region 备用字段
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? Remark1 { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? Remark2 { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? Remark3 { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? Remark4 { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? Remark5 { get; set; }
 | 
			
		||||
 | 
			
		||||
    #endregion 备用字段
 | 
			
		||||
 | 
			
		||||
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrEmpty(RegisterAddress) && string.IsNullOrEmpty(OtherMethod))
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("Both RegisterAddress and OtherMethod cannot be empty or null.", new[] { nameof(RegisterAddress), nameof(OtherMethod) });
 | 
			
		||||
        }
 | 
			
		||||
        if (HHAlarmEnable && HHAlarmCode == null)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("HHAlarmCode cannot be null when HHAlarmEnable is true", new[] { nameof(HHAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
        if (HAlarmEnable && HAlarmCode == null)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("HAlarmCode cannot be null when HAlarmEnable is true", new[] { nameof(HAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
        if (LAlarmEnable && LAlarmCode == null)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("LAlarmCode cannot be null when LAlarmEnable is true", new[] { nameof(LAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
        if (LLAlarmEnable && LLAlarmCode == null)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("LLAlarmCode cannot be null when LLAlarmEnable is true", new[] { nameof(LLAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (HHAlarmEnable && HAlarmEnable && HHAlarmCode <= HAlarmCode)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("HHAlarmCode must be greater than HAlarmCode", new[] { nameof(HHAlarmCode), nameof(HAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
        if (HAlarmEnable && LAlarmEnable && HAlarmCode <= LAlarmCode)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("HAlarmCode must be greater than LAlarmCode", new[] { nameof(HAlarmCode), nameof(LAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
        if (LAlarmEnable && LLAlarmEnable && LAlarmCode <= LLAlarmCode)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("LAlarmCode must be greater than LLAlarmCode", new[] { nameof(LAlarmCode), nameof(LLAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (HHAlarmEnable && LAlarmEnable && HHAlarmCode <= LAlarmCode)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("HHAlarmCode should be greater than or less than LAlarmCode", new[] { nameof(HHAlarmCode), nameof(LAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
        if (HHAlarmEnable && LLAlarmEnable && HHAlarmCode <= LLAlarmCode)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("HHAlarmCode should be greater than or less than LLAlarmCode", new[] { nameof(HHAlarmCode), nameof(LLAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
        if (HAlarmEnable && LLAlarmEnable && HAlarmCode <= LLAlarmCode)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("HAlarmCode should be greater than or less than LLAlarmCode", new[] { nameof(HAlarmCode), nameof(LLAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -45,7 +45,7 @@ public class GatewayExportController : ControllerBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    [HttpPost("device")]
 | 
			
		||||
    public async Task<IActionResult> DownloadDeviceAsync([FromBody] ExportFilter input)
 | 
			
		||||
    public async Task<IActionResult> DownloadDeviceAsync([FromBody] GatewayExportFilter input)
 | 
			
		||||
    {
 | 
			
		||||
        input.QueryPageOptions.IsPage = false;
 | 
			
		||||
        input.QueryPageOptions.IsVirtualScroll = false;
 | 
			
		||||
@@ -58,7 +58,7 @@ public class GatewayExportController : ControllerBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    [HttpPost("channel")]
 | 
			
		||||
    public async Task<IActionResult> DownloadChannelAsync([FromBody] ExportFilter input)
 | 
			
		||||
    public async Task<IActionResult> DownloadChannelAsync([FromBody] GatewayExportFilter input)
 | 
			
		||||
    {
 | 
			
		||||
        input.QueryPageOptions.IsPage = false;
 | 
			
		||||
        input.QueryPageOptions.IsVirtualScroll = false;
 | 
			
		||||
@@ -72,7 +72,7 @@ public class GatewayExportController : ControllerBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    [HttpPost("variable")]
 | 
			
		||||
    public async Task<IActionResult> DownloadVariableAsync([FromBody] ExportFilter input)
 | 
			
		||||
    public async Task<IActionResult> DownloadVariableAsync([FromBody] GatewayExportFilter input)
 | 
			
		||||
    {
 | 
			
		||||
        input.QueryPageOptions.IsPage = false;
 | 
			
		||||
        input.QueryPageOptions.IsVirtualScroll = false;
 | 
			
		||||
 
 | 
			
		||||
@@ -90,7 +90,7 @@ public class RuntimeInfoController : ControllerBase, IRpcServer
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public async Task<SqlSugarPagedList<AlarmVariable>> GetRealAlarmList([FromQuery][TouchSocket.WebApi.FromBody] AlarmVariablePageInput input)
 | 
			
		||||
    {
 | 
			
		||||
        var realAlarmVariables = await GlobalData.GetCurrentUserRealAlarmVariables().ConfigureAwait(false);
 | 
			
		||||
        var realAlarmVariables = await GlobalData.GetCurrentUserRealAlarmVariablesAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        var data = realAlarmVariables
 | 
			
		||||
            .WhereIF(!input.RegisterAddress.IsNullOrEmpty(), a => a.RegisterAddress == input.RegisterAddress)
 | 
			
		||||
@@ -145,7 +145,7 @@ public class RuntimeInfoController : ControllerBase, IRpcServer
 | 
			
		||||
    public SqlSugarPagedList<PluginInfo> GetPluginInfos([FromQuery][TouchSocket.WebApi.FromBody] PluginInfoPageInput input)
 | 
			
		||||
    {
 | 
			
		||||
        //指定关键词搜索为插件FullName
 | 
			
		||||
        return (GlobalData.PluginService.GetPluginListSync()).WhereIF(!input.Name.IsNullOrWhiteSpace(), a => a.Name == input.Name)
 | 
			
		||||
        return (GlobalData.PluginService.GetPluginList()).WhereIF(!input.Name.IsNullOrWhiteSpace(), a => a.Name == input.Name)
 | 
			
		||||
                .ToPagedList(input);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,14 +11,19 @@
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
using TouchSocket.Rpc;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
[Route("api/[controller]/[action]")]
 | 
			
		||||
[AllowAnonymous]
 | 
			
		||||
[ApiController]
 | 
			
		||||
public class TestController : ControllerBase
 | 
			
		||||
[TouchSocket.WebApi.Router("/miniapi/[api]/[action]")]
 | 
			
		||||
[TouchSocket.WebApi.EnableCors("cors")]
 | 
			
		||||
public class TestController : ControllerBase, IRpcServer
 | 
			
		||||
{
 | 
			
		||||
    [HttpGet]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Get)]
 | 
			
		||||
    public void Test()
 | 
			
		||||
    {
 | 
			
		||||
        GC.Collect();
 | 
			
		||||
@@ -57,6 +57,9 @@ public abstract class BusinessBase : DriverBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    protected Dictionary<string, List<VariableRuntime>> VariableRuntimeGroups { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#if !Management
 | 
			
		||||
 | 
			
		||||
    public override Task AfterVariablesChangedAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        LogMessage?.LogInformation("Refresh variable");
 | 
			
		||||
@@ -117,6 +120,8 @@ public abstract class BusinessBase : DriverBase
 | 
			
		||||
            };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 间隔执行
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -134,4 +139,5 @@ public abstract class BusinessBase : DriverBase
 | 
			
		||||
            CurrentDevice?.SetDeviceStatus(TimerX.Now, true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,12 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class BusinessBaseWithCache : BusinessBase
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    protected sealed override BusinessPropertyBase _businessPropertyBase => _businessPropertyWithCache;
 | 
			
		||||
 | 
			
		||||
    protected abstract BusinessPropertyWithCache _businessPropertyWithCache { get; }
 | 
			
		||||
 | 
			
		||||
#if !Management
 | 
			
		||||
    #region 条件
 | 
			
		||||
 | 
			
		||||
    protected abstract bool AlarmModelEnable { get; }
 | 
			
		||||
@@ -537,9 +543,7 @@ public abstract class BusinessBaseWithCache : BusinessBase
 | 
			
		||||
    private CacheDB DBCacheVar;
 | 
			
		||||
    private CacheDB DBCacheVars;
 | 
			
		||||
 | 
			
		||||
    protected sealed override BusinessPropertyBase _businessPropertyBase => _businessPropertyWithCache;
 | 
			
		||||
 | 
			
		||||
    protected abstract BusinessPropertyWithCache _businessPropertyWithCache { get; }
 | 
			
		||||
 | 
			
		||||
    protected object cacheLock = new();
 | 
			
		||||
 | 
			
		||||
@@ -976,4 +980,6 @@ public abstract class BusinessBaseWithCache : BusinessBase
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #endregion
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,7 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class BusinessBaseWithCacheAlarm : BusinessBaseWithCache
 | 
			
		||||
{
 | 
			
		||||
#if !Management
 | 
			
		||||
    protected override bool AlarmModelEnable => true;
 | 
			
		||||
 | 
			
		||||
    protected override bool DevModelEnable => false;
 | 
			
		||||
@@ -104,4 +105,6 @@ public abstract class BusinessBaseWithCacheAlarm : BusinessBaseWithCache
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,9 @@ public abstract class BusinessBaseWithCacheInterval : BusinessBaseWithCache
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    protected abstract BusinessPropertyWithCacheInterval _businessPropertyWithCacheInterval { get; }
 | 
			
		||||
 | 
			
		||||
#if !Management
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    protected internal override async Task InitChannelAsync(IChannel? channel, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        if (AlarmModelEnable)
 | 
			
		||||
@@ -340,5 +343,5 @@ public abstract class BusinessBaseWithCacheInterval : BusinessBaseWithCache
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ public abstract partial class BusinessBaseWithCacheIntervalScript : BusinessBase
 | 
			
		||||
 | 
			
		||||
    protected abstract BusinessPropertyWithCacheIntervalScript _businessPropertyWithCacheIntervalScript { get; }
 | 
			
		||||
 | 
			
		||||
#if !Management
 | 
			
		||||
 | 
			
		||||
    public virtual string[] Match(string input)
 | 
			
		||||
    {
 | 
			
		||||
        // 生成缓存键,以确保缓存的唯一性
 | 
			
		||||
@@ -347,4 +349,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript : BusinessBase
 | 
			
		||||
    [GeneratedRegex(@"\$\{(.+?)\}")]
 | 
			
		||||
    private static partial Regex TopicRegex();
 | 
			
		||||
    #endregion 封装方法
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,9 +15,12 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract partial class BusinessBaseWithCacheIntervalScriptAll : BusinessBaseWithCacheIntervalScript
 | 
			
		||||
{
 | 
			
		||||
#if !Management
 | 
			
		||||
    protected override bool AlarmModelEnable => true;
 | 
			
		||||
 | 
			
		||||
    protected override bool DevModelEnable => true;
 | 
			
		||||
 | 
			
		||||
    protected override bool VarModelEnable => true;
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class BusinessBaseWithCacheIntervalVariable : BusinessBaseWithCacheInterval
 | 
			
		||||
{
 | 
			
		||||
#if !Management
 | 
			
		||||
    protected override bool AlarmModelEnable => false;
 | 
			
		||||
 | 
			
		||||
    protected override bool DevModelEnable => false;
 | 
			
		||||
@@ -29,5 +30,5 @@ public abstract class BusinessBaseWithCacheIntervalVariable : BusinessBaseWithCa
 | 
			
		||||
    {
 | 
			
		||||
        throw new NotImplementedException();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
/// </summary>
 | 
			
		||||
public static class BusinessDatabaseUtil
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取数据库链接
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -45,6 +46,7 @@ public static class BusinessDatabaseUtil
 | 
			
		||||
        return sqlSugarClient;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#if !Management
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 按条件获取DB插件中的全部历史报警(不分页)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -144,4 +146,6 @@ public static class BusinessDatabaseUtil
 | 
			
		||||
            return new("GetDBHistoryValues Fail", ex);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,8 @@ public interface IDBHistoryAlarm
 | 
			
		||||
    string? Description { get; set; }
 | 
			
		||||
    string DeviceName { get; set; }
 | 
			
		||||
    DateTime EventTime { get; set; }
 | 
			
		||||
    DateTime FinishTime { get; set; }
 | 
			
		||||
    DateTime ConfirmTime { get; set; }
 | 
			
		||||
    EventTypeEnum EventType { get; set; }
 | 
			
		||||
    string Name { get; set; }
 | 
			
		||||
    string RegisterAddress { get; set; }
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,9 @@ using System.Collections.Concurrent;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Common.Extension;
 | 
			
		||||
using ThingsGateway.Extension.Generic;
 | 
			
		||||
#if !Management
 | 
			
		||||
using ThingsGateway.Gateway.Application.Extensions;
 | 
			
		||||
#endif
 | 
			
		||||
using ThingsGateway.NewLife.Json.Extension;
 | 
			
		||||
using ThingsGateway.NewLife.Threading;
 | 
			
		||||
 | 
			
		||||
@@ -29,20 +31,31 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
/// 采集插件,继承实现不同PLC通讯
 | 
			
		||||
/// <para></para>
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class CollectBase : DriverBase, IRpcDriver
 | 
			
		||||
public abstract partial class CollectBase : DriverBase
 | 
			
		||||
#if !Management
 | 
			
		||||
    , IRpcDriver
 | 
			
		||||
#endif
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 插件配置项
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public abstract CollectPropertyBase CollectProperties { get; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public sealed override object DriverProperties => CollectProperties;
 | 
			
		||||
 | 
			
		||||
    public virtual string GetAddressDescription()
 | 
			
		||||
    {
 | 
			
		||||
        return string.Empty;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#if !Management
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 特殊方法
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public List<DriverMethodInfo>? DriverMethodInfos { get; private set; }
 | 
			
		||||
 | 
			
		||||
    public sealed override object DriverProperties => CollectProperties;
 | 
			
		||||
 | 
			
		||||
    public override async Task AfterVariablesChangedAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        LogMessage?.LogInformation("Refresh variable");
 | 
			
		||||
@@ -195,13 +208,10 @@ public abstract class CollectBase : DriverBase, IRpcDriver
 | 
			
		||||
        // 从插件服务中获取当前设备关联的驱动方法信息列表
 | 
			
		||||
        DriverMethodInfos = GlobalData.PluginService.GetDriverMethodInfos(device.PluginName, this);
 | 
			
		||||
 | 
			
		||||
        ReadWriteLock = new(CollectProperties.DutyCycle);
 | 
			
		||||
        ReadWriteLock = new(CollectProperties.DutyCycle, CollectProperties.WritePriority);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public virtual string GetAddressDescription()
 | 
			
		||||
    {
 | 
			
		||||
        return string.Empty;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected virtual bool VariableSourceReadsEnable => true;
 | 
			
		||||
 | 
			
		||||
    protected List<IScheduledTask> VariableTasks = new List<IScheduledTask>();
 | 
			
		||||
@@ -278,11 +288,9 @@ public abstract class CollectBase : DriverBase, IRpcDriver
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #region private
 | 
			
		||||
 | 
			
		||||
    #region 执行方法
 | 
			
		||||
 | 
			
		||||
    async Task ReadVariableMed(object? state, CancellationToken cancellationToken)
 | 
			
		||||
    async ValueTask ReadVariableMed(object? state, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        if (state is not VariableMethod readVariableMethods) return;
 | 
			
		||||
        if (Pause)
 | 
			
		||||
@@ -348,25 +356,25 @@ public abstract class CollectBase : DriverBase, IRpcDriver
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #endregion
 | 
			
		||||
    private readonly LinkedCancellationTokenSourceCache _linkedCtsCache = new();
 | 
			
		||||
 | 
			
		||||
    #region 执行默认读取
 | 
			
		||||
 | 
			
		||||
    async Task ReadVariableSource(object? state, CancellationToken cancellationToken)
 | 
			
		||||
    async ValueTask ReadVariableSource(object? state, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        var readToken = await ReadWriteLock.ReaderLockAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        if (state is not VariableSourceRead variableSourceRead) return;
 | 
			
		||||
 | 
			
		||||
        if (Pause) return;
 | 
			
		||||
        if (cancellationToken.IsCancellationRequested) return;
 | 
			
		||||
 | 
			
		||||
        var readToken = await ReadWriteLock.ReaderLockAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        if (readToken.IsCancellationRequested)
 | 
			
		||||
        {
 | 
			
		||||
            await ReadVariableSource(state, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        using var allTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, readToken);
 | 
			
		||||
        var allTokenSource = _linkedCtsCache.GetLinkedTokenSource(cancellationToken, readToken);
 | 
			
		||||
        var allToken = allTokenSource.Token;
 | 
			
		||||
 | 
			
		||||
        //if (LogMessage?.LogLevel <= TouchSocket.Core.LogLevel.Trace)
 | 
			
		||||
@@ -434,13 +442,15 @@ public abstract class CollectBase : DriverBase, IRpcDriver
 | 
			
		||||
            variableSourceRead.LastErrorMessage = readResult.ErrorMessage;
 | 
			
		||||
            CurrentDevice.SetDeviceStatus(TimerX.Now, null, readResult.ErrorMessage);
 | 
			
		||||
            var time = DateTime.Now;
 | 
			
		||||
            variableSourceRead.VariableRuntimes.ForEach(a => a.SetValue(null, time, isOnline: false));
 | 
			
		||||
            foreach (var item in variableSourceRead.VariableRuntimes)
 | 
			
		||||
            {
 | 
			
		||||
                item.SetValue(null, time, isOnline: false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #endregion
 | 
			
		||||
 | 
			
		||||
    #endregion
 | 
			
		||||
 | 
			
		||||
    protected virtual Task TestOnline(object? state, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
@@ -468,6 +478,7 @@ public abstract class CollectBase : DriverBase, IRpcDriver
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 连读打包,返回实际通讯包信息<see cref="VariableSourceRead"/>
 | 
			
		||||
    /// <br></br>每个驱动打包方法不一样,所以需要实现这个接口
 | 
			
		||||
@@ -722,4 +733,14 @@ public abstract class CollectBase : DriverBase, IRpcDriver
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    #endregion 写入方法
 | 
			
		||||
 | 
			
		||||
    protected override async Task DisposeAsync(bool disposing)
 | 
			
		||||
    {
 | 
			
		||||
        _linkedCtsCache?.SafeDispose();
 | 
			
		||||
        if (ReadWriteLock != null)
 | 
			
		||||
            await ReadWriteLock.SafeDisposeAsync().ConfigureAwait(false);
 | 
			
		||||
        await base.DisposeAsync(disposing).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -33,13 +33,7 @@ public abstract class CollectFoundationBase : CollectBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual IDevice? FoundationDevice { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否连接成功
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public override bool IsConnected()
 | 
			
		||||
    {
 | 
			
		||||
        return FoundationDevice?.OnLine == true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public override string ToString()
 | 
			
		||||
    {
 | 
			
		||||
@@ -53,6 +47,24 @@ public abstract class CollectFoundationBase : CollectBase
 | 
			
		||||
            await FoundationDevice.SafeDisposeAsync().ConfigureAwait(false);
 | 
			
		||||
        await base.DisposeAsync(disposing).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public override string GetAddressDescription()
 | 
			
		||||
    {
 | 
			
		||||
        return FoundationDevice?.GetAddressDescription();
 | 
			
		||||
    }
 | 
			
		||||
#if !Management
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否连接成功
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public override bool IsConnected()
 | 
			
		||||
    {
 | 
			
		||||
        return FoundationDevice?.OnLine == true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 开始通讯执行的方法
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -66,10 +78,6 @@ public abstract class CollectFoundationBase : CollectBase
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override string GetAddressDescription()
 | 
			
		||||
    {
 | 
			
		||||
        return FoundationDevice?.GetAddressDescription();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override async Task TestOnline(object? state, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
@@ -216,4 +224,7 @@ public abstract class CollectFoundationBase : CollectBase
 | 
			
		||||
        // 返回包含操作结果的字典
 | 
			
		||||
        return new Dictionary<string, OperResult>(operResults);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -37,6 +37,12 @@ public abstract class CollectPropertyBase : DriverPropertyBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [MinValue(1)]
 | 
			
		||||
    public virtual int DutyCycle { get; set; } = 3;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 写优先,写入时强制读取消操作
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual bool WritePriority { get; set; } = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -56,4 +62,6 @@ public abstract class CollectPropertyRetryBase : CollectPropertyBase
 | 
			
		||||
    [MinValue(1)]
 | 
			
		||||
    public override int DutyCycle { get; set; } = 3;
 | 
			
		||||
 | 
			
		||||
    [DynamicProperty(Remark = "写优先,写入时强制读取消操作")]
 | 
			
		||||
    public override bool WritePriority { get; set; } = false;
 | 
			
		||||
}
 | 
			
		||||
@@ -37,20 +37,6 @@ public abstract class DriverBase : AsyncDisposableObject, IDriver
 | 
			
		||||
 | 
			
		||||
    #region 属性
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当前设备
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public DeviceRuntime? CurrentDevice { get; private set; }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当前设备Id
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public long DeviceId => CurrentDevice?.Id ?? 0;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当前设备名称
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? DeviceName => CurrentDevice?.Name;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 调试UI Type,如果不存在,返回null
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -76,25 +62,8 @@ public abstract class DriverBase : AsyncDisposableObject, IDriver
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public abstract object DriverProperties { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否执行了Start方法
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool IsStarted { get; protected set; } = false;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否初始化成功,失败时不再执行,等待检测重启
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool IsInitSuccess { get; internal set; } = true;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否采集插件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual bool? IsCollectDevice => CurrentDevice?.IsCollect;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 暂停
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool Pause => CurrentDevice?.Pause == true;
 | 
			
		||||
 | 
			
		||||
    private List<IEditorItem> pluginPropertyEditorItems;
 | 
			
		||||
    public List<IEditorItem> PluginPropertyEditorItems
 | 
			
		||||
@@ -112,6 +81,82 @@ public abstract class DriverBase : AsyncDisposableObject, IDriver
 | 
			
		||||
    private IStringLocalizer Localizer { get; }
 | 
			
		||||
 | 
			
		||||
    #endregion 属性
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public virtual bool GetAuthentication(out DateTime? expireTime)
 | 
			
		||||
    {
 | 
			
		||||
        expireTime = null;
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public string GetAuthString()
 | 
			
		||||
    {
 | 
			
		||||
        if (PluginServiceUtil.IsEducation(GetType()))
 | 
			
		||||
        {
 | 
			
		||||
            StringBuilder stringBuilder = new();
 | 
			
		||||
            var ret = GetAuthentication(out var expireTime);
 | 
			
		||||
            if (ret)
 | 
			
		||||
            {
 | 
			
		||||
                stringBuilder.Append(Localizer["Authorized"]);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                stringBuilder.Append(Localizer["Unauthorized"]);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            stringBuilder.Append("   ");
 | 
			
		||||
            if (expireTime.HasValue && (DateTime.Now - expireTime.Value).TotalHours > -72)
 | 
			
		||||
            {
 | 
			
		||||
                stringBuilder.Append(',');
 | 
			
		||||
                stringBuilder.Append(Localizer["ExpireTime", expireTime.Value.ToString("yyyy-MM-dd HH")]);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return stringBuilder.ToString();
 | 
			
		||||
        }
 | 
			
		||||
        return string.Empty;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#if !Management
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否执行了Start方法
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool IsStarted { get; protected set; } = false;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否初始化成功,失败时不再执行,等待检测重启
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool IsInitSuccess { get; internal set; } = true;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否采集插件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual bool? IsCollectDevice => CurrentDevice?.IsCollect;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当前设备
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public DeviceRuntime? CurrentDevice { get; private set; }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当前设备Id
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public long DeviceId => CurrentDevice?.Id ?? 0;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当前设备名称
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? DeviceName => CurrentDevice?.Name;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 暂停
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool Pause => CurrentDevice?.Pause == true;
 | 
			
		||||
 | 
			
		||||
    protected object pauseLock = new object();
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 暂停
 | 
			
		||||
@@ -378,38 +423,6 @@ public abstract class DriverBase : AsyncDisposableObject, IDriver
 | 
			
		||||
 | 
			
		||||
    #region 插件重写
 | 
			
		||||
 | 
			
		||||
    public virtual bool GetAuthentication(out DateTime? expireTime)
 | 
			
		||||
    {
 | 
			
		||||
        expireTime = null;
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public string GetAuthString()
 | 
			
		||||
    {
 | 
			
		||||
        if (PluginServiceUtil.IsEducation(GetType()))
 | 
			
		||||
        {
 | 
			
		||||
            StringBuilder stringBuilder = new();
 | 
			
		||||
            var ret = GetAuthentication(out var expireTime);
 | 
			
		||||
            if (ret)
 | 
			
		||||
            {
 | 
			
		||||
                stringBuilder.Append(Localizer["Authorized"]);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                stringBuilder.Append(Localizer["Unauthorized"]);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            stringBuilder.Append("   ");
 | 
			
		||||
            if (expireTime.HasValue && (DateTime.Now - expireTime.Value).TotalHours > -72)
 | 
			
		||||
            {
 | 
			
		||||
                stringBuilder.Append(',');
 | 
			
		||||
                stringBuilder.Append(Localizer["ExpireTime", expireTime.Value.ToString("yyyy-MM-dd HH")]);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return stringBuilder.ToString();
 | 
			
		||||
        }
 | 
			
		||||
        return string.Empty;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 开始通讯执行的方法
 | 
			
		||||
@@ -456,4 +469,7 @@ public abstract class DriverBase : AsyncDisposableObject, IDriver
 | 
			
		||||
    public abstract Task AfterVariablesChangedAsync(CancellationToken cancellationToken);
 | 
			
		||||
 | 
			
		||||
    #endregion 插件重写
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -37,6 +37,7 @@ public static class DynamicModelExtension
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#if !Management
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取变量的业务属性值
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -79,6 +80,8 @@ public static class DynamicModelExtension
 | 
			
		||||
        return null; // 未找到对应的业务设备Id,返回null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    public static IEnumerable<IGrouping<object[], T>> GroupByKeys<T>(this IEnumerable<T> values, params string[] keys)
 | 
			
		||||
    {
 | 
			
		||||
        // 获取动态对象集合中指定键的属性信息
 | 
			
		||||
 
 | 
			
		||||
@@ -16,25 +16,31 @@ namespace ThingsGateway.Gateway.Application
 | 
			
		||||
{
 | 
			
		||||
    public interface IDriver : IAsyncDisposable
 | 
			
		||||
    {
 | 
			
		||||
        bool DisposedValue { get; }
 | 
			
		||||
        ChannelRuntime CurrentChannel { get; }
 | 
			
		||||
        DeviceRuntime? CurrentDevice { get; }
 | 
			
		||||
        long DeviceId { get; }
 | 
			
		||||
        string? DeviceName { get; }
 | 
			
		||||
 | 
			
		||||
        Type DriverDebugUIType { get; }
 | 
			
		||||
        object DriverProperties { get; }
 | 
			
		||||
 | 
			
		||||
        Type DriverPropertyUIType { get; }
 | 
			
		||||
        Type DriverUIType { get; }
 | 
			
		||||
        Type DriverVariableAddressUIType { get; }
 | 
			
		||||
        List<IEditorItem> PluginPropertyEditorItems { get; }
 | 
			
		||||
 | 
			
		||||
#if !Management
 | 
			
		||||
 | 
			
		||||
        bool? IsCollectDevice { get; }
 | 
			
		||||
        bool DisposedValue { get; }
 | 
			
		||||
        ChannelRuntime CurrentChannel { get; }
 | 
			
		||||
        DeviceRuntime? CurrentDevice { get; }
 | 
			
		||||
        long DeviceId { get; }
 | 
			
		||||
        string? DeviceName { get; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        bool IsInitSuccess { get; }
 | 
			
		||||
        bool IsStarted { get; }
 | 
			
		||||
        bool Pause { get; }
 | 
			
		||||
        LoggerGroup LogMessage { get; }
 | 
			
		||||
        string LogPath { get; }
 | 
			
		||||
        bool Pause { get; }
 | 
			
		||||
        string PluginDirectory { get; }
 | 
			
		||||
        List<IEditorItem> PluginPropertyEditorItems { get; }
 | 
			
		||||
        Dictionary<long, VariableRuntime> IdVariableRuntimes { get; }
 | 
			
		||||
        IDeviceThreadManage DeviceThreadManage { get; }
 | 
			
		||||
 | 
			
		||||
@@ -42,6 +48,10 @@ namespace ThingsGateway.Gateway.Application
 | 
			
		||||
        void PauseThread(bool pause);
 | 
			
		||||
        Task SetLogAsync(LogLevel? logLevel = null, bool upDataBase = true);
 | 
			
		||||
        Task AfterVariablesChangedAsync(CancellationToken cancellationToken);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
        string GetAuthString();
 | 
			
		||||
        bool GetAuthentication(out DateTime? expireTime);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,284 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using BootstrapBlazor.Components;
 | 
			
		||||
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
public class AlarmPropertys : IValidatableObject
 | 
			
		||||
{
 | 
			
		||||
    private int alarmDelay;
 | 
			
		||||
    private int alarmLevel;
 | 
			
		||||
 | 
			
		||||
    private decimal hAlarmCode = 50;
 | 
			
		||||
    private decimal lAlarmCode = 10;
 | 
			
		||||
    private decimal hHAlarmCode = 90;
 | 
			
		||||
    private decimal lLAlarmCode = 0;
 | 
			
		||||
 | 
			
		||||
    private bool boolOpenAlarmEnable;
 | 
			
		||||
    private bool boolCloseAlarmEnable;
 | 
			
		||||
    private bool hAlarmEnable;
 | 
			
		||||
    private bool hHAlarmEnable;
 | 
			
		||||
    private bool lLAlarmEnable;
 | 
			
		||||
    private bool lAlarmEnable;
 | 
			
		||||
    private bool customAlarmEnable;
 | 
			
		||||
 | 
			
		||||
    private string boolOpenRestrainExpressions;
 | 
			
		||||
    private string boolOpenAlarmText;
 | 
			
		||||
    private string boolCloseRestrainExpressions;
 | 
			
		||||
    private string boolCloseAlarmText;
 | 
			
		||||
    private string hRestrainExpressions;
 | 
			
		||||
    private string hAlarmText;
 | 
			
		||||
    private string hHRestrainExpressions;
 | 
			
		||||
    private string hHAlarmText;
 | 
			
		||||
    private string lRestrainExpressions;
 | 
			
		||||
    private string lAlarmText;
 | 
			
		||||
 | 
			
		||||
    private string lLRestrainExpressions;
 | 
			
		||||
    private string lLAlarmText;
 | 
			
		||||
    private string customRestrainExpressions;
 | 
			
		||||
    private string customAlarmText;
 | 
			
		||||
    private string customAlarmCode;
 | 
			
		||||
 | 
			
		||||
    #region 报警
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 报警等级
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "报警等级")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public int AlarmLevel { get => alarmLevel; set => alarmLevel = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 报警延时
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "报警延时")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public int AlarmDelay { get => alarmDelay; set => alarmDelay = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 布尔开报警使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "布尔开报警使能")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public bool BoolOpenAlarmEnable { get => boolOpenAlarmEnable; set => boolOpenAlarmEnable = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 布尔开报警约束
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "布尔开报警约束", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string BoolOpenRestrainExpressions { get => boolOpenRestrainExpressions; set => boolOpenRestrainExpressions = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 布尔开报警文本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "布尔开报警文本", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string BoolOpenAlarmText { get => boolOpenAlarmText; set => boolOpenAlarmText = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 布尔关报警使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "布尔关报警使能")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public bool BoolCloseAlarmEnable { get => boolCloseAlarmEnable; set => boolCloseAlarmEnable = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 布尔关报警约束
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "布尔关报警约束", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string BoolCloseRestrainExpressions { get => boolCloseRestrainExpressions; set => boolCloseRestrainExpressions = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 布尔关报警文本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "布尔关报警文本", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string BoolCloseAlarmText { get => boolCloseAlarmText; set => boolCloseAlarmText = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高报使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "高报使能")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public bool HAlarmEnable { get => hAlarmEnable; set => hAlarmEnable = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高报约束
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "高报约束", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string HRestrainExpressions { get => hRestrainExpressions; set => hRestrainExpressions = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高报文本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "高报文本", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string HAlarmText { get => hAlarmText; set => hAlarmText = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高限值
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "高限值", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public decimal HAlarmCode { get => hAlarmCode; set => hAlarmCode = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高高报使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "高高报使能")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public bool HHAlarmEnable { get => hHAlarmEnable; set => hHAlarmEnable = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高高报约束
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "高高报约束", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string HHRestrainExpressions { get => hHRestrainExpressions; set => hHRestrainExpressions = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高高报文本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "高高报文本", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string HHAlarmText { get => hHAlarmText; set => hHAlarmText = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高高限值
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "高高限值", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public decimal HHAlarmCode { get => hHAlarmCode; set => hHAlarmCode = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低报使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "低报使能")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public bool LAlarmEnable { get => lAlarmEnable; set => lAlarmEnable = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低报约束
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "低报约束", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string LRestrainExpressions { get => lRestrainExpressions; set => lRestrainExpressions = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低报文本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "低报文本", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string LAlarmText { get => lAlarmText; set => lAlarmText = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低限值
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "低限值", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public decimal LAlarmCode { get => lAlarmCode; set => lAlarmCode = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低低报使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "低低报使能")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public bool LLAlarmEnable { get => lLAlarmEnable; set => lLAlarmEnable = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低低报约束
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "低低报约束", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string LLRestrainExpressions { get => lLRestrainExpressions; set => lLRestrainExpressions = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低低报文本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "低低报文本", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string LLAlarmText { get => lLAlarmText; set => lLAlarmText = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低低限值
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "低低限值", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public decimal LLAlarmCode { get => lLAlarmCode; set => lLAlarmCode = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义报警使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "自定义报警使能")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public bool CustomAlarmEnable { get => customAlarmEnable; set => customAlarmEnable = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义报警条件约束
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "自定义报警条件约束", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string CustomRestrainExpressions { get => customRestrainExpressions; set => customRestrainExpressions = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义文本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "自定义文本", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string CustomAlarmText { get => customAlarmText; set => customAlarmText = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义报警条件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "自定义报警条件", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string CustomAlarmCode { get => customAlarmCode; set => customAlarmCode = value; }
 | 
			
		||||
 | 
			
		||||
    #endregion 报警
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        if (HHAlarmEnable && HAlarmEnable && HHAlarmCode <= HAlarmCode)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("HHAlarmCode must be greater than HAlarmCode", new[] { nameof(HHAlarmCode), nameof(HAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
        if (HAlarmEnable && LAlarmEnable && HAlarmCode <= LAlarmCode)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("HAlarmCode must be greater than LAlarmCode", new[] { nameof(HAlarmCode), nameof(LAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
        if (LAlarmEnable && LLAlarmEnable && LAlarmCode <= LLAlarmCode)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("LAlarmCode must be greater than LLAlarmCode", new[] { nameof(LAlarmCode), nameof(LLAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (HHAlarmEnable && LAlarmEnable && HHAlarmCode <= LAlarmCode)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("HHAlarmCode should be greater than or less than LAlarmCode", new[] { nameof(HHAlarmCode), nameof(LAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
        if (HHAlarmEnable && LLAlarmEnable && HHAlarmCode <= LLAlarmCode)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("HHAlarmCode should be greater than or less than LLAlarmCode", new[] { nameof(HHAlarmCode), nameof(LLAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
        if (HAlarmEnable && LLAlarmEnable && HAlarmCode <= LLAlarmCode)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("HAlarmCode should be greater than or less than LLAlarmCode", new[] { nameof(HAlarmCode), nameof(LLAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -17,8 +17,10 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 后台日志表
 | 
			
		||||
///</summary>
 | 
			
		||||
#if !Management
 | 
			
		||||
[SugarTable("backend_log", TableDescription = "后台日志表")]
 | 
			
		||||
[Tenant(SqlSugarConst.DB_Log)]
 | 
			
		||||
#endif
 | 
			
		||||
public class BackendLog : PrimaryIdEntity
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -17,14 +17,15 @@ using TouchSocket.Core;
 | 
			
		||||
using TouchSocket.Sockets;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
#pragma warning disable CS0649
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 通道表
 | 
			
		||||
/// </summary>
 | 
			
		||||
#if !Management
 | 
			
		||||
[SugarTable("channel", TableDescription = "通道表")]
 | 
			
		||||
[Tenant(SqlSugarConst.DB_Custom)]
 | 
			
		||||
[SugarIndex("unique_channel_name", nameof(Channel.Name), OrderByType.Asc, true)]
 | 
			
		||||
#endif
 | 
			
		||||
public class Channel : ChannelOptionsBase, IPrimaryIdEntity, IBaseDataEntity, IBaseEntity
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -17,14 +17,15 @@ using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using ThingsGateway.NewLife.Extension;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
#pragma warning disable CS0649
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 设备表
 | 
			
		||||
/// </summary>
 | 
			
		||||
#if !Management
 | 
			
		||||
[SugarTable("device", TableDescription = "设备表")]
 | 
			
		||||
[Tenant(SqlSugarConst.DB_Custom)]
 | 
			
		||||
[SugarIndex("unique_device_name", nameof(Device.Name), OrderByType.Asc, true)]
 | 
			
		||||
#endif
 | 
			
		||||
public class Device : BaseDataEntity, IValidatableObject
 | 
			
		||||
{
 | 
			
		||||
    public override string ToString()
 | 
			
		||||
@@ -170,6 +171,7 @@ public class Device : BaseDataEntity, IValidatableObject
 | 
			
		||||
 | 
			
		||||
    #endregion 备用字段
 | 
			
		||||
 | 
			
		||||
#if !Management
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 导入验证专用
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -177,6 +179,9 @@ public class Device : BaseDataEntity, IValidatableObject
 | 
			
		||||
    [Newtonsoft.Json.JsonIgnore]
 | 
			
		||||
    internal bool IsUp;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 额外属性
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -184,6 +189,7 @@ public class Device : BaseDataEntity, IValidatableObject
 | 
			
		||||
    [Newtonsoft.Json.JsonIgnore]
 | 
			
		||||
    [MapperIgnore]
 | 
			
		||||
    public ModelValueValidateForm? ModelValueValidateForm;
 | 
			
		||||
 | 
			
		||||
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
 | 
			
		||||
    {
 | 
			
		||||
        if (RedundantEnable && RedundantDeviceId == null)
 | 
			
		||||
 
 | 
			
		||||
@@ -15,8 +15,10 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Rpc写入日志
 | 
			
		||||
///</summary>
 | 
			
		||||
#if !Management
 | 
			
		||||
[SugarTable("rpc_log", TableDescription = "RPC操作日志")]
 | 
			
		||||
[Tenant(SqlSugarConst.DB_Log)]
 | 
			
		||||
#endif
 | 
			
		||||
public class RpcLog : PrimaryIdEntity
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -16,15 +16,16 @@ using System.Collections.Concurrent;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
#pragma warning disable CS0649
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 设备变量表
 | 
			
		||||
/// </summary>
 | 
			
		||||
#if !Management
 | 
			
		||||
[SugarTable("variable", TableDescription = "设备变量表")]
 | 
			
		||||
[Tenant(SqlSugarConst.DB_Custom)]
 | 
			
		||||
[SugarIndex("index_device", nameof(Variable.DeviceId), OrderByType.Asc)]
 | 
			
		||||
[SugarIndex("unique_deviceid_variable_name", nameof(Variable.Name), OrderByType.Asc, nameof(Variable.DeviceId), OrderByType.Asc, true)]
 | 
			
		||||
#endif
 | 
			
		||||
public class Variable : BaseDataEntity, IValidatableObject
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -41,13 +42,9 @@ public class Variable : BaseDataEntity, IValidatableObject
 | 
			
		||||
    [System.Text.Json.Serialization.JsonIgnore]
 | 
			
		||||
    [Newtonsoft.Json.JsonIgnore]
 | 
			
		||||
    internal long Row;
 | 
			
		||||
    private double hAlarmCode = 50;
 | 
			
		||||
    private double lAlarmCode = 10;
 | 
			
		||||
    private double hHAlarmCode = 90;
 | 
			
		||||
    private double lLAlarmCode = 0;
 | 
			
		||||
    private long deviceId;
 | 
			
		||||
    private int? arrayLength;
 | 
			
		||||
    private int alarmDelay;
 | 
			
		||||
 | 
			
		||||
    private ProtectTypeEnum protectType = ProtectTypeEnum.ReadWrite;
 | 
			
		||||
    private DataTypeEnum dataType = DataTypeEnum.Int16;
 | 
			
		||||
 | 
			
		||||
@@ -61,13 +58,6 @@ public class Variable : BaseDataEntity, IValidatableObject
 | 
			
		||||
    public bool DynamicVariable;
 | 
			
		||||
    private bool rpcWriteEnable = true;
 | 
			
		||||
    private bool saveValue = false;
 | 
			
		||||
    private bool boolOpenAlarmEnable;
 | 
			
		||||
    private bool boolCloseAlarmEnable;
 | 
			
		||||
    private bool hAlarmEnable;
 | 
			
		||||
    private bool hHAlarmEnable;
 | 
			
		||||
    private bool lLAlarmEnable;
 | 
			
		||||
    private bool lAlarmEnable;
 | 
			
		||||
    private bool customAlarmEnable;
 | 
			
		||||
    private bool businessGroupUpdateTrigger = true;
 | 
			
		||||
    private bool rpcWriteCheck;
 | 
			
		||||
 | 
			
		||||
@@ -82,29 +72,19 @@ public class Variable : BaseDataEntity, IValidatableObject
 | 
			
		||||
    private string otherMethod;
 | 
			
		||||
    private string readExpressions;
 | 
			
		||||
    private string writeExpressions;
 | 
			
		||||
    private string boolOpenRestrainExpressions;
 | 
			
		||||
    private string boolOpenAlarmText;
 | 
			
		||||
    private string boolCloseRestrainExpressions;
 | 
			
		||||
    private string boolCloseAlarmText;
 | 
			
		||||
    private string hRestrainExpressions;
 | 
			
		||||
    private string hAlarmText;
 | 
			
		||||
    private Dictionary<long, Dictionary<string, string>>? variablePropertys;
 | 
			
		||||
    private string hHRestrainExpressions;
 | 
			
		||||
    private string hHAlarmText;
 | 
			
		||||
    private string lRestrainExpressions;
 | 
			
		||||
    private string lAlarmText;
 | 
			
		||||
 | 
			
		||||
    private string lLRestrainExpressions;
 | 
			
		||||
    private string lLAlarmText;
 | 
			
		||||
    private string customRestrainExpressions;
 | 
			
		||||
    private string customAlarmText;
 | 
			
		||||
    private string customAlarmCode;
 | 
			
		||||
    private Dictionary<long, Dictionary<string, string>>? variablePropertys;
 | 
			
		||||
    private string remark1;
 | 
			
		||||
    private string remark2;
 | 
			
		||||
    private string remark3;
 | 
			
		||||
    private string remark4;
 | 
			
		||||
    private string remark5;
 | 
			
		||||
 | 
			
		||||
    [System.Text.Json.Serialization.JsonIgnore]
 | 
			
		||||
    [Newtonsoft.Json.JsonIgnore]
 | 
			
		||||
    [MapperIgnore]
 | 
			
		||||
    public ValidateForm AlarmPropertysValidateForm;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 变量额外属性Json
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -273,197 +253,15 @@ public class Variable : BaseDataEntity, IValidatableObject
 | 
			
		||||
    [AutoGenerateColumn(Ignore = true)]
 | 
			
		||||
    public Dictionary<long, Dictionary<string, string>>? VariablePropertys { get => variablePropertys; set => variablePropertys = value; }
 | 
			
		||||
 | 
			
		||||
    #region 报警
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 报警延时
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "报警延时")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public int AlarmDelay { get => alarmDelay; set => alarmDelay = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 布尔开报警使能
 | 
			
		||||
    /// 变量报警属性Json
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "布尔开报警使能")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public bool BoolOpenAlarmEnable { get => boolOpenAlarmEnable; set => boolOpenAlarmEnable = value; }
 | 
			
		||||
    [SugarColumn(IsJson = true, ColumnDataType = StaticConfig.CodeFirst_BigString, ColumnDescription = "报警属性Json", IsNullable = true)]
 | 
			
		||||
    [IgnoreExcel]
 | 
			
		||||
    [AutoGenerateColumn(Ignore = true)]
 | 
			
		||||
    public AlarmPropertys? AlarmPropertys { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 布尔开报警约束
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "布尔开报警约束", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string BoolOpenRestrainExpressions { get => boolOpenRestrainExpressions; set => boolOpenRestrainExpressions = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 布尔开报警文本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "布尔开报警文本", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string BoolOpenAlarmText { get => boolOpenAlarmText; set => boolOpenAlarmText = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 布尔关报警使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "布尔关报警使能")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public bool BoolCloseAlarmEnable { get => boolCloseAlarmEnable; set => boolCloseAlarmEnable = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 布尔关报警约束
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "布尔关报警约束", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string BoolCloseRestrainExpressions { get => boolCloseRestrainExpressions; set => boolCloseRestrainExpressions = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 布尔关报警文本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "布尔关报警文本", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string BoolCloseAlarmText { get => boolCloseAlarmText; set => boolCloseAlarmText = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高报使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "高报使能")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public bool HAlarmEnable { get => hAlarmEnable; set => hAlarmEnable = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高报约束
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "高报约束", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string HRestrainExpressions { get => hRestrainExpressions; set => hRestrainExpressions = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高报文本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "高报文本", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string HAlarmText { get => hAlarmText; set => hAlarmText = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高限值
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "高限值", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public double HAlarmCode { get => hAlarmCode; set => hAlarmCode = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高高报使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "高高报使能")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public bool HHAlarmEnable { get => hHAlarmEnable; set => hHAlarmEnable = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高高报约束
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "高高报约束", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string HHRestrainExpressions { get => hHRestrainExpressions; set => hHRestrainExpressions = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高高报文本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "高高报文本", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string HHAlarmText { get => hHAlarmText; set => hHAlarmText = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 高高限值
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "高高限值", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public double HHAlarmCode { get => hHAlarmCode; set => hHAlarmCode = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低报使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "低报使能")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public bool LAlarmEnable { get => lAlarmEnable; set => lAlarmEnable = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低报约束
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "低报约束", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string LRestrainExpressions { get => lRestrainExpressions; set => lRestrainExpressions = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低报文本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "低报文本", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string LAlarmText { get => lAlarmText; set => lAlarmText = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低限值
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "低限值", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public double LAlarmCode { get => lAlarmCode; set => lAlarmCode = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低低报使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "低低报使能")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public bool LLAlarmEnable { get => lLAlarmEnable; set => lLAlarmEnable = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低低报约束
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "低低报约束", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string LLRestrainExpressions { get => lLRestrainExpressions; set => lLRestrainExpressions = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低低报文本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "低低报文本", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string LLAlarmText { get => lLAlarmText; set => lLAlarmText = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 低低限值
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "低低限值", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public double LLAlarmCode { get => lLAlarmCode; set => lLAlarmCode = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义报警使能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "自定义报警使能")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public bool CustomAlarmEnable { get => customAlarmEnable; set => customAlarmEnable = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义报警条件约束
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "自定义报警条件约束", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string CustomRestrainExpressions { get => customRestrainExpressions; set => customRestrainExpressions = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义文本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "自定义文本", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string CustomAlarmText { get => customAlarmText; set => customAlarmText = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义报警条件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "自定义报警条件", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public string CustomAlarmCode { get => customAlarmCode; set => customAlarmCode = value; }
 | 
			
		||||
 | 
			
		||||
    #endregion 报警
 | 
			
		||||
 | 
			
		||||
    #region 备用字段
 | 
			
		||||
 | 
			
		||||
@@ -511,30 +309,6 @@ public class Variable : BaseDataEntity, IValidatableObject
 | 
			
		||||
            yield return new ValidationResult("Both RegisterAddress and OtherMethod cannot be empty or null.", new[] { nameof(OtherMethod), nameof(RegisterAddress) });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (HHAlarmEnable && HAlarmEnable && HHAlarmCode <= HAlarmCode)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("HHAlarmCode must be greater than HAlarmCode", new[] { nameof(HHAlarmCode), nameof(HAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
        if (HAlarmEnable && LAlarmEnable && HAlarmCode <= LAlarmCode)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("HAlarmCode must be greater than LAlarmCode", new[] { nameof(HAlarmCode), nameof(LAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
        if (LAlarmEnable && LLAlarmEnable && LAlarmCode <= LLAlarmCode)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("LAlarmCode must be greater than LLAlarmCode", new[] { nameof(LAlarmCode), nameof(LLAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (HHAlarmEnable && LAlarmEnable && HHAlarmCode <= LAlarmCode)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("HHAlarmCode should be greater than or less than LAlarmCode", new[] { nameof(HHAlarmCode), nameof(LAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
        if (HHAlarmEnable && LLAlarmEnable && HHAlarmCode <= LLAlarmCode)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("HHAlarmCode should be greater than or less than LLAlarmCode", new[] { nameof(HHAlarmCode), nameof(LLAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
        if (HAlarmEnable && LLAlarmEnable && HAlarmCode <= LLAlarmCode)
 | 
			
		||||
        {
 | 
			
		||||
            yield return new ValidationResult("HAlarmCode should be greater than or less than LLAlarmCode", new[] { nameof(HAlarmCode), nameof(LLAlarmCode) });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -39,4 +39,16 @@ public enum EventTypeEnum
 | 
			
		||||
    /// 准备恢复
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    PrepareFinish,
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 报警确认并恢复
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    ConfirmAndFinish,
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 重启默认
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    Restart,
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -83,7 +83,7 @@ public static class GlobalData
 | 
			
		||||
          .WhereIf(dataScope?.Count == 0, u => u.Value.CreateUserId == UserManager.UserId).Select(a => a.Value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static async Task<IEnumerable<AlarmVariable>> GetCurrentUserRealAlarmVariables()
 | 
			
		||||
    public static async Task<IEnumerable<AlarmVariable>> GetCurrentUserRealAlarmVariablesAsync()
 | 
			
		||||
    {
 | 
			
		||||
        var dataScope = await GlobalData.SysUserService.GetCurrentUserDataScopeAsync().ConfigureAwait(false);
 | 
			
		||||
        return RealAlarmIdVariables.WhereIf(dataScope != null && dataScope?.Count > 0, u => dataScope.Contains(u.Value.CreateOrgId))//在指定机构列表查询
 | 
			
		||||
@@ -177,6 +177,15 @@ public static class GlobalData
 | 
			
		||||
        }
 | 
			
		||||
        return GlobalData.ChannelThreadManage.DeviceThreadManages.TryGetValue(deviceRuntime.ChannelId, out deviceThreadManage);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static IChannelThreadManage GetChannelThreadManage(ChannelRuntime channelRuntime)
 | 
			
		||||
    {
 | 
			
		||||
        if (channelRuntime.DeviceThreadManage?.ChannelThreadManage != null)
 | 
			
		||||
            return channelRuntime.DeviceThreadManage.ChannelThreadManage;
 | 
			
		||||
        else
 | 
			
		||||
            return GlobalData.ChannelThreadManage;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static Dictionary<IDeviceThreadManage, List<DeviceRuntime>> GetDeviceThreadManages(IEnumerable<DeviceRuntime> deviceRuntimes)
 | 
			
		||||
    {
 | 
			
		||||
        Dictionary<IDeviceThreadManage, List<DeviceRuntime>> deviceThreadManages = new();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,20 @@
 | 
			
		||||
{
 | 
			
		||||
  "ThingsGateway.Management.Application._Imports": {
 | 
			
		||||
    "Restart": "Restart",
 | 
			
		||||
    "Upgrade": "Upgrade"
 | 
			
		||||
  "ThingsGateway.Management.Application.ManagementExportString": {
 | 
			
		||||
 | 
			
		||||
    "ManagementConfigName": "ManagementConfigName"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Management.Application.SaveUpdateZipFile": {
 | 
			
		||||
    "DownTemplate": "Download Template",
 | 
			
		||||
    "SaveUpdateZipFile": "Upload Version Package"
 | 
			
		||||
  "ThingsGateway.Management.Application.ManagementConfig": {
 | 
			
		||||
 | 
			
		||||
    "Name": "Name",
 | 
			
		||||
    "ServerUri": "ServerUri",
 | 
			
		||||
    "Enable": "Enable",
 | 
			
		||||
    "IsServer": "IsServer",
 | 
			
		||||
    "VerifyToken": "VerifyToken",
 | 
			
		||||
    "HeartbeatInterval": "HeartbeatInterval",
 | 
			
		||||
 | 
			
		||||
    "ImportNullError": "Unable to recognize"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Management.Application.UpdateZipFile": {
 | 
			
		||||
    "AppName": "AppName",
 | 
			
		||||
    "Architecture": "Architecture",
 | 
			
		||||
@@ -18,7 +26,7 @@
 | 
			
		||||
    "Version": "Version"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Gateway.Application.DefaultDiagram": {
 | 
			
		||||
  "ThingsGateway.Gateway.Application.INode": {
 | 
			
		||||
 | 
			
		||||
    "Actuator": "Actuator",
 | 
			
		||||
    "AlarmChangedTriggerNode": "AlarmStateTrigger",
 | 
			
		||||
@@ -120,6 +128,7 @@
 | 
			
		||||
  "ThingsGateway.Gateway.Application.AlarmVariable": {
 | 
			
		||||
    "AlarmCode": "AlarmCode",
 | 
			
		||||
    "AlarmDelay": "AlarmDelay",
 | 
			
		||||
    "AlarmLevel": "AlarmLevel",
 | 
			
		||||
    "AlarmEnable": "AlarmEnable",
 | 
			
		||||
    "AlarmLimit": "AlarmLimit",
 | 
			
		||||
    "AlarmText": "AlarmText",
 | 
			
		||||
@@ -146,6 +155,8 @@
 | 
			
		||||
    "DeviceName": "DeviceName",
 | 
			
		||||
    "Enable": "Enable",
 | 
			
		||||
    "EventTime": "EventTime",
 | 
			
		||||
    "ConfirmTime": "ConfirmTime",
 | 
			
		||||
    "FinishTime": "FinishTime",
 | 
			
		||||
    "EventType": "EventType",
 | 
			
		||||
    "HAlarmCode": "HAlarmCode",
 | 
			
		||||
    "HAlarmEnable": "HAlarmEnable",
 | 
			
		||||
@@ -310,7 +321,8 @@
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.CollectPropertyRetryBase": {
 | 
			
		||||
    "RetryCount": "RetryCount",
 | 
			
		||||
    "DutyCycle": "DutyCycle"
 | 
			
		||||
    "DutyCycle": "DutyCycle",
 | 
			
		||||
    "WritePriority": "WritePriority"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.ControlController": {
 | 
			
		||||
    "BatchSaveChannelAsync": "BatchSaveChannel",
 | 
			
		||||
@@ -329,6 +341,7 @@
 | 
			
		||||
    "WriteVariablesAsync": "Write variables"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.Device": {
 | 
			
		||||
    "Pause": "Pause",
 | 
			
		||||
    "ChannelError": "Channel error",
 | 
			
		||||
    "ChannelId": "Channel",
 | 
			
		||||
    "ChannelId.MinValue": "{0} cannot be empty",
 | 
			
		||||
@@ -385,10 +398,11 @@
 | 
			
		||||
    "ExpireTime": "ExpireTime {0}",
 | 
			
		||||
    "Unauthorized": "Unauthorized"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.ExportString": {
 | 
			
		||||
  "ThingsGateway.Gateway.Application.GatewayExportString": {
 | 
			
		||||
    "BusinessDeviceName": "BusinessDevice",
 | 
			
		||||
    "ChannelName": "Channel",
 | 
			
		||||
    "DeviceName": "Device",
 | 
			
		||||
    "AlarmName": "Alarm",
 | 
			
		||||
    "RedundantDeviceName": "Redundant Device",
 | 
			
		||||
    "VariableName": "Variable"
 | 
			
		||||
  },
 | 
			
		||||
@@ -450,24 +464,16 @@
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.Variable": {
 | 
			
		||||
    "AddressOrOtherMethodNotNull": "Variable address or special method cannot be empty at the same time",
 | 
			
		||||
    "AlarmDelay": "AlarmDelay",
 | 
			
		||||
 | 
			
		||||
    "ArrayLength": "ArrayLength",
 | 
			
		||||
    "BoolCloseAlarmEnable": "BoolCloseAlarmEnable",
 | 
			
		||||
    "BoolCloseAlarmText": "BoolCloseAlarmText",
 | 
			
		||||
    "BoolCloseRestrainExpressions": "BoolCloseRestrainExpressions",
 | 
			
		||||
    "BoolOpenAlarmEnable": "BoolOpenAlarmEnable",
 | 
			
		||||
    "BoolOpenAlarmText": "BoolOpenAlarmText",
 | 
			
		||||
    "BoolOpenRestrainExpressions": "BoolOpenRestrainExpressions",
 | 
			
		||||
 | 
			
		||||
    "BusinessGroup": "BusinessGroup",
 | 
			
		||||
    "BusinessGroupUpdateTrigger": "BusinessGroupUpdateTrigger",
 | 
			
		||||
    "RpcWriteCheck": "RpcWriteCheck",
 | 
			
		||||
    "ClearVariable": "Clear Variable",
 | 
			
		||||
    "CollectGroup": "CollectGroup",
 | 
			
		||||
    "CopyVariable": "Copy Variable",
 | 
			
		||||
    "CustomAlarmCode": "CustomAlarmCode",
 | 
			
		||||
    "CustomAlarmEnable": "CustomAlarmEnable",
 | 
			
		||||
    "CustomAlarmText": "CustomAlarmText",
 | 
			
		||||
    "CustomRestrainExpressions": "CustomRestrainExpressions",
 | 
			
		||||
 | 
			
		||||
    "DataType": "DataType",
 | 
			
		||||
    "DeleteVariable": "Delete Variable",
 | 
			
		||||
    "Description": "Description",
 | 
			
		||||
@@ -478,26 +484,14 @@
 | 
			
		||||
    "DynamicVariable": "DynamicVariable",
 | 
			
		||||
    "Enable": "Enable",
 | 
			
		||||
    "ExportVariable": "Export Variable",
 | 
			
		||||
    "HAlarmCode": "HAlarmCode",
 | 
			
		||||
    "HAlarmEnable": "HAlarmEnable",
 | 
			
		||||
    "HAlarmText": "HAlarmText",
 | 
			
		||||
    "HHAlarmCode": "HHAlarmCode",
 | 
			
		||||
    "HHAlarmEnable": "HHAlarmEnable",
 | 
			
		||||
    "HHAlarmText": "HHAlarmText",
 | 
			
		||||
    "HHRestrainExpressions": "HHRestrainExpressions",
 | 
			
		||||
    "HRestrainExpressions": "HRestrainExpressions",
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    "ImportVariable": "Import Variable",
 | 
			
		||||
    "InitValue": "InitValue",
 | 
			
		||||
    "IntervalTime": "IntervalTime",
 | 
			
		||||
    "IntervalTime.MinValue": "{0} value is too small",
 | 
			
		||||
    "LAlarmCode": "LAlarmCode",
 | 
			
		||||
    "LAlarmEnable": "LAlarmEnable",
 | 
			
		||||
    "LAlarmText": "LAlarmText",
 | 
			
		||||
    "LLAlarmCode": "LLAlarmCode",
 | 
			
		||||
    "LLAlarmEnable": "LLAlarmEnable",
 | 
			
		||||
    "LLAlarmText": "LLAlarmText",
 | 
			
		||||
    "LLRestrainExpressions": "LLRestrainExpressions",
 | 
			
		||||
    "LRestrainExpressions": "LRestrainExpressions",
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    "Name": "Name",
 | 
			
		||||
    "Name.Required": "{0} cannot be empty",
 | 
			
		||||
    "NameDump": "Duplicate variable name {0}",
 | 
			
		||||
@@ -519,6 +513,38 @@
 | 
			
		||||
    "VariableNotNull": "Variable name does not exist",
 | 
			
		||||
    "WriteExpressions": "WriteExpressions"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Gateway.Application.AlarmPropertys": {
 | 
			
		||||
 | 
			
		||||
    "AlarmDelay": "AlarmDelay",
 | 
			
		||||
    "AlarmLevel": "AlarmLevel",
 | 
			
		||||
    "BoolCloseAlarmEnable": "BoolCloseAlarmEnable",
 | 
			
		||||
    "BoolCloseAlarmText": "BoolCloseAlarmText",
 | 
			
		||||
    "BoolCloseRestrainExpressions": "BoolCloseRestrainExpressions",
 | 
			
		||||
    "BoolOpenAlarmEnable": "BoolOpenAlarmEnable",
 | 
			
		||||
    "BoolOpenAlarmText": "BoolOpenAlarmText",
 | 
			
		||||
    "BoolOpenRestrainExpressions": "BoolOpenRestrainExpressions",
 | 
			
		||||
    "CustomAlarmCode": "CustomAlarmCode",
 | 
			
		||||
    "CustomAlarmEnable": "CustomAlarmEnable",
 | 
			
		||||
    "CustomAlarmText": "CustomAlarmText",
 | 
			
		||||
    "CustomRestrainExpressions": "CustomRestrainExpressions",
 | 
			
		||||
    "HAlarmCode": "HAlarmCode",
 | 
			
		||||
    "HAlarmEnable": "HAlarmEnable",
 | 
			
		||||
    "HAlarmText": "HAlarmText",
 | 
			
		||||
    "HHAlarmCode": "HHAlarmCode",
 | 
			
		||||
    "HHAlarmEnable": "HHAlarmEnable",
 | 
			
		||||
    "HHAlarmText": "HHAlarmText",
 | 
			
		||||
    "HHRestrainExpressions": "HHRestrainExpressions",
 | 
			
		||||
    "HRestrainExpressions": "HRestrainExpressions",
 | 
			
		||||
    "LAlarmCode": "LAlarmCode",
 | 
			
		||||
    "LAlarmEnable": "LAlarmEnable",
 | 
			
		||||
    "LAlarmText": "LAlarmText",
 | 
			
		||||
    "LLAlarmCode": "LLAlarmCode",
 | 
			
		||||
    "LLAlarmEnable": "LLAlarmEnable",
 | 
			
		||||
    "LLAlarmText": "LLAlarmText",
 | 
			
		||||
    "LLRestrainExpressions": "LLRestrainExpressions",
 | 
			
		||||
    "LRestrainExpressions": "LRestrainExpressions"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.VariableRuntime": {
 | 
			
		||||
    "AlarmCode": "AlarmCode",
 | 
			
		||||
    "AlarmEnable": "AlarmEnable",
 | 
			
		||||
@@ -533,6 +559,8 @@
 | 
			
		||||
    "DeviceName": "DeviceName",
 | 
			
		||||
    "DynamicVariable": "DynamicVariable",
 | 
			
		||||
    "EventTime": "EventTime",
 | 
			
		||||
    "ConfirmTime": "ConfirmTime",
 | 
			
		||||
    "FinishTime": "FinishTime",
 | 
			
		||||
    "EventType": "EventType",
 | 
			
		||||
    "IntervalTime.MinValue": "{0} value is too small",
 | 
			
		||||
    "IsOnline": "IsOnline",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,22 @@
 | 
			
		||||
{
 | 
			
		||||
  "ThingsGateway.Management.Application._Imports": {
 | 
			
		||||
    "Restart": "重启",
 | 
			
		||||
    "Upgrade": "更新"
 | 
			
		||||
  "ThingsGateway.Management.Application.ManagementExportString": {
 | 
			
		||||
 | 
			
		||||
    "ManagementConfigName": "通讯配置"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Management.Application.SaveUpdateZipFile": {
 | 
			
		||||
    "DownTemplate": "下载模板",
 | 
			
		||||
    "SaveUpdateZipFile": "上传版本包"
 | 
			
		||||
  "ThingsGateway.Management.Application.ManagementConfig": {
 | 
			
		||||
 | 
			
		||||
    "Name": "名称",
 | 
			
		||||
    "ServerUri": "服务端Url",
 | 
			
		||||
    "Enable": "启用",
 | 
			
		||||
    "IsServer": "服务端",
 | 
			
		||||
    "VerifyToken": "验证令牌",
 | 
			
		||||
    "HeartbeatInterval": "心跳间隔",
 | 
			
		||||
 | 
			
		||||
    "ImportNullError": "无法识别"
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Management.Application.UpdateZipFile": {
 | 
			
		||||
    "AppName": "名称",
 | 
			
		||||
    "Architecture": "架构",
 | 
			
		||||
@@ -18,7 +28,7 @@
 | 
			
		||||
    "Version": "版本"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Gateway.Application.DefaultDiagram": {
 | 
			
		||||
  "ThingsGateway.Gateway.Application.INode": {
 | 
			
		||||
 | 
			
		||||
    "Actuator": "执行",
 | 
			
		||||
    "AlarmChangedTriggerNode": "报警状态触发器",
 | 
			
		||||
@@ -97,7 +107,7 @@
 | 
			
		||||
    "Status": "当前站点状态",
 | 
			
		||||
    "Switch": "切换",
 | 
			
		||||
    "SyncInterval": "数据同步间隔",
 | 
			
		||||
    "VerifyToken": "Token"
 | 
			
		||||
    "VerifyToken": "验证令牌"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.RedundancyService": {
 | 
			
		||||
    "EditRedundancyOption": "修改网关冗余配置"
 | 
			
		||||
@@ -120,6 +130,7 @@
 | 
			
		||||
  "ThingsGateway.Gateway.Application.AlarmVariable": {
 | 
			
		||||
    "AlarmCode": "报警值",
 | 
			
		||||
    "AlarmDelay": "报警延时",
 | 
			
		||||
    "AlarmLevel": "报警等级",
 | 
			
		||||
    "AlarmEnable": "报警使能",
 | 
			
		||||
    "AlarmLimit": "报警限值",
 | 
			
		||||
    "AlarmText": "报警文本",
 | 
			
		||||
@@ -146,6 +157,8 @@
 | 
			
		||||
    "DeviceName": "设备名称",
 | 
			
		||||
    "Enable": "变量使能",
 | 
			
		||||
    "EventTime": "事件时间",
 | 
			
		||||
    "ConfirmTime": "确认时间",
 | 
			
		||||
    "FinishTime": "恢复时间",
 | 
			
		||||
    "EventType": "事件类型",
 | 
			
		||||
    "HAlarmCode": "高限值",
 | 
			
		||||
    "HAlarmEnable": "高报使能",
 | 
			
		||||
@@ -310,7 +323,8 @@
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.CollectPropertyRetryBase": {
 | 
			
		||||
    "RetryCount": "失败重试次数",
 | 
			
		||||
    "DutyCycle": "占空比"
 | 
			
		||||
    "DutyCycle": "占空比",
 | 
			
		||||
    "WritePriority": "写优先"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.ControlController": {
 | 
			
		||||
    "BatchSaveChannelAsync": "保存通道",
 | 
			
		||||
@@ -329,6 +343,7 @@
 | 
			
		||||
    "WriteVariablesAsync": "写入变量"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.Device": {
 | 
			
		||||
    "Pause": "暂停",
 | 
			
		||||
    "ChannelError": "通道错误",
 | 
			
		||||
    "ChannelId": "通道",
 | 
			
		||||
    "ChannelId.MinValue": " {0} 不可为空",
 | 
			
		||||
@@ -387,10 +402,11 @@
 | 
			
		||||
    "ExpireTime": "过期时间 {0}",
 | 
			
		||||
    "Unauthorized": "未授权"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.ExportString": {
 | 
			
		||||
  "ThingsGateway.Gateway.Application.GatewayExportString": {
 | 
			
		||||
    "BusinessDeviceName": "业务设备",
 | 
			
		||||
    "ChannelName": "通道",
 | 
			
		||||
    "DeviceName": "设备",
 | 
			
		||||
    "AlarmName": "报警",
 | 
			
		||||
    "RedundantDeviceName": "冗余设备",
 | 
			
		||||
    "VariableName": "变量"
 | 
			
		||||
  },
 | 
			
		||||
@@ -453,6 +469,7 @@
 | 
			
		||||
  "ThingsGateway.Gateway.Application.Variable": {
 | 
			
		||||
    "AddressOrOtherMethodNotNull": " 变量地址或特殊方法不能同时为空 ",
 | 
			
		||||
    "AlarmDelay": "报警延时",
 | 
			
		||||
    "AlarmLevel": "报警等级",
 | 
			
		||||
    "ArrayLength": "数组长度",
 | 
			
		||||
    "BoolCloseAlarmEnable": "布尔关报警使能",
 | 
			
		||||
    "BoolCloseAlarmText": "布尔关报警文本",
 | 
			
		||||
@@ -521,6 +538,40 @@
 | 
			
		||||
    "VariableNotNull": "变量名称不存在",
 | 
			
		||||
    "WriteExpressions": "写入表达式"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Gateway.Application.AlarmPropertys": {
 | 
			
		||||
 | 
			
		||||
    "AlarmDelay": "报警延时",
 | 
			
		||||
    "AlarmLevel": "报警等级",
 | 
			
		||||
    "BoolCloseAlarmEnable": "布尔关报警使能",
 | 
			
		||||
    "BoolCloseAlarmText": "布尔关报警文本",
 | 
			
		||||
    "BoolCloseRestrainExpressions": "布尔关报警约束",
 | 
			
		||||
    "BoolOpenAlarmEnable": "布尔开报警使能",
 | 
			
		||||
    "BoolOpenAlarmText": "布尔开报警文本",
 | 
			
		||||
    "BoolOpenRestrainExpressions": "布尔开报警约束",
 | 
			
		||||
    "CustomAlarmCode": "自定义报警限值",
 | 
			
		||||
    "CustomAlarmEnable": "自定义报警使能",
 | 
			
		||||
    "CustomAlarmText": "自定义报警文本",
 | 
			
		||||
    "CustomRestrainExpressions": "自定义报警约束",
 | 
			
		||||
    "HAlarmCode": "高限值",
 | 
			
		||||
    "HAlarmEnable": "高报使能",
 | 
			
		||||
    "HAlarmText": "高报文本",
 | 
			
		||||
    "HHAlarmCode": "高高限值",
 | 
			
		||||
    "HHAlarmEnable": "高高报使能",
 | 
			
		||||
    "HHAlarmText": "高高报文本",
 | 
			
		||||
    "HHRestrainExpressions": "高高报约束",
 | 
			
		||||
    "HRestrainExpressions": "高报约束",
 | 
			
		||||
    "LAlarmCode": "低限值",
 | 
			
		||||
    "LAlarmEnable": "低报使能",
 | 
			
		||||
    "LAlarmText": "低报文本",
 | 
			
		||||
    "LLAlarmCode": "低低限值",
 | 
			
		||||
    "LLAlarmEnable": "低低报使能",
 | 
			
		||||
    "LLAlarmText": "低低报文本",
 | 
			
		||||
    "LLRestrainExpressions": "低低报约束",
 | 
			
		||||
    "LRestrainExpressions": "低报约束"
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Gateway.Application.VariableRuntime": {
 | 
			
		||||
    "AlarmCode": "报警值",
 | 
			
		||||
    "AlarmEnable": "报警使能",
 | 
			
		||||
@@ -535,6 +586,8 @@
 | 
			
		||||
    "DeviceName": "设备名称",
 | 
			
		||||
    "DynamicVariable": "动态变量",
 | 
			
		||||
    "EventTime": "事件时间",
 | 
			
		||||
    "ConfirmTime": "确认时间",
 | 
			
		||||
    "FinishTime": "恢复时间",
 | 
			
		||||
    "EventType": "事件类型",
 | 
			
		||||
    "IntervalTime.MinValue": " {0} 值太小",
 | 
			
		||||
    "IsOnline": "在线",
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user