Compare commits
3 Commits
758f6772c6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d62916076 | |||
| 141b3112a4 | |||
| d00a907da0 |
@@ -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 交互”的协议字段(例如全局变量、事件名)必须注释说明来源与约束。
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
description: 强制文档同步规范:每次变更代码(如新增功能、修改接口、调整架构等)必须同步更新 README.md 和 docs 目录下的相关文档。
|
||||
---
|
||||
|
||||
# 文档同步规范(必须遵守)
|
||||
|
||||
## 通用原则
|
||||
|
||||
- **代码即文档,文档随代码**:文档不是静态的,它必须真实反映当前代码的状态。
|
||||
- **及时性**:在提交代码变更的同时(或紧随其后),必须完成相关文档的更新。
|
||||
- **准确性**:确保文档中的示例代码、接口说明、安装步骤与实际代码完全一致。
|
||||
- **协作友好(局部修改)**:当并行处理多个任务/需求时,更新文档应尽量只修改与本任务直接相关的段落/小节,避免对不相关内容做无意义的重排、改写或格式化;如必须调整非关联内容,应拆分为独立的变更说明清楚原因与影响范围。
|
||||
|
||||
## 更新范围
|
||||
|
||||
- **README.md**:
|
||||
- 如果变更涉及核心功能点(Features)、安装步骤(Installation)、快速开始(Quick Start)或 API 端点(API Endpoints),必须同步更新。
|
||||
- 变更涉及技术栈调整或项目结构变化时需更新。
|
||||
- **docs/ 目录文档**:
|
||||
- **接口变更**:若修改了 API,需同步更新 [技术设计文档](docs/技术设计文档.md) 中的接口部分。
|
||||
- **功能新增/调整**:需在 [产品需求文档](docs/产品需求文档.md) 和 [技术栈与模块](docs/技术栈与模块.md) 中体现。
|
||||
- **架构/模式变更**:需更新 [技术设计文档](docs/技术设计文档.md)。
|
||||
- **代码规范**:若引入了新的编码模式或工具,需更新 [代码规范文档](docs/代码规范文档.md)。
|
||||
- **版本记录**:所有非琐碎的变更必须在 [版本记录.md](docs/版本记录.md) 中添加记录。
|
||||
|
||||
## 检查清单
|
||||
|
||||
1. [ ] 是否有新增的 API 端点?(更新 README 和技术设计文档)
|
||||
2. [ ] 是否修改了现有的业务逻辑或数据结构?(更新技术设计文档)
|
||||
3. [ ] 是否有新增的功能模块?(更新产品需求文档和技术栈说明)
|
||||
4. [ ] 是否调整了开发环境或依赖?(更新 README)
|
||||
5. [ ] 是否在 [版本记录.md](docs/版本记录.md) 中记录了本次变更?
|
||||
6. [ ] 文档变更是否保持“局部修改”,只影响与本任务相关的段落/小节?(避免无关重排/改写)
|
||||
+2
-2
@@ -7,7 +7,7 @@
|
||||
</Configurations>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/Hua.Todo.Application/Hua.Todo.Application.csproj">
|
||||
<Build Solution="Debug|*" Project="false" />
|
||||
<Build Solution="Debug|*" Project="true" />
|
||||
</Project>
|
||||
<Project Path="src/Hua.Todo.Core/Hua.Todo.Core.csproj" />
|
||||
<Project Path="src/Hua.Todo.Host/Hua.Todo.Host.csproj" />
|
||||
@@ -15,4 +15,4 @@
|
||||
<Build Solution="Debug|*" Project="false" />
|
||||
</Project>
|
||||
</Folder>
|
||||
</Solution>
|
||||
</Solution>
|
||||
|
||||
@@ -12,33 +12,6 @@
|
||||
- **本地数据持久化**:使用 SQLite 数据库保存数据,支持完全离线使用
|
||||
- **HTTP API 通信**:前后端通过 RESTful API 进行数据交互
|
||||
|
||||
### 技术特性
|
||||
- **现代化架构**:MAUI + WebView + C# 后端 + Vue.js 前端
|
||||
- **分层设计**:Core(核心层)+ API(后端)+ Web(前端)
|
||||
- **响应式界面**:Vue.js 3 实现的现代化用户界面
|
||||
- **统一 API 设计**:RESTful API 风格,支持跨域请求
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
### 后端技术栈
|
||||
- **开发语言**:C# 10
|
||||
- **框架**:.NET 10
|
||||
- **UI 框架**:MAUI (Multi-platform App UI)
|
||||
- **Web 服务器**:Kestrel (ASP.NET Core 内置)
|
||||
- **API 框架**:ASP.NET Core Web API
|
||||
- **数据访问**:Entity Framework Core
|
||||
- **数据库**:SQLite (本地存储)
|
||||
- **依赖注入**:Microsoft.Extensions.DependencyInjection
|
||||
|
||||
### 前端技术栈
|
||||
- **开发语言**:TypeScript
|
||||
- **框架**:Vue.js 3
|
||||
- **构建工具**:Vite
|
||||
- **HTTP 客户端**:Axios
|
||||
- **状态管理**:Pinia
|
||||
- **UI 组件库**:Element Plus / Vant (移动端)
|
||||
- **CSS 预处理器**:SCSS
|
||||
|
||||
## 📦 安装与使用
|
||||
|
||||
### 环境要求
|
||||
@@ -59,9 +32,8 @@ cd Hua.Todo
|
||||
|
||||
#### 2. 启动后端 API
|
||||
```bash
|
||||
cd src/Hua.Todo.Api
|
||||
cd src/Hua.Todo.Host
|
||||
dotnet restore
|
||||
dotnet ef database update
|
||||
dotnet run
|
||||
```
|
||||
API 将在 `http://localhost:5173` 启动
|
||||
@@ -72,7 +44,7 @@ cd src/Hua.Todo.Web
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
前端将在 `http://localhost:5173` 启动
|
||||
前端将在 `http://localhost:5174` 启动,并自动代理 `/api` 请求到 `http://localhost:5173`
|
||||
|
||||
### 使用说明
|
||||
- **添加任务**:在前端界面中输入任务内容,设置优先级,点击添加按钮
|
||||
@@ -86,48 +58,14 @@ npm run dev
|
||||
```
|
||||
Hua.Todo/
|
||||
├── docs/ # 文档目录
|
||||
│ ├── 产品需求文档.md
|
||||
│ ├── 产品需求文档-1.1.0.md
|
||||
│ ├── 技术设计文档.md
|
||||
│ └── 代码规范文档.md
|
||||
│ ├── manual/ # 用户/开发者手册
|
||||
│ └── project/ # 项目进度/需求文档
|
||||
├── src/ # 源代码目录
|
||||
│ ├── Hua.Todo.Core/ # 核心业务逻辑层
|
||||
│ │ ├── Entities/ # 实体类
|
||||
│ │ │ ├── Task.cs
|
||||
│ │ │ └── TaskPriority.cs
|
||||
│ │ └── Interfaces/ # 接口定义
|
||||
│ │ ├── ITaskRepository.cs
|
||||
│ │ └── ITaskService.cs
|
||||
│ ├── Hua.Todo.Api/ # 后端 API 项目
|
||||
│ │ ├── Controllers/ # API 控制器
|
||||
│ │ │ └── TasksController.cs
|
||||
│ │ ├── Services/ # 业务服务
|
||||
│ │ │ └── TaskService.cs
|
||||
│ │ ├── Repositories/ # 数据访问层
|
||||
│ │ │ └── TaskRepository.cs
|
||||
│ │ ├── Data/ # 数据库上下文
|
||||
│ │ │ ├── TodoDbContext.cs
|
||||
│ │ │ └── Migrations/ # 数据库迁移
|
||||
│ │ ├── Models/ # 数据模型
|
||||
│ │ │ └── TaskModels.cs
|
||||
│ │ ├── Program.cs # API 入口
|
||||
│ │ └── Hua.Todo.Api.csproj # API 项目文件
|
||||
│ ├── Hua.Todo.Web/ # 前端 Web 项目 (Vue.js)
|
||||
│ │ ├── public/ # 静态资源
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── api/ # API 调用
|
||||
│ │ │ │ ├── client.ts
|
||||
│ │ │ │ └── tasks.ts
|
||||
│ │ │ ├── components/ # Vue 组件
|
||||
│ │ │ │ ├── TaskList.vue
|
||||
│ │ │ │ └── TaskItem.vue
|
||||
│ │ │ ├── types/ # TypeScript 类型定义
|
||||
│ │ │ │ └── task.ts
|
||||
│ │ │ ├── App.vue # 根组件
|
||||
│ │ │ └── main.ts # 应用入口
|
||||
│ │ ├── package.json # 依赖配置
|
||||
│ │ ├── vite.config.ts # Vite 配置
|
||||
│ │ └── tsconfig.json # TypeScript 配置
|
||||
│ ├── Hua.Todo.Core/ # 领域实体与基础接口
|
||||
│ ├── Hua.Todo.Application/ # 业务逻辑与应用层实现
|
||||
│ ├── Hua.Todo.Host/ # 后端 API 宿主项目 (Kestrel)
|
||||
│ ├── Hua.Todo.Web/ # 前端 Web 项目 (Vue.js 3 + Vite)
|
||||
│ ├── Hua.Todo.Maui/ # 跨平台客户端项目 (Windows/Android/iOS/macOS)
|
||||
│ └── Hua.Todo.slnx # 解决方案文件
|
||||
├── .gitignore # Git 忽略文件
|
||||
└── README.md # 项目说明文档
|
||||
@@ -141,48 +79,29 @@ Hua.Todo/
|
||||
- `PATCH /api/tasks/{id}/complete` - 切换完成状态
|
||||
- `DELETE /api/tasks/{id}` - 删除任务
|
||||
|
||||
## 🎯 核心模块说明
|
||||
## 🤝 交流与贡献
|
||||
|
||||
### Hua.Todo.Core
|
||||
核心业务逻辑层,定义领域模型和业务规则,提供核心业务接口。
|
||||
- **QQ 交流群**:2167048911 (Hua.Todo 交流群)
|
||||
- **项目地址**:[Hua.Todo](https://git.we965.cn/Tools/Hua.Todo)
|
||||
- **贡献指南**:欢迎提交 Pull Request,详见 [其他信息](docs/manual/其他信息.md)
|
||||
|
||||
### Hua.Todo.Api
|
||||
后端 API 项目,提供 RESTful API 接口,处理业务逻辑,管理数据访问和持久化。
|
||||
## 📄 开源协议
|
||||
|
||||
### Hua.Todo.Web
|
||||
前端 Web 项目,基于 Vue.js 3 + TypeScript,提供用户界面,通过 HTTP API 与后端通信。
|
||||
本项目采用 **AGPL-3.0** 许可证。详细内容请参阅 [LICENSE](LICENSE) 文件。
|
||||
|
||||
## 🔄 版本更新
|
||||
## 📚 更多文档
|
||||
|
||||
### 版本策略
|
||||
- 采用语义化版本号:`MAJOR.MINOR.PATCH`
|
||||
- v1.0.0:初始 WPF 版本
|
||||
- v1.1.0:MAUI + WebView 跨平台版本
|
||||
### 用户与开发者手册
|
||||
- [技术栈与模块说明](docs/manual/技术栈与模块.md)
|
||||
- [版本更新历史](docs/manual/版本记录.md)
|
||||
- [技术设计文档](docs/manual/技术设计文档.md)
|
||||
- [代码规范文档](docs/manual/代码规范文档.md)
|
||||
- [其他信息 (贡献、许可证、联系方式)](docs/manual/其他信息.md)
|
||||
|
||||
### v1.1.0 更新内容
|
||||
- 重构为 MAUI + WebView 架构
|
||||
- 实现跨平台支持
|
||||
- 使用 HTTP API 进行前后端通信
|
||||
- 采用 Vue.js 3 作为前端框架
|
||||
- 使用 SQLite 作为本地数据库
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 打开 Pull Request
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 AGPL-3.0 许可证 - 查看 [LICENSE](LICENSE) (英文) 或 [LICENSE.zh-CN](LICENSE.zh-CN) (中文) 文件了解详情
|
||||
|
||||
## 📞 联系方式
|
||||
|
||||
- 项目作者:ShaoHua
|
||||
- 项目地址:https://git.we965.cn/Tools/Hua.Todo
|
||||
### 项目进度与需求
|
||||
- [产品需求文档](docs/project/产品需求文档.md)
|
||||
- [Android 离线排查计划](docs/project/Android_NotFound_排查计划.md)
|
||||
- [实现对比文档](docs/project/实现对比文档.md)
|
||||
|
||||
---
|
||||
|
||||
**Hua.Todo** - 跨平台任务管理,让效率无处不在!
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# 其他信息
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 打开 Pull Request
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 AGPL-3.0 许可证 - 查看 [LICENSE](LICENSE) (英文) 或 [LICENSE.zh-CN](LICENSE.zh-CN) (中文) 文件了解详情
|
||||
|
||||
## 📞 联系方式
|
||||
|
||||
- 项目作者:ShaoHua
|
||||
- 项目地址:https://git.we965.cn/Tools/Hua.Todo
|
||||
- QQ 交流群:2167048911 (Hua.Todo 交流群)
|
||||
@@ -0,0 +1,39 @@
|
||||
# 技术栈与模块说明
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
### 后端技术栈
|
||||
- **开发语言**:C# 10
|
||||
- **框架**:.NET 10
|
||||
- **UI 框架**:MAUI (Multi-platform App UI)
|
||||
- **Web 服务器**:Kestrel (ASP.NET Core 内置)
|
||||
- **API 框架**:ASP.NET Core Web API
|
||||
- **数据访问**:Entity Framework Core
|
||||
- **数据库**:SQLite (本地存储)
|
||||
- **依赖注入**:Microsoft.Extensions.DependencyInjection
|
||||
|
||||
### 前端技术栈
|
||||
- **开发语言**:TypeScript
|
||||
- **框架**:Vue.js 3
|
||||
- **构建工具**:Vite
|
||||
- **HTTP 客户端**:Axios
|
||||
- **状态管理**:Pinia
|
||||
- **UI 组件库**:Element Plus / Vant (移动端)
|
||||
- **CSS 预处理器**:SCSS
|
||||
|
||||
## 🎯 核心模块说明
|
||||
|
||||
### Hua.Todo.Core
|
||||
领域实体层,定义核心实体(TaskEntity)、枚举(TaskPriority)以及仓储接口(ITaskRepository)。
|
||||
|
||||
### Hua.Todo.Application
|
||||
应用层实现,包含业务逻辑、动态 API 生成逻辑、EF Core 数据库上下文以及具体的服务实现(TaskService)。
|
||||
|
||||
### Hua.Todo.Host
|
||||
后端 API 宿主,提供运行环境和配置,是后端服务的启动入口。
|
||||
|
||||
### Hua.Todo.Web
|
||||
前端 Web 项目,基于 Vue.js 3 + TypeScript + Vite,提供用户界面,通过 HTTP API 与后端通信。
|
||||
|
||||
### Hua.Todo.Maui
|
||||
跨平台客户端项目,将 Web 内容嵌入到原生容器中,支持 Windows、Android、iOS 和 macOS。
|
||||
@@ -30,10 +30,14 @@
|
||||
```
|
||||
Hua.Todo/
|
||||
├── docs/ # 文档目录
|
||||
│ ├── PRD.md # 产品需求文档
|
||||
│ ├── PRD-1.1.0.md # v1.1.0 产品需求文档
|
||||
│ ├── TechnicalDesign.md # 技术设计文档(本文件)
|
||||
│ └── CodeStandards.md # 代码规范文档
|
||||
│ ├── manual/ # 用户/开发者手册
|
||||
│ │ ├── 技术栈与模块.md
|
||||
│ │ ├── 版本记录.md
|
||||
│ │ ├── 技术设计文档.md(本文件)
|
||||
│ │ └── 代码规范文档.md
|
||||
│ └── project/ # 项目进度/需求文档
|
||||
│ ├── 产品需求文档.md
|
||||
│ └── ...
|
||||
│
|
||||
├── src/ # 源代码目录
|
||||
│ ├── Hua.Todo.Maui/ # MAUI 主项目(跨平台入口)
|
||||
@@ -306,6 +310,7 @@ CREATE TABLE Settings (
|
||||
- **Windows**: WebView2 运行时要求
|
||||
- **macOS**: 代码签名和公证
|
||||
- **移动端**: 应用商店发布配置
|
||||
- **Linux**: .NET MAUI 无官方 Linux 目标,需要引入独立桌面宿主(例如基于 WebKitGTK 的方案)并处理运行时依赖与打包格式
|
||||
|
||||
## 9. 性能优化
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# 版本更新历史
|
||||
|
||||
## 🔄 版本更新
|
||||
|
||||
### 版本策略
|
||||
|
||||
- 采用语义化版本号:`MAJOR.MINOR.PATCH`
|
||||
- v1.0.0:初始 WPF 版本
|
||||
- v1.1.0:MAUI + WebView 跨平台版本
|
||||
- v1.2.0 (规划中):Linux 支持与增强功能
|
||||
|
||||
### v1.1.1 (2026-04-06)
|
||||
|
||||
- **文档规范增强**:新增文档同步规则,强制代码变更与文档更新保持同步。
|
||||
- **项目结构说明校准**:修正 README.md 和技术文档中对 `Hua.Todo.Host`、`Hua.Todo.Application` 等模块的路径与职责描述。
|
||||
- **端口配置校准**:修正文档中关于前端与后端 API 的端口说明(5173/5174)。
|
||||
- **PRD 校准**:移除 v1.2.0 PRD 中“本地迭代不支持”表述与“数据迁移(导入/导出)”小节。
|
||||
- **PRD 校准**:移除 v1.2.0 PRD 中“云同步”需求。
|
||||
|
||||
### v1.1.0 更新内容
|
||||
|
||||
- 重构为 MAUI + WebView 架构
|
||||
- 实现跨平台支持 (Windows, macOS, Android, iOS)
|
||||
- 使用 HTTP API 进行前后端通信
|
||||
- 采用 Vue.js 3 作为前端框架
|
||||
- 使用 SQLite 作为本地数据库
|
||||
- 实现子任务支持
|
||||
|
||||
### v1.2.0 规划内容 (即将推出)
|
||||
|
||||
- **Linux 官方支持**:正式适配 Linux 平台。
|
||||
- **关键词检索**:支持按任务标题关键词搜索。
|
||||
- **标签系统**:引入多标签支持,提升任务组织效率。
|
||||
- **暗色模式**:全平台适配暗色/深色主题。
|
||||
- **数据导出导入(后续)**:支持 JSON 格式数据备份与迁移(延期到后续版本)。
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# Hua.Todo 产品需求文档 (PRD) v1.2.0
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
在 v1.1.0 版本成功实现 MAUI + WebView 跨平台架构的基础上,v1.2.0 版本将以“**MAUI 入口保持不动 + Linux 新增 Avalonia 入口**”的方式落地 Linux 支持(继续使用 WebView 承载 Vue 前端),并在此基础上探索 Avalonia 对其他终端的支持情况;若验证效果良好,后续版本将推进各终端入口统一切换到 Avalonia。
|
||||
|
||||
## 2. 核心目标
|
||||
|
||||
- **完善平台覆盖**:正式支持 Linux 平台。
|
||||
- **增强任务组织**:引入搜索。
|
||||
- **云同步(基础可用)**:支持用户级任务数据同步与“可控落盘”(在不信任终端场景下可禁止落盘)。
|
||||
|
||||
## 3. 功能需求
|
||||
|
||||
### 3.1 平台支持:Linux 官方支持
|
||||
|
||||
- **客户端框架迁移**:保持原 MAUI 项目不动,实现 Avalonia 对 Linux 端支持,并探索 Avalonia 对 Android 和其他平台的支持。
|
||||
- **终端入口策略(v1.2.0)**:
|
||||
- **保持 MAUI 入口不动**:Windows/macOS/iOS/Android 继续沿用现有 MAUI 入口与 WebView 方案,保证迭代稳定性。
|
||||
- **Linux 使用 Avalonia 作为入口**:新增 Avalonia 桌面端宿主作为 Linux 入口,继续用 WebView 承载同一套 Vue 前端,并复用既有“本地 API ↔ 前端”的交互协议。
|
||||
- **评估全端切换可行性**:在完成 Linux 入口落地后,评估 Avalonia 在 Windows/macOS(以及后续 Android 等)的稳定性、WebView 兼容性与打包成本;若效果良好,后续版本将各终端入口统一切换到 Avalonia。
|
||||
- **适配性**:确保 WebView 在 Linux 环境下的正常渲染与交互。
|
||||
- **打包部署**:提供 Linux 平台的打包脚本(如 .deb 或 .tar.gz)。
|
||||
|
||||
### 3.2 任务检索 (Search)
|
||||
|
||||
- **关键词搜索**:在主界面顶部增加搜索框,支持按任务标题模糊匹配。
|
||||
|
||||
### 3.3 云同步(基础可用)
|
||||
|
||||
#### 3.3.1 服务端地址配置(用户手动指定)
|
||||
|
||||
- **用户手动指定服务端地址**:首次启用同步时,用户需要手动填写服务端地址(例如 `https://example.com`),并允许后续在设置中修改。
|
||||
- **地址有效性提示**:客户端需对地址格式做基础校验,并在保存时提示可达性/证书异常等风险信息。
|
||||
|
||||
#### 3.3.2 权限与二次认证(RBAC + 二次认证)
|
||||
|
||||
- **RBAC 权限模型**:服务端必须提供基于角色/权限的访问控制(Role-Based Access Control),以支持后续按功能、数据域进行授权。
|
||||
- **二次认证**:对高风险操作(例如启用/关闭同步、切换账号、调整“是否允许落盘”等安全相关配置)应支持二次认证机制(例如二次口令/一次性验证码等),具体实现以服务端能力为准。
|
||||
|
||||
#### 3.3.3 基于用户的数据隔离
|
||||
|
||||
- **用户级数据隔离**:服务端必须按用户隔离任务数据,任何同步、检索与配置读取都必须绑定当前登录用户上下文。
|
||||
- **最小权限原则**:客户端仅请求完成任务同步所需的最小权限范围,避免默认授予不必要的数据访问能力。
|
||||
|
||||
#### 3.3.4 同步工作流(登录后获取任务 + 可控落盘)
|
||||
|
||||
- **登录后拉取任务**:用户登录成功后,客户端从服务端获取该用户的任务信息并更新到前端展示。
|
||||
- **服务端配置驱动的落盘策略**:客户端需读取服务端针对该用户(或该会话/终端)的安全配置,决定任务信息是否允许落盘到本地存储。
|
||||
- **不信任终端禁止落盘**:当服务端标记“终端不可信/不允许落盘”时,客户端只能在内存中持有任务信息;退出应用后不保留任务数据。
|
||||
|
||||
## 4. 技术实现要点
|
||||
|
||||
### 4.1 Linux 适配
|
||||
|
||||
v1.1.0 起客户端使用 **MAUI WebView** 承载 Vue 前端;在 v1.2.0 中,保持现有 MAUI 入口不动,同时为 Linux 新增 **Avalonia 桌面端**入口,并继续以 WebView 承载同一套 Vue 前端。后续将基于 Linux 端落地经验评估 Avalonia 对 Windows/macOS(以及后续 Android 等)的覆盖与稳定性,满足条件后推进全端入口统一。
|
||||
|
||||
#### 4.1.1 Linux 解决方案(Avalonia 入口 + WebView 承载 Vue)
|
||||
|
||||
- **入口与架构**:Linux 端新增 Avalonia 桌面宿主作为应用入口;加载同一套 Vue 前端,并保持与现有 MAUI 端一致的“前端调用本地 API”协议与数据模型,确保业务逻辑与 UI 复用。
|
||||
- **WebView 方案**:选用可在 Avalonia/Linux 运行的 WebView 控件承载前端(以 WebKitGTK/Chromium 系为候选实现),统一约束:支持基础导航/本地资源加载、JS ↔ Native 双向通信、DevTools(至少开发环境可用)。
|
||||
- **发行版与依赖策略**:定义最低支持范围(例如 Ubuntu LTS 系列作为基线),并将 WebView 运行时依赖纳入打包交付(避免用户手动安装大量依赖导致不可用)。
|
||||
- **输入法/字体/DPI**:提供默认 CJK 字体与渲染配置,验证 IME、缩放、Wayland/X11 关键组合;遇到环境差异时以“可配置 + 明确降级”保证可用性。
|
||||
- **快捷键策略**:全局快捷键在 Linux 上采用“尽力而为”的可插拔实现;在 Wayland 等受限环境下自动降级为应用内快捷键,并在设置中提供可配置与提示。
|
||||
- **交付形式**:优先提供一种“自包含”的交付产物(例如 AppImage/Flatpak 之一)作为官方安装方式,同时保留 `.deb/.tar.gz` 作为补充分发形式(以脚本产出为准)。
|
||||
|
||||
## 5. 版本规划
|
||||
|
||||
### 5.1 v1.2.0 目标
|
||||
|
||||
- [ ] Linux 平台完整支持与打包脚本。
|
||||
- [ ] 搜索框(关键词检索)。
|
||||
- [ ] 云同步(基础可用:用户手动配置服务端地址、RBAC + 二次认证、用户级数据隔离、服务端配置驱动的可控落盘)。
|
||||
|
||||
### 5.2 后续版本规划
|
||||
|
||||
- **v1.3.0**:数据导入导出(JSON)、统计图表(任务完成趋势、时间分配分析)。
|
||||
- **v1.4.0**:高级过滤(优先级/时间段)与更丰富的视图(如看板/日历)。
|
||||
|
||||
## 6. 风险评估
|
||||
|
||||
- **Linux 碎片化**:不同发行版下 WebView 的依赖库可能不一致。
|
||||
- **凭据与合规**:第三方服务鉴权方式差异大,且必须保证凭据安全存储与最小化权限。
|
||||
- **终端可信度与数据落盘**:若需支持“不信任终端不落盘”,需要明确“可信度判定与配置下发”的服务端策略,并保证客户端在无落盘场景下的可用性与体验。
|
||||
@@ -3,14 +3,28 @@ using Hua.Todo.Core.Entities;
|
||||
|
||||
namespace Hua.Todo.Application.Data;
|
||||
|
||||
/// <summary>
|
||||
/// 应用程序数据库上下文(EF Core)。
|
||||
/// </summary>
|
||||
public class TodoDbContext : DbContext
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建 <see cref="TodoDbContext"/>。
|
||||
/// </summary>
|
||||
/// <param name="options">数据库上下文配置。</param>
|
||||
public TodoDbContext(DbContextOptions<TodoDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务集合。
|
||||
/// </summary>
|
||||
public DbSet<TaskEntity> Tasks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 配置实体模型映射。
|
||||
/// </summary>
|
||||
/// <param name="modelBuilder">模型构建器。</param>
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
@@ -2,8 +2,16 @@ using Microsoft.AspNetCore.Builder;
|
||||
|
||||
namespace Hua.Todo.Application.DynamicApi;
|
||||
|
||||
/// <summary>
|
||||
/// Dynamic API 中间件扩展方法。
|
||||
/// </summary>
|
||||
public static class DynamicApiExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册 Dynamic API 中间件。
|
||||
/// </summary>
|
||||
/// <param name="builder">应用构建器。</param>
|
||||
/// <returns>应用构建器。</returns>
|
||||
public static IApplicationBuilder UseDynamicApi(this IApplicationBuilder builder)
|
||||
{
|
||||
return builder.UseMiddleware<DynamicApiMiddleware>();
|
||||
|
||||
@@ -6,6 +6,10 @@ using Hua.Todo.Application.Interfaces;
|
||||
|
||||
namespace Hua.Todo.Application.DynamicApi;
|
||||
|
||||
/// <summary>
|
||||
/// Dynamic API 中间件。
|
||||
/// 根据路由约定将 <see cref="IDynamicApiService"/> 的接口方法映射为 HTTP API(/api/{service}/...)。
|
||||
/// </summary>
|
||||
public class DynamicApiMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
@@ -44,12 +48,21 @@ public class DynamicApiMiddleware
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 <see cref="DynamicApiMiddleware"/>。
|
||||
/// </summary>
|
||||
/// <param name="next">管道中的下一个中间件。</param>
|
||||
/// <param name="serviceProvider">应用根服务容器。</param>
|
||||
public DynamicApiMiddleware(RequestDelegate next, IServiceProvider serviceProvider)
|
||||
{
|
||||
_next = next;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理当前请求并尝试进行 Dynamic API 分发。
|
||||
/// </summary>
|
||||
/// <param name="context">HTTP 上下文。</param>
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var path = context.Request.Path.Value ?? string.Empty;
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
namespace Hua.Todo.Application.DynamicApi;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
/// <summary>
|
||||
/// 标记方法为 HTTP GET。
|
||||
/// </summary>
|
||||
public class HttpGetAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 可选路由模板。
|
||||
/// </summary>
|
||||
public string? Route { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建 <see cref="HttpGetAttribute"/>。
|
||||
/// </summary>
|
||||
public HttpGetAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 <see cref="HttpGetAttribute"/>。
|
||||
/// </summary>
|
||||
/// <param name="route">路由模板。</param>
|
||||
public HttpGetAttribute(string route)
|
||||
{
|
||||
Route = route;
|
||||
@@ -16,14 +29,27 @@ public class HttpGetAttribute : Attribute
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
/// <summary>
|
||||
/// 标记方法为 HTTP POST。
|
||||
/// </summary>
|
||||
public class HttpPostAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 可选路由模板。
|
||||
/// </summary>
|
||||
public string? Route { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建 <see cref="HttpPostAttribute"/>。
|
||||
/// </summary>
|
||||
public HttpPostAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 <see cref="HttpPostAttribute"/>。
|
||||
/// </summary>
|
||||
/// <param name="route">路由模板。</param>
|
||||
public HttpPostAttribute(string route)
|
||||
{
|
||||
Route = route;
|
||||
@@ -31,14 +57,27 @@ public class HttpPostAttribute : Attribute
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
/// <summary>
|
||||
/// 标记方法为 HTTP PUT。
|
||||
/// </summary>
|
||||
public class HttpPutAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 可选路由模板。
|
||||
/// </summary>
|
||||
public string? Route { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建 <see cref="HttpPutAttribute"/>。
|
||||
/// </summary>
|
||||
public HttpPutAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 <see cref="HttpPutAttribute"/>。
|
||||
/// </summary>
|
||||
/// <param name="route">路由模板。</param>
|
||||
public HttpPutAttribute(string route)
|
||||
{
|
||||
Route = route;
|
||||
@@ -46,14 +85,27 @@ public class HttpPutAttribute : Attribute
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
/// <summary>
|
||||
/// 标记方法为 HTTP DELETE。
|
||||
/// </summary>
|
||||
public class HttpDeleteAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 可选路由模板。
|
||||
/// </summary>
|
||||
public string? Route { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建 <see cref="HttpDeleteAttribute"/>。
|
||||
/// </summary>
|
||||
public HttpDeleteAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 <see cref="HttpDeleteAttribute"/>。
|
||||
/// </summary>
|
||||
/// <param name="route">路由模板。</param>
|
||||
public HttpDeleteAttribute(string route)
|
||||
{
|
||||
Route = route;
|
||||
@@ -61,14 +113,27 @@ public class HttpDeleteAttribute : Attribute
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
/// <summary>
|
||||
/// 标记方法为 HTTP PATCH。
|
||||
/// </summary>
|
||||
public class HttpPatchAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 可选路由模板。
|
||||
/// </summary>
|
||||
public string? Route { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建 <see cref="HttpPatchAttribute"/>。
|
||||
/// </summary>
|
||||
public HttpPatchAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 <see cref="HttpPatchAttribute"/>。
|
||||
/// </summary>
|
||||
/// <param name="route">路由模板。</param>
|
||||
public HttpPatchAttribute(string route)
|
||||
{
|
||||
Route = route;
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
namespace Hua.Todo.Application.DynamicApi;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
|
||||
/// <summary>
|
||||
/// 指示参数从 QueryString 绑定。
|
||||
/// </summary>
|
||||
public class FromQueryAttribute : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
|
||||
/// <summary>
|
||||
/// 指示参数从 Request Body 绑定。
|
||||
/// </summary>
|
||||
public class FromBodyAttribute : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
namespace Hua.Todo.Application.DynamicApi;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Method, AllowMultiple = false)]
|
||||
/// <summary>
|
||||
/// 标记服务/方法是否允许通过 Dynamic API 暴露。
|
||||
/// </summary>
|
||||
public class RemoteServiceAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否启用远程访问。
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
/// <summary>
|
||||
/// 是否启用元数据输出。
|
||||
/// </summary>
|
||||
public bool IsMetadataEnabled { get; set; } = true;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
namespace Hua.Todo.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Dynamic API 服务标记接口。
|
||||
/// 实现该接口的服务会被 Dynamic API 中间件发现并按约定暴露为 HTTP API。
|
||||
/// </summary>
|
||||
public interface IDynamicApiService
|
||||
{
|
||||
}
|
||||
|
||||
@@ -2,15 +2,67 @@ using Hua.Todo.Application.Models;
|
||||
|
||||
namespace Hua.Todo.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// 任务管理服务接口
|
||||
/// </summary>
|
||||
public interface ITaskService : IDynamicApiService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取所有任务
|
||||
/// </summary>
|
||||
/// <returns>包含所有任务的列表</returns>
|
||||
Task<List<TaskDto>> GetAllTasksAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ID 获取任务
|
||||
/// </summary>
|
||||
/// <param name="id">任务唯一标识符</param>
|
||||
/// <returns>匹配的任务 DTO,如果未找到则返回 null</returns>
|
||||
Task<TaskDto?> GetTaskByIdAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// 获取未完成的任务
|
||||
/// </summary>
|
||||
/// <returns>未完成任务的列表</returns>
|
||||
Task<List<TaskDto>> GetActiveTasksAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 获取已完成的任务
|
||||
/// </summary>
|
||||
/// <returns>已完成任务的列表</returns>
|
||||
Task<List<TaskDto>> GetCompletedTasksAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 创建新任务
|
||||
/// </summary>
|
||||
/// <param name="dto">创建任务所需的数据传输对象</param>
|
||||
/// <returns>创建成功的任务 DTO</returns>
|
||||
Task<TaskDto> CreateTaskAsync(CreateTaskDto dto);
|
||||
|
||||
/// <summary>
|
||||
/// 更新任务信息
|
||||
/// </summary>
|
||||
/// <param name="dto">包含更新信息的任务数据传输对象</param>
|
||||
/// <returns>更新后的任务 DTO</returns>
|
||||
Task<TaskDto> UpdateTaskAsync(UpdateTaskDto dto);
|
||||
|
||||
/// <summary>
|
||||
/// 切换任务完成状态
|
||||
/// </summary>
|
||||
/// <param name="id">任务唯一标识符</param>
|
||||
/// <returns>更新状态后的任务 DTO</returns>
|
||||
Task<TaskDto> ToggleCompleteAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// 删除任务
|
||||
/// </summary>
|
||||
/// <param name="id">要删除的任务唯一标识符</param>
|
||||
Task DeleteTaskAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// 获取子任务列表
|
||||
/// </summary>
|
||||
/// <param name="parentTaskId">父任务唯一标识符</param>
|
||||
/// <returns>该父任务下的所有子任务列表</returns>
|
||||
Task<List<TaskDto>> GetSubTasksAsync(int parentTaskId);
|
||||
}
|
||||
|
||||
@@ -3,45 +3,112 @@ using System.Text.Json.Serialization;
|
||||
|
||||
namespace Hua.Todo.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 创建任务请求 DTO。
|
||||
/// </summary>
|
||||
public class CreateTaskDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务标题。
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
/// <summary>
|
||||
/// 任务优先级。
|
||||
/// </summary>
|
||||
public TaskPriority Priority { get; set; } = TaskPriority.Medium;
|
||||
|
||||
/// <summary>
|
||||
/// 父任务 ID(用于创建子任务)。
|
||||
/// </summary>
|
||||
public int? ParentTaskId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新任务请求 DTO。
|
||||
/// </summary>
|
||||
public class UpdateTaskDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务 ID。
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 新标题(可选)。
|
||||
/// </summary>
|
||||
public string? Title { get; set; }
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
/// <summary>
|
||||
/// 新优先级(可选)。
|
||||
/// </summary>
|
||||
public TaskPriority? Priority { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务返回 DTO。
|
||||
/// </summary>
|
||||
public class TaskDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务 ID。
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
/// <summary>
|
||||
/// 任务标题。
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
/// <summary>
|
||||
/// 任务优先级。
|
||||
/// </summary>
|
||||
public TaskPriority Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否已完成。
|
||||
/// </summary>
|
||||
public bool IsCompleted { get; set; }
|
||||
/// <summary>
|
||||
/// 创建时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
/// <summary>
|
||||
/// 更新时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
/// <summary>
|
||||
/// 父任务 ID(可选)。
|
||||
/// </summary>
|
||||
public int? ParentTaskId { get; set; }
|
||||
/// <summary>
|
||||
/// 子任务列表。
|
||||
/// </summary>
|
||||
public List<TaskDto> SubTasks { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通用 API 响应包装。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">数据类型。</typeparam>
|
||||
public class ApiResponse<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否成功。
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
/// <summary>
|
||||
/// 返回数据(可选)。
|
||||
/// </summary>
|
||||
public T? Data { get; set; }
|
||||
/// <summary>
|
||||
/// 提示信息。
|
||||
/// </summary>
|
||||
public string Message { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// 错误列表。
|
||||
/// </summary>
|
||||
public List<string> Errors { get; set; } = new();
|
||||
}
|
||||
|
||||
@@ -5,15 +5,26 @@ using Hua.Todo.Core.Interfaces;
|
||||
|
||||
namespace Hua.Todo.Application.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 任务仓储实现(EF Core)。
|
||||
/// </summary>
|
||||
public class TaskRepository : ITaskRepository
|
||||
{
|
||||
private readonly TodoDbContext _context;
|
||||
|
||||
/// <summary>
|
||||
/// 创建 <see cref="TaskRepository"/>。
|
||||
/// </summary>
|
||||
/// <param name="context">数据库上下文。</param>
|
||||
public TaskRepository(TodoDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有任务。
|
||||
/// </summary>
|
||||
/// <returns>包含所有任务实体的列表。</returns>
|
||||
public async Task<List<TaskEntity>> GetAllAsync()
|
||||
{
|
||||
return await _context.Tasks
|
||||
@@ -21,6 +32,11 @@ public class TaskRepository : ITaskRepository
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ID 获取任务。
|
||||
/// </summary>
|
||||
/// <param name="id">任务 ID。</param>
|
||||
/// <returns>匹配的任务实体;如果不存在则返回 null。</returns>
|
||||
public async Task<TaskEntity?> GetByIdAsync(int id)
|
||||
{
|
||||
return await _context.Tasks
|
||||
@@ -28,6 +44,10 @@ public class TaskRepository : ITaskRepository
|
||||
.FirstOrDefaultAsync(t => t.Id == id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取未完成任务列表。
|
||||
/// </summary>
|
||||
/// <returns>未完成的任务实体列表。</returns>
|
||||
public async Task<List<TaskEntity>> GetActiveTasksAsync()
|
||||
{
|
||||
return await _context.Tasks
|
||||
@@ -36,6 +56,10 @@ public class TaskRepository : ITaskRepository
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取已完成任务列表。
|
||||
/// </summary>
|
||||
/// <returns>已完成的任务实体列表。</returns>
|
||||
public async Task<List<TaskEntity>> GetCompletedTasksAsync()
|
||||
{
|
||||
return await _context.Tasks
|
||||
@@ -44,6 +68,11 @@ public class TaskRepository : ITaskRepository
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增一个任务。
|
||||
/// </summary>
|
||||
/// <param name="taskEntity">要添加的任务实体。</param>
|
||||
/// <returns>已持久化的任务实体(包含生成的 ID)。</returns>
|
||||
public async Task<TaskEntity> AddAsync(TaskEntity taskEntity)
|
||||
{
|
||||
_context.Tasks.Add(taskEntity);
|
||||
@@ -51,6 +80,11 @@ public class TaskRepository : ITaskRepository
|
||||
return taskEntity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新现有任务信息。
|
||||
/// </summary>
|
||||
/// <param name="taskEntity">要更新的任务实体。</param>
|
||||
/// <returns>更新后的任务实体。</returns>
|
||||
public async Task<TaskEntity> UpdateAsync(TaskEntity taskEntity)
|
||||
{
|
||||
taskEntity.UpdatedAt = DateTime.UtcNow;
|
||||
@@ -59,6 +93,11 @@ public class TaskRepository : ITaskRepository
|
||||
return taskEntity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ID 删除任务。
|
||||
/// </summary>
|
||||
/// <param name="id">要删除的任务 ID。</param>
|
||||
/// <returns>表示删除操作的任务。</returns>
|
||||
public async Task DeleteAsync(int id)
|
||||
{
|
||||
var task = await _context.Tasks.FindAsync(id);
|
||||
@@ -69,6 +108,11 @@ public class TaskRepository : ITaskRepository
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定父任务的子任务列表。
|
||||
/// </summary>
|
||||
/// <param name="parentTaskId">父任务 ID。</param>
|
||||
/// <returns>子任务实体的列表。</returns>
|
||||
public async Task<List<TaskEntity>> GetSubTasksAsync(int parentTaskId)
|
||||
{
|
||||
return await _context.Tasks
|
||||
|
||||
@@ -9,8 +9,17 @@ using ITaskService = Hua.Todo.Application.Interfaces.ITaskService;
|
||||
|
||||
namespace Hua.Todo.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 应用层依赖注入扩展。
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册应用层服务与 EF Core DbContext。
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合。</param>
|
||||
/// <param name="connectionString">数据库连接字符串。</param>
|
||||
/// <returns>服务集合。</returns>
|
||||
public static IServiceCollection AddApplicationServices(this IServiceCollection services, string connectionString)
|
||||
{
|
||||
services.AddDbContext<TodoDbContext>(options =>
|
||||
|
||||
@@ -5,39 +5,68 @@ using Hua.Todo.Core.Interfaces;
|
||||
|
||||
namespace Hua.Todo.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 任务管理服务实现
|
||||
/// </summary>
|
||||
public class TaskService : ITaskService
|
||||
{
|
||||
private readonly ITaskRepository _taskRepository;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化任务管理服务的新实例
|
||||
/// </summary>
|
||||
/// <param name="taskRepository">任务仓储接口</param>
|
||||
public TaskService(ITaskRepository taskRepository)
|
||||
{
|
||||
_taskRepository = taskRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有任务
|
||||
/// </summary>
|
||||
/// <returns>所有任务的 DTO 列表</returns>
|
||||
public async Task<List<TaskDto>> GetAllTasksAsync()
|
||||
{
|
||||
var tasks = await _taskRepository.GetAllAsync();
|
||||
return tasks.Select(MapToDto).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ID 获取任务详情
|
||||
/// </summary>
|
||||
/// <param name="id">任务 ID</param>
|
||||
/// <returns>找到的任务 DTO,如果不存在则返回 null</returns>
|
||||
public async Task<TaskDto?> GetTaskByIdAsync(int id)
|
||||
{
|
||||
var task = await _taskRepository.GetByIdAsync(id);
|
||||
return task != null ? MapToDto(task) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有未完成的任务
|
||||
/// </summary>
|
||||
/// <returns>未完成任务的 DTO 列表</returns>
|
||||
public async Task<List<TaskDto>> GetActiveTasksAsync()
|
||||
{
|
||||
var allTasks = await _taskRepository.GetAllAsync();
|
||||
return allTasks.Where(t => !t.IsCompleted).Select(MapToDto).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有已完成的任务
|
||||
/// </summary>
|
||||
/// <returns>已完成任务的 DTO 列表</returns>
|
||||
public async Task<List<TaskDto>> GetCompletedTasksAsync()
|
||||
{
|
||||
var allTasks = await _taskRepository.GetAllAsync();
|
||||
return allTasks.Where(t => t.IsCompleted).Select(MapToDto).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建新任务
|
||||
/// </summary>
|
||||
/// <param name="dto">任务创建 DTO</param>
|
||||
/// <returns>新创建的任务 DTO</returns>
|
||||
public async Task<TaskDto> CreateTaskAsync(CreateTaskDto dto)
|
||||
{
|
||||
var task = new TaskEntity
|
||||
@@ -54,6 +83,12 @@ public class TaskService : ITaskService
|
||||
return MapToDto(createdTask);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新现有任务
|
||||
/// </summary>
|
||||
/// <param name="dto">包含更新内容的 DTO</param>
|
||||
/// <returns>更新后的任务 DTO</returns>
|
||||
/// <exception cref="KeyNotFoundException">当任务 ID 不存在时抛出</exception>
|
||||
public async Task<TaskDto> UpdateTaskAsync(UpdateTaskDto dto)
|
||||
{
|
||||
var task = await _taskRepository.GetByIdAsync(dto.Id);
|
||||
@@ -78,6 +113,12 @@ public class TaskService : ITaskService
|
||||
return MapToDto(updatedTask);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换任务的完成状态
|
||||
/// </summary>
|
||||
/// <param name="id">任务 ID</param>
|
||||
/// <returns>状态切换后的任务 DTO</returns>
|
||||
/// <exception cref="KeyNotFoundException">当任务 ID 不存在时抛出</exception>
|
||||
public async Task<TaskDto> ToggleCompleteAsync(int id)
|
||||
{
|
||||
var task = await _taskRepository.GetByIdAsync(id);
|
||||
@@ -93,6 +134,11 @@ public class TaskService : ITaskService
|
||||
return MapToDto(updatedTask);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除指定 ID 的任务
|
||||
/// </summary>
|
||||
/// <param name="id">任务 ID</param>
|
||||
/// <exception cref="KeyNotFoundException">当任务 ID 不存在时抛出</exception>
|
||||
public async Task DeleteTaskAsync(int id)
|
||||
{
|
||||
var task = await _taskRepository.GetByIdAsync(id);
|
||||
@@ -104,12 +150,20 @@ public class TaskService : ITaskService
|
||||
await _taskRepository.DeleteAsync(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定父任务的所有子任务
|
||||
/// </summary>
|
||||
/// <param name="parentTaskId">父任务 ID</param>
|
||||
/// <returns>子任务的 DTO 列表</returns>
|
||||
public async Task<List<TaskDto>> GetSubTasksAsync(int parentTaskId)
|
||||
{
|
||||
var allTasks = await _taskRepository.GetAllAsync();
|
||||
return allTasks.Where(t => t.ParentTaskId == parentTaskId).Select(MapToDto).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实体转 DTO 映射方法
|
||||
/// </summary>
|
||||
private TaskDto MapToDto(TaskEntity task)
|
||||
{
|
||||
return new TaskDto
|
||||
|
||||
@@ -10,52 +10,53 @@ public interface ITaskRepository
|
||||
/// <summary>
|
||||
/// 获取所有任务
|
||||
/// </summary>
|
||||
/// <returns>任务列表</returns>
|
||||
/// <returns>包含所有任务实体的列表任务</returns>
|
||||
System.Threading.Tasks.Task<List<TaskEntity>> GetAllAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取指定任务
|
||||
/// </summary>
|
||||
/// <param name="id">任务ID</param>
|
||||
/// <returns>任务对象,如果不存在则返回null</returns>
|
||||
/// <param name="id">任务唯一标识符</param>
|
||||
/// <returns>任务实体对象,如果不存在则返回 null 的任务</returns>
|
||||
System.Threading.Tasks.Task<TaskEntity?> GetByIdAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有未完成的任务
|
||||
/// </summary>
|
||||
/// <returns>未完成任务列表</returns>
|
||||
/// <returns>未完成任务实体的列表任务</returns>
|
||||
System.Threading.Tasks.Task<List<TaskEntity>> GetActiveTasksAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有已完成的任务
|
||||
/// </summary>
|
||||
/// <returns>已完成任务列表</returns>
|
||||
/// <returns>已完成任务实体的列表任务</returns>
|
||||
System.Threading.Tasks.Task<List<TaskEntity>> GetCompletedTasksAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 添加新任务
|
||||
/// </summary>
|
||||
/// <param name="taskEntity">要添加的任务对象</param>
|
||||
/// <returns>添加后的任务对象(包含生成的ID)</returns>
|
||||
/// <param name="taskEntity">要添加的任务实体对象</param>
|
||||
/// <returns>添加后的任务实体(包含生成的 ID)的任务</returns>
|
||||
System.Threading.Tasks.Task<TaskEntity> AddAsync(TaskEntity taskEntity);
|
||||
|
||||
/// <summary>
|
||||
/// 更新任务
|
||||
/// </summary>
|
||||
/// <param name="taskEntity">要更新的任务对象</param>
|
||||
/// <returns>更新后的任务对象</returns>
|
||||
/// <param name="taskEntity">要更新的任务实体对象</param>
|
||||
/// <returns>更新后的任务实体对象的任务</returns>
|
||||
System.Threading.Tasks.Task<TaskEntity> UpdateAsync(TaskEntity taskEntity);
|
||||
|
||||
/// <summary>
|
||||
/// 删除指定ID的任务
|
||||
/// </summary>
|
||||
/// <param name="id">任务ID</param>
|
||||
/// <param name="id">要删除的任务唯一标识符</param>
|
||||
/// <returns>表示删除操作的任务</returns>
|
||||
System.Threading.Tasks.Task DeleteAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定父任务的所有子任务
|
||||
/// </summary>
|
||||
/// <param name="parentTaskId">父任务ID</param>
|
||||
/// <returns>子任务列表</returns>
|
||||
/// <param name="parentTaskId">父任务唯一标识符</param>
|
||||
/// <returns>子任务实体的列表任务</returns>
|
||||
System.Threading.Tasks.Task<List<TaskEntity>> GetSubTasksAsync(int parentTaskId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#if !WINDOWS
|
||||
using Microsoft.Maui.Controls;
|
||||
|
||||
namespace Hua.Todo.Maui;
|
||||
|
||||
/// <summary>
|
||||
/// 非 Windows 平台下 <see cref="App"/> 的默认实现(分部类)。
|
||||
/// 用于提供 Windows 专属分部方法的默认行为,避免在其它平台出现缺失实现的编译错误。
|
||||
/// </summary>
|
||||
public partial class App
|
||||
{
|
||||
/// <summary>
|
||||
/// 非 Windows 平台不依赖 WinUI 句柄,因此视为已就绪。
|
||||
/// </summary>
|
||||
/// <param name="window">MAUI Window。</param>
|
||||
/// <returns>始终返回 true。</returns>
|
||||
private partial bool IsPlatformViewReady(Window window)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 非 Windows 平台不处理“恢复/激活窗口”等特定逻辑,由调用方走默认 OpenWindow 流程。
|
||||
/// </summary>
|
||||
/// <param name="window">MAUI Window。</param>
|
||||
/// <returns>始终返回 false。</returns>
|
||||
private partial bool PlatformShowMainWindow(Window window)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+62
-166
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 应用程序主入口类。
|
||||
/// 该类仅包含跨平台通用逻辑;各平台差异通过分部类拆分到 Platforms 目录中,避免在同一文件内混写大量条件编译代码。
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 创建 <see cref="App"/> 实例。
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">依赖注入容器。</param>
|
||||
public App(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
@@ -33,7 +35,10 @@ public partial class App : global::Microsoft.Maui.Controls.Application
|
||||
_trayService = serviceProvider.GetRequiredService<ISystemTrayService>();
|
||||
}
|
||||
|
||||
protected override Microsoft.Maui.Controls.Window CreateWindow(IActivationState? activationState)
|
||||
/// <summary>
|
||||
/// 创建主窗体,并绑定系统托盘与热键等生命周期逻辑。
|
||||
/// </summary>
|
||||
protected override Microsoft.Maui.Controls.Window CreateWindow(IActivationState? activationState)
|
||||
{
|
||||
_mainWindow = new Microsoft.Maui.Controls.Window(_serviceProvider.GetRequiredService<MainPage>())
|
||||
{
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当平台视图准备就绪时注册热键。
|
||||
/// 在某些平台(尤其 Windows)中,窗口 Handler 创建具有延迟,过早注册可能失败,因此采用重试策略。
|
||||
/// </summary>
|
||||
/// <param name="attempt">当前重试次数。</param>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 热键按下时的回调
|
||||
/// </summary>
|
||||
private void OnHotKeyPressed()
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
@@ -109,36 +106,41 @@ protected override Microsoft.Maui.Controls.Window CreateWindow(IActivationState?
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示主窗口。
|
||||
/// 优先使用平台特定逻辑(例如 Windows 恢复并激活窗口),否则走默认 MAUI 打开窗口流程。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 退出应用程序
|
||||
/// </summary>
|
||||
private void ExitApplication()
|
||||
{
|
||||
_trayService?.Dispose();
|
||||
global::Microsoft.Maui.Controls.Application.Current?.Quit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册全局热键,并监听配置变更以动态更新。
|
||||
/// </summary>
|
||||
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);
|
||||
/// <summary>
|
||||
/// 判断平台视图是否已就绪(用于热键/窗口等依赖平台句柄的逻辑)。
|
||||
/// </summary>
|
||||
/// <param name="window">MAUI Window。</param>
|
||||
/// <returns>视图已就绪返回 true;否则返回 false。</returns>
|
||||
private partial bool IsPlatformViewReady(Window window);
|
||||
/// <summary>
|
||||
/// 使用平台特定方式显示主窗口。
|
||||
/// </summary>
|
||||
/// <param name="window">MAUI Window。</param>
|
||||
/// <returns>如果平台已处理显示逻辑返回 true;否则返回 false。</returns>
|
||||
private partial bool PlatformShowMainWindow(Window window);
|
||||
}
|
||||
|
||||
@@ -10,8 +10,14 @@ using Hua.Todo.Maui.Services.Platforms;
|
||||
|
||||
namespace Hua.Todo.Maui;
|
||||
|
||||
/// <summary>
|
||||
/// MAUI 程序启动类
|
||||
/// </summary>
|
||||
public static class MauiProgram
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建并配置 MAUI 应用程序
|
||||
/// </summary>
|
||||
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<Views.MainPage>();
|
||||
|
||||
// 注册热键设置服务
|
||||
builder.Services.AddSingleton<IHotKeySettingsService>(sp =>
|
||||
new HotKeySettingsService(sp.GetRequiredService<AppSettings>()));
|
||||
|
||||
// 注册全局热键服务(工厂模式)
|
||||
builder.Services.AddSingleton<IGlobalHotKeyService>(sp => GlobalHotKeyServiceFactory.Create());
|
||||
|
||||
// 注册系统托盘服务(平台相关)
|
||||
builder.Services.AddSingleton<ISystemTrayService>(sp =>
|
||||
{
|
||||
#if WINDOWS
|
||||
@@ -55,6 +68,8 @@ public static class MauiProgram
|
||||
return new NullSystemTrayService();
|
||||
#endif
|
||||
});
|
||||
|
||||
// 注册嵌入式 Web 服务器(平台相关)
|
||||
#if WINDOWS
|
||||
builder.Services.AddSingleton<IEmbeddedWebServerService, EmbeddedWebServerService>();
|
||||
#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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 appsettings.json 加载配置
|
||||
/// </summary>
|
||||
private static AppSettings LoadAppSettings()
|
||||
{
|
||||
try
|
||||
@@ -114,6 +133,9 @@ public static class MauiProgram
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化数据库(执行迁移)
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动嵌入式 Web 服务器
|
||||
/// </summary>
|
||||
private static async Task StartWebServer(IServiceProvider services)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -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)]
|
||||
/// <summary>
|
||||
/// Android 平台入口 Activity。
|
||||
/// </summary>
|
||||
public class MainActivity : MauiAppCompatActivity
|
||||
{
|
||||
/// <summary>
|
||||
/// Activity 创建时回调。
|
||||
/// </summary>
|
||||
/// <param name="savedInstanceState">上次保存状态。</param>
|
||||
protected override void OnCreate(Bundle? savedInstanceState)
|
||||
{
|
||||
base.OnCreate(savedInstanceState);
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
using Android.App;
|
||||
using Android.App;
|
||||
using Android.Runtime;
|
||||
|
||||
namespace Hua.Todo.Maui;
|
||||
|
||||
[Application]
|
||||
/// <summary>
|
||||
/// Android 平台 Application。
|
||||
/// </summary>
|
||||
public class MainApplication : MauiApplication
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建 <see cref="MainApplication"/>。
|
||||
/// </summary>
|
||||
/// <param name="handle">JNI 句柄。</param>
|
||||
/// <param name="ownership">句柄所有权。</param>
|
||||
public MainApplication(IntPtr handle, JniHandleOwnership ownership)
|
||||
: base(handle, ownership)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 MAUI 应用。
|
||||
/// </summary>
|
||||
/// <returns>MAUI 应用实例。</returns>
|
||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ using Hua.Todo.Maui.Models;
|
||||
|
||||
namespace Hua.Todo.Maui.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Android 平台嵌入式 Web 服务器服务。
|
||||
/// 使用 TcpListener 自定义实现简单的 HTTP 服务器,用于离线模式下托管 API 和静态资源。
|
||||
/// </summary>
|
||||
public sealed class MobileEmbeddedWebServerService : IEmbeddedWebServerService
|
||||
{
|
||||
private readonly AppSettings _appSettings;
|
||||
@@ -18,15 +22,34 @@ public sealed class MobileEmbeddedWebServerService : IEmbeddedWebServerService
|
||||
private CancellationTokenSource? _cts;
|
||||
private Task? _acceptLoop;
|
||||
|
||||
/// <summary>
|
||||
/// 服务器是否正在运行。
|
||||
/// </summary>
|
||||
public bool IsRunning => _listener != null;
|
||||
|
||||
/// <summary>
|
||||
/// 服务器基础 URL。
|
||||
/// </summary>
|
||||
public string BaseUrl => $"http://localhost:{_appSettings.WebServer.Port}";
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 <see cref="MobileEmbeddedWebServerService"/>。
|
||||
/// </summary>
|
||||
/// <param name="appSettings">应用程序配置。</param>
|
||||
/// <param name="services">依赖注入服务提供者。</param>
|
||||
public MobileEmbeddedWebServerService(AppSettings appSettings, IServiceProvider services)
|
||||
{
|
||||
_appSettings = appSettings;
|
||||
_services = services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步启动服务器。
|
||||
/// 启动时机:在 Android 平台初始化或手动开启时调用。
|
||||
/// 错误处理:启动失败会记录日志,建议外部调用时关注 <see cref="IsRunning"/>。
|
||||
/// 线程安全:可在任何线程调用;内部通过状态检查避免重复启动。
|
||||
/// </summary>
|
||||
/// <returns>表示启动操作的任务。</returns>
|
||||
public Task StartAsync()
|
||||
{
|
||||
if (_listener != null) return Task.CompletedTask;
|
||||
@@ -39,6 +62,11 @@ public sealed class MobileEmbeddedWebServerService : IEmbeddedWebServerService
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步停止服务器。
|
||||
/// 释放所有资源并停止监听。
|
||||
/// </summary>
|
||||
/// <returns>表示停止操作的任务。</returns>
|
||||
public async Task StopAsync()
|
||||
{
|
||||
if (_listener == null) return;
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Windows 平台特定的 App 逻辑(分部类)。
|
||||
/// 该文件仅在 Windows 目标框架下参与编译,用于承载窗口句柄相关能力与 WinUI API 调用。
|
||||
/// </summary>
|
||||
public partial class App
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化 Windows 平台特定设置。
|
||||
/// 例如:自定义 MAUI TitleBar(不是 WinUI TitleBar)。
|
||||
/// </summary>
|
||||
partial void InitializePlatform()
|
||||
{
|
||||
if (_mainWindow != null)
|
||||
{
|
||||
_mainWindow.TitleBar = CreateWindowTitleBar();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Windows 平台窗口创建后的处理。
|
||||
/// 该回调发生在 Window Created 之后,但依然可能需要等待 Handler/PlatformView 就绪,因此使用 UI 线程调度。
|
||||
/// </summary>
|
||||
/// <param name="window">MAUI Window。</param>
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查 Windows 平台视图是否准备就绪
|
||||
/// </summary>
|
||||
/// <param name="window">MAUI Window。</param>
|
||||
/// <returns>当 PlatformView 是 WinUI Window 时返回 true。</returns>
|
||||
private partial bool IsPlatformViewReady(Window window)
|
||||
{
|
||||
return window.Handler?.PlatformView is WinUiWindow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在 Windows 平台上显示主窗口
|
||||
/// </summary>
|
||||
/// <param name="window">MAUI Window。</param>
|
||||
/// <returns>已恢复并激活窗口返回 true;否则返回 false。</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 Windows 标题栏(WinUI AppWindow.TitleBar)。
|
||||
/// 用于设置窗口标题、图标以及标题栏按钮样式等。
|
||||
/// </summary>
|
||||
/// <param name="platformWindow">WinUI Window。</param>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// 创建 MAUI 自定义标题栏内容。
|
||||
/// 用于在窗口标题栏区域展示应用图标与版本信息。
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 居中显示主窗口
|
||||
/// </summary>
|
||||
/// <param name="platformWindow">WinUI Window。</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Microsoft.Maui.Controls;
|
||||
|
||||
namespace Hua.Todo.Maui.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// Windows 平台特定的 MainPage 逻辑(分部类)。
|
||||
/// 该文件仅在 Windows 目标框架下参与编译,用于接入 Windows 全局键盘事件等能力。
|
||||
/// </summary>
|
||||
public partial class MainPage
|
||||
{
|
||||
private Platforms.Windows.WindowsKeyboardHandler? _keyboardHandler;
|
||||
|
||||
/// <summary>
|
||||
/// Windows 平台特定的键盘处理器设置。
|
||||
/// 当前仅监听 Esc 键,用于快速最小化窗口(与桌面端交互习惯保持一致)。
|
||||
/// </summary>
|
||||
partial void PlatformSetupKeyboardHandler()
|
||||
{
|
||||
_keyboardHandler = new Platforms.Windows.WindowsKeyboardHandler();
|
||||
_keyboardHandler.EscKeyPressed += OnEscKeyPressed;
|
||||
_keyboardHandler.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Windows 平台特定的 Esc 键处理(最小化窗口)。
|
||||
/// </summary>
|
||||
/// <param name="window">当前窗口。</param>
|
||||
partial void PlatformOnEscKeyPressed(Window window)
|
||||
{
|
||||
var windowService = new Platforms.Windows.WindowsWindowService();
|
||||
windowService.MinimizeWindow(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,32 @@ using System.Runtime.InteropServices;
|
||||
|
||||
namespace Hua.Todo.Maui.Platforms.Windows
|
||||
{
|
||||
/// <summary>
|
||||
/// Windows 全局键盘事件处理器。
|
||||
/// 用于监听按键并向上层暴露事件(例如 Esc)。
|
||||
/// </summary>
|
||||
public class WindowsKeyboardHandler : IDisposable
|
||||
{
|
||||
private KeyboardHook _keyboardHook;
|
||||
private bool _isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// 当检测到 Esc 键抬起时触发。
|
||||
/// </summary>
|
||||
public event EventHandler? EscKeyPressed;
|
||||
|
||||
/// <summary>
|
||||
/// 创建 <see cref="WindowsKeyboardHandler"/>。
|
||||
/// </summary>
|
||||
public WindowsKeyboardHandler()
|
||||
{
|
||||
_keyboardHook = new KeyboardHook();
|
||||
_keyboardHook.KeyPressed += OnKeyPressed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开始监听键盘事件。
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
_keyboardHook.Hook();
|
||||
@@ -28,6 +41,9 @@ namespace Hua.Todo.Maui.Platforms.Windows
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止监听并释放资源。
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_isDisposed)
|
||||
@@ -126,4 +142,4 @@ namespace Hua.Todo.Maui.Platforms.Windows
|
||||
IsKeyDown = isKeyDown;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ using WinRT.Interop;
|
||||
|
||||
namespace Hua.Todo.Maui.Platforms.Windows
|
||||
{
|
||||
/// <summary>
|
||||
/// Windows 平台窗口操作服务。
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏窗口(不退出进程)。
|
||||
/// </summary>
|
||||
/// <param name="window">MAUI Window。</param>
|
||||
public void HideWindow(Window window)
|
||||
{
|
||||
if (window == null) return;
|
||||
@@ -29,6 +36,10 @@ namespace Hua.Todo.Maui.Platforms.Windows
|
||||
ShowWindow(hWnd, SW_HIDE);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复窗口并显示。
|
||||
/// </summary>
|
||||
/// <param name="window">MAUI Window。</param>
|
||||
public void RestoreWindow(Window window)
|
||||
{
|
||||
if (window == null) return;
|
||||
@@ -44,6 +55,10 @@ namespace Hua.Todo.Maui.Platforms.Windows
|
||||
ShowWindow(hWnd, SW_RESTORE);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 最小化窗口。
|
||||
/// </summary>
|
||||
/// <param name="window">MAUI Window。</param>
|
||||
public void MinimizeWindow(Window window)
|
||||
{
|
||||
if (window == null) return;
|
||||
|
||||
@@ -13,19 +13,41 @@ using AppSettings = Hua.Todo.Maui.Models.AppSettings;
|
||||
|
||||
namespace Hua.Todo.Maui.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Windows 平台嵌入式 Web 服务器实现
|
||||
/// 使用 ASP.NET Core 运行
|
||||
/// </summary>
|
||||
public class EmbeddedWebServerService : IEmbeddedWebServerService
|
||||
{
|
||||
private WebApplication? _webApp;
|
||||
private readonly AppSettings _appSettings;
|
||||
|
||||
/// <summary>
|
||||
/// 服务器是否正在运行
|
||||
/// </summary>
|
||||
public bool IsRunning => _webApp != null;
|
||||
|
||||
/// <summary>
|
||||
/// 服务器基础 URL
|
||||
/// </summary>
|
||||
public string BaseUrl => _appSettings.WebServer.HostUrl;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化嵌入式 Web 服务器服务
|
||||
/// </summary>
|
||||
/// <param name="appSettings">应用程序配置</param>
|
||||
public EmbeddedWebServerService(AppSettings appSettings)
|
||||
{
|
||||
_appSettings = appSettings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步启动服务器。
|
||||
/// 启动时机:通常在应用启动或用户手动开启服务时调用。
|
||||
/// 错误处理:启动失败会抛出 ASP.NET Core 相关异常,建议在调用方进行 catch 处理。
|
||||
/// 线程安全:非 UI 线程相关,可在后台线程调用;方法内部已处理重入(若已运行则直接返回)。
|
||||
/// </summary>
|
||||
/// <returns>表示启动操作的任务</returns>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置静态文件服务(用于托管 Vue 前端)
|
||||
/// </summary>
|
||||
/// <param name="app">Web 应用程序实例</param>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步停止服务器。
|
||||
/// 释放服务器资源并停止监听。
|
||||
/// </summary>
|
||||
/// <returns>表示停止操作的任务</returns>
|
||||
public async Task StopAsync()
|
||||
{
|
||||
if (_webApp == null) return;
|
||||
|
||||
@@ -4,13 +4,30 @@ using Hua.Todo.Maui.Models;
|
||||
|
||||
namespace Hua.Todo.Maui.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// 热键设置服务接口
|
||||
/// </summary>
|
||||
public interface IHotKeySettingsService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取热键配置
|
||||
/// </summary>
|
||||
HotKeyConfig GetConfig();
|
||||
|
||||
/// <summary>
|
||||
/// 保存热键配置
|
||||
/// </summary>
|
||||
void SaveConfig(HotKeyConfig config);
|
||||
|
||||
/// <summary>
|
||||
/// 重置为默认配置
|
||||
/// </summary>
|
||||
void ResetToDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 热键设置服务实现,使用 Preferences 存储配置
|
||||
/// </summary>
|
||||
public class HotKeySettingsService : IHotKeySettingsService
|
||||
{
|
||||
private const string SettingsKey = "HotKeyConfig";
|
||||
@@ -21,6 +38,9 @@ namespace Hua.Todo.Maui.Services
|
||||
_appSettings = appSettings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前热键配置,如果不存在则返回默认配置
|
||||
/// </summary>
|
||||
public HotKeyConfig GetConfig()
|
||||
{
|
||||
var json = Preferences.Get(SettingsKey, string.Empty);
|
||||
@@ -39,18 +59,27 @@ namespace Hua.Todo.Maui.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将热键配置序列化并保存到 Preferences
|
||||
/// </summary>
|
||||
public void SaveConfig(HotKeyConfig config)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(config);
|
||||
Preferences.Set(SettingsKey, json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置为从 appsettings.json 中读取的默认值
|
||||
/// </summary>
|
||||
public void ResetToDefault()
|
||||
{
|
||||
var defaultConfig = GetDefaultConfig();
|
||||
SaveConfig(defaultConfig);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取默认热键配置
|
||||
/// </summary>
|
||||
private HotKeyConfig GetDefaultConfig()
|
||||
{
|
||||
return new HotKeyConfig
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
namespace Hua.Todo.Maui.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 嵌入式 Web 服务器服务接口
|
||||
/// </summary>
|
||||
public interface IEmbeddedWebServerService
|
||||
{
|
||||
/// <summary>
|
||||
/// 服务器是否正在运行
|
||||
/// </summary>
|
||||
bool IsRunning { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务器基础 URL
|
||||
/// </summary>
|
||||
string BaseUrl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 启动服务器
|
||||
/// </summary>
|
||||
Task StartAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 停止服务器
|
||||
/// </summary>
|
||||
Task StopAsync();
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@ namespace Hua.Todo.Maui.Services.Platforms
|
||||
/// <summary>
|
||||
/// 注册全局热键
|
||||
/// </summary>
|
||||
/// <param name="modifiers">修饰键字符串,多个键用逗号分隔(如 "Control,Alt")</param>
|
||||
/// <param name="key">主键字符串(如 "X")</param>
|
||||
/// <param name="callback">热键触发时的回调操作</param>
|
||||
public void RegisterHotKey(string modifiers, string key, Action callback)
|
||||
{
|
||||
if (_window == null)
|
||||
@@ -97,6 +100,8 @@ namespace Hua.Todo.Maui.Services.Platforms
|
||||
/// <summary>
|
||||
/// 更新热键配置
|
||||
/// </summary>
|
||||
/// <param name="modifiers">新的修饰键字符串</param>
|
||||
/// <param name="key">新的主键字符串</param>
|
||||
public void UpdateHotKey(string modifiers, string key)
|
||||
{
|
||||
if (_callback != null)
|
||||
|
||||
@@ -4,14 +4,20 @@ using Hua.Todo.Maui.Services;
|
||||
|
||||
namespace Hua.Todo.Maui.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// 应用程序主页面。
|
||||
/// 该页面承载 WebView(前端 UI),并通过 JavaScript 注入与事件机制实现与 MAUI 的通讯。
|
||||
/// </summary>
|
||||
public partial class MainPage : ContentPage
|
||||
{
|
||||
private readonly AppSettings _appSettings;
|
||||
private readonly IEmbeddedWebServerService? _webServer;
|
||||
#if WINDOWS
|
||||
private Platforms.Windows.WindowsKeyboardHandler? _keyboardHandler;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// 创建 <see cref="MainPage"/>。
|
||||
/// </summary>
|
||||
/// <param name="appSettings">应用配置。</param>
|
||||
/// <param name="webServer">嵌入式 Web 服务器服务(在不同平台可能为不同实现)。</param>
|
||||
public MainPage(AppSettings appSettings, IEmbeddedWebServerService webServer)
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -23,7 +29,10 @@ namespace Hua.Todo.Maui.Views
|
||||
SetupKeyboardHandler();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 设置 WebView 数据源。
|
||||
/// 当启用嵌入式服务器且使用静态文件时,直接指向本地服务器;否则使用配置的前端 URL。
|
||||
/// </summary>
|
||||
private void SetupWebViewSource()
|
||||
{
|
||||
if (_appSettings.WebServer.IsUsingStatic)
|
||||
@@ -38,27 +47,32 @@ namespace Hua.Todo.Maui.Views
|
||||
MainWebView.Source = NormalizeUrl(_appSettings.WebServer.ForEndUrl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置键盘处理器(平台差异通过分部类实现)。
|
||||
/// </summary>
|
||||
private void SetupKeyboardHandler()
|
||||
{
|
||||
#if WINDOWS
|
||||
_keyboardHandler = new Platforms.Windows.WindowsKeyboardHandler();
|
||||
_keyboardHandler.EscKeyPressed += OnEscKeyPressed;
|
||||
_keyboardHandler.Start();
|
||||
#endif
|
||||
PlatformSetupKeyboardHandler();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当 Esc 键按下时的回调
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 WebView 通讯逻辑。
|
||||
/// 约定:
|
||||
/// - 通过 window.__API_BASE_URL__ 注入后端 API 基地址(仅在嵌入式服务器运行时)
|
||||
/// - 通过自定义事件在 Web 与 MAUI 间传递热键配置等数据
|
||||
/// </summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 规格化 URL(针对 Android 模拟器处理 localhost)。
|
||||
/// Android 模拟器中 localhost 指向模拟器自身,需要替换为 10.0.2.2 才能访问宿主机服务。
|
||||
/// </summary>
|
||||
private static string NormalizeUrl(string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url)) return url;
|
||||
@@ -116,5 +135,16 @@ namespace Hua.Todo.Maui.Views
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
// 平台特定分部方法声明(实现位于 Platforms/* 目录)
|
||||
/// <summary>
|
||||
/// 平台特定的键盘处理器初始化。
|
||||
/// </summary>
|
||||
partial void PlatformSetupKeyboardHandler();
|
||||
/// <summary>
|
||||
/// 平台特定的 Esc 键处理逻辑。
|
||||
/// </summary>
|
||||
/// <param name="window">当前窗口。</param>
|
||||
partial void PlatformOnEscKeyPressed(Window window);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ApiResponse<Task[]>> {
|
||||
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<ApiResponse<Task | null>> {
|
||||
if (!LocalStorageService.isOnline()) {
|
||||
const localTasks = LocalStorageService.loadTasks();
|
||||
@@ -83,6 +93,11 @@ export const taskApi = {
|
||||
return apiResponse;
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建新任务
|
||||
* @param dto 创建任务的数据传输对象
|
||||
* @returns 包含新创建任务的响应对象
|
||||
*/
|
||||
async createTask(dto: CreateTaskDto): Promise<ApiResponse<Task>> {
|
||||
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<ApiResponse<Task>> {
|
||||
const updateDto = dto;
|
||||
|
||||
@@ -203,6 +224,11 @@ export const taskApi = {
|
||||
return apiResponse;
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换任务完成状态
|
||||
* @param id 任务 ID
|
||||
* @returns 包含更新后任务的响应对象
|
||||
*/
|
||||
async toggleComplete(id: number): Promise<ApiResponse<Task>> {
|
||||
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<ApiResponse<object>> {
|
||||
if (!LocalStorageService.isOnline()) {
|
||||
const localTasks = LocalStorageService.loadTasks();
|
||||
@@ -309,6 +340,10 @@ export const taskApi = {
|
||||
return apiResponse;
|
||||
},
|
||||
|
||||
/**
|
||||
* 同步本地更改到服务器
|
||||
* @returns 包含最新任务列表的响应对象
|
||||
*/
|
||||
async syncToServer(): Promise<ApiResponse<Task[]>> {
|
||||
if (!LocalStorageService.isOnline()) {
|
||||
return {
|
||||
|
||||
@@ -15,7 +15,7 @@ export default defineConfig({
|
||||
port: 5174,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:5057',
|
||||
target: 'http://localhost:5173',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user