Compare commits

...

128 Commits

Author SHA1 Message Date
Diego
0c31cfcbc5 fix: orm实体与表字段不一致时可能导致批量操作失败 2025-10-23 18:56:34 +08:00
2248356998 qq.com
0e44bc67cd feat: 系统总/可用内存读取 适配docker容器,硬件信息页面增加GC数据显示 2025-10-22 23:24:28 +08:00
2248356998 qq.com
12dfbba42c build: 10.12.6 2025-10-22 15:30:56 +08:00
2248356998 qq.com
96b4287f3a fix: operResult隐式转换递归错误 2025-10-22 13:43:39 +08:00
2248356998 qq.com
7d406de29f 调整 runtimeconfig.template.json 文件 2025-10-22 00:47:33 +08:00
2248356998 qq.com
81f0ef466a perf: 优化文本日志性能 2025-10-21 17:40:21 +08:00
2248356998 qq.com
3f2d6b133c 更新依赖包 2025-10-21 17:26:46 +08:00
Diego
e776dc67eb Remove language support from wiki.json
Removed supported languages and default language settings.
2025-10-21 15:58:01 +08:00
devin-ai-integration[bot]
bc5827d140 Update wiki.json (#26)
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-21 15:54:04 +08:00
2248356998 qq.com
21838bf4af perf: 异步池化性能 2025-10-21 10:56:11 +08:00
2248356998 qq.com
6090108597 build: 10.11.118 2025-10-20 22:15:24 +08:00
2248356998 qq.com
b47b9e6f43 build: 10.11.118 2025-10-20 22:15:10 +08:00
2248356998 qq.com
18d1cffb2d perf: 异步池化性能 2025-10-20 20:32:31 +08:00
2248356998 qq.com
516fd7f235 build: 10.11.116
fix: 网关监控页面 js释放
test: Benchmark编译错误
2025-10-20 15:10:50 +08:00
2248356998 qq.com
2ee16c3533 test: Benchmark编译错误 2025-10-20 14:38:05 +08:00
2248356998 qq.com
7d22f5c78e test: Benchmark编译错误 2025-10-20 14:36:52 +08:00
2248356998 qq.com
3e604ee2fd refactor: 变量删除创建机构等信息,改用绑定的设备的机构信息 2025-10-19 19:39:42 +08:00
2248356998 qq.com
47e442874c pref: 异步状态机优化 2025-10-18 23:14:55 +08:00
2248356998 qq.com
2a8c0cbab1 pref: 异步状态机优化 2025-10-18 03:18:45 +08:00
2248356998 qq.com
c26898b49d feat: 网关监控变量在线状态醒目显示 2025-10-17 12:30:05 +08:00
2248356998 qq.com
00c24d06a3 chore: 更新TouchSocket依赖 2025-10-17 00:50:36 +08:00
2248356998 qq.com
3461f34240 feat: 网关监控页面树节点js更新状态
perf: 优化变量页面刷新性能
perf: 优化热点方法异步性能
2025-10-17 00:41:47 +08:00
2248356998 qq.com
aa1ce08c02 perf: 优化变量页面刷新性能 2025-10-16 18:15:42 +08:00
2248356998 qq.com
9c230c2da9 chore: ui json显示不缩进 2025-10-16 17:39:57 +08:00
2248356998 qq.com
21215d0379 build: 10.11.109
feat: js刷新变量表数据
chore: 更新BootstrapBlazor.Chart依赖
2025-10-16 16:16:07 +08:00
Diego
7448183791 !78 feat: js刷新变量表数据
* 调整列宽
* feat: js刷新变量表数据
2025-10-16 08:09:29 +00:00
Diego
35edd7dc43 !77 update src/Admin/ThingsGateway.Common/Common/ConcurrentList.cs.
Merge pull request !77 from Sunny/v10
2025-10-16 06:14:04 +00:00
Sunny
bd178831e3 update src/Admin/ThingsGateway.Common/Common/ConcurrentList.cs.
修改 ConcurrentList.cs 一处错误代码

Signed-off-by: Sunny <yhuse@163.com>
2025-10-16 04:36:31 +00:00
2248356998 qq.com
fe9ec6ad10 fix: s7连接错误 2025-10-16 12:00:08 +08:00
2248356998 qq.com
6f9ec2e24b feat: 并发字典替换为 NonBlockingDictionary 类型 2025-10-15 17:40:33 +08:00
2248356998 qq.com
c0337e2b19 build: 10.11.105
feat: json添加AllowNamedFloatingPointLiterals
2025-10-15 11:16:18 +08:00
2248356998 qq.com
8a95f48f5a 适配 net10 rc2 2025-10-15 10:02:11 +08:00
2248356998 qq.com
14f3c31265 build: 10.11.102 2025-10-14 22:22:06 +08:00
2248356998 qq.com
1bad65378f build: 10.11.101 2025-10-14 11:46:26 +08:00
2248356998 qq.com
db3affc67e build: 10.11.100
feat(VariablePage): 优化UI性能
2025-10-14 10:18:09 +08:00
2248356998 qq.com
5ee8b50a92 build: 10.11.99 2025-10-13 22:11:05 +08:00
2248356998 qq.com
301beda2a2 build: 10.11.98 2025-10-13 21:10:39 +08:00
2248356998 qq.com
628b51a353 build: 10.11.97 2025-10-12 15:15:42 +08:00
2248356998 qq.com
f03445bc83 更新依赖 2025-10-12 00:25:57 +08:00
2248356998 qq.com
55a2ff5487 build: 10.11.96 2025-10-11 13:38:28 +08:00
2248356998 qq.com
0fef7dcf3b 更新依赖 2025-10-10 21:41:24 +08:00
2248356998 qq.com
19d9702606 fix: sqlite 批量更新 2025-10-10 14:19:43 +08:00
2248356998 qq.com
a8a9774932 fix: orm 序列化 2025-10-10 12:26:35 +08:00
2248356998 qq.com
aad0f0e8c3 fix: orm批量插入 2025-10-10 11:19:14 +08:00
2248356998 qq.com
e74eae50a7 build: 10.11.87 2025-10-09 20:58:16 +08:00
2248356998 qq.com
3b16d7019f feat: 优化orm 批量插入 2025-10-09 20:57:18 +08:00
2248356998 qq.com
3e038028c2 feat: 优化orm 批量插入 2025-10-09 19:19:18 +08:00
2248356998 qq.com
b1d8041f7e feat: 优化orm BulkCopy 2025-10-09 19:05:33 +08:00
2248356998 qq.com
53a98b26cd fix: 数据库插件保留天数逻辑错误 2025-10-09 08:52:02 +08:00
2248356998 qq.com
42c740fa1b 更新依赖 2025-10-07 22:25:38 +08:00
2248356998 qq.com
556819c90c 10.11.83 2025-09-30 15:25:15 +08:00
2248356998 qq.com
2522333a9c 更新依赖 2025-09-29 23:16:34 +08:00
2248356998 qq.com
bd4ce7c09b 适配net10 2025-09-29 17:47:24 +08:00
2248356998 qq.com
156ed88bd6 10.11.80 2025-09-29 17:09:12 +08:00
2248356998 qq.com
2416226eb0 10.11.78 2025-09-28 17:01:19 +08:00
2248356998 qq.com
976323a716 适配net10 2025-09-28 13:06:39 +08:00
2248356998 qq.com
3c9e397403 10.11.76 2025-09-28 00:33:56 +08:00
2248356998 qq.com
79406ad4a0 更新依赖 2025-09-27 23:59:38 +08:00
2248356998 qq.com
20c44f10ca 10.11.73 2025-09-27 20:18:56 +08:00
2248356998 qq.com
31d6b2a9e6 添加FastMapper 2025-09-26 15:24:37 +08:00
2248356998 qq.com
68e5a9c546 build: 10.11.71 2025-09-26 11:51:05 +08:00
2248356998 qq.com
b2ea9f99b9 适配net10 2025-09-26 10:58:57 +08:00
2248356998 qq.com
6d7d0e468a fix: opcua连接状态 2025-09-24 11:03:23 +08:00
2248356998 qq.com
ff1f632de2 build: 10.11.67 sqldb插件 2025-09-23 22:38:05 +08:00
2248356998 qq.com
fc3d7015ee sqldb插件 清理数据cron表达式错误 2025-09-23 22:36:45 +08:00
2248356998 qq.com
40c5acb522 调整UI 2025-09-23 13:41:20 +08:00
2248356998 qq.com
f9cc1cbb05 10.11.65 2025-09-23 12:02:17 +08:00
2248356998 qq.com
cf6e8b58f0 更新内存变量 2025-09-23 01:22:02 +08:00
2248356998 qq.com
615e3bb24c 修改 脚本调试项目 依赖项 2025-09-22 14:58:03 +08:00
2248356998 qq.com
4a7534b210 添加 脚本调试项目 2025-09-22 10:36:28 +08:00
2248356998 qq.com
58e099cb93 10.11.58 2025-09-21 20:53:50 +08:00
2248356998 qq.com
a94a9c953c 10.11.57 2025-09-19 14:32:34 +08:00
Diego
35e1ffa3e9 添加文件 2025-09-18 17:54:29 +08:00
Diego
4921642151 10.11.56 2025-09-18 17:15:57 +08:00
2248356998 qq.com
d71ee29da8 10.11.54 2025-09-17 20:50:41 +08:00
2248356998 qq.com
901aa2d59f 10.11.52 2025-09-17 11:07:45 +08:00
2248356998 qq.com
764957c014 10.11.51 2025-09-16 22:19:01 +08:00
Diego
0e3898218b 10.11.44 2025-09-16 19:13:19 +08:00
Diego
61f13cef3c 10.11.43 2025-09-15 17:35:49 +08:00
2248356998 qq.com
0b663d9e01 fix: modbusRtu 0x10 2025-09-14 11:18:41 +08:00
2248356998 qq.com
6c95c6209f 10.11.41 2025-09-13 14:22:13 +08:00
2248356998 qq.com
4d223d2622 10.11.40 2025-09-13 13:00:52 +08:00
yunqi
e8d7e91b64 !74 modify dlt645_2007 buildcmd
* modify dlt645_2007 buildcmd
2025-09-13 04:59:10 +00:00
2248356998 qq.com
8175f541ec 更新基准测试 2025-09-12 23:53:36 +08:00
2248356998 qq.com
0adbdb926b 更新基准测试 2025-09-12 23:49:18 +08:00
Diego
42adee9980 更新依赖 2025-09-11 22:22:39 +08:00
2248356998 qq.com
427a7404bc 更新UI 2025-09-11 19:05:14 +08:00
2248356998 qq.com
3658199e0a 10.11.37 2025-09-11 18:30:49 +08:00
Diego
82eedee50a 更新依赖 2025-09-11 18:12:40 +08:00
2248356998 qq.com
6a18fc3e06 10.11.36 2025-09-10 12:59:54 +08:00
2248356998 qq.com
c37e314ed6 更新依赖 2025-09-09 21:53:39 +08:00
2248356998 qq.com
a937a85d90 添加项目 2025-09-08 21:51:37 +08:00
2248356998 qq.com
35dd4ae9d3 更新依赖 2025-09-08 21:27:49 +08:00
2248356998 qq.com
0b829ac85c 10.11.33 2025-09-08 21:16:37 +08:00
2248356998 qq.com
aa247422d2 10.11.31 2025-09-08 17:56:19 +08:00
2248356998 qq.com
2e00e8c135 更新依赖 2025-09-08 08:46:52 +08:00
2248356998 qq.com
34dd2cf0a7 更新依赖 2025-09-07 21:52:41 +08:00
2248356998 qq.com
8404e20c5e build: 10.11.27
refactor: 更新依赖
2025-09-06 23:34:32 +08:00
2248356998 qq.com
662aa162e9 fix: sugar无实体curd 2025-09-05 13:16:18 +08:00
2248356998 qq.com
5927738c32 10.11.25 2025-09-05 11:30:33 +08:00
2248356998 qq.com
3c9f97a5c3 10.11.24 2025-09-04 22:39:21 +08:00
2248356998 qq.com
179ca0aa0e build: 10.11.23
feat: 增加部分demo
feat: 增加管理软件桌面端
2025-09-03 17:40:19 +08:00
Diego
fc09a52da1 build: 10.11.22
refactor:性能优化
2025-09-02 11:13:54 +08:00
Diego
5436b91c89 10.11.21 2025-09-01 18:40:12 +08:00
Diego
d1c46f51a6 添加api接口 2025-09-01 16:00:53 +08:00
Diego
4539d8d198 fix: 字符串反转功能失效 2025-09-01 11:52:03 +08:00
2248356998 qq.com
9ea9529a5f 10.11.19 2025-08-29 15:43:02 +08:00
2248356998 qq.com
4e6be23aac 10.11.17 2025-08-28 17:15:08 +08:00
2248356998 qq.com
2fabbd236b 10.11.16 2025-08-28 17:05:00 +08:00
2248356998 qq.com
163a66530e fix: dlt645校验和 2025-08-28 17:04:27 +08:00
2248356998 qq.com
29073a00c4 fix: dtu连接注册包解析错误 2025-08-28 15:35:43 +08:00
2248356998 qq.com
c6d4d1ecfa fix: dtu连接注册包解析错误 2025-08-28 15:20:00 +08:00
2248356998 qq.com
ba16889cad 10.11.14 2025-08-28 14:21:58 +08:00
2248356998 qq.com
5aaed35b0f 10.11.12 2025-08-28 01:42:59 +08:00
Diego
df067c91eb 10.11.12 2025-08-28 01:39:39 +08:00
2248356998 qq.com
2078b4a60b 补充忽略的文件 2025-08-26 08:54:09 +08:00
2248356998 qq.com
20a2e3ff8e 示例 2025-08-25 20:39:26 +08:00
Diego
61a973b1b5 !73 10.11.10 2025-08-25 12:21:12 +00:00
2248356998 qq.com
cbd72e2081 添加文本日志配置json文件 2025-08-22 16:17:25 +08:00
2248356998 qq.com
4e0377b20c 添加文本日志配置json文件 2025-08-22 16:15:54 +08:00
2248356998 qq.com
fd318d3cdc 移除警告项 2025-08-22 16:12:14 +08:00
2248356998 qq.com
515bdb9700 10.11.7 2025-08-21 23:35:12 +08:00
2248356998 qq.com
46c1780017 10.11.6 2025-08-21 22:40:53 +08:00
2248356998 qq.com
fe78a4c3ca 更新依赖库 2025-08-21 22:05:30 +08:00
2248356998 qq.com
2d7effadf9 10.11.4 2025-08-21 21:13:18 +08:00
Diego
346c560f8b !72 更新依赖项 2025-08-21 11:46:58 +00:00
Diego
8e3bd89f61 修改编译项 2025-08-18 17:30:34 +08:00
Diego
6da142d080 10.10.23 2025-08-18 17:03:40 +08:00
984 changed files with 30974 additions and 9943 deletions

3
.gitignore vendored
View File

@@ -364,8 +364,5 @@ FodyWeavers.xsd
/src/*Pro*/
/src/*Pro*
/src/**/*Pro*
/src/*pro*
/src/*pro*/
/src/ThingsGateway.Server/Configuration/GiteeOAuthSettings.json
/src/.idea/

View File

@@ -1,11 +1,18 @@
# ThingsGateway

[![star](https://gitee.com/ThingsGateway/ThingsGateway/badge/star.svg?theme=gvp)](https://gitee.com/ThingsGateway/ThingsGateway/stargazers)
[![star](https://img.shields.io/github/stars/ThingsGateway/ThingsGateway?logo=github)](https://github.com/ThingsGateway/ThingsGateway)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/ThingsGateway/ThingsGateway)
[![NuGet(ThingsGateway)](https://img.shields.io/nuget/v/ThingsGateway.Foundation.svg?label=ThingsGateway)](https://www.nuget.org/packages/ThingsGateway.Foundation/)
[![NuGet(ThingsGateway)](https://img.shields.io/nuget/dt/ThingsGateway.Foundation.svg)](https://www.nuget.org/packages/ThingsGateway.Foundation/)
[![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg)](https://thingsgateway.cn/docs/1)
<a href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=NnBjPO-8kcNFzo_RzSbdICflb97u2O1i&authKey=V1MI3iJtpDMHc08myszP262kDykbx2Yev6ebE4Me0elTe0P0IFAmtU5l7Sy5w0jx&noverify=0&group_code=605534569">
<img src="https://img.shields.io/badge/QQ群-605534569-red" alt="QQ">
</a>
## Introduction

A cross-platform, high-performance edge data collection gateway based on net9.
A cross-platform, high-performance edge data collection gateway based on net8/10.

## Documentation
@@ -29,7 +36,6 @@ Account: **SuperAdmin**
Password: **111111**

**In the upper-right corner, switch to the IoT Gateway module in the personal popup box**
## Docker

View File

@@ -1,8 +1,18 @@
# ThingsGateway
[![star](https://gitee.com/ThingsGateway/ThingsGateway/badge/star.svg?theme=gvp)](https://gitee.com/ThingsGateway/ThingsGateway/stargazers)
[![star](https://img.shields.io/github/stars/ThingsGateway/ThingsGateway?logo=github)](https://github.com/ThingsGateway/ThingsGateway)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/ThingsGateway/ThingsGateway)
[![NuGet(ThingsGateway)](https://img.shields.io/nuget/v/ThingsGateway.Foundation.svg?label=ThingsGateway)](https://www.nuget.org/packages/ThingsGateway.Foundation/)
[![NuGet(ThingsGateway)](https://img.shields.io/nuget/dt/ThingsGateway.Foundation.svg)](https://www.nuget.org/packages/ThingsGateway.Foundation/)
[![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg)](https://thingsgateway.cn/docs/1)
<a href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=NnBjPO-8kcNFzo_RzSbdICflb97u2O1i&authKey=V1MI3iJtpDMHc08myszP262kDykbx2Yev6ebE4Me0elTe0P0IFAmtU5l7Sy5w0jx&noverify=0&group_code=605534569">
<img src="https://img.shields.io/badge/QQ群-605534569-red" alt="QQ">
</a>
## 介绍
基于net9的跨平台高性能边缘采集网关
基于net8/10的跨平台高性能边缘采集网关
## 文档
@@ -19,7 +29,6 @@
密码 : **111111**
**右上角个人弹出框中,切换到物联网关模块**
## Docker

View File

@@ -27,6 +27,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" PrivateAssets="all" Private="false" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" PrivateAssets="all" Private="false" />
</ItemGroup>
</Project>

View File

@@ -37,9 +37,8 @@ public class FileController : ControllerBase
var root = Directory.GetCurrentDirectory();
var wwwroot = Path.Combine(root, "wwwroot");
var filePath = Path.Combine(wwwroot, fileName);
// 防止路径穿越攻击
#pragma warning disable CA3003
if (filePath.Contains("..") || !System.IO.File.Exists(filePath))
if ((!(fileName.StartsWith(@"../Logs") || fileName.StartsWith(@"..\Logs")) && filePath.Contains("..")) || !System.IO.File.Exists(filePath))
{
return NotFound();
}

View File

@@ -45,6 +45,7 @@ public class VerificatInfo : PrimaryIdEntity
/// 登录IP
/// </summary>
[AutoGenerateColumn(Filterable = true, Sortable = true, Width = 200)]
[SugarColumn(IsNullable = true)]
public string LoginIp { get; set; }
/// <summary>
@@ -78,5 +79,6 @@ public class VerificatInfo : PrimaryIdEntity
/// 登录设备
/// </summary>
[AutoGenerateColumn(Filterable = true, Sortable = true, Width = 100)]
[SugarColumn(IsNullable = true)]
public string Device { get; set; }
}

View File

@@ -251,11 +251,13 @@ public class RequestAuditFilter : IAsyncActionFilter, IOrderedFilter
if (exception == null)
{
logger.Log(LogLevel.Information, $"{logData.Method}:{logData.Path}-{logData.Operation}");
if (logger.IsEnabled(LogLevel.Information))
logger.Log(LogLevel.Information, $"{logData.Method}:{logData.Path}-{logData.Operation}");
}
else
{
logger.Log(LogLevel.Warning, $"{logData.Method}:{logData.Path}-{logData.Operation}{Environment.NewLine}{logData.Exception?.ToSystemTextJsonString()}{Environment.NewLine}{logData.Validation?.ToSystemTextJsonString()}");
if (logger.IsEnabled(LogLevel.Warning))
logger.Log(LogLevel.Warning, $"{logData.Method}:{logData.Path}-{logData.Operation}{Environment.NewLine}{logData.Exception?.ToSystemTextJsonString()}{Environment.NewLine}{logData.Validation?.ToSystemTextJsonString()}");
}
}

View File

@@ -8,6 +8,9 @@
// QQ群605534569
//------------------------------------------------------------------------------
using System.ComponentModel;
using System.Runtime;
using ThingsGateway.NewLife;
namespace ThingsGateway.Admin.Application;
@@ -19,11 +22,7 @@ public class HardwareInfo
/// 当前磁盘信息
/// </summary>
public DriveInfo DriveInfo { get; set; }
/// <summary>
/// 硬件信息获取
/// </summary>
public MachineInfo? MachineInfo { get; set; }
/// <summary>
/// 主机环境
@@ -40,19 +39,118 @@ public class HardwareInfo
/// </summary>
public string OsArchitecture { get; set; }
/// <summary>
/// 唯一编码
/// </summary>
public string UUID { get; set; }
/// <summary>
/// 进程占用内存
/// </summary>
[AutoGenerateColumn(Ignore = true)]
public int WorkingSet { get; set; }
/// <summary>系统名称</summary>
public String OSName { get; set; }
/// <summary>系统版本</summary>
public String OSVersion { get; set; }
public String UUID { get; set; }
/// <summary>内存总量。单位MB</summary>
public UInt64 Memory { get; set; }
/// <summary>可用内存。单位MB</summary>
public UInt64 AvailableMemory { get; set; }
/// <summary>CPU占用率</summary>
public Double CpuRate { get; set; }
public Double Battery { get; set; }
public Double Temperature { get; set; }
/// <summary>处理器型号</summary>
public String? Processor { get; set; }
#region GC与进程内存信息
/// <summary>GC 认为“内存吃紧”的阈值。单位MB</summary>
[DisplayName("GC高内存阈值")]
public UInt64 HighMemoryLoadThreshold { get; set; }
/// <summary>GC 可用内存上限。单位MB</summary>
[DisplayName("GC可用内存上限")]
public UInt64 TotalAvailableMemory { get; set; }
/// <summary>当前托管堆容量。单位MB</summary>
[DisplayName("托管堆容量")]
public UInt64 HeapSize { get; set; }
/// <summary>托管堆已用内存。单位MB</summary>
[DisplayName("托管堆已用")]
public UInt64 TotalMemory { get; set; }
/// <summary>托管堆碎片大小。单位MB</summary>
[DisplayName("托管堆碎片")]
public UInt64 FragmentedBytes { get; set; }
/// <summary>GC识别可用内存。单位MB</summary>
[DisplayName("GC识别可用内存")]
public UInt64 GCAvailableMemory { get; set; }
/// <summary>GC 已提交的内存。单位MB</summary>
[DisplayName("GC已提交内存")]
public UInt64 CommittedBytes { get; set; }
/// <summary>GC 累计分配的托管内存。单位MB</summary>
[DisplayName("GC累计分配")]
public UInt64 TotalAllocatedBytes { get; set; }
/// <summary>GC 暂停累计时间。单位:毫秒</summary>
[DisplayName("GC累计暂停时间")]
public UInt64 TotalPauseDurationMs { get; set; }
/// <summary>GC 代0收集次数</summary>
[DisplayName("GC Gen0 次数")]
public Int32 GcGen0Count { get; set; }
/// <summary>GC 代1收集次数</summary>
[DisplayName("GC Gen1 次数")]
public Int32 GcGen1Count { get; set; }
/// <summary>GC 代2收集次数</summary>
[DisplayName("GC Gen2 次数")]
public Int32 GcGen2Count { get; set; }
/// <summary>Server GC 是否启用</summary>
[DisplayName("是否使用Server GC")]
public Boolean IsServerGC { get; set; }
/// <summary>GC 延迟模式</summary>
[DisplayName("GC延迟模式")]
public GCLatencyMode? GCLatencyMode { get; set; }
/// <summary>GC 固定对象数</summary>
[DisplayName("固定对象数")]
public Int64 PinnedObjectsCount { get; set; }
/// <summary>终结队列挂起对象数</summary>
[DisplayName("终结挂起数")]
public Int64 FinalizationPendingCount { get; set; }
#endregion
#region
/// <summary>进程虚拟内存使用量。单位MB</summary>
[DisplayName("虚拟内存")]
public UInt64 VirtualMemory { get; set; }
/// <summary>进程私有内存使用量。单位MB</summary>
[DisplayName("私有内存")]
public UInt64 PrivateMemory { get; set; }
/// <summary>进程峰值工作集。单位MB</summary>
[DisplayName("峰值工作集")]
public UInt64 PeakWorkingSet { get; set; }
/// <summary>进程当前工作集。单位MB</summary>
[DisplayName("当前工作集")]
public UInt64 WorkingSet { get; set; }
#endregion
/// <summary>
/// 更新时间
/// </summary>
public string UpdateTime { get; set; }
public DateTime UpdateTime { get; set; }
public ulong AppRunTotalMinute { get; set; }
public ulong SystemRunTotalMinute { get; set; }
}

View File

@@ -8,12 +8,10 @@
// QQ群605534569
// ------------------------------------------------------------------------------
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Runtime.InteropServices;
using ThingsGateway.Extension;
using ThingsGateway.NewLife;
using ThingsGateway.NewLife.Caching;
using ThingsGateway.NewLife.Threading;
@@ -43,7 +41,7 @@ public class HardwareJob : IJob, IHardwareJob
/// <summary>
/// 运行信息获取
/// </summary>
public HardwareInfo HardwareInfo { get; } = new();
public HardwareInfo HardwareInfo { get; private set; }
/// <inheritdoc/>
public HardwareInfoOptions HardwareInfoOptions { get; private set; }
@@ -76,9 +74,10 @@ public class HardwareJob : IJob, IHardwareJob
{
try
{
if (HardwareInfo.MachineInfo == null)
var machine = MachineInfo.GetCurrent();
if (HardwareInfo == null)
{
HardwareInfo.MachineInfo = MachineInfo.GetCurrent();
HardwareInfo=machine.AdaptHardwareInfo();
string currentPath = Directory.GetCurrentDirectory();
DriveInfo drive = new(Path.GetPathRoot(currentPath));
@@ -88,10 +87,9 @@ public class HardwareJob : IJob, IHardwareJob
HardwareInfo.DriveInfo = drive;
HardwareInfo.OsArchitecture = Environment.OSVersion.Platform.ToString() + " " + RuntimeInformation.OSArchitecture.ToString(); // 系统架构
HardwareInfo.FrameworkDescription = RuntimeInformation.FrameworkDescription; // NET框架
HardwareInfo.Environment = App.HostEnvironment.IsDevelopment() ? "Development" : "Production";
HardwareInfo.UUID = HardwareInfo.MachineInfo.UUID;
HardwareInfo.Environment = App.HostEnvironment.EnvironmentName;
HardwareInfo.UpdateTime = TimerX.Now.ToDefaultDateTimeFormat();
HardwareInfo.UpdateTime = TimerX.Now;
}
}
catch
@@ -99,9 +97,12 @@ public class HardwareJob : IJob, IHardwareJob
}
try
{
HardwareInfo.MachineInfo.Refresh();
HardwareInfo.UpdateTime = TimerX.Now.ToDefaultDateTimeFormat();
HardwareInfo.WorkingSet = (Environment.WorkingSet / 1024.0 / 1024.0).ToInt();
var machine = MachineInfo.GetCurrent();
machine.Refresh();
machine.AdaptHardwareInfo(HardwareInfo);
HardwareInfo.AppRunTotalMinute = (ulong)Runtime.AppTickCount64 / 1000 /60;
HardwareInfo.SystemRunTotalMinute = (ulong)Runtime.TickCount64 / 1000 /60;
HardwareInfo.UpdateTime = TimerX.Now;
error = false;
}
catch (Exception ex)
@@ -123,10 +124,10 @@ public class HardwareJob : IJob, IHardwareJob
{
Date = TimerX.Now,
DriveUsage = (100 - (HardwareInfo.DriveInfo.TotalFreeSpace * 100.00 / HardwareInfo.DriveInfo.TotalSize)).ToInt(),
Battery = (HardwareInfo.MachineInfo.Battery * 100).ToInt(),
Battery = (HardwareInfo.Battery * 100).ToInt(),
MemoryUsage = (HardwareInfo.WorkingSet),
CpuUsage = (HardwareInfo.MachineInfo.CpuRate * 100).ToInt(),
Temperature = (HardwareInfo.MachineInfo.Temperature).ToInt(),
CpuUsage = (HardwareInfo.CpuRate * 100).ToInt(),
Temperature = (HardwareInfo.Temperature).ToInt(),
};
await _db.InsertableT(his).ExecuteCommandAsync(stoppingToken).ConfigureAwait(false);
MemoryCache.Remove(CacheKey);

View File

@@ -21,7 +21,7 @@ public class HistoryHardwareInfo
/// <inheritdoc/>
[SugarColumn(ColumnDescription = "内存")]
public int MemoryUsage { get; set; }
public UInt64 MemoryUsage { get; set; }
/// <inheritdoc/>
[SugarColumn(ColumnDescription = "CPU使用率")]

View File

@@ -21,7 +21,7 @@
"UserNoModule": "This account has not been assigned a module. Please contact the administrator",
"UserNull": "User {0} does not exist"
},
"ThingsGateway.Admin.Application.BlazorAuthenticationHandler": {
"UserExpire": "User expired, please login again"
},
@@ -46,12 +46,44 @@
"FileTypeError": "Not supported format {0}"
},
"ThingsGateway.Admin.Application.HardwareInfo": {
"Environment": "HostEnvironment",
"FrameworkDescription": ".NETFramework",
"OsArchitecture": "System Architecture",
"UpdateTime": "UpdateTime",
"UUID": "UUID"
"DriveInfo": "Current Disk Info",
"AppRunTotalMinute": "AppRunTotalMinute(min)",
"SystemRunTotalMinute": "SystemRunTotalMinute(min)",
"Environment": "Host Environment",
"FrameworkDescription": ".NET Framework",
"OsArchitecture": "OS Architecture",
"OSName": "OS Name",
"OSVersion": "OS Version",
"UUID": "UUID",
"Memory": "Total Memory",
"AvailableMemory": "Available Memory",
"CpuRate": "CPU Usage",
"Battery": "Battery Level",
"Temperature": "Temperature",
"Processor": "Processor Model",
"HighMemoryLoadThreshold": "GC High Memory Threshold",
"TotalAvailableMemory": "GC Total Available Memory",
"HeapSize": "Managed Heap Size",
"TotalMemory": "Managed Heap Used",
"FragmentedBytes": "Managed Heap Fragmented",
"GCAvailableMemory": "GC Available Memory",
"CommittedBytes": "GC Committed Bytes",
"TotalAllocatedBytes": "GC Total Allocated (MB)",
"TotalPauseDurationMs": "GC Total Pause Duration (ms)",
"GcGen0Count": "GC Gen0 Count",
"GcGen1Count": "GC Gen1 Count",
"GcGen2Count": "GC Gen2 Count",
"IsServerGC": "Server GC",
"GCLatencyMode": "GC Latency Mode",
"PinnedObjectsCount": "Pinned Objects Count",
"FinalizationPendingCount": "Finalization Pending Count",
"VirtualMemory": "Virtual Memory",
"PrivateMemory": "Private Memory",
"PeakWorkingSet": "Peak Working Set",
"WorkingSet": "Current Working Set",
"UpdateTime": "Update Time"
},
"ThingsGateway.Admin.Application.HardwareJob": {
"GetHardwareInfoFail": "Get Hardwareinfo Fail"
},

View File

@@ -46,11 +46,42 @@
"FileTypeError": "不支持 {0} 格式"
},
"ThingsGateway.Admin.Application.HardwareInfo": {
"DriveInfo": "当前磁盘信息",
"AppRunTotalMinute": "软件运行时长(min)",
"SystemRunTotalMinute": "系统运行时长(min)",
"Environment": "主机环境",
"FrameworkDescription": "NET框架",
"FrameworkDescription": ".NET 框架",
"OsArchitecture": "系统架构",
"UpdateTime": "更新时间",
"UUID": "唯一编码"
"OSName": "系统名称",
"OSVersion": "系统版本",
"UUID": "UUID",
"Memory": "系统总内存",
"AvailableMemory": "系统可用内存",
"CpuRate": "CPU 占用率",
"Battery": "电池电量",
"Temperature": "温度",
"Processor": "处理器型号",
"HighMemoryLoadThreshold": "GC 高内存阈值",
"TotalAvailableMemory": "GC 总内存阈值",
"HeapSize": "托管堆容量",
"TotalMemory": "托管对象占用",
"FragmentedBytes": "托管堆碎片",
"GCAvailableMemory": "GC 可用内存",
"CommittedBytes": "GC 提交内存总量",
"TotalAllocatedBytes": "GC 累计分配(MB)",
"TotalPauseDurationMs": "GC 累计暂停时间(ms)",
"GcGen0Count": "GC Gen0 次数",
"GcGen1Count": "GC Gen1 次数",
"GcGen2Count": "GC Gen2 次数",
"IsServerGC": "Server GC",
"GCLatencyMode": "GC 延迟模式",
"PinnedObjectsCount": "固定对象数",
"FinalizationPendingCount": "终结挂起数",
"VirtualMemory": "虚拟内存",
"PrivateMemory": "私有内存",
"PeakWorkingSet": "峰值工作集",
"WorkingSet": "当前工作集",
"UpdateTime": "更新时间"
},
"ThingsGateway.Admin.Application.HardwareJob": {
"GetHardwareInfoFail": "获取硬件信息出错"

View File

@@ -8,12 +8,20 @@
// QQ群605534569
//------------------------------------------------------------------------------
using BootstrapBlazor.Components;
using Riok.Mapperly.Abstractions;
using ThingsGateway.NewLife;
namespace ThingsGateway.Admin.Application;
[Mapper(UseDeepCloning = true, EnumMappingStrategy = EnumMappingStrategy.ByName, RequiredMappingStrategy = RequiredMappingStrategy.None)]
public static partial class AdminMapper
{
public static partial HardwareInfo AdaptHardwareInfo(this MachineInfo src);
public static partial void AdaptHardwareInfo(this MachineInfo src, HardwareInfo dto);
public static partial LoginInput AdaptLoginInput(this OpenApiLoginInput src);
public static partial OpenApiLoginOutput AdaptOpenApiLoginOutput(this LoginOutput src);
public static partial SessionOutput AdaptSessionOutput(this SysUser src);

View File

@@ -145,7 +145,7 @@ public class AdminOAuthHandler<TOptions>(
var loginEvent = new LoginEvent
{
Ip = appService.RemoteIpAddress,
Device = appService.UserAgent?.Platform,
Device = appService.UserAgent?.Platform ?? "Unknown",
Expire = expire,
SysUser = sysUser,
VerificatId = CommonUtils.GetSingleId()
@@ -156,7 +156,7 @@ public class AdminOAuthHandler<TOptions>(
//生成verificat信息
var verificatInfo = new VerificatInfo
{
Device = loginEvent.Device,
Device = loginEvent.Device ?? "Unknown",
Expire = loginEvent.Expire,
VerificatTimeout = tokenTimeout,
Id = loginEvent.VerificatId,

View File

@@ -26,7 +26,7 @@
"Module": 2,
"Title": "权限管理",
"Code": "System",
"NavLinkMatch": "All",
"NavLinkMatch": "Prefix",
"Category": "MENU",
"Target": "_self",
"Href": null,
@@ -47,7 +47,7 @@
"ParentId": 0,
"Module": 2,
"Title": "系统运维",
"NavLinkMatch": "All",
"NavLinkMatch": "Prefix",
"Code": "System",
"Category": "MENU",
"Target": "_self",

View File

@@ -235,7 +235,7 @@ public class AuthService : IAuthService
var logingEvent = new LoginEvent
{
Ip = _appService.RemoteIpAddress,
Device = _appService.UserAgent?.Platform,
Device = _appService.UserAgent?.Platform ?? "Unknown",
Expire = expire,
SysUser = sysUser,
VerificatId = verificatId
@@ -344,7 +344,7 @@ public class AuthService : IAuthService
//生成verificat信息
var verificatInfo = new VerificatInfo
{
Device = loginEvent.Device,
Device = loginEvent.Device ?? "Unknown",
Expire = loginEvent.Expire,
VerificatTimeout = tokenTimeout,
Id = loginEvent.VerificatId,

View File

@@ -20,7 +20,7 @@ namespace ThingsGateway.Admin.Application;
/// <typeparam name="TEntry"></typeparam>
public class EventService<TEntry> : IEventService<TEntry>, IDisposable
{
private ConcurrentDictionary<string, Func<TEntry, Task>> Cache = new();
private NonBlockingDictionary<string, Func<TEntry, Task>> Cache = new();
public void Dispose()
{

View File

@@ -9,6 +9,7 @@
//------------------------------------------------------------------------------
namespace ThingsGateway.Admin.Application;
internal sealed class NoticeService : INoticeService
{
private IEventService<AppMessage>? MessageDispatchService { get; set; }

View File

@@ -282,7 +282,7 @@ internal sealed class SysRoleService : BaseService<SysRole>, ISysRoleService
if (sysRole != null)
{
var resources = await _sysResourceService.GetAllAsync().ConfigureAwait(false);
var menusList = resources.Where(a => a.Category == ResourceCategoryEnum.Menu).Where(a => menuIds.Contains(a.Id));
var menusList = resources.Where(a => a.Category == ResourceCategoryEnum.Menu && menuIds.Contains(a.Id));
#region

View File

@@ -377,9 +377,9 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
/// 获取用户拥有的资源
/// </summary>
/// <param name="id">用户id</param>
public async Task<GrantResourceData> OwnResourceAsync(long id)
public Task<GrantResourceData> OwnResourceAsync(long id)
{
return await _roleService.OwnResourceAsync(id, RelationCategoryEnum.UserHasResource).ConfigureAwait(false);
return _roleService.OwnResourceAsync(id, RelationCategoryEnum.UserHasResource);
}
/// <summary>
@@ -505,10 +505,10 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
var password = await GetDefaultPassWord(true).ConfigureAwait(false);//获取默认密码,这里不走Aop所以需要加密一下
using var db = GetDB();
//重置密码
if (await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser
if ((await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser
{
Password = password
}, it => it.Id == id).ConfigureAwait(false))
}, it => it.Id == id).ConfigureAwait(false)) > 0)
{
DeleteUserFromCache(id);//从cache删除用户信息
var verificatInfoIds = _verificatInfoService.GetListByUserId(id);
@@ -550,7 +550,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
if (sysUser != null)
{
var resources = await _sysResourceService.GetAllAsync().ConfigureAwait(false);
var menusList = resources.Where(a => a.Category == ResourceCategoryEnum.Menu).Where(a => menuIds.Contains(a.Id));
var menusList = resources.Where(a => a.Category == ResourceCategoryEnum.Menu && menuIds.Contains(a.Id));
#region

View File

@@ -185,12 +185,12 @@ internal sealed class UserCenterService : BaseService<SysUser>, IUserCenterServi
using var db = GetDB();
//更新指定字段
var result = await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser
var result = (await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser
{
Email = input.Email,
Phone = input.Phone,
Avatar = input.Avatar,
}, it => it.Id == UserManager.UserId).ConfigureAwait(false);
}, it => it.Id == UserManager.UserId).ConfigureAwait(false)) > 0;
if (result)
_userService.DeleteUserFromCache(UserManager.UserId);//cache删除用户数据
}

View File

@@ -119,7 +119,7 @@ internal sealed class VerificatInfoService : BaseService<VerificatInfo>, IVerifi
public void Add(VerificatInfo verificatInfo)
{
using var db = GetDB();
db.Insertable<VerificatInfo>(verificatInfo).ExecuteCommand();
db.InsertableT<VerificatInfo>(verificatInfo).ExecuteCommand();
VerificatInfoService.RemoveCache(verificatInfo.Id);
if (verificatInfo != null)
VerificatInfoService.SetCahce(verificatInfo);
@@ -132,7 +132,7 @@ internal sealed class VerificatInfoService : BaseService<VerificatInfo>, IVerifi
public void Update(VerificatInfo verificatInfo)
{
using var db = GetDB();
db.Updateable<VerificatInfo>(verificatInfo).ExecuteCommand();
db.UpdateableT<VerificatInfo>(verificatInfo).ExecuteCommand();
VerificatInfoService.RemoveCache(verificatInfo.Id);
if (verificatInfo != null)
VerificatInfoService.SetCahce(verificatInfo);

View File

@@ -8,7 +8,7 @@
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
<TargetFrameworks>net8.0;$(OtherTargetFrameworks);</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
@@ -19,19 +19,21 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Riok.Mapperly" Version="4.2.1" ExcludeAssets="runtime" PrivateAssets="all" />
<PackageReference Include="Rougamo.Fody" Version="5.0.1" />
<PackageReference Include="Riok.Mapperly" Version="4.3.0" ExcludeAssets="runtime" PrivateAssets="all">
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Rougamo.Fody" Version="5.0.2" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
<PackageReference Include="System.Formats.Asn1" Version="8.0.2" />
<PackageReference Include="System.Formats.Asn1" Version="9.0.10" />
<PackageReference Include="System.Threading.RateLimiting" Version="8.0.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(NET9Version)" />
<PackageReference Include="System.Formats.Asn1" Version="$(NET9Version)" />
<PackageReference Include="System.Threading.RateLimiting" Version="$(NET9Version)" />
<ItemGroup Condition=" '$(TargetFramework)' == 'net10.0' ">
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(NET10Version)" />
<PackageReference Include="System.Formats.Asn1" Version="$(NET10Version)" />
<PackageReference Include="System.Threading.RateLimiting" Version="$(NET10Version)" />
</ItemGroup>
<ItemGroup>
<Content Remove="SeedData\Admin\*.json" />

View File

@@ -4,9 +4,9 @@
<div class="tg-table h-100">
<Table TItem="TItem" IsBordered="true" IsStriped="true" TableSize="TableSize.Compact" IsMultipleSelect="IsMultipleSelect" @ref="Instance" SearchTemplate="SearchTemplate"
DataService="DataService" CreateItemCallback="CreateItemCallback!"
IsPagination="IsPagination" PageItemsSource="PageItemsSource" IsFixedHeader="IsFixedHeader" IndentSize=24 RowHeight=RowHeight ShowSearchText="ShowSearchText" ShowSearchButton="ShowSearchButton" BeforeShowEditDialogCallback="BeforeShowEditDialogCallback!"
<Table Id=@Id TItem="TItem" IsBordered="true" IsStriped="true" TableSize="TableSize.Compact" SelectedRows=SelectedRows SelectedRowsChanged=privateSelectedRowsChanged IsMultipleSelect="IsMultipleSelect" @ref="Instance" SearchTemplate="SearchTemplate"
DataService="DataService" CreateItemCallback="CreateItemCallback!" RenderMode=RenderMode OnColumnCreating=OnColumnCreating
IsPagination="IsPagination" PageItemsSource="PageItemsSource" IsFixedHeader="IsFixedHeader" IndentSize=24 RowHeight=RowHeight ShowSearchText="ShowSearchText" ShowSearchButton="ShowSearchButton" DisableEditButtonCallback="DisableEditButtonCallback" DisableDeleteButtonCallback="DisableDeleteButtonCallback" BeforeShowEditDialogCallback=" BeforeShowEditDialogCallback!"
IsTree="IsTree" OnTreeExpand="OnTreeExpand!" TreeNodeConverter="TreeNodeConverter!" TreeIcon="fa-solid fa-circle-chevron-right" TreeExpandIcon="fa-solid fa-circle-chevron-right fa-rotate-90" IsAutoQueryFirstRender=IsAutoQueryFirstRender
ShowDefaultButtons="ShowDefaultButtons" ShowAdvancedSearch="ShowAdvancedSearch" ShowResetButton=ShowResetButton
ShowEmpty="ShowEmpty" EmptyText="@EmptyText" EmptyImage="@($"{WebsiteConst.DefaultResourceUrl}images/empty.svg")" SortString="@SortString" EditDialogSize="EditDialogSize"
@@ -14,7 +14,7 @@
ShowSkeleton="true" ShowLoading="ShowLoading" ShowSearch="ShowSearch" SearchModel=@SearchModel ShowLineNo
SearchMode=SearchMode ShowExportPdfButton=ShowExportPdfButton ExportButtonText=@ExportButtonText
ShowExportButton=@ShowExportButton Items=Items ClickToSelect=ClickToSelect ScrollMode=ScrollMode
ShowExportCsvButton=@ShowExportCsvButton SelectedRowsChanged=SelectedRowsChanged ShowCardView=ShowCardView
ShowExportCsvButton=@ShowExportCsvButton ShowCardView=ShowCardView OnColumnVisibleChanged=OnColumnVisibleChanged
FixedExtendButtonsColumn=FixedExtendButtonsColumn FixedMultipleColumn=FixedMultipleColumn FixedDetailRowHeaderColumn=FixedDetailRowHeaderColumn FixedLineNoColumn=FixedLineNoColumn
IsAutoRefresh=IsAutoRefresh AutoRefreshInterval=AutoRefreshInterval
AllowDragColumn=@AllowDragColumn Height=@Height ShowRefresh=ShowRefresh
@@ -29,7 +29,7 @@
ShowMultiFilterHeader=ShowMultiFilterHeader
ShowFilterHeader=ShowFilterHeader
ShowColumnList=ShowColumnList ExtendButtonColumnWidth="@ExtendButtonColumnWidth"
CustomerSearchModel="CustomerSearchModel" SelectedRows="SelectedRows" ModelEqualityComparer="ModelEqualityComparer!"
CustomerSearchModel="CustomerSearchModel" ModelEqualityComparer="ModelEqualityComparer!"
ShowExtendEditButtonCallback="ShowExtendEditButtonCallback!" ShowExtendDeleteButtonCallback="ShowExtendDeleteButtonCallback!"
DisableExtendEditButton="DisableExtendEditButton!" DisableExtendDeleteButton="DisableExtendDeleteButton!"
DisableExtendEditButtonCallback="DisableExtendEditButtonCallback!" DisableExtendDeleteButtonCallback="DisableExtendDeleteButtonCallback!"
@@ -41,6 +41,7 @@
DoubleClickToEdit="DoubleClickToEdit"
OnDoubleClickCellCallback="OnDoubleClickCellCallback"
OnDoubleClickRowCallback="OnDoubleClickRowCallback"
RowContentTemplate="RowContentTemplate"
OnClickRowCallback="OnClickRowCallback">
</Table>
</div>

View File

@@ -13,6 +13,41 @@ namespace ThingsGateway.Admin.Razor;
[CascadingTypeParameter(nameof(TItem))]
public partial class AdminTable<TItem> where TItem : class, new()
{
/// <inheritdoc cref="Table{TItem}.OnColumnVisibleChanged"/>
[Parameter]
public Func<string, bool, Task> OnColumnVisibleChanged { get; set; }
/// <inheritdoc cref="Table{TItem}.OnColumnCreating"/>
[Parameter]
public Func<List<ITableColumn>, Task> OnColumnCreating { get; set; }
/// <inheritdoc cref="Table{TItem}.RenderMode"/>
[Parameter]
public TableRenderMode RenderMode { get; set; }
public List<ITableColumn> Columns => Instance?.Columns;
public IEnumerable<ITableColumn> GetVisibleColumns => Instance?.GetVisibleColumns();
public List<TItem> Rows => Instance?.Rows;
/// <inheritdoc cref="Table{TItem}.SelectedRowsChanged"/>
[Parameter]
public EventCallback<List<TItem>> SelectedRowsChanged { get; set; }
/// <inheritdoc cref="Table{TItem}.SelectedRows"/>
[Parameter]
public List<TItem> SelectedRows { get; set; } = new();
private async Task privateSelectedRowsChanged(List<TItem> items)
{
SelectedRows = items;
if (SelectedRowsChanged.HasDelegate)
await SelectedRowsChanged.InvokeAsync(items);
}
/// <inheritdoc cref="Table{TItem}.DoubleClickToEdit"/>
[Parameter]
public bool DoubleClickToEdit { get; set; } = false;
@@ -22,6 +57,10 @@ public partial class AdminTable<TItem> where TItem : class, new()
/// <inheritdoc cref="Table{TItem}.OnDoubleClickRowCallback"/>
[Parameter]
public Func<TItem, Task>? OnDoubleClickRowCallback { get; set; }
/// <inheritdoc cref="Table{TItem}.RowContentTemplate"/>
[Parameter]
public RenderFragment<TableRowContext<TItem>>? RowContentTemplate { get; set; }
/// <inheritdoc cref="Table{TItem}.OnClickRowCallback"/>
[Parameter]
public Func<TItem, Task>? OnClickRowCallback { get; set; }
@@ -128,6 +167,9 @@ public partial class AdminTable<TItem> where TItem : class, new()
[Parameter]
public IDataService<TItem> DataService { get; set; }
[Parameter]
public string? Id { get; set; }
/// <inheritdoc cref="Table{TItem}.CreateItemCallback"/>
[Parameter]
public Func<TItem> CreateItemCallback { get; set; }
@@ -210,14 +252,6 @@ public partial class AdminTable<TItem> where TItem : class, new()
[Parameter]
public RenderFragment<TItem>? SearchTemplate { get; set; }
/// <inheritdoc cref="Table{TItem}.SelectedRows"/>
[Parameter]
public List<TItem>? SelectedRows { get; set; } = new List<TItem>();
/// <inheritdoc cref="Table{TItem}.SelectedRowsChanged"/>
[Parameter]
public EventCallback<List<TItem>> SelectedRowsChanged { get; set; }
/// <inheritdoc cref="Table{TItem}.SetRowClassFormatter"/>
[Parameter]
public Func<TItem, string?>? SetRowClassFormatter { get; set; }
@@ -266,6 +300,15 @@ public partial class AdminTable<TItem> where TItem : class, new()
[Parameter]
public bool ShowExportButton { get; set; } = false;
/// <inheritdoc cref="Table{TItem}.DisableEditButtonCallback"/>
public Func<List<TItem>, bool> DisableEditButtonCallback { get; set; } = (list) =>
list.Count != 1;
/// <inheritdoc cref="Table{TItem}.DisableDeleteButtonCallback"/>
[Parameter]
public Func<List<TItem>, bool> DisableDeleteButtonCallback { get; set; } = (list) =>
list.Count <= 0;
/// <inheritdoc cref="Table{TItem}.ShowExportCsvButton"/>
[Parameter]
public bool ShowExportCsvButton { get; set; } = false;

View File

@@ -125,13 +125,22 @@ public class BlazorAppContext
var ownMenus = OwnMenus.Where(a => a.Module == CurrentModuleId);
OwnMenuItems = AdminResourceUtil.BuildMenuTrees(ownMenus).ToList();
AllOwnMenuItems = AdminResourceUtil.BuildMenuTrees(OwnMenus).ToList();
OwnSameLevelMenuItems = ownMenus.Where(a => !a.Href.IsNullOrWhiteSpace()).Select(item => new MenuItem()
OwnSameLevelMenuItems = ownMenus.Where(a => !a.Href.IsNullOrWhiteSpace()).Select(item =>
{
Match = item.NavLinkMatch ?? Microsoft.AspNetCore.Components.Routing.NavLinkMatch.All,
Text = item.Title,
Icon = item.Icon,
Url = item.Href,
Target = item.Target.ToString(),
var menu = new MenuItem()
{
Match = item.NavLinkMatch ?? Microsoft.AspNetCore.Components.Routing.NavLinkMatch.Prefix,
Text = item.Title,
Icon = item.Icon,
Url = item.Href,
Target = item.Target.ToString(),
};
if (menu.Url.IsNullOrEmpty())
{
menu.Match = Microsoft.AspNetCore.Components.Routing.NavLinkMatch.Prefix;
}
return menu;
}).ToList();
UserWorkbenchOutputs = AllMenus.Where(it => UserWorkBench.Shortcuts.Contains(it.Id)).ToList();
}

View File

@@ -22,6 +22,9 @@
"SetDefaultModule": "Set as default module"
},
"ThingsGateway.Admin.Razor.HardwareInfoPage": {
"GCMemoryInfoConfig": "GCMemoryInfoConfig(MB)",
"GCMemoryInfo": "GCMemoryInfo(MB)",
"GCAnalyzeInfo": "GCAnalyzeInfo",
"AvailableDisk": "Available Disk",
"AvailableMemory": "Available Memory",
"CpuUsage": "CPU Usage",

View File

@@ -22,6 +22,10 @@
"SetDefaultModule": "设置为默认模块"
},
"ThingsGateway.Admin.Razor.HardwareInfoPage": {
"GCMemoryInfoConfig": "GC配置信息(MB)",
"GCMemoryInfo": "GC内存信息(MB)",
"GCAnalyzeInfo": "GC统计",
"AvailableDisk": "可用磁盘",
"AvailableMemory": "可用内存",
"CpuUsage": "CPU使用率",

View File

@@ -5,131 +5,189 @@
@using ThingsGateway.Admin.Application
@namespace ThingsGateway.Admin.Razor
<div class="row g-2 mx-1 form-inline">
<div class="col-12 col-md-4">
<div class="col-12 col-md-12">
<Card IsShadow=true class="m-2 flex-fill" Color="Color.Primary">
<HeaderTemplate>
@Localizer["SystemInfo"]
</HeaderTemplate>
<BodyTemplate>
<EditorFormObject IsDisplay Model="HardwareJob.HardwareInfo" ItemsPerRow="1" RowType=RowType.Inline LabelWidth="160">
<FieldItems>
<EditorItem @bind-Field="@context.MachineInfo">
<EditTemplate Context="value">
<div class="col-12 col-md-12">
<Display @bind-Value="@value.MachineInfo.OSName" DisplayText="@Localizer[nameof(value.MachineInfo.OSName)]" ShowTooltip ShowLabel=true>
</Display>
</div>
<div class="col-12 col-md-12">
<Display @bind-Value="@value.MachineInfo.OSVersion" DisplayText="@Localizer[nameof(value.MachineInfo.OSVersion)]" ShowTooltip ShowLabel=true>
</Display>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.UUID">
<EditTemplate Context="value">
<div class="col-12 col-md-12">
<Display @bind-Value="@value.UUID" ShowTooltip ShowLabel=true>
</Display>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.DriveInfo" Ignore=true>
</EditorItem>
</FieldItems>
</EditorFormObject>
</BodyTemplate>
</Card>
</div>
<div class="col-12 col-md-8">
<Card IsShadow=true class="m-2 flex-fill" Color="Color.Primary">
<HeaderTemplate>
@Localizer["HardwareInfo"]
</HeaderTemplate>
<BodyTemplate>
<div class="d-flex align-items-center justify-content-center w-100 h-100" >
<div class="row g-2 mx-1 form-inline d-flex justify-content-center w-100">
<div class="col-12 col-md-4 justify-content-center h-100" >
@{
var item = HardwareJob.HardwareInfo.MachineInfo.CpuRate;
}
<div class="d-flex flex-column align-items-center">
<Circle Width="200" class="m-3"
Value=@((int)(item*100<=100?item*100:100))
Color=@((item*100>70?Color.Warning:Color.Success))
StrokeWidth="4" ShowProgress=false>
<div class="circle-hardware">
<span>
@Localizer["CpuUsage"] <i> @((item * 100).ToString("F0") + " %")</i>
</span>
</div>
</Circle>
<div class="mt-1">
<span>@(HardwareJob.HardwareInfo.MachineInfo.Processor)</span>
</div>
</div>
</div>
<div class="col-12 col-md-4 justify-content-center h-100">
@{
var availableMemory = HardwareJob.HardwareInfo.MachineInfo.AvailableMemory;
var memory = HardwareJob.HardwareInfo.MachineInfo.Memory;
}
<div class="d-flex flex-column align-items-center ">
<Circle Width="200" class="m-3"
Value=@((int)(memory>availableMemory?100 - (availableMemory * 100.00 / memory):100))
Color=@((availableMemory * 100.00 / memory<20?Color.Warning:Color.Success))
StrokeWidth="4">
<div class="circle-hardware">
<h6> @((100 - (availableMemory * 100.00 / memory)).ToString("F2") + " %") </h6>
<span> @Localizer["WorkingSet"] <i> @(HardwareJob.HardwareInfo.WorkingSet + " MB")</i></span>
<span> @Localizer["AvailableMemory"] <i> @((availableMemory / 1024.00 / 1024).ToString("F2") + " GB")</i></span>
<span> @Localizer["TotalMemory"] <i> @((memory / 1024.00 / 1024).ToString("F2") + " GB")</i></span>
</div>
</Circle>
<div class="mt-1">
<span> @Localizer["MemoryUsage"] </span>
</div>
</div>
</div>
<div class="col-12 col-md-4 justify-content-center h-100">
@{
var totalFreeSpace = HardwareJob.HardwareInfo.DriveInfo.TotalFreeSpace;
var totalSize = HardwareJob.HardwareInfo.DriveInfo.TotalSize;
}
<div class="d-flex flex-column align-items-center ">
<Circle Width="200" class="m-3"
Value=@((int)(totalSize>totalFreeSpace?100 - (totalFreeSpace * 100.00 / totalSize):100))
Color=@((totalFreeSpace * 100.00 / totalSize<20?Color.Warning:Color.Success))
StrokeWidth="4">
<div class="circle-hardware">
<h6> @((100 - (totalFreeSpace * 100.00 / totalSize)).ToString("F2") + " %")</h6>
<span> @(HardwareJob.HardwareInfo.DriveInfo.VolumeLabel) </span>
<span> @Localizer["AvailableDisk"] <i> @((totalFreeSpace / 1024.00 / 1024 / 1024).ToString("F2") + " GB")</i></span>
<span> @Localizer["TotalDisk"] <i> @((totalSize / 1024.00 / 1024 / 1024).ToString("F2") + " GB")</i></span>
</div>
</Circle>
<div class="mt-1">
<span> @Localizer["DiskUsage"] </span>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-between align-items-center w-100">
<span>@Localizer["SystemInfo"]</span>
<small class="text-muted">
@HardwareJob.HardwareInfo.UpdateTime.ToString("yyyy-MM-dd HH:mm:ss")
</small>
</div>
</HeaderTemplate>
<BodyTemplate>
<EditorForm class="mt-3" AutoGenerateAllItem="false" IsDisplay Model="HardwareJob.HardwareInfo" ItemsPerRow=3 RowType=RowType.Inline LabelWidth=300>
<FieldItems>
<EditorItem @bind-Field="@context.OSName" GroupName="@Localizer["SystemInfo"]" />
<EditorItem @bind-Field="@context.OsArchitecture" GroupName="@Localizer["SystemInfo"]" />
<EditorItem @bind-Field="@context.OSVersion" GroupName="@Localizer["SystemInfo"]" />
<EditorItem @bind-Field="@context.Environment" GroupName="@Localizer["SystemInfo"]" />
<EditorItem @bind-Field="@context.FrameworkDescription" GroupName="@Localizer["SystemInfo"]" />
<EditorItem @bind-Field="@context.UUID" GroupName="@Localizer["SystemInfo"]" />
<EditorItem @bind-Field="@context.SystemRunTotalMinute" GroupName="@Localizer["SystemInfo"]" />
<EditorItem @bind-Field="@context.AppRunTotalMinute" GroupName="@Localizer["SystemInfo"]" />
<EditorItem @bind-Field="@context.Memory" GroupName="@Localizer["GCMemoryInfoConfig"]" GroupOrder=2 />
<EditorItem @bind-Field="@context.TotalAvailableMemory" GroupName="@Localizer["GCMemoryInfoConfig"]" GroupOrder=2 />
<EditorItem @bind-Field="@context.HighMemoryLoadThreshold" GroupName="@Localizer["GCMemoryConfig"]" GroupOrder=2 />
<EditorItem TValue="bool" TModel="HardwareInfo" @bind-Field="@context.IsServerGC" GroupName="@Localizer["GCMemoryConfig"]" GroupOrder=2>
<EditTemplate Context="value">
<div class="col-12">
<h6></h6>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.IsServerGC" GroupName="@Localizer["GCMemoryInfoConfig"]" GroupOrder=2 />
<EditorItem @bind-Field="@context.GCLatencyMode" GroupName="@Localizer["GCMemoryInfoConfig"]" GroupOrder=2 />
<EditorItem @bind-Field="@context.WorkingSet" GroupName="@Localizer["GCMemoryInfo"]" GroupOrder=3 />
<EditorItem @bind-Field="@context.PrivateMemory" GroupName="@Localizer["GCMemoryInfo"]" GroupOrder=3 />
<EditorItem @bind-Field="@context.PeakWorkingSet" GroupName="@Localizer["GCMemoryInfo"]" GroupOrder=3 />
<EditorItem TValue="ulong" TModel="HardwareInfo" @bind-Field="@context.HeapSize" GroupName="@Localizer["GCMemoryInfo"]" GroupOrder=3>
<EditTemplate Context="value">
<div class="col-12">
<h6></h6>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.HeapSize" GroupName="@Localizer["GCMemoryInfo"]" GroupOrder=3 />
<EditorItem @bind-Field="@context.TotalMemory" GroupName="@Localizer["GCMemoryInfo"]" GroupOrder=3 />
<EditorItem @bind-Field="@context.FragmentedBytes" GroupName="@Localizer["GCMemoryInfo"]" GroupOrder=3 />
<EditorItem @bind-Field="@context.CommittedBytes" GroupName="@Localizer["GCMemoryInfo"]" GroupOrder=3 />
<EditorItem TValue="ulong" TModel="HardwareInfo" @bind-Field="@context.AvailableMemory" GroupName="@Localizer["GCMemoryInfo"]" GroupOrder=3>
<EditTemplate Context="value">
<div class="col-12">
<h6></h6>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.AvailableMemory" GroupName="@Localizer["GCMemoryInfo"]" GroupOrder=3 />
<EditorItem @bind-Field="@context.GCAvailableMemory" GroupName="@Localizer["GCMemoryInfo"]" GroupOrder=3 />
<EditorItem @bind-Field="@context.VirtualMemory" GroupName="@Localizer["GCMemoryInfo"]" GroupOrder=3 />
<EditorItem @bind-Field="@context.GcGen0Count" GroupName="@Localizer["GCAnalyzeInfo"]" GroupOrder=4 />
<EditorItem @bind-Field="@context.GcGen1Count" GroupName="@Localizer["GCAnalyzeInfo"]" GroupOrder=4 />
<EditorItem @bind-Field="@context.GcGen2Count" GroupName="@Localizer["GCAnalyzeInfo"]" GroupOrder=4 />
<EditorItem TValue="ulong" TModel="HardwareInfo" @bind-Field="@context.TotalAllocatedBytes" GroupName="@Localizer["GCAnalyzeInfo"]" GroupOrder=4>
<EditTemplate Context="value">
<div class="col-12">
<h6></h6>
</div>
</EditTemplate>
</EditorItem>
<EditorItem @bind-Field="@context.TotalAllocatedBytes" GroupName="@Localizer["GCAnalyzeInfo"]" GroupOrder=4 />
<EditorItem @bind-Field="@context.TotalPauseDurationMs" GroupName="@Localizer["GCAnalyzeInfo"]" GroupOrder=4 />
</FieldItems>
</EditorForm>
</BodyTemplate>
</Card>
</div>
</div>
<div class="row g-2 mx-1 form-inline mt-2">
<div class="col-12 col-md-12">
<Card IsShadow=true class="m-2 flex-fill" Color="Color.Primary">
<HeaderTemplate>
@Localizer["HardwareInfo"]
</HeaderTemplate>
<BodyTemplate>
<div class="d-flex align-items-center justify-content-center w-100 h-100">
<div class="row g-2 mx-1 form-inline d-flex justify-content-center w-100">
<!-- ✅ CPU 使用率 -->
<div class="col-12 col-md-4 justify-content-center h-100">
@{
var cpuUsage = HardwareJob.HardwareInfo.CpuRate;
}
<div class="d-flex flex-column align-items-center">
<Circle Width="200" class="m-3"
Value=@((int)(cpuUsage*100<=100?cpuUsage*100:100))
Color=@((cpuUsage*100>70?Color.Warning:Color.Success))
StrokeWidth="4" ShowProgress=false>
<div class="circle-hardware">
<span>
@Localizer["CpuUsage"] <i>@((cpuUsage * 100).ToString("F0") + " %")</i>
</span>
</div>
</Circle>
<div class="mt-1">
<span>@(HardwareJob.HardwareInfo.Processor)</span>
</div>
</div>
</div>
<!-- ✅ 内存使用率 -->
<div class="col-12 col-md-4 justify-content-center h-100">
@{
var availableMemory = HardwareJob.HardwareInfo.AvailableMemory;
var memory = HardwareJob.HardwareInfo.Memory;
}
<div class="d-flex flex-column align-items-center">
<Circle Width="200" class="m-3"
Value=@((int)(memory>availableMemory?100 - (availableMemory * 100.00 / memory):100))
Color=@((availableMemory * 100.00 / memory<20?Color.Warning:Color.Success))
StrokeWidth="4">
<div class="circle-hardware">
<h6>@((100 - (availableMemory * 100.00 / memory)).ToString("F2") + " %")</h6>
<span>@Localizer["WorkingSet"] <i>@(HardwareJob.HardwareInfo.WorkingSet + " MB")</i></span>
<span>@Localizer["AvailableMemory"] <i>@((availableMemory / 1024.00).ToString("F2") + " GB")</i></span>
<span>@Localizer["TotalMemory"] <i>@((memory / 1024.00).ToString("F2") + " GB")</i></span>
</div>
</Circle>
<div class="mt-1">
<span>@Localizer["MemoryUsage"]</span>
</div>
</div>
</div>
<!-- ✅ 磁盘使用率 -->
<div class="col-12 col-md-4 justify-content-center h-100">
@{
var totalFreeSpace = HardwareJob.HardwareInfo.DriveInfo.TotalFreeSpace;
var totalSize = HardwareJob.HardwareInfo.DriveInfo.TotalSize;
}
<div class="d-flex flex-column align-items-center">
<Circle Width="200" class="m-3"
Value=@((int)(totalSize>totalFreeSpace?100 - (totalFreeSpace * 100.00 / totalSize):100))
Color=@((totalFreeSpace * 100.00 / totalSize<20?Color.Warning:Color.Success))
StrokeWidth="4">
<div class="circle-hardware">
<h6>@((100 - (totalFreeSpace * 100.00 / totalSize)).ToString("F2") + " %")</h6>
<span>@(HardwareJob.HardwareInfo.DriveInfo.VolumeLabel)</span>
<span>@Localizer["AvailableDisk"] <i>@((totalFreeSpace / 1024.00 / 1024 / 1024).ToString("F2") + " GB")</i></span>
<span>@Localizer["TotalDisk"] <i>@((totalSize / 1024.00 / 1024 / 1024).ToString("F2") + " GB")</i></span>
</div>
</Circle>
<div class="mt-1">
<span>@Localizer["DiskUsage"]</span>
</div>
</div>
</div>
</div>
</div>
</BodyTemplate>
</Card>
</div>
</div>
<div class="row g-2 mx-1 form-inline">
<div class="col-12 col-md-12">
@@ -140,7 +198,7 @@
</HeaderTemplate>
<BodyTemplate>
<Chart @ref=LineChart OnInitAsync="OnInit" Height="var(--line-chart-height)" Width="100%" OnAfterInitAsync="()=>{chartInit=true;return Task.CompletedTask;}" />
<Chart @ref=LineChart OnInitAsync="OnInit" Height="var(--line-chart-height)" Width="100%" OnAfterInitAsync="() => { chartInit = true; return Task.CompletedTask; }" />
</BodyTemplate>
</Card>
</div>

View File

@@ -72,7 +72,7 @@ public partial class HardwareInfoPage : IDisposable
ChartDataSource.Options.Title = Localizer[nameof(HistoryHardwareInfo)];
ChartDataSource.Options.X.Title = Localizer["DateTime"];
ChartDataSource.Options.Y.Title = Localizer["Data"];
ChartDataSource.Labels = hisHardwareInfos.Select(a => a.Date.ToString("dd HH:mm zz"));
ChartDataSource.Labels = hisHardwareInfos.Select(a => a.Date.ToString("dd-HH:mm"));
ChartDataSource.Data.Add(new ChartDataset()
{
Tension = 0.4f,
@@ -116,7 +116,7 @@ public partial class HardwareInfoPage : IDisposable
else
{
var hisHardwareInfos = await HardwareJob.GetHistoryHardwareInfos();
ChartDataSource.Labels = hisHardwareInfos.Select(a => a.Date.ToString("dd HH:mm zz"));
ChartDataSource.Labels = hisHardwareInfos.Select(a => a.Date.ToString("dd-HH:mm"));
ChartDataSource.Data[0].Data = hisHardwareInfos.Select(a => (object)a.CpuUsage);
ChartDataSource.Data[1].Data = hisHardwareInfos.Select(a => (object)a.MemoryUsage);
ChartDataSource.Data[2].Data = hisHardwareInfos.Select(a => (object)a.DriveUsage);

View File

@@ -15,7 +15,7 @@
</Card>
</div>
<div class="col-12 col-sm-10 h-100">
<div class="col-12 col-sm-10 h-100 ps-2">
<AdminTable @ref=table TItem="SysUser"
AutoGenerateColumns="true"
ShowAdvancedSearch=false

View File

@@ -20,5 +20,6 @@ public class Startup : AppStartup
services.AddScoped<IMenuService, MenuService>();
services.AddScoped<IAuthRazorService, AuthRazorService>();
services.AddBootstrapBlazorTableExportService();
services.AddBootstrapBlazorWinBoxService();
}
}

View File

@@ -5,19 +5,21 @@
<ItemGroup>
<ProjectReference Include="..\ThingsGateway.Admin.Application\ThingsGateway.Admin.Application.csproj" />
<PackageReference Include="BootstrapBlazor.Chart" Version="9.0.0" />
<PackageReference Include="BootstrapBlazor.Chart" Version="9.0.4" />
<PackageReference Include="BootstrapBlazor.UniverSheet" Version="9.0.5" />
<PackageReference Include="BootstrapBlazor.WinBox" Version="9.0.7" />
<PackageReference Include="BootstrapBlazor.CodeEditor" Version="9.0.3" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)'=='net8.0'">
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="$(NET8Version)" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)'=='net9.0'">
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="$(NET9Version)" />
<ItemGroup Condition="'$(TargetFramework)'=='net10.0'">
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="$(NET10Version)" />
</ItemGroup>
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>net8.0;$(OtherTargetFrameworks);</TargetFrameworks>
<!--<UseRazorSourceGenerator>false</UseRazorSourceGenerator>-->
</PropertyGroup>

View File

@@ -41,15 +41,22 @@ public static class AdminResourceUtil
return items
.Where(it => it.ParentId == parentId)
.Select((item, index) =>
new MenuItem()
{
var menu = new MenuItem()
{
Match = item.NavLinkMatch ?? Microsoft.AspNetCore.Components.Routing.NavLinkMatch.All,
Match = item.NavLinkMatch ?? Microsoft.AspNetCore.Components.Routing.NavLinkMatch.Prefix,
Text = item.Title,
Icon = item.Icon,
Url = item.Href,
Target = item.Target.ToString(),
Items = BuildMenuTrees(items, item.Id).ToList()
};
if (menu.Url.IsNullOrEmpty())
{
menu.Match = Microsoft.AspNetCore.Components.Routing.NavLinkMatch.Prefix;
}
return menu;
}
);
}

View File

@@ -29,7 +29,7 @@
<Target Name="AdminPostPublish" AfterTargets="Publish">
<ItemGroup>
<!-- setting up the variable for convenience -->
<AdminFiles Include="bin\$(Configuration)\$(TargetFramework)\SeedData\**" />
<AdminFiles Include="$(OutputPath)\$(TargetFramework)\SeedData\**" />
</ItemGroup>
<PropertyGroup>
</PropertyGroup>

View File

@@ -21,11 +21,13 @@
<link rel="apple-touch-icon" href="favicon.png">
<base href="/" />
<title>ThingsGateway</title>
<link rel="stylesheet" href=@($"_content/BootstrapBlazor.FontAwesome/css/font-awesome.min.css?v={this.GetType().Assembly.GetName().Version}") />
<link rel="stylesheet" href=@($"_content/BootstrapBlazor/css/bootstrap.blazor.bundle.min.css?v={this.GetType().Assembly.GetName().Version}") />
<link rel="stylesheet" href=@($"_content/BootstrapBlazor/css/motronic.min.css?v={this.GetType().Assembly.GetName().Version}") />
<link rel="stylesheet" href=@($"ThingsGateway.AdminServer.styles.css?v={this.GetType().Assembly.GetName().Version}") />
<link rel="stylesheet" href=@($"{WebsiteConst.DefaultResourceUrl}css/site.css?v={this.GetType().Assembly.GetName().Version}") />
<link rel="stylesheet" href=@($"_content/BootstrapBlazor.FontAwesome/css/font-awesome.min.css") />
<link rel="stylesheet" href=@($"_content/BootstrapBlazor/css/bootstrap.blazor.bundle.min.css") />
<link rel="stylesheet" href=@($"_content/BootstrapBlazor/css/motronic.min.css") />
<link rel="stylesheet" href=@($"ThingsGateway.AdminServer.styles.css") />
<link rel="stylesheet" href=@($"{WebsiteConst.DefaultResourceUrl}css/site.css") />
<link rel="stylesheet" href=@($"{WebsiteConst.DefaultResourceUrl}css/devui.css") />
@* <script src=@($"{WebsiteConst.DefaultResourceUrl}js/theme.js") type="module"></script><!-- 初始主题 --> *@
<!-- PWA Manifest -->
<link rel="manifest" href="./manifest.json" />
@@ -38,12 +40,13 @@
<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/localStorageUtil.js?v={this.GetType().Assembly.GetName().Version}")></script>
<script src=@($"_content/BootstrapBlazor/js/bootstrap.blazor.bundle.min.js")></script>
<script src=@($"{WebsiteConst.DefaultResourceUrl}js/localStorageUtil.js")></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>
<script src="pwa-install.js"></script>
</body>
</html>

View File

@@ -70,7 +70,7 @@
<Button @onclick="ShowAbout" class="layout-header-bar d-none d-lg-flex px-2" Icon="fa fa-info" Color="Color.None" TooltipText="@Localizer[nameof(About)]" />
}
@* 版本号 *@
<div class="px-1 navbar-header-text d-none d-lg-block">@_versionString</div>
<div class="px-1 navbar-header-text text-nowrap d-none d-lg-block">@_versionString</div>
@* 主题切换 *@
@* <ThemeToggle /> *@
@@ -89,12 +89,19 @@
</div>
</Side>
<Main>
<Tab @ref=_tab ClickTabToNavigation="true" ShowToolbar="true" ShowContextMenu="true" ShowContextMenuFullScreen="true" ShowExtendButtons="false" ShowClose="true" AllowDrag=true
AdditionalAssemblies="@App.RazorAssemblies" Menus="@MenuService.AllOwnMenuItems"
DefaultUrl=@("/") Body=@(Body!) OnCloseTabItemAsync=@((a)=>
{
return Task.FromResult(!(a.Url=="/"||a.Url.IsNullOrEmpty()));
})>
<Tab @ref=_tab ClickTabToNavigation="true" ShowToolbar="true" ShowContextMenu="true" ShowExtendButtons="false" ShowClose="true" AllowDrag=true
ShowFullscreenToolbarButton=false ShowContextMenuFullScreen=false ShowFullScreen=false AdditionalAssemblies="@App.RazorAssemblies" Menus="@MenuService.AllOwnMenuItems"
DefaultUrl=@("/") Body=@(Body!) OnCloseTabItemAsync=@((a)=>
{
return Task.FromResult(!(a.Url == "/" || a.Url.IsNullOrEmpty()));
})
>
<BeforeContextMenuTemplate>
<ContextMenuItem Icon="fa fa-window-restore" Text="@Localizer["WindowRestore"]" OnClick="WinboxRender"></ContextMenuItem>
<ContextMenuDivider></ContextMenuDivider>
</BeforeContextMenuTemplate>
</Tab>
</Main>
<NotAuthorized>

View File

@@ -120,6 +120,38 @@ public partial class MainLayout : IDisposable
#endregion
private async Task WinboxRender(ContextMenuItem item, object? context)
{
if (context is TabItem tabItem)
{
await WinboxRender(tabItem.ChildContent, tabItem.Text);
//await _tab.RemoveTab(tabItem);
}
}
[Inject]
[NotNull]
private WinBoxService? WinBoxService { get; set; }
private async Task WinboxRender(RenderFragment item, string title)
{
if (item != null)
{
var option = new WinBoxOption()
{
Title = title,
ContentTemplate = item,
Max = false,
Width = "80%",
Height = "80%",
Top = "0%",
Left = "10%",
Background = "var(--bb-primary-color)",
Overflow = true
};
await WinBoxService.Show(option);
}
}
private string _versionString = string.Empty;
[Inject]
[NotNull]

View File

@@ -15,6 +15,7 @@ using System.Text;
using ThingsGateway.Admin.Application;
using ThingsGateway.DB;
using ThingsGateway.NewLife;
using ThingsGateway.NewLife.Log;
namespace ThingsGateway.AdminServer;
@@ -64,7 +65,7 @@ public class Program
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
builder.Host.UseSystemd();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
if (Runtime.IsLegacyWindows)
builder.Logging.ClearProviders(); //去除默认的事件日志提供者,某些情况下会日志输出异常,导致程序崩溃
}).ConfigureBuilder(builder =>
{

View File

@@ -92,7 +92,8 @@ public class Startup : AppStartup
options.RootComponents.MaxJSRootComponents = 500;
options.JSInteropDefaultCallTimeout = TimeSpan.FromMinutes(2);
options.MaxBufferedUnacknowledgedRenderBatches = 20;
options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(10);
options.DisconnectedCircuitMaxRetained = 1;
options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromSeconds(10);
})
.AddHubOptions(options =>
{
@@ -103,6 +104,7 @@ public class Startup : AppStartup
options.ClientTimeoutInterval = TimeSpan.FromMinutes(2);
options.KeepAliveInterval = TimeSpan.FromSeconds(15);
options.HandshakeTimeout = TimeSpan.FromSeconds(30);
});
#else
@@ -112,7 +114,8 @@ public class Startup : AppStartup
options.RootComponents.MaxJSRootComponents = 500;
options.JSInteropDefaultCallTimeout = TimeSpan.FromMinutes(2);
options.MaxBufferedUnacknowledgedRenderBatches = 20;
options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(10);
options.DisconnectedCircuitMaxRetained = 1;
options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromSeconds(10);
}).AddHubOptions(options =>
{
//单个传入集线器消息的最大大小。默认 32 KB
@@ -132,7 +135,11 @@ public class Startup : AppStartup
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.All;
#if NET10_0_OR_GREATER
options.KnownIPNetworks.Clear();
#else
options.KnownNetworks.Clear();
#endif
options.KnownProxies.Clear();
});
@@ -183,25 +190,37 @@ public class Startup : AppStartup
services.AddScoped<IAuthorizationHandler, BlazorServerAuthenticationHandler>();
services.AddScoped<AuthenticationStateProvider, BlazorServerAuthenticationStateProvider>();
if (!NewLife.Runtime.IsLegacyWindows)
{
#if NET9_0_OR_GREATER
var certificate = X509CertificateLoader.LoadPkcs12FromFile("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
var certificate = X509CertificateLoader.LoadPkcs12FromFile("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
#else
var certificate = new X509Certificate2("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
var certificate = new X509Certificate2("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
#endif
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("Keys"))
.ProtectKeysWithCertificate(certificate)
.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
{
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
});
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("Keys"))
.ProtectKeysWithCertificate(certificate)
.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
{
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
});
}
}
public void Use(IApplicationBuilder applicationBuilder, IWebHostEnvironment env)
{
var app = (WebApplication)applicationBuilder;
app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All, KnownNetworks = { }, KnownProxies = { } });
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.All,
#if NET10_0_OR_GREATER
KnownIPNetworks = { },
#else
KnownNetworks = { },
#endif
KnownProxies = { }
});
app.UseBootstrapBlazor();
// 启用本地化

View File

@@ -4,7 +4,7 @@
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
<TargetFrameworks>net8.0;$(OtherTargetFrameworks);</TargetFrameworks>
</PropertyGroup>
@@ -15,15 +15,11 @@
<PublishReadyToRunComposite>true</PublishReadyToRunComposite>
<ApplicationIcon>wwwroot\favicon.ico</ApplicationIcon>
<CETCompat>false</CETCompat>
<ServerGarbageCollection>true</ServerGarbageCollection>
<!--动态适用GC-->
<GarbageCollectionAdaptationMode>1</GarbageCollectionAdaptationMode>
<CETCompat>false</CETCompat>
<!--使用自托管线程池-->
<!--<UseWindowsThreadPool>false</UseWindowsThreadPool> -->
<!--使用工作站GC-->
<!--<ServerGarbageCollection>true</ServerGarbageCollection>-->
<!--<PlatformTarget>x86</PlatformTarget>-->
</PropertyGroup>
@@ -53,9 +49,9 @@
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.1" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="$(NET9Version)" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="$(NET9Version)" />
<ItemGroup Condition=" '$(TargetFramework)' == 'net10.0' ">
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="$(NET10Version)" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="$(NET10Version)" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,34 @@
let installPromptTriggered = false;
function getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : null;
}
function hasShownInstallPrompt() {
return getCookie("tgPWAInstallPromptShown") === "true";
}
function markInstallPromptShown() {
document.cookie = "tgPWAInstallPromptShown=true; max-age=31536000; path=/";
}
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
if (!hasShownInstallPrompt() && !installPromptTriggered) {
installPromptTriggered = true;
setTimeout(() => {
e.prompt()
.then(() => e.userChoice)
.then(choiceResult => {
markInstallPromptShown();
})
.catch(err => {
// 可选错误处理
});
}, 2000); // 延迟 2 秒提示
} else {
// console.log("已提示过安装,不再弹出");
}
});

View File

@@ -1,4 +1,4 @@
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
@@ -477,7 +477,7 @@ public class ConcurrentList<T> : IList<T>, IReadOnlyList<T>
{
lock (((ICollection)m_list).SyncRoot)
{
return m_list.IndexOf(item);
return m_list.LastIndexOf(item);
}
}

View File

@@ -30,9 +30,27 @@ public class ImportPreviewOutputBase
/// <summary>
/// 返回状态
/// </summary>
public ConcurrentList<(int Row, bool Success, string? ErrorMessage)> Results { get; set; } = new();
public ConcurrentList<ImportPreviewResult> Results { get; set; } = new();
}
public class ImportPreviewResult
{
public ImportPreviewResult()
{
}
public ImportPreviewResult(int row, bool success, string error)
{
this.Row = row;
this.Success = success;
this.ErrorMessage = error;
}
public int Row { get; set; }
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
}
/// <summary>
/// 导入预览
/// </summary>

View File

@@ -31,8 +31,8 @@ public static class GenericExtensions
// 比较oldModel和model的属性找出差异
var differences = properties
.Where(prop => prop.CanRead && prop.CanWrite) // 确保属性可读可写
.Where(prop => !Equals(prop.GetValue(oldModel), prop.GetValue(model))) // 找出值不同的属性
.Where(prop => prop.CanRead && prop.CanWrite && !Equals(prop.GetValue(oldModel), prop.GetValue(model))) // 确保属性可读可写
// 找出值不同的属性
.ToDictionary(prop => prop.Name, prop => prop.GetValue(model)); // 将属性名和新值存储到字典中
// 应用差异到channels列表中的每个Channel对象

View File

@@ -16,6 +16,8 @@ using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.RegularExpressions;
using ThingsGateway.Extension;
namespace ThingsGateway.Common.Extension;
/// <summary>
/// 对象拓展类
@@ -48,113 +50,7 @@ public static class ObjectExtensions
bool IsTheRawGenericType(Type type) => generic == (type.IsGenericType ? type.GetGenericTypeDefinition() : type);
}
/// <summary>
/// 将 DateTimeOffset 转换成本地 DateTime
/// </summary>
/// <param name="dateTime"></param>
/// <returns></returns>
public static DateTime ConvertToDateTime(this DateTimeOffset dateTime)
{
if (dateTime.Offset.Equals(TimeSpan.Zero))
return dateTime.UtcDateTime;
if (dateTime.Offset.Equals(TimeZoneInfo.Local.GetUtcOffset(dateTime.DateTime)))
return dateTime.ToLocalTime().DateTime;
else
return dateTime.DateTime;
}
/// <summary>
/// 将 DateTimeOffset? 转换成本地 DateTime?
/// </summary>
/// <param name="dateTime"></param>
/// <returns></returns>
public static DateTime? ConvertToDateTime(this DateTimeOffset? dateTime)
{
return dateTime.HasValue ? dateTime.Value.ConvertToDateTime() : null;
}
/// <summary>
/// 将 DateTime 转换成 DateTimeOffset
/// </summary>
/// <param name="dateTime"></param>
/// <returns></returns>
public static DateTimeOffset ConvertToDateTimeOffset(this DateTime dateTime)
{
return DateTime.SpecifyKind(dateTime, DateTimeKind.Local);
}
/// <summary>
/// 将 DateTime? 转换成 DateTimeOffset?
/// </summary>
/// <param name="dateTime"></param>
/// <returns></returns>
public static DateTimeOffset? ConvertToDateTimeOffset(this DateTime? dateTime)
{
return dateTime.HasValue ? dateTime.Value.ConvertToDateTimeOffset() : null;
}
/// <summary>
/// 将流保存到本地磁盘
/// </summary>
/// <param name="stream"></param>
/// <param name="path"></param>
/// <returns></returns>
public static void CopyToSave(this Stream stream, string path)
{
// 空检查
if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullException(nameof(path));
using var fileStream = File.Create(path);
stream.CopyTo(fileStream);
}
/// <summary>
/// 将字节数组保存到本地磁盘
/// </summary>
/// <param name="bytes"></param>
/// <param name="path"></param>
/// <returns></returns>
public static void CopyToSave(this byte[] bytes, string path)
{
using var stream = new MemoryStream(bytes);
stream.CopyToSave(path);
}
/// <summary>
/// 将流保存到本地磁盘
/// </summary>
/// <param name="stream"></param>
/// <param name="path">需包含文件名完整路径</param>
/// <returns></returns>
public static async Task CopyToSaveAsync(this Stream stream, string path)
{
// 空检查
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentNullException(nameof(path));
}
// 文件名判断
if (string.IsNullOrWhiteSpace(Path.GetFileName(path)))
{
throw new ArgumentException("The parameter of <path> parameter must include the complete file name.");
}
using var fileStream = File.Create(path);
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
}
/// <summary>
/// 将字节数组保存到本地磁盘
/// </summary>
/// <param name="bytes"></param>
/// <param name="path"></param>
/// <returns></returns>
public static async Task CopyToSaveAsync(this byte[] bytes, string path)
{
using var stream = new MemoryStream(bytes);
await stream.CopyToSaveAsync(path).ConfigureAwait(false);
}
/// <summary>
/// 合并两个字典
@@ -186,7 +82,7 @@ public static class ObjectExtensions
/// <typeparam name="T"></typeparam>
/// <param name="dic">字典</param>
/// <param name="newDic">新字典</param>
internal static void AddOrUpdate<T>(this ConcurrentDictionary<string, T> dic, Dictionary<string, T> newDic)
internal static void AddOrUpdate<T>(this NonBlockingDictionary<string, T> dic, Dictionary<string, T> newDic)
{
foreach (var (key, value) in newDic)
{

View File

@@ -24,7 +24,7 @@ public static class ParallelExtensions
/// <typeparam name="T">集合元素类型</typeparam>
/// <param name="source">要操作的集合</param>
/// <param name="body">要执行的操作</param>
public static void ParallelForEach<T>(this IList<T> source, Action<T> body)
public static void ParallelForEach<T>(this IEnumerable<T> source, Action<T> body)
{
ParallelOptions options = new();
options.MaxDegreeOfParallelism = Environment.ProcessorCount;
@@ -38,7 +38,7 @@ public static class ParallelExtensions
/// <typeparam name="T">集合元素类型</typeparam>
/// <param name="source">要操作的集合</param>
/// <param name="body">要执行的操作</param>
public static void ParallelForEach<T>(this IList<T> source, Action<T, ParallelLoopState, long> body)
public static void ParallelForEach<T>(this IEnumerable<T> source, Action<T, ParallelLoopState, long> body)
{
ParallelOptions options = new();
options.MaxDegreeOfParallelism = Environment.ProcessorCount;
@@ -53,7 +53,7 @@ public static class ParallelExtensions
/// <param name="source">要操作的集合</param>
/// <param name="body">要执行的操作</param>
/// <param name="parallelCount">最大并行度</param>
public static void ParallelForEach<T>(this IList<T> source, Action<T> body, int parallelCount)
public static void ParallelForEach<T>(this IEnumerable<T> source, Action<T> body, int parallelCount)
{
// 创建并行操作的选项对象,设置最大并行度为指定的值
var options = new ParallelOptions();
@@ -109,7 +109,7 @@ public static class ParallelExtensions
/// <param name="parallelCount">最大并行度</param>
/// <param name="cancellationToken">取消操作的标志</param>
/// <returns>表示异步操作的任务</returns>
public static Task ParallelForEachAsync<T>(this IList<T> source, Func<T, CancellationToken, ValueTask> body, int parallelCount, CancellationToken cancellationToken = default)
public static Task ParallelForEachAsync<T>(this IEnumerable<T> source, Func<T, CancellationToken, ValueTask> body, int parallelCount, CancellationToken cancellationToken = default)
{
// 创建并行操作的选项对象,设置最大并行度和取消标志
var options = new ParallelOptions();
@@ -126,7 +126,7 @@ public static class ParallelExtensions
/// <param name="body">异步执行的操作</param>
/// <param name="cancellationToken">取消操作的标志</param>
/// <returns>表示异步操作的任务</returns>
public static Task ParallelForEachAsync<T>(this IList<T> source, Func<T, CancellationToken, ValueTask> body, CancellationToken cancellationToken = default)
public static Task ParallelForEachAsync<T>(this IEnumerable<T> source, Func<T, CancellationToken, ValueTask> body, CancellationToken cancellationToken = default)
{
return ParallelForEachAsync(source, body, Environment.ProcessorCount, cancellationToken);
}

View File

@@ -27,7 +27,7 @@ internal class CacheManager
{
private IMemoryCache Cache { get; set; }
private IServiceProvider Provider { get; set; }
private static IServiceProvider Provider => App.RootServices;
[NotNull]
private static CacheManager? Instance { get; set; }
@@ -40,8 +40,7 @@ internal class CacheManager
static CacheManager()
{
Instance = new();
Instance.Provider = App.RootServices;
Instance.Cache = Instance.Provider.GetRequiredService<IMemoryCache>();
Instance.Cache = Provider.GetRequiredService<IMemoryCache>();
Options = App.RootServices.GetRequiredService<IOptions<BootstrapBlazorOptions>>().Value;
}
@@ -105,37 +104,6 @@ internal class CacheManager
}
}
/// <summary>
/// 设置 App 开始时间
/// </summary>
public void SetStartTime() => SetStartTime(DateTimeOffset.Now);
/// <summary>
/// 设置 App 开始时间
/// </summary>
private void SetStartTime(DateTimeOffset startDateTimeOffset)
{
GetOrCreate("BootstrapBlazor_StartTime", entry =>
{
entry.Priority = CacheItemPriority.NeverRemove;
return startDateTimeOffset;
});
}
/// <summary>
/// 获取 App 开始时间
/// </summary>
/// <returns></returns>
public DateTimeOffset GetStartTime()
{
var ret = DateTimeOffset.MinValue;
if (Cache.TryGetValue("BootstrapBlazor_StartTime", out var v) && v is DateTimeOffset d)
{
ret = d;
}
return ret;
}
/// <summary>
/// 获得 缓存数量
/// </summary>
@@ -236,7 +204,7 @@ internal class CacheManager
/// <returns></returns>
public static IStringLocalizer? CreateLocalizerByType(Type resourceSource) => resourceSource.Assembly.IsDynamic
? null
: Instance.Provider.GetRequiredService<IStringLocalizerFactory>().Create(resourceSource);
: Provider.GetRequiredService<IStringLocalizerFactory>().Create(resourceSource);
/// <summary>
/// 获得 <see cref="JsonLocalizationOptions"/> 值
@@ -244,7 +212,7 @@ internal class CacheManager
/// <returns></returns>
private static JsonLocalizationOptions GetJsonLocalizationOption()
{
var localizationOptions = Instance.Provider.GetRequiredService<IOptions<JsonLocalizationOptions>>();
var localizationOptions = Provider.GetRequiredService<IOptions<JsonLocalizationOptions>>();
return localizationOptions.Value;
}
/// <summary>
@@ -253,7 +221,7 @@ internal class CacheManager
/// <returns></returns>
private static BootstrapBlazorOptions GetBootstrapBlazorOption()
{
var localizationOptions = Instance.Provider.GetRequiredService<IOptions<BootstrapBlazorOptions>>();
var localizationOptions = Provider.GetRequiredService<IOptions<BootstrapBlazorOptions>>();
return localizationOptions.Value;
}
/// <summary>
@@ -269,7 +237,7 @@ internal class CacheManager
return null;
}
IStringLocalizer? ret = null;
var factories = Instance.Provider.GetServices<IStringLocalizerFactory>();
var factories = Provider.GetServices<IStringLocalizerFactory>();
var factory = factories.LastOrDefault(a => a is not JsonStringLocalizerFactory);
if (factory != null)
{
@@ -345,7 +313,7 @@ internal class CacheManager
/// <param name="typeName"></param>
/// <param name="includeParentCultures"></param>
/// <returns></returns>
public static IEnumerable<LocalizedString> GetTypeStringsFromResolve(string typeName, bool includeParentCultures = true) => Instance.Provider.GetRequiredService<ILocalizationResolve>().GetAllStringsByType(typeName, includeParentCultures);
public static IEnumerable<LocalizedString> GetTypeStringsFromResolve(string typeName, bool includeParentCultures = true) => Provider.GetRequiredService<ILocalizationResolve>().GetAllStringsByType(typeName, includeParentCultures);
#endregion
#region DisplayName

View File

@@ -66,7 +66,8 @@ internal class JsonStringLocalizer(Assembly assembly, string typeName, string ba
}
catch (Exception ex)
{
Logger.LogError(ex, "{JsonStringLocalizerName} searched for '{Name}' in '{typeName}' with culture '{CultureName}' throw exception.", nameof(JsonStringLocalizer), name, typeName, CultureInfo.CurrentUICulture.Name);
if (Logger?.IsEnabled(LogLevel.Error) == true)
Logger.LogError(ex, "{JsonStringLocalizerName} searched for '{Name}' in '{typeName}' with culture '{CultureName}' throw exception.", nameof(JsonStringLocalizer), name, typeName, CultureInfo.CurrentUICulture.Name);
}
return ret;
}
@@ -176,7 +177,8 @@ internal class JsonStringLocalizer(Assembly assembly, string typeName, string ba
localizationMissingItemHandler.HandleMissingItem(name, typeName, CultureInfo.CurrentUICulture.Name);
if (jsonLocalizationOptions.IgnoreLocalizerMissing == false)
{
Logger.LogInformation("{JsonStringLocalizerName} searched for '{Name}' in '{TypeName}' with culture '{CultureName}' not found.", nameof(JsonStringLocalizer), name, typeName, CultureInfo.CurrentUICulture.Name);
if (Logger?.IsEnabled(LogLevel.Information) == true)
Logger.LogInformation("{JsonStringLocalizerName} searched for '{Name}' in '{TypeName}' with culture '{CultureName}' not found.", nameof(JsonStringLocalizer), name, typeName, CultureInfo.CurrentUICulture.Name);
}
_missingManifestCache.TryAdd($"name={name}&culture={CultureInfo.CurrentUICulture.Name}");
}

View File

@@ -8,13 +8,13 @@
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
<TargetFrameworks>net8.0;$(OtherTargetFrameworks);</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.6" />
<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.7" />
<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
<PackageReference Include="BootstrapBlazor" Version="9.9.2" />
<PackageReference Include="BootstrapBlazor" Version="9.11.4" />
</ItemGroup>
<ItemGroup>

View File

@@ -27,10 +27,15 @@ public abstract class PrimaryIdEntity : IPrimaryIdEntity
public virtual long Id { get; set; }
}
public interface IPrimaryKeyEntity
{
string ExtJson { get; set; }
}
/// <summary>
/// 主键实体基类
/// </summary>
public abstract class PrimaryKeyEntity : PrimaryIdEntity
public abstract class PrimaryKeyEntity : PrimaryIdEntity, IPrimaryKeyEntity
{
/// <summary>
/// 拓展信息

View File

@@ -9,6 +9,7 @@
// ------------------------------------------------------------------------------
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Http;
namespace ThingsGateway.DB;
@@ -41,4 +42,31 @@ public static class FileExtensions
}
return fileName;
}
/// <summary>
/// 存储本地文件
/// </summary>
/// <param name="pPath">存储的第一层目录</param>
/// <param name="file"></param>
/// <returns>文件全路径</returns>
public static async Task<string> StorageLocal(this IFormFile file, string pPath = "imports")
{
string uploadFileFolder = App.WebHostEnvironment?.WebRootPath ?? "wwwroot"!;//赋值路径
var now = CommonUtils.GetSingleId();
var filePath = Path.Combine(uploadFileFolder, pPath);
if (!Directory.Exists(filePath))//如果不存在就创建文件夹
Directory.CreateDirectory(filePath);
//var fileSuffix = Path.GetExtension(file.Name).ToLower();// 文件后缀
var fileObjectName = $"{now}{file.Name}";//存储后的文件名
var fileName = Path.Combine(filePath, fileObjectName);//获取文件全路径
fileName = fileName.Replace("\\", "/");//格式化一系
//存储文件
using (var stream = File.Create(Path.Combine(filePath, fileObjectName)))
{
await file.CopyToAsync(stream).ConfigureAwait(false);
}
return fileName;
}
}

View File

@@ -209,16 +209,10 @@ public static class SqlSugarExtensions
}
/// <inheritdoc/>
public static async Task<bool> UpdateRangeAsync<T>(this SqlSugarClient db, List<T> updateObjs) where T : class, new()
public static Task<int> UpdateSetColumnsTrueAsync<T>(this SqlSugarClient db, Expression<Func<T, T>> columns, Expression<Func<T, bool>> whereExpression) where T : class, new()
{
return await db.Updateable(updateObjs).ExecuteCommandAsync().ConfigureAwait(false) > 0;
}
/// <inheritdoc/>
public static async Task<bool> UpdateSetColumnsTrueAsync<T>(this SqlSugarClient db, Expression<Func<T, T>> columns, Expression<Func<T, bool>> whereExpression) where T : class, new()
{
return await db.Updateable<T>().SetColumns(columns, appendColumnsByDataFilter: true).Where(whereExpression)
.ExecuteCommandAsync().ConfigureAwait(false) > 0;
return db.Updateable<T>().SetColumns(columns, appendColumnsByDataFilter: true).Where(whereExpression)
.ExecuteCommandAsync();
}
private static IEnumerable<T> Sort<T>(this IEnumerable<T> list, BasePageInput basePageInput)

View File

@@ -8,7 +8,7 @@
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
<TargetFrameworks>net8.0;$(OtherTargetFrameworks);</TargetFrameworks>
</PropertyGroup>

View File

@@ -1 +1 @@
https://gitee.com/dotnetchina/Furion/commit/8bf85f6908c1630268e45eeec607267a03947d2b
https://gitee.com/dotnetchina/Furion/commit/f1c07d65cccb623aca9d1905bf2e1ac6e4f4b714

View File

@@ -12,7 +12,7 @@
using Microsoft.AspNetCore.Hosting;
using ThingsGateway;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
using ThingsGateway.Reflection;
namespace Microsoft.Extensions.Hosting;
@@ -44,7 +44,7 @@ public static class HostBuilderExtensions
hostBuilder.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, combineAssembliesName);
// 实现假的 Starup解决泛型主机启动问题
// 实现假的 Startup解决泛型主机启动问题
hostBuilder.UseStartup<FakeStartup>();
return hostBuilder;
}

View File

@@ -20,7 +20,7 @@ using System.Text.RegularExpressions;
using ThingsGateway.NewLife;
namespace ThingsGateway.Extensions;
namespace ThingsGateway.Extension;
/// <summary>
/// 对象拓展类
@@ -28,70 +28,10 @@ namespace ThingsGateway.Extensions;
[SuppressSniffer]
public static class ObjectExtensions
{
/// <summary>
/// 将 DateTimeOffset 转换成本地 DateTime
/// </summary>
/// <param name="dateTime"></param>
/// <returns></returns>
public static DateTime ConvertToDateTime(this DateTimeOffset dateTime)
{
if (dateTime.Offset.Equals(TimeSpan.Zero))
return dateTime.UtcDateTime;
if (dateTime.Offset.Equals(TimeZoneInfo.Local.GetUtcOffset(dateTime.DateTime)))
return dateTime.ToLocalTime().DateTime;
else
return dateTime.DateTime;
}
/// <summary>
/// 将 DateTimeOffset? 转换成本地 DateTime?
/// </summary>
/// <param name="dateTime"></param>
/// <returns></returns>
public static DateTime? ConvertToDateTime(this DateTimeOffset? dateTime)
{
return dateTime.HasValue ? dateTime.Value.ConvertToDateTime() : null;
}
/// <summary>
/// 将 DateTime 转换成 DateTimeOffset
/// </summary>
/// <param name="dateTime"></param>
/// <returns></returns>
public static DateTimeOffset ConvertToDateTimeOffset(this DateTime dateTime)
{
return DateTime.SpecifyKind(dateTime, DateTimeKind.Local);
}
/// <summary>
/// 将 DateTime? 转换成 DateTimeOffset?
/// </summary>
/// <param name="dateTime"></param>
/// <returns></returns>
public static DateTimeOffset? ConvertToDateTimeOffset(this DateTime? dateTime)
{
return dateTime.HasValue ? dateTime.Value.ConvertToDateTimeOffset() : null;
}
/// <summary>
/// 将时间戳转换为 DateTime
/// </summary>
/// <param name="timestamp"></param>
/// <returns></returns>
internal static DateTime ConvertToDateTime(this long timestamp)
{
var timeStampDateTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var digitCount = (int)Math.Floor(Math.Log10(timestamp) + 1);
if (digitCount != 13 && digitCount != 10)
{
throw new ArgumentException("Data is not a valid timestamp format.");
}
return (digitCount == 13
? timeStampDateTime.AddMilliseconds(timestamp) // 13 位时间戳
: timeStampDateTime.AddSeconds(timestamp)).ToLocalTime(); // 10 位时间戳
}
/// <summary>
/// 将 IFormFile 转换成 byte[]
@@ -265,7 +205,7 @@ public static class ObjectExtensions
/// <typeparam name="T"></typeparam>
/// <param name="dic">字典</param>
/// <param name="newDic">新字典</param>
internal static void AddOrUpdate<T>(this ConcurrentDictionary<string, T> dic, Dictionary<string, T> newDic)
internal static void AddOrUpdate<T>(this NonBlockingDictionary<string, T> dic, Dictionary<string, T> newDic)
{
foreach (var (key, value) in newDic)
{

View File

@@ -47,7 +47,7 @@ public class StartupFilter : IStartupFilter
else
{
// 输出当前环境标识
context.Response.Headers["environment"] = envName;
context.Response.Headers["Environment"] = envName;
// 输出框架版本
context.Response.Headers[nameof(ThingsGateway)] = version;

View File

@@ -215,6 +215,10 @@ internal static class InternalApp
// 获取环境变量名,如果没找到,则读取 NETCORE_ENVIRONMENT 环境变量信息识别(用于非 Web 环境)
var envName = hostEnvironment?.EnvironmentName ?? Environment.GetEnvironmentVariable("NETCORE_ENVIRONMENT") ?? "Unknown";
// 获取 JSON 文件扫描配置2025.07.25),修复 docker 中挂载大文件数据卷导致启动缓慢的问题
var jsonFileScanner = configuration.GetSection("AppSettings:JsonFileScanner")
.Get<JsonFileScanner>() ?? new JsonFileScanner();
// 读取忽略的配置文件
var ignoreConfigurationFiles = (configuration.GetSection("IgnoreConfigurationFiles")
.Get<string[]>()
@@ -240,7 +244,7 @@ internal static class InternalApp
// 循环加载
foreach (var jsonFile in files)
{
configurationBuilder.AddJsonFile(jsonFile, optional: true, reloadOnChange: true);
configurationBuilder.AddJsonFile(jsonFile, optional: jsonFileScanner.Optional, reloadOnChange: jsonFileScanner.ReloadOnChange);
}
}
}

View File

@@ -51,7 +51,10 @@ public sealed class AppSettingsOptions : IConfigurableOptions<AppSettingsOptions
/// 【部署】二级虚拟目录
/// </summary>
public string VirtualPath { get; set; }
/// <summary>
/// JSON 文件扫描配置
/// </summary>
public JsonFileScanner JsonFileScanner { get; set; }
/// <summary>
/// 后期配置
/// </summary>
@@ -67,3 +70,20 @@ public sealed class AppSettingsOptions : IConfigurableOptions<AppSettingsOptions
options.VirtualPath ??= string.Empty;
}
}
/// <summary>
/// JSON 文件扫描配置
/// </summary>
/// <remarks>修复 docker 中挂载大文件数据卷导致启动缓慢的问题。</remarks>
public class JsonFileScanner
{
/// <summary>
/// 是否可选
/// </summary>
public bool Optional { get; set; } = true;
/// <summary>
/// 是否改变的时候重载
/// </summary>
public bool ReloadOnChange { get; set; } = true;
}

View File

@@ -701,7 +701,7 @@ public static class Serve
var applicationPartManager = app.Services.GetService<ApplicationPartManager>();
applicationPartManager?.ApplicationParts?.RemoveWhere(p => App.BakImageNames.Any(b => b == p.Name));
// 配置所有 Starup Configure
// 配置所有 Startup Configure
UseStartups(app);
UseStartups(app.Services);
@@ -820,7 +820,7 @@ public static class Serve
var applicationPartManager = app.Services.GetService<ApplicationPartManager>();
applicationPartManager?.ApplicationParts?.RemoveWhere(p => App.BakImageNames.Any(b => b == p.Name));
// 配置所有 Starup Configure
// 配置所有 Startup Configure
UseStartups(app.Services);
// 释放内存
App.AppStartups.Clear();
@@ -943,7 +943,7 @@ public static class Serve
var applicationPartManager = app.Services.GetService<ApplicationPartManager>();
applicationPartManager?.ApplicationParts?.RemoveWhere(p => App.BakImageNames.Any(b => b == p.Name));
// 配置所有 Starup Configure
// 配置所有 Startup Configure
UseStartups(app.Services);
// 释放内存
App.AppStartups.Clear();
@@ -1005,7 +1005,7 @@ public static class Serve
var applicationPartManager = app.Services.GetService<ApplicationPartManager>();
applicationPartManager?.ApplicationParts?.RemoveWhere(p => App.BakImageNames.Any(b => b == p.Name));
// 配置所有 Starup Configure
// 配置所有 Startup Configure
UseStartups(app.Services);
// 释放内存
App.AppStartups.Clear();

View File

@@ -94,7 +94,7 @@ public static class AspNetCoreBuilderServiceCollectionExtensions
/// <param name="mvcBuilder"></param>
/// <param name="configure"></param>
/// <returns></returns>
public static IMvcBuilder AddFromConvertBinding(this IMvcBuilder mvcBuilder, Action<ConcurrentDictionary<Type, Type>> configure = default)
public static IMvcBuilder AddFromConvertBinding(this IMvcBuilder mvcBuilder, Action<NonBlockingDictionary<Type, Type>> configure = default)
{
mvcBuilder.Services.AddFromConvertBinding(configure);
@@ -107,13 +107,13 @@ public static class AspNetCoreBuilderServiceCollectionExtensions
/// <param name="services"></param>
/// <param name="configure"></param>
/// <returns></returns>
public static IServiceCollection AddFromConvertBinding(this IServiceCollection services, Action<ConcurrentDictionary<Type, Type>> configure = default)
public static IServiceCollection AddFromConvertBinding(this IServiceCollection services, Action<NonBlockingDictionary<Type, Type>> configure = default)
{
// 非 Web 环境跳过注册
if (App.WebHostEnvironment == default) return services;
// 定义模型绑定转换器集合
var modelBinderConverts = new ConcurrentDictionary<Type, Type>();
var modelBinderConverts = new NonBlockingDictionary<Type, Type>();
modelBinderConverts.TryAdd(typeof(DateTime), typeof(DateTimeModelConvertBinder));
modelBinderConverts.TryAdd(typeof(DateTimeOffset), typeof(DateTimeOffsetModelConvertBinder));

View File

@@ -11,7 +11,7 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
namespace ThingsGateway.AspNetCore;

View File

@@ -27,13 +27,13 @@ public class FromConvertBinder : IModelBinder
/// <summary>
/// 定义模型绑定转换器集合
/// </summary>
private readonly ConcurrentDictionary<Type, Type> _modelBinderConverts;
private readonly NonBlockingDictionary<Type, Type> _modelBinderConverts;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="modelBinderConverts">定义模型绑定转换器集合</param>
public FromConvertBinder(ConcurrentDictionary<Type, Type> modelBinderConverts)
public FromConvertBinder(NonBlockingDictionary<Type, Type> modelBinderConverts)
{
_modelBinderConverts = modelBinderConverts;
}

View File

@@ -28,13 +28,13 @@ public class FromConvertBinderProvider : IModelBinderProvider
/// <summary>
/// 定义模型绑定转换器集合
/// </summary>
private readonly ConcurrentDictionary<Type, Type> _modelBinderConverts;
private readonly NonBlockingDictionary<Type, Type> _modelBinderConverts;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="modelBinderConverts">定义模型绑定转换器集合</param>
public FromConvertBinderProvider(ConcurrentDictionary<Type, Type> modelBinderConverts)
public FromConvertBinderProvider(NonBlockingDictionary<Type, Type> modelBinderConverts)
{
_modelBinderConverts = modelBinderConverts;
}

View File

@@ -11,7 +11,7 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
namespace ThingsGateway.AspNetCore;

View File

@@ -12,7 +12,7 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
namespace ThingsGateway.AspNetCore;

View File

@@ -18,7 +18,7 @@ using System.Reflection;
using ThingsGateway;
using ThingsGateway.ConfigurableOptions;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
namespace Microsoft.Extensions.DependencyInjection;

View File

@@ -34,9 +34,12 @@ public static class PBKDF2Encryption
using var rng = RandomNumberGenerator.Create();
var salt = new byte[saltSize];
rng.GetBytes(salt);
#if NET10_0_OR_GREATER
var hash = Rfc2898DeriveBytes.Pbkdf2(System.Text.Encoding.UTF8.GetBytes(text), salt, iterationCount, HashAlgorithmName.SHA256, derivedKeyLength);
#else
using var pbkdf2 = new Rfc2898DeriveBytes(text, salt, iterationCount, HashAlgorithmName.SHA256);
var hash = pbkdf2.GetBytes(derivedKeyLength);
#endif
// 分别编码盐和哈希,并用分隔符拼接
return Convert.ToBase64String(salt) + SaltHashSeparator + Convert.ToBase64String(hash);
@@ -65,8 +68,12 @@ public static class PBKDF2Encryption
if (saltBytes.Length != saltSize || storedHashBytes.Length != derivedKeyLength)
return false;
#if NET10_0_OR_GREATER
var computedHash = Rfc2898DeriveBytes.Pbkdf2(System.Text.Encoding.UTF8.GetBytes(text), saltBytes, iterationCount, HashAlgorithmName.SHA256, derivedKeyLength);
#else
using var pbkdf2 = new Rfc2898DeriveBytes(text, saltBytes, iterationCount, HashAlgorithmName.SHA256);
var computedHash = pbkdf2.GetBytes(derivedKeyLength);
#endif
return computedHash.SequenceEqual(storedHashBytes);
}

View File

@@ -16,7 +16,7 @@ using System.ComponentModel.DataAnnotations;
using System.Reflection;
using System.Text.RegularExpressions;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
using ThingsGateway.Templates.Extensions;
namespace ThingsGateway.DataValidation;
@@ -40,7 +40,7 @@ public static class DataValidator
/// <summary>
/// 验证类型正则表达式
/// </summary>
private static readonly ConcurrentDictionary<string, ValidationItemMetadataAttribute> ValidationItemMetadatas;
private static readonly NonBlockingDictionary<string, ValidationItemMetadataAttribute> ValidationItemMetadatas;
/// <summary>
/// 构造函数
@@ -57,7 +57,7 @@ public static class DataValidator
ValidationItemMetadatas = GetValidationValidationItemMetadatas();
// 缓存所有正则表达式
GetValidationTypeValidationItemMetadataCached = new ConcurrentDictionary<object, (string, ValidationItemMetadataAttribute)>();
GetValidationTypeValidationItemMetadataCached = new NonBlockingDictionary<object, (string, ValidationItemMetadataAttribute)>();
}
/// <summary>
@@ -203,7 +203,7 @@ public static class DataValidator
/// <summary>
/// 获取验证类型验证Item集合
/// </summary>
private static readonly ConcurrentDictionary<object, (string, ValidationItemMetadataAttribute)> GetValidationTypeValidationItemMetadataCached;
private static readonly NonBlockingDictionary<object, (string, ValidationItemMetadataAttribute)> GetValidationTypeValidationItemMetadataCached;
/// <summary>
/// 获取验证类型正则表达式(需要缓存)
@@ -267,9 +267,9 @@ public static class DataValidator
/// 获取验证类型所有有效的正则表达式
/// </summary>
/// <returns></returns>
private static ConcurrentDictionary<string, ValidationItemMetadataAttribute> GetValidationValidationItemMetadatas()
private static NonBlockingDictionary<string, ValidationItemMetadataAttribute> GetValidationValidationItemMetadatas()
{
var vaidationItems = new ConcurrentDictionary<string, ValidationItemMetadataAttribute>();
var vaidationItems = new NonBlockingDictionary<string, ValidationItemMetadataAttribute>();
// 查找所有 [ValidationMessageType] 类型中的 [ValidationMessage] 消息定义
var customErrorMessages = ValidationMessageTypes.SelectMany(u => u.GetFields()

View File

@@ -353,7 +353,7 @@ public static class DependencyInjectionServiceCollectionExtensions
/// <summary>
/// 类型名称集合
/// </summary>
private static readonly ConcurrentDictionary<string, Type> TypeNamedCollection;
private static readonly NonBlockingDictionary<string, Type> TypeNamedCollection;
/// <summary>
/// 创建代理方法
@@ -374,7 +374,7 @@ public static class DependencyInjectionServiceCollectionExtensions
GlobalServiceProxyType = App.EffectiveTypes
.FirstOrDefault(u => typeof(AspectDispatchProxy).IsAssignableFrom(u) && typeof(IGlobalDispatchProxy).IsAssignableFrom(u) && u.IsClass && !u.IsInterface && !u.IsAbstract);
TypeNamedCollection = new ConcurrentDictionary<string, Type>();
TypeNamedCollection = new NonBlockingDictionary<string, Type>();
DispatchCreateMethod = typeof(AspectDispatchProxy).GetMethod(nameof(AspectDispatchProxy.Create));
}
}

View File

@@ -50,9 +50,9 @@ internal sealed class NamedServiceProvider<TService> : INamedServiceProvider<TSe
#pragma warning disable CA1851
if (services
.OfType<AspectDispatchProxy>()
.FirstOrDefault(u => ResovleServiceName(((dynamic)u).Target.GetType()) == serviceName) is not TService service)
.FirstOrDefault(u => ResolveServiceName(((dynamic)u).Target.GetType()) == serviceName) is not TService service)
{
service = services.FirstOrDefault(u => ResovleServiceName(u.GetType()) == serviceName);
service = services.FirstOrDefault(u => ResolveServiceName(u.GetType()) == serviceName);
}
#pragma warning restore CA1851
@@ -85,9 +85,9 @@ internal sealed class NamedServiceProvider<TService> : INamedServiceProvider<TSe
#pragma warning disable CA1851
if (services
.OfType<AspectDispatchProxy>()
.FirstOrDefault(u => ResovleServiceName(((dynamic)u).Target.GetType()) == serviceName) is not TService service)
.FirstOrDefault(u => ResolveServiceName(((dynamic)u).Target.GetType()) == serviceName) is not TService service)
{
service = services.FirstOrDefault(u => ResovleServiceName(u.GetType()) == serviceName);
service = services.FirstOrDefault(u => ResolveServiceName(u.GetType()) == serviceName);
}
#pragma warning restore CA1851
@@ -116,7 +116,7 @@ internal sealed class NamedServiceProvider<TService> : INamedServiceProvider<TSe
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
private static string ResovleServiceName(Type type)
private static string ResolveServiceName(Type type)
{
if (type.IsDefined(typeof(InjectionAttribute)))
{

View File

@@ -21,7 +21,7 @@ using System.Collections.Concurrent;
using System.Reflection;
using System.Text.RegularExpressions;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
using ThingsGateway.UnifyResult;
namespace ThingsGateway.DynamicApiController;

View File

@@ -28,21 +28,21 @@ internal static class Penetrates
/// <summary>
/// 请求动词映射字典
/// </summary>
internal static ConcurrentDictionary<string, string> VerbToHttpMethods { get; private set; }
internal static NonBlockingDictionary<string, string> VerbToHttpMethods { get; private set; }
/// <summary>
/// 控制器排序集合
/// </summary>
internal static ConcurrentDictionary<string, (string, int, Type)> ControllerOrderCollection { get; set; }
internal static NonBlockingDictionary<string, (string, int, Type)> ControllerOrderCollection { get; set; }
/// <summary>
/// 构造函数
/// </summary>
static Penetrates()
{
ControllerOrderCollection = new ConcurrentDictionary<string, (string, int, Type)>();
ControllerOrderCollection = new NonBlockingDictionary<string, (string, int, Type)>();
VerbToHttpMethods = new ConcurrentDictionary<string, string>
VerbToHttpMethods = new NonBlockingDictionary<string, string>
{
["post"] = "POST",
["add"] = "POST",
@@ -67,13 +67,13 @@ internal static class Penetrates
["patch"] = "PATCH"
};
//IsApiControllerCached = new ConcurrentDictionary<Type, bool>();
//IsApiControllerCached = new NonBlockingDictionary<Type, bool>();
}
///// <summary>
///// <see cref="IsApiController(Type)"/> 缓存集合
///// </summary>
//private static readonly ConcurrentDictionary<Type, bool> IsApiControllerCached;
//private static readonly NonBlockingDictionary<Type, bool> IsApiControllerCached;
/// <summary>
/// 是否是Api控制器

View File

@@ -61,7 +61,7 @@ internal sealed class EventBusHostedService : BackgroundService
/// <summary>
/// 事件处理程序集合
/// </summary>
private readonly ConcurrentDictionary<EventHandlerWrapper, EventHandlerWrapper> _eventHandlers = new();
private readonly NonBlockingDictionary<EventHandlerWrapper, EventHandlerWrapper> _eventHandlers = new();
/// <summary>
/// 构造函数
@@ -295,7 +295,8 @@ internal sealed class EventBusHostedService : BackgroundService
, retryAction: (total, times) =>
{
// 输出重试日志
_logger.LogWarning("Retrying {times}/{total} times for {EventId}", times, total, eventSource.EventId);
if (_logger?.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Warning) == true)
_logger.LogWarning("Retrying {times}/{total} times for {EventId}", times, total, eventSource.EventId);
}).ConfigureAwait(false);
}
else

View File

@@ -17,7 +17,7 @@ using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Reflection;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
using ThingsGateway.Templates.Extensions;
namespace ThingsGateway.FriendlyException;
@@ -31,7 +31,7 @@ public static class Oops
/// <summary>
/// 方法错误异常特性
/// </summary>
private static readonly ConcurrentDictionary<MethodBase, MethodIfException> _errorMethods;
private static readonly NonBlockingDictionary<MethodBase, MethodIfException> _errorMethods;
/// <summary>
/// 错误代码类型
@@ -41,7 +41,7 @@ public static class Oops
/// <summary>
/// 错误消息字典
/// </summary>
private static readonly ConcurrentDictionary<string, string> _errorCodeMessages;
private static readonly NonBlockingDictionary<string, string> _errorCodeMessages;
/// <summary>
/// 友好异常设置
@@ -53,7 +53,7 @@ public static class Oops
/// </summary>
static Oops()
{
_errorMethods = new ConcurrentDictionary<MethodBase, MethodIfException>();
_errorMethods = new NonBlockingDictionary<MethodBase, MethodIfException>();
_friendlyExceptionSettings = App.GetConfig<FriendlyExceptionSettingsOptions>("FriendlyExceptionSettings", true);
_errorCodeTypes = GetErrorCodeTypes();
_errorCodeMessages = GetErrorCodeMessages();
@@ -258,9 +258,9 @@ public static class Oops
/// 获取所有错误消息
/// </summary>
/// <returns></returns>
private static ConcurrentDictionary<string, string> GetErrorCodeMessages()
private static NonBlockingDictionary<string, string> GetErrorCodeMessages()
{
var defaultErrorCodeMessages = new ConcurrentDictionary<string, string>();
var defaultErrorCodeMessages = new NonBlockingDictionary<string, string>();
// 查找所有 [ErrorCodeType] 类型中的 [ErrorCodeMetadata] 元数据定义
var errorCodeMessages = _errorCodeTypes.SelectMany(u => u.GetFields().Where(u => u.IsDefined(typeof(ErrorCodeItemMetadataAttribute))))

View File

@@ -27,13 +27,15 @@ public sealed class Retry
/// <param name="exceptionTypes">异常类型,可多个</param>
/// <param name="fallbackPolicy">重试失败回调</param>
/// <param name="retryAction">重试时调用方法</param>
/// <param name="shouldExit">退出条件</param>
public static void Invoke(Action action
, int numRetries
, int retryTimeout = 1000
, bool finalThrow = true
, Type[] exceptionTypes = default
, Action<Exception> fallbackPolicy = default
, Action<int, int> retryAction = default)
, Action<int, int> retryAction = default
, Func<bool> shouldExit = default)
{
if (action == null) throw new ArgumentNullException(nameof(action));
@@ -46,7 +48,7 @@ public sealed class Retry
{
fallbackPolicy?.Invoke(ex);
return Task.CompletedTask;
}, retryAction).GetAwaiter().GetResult();
}, retryAction, shouldExit).GetAwaiter().GetResult();
}
/// <summary>
@@ -59,6 +61,7 @@ public sealed class Retry
/// <param name="exceptionTypes">异常类型,可多个</param>
/// <param name="fallbackPolicy">重试失败回调</param>
/// <param name="retryAction">重试时调用方法</param>
/// <param name="shouldExit">退出条件</param>
/// <returns><see cref="Task"/></returns>
public static async Task InvokeAsync(Func<Task> action
, int numRetries
@@ -66,7 +69,8 @@ public sealed class Retry
, bool finalThrow = true
, Type[] exceptionTypes = default
, Func<Exception, Task> fallbackPolicy = default
, Action<int, int> retryAction = default)
, Action<int, int> retryAction = default
, Func<bool> shouldExit = default)
{
if (action == null) throw new ArgumentNullException(nameof(action));
@@ -117,6 +121,12 @@ public sealed class Retry
// 如果可重试异常数大于 0则间隔指定时间后继续执行
if (retryTimeout > 0) await Task.Delay(retryTimeout).ConfigureAwait(false);
// 处理退出机制
if (shouldExit != null && shouldExit())
{
return;
}
}
}
}

