refactor: 重构待办事项模块结构与命名
This commit is contained in:
@@ -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 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 Runtime(Evergreen),安装完成后重新打开应用。",
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user