Browse Source

初始化提交

master
root 1 month ago
commit
2f68719515
  1. 332
      .gitignore
  2. 22
      LTEMvcApp.sln
  3. 143
      LTEMvcApp/Controllers/HomeController.cs
  4. 262
      LTEMvcApp/Controllers/WebSocketController.cs
  5. 14
      LTEMvcApp/LTEMvcApp.csproj
  6. 8
      LTEMvcApp/Models/ErrorViewModel.cs
  7. 422
      LTEMvcApp/Models/LTEClient.cs
  8. 204
      LTEMvcApp/Models/LTELog.cs
  9. 38
      LTEMvcApp/Program.cs
  10. 38
      LTEMvcApp/Properties/launchSettings.json
  11. 52
      LTEMvcApp/README.md
  12. 964
      LTEMvcApp/Services/LTEClientWebSocket.cs
  13. 638
      LTEMvcApp/Services/LogParserService.cs
  14. 400
      LTEMvcApp/Services/WebSocketManagerService.cs
  15. 240
      LTEMvcApp/Views/Home/Index.cshtml
  16. 6
      LTEMvcApp/Views/Home/Privacy.cshtml
  17. 25
      LTEMvcApp/Views/Shared/Error.cshtml
  18. 49
      LTEMvcApp/Views/Shared/_Layout.cshtml
  19. 48
      LTEMvcApp/Views/Shared/_Layout.cshtml.css
  20. 2
      LTEMvcApp/Views/Shared/_ValidationScriptsPartial.cshtml
  21. 3
      LTEMvcApp/Views/_ViewImports.cshtml
  22. 3
      LTEMvcApp/Views/_ViewStart.cshtml
  23. 8
      LTEMvcApp/appsettings.Development.json
  24. 9
      LTEMvcApp/appsettings.json
  25. 22
      LTEMvcApp/wwwroot/css/site.css
  26. BIN
      LTEMvcApp/wwwroot/favicon.ico
  27. 4
      LTEMvcApp/wwwroot/js/site.js
  28. 22
      LTEMvcApp/wwwroot/lib/bootstrap/LICENSE
  29. 4997
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
  30. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
  31. 7
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
  32. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map
  33. 4996
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
  34. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
  35. 7
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
  36. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
  37. 427
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
  38. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map
  39. 8
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
  40. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
  41. 424
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
  42. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
  43. 8
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
  44. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
  45. 4866
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
  46. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map
  47. 7
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css
  48. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
  49. 4857
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
  50. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
  51. 7
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
  52. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
  53. 11221
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap.css
  54. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map
  55. 7
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css
  56. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map
  57. 11197
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css
  58. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map
  59. 7
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css
  60. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
  61. 6780
      LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js
  62. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map
  63. 7
      LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js
  64. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map
  65. 4977
      LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js
  66. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map
  67. 7
      LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js
  68. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map
  69. 5026
      LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.js
  70. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map
  71. 7
      LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js
  72. 1
      LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map
  73. 23
      LTEMvcApp/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt
  74. 435
      LTEMvcApp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js
  75. 8
      LTEMvcApp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js
  76. 22
      LTEMvcApp/wwwroot/lib/jquery-validation/LICENSE.md
  77. 1512
      LTEMvcApp/wwwroot/lib/jquery-validation/dist/additional-methods.js
  78. 4
      LTEMvcApp/wwwroot/lib/jquery-validation/dist/additional-methods.min.js
  79. 1661
      LTEMvcApp/wwwroot/lib/jquery-validation/dist/jquery.validate.js
  80. 4
      LTEMvcApp/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js
  81. 21
      LTEMvcApp/wwwroot/lib/jquery/LICENSE.txt
  82. 10881
      LTEMvcApp/wwwroot/lib/jquery/dist/jquery.js
  83. 2
      LTEMvcApp/wwwroot/lib/jquery/dist/jquery.min.js
  84. 1
      LTEMvcApp/wwwroot/lib/jquery/dist/jquery.min.map

332
.gitignore

@ -0,0 +1,332 @@
# Visual Studio files
.vs/
bin/
obj/
*.user
*.suo
*.userosscache
*.sln.docstates
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these files may be visible to others.
*.azurePubxml
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment the next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
CConversionReportFiles/
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/

22
LTEMvcApp.sln

@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LTEMvcApp", "LTEMvcApp\LTEMvcApp.csproj", "{8D571BC4-879A-F1EC-56AF-294A73296DD6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8D571BC4-879A-F1EC-56AF-294A73296DD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8D571BC4-879A-F1EC-56AF-294A73296DD6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8D571BC4-879A-F1EC-56AF-294A73296DD6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8D571BC4-879A-F1EC-56AF-294A73296DD6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

143
LTEMvcApp/Controllers/HomeController.cs

@ -0,0 +1,143 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using LTEMvcApp.Models;
using LTEMvcApp.Services;
namespace LTEMvcApp.Controllers;
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly WebSocketManagerService _webSocketManager;
public HomeController(ILogger<HomeController> logger, WebSocketManagerService webSocketManager)
{
_logger = logger;
_webSocketManager = webSocketManager;
}
public IActionResult Index()
{
_logger.LogInformation("访问首页");
// 获取连接统计信息
var stats = _webSocketManager.GetConnectionStatistics();
ViewBag.ConnectionStats = stats;
// 获取所有客户端配置
var configs = _webSocketManager.GetAllClientConfigs();
ViewBag.ClientConfigs = configs;
return View();
}
public IActionResult Privacy()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
/// <summary>
/// WebSocket测试页面
/// </summary>
public IActionResult WebSocketTest()
{
return View();
}
/// <summary>
/// 添加测试客户端配置
/// </summary>
[HttpPost]
public IActionResult AddTestClient()
{
_logger.LogInformation("添加测试客户端配置");
var config = new ClientConfig
{
Name = "测试客户端",
Address = "192.168.13.12:9001",
Ssl = false,
Enabled = true,
ReconnectDelay = 5000,
Password = "test123",
Logs = new Dictionary<string, object>
{
["layers"] = new Dictionary<string, object>
{
["PHY"] = new Dictionary<string, object>
{
["level"] = "debug",
["filter"] = "debug",
["max_size"] = 1000,
["payload"] = false
},
["RRC"] = new Dictionary<string, object>
{
["level"] = "debug",
["filter"] = "debug",
["max_size"] = 1000,
["payload"] = false
}
},
["signal"] = true,
["cch"] = true
}
};
var success = _webSocketManager.AddClientConfig(config);
if (success)
{
TempData["Message"] = "测试客户端配置已添加";
}
else
{
TempData["Error"] = "添加测试客户端配置失败";
}
return RedirectToAction(nameof(Index));
}
/// <summary>
/// 启动测试客户端
/// </summary>
[HttpPost]
public IActionResult StartTestClient()
{
_logger.LogInformation("启动测试客户端");
var success = _webSocketManager.StartClient("测试客户端");
if (success)
{
TempData["Message"] = "测试客户端已启动";
}
else
{
TempData["Error"] = "启动测试客户端失败";
}
return RedirectToAction(nameof(Index));
}
/// <summary>
/// 停止测试客户端
/// </summary>
[HttpPost]
public IActionResult StopTestClient()
{
_logger.LogInformation("停止测试客户端");
var success = _webSocketManager.StopClient("测试客户端");
if (success)
{
TempData["Message"] = "测试客户端已停止";
}
else
{
TempData["Error"] = "停止测试客户端失败";
}
return RedirectToAction(nameof(Index));
}
}

262
LTEMvcApp/Controllers/WebSocketController.cs

@ -0,0 +1,262 @@
using Microsoft.AspNetCore.Mvc;
using LTEMvcApp.Models;
using LTEMvcApp.Services;
using Newtonsoft.Json.Linq;
using Microsoft.Extensions.Logging;
namespace LTEMvcApp.Controllers
{
/// <summary>
/// WebSocket控制器 - 提供LTE客户端WebSocket管理API
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class WebSocketController : ControllerBase
{
private readonly WebSocketManagerService _webSocketManager;
private readonly ILogger<WebSocketController> _logger;
public WebSocketController(WebSocketManagerService webSocketManager, ILogger<WebSocketController> logger)
{
_webSocketManager = webSocketManager;
_logger = logger;
}
/// <summary>
/// 获取所有客户端状态
/// </summary>
/// <returns>客户端状态列表</returns>
[HttpGet("clients")]
public ActionResult<Dictionary<string, ClientState>> GetClientStates()
{
_logger.LogInformation("获取所有客户端状态");
var states = _webSocketManager.GetAllClientStates();
return Ok(states);
}
/// <summary>
/// 获取客户端配置
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>客户端配置</returns>
[HttpGet("clients/{clientName}/config")]
public ActionResult<ClientConfig?> GetClientConfig(string clientName)
{
var config = _webSocketManager.GetClientConfig(clientName);
if (config == null)
return NotFound($"客户端 '{clientName}' 不存在");
return Ok(config);
}
/// <summary>
/// 获取所有客户端配置
/// </summary>
/// <returns>客户端配置列表</returns>
[HttpGet("configs")]
public ActionResult<List<ClientConfig>> GetAllConfigs()
{
var configs = _webSocketManager.GetAllClientConfigs();
return Ok(configs);
}
/// <summary>
/// 添加客户端配置
/// </summary>
/// <param name="config">客户端配置</param>
/// <returns>操作结果</returns>
[HttpPost("configs")]
public ActionResult AddClientConfig([FromBody] ClientConfig config)
{
if (string.IsNullOrEmpty(config.Name))
return BadRequest("客户端名称不能为空");
var success = _webSocketManager.AddClientConfig(config);
if (success)
return Ok(new { message = $"客户端 '{config.Name}' 配置已添加" });
else
return BadRequest("添加客户端配置失败");
}
/// <summary>
/// 启动客户端
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>操作结果</returns>
[HttpPost("clients/{clientName}/start")]
public ActionResult StartClient(string clientName)
{
_logger.LogInformation($"API请求: 启动客户端 {clientName}");
var success = _webSocketManager.StartClient(clientName);
if (success)
{
_logger.LogInformation($"客户端 {clientName} 启动成功");
return Ok(new { message = $"客户端 '{clientName}' 已启动" });
}
else
{
_logger.LogWarning($"客户端 {clientName} 启动失败");
return BadRequest($"启动客户端 '{clientName}' 失败");
}
}
/// <summary>
/// 停止客户端
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>操作结果</returns>
[HttpPost("clients/{clientName}/stop")]
public ActionResult StopClient(string clientName)
{
var success = _webSocketManager.StopClient(clientName);
if (success)
return Ok(new { message = $"客户端 '{clientName}' 已停止" });
else
return BadRequest($"停止客户端 '{clientName}' 失败");
}
/// <summary>
/// 播放/暂停客户端
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>操作结果</returns>
[HttpPost("clients/{clientName}/playpause")]
public ActionResult PlayPauseClient(string clientName)
{
var success = _webSocketManager.PlayPauseClient(clientName);
if (success)
return Ok(new { message = $"客户端 '{clientName}' 播放/暂停状态已切换" });
else
return BadRequest($"切换客户端 '{clientName}' 播放/暂停状态失败");
}
/// <summary>
/// 重置客户端日志
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>操作结果</returns>
[HttpPost("clients/{clientName}/reset-logs")]
public ActionResult ResetClientLogs(string clientName)
{
var success = _webSocketManager.ResetClientLogs(clientName);
if (success)
return Ok(new { message = $"客户端 '{clientName}' 日志已重置" });
else
return BadRequest($"重置客户端 '{clientName}' 日志失败");
}
/// <summary>
/// 获取客户端日志
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <param name="limit">日志数量限制</param>
/// <returns>日志列表</returns>
[HttpGet("clients/{clientName}/logs")]
public ActionResult<List<LTELog>?> GetClientLogs(string clientName, [FromQuery] int limit = 100)
{
var logs = _webSocketManager.GetClientLogs(clientName);
if (logs == null)
return NotFound($"客户端 '{clientName}' 不存在或未连接");
// 限制返回的日志数量
var limitedLogs = logs.TakeLast(limit).ToList();
return Ok(limitedLogs);
}
/// <summary>
/// 设置客户端日志配置
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <param name="request">日志配置请求</param>
/// <returns>操作结果</returns>
[HttpPost("clients/{clientName}/logs-config")]
public ActionResult SetClientLogsConfig(string clientName, [FromBody] LogsConfigRequest request)
{
var success = _webSocketManager.SetClientLogsConfig(clientName, request.Config, request.Save);
if (success)
return Ok(new { message = $"客户端 '{clientName}' 日志配置已更新" });
else
return BadRequest($"更新客户端 '{clientName}' 日志配置失败");
}
/// <summary>
/// 发送消息到客户端
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <param name="message">消息内容</param>
/// <returns>操作结果</returns>
[HttpPost("clients/{clientName}/send-message")]
public ActionResult SendMessage(string clientName, [FromBody] JObject message)
{
var messageId = _webSocketManager.SendMessageToClient(clientName, message);
if (messageId >= 0)
return Ok(new { messageId, message = $"消息已发送到客户端 '{clientName}'" });
else
return BadRequest($"发送消息到客户端 '{clientName}' 失败");
}
/// <summary>
/// 获取连接统计信息
/// </summary>
/// <returns>统计信息</returns>
[HttpGet("statistics")]
public ActionResult<ConnectionStatistics> GetStatistics()
{
var stats = _webSocketManager.GetConnectionStatistics();
return Ok(stats);
}
/// <summary>
/// 启动所有已配置的客户端
/// </summary>
/// <returns>操作结果</returns>
[HttpPost("start-all")]
public ActionResult StartAllClients()
{
_webSocketManager.StartAllConfiguredClients();
return Ok(new { message = "所有已配置的客户端已启动" });
}
/// <summary>
/// 停止所有客户端
/// </summary>
/// <returns>操作结果</returns>
[HttpPost("stop-all")]
public ActionResult StopAllClients()
{
_webSocketManager.StopAllClients();
return Ok(new { message = "所有客户端已停止" });
}
/// <summary>
/// 移除客户端配置
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>操作结果</returns>
[HttpDelete("configs/{clientName}")]
public ActionResult RemoveClientConfig(string clientName)
{
var success = _webSocketManager.RemoveClientConfig(clientName);
if (success)
return Ok(new { message = $"客户端 '{clientName}' 配置已移除" });
else
return BadRequest($"移除客户端 '{clientName}' 配置失败");
}
}
/// <summary>
/// 日志配置请求
/// </summary>
public class LogsConfigRequest
{
/// <summary>
/// 日志配置
/// </summary>
public Dictionary<string, object> Config { get; set; } = new();
/// <summary>
/// 是否保存配置
/// </summary>
public bool Save { get; set; } = false;
}
}

