From 506ab4857aa3eb40017e2e18c05f59d4d53f08df Mon Sep 17 00:00:00 2001
From: luoqiang <2769838458@qq.com>
Date: Thu, 23 Apr 2026 17:25:31 +0800
Subject: [PATCH] =?UTF-8?q?=E5=BC=95=E5=85=A5=E5=90=8E=E7=AB=AFAPI?=
=?UTF-8?q?=E9=A1=B9=E7=9B=AE=E5=B9=B6=E9=87=8D=E6=9E=84=E5=89=8D=E5=90=8E?=
=?UTF-8?q?=E7=AB=AF=E6=A1=A5=E6=8E=A5=E4=B8=8E=E8=B7=AF=E7=94=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
本次提交新增了 Avalonia-API(Web API)与 Avalonia-Services(业务服务)两个项目,完善了解决方案结构。重构了 Avalonia-PC 前端与后端的桥接逻辑,实现了基于前缀的路由分发、静态服务、开发者工具等功能。同步更新了 .gitignore、api.js 及相关配置文件,为后续业务扩展和维护打下基础。
---
.gitignore | 6 +-
Avalonia-API/Avalonia-API.csproj | 18 ++
Avalonia-API/Avalonia-API.csproj.user | 6 +
Avalonia-API/Avalonia-API.http | 6 +
.../Configuration/ServicesConfiguration.cs | 15 +
.../Controllers/WeatherForecastController.cs | 20 ++
Avalonia-API/Program.cs | 23 ++
Avalonia-API/Properties/launchSettings.json | 23 ++
Avalonia-API/appsettings.Development.json | 8 +
Avalonia-API/appsettings.json | 9 +
Avalonia-PC/Avalonia-PC.slnx | 2 +
Avalonia-PC/Views/MainWindow.axaml.cs | 297 ++++++++++++++++--
Avalonia-PC/www/api.js | 2 +-
Avalonia-Services/Avalonia-Services.csproj | 10 +
Avalonia-Services/Models/WeatherForecast.cs | 13 +
.../Services/WeatherForecastService.cs | 23 ++
16 files changed, 455 insertions(+), 26 deletions(-)
create mode 100644 Avalonia-API/Avalonia-API.csproj
create mode 100644 Avalonia-API/Avalonia-API.csproj.user
create mode 100644 Avalonia-API/Avalonia-API.http
create mode 100644 Avalonia-API/Configuration/ServicesConfiguration.cs
create mode 100644 Avalonia-API/Controllers/WeatherForecastController.cs
create mode 100644 Avalonia-API/Program.cs
create mode 100644 Avalonia-API/Properties/launchSettings.json
create mode 100644 Avalonia-API/appsettings.Development.json
create mode 100644 Avalonia-API/appsettings.json
create mode 100644 Avalonia-Services/Avalonia-Services.csproj
create mode 100644 Avalonia-Services/Models/WeatherForecast.cs
create mode 100644 Avalonia-Services/Services/WeatherForecastService.cs
diff --git a/.gitignore b/.gitignore
index 4059021..3eadd6c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,8 @@
/Avalonia-PC/bin
/Avalonia-PC/.vs
-/Avalonia-PC/obj
\ No newline at end of file
+/Avalonia-PC/obj
+/Avalonia-API/bin
+/Avalonia-API/obj
+/Avalonia-Services/bin
+/Avalonia-Services/obj
\ No newline at end of file
diff --git a/Avalonia-API/Avalonia-API.csproj b/Avalonia-API/Avalonia-API.csproj
new file mode 100644
index 0000000..3b60685
--- /dev/null
+++ b/Avalonia-API/Avalonia-API.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net10.0
+ enable
+ enable
+ Avalonia_API
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Avalonia-API/Avalonia-API.csproj.user b/Avalonia-API/Avalonia-API.csproj.user
new file mode 100644
index 0000000..9ff5820
--- /dev/null
+++ b/Avalonia-API/Avalonia-API.csproj.user
@@ -0,0 +1,6 @@
+
+
+
+ https
+
+
\ No newline at end of file
diff --git a/Avalonia-API/Avalonia-API.http b/Avalonia-API/Avalonia-API.http
new file mode 100644
index 0000000..3dd7f88
--- /dev/null
+++ b/Avalonia-API/Avalonia-API.http
@@ -0,0 +1,6 @@
+@Avalonia_API_HostAddress = http://localhost:5206
+
+GET {{Avalonia_API_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###
diff --git a/Avalonia-API/Configuration/ServicesConfiguration.cs b/Avalonia-API/Configuration/ServicesConfiguration.cs
new file mode 100644
index 0000000..823bc0f
--- /dev/null
+++ b/Avalonia-API/Configuration/ServicesConfiguration.cs
@@ -0,0 +1,15 @@
+using Avalonia_Services.Services;
+
+namespace Avalonia_API.Configuration
+{
+ public static class ServicesConfiguration
+ {
+ public static void ConfigureServices(this IServiceCollection services)
+ {
+ // Register your services here
+ // For example:
+ // services.AddSingleton();
+ services.AddScoped();
+ }
+ }
+}
diff --git a/Avalonia-API/Controllers/WeatherForecastController.cs b/Avalonia-API/Controllers/WeatherForecastController.cs
new file mode 100644
index 0000000..7b83edf
--- /dev/null
+++ b/Avalonia-API/Controllers/WeatherForecastController.cs
@@ -0,0 +1,20 @@
+using Avalonia_Services.Models;
+using Avalonia_Services.Services;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Avalonia_API.Controllers
+{
+ [ApiController]
+ [Route("[controller]")]
+ public class WeatherForecastController(WeatherForecastService weatherForecastService) : ControllerBase
+ {
+
+ private readonly WeatherForecastService _weatherForecastService = weatherForecastService;
+
+ [HttpGet(Name = "GetWeatherForecast")]
+ public IEnumerable Get()
+ {
+ return _weatherForecastService.GetWeatherForecasts();
+ }
+ }
+}
diff --git a/Avalonia-API/Program.cs b/Avalonia-API/Program.cs
new file mode 100644
index 0000000..666a9c5
--- /dev/null
+++ b/Avalonia-API/Program.cs
@@ -0,0 +1,23 @@
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+
+builder.Services.AddControllers();
+// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
+builder.Services.AddOpenApi();
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.MapOpenApi();
+}
+
+app.UseHttpsRedirection();
+
+app.UseAuthorization();
+
+app.MapControllers();
+
+app.Run();
diff --git a/Avalonia-API/Properties/launchSettings.json b/Avalonia-API/Properties/launchSettings.json
new file mode 100644
index 0000000..00c28bd
--- /dev/null
+++ b/Avalonia-API/Properties/launchSettings.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5206",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "https://localhost:7165;http://localhost:5206",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/Avalonia-API/appsettings.Development.json b/Avalonia-API/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/Avalonia-API/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/Avalonia-API/appsettings.json b/Avalonia-API/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/Avalonia-API/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/Avalonia-PC/Avalonia-PC.slnx b/Avalonia-PC/Avalonia-PC.slnx
index 532a743..9200293 100644
--- a/Avalonia-PC/Avalonia-PC.slnx
+++ b/Avalonia-PC/Avalonia-PC.slnx
@@ -1,3 +1,5 @@
+
+
diff --git a/Avalonia-PC/Views/MainWindow.axaml.cs b/Avalonia-PC/Views/MainWindow.axaml.cs
index 2964059..267b166 100644
--- a/Avalonia-PC/Views/MainWindow.axaml.cs
+++ b/Avalonia-PC/Views/MainWindow.axaml.cs
@@ -15,6 +15,7 @@ namespace Avalonia_PC.Views
public partial class MainWindow : Window
{
private const string AppScheme = "app";
+ //private const string? OnlineStartupUrl = "https://re.laitool.cn";
private const string? OnlineStartupUrl = null;
private const string? LocalStartupPath = null;
private static readonly JsonSerializerOptions BridgeJsonSerializerOptions = new()
@@ -30,6 +31,11 @@ namespace Avalonia_PC.Views
private string? _localHttpBaseUrl;
private string? _localHttpRoot;
+ #region 生命周期与 WebView 事件
+
+ ///
+ /// 初始化窗口并注册生命周期事件。
+ ///
public MainWindow()
{
InitializeComponent();
@@ -38,6 +44,9 @@ namespace Avalonia_PC.Views
Closed += OnClosed;
}
+ ///
+ /// 窗口打开后初始化 WebView、挂载事件并加载入口页面。
+ ///
private async void OnOpened(object? sender, EventArgs e)
{
if (_eventsAttached)
@@ -59,11 +68,17 @@ namespace Avalonia_PC.Views
await LoadInitialContentAsync();
}
+ ///
+ /// WebView 适配器创建后缓存实例,用于后续打开开发者工具。
+ ///
private void OnAdapterCreated(object? sender, WebViewAdapterEventArgs e)
{
_webViewAdapter = e.GetType().GetProperty("Adapter")?.GetValue(e);
}
+ ///
+ /// 窗口关闭时解绑事件并释放本地资源。
+ ///
private void OnClosed(object? sender, EventArgs e)
{
if (_webView is not null)
@@ -77,11 +92,21 @@ namespace Avalonia_PC.Views
StopLocalHttpServer();
}
+ ///
+ /// 页面导航完成后注入 JS 桥接脚本。
+ ///
private async void OnNavigationCompleted(object? sender, WebViewNavigationCompletedEventArgs e)
{
await InjectBridgeScriptAsync();
}
+ #endregion
+
+ #region 前端桥接与页面加载
+
+ ///
+ /// 接收前端消息并进行分发(打开调试工具 / 处理 app 请求)。
+ ///
private async void OnWebMessageReceived(object? sender, WebMessageReceivedEventArgs e)
{
var messageJson = e.Body;
@@ -137,6 +162,9 @@ namespace Avalonia_PC.Views
}
}
+ ///
+ /// 加载初始页面:优先在线地址,其次本地路径(通过本地 HTTP 服务托管)。
+ ///
private async Task LoadInitialContentAsync()
{
if (_webView is null)
@@ -174,6 +202,9 @@ namespace Avalonia_PC.Views
_webView.Source = new Uri(new Uri(_localHttpBaseUrl), Path.GetFileName(localHtmlPath));
}
+ ///
+ /// 向页面注入桥接脚本,接管 app:// 请求并回传到 C# 处理。
+ ///
private async Task InjectBridgeScriptAsync()
{
if (_webView is null)
@@ -278,6 +309,13 @@ if (!window.__appBridgeInstalled) {
await _webView.InvokeScript(script);
}
+ #endregion
+
+ #region 请求分发与通用响应
+
+ ///
+ /// 解析前端请求消息并转发到统一请求处理入口。
+ ///
private async Task HandleAppRequestAsync(JsonElement request)
{
var id = request.TryGetProperty("id", out var idProperty) ? idProperty.GetString() : null;
@@ -289,6 +327,9 @@ if (!window.__appBridgeInstalled) {
return await HandleAppRequestAsync(id, url, method, body, headers);
}
+ ///
+ /// 统一请求处理:构建上下文、处理 OPTIONS、按前缀分发并封装标准响应。
+ ///
private async Task HandleAppRequestAsync(
string? id,
string? rawUrl,
@@ -308,10 +349,7 @@ if (!window.__appBridgeInstalled) {
try
{
var uri = new Uri(rawUrl ?? throw new InvalidOperationException("请求地址不能为空。"));
- var path = uri.AbsolutePath.TrimStart('/');
- var normalizedPath = path.StartsWith("api/", StringComparison.OrdinalIgnoreCase)
- ? path
- : $"api/{path}";
+ var requestContext = CreateRouteRequestContext(uri, body);
var authorization = GetAuthorizationHeader(headers);
_ = authorization;
@@ -323,27 +361,12 @@ if (!window.__appBridgeInstalled) {
return response;
}
- if (string.Equals(normalizedPath, "api/getUser", StringComparison.OrdinalIgnoreCase))
+ var routeResult = await DispatchByPrefixAsync(requestContext);
+ if (routeResult.IsMatched)
{
- var user = await GetUserFromDatabaseAsync();
- response.Body = JsonSerializer.Serialize(new { success = true, data = user });
- return response;
- }
-
- if (string.Equals(normalizedPath, "api/processData", StringComparison.OrdinalIgnoreCase))
- {
- var input = string.Empty;
- if (!string.IsNullOrWhiteSpace(body))
- {
- using var jsonDocument = JsonDocument.Parse(body);
- if (jsonDocument.RootElement.TryGetProperty("input", out var inputProperty))
- {
- input = inputProperty.GetString() ?? string.Empty;
- }
- }
-
- var result = await ProcessDataAsync(input);
- response.Body = JsonSerializer.Serialize(new { success = true, result });
+ response.StatusCode = routeResult.StatusCode;
+ response.StatusMessage = routeResult.StatusMessage;
+ response.Body = BuildSuccessResponseBody(routeResult.Data);
return response;
}
@@ -361,6 +384,137 @@ if (!window.__appBridgeInstalled) {
}
}
+ ///
+ /// 按请求前缀分发处理器(例如 api、sys、admin 等)。
+ ///
+ private async Task DispatchByPrefixAsync(RouteRequestContext requestContext)
+ {
+ if (requestContext.PathSegments.Length > 0 &&
+ string.Equals(requestContext.PathSegments[0], "api", StringComparison.OrdinalIgnoreCase))
+ {
+ return await HandleApiPrefixAsync(requestContext);
+ }
+
+ return RouteDispatchResult.NotMatched();
+ }
+
+ ///
+ /// 处理 api 前缀下的具体业务路由。
+ ///
+ private static async Task HandleApiPrefixAsync(RouteRequestContext requestContext)
+ {
+ if (string.Equals(requestContext.NormalizedPath, "api/getUser", StringComparison.OrdinalIgnoreCase))
+ {
+ var user = await GetUserFromDatabaseAsync();
+ return RouteDispatchResult.Success(user);
+ }
+
+ if (string.Equals(requestContext.NormalizedPath, "api/processData", StringComparison.OrdinalIgnoreCase) ||
+ (requestContext.PathSegments.Length > 1 &&
+ string.Equals(requestContext.PathSegments[1], "processData", StringComparison.OrdinalIgnoreCase)))
+ {
+ var input = ExtractInput(requestContext);
+ var result = await ProcessDataAsync(input);
+ return RouteDispatchResult.Success(result);
+ }
+
+ return RouteDispatchResult.NotMatched();
+ }
+
+ ///
+ /// 统一构建成功响应体,保持前后端响应结构一致。
+ ///
+ private static string BuildSuccessResponseBody(object? data)
+ {
+ return JsonSerializer.Serialize(new { success = true, data });
+ }
+
+ ///
+ /// 从 URI 解析路径段、查询参数和 body,构建路由上下文。
+ ///
+ private static RouteRequestContext CreateRouteRequestContext(Uri uri, string? body)
+ {
+ var host = uri.Host ?? string.Empty;
+ var absolutePath = uri.AbsolutePath ?? string.Empty;
+ var combinedPath = $"{host}/{absolutePath}";
+
+ var pathSegments = combinedPath
+ .Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .Select(Uri.UnescapeDataString)
+ .ToArray();
+
+ var normalizedPath = string.Join('/', pathSegments);
+ var query = ParseQueryParameters(uri.Query);
+
+ return new RouteRequestContext
+ {
+ NormalizedPath = normalizedPath,
+ PathSegments = pathSegments,
+ Query = query,
+ Body = body,
+ };
+ }
+
+ ///
+ /// 解析查询字符串为忽略大小写的字典。
+ ///
+ private static Dictionary ParseQueryParameters(string? queryString)
+ {
+ var query = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ if (string.IsNullOrWhiteSpace(queryString))
+ {
+ return query;
+ }
+
+ var raw = queryString.TrimStart('?');
+ foreach (var pair in raw.Split('&', StringSplitOptions.RemoveEmptyEntries))
+ {
+ var separatorIndex = pair.IndexOf('=');
+ if (separatorIndex < 0)
+ {
+ query[Uri.UnescapeDataString(pair)] = string.Empty;
+ continue;
+ }
+
+ var key = Uri.UnescapeDataString(pair[..separatorIndex]);
+ var value = Uri.UnescapeDataString(pair[(separatorIndex + 1)..]);
+ query[key] = value;
+ }
+
+ return query;
+ }
+
+ ///
+ /// 按 body -> query -> path 的优先级提取业务输入参数。
+ ///
+ private static string ExtractInput(RouteRequestContext requestContext)
+ {
+ if (!string.IsNullOrWhiteSpace(requestContext.Body))
+ {
+ using var jsonDocument = JsonDocument.Parse(requestContext.Body);
+ if (jsonDocument.RootElement.TryGetProperty("input", out var inputProperty))
+ {
+ return inputProperty.GetString() ?? string.Empty;
+ }
+ }
+
+ if (requestContext.Query.TryGetValue("input", out var inputFromQuery) &&
+ !string.IsNullOrWhiteSpace(inputFromQuery))
+ {
+ return inputFromQuery;
+ }
+
+ if (requestContext.PathSegments.Length > 2)
+ {
+ return string.Join('/', requestContext.PathSegments.Skip(2));
+ }
+
+ return string.Empty;
+ }
+
+ ///
+ /// 创建桥接响应的默认 JSON/CORS 头。
+ ///
private static Dictionary CreateJsonHeaders() => new()
{
["Content-Type"] = "application/json; charset=utf-8",
@@ -369,6 +523,9 @@ if (!window.__appBridgeInstalled) {
["Access-Control-Allow-Headers"] = "Content-Type, Authorization",
};
+ ///
+ /// 从前端请求消息中提取请求头。
+ ///
private static Dictionary ExtractHeaders(JsonElement request)
{
if (!request.TryGetProperty("headers", out var headersElement) ||
@@ -386,12 +543,18 @@ if (!window.__appBridgeInstalled) {
return headers;
}
+ ///
+ /// 获取授权头,供鉴权逻辑扩展使用。
+ ///
private static string? GetAuthorizationHeader(Dictionary headers)
{
return headers.FirstOrDefault(
entry => string.Equals(entry.Key, "Authorization", StringComparison.OrdinalIgnoreCase)).Value;
}
+ ///
+ /// 在异常情况下尝试提取请求 id,确保前端可收到对应错误响应。
+ ///
private static string? TryGetRequestId(string messageJson)
{
try
@@ -405,6 +568,13 @@ if (!window.__appBridgeInstalled) {
}
}
+ #endregion
+
+ #region 页面地址配置与本地静态服务
+
+ ///
+ /// 获取在线启动地址配置(仅允许 http/https)。
+ ///
private static Uri? GetConfiguredOnlineStartupUrl()
{
if (string.IsNullOrWhiteSpace(OnlineStartupUrl))
@@ -420,6 +590,9 @@ if (!window.__appBridgeInstalled) {
return uri.Scheme is "http" or "https" ? uri : null;
}
+ ///
+ /// 获取本地启动文件路径,未配置时默认使用输出目录 www/index.html。
+ ///
private static string? GetConfiguredLocalStartupPath()
{
if (!string.IsNullOrWhiteSpace(LocalStartupPath))
@@ -430,6 +603,9 @@ if (!window.__appBridgeInstalled) {
return Path.Combine(AppContext.BaseDirectory, "www", "index.html");
}
+ ///
+ /// 确保本地 HTTP 静态服务已启动;根目录变化时会重启。
+ ///
private async Task EnsureLocalHttpServerStartedAsync(string localRoot)
{
if (!string.IsNullOrWhiteSpace(_localHttpBaseUrl) &&
@@ -454,6 +630,9 @@ if (!window.__appBridgeInstalled) {
_ = Task.Run(() => RunLocalHttpServerLoopAsync(_localHttpServer, _localHttpServerCts.Token, localRoot));
}
+ ///
+ /// 本地静态服务主循环,持续接收并分发请求。
+ ///
private static async Task RunLocalHttpServerLoopAsync(HttpListener listener, CancellationToken cancellationToken, string wwwRoot)
{
try
@@ -469,6 +648,9 @@ if (!window.__appBridgeInstalled) {
}
}
+ ///
+ /// 处理本地静态资源请求并返回文件内容。
+ ///
private static async Task HandleLocalHttpRequest(HttpListenerContext context, string wwwRoot)
{
try
@@ -509,6 +691,9 @@ if (!window.__appBridgeInstalled) {
}
}
+ ///
+ /// 根据后缀返回静态资源 Content-Type。
+ ///
private static string GetContentType(string filePath)
{
return Path.GetExtension(filePath).ToLowerInvariant() switch
@@ -521,6 +706,9 @@ if (!window.__appBridgeInstalled) {
};
}
+ ///
+ /// 获取一个可用本地端口,用于启动本地静态服务。
+ ///
private static int GetAvailableTcpPort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
@@ -530,6 +718,9 @@ if (!window.__appBridgeInstalled) {
return port;
}
+ ///
+ /// 尝试打开 WebView 开发者工具(兼容不同适配器方法名)。
+ ///
private void TryOpenDevTools()
{
if (_webViewAdapter is null)
@@ -543,6 +734,9 @@ if (!window.__appBridgeInstalled) {
method?.Invoke(_webViewAdapter, null);
}
+ ///
+ /// 停止并释放本地静态服务资源。
+ ///
private void StopLocalHttpServer()
{
try
@@ -564,18 +758,32 @@ if (!window.__appBridgeInstalled) {
}
}
+ #endregion
+
+ #region 业务示例方法
+
+ ///
+ /// 示例:模拟读取用户数据。
+ ///
private static async Task