View File

@@ -16,7 +16,7 @@ using Microsoft.AspNetCore.SignalR;
using System.Reflection;
using ThingsGateway;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
using ThingsGateway.InstantMessaging;
namespace Microsoft.AspNetCore.Builder;

View File

@@ -23,7 +23,7 @@ public static class ILoggerExtensions
/// 设置日志上下文
/// </summary>
/// <param name="logger"></param>
/// <param name="properties">建议使用 ConcurrentDictionary 类型</param>
/// <param name="properties">建议使用 NonBlockingDictionary 类型</param>
/// <returns></returns>
public static IDisposable ScopeContext(this ILogger logger, IDictionary<string, object> properties)
{

View File

@@ -9,7 +9,7 @@
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
namespace ThingsGateway.Logging;

View File

@@ -82,7 +82,7 @@ public static class StringLoggingExtensions
/// 配置日志上下文
/// </summary>
/// <param name="message"></param>
/// <param name="properties">建议使用 ConcurrentDictionary 类型</param>
/// <param name="properties">建议使用 NonBlockingDictionary 类型</param>
/// <returns></returns>
public static StringLoggingPart ScopeContext(this string message, IDictionary<string, object> properties)
{

View File

@@ -44,7 +44,7 @@ public sealed class FileLoggerProvider : ILoggerProvider, ISupportExternalScope
/// 记录日志所有滚动文件名
/// </summary>
/// <remarks>只有 MaxRollingFiles 和 FileSizeLimitBytes 大于 0 有效</remarks>
internal readonly ConcurrentDictionary<string, FileInfo> _rollingFileNames = new();
internal readonly NonBlockingDictionary<string, FileInfo> _rollingFileNames = new();
/// <summary>
/// 文件日志写入器

View File

@@ -36,8 +36,9 @@ using System.Text.Json;
using ThingsGateway;
using ThingsGateway.DataValidation;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
using ThingsGateway.FriendlyException;
using ThingsGateway.JsonSerialization;
using ThingsGateway.Logging;
using ThingsGateway.Templates;
using ThingsGateway.UnifyResult;
@@ -150,7 +151,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
return;
}
await MonitorAsync(actionMethod, context.ActionArguments, context, next).ConfigureAwait(false);
await MonitorAsync(actionMethod, context.ActionArguments, context, () => next()).ConfigureAwait(false);
}
/// <summary>
@@ -182,7 +183,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
return;
}
await MonitorAsync(actionMethod, context.HandlerArguments, context, next).ConfigureAwait(false);
await MonitorAsync(actionMethod, context.HandlerArguments, context, () => next()).ConfigureAwait(false);
}
/// <summary>
@@ -639,6 +640,11 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
// 解决 JsonElement 序列化问题
jsonSerializerSettings.Converters.Add(new JsonElementConverter());
// 解决 JsonObject 和 JsonArray 序列化问题
jsonSerializerSettings.Converters.Add(new NewtonsoftJsonJsonObjectJsonConverter());
jsonSerializerSettings.Converters.Add(new NewtonsoftJsonJsonArrayJsonConverter());
// 解决 DateTimeOffset 序列化/反序列化问题
if (obj is DateTimeOffset)
{
@@ -783,12 +789,12 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
return typeName;
}
private async Task MonitorAsync(MethodInfo actionMethod, IDictionary<string, object> parameterValues, FilterContext context, dynamic next)
private async Task MonitorAsync<T>(MethodInfo actionMethod, IDictionary<string, object> parameterValues, FilterContext context, Func<Task<T>> next)
{
// 排除 WebSocket 请求处理
if (context.HttpContext.IsWebSocketRequest())
{
_ = await next();
_ = await next().ConfigureAwait(false);
return;
}
@@ -799,14 +805,14 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
if (actionMethod.IsDefined(typeof(SuppressMonitorAttribute), true)
|| actionMethod.DeclaringType.IsDefined(typeof(SuppressMonitorAttribute), true))
{
_ = await next();
_ = await next().ConfigureAwait(false);
return;
}
// 判断是否自定义了日志筛选器,如果是则检查是否符合条件
if (LoggingMonitorSettings.InternalWriteFilter?.Invoke(context) == false)
{
_ = await next();
_ = await next().ConfigureAwait(false);
return;
}
@@ -819,7 +825,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
// 解决局部和全局触发器同时配置触发两次问题
if (isDefinedScopedAttribute && Settings.FromGlobalFilter == true)
{
_ = await next();
_ = await next().ConfigureAwait(false);
return;
}
@@ -833,7 +839,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
&& !Settings.IncludeOfMethods.Contains(methodFullName, StringComparer.OrdinalIgnoreCase))
{
// 查找是否包含匹配,忽略大小写
_ = await next();
_ = await next().ConfigureAwait(false);
return;
}
@@ -841,7 +847,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
if (Settings.GlobalEnabled
&& Settings.ExcludeOfMethods.Contains(methodFullName, StringComparer.OrdinalIgnoreCase))
{
_ = await next();
_ = await next().ConfigureAwait(false);
return;
}
}
@@ -952,7 +958,8 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
// 计算接口执行时间
var timeOperation = Stopwatch.StartNew();
var resultContext = await next();
var resultContext = await next().ConfigureAwait(false);
timeOperation.Stop();
writer.WriteNumber("timeOperationElapsedMilliseconds", timeOperation.ElapsedMilliseconds);
@@ -1008,8 +1015,13 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
var environment = httpContext.RequestServices.GetRequiredService<IWebHostEnvironment>().EnvironmentName;
writer.WriteString(nameof(environment), environment);
Exception exception = null;
// 获取异常对象情况
Exception exception = resultContext.Exception;
if (resultContext is PageHandlerExecutedContext pageHandlerExecutedContext)
exception = pageHandlerExecutedContext.Exception;
else if (resultContext is ActionExecutedContext actionExecutedContext)
exception = actionExecutedContext.Exception;
if (exception == null)
{
// 解析存储的验证信息

View File

@@ -94,7 +94,7 @@ public sealed partial class StringLoggingPart
/// <summary>
/// 配置日志上下文
/// </summary>
/// <param name="properties">建议使用 ConcurrentDictionary 类型</param>
/// <param name="properties">建议使用 NonBlockingDictionary 类型</param>
/// <returns></returns>
public StringLoggingPart ScopeContext(IDictionary<string, object> properties)
{

View File

@@ -57,7 +57,7 @@ public static class Log
/// <summary>
/// 配置日志上下文
/// </summary>
/// <param name="properties">建议使用 ConcurrentDictionary 类型</param>
/// <param name="properties">建议使用 NonBlockingDictionary 类型</param>
/// <returns></returns>
public static (ILogger logger, IDisposable scope) ScopeContext(IDictionary<string, object> properties)
{

View File

@@ -16,7 +16,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Linq.Expressions;
using System.Reflection;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
using ThingsGateway.Options;
namespace Microsoft.Extensions.Options;

View File

@@ -20,9 +20,10 @@ public sealed class DailyAtAttribute : CronAttribute
/// <summary>
/// 构造函数
/// </summary>
/// <param name="field">字段值</param>
/// <param name="fields">字段值</param>
public DailyAtAttribute(params object[] fields)
: base("@daily", fields)
public DailyAtAttribute(object field, params object[] fields)
: base("@daily", new[] { field }.Concat(fields).ToArray())
{
}
}

View File

@@ -20,9 +20,10 @@ public sealed class HourlyAtAttribute : CronAttribute
/// <summary>
/// 构造函数
/// </summary>
/// <param name="field">字段值</param>
/// <param name="fields">字段值</param>
public HourlyAtAttribute(params object[] fields)
: base("@hourly", fields)
public HourlyAtAttribute(object field, params object[] fields)
: base("@hourly", new[] { field }.Concat(fields).ToArray())
{
}
}

View File

@@ -20,9 +20,10 @@ public sealed class MinutelyAtAttribute : CronAttribute
/// <summary>
/// 构造函数
/// </summary>
/// <param name="field">字段值</param>
/// <param name="fields">字段值</param>
public MinutelyAtAttribute(params object[] fields)
: base("@minutely", fields)
public MinutelyAtAttribute(object field, params object[] fields)
: base("@minutely", new[] { field }.Concat(fields).ToArray())
{
}
}