14
LTEMvcApp/LTEMvcApp.csproj

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="WebSocket4Net" Version="0.15.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>

8
LTEMvcApp/Models/ErrorViewModel.cs

@ -0,0 +1,8 @@
namespace LTEMvcApp.Models;
public class ErrorViewModel
{
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
}

422
LTEMvcApp/Models/LTEClient.cs

@ -0,0 +1,422 @@
using System.Collections.Immutable;
using System.Text.RegularExpressions;
namespace LTEMvcApp.Models;
/// <summary>
/// LTE客户端类 - 对应JavaScript中的lte.client对象
/// </summary>
public class LTEClient
{
#region 基础属性
/// <summary>
/// 客户端ID
/// </summary>
public int ClientId { get; set; }
/// <summary>
/// 客户端名称
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 客户端配置
/// </summary>
public ClientConfig Config { get; set; } = new();
/// <summary>
/// 客户端状态
/// </summary>
public ClientState State { get; set; } = ClientState.Stop;
/// <summary>
/// 版本信息
/// </summary>
public string? Version { get; set; }
/// <summary>
/// 许可证信息
/// </summary>
public string? License { get; set; }
/// <summary>
/// 模型类型
/// </summary>
public string? Model { get; set; }
#endregion
#region 日志相关属性
/// <summary>
/// 日志列表
/// </summary>
public List<LTELog> Logs { get; set; } = new();
/// <summary>
/// 日志数量
/// </summary>
public int LogCount => Logs.Count;
/// <summary>
/// 是否已过滤
/// </summary>
public bool Filtered { get; set; }
/// <summary>
/// 重置标志
/// </summary>
public bool ResetFlag { get; set; }
#endregion
#region 解析器状态
/// <summary>
/// 最后HARQ信息
/// </summary>
public Dictionary<int, Dictionary<int, LTELog>> LastHarq { get; set; } = new();
/// <summary>
/// 最后NB HARQ信息
/// </summary>
public Dictionary<int, Dictionary<int, List<LTELog>>> LastNbHarq { get; set; } = new();
/// <summary>
/// 帧信息
/// </summary>
public FrameInfo Frame { get; set; } = new();
/// <summary>
/// TMSI到UE ID的映射
/// </summary>
public Dictionary<int, int> TmsiToUeId { get; set; } = new();
/// <summary>
/// RNTI到UE ID的映射
/// </summary>
public Dictionary<int, int> RntiToUeId { get; set; } = new();
/// <summary>
/// UE列表
/// </summary>
public Dictionary<int, UEInfo> UeList { get; set; } = new();
/// <summary>
/// 最后时间戳
/// </summary>
public long LastTimestamp { get; set; }
/// <summary>
/// 时间戳偏移
/// </summary>
public long TimestampOffset { get; set; }
/// <summary>
/// 最后小区
/// </summary>
public int? LastCell { get; set; }
#endregion
#region 功能标志
/// <summary>
/// 是否有小区信息
/// </summary>
public bool HasCell { get; set; }
/// <summary>
/// 是否有物理层信息
/// </summary>
public bool HasPhy { get; set; }
/// <summary>
/// 是否有数据
/// </summary>
public bool HasData { get; set; }
/// <summary>
/// 是否有RNTI
/// </summary>
public bool HasRnti { get; set; }
/// <summary>
/// 是否有资源块
/// </summary>
public bool HasRb { get; set; }
/// <summary>
/// 是否有信号记录
/// </summary>
public bool HasSignalRecord { get; set; }
#endregion
#region 参数和组件
/// <summary>
/// 参数信息
/// </summary>
public Dictionary<string, object> Parameters { get; set; } = new();
/// <summary>
/// 组件列表
/// </summary>
public Dictionary<string, object> Components { get; set; } = new();
#endregion
#region 构造函数
public LTEClient(ClientConfig config)
{
Config = config;
Name = config.Name;
ClientId = GenerateClientId();
ResetParserState();
}
/// <summary>
/// 使用名称创建客户端
/// </summary>
/// <param name="name">客户端名称</param>
public LTEClient(string name)
{
Config = new ClientConfig { Name = name, Enabled = true };
Name = name;
ClientId = GenerateClientId();
ResetParserState();
}
#endregion
#region 公共方法
/// <summary>
/// 启动客户端
/// </summary>
public void Start()
{
SetState(ClientState.Start);
}
/// <summary>
/// 停止客户端
/// </summary>
public void Stop()
{
SetState(ClientState.Stop);
}
/// <summary>
/// 销毁客户端
/// </summary>
public void Destroy()
{
SetState(ClientState.Destroy);
}
/// <summary>
/// 重置日志
/// </summary>
public void ResetLogs()
{
if (LogCount > 0)
{
ResetFlag = true;
Logs.Clear();
ResetParserState();
HasCell = false;
HasPhy = false;
HasData = false;
HasRnti = false;
HasRb = false;
HasSignalRecord = false;
}
}
/// <summary>
/// 添加日志
/// </summary>
public void AddLog(LTELog log)
{
// 检查时间戳回绕
var timestamp = log.Timestamp + TimestampOffset;
if (timestamp < LastTimestamp - 100)
{
Console.WriteLine($"Log wrap by {LastTimestamp - timestamp}");
timestamp += 86400000; // 24小时
TimestampOffset += 86400000;
}
LastTimestamp = log.Timestamp = timestamp;
log.Client = this;
log.Id = GenerateLogId();
}
/// <summary>
/// 设置头信息
/// </summary>
public void SetHeaders(string[] headers)
{
// 实现头信息设置逻辑
}
/// <summary>
/// 初始化模型猜测
/// </summary>
public void LogModelGuessInit()
{
// 实现模型猜测初始化逻辑
}
/// <summary>
/// 模型猜测
/// </summary>
public void LogModelGuess(List<Dictionary<string, object>> logs)
{
// 实现模型猜测逻辑
}
/// <summary>
/// 方向转换
/// </summary>
public int DirConvert(LTELog log)
{
// 实现方向转换逻辑
return 0;
}
/// <summary>
/// 字符串转ID
/// </summary>
public int StringToId(string str)
{
// 实现字符串转ID逻辑
return 0;
}
#endregion
#region 私有方法
/// <summary>
/// 设置状态
/// </summary>
private void SetState(ClientState state)
{
if (State != state)
{
State = state;
switch (state)
{
case ClientState.Stop:
ResetLogs();
break;
case ClientState.Start:
case ClientState.Connected:
break;
case ClientState.Destroy:
return;
}
}
}
/// <summary>
/// 重置解析器状态
/// </summary>
private void ResetParserState()
{
LastHarq.Clear();
LastNbHarq.Clear();
Frame = new FrameInfo();
TmsiToUeId.Clear();
RntiToUeId.Clear();
UeList.Clear();
LastTimestamp = 0;
TimestampOffset = 0;
LastCell = null;
}
/// <summary>
/// 生成客户端ID
/// </summary>
private static int GenerateClientId()
{
return Interlocked.Increment(ref _clientIdCounter);
}
/// <summary>
/// 生成日志ID
/// </summary>
private static int GenerateLogId()
{
return Interlocked.Increment(ref _logIdCounter);
}
#endregion
#region 静态字段
private static int _clientIdCounter = 0;
private static int _logIdCounter = 0;
#endregion
}
/// <summary>
/// 客户端配置
/// </summary>
public class ClientConfig
{
public string Name { get; set; } = string.Empty;
public bool Enabled { get; set; } = true;
public string? Model { get; set; }
public string? Address { get; set; }
public bool Ssl { get; set; }
public int ReconnectDelay { get; set; } = 15000;
public bool Pause { get; set; }
public bool Readonly { get; set; }
public bool SkipLogMenu { get; set; }
public bool Locked { get; set; }
public bool Active { get; set; }
public string? Password { get; set; }
public Dictionary<string, object> Logs { get; set; } = new();
}
/// <summary>
/// 客户端状态
/// </summary>
public enum ClientState
{
Stop,
Start,
Loading,
Connecting,
Connected,
Error,
Destroy
}
/// <summary>
/// 帧信息
/// </summary>
public class FrameInfo
{
public int Last { get; set; }
public int Hfn { get; set; }
public long Timestamp { get; set; } = -1;
}
/// <summary>
/// UE信息
/// </summary>
public class UEInfo
{
public int UeId { get; set; }
public string? Imsi { get; set; }
public string? Imei { get; set; }
public object? Caps { get; set; }
}

204
LTEMvcApp/Models/LTELog.cs

@ -0,0 +1,204 @@
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
namespace LTEMvcApp.Models;
/// <summary>
/// LTE日志实体类 - 对应JavaScript中的LTELog对象
/// </summary>
public class LTELog
{
#region 基础属性
/// <summary>
/// 日志唯一标识符
/// </summary>
public int Id { get; set; }
/// <summary>
/// 时间戳(毫秒)
/// </summary>
public long Timestamp { get; set; }
/// <summary>
/// 协议层(PHY, RRC, NAS, MAC等)
/// </summary>
public string Layer { get; set; } = string.Empty;
/// <summary>
/// 传输方向(UL/DL)
/// </summary>
public int Direction { get; set; }
/// <summary>
/// 消息内容
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// 消息信息类型
/// </summary>
public int? Info { get; set; }
/// <summary>
/// UE标识符
/// </summary>
public int? UeId { get; set; }
/// <summary>
/// RNTI(无线网络临时标识符)
/// </summary>
public int? Rnti { get; set; }
/// <summary>
/// 日志数据内容
/// </summary>
public List<string> Data { get; set; } = new();
/// <summary>
/// 客户端引用
/// </summary>
[JsonIgnore]
public LTEClient? Client { get; set; }
/// <summary>
/// 是否已解码
/// </summary>
public bool Decoded { get; set; }
/// <summary>
/// 标记信息
/// </summary>
public string? Marker { get; set; }
#endregion
#region PHY层相关属性
/// <summary>
/// 小区标识
/// </summary>
public int? Cell { get; set; }
/// <summary>
/// 物理信道类型
/// </summary>
public string? Channel { get; set; }
/// <summary>
/// 帧号
/// </summary>
public int? Frame { get; set; }
/// <summary>
/// 子帧号
/// </summary>
public int? Subframe { get; set; }
/// <summary>
/// 时隙号
/// </summary>
public int? Slot { get; set; }
/// <summary>
/// 符号号
/// </summary>
public int? Symbol { get; set; }
/// <summary>
/// 天线端口
/// </summary>
public int? AntennaPort { get; set; }
/// <summary>
/// 资源块起始位置
/// </summary>
public int? RbStart { get; set; }
/// <summary>
/// 资源块数量
/// </summary>
public int? RbCount { get; set; }
/// <summary>
/// 调制编码方案
/// </summary>
public int? Mcs { get; set; }
/// <summary>
/// 传输块大小
/// </summary>
public int? Tbs { get; set; }
/// <summary>
/// HARQ进程ID
/// </summary>
public int? HarqId { get; set; }
/// <summary>
/// HARQ新数据指示
/// </summary>
public bool? HarqNdi { get; set; }
/// <summary>
/// HARQ重传次数
/// </summary>
public int? HarqRedundancyVersion { get; set; }
#endregion
#region 数据相关属性
/// <summary>
/// IP长度
/// </summary>
public int? IpLen { get; set; }
/// <summary>
/// SDU长度
/// </summary>
public int? SduLen { get; set; }
/// <summary>
/// 链路ID
/// </summary>
public LinkIds? LinkIds { get; set; }
/// <summary>
/// 信号记录
/// </summary>
public Dictionary<string, object>? SignalRecord { get; set; }
#endregion
#region 扩展方法
/// <summary>
/// 获取数据字符串
/// </summary>
/// <returns>数据字符串</returns>
public string GetDataString()
{
return string.Join("\n", Data);
}
/// <summary>
/// 获取数据数组
/// </summary>
/// <returns>数据数组</returns>
public List<string> GetData()
{
return Data;
}
#endregion
}
/// <summary>
/// 链路ID
/// </summary>
public class LinkIds
{
public int? Core { get; set; }
public int? Ran { get; set; }
}

