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,
},