1.MAUI Android可以正常显示
This commit is contained in:
@@ -14,6 +14,8 @@
|
||||
|
||||
<!-- 编译优化选项 -->
|
||||
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
|
||||
<!-- 禁用 MSBuild 节点重用,避免在多项目并发或调试期间出现“文件正由另一进程使用” (XARDF7024) 的问题。 -->
|
||||
<MSBuildDisableNodeReuse>true</MSBuildDisableNodeReuse>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -9,13 +9,23 @@
|
||||
- v1.1.0:MAUI + 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.0(2026-04-07)
|
||||
|
||||
- **Linux 官方支持**:新增 `Hua.Todo.Avalonia` 项目,正式适配 Linux 平台,同时支持 Windows 和 macOS。
|
||||
- **Avalonia 桌面交互**:增加托盘菜单(显示/退出)、关闭隐藏到托盘、Windows 全局热键唤起主窗口、热键配置本地持久化;并对齐 Avalonia 的 appsettings 默认值。
|
||||
- **关键词检索**:主界面增加搜索框,按任务标题实时过滤;采用“命中即显示(含上下文)”策略;支持 Esc 清空;英文大小写不敏感。
|
||||
- **云同步(基础可用)**:新增“云同步设置”弹窗,支持手动配置服务端地址(格式校验 + 保存时可达性/风险提示);登录成功后拉取云端任务并刷新主界面(v1.2.0 为只读展示);401/403 时会自动清会话并弹出登录入口。
|
||||
- **MAUI(Windows)内嵌 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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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&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&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)))
|
||||
|
||||
Reference in New Issue
Block a user