38
LTEMvcApp/Program.cs

@ -0,0 +1,38 @@
using LTEMvcApp.Services;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
// 注册WebSocket管理服务
builder.Services.AddSingleton<WebSocketManagerService>(sp =>
new WebSocketManagerService(
sp.GetRequiredService<ILogger<WebSocketManagerService>>(),
sp.GetRequiredService<LogParserService>(),
sp
));
builder.Services.AddSingleton<LogParserService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();

38
LTEMvcApp/Properties/launchSettings.json

@ -0,0 +1,38 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:59049",
"sslPort": 44395
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5185",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7017;http://localhost:5185",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

52
LTEMvcApp/README.md

@ -0,0 +1,52 @@
# LTEMvcApp - ASP.NET Core MVC 项目
这是一个基于 .NET 8 的 ASP.NET Core MVC 应用程序。
## 项目结构
```
LTEMvcApp/
├── Controllers/ # 控制器
├── Models/ # 模型
├── Views/ # 视图
├── wwwroot/ # 静态文件
├── Program.cs # 应用程序入口点
└── appsettings.json # 配置文件
```
## 技术栈
- **.NET 8.0** - 最新的 .NET 框架
- **ASP.NET Core MVC** - Web 应用程序框架
- **Razor Pages** - 视图引擎
## 运行项目
1. 确保已安装 .NET 8 SDK
2. 在项目根目录执行:
```bash
dotnet restore
dotnet build
dotnet run
```
3. 打开浏览器访问 `https://localhost:5001``http://localhost:5000`
## 开发
- 项目使用热重载,修改代码后会自动重新编译
- 支持 HTTPS 开发证书
- 包含基本的错误处理和日志记录
## 功能
- 主页 (Home/Index)
- 隐私页面 (Home/Privacy)
- 错误处理页面 (Home/Error)
## 下一步
可以根据需要添加:
- 数据库连接
- 用户认证
- API 控制器
- 自定义模型和视图

964
LTEMvcApp/Services/LTEClientWebSocket.cs

