mirror of
https://gitee.com/ThingsGateway/ThingsGateway.git
synced 2025-10-20 18:51:28 +08:00
Compare commits
121 Commits
10.9.98.0
...
10.11.99.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5ee8b50a92 | ||
![]() |
301beda2a2 | ||
![]() |
628b51a353 | ||
![]() |
f03445bc83 | ||
![]() |
55a2ff5487 | ||
![]() |
0fef7dcf3b | ||
![]() |
19d9702606 | ||
![]() |
a8a9774932 | ||
![]() |
aad0f0e8c3 | ||
![]() |
e74eae50a7 | ||
![]() |
3b16d7019f | ||
![]() |
3e038028c2 | ||
![]() |
b1d8041f7e | ||
![]() |
53a98b26cd | ||
![]() |
42c740fa1b | ||
![]() |
556819c90c | ||
![]() |
2522333a9c | ||
![]() |
bd4ce7c09b | ||
![]() |
156ed88bd6 | ||
![]() |
2416226eb0 | ||
![]() |
976323a716 | ||
![]() |
3c9e397403 | ||
![]() |
79406ad4a0 | ||
![]() |
20c44f10ca | ||
![]() |
31d6b2a9e6 | ||
![]() |
68e5a9c546 | ||
![]() |
b2ea9f99b9 | ||
![]() |
6d7d0e468a | ||
![]() |
ff1f632de2 | ||
![]() |
fc3d7015ee | ||
![]() |
40c5acb522 | ||
![]() |
f9cc1cbb05 | ||
![]() |
cf6e8b58f0 | ||
![]() |
615e3bb24c | ||
![]() |
4a7534b210 | ||
![]() |
58e099cb93 | ||
![]() |
a94a9c953c | ||
![]() |
35e1ffa3e9 | ||
![]() |
4921642151 | ||
![]() |
d71ee29da8 | ||
![]() |
901aa2d59f | ||
![]() |
764957c014 | ||
![]() |
0e3898218b | ||
![]() |
61f13cef3c | ||
![]() |
0b663d9e01 | ||
![]() |
6c95c6209f | ||
![]() |
4d223d2622 | ||
![]() |
e8d7e91b64 | ||
![]() |
8175f541ec | ||
![]() |
0adbdb926b | ||
![]() |
42adee9980 | ||
![]() |
427a7404bc | ||
![]() |
3658199e0a | ||
![]() |
82eedee50a | ||
![]() |
6a18fc3e06 | ||
![]() |
c37e314ed6 | ||
![]() |
a937a85d90 | ||
![]() |
35dd4ae9d3 | ||
![]() |
0b829ac85c | ||
![]() |
aa247422d2 | ||
![]() |
2e00e8c135 | ||
![]() |
34dd2cf0a7 | ||
![]() |
8404e20c5e | ||
![]() |
662aa162e9 | ||
![]() |
5927738c32 | ||
![]() |
3c9f97a5c3 | ||
![]() |
179ca0aa0e | ||
![]() |
fc09a52da1 | ||
![]() |
5436b91c89 | ||
![]() |
d1c46f51a6 | ||
![]() |
4539d8d198 | ||
![]() |
9ea9529a5f | ||
![]() |
4e6be23aac | ||
![]() |
2fabbd236b | ||
![]() |
163a66530e | ||
![]() |
29073a00c4 | ||
![]() |
c6d4d1ecfa | ||
![]() |
ba16889cad | ||
![]() |
5aaed35b0f | ||
![]() |
df067c91eb | ||
![]() |
2078b4a60b | ||
![]() |
20a2e3ff8e | ||
![]() |
61a973b1b5 | ||
![]() |
cbd72e2081 | ||
![]() |
4e0377b20c | ||
![]() |
fd318d3cdc | ||
![]() |
515bdb9700 | ||
![]() |
46c1780017 | ||
![]() |
fe78a4c3ca | ||
![]() |
2d7effadf9 | ||
![]() |
346c560f8b | ||
![]() |
8e3bd89f61 | ||
![]() |
6da142d080 | ||
![]() |
ff7d029e6f | ||
![]() |
21b4695683 | ||
![]() |
02ad494a26 | ||
![]() |
280366e1b2 | ||
![]() |
6660ce3e34 | ||
![]() |
7499162c1a | ||
![]() |
40208a5cd6 | ||
![]() |
fa347f4f68 | ||
![]() |
d7df6fc605 | ||
![]() |
eb4bb2fd48 | ||
![]() |
faa9858974 | ||
![]() |
1b3d2dda49 | ||
![]() |
a8a9453611 | ||
![]() |
e84f42ce14 | ||
![]() |
6f814cf6b8 | ||
![]() |
e36432e4e9 | ||
![]() |
ebd71e807b | ||
![]() |
34000d8d7d | ||
![]() |
e785f6660c | ||
![]() |
831c611797 | ||
![]() |
453817ef86 | ||
![]() |
8ce0b981c1 | ||
![]() |
4e5c51b54c | ||
![]() |
3cc9d31f28 | ||
![]() |
10391f869b | ||
![]() |
fba0723a6d | ||
![]() |
2db3f78f0c | ||
![]() |
badf61fe01 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -364,7 +364,5 @@ FodyWeavers.xsd
|
||||
|
||||
/src/*Pro*/
|
||||
/src/*Pro*
|
||||
/src/*pro*
|
||||
/src/*pro*/
|
||||
/src/ThingsGateway.Server/Configuration/GiteeOAuthSettings.json
|
||||
/src/.idea/
|
||||
|
@@ -11,6 +11,7 @@
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<!-- 避免 DLL 被打包到 lib/ -->
|
||||
<EnableSourceGenerator>true</EnableSourceGenerator>
|
||||
|
||||
<!-- 可选 -->
|
||||
|
||||
|
||||
@@ -26,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>
|
||||
|
@@ -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.StartsWith(wwwroot, StringComparison.OrdinalIgnoreCase) || !System.IO.File.Exists(filePath))
|
||||
if ((!(fileName.StartsWith(@"../Logs") || fileName.StartsWith(@"..\Logs")) && filePath.Contains("..")) || !System.IO.File.Exists(filePath))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
@@ -49,6 +48,6 @@ public class FileController : ControllerBase
|
||||
|
||||
Response.Headers.Append("Access-Control-Expose-Headers", "Content-Disposition");
|
||||
|
||||
return File(fileStream, "application/octet-stream", (fileName.Replace('/', '_')));
|
||||
return File(fileStream, "application/octet-stream", (Path.GetFileName(filePath).Replace('/', '_')));
|
||||
}
|
||||
}
|
||||
|
@@ -38,12 +38,14 @@ public class VerificatInfo : PrimaryIdEntity
|
||||
[AutoGenerateColumn(Filterable = true, Sortable = true)]
|
||||
[SugarColumn(ColumnDescription = "Id", IsPrimaryKey = true)]
|
||||
[IgnoreExcel]
|
||||
[System.ComponentModel.DataAnnotations.Key]
|
||||
public override long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 登录IP
|
||||
/// </summary>
|
||||
[AutoGenerateColumn(Filterable = true, Sortable = true, Width = 200)]
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public string LoginIp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -77,5 +79,6 @@ public class VerificatInfo : PrimaryIdEntity
|
||||
/// 登录设备
|
||||
/// </summary>
|
||||
[AutoGenerateColumn(Filterable = true, Sortable = true, Width = 100)]
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public string Device { get; set; }
|
||||
}
|
||||
|
@@ -21,16 +21,7 @@
|
||||
"UserNoModule": "This account has not been assigned a module. Please contact the administrator",
|
||||
"UserNull": "User {0} does not exist"
|
||||
},
|
||||
"ThingsGateway.Admin.Application.BaseDataEntity": {
|
||||
"CreateOrgId": "CreateOrgId"
|
||||
},
|
||||
"ThingsGateway.Admin.Application.BaseEntity": {
|
||||
"CreateTime": "CreateTime",
|
||||
"CreateUser": "CreateUser",
|
||||
"SortCode": "SortCode",
|
||||
"UpdateTime": "UpdateTime",
|
||||
"UpdateUser": "UpdateUser"
|
||||
},
|
||||
|
||||
"ThingsGateway.Admin.Application.BlazorAuthenticationHandler": {
|
||||
"UserExpire": "User expired, please login again"
|
||||
},
|
||||
|
@@ -21,16 +21,7 @@
|
||||
"UserNoModule": "该账号未分配模块,请联系管理员",
|
||||
"UserNull": "用户 {0} 不存在"
|
||||
},
|
||||
"ThingsGateway.Admin.Application.BaseDataEntity": {
|
||||
"CreateOrgId": "创建机构Id"
|
||||
},
|
||||
"ThingsGateway.Admin.Application.BaseEntity": {
|
||||
"CreateTime": "创建时间",
|
||||
"CreateUser": "创建人",
|
||||
"SortCode": "排序",
|
||||
"UpdateTime": "更新时间",
|
||||
"UpdateUser": "更新人"
|
||||
},
|
||||
|
||||
"ThingsGateway.Admin.Application.BlazorAuthenticationHandler": {
|
||||
"UserExpire": "用户登录已过期,请重新登录"
|
||||
},
|
||||
|
@@ -11,6 +11,7 @@
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
namespace ThingsGateway.Admin.Application;
|
||||
|
||||
[Mapper(UseDeepCloning = true, EnumMappingStrategy = EnumMappingStrategy.ByName, RequiredMappingStrategy = RequiredMappingStrategy.None)]
|
||||
public static partial class AdminMapper
|
||||
{
|
||||
|
@@ -8,7 +8,7 @@
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace ThingsGateway.Gateway.Application;
|
||||
namespace ThingsGateway.Admin.Application;
|
||||
|
||||
public class USheetDatas
|
||||
{
|
@@ -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,
|
||||
|
@@ -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",
|
||||
|
@@ -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,
|
||||
|
@@ -9,6 +9,7 @@
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace ThingsGateway.Admin.Application;
|
||||
|
||||
internal sealed class NoticeService : INoticeService
|
||||
{
|
||||
private IEventService<AppMessage>? MessageDispatchService { get; set; }
|
||||
|
@@ -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 角色模块处理
|
||||
|
||||
|
@@ -18,6 +18,7 @@ public class SessionOutput : PrimaryIdEntity
|
||||
/// <summary>
|
||||
/// 主键Id
|
||||
/// </summary>
|
||||
[System.ComponentModel.DataAnnotations.Key]
|
||||
public override long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
@@ -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 用户模块处理
|
||||
|
||||
|
@@ -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删除用户数据
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -5,9 +5,10 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0;$(OtherTargetFrameworks);</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -19,7 +20,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Riok.Mapperly" Version="4.2.1" ExcludeAssets="runtime" PrivateAssets="all" />
|
||||
<PackageReference Include="Rougamo.Fody" Version="5.0.1" />
|
||||
<PackageReference Include="Rougamo.Fody" Version="5.0.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
|
||||
@@ -27,10 +28,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" />
|
||||
|
@@ -0,0 +1,57 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace ThingsGateway.Admin.Application;
|
||||
|
||||
public static class USheetDataHelpers
|
||||
{
|
||||
public static USheetDatas GetUSheetDatas(Dictionary<string, object> data)
|
||||
{
|
||||
var uSheetDatas = new USheetDatas();
|
||||
|
||||
foreach (var a in data)
|
||||
{
|
||||
var value = (a.Value as IEnumerable<Dictionary<string, object>>).ToList();
|
||||
|
||||
var uSheetData = new USheetData();
|
||||
uSheetData.id = a.Key;
|
||||
uSheetData.name = a.Key;
|
||||
|
||||
for (int row1 = 0; row1 < value.Count; row1++)
|
||||
{
|
||||
if (row1 == 0)
|
||||
{
|
||||
Dictionary<int, USheetCelldata> usheetColldata = new();
|
||||
int col = 0;
|
||||
foreach (var colData in value[row1])
|
||||
{
|
||||
usheetColldata.Add(col, new USheetCelldata() { v = colData.Key });
|
||||
col++;
|
||||
}
|
||||
uSheetData.cellData.Add(row1, usheetColldata);
|
||||
}
|
||||
{
|
||||
Dictionary<int, USheetCelldata> usheetColldata = new();
|
||||
int col = 0;
|
||||
foreach (var colData in value[row1])
|
||||
{
|
||||
usheetColldata.Add(col, new USheetCelldata() { v = colData.Value });
|
||||
col++;
|
||||
}
|
||||
uSheetData.cellData.Add(row1 + 1, usheetColldata);
|
||||
}
|
||||
}
|
||||
uSheetData.rowCount = uSheetData.cellData.Count + 100;
|
||||
uSheetData.columnCount = uSheetData.cellData.FirstOrDefault().Value?.Count ?? 0;
|
||||
uSheetDatas.sheets.Add(a.Key, uSheetData);
|
||||
}
|
||||
return uSheetDatas;
|
||||
}
|
||||
}
|
@@ -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"
|
||||
<Table TItem="TItem" IsBordered="true" IsStriped="true" TableSize="TableSize.Compact" SelectedRows=SelectedRows SelectedRowsChanged=privateSelectedRowsChanged 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!"
|
||||
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
|
||||
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!"
|
||||
|
@@ -13,6 +13,24 @@ namespace ThingsGateway.Admin.Razor;
|
||||
[CascadingTypeParameter(nameof(TItem))]
|
||||
public partial class AdminTable<TItem> where TItem : class, new()
|
||||
{
|
||||
/// <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;
|
||||
@@ -210,14 +228,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 +276,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;
|
||||
|
@@ -1,7 +1,6 @@
|
||||
@namespace ThingsGateway.Gateway.Razor
|
||||
@namespace ThingsGateway.Admin.Razor
|
||||
@using ThingsGateway.Admin.Application
|
||||
@using ThingsGateway.Admin.Razor
|
||||
@using ThingsGateway.Gateway.Application
|
||||
|
||||
<div class="h-600px">
|
||||
<UniverSheet @ref="_sheetExcel" OnReadyAsync="OnReadyAsync"></UniverSheet>
|
@@ -8,9 +8,10 @@
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using ThingsGateway.Admin.Application;
|
||||
using ThingsGateway.NewLife.Json.Extension;
|
||||
|
||||
namespace ThingsGateway.Gateway.Razor;
|
||||
namespace ThingsGateway.Admin.Razor;
|
||||
|
||||
public partial class USheet
|
||||
{
|
@@ -30,7 +30,7 @@ public class BlazorAppContext
|
||||
/// <summary>
|
||||
/// 全部菜单
|
||||
/// </summary>
|
||||
public IEnumerable<SysResource> AllMenus { get; private set; }
|
||||
public List<SysResource> AllMenus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前用户
|
||||
@@ -42,22 +42,22 @@ public class BlazorAppContext
|
||||
/// <summary>
|
||||
/// 用户个人菜单
|
||||
/// </summary>
|
||||
public IEnumerable<MenuItem> OwnMenuItems { get; private set; }
|
||||
public List<MenuItem> OwnMenuItems { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 不同模块的菜单
|
||||
/// </summary>
|
||||
public IEnumerable<MenuItem> AllOwnMenuItems { get; private set; }
|
||||
public List<MenuItem> AllOwnMenuItems { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户个人菜单,多个模块
|
||||
/// </summary>
|
||||
public IEnumerable<SysResource> OwnMenus { get; private set; }
|
||||
public List<SysResource> OwnMenus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户个人菜单,非树形
|
||||
/// </summary>
|
||||
public IEnumerable<MenuItem> OwnSameLevelMenuItems { get; private set; }
|
||||
public List<MenuItem> OwnSameLevelMenuItems { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 个人工作台
|
||||
@@ -67,9 +67,9 @@ public class BlazorAppContext
|
||||
/// <summary>
|
||||
/// 用户个人快捷方式菜单
|
||||
/// </summary>
|
||||
public IEnumerable<SysResource> UserWorkbenchOutputs { get; private set; }
|
||||
public List<SysResource> UserWorkbenchOutputs { get; private set; }
|
||||
|
||||
public IEnumerable<SysResource> AllResource { get; private set; }
|
||||
public List<SysResource> AllResource { get; private set; }
|
||||
|
||||
private ISysResourceService ResourceService { get; }
|
||||
private ISysUserService SysUserService { get; }
|
||||
@@ -93,7 +93,7 @@ public class BlazorAppContext
|
||||
AllResource = sysResources;
|
||||
var ids = CurrentUser.ModuleList.Select(a => a.Id).ToHashSet();
|
||||
CurrentUser.ModuleList = AllResource.Where(a => ids.Contains(a.Id)).OrderBy(a => a.SortCode).ToList();
|
||||
AllMenus = AllResource.Where(a => a.Category == ResourceCategoryEnum.Menu);
|
||||
AllMenus = AllResource.Where(a => a.Category == ResourceCategoryEnum.Menu).ToList();
|
||||
|
||||
if (moduleId == null)
|
||||
{
|
||||
@@ -123,17 +123,26 @@ public class BlazorAppContext
|
||||
}
|
||||
}
|
||||
var ownMenus = OwnMenus.Where(a => a.Module == CurrentModuleId);
|
||||
OwnMenuItems = ResourceUtil.BuildMenuTrees(ownMenus).ToList();
|
||||
AllOwnMenuItems = ResourceUtil.BuildMenuTrees(OwnMenus).ToList();
|
||||
OwnSameLevelMenuItems = ownMenus.Where(a => !a.Href.IsNullOrWhiteSpace()).Select(item => new MenuItem()
|
||||
OwnMenuItems = AdminResourceUtil.BuildMenuTrees(ownMenus).ToList();
|
||||
AllOwnMenuItems = AdminResourceUtil.BuildMenuTrees(OwnMenus).ToList();
|
||||
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(),
|
||||
});
|
||||
UserWorkbenchOutputs = AllMenus.Where(it => UserWorkBench.Shortcuts.Contains(it.Id));
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -29,7 +29,7 @@ public partial class EditPagePolicy
|
||||
|
||||
protected override Task OnParametersSetAsync()
|
||||
{
|
||||
ShortcutsTreeViewItems = ResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
|
||||
ShortcutsTreeViewItems = AdminResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
|
||||
return base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
@@ -48,6 +48,6 @@ public partial class EditPagePolicy
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
ShortcutsSearchText = searchText;
|
||||
return ResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
|
||||
return AdminResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
|
||||
}
|
||||
}
|
||||
|
@@ -41,7 +41,7 @@ public partial class MenuChoiceDialog
|
||||
var all = (await SysResourceService.GetAllAsync());
|
||||
var items = all.Where(a => a.Category == ResourceCategoryEnum.Menu && a.Module == ModuleId);
|
||||
ModuleTitle = all.FirstOrDefault(a => a.Id == ModuleId)?.Title;
|
||||
Items = ResourceUtil.BuildTreeItemList(items, new List<long> { Value }, RenderTreeItem);
|
||||
Items = AdminResourceUtil.BuildTreeItemList(items, new List<long> { Value }, RenderTreeItem);
|
||||
await base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
|
@@ -26,12 +26,12 @@
|
||||
OnQueryAsync="OnQueryAsync" CustomerSearchModel="@CustomerSearchModel"
|
||||
OnSaveAsync="Save" OnDeleteAsync="Delete">
|
||||
<TableToolbarTemplate>
|
||||
<PopConfirmButton Color=Color.Warning IsDisabled="SelectedRows.Count<=0||!AuthorizeButton(AdminOperConst.Add)" Text=@OperDescLocalizer["CopyResource"] Icon="fa fa-copy" OnConfirm="OnCopy">
|
||||
<PopConfirmButton Color=Color.Warning IsKeepDisabled="SelectedRows.Count <= 0 || !AuthorizeButton(AdminOperConst.Add)" Text=@OperDescLocalizer["CopyResource"] Icon="fa fa-copy" OnConfirm="OnCopy">
|
||||
<BodyTemplate>
|
||||
<Select Items="ModuleSelectedItems" @bind-Value=CopyModule ShowLabel="false" />
|
||||
</BodyTemplate>
|
||||
</PopConfirmButton>
|
||||
<PopConfirmButton Color=Color.Warning IsDisabled="SelectedRows.Count!=1||!AuthorizeButton(AdminOperConst.Edit)" Text=@OperDescLocalizer["ChangeParentResource"] Icon="fa fa-copy" OnConfirm="OnChangeParent">
|
||||
<PopConfirmButton Color=Color.Warning IsKeepDisabled="SelectedRows.Count != 1 || !AuthorizeButton(AdminOperConst.Edit)" Text=@OperDescLocalizer["ChangeParentResource"] Icon="fa fa-copy" OnConfirm="OnChangeParent">
|
||||
<BodyTemplate>
|
||||
<div class="overflow-y-auto" style="height:500px">
|
||||
<TreeView Items="MenuTreeItems" IsVirtualize="true" OnTreeItemClick="a=>{ChangeParentId=a.Value.Id;return Task.CompletedTask;}" />
|
||||
|
@@ -39,8 +39,8 @@ public partial class SysResourcePage
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
ModuleSelectedItems = ResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
|
||||
MenuItems = ResourceUtil.BuildMenuSelectList((await SysResourceService.GetAllAsync())).Concat(new List<SelectedItem>() { new("0", AdminLocalizer["Root"]) }).ToList();
|
||||
ModuleSelectedItems = AdminResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
|
||||
MenuItems = AdminResourceUtil.BuildMenuSelectList((await SysResourceService.GetAllAsync())).Concat(new List<SelectedItem>() { new("0", AdminLocalizer["Root"]) }).ToList();
|
||||
|
||||
await base.OnParametersSetAsync();
|
||||
}
|
||||
@@ -49,7 +49,7 @@ public partial class SysResourcePage
|
||||
|
||||
private async Task<QueryData<SysResource>> OnQueryAsync(QueryPageOptions options)
|
||||
{
|
||||
MenuTreeItems = new List<TreeViewItem<SysResource>>() { new TreeViewItem<SysResource>(new SysResource()) { Text = AdminLocalizer["Root"] } }.Concat(ResourceUtil.BuildTreeItemList((await SysResourceService.GetAllAsync()).Where(a => a.Module == CustomerSearchModel.Module), new(), null)).ToList();
|
||||
MenuTreeItems = new List<TreeViewItem<SysResource>>() { new TreeViewItem<SysResource>(new SysResource()) { Text = AdminLocalizer["Root"] } }.Concat(AdminResourceUtil.BuildTreeItemList((await SysResourceService.GetAllAsync()).Where(a => a.Module == CustomerSearchModel.Module), new(), null)).ToList();
|
||||
|
||||
var data = await SysResourceService.PageAsync(options, CustomerSearchModel);
|
||||
return data;
|
||||
@@ -136,14 +136,14 @@ public partial class SysResourcePage
|
||||
private async Task<IEnumerable<TableTreeNode<SysResource>>> OnTreeExpand(SysResource menu)
|
||||
{
|
||||
var sysResources = await SysResourceService.GetAllAsync();
|
||||
var result = ResourceUtil.BuildTableTrees(sysResources, menu.Id);
|
||||
var result = AdminResourceUtil.BuildTableTrees(sysResources, menu.Id);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static async Task<IEnumerable<TableTreeNode<SysResource>>> TreeNodeConverter(IEnumerable<SysResource> items)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
var result = ResourceUtil.BuildTableTrees(items, 0);
|
||||
var result = AdminResourceUtil.BuildTableTrees(items, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@@ -35,7 +35,7 @@ public partial class GrantResourceDialog
|
||||
{
|
||||
var items = (await SysResourceService.GetAllAsync()).Where(a => a.Category != ResourceCategoryEnum.Module).OrderBy(a => a.Module).ThenBy(a => a.Id).ToList();
|
||||
|
||||
Items = ResourceUtil.BuildTreeItemList(items, Value, RenderTreeItem);
|
||||
Items = AdminResourceUtil.BuildTreeItemList(items, Value, RenderTreeItem);
|
||||
ModuleList = (await SysResourceService.GetAllAsync()).Where(a => a.Category == ResourceCategoryEnum.Module).ToList();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
@@ -35,7 +35,7 @@ public partial class SysUserEdit
|
||||
BoolItems = LocalizerUtil.GetBoolItems(Model.GetType(), nameof(Model.Status));
|
||||
var items = await SysPositionService.SelectorAsync(new PositionSelectorInput());
|
||||
Items = PositionUtil.BuildCascaderItemList(items);
|
||||
ModuleSelectedItems = ResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
|
||||
ModuleSelectedItems = AdminResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -20,5 +20,6 @@ public class Startup : AppStartup
|
||||
services.AddScoped<IMenuService, MenuService>();
|
||||
services.AddScoped<IAuthRazorService, AuthRazorService>();
|
||||
services.AddBootstrapBlazorTableExportService();
|
||||
services.AddBootstrapBlazorWinBoxService();
|
||||
}
|
||||
}
|
||||
|
@@ -5,18 +5,22 @@
|
||||
|
||||
<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.3" />
|
||||
<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>
|
||||
<ItemGroup>
|
||||
|
@@ -14,7 +14,7 @@ namespace ThingsGateway.Admin.Razor;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[ThingsGateway.DependencyInjection.SuppressSniffer]
|
||||
public static class ResourceUtil
|
||||
public static class AdminResourceUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造选择项,ID/TITLE
|
||||
@@ -41,15 +41,22 @@ public static class ResourceUtil
|
||||
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;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -5,7 +5,8 @@
|
||||
#推送:docker push registry.cn-shenzhen.aliyuncs.com/thingsgateway/thingsgateway
|
||||
|
||||
#aspnetcore9.0环境
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
#FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble AS base
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
#默认web
|
||||
@@ -13,6 +14,8 @@ EXPOSE 5000
|
||||
|
||||
# 添加时区环境变量,亚洲,上海
|
||||
ENV TimeZone=Asia/Shanghai
|
||||
# 转发头
|
||||
ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
|
||||
# 使用软连接,并且将时区配置覆盖/etc/timezone
|
||||
RUN ln -snf /usr/share/zoneinfo/$TimeZone /etc/localtime && echo $TimeZone > /etc/timezone
|
||||
|
||||
|
@@ -5,7 +5,8 @@
|
||||
#推送:docker push registry.cn-shenzhen.aliyuncs.com/thingsgateway/thingsgateway_arm64
|
||||
|
||||
#aspnetcore9.0环境
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine-arm64v8 AS base
|
||||
#FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine-arm64v8 AS base
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble-arm64v8 AS base
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
#默认web
|
||||
@@ -13,6 +14,8 @@ EXPOSE 5000
|
||||
|
||||
# 添加时区环境变量,亚洲,上海
|
||||
ENV TimeZone=Asia/Shanghai
|
||||
# 转发头
|
||||
ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
|
||||
# 使用软连接,并且将时区配置覆盖/etc/timezone
|
||||
RUN ln -snf /usr/share/zoneinfo/$TimeZone /etc/localtime && echo $TimeZone > /etc/timezone
|
||||
|
||||
|
@@ -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/culture.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>
|
||||
|
@@ -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>
|
||||
|
@@ -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]
|
||||
|
@@ -45,7 +45,7 @@
|
||||
</app>
|
||||
|
||||
<script src="_content/BootstrapBlazor/js/bootstrap.blazor.bundle.min.js"></script>
|
||||
<script src=@($"{WebsiteConst.DefaultResourceUrl}js/culture.js")></script>
|
||||
<script src=@($"{WebsiteConst.DefaultResourceUrl}js/localStorageUtil.js")></script>
|
||||
<script src="_framework/blazor.server.js"></script>
|
||||
|
||||
<!-- PWA Service Worker -->
|
||||
|
@@ -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 =>
|
||||
{
|
||||
|
@@ -45,11 +45,11 @@ public class Startup : AppStartup
|
||||
options.ServicesStopConcurrently = true;
|
||||
});
|
||||
|
||||
//// 事件总线
|
||||
//services.AddEventBus(options =>
|
||||
//{
|
||||
// 事件总线
|
||||
services.AddEventBus(options =>
|
||||
{
|
||||
|
||||
//});
|
||||
});
|
||||
|
||||
// 任务调度
|
||||
services.AddSchedule(options => options.AddPersistence<JobPersistence>());
|
||||
@@ -132,7 +132,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 +187,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();
|
||||
|
||||
// 启用本地化
|
||||
|
@@ -4,7 +4,8 @@
|
||||
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0;$(OtherTargetFrameworks);</TargetFrameworks>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<!--<Import Project="Admin.targets" Condition=" '$(Configuration)' != 'Debug' " />-->
|
||||
@@ -52,9 +53,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>
|
||||
|
34
src/Admin/ThingsGateway.AdminServer/wwwroot/pwa-install.js
Normal file
34
src/Admin/ThingsGateway.AdminServer/wwwroot/pwa-install.js
Normal 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("已提示过安装,不再弹出");
|
||||
}
|
||||
});
|
@@ -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>
|
||||
|
@@ -8,7 +8,7 @@
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace ThingsGateway.Gateway.Application;
|
||||
namespace ThingsGateway.Common;
|
||||
|
||||
public class SmartTriggerScheduler
|
||||
{
|
@@ -8,7 +8,7 @@
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace ThingsGateway.Gateway.Application;
|
||||
namespace ThingsGateway.Common;
|
||||
|
||||
public sealed class StringOrdinalIgnoreCaseEqualityComparer : EqualityComparer<string>
|
||||
{
|
@@ -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对象
|
||||
|
@@ -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>
|
||||
/// 合并两个字典
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -27,11 +27,11 @@ public class WebsiteOptions : IConfigurableOptions
|
||||
/// </summary>
|
||||
public bool Demo { get; set; }
|
||||
|
||||
public bool WebPageEnable { get; set; } = true;
|
||||
|
||||
public int MaxBlazorConnections { get; set; } = 5;
|
||||
public bool BlazorConnectionLimitEnable { get; set; } = false;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示关于页面
|
||||
/// </summary>
|
||||
|
@@ -5,15 +5,16 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
|
||||
</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.0" />
|
||||
<PackageReference Include="BootstrapBlazor" Version="9.11.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@@ -23,13 +23,19 @@ public abstract class PrimaryIdEntity : IPrimaryIdEntity
|
||||
[SugarColumn(ColumnDescription = "Id", IsPrimaryKey = true)]
|
||||
[IgnoreExcel]
|
||||
[AutoGenerateColumn(Visible = false, IsVisibleWhenEdit = false, IsVisibleWhenAdd = false, Sortable = true, DefaultSort = true, DefaultSortOrder = SortOrder.Asc)]
|
||||
[System.ComponentModel.DataAnnotations.Key]
|
||||
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>
|
||||
/// 拓展信息
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@@ -53,6 +53,8 @@ public static class QueryPageOptionsExtensions
|
||||
return datas;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static IEnumerable<T> GetQuery<T>(this IEnumerable<T> query, QueryPageOptions option, Func<IEnumerable<T>, IEnumerable<T>>? queryFunc = null, FilterKeyValueAction where = null)
|
||||
{
|
||||
if (queryFunc != null)
|
||||
@@ -123,7 +125,36 @@ public static class QueryPageOptionsExtensions
|
||||
};
|
||||
var items = datas.GetData(option, out var totalCount, where);
|
||||
ret.TotalCount = totalCount;
|
||||
|
||||
if (totalCount > 0)
|
||||
{
|
||||
if (!items.Any() && option.PageIndex != 1)
|
||||
{
|
||||
option.PageIndex = 1;
|
||||
items = datas.GetData(option, out totalCount, where);
|
||||
}
|
||||
}
|
||||
ret.Items = items.ToList();
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据查询条件返回QueryData
|
||||
/// </summary>
|
||||
public static QueryData<SelectedItem> GetQueryData<T>(this IEnumerable<T> datas, VirtualizeQueryOption option, Func<IEnumerable<T>, IEnumerable<SelectedItem>> func, FilterKeyValueAction where = null)
|
||||
{
|
||||
var ret = new QueryData<SelectedItem>()
|
||||
{
|
||||
IsSorted = false,
|
||||
IsFiltered = false,
|
||||
IsAdvanceSearch = false,
|
||||
IsSearch = !option.SearchText.IsNullOrWhiteSpace()
|
||||
};
|
||||
|
||||
var items = datas.Skip((option.StartIndex)).Take(option.Count);
|
||||
ret.TotalCount = datas.Count();
|
||||
|
||||
ret.Items = func(items).ToList();
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
|
13
src/Admin/ThingsGateway.DB/Locales/en-US.json
Normal file
13
src/Admin/ThingsGateway.DB/Locales/en-US.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
|
||||
"ThingsGateway.Admin.Application.BaseDataEntity": {
|
||||
"CreateOrgId": "CreateOrgId"
|
||||
},
|
||||
"ThingsGateway.Admin.Application.BaseEntity": {
|
||||
"CreateTime": "CreateTime",
|
||||
"CreateUser": "CreateUser",
|
||||
"SortCode": "SortCode",
|
||||
"UpdateTime": "UpdateTime",
|
||||
"UpdateUser": "UpdateUser"
|
||||
}
|
||||
}
|
13
src/Admin/ThingsGateway.DB/Locales/zh-CN.json
Normal file
13
src/Admin/ThingsGateway.DB/Locales/zh-CN.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
|
||||
"ThingsGateway.DB.BaseDataEntity": {
|
||||
"CreateOrgId": "创建机构Id"
|
||||
},
|
||||
"ThingsGateway.DB.BaseEntity": {
|
||||
"CreateTime": "创建时间",
|
||||
"CreateUser": "创建人",
|
||||
"SortCode": "排序",
|
||||
"UpdateTime": "更新时间",
|
||||
"UpdateUser": "更新人"
|
||||
}
|
||||
}
|
@@ -46,7 +46,7 @@ public class BaseService<T> : IDataService<T>, IDisposable where T : class, new(
|
||||
public async Task<bool> DeleteAsync(IEnumerable<T> models)
|
||||
{
|
||||
using var db = GetDB();
|
||||
return await db.Deleteable<T>().In(models.ToList()).ExecuteCommandHasChangeAsync().ConfigureAwait(false);
|
||||
return await db.Deleteable<T>(models.ToList()).ExecuteCommandHasChangeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -140,18 +140,22 @@ public class BaseService<T> : IDataService<T>, IDisposable where T : class, new(
|
||||
return (await db.UpdateableT(model).ExecuteCommandAsync().ConfigureAwait(false)) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task<bool> SaveAsync(List<T> model, ItemChangedType changedType)
|
||||
{
|
||||
return (await SaveReturnCountAsync(model, changedType).ConfigureAwait(false)) > 0;
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public async Task<int> SaveReturnCountAsync(List<T> model, ItemChangedType changedType)
|
||||
{
|
||||
using var db = GetDB();
|
||||
if (changedType == ItemChangedType.Add)
|
||||
{
|
||||
return (await db.Insertable(model).ExecuteCommandAsync().ConfigureAwait(false)) > 0;
|
||||
return (await db.Insertable(model).ExecuteCommandAsync().ConfigureAwait(false));
|
||||
}
|
||||
else
|
||||
{
|
||||
return (await db.Updateable(model).ExecuteCommandAsync().ConfigureAwait(false)) > 0;
|
||||
return (await db.Updateable(model).ExecuteCommandAsync().ConfigureAwait(false));
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
|
@@ -5,9 +5,10 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0;$(OtherTargetFrameworks);</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -18,6 +19,12 @@
|
||||
<None Remove="..\..\..\README.zh-CN.md" Pack="false" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Locales\en-US.json" />
|
||||
<EmbeddedResource Include="Locales\zh-CN.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!--<PackageReference Include="ThingsGateway.Razor" Version="$(SourceGeneratorVersion)" />-->
|
||||
<!--<ProjectReference Include="..\ThingsGateway.Razor\ThingsGateway.Razor.csproj" />-->
|
||||
|
@@ -1 +1 @@
|
||||
https://gitee.com/dotnetchina/Furion/commit/8bf85f6908c1630268e45eeec607267a03947d2b
|
||||
https://gitee.com/dotnetchina/Furion/commit/f1c07d65cccb623aca9d1905bf2e1ac6e4f4b714
|
@@ -27,18 +27,27 @@ using System.Security.Claims;
|
||||
using ThingsGateway.ConfigurableOptions;
|
||||
using ThingsGateway.NewLife.Caching;
|
||||
using ThingsGateway.NewLife.Collections;
|
||||
using ThingsGateway.NewLife.Extension;
|
||||
using ThingsGateway.NewLife.Log;
|
||||
using ThingsGateway.Reflection;
|
||||
using ThingsGateway.Templates;
|
||||
|
||||
namespace ThingsGateway;
|
||||
|
||||
|
||||
public static class WebEnableVariable
|
||||
{
|
||||
public static bool WebEnable => Environment.GetEnvironmentVariable(nameof(WebEnable)).ToBoolean(true);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全局应用类
|
||||
/// </summary>
|
||||
[SuppressSniffer]
|
||||
public static class App
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 私有设置,避免重复解析
|
||||
/// </summary>
|
||||
@@ -157,7 +166,7 @@ public static class App
|
||||
var httpContextAccessor = RootServices?.GetService<IHttpContextAccessor>();
|
||||
try
|
||||
{
|
||||
return httpContextAccessor.HttpContext;
|
||||
return httpContextAccessor?.HttpContext;
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -545,10 +554,9 @@ public static class App
|
||||
{
|
||||
types = ass.GetTypes();
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
XTrace.Log.Warn($"Error load `{ass.FullName}` assembly.");
|
||||
Console.WriteLine($"Error load `{ass.FullName}` assembly.");
|
||||
XTrace.Log.Warn($"Error load `{ass.FullName}` assembly. : {ex.Message}");
|
||||
}
|
||||
|
||||
return types.Where(u => u.IsPublic && !u.IsDefined(typeof(SuppressSnifferAttribute), false));
|
||||
|
@@ -213,12 +213,18 @@ public static class AppServiceCollectionExtensions
|
||||
// 缓存
|
||||
if (cacheOptions.CacheType == CacheType.Memory)
|
||||
{
|
||||
services.AddSingleton<ICache, MemoryCache>(a => new()
|
||||
services.AddSingleton<ICache>(a =>
|
||||
{
|
||||
Capacity = cacheOptions.MemoryCacheOptions.Capacity,
|
||||
Expire = cacheOptions.MemoryCacheOptions.Expire,
|
||||
Period = cacheOptions.MemoryCacheOptions.Period
|
||||
});
|
||||
Cache.Default = new MemoryCache()
|
||||
{
|
||||
Capacity = cacheOptions.MemoryCacheOptions.Capacity,
|
||||
Expire = cacheOptions.MemoryCacheOptions.Expire,
|
||||
Period = cacheOptions.MemoryCacheOptions.Period
|
||||
};
|
||||
return Cache.Default;
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
else if (cacheOptions.CacheType == CacheType.Redis)
|
||||
{
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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[]
|
||||
|
@@ -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;
|
||||
|
@@ -85,11 +85,14 @@ internal static class InternalApp
|
||||
// 存储根服务(解决 Web 主机还未启动时在 HostedService 中使用 App.GetService 问题
|
||||
services.AddHostedService<GenericHostLifetimeEventsHostedService>();
|
||||
|
||||
// 注册 Startup 过滤器
|
||||
services.AddTransient<IStartupFilter, StartupFilter>();
|
||||
if (WebEnableVariable.WebEnable == true)
|
||||
{
|
||||
// 注册 Startup 过滤器
|
||||
services.AddTransient<IStartupFilter, StartupFilter>();
|
||||
|
||||
// 注册 HttpContextAccessor 服务
|
||||
services.AddHttpContextAccessor();
|
||||
// 注册 HttpContextAccessor 服务
|
||||
services.AddHttpContextAccessor();
|
||||
}
|
||||
|
||||
// 初始化应用服务
|
||||
services.AddApp();
|
||||
@@ -212,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[]>()
|
||||
@@ -237,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ namespace ThingsGateway;
|
||||
/// </summary>
|
||||
public sealed class AppSettingsOptions : IConfigurableOptions<AppSettingsOptions>
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用规范化文档
|
||||
/// </summary>
|
||||
@@ -50,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>
|
||||
@@ -66,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;
|
||||
}
|
341
src/Admin/ThingsGateway.Furion/App/Options/MiniRunOptions.cs
Normal file
341
src/Admin/ThingsGateway.Furion/App/Options/MiniRunOptions.cs
Normal file
@@ -0,0 +1,341 @@
|
||||
// ------------------------------------------------------------------------
|
||||
// 版权信息
|
||||
// 版权归百小僧及百签科技(广东)有限公司所有。
|
||||
// 所有权利保留。
|
||||
// 官方网站:https://baiqian.com
|
||||
//
|
||||
// 许可证信息
|
||||
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
using ThingsGateway;
|
||||
|
||||
namespace System;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="WebApplication"/> 方式配置选项
|
||||
/// </summary>
|
||||
[SuppressSniffer]
|
||||
public sealed class MiniRunOptions : IRunOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 内部构造函数
|
||||
/// </summary>
|
||||
internal MiniRunOptions()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 默认配置
|
||||
/// </summary>
|
||||
public static MiniRunOptions Default { get; } = new MiniRunOptions();
|
||||
|
||||
/// <summary>
|
||||
/// 默认配置(带启动参数)
|
||||
/// </summary>
|
||||
public static MiniRunOptions Main(string[] args)
|
||||
{
|
||||
return Default.WithArgs(args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 默认配置(静默启动)
|
||||
/// </summary>
|
||||
public static MiniRunOptions DefaultSilence { get; } = new MiniRunOptions().Silence();
|
||||
|
||||
/// <summary>
|
||||
/// 默认配置(静默启动 + 启动参数)
|
||||
/// </summary>
|
||||
public static MiniRunOptions MainSilence(string[] args)
|
||||
{
|
||||
return DefaultSilence.WithArgs(args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 <see cref="WebApplicationOptions"/>
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
/// <returns><see cref="MiniRunOptions"/></returns>
|
||||
public MiniRunOptions ConfigureOptions(WebApplicationOptions options)
|
||||
{
|
||||
Options = options;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 <see cref="IWebHostBuilder"/>
|
||||
/// </summary>
|
||||
/// <param name="configureAction"></param>
|
||||
/// <returns><see cref="MiniRunOptions"/></returns>
|
||||
public MiniRunOptions ConfigureBuilder(Action<IWebHostBuilder> configureAction)
|
||||
{
|
||||
ActionBuilder = configureAction;
|
||||
return this;
|
||||
}
|
||||
/// <summary>
|
||||
/// 配置 <see cref="IHostBuilder"/>
|
||||
/// </summary>
|
||||
/// <param name="configureAction"></param>
|
||||
/// <returns><see cref="MiniRunOptions"/></returns>
|
||||
public MiniRunOptions ConfigureFirstActionBuilder(Action<IHostBuilder> configureAction)
|
||||
{
|
||||
FirstActionBuilder = configureAction;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 <see cref="IServiceCollection"/>
|
||||
/// </summary>
|
||||
/// <param name="configureAction"></param>
|
||||
/// <returns><see cref="MiniRunOptions"/></returns>
|
||||
public MiniRunOptions ConfigureServices(Action<IServiceCollection> configureAction)
|
||||
{
|
||||
ActionServices = configureAction;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 <see cref="InjectOptions"/>
|
||||
/// </summary>
|
||||
/// <param name="configureAction"></param>
|
||||
/// <returns><see cref="MiniRunOptions"/></returns>
|
||||
public MiniRunOptions ConfigureInject(Action<IWebHostBuilder, InjectOptions> configureAction)
|
||||
{
|
||||
ActionInject = configureAction;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 <see cref="WebApplication"/>
|
||||
/// </summary>
|
||||
/// <param name="configureAction">配置委托</param>
|
||||
/// <returns><see cref="MiniRunOptions"/></returns>
|
||||
public MiniRunOptions Configure(Action<IHost> configureAction)
|
||||
{
|
||||
ActionConfigure = configureAction;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 <see cref="ConfigurationManager"/>
|
||||
/// </summary>
|
||||
/// <param name="configureAction">配置委托</param>
|
||||
/// <returns><see cref="MiniRunOptions"/></returns>
|
||||
public MiniRunOptions ConfigureConfiguration(Action<IHostEnvironment, IConfiguration> configureAction)
|
||||
{
|
||||
ActionConfigurationManager = configureAction;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加应用服务组件
|
||||
/// </summary>
|
||||
/// <typeparam name="TComponent">组件类型</typeparam>
|
||||
/// <returns></returns>
|
||||
public MiniRunOptions AddComponent<TComponent>()
|
||||
where TComponent : class, IServiceComponent, new()
|
||||
{
|
||||
ServiceComponents.Add(typeof(TComponent), null);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加应用服务组件
|
||||
/// </summary>
|
||||
/// <typeparam name="TComponent">组件类型</typeparam>
|
||||
/// <typeparam name="TComponentOptions"></typeparam>
|
||||
/// <param name="options">组件参数</param>
|
||||
/// <returns></returns>
|
||||
public MiniRunOptions AddComponent<TComponent, TComponentOptions>(TComponentOptions options)
|
||||
where TComponent : class, IServiceComponent, new()
|
||||
{
|
||||
ServiceComponents.Add(typeof(TComponent), options);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加应用服务组件
|
||||
/// </summary>
|
||||
/// <param name="componentType">组件类型</param>
|
||||
/// <param name="options">组件参数</param>
|
||||
/// <returns></returns>
|
||||
public MiniRunOptions AddComponent(Type componentType, object options)
|
||||
{
|
||||
ServiceComponents.Add(componentType, options);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加应用中间件组件
|
||||
/// </summary>
|
||||
/// <typeparam name="TComponent">组件类型</typeparam>
|
||||
/// <returns></returns>
|
||||
public MiniRunOptions UseComponent<TComponent>()
|
||||
where TComponent : class, IApplicationComponent, new()
|
||||
{
|
||||
ApplicationComponents.Add(typeof(TComponent), null);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加应用中间件组件
|
||||
/// </summary>
|
||||
/// <typeparam name="TComponent">组件类型</typeparam>
|
||||
/// <typeparam name="TComponentOptions"></typeparam>
|
||||
/// <param name="options">组件参数</param>
|
||||
/// <returns></returns>
|
||||
public MiniRunOptions UseComponent<TComponent, TComponentOptions>(TComponentOptions options)
|
||||
where TComponent : class, IApplicationComponent, new()
|
||||
{
|
||||
ApplicationComponents.Add(typeof(TComponent), options);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加应用中间件组件
|
||||
/// </summary>
|
||||
/// <param name="componentType">组件类型</param>
|
||||
/// <param name="options">组件参数</param>
|
||||
/// <returns></returns>
|
||||
public MiniRunOptions UseComponent(Type componentType, object options)
|
||||
{
|
||||
ApplicationComponents.Add(componentType, options);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加 IWebHostBuilder 组件
|
||||
/// </summary>
|
||||
/// <typeparam name="TComponent">组件类型</typeparam>
|
||||
/// <returns></returns>
|
||||
public MiniRunOptions AddWebComponent<TComponent>()
|
||||
where TComponent : class, IWebComponent, new()
|
||||
{
|
||||
WebComponents.Add(typeof(TComponent), null);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加 IWebHostBuilder 组件
|
||||
/// </summary>
|
||||
/// <typeparam name="TComponent">组件类型</typeparam>
|
||||
/// <typeparam name="TComponentOptions"></typeparam>
|
||||
/// <param name="options">组件参数</param>
|
||||
/// <returns></returns>
|
||||
public MiniRunOptions AddWebComponent<TComponent, TComponentOptions>(TComponentOptions options)
|
||||
where TComponent : class, IWebComponent, new()
|
||||
{
|
||||
WebComponents.Add(typeof(TComponent), options);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加 IWebHostBuilder 组件
|
||||
/// </summary>
|
||||
/// <param name="componentType">组件类型</param>
|
||||
/// <param name="options">组件参数</param>
|
||||
/// <returns></returns>
|
||||
public MiniRunOptions AddWebComponent(Type componentType, object options)
|
||||
{
|
||||
WebComponents.Add(componentType, options);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标识主机静默启动
|
||||
/// </summary>
|
||||
/// <remarks>不阻塞程序运行</remarks>
|
||||
/// <param name="silence">静默启动</param>
|
||||
/// <param name="logging">静默启动日志状态,默认 false</param>
|
||||
/// <returns></returns>
|
||||
public MiniRunOptions Silence(bool silence = true, bool logging = false)
|
||||
{
|
||||
IsSilence = silence;
|
||||
SilenceLogging = logging;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置进程启动参数
|
||||
/// </summary>
|
||||
/// <param name="args">启动参数</param>
|
||||
/// <returns></returns>
|
||||
public MiniRunOptions WithArgs(string[] args)
|
||||
{
|
||||
Args = args;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="WebApplicationOptions"/>
|
||||
/// </summary>
|
||||
internal WebApplicationOptions Options { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 自定义 <see cref="IServiceCollection"/> 委托
|
||||
/// </summary>
|
||||
internal Action<IServiceCollection> ActionServices { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 自定义 <see cref="IWebHostBuilder"/> 委托
|
||||
/// </summary>
|
||||
internal Action<IHostBuilder> FirstActionBuilder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 自定义 <see cref="IWebHostBuilder"/> 委托
|
||||
/// </summary>
|
||||
internal Action<IWebHostBuilder> ActionBuilder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 自定义 <see cref="InjectOptions"/> 委托
|
||||
/// </summary>
|
||||
internal Action<IWebHostBuilder, InjectOptions> ActionInject { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 自定义 <see cref="IHost"/> 委托
|
||||
/// </summary>
|
||||
internal Action<IHost> ActionConfigure { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 自定义 <see cref="IConfiguration"/> 委托
|
||||
/// </summary>
|
||||
internal Action<IHostEnvironment, IConfiguration> ActionConfigurationManager { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用服务组件
|
||||
/// </summary>
|
||||
internal Dictionary<Type, object> ServiceComponents { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// IWebHostBuilder 组件
|
||||
/// </summary>
|
||||
internal Dictionary<Type, object> WebComponents { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 应用中间件组件
|
||||
/// </summary>
|
||||
internal Dictionary<Type, object> ApplicationComponents { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 静默启动
|
||||
/// </summary>
|
||||
/// <remarks>不阻塞程序运行</remarks>
|
||||
internal bool IsSilence { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 静默启动日志状态
|
||||
/// </summary>
|
||||
internal bool SilenceLogging { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 命令行参数
|
||||
/// </summary>
|
||||
internal string[] Args { get; set; }
|
||||
}
|
@@ -602,6 +602,33 @@ public static class Serve
|
||||
return app;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动 WebApplication 主机
|
||||
/// </summary>
|
||||
/// <remarks>未包含 Web 基础功能,需手动注册服务/中间件</remarks>
|
||||
/// <param name="options">配置选项</param>
|
||||
/// <param name="urls">默认 5000/5001 端口</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns><see cref="IHost"/></returns>
|
||||
public static async Task<IHost> RunAsync(MiniRunOptions options, string urls = default, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 构建 WebApplication 对象
|
||||
BuildMiniApplication(options, urls, out var app);
|
||||
|
||||
// 是否静默启动
|
||||
if (!options.IsSilence)
|
||||
{
|
||||
// 配置启动地址和端口
|
||||
await app.RunAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await app.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建 WebApplication 对象
|
||||
/// </summary>
|
||||
@@ -616,8 +643,8 @@ public static class Serve
|
||||
|
||||
// 初始化 WebApplicationBuilder
|
||||
var builder = (options.Options == null
|
||||
? WebApplication.CreateBuilder(args)
|
||||
: WebApplication.CreateBuilder(options.Options));
|
||||
? WebApplication.CreateBuilder(args)
|
||||
: WebApplication.CreateBuilder(options.Options));
|
||||
|
||||
// 调用自定义配置服务
|
||||
options?.FirstActionBuilder?.Invoke(builder);
|
||||
@@ -674,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);
|
||||
|
||||
@@ -793,12 +820,138 @@ 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建 IHost 对象
|
||||
/// </summary>
|
||||
/// <param name="options">配置选项</param>
|
||||
/// <param name="urls">默认 5000/5001 端口</param>
|
||||
/// <param name="app"><see cref="IHost"/></param>
|
||||
public static void BuildMiniApplication(MiniRunOptions options, string urls, out IHost app)
|
||||
{
|
||||
// 获取命令行参数
|
||||
var args = options.Args ?? Environment.GetCommandLineArgs().Skip(1).ToArray();
|
||||
|
||||
|
||||
var builder = Host.CreateDefaultBuilder(args);
|
||||
|
||||
// 静默启动排除指定日志类名
|
||||
if (options.IsSilence && !options.SilenceLogging)
|
||||
{
|
||||
builder = builder.ConfigureLogging(logging =>
|
||||
{
|
||||
logging.AddFilter((provider, category, logLevel) => !SilenceExcludesOfLogCategoryName.Any(u => category.StartsWith(u)));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 配置 Web 主机
|
||||
builder = builder.ConfigureWebHost(webHostBuilder =>
|
||||
{
|
||||
|
||||
|
||||
// 调用自定义配置服务
|
||||
options?.FirstActionBuilder?.Invoke(builder);
|
||||
|
||||
// 注册 WebApplicationBuilder 组件
|
||||
if (options.WebComponents.Count > 0)
|
||||
{
|
||||
foreach (var (componentType, opt) in options.WebComponents)
|
||||
{
|
||||
webHostBuilder.AddWebComponent(componentType, opt);
|
||||
}
|
||||
}
|
||||
|
||||
webHostBuilder.Configure((WebHostBuilderContext app, IApplicationBuilder applicationBuilder) =>
|
||||
{
|
||||
|
||||
// 添加自定义配置
|
||||
options.ActionConfigurationManager?.Invoke(app.HostingEnvironment, app.Configuration);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
// 初始化框架
|
||||
webHostBuilder.Inject(options.ActionInject);
|
||||
|
||||
|
||||
|
||||
// 配置服务
|
||||
if (options.ServiceComponents.Count > 0)
|
||||
{
|
||||
webHostBuilder = webHostBuilder.ConfigureServices(services =>
|
||||
{
|
||||
// 注册应用服务组件
|
||||
foreach (var (componentType, opt) in options.ServiceComponents)
|
||||
{
|
||||
services.AddComponent(componentType, opt);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
// 配置启动地址和端口
|
||||
var startUrls = !string.IsNullOrWhiteSpace(urls) ? urls : webHostBuilder.GetSetting(nameof(urls));
|
||||
|
||||
// 自定义启动端口
|
||||
if (!string.IsNullOrWhiteSpace(startUrls))
|
||||
{
|
||||
webHostBuilder = webHostBuilder.UseUrls(startUrls);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// 调用自定义配置
|
||||
options?.ActionBuilder?.Invoke(webHostBuilder);
|
||||
|
||||
// 配置中间件
|
||||
if (options.ApplicationComponents.Count > 0)
|
||||
{
|
||||
webHostBuilder = webHostBuilder.Configure((context, app) =>
|
||||
{
|
||||
// 注册应用中间件组件
|
||||
foreach (var (componentType, opt) in options.ApplicationComponents)
|
||||
{
|
||||
app.UseComponent(context.HostingEnvironment, componentType, opt);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
builder = builder.ConfigureServices(services =>
|
||||
{
|
||||
// 调用自定义配置服务
|
||||
options?.ActionServices?.Invoke(services);
|
||||
});
|
||||
|
||||
// 构建主机
|
||||
app = builder.Build();
|
||||
|
||||
InternalApp.RootServices ??= app.Services;
|
||||
|
||||
var applicationPartManager = app.Services.GetService<ApplicationPartManager>();
|
||||
|
||||
applicationPartManager?.ApplicationParts?.RemoveWhere(p => App.BakImageNames.Any(b => b == p.Name));
|
||||
// 配置所有 Startup Configure
|
||||
UseStartups(app.Services);
|
||||
// 释放内存
|
||||
App.AppStartups.Clear();
|
||||
// 调用自定义配置
|
||||
options?.ActionConfigure?.Invoke(app);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 构建 IHost 对象
|
||||
/// </summary>
|
||||
@@ -852,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();
|
||||
|
@@ -11,7 +11,7 @@
|
||||
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Extension;
|
||||
|
||||
namespace ThingsGateway.AspNetCore;
|
||||
|
||||
|
@@ -11,7 +11,7 @@
|
||||
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Extension;
|
||||
|
||||
namespace ThingsGateway.AspNetCore;
|
||||
|
||||
|
@@ -12,7 +12,7 @@
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Extension;
|
||||
|
||||
namespace ThingsGateway.AspNetCore;
|
||||
|
||||
|
@@ -18,7 +18,7 @@ using System.Reflection;
|
||||
|
||||
using ThingsGateway;
|
||||
using ThingsGateway.ConfigurableOptions;
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Extension;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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)))
|
||||
{
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -9,7 +9,7 @@
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Extension;
|
||||
|
||||
namespace ThingsGateway.Logging;
|
||||
|
||||
|
@@ -127,7 +127,8 @@ public sealed class DatabaseLogger : ILogger, IDisposable
|
||||
// 设置日志消息模板
|
||||
logMsg.Message = _options.MessageFormat != null
|
||||
? _options.MessageFormat(logMsg)
|
||||
: Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame, provider: _options.FormatProvider);
|
||||
: string.Empty;
|
||||
//: Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame, provider: _options.FormatProvider);
|
||||
|
||||
// 空检查
|
||||
if (logMsg.Message is null)
|
||||
|
@@ -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)
|
||||
{
|
||||
// 解析存储的验证信息
|
||||
|
@@ -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;
|
||||
|
@@ -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())
|
||||
{
|
||||
}
|
||||
}
|
@@ -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())
|
||||
{
|
||||
}
|
||||
}
|
@@ -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())
|
||||
{
|
||||
}
|
||||
}
|
@@ -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())
|
||||
{
|
||||
}
|
||||
}
|
@@ -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())
|
||||
{
|
||||
}
|
||||
}
|
@@ -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())
|
||||
{
|
||||
}
|
||||
}
|
@@ -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())
|
||||
{
|
||||
}
|
||||
}
|
@@ -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)))
|
||||
{
|
||||
// 待输出
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
|
@@ -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"
|
||||
]
|
||||
}
|
@@ -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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user