合并对设置系统的更新 (#11)

* Add Windows system chrome patchers (Harmony)

Introduce support for toggling the system chrome on Windows using Harmony patchers. Adds Lib.Harmony.Thin to package props and project, new patcher infrastructure (ChromePatchState, PatcherEntrance) and two Harmony patches that disable FluentAvalonia's Windows chrome when configured. Program.cs now loads the chrome setting and installs patchers conditionally on Windows/x86-x64. Settings viewmodel and view updated: expose IsWindowsOs, require restart on appearance changes, migrate SettingsWindow to FAAppWindow and adapt titlebar/layout (include Windows caption placeholder and footer menu items). Also add a .gitkeep and a build log file.

* Refactor settings window UI and theming

Improve theming and layout for the Settings window and related services.

- MaterialSurfaceService: add special material parameters for SettingsWindowBackground (lower alpha, no blur) and avoid hot-switching real backdrops for non-settings windows.
- GlassEffectService: add AdaptiveSettingsWindowTintBrush + ResolveSettingsWindowTintAlpha to provide optional content tinting tied to system material mode.
- SettingsWindowService: refactor theme application into ApplyThemeVariantAndResources, ensure settings window material is applied at show/activate times, and tidy theme/resource application flow.
- SettingsWindow.axaml / .axaml.cs: restructure title bar (separate Grid.Row=0 border) and FANavigationView host, add pane-footer toggle button for :minimal layout, use dynamic corner radius resource, and update toggle/visibility/icon logic and responsive layout code.
- SettingsPages: remove some IconText usages and adjust margins; use DesignCornerRadiusLg for update card corner radius.
- Add NuGet.Config to set local globalPackagesFolder and ignore .nuget/packages in .gitignore.

These changes aim to improve visuals, avoid backdrop overdraw, and make the settings window behavior consistent across themes and layouts.

* Add localization and localize settings pages

Add many new localization keys (en-US and zh-CN) for notifications, developer tools, about page, status bar, and video wallpaper. Update Notification, Dev, About and StatusBar view models to use LocalizationService, expose localized ObservableProperties, and refresh localized text at construction. Localize selection options and test notification texts, and fix notification severity handling. Wire up XAML to the new localized properties (About/Dev/StatusBar pages) and update the settings page title for notifications. Also adjust copyright line generation and replace hardcoded placeholders with bound Watermark properties.

* Redesign settings window with fluent shell & search

Rebuild the settings window as a Fluent shell: adds a custom 48-DIP titlebar with Back, pane toggle, icon/title, search box, restart/more menu, and caption-button spacer; moves compact pane toggle into the titlebar and preserves FANavigationView as the primary navigation surface. Introduces a SettingsSearchService (with UI AutoComplete integration, search indexing, navigation-by-result, and search result highlighting) plus focused tests for search filtering and theme material normalization. Adds navigation history/back stack, updates SettingsViewModels for new bindings and localization keys, and updates General/Apearance pages to expose new strings and options. Implements an "auto" system material mode: default in AppSettingsSnapshot, new MaterialAuto constants and normalization/resolution logic in ThemeAppearanceValues, WindowMaterialService and MaterialSurfaceService adjustments to prefer Mica on Win11 and Acrylic on Win10 using TransparencyLevelHint. GlassEffectService and AppearanceThemeService updated to use effective material mode and to track live theme state changes. Adds localization entries (en-US, zh-CN), spec/tasks docs, and other UI/style tweaks to support the redesign.

* fix.修折叠与展开按钮

* Add OOBE startup presentation and settings merge

Introduce a new OOBE step for "Startup & Presentation" that exposes startup and UI preferences in OobeWindow (toggles for taskbar, slide/fade transitions, fused popup, and autostart). Add HostAppSettingsOobeMerger to read/write Host settings.json (PascalCase fields) and MergeStartupPresentation behavior, plus LauncherWindowsStartupService to sync the current Launcher into the Windows Run key on Windows. Wire UI handlers, persist choices on Next, and load defaults when entering the step. Include unit tests for the merger, adjust SettingsWindow navigation pane/toggle handling, and update docs/LAUNCHER.md to describe the new OOBE step and implementation files.

* Move whiteboard persistence to file storage

Switch whiteboard note storage from legacy DB rows to per-note JSON files and add migration support. Update WhiteboardNoteSnapshot schema (version bump, viewport, canvas, expires, PathSvgData) and change IWhiteboardNotePersistenceService.SaveNote to return bool to surface write failures (e.g. read-only files). Implement file-based WhiteboardNotePersistenceService with legacy DB migration/cleanup, retention handling, and logging. Add comprehensive unit tests for persistence, stroke path builder, SVG import and viewport helper. Also add ThirdParty/DotNetCampus.InkCanvas project and reference it in the main csproj, and bump PostHog package to 2.6.0.

* Introduce render gate and chart caching

Replace UI DispatcherTimer polling with a StudySnapshotRenderGate across multiple widgets to queue and apply only the latest analytics snapshot; components updated include StudyDeductionReasonsWidget, StudyEnvironmentWidget, StudyInterruptDensityWidget, StudyNoiseCurveWidget. Add StudySnapshotRenderGate implementation to coordinate rendering and monitoring leases and update subscription/lease lifecycle handling (subscribe/unsubscribe, Acquire/Dispose leases, Clear/Dispose gate). Rewrite chart controls (StudyNoiseCurveChartControl and StudyNoiseDistributionScatterChartControl) to use stable logical-time origins, split series into static vs dynamic tails, add geometry/sample caching, stable jitter/coordinate mapping helpers, and expose internal helpers & counts for testing. Add unit tests (StudyComponentRenderingTests) covering the render gate and chart behaviors (layer counts, logical X mapping, stable jitter, cache rebuild). These changes improve rendering correctness and performance by avoiding redundant renders and enabling deterministic chart layout.

* Use MaterialColorSnapshot in appearance flow

Introduce unified material/color spec and tests, and refactor appearance plumbing to use MaterialColorSnapshot as the single source of truth. Add .trae material-color-service spec/checklist/tasks and integration/unit tests for plugin mapping and appearance VM behavior. AppearanceChangedEvent extended with new appearance change flags and HasChanged logic. ComponentEditorMaterialThemeAdapter rewritten to accept MaterialColorSnapshot and derive palette from snapshot data. Simplify AppearanceSettingsPageViewModel and related view code: remove legacy preview/custom-seed UI logic, preserve material/color fields when updating theme or corner radius, and update save calls to use with-expressions. Update ComponentEditorWindow to use adapter-provided OnPrimary brush and minor docs updates.

* Add material color services, plugin DTOs, and tests

Introduce IPC wire-format appearance DTOs (PluginIsolation.Contracts) and clarify they are distinct from the runtime PluginSdk snapshot. Update PluginSdk comments to document the runtime-facing snapshot shape. Change ComponentColorSchemeHelper to use the HostMaterialColorProvider and add an overload that accepts a MaterialColorSnapshot. Add new services and pipelines (MaterialColorService, MaterialSurfaceService, WindowMaterialService, WallpaperColorPipeline) and refactor AppearanceThemeService to depend on MaterialColorService while removing legacy internal implementations. Add multiple unit tests (ComponentColorSchemeHelper, PluginAppearanceBoundary, SettingsCatalogService, WallpaperSettingsPageViewModel) and update localization resources with new material_color and wallpaper keys.

* Add CODE_WIKI and update localization

Add a comprehensive CODE_WIKI.md documenting project architecture, modules, startup flow, plugin system, testing and developer workflows. Update localization resources (en-US.json, zh-CN.json) with new/translated keys for wallpaper controls (custom color UI), material & color settings (semantic roles, surfaces, refresh/polling state), appearance (corner radius), status bar font size options, privacy policy text, component library labels, clock settings, and new language entry (Korean). Also modify settings-related ViewModels and Settings page views to surface these new features and texts (MaterialColorSettingsPageViewModel.cs, SettingsViewModels.cs, WallpaperSettingsPageViewModel.cs, MainWindow.SettingsHardCut.Stubs.cs, ComponentsSettingsPage.axaml, WallpaperSettingsPage.axaml).

* Add Data settings page and storage scanner

Introduce a new "Data" settings page to visualize and manage local app storage. Adds DataStorageService (scanning, disk info, clean operations), DataSettingsPageViewModel, XAML view and code-behind, and HexToColor/HexToBrush converters; registers converters in App.axaml. Also update localization strings for the new page and add icon mapping so the settings entry uses the Database icon. Enables per-category and global cleaning workflows and formatted size display.

* Add IPC backoff/retries and safer disposal

Introduce exponential backoff, jitter and retry logic across IPC components to improve robustness and avoid tight retry loops; make disposal idempotent and add connection guards. Key changes:
- LauncherCoordinatorIpcServer / LauncherIpcServer: add backoff constants, ComputeBackoff(), consecutive error tracking and delayed retries with jitter.
- LanMountainDesktopIpcClient / LauncherIpcClient: add connect retry loops, timeouts, delayed retries, improved error logging, and use ArrayPool for buffered async writes; ensure proper cleanup on failures.
- PublicIpcHostService: add disposed flag, guard OnPeerConnected and Dispose, and clear connected peers on dispose.
- Add many auto-generated commit analysis docs under docs/auto_commit_md and new scripts for analyzing/generating commit docs.
These changes aim to make IPC connection handling more resilient and resource-safe.

* Add preview controls and settings UI tweaks

Introduce GridPreviewControl and CornerRadiusPreviewControl for visual previews and wire them into the Components settings (add ScreenAspectRatio, CornerRadiusPreviewValue, and screen aspect init). Refactor ComponentsSettingsPage UI to show live previews. Improve DataSettingsPage layout and storage bar logic (use item percentages directly, include remaining segment, adjust visuals and visibility triggers). Simplify LauncherSettingsPage header/appearance layout. Add SECURITY_AUDIT_REPORT.md, analysis summary, mockup HTML, and a local .claude settings file.

* Add install checkpoint/resume and DDSS workflows

Introduce install checkpoint support and resume logic for updates, plus related locking and validation. Adds InstallCheckpoint model, AppJsonContext serialization, and UpdatePaths helpers for deployment lock, apply-in-progress lock and install-checkpoint path. UpdateEngineService gains checkpoint load/save/delete, incoming-state validation, resume logic for PLONDS and legacy updates, apply lock handling, and safer cleanup; ApplyPendingPlondsUpdateAsync and ApplyPendingUpdate flow updated accordingly. Add DeploymentLock contract and extend UpdateState with pause/resume/cancel helpers. Tests updated to cover stale/valid checkpoint resume and legacy/PLONDS flows. CI: enhance ddss-publish to detect release channel, validate S3 assets, prepare and atomically publish channel pointer; add ddss-rollback workflow to publish rollbacks; adjust plonds-build concurrency and release events.

* changed.更了好多

* fix.消息盒子媒体播放器组件服务修复

* change.重做天气,为回到系统提供自定义功能。

* feat.airapp与融合桌面

* feat.动画优化与更新界面

* feat.数字时钟,白板功能修复

* feat.完善了时钟轻应用,为启动器提供了多语言支持

* feat.发布与打包优化

* changed.天气选项卡更新
This commit is contained in:
lincube
2026-05-19 07:55:21 +08:00
committed by GitHub
parent 458494d131
commit 7a70476ce8
904 changed files with 78052 additions and 18366 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

@@ -0,0 +1,850 @@
# 启动器 RESX 多语言适配实施计划
> **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:** 为 LanMountainDesktop.Launcher 引入 RESX 资源文件,实现启动器 UI 的多语言适配,消除所有硬编码中英文字符串。
**Architecture:** 在 Launcher 项目中创建 RESX 资源文件体系(默认 zh-CN + en-US/ja-JP/ko-KR通过 .NET 内置 `ResourceManager` 机制实现本地化。启动时从主应用 `settings.json` 读取 `LanguageCode` 字段设置 `CultureInfo.CurrentUICulture`AXAML 中使用 `x:Static` 引用资源C# 代码中通过 `Strings.ResourceName` 强类型访问。
**Tech Stack:** .NET RESX 资源文件、Avalonia `x:Static` 标记扩展、`System.Globalization.CultureInfo`
---
## 现状分析
### 问题概述
1. **启动器完全没有本地化支持**:所有 UI 字符串硬编码,中英文混杂严重
2. **纯英文窗口**SplashWindow、ErrorWindow、MultiInstancePromptWindow、DataLocationPromptWindow、LoadingDetailsWindow
3. **纯中文窗口**OobeWindow、MigrationPromptWindow、UpdateWindow、ErrorDebugWindow、DevDebugWindow、PrivacyPolicyWindow
4. **启动器不读取主应用语言设置**:没有 `LanguageCode` 相关代码
5. **硬编码字符串总量约 180+ 条**,分布在 11 个 AXAML 视图和 11 个 C# code-behind 文件中
### 方案选择RESX vs JSON
| 维度 | RESX本方案 | JSON主项目模式 |
|------|---------------|-------------------|
| 编译时安全 | ✅ 强类型 `Strings.KeyName` | ❌ 字符串键值 `L("key", "fallback")` |
| AXAML 集成 | ✅ `x:Static` 直接引用 | ❌ 需 code-behind 赋值 |
| 回退机制 | ✅ 内置(默认资源 → 特定文化) | ✅ 自定义 `fallback` 参数 |
| 新增语言 | 需添加 RESX 文件并重新编译 | 仅添加 JSON 文件 |
| AOT 兼容性 | ⚠️ 需额外配置 | ✅ 已验证 |
| 与主项目一致性 | ❌ 不同模式 | ✅ 一致 |
**选择 RESX 的理由**:启动器是独立轻量进程,不需要运行时语言切换;强类型访问减少拼写错误;`x:Static` 比 code-behind 赋值更清晰RESX 的内置回退机制足够满足启动器需求。
### AOT 兼容性说明
Launcher 项目支持 Native AOT 发布。RESX 的 `ResourceManager` 依赖反射,需要:
1.`.csproj` 中添加 `<EmbeddedResource>` 确保资源不被修剪
2. 在 AOT props 中添加 `TrimmerRootAssembly` 保留资源程序集
3. 发布后进行 AOT 冒烟测试验证
---
## 文件结构规划
### 新增文件
| 文件 | 职责 |
|------|------|
| `Resources/Strings.resx` | 默认资源文件zh-CN回退资源 |
| `Resources/Strings.en-US.resx` | 英语资源 |
| `Resources/Strings.ja-JP.resx` | 日语资源 |
| `Resources/Strings.ko-KR.resx` | 韩语资源 |
| `Services/LanguagePreferenceService.cs` | 从 settings.json 读取 LanguageCode 并设置 CultureInfo |
### 修改文件
| 文件 | 改动内容 |
|------|---------|
| `LanMountainDesktop.Launcher.csproj` | 添加 RESX 嵌入资源配置 |
| `LanMountainDesktop.Launcher.AOT.props` | 添加资源程序集修剪保留 |
| `Program.cs` | 启动时调用语言偏好初始化 |
| `Views/SplashWindow.axaml` | 替换硬编码字符串为 `x:Static` |
| `Views/SplashWindow.axaml.cs` | 替换 C# 硬编码字符串为 `Strings.XXX` |
| `Views/ErrorWindow.axaml` | 同上 |
| `Views/ErrorWindow.axaml.cs` | 同上 |
| `Views/MultiInstancePromptWindow.axaml` | 同上 |
| `Views/MultiInstancePromptWindow.axaml.cs` | 同上 |
| `Views/DataLocationPromptWindow.axaml` | 同上 |
| `Views/DataLocationPromptWindow.axaml.cs` | 同上 |
| `Views/LoadingDetailsWindow.axaml` | 同上 |
| `Views/LoadingDetailsWindow.axaml.cs` | 同上 |
| `Views/UpdateWindow.axaml` | 同上 |
| `Views/UpdateWindow.axaml.cs` | 同上 |
| `Views/ErrorDebugWindow.axaml` | 同上 |
| `Views/ErrorDebugWindow.axaml.cs` | 同上 |
| `Views/OobeWindow.axaml` | 同上 |
| `Views/OobeWindow.axaml.cs` | 同上 |
| `Views/MigrationPromptWindow.axaml` | 同上 |
| `Views/MigrationPromptWindow.axaml.cs` | 同上 |
| `Views/PrivacyPolicyWindow.axaml` | 同上 |
| `Views/PrivacyPolicyWindow.axaml.cs` | 同上 |
| `Views/DevDebugWindow.axaml` | 同上 |
| `Views/DevDebugWindow.axaml.cs` | 同上 |
| `Services/LauncherFlowCoordinator.cs` | 替换硬编码字符串 |
| `App.axaml.cs` | 替换预览模式硬编码字符串 |
---
## RESX 键命名规范
采用 `ViewName_ElementDescription` 模式PascalCase 分隔:
- 窗口标题:`Splash_Title``Error_Title``MultiInstance_Title`
- 按钮文本:`Error_ButtonOpenLogs``Error_ButtonCopy``Error_ButtonRetry`
- 状态文本:`Splash_StatusInitializing``Loading_StatusPreparing`
- 描述文本:`DataLocation_DescSystemProfile``DataLocation_DescPortable`
- OOBE 步骤:`Oobe_StepWelcomeTitle``Oobe_StepAppearanceTitle`
---
## 实施任务
### Task 1: 创建 RESX 基础设施
**Files:**
- Create: `LanMountainDesktop.Launcher/Resources/Strings.resx`
- Create: `LanMountainDesktop.Launcher/Resources/Strings.en-US.resx`
- Create: `LanMountainDesktop.Launcher/Resources/Strings.ja-JP.resx`
- Create: `LanMountainDesktop.Launcher/Resources/Strings.ko-KR.resx`
- Modify: `LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj`
- Modify: `LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.AOT.props`
- [ ] **Step 1: 创建默认 RESX 文件zh-CN 回退资源)**
创建 `Resources/Strings.resx`,包含所有 180+ 条字符串的中文翻译。此文件同时作为回退资源和中文资源。
```xml
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="type" type="xsd:string" use="optional" />
<xsd:attribute name="mimetype" type="xsd:string" use="optional" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype"><value>text/microsoft-resx</value></resheader>
<resheader name="version"><value>2.0</value></resheader>
<resheader name="reader"><value>System.Resources.ResXResourceReader, System.Windows.Forms</value></resheader>
<resheader name="writer"><value>System.Resources.ResXResourceWriter, System.Windows.Forms</value></resheader>
<!-- SplashWindow -->
<data name="Splash_Title" xml:space="preserve"><value>阑山桌面</value></data>
<data name="Splash_AppName" xml:space="preserve"><value>阑山桌面</value></data>
<data name="Splash_StatusInitializing" xml:space="preserve"><value>正在初始化...</value></data>
<data name="Splash_DebugPreview" xml:space="preserve"><value>[调试模式] 启动画面预览</value></data>
<!-- ErrorWindow -->
<data name="Error_Title" xml:space="preserve"><value>阑山桌面</value></data>
<data name="Error_TitleCannotConfirm" xml:space="preserve"><value>启动器无法确认启动状态</value></data>
<data name="Error_MessageNotReached" xml:space="preserve"><value>阑山桌面未达到预期的启动状态。</value></data>
<data name="Error_SuggestionTitle" xml:space="preserve"><value>启动恢复</value></data>
<data name="Error_SuggestionMessage" xml:space="preserve"><value>您可以检查日志、等待当前进程或激活正在运行的桌面实例。</value></data>
<data name="Error_DiagnosticHeader" xml:space="preserve"><value>诊断详情</value></data>
<data name="Error_ButtonOpenLogs" xml:space="preserve"><value>打开日志</value></data>
<data name="Error_ButtonCopy" xml:space="preserve"><value>复制</value></data>
<data name="Error_ButtonWait" xml:space="preserve"><value>等待</value></data>
<data name="Error_ButtonExit" xml:space="preserve"><value>退出</value></data>
<data name="Error_ButtonRetry" xml:space="preserve"><value>重试</value></data>
<data name="Error_ButtonActivate" xml:space="preserve"><value>激活</value></data>
<data name="Error_DebugTitle" xml:space="preserve"><value>[调试] 启动器错误</value></data>
<data name="Error_HostNotFoundTitle" xml:space="preserve"><value>启动器找不到桌面可执行文件</value></data>
<data name="Error_HostNotFoundMessage" xml:space="preserve"><value>在调试模式下选择另一个可执行文件、检查日志,或在修复部署路径后重试。</value></data>
<data name="Error_GenericMessage" xml:space="preserve"><value>检查日志后重试,等待上一次启动尝试完全结束。</value></data>
<data name="Error_RunningHostMessage" xml:space="preserve"><value>检查日志或退出。旧进程仍在运行时,启动器不会创建新的桌面进程。</value></data>
<data name="Error_PendingTitle" xml:space="preserve"><value>启动仍在进行中</value></data>
<data name="Error_PendingMessage" xml:space="preserve"><value>桌面进程仍在运行,启动器不会启动第二个实例。</value></data>
<!-- MultiInstancePromptWindow -->
<data name="MultiInstance_Title" xml:space="preserve"><value>阑山桌面</value></data>
<data name="MultiInstance_AlreadyRunning" xml:space="preserve"><value>阑山桌面已在运行</value></data>
<data name="MultiInstance_AlreadyRunningMessage" xml:space="preserve"><value>启动器检测到已存在的桌面实例,未启动新进程。</value></data>
<data name="MultiInstance_RepeatedLaunchTitle" xml:space="preserve"><value>重复启动</value></data>
<data name="MultiInstance_RepeatedLaunchMessage" xml:space="preserve"><value>您当前的设置为显示此提示而不自动打开桌面。</value></data>
<data name="MultiInstance_NoSecondProcess" xml:space="preserve"><value>未创建第二个主进程。</value></data>
<data name="MultiInstance_ButtonCopy" xml:space="preserve"><value>复制</value></data>
<data name="MultiInstance_ButtonClose" xml:space="preserve"><value>关闭</value></data>
<data name="MultiInstance_ButtonOpenDesktop" xml:space="preserve"><value>打开桌面</value></data>
<data name="MultiInstance_DetailsFormat" xml:space="preserve"><value>现有主进程 PID: {0}\nShell 状态: {1}\n未创建第二个主进程。</value></data>
<!-- DataLocationPromptWindow -->
<data name="DataLocation_Title" xml:space="preserve"><value>选择数据保存位置</value></data>
<data name="DataLocation_ChooseLocation" xml:space="preserve"><value>选择数据保存位置</value></data>
<data name="DataLocation_ChooseLocationDesc" xml:space="preserve"><value>选择启动器和桌面数据的存储位置。您可以稍后在设置中更改。</value></data>
<data name="DataLocation_NotWritable" xml:space="preserve"><value>应用目录不可写入</value></data>
<data name="DataLocation_NotWritableDesc" xml:space="preserve"><value>当前安装目录需要管理员权限才能写入。数据将存储在系统用户目录中。</value></data>
<data name="DataLocation_SystemProfile" xml:space="preserve"><value>保存在系统用户目录(推荐)</value></data>
<data name="DataLocation_SystemProfileDesc" xml:space="preserve"><value>数据与当前 Windows 用户绑定,在应用重新安装和更新后保持完整。</value></data>
<data name="DataLocation_Portable" xml:space="preserve"><value>保存在应用安装目录(便携模式)</value></data>
<data name="DataLocation_PortableDesc" xml:space="preserve"><value>适用于便携安装。整个应用文件夹可以连同数据一起移动到另一台机器。</value></data>
<data name="DataLocation_ButtonCancel" xml:space="preserve"><value>取消</value></data>
<data name="DataLocation_ButtonConfirm" xml:space="preserve"><value>确认</value></data>
<data name="DataLocation_MigrateWarning" xml:space="preserve"><value>检测到已有的系统数据。选择便携模式将自动迁移当前数据。</value></data>
<!-- LoadingDetailsWindow -->
<data name="Loading_Title" xml:space="preserve"><value>阑山桌面 - 加载详情</value></data>
<data name="Loading_StartingDesktop" xml:space="preserve"><value>正在启动阑山桌面</value></data>
<data name="Loading_StatusInitializing" xml:space="preserve"><value>正在初始化...</value></data>
<data name="Loading_StatusPreparing" xml:space="preserve"><value>正在准备组件</value></data>
<data name="Loading_LoadingItems" xml:space="preserve"><value>加载项目</value></data>
<data name="Loading_Done" xml:space="preserve"><value>完成</value></data>
<data name="Loading_ErrorOccurred" xml:space="preserve"><value>加载时发生错误。</value></data>
<data name="Loading_ButtonDetails" xml:space="preserve"><value>详情</value></data>
<data name="Loading_ButtonCancel" xml:space="preserve"><value>取消</value></data>
<data name="Loading_StageReady" xml:space="preserve"><value>准备就绪</value></data>
<data name="Loading_ItemPlugin" xml:space="preserve"><value>正在加载插件...</value></data>
<data name="Loading_ItemComponent" xml:space="preserve"><value>正在加载组件...</value></data>
<data name="Loading_ItemResource" xml:space="preserve"><value>正在加载资源...</value></data>
<data name="Loading_ItemData" xml:space="preserve"><value>正在加载数据...</value></data>
<data name="Loading_ItemDownload" xml:space="preserve"><value>正在下载...</value></data>
<data name="Loading_ItemProcess" xml:space="preserve"><value>正在处理...</value></data>
<data name="Loading_ItemComplete" xml:space="preserve"><value>完成</value></data>
<data name="Loading_TypePlugin" xml:space="preserve"><value>插件</value></data>
<data name="Loading_TypeComponent" xml:space="preserve"><value>组件</value></data>
<data name="Loading_TypeResource" xml:space="preserve"><value>资源</value></data>
<data name="Loading_TypeData" xml:space="preserve"><value>数据</value></data>
<data name="Loading_TypeNetwork" xml:space="preserve"><value>网络</value></data>
<data name="Loading_TypeSettings" xml:space="preserve"><value>设置</value></data>
<data name="Loading_TypeSystem" xml:space="preserve"><value>系统</value></data>
<data name="Loading_TypeOther" xml:space="preserve"><value>其他</value></data>
<!-- UpdateWindow -->
<data name="Update_Title" xml:space="preserve"><value>阑山桌面 - 更新</value></data>
<data name="Update_AppName" xml:space="preserve"><value>阑山桌面</value></data>
<data name="Update_StatusUpdate" xml:space="preserve"><value>更新</value></data>
<data name="Update_StatusUpdating" xml:space="preserve"><value>正在更新,请稍候...</value></data>
<data name="Update_Complete" xml:space="preserve"><value>更新完成</value></data>
<data name="Update_Failed" xml:space="preserve"><value>更新失败</value></data>
<data name="Update_FailedMessage" xml:space="preserve"><value>更新过程中发生错误</value></data>
<data name="Update_DebugTitle" xml:space="preserve"><value>[调试模式] 更新页面</value></data>
<data name="Update_DebugMessage" xml:space="preserve"><value>预览更新进度界面</value></data>
<!-- ErrorDebugWindow -->
<data name="DebugDebug_Title" xml:space="preserve"><value>调试模式</value></data>
<data name="DebugDebug_SettingsTitle" xml:space="preserve"><value>调试设置</value></data>
<data name="DebugDebug_DevMode" xml:space="preserve"><value>开发模式</value></data>
<data name="DebugDebug_DevModeDesc" xml:space="preserve"><value>启用后自动扫描开发目录</value></data>
<data name="DebugDebug_On" xml:space="preserve"><value></value></data>
<data name="DebugDebug_Off" xml:space="preserve"><value></value></data>
<data name="DebugDebug_AppPath" xml:space="preserve"><value>应用路径</value></data>
<data name="DebugDebug_NotSelected" xml:space="preserve"><value>未选择</value></data>
<data name="DebugDebug_Browse" xml:space="preserve"><value>浏览...</value></data>
<data name="DebugDebug_Warning" xml:space="preserve"><value>此功能仅供开发人员使用</value></data>
<data name="DebugDebug_ButtonCancel" xml:space="preserve"><value>取消</value></data>
<data name="DebugDebug_ButtonOk" xml:space="preserve"><value>确定</value></data>
<data name="DebugDebug_SelectExeDialog" xml:space="preserve"><value>选择阑山桌面主程序可执行文件</value></data>
<!-- OobeWindow -->
<data name="Oobe_Title" xml:space="preserve"><value>欢迎使用阑山桌面</value></data>
<data name="Oobe_WelcomeTitle" xml:space="preserve"><value>欢迎使用阑山桌面</value></data>
<data name="Oobe_WelcomeSubtitle" xml:space="preserve"><value>你的桌面,不止一面</value></data>
<data name="Oobe_ButtonGetStarted" xml:space="preserve"><value>开始使用</value></data>
<data name="Oobe_AppearanceTitle" xml:space="preserve"><value>个性化你的桌面</value></data>
<data name="Oobe_AppearanceDesc" xml:space="preserve"><value>选择你喜欢的主题样式,可随时在设置中更改</value></data>
<data name="Oobe_AppearanceMode" xml:space="preserve"><value>外观模式</value></data>
<data name="Oobe_LightMode" xml:space="preserve"><value>浅色模式</value></data>
<data name="Oobe_DarkMode" xml:space="preserve"><value>深色模式</value></data>
<data name="Oobe_ThemeColor" xml:space="preserve"><value>主题色</value></data>
<data name="Oobe_MonetSource" xml:space="preserve"><value>莫奈取色来源</value></data>
<data name="Oobe_MonetFromWallpaper" xml:space="preserve"><value>从桌面壁纸取色</value></data>
<data name="Oobe_MonetFromCustomImage" xml:space="preserve"><value>自定义图片取色</value></data>
<data name="Oobe_MonetDisabled" xml:space="preserve"><value>不使用莫奈取色</value></data>
<data name="Oobe_DataLocationTitle" xml:space="preserve"><value>选择数据保存位置</value></data>
<data name="Oobe_SystemProfile" xml:space="preserve"><value>保存在系统用户目录(推荐)</value></data>
<data name="Oobe_SystemProfileDesc" xml:space="preserve"><value>数据与当前 Windows 用户绑定,在应用重新安装和更新后保持完整。</value></data>
<data name="Oobe_Portable" xml:space="preserve"><value>保存在应用安装目录(便携模式)</value></data>
<data name="Oobe_PortableDesc" xml:space="preserve"><value>适用于便携安装。整个应用文件夹可以连同数据一起移动到另一台机器。</value></data>
<data name="Oobe_NotWritable" xml:space="preserve"><value>无法保存到应用目录</value></data>
<data name="Oobe_NotWritableDesc" xml:space="preserve"><value>当前安装目录需要管理员权限才能写入。数据将存储在系统用户目录中。</value></data>
<data name="Oobe_StartupTitle" xml:space="preserve"><value>启动与展示</value></data>
<data name="Oobe_ShowInTaskbar" xml:space="preserve"><value>在任务栏显示主桌面窗口</value></data>
<data name="Oobe_SlideTransition" xml:space="preserve"><value>以滑动方式显示主窗口</value></data>
<data name="Oobe_FadeTransition" xml:space="preserve"><value>启动时使用淡入过渡</value></data>
<data name="Oobe_FusedDesktop" xml:space="preserve"><value>融合桌面与弹入手势</value></data>
<data name="Oobe_AutoStart" xml:space="preserve"><value>登录 Windows 时自动启动阑山桌面</value></data>
<data name="Oobe_PrivacyTitle" xml:space="preserve"><value>信息与隐私</value></data>
<data name="Oobe_CrashReports" xml:space="preserve"><value>发送匿名崩溃报告</value></data>
<data name="Oobe_UsageStats" xml:space="preserve"><value>发送匿名使用统计</value></data>
<data name="Oobe_PrivacyTrackingId" xml:space="preserve"><value>隐私追踪 ID</value></data>
<data name="Oobe_Agree" xml:space="preserve"><value>同意</value></data>
<data name="Oobe_PrivacyPolicyLink" xml:space="preserve"><value>《阑山桌面遥测隐私数据收集协议》</value></data>
<data name="Oobe_ButtonBack" xml:space="preserve"><value>返回</value></data>
<data name="Oobe_ButtonNext" xml:space="preserve"><value>下一步</value></data>
<data name="Oobe_CompleteTitle" xml:space="preserve"><value>欢迎使用阑山桌面</value></data>
<data name="Oobe_CompleteSubtitle" xml:space="preserve"><value>你的桌面,不止一面</value></data>
<!-- MigrationPromptWindow -->
<data name="Migration_Title" xml:space="preserve"><value>阑山桌面 - 版本迁移</value></data>
<data name="Migration_DetectedOldVersion" xml:space="preserve"><value>检测到旧版本</value></data>
<data name="Migration_DetectedDesc" xml:space="preserve"><value>检测到您的系统中安装了旧版本的阑山桌面0.8.4...</value></data>
<data name="Migration_Version" xml:space="preserve"><value>版本:</value></data>
<data name="Migration_Location" xml:space="preserve"><value>位置:</value></data>
<data name="Migration_Type" xml:space="preserve"><value>类型:</value></data>
<data name="Migration_Installed" xml:space="preserve"><value>安装版</value></data>
<data name="Migration_UninstallNote" xml:space="preserve"><value>卸载旧版本不会影响新版本的使用,您的个人数据将保留。</value></data>
<data name="Migration_ButtonViewLocation" xml:space="preserve"><value>查看位置</value></data>
<data name="Migration_ButtonSkip" xml:space="preserve"><value>暂不处理</value></data>
<data name="Migration_ButtonUninstall" xml:space="preserve"><value>卸载旧版本</value></data>
<!-- PrivacyPolicyWindow -->
<data name="Privacy_Title" xml:space="preserve"><value>阑山桌面遥测隐私数据收集协议</value></data>
<data name="Privacy_Header" xml:space="preserve"><value>阑山桌面遥测隐私数据收集协议</value></data>
<data name="Privacy_Description" xml:space="preserve"><value>请仔细阅读以下协议内容,了解我们如何收集、使用和保护您的数据</value></data>
<data name="Privacy_ButtonClose" xml:space="preserve"><value>关闭</value></data>
<!-- DevDebugWindow -->
<data name="DevDebug_Title" xml:space="preserve"><value>开发调试窗口</value></data>
<data name="DevDebug_Splash" xml:space="preserve"><value>启动画面</value></data>
<data name="DevDebug_Error" xml:space="preserve"><value>错误页面</value></data>
<data name="DevDebug_Update" xml:space="preserve"><value>更新页面</value></data>
<data name="DevDebug_Oobe" xml:space="preserve"><value>OOBE页面</value></data>
<data name="DevDebug_DataLocation" xml:space="preserve"><value>数据位置选择</value></data>
<data name="DevDebug_EnableFeature" xml:space="preserve"><value>启用功能</value></data>
<data name="DevDebug_Open" xml:space="preserve"><value>打开</value></data>
<data name="DevDebug_SetAllViewMode" xml:space="preserve"><value>全部设为查看模式</value></data>
<data name="DevDebug_SetAllFunctionMode" xml:space="preserve"><value>全部设为功能模式</value></data>
<data name="DevDebug_Close" xml:space="preserve"><value>关闭</value></data>
<!-- LauncherFlowCoordinator -->
<data name="Coordinator_SlowDeviceMessage" xml:space="preserve"><value>设备较慢,仍在启动,请稍候。</value></data>
<data name="Coordinator_RunningHostMessage" xml:space="preserve"><value>桌面主进程仍在运行Launcher 会继续等待,不会重复启动。</value></data>
<!-- App.axaml.cs preview strings -->
<data name="Preview_SplashInitializing" xml:space="preserve"><value>正在初始化...</value></data>
<data name="Preview_SplashCheckingUpdates" xml:space="preserve"><value>正在检查更新...</value></data>
<data name="Preview_SplashCheckingPlugins" xml:space="preserve"><value>正在检查插件...</value></data>
<data name="Preview_SplashLaunchingHost" xml:space="preserve"><value>正在启动主程序...</value></data>
<data name="Preview_SplashReady" xml:space="preserve"><value>准备就绪</value></data>
<data name="Preview_ErrorMessage" xml:space="preserve"><value>[预览] 这是启动器错误窗口预览。</value></data>
<data name="Preview_UpdateProcessing" xml:space="preserve"><value>正在处理 {0}...</value></data>
<data name="Preview_ActivationConnecting" xml:space="preserve"><value>正在连接到活跃的启动器...</value></data>
</root>
```
- [ ] **Step 2: 创建 en-US RESX 文件**
创建 `Resources/Strings.en-US.resx`,包含所有字符串的英文翻译。结构与默认文件相同,仅 `<value>` 内容为英文。
```xml
<!-- 示例条目 -->
<data name="Splash_Title" xml:space="preserve"><value>LanMountain Desktop</value></data>
<data name="Splash_AppName" xml:space="preserve"><value>LanMountain Desktop</value></data>
<data name="Splash_StatusInitializing" xml:space="preserve"><value>Initializing...</value></data>
<data name="Error_TitleCannotConfirm" xml:space="preserve"><value>Launcher could not confirm startup</value></data>
<data name="Error_MessageNotReached" xml:space="preserve"><value>LanMountain Desktop did not reach the expected startup state.</value></data>
<!-- ... 所有键的英文翻译 ... -->
```
- [ ] **Step 3: 创建 ja-JP RESX 文件**
创建 `Resources/Strings.ja-JP.resx`,包含所有字符串的日语翻译。
- [ ] **Step 4: 创建 ko-KR RESX 文件**
创建 `Resources/Strings.ko-KR.resx`,包含所有字符串的韩语翻译。
- [ ] **Step 5: 修改 .csproj 添加 RESX 配置**
`LanMountainDesktop.Launcher.csproj``<ItemGroup>` 中添加:
```xml
<ItemGroup>
<EmbeddedResource Update="Resources\Strings.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Strings.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
```
注意:使用 `PublicResXFileCodeGenerator` 而非 `ResXFileCodeGenerator`,生成 `public` 类以便 AXAML 的 `x:Static` 可以访问。
- [ ] **Step 6: 修改 AOT props 添加资源程序集保留**
`LanMountainDesktop.Launcher.AOT.props` 的 AOT 修剪配置 `<ItemGroup>` 中添加:
```xml
<TrimmerRootAssembly Include="LanMountainDesktop.Launcher" />
```
- [ ] **Step 7: 运行构建验证 RESX 生成**
Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug`
Expected: 构建成功,`Resources/Strings.Designer.cs` 自动生成
---
### Task 2: 创建语言偏好服务
**Files:**
- Create: `LanMountainDesktop.Launcher/Services/LanguagePreferenceService.cs`
- Modify: `LanMountainDesktop.Launcher/Program.cs`
- [ ] **Step 1: 创建 LanguagePreferenceService**
```csharp
using System.Globalization;
using System.Text.Json.Nodes;
namespace LanMountainDesktop.Launcher.Services;
internal static class LanguagePreferenceService
{
public static string ResolveLanguageCode(string appRoot)
{
try
{
var dataLocationResolver = new DataLocationResolver(appRoot);
var settingsPath = HostAppSettingsOobeMerger.GetSettingsFilePath(dataLocationResolver.ResolveDataRoot());
if (!File.Exists(settingsPath))
{
return "zh-CN";
}
var root = JsonNode.Parse(File.ReadAllText(settingsPath))?.AsObject();
if (root is not null &&
root.TryGetPropertyValue("LanguageCode", out var node) &&
node is JsonValue value &&
value.TryGetValue<string>(out var code) &&
!string.IsNullOrWhiteSpace(code))
{
return NormalizeLanguageCode(code);
}
}
catch
{
}
return "zh-CN";
}
public static void ApplyLanguage(string languageCode)
{
var normalized = NormalizeLanguageCode(languageCode);
var culture = CultureInfo.GetCultureInfo(normalized);
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
}
private static string NormalizeLanguageCode(string code)
{
return code.ToLowerInvariant() switch
{
"en-us" or "en" => "en-US",
"ja-jp" or "ja" => "ja-JP",
"ko-kr" or "ko" => "ko-KR",
_ => "zh-CN"
};
}
}
```
- [ ] **Step 2: 在 Program.cs 中调用语言初始化**
`Program.Main` 方法中,`BuildAvaloniaApp().StartWithClassicDesktopLifetime(args)` 之前添加语言初始化:
```csharp
var appRoot = Commands.ResolveAppRoot(commandContext);
var languageCode = LanguagePreferenceService.ResolveLanguageCode(appRoot);
LanguagePreferenceService.ApplyLanguage(languageCode);
```
- [ ] **Step 3: 构建验证**
Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug`
Expected: 构建成功
---
### Task 3: 替换 SplashWindow 硬编码字符串
**Files:**
- Modify: `LanMountainDesktop.Launcher/Views/SplashWindow.axaml`
- Modify: `LanMountainDesktop.Launcher/Views/SplashWindow.axaml.cs`
- [ ] **Step 1: 在 SplashWindow.axaml 中添加 RESX 命名空间并替换字符串**
`<Window>` 标签添加命名空间:
```xml
xmlns:res="clr-namespace:LanMountainDesktop.Launcher.Resources"
```
替换硬编码字符串:
- `Title="LanMountain Desktop"``Title="{x:Static res:Strings.Splash_Title}"`
- `Text="LanMountain Desktop"` (AppNameText) → `Text="{x:Static res:Strings.Splash_AppName}"`
- `Text="Initializing..."` (StatusText) → `Text="{x:Static res:Strings.Splash_StatusInitializing}"`
注意:`VersionText``Text="0.0.0-dev (Administrate)"` 是动态设置的占位文本,保留原样(由 code-behind `SetVersionInfo` 方法设置)。
- [ ] **Step 2: 在 SplashWindow.axaml.cs 中替换 C# 硬编码字符串**
`"[Debug Mode] Splash Preview"` 替换为 `Strings.Splash_DebugPreview`
- [ ] **Step 3: 构建验证**
Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug`
Expected: 构建成功
---
### Task 4: 替换 ErrorWindow 硬编码字符串
**Files:**
- Modify: `LanMountainDesktop.Launcher/Views/ErrorWindow.axaml`
- Modify: `LanMountainDesktop.Launcher/Views/ErrorWindow.axaml.cs`
- [ ] **Step 1: 在 ErrorWindow.axaml 中添加 RESX 命名空间并替换字符串**
添加命名空间 `xmlns:res="clr-namespace:LanMountainDesktop.Launcher.Resources"`
AXAML 替换:
- `Title="LanMountain Desktop"``Title="{x:Static res:Strings.Error_Title}"`
- `Text="Launcher could not confirm startup"``Text="{x:Static res:Strings.Error_TitleCannotConfirm}"`
- `Text="LanMountain Desktop did not reach..."``Text="{x:Static res:Strings.Error_MessageNotReached}"`
- `Title="Startup recovery"``Title="{x:Static res:Strings.Error_SuggestionTitle}"`
- `Message="You can inspect logs..."``Message="{x:Static res:Strings.Error_SuggestionMessage}"`
- `Header="Diagnostic details"``Header="{x:Static res:Strings.Error_DiagnosticHeader}"`
- `Text="Open Logs"``Text="{x:Static res:Strings.Error_ButtonOpenLogs}"`
- `Text="Copy"``Text="{x:Static res:Strings.Error_ButtonCopy}"`
- `Content="Wait"``Content="{x:Static res:Strings.Error_ButtonWait}"`
- `Text="Exit"``Text="{x:Static res:Strings.Error_ButtonExit}"`
- `Content="Retry"``Content="{x:Static res:Strings.Error_ButtonRetry}"`
- [ ] **Step 2: 在 ErrorWindow.axaml.cs 中替换 C# 硬编码字符串**
将所有硬编码字符串替换为 `Strings.XXX` 调用:
- `"LanMountain Desktop did not reach..."``Strings.Error_MessageNotReached`
- `"[Debug] Launcher error"``Strings.Error_DebugTitle`
- `"Launcher could not find the desktop executable"``Strings.Error_HostNotFoundTitle`
- `"Pick another executable..."``Strings.Error_HostNotFoundMessage`
- `"Launcher could not confirm startup"``Strings.Error_TitleCannotConfirm`
- `"Inspect logs, then retry..."``Strings.Error_GenericMessage`
- `"Inspect logs or exit..."``Strings.Error_RunningHostMessage`
- `"Retry"``Strings.Error_ButtonRetry`
- `"Activate"``Strings.Error_ButtonActivate`
- `"Wait"``Strings.Error_ButtonWait`
- `"Startup is still pending"``Strings.Error_PendingTitle`
- `"The desktop process is still running..."``Strings.Error_PendingMessage`
- [ ] **Step 3: 构建验证**
Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug`
Expected: 构建成功
---
### Task 5: 替换 MultiInstancePromptWindow 硬编码字符串
**Files:**
- Modify: `LanMountainDesktop.Launcher/Views/MultiInstancePromptWindow.axaml`
- Modify: `LanMountainDesktop.Launcher/Views/MultiInstancePromptWindow.axaml.cs`
- [ ] **Step 1: 在 MultiInstancePromptWindow.axaml 中替换字符串**
添加命名空间,替换:
- `Title="LanMountain Desktop"``Title="{x:Static res:Strings.MultiInstance_Title}"`
- `Text="LanMountain Desktop is already running"``Text="{x:Static res:Strings.MultiInstance_AlreadyRunning}"`
- `Text="Launcher found an existing..."``Text="{x:Static res:Strings.MultiInstance_AlreadyRunningMessage}"`
- `Title="Repeated launch"``Title="{x:Static res:Strings.MultiInstance_RepeatedLaunchTitle}"`
- `Message="Your current setting..."``Message="{x:Static res:Strings.MultiInstance_RepeatedLaunchMessage}"`
- `Text="No second Host process..."``Text="{x:Static res:Strings.MultiInstance_NoSecondProcess}"`
- `Text="Copy"``Text="{x:Static res:Strings.MultiInstance_ButtonCopy}"`
- `Text="Close"``Text="{x:Static res:Strings.MultiInstance_ButtonClose}"`
- `Text="Open desktop"``Text="{x:Static res:Strings.MultiInstance_ButtonOpenDesktop}"`
- [ ] **Step 2: 在 MultiInstancePromptWindow.axaml.cs 中替换 C# 硬编码字符串**
将格式化字符串替换为 `string.Format(Strings.MultiInstance_DetailsFormat, processId, shellState)` 等。
- [ ] **Step 3: 构建验证**
Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug`
Expected: 构建成功
---
### Task 6: 替换 DataLocationPromptWindow 硬编码字符串
**Files:**
- Modify: `LanMountainDesktop.Launcher/Views/DataLocationPromptWindow.axaml`
- Modify: `LanMountainDesktop.Launcher/Views/DataLocationPromptWindow.axaml.cs`
- [ ] **Step 1: 在 DataLocationPromptWindow.axaml 中替换字符串**
替换所有 12 个硬编码字符串为 `x:Static` 引用。
- [ ] **Step 2: 在 DataLocationPromptWindow.axaml.cs 中替换 C# 硬编码字符串**
`"Existing system data was detected..."` 替换为 `Strings.DataLocation_MigrateWarning`
- [ ] **Step 3: 构建验证**
Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug`
Expected: 构建成功
---
### Task 7: 替换 LoadingDetailsWindow 硬编码字符串
**Files:**
- Modify: `LanMountainDesktop.Launcher/Views/LoadingDetailsWindow.axaml`
- Modify: `LanMountainDesktop.Launcher/Views/LoadingDetailsWindow.axaml.cs`
- [ ] **Step 1: 在 LoadingDetailsWindow.axaml 中替换字符串**
替换所有硬编码字符串为 `x:Static` 引用。
- [ ] **Step 2: 在 LoadingDetailsWindow.axaml.cs 中替换 C# 硬编码字符串**
替换 `GetStageDescription``GetItemDescription``GetTypeLabel` 方法中的硬编码字符串为 `Strings.XXX` 调用。
- [ ] **Step 3: 构建验证**
Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug`
Expected: 构建成功
---
### Task 8: 替换 UpdateWindow 硬编码字符串
**Files:**
- Modify: `LanMountainDesktop.Launcher/Views/UpdateWindow.axaml`
- Modify: `LanMountainDesktop.Launcher/Views/UpdateWindow.axaml.cs`
- [ ] **Step 1: 在 UpdateWindow.axaml 中替换字符串**
替换 `"Update"``x:Static res:Strings.Update_StatusUpdate`
- [ ] **Step 2: 在 UpdateWindow.axaml.cs 中替换 C# 硬编码字符串**
替换 `"更新完成"``"更新失败"``"更新过程中发生错误"``"[调试模式] 更新页面"``"预览更新进度界面"``Strings.XXX` 调用。
- [ ] **Step 3: 构建验证**
Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug`
Expected: 构建成功
---
### Task 9: 替换 ErrorDebugWindow 硬编码字符串
**Files:**
- Modify: `LanMountainDesktop.Launcher/Views/ErrorDebugWindow.axaml`
- Modify: `LanMountainDesktop.Launcher/Views/ErrorDebugWindow.axaml.cs`
- [ ] **Step 1: 在 ErrorDebugWindow.axaml 中替换字符串**
该窗口已使用中文,替换所有硬编码中文字符串为 `x:Static` 引用。
- [ ] **Step 2: 在 ErrorDebugWindow.axaml.cs 中替换 C# 硬编码字符串**
替换 `"Select LanMountainDesktop host executable"``"Not selected"``Strings.DebugDebug_SelectExeDialog``Strings.DebugDebug_NotSelected`
- [ ] **Step 3: 构建验证**
Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug`
Expected: 构建成功
---
### Task 10: 替换 OobeWindow 硬编码字符串
**Files:**
- Modify: `LanMountainDesktop.Launcher/Views/OobeWindow.axaml`
- Modify: `LanMountainDesktop.Launcher/Views/OobeWindow.axaml.cs`
这是最大的单个任务OobeWindow 有约 42 个硬编码字符串。
- [ ] **Step 1: 在 OobeWindow.axaml 中替换字符串**
添加命名空间,逐个替换所有硬编码中文字符串为 `x:Static` 引用。包括:
- 窗口标题、欢迎页文本
- 外观设置页文本
- 数据位置页文本
- 启动展示页文本
- 隐私页文本
- 完成页文本
- 导航按钮文本
- [ ] **Step 2: 在 OobeWindow.axaml.cs 中替换 C# 硬编码字符串(如有)**
检查 code-behind 中是否有动态设置的硬编码字符串并替换。
- [ ] **Step 3: 构建验证**
Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug`
Expected: 构建成功
---
### Task 11: 替换 MigrationPromptWindow 硬编码字符串
**Files:**
- Modify: `LanMountainDesktop.Launcher/Views/MigrationPromptWindow.axaml`
- Modify: `LanMountainDesktop.Launcher/Views/MigrationPromptWindow.axaml.cs`
- [ ] **Step 1: 在 MigrationPromptWindow.axaml 中替换字符串**
替换所有硬编码中文字符串为 `x:Static` 引用。
- [ ] **Step 2: 在 MigrationPromptWindow.axaml.cs 中替换 C# 硬编码字符串(如有)**
- [ ] **Step 3: 构建验证**
Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug`
Expected: 构建成功
---
### Task 12: 替换 PrivacyPolicyWindow 硬编码字符串
**Files:**
- Modify: `LanMountainDesktop.Launcher/Views/PrivacyPolicyWindow.axaml`
- Modify: `LanMountainDesktop.Launcher/Views/PrivacyPolicyWindow.axaml.cs`
- [ ] **Step 1: 在 PrivacyPolicyWindow.axaml 中替换字符串**
替换标题、描述、关闭按钮等硬编码字符串。
- [ ] **Step 2: 在 PrivacyPolicyWindow.axaml.cs 中处理隐私政策正文**
隐私政策正文(约 80 行 Markdown目前硬编码在 C# 中。考虑:
- 方案 A将 Markdown 正文也放入 RESX支持多语言隐私政策
- 方案 B保留 Markdown 正文在 C# 中,仅替换窗口标题和按钮
推荐方案 A将隐私政策 Markdown 正文放入 RESX 的 `Privacy_PolicyContent` 键中。
- [ ] **Step 3: 构建验证**
Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug`
Expected: 构建成功
---
### Task 13: 替换 DevDebugWindow 硬编码字符串
**Files:**
- Modify: `LanMountainDesktop.Launcher/Views/DevDebugWindow.axaml`
- Modify: `LanMountainDesktop.Launcher/Views/DevDebugWindow.axaml.cs`
- [ ] **Step 1: 在 DevDebugWindow.axaml 中替换字符串**
替换所有硬编码中文字符串为 `x:Static` 引用。
- [ ] **Step 2: 在 DevDebugWindow.axaml.cs 中替换 C# 硬编码字符串(如有)**
- [ ] **Step 3: 构建验证**
Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug`
Expected: 构建成功
---
### Task 14: 替换 LauncherFlowCoordinator 和 App.axaml.cs 硬编码字符串
**Files:**
- Modify: `LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs`
- Modify: `LanMountainDesktop.Launcher/App.axaml.cs`
- [ ] **Step 1: 在 LauncherFlowCoordinator.cs 中替换字符串**
替换:
- `"设备较慢,仍在启动,请稍候。"``Strings.Coordinator_SlowDeviceMessage`
- `"桌面主进程仍在运行..."``Strings.Coordinator_RunningHostMessage`
- [ ] **Step 2: 在 App.axaml.cs 中替换预览模式字符串**
替换 `SimulateSplashPreviewAsync` 中的硬编码消息数组:
```csharp
var messages = new[] { Strings.Preview_SplashInitializing, Strings.Preview_SplashCheckingUpdates, Strings.Preview_SplashCheckingPlugins, Strings.Preview_SplashLaunchingHost, Strings.Preview_SplashReady };
```
替换 `HandlePreviewCommand` 中的 `"[Preview] This is the launcher error window preview."``Strings.Preview_ErrorMessage`
替换 `RunApplyUpdateWithWindowAsync` 中的硬编码字符串:
- `"Verifying update..."` → 使用 RESX 键
- `"Applying plugin upgrades..."` → 使用 RESX 键
- `"Cleaning up old deployments..."` → 使用 RESX 键
替换 `SimulateUpdatePreviewAsync` 中的 `$"Processing {stages[i]}..."``string.Format(Strings.Preview_UpdateProcessing, stages[i])`
替换 `AttachToExistingCoordinatorAsync` 中的 `"Connecting to the active launcher..."``Strings.Preview_ActivationConnecting`
- [ ] **Step 3: 构建验证**
Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug`
Expected: 构建成功
---
### Task 15: 完整构建和运行验证
**Files:** 无新增/修改
- [ ] **Step 1: 完整解决方案构建**
Run: `dotnet build LanMountainDesktop.slnx -c Debug`
Expected: 构建成功,无错误
- [ ] **Step 2: 运行启动器预览命令验证中文**
Run: `dotnet run --project LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -- preview-splash`
Expected: 启动画面显示中文
- [ ] **Step 3: 验证英文模式**
临时将 `LanguagePreferenceService.ResolveLanguageCode` 返回 `"en-US"` 后运行预览命令,验证英文显示。
- [ ] **Step 4: 运行测试**
Run: `dotnet test LanMountainDesktop.slnx -c Debug`
Expected: 所有测试通过
---
### Task 16: AOT 发布冒烟测试
**Files:** 无新增/修改
- [ ] **Step 1: AOT 发布测试**
Run: `dotnet publish LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Release -r win-x64 /p:PublishAot=true`
Expected: 发布成功
- [ ] **Step 2: 运行 AOT 发布产物验证**
运行发布后的可执行文件,验证 RESX 资源正确加载。
---
## 实施顺序建议
1. **Task 1** (RESX 基础设施) → **Task 2** (语言偏好服务) — 必须首先完成
2. **Task 3-9** (英文窗口) — 优先处理,解决用户提出的"只有英文"问题
3. **Task 10-13** (中文窗口) — 次优先,完成完整 i18n 覆盖
4. **Task 14** (服务层和 App) — 与 Task 3-13 并行或随后
5. **Task 15-16** (验证) — 最后执行
## 风险与注意事项
1. **AOT 兼容性**`ResourceManager` 在 Native AOT 下可能需要额外配置。如果 AOT 发布失败,需要添加 `DynamicDependency` 属性或使用 `System.Resources.Extensions` 包的源生成器。
2. **OOBE 首次运行**OOBE 在首次运行时 `settings.json` 不存在,此时 `LanguagePreferenceService` 会回退到 `zh-CN`。这是合理的行为。
3. **`x:Static` 与 Avalonia CompiledBindings**:项目启用了 `AvaloniaUseCompiledBindingsByDefault`,需要确认 `x:Static` 在编译绑定模式下正常工作。如有问题,可在特定 AXAML 文件中添加 `x:CompileBindings="False"`
4. **RESX Designer.cs 生成**:确保 `.csproj` 中使用 `PublicResXFileCodeGenerator` 生成 `public` 类,否则 `x:Static` 无法访问。
5. **隐私政策多语言**:隐私政策 Markdown 正文较长,放入 RESX 可能影响可读性。可考虑保留在 C# 中或使用独立资源文件。

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

@@ -0,0 +1,559 @@
# 天气组件 Material Design 重设计计划
> **目标:** 全面重构阑山桌面天气组件的视觉设计,遵循 Material Design 3 规范,参考 Google Weather、几何天气、Breez 天气和柠檬天气的设计语言。
---
## 当前状态分析
### 现有组件
1. **WeatherWidget** - 基础天气(温度+天气状况+位置)
2. **ExtendedWeatherWidget** - 扩展天气(含指标、逐小时、逐日预报)
3. **HourlyWeatherWidget** - 逐小时天气
4. **MultiDayWeatherWidget** - 多日天气
5. **WeatherClockWidget** - 天气时钟
### 现有问题
- 排版层次不清晰,文字大小对比不够
- 布局过于紧凑,缺乏呼吸感
- 内部卡片使用简单纯色背景,缺乏 Material 风格
- 背景场景和前景内容缺乏深度分离
- 圆角和间距不统一
### 现有视觉系统
- 4套调色板Google默认、Geometric、Breezy、LemonFlutter
- 动态背景场景MaterialWeatherSceneControl 绘制渐变+装饰
- 图标系统WeatherIconView + WeatherIconAssetResolver
---
## 设计方向
### 核心原则
1. **Material Design 3** - 使用 M3 的排版、颜色、间距和形状规范
2. **信息层级清晰** - 大字体温度、次要信息弱化
3. **呼吸感** - 合理的间距和留白
4. **深度感** - 前景卡片与背景场景分离
5. **圆角一致性** - 遵循 DesignCornerRadius 规范
### 参考风格
- **Google Weather** - 大字体温度、清晰层级、圆角卡片、柔和渐变
- **几何天气** - 几何装饰、现代感
- **Breez** - 清新留白、柔和色彩
- **柠檬天气** - 活泼明亮
---
## 具体改动计划
### Task 1: 优化 MaterialWeatherPalette 和调色板系统
**文件:** `LanMountainDesktop/Views/Components/MaterialWeatherVisualTheme.cs`
**改动:**
- 调整所有调色板的对比度,确保文字可读性
- 优化背景渐变色彩,更加柔和自然
- 统一文字主色和次色的对比度比例
- 为每个风格增加 `SurfaceColor``SurfaceVariantColor` 用于卡片背景
**当前调色板字段:**
```csharp
public sealed record MaterialWeatherPalette(
Color BackgroundTop,
Color BackgroundBottom,
Color PrimaryShape,
Color SecondaryShape,
Color AccentShape,
Color TextPrimary,
Color TextSecondary,
Color SurfaceTint,
Color OverlayTint);
```
**新增字段:**
```csharp
Color SurfaceColor, // 卡片表面色(低透明度白色/黑色)
Color SurfaceVariantColor, // 变体表面色
Color OutlineColor // 分割线/边框色
```
---
### Task 2: 重构 WeatherWidget基础天气组件
**文件:**
- `LanMountainDesktop/Views/Components/WeatherWidget.axaml`
- `LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs`
**设计目标:**
- 大字体温度显示(类似 Google Weather
- 天气状况文字清晰可读
- 位置和温度范围弱化显示
- 图标与文字对齐优化
**XAML 改动:**
```xml
<Border x:Name="RootBorder"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
ClipToBounds="True">
<Grid>
<components:MaterialWeatherSceneControl x:Name="Scene" />
<Border x:Name="OverlayBorder" />
<!-- 主内容区 -->
<Grid x:Name="ContentGrid"
RowDefinitions="*,Auto"
Margin="20,16,20,14">
<!-- 上半区:温度 + 图标 -->
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="4">
<!-- 温度:超大字体 -->
<TextBlock x:Name="TemperatureTextBlock"
Text="--°"
FontSize="72"
FontWeight="Bold"
LineHeight="72" />
<!-- 天气状况 -->
<TextBlock x:Name="ConditionTextBlock"
Text="Loading"
FontSize="18"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis" />
</StackPanel>
<!-- 右侧图标 -->
<components:WeatherIconView x:Name="MainIcon"
Grid.Column="1"
Width="72"
Height="72"
HorizontalAlignment="Right"
VerticalAlignment="Center" />
</Grid>
<!-- 底部信息栏 -->
<Grid Grid.Row="1" ColumnDefinitions="*,Auto">
<TextBlock x:Name="LocationTextBlock"
Text="Weather"
FontSize="13"
FontWeight="Medium"
TextTrimming="CharacterEllipsis"
VerticalAlignment="Bottom" />
<TextBlock x:Name="RangeTextBlock"
Grid.Column="1"
Text="-- / --"
FontSize="13"
FontWeight="Medium"
HorizontalAlignment="Right"
VerticalAlignment="Bottom" />
</Grid>
</Grid>
</Grid>
</Border>
```
**CS 改动:**
- 调整响应式布局的字体缩放比例
- 更新颜色绑定使用新的调色板字段
---
### Task 3: 重构 ExtendedWeatherWidget扩展天气组件
**文件:**
- `LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml`
- `LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs`
**设计目标:**
- 顶部区域:位置+温度+图标横向排列
- 指标区域:使用 Material 3 风格的标签卡片
- 逐小时预报:水平滚动卡片,时间+图标+温度
- 逐日预报:列表式布局,日期+图标+高低温
**XAML 改动:**
```xml
<Border x:Name="RootBorder"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
ClipToBounds="True">
<Grid>
<components:MaterialWeatherSceneControl x:Name="Scene" />
<Border x:Name="OverlayBorder" />
<Grid x:Name="ContentGrid"
RowDefinitions="Auto,Auto,Auto,Auto"
Margin="20,16,20,14"
RowSpacing="12">
<!-- 顶部:位置 + 图标 + 温度 -->
<Grid ColumnDefinitions="*,Auto,Auto" VerticalAlignment="Center">
<StackPanel VerticalAlignment="Center">
<TextBlock x:Name="LocationTextBlock"
Text="Weather"
FontSize="13"
FontWeight="Medium"
TextTrimming="CharacterEllipsis"
Opacity="0.72" />
<TextBlock x:Name="ConditionTextBlock"
Text="Loading"
FontSize="16"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis" />
</StackPanel>
<components:WeatherIconView x:Name="MainIcon"
Grid.Column="1"
Width="56"
Height="56"
Margin="0,0,10,0" />
<TextBlock x:Name="TemperatureTextBlock"
Grid.Column="2"
Text="--°"
FontSize="56"
FontWeight="Bold"
VerticalAlignment="Center" />
</Grid>
<!-- 指标区域 -->
<UniformGrid x:Name="MetricGrid" Grid.Row="1" Rows="1" Columns="3" />
<!-- 逐小时预报 -->
<Border Grid.Row="2"
Background="{DynamicResource SurfaceColor}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
Padding="10,8">
<UniformGrid x:Name="HourlyGrid" Rows="1" Columns="6" />
</Border>
<!-- 逐日预报 -->
<Border Grid.Row="3"
Background="{DynamicResource SurfaceColor}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
Padding="10,8">
<UniformGrid x:Name="DailyGrid" Rows="1" Columns="5" />
</Border>
</Grid>
</Grid>
</Border>
```
**CS 改动:**
- `CreateMetric` 方法使用圆角卡片Material 3 风格标签
- `BuildHourlyItems` 方法:改进卡片样式,统一圆角
- `BuildDailyItems` 方法:改进卡片样式,统一圆角
---
### Task 4: 重构 HourlyWeatherWidget逐小时天气组件
**文件:**
- `LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml`
- `LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs`
**设计目标:**
- 顶部简洁信息栏
- 逐小时预报使用 Material 卡片风格
- 时间、图标、温度垂直排列
**XAML 改动:**
```xml
<Border x:Name="RootBorder"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
ClipToBounds="True">
<Grid>
<components:MaterialWeatherSceneControl x:Name="Scene" />
<Border x:Name="OverlayBorder" />
<Grid x:Name="ContentGrid"
RowDefinitions="Auto,*"
Margin="18,14"
RowSpacing="12">
<!-- 顶部信息栏 -->
<Grid ColumnDefinitions="Auto,*,Auto,Auto" VerticalAlignment="Center">
<TextBlock x:Name="TemperatureTextBlock"
Text="--°"
FontSize="42"
FontWeight="Bold"
VerticalAlignment="Center" />
<StackPanel Grid.Column="1" Margin="12,0,0,0" VerticalAlignment="Center">
<TextBlock x:Name="ConditionTextBlock"
Text="Loading"
FontSize="15"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis" />
<TextBlock x:Name="LocationTextBlock"
Text="Weather"
FontSize="12"
FontWeight="Medium"
Opacity="0.72"
TextTrimming="CharacterEllipsis" />
</StackPanel>
<TextBlock x:Name="RangeTextBlock"
Grid.Column="2"
Text="-- / --"
FontSize="12"
FontWeight="Medium"
VerticalAlignment="Center"
Opacity="0.72"
Margin="0,0,10,0" />
<components:WeatherIconView x:Name="MainIcon"
Grid.Column="3"
Width="48"
Height="48" />
</Grid>
<!-- 逐小时预报卡片容器 -->
<Border Grid.Row="1"
Background="{DynamicResource SurfaceColor}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
Padding="8,6">
<UniformGrid x:Name="HourlyGrid" Rows="1" Columns="6" />
</Border>
</Grid>
</Grid>
</Border>
```
---
### Task 5: 重构 MultiDayWeatherWidget多日天气组件
**文件:**
- `LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml`
- `LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs`
**设计目标:**
- 左侧:当前天气信息(图标+温度+状况+位置)
- 右侧:多日预报列表,使用行式布局
**XAML 改动:**
```xml
<Border x:Name="RootBorder"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
ClipToBounds="True">
<Grid>
<components:MaterialWeatherSceneControl x:Name="Scene" />
<Border x:Name="OverlayBorder" />
<Grid x:Name="ContentGrid"
ColumnDefinitions="1.2*,1.6*"
Margin="18,14"
ColumnSpacing="14">
<!-- 左侧当前天气 -->
<StackPanel VerticalAlignment="Center" Spacing="6">
<components:WeatherIconView x:Name="MainIcon"
Width="64"
Height="64"
HorizontalAlignment="Left" />
<TextBlock x:Name="TemperatureTextBlock"
Text="--°"
FontSize="42"
FontWeight="Bold" />
<TextBlock x:Name="ConditionTextBlock"
Text="Loading"
FontSize="15"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis" />
<TextBlock x:Name="LocationTextBlock"
Text="Weather"
FontSize="12"
FontWeight="Medium"
Opacity="0.72"
TextTrimming="CharacterEllipsis" />
</StackPanel>
<!-- 右侧多日预报 -->
<Border Grid.Column="1"
Background="{DynamicResource SurfaceColor}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
Padding="10,8">
<ItemsControl x:Name="DailyItemsControl" />
</Border>
</Grid>
</Grid>
</Border>
```
---
### Task 6: 重构 WeatherClockWidget天气时钟组件
**文件:**
- `LanMountainDesktop/Views/Components/WeatherClockWidget.axaml`
- `LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs`
**设计目标:**
- 左侧:大字体时间+日期
- 右侧:天气图标+温度+状况
- 信息层级清晰
**XAML 改动:**
```xml
<Border x:Name="RootBorder"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
ClipToBounds="True">
<Grid>
<components:MaterialWeatherSceneControl x:Name="Scene" />
<Border x:Name="OverlayBorder" />
<Grid x:Name="ContentGrid"
ColumnDefinitions="*,Auto"
Margin="18,12"
ColumnSpacing="12">
<!-- 左侧时间 -->
<StackPanel VerticalAlignment="Center" Spacing="2">
<TextBlock x:Name="TimeTextBlock"
Text="--:--"
FontSize="38"
FontWeight="Bold"
LineHeight="38" />
<TextBlock x:Name="DateTextBlock"
Text="Weather"
FontSize="12"
FontWeight="Medium"
Opacity="0.72"
TextTrimming="CharacterEllipsis" />
</StackPanel>
<!-- 右侧天气 -->
<StackPanel Grid.Column="1"
VerticalAlignment="Center"
HorizontalAlignment="Right"
Spacing="1">
<components:WeatherIconView x:Name="MainIcon"
Width="44"
Height="44"
HorizontalAlignment="Right" />
<TextBlock x:Name="TemperatureTextBlock"
Text="--°"
FontSize="20"
FontWeight="SemiBold"
HorizontalAlignment="Right" />
<TextBlock x:Name="ConditionTextBlock"
Text="Loading"
FontSize="11"
FontWeight="Medium"
HorizontalAlignment="Right"
TextTrimming="CharacterEllipsis"
MaxWidth="100"
Opacity="0.82" />
</StackPanel>
</Grid>
</Grid>
</Border>
```
---
### Task 7: 更新 ExtendedWeatherWidget 的代码后置文件
**文件:** `LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs`
**改动:**
- `CreateMetric` 方法改进:
- 使用 `DesignCornerRadiusSm` 圆角
- 使用新的 `SurfaceColor` 作为卡片背景
- 优化字体大小和间距
- `BuildHourlyItems` 方法改进:
- 使用 `DesignCornerRadiusSm` 圆角
- 使用 `SurfaceColor` 作为卡片背景
- 时间、图标、温度垂直排列,居中对齐
- `BuildDailyItems` 方法改进:
- 使用 `DesignCornerRadiusSm` 圆角
- 使用 `SurfaceColor` 作为卡片背景
- 日期、图标、高低温垂直排列
---
### Task 8: 更新 HourlyWeatherWidget 的代码后置文件
**文件:** `LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs`
**改动:**
- `CreateChip` 方法改进:
- 使用 `DesignCornerRadiusSm` 圆角
- 使用 `SurfaceColor` 作为卡片背景
- 优化垂直排列的间距
---
### Task 9: 更新 MultiDayWeatherWidget 的代码后置文件
**文件:** `LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs`
**改动:**
- `CreateRow` 方法改进:
- 添加底部分割线(除最后一行)
- 优化列间距和对齐
- 高低温使用不同透明度区分
---
### Task 10: 更新 MaterialWeatherVisualTheme 调色板
**文件:** `LanMountainDesktop/Views/Components/MaterialWeatherVisualTheme.cs`
**改动:**
-`MaterialWeatherPalette` 添加新字段:
- `SurfaceColor` - 用于卡片表面
- `SurfaceVariantColor` - 用于变体表面
- `OutlineColor` - 用于分割线
- 更新所有调色板生成方法:
- `ResolveGooglePalette`
- `ResolveGeometricPalette`
- `ResolveBreezyPalette`
- `ResolveLemonPalette`
- 每个调色板需要为白天/夜晚模式提供合适的 SurfaceColor
- 白天:低透明度白色(如 `#14FFFFFF`
- 夜晚:低透明度黑色(如 `#1A000000`
---
### Task 11: 构建和测试
**命令:**
```bash
dotnet build LanMountainDesktop.slnx -c Debug
dotnet test LanMountainDesktop.slnx -c Debug
```
**验证清单:**
- [ ] 所有天气组件正常编译
- [ ] 运行时无异常
- [ ] 4套视觉风格正常切换
- [ ] 响应式布局正常工作
- [ ] 圆角资源正确应用
---
## 文件改动汇总
| 文件 | 改动类型 | 说明 |
|------|---------|------|
| `MaterialWeatherVisualTheme.cs` | 修改 | 添加 SurfaceColor 等字段,更新所有调色板 |
| `WeatherWidget.axaml` | 修改 | 重构布局,优化排版 |
| `WeatherWidget.axaml.cs` | 修改 | 调整响应式布局和颜色绑定 |
| `ExtendedWeatherWidget.axaml` | 修改 | 重构布局,添加卡片容器 |
| `ExtendedWeatherWidget.axaml.cs` | 修改 | 改进卡片创建方法 |
| `HourlyWeatherWidget.axaml` | 修改 | 重构布局,添加卡片容器 |
| `HourlyWeatherWidget.axaml.cs` | 修改 | 改进卡片创建方法 |
| `MultiDayWeatherWidget.axaml` | 修改 | 重构布局,添加卡片容器 |
| `MultiDayWeatherWidget.axaml.cs` | 修改 | 改进行创建方法 |
| `WeatherClockWidget.axaml` | 修改 | 重构布局,优化排版 |
| `WeatherClockWidget.axaml.cs` | 修改 | 调整响应式布局 |
---
## 设计规范检查清单
- [ ] 所有组件根容器使用 `DesignCornerRadiusComponent`
- [ ] 内部卡片使用 `DesignCornerRadiusMd``DesignCornerRadiusSm`
- [ ] 不使用硬编码圆角值
- [ ] 文字对比度符合 VISUAL_SPEC 要求
- [ ] 间距使用一致的倍数4px 基线)
- [ ] 字体层级:温度(64-72px) > 状况(16-18px) > 位置/范围(12-13px)

View File

@@ -0,0 +1,342 @@
# 天气组件视觉重构 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:** 彻底重构阑山桌面天气系列组件的背景视觉和文字排版为每种图标风格Google Weather / Geometric / Breezy / Lemon提供独立的背景配色和视觉质感参考各天气 App 的 Material Design 风格,实现几何质感+柔和渐变+层次分明的排版。
**Architecture:** 保留现有数据层WeatherWidgetBase、WeatherSnapshot、WeatherIconAssetResolver和组件注册机制不变。核心改动1) 将 `MaterialWeatherVisualTheme.ResolvePalette()` 扩展为按 styleId 分派不同配色方案2) 重构 `MaterialWeatherSceneControl` 为按 styleId 渲染不同背景风格3) 改进各天气 Widget 的文字排版层次。先创建 HTML Mock 验证视觉效果。
**Tech Stack:** Avalonia UI (XAML + C# code-behind)、HTML/CSS (Mock 预览)
---
## 当前状态分析
### 现有天气组件体系
5 个天气组件,全部继承自 `WeatherWidgetBase`
| 组件 | 文件 | 功能 |
|------|------|------|
| WeatherWidget | `WeatherWidget.axaml(.cs)` | 基础天气:温度+状况+图标+位置 |
| WeatherClockWidget | `WeatherClockWidget.axaml(.cs)` | 天气+时钟 |
| ExtendedWeatherWidget | `ExtendedWeatherWidget.axaml(.cs)` | 扩展天气:含指标/小时/多日预报 |
| HourlyWeatherWidget | `HourlyWeatherWidget.axaml(.cs)` | 逐小时天气 |
| MultiDayWeatherWidget | `MultiDayWeatherWidget.axaml(.cs)` | 多日天气 |
### 核心问题
1. **背景与图标风格脱钩**: `MaterialWeatherVisualTheme.ResolvePalette()` 只返回一套配色,与 `WeatherVisualStyleId`GoogleWeatherV4/Geometric/Breezy/LemonFlutter完全无关。切换图标风格时背景不变。
2. **背景视觉单调**: `MaterialWeatherSceneControl` 只有一种手绘几何风格(椭圆+云+雨滴),质感差,缺乏各 App 的特色。
3. **文字排版粗糙**: 温度数字不够大,信息层次不分明,指标用纯文字堆叠,预报区域无卡片样式。
4. **半透明遮罩硬编码**: 所有组件都覆盖 `<Border Background="#30FFFFFF" />` 等硬编码遮罩,不随风格变化。
### 各天气 App 风格特征
**Google Weather (v4)**:
- 背景:大面积柔和蓝白渐变,晴天偏暖黄蓝,雨天偏深蓝灰
- 装饰:极简,几乎无几何装饰,纯靠渐变色彩表现天气氛围
- 排版温度超大72px+),天气状况中等,位置小字
**Geometric Weather (几何天气)**:
- 背景:深色系渐变(深蓝/深紫/深灰),搭配半透明几何圆形装饰
- 装饰:大面积半透明圆形叠加,营造深度感
- 排版:紧凑信息密度,指标用小标签
**Breezy Weather (微风天气)**:
- 背景:清新渐变(浅蓝/浅绿/浅紫),比 Geometric 更明亮
- 装饰:柔和波浪线条 + 少量几何装饰Material Design 风格
- 排版:卡片式预报,圆角芯片
**Lemon Weather (柠檬天气)**:
- 背景:暖色系渐变(橙黄/粉紫/暖蓝柠檬2偏扁平柠檬3偏Material
- 装饰:天气场景装饰(太阳光芒/云朵轮廓/雨丝),更有场景感
- 排版:温度超大,天气图标突出
---
## 设计方案
### 视觉论文 (Visual Thesis)
每种图标风格拥有独特的背景渐变配色和几何装饰语言——Google 纯净渐变、Geometric 深色几何、Breezy 清新波浪、Lemon 暖色场景——配合超大温度数字和层次分明的排版,在桌面小组件空间内实现 Material Design 的几何质感。
### 配色方案设计
每种风格 × 每种天气条件 × 昼夜 = 独立配色。以下为关键配色定义:
#### Google Weather 风格
| 天气 | 白天 Top→Bottom | 夜晚 Top→Bottom |
|------|----------------|----------------|
| Clear | #4FC3F7#B3E5FC | #0D47A1#1A237E |
| PartlyCloudy | #81D4FA#E1F5FE | #1565C0#283593 |
| Cloudy | #90A4AE#CFD8DC | #37474F#455A64 |
| Rain | #78909C#B0BEC5 | #263238#37474F |
| Storm | #546E7A#78909C | #1A1A2E#263238 |
| Snow | #E1F5FE#FFFFFF | #1A237E#283593 |
| Fog/Haze | #B0BEC5#ECEFF1 | #455A64#546E7A |
#### Geometric 风格
| 天气 | 白天 Top→Bottom | 夜晚 Top→Bottom |
|------|----------------|----------------|
| Clear | #1A237E#3949AB | #0A0E27#1A1A3E |
| PartlyCloudy | #283593#5C6BC0 | #0D1033#1E1E4A |
| Cloudy | #37474F#607D8B | #1A1A2E#2D2D44 |
| Rain | #1A237E#3F51B5 | #0A0E27#1A1A3E |
| Storm | #1A1A2E#3F51B5 | #050510#1A1A2E |
| Snow | #E8EAF6#C5CAE9 | #1A237E#283593 |
| Fog/Haze | #455A64#78909C | #1A1A2E#37474F |
#### Breezy 风格
| 天气 | 白天 Top→Bottom | 夜晚 Top→Bottom |
|------|----------------|----------------|
| Clear | #4DD0E1#80DEEA | #006064#00838F |
| PartlyCloudy | #4FC3F7#B2EBF2 | #00695C#00897B |
| Cloudy | #80CBC4#B2DFDB | #37474F#546E7A |
| Rain | #4DB6AC#80CBC4 | #004D40#00695C |
| Storm | #26A69A#4DB6AC | #1A1A2E#004D40 |
| Snow | #E0F7FA#FFFFFF | #006064#00838F |
| Fog/Haze | #80CBC4#E0F7FA | #37474F#546E7A |
#### Lemon 风格
| 天气 | 白天 Top→Bottom | 夜晚 Top→Bottom |
|------|----------------|----------------|
| Clear | #FFB74D#FFF176 | #1A237E#311B92 |
| PartlyCloudy | #FF8A65#FFCC80 | #283593#4A148C |
| Cloudy | #BCAAA4#D7CCC8 | #37474F#4E342E |
| Rain | #90A4AE#B0BEC5 | #1A1A2E#311B92 |
| Storm | #78909C#90A4AE | #0D0D1A#1A1A2E |
| Snow | #FFF9C4#FFFFFF | #1A237E#311B92 |
| Fog/Haze | #D7CCC8#EFEBE9 | #4E342E#5D4037 |
### 排版改进方案
1. **温度超大化**: 温度字号从 56-58px 提升到 64-72px基础组件形成视觉锚点
2. **层次分明**: 温度 → 天气状况 → 位置/指标,字号递减,透明度递减
3. **指标标签化**: 湿度/风速/AQI 用半透明圆角标签展示,而非纯文字
4. **预报芯片化**: 小时/每日预报用圆角半透明芯片卡片
5. **图标间距**: 天气图标与文字之间增加 8-12px 间距
---
## 文件变更清单
| 文件 | 操作 | 说明 |
|------|------|------|
| `Views/Components/MaterialWeatherVisualTheme.cs` | 修改 | 扩展 ResolvePalette 支持 styleId 分派新增4套风格配色 |
| `Views/Components/MaterialWeatherSceneControl.cs` | 修改 | 按 styleId 渲染不同背景风格(纯渐变/深色几何/清新波浪/暖色场景) |
| `Views/Components/WeatherWidgetBase.cs` | 修改 | 传递 styleId 到 SceneControl.Apply(),移除硬编码遮罩 |
| `Views/Components/WeatherWidget.axaml` | 修改 | 改进排版层次,移除硬编码遮罩 |
| `Views/Components/WeatherWidget.axaml.cs` | 修改 | 适配新排版 |
| `Views/Components/WeatherClockWidget.axaml` | 修改 | 改进排版,移除硬编码遮罩 |
| `Views/Components/WeatherClockWidget.axaml.cs` | 修改 | 适配新排版 |
| `Views/Components/ExtendedWeatherWidget.axaml` | 修改 | 改进排版,指标标签化,预报芯片化 |
| `Views/Components/ExtendedWeatherWidget.axaml.cs` | 修改 | 适配新排版+标签+芯片 |
| `Views/Components/HourlyWeatherWidget.axaml` | 修改 | 改进排版,预报芯片化 |
| `Views/Components/HourlyWeatherWidget.axaml.cs` | 修改 | 适配新排版+芯片 |
| `Views/Components/MultiDayWeatherWidget.axaml` | 修改 | 改进排版 |
| `Views/Components/MultiDayWeatherWidget.axaml.cs` | 修改 | 适配新排版 |
| `mocks/weather-widget-mock.html` | 新建 | HTML Mock 预览4种风格×2种天气×2种主题 |
---
## Task 分解
### Task 1: 创建 HTML Mock 预览
**Files:**
- Create: `mocks/weather-widget-mock.html`
- [ ] **Step 1: 创建 HTML Mock 文件**
创建完整的 HTML Mock包含
- 4 种风格Google / Geometric / Breezy / Lemon× 2 种天气(晴/雨)× 2 种主题(亮/暗)
- 每种风格展示基础天气组件(温度+状况+图标+位置)
- 改进后的排版:超大温度、层次分明、指标标签化
- 亮色/暗色主题切换按钮
- [ ] **Step 2: 在浏览器中打开 Mock 验证效果**
Run: `start mocks/weather-widget-mock.html`
---
### Task 2: 扩展 MaterialWeatherVisualTheme 支持多风格配色
**Files:**
- Modify: `LanMountainDesktop/Views/Components/MaterialWeatherVisualTheme.cs`
- [ ] **Step 1: 修改 ResolvePalette 方法签名**
`ResolvePalette(MaterialWeatherCondition condition, bool isNight)` 改为 `ResolvePalette(string? styleId, MaterialWeatherCondition condition, bool isNight)`,内部按 styleId 分派到不同配色方案。
- [ ] **Step 2: 新增 Google Weather 配色表**
为 GoogleWeatherV4 风格定义所有天气条件×昼夜的配色(参考上面配色方案设计章节)。
- [ ] **Step 3: 新增 Geometric 配色表**
为 Geometric 风格定义深色系配色。
- [ ] **Step 4: 新增 Breezy 配色表**
为 Breezy 风格定义清新渐变配色。
- [ ] **Step 5: 新增 Lemon 配色表**
为 LemonFlutter 风格定义暖色系配色。
- [ ] **Step 6: 更新所有调用点**
将所有 `ResolvePalette(condition, isNight)` 调用改为 `ResolvePalette(styleId, condition, isNight)`
---
### Task 3: 重构 MaterialWeatherSceneControl 支持多风格背景
**Files:**
- Modify: `LanMountainDesktop/Views/Components/MaterialWeatherSceneControl.cs`
- [ ] **Step 1: 扩展 Apply 方法签名**
`Apply(MaterialWeatherCondition condition, MaterialWeatherPalette palette, bool isLive)` 改为 `Apply(string? styleId, MaterialWeatherCondition condition, MaterialWeatherPalette palette, bool isLive)`,存储 styleId。
- [ ] **Step 2: 实现 Google Weather 风格渲染**
纯渐变背景,无几何装饰。背景使用 palette 的 BackgroundTop→BackgroundBottom 渐变。仅保留天气特效(雨滴/雪花/雾线)。
- [ ] **Step 3: 实现 Geometric 风格渲染**
深色渐变 + 大面积半透明几何圆形叠加。在基础渐变上叠加 2-3 个大椭圆(使用 palette 的 PrimaryShape/SecondaryShape/AccentShape营造深度感。保留天气特效。
- [ ] **Step 4: 实现 Breezy 风格渲染**
清新渐变 + 柔和波浪线条。在基础渐变上绘制 2-3 条正弦波浪线(使用 palette 的 SurfaceTint营造微风感。保留天气特效。
- [ ] **Step 5: 实现 Lemon 风格渲染**
暖色渐变 + 天气场景装饰。晴天绘制太阳光芒(放射线),多云绘制云朵轮廓,雨天绘制雨丝。保留天气特效。
- [ ] **Step 6: 更新所有调用点**
将所有 `SceneControl.Apply(condition, palette, isLive)` 改为 `SceneControl.Apply(styleId, condition, palette, isLive)`
---
### Task 4: 更新 WeatherWidgetBase 传递 styleId
**Files:**
- Modify: `LanMountainDesktop/Views/Components/WeatherWidgetBase.cs`
- [ ] **Step 1: 修改 ApplyCurrentScene 方法**
`ApplyCurrentScene()` 中将 `CurrentVisualStyleId` 传递给 `SceneControl.Apply()`
- [ ] **Step 2: 修改 ApplySnapshot 中的 ResolvePalette 调用**
`MaterialWeatherVisualTheme.ResolvePalette(CurrentCondition, isNight)` 改为 `MaterialWeatherVisualTheme.ResolvePalette(CurrentVisualStyleId, CurrentCondition, isNight)`
---
### Task 5: 改进各天气 Widget 的 XAML 排版
**Files:**
- Modify: `WeatherWidget.axaml` — 移除硬编码遮罩 `<Border Background="#30FFFFFF" />`,改用 palette 驱动的半透明遮罩
- Modify: `WeatherClockWidget.axaml` — 同上
- Modify: `ExtendedWeatherWidget.axaml` — 同上 + 指标区域改用标签样式
- Modify: `HourlyWeatherWidget.axaml` — 同上 + 预报区域改用芯片样式
- Modify: `MultiDayWeatherWidget.axaml` — 同上
- [ ] **Step 1: 移除所有硬编码遮罩**
`<Border Background="#30FFFFFF" />` / `#42FFFFFF` / `#34FFFFFF` / `#38FFFFFF` / `#3CFFFFFF` 替换为 `<Border x:Name="OverlayBorder" />`,在 code-behind 中根据 palette 设置遮罩颜色。
- [ ] **Step 2: 改进 WeatherWidget 排版**
增大温度字号58→64增加图标与文字间距调整位置文字透明度。
- [ ] **Step 3: 改进 WeatherClockWidget 排版**
增大时钟字号,增加天气信息与时间间距。
- [ ] **Step 4: 改进 ExtendedWeatherWidget 排版**
指标用半透明圆角标签,小时/每日预报用圆角芯片卡片。
- [ ] **Step 5: 改进 HourlyWeatherWidget 排版**
预报区域用圆角芯片卡片样式。
- [ ] **Step 6: 改进 MultiDayWeatherWidget 排版**
每日预报行增加分隔线和更好的间距。
---
### Task 6: 更新各天气 Widget 的 code-behind
**Files:**
- Modify: 所有天气 Widget 的 `.axaml.cs` 文件
- [ ] **Step 1: 更新 WeatherWidget.axaml.cs**
- 设置 OverlayBorder 背景
- 增大温度字号
- 适配新排版参数
- [ ] **Step 2: 更新 WeatherClockWidget.axaml.cs**
- 设置 OverlayBorder 背景
- 适配新排版
- [ ] **Step 3: 更新 ExtendedWeatherWidget.axaml.cs**
- 设置 OverlayBorder 背景
- 指标标签化CreateMetric 改为带圆角背景的标签)
- 预报芯片化
- [ ] **Step 4: 更新 HourlyWeatherWidget.axaml.cs**
- 设置 OverlayBorder 背景
- 预报芯片化CreateChip 改为带圆角背景的芯片)
- [ ] **Step 5: 更新 MultiDayWeatherWidget.axaml.cs**
- 设置 OverlayBorder 背景
- 适配新排版
---
### Task 7: 验证与测试
- [ ] **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. **4 套独立风格**: 每种图标风格对应独立的背景配色和装饰风格,切换图标风格时背景也跟着变
2. **配色表驱动**: 所有颜色定义在 `MaterialWeatherVisualTheme` 中,不硬编码到 SceneControl
3. **保留天气特效**: 雨滴/雪花/雾线/闪电等天气特效在所有风格中保留,但颜色跟随 palette
4. **遮罩动态化**: 半透明遮罩颜色从 palette 中派生,而非硬编码 `#30FFFFFF`
5. **排版渐进改进**: 不做大规模 XAML 重构,而是在现有结构上优化字号/间距/透明度
6. **数据层不变**: WeatherSnapshot、WeatherIconAssetResolver、WeatherWidgetBase 的数据逻辑不变
7. **接口兼容**: IDesktopComponentWidget 等接口实现不变
## 验证步骤
1. HTML Mock 在浏览器中展示 4 种风格效果满意
2. Avalonia 项目编译通过
3. 运行项目,切换图标风格时背景配色和装饰风格跟着变化
4. 亮色/暗色主题切换正常
5. 5 个天气组件排版层次分明
6. 指标标签化和预报芯片化正常显示
7. 测试通过