@ -0,0 +1,964 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using WebSocket4Net;
using LTEMvcApp.Models;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
namespace LTEMvcApp.Services
{
/// <summary>
/// LTE客户端WebSocket实现 - 对应JavaScript中的lte.client.server
/// </summary>
public class LTEClientWebSocket : IDisposable
{
#region 私有字段
private WebSocket? _webSocket;
private readonly LTEClient _client;
private readonly ClientConfig _config;
private readonly ConcurrentDictionary<int, MessageHandler> _messageHandlers;
private readonly ConcurrentDictionary<string, MessageHandler> _messageHandlersByName;
private readonly ConcurrentQueue<JObject> _messageFifo;
private readonly CancellationTokenSource _cancellationTokenSource;
private Timer? _reconnectTimer;
private Timer? _statsTimer;
private Timer? _messageDeferTimer;
private Timer? _readyTimer;
private int _messageId;
private int _logGetId;
private bool _disposed;
private LogParserService logParser = new LogParserService();
private readonly ILogger<LTEClientWebSocket> _logger;
#endregion
#region 事件
/// <summary>
/// 连接打开事件
/// </summary>
public event EventHandler? ConnectionOpened;
/// <summary>
/// 连接关闭事件
/// </summary>
public event EventHandler? ConnectionClosed;
/// <summary>
/// 连接错误事件
/// </summary>
public event EventHandler<string>? ConnectionError;
/// <summary>
/// 消息接收事件
/// </summary>
public event EventHandler<JObject>? MessageReceived;
/// <summary>
/// 日志接收事件
/// </summary>
public event EventHandler<List<LTELog>>? LogsReceived;
/// <summary>
/// 统计信息接收事件
/// </summary>
public event EventHandler<JObject>? StatsReceived;
/// <summary>
/// 状态改变事件
/// </summary>
public event EventHandler<ClientState>? StateChanged;
/// <summary>
/// 客户端网格刷新事件
/// </summary>
public event EventHandler? ClientGridRefreshed;
#endregion
#region 属性
/// <summary>
/// 客户端
/// </summary>
public LTEClient Client => _client;
/// <summary>
/// 配置
/// </summary>
public ClientConfig Config => _config;
/// <summary>
/// 是否已连接
/// </summary>
public bool IsConnected => _webSocket?.State == WebSocketState.Open;
/// <summary>
/// 当前状态
/// </summary>
public ClientState State => _client.State;
/// <summary>
/// 是否暂停
/// </summary>
public bool IsPaused => _config.Pause;
/// <summary>
/// 是否只读
/// </summary>
public bool IsReadonly => _config.Readonly;
#endregion
#region 构造函数
/// <summary>
/// 构造函数
/// </summary>
/// <param name="config">客户端配置</param>
/// <param name="logger">ILogger实例</param>
public LTEClientWebSocket(ClientConfig config, ILogger<LTEClientWebSocket> logger)
{
_config = config;
_client = new LTEClient(config);
_messageHandlers = new ConcurrentDictionary<int, MessageHandler>();
_messageHandlersByName = new ConcurrentDictionary<string, MessageHandler>();
_messageFifo = new ConcurrentQueue<JObject>();
_cancellationTokenSource = new CancellationTokenSource();
_messageId = 0;
_logger = logger;
_logger.LogInformation($"创建WebSocket客户端: {config.Name}");
}
#endregion
#region 公共方法
/// <summary>
/// 启动WebSocket连接
/// </summary>
public void Start()
{
if (_disposed) return;
try
{
_logger.LogInformation($"[{_config.Name}] 尝试连接: {_config.Address}");
SetState(ClientState.Connecting);
// 构建WebSocket URL
var address = _config.Address ?? "192.168.13.12:9001";
var url = (_config.Ssl ? "wss://" : "ws://") + address;
_webSocket = new WebSocket(url);
_webSocket.EnableAutoSendPing = false;
// 绑定事件处理器
_webSocket.Opened += OnSocketOpened;
_webSocket.Closed += OnSocketClosed;
_webSocket.MessageReceived += OnSocketMessage0;
_webSocket.Error += OnSocketError;
_webSocket.Open();
}
catch (Exception ex)
{
_logger.LogError(ex, $"[{_config.Name}] 连接异常: {ex.Message}");
ConnectionError?.Invoke(this, $"无法连接到 {_config.Address}: {ex.Message}");
SetState(ClientState.Error);
}
}
/// <summary>
/// 停止WebSocket连接
/// </summary>
public void Stop()
{
SetState(ClientState.Stop);
StopTimers();
}
/// <summary>
/// 重置日志
/// </summary>
public void ResetLogs()
{
if (State == ClientState.Connected)
{
SendMessage(new JObject { ["message"] = "log_reset" }, response =>
{
_client.ResetLogs();
});
}
else
{
_client.ResetLogs();
}
}
/// <summary>
/// 切换播放/暂停状态
/// </summary>
public void PlayPause()
{
if (_config.Pause)
{
_config.Pause = false;
if (State == ClientState.Connected)
{
LogGet();
}
}
else
{
_config.Pause = true;
}
}
/// <summary>
/// 设置日志配置
/// </summary>
/// <param name="config">日志配置</param>
/// <param name="save">是否保存</param>
public void SetLogsConfig(Dictionary<string, object> config, bool save = false)
{
// 重新格式化配置
var logs = new JObject();
foreach (var kvp in config)
{
switch (kvp.Key)
{
case "layers":
var layers = new JObject();
var layersDict = kvp.Value as Dictionary<string, object>;
if (layersDict != null)
{
foreach (var layer in layersDict)
{
var layerConfig = layer.Value as Dictionary<string, object>;
if (layerConfig != null)
{
layers[layer.Key] = new JObject
{
["level"] = layerConfig.GetValueOrDefault("level", "warn").ToString(),
["max_size"] = Convert.ToInt32(layerConfig.GetValueOrDefault("max_size", 0)),
["payload"] = Convert.ToBoolean(layerConfig.GetValueOrDefault("payload", false))
};
}
}
}
logs["layers"] = layers;
break;
default:
if (IsLogParameter(kvp.Key))
{
logs[kvp.Key] = JToken.FromObject(kvp.Value);
}
break;
}
}
SendMessage(new JObject
{
["message"] = "config_set",
["logs"] = logs
}, response =>
{
if (save)
{
// 保存配置
foreach (var kvp in config)
{
if (_config.Logs.ContainsKey(kvp.Key))
{
_config.Logs[kvp.Key] = kvp.Value;
}
else
{
_config.Logs.Add(kvp.Key, kvp.Value);
}
}
}
LogGet(new Dictionary<string, object> { ["timeout"] = 0 });
}, true);
}
/// <summary>
/// 设置消息处理器
/// </summary>
/// <param name="names">消息名称</param>
/// <param name="handler">处理器</param>
public void SetMessageHandler(string[] names, MessageHandler handler)
{
SendMessage(new JObject
{
["message"] = "register",
["register"] = string.Join(",", names)
});
foreach (var name in names)
{
_messageHandlersByName[name] = handler;
}
}
/// <summary>
/// 取消消息处理器
/// </summary>
/// <param name="names">消息名称</param>
public void UnsetMessageHandler(string[] names)
{
SendMessage(new JObject
{
["message"] = "register",
["unregister"] = string.Join(",", names)
});
foreach (var name in names)
{
_messageHandlersByName.TryRemove(name, out _);
}
}
/// <summary>
/// 发送消息
/// </summary>
/// <param name="message">消息</param>
/// <param name="callback">回调</param>
/// <param name="errorHandler">错误处理器</param>
/// <returns>消息ID</returns>
public int SendMessage(JObject message, Action<JObject>? callback = null, bool errorHandler = false)
{
if (_webSocket?.State != WebSocketState.Open) return -1;
if (message == null) return -1;
var id = ++_messageId;
message["message_id"] = id;
if (callback != null)
{
_messageHandlers[id] = new MessageHandler
{
Callback = callback,
ErrorHandler = errorHandler
};
}
if (_messageDeferTimer != null)
{
_messageDeferTimer.Dispose();
_messageDeferTimer = null;
}
_messageFifo.Enqueue(message);
if (_messageFifo.Count < 100) // 批处理大小
{
_messageDeferTimer = new Timer(_ => SendMessageNow(), null, 1, Timeout.Infinite);
}
else
{
SendMessageNow();
}
return id;
}
/// <summary>
/// 获取日志
/// </summary>
/// <param name="parameters">参数</param>
public void LogGet(Dictionary<string, object>? parameters = null)
{
var config = _config;
if (config.Pause)
return;
var layers = new JObject();
if (config.Logs.ContainsKey("layers"))
{
// 处理layers配置,确保键是字符串类型
if (config.Logs["layers"] is JObject layersJObject)
{
foreach (var layer in layersJObject)
{
var layerName = layer.Key; // 这里layer.Key已经是字符串
var layerConfig = layer.Value as JObject;
if (layerConfig != null && layerConfig.ContainsKey("filter"))
{
layers[layerName] = layerConfig["filter"].ToString();
}
}
}
else if (config.Logs["layers"] is Dictionary<string, object> layersDict)
{
foreach (var layer in layersDict)
{
var layerConfig = layer.Value as Dictionary<string, object>;
if (layerConfig != null && layerConfig.ContainsKey("filter"))
{
layers[layer.Key] = layerConfig["filter"].ToString();
}
}
}
}
var message = new JObject
{
["timeout"] = 1,
["min"] = 64,
["max"] = 2048,
["layers"] = layers,
["message"] = "log_get",
["headers"] = _client.LogCount == 0
};
if (parameters != null)
{
foreach (var param in parameters)
{
message[param.Key] = JToken.FromObject(param.Value);
}
}
_logGetId = SendMessage(message, LogGetParse);
}
#endregion
#region 私有方法
/// <summary>
/// WebSocket连接打开事件
/// </summary>
private void OnSocketOpened(object? sender, EventArgs e)
{
_logger.LogInformation($"[{_config.Name}] WebSocket连接已打开");
StopTimers();
_readyTimer = new Timer(_ => OnSocketReady(), null, 2500, Timeout.Infinite);
ConnectionOpened?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// WebSocket连接关闭事件
/// </summary>
private void OnSocketClosed(object? sender, EventArgs e)
{
_logger.LogWarning($"[{_config.Name}] WebSocket连接已关闭");
ConnectionClosed?.Invoke(this, EventArgs.Empty);
StopTimers();
CloseComponents();
if (State == ClientState.Connected)
{
// 处理监控窗口停止
}
if (_config.Enabled)
{
Console.WriteLine("启动重连定时器");
if (State != ClientState.Stop)
{
SetState(ClientState.Error);
}
_reconnectTimer = new Timer(_ => Start(), null, _config.ReconnectDelay, Timeout.Infinite);
}
}
/// <summary>
/// WebSocket错误事件
/// </summary>
private void OnSocketError(object? sender, SuperSocket.ClientEngine.ErrorEventArgs e)
{
_logger.LogError(e.Exception, $"[{_config.Name}] WebSocket错误: {e.Exception.Message}");
SetState(ClientState.Error);
ConnectionError?.Invoke(this, e.Exception.Message);
}
/// <summary>
/// 初始消息处理(对应_onSocketMessage0)
/// </summary>
private void OnSocketMessage0(object? sender, MessageReceivedEventArgs e)
{
_logger.LogDebug($"[{_config.Name}] 收到初始消息: {e.Message}");
StopTimers();
try
{
var data = e.Message;
var msg = JObject.Parse(data);
switch (msg["message"]?.ToString())
{
case "authenticate":
if (msg["ready"]?.Value<bool>() == true)
{
OnSocketReady();
}
else if (msg["error"] == null && !string.IsNullOrEmpty(_config.Password))
{
// 重新认证
Authenticate(_config.Password, msg["challenge"]?.ToString() ?? "");
}
else
{
// 提示输入密码
PromptPassword(msg);
}
break;
case "ready":
OnSocketReady();
break;
default:
break;
}
}
catch (Exception ex)
{
ConnectionError?.Invoke(this, $"JSON解析错误: {ex.Message}");
}
}
/// <summary>
/// WebSocket准备就绪(对应_onSocketReady)
/// </summary>
private void OnSocketReady()
{
if (_webSocket == null) return;
// 切换到正常的消息处理函数
_webSocket.MessageReceived -= OnSocketMessage0;
_webSocket.MessageReceived += OnSocketMessage;
_messageFifo.Clear();
// 检查当前配置
var firstCon = !_config.Logs.ContainsKey("layers");
// 获取配置
SendMessage(new JObject { ["message"] = "config_get" }, config =>
{
Console.WriteLine("配置已接收");
_client.ResetLogs();
// 设置基本信息
_client.Version = config["version"]?.ToString();
_client.Name = config["name"]?.ToString() ?? _config.Name;
_client.Model = config["type"]?.ToString();
if (config["profiling"]?.Value<bool>() == true)
{
// 设置性能分析可用标志
}
var ro = _config.Readonly;
if (ro || firstCon)
{
_config.Logs = config["logs"]?.ToObject<Dictionary<string, object>>() ?? new Dictionary<string, object>();
}
else
{
var serverLogs = config["logs"]?.ToObject<Dictionary<string, object>>() ?? new Dictionary<string, object>();
foreach (var kvp in serverLogs)
{
if (!_config.Logs.ContainsKey(kvp.Key))
{
_config.Logs[kvp.Key] = kvp.Value;
}
}
}
// 清理和设置层配置
if (_config.Logs.ContainsKey("layers"))
{
var layers = _config.Logs["layers"] as Dictionary<string, object>;
var configLayers = config["logs"]?["layers"]?.ToObject<Dictionary<string, object>>();
if (layers != null && configLayers != null)
{
var keysToRemove = layers.Keys.Where(k => !configLayers.ContainsKey(k)).ToList();
foreach (var key in keysToRemove)
{
layers.Remove(key);
}
foreach (var layer in layers)
{
var layerConfig = layer.Value as Dictionary<string, object>;
if (layerConfig != null && !layerConfig.ContainsKey("filter"))
{
if (ro)
{
layerConfig["filter"] = layerConfig.GetValueOrDefault("level", "warn").ToString();
}
else
{
layerConfig["filter"] = GetDefaultFilter(layer.Key);
}
}
}
}
}
// 添加小区信息
if (config["cells"] != null)
{
var cells = config["cells"]?.ToObject<Dictionary<string, object>>();
if (cells != null)
{
foreach (var cell in cells)
{
// 添加小区配置
}
}
}
SetState(ClientState.Connected);
if (firstCon && !_config.SkipLogMenu)
{
// 显示配置窗口
Console.WriteLine("请配置日志");
}
else if (ro)
{
LogGet(new Dictionary<string, object> { ["timeout"] = 0 });
}
else
{
SetLogsConfig(_config.Logs);
}
});
}
/// <summary>
/// 正常消息处理(对应_onSocketMessage)
/// </summary>
private void OnSocketMessage(object? sender, MessageReceivedEventArgs e)
{
_logger.LogDebug($"[{_config.Name}] 收到消息: {e.Message}");
try
{
var data = e.Message;
JObject msg;
if (data.StartsWith("["))
{
// 处理二进制数据
var array = JArray.Parse(data);
msg = array[0] as JObject ?? new JObject();
// 处理二进制数据部分
}
else
{
msg = JObject.Parse(data);
}
MessageReceived?.Invoke(this, msg);
// 检查消息处理器
var id = msg["message_id"]?.Value<int>();
if (id.HasValue && _messageHandlers.TryGetValue(id.Value, out var handler))
{
if (msg["notification"]?.Value<bool>() != true)
{
_messageHandlers.TryRemove(id.Value, out _);
}
if (msg["error"] != null)
{
if (!handler.ErrorHandler)
{
ConnectionError?.Invoke(this, msg["error"]?.ToString() ?? "未知错误");
}
else
{
handler.Callback?.Invoke(msg);
}
return;
}
handler.Callback?.Invoke(msg);
return;
}
// 检查按名称的消息处理器
var name = msg["message"]?.ToString();
if (!string.IsNullOrEmpty(name) && _messageHandlersByName.TryGetValue(name, out var nameHandler))
{
nameHandler.Callback?.Invoke(msg);
return;
}
// 处理特定消息类型
switch (name)
{
case "log_get":
LogGetParse(msg);
break;
case "stats":
StatsReceived?.Invoke(this, msg);
break;
default:
Console.WriteLine($"未知消息: {name}");
break;
}
}
catch (Exception ex)
{
ConnectionError?.Invoke(this, $"消息处理错误: {ex.Message}");
}
}
/// <summary>
/// 日志获取解析
/// </summary>
private void LogGetParse(JObject msg)
{
_logger.LogInformation($"[{_config.Name}] 解析日志消息: {msg}");
var count = _client.LogCount;
if (msg["headers"] != null)
{
var headers = msg["headers"]?.ToObject<string[]>();
if (headers != null)
{
_client.SetHeaders(headers);
}
}
// 初始化模型猜测
_client.LogModelGuessInit();
var logs = msg["logs"];
if (logs != null)
{
Console.WriteLine($"接收到日志: {logs.Count()} 条");
if (logs is JArray logsArray)
{
// 模型猜测
_client.LogModelGuess(logsArray.ToObject<List<Dictionary<string, object>>>() ?? new List<Dictionary<string, object>>());
var logList = new List<LTELog>();
foreach (var logItem in logsArray)
{
if (logItem is JObject logObj)
{
//var logData = logObj.ToObject<Dictionary<string, object>>() ?? new Dictionary<string, object>();
// 创建LTELog对象
var log = JsonConvert.DeserializeObject<LTELog>(logObj.ToString()); //new LTELog(logData);
// 处理消息和数据
if (log.Data is List<string> dataList && dataList.Count > 0)
{
log.Message = dataList[0];
dataList.RemoveAt(0);
}
// 设置方向
log.Direction = _client.DirConvert(log);
// 处理信息字段
if (log.Info != null)
{
log.Info = _client.StringToId(log.Info.ToString());
}
// 处理PHY层的信号记录
if (log.Layer == "PHY" && log.Data is List<string> data)
{
var signalRecord = new Dictionary<string, object>();
for (int j = data.Count - 1; j >= 0; j--)
{
var line = data[j];
var match = Regex.Match(line, @"Link:\s([\w\d]+)@(\d+)");
if (match.Success)
{
var linkName = match.Groups[1].Value;
var offset = uint.Parse(match.Groups[2].Value);
signalRecord[linkName] = new { offset = offset };
data.RemoveAt(j);
}
}
if (signalRecord.Count > 0)
{
//log.SignalRecord = signalRecord;
_client.HasSignalRecord = true;
}
}
log.Client = _client;
logList.Add(log);
}
}
logParser.ParseLogList(_client, logList, true);
//_client.ParseLogList(logList, true);
LogsReceived?.Invoke(this, logList);
}
}
if (count == 0 && _client.LogCount > 0)
{
// 刷新客户端网格
ClientGridRefreshed?.Invoke(this, EventArgs.Empty);
}
// 更新日志获取 - 只有在最后一个请求时才更新
if (msg["message_id"]?.Value<int>() == _logGetId)
{
LogGet();
}
}
/// <summary>
/// 发送消息
/// </summary>
private void SendMessageNow()
{
if (_webSocket?.State != WebSocketState.Open) return;
var messages = new List<JObject>();
while (_messageFifo.TryDequeue(out var message))
{
messages.Add(message);
}
if (messages.Count == 1)
{
var json = JsonConvert.SerializeObject(messages[0]);
_webSocket.Send(json);
}
else
{
var json = JsonConvert.SerializeObject(messages);
_webSocket.Send(json);
}
_messageDeferTimer?.Dispose();
_messageDeferTimer = null;
}
/// <summary>
/// 停止定时器
/// </summary>
private void StopTimers()
{
_reconnectTimer?.Dispose();
_reconnectTimer = null;
_statsTimer?.Dispose();
_statsTimer = null;
_messageDeferTimer?.Dispose();
_messageDeferTimer = null;
_readyTimer?.Dispose();
_readyTimer = null;
}
/// <summary>
/// 关闭组件
/// </summary>
private void CloseComponents()
{
// 关闭所有组件
}
/// <summary>
/// 设置状态
/// </summary>
private void SetState(ClientState state)
{
if (_client.State != state)
{
_client.State = state;
StateChanged?.Invoke(this, state);
}
}
/// <summary>
/// 获取默认过滤器
/// </summary>
private string GetDefaultFilter(string layer)
{
return layer switch
{
"NAS" or "RRC" => "debug",
"EVENT" or "ALARM" or "MON" or "PROD" => "info",
_ => "warn"
};
}
/// <summary>
/// 检查是否为日志参数
/// </summary>
private bool IsLogParameter(string parameter)
{
var logParams = new[] { "signal", "cch", "bcch", "mib", "rep", "csi", "dci_size", "cell_meas" };
return logParams.Contains(parameter);
}
/// <summary>
/// 认证
/// </summary>
private void Authenticate(string password, string challenge)
{
// 实现认证逻辑
var authMessage = new JObject
{
["message"] = "authenticate",
["password"] = password
};
SendMessage(authMessage);
}
/// <summary>
/// 提示输入密码
/// </summary>
private void PromptPassword(JObject msg)
{
// 实现密码提示逻辑
Console.WriteLine("需要认证,请输入密码");
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
Stop();
StopTimers();
_cancellationTokenSource.Cancel();
_cancellationTokenSource.Dispose();
_webSocket?.Dispose();
_logger.LogInformation($"[{_config.Name}] 释放WebSocket客户端");
}
#endregion
}
/// <summary>
/// 消息处理器
/// </summary>
public class MessageHandler
{
public Action<JObject>? Callback { get; set; }
public bool ErrorHandler { get; set; }
}
}

638
LTEMvcApp/Services/LogParserService.cs

@ -0,0 +1,638 @@
using LTEMvcApp.Models;
using Newtonsoft.Json;
using System.Text.RegularExpressions;
namespace LTEMvcApp.Services;
/// <summary>
/// LTE日志解析服务 - 实现JavaScript _logListParse方法的核心功能
/// </summary>
public class LogParserService
{
#region 常量定义
/// <summary>
/// HFN回绕阈值
/// </summary>
private const int HFN_WRAP_THRESHOLD = 512;
/// <summary>
/// 最大日志数量
/// </summary>
private const int LOGS_MAX = 2000000;
#endregion
#region 正则表达式
/// <summary>
/// PHY层日志正则表达式
/// </summary>
private static readonly Regex RegExpPhy = new(@"^([a-f0-9\-]+)\s+([a-f0-9\-]+)\s+([\d\.\-]+) (\w+): (.+)", RegexOptions.Compiled);
/// <summary>
/// 信息1正则表达式
/// </summary>
private static readonly Regex RegExpInfo1 = new(@"^([\w\-]+): (.+)", RegexOptions.Compiled);
/// <summary>
/// 信息2正则表达式
/// </summary>
private static readonly Regex RegExpInfo2 = new(@"^([\w]+) (.+)", RegexOptions.Compiled);
/// <summary>
/// IP日志正则表达式
/// </summary>
private static readonly Regex RegExpIP = new(@"^(len=\d+)\s+(\S+)\s+(.*)", RegexOptions.Compiled);
/// <summary>
/// IPsec日志正则表达式
/// </summary>
private static readonly Regex RegExpIPsec = new(@"^len=(\d+)\s+(.*)", RegexOptions.Compiled);
/// <summary>
/// SDU长度正则表达式
/// </summary>
private static readonly Regex RegExpSDULen = new(@"SDU_len=(\d+)", RegexOptions.Compiled);
/// <summary>
/// SIP日志正则表达式
/// </summary>
private static readonly Regex RegExpSIP = new(@"^([:\.\[\]\da-f]+)\s+(\S+) (.+)", RegexOptions.Compiled);
/// <summary>
/// 媒体请求正则表达式
/// </summary>
private static readonly Regex RegExpMediaReq = new(@"^(\S+) (.+)", RegexOptions.Compiled);
/// <summary>
/// 信号记录正则表达式
/// </summary>
private static readonly Regex RegExpSignalRecord = new(@"Link:\s([\w\d]+)@(\d+)", RegexOptions.Compiled);
/// <summary>
/// 小区ID正则表达式
/// </summary>
private static readonly Regex RegExpCellID = new(@"^([a-f0-9\-]+) (.+)", RegexOptions.Compiled);
/// <summary>
/// RRC UE ID正则表达式
/// </summary>
private static readonly Regex RegExpRRC_UE_ID = new(@"Changing UE_ID to 0x(\d+)", RegexOptions.Compiled);
/// <summary>
/// RRC TMSI正则表达式
/// </summary>
private static readonly Regex RegExpRRC_TMSI = new(@"(5G|m)-TMSI '([\dA-F]+)'H", RegexOptions.Compiled);
/// <summary>
/// RRC新ID正则表达式
/// </summary>
private static readonly Regex RegExpRRC_NEW_ID = new(@"newUE-Identity (['\dA-FH]+)", RegexOptions.Compiled);
/// <summary>
/// RRC CRNTI正则表达式
/// </summary>
private static readonly Regex RegExpRRC_CRNTI = new(@"c-RNTI '([\dA-F]+)'H", RegexOptions.Compiled);
/// <summary>
/// NAS TMSI正则表达式
/// </summary>
private static readonly Regex RegExpNAS_TMSI = new(@"m-TMSI = 0x([\da-f]+)", RegexOptions.Compiled);
/// <summary>
/// NAS 5G TMSI正则表达式
/// </summary>
private static readonly Regex RegExpNAS_5GTMSI = new(@"5G-TMSI = 0x([\da-f]+)", RegexOptions.Compiled);
/// <summary>
/// RRC频段组合正则表达式
/// </summary>
private static readonly Regex RegExpRRC_BC = new(@"(EUTRA|MRDC|NR|NRDC) band combinations", RegexOptions.Compiled);
/// <summary>
/// PDCCH正则表达式
/// </summary>
private static readonly Regex RegExpPDCCH = new(@"^\s*(.+)=(\d+)$", RegexOptions.Compiled);
/// <summary>
/// S1/NGAP正则表达式
/// </summary>
private static readonly Regex RegExpS1NGAP = new(@"^([\da-f\-]+)\s+([\da-f\-]+) (([^\s]+) .+)", RegexOptions.Compiled);
/// <summary>
/// 十六进制转储正则表达式
/// </summary>
private static readonly Regex RegExpHexDump = new(@"^[\da-f]+:(\s+[\da-f]{2}){1,16}\s+.{1,16}$", RegexOptions.Compiled);
#endregion
#region 私有字段
/// <summary>
/// 字符串到ID的映射缓存
/// </summary>
private readonly Dictionary<string, int> _stringToIdCache = new();
/// <summary>
/// ID到字符串的映射缓存
/// </summary>
private readonly Dictionary<int, string> _idToStringCache = new();
/// <summary>
/// 字符串ID计数器
/// </summary>
private int _stringIdCounter = 0;
#endregion
#region 公共方法
/// <summary>
/// 解析日志列表 - 对应JavaScript的_logListParse方法
/// </summary>
/// <param name="client">LTE客户端</param>
/// <param name="logs">日志列表</param>
/// <param name="isWebSocket">是否为WebSocket消息</param>
public void ParseLogList(LTEClient client, List<LTELog> logs, bool isWebSocket = false)
{
var length = logs.Count;
for (int i = 0; i < length; i++)
{
var log = logs[i];
log.Message = log.Message + "";
client.AddLog(log);
// 解析消息
switch (log.Layer)
{
case "PHY":
if (!ParsePhyLog(client, log, log.Message, isWebSocket)) continue;
ProcessPhyLog(client, log);
break;
case "RRC":
ParseCellId(client, log, isWebSocket);
var rrcUeIdMatch = RegExpRRC_UE_ID.Match(log.Message);
if (rrcUeIdMatch.Success)
{
SetSameUe(client, log, int.Parse(rrcUeIdMatch.Groups[1].Value, System.Globalization.NumberStyles.HexNumber));
continue;
}
var infoMatch = RegExpInfo1.Match(log.Message);
if (infoMatch.Success)
{
if (!SetLogInfo(client, log, infoMatch.Groups[1].Value)) continue;
log.Message = infoMatch.Groups[2].Value;
ProcessRrcLog(client, log);
}
var bcMatch = RegExpRRC_BC.Match(log.Message);
if (bcMatch.Success)
{
try
{
var data = log.GetDataString();
var jsonData = JsonConvert.DeserializeObject<object>(data);
// 处理频段组合信息
}
catch
{
// 忽略JSON解析错误
}
}
break;
case "NAS":
ParseCellId(client, log, isWebSocket);
var nasTmsiMatch = RegExpNAS_TMSI.Match(log.Message);
if (nasTmsiMatch.Success)
{
SetTmsi(client, log, nasTmsiMatch.Groups[1].Value);
continue;
}
var nas5gTmsiMatch = RegExpNAS_5GTMSI.Match(log.Message);
if (nas5gTmsiMatch.Success)
{
SetTmsi(client, log, nas5gTmsiMatch.Groups[1].Value);
continue;
}
var nasInfoMatch = RegExpInfo2.Match(log.Message);
if (nasInfoMatch.Success)
{
if (!SetLogInfo(client, log, nasInfoMatch.Groups[1].Value)) continue;
log.Message = nasInfoMatch.Groups[2].Value;
ProcessNasLog(client, log);
}
break;
case "MAC":
ParseCellId(client, log, isWebSocket);
ParseMacLog(client, log);
break;
case "IP":
var ipMatch = RegExpIP.Match(log.Message);
if (ipMatch.Success)
{
var lenPart = ipMatch.Groups[1].Value;
log.IpLen = int.Parse(lenPart.Split('=')[1]);
if (!SetLogInfo(client, log, ipMatch.Groups[2].Value)) continue;
log.Message = ipMatch.Groups[3].Value;
client.HasData = true;
}
break;
case "GTPU":
var sduLenMatch = RegExpSDULen.Match(log.Message);
if (sduLenMatch.Success)
{
log.SduLen = int.Parse(sduLenMatch.Groups[1].Value);
client.HasData = true;
}
break;
case "S1AP":
case "NGAP":
var s1ngMatch = RegExpS1NGAP.Match(log.Message);
if (s1ngMatch.Success)
{
log.Message = s1ngMatch.Groups[3].Value;
var coreId = int.TryParse(s1ngMatch.Groups[1].Value, System.Globalization.NumberStyles.HexNumber, null, out var core) ? core : (int?)null;
var ranId = int.TryParse(s1ngMatch.Groups[2].Value, System.Globalization.NumberStyles.HexNumber, null, out var ran) ? ran : (int?)null;
log.LinkIds = new LinkIds { Core = coreId, Ran = ranId };
}
break;
case "SIP":
var sipMatch = RegExpSIP.Match(log.Message);
if (sipMatch.Success)
{
if (!SetLogInfo(client, log, sipMatch.Groups[2].Value)) continue;
log.Message = sipMatch.Groups[3].Value;
}
break;
case "MEDIA":
var mediaMatch = RegExpMediaReq.Match(log.Message);
if (mediaMatch.Success)
{
if (!SetLogInfo(client, log, mediaMatch.Groups[1].Value)) continue;
log.Message = mediaMatch.Groups[2].Value;
}
break;
case "IPsec":
var ipsecMatch = RegExpIPsec.Match(log.Message);
if (ipsecMatch.Success)
{
log.IpLen = int.Parse(ipsecMatch.Groups[1].Value);
log.Message = ipsecMatch.Groups[2].Value;
}
break;
default:
break;
}
// 处理提示信息
ProcessHints(client, log);
}
// 限制日志数量
if (client.LogCount > LOGS_MAX)
{
var excess = client.LogCount - LOGS_MAX;
client.Logs.RemoveRange(0, excess);
}
}
#endregion
#region 私有方法
/// <summary>
/// 解析PHY日志
/// </summary>
private bool ParsePhyLog(LTEClient client, LTELog log, string message, bool isWebSocket)
{
if (!isWebSocket)
{
var phyMatch = RegExpPhy.Match(message);
if (!phyMatch.Success)
{
Console.WriteLine($"Bad PHY log: {log}");
return false;
}
log.Cell = int.Parse(phyMatch.Groups[1].Value, System.Globalization.NumberStyles.HexNumber);
log.Rnti = int.Parse(phyMatch.Groups[2].Value, System.Globalization.NumberStyles.HexNumber);
log.Channel = phyMatch.Groups[4].Value;
log.Message = phyMatch.Groups[5].Value;
}
client.HasPhy = true;
client.HasCell = true;
client.HasRnti = true;
// 解析PHY参数
var lines = log.GetData();
foreach (var line in lines)
{
var parts = line.Split('=');
if (parts.Length == 2)
{
ParsePhyParameter(client, log, parts[0].Trim(), parts[1].Trim());
}
}
return true;
}
/// <summary>
/// 解析PHY参数
/// </summary>
private void ParsePhyParameter(LTEClient client, LTELog log, string param, string value)
{
switch (param.ToLower())
{
case "frame":
if (int.TryParse(value, out var frame))
{
log.Frame = frame;
client.HasPhy = true;
}
break;
case "subframe":
if (int.TryParse(value, out var subframe))
{
log.Subframe = subframe;
}
break;
case "slot":
if (int.TryParse(value, out var slot))
{
log.Slot = slot;
}
break;
case "symbol":
if (int.TryParse(value, out var symbol))
{
log.Symbol = symbol;
}
break;
case "ant":
if (int.TryParse(value, out var ant))
{
log.AntennaPort = ant;
}
break;
case "rb_start":
if (int.TryParse(value, out var rbStart))
{
log.RbStart = rbStart;
client.HasRb = true;
}
break;
case "rb_count":
if (int.TryParse(value, out var rbCount))
{
log.RbCount = rbCount;
client.HasRb = true;
}
break;
case "mcs":
if (int.TryParse(value, out var mcs))
{
log.Mcs = mcs;
}
break;
case "tbs":
if (int.TryParse(value, out var tbs))
{
log.Tbs = tbs;
}
break;
case "harq_id":
if (int.TryParse(value, out var harqId))
{
log.HarqId = harqId;
}
break;
case "harq_ndi":
if (bool.TryParse(value, out var harqNdi))
{
log.HarqNdi = harqNdi;
}
break;
case "harq_rv":
if (int.TryParse(value, out var harqRv))
{
log.HarqRedundancyVersion = harqRv;
}
break;
}
}
/// <summary>
/// 处理PHY日志
/// </summary>
private void ProcessPhyLog(LTEClient client, LTELog log)
{
// 处理HARQ信息
if (log.HarqId.HasValue)
{
ProcessHarqLog(client, log, log.HarqId.Value);
}
// 处理帧信息
if (log.Frame.HasValue)
{
var frame = log.Frame.Value;
var hfn = frame >> 10;
var sfn = frame & 0x3FF;
if (client.Frame.Timestamp == -1)
{
client.Frame.Timestamp = log.Timestamp;
client.Frame.Hfn = hfn;
client.Frame.Last = sfn;
}
else
{
var diff = sfn - client.Frame.Last;
if (diff < -HFN_WRAP_THRESHOLD)
{
client.Frame.Hfn++;
}
else if (diff > HFN_WRAP_THRESHOLD)
{
client.Frame.Hfn--;
}
client.Frame.Last = sfn;
}
}
}
/// <summary>
/// 处理HARQ日志
/// </summary>
private void ProcessHarqLog(LTEClient client, LTELog log, int harq)
{
if (log.Cell.HasValue && log.UeId.HasValue)
{
var cell = log.Cell.Value;
var ueId = log.UeId.Value;
var key = ueId * 32 + harq;
if (!client.LastHarq.ContainsKey(cell))
{
client.LastHarq[cell] = new Dictionary<int, LTELog>();
}
client.LastHarq[cell][key] = log;
}
}
/// <summary>
/// 解析小区ID
/// </summary>
private void ParseCellId(LTEClient client, LTELog log, bool isWebSocket)
{
if (!isWebSocket)
{
var cellMatch = RegExpCellID.Match(log.Message);
if (cellMatch.Success)
{
log.Cell = int.Parse(cellMatch.Groups[1].Value, System.Globalization.NumberStyles.HexNumber);
log.Message = cellMatch.Groups[2].Value;
client.HasCell = true;
}
}
}
/// <summary>
/// 设置日志信息
/// </summary>
private bool SetLogInfo(LTEClient client, LTELog log, string info)
{
if (string.IsNullOrEmpty(info) || info == "?")
return false;
log.Info = StringToId(info);
return true;
}
/// <summary>
/// 字符串转ID
/// </summary>
private int StringToId(string str)
{
if (_stringToIdCache.TryGetValue(str, out var id))
{
return id;
}
id = ++_stringIdCounter;
_stringToIdCache[str] = id;
_idToStringCache[id] = str;
return id;
}
/// <summary>
/// ID转字符串
/// </summary>
private string IdToString(int id)
{
return _idToStringCache.TryGetValue(id, out var str) ? str : "";
}
/// <summary>
/// 设置相同UE
/// </summary>
private void SetSameUe(LTEClient client, LTELog log, int ueId)
{
log.UeId = ueId;
if (!client.UeList.ContainsKey(ueId))
{
client.UeList[ueId] = new UEInfo { UeId = ueId };
}
}
/// <summary>
/// 处理RRC日志
/// </summary>
private void ProcessRrcLog(LTEClient client, LTELog log)
{
// 处理RRC TMSI
var tmsiMatch = RegExpRRC_TMSI.Match(log.Message);
if (tmsiMatch.Success)
{
SetTmsi(client, log, tmsiMatch.Groups[2].Value);
}
// 处理RRC CRNTI
var crntiMatch = RegExpRRC_CRNTI.Match(log.Message);
if (crntiMatch.Success)
{
SetRnti(client, log, crntiMatch.Groups[1].Value);
}
}
/// <summary>
/// 处理NAS日志
/// </summary>
private void ProcessNasLog(LTEClient client, LTELog log)
{
// NAS日志处理逻辑
}
/// <summary>
/// 解析MAC日志
/// </summary>
private void ParseMacLog(LTEClient client, LTELog log)
{
// MAC日志解析逻辑
}
/// <summary>
/// 处理提示信息
/// </summary>
private void ProcessHints(LTEClient client, LTELog log, Dictionary<string, object>? config = null)
{
// 处理提示信息逻辑
}
/// <summary>
/// 设置TMSI
/// </summary>
private void SetTmsi(LTEClient client, LTELog log, string tmsi)
{
var tmsiId = int.Parse(tmsi, System.Globalization.NumberStyles.HexNumber);
if (log.UeId.HasValue)
{
client.TmsiToUeId[tmsiId] = log.UeId.Value;
}
}
/// <summary>
/// 设置RNTI
/// </summary>
private void SetRnti(LTEClient client, LTELog log, string rnti)
{
var rntiId = int.Parse(rnti, System.Globalization.NumberStyles.HexNumber);
if (log.UeId.HasValue)
{
client.RntiToUeId[rntiId] = log.UeId.Value;
}
}
#endregion
}

400
LTEMvcApp/Services/WebSocketManagerService.cs

@ -0,0 +1,400 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using LTEMvcApp.Models;
using Newtonsoft.Json.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
namespace LTEMvcApp.Services
{
/// <summary>
/// WebSocket管理器服务 - 管理多个LTE客户端连接
/// </summary>
public class WebSocketManagerService
{
#region 私有字段
private readonly ConcurrentDictionary<string, LTEClientWebSocket> _clients;
private readonly ConcurrentDictionary<string, ClientConfig> _configs;
private readonly LogParserService _logParser;
private readonly ILogger<WebSocketManagerService> _logger;
private readonly IServiceProvider _serviceProvider;
#endregion
#region 事件
/// <summary>
/// 客户端连接事件
/// </summary>
public event EventHandler<LTEClientWebSocket>? ClientConnected;
/// <summary>
/// 客户端断开事件
/// </summary>
public event EventHandler<LTEClientWebSocket>? ClientDisconnected;
/// <summary>
/// 日志接收事件
/// </summary>
public event EventHandler<(string clientName, List<LTELog> logs)>? LogsReceived;
/// <summary>
/// 状态变化事件
/// </summary>
public event EventHandler<(string clientName, ClientState state)>? StateChanged;
#endregion
#region 构造函数
/// <summary>
/// 构造函数
/// </summary>
public WebSocketManagerService(ILogger<WebSocketManagerService> logger, LogParserService logParser, IServiceProvider serviceProvider)
{
_clients = new ConcurrentDictionary<string, LTEClientWebSocket>();
_configs = new ConcurrentDictionary<string, ClientConfig>();
_logParser = logParser;
_logger = logger;
_serviceProvider = serviceProvider;
_logger.LogInformation("WebSocketManagerService 初始化");
}
#endregion
#region 公共方法
/// <summary>
/// 添加客户端配置
/// </summary>
/// <param name="config">客户端配置</param>
/// <returns>是否成功添加</returns>
public bool AddClientConfig(ClientConfig config)
{
if (string.IsNullOrEmpty(config.Name))
{
_logger.LogWarning("尝试添加空名称客户端配置");
return false;
}
_logger.LogInformation($"添加客户端配置: {config.Name}");
_configs[config.Name] = config;
return true;
}
/// <summary>
/// 移除客户端配置
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>是否成功移除</returns>
public bool RemoveClientConfig(string clientName)
{
_logger.LogInformation($"移除客户端配置: {clientName}");
if (_configs.TryRemove(clientName, out _))
{
// 如果客户端正在运行,停止它
if (_clients.TryGetValue(clientName, out var client))
{
client.Stop();
_clients.TryRemove(clientName, out _);
}
return true;
}
return false;
}
/// <summary>
/// 启动客户端
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>是否成功启动</returns>
public bool StartClient(string clientName)
{
_logger.LogInformation($"启动客户端: {clientName}");
if (!_configs.TryGetValue(clientName, out var config))
return false;
// 如果客户端已存在,先停止
if (_clients.TryGetValue(clientName, out var existingClient))
{
existingClient.Stop();
_clients.TryRemove(clientName, out _);
}
// 通过依赖注入获取ILogger<LTEClientWebSocket>
var logger = _serviceProvider.GetService(typeof(ILogger<LTEClientWebSocket>)) as ILogger<LTEClientWebSocket>;
var client = new LTEClientWebSocket(config, logger!);
// 订阅事件
client.ConnectionOpened += (sender, e) => OnClientConnected(client);
client.ConnectionClosed += (sender, e) => OnClientDisconnected(client);
client.LogsReceived += (sender, logs) => OnLogsReceived(clientName, logs);
client.StateChanged += (sender, state) => OnStateChanged(clientName, state);
// 启动客户端
client.Start();
// 添加到客户端列表
_clients[clientName] = client;
return true;
}
/// <summary>
/// 停止客户端
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>是否成功停止</returns>
public bool StopClient(string clientName)
{
_logger.LogInformation($"停止客户端: {clientName}");
if (_clients.TryGetValue(clientName, out var client))
{
client.Stop();
_clients.TryRemove(clientName, out _);
return true;
}
return false;
}
/// <summary>
/// 获取客户端状态
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>客户端状态</returns>
public ClientState? GetClientState(string clientName)
{
if (_clients.TryGetValue(clientName, out var client))
{
return client.State;
}
return null;
}
/// <summary>
/// 获取所有客户端状态
/// </summary>
/// <returns>客户端状态字典</returns>
public Dictionary<string, ClientState> GetAllClientStates()
{
return _clients.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.State);
}
/// <summary>
/// 获取客户端配置
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>客户端配置</returns>
public ClientConfig? GetClientConfig(string clientName)
{
_configs.TryGetValue(clientName, out var config);
return config;
}
/// <summary>
/// 获取所有客户端配置
/// </summary>
/// <returns>客户端配置列表</returns>
public List<ClientConfig> GetAllClientConfigs()
{
return _configs.Values.ToList();
}
/// <summary>
/// 获取客户端日志
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>客户端日志列表</returns>
public List<LTELog>? GetClientLogs(string clientName)
{
if (_clients.TryGetValue(clientName, out var client))
{
return client.Client.Logs;
}
return null;
}
/// <summary>
/// 重置客户端日志
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>是否成功重置</returns>
public bool ResetClientLogs(string clientName)
{
if (_clients.TryGetValue(clientName, out var client))
{
client.ResetLogs();
return true;
}
return false;
}
/// <summary>
/// 设置客户端日志配置
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <param name="logsConfig">日志配置</param>
/// <param name="save">是否保存</param>
/// <returns>是否成功设置</returns>
public bool SetClientLogsConfig(string clientName, Dictionary<string, object> logsConfig, bool save = false)
{
if (_clients.TryGetValue(clientName, out var client))
{
client.SetLogsConfig(logsConfig, save);
return true;
}
return false;
}
/// <summary>
/// 播放/暂停客户端
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <returns>是否成功操作</returns>
public bool PlayPauseClient(string clientName)
{
if (_clients.TryGetValue(clientName, out var client))
{
client.PlayPause();
return true;
}
return false;
}
/// <summary>
/// 发送消息到客户端
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <param name="message">消息</param>
/// <param name="callback">回调</param>
/// <returns>消息ID</returns>
public int SendMessageToClient(string clientName, JObject message, Action<JObject>? callback = null)
{
if (_clients.TryGetValue(clientName, out var client))
{
return client.SendMessage(message, callback);
}
return -1;
}
/// <summary>
/// 获取连接统计信息
/// </summary>
/// <returns>连接统计信息</returns>
public ConnectionStatistics GetConnectionStatistics()
{
var stats = new ConnectionStatistics
{
TotalClients = _clients.Count,
ConnectedClients = _clients.Values.Count(c => c.IsConnected),
DisconnectedClients = _clients.Values.Count(c => !c.IsConnected),
TotalLogs = _clients.Values.Sum(c => c.Client.LogCount),
ClientStates = GetAllClientStates()
};
return stats;
}
/// <summary>
/// 停止所有客户端
/// </summary>
public void StopAllClients()
{
foreach (var client in _clients.Values)
{
client.Stop();
}
_clients.Clear();
}
/// <summary>
/// 启动所有已配置的客户端
/// </summary>
public void StartAllConfiguredClients()
{
foreach (var config in _configs.Values)
{
if (config.Enabled)
{
StartClient(config.Name);
}
}
}
#endregion
#region 私有方法
/// <summary>
/// 客户端连接事件处理
/// </summary>
private void OnClientConnected(LTEClientWebSocket client)
{
_logger.LogInformation($"客户端已连接: {client.Client.Config.Name}");
ClientConnected?.Invoke(this, client);
}
/// <summary>
/// 客户端断开事件处理
/// </summary>
private void OnClientDisconnected(LTEClientWebSocket client)
{
_logger.LogWarning($"客户端已断开: {client.Client.Config.Name}");
ClientDisconnected?.Invoke(this, client);
}
/// <summary>
/// 日志接收事件处理
/// </summary>
private void OnLogsReceived(string clientName, List<LTELog> logs)
{
_logger.LogInformation($"客户端 {clientName} 收到日志: {logs.Count} 条");
LogsReceived?.Invoke(this, (clientName, logs));
}
/// <summary>
/// 状态变化事件处理
/// </summary>
private void OnStateChanged(string clientName, ClientState state)
{
_logger.LogInformation($"客户端 {clientName} 状态变更: {state}");
StateChanged?.Invoke(this, (clientName, state));
}
#endregion
}
/// <summary>
/// 连接统计信息
/// </summary>
public class ConnectionStatistics
{
/// <summary>
/// 总客户端数
/// </summary>
public int TotalClients { get; set; }
/// <summary>
/// 已连接客户端数
/// </summary>
public int ConnectedClients { get; set; }
/// <summary>
/// 未连接客户端数
/// </summary>
public int DisconnectedClients { get; set; }
/// <summary>
/// 总日志数
/// </summary>
public int TotalLogs { get; set; }
/// <summary>
/// 客户端状态字典
/// </summary>
public Dictionary<string, ClientState> ClientStates { get; set; } = new();
}
}

