From aa5407daebd0d73642821fdeaac22cf0fc072b33 Mon Sep 17 00:00:00 2001
From: ShaoHua <345265198@qqcom>
Date: Sat, 4 Apr 2026 22:11:18 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=20TodoList=20?=
=?UTF-8?q?=E6=9E=B6=E6=9E=84=EF=BC=8C=E6=96=B0=E5=A2=9E=E5=8A=A8=E6=80=81?=
=?UTF-8?q?=20API=20=E4=B8=8E=20MAUI=20=E5=86=85=E5=B5=8C=20Web=20?=
=?UTF-8?q?=E6=9C=8D=E5=8A=A1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 2 +
.vscode/launch.json | 35 +
.vscode/tasks.json | 41 ++
Publish.ps1 | 46 ++
README.md | 2 +-
SCRIPTS_README.md | 10 +-
TodoList.slnx | 3 +-
TodoList/App.xaml.cs | 44 +-
TodoList/TodoList.csproj | 1 +
TodoList/ViewModels/MainViewModel.cs | 2 -
TodoList/Views/MainWindow.xaml | 2 +-
TodoList/Views/MainWindow.xaml.cs | 2 +-
docs/实现对比文档.md | 2 +-
restart-service.ps1 | 92 ---
restart-web.ps1 | 101 +++
.../Controllers/TasksController.cs | 291 --------
src/TodoList.Api/Models/TaskModels.cs | 117 ----
.../Repositories/TaskRepository.cs | 122 ----
src/TodoList.Api/Services/TaskService.cs | 185 -----
src/TodoList.Api/TodoList.Api.http | 6 -
.../Data/TodoDbContext.cs | 25 +-
.../DynamicApi/DynamicApiExtensions.cs | 11 +
.../DynamicApi/DynamicApiMiddleware.cs | 637 ++++++++++++++++++
.../DynamicApi/HttpAttributes.cs | 76 +++
.../DynamicApi/ParameterBindingAttributes.cs | 11 +
.../DynamicApi/RemoteServiceAttribute.cs | 8 +
.../Interfaces/IDynamicApiService.cs | 5 +
.../Interfaces/ITaskService.cs | 17 +
.../20260313044926_InitialCreate.Designer.cs | 8 +-
.../20260313044926_InitialCreate.cs | 4 +-
...20260313092658_AddParentTaskId.Designer.cs | 14 +-
.../20260313092658_AddParentTaskId.cs | 4 +-
.../Migrations/TodoDbContextModelSnapshot.cs | 14 +-
src/TodoList.Application/Models/TaskModels.cs | 47 ++
.../Properties/launchSettings.json | 12 +
.../Repositories/TaskRepository.cs | 79 +++
.../ServiceCollectionExtensions.cs | 23 +
.../Services/TaskService.cs | 127 ++++
.../TodoList.Application.csproj | 22 +
src/TodoList.Core/Class1.cs | 6 -
.../Entities/{Task.cs => TaskEntity.cs} | 6 +-
.../Interfaces/ITaskRepository.cs | 20 +-
src/TodoList.Core/Interfaces/ITaskService.cs | 73 --
src/TodoList.Core/TodoList.Core.csproj | 4 +-
.../Program.cs | 30 +-
.../Properties/launchSettings.json | 6 +-
.../TodoList.Host.csproj} | 6 +-
.../appsettings.Development.json | 0
.../appsettings.json | 0
.../todolist.db | Bin 32768 -> 32768 bytes
src/TodoList.Maui/App.xaml.cs | 230 +++++--
src/TodoList.Maui/AppShell.xaml | 2 -
src/TodoList.Maui/BuildSetup.ps1 | 34 +-
src/TodoList.Maui/MauiProgram.cs | 83 ++-
src/TodoList.Maui/Models/AppSettings.cs | 44 ++
.../Platforms/Windows/WindowsWindowService.cs | 42 +-
src/TodoList.Maui/README.md | 2 +-
.../Resources/Images/app_titlebar_icon.svg | 8 +
.../Resources/Images/titlebar_icon.png | Bin 0 -> 106636 bytes
src/TodoList.Maui/Services/AppMetadata.cs | 60 ++
.../Services/EmbeddedWebServerService.cs | 138 ++++
.../Services/HotKeySettingsService.cs | 48 +-
.../Services/IEmbeddedWebServerService.cs | 9 +
.../Platforms/WindowsGlobalHotKeyService.cs | 104 ++-
.../Platforms/WindowsSystemTrayService.cs | 17 +-
src/TodoList.Maui/TodoList.Maui.csproj | 100 ++-
.../TodoList.Maui.csproj.DotSettings | 2 +
.../ViewModels/QuickEntryViewModel.cs | 53 --
src/TodoList.Maui/Views/MainPage.xaml | 30 +-
src/TodoList.Maui/Views/MainPage.xaml.cs | 63 +-
src/TodoList.Maui/Views/QuickEntryPage.xaml | 46 --
.../Views/QuickEntryPage.xaml.cs | 22 -
src/TodoList.Maui/appsettings.json | 16 +
src/TodoList.Maui/icon.jpg | Bin 0 -> 50666 bytes
src/TodoList.Maui/setup.iss | 6 +-
src/TodoList.Web/src/App.vue | 103 ++-
src/TodoList.Web/src/api/client.ts | 55 +-
src/TodoList.Web/src/api/tasks.ts | 137 +++-
.../src/components/HotKeySettingsDialog.vue | 4 +-
.../src/components/TaskEditDialog.vue | 147 ++--
src/TodoList.Web/src/components/TaskItem.vue | 404 +++++++++--
src/TodoList.Web/src/components/TaskList.vue | 488 +++++++-------
.../src/services/localStorageService.ts | 5 +-
.../src/services/taskNormalizer.ts | 68 ++
src/TodoList.Web/src/style.css | 8 +-
src/TodoList.Web/src/types/task.ts | 3 +-
src/TodoList.Web/vite.config.ts | 19 +-
start-forend.ps1 | 81 +++
start-maui.ps1 | 96 +++
start-service.ps1 | 128 ----
stop-service.ps1 | 97 ---
91 files changed, 3425 insertions(+), 1978 deletions(-)
create mode 100644 .vscode/launch.json
create mode 100644 .vscode/tasks.json
create mode 100644 Publish.ps1
delete mode 100644 restart-service.ps1
create mode 100644 restart-web.ps1
delete mode 100644 src/TodoList.Api/Controllers/TasksController.cs
delete mode 100644 src/TodoList.Api/Models/TaskModels.cs
delete mode 100644 src/TodoList.Api/Repositories/TaskRepository.cs
delete mode 100644 src/TodoList.Api/Services/TaskService.cs
delete mode 100644 src/TodoList.Api/TodoList.Api.http
rename src/{TodoList.Api => TodoList.Application}/Data/TodoDbContext.cs (54%)
create mode 100644 src/TodoList.Application/DynamicApi/DynamicApiExtensions.cs
create mode 100644 src/TodoList.Application/DynamicApi/DynamicApiMiddleware.cs
create mode 100644 src/TodoList.Application/DynamicApi/HttpAttributes.cs
create mode 100644 src/TodoList.Application/DynamicApi/ParameterBindingAttributes.cs
create mode 100644 src/TodoList.Application/DynamicApi/RemoteServiceAttribute.cs
create mode 100644 src/TodoList.Application/Interfaces/IDynamicApiService.cs
create mode 100644 src/TodoList.Application/Interfaces/ITaskService.cs
rename src/{TodoList.Api => TodoList.Application}/Migrations/20260313044926_InitialCreate.Designer.cs (91%)
rename src/{TodoList.Api => TodoList.Application}/Migrations/20260313044926_InitialCreate.cs (96%)
rename src/{TodoList.Api => TodoList.Application}/Migrations/20260313092658_AddParentTaskId.Designer.cs (85%)
rename src/{TodoList.Api => TodoList.Application}/Migrations/20260313092658_AddParentTaskId.cs (93%)
rename src/{TodoList.Api => TodoList.Application}/Migrations/TodoDbContextModelSnapshot.cs (85%)
create mode 100644 src/TodoList.Application/Models/TaskModels.cs
create mode 100644 src/TodoList.Application/Properties/launchSettings.json
create mode 100644 src/TodoList.Application/Repositories/TaskRepository.cs
create mode 100644 src/TodoList.Application/ServiceCollectionExtensions.cs
create mode 100644 src/TodoList.Application/Services/TaskService.cs
create mode 100644 src/TodoList.Application/TodoList.Application.csproj
delete mode 100644 src/TodoList.Core/Class1.cs
rename src/TodoList.Core/Entities/{Task.cs => TaskEntity.cs} (89%)
delete mode 100644 src/TodoList.Core/Interfaces/ITaskService.cs
rename src/{TodoList.Api => TodoList.Host}/Program.cs (52%)
rename src/{TodoList.Api => TodoList.Host}/Properties/launchSettings.json (90%)
rename src/{TodoList.Api/TodoList.Api.csproj => TodoList.Host/TodoList.Host.csproj} (69%)
rename src/{TodoList.Api => TodoList.Host}/appsettings.Development.json (100%)
rename src/{TodoList.Api => TodoList.Host}/appsettings.json (100%)
rename src/{TodoList.Api => TodoList.Host}/todolist.db (97%)
create mode 100644 src/TodoList.Maui/Models/AppSettings.cs
create mode 100644 src/TodoList.Maui/Resources/Images/app_titlebar_icon.svg
create mode 100644 src/TodoList.Maui/Resources/Images/titlebar_icon.png
create mode 100644 src/TodoList.Maui/Services/AppMetadata.cs
create mode 100644 src/TodoList.Maui/Services/EmbeddedWebServerService.cs
create mode 100644 src/TodoList.Maui/Services/IEmbeddedWebServerService.cs
create mode 100644 src/TodoList.Maui/TodoList.Maui.csproj.DotSettings
delete mode 100644 src/TodoList.Maui/ViewModels/QuickEntryViewModel.cs
delete mode 100644 src/TodoList.Maui/Views/QuickEntryPage.xaml
delete mode 100644 src/TodoList.Maui/Views/QuickEntryPage.xaml.cs
create mode 100644 src/TodoList.Maui/appsettings.json
create mode 100644 src/TodoList.Maui/icon.jpg
create mode 100644 src/TodoList.Web/src/services/taskNormalizer.ts
create mode 100644 start-forend.ps1
create mode 100644 start-maui.ps1
delete mode 100644 start-service.ps1
delete mode 100644 stop-service.ps1
diff --git a/.gitignore b/.gitignore
index 16c6ee3..9d61aa7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,6 +37,7 @@ bld/
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
+src/TodoList.Maui/wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
@@ -363,3 +364,4 @@ MigrationBackup/
FodyWeavers.xsd
/Setup/Output
/TodoList/Output
+/src/TodoList.Maui/Output
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..0532146
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,35 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ // 使用 IntelliSense 找出 C# 调试存在哪些属性
+ // 将悬停用于现有属性的说明
+ // 有关详细信息,请访问 https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md。
+ "name": ".NET Core Launch (web)",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build",
+ // 如果已更改目标框架,请确保更新程序路径。
+ "program": "${workspaceFolder}/src/TodoList.Host/bin/Debug/net10.0/TodoList.Host.dll",
+ "args": [],
+ "cwd": "${workspaceFolder}/src/TodoList.Host",
+ "stopAtEntry": false,
+ // 启用在启动 ASP.NET Core 时启动 Web 浏览器。有关详细信息: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
+ "serverReadyAction": {
+ "action": "openExternally",
+ "pattern": "\\bNow listening on:\\s+(https?://\\S+)"
+ },
+ "env": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "sourceFileMap": {
+ "/Views": "${workspaceFolder}/Views"
+ }
+ },
+ {
+ "name": ".NET Core Attach",
+ "type": "coreclr",
+ "request": "attach"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..bcea2f5
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,41 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "build",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "build",
+ "${workspaceFolder}/src/TodoList.Host/TodoList.Host.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary;ForceNoAlign"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "publish",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "publish",
+ "${workspaceFolder}/src/TodoList.Host/TodoList.Host.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary;ForceNoAlign"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "watch",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "watch",
+ "run",
+ "--project",
+ "${workspaceFolder}/src/TodoList.Host/TodoList.Host.csproj"
+ ],
+ "problemMatcher": "$msCompile"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Publish.ps1 b/Publish.ps1
new file mode 100644
index 0000000..d63c8ee
--- /dev/null
+++ b/Publish.ps1
@@ -0,0 +1,46 @@
+$ErrorActionPreference = "Stop"
+
+$ScriptPath = $PSScriptRoot
+$MauiProjectPath = "d:\Proj\TodoList\src\TodoList.Maui"
+$Timestamp = Get-Date -Format "yyyy_MM_dd_HH_mm"
+
+$OutputBasePath = Join-Path $ScriptPath "PublishOutput"
+$MauiPublishPath = Join-Path $OutputBasePath "TodoList.Maui"
+$ZipFileName = "TodoList_Publish_$Timestamp.zip"
+$ZipFilePath = Join-Path $ScriptPath $ZipFileName
+
+Write-Host "========================================" -ForegroundColor Cyan
+Write-Host "TodoList Publish Script" -ForegroundColor Cyan
+Write-Host "Timestamp: $Timestamp" -ForegroundColor Cyan
+Write-Host "========================================" -ForegroundColor Cyan
+Write-Host ""
+
+if (Test-Path $OutputBasePath) {
+ Write-Host "Cleaning previous publish output..." -ForegroundColor Yellow
+ Remove-Item $OutputBasePath -Recurse -Force
+}
+New-Item -ItemType Directory -Path $OutputBasePath -Force | Out-Null
+
+Write-Host "[Step 1/2] Publishing TodoList.Maui..." -ForegroundColor Cyan
+$MauiProjectFile = Join-Path $MauiProjectPath "TodoList.Maui.csproj"
+dotnet publish $MauiProjectFile -f net10.0-windows10.0.19041.0 -c Release --self-contained false -o $MauiPublishPath
+if ($LASTEXITCODE -ne 0) {
+ Write-Error "MAUI publish failed"
+ exit 1
+}
+Write-Host "TodoList.Maui publish completed successfully!" -ForegroundColor Green
+Write-Host ""
+
+Write-Host "[Step 2/2] Creating ZIP package..." -ForegroundColor Cyan
+if (Test-Path $ZipFilePath) {
+ Write-Host "Removing existing ZIP file..." -ForegroundColor Gray
+ Remove-Item $ZipFilePath -Force
+}
+Compress-Archive -Path "$MauiPublishPath\*" -DestinationPath $ZipFilePath -Force
+Write-Host "ZIP package created: $ZipFilePath" -ForegroundColor Green
+Write-Host ""
+
+Write-Host "========================================" -ForegroundColor Cyan
+Write-Host "Publish completed successfully!" -ForegroundColor Green
+Write-Host "Output: $ZipFilePath" -ForegroundColor Green
+Write-Host "========================================" -ForegroundColor Cyan
diff --git a/README.md b/README.md
index cb18958..22738db 100644
--- a/README.md
+++ b/README.md
@@ -64,7 +64,7 @@ dotnet restore
dotnet ef database update
dotnet run
```
-API 将在 `http://localhost:5057` 启动
+API 将在 `http://localhost:5173` 启动
#### 3. 启动前端 Web
```bash
diff --git a/SCRIPTS_README.md b/SCRIPTS_README.md
index c78d47e..2364642 100644
--- a/SCRIPTS_README.md
+++ b/SCRIPTS_README.md
@@ -36,8 +36,8 @@
🚀 启动服务...
✅ TodoList.Api 服务已启动
进程 ID: 65992
- 访问地址: http://localhost:5057
- Swagger 文档: http://localhost:5057/swagger
+ 访问地址: http://localhost:5173
+ Swagger 文档: http://localhost:5173/swagger
[3/3] 启动 TodoList.Maui 应用...
🚀 启动 TodoList.Maui...
@@ -208,8 +208,8 @@ Setup package created successfully!
# 启动 API 服务和 MAUI 应用(默认)
.\start-service.ps1
-# 访问 http://localhost:5057 查看服务
-# 访问 http://localhost:5057/swagger 查看 API 文档
+# 访问 http://localhost:5173 查看服务
+# 访问 http://localhost:5173/swagger 查看 API 文档
```
### 场景 2: 只启动 API 服务
@@ -273,7 +273,7 @@ Setup package created successfully!
3. **端口占用**
- 如果端口 5057 被占用,启动会失败
- - 使用 `netstat -ano | findstr :5057` 检查端口占用情况
+ - 使用 `netstat -ano | findstr :5173` 检查端口占用情况
4. **MAUI 应用构建**
- 如果 MAUI 应用不存在,需要先构建:`dotnet build src\TodoList.Maui\TodoList.Maui.csproj`
diff --git a/TodoList.slnx b/TodoList.slnx
index e12c6c2..e6f9e0e 100644
--- a/TodoList.slnx
+++ b/TodoList.slnx
@@ -6,7 +6,8 @@
-
+
+
\ No newline at end of file
diff --git a/TodoList/App.xaml.cs b/TodoList/App.xaml.cs
index df48065..033b4e6 100644
--- a/TodoList/App.xaml.cs
+++ b/TodoList/App.xaml.cs
@@ -7,17 +7,19 @@ using TodoList.Services;
using TodoList.ViewModels;
using TodoList.Views;
using System.Linq;
+using System.Reflection;
namespace TodoList
{
public partial class App : System.Windows.Application
{
+ private const string MainWindowTitle = "待办事项";
private IDataService _dataService;
private GlobalShortcutService _shortcutService;
private MainWindow _mainWindow;
private QuickEntryWindow? _quickEntryWindow;
private SettingsService _settingsService;
- private System.Windows.Forms.NotifyIcon _notifyIcon;
+ private NotifyIcon _notifyIcon;
private Mutex _mutex;
private EventWaitHandle _eventWaitHandle;
private const string UniqueEventName = "Global\\TodoListApp_Event_v1";
@@ -60,7 +62,7 @@ namespace TodoList
catch
{
// Fallback to old method if event open fails
- var hWnd = FindWindow(null, "待办事项");
+ var hWnd = FindWindow(null, MainWindowTitle);
if (hWnd != IntPtr.Zero)
{
ShowWindow(hWnd, 9); // SW_RESTORE
@@ -108,6 +110,7 @@ namespace TodoList
var mainViewModel = new MainViewModel(_dataService, _settingsService);
_mainWindow = new MainWindow(mainViewModel);
+ _mainWindow.Title = MainWindowTitle;
_mainWindow.Loaded += MainWindow_Loaded;
// Initialize Tray Icon
@@ -132,7 +135,7 @@ namespace TodoList
{
try
{
- var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
+ var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName;
string cmd = $"\"{exePath}\" --silent";
// If running as dotnet tool, try to find the shim or stable entry point
@@ -168,7 +171,7 @@ namespace TodoList
private void InitializeTrayIcon()
{
- _notifyIcon = new System.Windows.Forms.NotifyIcon();
+ _notifyIcon = new NotifyIcon();
// Try load icon from resource or file
try
@@ -195,15 +198,43 @@ namespace TodoList
}
_notifyIcon.Visible = true;
- _notifyIcon.Text = "TodoList";
+ _notifyIcon.Text = GetNotifyIconText();
_notifyIcon.DoubleClick += (s, e) => ShowMainWindow();
- var contextMenu = new System.Windows.Forms.ContextMenuStrip();
+ var contextMenu = new ContextMenuStrip();
contextMenu.Items.Add("打开主界面", null, (s, e) => ShowMainWindow());
contextMenu.Items.Add("退出", null, (s, e) => ExitApplication());
_notifyIcon.ContextMenuStrip = contextMenu;
}
+ private static string GetNotifyIconText()
+ {
+ var version = GetDisplayVersion();
+ var text = string.IsNullOrWhiteSpace(version) ? MainWindowTitle : $"{MainWindowTitle} v{version}";
+
+ return text.Length > 63 ? text[..63] : text;
+ }
+
+ private static string? GetDisplayVersion()
+ {
+ var asm = Assembly.GetExecutingAssembly();
+
+ var info = asm.GetCustomAttribute()?.InformationalVersion?.Trim();
+ if (!string.IsNullOrWhiteSpace(info))
+ {
+ var plus = info.IndexOf('+');
+ return plus >= 0 ? info[..plus] : info;
+ }
+
+ var file = asm.GetCustomAttribute()?.Version?.Trim();
+ if (!string.IsNullOrWhiteSpace(file))
+ {
+ return file;
+ }
+
+ return asm.GetName().Version?.ToString();
+ }
+
private void ShowMainWindow()
{
Log("ShowMainWindow called");
@@ -298,6 +329,7 @@ namespace TodoList
if (_quickEntryWindow == null)
{
_quickEntryWindow = new QuickEntryWindow(_dataService);
+ _quickEntryWindow.Title = "新建待办";
}
if (_quickEntryWindow.WindowState == WindowState.Minimized)
diff --git a/TodoList/TodoList.csproj b/TodoList/TodoList.csproj
index ceb6fe5..f8328a4 100644
--- a/TodoList/TodoList.csproj
+++ b/TodoList/TodoList.csproj
@@ -5,6 +5,7 @@
net8.0-windows
enable
enable
+ 65001
true
true
icon.ico
diff --git a/TodoList/ViewModels/MainViewModel.cs b/TodoList/ViewModels/MainViewModel.cs
index c0afc50..83c7454 100644
--- a/TodoList/ViewModels/MainViewModel.cs
+++ b/TodoList/ViewModels/MainViewModel.cs
@@ -55,8 +55,6 @@ namespace TodoList.ViewModels
[ObservableProperty]
private Models.SortOrder sortOrder = Models.SortOrder.Descending;
- public string AppVersion => System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "1.0.0";
-
public string FullShortcut
{
get
diff --git a/TodoList/Views/MainWindow.xaml b/TodoList/Views/MainWindow.xaml
index 58a45d6..1fd19d8 100644
--- a/TodoList/Views/MainWindow.xaml
+++ b/TodoList/Views/MainWindow.xaml
@@ -114,7 +114,7 @@
-
diff --git a/TodoList/Views/MainWindow.xaml.cs b/TodoList/Views/MainWindow.xaml.cs
index 90ad15c..adef138 100644
--- a/TodoList/Views/MainWindow.xaml.cs
+++ b/TodoList/Views/MainWindow.xaml.cs
@@ -28,7 +28,7 @@ namespace TodoList.Views
// If settings are open, close settings?
// But user requirement is "Equals pressing X button", which usually means Close/Hide window.
// However, if we want better UX:
- if (DataContext is MainViewModel vm && vm.IsSettingsOpen)
+ if (DataContext is MainViewModel { IsSettingsOpen: true } vm)
{
vm.IsSettingsOpen = false;
e.Handled = true;
diff --git a/docs/实现对比文档.md b/docs/实现对比文档.md
index 9992e8b..9d7cd78 100644
--- a/docs/实现对比文档.md
+++ b/docs/实现对比文档.md
@@ -182,7 +182,7 @@
## 当前运行状态
### 服务状态
-- ✅ **TodoList.Api**: 运行中 (http://localhost:5057)
+- ✅ **TodoList.Api**: 运行中 (http://localhost:5173)
- ✅ **TodoList.Web**: 运行中 (http://localhost:5173)
- ✅ **TodoList.Maui**: 运行中 (Windows 桌面应用)
diff --git a/restart-service.ps1 b/restart-service.ps1
deleted file mode 100644
index 7b03cc9..0000000
--- a/restart-service.ps1
+++ /dev/null
@@ -1,92 +0,0 @@
-param(
- [switch]$StartMaui = $false,
- [switch]$Force = $false
-)
-
-$ErrorActionPreference = "Stop"
-
-Write-Host "====================================" -ForegroundColor Cyan
-Write-Host " TodoList 服务重启脚本" -ForegroundColor Cyan
-Write-Host "====================================" -ForegroundColor Cyan
-Write-Host ""
-
-Write-Host "[1/3] 停止现有服务..." -ForegroundColor Yellow
-
-$stopScriptPath = Join-Path $PSScriptRoot "stop-service.ps1"
-
-if (!(Test-Path $stopScriptPath)) {
- Write-Host "❌ 停止脚本不存在: $stopScriptPath" -ForegroundColor Red
- exit 1
-}
-
-try {
- if ($Force) {
- & $stopScriptPath -Force
- } else {
- & $stopScriptPath
- }
- Write-Host "✅ 服务已停止" -ForegroundColor Green
-} catch {
- Write-Host "❌ 停止服务失败: $_" -ForegroundColor Red
- exit 1
-}
-
-Write-Host ""
-Write-Host "[2/3] 等待进程完全关闭..." -ForegroundColor Yellow
-
-$timeout = 10
-$elapsed = 0
-
-while ($elapsed -lt $timeout) {
- $apiRunning = Get-Process -Name "dotnet" -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowTitle -like "*TodoList.Api*" }
- $mauiRunning = Get-Process -Name "TodoList.Maui" -ErrorAction SilentlyContinue
-
- if (-not $apiRunning -and -not $mauiRunning) {
- Write-Host "✅ 所有进程已关闭" -ForegroundColor Green
- break
- }
-
- Start-Sleep -Seconds 1
- $elapsed++
-
- if ($elapsed -lt $timeout) {
- Write-Host " 等待中... ($elapsed/$timeout 秒)" -ForegroundColor Gray
- }
-}
-
-if ($elapsed -ge $timeout) {
- Write-Host "⚠️ 等待超时,继续启动..." -ForegroundColor Yellow
-}
-
-Write-Host ""
-Write-Host "[3/3] 启动服务..." -ForegroundColor Yellow
-
-$startScriptPath = Join-Path $PSScriptRoot "start-service.ps1"
-
-if (!(Test-Path $startScriptPath)) {
- Write-Host "❌ 启动脚本不存在: $startScriptPath" -ForegroundColor Red
- exit 1
-}
-
-try {
- if ($StartMaui) {
- & $startScriptPath -StartMaui
- } else {
- & $startScriptPath
- }
- Write-Host "✅ 服务已启动" -ForegroundColor Green
-} catch {
- Write-Host "❌ 启动服务失败: $_" -ForegroundColor Red
- exit 1
-}
-
-Write-Host ""
-Write-Host "====================================" -ForegroundColor Cyan
-Write-Host " 重启完成" -ForegroundColor Green
-Write-Host "====================================" -ForegroundColor Cyan
-Write-Host ""
-Write-Host "💡 提示:" -ForegroundColor Yellow
-Write-Host " - 按 Ctrl+C 可以停止服务" -ForegroundColor Gray
-Write-Host " - 运行 stop-service.bat 可以关闭服务" -ForegroundColor Gray
-Write-Host " - 运行 restart-service.bat 可以重启服务" -ForegroundColor Gray
-Write-Host ""
\ No newline at end of file
diff --git a/restart-web.ps1 b/restart-web.ps1
new file mode 100644
index 0000000..93c7e3b
--- /dev/null
+++ b/restart-web.ps1
@@ -0,0 +1,101 @@
+param(
+ [switch]$Force = $false
+)
+
+$ErrorActionPreference = "Stop"
+
+Write-Host "====================================" -ForegroundColor Cyan
+Write-Host " TodoList.Web Restart Script" -ForegroundColor Cyan
+Write-Host "====================================" -ForegroundColor Cyan
+Write-Host ""
+
+$webProjectPath = Join-Path $PSScriptRoot "src\TodoList.Web"
+
+if (!(Test-Path $webProjectPath)) {
+ Write-Host "ERROR: Web project path not found: $webProjectPath" -ForegroundColor Red
+ exit 1
+}
+
+function Get-TodoListWebProcesses {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$WebProjectPath
+ )
+
+ Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Where-Object {
+ $_.CommandLine -and
+ $_.CommandLine -like "*vite*" -and
+ $_.CommandLine -like "*$WebProjectPath*"
+ }
+}
+
+Write-Host "[1/3] Stopping existing service..." -ForegroundColor Yellow
+
+$webProcesses = @(Get-TodoListWebProcesses -WebProjectPath $webProjectPath)
+
+if ($webProcesses.Count -eq 0) {
+ Write-Host "OK: TodoList.Web is not running" -ForegroundColor Green
+} else {
+ Write-Host "Found $($webProcesses.Count) process(es)" -ForegroundColor Yellow
+ foreach ($process in $webProcesses) {
+ $processId = $process.ProcessId
+ Write-Host "Stopping PID: $processId" -ForegroundColor Gray
+ try {
+ Stop-Process -Id $processId -Force:$Force -ErrorAction Stop
+ Write-Host "OK: Stopped PID: $processId" -ForegroundColor Green
+ } catch {
+ Write-Host "WARN: Failed to stop PID: $processId. $_" -ForegroundColor Yellow
+ }
+ }
+}
+
+Write-Host ""
+Write-Host "[2/3] Waiting for processes to exit..." -ForegroundColor Yellow
+
+$timeout = 10
+$elapsed = 0
+
+while ($elapsed -lt $timeout) {
+ $webRunning = @(Get-TodoListWebProcesses -WebProjectPath $webProjectPath)
+
+ if ($webRunning.Count -eq 0) {
+ Write-Host "OK: All processes exited" -ForegroundColor Green
+ break
+ }
+
+ Start-Sleep -Seconds 1
+ $elapsed++
+
+ if ($elapsed -lt $timeout) {
+ Write-Host "Waiting... ($elapsed/$timeout sec)" -ForegroundColor Gray
+ }
+}
+
+if ($elapsed -ge $timeout) {
+ Write-Host "WARN: Timeout reached, continuing to start..." -ForegroundColor Yellow
+}
+
+Write-Host ""
+Write-Host "[3/3] Starting service..." -ForegroundColor Yellow
+
+try {
+ Write-Host "Working directory: $webProjectPath" -ForegroundColor Gray
+ Write-Host "Running: npm run dev" -ForegroundColor Green
+
+ $webProcess = Start-Process -FilePath "npm.cmd" -ArgumentList @("run", "dev") -WorkingDirectory $webProjectPath -PassThru
+
+ Write-Host "OK: Started TodoList.Web" -ForegroundColor Green
+ Write-Host "PID: $($webProcess.Id)" -ForegroundColor Gray
+} catch {
+ Write-Host "ERROR: Failed to start service. $_" -ForegroundColor Red
+ exit 1
+}
+
+Write-Host ""
+Write-Host "====================================" -ForegroundColor Cyan
+Write-Host " Done" -ForegroundColor Green
+Write-Host "====================================" -ForegroundColor Cyan
+Write-Host ""
+Write-Host "Notes:" -ForegroundColor Yellow
+Write-Host " - Vite will choose an available port automatically (no explicit port required)" -ForegroundColor Gray
+Write-Host " - Check the npm/vite output for the Local URL" -ForegroundColor Gray
diff --git a/src/TodoList.Api/Controllers/TasksController.cs b/src/TodoList.Api/Controllers/TasksController.cs
deleted file mode 100644
index 3f09eea..0000000
--- a/src/TodoList.Api/Controllers/TasksController.cs
+++ /dev/null
@@ -1,291 +0,0 @@
-using Microsoft.AspNetCore.Mvc;
-using TodoList.Api.Models;
-using TodoTask = TodoList.Core.Entities.Task;
-using TodoList.Core.Entities;
-using TodoList.Core.Interfaces;
-
-namespace TodoList.Api.Controllers;
-
-///
-/// 任务控制器,提供任务的 RESTful API 接口
-///
-[ApiController]
-[Route("api/[controller]")]
-public class TasksController : ControllerBase
-{
- private readonly ITaskService _taskService;
-
- ///
- /// 构造函数,注入任务服务
- ///
- /// 任务服务接口
- public TasksController(ITaskService taskService)
- {
- _taskService = taskService;
- }
-
- ///
- /// 获取任务列表
- ///
- /// 可选参数,true 获取已完成任务,false 获取未完成任务,不传则获取所有任务
- /// 任务列表响应
- [HttpGet]
- public async Task>>> GetTasks([FromQuery] bool? completed = null)
- {
- try
- {
- List tasks;
-
- if (completed.HasValue)
- {
- tasks = completed.Value
- ? await _taskService.GetCompletedTasksAsync()
- : await _taskService.GetActiveTasksAsync();
- }
- else
- {
- tasks = await _taskService.GetAllTasksAsync();
- }
-
- var taskDtos = tasks.Select(MapToDto).ToList();
-
- return Ok(new ApiResponse>
- {
- Success = true,
- Data = taskDtos,
- Message = "获取任务列表成功"
- });
- }
- catch (Exception ex)
- {
- return StatusCode(500, new ApiResponse>
- {
- Success = false,
- Message = "获取任务列表失败",
- Errors = new List { ex.Message }
- });
- }
- }
-
- ///
- /// 根据ID获取指定任务
- ///
- /// 任务ID
- /// 任务详情响应
- [HttpGet("{id}")]
- public async Task>> GetTask(int id)
- {
- try
- {
- var task = await _taskService.GetTaskByIdAsync(id);
- if (task == null)
- {
- return NotFound(new ApiResponse
- {
- Success = false,
- Message = $"未找到ID为 {id} 的任务"
- });
- }
-
- return Ok(new ApiResponse
- {
- Success = true,
- Data = MapToDto(task),
- Message = "获取任务成功"
- });
- }
- catch (Exception ex)
- {
- return StatusCode(500, new ApiResponse
- {
- Success = false,
- Message = "获取任务失败",
- Errors = new List { ex.Message }
- });
- }
- }
-
- ///
- /// 创建新任务
- ///
- /// 创建任务的数据传输对象
- /// 创建的任务响应
- [HttpPost]
- public async Task>> CreateTask([FromBody] CreateTaskDto dto)
- {
- try
- {
- if (string.IsNullOrWhiteSpace(dto.Title))
- {
- return BadRequest(new ApiResponse
- {
- Success = false,
- Message = "任务标题不能为空",
- Errors = new List { "Title is required" }
- });
- }
-
- var task = await _taskService.CreateTaskAsync(dto.Title, dto.Priority, dto.ParentTaskId);
-
- return CreatedAtAction(nameof(GetTask), new { id = task.Id }, new ApiResponse
- {
- Success = true,
- Data = MapToDto(task),
- Message = "创建任务成功"
- });
- }
- catch (Exception ex)
- {
- return StatusCode(500, new ApiResponse
- {
- Success = false,
- Message = "创建任务失败",
- Errors = new List { ex.Message }
- });
- }
- }
-
- ///
- /// 更新任务
- ///
- /// 任务ID
- /// 更新任务的数据传输对象
- /// 更新后的任务响应
- [HttpPut("{id}")]
- public async Task>> UpdateTask(int id, [FromBody] UpdateTaskDto dto)
- {
- try
- {
- var task = await _taskService.UpdateTaskAsync(id, dto.Title, dto.Priority);
-
- return Ok(new ApiResponse
- {
- Success = true,
- Data = MapToDto(task),
- Message = "更新任务成功"
- });
- }
- catch (KeyNotFoundException)
- {
- return NotFound(new ApiResponse
- {
- Success = false,
- Message = $"未找到ID为 {id} 的任务"
- });
- }
- catch (Exception ex)
- {
- return StatusCode(500, new ApiResponse
- {
- Success = false,
- Message = "更新任务失败",
- Errors = new List { ex.Message }
- });
- }
- }
-
- ///
- /// 切换任务的完成状态
- ///
- /// 任务ID
- /// 更新后的任务响应
- [HttpPatch("{id}/complete")]
- public async Task>> ToggleComplete(int id)
- {
- try
- {
- var task = await _taskService.ToggleCompleteAsync(id);
-
- return Ok(new ApiResponse
- {
- Success = true,
- Data = MapToDto(task),
- Message = task.IsCompleted ? "任务已完成" : "任务已取消完成"
- });
- }
- catch (KeyNotFoundException)
- {
- return NotFound(new ApiResponse
- {
- Success = false,
- Message = $"未找到ID为 {id} 的任务"
- });
- }
- catch (Exception ex)
- {
- return StatusCode(500, new ApiResponse
- {
- Success = false,
- Message = "更新任务状态失败",
- Errors = new List { ex.Message }
- });
- }
- }
-
- ///
- /// 删除任务
- ///
- /// 任务ID
- /// 删除结果响应
- [HttpDelete("{id}")]
- public async Task>> DeleteTask(int id)
- {
- try
- {
- await _taskService.DeleteTaskAsync(id);
-
- return Ok(new ApiResponse