changed.更了好多

This commit is contained in:
lincube
2026-05-12 16:46:49 +08:00
parent 563f12caa1
commit 33c264f6dd
127 changed files with 5257 additions and 10534 deletions

View File

@@ -0,0 +1,403 @@
# 课程表组件视觉重构 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 彻底重构阑山桌面的课程表ClassScheduleWidget组件视觉设计参考小爱课程表的桌面小部件风格实现时间轴+色块卡片布局、科目自动配色、当前课程进度高亮等现代化视觉效果。
**Architecture:** 保留现有数据层ClassIslandScheduleDataService、Models和组件注册机制不变仅重构 Widget 的 UI 渲染层XAML + code-behind 中的渲染逻辑)。新增科目配色服务,为每门课程分配稳定的区分色。先创建 HTML Mock 验证视觉效果,再移植到 Avalonia XAML。
**Tech Stack:** Avalonia UI (XAML + C# code-behind)、HTML/CSS (Mock 预览)
---
## 当前状态分析
### 现有组件结构
- **XAML**: `ClassScheduleWidget.axaml` — 仅定义了 RootBorder、HeaderGrid日期+星期+课数、ScrollViewer+CourseListPanel、StatusTextBlock
- **Code-behind**: `ClassScheduleWidget.axaml.cs` — 所有课程项 UI 在 `CreateSingleItemControl()` 中手动构建:圆点(Bullet) + 文字栈(课程名/时间/详情)
- **数据层**: `ClassIslandScheduleDataService` + `ClassIslandScheduleModels` — 不变
- **编辑器**: `ClassScheduleComponentEditor.axaml(.cs)` — 不变
### 现有设计问题
1. **视觉单调**: 仅用小圆点区分课程,所有课程外观一致,缺乏层次感
2. **信息密度低**: 课程名、时间、教师名挤在一行,可读性差
3. **当前课不突出**: 仅通过圆点颜色变化标识当前课程,几乎无法一眼识别
4. **色彩硬编码**: 颜色值直接写在 C# 中,不使用语义资源键,不遵循 VISUAL_SPEC
5. **无时间轴感**: 列表式排列无法体现课程的时间先后和持续长度
### 小爱课程表参考设计特征
1. **时间轴布局**: 左侧显示时间刻度,右侧是课程色块卡片
2. **科目配色**: 每门课程自动分配一种柔和区分色,卡片使用对应色块背景
3. **当前课高亮**: 正在进行的课程有明显的视觉强调(放大/进度条/发光)
4. **进度指示**: 当前课程显示上课进度(已过时间/总时长)
5. **紧凑信息**: 课程名+教室/教师信息在色块内清晰排列
6. **课间分隔**: 课间休息区域有视觉分隔(虚线/淡色区域)
---
## 设计方案
### 视觉论文 (Visual Thesis)
时间轴驱动的色块卡片布局,柔和科目配色,当前课程进度高亮——在桌面小组件有限空间内实现信息密度与美感的平衡。
### 布局结构
```
┌─────────────────────────────────────┐
│ 7/24 周一 今天3节课 │ ← 头部:日期 + 星期 + 课数
├─────────────────────────────────────┤
│ 08:00 ┌──────────────────────┐ │
│ │ 语文 │ │ ← 科目色块卡片
│ │ 王老师 · 教室301 │ │
│ 08:45 └──────────────────────┘ │
│ ┌──────────────────────┐ │
│ │ 数学 ████████░░ 75% │ │ ← 当前课:进度条 + 高亮
│ │ 李老师 · 教室205 │ │
│ 09:30 └──────────────────────┘ │
│ ... │
└─────────────────────────────────────┘
```
### 科目配色方案
使用一组预定义的柔和色彩,按科目名哈希值稳定分配:
- 语文: #5B8FF9 (蓝)
- 数学: #F6903D (橙)
- 英语: #5AD8A6 (绿)
- 物理: #E8684A (红)
- 化学: #9270CA (紫)
- 生物: #FF9845 (琥珀)
- 历史: #1E9493 (青)
- 地理: #FF99C3 (粉)
- 政治: #7262FD (靛)
- 体育: #78D3F8 (天蓝)
- 默认: #8B95A5 (灰)
### 当前课程高亮
- 卡片左侧显示 3px 宽的强调色竖条
- 卡片底部显示细进度条(已过时间/总时长)
- 卡片背景使用科目色的 15% 透明度版本
- 非当前课程使用科目色的 8% 透明度版本
---
## 文件变更清单
| 文件 | 操作 | 说明 |
|------|------|------|
| `LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml` | 修改 | 重构 XAML 布局:时间轴+卡片区域 |
| `LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs` | 修改 | 重构渲染逻辑:色块卡片、科目配色、进度条 |
| `LanMountainDesktop/Views/Components/SubjectColorService.cs` | 新建 | 科目配色服务:稳定哈希分配颜色 |
| `mocks/class-schedule-mock.html` | 新建 | HTML Mock 预览(亮色+暗色) |
---
## Task 分解
### Task 1: 创建 HTML Mock 预览
**Files:**
- Create: `mocks/class-schedule-mock.html`
- [ ] **Step 1: 创建 HTML Mock 文件**
创建完整的 HTML Mock包含
- 亮色/暗色主题切换
- 时间轴+色块卡片布局
- 科目自动配色
- 当前课程进度条高亮
- 课间分隔区域
- 响应式尺寸(模拟桌面组件 2x4 / 4x4 等尺寸)
Mock 中应包含示例数据:
```
08:00-08:45 语文 王老师
08:55-09:40 数学 李老师 (当前课,进度 60%)
09:50-10:35 英语 张老师
10:45-11:30 物理 赵老师
14:00-14:45 化学 陈老师
14:55-15:40 生物 刘老师
```
- [ ] **Step 2: 在浏览器中打开 Mock 验证效果**
Run: `start mocks/class-schedule-mock.html`
- [ ] **Step 3: 根据视觉效果调整 Mock 细节**
调整间距、色值、字体大小、进度条样式等直到满意。
---
### Task 2: 创建科目配色服务
**Files:**
- Create: `LanMountainDesktop/Views/Components/SubjectColorService.cs`
- [ ] **Step 1: 实现 SubjectColorService**
```csharp
using System;
using Avalonia.Media;
namespace LanMountainDesktop.Views.Components;
internal static class SubjectColorService
{
private static readonly (string Name, string Hex)[] Palette = [
("语文", "#5B8FF9"),
("数学", "#F6903D"),
("英语", "#5AD8A6"),
("物理", "#E8684A"),
("化学", "#9270CA"),
("生物", "#FF9845"),
("历史", "#1E9493"),
("地理", "#FF99C3"),
("政治", "#7262FD"),
("体育", "#78D3F8"),
("音乐", "#F25E7E"),
("美术", "#C2A1FD"),
];
private static readonly string DefaultHex = "#8B95A5";
public static Color ResolveColor(string subjectName)
{
foreach (var (name, hex) in Palette)
{
if (subjectName.Contains(name, StringComparison.OrdinalIgnoreCase))
{
return Color.Parse(hex);
}
}
var hash = StableHash(subjectName);
var index = (int)(hash % (uint)Palette.Length);
return Color.Parse(Palette[index].Hex);
}
public static Color ResolveBackgroundColor(string subjectName, bool isCurrent, bool isNight)
{
var baseColor = ResolveColor(subjectName);
var alpha = isCurrent ? 0.18 : 0.08;
return new Color(
(byte)(alpha * 255),
baseColor.R,
baseColor.G,
baseColor.B);
}
public static Color ResolveForegroundColor(string subjectName, bool isNight)
{
var baseColor = ResolveColor(subjectName);
return isNight
? new Color(0xFF, (byte)Math.Min(255, baseColor.R + 60), (byte)Math.Min(255, baseColor.G + 60), (byte)Math.Min(255, baseColor.B + 60))
: baseColor;
}
private static uint StableHash(string input)
{
uint hash = 5381;
foreach (var c in input)
{
hash = ((hash << 5) + hash) ^ (uint)c;
}
return hash;
}
}
```
- [ ] **Step 2: 验证编译通过**
Run: `dotnet build LanMountainDesktop/LanMountainDesktop.csproj -c Debug --no-restore`
---
### Task 3: 重构 ClassScheduleWidget XAML 布局
**Files:**
- Modify: `LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml`
- [ ] **Step 1: 重写 XAML 布局**
新的 XAML 结构:
- RootBorder 保持 `DesignCornerRadiusComponent`
- 头部区域:日期(大号)+ 星期 + 课数 + 进度摘要
- 课程列表区域ScrollViewer 包裹 StackPanel
- 每个 CourseItem 将在 code-behind 中构建为Grid(时间列 + 卡片列)
- 时间列StartTime / EndTime 垂直排列
- 卡片列Border(科目色背景) > StackPanel(课程名 + 教师信息 + 进度条)
XAML 只定义骨架,课程项仍由 code-behind 动态构建(因为需要科目配色和进度计算)。
```xml
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="LanMountainDesktop.Views.Components.ClassScheduleWidget">
<Border x:Name="RootBorder"
Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
Padding="0">
<Grid x:Name="LayoutGrid"
RowDefinitions="Auto,*">
<Grid x:Name="HeaderGrid"
ColumnDefinitions="Auto,*,Auto"
Padding="16,12,16,8">
<StackPanel x:Name="DateGroup"
Orientation="Horizontal"
VerticalAlignment="Center">
<TextBlock x:Name="MonthTextBlock"
FontWeight="Bold"
TextTrimming="CharacterEllipsis" />
<TextBlock x:Name="SlashTextBlock"
Text="/"
FontWeight="Bold" />
<TextBlock x:Name="DayTextBlock"
FontWeight="Bold"
TextTrimming="CharacterEllipsis" />
</StackPanel>
<TextBlock x:Name="WeekdayTextBlock"
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis" />
<Border x:Name="ClassCountBadge"
Grid.Column="2"
VerticalAlignment="Center"
Padding="8,3"
CornerRadius="{DynamicResource DesignCornerRadiusMicro}">
<TextBlock x:Name="ClassCountTextBlock"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis" />
</Border>
</Grid>
<ScrollViewer x:Name="ContentScrollViewer"
Grid.Row="1"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel x:Name="CourseListPanel"
Spacing="4" />
</ScrollViewer>
<TextBlock x:Name="StatusTextBlock"
Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
TextAlignment="Center"
IsVisible="False"
TextWrapping="Wrap" />
</Grid>
</Border>
</UserControl>
```
---
### Task 4: 重构 ClassScheduleWidget 渲染逻辑
**Files:**
- Modify: `LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs`
- [ ] **Step 1: 扩展 CourseItemViewModel**
在现有 record 中增加字段:
```csharp
private sealed record CourseItemViewModel(
string Name,
string TimeRange,
string Detail,
bool IsCurrent,
TimeSpan StartTime,
TimeSpan EndTime,
double Progress);
```
- [ ] **Step 2: 修改 BuildCourseItemViewModels 计算进度**
在构建 ViewModel 时,对当前课程计算 Progress = (now - startTime) / (endTime - startTime)。
- [ ] **Step 3: 重写 CreateSingleItemControl**
新的课程项 UI 结构:
```
Grid (2列: 时间列 Auto + 卡片列 *)
├── StackPanel (时间列)
│ ├── TextBlock (开始时间, 如 "08:00")
│ └── TextBlock (结束时间, 如 "08:45", 较淡)
└── Border (卡片列, 科目色背景, 圆角 DesignCornerRadiusSm)
├── 左侧强调竖条 (当前课显示, 3px宽, 科目色)
└── StackPanel
├── TextBlock (课程名, 科目色前景, 加粗)
├── TextBlock (教师/教室, 次要色)
└── ProgressBar (当前课显示, 科目色)
```
关键改动点:
1. 移除圆点(Bullet),改用时间轴左侧时间标签
2. 课程卡片使用 `SubjectColorService` 配色
3. 当前课程卡片左侧显示强调竖条 + 底部进度条
4. 课间区域用淡色分隔线标识
5. 颜色使用语义资源键(`AdaptiveTextPrimaryBrush` 等),科目色通过 `SubjectColorService` 获取
- [ ] **Step 4: 重写 ApplyAdaptiveLayout**
更新自适应布局逻辑:
- 头部日期/星期/课数徽章的字号和间距
- 移除旧的圆点、文字栈相关计算
- 新增时间列宽度、卡片圆角、进度条高度等计算
- 使用 `ComponentChromeCornerRadiusHelper` 获取圆角 Token
- [ ] **Step 5: 更新 IncrementalUpdateItems 和 IncrementalUpdateCurrentCourseHighlight**
适配新的 UI 结构:
- 更新进度条值
- 更新科目色背景
- 更新强调竖条可见性
- [ ] **Step 6: 更新 RefreshSchedule 中的时间计算**
`BuildCourseItemViewModels` 中传入 `StartTime`/`EndTime`/`Progress`
- [ ] **Step 7: 验证编译通过**
Run: `dotnet build LanMountainDesktop/LanMountainDesktop.csproj -c Debug`
---
### Task 5: 验证与测试
- [ ] **Step 1: 运行项目查看效果**
Run: `dotnet run --project LanMountainDesktop/LanMountainDesktop.csproj`
- [ ] **Step 2: 运行相关测试**
Run: `dotnet test LanMountainDesktop.slnx -c Debug`
- [ ] **Step 3: 检查圆角规范合规**
确认 RootBorder 使用 `DesignCornerRadiusComponent`,内部卡片使用 `DesignCornerRadiusSm`/`DesignCornerRadiusMd`,无硬编码圆角值。
---
## 假设与决策
1. **科目配色**: 使用预定义调色板 + 哈希回退,不依赖 ClassIsland 数据中的科目颜色(因为 ClassIsland 不提供科目颜色字段)
2. **进度条**: 仅当前课程显示进度条,非当前课程不显示
3. **课间分隔**: 用 4px 间距 + 可选的淡色虚线分隔,不做复杂的课间休息区域
4. **Mock 优先**: 先完成 HTML Mock 确认视觉效果,再实现 Avalonia 代码
5. **编辑器不变**: ClassScheduleComponentEditor 不需要修改
6. **数据层不变**: ClassIslandScheduleDataService 和 Models 不需要修改
7. **接口兼容**: IDesktopComponentWidget、ITimeZoneAwareComponentWidget、IComponentPlacementContextAware 接口实现不变
## 验证步骤
1. HTML Mock 在浏览器中展示效果满意
2. Avalonia 项目编译通过
3. 运行项目,课程表组件显示新布局
4. 亮色/暗色主题切换正常
5. 当前课程高亮和进度条正常
6. 科目配色稳定(同一科目每次显示颜色一致)
7. 测试通过

View File

@@ -154,7 +154,7 @@
│ │
│ 方案 2: 命名管道(推荐用于进度报告) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Launcher 创建命名管道: \\.\pipe\LanMountainDesktop_Launcher │ │
│ │ [历史方案] Launcher 创建命名管道: \\.\pipe\LanMountainDesktop_Launcher │ │
│ │ 主程序连接并发送进度消息 │ │
│ │ │ │
│ │ 消息格式: JSON │ │
@@ -289,7 +289,7 @@ public static class LauncherIpcConstants
#### 4. 实现 IPC 服务端
**新建文件**: `LanMountainDesktop.Launcher/Services/Ipc/LauncherIpcServer.cs`
**历史方案,已废弃**: `LanMountainDesktop.Launcher/Services/Ipc/LauncherIpcServer.cs`
```csharp
using System.IO.Pipes;
@@ -428,7 +428,7 @@ public async Task<LauncherResult> RunAsync()
#### 6. 实现 IPC 客户端
**新建文件**: `LanMountainDesktop/Services/Launcher/LauncherIpcClient.cs`
**历史方案,已废弃**: `LanMountainDesktop/Services/Launcher/LauncherIpcClient.cs`
```csharp
using System.IO.Pipes;
@@ -672,8 +672,8 @@ public class UpdateInstallationService
### 新增文件
1. `LanMountainDesktop.Shared.Contracts/Launcher/LauncherIpc.cs` - IPC 契约
2. `LanMountainDesktop.Launcher/Services/Ipc/LauncherIpcServer.cs` - IPC 服务端
3. `LanMountainDesktop/Services/Launcher/LauncherIpcClient.cs` - IPC 客户端
2. `LanMountainDesktop.Launcher/Services/Ipc/LauncherIpcServer.cs` - 历史启动进度 IPC 服务端,已由公共 IPC 通知替代
3. `LanMountainDesktop/Services/Launcher/LauncherIpcClient.cs` - 历史启动进度 IPC 客户端,已由公共 IPC 通知替代
4. `LanMountainDesktop.Launcher/Services/Update/UpdateInstallationService.cs` - 更新安装
### 删除文件
@@ -715,3 +715,11 @@ public class UpdateInstallationService
- [ ] GitHub Actions 打包成功
- [ ] 安装程序图标正常
- [ ] 快捷方式图标正常
## 2026 Multi-instance Policy Update
- The old launcher progress pipe is historical only; current startup progress uses public IPC.
- Launcher now reads Host `settings.json` for `MultiInstanceLaunchBehavior` before normal launch.
- Existing Host behavior is policy-driven: restart app, open desktop silently, prompt only, or notify and open desktop.
- Host no longer owns the single-instance listener or already-running prompt; repeated-launch policy lives in Launcher.
- The repeated-launch prompt is a Fluent Launcher window; Host public IPC only exposes execution actions such as activate, restart, and exit.

View File

@@ -0,0 +1,212 @@
# 更新设置界面重设计实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将更新设置页面从丑陋的卡片堆叠布局重设计为遵循 Fluent Design 的 FASettingsExpander 列表布局,与项目其他设置页面保持视觉一致性。
**Architecture:** 移除所有 `Border.settings-section-card` 包裹,改用 `FASettingsExpander` + `IconText` 分节标题 + `Separator` 分隔线的统一模式。操作按钮改为仅显示当前可用操作。版本信息改为 `FASettingsExpanderItem` 行项目展示。ViewModel 层新增 `ActiveActions` 计算属性来驱动按钮可见性。
**Tech Stack:** Avalonia UI 11, FluentAvalonia 2.x, CommunityToolkit.Mvvm
---
## 当前状态分析
### 现有文件
| 文件 | 职责 |
|------|------|
| `LanMountainDesktop/Views/SettingsPages/UpdateSettingsPage.axaml` | 更新页面 AXAML 布局 |
| `LanMountainDesktop/Views/SettingsPages/UpdateSettingsPage.axaml.cs` | 代码隐藏 |
| `LanMountainDesktop/ViewModels/UpdateSettingsViewModel.cs` | 视图模型 |
| `LanMountainDesktop/Styles/SettingsCardStyles.axaml` | 通用设置样式 |
| `LanMountainDesktop/Controls/IconText.axaml(.cs)` | 分节标题控件 |
| `LanMountainDesktop.Shared.Contracts/Update/UpdateState.cs` | UpdatePhase 枚举和扩展方法 |
### 核心问题
1. **4 个 `Border.settings-section-card` 卡片**:状态卡、版本信息卡、进度卡、操作卡,每个都带边框+阴影+圆角,视觉零碎
2. **FAInfoBar 嵌套在卡片内**:冗余的容器层级
3. **7 个按钮 3×3 网格**:大量按钮在当前阶段不可用但仍然占据空间
4. **与其他设置页面风格不一致**GeneralSettingsPage、AppearanceSettingsPage 等全部使用 `FASettingsExpander` 列表
### 参考基准
- [GeneralSettingsPage.axaml](file:///d:/github/LanMountainDesktop/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml)`IconText` 分节标题 → `FASettingsExpander` 列表 → `Separator` 分隔
- [AppearanceSettingsPage.axaml](file:///d:/github/LanMountainDesktop/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml):同上模式
- [AboutSettingsPage.axaml](file:///d:/github/LanMountainDesktop/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml)`FAInfoBar` 用于静态信息展示
- Windows 11 设置 > Windows Update顶部状态区 + 进度条 + 操作按钮,下方展开区展示详情
---
## 设计决策
| 决策项 | 选择 | 理由 |
|--------|------|------|
| 布局模式 | FASettingsExpander 列表 | 与其他设置页面统一Fluent Design 原生控件 |
| 按钮策略 | 仅显示可用操作 | 简洁、不混乱Windows 11 更新页面也是此模式 |
| 版本信息 | FASettingsExpanderItem 行项目 | 每行一个信息,干净可扫描 |
| 进度展示 | 内嵌在状态 Expander 内 | 进度是状态的一部分,不应独立成卡 |
| 偏好设置 | 保留 FASettingsExpander | 已经是正确模式,微调即可 |
---
## 新布局结构
```
ScrollViewer
└── StackPanel (settings-page-container settings-page-animated)
├── TextBlock (settings-section-title: "更新")
├── TextBlock (settings-section-description: 描述文字)
├── IconText (Icon="ArrowSync", Text="更新状态")
├── FASettingsExpander "检查更新" (IsClickEnabled=True, Command=CheckCommand)
│ ├── IconSource: ArrowSync 图标
│ └── Footer: Button "检查更新" (仅 CanCheck 时可见)
├── FASettingsExpander "更新进度" (IsVisible=IsBusy||IsProgressVisible||IsPaused)
│ ├── IconSource: FAProgressRing / 对应阶段图标
│ ├── Footer: PhaseText + ProgressFraction
│ └── FASettingsExpanderItem
│ ├── ProgressBar (ProgressFraction)
│ ├── ProgressDetail 文字
│ └── 操作按钮行 (仅可用操作)
│ ├── Button "下载" (CanDownload)
│ ├── Button "安装" (CanInstall)
│ ├── Button "暂停" (CanPause)
│ ├── Button "继续" (CanResume)
│ ├── Button "回滚" (CanRollback)
│ └── Button "取消" (CanCancel)
├── FASettingsExpander "暂停" (IsVisible=IsPaused)
│ └── FAInfoBar (PausedBadgeText + PausedHintText)
├── Separator (settings-separator)
├── IconText (Icon="Info", Text="版本信息")
├── FASettingsExpander "当前版本" (IsClickEnabled=False)
│ ├── IconSource: 版本图标
│ └── Footer: CurrentVersionText
├── FASettingsExpander "最新版本" (IsClickEnabled=False)
│ ├── IconSource: 下载图标
│ └── Footer: LatestVersionText (或 "已是最新")
├── FASettingsExpander "发布时间" (IsClickEnabled=False)
│ ├── IconSource: 日历图标
│ └── Footer: PublishedAtText
├── FASettingsExpander "上次检查" (IsClickEnabled=False)
│ ├── IconSource: 时钟图标
│ └── Footer: LastCheckedText
├── FASettingsExpander "更新类型" (IsClickEnabled=False)
│ ├── IconSource: 标签图标
│ └── Footer: UpdateTypeText
├── Separator (settings-separator)
├── IconText (Icon="Settings", Text="更新偏好")
└── FASettingsExpander "更新偏好" (IsExpanded=True)
├── IconSource: 设置齿轮图标
├── FASettingsExpanderItem "更新频道"
│ └── Footer: ComboBox (stable/preview)
├── FASettingsExpanderItem "下载源"
│ └── Footer: ComboBox (plonds/github/proxy)
├── FASettingsExpanderItem "更新模式"
│ └── Footer: ComboBox (manual/confirm/silent)
└── FASettingsExpanderItem "下载线程数"
└── Footer: Slider + TextBlock
```
---
## Proposed Changes
### Task 1: 重写 UpdateSettingsPage.axaml 布局
**Files:**
- Modify: `LanMountainDesktop/Views/SettingsPages/UpdateSettingsPage.axaml`
**What:** 完全重写 AXAML将 4 个 `Border.settings-section-card` 替换为 `FASettingsExpander` 列表布局。
**Key changes:**
1. 移除所有 `Border.settings-section-card` 包裹
2. 使用 `controls:IconText` 做分节标题(与 GeneralSettingsPage 一致)
3. 状态区域:`FASettingsExpander` + `IsClickEnabled=True` + `Command=CheckCommand`Footer 放检查按钮
4. 进度区域:`FASettingsExpander` 内嵌 ProgressBar + 操作按钮,仅 `IsBusy||IsProgressVisible||IsPaused` 时可见
5. 版本信息:每个字段一个 `FASettingsExpander`Footer 直接显示值(参考 Windows 11 更新页面的行项目模式)
6. 偏好设置:保留 `FASettingsExpander` + `FASettingsExpanderItem` 模式,但将 TextBox 改为 ComboBox更符合 Fluent 规范)
7. 使用 `Separator classes="settings-separator"` 分隔三大区域
**Why:** 与项目其他设置页面统一风格,遵循 Fluent Design消除卡片堆叠的视觉噪音。
**How:**
- 参照 GeneralSettingsPage.axaml 的布局模式
- 参照 AppearanceSettingsPage.axaml 的 FASettingsExpander 使用方式
- 参照 AboutSettingsPage.axaml 的 FAInfoBar 使用方式
### Task 2: 更新 ViewModel — 添加 ComboBox 数据源和按钮可见性属性
**Files:**
- Modify: `LanMountainDesktop/ViewModels/UpdateSettingsViewModel.cs`
**What:**
1. 将更新频道、下载源、更新模式从 `TextBox` 绑定改为 `ComboBox` 绑定,添加 `ObservableCollection<SelectionOption>` 类型的数据源属性
2. 添加 `IsProgressSectionVisible` 计算属性(`IsBusy || IsProgressVisible || IsPaused`
3. 添加 `IsUpdateAvailableSectionVisible` 计算属性(`IsUpdateAvailable`
4. 添加 `IsStatusInfoVisible` 计算属性(有 StatusMessage 且非空闲时)
5. 移除不再需要的独立按钮文本属性CheckButtonText 保留,其他按钮文本属性保留但仅在可见时使用)
**Why:** ComboBox 比 TextBox 更适合有限选项的输入,且与 GeneralSettingsPage 的模式一致。按钮可见性属性让 AXAML 可以用 `IsVisible` 绑定控制按钮显示。
**How:**
- 参考 GeneralSettingsPageViewModel 中 SelectionOption 的使用方式
-`OnCurrentPhaseChanged` 中触发新属性的 OnPropertyChanged
### Task 3: 将偏好设置 TextBox 替换为 ComboBox
**Files:**
- Modify: `LanMountainDesktop/Views/SettingsPages/UpdateSettingsPage.axaml` (在 Task 1 中一并完成)
- Modify: `LanMountainDesktop/ViewModels/UpdateSettingsViewModel.cs` (在 Task 2 中一并完成)
**What:** 将更新频道、下载源、更新模式三个 `TextBox` 替换为 `ComboBox`,使用 `SelectionOption` 数据模板。
**Why:** 有限选项应使用 ComboBox 而非自由文本输入,这是 Fluent Design 的基本规范,也与 GeneralSettingsPage 中的语言/时区选择一致。
### Task 4: 构建验证
**Files:**
- 无新文件
**What:** 运行 `dotnet build` 确保编译通过,检查 AXAML 绑定是否正确。
---
## Assumptions & Decisions
1. **不修改 UpdateOrchestrator 和 UpdateState** — 只改 UI 层和 ViewModel 的展示逻辑,不改底层更新引擎
2. **不修改 SettingsCardStyles.axaml** — 通用样式保持不变,移除的是 UpdateSettingsPage 对它的使用
3. **保留所有 ViewModel 属性** — 即使某些属性在新布局中不再直接使用(如独立的 ActionsTitle也保留以避免破坏本地化系统
4. **ComboBox 选项硬编码在 ViewModel** — 参考 GeneralSettingsPageViewModel 的 SelectionOption 模式
5. **进度区域在空闲时隐藏** — 不显示空的进度条,只在有活动时展示
6. **FAInfoBar 仅用于暂停/错误提示** — 不再嵌套在卡片内,直接放在 FASettingsExpanderItem 内
---
## Verification Steps
1. `dotnet build LanMountainDesktop.slnx -c Debug` 编译通过
2. 运行应用,导航到设置 > 更新页面,验证:
- 页面布局与 GeneralSettingsPage 风格一致
- 无圆角矩形卡片包裹
- 检查更新按钮可用
- 进度区域在空闲时隐藏
- 版本信息以行项目形式展示
- 偏好设置使用 ComboBox
- 操作按钮仅显示当前可用的
3. 点击「检查更新」,验证状态变化和进度展示
4. 验证偏好设置的 ComboBox 选择能正确保存和加载

View File

@@ -65,3 +65,19 @@
- 托盘失败时应用仍保持可恢复。
- Launcher 与应用设置页显示相同版本。
- 100% / 150% / 200% / 250% 缩放下Launcher OOBE、主窗口入场、通知位置与动画正常。
### 5. Launcher IPC and error surface follow-up
- The legacy `LanMountainDesktop_Launcher` named-pipe startup progress channel is retired. Public IPC notifications and host exit codes are the only startup state sources.
- Normal Launcher launches must probe public IPC for an existing Host before starting a new Host process. Host no longer owns multi-instance policy, activation prompts, or the old single-instance pipe.
- `SecondaryActivationSucceeded` is a success terminal state. `SecondaryActivationFailed` and `RestartLockNotAcquired` may surface as failures only after public IPC recovery has failed.
- Launcher startup errors must use FluentAvalonia resources, Fluent icons, an InfoBar recovery hint, and copyable diagnostics instead of the old hard-coded dark panel.
### 6. Multi-instance behavior setting
- App settings include `MultiInstanceLaunchBehavior` with default `NotifyAndOpenDesktop`.
- General settings exposes the behavior under Basic Settings with four choices: restart app, open desktop silently, prompt only, and notify plus open desktop.
- Launcher reads the Host `settings.json` before a normal launch and applies the selected behavior when public IPC reports an existing Host.
- `PromptOnly` shows a Fluent Launcher prompt and does not open the desktop automatically.
- `NotifyAndOpenDesktop` activates the existing Host and shows the already-running notice from Launcher.
- `RestartApp` requests restart through public IPC and must not create a second Host if the restart request fails.

View File

@@ -12,3 +12,10 @@
- [x] 修复主窗口入场、通知定位和 Launcher OOBE 的高分屏动画/定位问题。
- [x] 补充规格与版本同步说明文档。
- [ ] 追加针对托盘恢复和启动判定的自动化回归测试。
- [x] Remove the legacy `LanMountainDesktop_Launcher` startup progress pipe; launcher progress now uses public IPC plus host exit-code classification only.
- [x] Move normal multi-open probing into Launcher before host launch and remove Host-side single-instance prompt/listener code.
- [x] Refresh the Launcher error window with Fluent resources, InfoBar, Fluent icons, command bar actions, and copyable diagnostic details.
- [x] Add app-level `MultiInstanceLaunchBehavior` setting and expose it in General > Basic Settings.
- [x] Make Launcher apply restart/open silently/prompt only/notify and open behavior before starting a new Host.
- [x] Add a Fluent Launcher multi-instance prompt; Host public IPC stays limited to activation/status/restart/exit actions.

View File

@@ -4,14 +4,14 @@
- Tray menu `Exit App` must commit an irreversible host shutdown request.
- Once shutdown is committed, tray menu actions must not reopen the desktop, settings window, or component library.
- Shutdown cleanup must release Public IPC, plugin runtime, tray icon, fused desktop edit UI, telemetry resources, and the single-instance lock before the forced-exit deadline.
- Shutdown cleanup must release Public IPC, plugin runtime, tray icon, fused desktop edit UI, and telemetry resources before the forced-exit deadline.
- Forced process termination must be scheduled when the shutdown request is accepted, not only after Avalonia lifetime exit.
- Restart must preserve `RestartRequested` intent and must not route through an exit path that overwrites it.
- Fused desktop component library menu activation must reuse the existing library window and must exit edit mode if opening fails.
## Acceptance
- Selecting `Exit App` from the tray leaves no background host process and allows a later Launcher start to acquire the single-instance lock.
- Selecting `Exit App` from the tray leaves no background host process and allows a later Launcher start to perform multi-instance detection through public IPC.
- Selecting `Restart App` starts the Launcher or upgrade helper once, then shuts down the old host as a restart.
- Repeated tray clicks during shutdown are ignored and logged.
- Repeated component-library clicks focus the existing window instead of opening duplicates.