240
LTEMvcApp/Views/Home/Index.cshtml

@ -0,0 +1,240 @@
@{
ViewData["Title"] = "LTE WebSocket 客户端管理";
}
<div class="text-center">
<h1 class="display-4">LTE WebSocket 客户端管理</h1>
<p>基于 .NET 8 的 LTE WebSocket 客户端实现</p>
</div>
<!-- 消息提示 -->
@if (TempData["Message"] != null)
{
<div class="alert alert-success alert-dismissible fade show" role="alert">
@TempData["Message"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
@if (TempData["Error"] != null)
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
@TempData["Error"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
<!-- 连接统计信息 -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">连接统计信息</h5>
</div>
<div class="card-body">
@if (ViewBag.ConnectionStats != null)
{
var stats = ViewBag.ConnectionStats as LTEMvcApp.Services.ConnectionStatistics;
<div class="row">
<div class="col-md-3">
<div class="text-center">
<h4 class="text-primary">@(stats?.TotalClients ?? 0)</h4>
<p class="text-muted">总客户端数</p>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h4 class="text-success">@(stats?.ConnectedClients ?? 0)</h4>
<p class="text-muted">已连接</p>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h4 class="text-warning">@(stats?.DisconnectedClients ?? 0)</h4>
<p class="text-muted">未连接</p>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h4 class="text-info">@(stats?.TotalLogs ?? 0)</h4>
<p class="text-muted">总日志数</p>
</div>
</div>
</div>
}
else
{
<p class="text-muted">暂无统计信息</p>
}
</div>
</div>
</div>
</div>
<!-- 客户端管理 -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">客户端管理</h5>
</div>
<div class="card-body">
<!-- 快速操作按钮 -->
<div class="row mb-3">
<div class="col-md-12">
<form method="post" style="display: inline;">
<button type="submit" asp-action="AddTestClient" class="btn btn-primary me-2">
<i class="bi bi-plus-circle"></i> 添加测试客户端
</button>
</form>
<form method="post" style="display: inline;">
<button type="submit" asp-action="StartTestClient" class="btn btn-success me-2">
<i class="bi bi-play-circle"></i> 启动测试客户端
</button>
</form>
<form method="post" style="display: inline;">
<button type="submit" asp-action="StopTestClient" class="btn btn-danger me-2">
<i class="bi bi-stop-circle"></i> 停止测试客户端
</button>
</form>
<a href="@Url.Action("WebSocketTest")" class="btn btn-info">
<i class="bi bi-gear"></i> WebSocket 测试
</a>
</div>
</div>
<!-- 客户端配置列表 -->
@if (ViewBag.ClientConfigs != null && ((List<LTEMvcApp.Models.ClientConfig>)ViewBag.ClientConfigs).Count > 0)
{
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>客户端名称</th>
<th>地址</th>
<th>状态</th>
<th>SSL</th>
<th>启用</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@foreach (var config in ViewBag.ClientConfigs as List<LTEMvcApp.Models.ClientConfig>)
{
var stats = ViewBag.ConnectionStats as LTEMvcApp.Services.ConnectionStatistics;
var clientStates = stats?.ClientStates ?? new Dictionary<string, LTEMvcApp.Models.ClientState>();
var state = clientStates.GetValueOrDefault(config.Name, LTEMvcApp.Models.ClientState.Stop);
<tr>
<td>@config.Name</td>
<td>@(config.Ssl ? "wss://" : "ws://")@config.Address</td>
<td>
<span class="badge @(state == LTEMvcApp.Models.ClientState.Connected ? "bg-success" :
state == LTEMvcApp.Models.ClientState.Connecting ? "bg-warning" :
state == LTEMvcApp.Models.ClientState.Error ? "bg-danger" : "bg-secondary")">
@state
</span>
</td>
<td>
<i class="bi @(config.Ssl ? "bi-check-circle-fill text-success" : "bi-x-circle-fill text-muted")"></i>
</td>
<td>
<i class="bi @(config.Enabled ? "bi-check-circle-fill text-success" : "bi-x-circle-fill text-muted")"></i>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-primary" onclick="startClient('@config.Name')">
<i class="bi bi-play"></i>
</button>
<button type="button" class="btn btn-outline-danger" onclick="stopClient('@config.Name')">
<i class="bi bi-stop"></i>
</button>
<button type="button" class="btn btn-outline-info" onclick="viewLogs('@config.Name')">
<i class="bi bi-list-ul"></i>
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
else
{
<div class="text-center text-muted">
<i class="bi bi-info-circle" style="font-size: 2rem;"></i>
<p class="mt-2">暂无客户端配置</p>
<p>点击"添加测试客户端"按钮来创建第一个客户端配置</p>
</div>
}
</div>
</div>
</div>
</div>
<!-- API 信息 -->
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">API 接口</h5>
</div>
<div class="card-body">
<p>以下API接口可用于程序化控制WebSocket客户端:</p>
<ul class="list-unstyled">
<li><code>GET /api/websocket/clients</code> - 获取所有客户端状态</li>
<li><code>GET /api/websocket/configs</code> - 获取所有客户端配置</li>
<li><code>POST /api/websocket/configs</code> - 添加客户端配置</li>
<li><code>POST /api/websocket/clients/{name}/start</code> - 启动客户端</li>
<li><code>POST /api/websocket/clients/{name}/stop</code> - 停止客户端</li>
<li><code>GET /api/websocket/clients/{name}/logs</code> - 获取客户端日志</li>
<li><code>GET /api/websocket/statistics</code> - 获取连接统计信息</li>
</ul>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
function startClient(clientName) {
fetch(`/api/websocket/clients/${encodeURIComponent(clientName)}/start`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
alert(data.message || '操作成功');
location.reload();
})
.catch(error => {
console.error('Error:', error);
alert('操作失败');
});
}
function stopClient(clientName) {
fetch(`/api/websocket/clients/${encodeURIComponent(clientName)}/stop`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
alert(data.message || '操作成功');
location.reload();
})
.catch(error => {
console.error('Error:', error);
alert('操作失败');
});
}
function viewLogs(clientName) {
window.open(`/api/websocket/clients/${encodeURIComponent(clientName)}/logs?limit=100`, '_blank');
}
</script>
}

6
LTEMvcApp/Views/Home/Privacy.cshtml

@ -0,0 +1,6 @@
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>
<p>Use this page to detail your site's privacy policy.</p>

25
LTEMvcApp/Views/Shared/Error.cshtml

@ -0,0 +1,25 @@
@model ErrorViewModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>

49
LTEMvcApp/Views/Shared/_Layout.cshtml

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - LTEMvcApp</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/LTEMvcApp.styles.css" asp-append-version="true" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container-fluid">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">LTEMvcApp</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<footer class="border-top footer text-muted">
<div class="container">
&copy; 2025 - LTEMvcApp - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

48
LTEMvcApp/Views/Shared/_Layout.cshtml.css

@ -0,0 +1,48 @@
/* Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
for details on configuring this project to bundle and minify static web assets. */
a.navbar-brand {
white-space: normal;
text-align: center;
word-break: break-all;
}
a {
color: #0077cc;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.border-top {
border-top: 1px solid #e5e5e5;
}
.border-bottom {
border-bottom: 1px solid #e5e5e5;
}
.box-shadow {
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
}
button.accept-policy {
font-size: 1rem;
line-height: inherit;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
white-space: nowrap;
line-height: 60px;
}

2
LTEMvcApp/Views/Shared/_ValidationScriptsPartial.cshtml

@ -0,0 +1,2 @@
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>

3
LTEMvcApp/Views/_ViewImports.cshtml

@ -0,0 +1,3 @@
@using LTEMvcApp
@using LTEMvcApp.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

3
LTEMvcApp/Views/_ViewStart.cshtml

@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}

