refactor: 重构待办事项模块结构与命名

This commit is contained in:
ShaoHua
2026-04-08 19:59:50 +08:00
parent 7a4c516a20
commit 04263dff4e
30 changed files with 888 additions and 320 deletions
+15 -4
View File
@@ -1,8 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net10.0-windows10.0.19041.0</TargetFrameworks>
<TargetFrameworks>net10.0-android</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">net10.0-windows10.0.19041.0</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('osx'))">$(TargetFrameworks);net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
<!-- Note for MacCatalyst:
The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
@@ -29,7 +30,7 @@
<ApplicationId>com.companyname.Hua.Todo.maui</ApplicationId>
<!-- Versions -->
<Version>1.1.9</Version>
<Version>1.2.0/Version>
<ApplicationDisplayVersion>$(Version)</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
@@ -54,6 +55,10 @@
<ForceWebBuild>false</ForceWebBuild>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
<ApplicationIcon>icon.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net10.0-android|AnyCPU'">
<AndroidPackageFormat>aab</AndroidPackageFormat>
<AndroidUseAapt2>True</AndroidUseAapt2>
@@ -127,7 +132,7 @@
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0-windows10.0.19041.0'">
<Content Include="icon.ico" CopyToOutputDirectory="PreserveNewest" />
<Content Include="icon.ico" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0-windows10.0.19041.0'">
@@ -138,6 +143,10 @@
<FrameworkReference Include="Microsoft.WindowsDesktop.App" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0-windows10.0.19041.0' And '$(Configuration)' == 'Debug'">
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0-maccatalyst'">
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
</ItemGroup>
@@ -200,3 +209,5 @@
+15 -2
View File
@@ -13,13 +13,14 @@ namespace Hua.Todo.Maui;
/// <summary>
/// MAUI 程序启动类
/// </summary>
public static class MauiProgram
public static partial class MauiProgram
{
/// <summary>
/// 创建并配置 MAUI 应用程序
/// </summary>
public static MauiApp CreateMauiApp()
{
ConfigurePlatformWebViewContainer();
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
@@ -71,7 +72,17 @@ public static class MauiProgram
// 注册嵌入式 Web 服务器(平台相关)
#if WINDOWS
builder.Services.AddSingleton<IEmbeddedWebServerService, EmbeddedWebServerService>();
// Windows 平台下嵌入式 WebServer 的启用策略:
// - 静态托管模式(IsUsingStatic=true):由 MAUI 内置 WebServer 提供 wwwroot 与本地 API。
// - 开发三件套模式(IsUsingStatic=false,前端走 Vite):API 由独立 Host(5173) 提供,避免在 MAUI 内启动 WebServer 导致注入覆盖前端代理配置。
if (appSettings.WebServer.IsUsingStatic)
{
builder.Services.AddSingleton<IEmbeddedWebServerService, EmbeddedWebServerService>();
}
else
{
builder.Services.AddSingleton<IEmbeddedWebServerService, NoopEmbeddedWebServerService>();
}
#elif ANDROID
builder.Services.AddSingleton<IEmbeddedWebServerService, MobileEmbeddedWebServerService>();
#else
@@ -101,6 +112,8 @@ public static class MauiProgram
return app;
}
static partial void ConfigurePlatformWebViewContainer();
/// <summary>
/// 从 appsettings.json 加载配置
/// </summary>
@@ -1,4 +1,5 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui.ApplicationModel;
namespace Hua.Todo.Maui.Views
{
@@ -30,5 +31,53 @@ namespace Hua.Todo.Maui.Views
var windowService = new Platforms.Windows.WindowsWindowService();
windowService.MinimizeWindow(window);
}
partial void PlatformPrepareWebViewContainer()
{
if (Platforms.Windows.WebView2RuntimeDetector.IsRuntimeInstalled(out _))
{
return;
}
_isWebViewContainerReady = false;
var downloadUrl = "https://developer.microsoft.com/microsoft-edge/webview2/";
var content = new VerticalStackLayout
{
Padding = new Thickness(20),
Spacing = 12,
Children =
{
new Label
{
Text = "检测到系统未安装 WebView2 Runtime,无法加载主界面。",
FontSize = 16
},
new Label
{
Text = "请安装 Microsoft Edge WebView2 RuntimeEvergreen),安装完成后重新打开应用。",
Opacity = 0.85
},
new Button
{
Text = "打开下载页面",
Command = new Command(async () =>
{
await Launcher.Default.OpenAsync(downloadUrl);
})
},
new Button
{
Text = "退出应用",
Command = new Command(() =>
{
Microsoft.Maui.Controls.Application.Current?.Quit();
})
}
}
};
Content = new ScrollView { Content = content };
}
}
}
@@ -0,0 +1,99 @@
using System.Reflection;
namespace Hua.Todo.Maui.Platforms.Windows;
internal static class WebView2RuntimeDetector
{
/// <summary>
/// 判断当前 Windows 系统是否可用 WebView2 Runtime。
/// 优先通过已知的 Evergreen 安装目录探测(避免因托管程序集缺失/裁剪导致误判),
/// 其次再尝试通过 WebView2 SDK 的 <c>CoreWebView2Environment.GetAvailableBrowserVersionString</c> 获取版本。
/// </summary>
/// <param name="version">检测到的运行时版本(若可用)。</param>
/// <returns>若系统存在可用的 WebView2 Runtime,则返回 <c>true</c>;否则返回 <c>false</c>。</returns>
internal static bool IsRuntimeInstalled(out string? version)
{
version = null;
try
{
version = TryGetEvergreenVersionFromKnownLocations();
if (!string.IsNullOrWhiteSpace(version))
{
return true;
}
var type = Type.GetType("Microsoft.Web.WebView2.Core.CoreWebView2Environment, Microsoft.Web.WebView2.Core");
if (type == null)
{
return false;
}
version = TryInvokeVersionGetter(type);
return !string.IsNullOrWhiteSpace(version);
}
catch
{
return false;
}
}
private static string? TryGetEvergreenVersionFromKnownLocations()
{
var candidates = new[]
{
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Microsoft", "EdgeWebView", "Application"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Microsoft", "EdgeWebView", "Application"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft", "EdgeWebView", "Application"),
};
Version? best = null;
foreach (var baseDir in candidates)
{
if (string.IsNullOrWhiteSpace(baseDir) || !Directory.Exists(baseDir))
{
continue;
}
foreach (var dir in Directory.EnumerateDirectories(baseDir))
{
var name = Path.GetFileName(dir);
if (!Version.TryParse(name, out var v))
{
continue;
}
if (!File.Exists(Path.Combine(dir, "msedgewebview2.exe")))
{
continue;
}
if (best == null || v > best)
{
best = v;
}
}
}
return best?.ToString();
}
private static string? TryInvokeVersionGetter(Type environmentType)
{
var noArg = environmentType.GetMethod("GetAvailableBrowserVersionString", BindingFlags.Public | BindingFlags.Static, Type.DefaultBinder, Type.EmptyTypes, null);
if (noArg != null)
{
return noArg.Invoke(null, null) as string;
}
var oneArg = environmentType.GetMethod("GetAvailableBrowserVersionString", BindingFlags.Public | BindingFlags.Static, Type.DefaultBinder, new[] { typeof(string) }, null);
if (oneArg != null)
{
return oneArg.Invoke(null, new object?[] { null }) as string;
}
return null;
}
}
+23 -2
View File
@@ -62,6 +62,25 @@ cd Hua.Todo.Maui
dotnet build -f net10.0-windows10.0.19041.0
dotnet run -f net10.0-windows10.0.19041.0
```
Windows Debug 编译下,MAUI 内嵌 WebServer 会提供接口文档:
- Swagger UI`{HostUrl}/swagger`
- OpenAPI JSON`{HostUrl}/swagger/v1/swagger.json`
其中 `{HostUrl}` 来自 `appsettings.json: WebServer.HostUrl`(默认 `http://localhost:5057`)。
按默认配置,对应地址为:
- Swagger UI`http://localhost:5057/swagger`
- OpenAPI JSON`http://localhost:5057/swagger/v1/swagger.json`
#### Windows(三件套热更新:MAUI + Vite + Host
该模式用于开发阶段获得最佳热更新体验:
- `Hua.Todo.Host`:提供 API`http://localhost:5173`
- `Hua.Todo.Web`Vite dev server`http://localhost:5174`),并将 `/api` 代理到 5173
- `Hua.Todo.Maui`WebView 加载 5174
在该模式下,Swagger UI 地址为:`http://localhost:5173/swagger`(仅 `ASPNETCORE_ENVIRONMENT=Development` 时启用)。
```powershell
.\start-dev.ps1
```
然后在 Visual Studio 中启动 `Hua.Todo.Maui`F5)。
#### macOS
```bash
@@ -134,7 +153,9 @@ dotnet run -f net10.0-android
1. **macOS 权限**: 首次运行时需要在系统设置中授予辅助功能权限
2. **Windows UAC**: 某些情况下可能需要管理员权限
3. **移动端限制**: 移动端不支持真正的全局快捷键,使用通知快捷方式替代
4. **WebView**: 确保 Hua.Todo.Api 服务`http://localhost:5173` 运行
4. **WebView(开发)**: 三件套模式下需要确保 `Hua.Todo.Host` `http://localhost:5173` 运行,同时 `Hua.Todo.Web``http://localhost:5174` 运行
5. **Windows WebView2 数据目录**: WebView2 的缓存/存储写入 `%LocalAppData%\Hua.Todo\WebView2`,避免在安装目录生成 `*.WebView2` 文件夹
6. **Windows WebView2 Runtime**: 依赖系统安装的 Microsoft Edge WebView2 Runtime;若缺失应用会提示下载安装
## 后续计划
@@ -146,4 +167,4 @@ dotnet run -f net10.0-android
## 许可证
AGPL-3.0 License ([English](file:///d:/Proj/Hua.Todo/LICENSE) | [中文](file:///d:/Proj/Hua.Todo/LICENSE.zh-CN))
AGPL-3.0 License ([English](file:///d:/Proj/Hua.Todo/LICENSE) | [中文](file:///d:/Proj/Hua.Todo/LICENSE.zh-CN))
+1 -1
View File
@@ -6,7 +6,7 @@ namespace Hua.Todo.Maui.Services;
public static class AppMetadata
{
private const string AppNameText = "\u5F85\u529E\u4E8B\u9879";
private const string AppNameText = "Hua.Todo";
public static string AppName => AppNameText;
@@ -8,6 +8,7 @@ using Microsoft.Extensions.Hosting;
using System.Text.Json;
using Hua.Todo.Application;
using Hua.Todo.Application.DynamicApi;
using Hua.Todo.Application.DynamicApi.Swagger;
using Hua.Todo.Maui.Models;
using AppSettings = Hua.Todo.Maui.Models.AppSettings;
@@ -65,6 +66,12 @@ public class EmbeddedWebServerService : IEmbeddedWebServerService
});
builder.Services.AddEndpointsApiExplorer();
#if DEBUG
builder.Services.AddSwaggerGen(options =>
{
options.DocumentFilter<DynamicApiSwaggerDocumentFilter>();
});
#endif
// 注册应用逻辑服务
builder.Services.AddApplicationServices(_appSettings.WebServer.ConnectionString);
@@ -82,6 +89,11 @@ public class EmbeddedWebServerService : IEmbeddedWebServerService
var app = builder.Build();
#if DEBUG
app.UseSwagger();
app.UseSwaggerUI();
#endif
// 如果配置为使用静态文件(前端托管),则配置静态文件服务
if (_appSettings.WebServer.IsUsingStatic)
{
@@ -135,7 +147,18 @@ public class EmbeddedWebServerService : IEmbeddedWebServerService
if (context.Request.Path.HasValue)
{
var path = context.Request.Path.Value;
if (path != "/" && !path.StartsWith("/assets", StringComparison.OrdinalIgnoreCase) && !path.StartsWith("/api", StringComparison.OrdinalIgnoreCase))
// Swagger 仅在 DEBUG 下启用;Release 下不应把 /swagger 当作“后端专用路径”排除,
// 否则访问 /swagger 会直接 404 而不会回落到 SPA/index.html)。
#if DEBUG
var isSwaggerPath = path.StartsWith("/swagger", StringComparison.OrdinalIgnoreCase);
#else
var isSwaggerPath = false;
#endif
if (path != "/"
&& !path.StartsWith("/assets", StringComparison.OrdinalIgnoreCase)
&& !path.StartsWith("/api", StringComparison.OrdinalIgnoreCase)
&& !isSwaggerPath)
{
var ext = Path.GetExtension(path);
if (string.IsNullOrEmpty(ext))
+9
View File
@@ -12,6 +12,7 @@ namespace Hua.Todo.Maui.Views
{
private readonly AppSettings _appSettings;
private readonly IEmbeddedWebServerService? _webServer;
private bool _isWebViewContainerReady = true;
/// <summary>
/// 创建 <see cref="MainPage"/>。
@@ -24,6 +25,12 @@ namespace Hua.Todo.Maui.Views
_appSettings = appSettings;
_webServer = webServer;
PlatformPrepareWebViewContainer();
if (!_isWebViewContainerReady)
{
return;
}
SetupWebViewSource();
SetupWebViewCommunication();
SetupKeyboardHandler();
@@ -141,10 +148,12 @@ namespace Hua.Todo.Maui.Views
/// 平台特定的键盘处理器初始化。
/// </summary>
partial void PlatformSetupKeyboardHandler();
/// <summary>
/// 平台特定的 Esc 键处理逻辑。
/// </summary>
/// <param name="window">当前窗口。</param>
partial void PlatformOnEscKeyPressed(Window window);
partial void PlatformPrepareWebViewContainer();
}
}
+2 -2
View File
@@ -1,10 +1,10 @@
{
"WebServer": {
"Port": 5057,
"IsUsingStatic": false,
"IsUsingStatic": true,
"ConnectionString": "",
"HostUrl": "http://localhost:5057",
"ForEndUrl": "http://localhost:5174"
"ForEndUrl": "http://localhost:5057"
},
"Development": {
},
+10 -3
View File
@@ -1,5 +1,5 @@
#define MyAppName "Hua.Todo"
#define MyAppVersion "1.1.8"
#define MyAppVersion "1.1.10"
#define MyAppPublisher "ShaoHua"
#define MyAppURL "https://git.we965.cn/Tools/Hua.Todo"
#define MyAppExeName "Hua.Todo.Maui.exe"
@@ -34,6 +34,7 @@ PrivilegesRequired=lowest
OutputDir=Output
OutputBaseFilename={#MyAppName}_Setup_v{#MyAppVersion}
SetupIconFile=icon.ico
UninstallDisplayIcon={app}\icon.ico
SolidCompression=no
WizardStyle=modern
@@ -47,9 +48,15 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{
Source: "bin\Release\net10.0-windows10.0.19041.0\win-x64\publish\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; 注意: 请勿在任何共享系统文件上使用“Flags: ignoreversion”
[InstallDelete]
Type: filesandordirs; Name: "{app}\{#MyAppExeName}.WebView2"
[UninstallDelete]
Type: filesandordirs; Name: "{app}\{#MyAppExeName}.WebView2"
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\icon.ico"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; IconFilename: "{app}\icon.ico"
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent