From d00a907da020e57bb9c6cc7b27c247c719f2104f Mon Sep 17 00:00:00 2001 From: ShaoHua <345265198@qqcom> Date: Mon, 6 Apr 2026 22:59:16 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=E8=A7=84=E8=8C=83=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=92=8C=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .trae/rules/commenting.md | 33 +++ Hua.Todo.slnx | 4 +- README.md | 12 +- docs/产品需求文档-1.2.0.md | 77 ++++++ .../Data/TodoDbContext.cs | 14 ++ .../DynamicApi/DynamicApiExtensions.cs | 8 + .../DynamicApi/DynamicApiMiddleware.cs | 13 + .../DynamicApi/HttpAttributes.cs | 65 +++++ .../DynamicApi/ParameterBindingAttributes.cs | 6 + .../DynamicApi/RemoteServiceAttribute.cs | 9 + .../Interfaces/IDynamicApiService.cs | 4 + .../Interfaces/ITaskService.cs | 52 ++++ src/Hua.Todo.Application/Models/TaskModels.cs | 67 +++++ .../Repositories/TaskRepository.cs | 44 ++++ .../ServiceCollectionExtensions.cs | 9 + .../Services/TaskService.cs | 54 +++++ .../Interfaces/ITaskRepository.cs | 25 +- src/Hua.Todo.Maui/App.NonWindows.cs | 32 +++ src/Hua.Todo.Maui/App.xaml.cs | 228 +++++------------- src/Hua.Todo.Maui/MauiProgram.cs | 29 ++- .../Platforms/Android/MainActivity.cs | 7 + .../Platforms/Android/MainApplication.cs | 14 +- .../Android/MobileEmbeddedWebServerService.cs | 28 +++ .../Platforms/Windows/App.Windows.cs | 212 ++++++++++++++++ .../Platforms/Windows/MainPage.Windows.cs | 34 +++ .../Windows/WindowsKeyboardHandler.cs | 18 +- .../Platforms/Windows/WindowsWindowService.cs | 15 ++ .../Services/EmbeddedWebServerService.cs | 37 ++- .../Services/HotKeySettingsService.cs | 29 +++ .../Services/IEmbeddedWebServerService.cs | 18 ++ .../Platforms/WindowsGlobalHotKeyService.cs | 5 + src/Hua.Todo.Maui/Views/MainPage.xaml.cs | 56 ++++- src/Hua.Todo.Web/src/api/tasks.ts | 35 +++ src/Hua.Todo.Web/vite.config.ts | 2 +- 34 files changed, 1095 insertions(+), 200 deletions(-) create mode 100644 .trae/rules/commenting.md create mode 100644 docs/产品需求文档-1.2.0.md create mode 100644 src/Hua.Todo.Maui/App.NonWindows.cs create mode 100644 src/Hua.Todo.Maui/Platforms/Windows/App.Windows.cs create mode 100644 src/Hua.Todo.Maui/Platforms/Windows/MainPage.Windows.cs diff --git a/.trae/rules/commenting.md b/.trae/rules/commenting.md new file mode 100644 index 0000000..9c3139c --- /dev/null +++ b/.trae/rules/commenting.md @@ -0,0 +1,33 @@ +--- +alwaysApply: true +description: 强制项目注释规范(C# / TypeScript):新增或修改代码必须补全必要注释,便于维护与跨平台开发。 +--- + +# 注释规范(必须遵守) + +## 通用 + +- 新增或修改的代码必须包含足够注释,使“不了解该模块的人”也能理解其职责、边界与关键决策。 +- 优先使用 **XML 文档注释**(`///`),而不是随意的行内注释。 +- 不允许无意义注释(例如“初始化变量”“进入方法”)。注释必须解释“为什么/约束/边界/副作用”。 +- 不允许出现“TODO/FIXME”但无上下文或无处理方案的注释。 + +## C#(.NET / MAUI) + +- 所有 `public` / `protected` 的 **类、接口、方法、属性** 必须提供 XML 文档注释,至少包含: + - `summary`:一句话说明用途 + - 对关键参数/返回值:`param` / `returns` + - 对异常或副作用:在 `summary` 中明确说明(例如会注册系统钩子/会启动后台服务) +- 对 **跨平台逻辑**: + - 禁止在同一文件内混写多个平台的大段 `#if` 实现;应优先使用 `partial`、接口与平台目录分离。 + - 平台分离后的公共入口处必须说明“平台差异在哪里、默认实现是什么、为什么这么做”。 +- 对 **异步/后台任务**: + - 必须说明启动时机、错误处理策略、是否需要 UI 线程、以及是否可并发/可重入。 +- 对 **安全/隐私**: + - 禁止在日志或注释中输出密钥、Token、用户隐私信息。 + +## TypeScript / Vue(前端) + +- 对导出的函数/类型必须有注释,解释用途与输入输出。 +- 对“与后端/MAUI 交互”的协议字段(例如全局变量、事件名)必须注释说明来源与约束。 + diff --git a/Hua.Todo.slnx b/Hua.Todo.slnx index 688360a..c1f8009 100644 --- a/Hua.Todo.slnx +++ b/Hua.Todo.slnx @@ -7,7 +7,7 @@ - + @@ -15,4 +15,4 @@ - \ No newline at end of file + diff --git a/README.md b/README.md index 0b5f681..122ae30 100644 --- a/README.md +++ b/README.md @@ -158,13 +158,23 @@ Hua.Todo/ - 采用语义化版本号:`MAJOR.MINOR.PATCH` - v1.0.0:初始 WPF 版本 - v1.1.0:MAUI + WebView 跨平台版本 +- v1.2.0 (规划中):Linux 支持与增强功能 ### v1.1.0 更新内容 - 重构为 MAUI + WebView 架构 -- 实现跨平台支持 +- 实现跨平台支持 (Windows, macOS, Android, iOS) - 使用 HTTP API 进行前后端通信 - 采用 Vue.js 3 作为前端框架 - 使用 SQLite 作为本地数据库 +- 实现子任务支持 + +### v1.2.0 规划内容 (即将推出) +- **Linux 官方支持**:正式适配 Linux 平台。 +- **搜索与过滤**:支持按标题搜索、按优先级和状态过滤。 +- **本地提醒**:支持设置任务提醒时间并发送本地通知。 +- **标签系统**:引入多标签支持,提升任务组织效率。 +- **暗色模式**:全平台适配暗色/深色主题。 +- **数据导出导入**:支持 JSON 格式数据备份与迁移。 ## 🤝 贡献指南 diff --git a/docs/产品需求文档-1.2.0.md b/docs/产品需求文档-1.2.0.md new file mode 100644 index 0000000..8958278 --- /dev/null +++ b/docs/产品需求文档-1.2.0.md @@ -0,0 +1,77 @@ +# Hua.Todo 产品需求文档 (PRD) v1.2.0 + +## 1. 项目概述 +在 v1.1.0 版本成功实现 MAUI + WebView 跨平台架构的基础上,v1.2.0 版本将重点提升任务管理的精细化程度,完善平台支持(Linux),并增强用户体验(暗色模式、提醒功能)。 + +## 2. 核心目标 +- **完善平台覆盖**:正式支持 Linux 平台。 +- **增强任务组织**:引入搜索、过滤和标签系统。 +- **提升交互体验**:支持本地通知提醒及暗色模式。 +- **数据安全保障**:实现基础的数据导入导出功能。 + +## 3. 功能需求 + +### 3.1 平台支持:Linux 官方支持 +- **适配性**:确保 WebView 在 Linux 环境下的正常渲染与交互。 +- **打包部署**:提供 Linux 平台的打包脚本(如 .deb 或 .tar.gz)。 + +### 3.2 任务检索与过滤 (Search & Filter) +- **关键词搜索**:在主界面顶部增加搜索框,支持按任务标题模糊匹配。 +- **高级过滤**: + - 按优先级过滤(高、中、低)。 + - 按标签过滤。 + - 按创建/完成时间段过滤。 + +### 3.3 本地提醒 (Local Reminders) +- **提醒设置**:在任务编辑界面增加“提醒时间”字段。 +- **通知触发**:当达到提醒时间时,通过平台原生 API 发送本地通知(Notification)。 +- **状态反馈**:已过期的提醒任务在列表中有明显的视觉标识。 + +### 3.4 标签系统 (Tag System) +- **多标签支持**:一个任务可关联多个标签。 +- **标签管理**:支持自定义标签名称和颜色。 +- **快速归类**:通过标签快速筛选相关任务。 + +### 3.5 主题增强:暗色模式 (Dark Mode) +- **自动切换**:根据系统主题自动切换浅色/深色模式。 +- **手动控制**:在设置中提供手动切换开关。 +- **UI 适配**:Vue 前端及 MAUI 原生部分均需完成暗色模式适配。 + +### 3.6 数据迁移 (Data Migration) +- **导出功能**:支持将所有任务数据导出为 JSON 格式文件。 +- **导入功能**:支持从 JSON 文件恢复数据,解决跨设备迁移问题。 + +## 4. 技术实现要点 + +### 4.1 Linux 适配 +- 使用 WebKitGTK 作为 WebView 容器。 +- 适配 Linux 下的全局快捷键实现。 + +### 4.2 本地通知 +- 使用 `Plugin.LocalNotification` 或 MAUI 自带的通知机制(取决于 .NET 10 的最新支持)。 +- 确保后台服务在移动端能准时触发提醒。 + +### 4.3 标签数据结构 +- 在 `TaskEntity` 中增加 `Tags` 关联(多对多关系或简单的 JSON 存储)。 + +### 4.4 主题管理 +- 使用 CSS Variables (Custom Properties) 管理前端主题颜色。 +- 监听 `(prefers-color-scheme: dark)` 媒体查询。 + +## 5. 版本规划 + +### 5.1 v1.2.0 目标 +- [ ] Linux 平台完整支持与打包脚本。 +- [ ] 搜索框及多维过滤功能。 +- [ ] 本地提醒(提醒时间设置与通知触发)。 +- [ ] 基础标签功能(创建、关联、过滤)。 +- [ ] 全局暗色模式适配。 +- [ ] 数据导入导出(JSON)。 + +### 5.2 后续版本规划 +- **v1.3.0**:云同步功能(接入第三方云盘或自建服务)。 +- **v1.4.0**:统计图表(任务完成趋势、时间分配分析)。 + +## 6. 风险评估 +- **Linux 碎片化**:不同发行版下 WebView 的依赖库可能不一致。 +- **移动端后台限制**:Android/iOS 对后台进程的限制可能导致提醒不准时。 diff --git a/src/Hua.Todo.Application/Data/TodoDbContext.cs b/src/Hua.Todo.Application/Data/TodoDbContext.cs index 5e6d359..bb6f879 100644 --- a/src/Hua.Todo.Application/Data/TodoDbContext.cs +++ b/src/Hua.Todo.Application/Data/TodoDbContext.cs @@ -3,14 +3,28 @@ using Hua.Todo.Core.Entities; namespace Hua.Todo.Application.Data; +/// +/// 应用程序数据库上下文(EF Core)。 +/// public class TodoDbContext : DbContext { + /// + /// 创建 。 + /// + /// 数据库上下文配置。 public TodoDbContext(DbContextOptions options) : base(options) { } + /// + /// 任务集合。 + /// public DbSet Tasks { get; set; } + /// + /// 配置实体模型映射。 + /// + /// 模型构建器。 protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); diff --git a/src/Hua.Todo.Application/DynamicApi/DynamicApiExtensions.cs b/src/Hua.Todo.Application/DynamicApi/DynamicApiExtensions.cs index 766d26d..40cbc90 100644 --- a/src/Hua.Todo.Application/DynamicApi/DynamicApiExtensions.cs +++ b/src/Hua.Todo.Application/DynamicApi/DynamicApiExtensions.cs @@ -2,8 +2,16 @@ using Microsoft.AspNetCore.Builder; namespace Hua.Todo.Application.DynamicApi; +/// +/// Dynamic API 中间件扩展方法。 +/// public static class DynamicApiExtensions { + /// + /// 注册 Dynamic API 中间件。 + /// + /// 应用构建器。 + /// 应用构建器。 public static IApplicationBuilder UseDynamicApi(this IApplicationBuilder builder) { return builder.UseMiddleware(); diff --git a/src/Hua.Todo.Application/DynamicApi/DynamicApiMiddleware.cs b/src/Hua.Todo.Application/DynamicApi/DynamicApiMiddleware.cs index 0e91834..3adf2b3 100644 --- a/src/Hua.Todo.Application/DynamicApi/DynamicApiMiddleware.cs +++ b/src/Hua.Todo.Application/DynamicApi/DynamicApiMiddleware.cs @@ -6,6 +6,10 @@ using Hua.Todo.Application.Interfaces; namespace Hua.Todo.Application.DynamicApi; +/// +/// Dynamic API 中间件。 +/// 根据路由约定将 的接口方法映射为 HTTP API(/api/{service}/...)。 +/// public class DynamicApiMiddleware { private readonly RequestDelegate _next; @@ -44,12 +48,21 @@ public class DynamicApiMiddleware } } + /// + /// 创建 。 + /// + /// 管道中的下一个中间件。 + /// 应用根服务容器。 public DynamicApiMiddleware(RequestDelegate next, IServiceProvider serviceProvider) { _next = next; _serviceProvider = serviceProvider; } + /// + /// 处理当前请求并尝试进行 Dynamic API 分发。 + /// + /// HTTP 上下文。 public async Task InvokeAsync(HttpContext context) { var path = context.Request.Path.Value ?? string.Empty; diff --git a/src/Hua.Todo.Application/DynamicApi/HttpAttributes.cs b/src/Hua.Todo.Application/DynamicApi/HttpAttributes.cs index e7ee1e1..b9784d1 100644 --- a/src/Hua.Todo.Application/DynamicApi/HttpAttributes.cs +++ b/src/Hua.Todo.Application/DynamicApi/HttpAttributes.cs @@ -1,14 +1,27 @@ namespace Hua.Todo.Application.DynamicApi; [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +/// +/// 标记方法为 HTTP GET。 +/// public class HttpGetAttribute : Attribute { + /// + /// 可选路由模板。 + /// public string? Route { get; set; } + /// + /// 创建 。 + /// public HttpGetAttribute() { } + /// + /// 创建 。 + /// + /// 路由模板。 public HttpGetAttribute(string route) { Route = route; @@ -16,14 +29,27 @@ public class HttpGetAttribute : Attribute } [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +/// +/// 标记方法为 HTTP POST。 +/// public class HttpPostAttribute : Attribute { + /// + /// 可选路由模板。 + /// public string? Route { get; set; } + /// + /// 创建 。 + /// public HttpPostAttribute() { } + /// + /// 创建 。 + /// + /// 路由模板。 public HttpPostAttribute(string route) { Route = route; @@ -31,14 +57,27 @@ public class HttpPostAttribute : Attribute } [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +/// +/// 标记方法为 HTTP PUT。 +/// public class HttpPutAttribute : Attribute { + /// + /// 可选路由模板。 + /// public string? Route { get; set; } + /// + /// 创建 。 + /// public HttpPutAttribute() { } + /// + /// 创建 。 + /// + /// 路由模板。 public HttpPutAttribute(string route) { Route = route; @@ -46,14 +85,27 @@ public class HttpPutAttribute : Attribute } [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +/// +/// 标记方法为 HTTP DELETE。 +/// public class HttpDeleteAttribute : Attribute { + /// + /// 可选路由模板。 + /// public string? Route { get; set; } + /// + /// 创建 。 + /// public HttpDeleteAttribute() { } + /// + /// 创建 。 + /// + /// 路由模板。 public HttpDeleteAttribute(string route) { Route = route; @@ -61,14 +113,27 @@ public class HttpDeleteAttribute : Attribute } [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +/// +/// 标记方法为 HTTP PATCH。 +/// public class HttpPatchAttribute : Attribute { + /// + /// 可选路由模板。 + /// public string? Route { get; set; } + /// + /// 创建 。 + /// public HttpPatchAttribute() { } + /// + /// 创建 。 + /// + /// 路由模板。 public HttpPatchAttribute(string route) { Route = route; diff --git a/src/Hua.Todo.Application/DynamicApi/ParameterBindingAttributes.cs b/src/Hua.Todo.Application/DynamicApi/ParameterBindingAttributes.cs index d752d30..ea72aad 100644 --- a/src/Hua.Todo.Application/DynamicApi/ParameterBindingAttributes.cs +++ b/src/Hua.Todo.Application/DynamicApi/ParameterBindingAttributes.cs @@ -1,11 +1,17 @@ namespace Hua.Todo.Application.DynamicApi; [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] +/// +/// 指示参数从 QueryString 绑定。 +/// public class FromQueryAttribute : Attribute { } [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] +/// +/// 指示参数从 Request Body 绑定。 +/// public class FromBodyAttribute : Attribute { } diff --git a/src/Hua.Todo.Application/DynamicApi/RemoteServiceAttribute.cs b/src/Hua.Todo.Application/DynamicApi/RemoteServiceAttribute.cs index 4f9a49d..4f2595d 100644 --- a/src/Hua.Todo.Application/DynamicApi/RemoteServiceAttribute.cs +++ b/src/Hua.Todo.Application/DynamicApi/RemoteServiceAttribute.cs @@ -1,8 +1,17 @@ namespace Hua.Todo.Application.DynamicApi; [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Method, AllowMultiple = false)] +/// +/// 标记服务/方法是否允许通过 Dynamic API 暴露。 +/// public class RemoteServiceAttribute : Attribute { + /// + /// 是否启用远程访问。 + /// public bool IsEnabled { get; set; } = true; + /// + /// 是否启用元数据输出。 + /// public bool IsMetadataEnabled { get; set; } = true; } diff --git a/src/Hua.Todo.Application/Interfaces/IDynamicApiService.cs b/src/Hua.Todo.Application/Interfaces/IDynamicApiService.cs index b1f989e..5e24545 100644 --- a/src/Hua.Todo.Application/Interfaces/IDynamicApiService.cs +++ b/src/Hua.Todo.Application/Interfaces/IDynamicApiService.cs @@ -1,5 +1,9 @@ namespace Hua.Todo.Application.Interfaces; +/// +/// Dynamic API 服务标记接口。 +/// 实现该接口的服务会被 Dynamic API 中间件发现并按约定暴露为 HTTP API。 +/// public interface IDynamicApiService { } diff --git a/src/Hua.Todo.Application/Interfaces/ITaskService.cs b/src/Hua.Todo.Application/Interfaces/ITaskService.cs index 800a62f..d5df029 100644 --- a/src/Hua.Todo.Application/Interfaces/ITaskService.cs +++ b/src/Hua.Todo.Application/Interfaces/ITaskService.cs @@ -2,15 +2,67 @@ using Hua.Todo.Application.Models; namespace Hua.Todo.Application.Interfaces; +/// +/// 任务管理服务接口 +/// public interface ITaskService : IDynamicApiService { + /// + /// 获取所有任务 + /// + /// 包含所有任务的列表 Task> GetAllTasksAsync(); + + /// + /// 根据 ID 获取任务 + /// + /// 任务唯一标识符 + /// 匹配的任务 DTO,如果未找到则返回 null Task GetTaskByIdAsync(int id); + + /// + /// 获取未完成的任务 + /// + /// 未完成任务的列表 Task> GetActiveTasksAsync(); + + /// + /// 获取已完成的任务 + /// + /// 已完成任务的列表 Task> GetCompletedTasksAsync(); + + /// + /// 创建新任务 + /// + /// 创建任务所需的数据传输对象 + /// 创建成功的任务 DTO Task CreateTaskAsync(CreateTaskDto dto); + + /// + /// 更新任务信息 + /// + /// 包含更新信息的任务数据传输对象 + /// 更新后的任务 DTO Task UpdateTaskAsync(UpdateTaskDto dto); + + /// + /// 切换任务完成状态 + /// + /// 任务唯一标识符 + /// 更新状态后的任务 DTO Task ToggleCompleteAsync(int id); + + /// + /// 删除任务 + /// + /// 要删除的任务唯一标识符 Task DeleteTaskAsync(int id); + + /// + /// 获取子任务列表 + /// + /// 父任务唯一标识符 + /// 该父任务下的所有子任务列表 Task> GetSubTasksAsync(int parentTaskId); } diff --git a/src/Hua.Todo.Application/Models/TaskModels.cs b/src/Hua.Todo.Application/Models/TaskModels.cs index eb8c586..0493a0b 100644 --- a/src/Hua.Todo.Application/Models/TaskModels.cs +++ b/src/Hua.Todo.Application/Models/TaskModels.cs @@ -3,45 +3,112 @@ using System.Text.Json.Serialization; namespace Hua.Todo.Application.Models; +/// +/// 创建任务请求 DTO。 +/// public class CreateTaskDto { + /// + /// 任务标题。 + /// public string Title { get; set; } = string.Empty; [JsonConverter(typeof(JsonStringEnumConverter))] + /// + /// 任务优先级。 + /// public TaskPriority Priority { get; set; } = TaskPriority.Medium; + /// + /// 父任务 ID(用于创建子任务)。 + /// public int? ParentTaskId { get; set; } } +/// +/// 更新任务请求 DTO。 +/// public class UpdateTaskDto { + /// + /// 任务 ID。 + /// public int Id { get; set; } + /// + /// 新标题(可选)。 + /// public string? Title { get; set; } [JsonConverter(typeof(JsonStringEnumConverter))] + /// + /// 新优先级(可选)。 + /// public TaskPriority? Priority { get; set; } } +/// +/// 任务返回 DTO。 +/// public class TaskDto { + /// + /// 任务 ID。 + /// public int Id { get; set; } + /// + /// 任务标题。 + /// public string Title { get; set; } = string.Empty; [JsonConverter(typeof(JsonStringEnumConverter))] + /// + /// 任务优先级。 + /// public TaskPriority Priority { get; set; } + /// + /// 是否已完成。 + /// public bool IsCompleted { get; set; } + /// + /// 创建时间(UTC)。 + /// public DateTime CreatedAt { get; set; } + /// + /// 更新时间(UTC)。 + /// public DateTime UpdatedAt { get; set; } + /// + /// 父任务 ID(可选)。 + /// public int? ParentTaskId { get; set; } + /// + /// 子任务列表。 + /// public List SubTasks { get; set; } = new(); } +/// +/// 通用 API 响应包装。 +/// +/// 数据类型。 public class ApiResponse { + /// + /// 是否成功。 + /// public bool Success { get; set; } + /// + /// 返回数据(可选)。 + /// public T? Data { get; set; } + /// + /// 提示信息。 + /// public string Message { get; set; } = string.Empty; + /// + /// 错误列表。 + /// public List Errors { get; set; } = new(); } diff --git a/src/Hua.Todo.Application/Repositories/TaskRepository.cs b/src/Hua.Todo.Application/Repositories/TaskRepository.cs index 8cc8cfa..b352d99 100644 --- a/src/Hua.Todo.Application/Repositories/TaskRepository.cs +++ b/src/Hua.Todo.Application/Repositories/TaskRepository.cs @@ -5,15 +5,26 @@ using Hua.Todo.Core.Interfaces; namespace Hua.Todo.Application.Repositories; +/// +/// 任务仓储实现(EF Core)。 +/// public class TaskRepository : ITaskRepository { private readonly TodoDbContext _context; + /// + /// 创建 。 + /// + /// 数据库上下文。 public TaskRepository(TodoDbContext context) { _context = context; } + /// + /// 获取所有任务。 + /// + /// 包含所有任务实体的列表。 public async Task> GetAllAsync() { return await _context.Tasks @@ -21,6 +32,11 @@ public class TaskRepository : ITaskRepository .ToListAsync(); } + /// + /// 根据 ID 获取任务。 + /// + /// 任务 ID。 + /// 匹配的任务实体;如果不存在则返回 null。 public async Task GetByIdAsync(int id) { return await _context.Tasks @@ -28,6 +44,10 @@ public class TaskRepository : ITaskRepository .FirstOrDefaultAsync(t => t.Id == id); } + /// + /// 获取未完成任务列表。 + /// + /// 未完成的任务实体列表。 public async Task> GetActiveTasksAsync() { return await _context.Tasks @@ -36,6 +56,10 @@ public class TaskRepository : ITaskRepository .ToListAsync(); } + /// + /// 获取已完成任务列表。 + /// + /// 已完成的任务实体列表。 public async Task> GetCompletedTasksAsync() { return await _context.Tasks @@ -44,6 +68,11 @@ public class TaskRepository : ITaskRepository .ToListAsync(); } + /// + /// 新增一个任务。 + /// + /// 要添加的任务实体。 + /// 已持久化的任务实体(包含生成的 ID)。 public async Task AddAsync(TaskEntity taskEntity) { _context.Tasks.Add(taskEntity); @@ -51,6 +80,11 @@ public class TaskRepository : ITaskRepository return taskEntity; } + /// + /// 更新现有任务信息。 + /// + /// 要更新的任务实体。 + /// 更新后的任务实体。 public async Task UpdateAsync(TaskEntity taskEntity) { taskEntity.UpdatedAt = DateTime.UtcNow; @@ -59,6 +93,11 @@ public class TaskRepository : ITaskRepository return taskEntity; } + /// + /// 根据 ID 删除任务。 + /// + /// 要删除的任务 ID。 + /// 表示删除操作的任务。 public async Task DeleteAsync(int id) { var task = await _context.Tasks.FindAsync(id); @@ -69,6 +108,11 @@ public class TaskRepository : ITaskRepository } } + /// + /// 获取指定父任务的子任务列表。 + /// + /// 父任务 ID。 + /// 子任务实体的列表。 public async Task> GetSubTasksAsync(int parentTaskId) { return await _context.Tasks diff --git a/src/Hua.Todo.Application/ServiceCollectionExtensions.cs b/src/Hua.Todo.Application/ServiceCollectionExtensions.cs index 21ccc79..ef84c59 100644 --- a/src/Hua.Todo.Application/ServiceCollectionExtensions.cs +++ b/src/Hua.Todo.Application/ServiceCollectionExtensions.cs @@ -9,8 +9,17 @@ using ITaskService = Hua.Todo.Application.Interfaces.ITaskService; namespace Hua.Todo.Application; +/// +/// 应用层依赖注入扩展。 +/// public static class ServiceCollectionExtensions { + /// + /// 注册应用层服务与 EF Core DbContext。 + /// + /// 服务集合。 + /// 数据库连接字符串。 + /// 服务集合。 public static IServiceCollection AddApplicationServices(this IServiceCollection services, string connectionString) { services.AddDbContext(options => diff --git a/src/Hua.Todo.Application/Services/TaskService.cs b/src/Hua.Todo.Application/Services/TaskService.cs index adc9315..494a9ea 100644 --- a/src/Hua.Todo.Application/Services/TaskService.cs +++ b/src/Hua.Todo.Application/Services/TaskService.cs @@ -5,39 +5,68 @@ using Hua.Todo.Core.Interfaces; namespace Hua.Todo.Application.Services; +/// +/// 任务管理服务实现 +/// public class TaskService : ITaskService { private readonly ITaskRepository _taskRepository; + /// + /// 初始化任务管理服务的新实例 + /// + /// 任务仓储接口 public TaskService(ITaskRepository taskRepository) { _taskRepository = taskRepository; } + /// + /// 获取所有任务 + /// + /// 所有任务的 DTO 列表 public async Task> GetAllTasksAsync() { var tasks = await _taskRepository.GetAllAsync(); return tasks.Select(MapToDto).ToList(); } + /// + /// 根据 ID 获取任务详情 + /// + /// 任务 ID + /// 找到的任务 DTO,如果不存在则返回 null public async Task GetTaskByIdAsync(int id) { var task = await _taskRepository.GetByIdAsync(id); return task != null ? MapToDto(task) : null; } + /// + /// 获取所有未完成的任务 + /// + /// 未完成任务的 DTO 列表 public async Task> GetActiveTasksAsync() { var allTasks = await _taskRepository.GetAllAsync(); return allTasks.Where(t => !t.IsCompleted).Select(MapToDto).ToList(); } + /// + /// 获取所有已完成的任务 + /// + /// 已完成任务的 DTO 列表 public async Task> GetCompletedTasksAsync() { var allTasks = await _taskRepository.GetAllAsync(); return allTasks.Where(t => t.IsCompleted).Select(MapToDto).ToList(); } + /// + /// 创建新任务 + /// + /// 任务创建 DTO + /// 新创建的任务 DTO public async Task CreateTaskAsync(CreateTaskDto dto) { var task = new TaskEntity @@ -54,6 +83,12 @@ public class TaskService : ITaskService return MapToDto(createdTask); } + /// + /// 更新现有任务 + /// + /// 包含更新内容的 DTO + /// 更新后的任务 DTO + /// 当任务 ID 不存在时抛出 public async Task UpdateTaskAsync(UpdateTaskDto dto) { var task = await _taskRepository.GetByIdAsync(dto.Id); @@ -78,6 +113,12 @@ public class TaskService : ITaskService return MapToDto(updatedTask); } + /// + /// 切换任务的完成状态 + /// + /// 任务 ID + /// 状态切换后的任务 DTO + /// 当任务 ID 不存在时抛出 public async Task ToggleCompleteAsync(int id) { var task = await _taskRepository.GetByIdAsync(id); @@ -93,6 +134,11 @@ public class TaskService : ITaskService return MapToDto(updatedTask); } + /// + /// 删除指定 ID 的任务 + /// + /// 任务 ID + /// 当任务 ID 不存在时抛出 public async Task DeleteTaskAsync(int id) { var task = await _taskRepository.GetByIdAsync(id); @@ -104,12 +150,20 @@ public class TaskService : ITaskService await _taskRepository.DeleteAsync(id); } + /// + /// 获取指定父任务的所有子任务 + /// + /// 父任务 ID + /// 子任务的 DTO 列表 public async Task> GetSubTasksAsync(int parentTaskId) { var allTasks = await _taskRepository.GetAllAsync(); return allTasks.Where(t => t.ParentTaskId == parentTaskId).Select(MapToDto).ToList(); } + /// + /// 实体转 DTO 映射方法 + /// private TaskDto MapToDto(TaskEntity task) { return new TaskDto diff --git a/src/Hua.Todo.Core/Interfaces/ITaskRepository.cs b/src/Hua.Todo.Core/Interfaces/ITaskRepository.cs index a158d70..7dd1257 100644 --- a/src/Hua.Todo.Core/Interfaces/ITaskRepository.cs +++ b/src/Hua.Todo.Core/Interfaces/ITaskRepository.cs @@ -10,52 +10,53 @@ public interface ITaskRepository /// /// 获取所有任务 /// - /// 任务列表 + /// 包含所有任务实体的列表任务 System.Threading.Tasks.Task> GetAllAsync(); /// /// 根据ID获取指定任务 /// - /// 任务ID - /// 任务对象,如果不存在则返回null + /// 任务唯一标识符 + /// 任务实体对象,如果不存在则返回 null 的任务 System.Threading.Tasks.Task GetByIdAsync(int id); /// /// 获取所有未完成的任务 /// - /// 未完成任务列表 + /// 未完成任务实体的列表任务 System.Threading.Tasks.Task> GetActiveTasksAsync(); /// /// 获取所有已完成的任务 /// - /// 已完成任务列表 + /// 已完成任务实体的列表任务 System.Threading.Tasks.Task> GetCompletedTasksAsync(); /// /// 添加新任务 /// - /// 要添加的任务对象 - /// 添加后的任务对象(包含生成的ID) + /// 要添加的任务实体对象 + /// 添加后的任务实体(包含生成的 ID)的任务 System.Threading.Tasks.Task AddAsync(TaskEntity taskEntity); /// /// 更新任务 /// - /// 要更新的任务对象 - /// 更新后的任务对象 + /// 要更新的任务实体对象 + /// 更新后的任务实体对象的任务 System.Threading.Tasks.Task UpdateAsync(TaskEntity taskEntity); /// /// 删除指定ID的任务 /// - /// 任务ID + /// 要删除的任务唯一标识符 + /// 表示删除操作的任务 System.Threading.Tasks.Task DeleteAsync(int id); /// /// 获取指定父任务的所有子任务 /// - /// 父任务ID - /// 子任务列表 + /// 父任务唯一标识符 + /// 子任务实体的列表任务 System.Threading.Tasks.Task> GetSubTasksAsync(int parentTaskId); } diff --git a/src/Hua.Todo.Maui/App.NonWindows.cs b/src/Hua.Todo.Maui/App.NonWindows.cs new file mode 100644 index 0000000..50d9dbf --- /dev/null +++ b/src/Hua.Todo.Maui/App.NonWindows.cs @@ -0,0 +1,32 @@ +#if !WINDOWS +using Microsoft.Maui.Controls; + +namespace Hua.Todo.Maui; + +/// +/// 非 Windows 平台下 的默认实现(分部类)。 +/// 用于提供 Windows 专属分部方法的默认行为,避免在其它平台出现缺失实现的编译错误。 +/// +public partial class App +{ + /// + /// 非 Windows 平台不依赖 WinUI 句柄,因此视为已就绪。 + /// + /// MAUI Window。 + /// 始终返回 true。 + private partial bool IsPlatformViewReady(Window window) + { + return true; + } + + /// + /// 非 Windows 平台不处理“恢复/激活窗口”等特定逻辑,由调用方走默认 OpenWindow 流程。 + /// + /// MAUI Window。 + /// 始终返回 false。 + private partial bool PlatformShowMainWindow(Window window) + { + return false; + } +} +#endif diff --git a/src/Hua.Todo.Maui/App.xaml.cs b/src/Hua.Todo.Maui/App.xaml.cs index cb3588d..b3fcabd 100644 --- a/src/Hua.Todo.Maui/App.xaml.cs +++ b/src/Hua.Todo.Maui/App.xaml.cs @@ -4,15 +4,13 @@ using System.IO; using Hua.Todo.Maui.Services; using Hua.Todo.Maui.Views; using Hua.Todo.Maui.Models; -#if WINDOWS -using System.Runtime.InteropServices; -using Windowing = Microsoft.UI.Windowing; -using WinUiWindow = Microsoft.UI.Xaml.Window; -using WinRT.Interop; -#endif namespace Hua.Todo.Maui; +/// +/// 应用程序主入口类。 +/// 该类仅包含跨平台通用逻辑;各平台差异通过分部类拆分到 Platforms 目录中,避免在同一文件内混写大量条件编译代码。 +/// public partial class App : global::Microsoft.Maui.Controls.Application { private readonly IServiceProvider _serviceProvider; @@ -23,6 +21,10 @@ public partial class App : global::Microsoft.Maui.Controls.Application private bool _isHotkeyRegistered; private bool _isWindowCentered; + /// + /// 创建 实例。 + /// + /// 依赖注入容器。 public App(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; @@ -33,7 +35,10 @@ public partial class App : global::Microsoft.Maui.Controls.Application _trayService = serviceProvider.GetRequiredService(); } -protected override Microsoft.Maui.Controls.Window CreateWindow(IActivationState? activationState) + /// + /// 创建主窗体,并绑定系统托盘与热键等生命周期逻辑。 + /// + protected override Microsoft.Maui.Controls.Window CreateWindow(IActivationState? activationState) { _mainWindow = new Microsoft.Maui.Controls.Window(_serviceProvider.GetRequiredService()) { @@ -42,9 +47,8 @@ protected override Microsoft.Maui.Controls.Window CreateWindow(IActivationState? Title = AppMetadata.GetWindowTitle() }; -#if WINDOWS - _mainWindow.TitleBar = CreateWindowTitleBar(); -#endif + // 平台差异通过分部类实现(例如 Windows 标题栏、窗口显示方式等)。 + InitializePlatform(); _mainWindow.Destroying += (s, e) => { @@ -58,49 +62,42 @@ protected override Microsoft.Maui.Controls.Window CreateWindow(IActivationState? _mainWindow.Created += (s, e) => { + // 系统托盘初始化需要持有 Window 引用,并提供“显示主窗体/退出应用”的回调。 _trayService.Initialize(_mainWindow, ShowMainWindow, ExitApplication); RegisterHotkeyWhenReady(); -#if WINDOWS - MainThread.BeginInvokeOnMainThread(() => - { - if (_mainWindow.Handler?.PlatformView is WinUiWindow platformWindow) - { - // Ensure app doesn't shutdown when main window closes (we hide it) - platformWindow.AppWindow.Closing += (sender, args) => - { - args.Cancel = true; - new Hua.Todo.Maui.Platforms.Windows.WindowsWindowService().HideWindow(_mainWindow); - }; - - CenterMainWindow(platformWindow); - ConfigureWindowsTitleBar(platformWindow); - } - }); -#endif + // Window 已创建但 Handler 可能尚未完全就绪,平台侧可在此做进一步处理(例如订阅关闭事件、居中等)。 + OnPlatformWindowCreated(_mainWindow); }; return _mainWindow; } + /// + /// 当平台视图准备就绪时注册热键。 + /// 在某些平台(尤其 Windows)中,窗口 Handler 创建具有延迟,过早注册可能失败,因此采用重试策略。 + /// + /// 当前重试次数。 private void RegisterHotkeyWhenReady(int attempt = 0) { if (_mainWindow == null) return; -#if WINDOWS - if (_mainWindow.Handler?.PlatformView is not WinUiWindow) + if (!IsPlatformViewReady(_mainWindow)) { if (attempt < 30) { + // 使用短间隔重试,避免阻塞 UI 线程,同时保证窗口句柄就绪后尽快完成注册。 _mainWindow.Dispatcher.DispatchDelayed(TimeSpan.FromMilliseconds(100), () => RegisterHotkeyWhenReady(attempt + 1)); } return; } -#endif RegisterHotkey(); } + /// + /// 热键按下时的回调 + /// private void OnHotKeyPressed() { MainThread.BeginInvokeOnMainThread(() => @@ -109,36 +106,41 @@ protected override Microsoft.Maui.Controls.Window CreateWindow(IActivationState? }); } + /// + /// 显示主窗口。 + /// 优先使用平台特定逻辑(例如 Windows 恢复并激活窗口),否则走默认 MAUI 打开窗口流程。 + /// private void ShowMainWindow() { if (_mainWindow != null) { _mainWindow.Dispatcher.Dispatch(() => - { -#if WINDOWS - if (_mainWindow.Handler != null) { - new Hua.Todo.Maui.Platforms.Windows.WindowsWindowService().RestoreWindow(_mainWindow); - var platformWindow = _mainWindow.Handler.PlatformView as WinUiWindow; - platformWindow?.Activate(); - } -#else - if (global::Microsoft.Maui.Controls.Application.Current != null && - !global::Microsoft.Maui.Controls.Application.Current.Windows.Contains(_mainWindow)) - { - global::Microsoft.Maui.Controls.Application.Current.OpenWindow(_mainWindow); - } -#endif - }); + if (!PlatformShowMainWindow(_mainWindow)) + { + // 默认显示逻辑 + if (global::Microsoft.Maui.Controls.Application.Current != null && + !global::Microsoft.Maui.Controls.Application.Current.Windows.Contains(_mainWindow)) + { + global::Microsoft.Maui.Controls.Application.Current.OpenWindow(_mainWindow); + } + } + }); } } + /// + /// 退出应用程序 + /// private void ExitApplication() { _trayService?.Dispose(); global::Microsoft.Maui.Controls.Application.Current?.Quit(); } + /// + /// 注册全局热键,并监听配置变更以动态更新。 + /// private void RegisterHotkey() { if (_hotKeyService == null || _settingsService == null) return; @@ -169,125 +171,19 @@ protected override Microsoft.Maui.Controls.Window CreateWindow(IActivationState? } } -#if WINDOWS - private void ConfigureWindowsTitleBar(WinUiWindow platformWindow) - { - var title = AppMetadata.GetWindowTitle(); - - platformWindow.Title = title; - if (_mainWindow != null) - { - _mainWindow.Title = title; - } - - var appWindow = platformWindow.AppWindow; - if (appWindow != null) - { - appWindow.Title = title; - - var hWnd = WindowNative.GetWindowHandle(platformWindow); - if (hWnd != IntPtr.Zero) - { - SetWindowText(hWnd, title); - } - - var iconPath = Path.Combine(AppContext.BaseDirectory, "icon.ico"); - if (File.Exists(iconPath)) - { - appWindow.SetIcon(iconPath); - } - - var titleBar = appWindow.TitleBar; - titleBar.IconShowOptions = Windowing.IconShowOptions.ShowIconAndSystemMenu; - - titleBar.BackgroundColor = Microsoft.UI.Colors.Transparent; - titleBar.InactiveBackgroundColor = Microsoft.UI.Colors.Transparent; - titleBar.ButtonBackgroundColor = Microsoft.UI.Colors.Transparent; - titleBar.ButtonInactiveBackgroundColor = Microsoft.UI.Colors.Transparent; - titleBar.ButtonHoverBackgroundColor = Microsoft.UI.Colors.Transparent; - titleBar.ButtonPressedBackgroundColor = Microsoft.UI.Colors.Transparent; - titleBar.ButtonForegroundColor = Microsoft.UI.Colors.Black; - titleBar.ButtonInactiveForegroundColor = Microsoft.UI.Colors.Black; - titleBar.ButtonHoverForegroundColor = Microsoft.UI.Colors.Black; - titleBar.ButtonPressedForegroundColor = Microsoft.UI.Colors.Black; - } - } - - [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "SetWindowTextW")] - private static extern bool SetWindowText(IntPtr hWnd, string lpString); - - private static TitleBar CreateWindowTitleBar() - { - return new TitleBar - { - BackgroundColor = Microsoft.Maui.Graphics.Color.FromArgb("#F5F5F5"), - Icon = string.Empty, - Title = string.Empty, - ForegroundColor = Microsoft.Maui.Graphics.Color.FromArgb("#333333"), - LeadingContent = new HorizontalStackLayout - { - Spacing = 4, - Padding = new Microsoft.Maui.Thickness(10, 0, 0, 0), - VerticalOptions = LayoutOptions.Center, - Children = - { - new Image - { - Source = "icon.jpg", - WidthRequest = 22, - HeightRequest = 22, - VerticalOptions = LayoutOptions.Center - }, - new Label - { - Text = AppMetadata.GetTitleBarVersionText(), - FontFamily = "Microsoft YaHei UI", - TextColor = Microsoft.Maui.Graphics.Color.FromArgb("#333333"), - VerticalTextAlignment = Microsoft.Maui.TextAlignment.Center, - VerticalOptions = LayoutOptions.Center, - FontSize = 14 - } - } - } - }; - } - - private void CenterMainWindow(WinUiWindow platformWindow) - { - if (_isWindowCentered) return; - - var appWindow = platformWindow.AppWindow; - if (appWindow == null) return; - - var displayArea = Windowing.DisplayArea.GetFromWindowId( - appWindow.Id, - Windowing.DisplayAreaFallback.Primary); - - var workArea = displayArea.WorkArea; - - var windowWidthPx = appWindow.Size.Width; - var windowHeightPx = appWindow.Size.Height; - - if (windowWidthPx <= 0 || windowHeightPx <= 0) - { - var scale = platformWindow.Content?.XamlRoot?.RasterizationScale ?? 1.0; - windowWidthPx = windowWidthPx <= 0 ? (int)Math.Round((_mainWindow?.Width ?? 450) * scale) : windowWidthPx; - windowHeightPx = windowHeightPx <= 0 ? (int)Math.Round((_mainWindow?.Height ?? 640) * scale) : windowHeightPx; - } - - if (windowWidthPx <= 0 || windowHeightPx <= 0) return; - - var x = workArea.X + (workArea.Width - windowWidthPx) / 2; - var y = workArea.Y + (workArea.Height - windowHeightPx) / 2; - - if (windowWidthPx >= workArea.Width) x = workArea.X; - if (windowHeightPx >= workArea.Height) y = workArea.Y; - - x = Math.Max(workArea.X, Math.Min(x, workArea.X + workArea.Width - windowWidthPx)); - y = Math.Max(workArea.Y, Math.Min(y, workArea.Y + workArea.Height - windowHeightPx)); - - appWindow.Move(new Windows.Graphics.PointInt32(x, y)); - _isWindowCentered = true; - } -#endif + // 平台特定分部方法声明(实现位于 Platforms/* 目录) + partial void InitializePlatform(); + partial void OnPlatformWindowCreated(Window window); + /// + /// 判断平台视图是否已就绪(用于热键/窗口等依赖平台句柄的逻辑)。 + /// + /// MAUI Window。 + /// 视图已就绪返回 true;否则返回 false。 + private partial bool IsPlatformViewReady(Window window); + /// + /// 使用平台特定方式显示主窗口。 + /// + /// MAUI Window。 + /// 如果平台已处理显示逻辑返回 true;否则返回 false。 + private partial bool PlatformShowMainWindow(Window window); } diff --git a/src/Hua.Todo.Maui/MauiProgram.cs b/src/Hua.Todo.Maui/MauiProgram.cs index bf16cee..e409848 100644 --- a/src/Hua.Todo.Maui/MauiProgram.cs +++ b/src/Hua.Todo.Maui/MauiProgram.cs @@ -10,8 +10,14 @@ using Hua.Todo.Maui.Services.Platforms; namespace Hua.Todo.Maui; +/// +/// MAUI 程序启动类 +/// public static class MauiProgram { + /// + /// 创建并配置 MAUI 应用程序 + /// public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); @@ -23,9 +29,10 @@ public static class MauiProgram fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); }); + // 加载应用程序配置 var appSettings = LoadAppSettings(); - // Set default connection string if not provided + // 如果未提供连接字符串,则设置默认的 SQLite 连接字符串 if (string.IsNullOrEmpty(appSettings.WebServer.ConnectionString)) { var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); @@ -41,12 +48,18 @@ public static class MauiProgram builder.Services.AddSingleton(appSettings); var connectionString = appSettings.WebServer.ConnectionString; + // 注册应用服务 builder.Services.AddApplicationServices(connectionString); builder.Services.AddTransient(); + // 注册热键设置服务 builder.Services.AddSingleton(sp => new HotKeySettingsService(sp.GetRequiredService())); + + // 注册全局热键服务(工厂模式) builder.Services.AddSingleton(sp => GlobalHotKeyServiceFactory.Create()); + + // 注册系统托盘服务(平台相关) builder.Services.AddSingleton(sp => { #if WINDOWS @@ -55,6 +68,8 @@ public static class MauiProgram return new NullSystemTrayService(); #endif }); + + // 注册嵌入式 Web 服务器(平台相关) #if WINDOWS builder.Services.AddSingleton(); #elif ANDROID @@ -69,6 +84,7 @@ public static class MauiProgram var app = builder.Build(); + // 异步初始化数据库和 Web 服务器 _ = Task.Run(async () => { try @@ -85,6 +101,9 @@ public static class MauiProgram return app; } + /// + /// 从 appsettings.json 加载配置 + /// private static AppSettings LoadAppSettings() { try @@ -114,6 +133,9 @@ public static class MauiProgram } } + /// + /// 初始化数据库(执行迁移) + /// private static void InitializeDatabase(IServiceProvider services, string connectionString) { using var scope = services.CreateScope(); @@ -133,7 +155,7 @@ public static class MauiProgram } } - // Ensure WAL mode to avoid locking issues + // 确保使用 WAL 模式以避免锁定问题 dbContext.Database.ExecuteSqlRaw("PRAGMA journal_mode=WAL;"); dbContext.Database.Migrate(); } @@ -152,6 +174,9 @@ public static class MauiProgram } } + /// + /// 启动嵌入式 Web 服务器 + /// private static async Task StartWebServer(IServiceProvider services) { try diff --git a/src/Hua.Todo.Maui/Platforms/Android/MainActivity.cs b/src/Hua.Todo.Maui/Platforms/Android/MainActivity.cs index a3b605f..19247b5 100644 --- a/src/Hua.Todo.Maui/Platforms/Android/MainActivity.cs +++ b/src/Hua.Todo.Maui/Platforms/Android/MainActivity.cs @@ -5,8 +5,15 @@ using Android.OS; namespace Hua.Todo.Maui; [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] +/// +/// Android 平台入口 Activity。 +/// public class MainActivity : MauiAppCompatActivity { + /// + /// Activity 创建时回调。 + /// + /// 上次保存状态。 protected override void OnCreate(Bundle? savedInstanceState) { base.OnCreate(savedInstanceState); diff --git a/src/Hua.Todo.Maui/Platforms/Android/MainApplication.cs b/src/Hua.Todo.Maui/Platforms/Android/MainApplication.cs index 4fad2c3..c536428 100644 --- a/src/Hua.Todo.Maui/Platforms/Android/MainApplication.cs +++ b/src/Hua.Todo.Maui/Platforms/Android/MainApplication.cs @@ -1,15 +1,27 @@ -using Android.App; +using Android.App; using Android.Runtime; namespace Hua.Todo.Maui; [Application] +/// +/// Android 平台 Application。 +/// public class MainApplication : MauiApplication { + /// + /// 创建 。 + /// + /// JNI 句柄。 + /// 句柄所有权。 public MainApplication(IntPtr handle, JniHandleOwnership ownership) : base(handle, ownership) { } + /// + /// 创建 MAUI 应用。 + /// + /// MAUI 应用实例。 protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); } diff --git a/src/Hua.Todo.Maui/Platforms/Android/MobileEmbeddedWebServerService.cs b/src/Hua.Todo.Maui/Platforms/Android/MobileEmbeddedWebServerService.cs index 7fc1aed..5d08f2d 100644 --- a/src/Hua.Todo.Maui/Platforms/Android/MobileEmbeddedWebServerService.cs +++ b/src/Hua.Todo.Maui/Platforms/Android/MobileEmbeddedWebServerService.cs @@ -9,6 +9,10 @@ using Hua.Todo.Maui.Models; namespace Hua.Todo.Maui.Services; +/// +/// Android 平台嵌入式 Web 服务器服务。 +/// 使用 TcpListener 自定义实现简单的 HTTP 服务器,用于离线模式下托管 API 和静态资源。 +/// public sealed class MobileEmbeddedWebServerService : IEmbeddedWebServerService { private readonly AppSettings _appSettings; @@ -18,15 +22,34 @@ public sealed class MobileEmbeddedWebServerService : IEmbeddedWebServerService private CancellationTokenSource? _cts; private Task? _acceptLoop; + /// + /// 服务器是否正在运行。 + /// public bool IsRunning => _listener != null; + + /// + /// 服务器基础 URL。 + /// public string BaseUrl => $"http://localhost:{_appSettings.WebServer.Port}"; + /// + /// 初始化 。 + /// + /// 应用程序配置。 + /// 依赖注入服务提供者。 public MobileEmbeddedWebServerService(AppSettings appSettings, IServiceProvider services) { _appSettings = appSettings; _services = services; } + /// + /// 异步启动服务器。 + /// 启动时机:在 Android 平台初始化或手动开启时调用。 + /// 错误处理:启动失败会记录日志,建议外部调用时关注 。 + /// 线程安全:可在任何线程调用;内部通过状态检查避免重复启动。 + /// + /// 表示启动操作的任务。 public Task StartAsync() { if (_listener != null) return Task.CompletedTask; @@ -39,6 +62,11 @@ public sealed class MobileEmbeddedWebServerService : IEmbeddedWebServerService return Task.CompletedTask; } + /// + /// 异步停止服务器。 + /// 释放所有资源并停止监听。 + /// + /// 表示停止操作的任务。 public async Task StopAsync() { if (_listener == null) return; diff --git a/src/Hua.Todo.Maui/Platforms/Windows/App.Windows.cs b/src/Hua.Todo.Maui/Platforms/Windows/App.Windows.cs new file mode 100644 index 0000000..7d644e5 --- /dev/null +++ b/src/Hua.Todo.Maui/Platforms/Windows/App.Windows.cs @@ -0,0 +1,212 @@ +using Microsoft.Maui.Controls; +using Hua.Todo.Maui.Services; +using System.Runtime.InteropServices; +using Windowing = Microsoft.UI.Windowing; +using WinUiWindow = Microsoft.UI.Xaml.Window; +using WinRT.Interop; + +namespace Hua.Todo.Maui; + +/// +/// Windows 平台特定的 App 逻辑(分部类)。 +/// 该文件仅在 Windows 目标框架下参与编译,用于承载窗口句柄相关能力与 WinUI API 调用。 +/// +public partial class App +{ + /// + /// 初始化 Windows 平台特定设置。 + /// 例如:自定义 MAUI TitleBar(不是 WinUI TitleBar)。 + /// + partial void InitializePlatform() + { + if (_mainWindow != null) + { + _mainWindow.TitleBar = CreateWindowTitleBar(); + } + } + + /// + /// Windows 平台窗口创建后的处理。 + /// 该回调发生在 Window Created 之后,但依然可能需要等待 Handler/PlatformView 就绪,因此使用 UI 线程调度。 + /// + /// MAUI Window。 + partial void OnPlatformWindowCreated(Window window) + { + MainThread.BeginInvokeOnMainThread(() => + { + if (window.Handler?.PlatformView is WinUiWindow platformWindow) + { + // 关闭按钮行为:拦截关闭并隐藏到系统托盘,避免进程退出。 + // 注意:该逻辑与系统托盘功能配套,托盘菜单提供“退出应用”入口。 + platformWindow.AppWindow.Closing += (sender, args) => + { + args.Cancel = true; + new Hua.Todo.Maui.Platforms.Windows.WindowsWindowService().HideWindow(window); + }; + + CenterMainWindow(platformWindow); + ConfigureWindowsTitleBar(platformWindow); + } + }); + } + + /// + /// 检查 Windows 平台视图是否准备就绪 + /// + /// MAUI Window。 + /// 当 PlatformView 是 WinUI Window 时返回 true。 + private partial bool IsPlatformViewReady(Window window) + { + return window.Handler?.PlatformView is WinUiWindow; + } + + /// + /// 在 Windows 平台上显示主窗口 + /// + /// MAUI Window。 + /// 已恢复并激活窗口返回 true;否则返回 false。 + private partial bool PlatformShowMainWindow(Window window) + { + if (window.Handler != null) + { + new Hua.Todo.Maui.Platforms.Windows.WindowsWindowService().RestoreWindow(window); + var platformWindow = window.Handler.PlatformView as WinUiWindow; + platformWindow?.Activate(); + return true; + } + return false; + } + + /// + /// 配置 Windows 标题栏(WinUI AppWindow.TitleBar)。 + /// 用于设置窗口标题、图标以及标题栏按钮样式等。 + /// + /// WinUI Window。 + private void ConfigureWindowsTitleBar(WinUiWindow platformWindow) + { + var title = AppMetadata.GetWindowTitle(); + + platformWindow.Title = title; + if (_mainWindow != null) + { + _mainWindow.Title = title; + } + + var appWindow = platformWindow.AppWindow; + if (appWindow != null) + { + appWindow.Title = title; + + var hWnd = WindowNative.GetWindowHandle(platformWindow); + if (hWnd != IntPtr.Zero) + { + SetWindowText(hWnd, title); + } + + var iconPath = Path.Combine(AppContext.BaseDirectory, "icon.ico"); + if (File.Exists(iconPath)) + { + appWindow.SetIcon(iconPath); + } + + var titleBar = appWindow.TitleBar; + titleBar.IconShowOptions = Windowing.IconShowOptions.ShowIconAndSystemMenu; + + titleBar.BackgroundColor = Microsoft.UI.Colors.Transparent; + titleBar.InactiveBackgroundColor = Microsoft.UI.Colors.Transparent; + titleBar.ButtonBackgroundColor = Microsoft.UI.Colors.Transparent; + titleBar.ButtonInactiveBackgroundColor = Microsoft.UI.Colors.Transparent; + titleBar.ButtonHoverBackgroundColor = Microsoft.UI.Colors.Transparent; + titleBar.ButtonPressedBackgroundColor = Microsoft.UI.Colors.Transparent; + titleBar.ButtonForegroundColor = Microsoft.UI.Colors.Black; + titleBar.ButtonInactiveForegroundColor = Microsoft.UI.Colors.Black; + titleBar.ButtonHoverForegroundColor = Microsoft.UI.Colors.Black; + titleBar.ButtonPressedForegroundColor = Microsoft.UI.Colors.Black; + } + } + + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "SetWindowTextW")] + private static extern bool SetWindowText(IntPtr hWnd, string lpString); + + /// + /// 创建 MAUI 自定义标题栏内容。 + /// 用于在窗口标题栏区域展示应用图标与版本信息。 + /// + private static TitleBar CreateWindowTitleBar() + { + return new TitleBar + { + BackgroundColor = Microsoft.Maui.Graphics.Color.FromArgb("#F5F5F5"), + Icon = string.Empty, + Title = string.Empty, + ForegroundColor = Microsoft.Maui.Graphics.Color.FromArgb("#333333"), + LeadingContent = new HorizontalStackLayout + { + Spacing = 4, + Padding = new Microsoft.Maui.Thickness(10, 0, 0, 0), + VerticalOptions = LayoutOptions.Center, + Children = + { + new Image + { + Source = "icon.jpg", + WidthRequest = 22, + HeightRequest = 22, + VerticalOptions = LayoutOptions.Center + }, + new Label + { + Text = AppMetadata.GetTitleBarVersionText(), + FontFamily = "Microsoft YaHei UI", + TextColor = Microsoft.Maui.Graphics.Color.FromArgb("#333333"), + VerticalTextAlignment = Microsoft.Maui.TextAlignment.Center, + VerticalOptions = LayoutOptions.Center, + FontSize = 14 + } + } + } + }; + } + + /// + /// 居中显示主窗口 + /// + /// WinUI Window。 + private void CenterMainWindow(WinUiWindow platformWindow) + { + if (_isWindowCentered) return; + + var appWindow = platformWindow.AppWindow; + if (appWindow == null) return; + + var displayArea = Windowing.DisplayArea.GetFromWindowId( + appWindow.Id, + Windowing.DisplayAreaFallback.Primary); + + var workArea = displayArea.WorkArea; + + var windowWidthPx = appWindow.Size.Width; + var windowHeightPx = appWindow.Size.Height; + + if (windowWidthPx <= 0 || windowHeightPx <= 0) + { + var scale = platformWindow.Content?.XamlRoot?.RasterizationScale ?? 1.0; + windowWidthPx = windowWidthPx <= 0 ? (int)Math.Round((_mainWindow?.Width ?? 450) * scale) : windowWidthPx; + windowHeightPx = windowHeightPx <= 0 ? (int)Math.Round((_mainWindow?.Height ?? 640) * scale) : windowHeightPx; + } + + if (windowWidthPx <= 0 || windowHeightPx <= 0) return; + + var x = workArea.X + (workArea.Width - windowWidthPx) / 2; + var y = workArea.Y + (workArea.Height - windowHeightPx) / 2; + + if (windowWidthPx >= workArea.Width) x = workArea.X; + if (windowHeightPx >= workArea.Height) y = workArea.Y; + + x = Math.Max(workArea.X, Math.Min(x, workArea.X + workArea.Width - windowWidthPx)); + y = Math.Max(workArea.Y, Math.Min(y, workArea.Y + workArea.Height - windowHeightPx)); + + appWindow.Move(new Windows.Graphics.PointInt32(x, y)); + _isWindowCentered = true; + } +} diff --git a/src/Hua.Todo.Maui/Platforms/Windows/MainPage.Windows.cs b/src/Hua.Todo.Maui/Platforms/Windows/MainPage.Windows.cs new file mode 100644 index 0000000..0242894 --- /dev/null +++ b/src/Hua.Todo.Maui/Platforms/Windows/MainPage.Windows.cs @@ -0,0 +1,34 @@ +using Microsoft.Maui.Controls; + +namespace Hua.Todo.Maui.Views +{ + /// + /// Windows 平台特定的 MainPage 逻辑(分部类)。 + /// 该文件仅在 Windows 目标框架下参与编译,用于接入 Windows 全局键盘事件等能力。 + /// + public partial class MainPage + { + private Platforms.Windows.WindowsKeyboardHandler? _keyboardHandler; + + /// + /// Windows 平台特定的键盘处理器设置。 + /// 当前仅监听 Esc 键,用于快速最小化窗口(与桌面端交互习惯保持一致)。 + /// + partial void PlatformSetupKeyboardHandler() + { + _keyboardHandler = new Platforms.Windows.WindowsKeyboardHandler(); + _keyboardHandler.EscKeyPressed += OnEscKeyPressed; + _keyboardHandler.Start(); + } + + /// + /// Windows 平台特定的 Esc 键处理(最小化窗口)。 + /// + /// 当前窗口。 + partial void PlatformOnEscKeyPressed(Window window) + { + var windowService = new Platforms.Windows.WindowsWindowService(); + windowService.MinimizeWindow(window); + } + } +} diff --git a/src/Hua.Todo.Maui/Platforms/Windows/WindowsKeyboardHandler.cs b/src/Hua.Todo.Maui/Platforms/Windows/WindowsKeyboardHandler.cs index d568553..90c2358 100644 --- a/src/Hua.Todo.Maui/Platforms/Windows/WindowsKeyboardHandler.cs +++ b/src/Hua.Todo.Maui/Platforms/Windows/WindowsKeyboardHandler.cs @@ -2,19 +2,32 @@ using System.Runtime.InteropServices; namespace Hua.Todo.Maui.Platforms.Windows { + /// + /// Windows 全局键盘事件处理器。 + /// 用于监听按键并向上层暴露事件(例如 Esc)。 + /// public class WindowsKeyboardHandler : IDisposable { private KeyboardHook _keyboardHook; private bool _isDisposed; + /// + /// 当检测到 Esc 键抬起时触发。 + /// public event EventHandler? EscKeyPressed; + /// + /// 创建 。 + /// public WindowsKeyboardHandler() { _keyboardHook = new KeyboardHook(); _keyboardHook.KeyPressed += OnKeyPressed; } + /// + /// 开始监听键盘事件。 + /// public void Start() { _keyboardHook.Hook(); @@ -28,6 +41,9 @@ namespace Hua.Todo.Maui.Platforms.Windows } } + /// + /// 停止监听并释放资源。 + /// public void Dispose() { if (!_isDisposed) @@ -126,4 +142,4 @@ namespace Hua.Todo.Maui.Platforms.Windows IsKeyDown = isKeyDown; } } -} \ No newline at end of file +} diff --git a/src/Hua.Todo.Maui/Platforms/Windows/WindowsWindowService.cs b/src/Hua.Todo.Maui/Platforms/Windows/WindowsWindowService.cs index 34053e2..a518167 100644 --- a/src/Hua.Todo.Maui/Platforms/Windows/WindowsWindowService.cs +++ b/src/Hua.Todo.Maui/Platforms/Windows/WindowsWindowService.cs @@ -6,6 +6,9 @@ using WinRT.Interop; namespace Hua.Todo.Maui.Platforms.Windows { + /// + /// Windows 平台窗口操作服务。 + /// public class WindowsWindowService { private const int SW_HIDE = 0; @@ -15,6 +18,10 @@ namespace Hua.Todo.Maui.Platforms.Windows [DllImport("user32.dll", SetLastError = true)] private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + /// + /// 隐藏窗口(不退出进程)。 + /// + /// MAUI Window。 public void HideWindow(Window window) { if (window == null) return; @@ -29,6 +36,10 @@ namespace Hua.Todo.Maui.Platforms.Windows ShowWindow(hWnd, SW_HIDE); } + /// + /// 恢复窗口并显示。 + /// + /// MAUI Window。 public void RestoreWindow(Window window) { if (window == null) return; @@ -44,6 +55,10 @@ namespace Hua.Todo.Maui.Platforms.Windows ShowWindow(hWnd, SW_RESTORE); } + /// + /// 最小化窗口。 + /// + /// MAUI Window。 public void MinimizeWindow(Window window) { if (window == null) return; diff --git a/src/Hua.Todo.Maui/Services/EmbeddedWebServerService.cs b/src/Hua.Todo.Maui/Services/EmbeddedWebServerService.cs index e1d1eef..c192dba 100644 --- a/src/Hua.Todo.Maui/Services/EmbeddedWebServerService.cs +++ b/src/Hua.Todo.Maui/Services/EmbeddedWebServerService.cs @@ -13,19 +13,41 @@ using AppSettings = Hua.Todo.Maui.Models.AppSettings; namespace Hua.Todo.Maui.Services; +/// +/// Windows 平台嵌入式 Web 服务器实现 +/// 使用 ASP.NET Core 运行 +/// public class EmbeddedWebServerService : IEmbeddedWebServerService { private WebApplication? _webApp; private readonly AppSettings _appSettings; + /// + /// 服务器是否正在运行 + /// public bool IsRunning => _webApp != null; + + /// + /// 服务器基础 URL + /// public string BaseUrl => _appSettings.WebServer.HostUrl; + /// + /// 初始化嵌入式 Web 服务器服务 + /// + /// 应用程序配置 public EmbeddedWebServerService(AppSettings appSettings) { _appSettings = appSettings; } + /// + /// 异步启动服务器。 + /// 启动时机:通常在应用启动或用户手动开启服务时调用。 + /// 错误处理:启动失败会抛出 ASP.NET Core 相关异常,建议在调用方进行 catch 处理。 + /// 线程安全:非 UI 线程相关,可在后台线程调用;方法内部已处理重入(若已运行则直接返回)。 + /// + /// 表示启动操作的任务 public async Task StartAsync() { if (_webApp != null) return; @@ -34,6 +56,7 @@ public class EmbeddedWebServerService : IEmbeddedWebServerService builder.WebHost.UseUrls(_appSettings.WebServer.HostUrl); + // 配置控制器和 JSON 选项 builder.Services.AddControllers() .AddApplicationPart(typeof(Hua.Todo.Application.ServiceCollectionExtensions).Assembly) .AddJsonOptions(options => @@ -43,9 +66,10 @@ public class EmbeddedWebServerService : IEmbeddedWebServerService builder.Services.AddEndpointsApiExplorer(); - + // 注册应用逻辑服务 builder.Services.AddApplicationServices(_appSettings.WebServer.ConnectionString); + // 配置跨域策略 builder.Services.AddCors(options => { options.AddPolicy("AllowAll", policy => @@ -58,6 +82,7 @@ public class EmbeddedWebServerService : IEmbeddedWebServerService var app = builder.Build(); + // 如果配置为使用静态文件(前端托管),则配置静态文件服务 if (_appSettings.WebServer.IsUsingStatic) { ServeStaticFiles(app); @@ -73,6 +98,10 @@ public class EmbeddedWebServerService : IEmbeddedWebServerService await _webApp.StartAsync(); } + /// + /// 配置静态文件服务(用于托管 Vue 前端) + /// + /// Web 应用程序实例 private void ServeStaticFiles(WebApplication app) { try @@ -100,6 +129,7 @@ public class EmbeddedWebServerService : IEmbeddedWebServerService }; app.UseStaticFiles(staticFileOptions); + // 处理 SPA 路由 app.Use(async (context, next) => { if (context.Request.Path.HasValue) @@ -125,6 +155,11 @@ public class EmbeddedWebServerService : IEmbeddedWebServerService } } + /// + /// 异步停止服务器。 + /// 释放服务器资源并停止监听。 + /// + /// 表示停止操作的任务 public async Task StopAsync() { if (_webApp == null) return; diff --git a/src/Hua.Todo.Maui/Services/HotKeySettingsService.cs b/src/Hua.Todo.Maui/Services/HotKeySettingsService.cs index e9c7db1..8f10601 100644 --- a/src/Hua.Todo.Maui/Services/HotKeySettingsService.cs +++ b/src/Hua.Todo.Maui/Services/HotKeySettingsService.cs @@ -4,13 +4,30 @@ using Hua.Todo.Maui.Models; namespace Hua.Todo.Maui.Services { + /// + /// 热键设置服务接口 + /// public interface IHotKeySettingsService { + /// + /// 获取热键配置 + /// HotKeyConfig GetConfig(); + + /// + /// 保存热键配置 + /// void SaveConfig(HotKeyConfig config); + + /// + /// 重置为默认配置 + /// void ResetToDefault(); } + /// + /// 热键设置服务实现,使用 Preferences 存储配置 + /// public class HotKeySettingsService : IHotKeySettingsService { private const string SettingsKey = "HotKeyConfig"; @@ -21,6 +38,9 @@ namespace Hua.Todo.Maui.Services _appSettings = appSettings; } + /// + /// 获取当前热键配置,如果不存在则返回默认配置 + /// public HotKeyConfig GetConfig() { var json = Preferences.Get(SettingsKey, string.Empty); @@ -39,18 +59,27 @@ namespace Hua.Todo.Maui.Services } } + /// + /// 将热键配置序列化并保存到 Preferences + /// public void SaveConfig(HotKeyConfig config) { var json = JsonSerializer.Serialize(config); Preferences.Set(SettingsKey, json); } + /// + /// 重置为从 appsettings.json 中读取的默认值 + /// public void ResetToDefault() { var defaultConfig = GetDefaultConfig(); SaveConfig(defaultConfig); } + /// + /// 获取默认热键配置 + /// private HotKeyConfig GetDefaultConfig() { return new HotKeyConfig diff --git a/src/Hua.Todo.Maui/Services/IEmbeddedWebServerService.cs b/src/Hua.Todo.Maui/Services/IEmbeddedWebServerService.cs index bd8e32b..f5fa3e1 100644 --- a/src/Hua.Todo.Maui/Services/IEmbeddedWebServerService.cs +++ b/src/Hua.Todo.Maui/Services/IEmbeddedWebServerService.cs @@ -1,9 +1,27 @@ namespace Hua.Todo.Maui.Services; +/// +/// 嵌入式 Web 服务器服务接口 +/// public interface IEmbeddedWebServerService { + /// + /// 服务器是否正在运行 + /// bool IsRunning { get; } + + /// + /// 服务器基础 URL + /// string BaseUrl { get; } + + /// + /// 启动服务器 + /// Task StartAsync(); + + /// + /// 停止服务器 + /// Task StopAsync(); } diff --git a/src/Hua.Todo.Maui/Services/Platforms/WindowsGlobalHotKeyService.cs b/src/Hua.Todo.Maui/Services/Platforms/WindowsGlobalHotKeyService.cs index 6d0406c..6171371 100644 --- a/src/Hua.Todo.Maui/Services/Platforms/WindowsGlobalHotKeyService.cs +++ b/src/Hua.Todo.Maui/Services/Platforms/WindowsGlobalHotKeyService.cs @@ -43,6 +43,9 @@ namespace Hua.Todo.Maui.Services.Platforms /// /// 注册全局热键 /// + /// 修饰键字符串,多个键用逗号分隔(如 "Control,Alt") + /// 主键字符串(如 "X") + /// 热键触发时的回调操作 public void RegisterHotKey(string modifiers, string key, Action callback) { if (_window == null) @@ -97,6 +100,8 @@ namespace Hua.Todo.Maui.Services.Platforms /// /// 更新热键配置 /// + /// 新的修饰键字符串 + /// 新的主键字符串 public void UpdateHotKey(string modifiers, string key) { if (_callback != null) diff --git a/src/Hua.Todo.Maui/Views/MainPage.xaml.cs b/src/Hua.Todo.Maui/Views/MainPage.xaml.cs index d4ef63a..c9e0d8d 100644 --- a/src/Hua.Todo.Maui/Views/MainPage.xaml.cs +++ b/src/Hua.Todo.Maui/Views/MainPage.xaml.cs @@ -4,14 +4,20 @@ using Hua.Todo.Maui.Services; namespace Hua.Todo.Maui.Views { + /// + /// 应用程序主页面。 + /// 该页面承载 WebView(前端 UI),并通过 JavaScript 注入与事件机制实现与 MAUI 的通讯。 + /// public partial class MainPage : ContentPage { private readonly AppSettings _appSettings; private readonly IEmbeddedWebServerService? _webServer; -#if WINDOWS - private Platforms.Windows.WindowsKeyboardHandler? _keyboardHandler; -#endif + /// + /// 创建 。 + /// + /// 应用配置。 + /// 嵌入式 Web 服务器服务(在不同平台可能为不同实现)。 public MainPage(AppSettings appSettings, IEmbeddedWebServerService webServer) { InitializeComponent(); @@ -23,7 +29,10 @@ namespace Hua.Todo.Maui.Views SetupKeyboardHandler(); } - + /// + /// 设置 WebView 数据源。 + /// 当启用嵌入式服务器且使用静态文件时,直接指向本地服务器;否则使用配置的前端 URL。 + /// private void SetupWebViewSource() { if (_appSettings.WebServer.IsUsingStatic) @@ -38,27 +47,32 @@ namespace Hua.Todo.Maui.Views MainWebView.Source = NormalizeUrl(_appSettings.WebServer.ForEndUrl); } + /// + /// 设置键盘处理器(平台差异通过分部类实现)。 + /// private void SetupKeyboardHandler() { -#if WINDOWS - _keyboardHandler = new Platforms.Windows.WindowsKeyboardHandler(); - _keyboardHandler.EscKeyPressed += OnEscKeyPressed; - _keyboardHandler.Start(); -#endif + PlatformSetupKeyboardHandler(); } + /// + /// 当 Esc 键按下时的回调 + /// private void OnEscKeyPressed(object? sender, EventArgs e) { var window = Microsoft.Maui.Controls.Application.Current?.Windows.FirstOrDefault(); if (window != null) { -#if WINDOWS - var windowService = new Platforms.Windows.WindowsWindowService(); - windowService.MinimizeWindow(window); -#endif + PlatformOnEscKeyPressed(window); } } + /// + /// 设置 WebView 通讯逻辑。 + /// 约定: + /// - 通过 window.__API_BASE_URL__ 注入后端 API 基地址(仅在嵌入式服务器运行时) + /// - 通过自定义事件在 Web 与 MAUI 间传递热键配置等数据 + /// private void SetupWebViewCommunication() { MainWebView.Navigated += async (s, e) => @@ -76,6 +90,7 @@ namespace Hua.Todo.Maui.Views await MainWebView.EvaluateJavaScriptAsync($"window.__API_BASE_URL__ = '{apiBase}';"); } + // 注入前端与 MAUI 的通讯桥(事件名/字段名属于协议的一部分,修改需同步前端)。 await MainWebView.EvaluateJavaScriptAsync(@" window.mauiInterop = { onHotKeyConfigUpdated: null, @@ -100,6 +115,10 @@ namespace Hua.Todo.Maui.Views }; } + /// + /// 规格化 URL(针对 Android 模拟器处理 localhost)。 + /// Android 模拟器中 localhost 指向模拟器自身,需要替换为 10.0.2.2 才能访问宿主机服务。 + /// private static string NormalizeUrl(string url) { if (string.IsNullOrWhiteSpace(url)) return url; @@ -116,5 +135,16 @@ namespace Hua.Todo.Maui.Views return url; } + + // 平台特定分部方法声明(实现位于 Platforms/* 目录) + /// + /// 平台特定的键盘处理器初始化。 + /// + partial void PlatformSetupKeyboardHandler(); + /// + /// 平台特定的 Esc 键处理逻辑。 + /// + /// 当前窗口。 + partial void PlatformOnEscKeyPressed(Window window); } } diff --git a/src/Hua.Todo.Web/src/api/tasks.ts b/src/Hua.Todo.Web/src/api/tasks.ts index c957006..2c7cbc6 100644 --- a/src/Hua.Todo.Web/src/api/tasks.ts +++ b/src/Hua.Todo.Web/src/api/tasks.ts @@ -4,6 +4,11 @@ import LocalStorageService from '../services/localStorageService'; import { normalizeTask, normalizeTasks } from '../services/taskNormalizer'; export const taskApi = { + /** + * 获取任务列表 + * @param completed 可选,过滤已完成或未完成的任务 + * @returns 包含任务数组的响应对象 + */ async getTasks(completed?: boolean): Promise> { if (!LocalStorageService.isOnline()) { const localTasks = LocalStorageService.loadTasks(); @@ -50,6 +55,11 @@ export const taskApi = { return apiResponse; }, + /** + * 根据 ID 获取任务详情 + * @param id 任务 ID + * @returns 包含任务对象或 null 的响应对象 + */ async getTask(id: number): Promise> { if (!LocalStorageService.isOnline()) { const localTasks = LocalStorageService.loadTasks(); @@ -83,6 +93,11 @@ export const taskApi = { return apiResponse; }, + /** + * 创建新任务 + * @param dto 创建任务的数据传输对象 + * @returns 包含新创建任务的响应对象 + */ async createTask(dto: CreateTaskDto): Promise> { const newTask: Task = { id: Date.now(), @@ -141,6 +156,12 @@ export const taskApi = { return apiResponse; }, + /** + * 更新任务信息 + * @param id 任务 ID + * @param dto 更新任务的数据传输对象 + * @returns 包含更新后任务的响应对象 + */ async updateTask(id: number, dto: UpdateTaskDto): Promise> { const updateDto = dto; @@ -203,6 +224,11 @@ export const taskApi = { return apiResponse; }, + /** + * 切换任务完成状态 + * @param id 任务 ID + * @returns 包含更新后任务的响应对象 + */ async toggleComplete(id: number): Promise> { if (!LocalStorageService.isOnline()) { const localTasks = LocalStorageService.loadTasks(); @@ -258,6 +284,11 @@ export const taskApi = { return apiResponse; }, + /** + * 删除任务 + * @param id 任务 ID + * @returns 包含操作结果的响应对象 + */ async deleteTask(id: number): Promise> { if (!LocalStorageService.isOnline()) { const localTasks = LocalStorageService.loadTasks(); @@ -309,6 +340,10 @@ export const taskApi = { return apiResponse; }, + /** + * 同步本地更改到服务器 + * @returns 包含最新任务列表的响应对象 + */ async syncToServer(): Promise> { if (!LocalStorageService.isOnline()) { return { diff --git a/src/Hua.Todo.Web/vite.config.ts b/src/Hua.Todo.Web/vite.config.ts index 20ee1fc..928d821 100644 --- a/src/Hua.Todo.Web/vite.config.ts +++ b/src/Hua.Todo.Web/vite.config.ts @@ -15,7 +15,7 @@ export default defineConfig({ port: 5174, proxy: { '/api': { - target: 'http://localhost:5057', + target: 'http://localhost:5173', changeOrigin: true, secure: false, },