1.MAUI Android可以正常显示

This commit is contained in:
ShaoHua
2026-04-13 21:17:15 +08:00
parent d94d1f36d2
commit 1f87565d5a
14 changed files with 228 additions and 60 deletions
+2
View File
@@ -14,6 +14,8 @@
<!-- 编译优化选项 -->
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
<!-- 禁用 MSBuild 节点重用,避免在多项目并发或调试期间出现“文件正由另一进程使用” (XARDF7024) 的问题。 -->
<MSBuildDisableNodeReuse>true</MSBuildDisableNodeReuse>
</PropertyGroup>
</Project>
+1 -1
View File
@@ -11,7 +11,7 @@
<Project Path="src/Hua.Todo.Core/Hua.Todo.Core.csproj" />
<Project Path="src/Hua.Todo.Host/Hua.Todo.Host.csproj" />
<Project Path="src/Hua.Todo.Maui/Hua.Todo.Maui.csproj">
<Build Solution="Debug|*" Project="false" />
<Deploy Solution="Debug|Any CPU" />
</Project>
</Folder>
</Solution>
+8 -9
View File
@@ -1,17 +1,16 @@
# Hua.Todo 跨平台代办管理应用
# Hua.Todo 跨平台代办管理应用 v1.2.8
一个基于 WebView 容器(MAUI / Avalonia+ 嵌入式 ASP.NET Core WebServer 架构开发的跨平台代办管理应用,支持 Windows、macOS、Android、iOS 和 Linux(预览)平台。通过 HTTP API 实现前后端通信,提供轻量、高效的任务管理体验。
一个基于 WebView 容器(MAUI / Avalonia+ 嵌入式 ASP.NET Core WebServer 架构开发的跨平台代办管理应用,支持 Windows、macOS、Android、iOS 和 Linux 平台。通过 HTTP API 实现前后端通信,提供轻量、高效的任务管理体验。
## 🚀 功能特点
### 核心功能
- **跨平台支持**:基于 MAUI / Avalonia + WebView 架构,支持 Windows、macOS、Android、iOS 和 Linux(预览)
- **任务管理**:支持创建、编辑、删除、完成状态切换
- **优先级管理**:支持高、中、低三种优先级设置,通过颜色直观区分
- **任务状态跟踪**:清晰标记任务完成状态,支持过滤查看(全部/进行中/已完成)
- **本地数据持久化**:使用 SQLite 数据库保存数据,支持完全离线使用
- **HTTP API 通信**后端通过 RESTful API 进行数据交互
- **云同步(基础)**:支持手动配置服务端地址并登录后拉取云端任务(v1.2.0 为只读展示)
- **跨平台支持**:基于 MAUI / Avalonia + WebView 架构,支持 Windows、macOS、Android、iOS 和 Linux
- **任务管理**:支持创建、编辑、删除、完成状态切换、子任务管理。
- **云同步 (CloudSync)**:支持手动配置服务端地址并登录后拉取云端任务,支持安全策略(SecurityPolicy)配置。
- **关键词检索**:支持按任务标题实时过滤,支持 Esc 清空,大小写不敏感。
- **本地数据持久化**:使用 SQLite 数据库保存数据,支持 DateTime 兼容性解析。
- **动态 API**:后端自动生成 RESTful API 并集成 Swagger UI,便于联调与调试。
## 📦 安装与使用
+15 -7
View File
@@ -1,4 +1,4 @@
# Hua.Todo 代码规范文档 v1.1.0
# Hua.Todo 代码规范文档 v1.2.8
## 1. 概述
本文档定义 Hua.Todo 项目的代码规范,包括 C#、JavaScript/TypeScript、Vue.js 和其他相关技术的编码标准。遵循这些规范有助于提高代码质量、可读性和可维护性。
@@ -10,21 +10,29 @@
- **避免缩写**: 除非是广泛认知的缩写(如 ID、URL、API)
- **一致性**: 在整个项目中保持命名风格一致
### 2.2 注释规范
- **公共 API 必须添加 XML 文档注释**
### 2.2 注释规范 (强制)
- **公共 API 必须添加 XML 文档注释** (包括 `public` / `protected` 的类、接口、方法、属性)
- ** summary**:一句话说明用途
- ** param / returns**:关键参数/返回值说明
- ** 异常或副作用**:在 summary 中明确说明(例如会注册系统钩子/会启动后台服务)
- **复杂逻辑添加行内注释**
- **避免注释显而易见的代码**
- **保持注释与代码同步更新**
- **禁止在日志或注释中输出密钥、Token、用户隐私信息**
### 2.3 代码格式化
- **使用统一的代码格式化工具**
- **使用统一的代码格式化工具** (VS / IDE 默认格式化)
- **保持一致的缩进和空格**
- **每行代码不超过 120 字符**
- **文件末尾保留一个空行**
## 3. C# 代码规范
### 3.1 命名规范
### 3.1 跨平台逻辑规范
- **禁止混写 `#if`**:禁止在同一文件内混写多个平台的大段 `#if` 实现。
- **优先使用 partial/接口**:应优先使用 `partial` 类、接口与平台目录分离。
- **说明平台差异**:平台分离后的公共入口处必须说明“平台差异在哪里、默认实现是什么、为什么这么做”。
### 3.2 异步/后台任务
- **必须说明启动时机、错误处理策略、是否需要 UI 线程、以及是否可并发/可重入**。
#### 类和接口
```csharp
+13 -9
View File
@@ -3,19 +3,19 @@
## 🛠️ 技术栈
### 后端技术栈
- **开发语言**C# 10
- **框架**.NET 10
- **开发语言**C# 13 (基于 .NET 10)
- **框架**.NET 10 (Preview/Early Adopter)
- **UI 框架**MAUI(移动端/部分桌面) + Avalonia(桌面端)
- **Web 服务器**Kestrel (ASP.NET Core 内置)
- **API 框架**ASP.NET Core Web API
- **数据访问**Entity Framework Core
- **API 框架**ASP.NET Core Web API (支持动态 API 生成)
- **数据访问**Entity Framework Core 10.0
- **数据库**SQLite (本地存储)
- **依赖注入**Microsoft.Extensions.DependencyInjection
### 前端技术栈
- **开发语言**TypeScript
- **开发语言**TypeScript 5+
- **框架**Vue.js 3
- **构建工具**Vite
- **构建工具**Vite 5+
- **HTTP 客户端**Axios
- **状态管理**Pinia
- **UI 组件库**Element Plus / Vant (移动端)
@@ -24,13 +24,17 @@
## 🎯 核心模块说明
### Hua.Todo.Core
领域实体层,定义核心实体(TaskEntity)、枚举(TaskPriority)以及仓储接口(ITaskRepository)。
领域实体层,定义核心实体(TaskEntity)、安全策略实体(SecurityPolicyEntity)、枚举(TaskPriority)以及仓储接口(ITaskRepository)。
### Hua.Todo.Application
应用层实现,包含业务逻辑、动态 API 生成逻辑、EF Core 数据库上下文以及具体的服务实现(TaskService)。
应用层实现,包含
- **业务逻辑**TaskService 实现。
- **动态 API**:基于 Middleware 的动态 API 生成逻辑。
- **云同步 (CloudSync)**:包含 Auth 验证、同步服务、安全策略管理等。
- **数据访问**EF Core 数据库上下文(TodoDbContext)与迁移。
### Hua.Todo.Host
后端 API 宿主,提供运行环境和配置,是后端服务的启动入口
后端 API 宿主,作为独立 Server 运行时提供运行环境和配置。
### Hua.Todo.Web
前端 Web 项目,基于 Vue.js 3 + TypeScript + Vite,提供用户界面,通过 HTTP API 与后端通信。
+12 -2
View File
@@ -9,13 +9,23 @@
- v1.1.0MAUI + WebView 跨平台版本
- v1.2.0 (规划中)Linux 支持与增强功能
### v1.2.0(开发中,2026-04-07
### v1.2.8 (2026-04-13)
- **云同步增强**:在 `Hua.Todo.Application` 中深度集成 `CloudSync` 模块,支持权限验证、安全策略(SecurityPolicy)与任务同步 DTO。
- **动态 API 增强**:完善 `DynamicApi` 逻辑,支持 Swagger 自动过滤与中间件拦截。
- **代码规范同步**:强制执行 XML 文档注释与跨平台逻辑分离规范,更新 `.trae/rules` 规则库。
- **多平台构建优化**:优化 `Directory.Build.props``Directory.Build.targets`,精细化控制各平台(Windows/Android/iOS/Linux)的构建开关与依赖。
- **版本号统一**:全项目版本号提升至 `v1.2.8`,同步更新各平台安装包与发布脚本。
### v1.2.02026-04-07
- **Linux 官方支持**:新增 `Hua.Todo.Avalonia` 项目,正式适配 Linux 平台,同时支持 Windows 和 macOS。
- **Avalonia 桌面交互**:增加托盘菜单(显示/退出)、关闭隐藏到托盘、Windows 全局热键唤起主窗口、热键配置本地持久化;并对齐 Avalonia 的 appsettings 默认值。
- **关键词检索**:主界面增加搜索框,按任务标题实时过滤;采用“命中即显示(含上下文)”策略;支持 Esc 清空;英文大小写不敏感。
- **云同步(基础可用)**:新增“云同步设置”弹窗,支持手动配置服务端地址(格式校验 + 保存时可达性/风险提示);登录成功后拉取云端任务并刷新主界面(v1.2.0 为只读展示);401/403 时会自动清会话并弹出登录入口。
- **MAUIWindows)内嵌 API 文档**Debug 模式下,内嵌 WebServer 默认提供 Swagger UI`{HostUrl}/swagger`)与 OpenAPI JSON`{HostUrl}/swagger/v1/swagger.json`),便于本地接口调试。
- **Android 启动稳定性修复**:在 AndroidManifest 中移除 `androidx.startup.InitializationProvider` 自动初始化入口,规避 `androidx.lifecycle.ProcessLifecycleInitializer` 缺失导致的启动崩溃(`NoClassDefFoundError`)。
- **MAUI Android 调试配置修复**:在 `Hua.Todo.Maui.csproj` 中显式启用 `AndroidApplication`,并将调试架构配置从 `AndroidSupportedAbis` 切换为 `RuntimeIdentifiers=android-x64`,减少 Visual Studio 启动 Android 调试时的项目识别与模拟器架构问题。
- **Swagger 输出补齐 Dynamic API**:任务管理等 Dynamic API 端点会出现在 `swagger.json` 中,避免“接口缺失”导致联调困难。
- **SQLite DateTime 兼容修复**:新增 `LenientUtcDateTimeStringConverter`,本地数据库中若存在历史遗留的 DateTime “ticks/时间戳字符串”脏数据,读取时将被兼容解析,避免 `/api/task` 等查询因单条坏数据整体失败。
- **SPA 路由回落行为修复**:当 Release/非 Debug 未启用 Swagger 时,`/swagger` 不再被当作“后端专用路径”排除,访问会按 SPA 路由规则回落到 `/index.html`,避免直接 404。
@@ -26,7 +36,7 @@
- **Windows WebView2 Runtime 误判修复**:当系统已安装 WebView2 Runtime 但发布产物缺少/裁剪 WebView2 托管程序集时,旧检测逻辑会误判为“未安装”;现改为优先从常见安装目录探测 Evergreen 版本,避免阻断主界面加载。
- **Windows 三件套开发体验**:新增 `start-host.ps1` / `start-dev.ps1`,并在 MAUI 中约定 `IsUsingStatic=false` 时不启动内置 WebServer,避免注入覆盖 Vite 的 `/api -> 5173` 代理配置。
- **文档与部署指南**:新增 `docs/manual/部署文档.md`,详细说明开发环境搭建、多平台发布流程(Windows/Linux/Docker)以及关键配置项;并在技术设计文档中建立链接。
- **用户文档完善**:新增 `docs/manual/新手指南.md``docs/manual/用户指南.md`,提供详细的使用说明和操作指南
- **用户文档完善**在规划中新增 `docs/manual/新手指南.md``docs/manual/用户指南.md`
27→
28→### v1.1.1 (2026-04-06)
@@ -4,7 +4,7 @@
<!-- 当指定桌面平台 RID 或显式标记为桌面构建时,仅保留 net10.0 目标框架,避免还原阶段为移动端 TFM 解析 Mono runtime pack。 -->
<IsDesktopRID Condition="'$(IsDesktopBuild)' == 'true' or ('$(RuntimeIdentifier)' != '' and ($([System.String]::Copy($(RuntimeIdentifier)).StartsWith('win-')) or $([System.String]::Copy($(RuntimeIdentifier)).StartsWith('linux-')) or $([System.String]::Copy($(RuntimeIdentifier)).StartsWith('osx-'))))">true</IsDesktopRID>
<TargetFrameworks Condition="'$(IsDesktopRID)' == 'true'">net10.0</TargetFrameworks>
<TargetFrameworks Condition="'$(IsDesktopRID)' != 'true'">net10.0;net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="'$(IsDesktopRID)' != 'true'">net10.0;net10.0-android36.0;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
+26 -1
View File
@@ -1,10 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">net10.0-windows10.0.19041.0</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('linux'))">net10.0</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('osx'))">net10.0</TargetFrameworks>
<!-- 追加 Android 目标,作为未来的移动端主入口。 -->
<TargetFrameworks>$(TargetFrameworks);net10.0-android</TargetFrameworks>
<Nullable>enable</Nullable>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
@@ -14,6 +17,21 @@
<ForceWebBuild>false</ForceWebBuild>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net10.0-android'">
<SupportedOSPlatformVersion>21.0</SupportedOSPlatformVersion>
<ApplicationId>com.hua.todo</ApplicationId>
<ApplicationTitle>代办</ApplicationTitle>
<AndroidApplication>true</AndroidApplication>
<OutputType>Exe</OutputType>
<!-- 彻底解决 Java.Lang.NoClassDefFoundError:
在 .NET 10 Preview 环境中,Fast Deployment 往往无法正确处理动态加载的 AndroidX 资源。
通过禁用 Fast Deployment 并强制将所有程序集嵌入 APK,可以解决此问题。 -->
<AndroidUseSharedRuntime>False</AndroidUseSharedRuntime>
<AndroidEnableFastDeployment>False</AndroidEnableFastDeployment>
<EmbedAssembliesIntoApk>True</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<Folder Include="Models\" />
<AvaloniaResource Include="icon.ico" />
@@ -21,17 +39,24 @@
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.13" />
<PackageReference Include="Avalonia.Android" Version="11.3.13" Condition="'$(TargetFramework)' == 'net10.0-android'" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.13" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.13" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.13" />
<PackageReference Include="WebView.Avalonia" Version="11.0.0.1" />
<PackageReference Include="WebView.Avalonia.Desktop" Version="11.0.0.1" />
<!-- 增加 Android 下的 WebView 支持包,避免跨平台逻辑中断。 -->
<PackageReference Include="WebView.Avalonia.Android" Version="11.0.0.1" Condition="'$(TargetFramework)' == 'net10.0-android'" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.13">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<!-- 为了修复 NoClassDefFoundError,我们需要显式确保核心库在 AOT 或 裁剪时被保留。 -->
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.7.1.1" Condition="'$(TargetFramework)' == 'net10.0-android'" />
</ItemGroup>
<ItemGroup>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.hua.todo">
<application android:allowBackup="true" android:icon="@drawable/icon" android:label="代办" android:supportsRtl="true">
</application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
@@ -0,0 +1,24 @@
using Android.App;
using Android.Content.PM;
using Avalonia;
using Avalonia.Android;
namespace Hua.Todo.Avalonia.Platforms.Android;
/// <summary>
/// Avalonia Android 平台入口 Activity。
/// </summary>
[Activity(
Label = "代办",
Theme = "@style/AvaloniaMainActivityTheme",
Icon = "@drawable/icon",
MainLauncher = true,
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
public class MainActivity : AvaloniaMainActivity<App>
{
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
{
return base.CustomizeAppBuilder(builder)
.WithInterFont();
}
}
+25 -16
View File
@@ -14,6 +14,7 @@
<!-- For example: <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->
<OutputType>Exe</OutputType>
<AndroidApplication>true</AndroidApplication>
<RootNamespace>Hua.Todo.Maui</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
@@ -28,7 +29,7 @@
<ApplicationTitle>代办</ApplicationTitle>
<!-- App Identifier -->
<ApplicationId>com.companyname.Hua.Todo.maui</ApplicationId>
<ApplicationId>com.hua.todo</ApplicationId>
<!-- Versions -->
<!-- Unified in Directory.Build.props -->
@@ -70,16 +71,12 @@
<SkipWebBuild>true</SkipWebBuild>
<AndroidPackageFormat>apk</AndroidPackageFormat>
<AndroidCreatePackagePerAbi>False</AndroidCreatePackagePerAbi>
<!-- 默认启用 Fast Deployment,加速日常 C#/XAML 调试。 -->
<AndroidUseSharedRuntime Condition="'$(ForceFullAndroidDeploy)' != 'true'">True</AndroidUseSharedRuntime>
<AndroidEnableFastDeployment Condition="'$(ForceFullAndroidDeploy)' != 'true'">True</AndroidEnableFastDeployment>
<AndroidFastDeploymentType Condition="'$(ForceFullAndroidDeploy)' != 'true'">Assemblies</AndroidFastDeploymentType>
<EmbedAssembliesIntoApk Condition="'$(ForceFullAndroidDeploy)' != 'true'">False</EmbedAssembliesIntoApk>
<!-- 需要验证 Android 资源变更时,传入 -p:ForceFullAndroidDeploy=true。 -->
<AndroidUseSharedRuntime Condition="'$(ForceFullAndroidDeploy)' == 'true'">False</AndroidUseSharedRuntime>
<AndroidEnableFastDeployment Condition="'$(ForceFullAndroidDeploy)' == 'true'">False</AndroidEnableFastDeployment>
<EmbedAssembliesIntoApk Condition="'$(ForceFullAndroidDeploy)' == 'true'">True</EmbedAssembliesIntoApk>
<AndroidSupportedAbis>x86_64</AndroidSupportedAbis>
<!-- .NET 10 Preview 环境下,Shared Runtime 和 Fast Deployment 往往会导致 AndroidX 依赖解析异常 (NoClassDefFoundError)。
为了稳定性,默认启用完整部署模式 (EmbedAssembliesIntoApk=True)。 -->
<AndroidUseSharedRuntime>False</AndroidUseSharedRuntime>
<AndroidEnableFastDeployment>False</AndroidEnableFastDeployment>
<EmbedAssembliesIntoApk>True</EmbedAssembliesIntoApk>
<RuntimeIdentifiers>android-x64</RuntimeIdentifiers>
<UseAppHost>false</UseAppHost>
</PropertyGroup>
@@ -166,7 +163,7 @@
</ItemGroup>
<!-- Build web assets into MAUI `wwwroot` so both Windows publish and Android packaging can reuse the same folder. -->
<Target Name="BuildTodoWeb" BeforeTargets="UpdateAndroidAssets;BeforeBuild" Condition="'$(SkipWebBuild)' != 'true' And Exists('$(TodoWebDir)') And ('$(ForceWebBuild)' == 'true' Or !Exists('$(MSBuildProjectDirectory)\wwwroot\index.html'))">
<Target Name="BuildTodoWeb" BeforeTargets="UpdateAndroidAssets;BeforeBuild" Condition="Exists('$(TodoWebDir)') And (!Exists('$(MSBuildProjectDirectory)\wwwroot\index.html') Or '$(SkipWebBuild)' != 'true' Or '$(ForceWebBuild)' == 'true')">
<Exec Command="npm ci" WorkingDirectory="$(TodoWebDir)" Condition="Exists('$(TodoWebDir)\package-lock.json') And !Exists('$(TodoWebDir)\node_modules')" />
<Exec Command="npm install" WorkingDirectory="$(TodoWebDir)" Condition="!Exists('$(TodoWebDir)\package-lock.json') And !Exists('$(TodoWebDir)\node_modules')" />
<Exec Command="npm run build:maui" WorkingDirectory="$(TodoWebDir)" />
@@ -184,21 +181,33 @@
<ItemGroup>
<MauiAsset Include="@(_TodoWebDistFiles)">
<LogicalName>wwwroot/%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
<LogicalName>$([System.String]::Copy('wwwroot/%(RecursiveDir)%(Filename)%(Extension)').Replace('\', '/'))</LogicalName>
</MauiAsset>
</ItemGroup>
</Target>
<Target Name="IncludeMauiWwwrootAsMauiAssets" BeforeTargets="ProcessMauiAssets" DependsOnTargets="BuildTodoWeb" Condition="'$(TargetFramework)' == 'net10.0-android' And Exists('$(MSBuildProjectDirectory)\wwwroot\index.html')">
<Target Name="IncludeMauiWwwrootAsMauiAssets" BeforeTargets="BeforeBuild;ProcessMauiAssets" DependsOnTargets="BuildTodoWeb" Condition="'$(TargetFramework)' == 'net10.0-android'">
<ItemGroup>
<_MauiWwwrootFiles Include="$(MSBuildProjectDirectory)\wwwroot\**\*" />
<!-- 确保包含所有 wwwroot 下的文件,包括子目录。 -->
<_MauiWwwrootFiles Include="wwwroot\**\*" />
</ItemGroup>
<ItemGroup>
<!-- 显式设置 Link 和 LogicalName。
在 Android 上,LogicalName 决定了 AssetManager.Open() 的路径。 -->
<MauiAsset Include="@(_MauiWwwrootFiles)">
<LogicalName>wwwroot/%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
<Link>wwwroot\%(RecursiveDir)%(Filename)%(Extension)</Link>
<LogicalName>$([System.String]::Copy('wwwroot/%(RecursiveDir)%(Filename)%(Extension)').Replace('\', '/'))</LogicalName>
</MauiAsset>
<!-- 同时尝试在根目录也放一份 index.html,增加容错。 -->
<MauiAsset Include="wwwroot\index.html" Condition="Exists('wwwroot\index.html')">
<Link>index.html</Link>
<LogicalName>index.html</LogicalName>
</MauiAsset>
</ItemGroup>
<Message Importance="high" Text="[Build] Included $(_MauiWwwrootFiles->Count()) assets from wwwroot into MauiAsset (with wwwroot/ prefix and root fallback)" />
</Target>
</Project>
+2 -1
View File
@@ -100,8 +100,9 @@ public static partial class MauiProgram
{
try
{
InitializeDatabase(app.Services, connectionString);
// 优先启动内嵌 WebServer,确保 WebView 加载时不再 ERR_CONNECTION_REFUSED。
await StartWebServer(app.Services);
InitializeDatabase(app.Services, connectionString);
}
catch (Exception ex)
{
@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.hua.todo">
<application android:allowBackup="true" android:icon="@drawable/appicon" android:supportsRtl="true" android:networkSecurityConfig="@xml/network_security_config" android:label="To&amp;Do"></application>
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.hua.todo">
<application android:allowBackup="true" android:icon="@drawable/appicon" android:supportsRtl="true" android:networkSecurityConfig="@xml/network_security_config" android:label="To&amp;Do">
<provider android:name="androidx.startup.InitializationProvider" android:authorities="${applicationId}.androidx-startup" tools:node="remove" />
</application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
</manifest>
@@ -55,7 +55,9 @@ public sealed class MobileEmbeddedWebServerService : IEmbeddedWebServerService
if (_listener != null) return Task.CompletedTask;
_cts = new CancellationTokenSource();
_listener = new TcpListener(IPAddress.Loopback, _appSettings.WebServer.Port);
// 使用 IPAddress.Any 而非 Loopback,避免 localhost 解析到 IPv6 [::1] 时连接被拒绝。
// 同时确保在 Android 模拟器/设备上 localhost 能正确连通。
_listener = new TcpListener(IPAddress.Any, _appSettings.WebServer.Port);
_listener.Start();
_acceptLoop = Task.Run(() => AcceptLoopAsync(_cts.Token));
@@ -257,12 +259,42 @@ public sealed class MobileEmbeddedWebServerService : IEmbeddedWebServerService
return;
}
Console.WriteLine($"[WebServer] Serving static: {path} -> {assetPath}");
if (!TryOpenAsset(assetPath, out var assetStream))
{
// 如果 wwwroot/path 找不到,尝试直接加载 path(应对路径扁平化)
var fallbackAssetPath = path.TrimStart('/');
if (!string.IsNullOrEmpty(fallbackAssetPath) && !fallbackAssetPath.StartsWith("wwwroot", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine($"[WebServer] {assetPath} not found, trying fallback path: {fallbackAssetPath}");
if (TryOpenAsset(fallbackAssetPath, out assetStream))
{
await using (assetStream)
{
await WriteStreamAsync(stream, 200, assetStream, GetContentType(path), token);
}
return;
}
}
if (!Path.HasExtension(path))
{
assetPath = "wwwroot/index.html";
if (TryOpenAsset(assetPath, out assetStream))
var fallbackHtmlPath = "wwwroot/index.html";
Console.WriteLine($"[WebServer] {assetPath} not found, trying SPA fallback: {fallbackHtmlPath}");
if (TryOpenAsset(fallbackHtmlPath, out assetStream))
{
await using (assetStream)
{
await WriteStreamAsync(stream, 200, assetStream, "text/html; charset=utf-8", token);
}
return;
}
// 再次尝试不带 wwwroot 的 index.html
fallbackHtmlPath = "index.html";
Console.WriteLine($"[WebServer] {assetPath} not found, trying SPA fallback: {fallbackHtmlPath}");
if (TryOpenAsset(fallbackHtmlPath, out assetStream))
{
await using (assetStream)
{
@@ -272,6 +304,7 @@ public sealed class MobileEmbeddedWebServerService : IEmbeddedWebServerService
}
}
Console.WriteLine($"[WebServer] 404 Not Found: {assetPath}");
await WriteTextAsync(stream, 404, "Not Found", "text/plain; charset=utf-8", token);
return;
}
@@ -286,11 +319,43 @@ public sealed class MobileEmbeddedWebServerService : IEmbeddedWebServerService
{
try
{
stream = global::Android.App.Application.Context.Assets.Open(assetPath);
// Android AssetManager.Open() 路径不能以 / 开头,且必须使用正斜杠
var normalizedPath = assetPath.TrimStart('/').Replace('\\', '/');
stream = global::Android.App.Application.Context.Assets.Open(normalizedPath);
return true;
}
catch
catch (Exception ex)
{
Console.WriteLine($"[WebServer] Failed to open asset '{assetPath}': {ex.Message}");
// 只在 wwwroot/index.html 失败时尝试列出文件,帮助排查打包结构
if (assetPath.EndsWith("index.html", StringComparison.OrdinalIgnoreCase))
{
try
{
// 列出 root assets 和 wwwroot 目录,帮助排查打包结构
var rootAssets = global::Android.App.Application.Context.Assets.List("");
if (rootAssets != null)
{
Console.WriteLine($"[WebServer] Root Assets: {string.Join(", ", rootAssets)}");
}
var assets = global::Android.App.Application.Context.Assets.List("wwwroot");
if (assets != null && assets.Length > 0)
{
Console.WriteLine($"[WebServer] Assets in 'wwwroot/': {string.Join(", ", assets)}");
}
else
{
Console.WriteLine("[WebServer] 'wwwroot/' directory is empty or not found in assets.");
}
}
catch (Exception listEx)
{
Console.WriteLine($"[WebServer] Failed to list assets: {listEx.Message}");
}
}
stream = Stream.Null;
return false;
}
@@ -423,6 +488,8 @@ public sealed class MobileEmbeddedWebServerService : IEmbeddedWebServerService
var requestLine = await reader.ReadLineAsync(token);
if (string.IsNullOrWhiteSpace(requestLine)) throw new InvalidOperationException("empty request line");
Console.WriteLine($"[WebServer] Raw Request Line: {requestLine}");
var parts = requestLine.Split(' ');
if (parts.Length < 2) throw new InvalidOperationException("invalid request line");
@@ -430,11 +497,19 @@ public sealed class MobileEmbeddedWebServerService : IEmbeddedWebServerService
var target = parts[1].Trim();
string path;
if (Uri.TryCreate(target, UriKind.Absolute, out var absoluteUri) &&
(absoluteUri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) ||
absoluteUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)))
if (target.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
target.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
path = absoluteUri.AbsolutePath;
// WebView 有时会发送绝对 URL(例如 GET http://localhost:5057/ HTTP/1.1
if (Uri.TryCreate(target, UriKind.Absolute, out var absoluteUri))
{
path = absoluteUri.AbsolutePath;
Console.WriteLine($"[WebServer] Parsed path from absolute URI: {path}");
}
else
{
path = target;
}
}
else if (target.StartsWith("//", StringComparison.Ordinal) &&
Uri.TryCreate("http:" + target, UriKind.Absolute, out var authorityUri))
@@ -451,6 +526,8 @@ public sealed class MobileEmbeddedWebServerService : IEmbeddedWebServerService
if (string.IsNullOrEmpty(path)) path = "/";
if (!path.StartsWith("/", StringComparison.Ordinal)) path = "/" + path;
Console.WriteLine($"[WebServer] Final Resolved Path: {path}");
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
string? line;
while (!string.IsNullOrEmpty(line = await reader.ReadLineAsync(token)))