View File

@@ -20,9 +20,10 @@ public sealed class MonthlyAtAttribute : CronAttribute
/// <summary>
/// 构造函数
/// </summary>
/// <param name="field">字段值</param>
/// <param name="fields">字段值</param>
public MonthlyAtAttribute(params object[] fields)
: base("@monthly", fields)
public MonthlyAtAttribute(object field, params object[] fields)
: base("@monthly", new[] { field }.Concat(fields).ToArray())
{
}
}

View File

@@ -14,15 +14,16 @@ namespace ThingsGateway.Schedule;
/// <summary>
/// 特定秒开始作业触发器特性
/// </summary>
[SecondlyAtAttribute, AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
[SuppressSniffer, AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class SecondlyAtAttribute : CronAttribute
{
/// <summary>
/// 构造函数
/// </summary>
/// <param name="field">字段值</param>
/// <param name="fields">字段值</param>
public SecondlyAtAttribute(params object[] fields)
: base("@secondly", fields)
public SecondlyAtAttribute(object field, params object[] fields)
: base("@secondly", new[] { field }.Concat(fields).ToArray())
{
}
}

View File

@@ -20,9 +20,10 @@ public sealed class WeeklyAtAttribute : CronAttribute
/// <summary>
/// 构造函数
/// </summary>
/// <param name="field">字段值</param>
/// <param name="fields">字段值</param>
public WeeklyAtAttribute(params object[] fields)
: base("@weekly", fields)
public WeeklyAtAttribute(object field, params object[] fields)
: base("@weekly", new[] { field }.Concat(fields).ToArray())
{
}
}

View File

@@ -20,9 +20,10 @@ public sealed class YearlyAtAttribute : CronAttribute
/// <summary>
/// 构造函数
/// </summary>
/// <param name="field">字段值</param>
/// <param name="fields">字段值</param>
public YearlyAtAttribute(params object[] fields)
: base("@yearly", fields)
public YearlyAtAttribute(object field, params object[] fields)
: base("@yearly", new[] { field }.Concat(fields).ToArray())
{
}
}