8
LTEMvcApp/appsettings.Development.json

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

9
LTEMvcApp/appsettings.json

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

22
LTEMvcApp/wwwroot/css/site.css

@ -0,0 +1,22 @@
html {
font-size: 14px;
}
@media (min-width: 768px) {
html {
font-size: 16px;
}
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
html {
position: relative;
min-height: 100%;
}
body {
margin-bottom: 60px;
}

BIN
LTEMvcApp/wwwroot/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

4
LTEMvcApp/wwwroot/js/site.js

@ -0,0 +1,4 @@
// Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
// for details on configuring this project to bundle and minify static web assets.
// Write your JavaScript code.

22
LTEMvcApp/wwwroot/lib/bootstrap/LICENSE

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2011-2021 Twitter, Inc.
Copyright (c) 2011-2021 The Bootstrap Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

4997
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css

File diff suppressed because it is too large

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map

File diff suppressed because one or more lines are too long

7
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css

File diff suppressed because one or more lines are too long

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map

File diff suppressed because one or more lines are too long

4996
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css

File diff suppressed because it is too large

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map

File diff suppressed because one or more lines are too long

7
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css

File diff suppressed because one or more lines are too long

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map

File diff suppressed because one or more lines are too long

427
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css

@ -0,0 +1,427 @@
/*!
* Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0a58ca;
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1em;
direction: ltr /* rtl:ignore */;
unicode-bidi: bidi-override;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map

File diff suppressed because one or more lines are too long

8
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css

@ -0,0 +1,8 @@
/*!
* Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map

File diff suppressed because one or more lines are too long

424
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css

@ -0,0 +1,424 @@
/*!
* Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0a58ca;
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1em;
direction: ltr ;
unicode-bidi: bidi-override;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map

File diff suppressed because one or more lines are too long

8
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css

@ -0,0 +1,8 @@
/*!
* Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map

File diff suppressed because one or more lines are too long

4866
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css

File diff suppressed because it is too large

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map

File diff suppressed because one or more lines are too long

7
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css

File diff suppressed because one or more lines are too long

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map

File diff suppressed because one or more lines are too long

4857
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css

File diff suppressed because it is too large

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map

File diff suppressed because one or more lines are too long

7
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css

File diff suppressed because one or more lines are too long

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map

File diff suppressed because one or more lines are too long

11221
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap.css

File diff suppressed because it is too large

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map

File diff suppressed because one or more lines are too long

7
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css

File diff suppressed because one or more lines are too long

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map

File diff suppressed because one or more lines are too long

11197
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css

File diff suppressed because it is too large

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map

File diff suppressed because one or more lines are too long

7
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css

File diff suppressed because one or more lines are too long

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map

File diff suppressed because one or more lines are too long

6780
LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js

File diff suppressed because it is too large

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map

File diff suppressed because one or more lines are too long

7
LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js

File diff suppressed because one or more lines are too long

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map

File diff suppressed because one or more lines are too long

4977
LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js

File diff suppressed because it is too large

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map

File diff suppressed because one or more lines are too long

7
LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js

File diff suppressed because one or more lines are too long

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map

File diff suppressed because one or more lines are too long

5026
LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.js

File diff suppressed because it is too large

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map

File diff suppressed because one or more lines are too long

7
LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js

File diff suppressed because one or more lines are too long

1
LTEMvcApp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map

File diff suppressed because one or more lines are too long

23
LTEMvcApp/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt

@ -0,0 +1,23 @@
The MIT License (MIT)
Copyright (c) .NET Foundation and Contributors
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

435
LTEMvcApp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js

@ -0,0 +1,435 @@
/**
* @license
* Unobtrusive validation support library for jQuery and jQuery Validate
* Copyright (c) .NET Foundation. All rights reserved.
* Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
* @version v4.0.0
*/
/*jslint white: true, browser: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true, strict: false */
/*global document: false, jQuery: false */
(function (factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define("jquery.validate.unobtrusive", ['jquery-validation'], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS-like environments that support module.exports
module.exports = factory(require('jquery-validation'));
} else {
// Browser global
jQuery.validator.unobtrusive = factory(jQuery);
}
}(function ($) {
var $jQval = $.validator,
adapters,
data_validation = "unobtrusiveValidation";
function setValidationValues(options, ruleName, value) {
options.rules[ruleName] = value;
if (options.message) {
options.messages[ruleName] = options.message;
}
}
function splitAndTrim(value) {
return value.replace(/^\s+|\s+$/g, "").split(/\s*,\s*/g);
}
function escapeAttributeValue(value) {
// As mentioned on http://api.jquery.com/category/selectors/
return value.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g, "\\$1");
}
function getModelPrefix(fieldName) {
return fieldName.substr(0, fieldName.lastIndexOf(".") + 1);
}
function appendModelPrefix(value, prefix) {
if (value.indexOf("*.") === 0) {
value = value.replace("*.", prefix);
}
return value;
}
function onError(error, inputElement) { // 'this' is the form element
var container = $(this).find("[data-valmsg-for='" + escapeAttributeValue(inputElement[0].name) + "']"),
replaceAttrValue = container.attr("data-valmsg-replace"),
replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) !== false : null;
container.removeClass("field-validation-valid").addClass("field-validation-error");
error.data("unobtrusiveContainer", container);
if (replace) {
container.empty();
error.removeClass("input-validation-error").appendTo(container);
}
else {
error.hide();
}
}
function onErrors(event, validator) { // 'this' is the form element
var container = $(this).find("[data-valmsg-summary=true]"),
list = container.find("ul");
if (list && list.length && validator.errorList.length) {
list.empty();
container.addClass("validation-summary-errors").removeClass("validation-summary-valid");
$.each(validator.errorList, function () {
$("<li />").html(this.message).appendTo(list);
});
}
}
function onSuccess(error) { // 'this' is the form element
var container = error.data("unobtrusiveContainer");
if (container) {
var replaceAttrValue = container.attr("data-valmsg-replace"),
replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) : null;
container.addClass("field-validation-valid").removeClass("field-validation-error");
error.removeData("unobtrusiveContainer");
if (replace) {
container.empty();
}
}
}
function onReset(event) { // 'this' is the form element
var $form = $(this),
key = '__jquery_unobtrusive_validation_form_reset';
if ($form.data(key)) {
return;
}
// Set a flag that indicates we're currently resetting the form.
$form.data(key, true);
try {
$form.data("validator").resetForm();
} finally {
$form.removeData(key);
}
$form.find(".validation-summary-errors")
.addClass("validation-summary-valid")
.removeClass("validation-summary-errors");
$form.find(".field-validation-error")
.addClass("field-validation-valid")
.removeClass("field-validation-error")
.removeData("unobtrusiveContainer")
.find(">*") // If we were using valmsg-replace, get the underlying error
.removeData("unobtrusiveContainer");
}
function validationInfo(form) {
var $form = $(form),
result = $form.data(data_validation),
onResetProxy = $.proxy(onReset, form),
defaultOptions = $jQval.unobtrusive.options || {},
execInContext = function (name, args) {
var func = defaultOptions[name];
func && $.isFunction(func) && func.apply(form, args);
};
if (!result) {
result = {
options: { // options structure passed to jQuery Validate's validate() method
errorClass: defaultOptions.errorClass || "input-validation-error",
errorElement: defaultOptions.errorElement || "span",
errorPlacement: function () {
onError.apply(form, arguments);
execInContext("errorPlacement", arguments);
},
invalidHandler: function () {
onErrors.apply(form, arguments);
execInContext("invalidHandler", arguments);
},
messages: {},
rules: {},
success: function () {
onSuccess.apply(form, arguments);
execInContext("success", arguments);
}
},
attachValidation: function () {
$form
.off("reset." + data_validation, onResetProxy)
.on("reset." + data_validation, onResetProxy)
.validate(this.options);
},
validate: function () { // a validation function that is called by unobtrusive Ajax
$form.validate();
return $form.valid();
}
};
$form.data(data_validation, result);
}
return result;
}
$jQval.unobtrusive = {
adapters: [],
parseElement: function (element, skipAttach) {
/// <summary>
/// Parses a single HTML element for unobtrusive validation attributes.
/// </summary>
/// <param name="element" domElement="true">The HTML element to be parsed.</param>
/// <param name="skipAttach" type="Boolean">[Optional] true to skip attaching the
/// validation to the form. If parsing just this single element, you should specify true.
/// If parsing several elements, you should specify false, and manually attach the validation
/// to the form when you are finished. The default is false.</param>
var $element = $(element),
form = $element.parents("form")[0],
valInfo, rules, messages;
if (!form) { // Cannot do client-side validation without a form
return;
}
valInfo = validationInfo(form);
valInfo.options.rules[element.name] = rules = {};
valInfo.options.messages[element.name] = messages = {};
$.each(this.adapters, function () {
var prefix = "data-val-" + this.name,
message = $element.attr(prefix),
paramValues = {};
if (message !== undefined) { // Compare against undefined, because an empty message is legal (and falsy)
prefix += "-";
$.each(this.params, function () {
paramValues[this] = $element.attr(prefix + this);
});
this.adapt({
element: element,
form: form,
message: message,
params: paramValues,
rules: rules,
messages: messages
});
}
});
$.extend(rules, { "__dummy__": true });
if (!skipAttach) {
valInfo.attachValidation();
}
},
parse: function (selector) {
/// <summary>
/// Parses all the HTML elements in the specified selector. It looks for input elements decorated
/// with the [data-val=true] attribute value and enables validation according to the data-val-*
/// attribute values.
/// </summary>
/// <param name="selector" type="String">Any valid jQuery selector.</param>
// $forms includes all forms in selector's DOM hierarchy (parent, children and self) that have at least one
// element with data-val=true
var $selector = $(selector),
$forms = $selector.parents()
.addBack()
.filter("form")
.add($selector.find("form"))
.has("[data-val=true]");
$selector.find("[data-val=true]").each(function () {
$jQval.unobtrusive.parseElement(this, true);
});
$forms.each(function () {
var info = validationInfo(this);
if (info) {
info.attachValidation();
}
});
}
};
adapters = $jQval.unobtrusive.adapters;
adapters.add = function (adapterName, params, fn) {
/// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation.</summary>
/// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used
/// in the data-val-nnnn HTML attribute (where nnnn is the adapter name).</param>
/// <param name="params" type="Array" optional="true">[Optional] An array of parameter names (strings) that will
/// be extracted from the data-val-nnnn-mmmm HTML attributes (where nnnn is the adapter name, and
/// mmmm is the parameter name).</param>
/// <param name="fn" type="Function">The function to call, which adapts the values from the HTML
/// attributes into jQuery Validate rules and/or messages.</param>
/// <returns type="jQuery.validator.unobtrusive.adapters" />
if (!fn) { // Called with no params, just a function
fn = params;
params = [];
}
this.push({ name: adapterName, params: params, adapt: fn });
return this;
};
adapters.addBool = function (adapterName, ruleName) {
/// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where
/// the jQuery Validate validation rule has no parameter values.</summary>
/// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used
/// in the data-val-nnnn HTML attribute (where nnnn is the adapter name).</param>
/// <param name="ruleName" type="String" optional="true">[Optional] The name of the jQuery Validate rule. If not provided, the value
/// of adapterName will be used instead.</param>
/// <returns type="jQuery.validator.unobtrusive.adapters" />
return this.add(adapterName, function (options) {
setValidationValues(options, ruleName || adapterName, true);
});
};
adapters.addMinMax = function (adapterName, minRuleName, maxRuleName, minMaxRuleName, minAttribute, maxAttribute) {
/// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where
/// the jQuery Validate validation has three potential rules (one for min-only, one for max-only, and
/// one for min-and-max). The HTML parameters are expected to be named -min and -max.</summary>
/// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used
/// in the data-val-nnnn HTML attribute (where nnnn is the adapter name).</param>
/// <param name="minRuleName" type="String">The name of the jQuery Validate rule to be used when you only
/// have a minimum value.</param>
/// <param name="maxRuleName" type="String">The name of the jQuery Validate rule to be used when you only
/// have a maximum value.</param>
/// <param name="minMaxRuleName" type="String">The name of the jQuery Validate rule to be used when you
/// have both a minimum and maximum value.</param>
/// <param name="minAttribute" type="String" optional="true">[Optional] The name of the HTML attribute that
/// contains the minimum value. The default is "min".</param>
/// <param name="maxAttribute" type="String" optional="true">[Optional] The name of the HTML attribute that
/// contains the maximum value. The default is "max".</param>
/// <returns type="jQuery.validator.unobtrusive.adapters" />
return this.add(adapterName, [minAttribute || "min", maxAttribute || "max"], function (options) {
var min = options.params.min,
max = options.params.max;
if (min && max) {
setValidationValues(options, minMaxRuleName, [min, max]);
}
else if (min) {
setValidationValues(options, minRuleName, min);
}
else if (max) {
setValidationValues(options, maxRuleName, max);
}
});
};
adapters.addSingleVal = function (adapterName, attribute, ruleName) {
/// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where
/// the jQuery Validate validation rule has a single value.</summary>
/// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used
/// in the data-val-nnnn HTML attribute(where nnnn is the adapter name).</param>
/// <param name="attribute" type="String">[Optional] The name of the HTML attribute that contains the value.
/// The default is "val".</param>
/// <param name="ruleName" type="String" optional="true">[Optional] The name of the jQuery Validate rule. If not provided, the value
/// of adapterName will be used instead.</param>
/// <returns type="jQuery.validator.unobtrusive.adapters" />
return this.add(adapterName, [attribute || "val"], function (options) {
setValidationValues(options, ruleName || adapterName, options.params[attribute]);
});
};
$jQval.addMethod("__dummy__", function (value, element, params) {
return true;
});
$jQval.addMethod("regex", function (value, element, params) {
var match;
if (this.optional(element)) {
return true;
}
match = new RegExp(params).exec(value);
return (match && (match.index === 0) && (match[0].length === value.length));
});
$jQval.addMethod("nonalphamin", function (value, element, nonalphamin) {
var match;
if (nonalphamin) {
match = value.match(/\W/g);
match = match && match.length >= nonalphamin;
}
return match;
});
if ($jQval.methods.extension) {
adapters.addSingleVal("accept", "mimtype");
adapters.addSingleVal("extension", "extension");
} else {
// for backward compatibility, when the 'extension' validation method does not exist, such as with versions
// of JQuery Validation plugin prior to 1.10, we should use the 'accept' method for
// validating the extension, and ignore mime-type validations as they are not supported.
adapters.addSingleVal("extension", "extension", "accept");
}
adapters.addSingleVal("regex", "pattern");
adapters.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url");
adapters.addMinMax("length", "minlength", "maxlength", "rangelength").addMinMax("range", "min", "max", "range");
adapters.addMinMax("minlength", "minlength").addMinMax("maxlength", "minlength", "maxlength");
adapters.add("equalto", ["other"], function (options) {
var prefix = getModelPrefix(options.element.name),
other = options.params.other,
fullOtherName = appendModelPrefix(other, prefix),
element = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(fullOtherName) + "']")[0];
setValidationValues(options, "equalTo", element);
});
adapters.add("required", function (options) {
// jQuery Validate equates "required" with "mandatory" for checkbox elements
if (options.element.tagName.toUpperCase() !== "INPUT" || options.element.type.toUpperCase() !== "CHECKBOX") {
setValidationValues(options, "required", true);
}
});
adapters.add("remote", ["url", "type", "additionalfields"], function (options) {
var value = {
url: options.params.url,
type: options.params.type || "GET",
data: {}
},
prefix = getModelPrefix(options.element.name);
$.each(splitAndTrim(options.params.additionalfields || options.element.name), function (i, fieldName) {
var paramName = appendModelPrefix(fieldName, prefix);
value.data[paramName] = function () {
var field = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(paramName) + "']");
// For checkboxes and radio buttons, only pick up values from checked fields.
if (field.is(":checkbox")) {
return field.filter(":checked").val() || field.filter(":hidden").val() || '';
}
else if (field.is(":radio")) {
return field.filter(":checked").val() || '';
}
return field.val();
};
});
setValidationValues(options, "remote", value);
});
adapters.add("password", ["min", "nonalphamin", "regex"], function (options) {
if (options.params.min) {
setValidationValues(options, "minlength", options.params.min);
}
if (options.params.nonalphamin) {
setValidationValues(options, "nonalphamin", options.params.nonalphamin);
}
if (options.params.regex) {
setValidationValues(options, "regex", options.params.regex);
}
});
adapters.add("fileextensions", ["extensions"], function (options) {
setValidationValues(options, "extension", options.params.extensions);
});
$(function () {
$jQval.unobtrusive.parse(document);
});
return $jQval.unobtrusive;
}));