View File

@@ -21,7 +21,7 @@ internal sealed class JobCancellationToken : IJobCancellationToken
/// <summary>
/// 取消作业执行 Token 集合
/// </summary>
private readonly ConcurrentDictionary<string, CancellationTokenSource> _cancellationTokenSources;
private readonly NonBlockingDictionary<string, CancellationTokenSource> _cancellationTokenSources;
/// <summary>
/// 作业调度器日志服务
@@ -93,12 +93,14 @@ internal sealed class JobCancellationToken : IJobCancellationToken
}
}
}
catch (OperationCanceledException) { }
catch (Exception ex) when (!(ex is OperationCanceledException ||
ex is ObjectDisposedException ||
(ex is AggregateException aggEx && aggEx.InnerExceptions.All(e => e is OperationCanceledException || e is ObjectDisposedException))))
{ }
catch { }
catch (Exception ex)
{
// 输出非任务取消异常日志
if (!(ex is OperationCanceledException || (ex is AggregateException aggEx && aggEx.InnerExceptions.Count == 1 && aggEx.InnerExceptions[0] is TaskCanceledException)))
{
// 待输出
}
}
}
}

View File

@@ -9,6 +9,8 @@
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using Microsoft.Extensions.DependencyInjection;
namespace ThingsGateway.Schedule;
/// <summary>
@@ -152,7 +154,31 @@ public abstract class JobExecutionContext
writer.WriteEndObject();
});
}
/// <summary>
/// 检查作业任务是否处于正常状态
/// </summary>
/// <param name="schedulerFactory"><see cref="ISchedulerFactory"/></param>
/// <returns><see cref="bool"/></returns>
public bool IsNormalStatus(ISchedulerFactory schedulerFactory = null)
{
// 解析作业计划工厂服务
schedulerFactory ??= ServiceProvider.GetRequiredService<ISchedulerFactory>();
// 情况 1检查作业是否存在
if (schedulerFactory.TryGetJob(JobId, out var scheduler) != ScheduleResult.Succeed)
{
return false;
}
// 情况 2检查作业触发器是否存在
if (scheduler.TryGetTrigger(TriggerId, out var trigger) != ScheduleResult.Succeed)
{
return false;
}
// 情况 3检查作业触发器是否正常运行
return trigger.IsNormalStatus();
}
/// <summary>
/// 作业执行上下文转字符串输出输出
/// </summary>