8
LTEMvcApp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js

File diff suppressed because one or more lines are too long

22
LTEMvcApp/wwwroot/lib/jquery-validation/LICENSE.md

@ -0,0 +1,22 @@
The MIT License (MIT)
=====================
Copyright Jörn Zaefferer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

1512
LTEMvcApp/wwwroot/lib/jquery-validation/dist/additional-methods.js

File diff suppressed because it is too large

4
LTEMvcApp/wwwroot/lib/jquery-validation/dist/additional-methods.min.js

File diff suppressed because one or more lines are too long

1661
LTEMvcApp/wwwroot/lib/jquery-validation/dist/jquery.validate.js

File diff suppressed because it is too large

4
LTEMvcApp/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js

File diff suppressed because one or more lines are too long

21
LTEMvcApp/wwwroot/lib/jquery/LICENSE.txt

@ -0,0 +1,21 @@
Copyright OpenJS Foundation and other contributors, https://openjsf.org/
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

10881
LTEMvcApp/wwwroot/lib/jquery/dist/jquery.js

File diff suppressed because it is too large

2
LTEMvcApp/wwwroot/lib/jquery/dist/jquery.min.js

File diff suppressed because one or more lines are too long

1
LTEMvcApp/wwwroot/lib/jquery/dist/jquery.min.map

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save