View File

@@ -209,7 +209,7 @@ public sealed class ScheduleUIMiddleware
case "remove":
_schedulerFactory.RemoveJob(jobId);
break;
// 立即执行
// 手动执行
case "run":
_schedulerFactory.RunJob(jobId);
break;
@@ -264,7 +264,7 @@ public sealed class ScheduleUIMiddleware
case "remove":
scheduler1?.RemoveTrigger(triggerId);
break;
// 立即执行
// 手动执行
case "run":
scheduler1?.Run(triggerId);
break;

View File

@@ -1,13 +1,13 @@
{
"files": {
"main.css": "/__schedule__/static/css/main.fbe5db1c.css",
"main.js": "/__schedule__/static/js/main.851eb0b3.js",
"main.css": "/__schedule__/static/css/main.765127e9.css",
"main.js": "/__schedule__/static/js/main.326c761f.js",
"index.html": "/__schedule__/index.html",
"main.fbe5db1c.css.map": "/__schedule__/static/css/main.fbe5db1c.css.map",
"main.851eb0b3.js.map": "/__schedule__/static/js/main.851eb0b3.js.map"
"main.765127e9.css.map": "/__schedule__/static/css/main.765127e9.css.map",
"main.326c761f.js.map": "/__schedule__/static/js/main.326c761f.js.map"
},
"entrypoints": [
"static/css/main.fbe5db1c.css",
"static/js/main.851eb0b3.js"
"static/css/main.765127e9.css",
"static/js/main.326c761f.js"
]
}

View File

@@ -1 +1,19 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/__schedule__/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Schedule Dashboard"/><link rel="apple-touch-icon" href="/__schedule__/logo192.png"/><script src="/__schedule__/apiconfig.js"></script><title>Schedule Dashboard</title><script defer="defer" src="/__schedule__/static/js/main.851eb0b3.js"></script><link href="/__schedule__/static/css/main.fbe5db1c.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>document.title=window.apiconfig.title</script></body></html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/__schedule__/favicon.ico" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Schedule Dashboard" />
<link rel="apple-touch-icon" href="/__schedule__/logo192.png" />
<script src="/__schedule__/apiconfig.js"></script>
<title>Schedule Dashboard</title>
<script defer="defer" src="/__schedule__/static/js/main.326c761f.js"></script>
<link href="/__schedule__/static/css/main.765127e9.css" rel="stylesheet">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div>
<script>document.title = window.apiconfig.title</script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More