Compare commits

...

4 Commits

13 changed files with 1226 additions and 372 deletions

View File

@@ -1,100 +1,166 @@
# 融合桌面组件库窗口重设计规格
## Why
当前融合桌面组件库窗口FusedDesktopComponentLibraryWindow的UI设计较为基础与Windows 11小组件编辑面板相比缺乏现代化的交互体验和视觉层次。用户需要一个更直观、更美观的界面来浏览和添加组件到系统桌面负一屏
参考Windows 11小组件编辑面板的设计特点
- 左侧分类列表,右侧选中组件的详细预览
- 大型组件预览区域,让用户清楚看到组件效果
- 底部明显的"添加"操作按钮
- 简洁的关闭按钮X在右上角
- 深色主题配合毛玻璃效果
* 左侧分类列表,右侧选中组件的详细预览
* 大型组件预览区域,让用户清楚看到组件效果
* 底部明显的"添加"操作按钮
* 简洁的关闭按钮X在右上角
* 深色主题配合毛玻璃效果
## What Changes
- **重新设计窗口布局**:从左右分栏(分类列表+组件网格)改为左侧面板+右侧预览区的布局
- **添加组件详情预览区**:选中组件后右侧显示大尺寸预览和组件信息
- **优化关闭按钮**使用标准的X图标按钮不使用圆形样式
- **添加底部操作栏**:包含"添加到桌面"主操作按钮和"查找更多组件"链接
- **复用阑山桌面组件库分类**使用相同的分类ID、图标和本地化文本
- **移除搜索功能**参考Windows 11设计暂不提供搜索
* **重新设计窗口布局**:从左右分栏(分类列表+组件网格)改为左侧面板+右侧预览区的布局
* **添加组件详情预览区**:选中组件后右侧显示大尺寸预览和组件信息
* **优化关闭按钮**使用标准的X图标按钮不使用圆形样式
* **添加底部操作栏**:包含"添加到桌面"主操作按钮和"查找更多组件"链接
* **复用阑山桌面组件库分类**使用相同的分类ID、图标和本地化文本
* **移除搜索功能**参考Windows 11设计暂不提供搜索
## Impact
- 受影响文件:
- `LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml`
- `LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml.cs`
- `LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml`
- `LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml.cs`
- `LanMountainDesktop/ViewModels/ComponentLibraryWindowViewModel.cs`(可能需要添加新属性)
* 受影响文件:
* `LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml`
* `LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml.cs`
* `LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml`
* `LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml.cs`
* `LanMountainDesktop/ViewModels/ComponentLibraryWindowViewModel.cs`(可能需要添加新属性)
## ADDED Requirements
### Requirement: 窗口布局重设计
系统应提供一个类似于Windows 11小组件编辑面板的组件库窗口。
#### Scenario: 窗口整体结构
- **GIVEN** 用户从托盘菜单打开融合桌面组件库
- **WHEN** 窗口显示时
- **THEN** 窗口应呈现:
- 顶部标题栏:左侧显示"添加小组件"标题右侧有关闭按钮X
- 左侧面板:分类列表(复用阑山桌面组件库的分类和图标)
- 右侧主区域:选中组件的大尺寸预览 + 组件信息 + 添加按钮
- 底部:"查找更多组件"链接
* **GIVEN** 用户从托盘菜单打开融合桌面组件库
* **WHEN** 窗口显示时
* **THEN** 窗口应呈现:
* 顶部标题栏:左侧显示"添加小组件"标题右侧有关闭按钮X
* 左侧面板:分类列表(复用阑山桌面组件库的分类和图标)
* 右侧主区域:选中组件的大尺寸预览 + 组件信息 + 添加按钮
* 底部:"查找更多组件"链接
#### Scenario: 分类列表交互
- **GIVEN** 左侧显示组件分类列表
- **WHEN** 用户点击某个分类
- **THEN** 右侧应显示该分类下的第一个组件的预览
- **AND** 分类项应有选中状态视觉反馈
- **AND** 分类图标和名称应与阑山桌面组件库保持一致
* **GIVEN** 左侧显示组件分类列表
* **WHEN** 用户点击某个分类
* **THEN** 右侧应显示该分类下的第一个组件的预览
* **AND** 分类项应有选中状态视觉反馈
* **AND** 分类图标和名称应与阑山桌面组件库保持一致
#### Scenario: 组件预览区
- **GIVEN** 用户选中一个组件
- **WHEN** 预览区显示时
- **THEN** 应显示:
- 组件标题(大字号)
- 大尺寸组件预览图(接近实际尺寸)
- 组件描述/功能说明
- 底部"添加到桌面"按钮
* **GIVEN** 用户选中一个组件
* **WHEN** 预览区显示时
* **THEN** 应显示:
* 组件标题(大字号)
* 大尺寸组件预览图(接近实际尺寸)
* 组件描述/功能说明
* 底部"添加到桌面"按钮
#### Scenario: 添加组件操作
- **GIVEN** 用户查看组件预览
- **WHEN** 用户点击"添加到桌面"按钮
- **THEN** 组件应被添加到系统桌面(负一屏)中央
- **AND** 窗口应关闭
* **GIVEN** 用户查看组件预览
* **WHEN** 用户点击"添加到桌面"按钮
* **THEN** 组件应被添加到系统桌面(负一屏)中央
* **AND** 窗口应关闭
#### Scenario: 关闭按钮样式
- **GIVEN** 窗口标题栏有关闭按钮
- **THEN** 关闭按钮应使用标准的X图标
- **AND** 不使用圆形背景或特殊样式
- **AND** 使用 `DesignCornerRadiusSm` 动态资源
* **GIVEN** 窗口标题栏有关闭按钮
* **THEN** 关闭按钮应使用标准的X图标
* **AND** 不使用圆形背景或特殊样式
* **AND** 使用 `DesignCornerRadiusSm` 动态资源
#### Scenario: 查找更多组件链接
- **GIVEN** 窗口底部显示"查找更多组件"链接
- **WHEN** 用户点击该链接
- **THEN** 应打开设置窗口的插件目录页面(后续将改为插件市场)
* **GIVEN** 窗口底部显示"查找更多组件"链接
* **WHEN** 用户点击该链接
* **THEN** 应打开设置窗口的插件目录页面(后续将改为插件市场)
## MODIFIED Requirements
### Requirement: 组件列表展示
原实现使用网格展示所有组件,新实现改为:
- 左侧列表仅显示分类复用阑山桌面组件库的分类ID和图标映射
- 右侧预览区一次只显示一个组件的详细信息
- ~~移除搜索功能~~根据Windows 11设计暂不提供搜索
* 左侧列表仅显示分类复用阑山桌面组件库的分类ID和图标映射
* 右侧预览区一次只显示一个组件的详细信息
* ~~移除搜索功能~~根据Windows 11设计暂不提供搜索
### Requirement: 关闭按钮圆角规范
原实现关闭按钮使用硬编码 `CornerRadius="18"`,应改为使用动态资源 `DesignCornerRadiusSm`
### Requirement: 分类图标复用
分类图标映射应与阑山桌面组件库保持一致:
- Clock -> Symbol.Clock
- Date -> Symbol.CalendarDate
- Weather -> Symbol.WeatherSunny
- Board -> Symbol.Edit
- Media -> Symbol.Play
- Info -> Symbol.Info
- Calculator -> Symbol.Calculator
- Study -> Symbol.Hourglass
- 其他 -> Symbol.Apps
* Clock -> Symbol.Clock
* Date -> Symbol.CalendarDate
* Weather -> Symbol.WeatherSunny
* Board -> Symbol.Edit
* Media -> Symbol.Play
* Info -> Symbol.Info
* Calculator -> Symbol.Calculator
* Study -> Symbol.Hourglass
* 其他 -> Symbol.Apps
## REMOVED Requirements
- ~~搜索功能~~根据Windows 11小组件面板设计暂不提供搜索功能
* ~~搜索功能~~根据Windows 11小组件面板设计暂不提供搜索功能

View File

@@ -1,27 +1,5 @@
# 更新日志 / Changelog
所有重要的更改都将记录在此文件中。
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
并且本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
---
## [Unreleased]
### 新增 (Added)
- 待发布的新功能
### 变更 (Changed)
- 待发布的变更
### 修复 (Fixed)
- 待发布的修复
### 移除 (Removed)
- 待发布的移除项
---
## [0.8.3.2] - 2026-04-09
@@ -70,6 +48,28 @@
---
所有重要的更改都将记录在此文件中。
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
并且本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
---
## [格式示例]
### 新增 (Added)
- 待发布的新功能
### 变更 (Changed)
- 待发布的变更
### 修复 (Fixed)
- 待发布的修复
### 移除 (Removed)
- 待发布的移除项
---
## 版本说明
### 版本号规则

View File

@@ -698,6 +698,7 @@
"component.editor.placement_label": "Placement ID",
"component.editor.scope_label": "Scope",
"component.editor.scope_instance": "Instance-scoped editor",
"component_category.all": "All",
"component_category.clock": "Clock",
"component_category.date": "Calendar",
"component_category.weather": "Weather",

View File

@@ -692,6 +692,7 @@
"component.editor.placement_label": "实例 ID",
"component.editor.scope_label": "作用域",
"component.editor.scope_instance": "实例级编辑器",
"component_category.all": "全部",
"component_category.clock": "时钟",
"component_category.date": "日历",
"component_category.weather": "天气",

View File

@@ -34,11 +34,13 @@
<TextBlock x:Name="WeekdayTextBlock"
Text="周一"
TextAlignment="Right"
FontWeight="SemiBold" />
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis" />
<TextBlock x:Name="ClassCountTextBlock"
Text="0节课"
TextAlignment="Right"
FontWeight="SemiBold" />
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis" />
</StackPanel>
</Grid>

View File

@@ -928,7 +928,28 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
MetaStack.Spacing = Math.Clamp(6 * scale, 3, 10);
CourseListPanel.Spacing = Math.Clamp(6 * scale, 3, 10);
var dateFont = Math.Clamp(66 * scale, 26, 82);
var dateFontByScale = Math.Clamp(66 * scale, 26, 82);
var weekdayFontByScale = Math.Clamp(34 * scale, 13, 32);
var classCountFontByScale = Math.Clamp(40 * scale, 14, 36);
// 宽度感知:当头部内容总需求超过可用宽度时,按比例缩小日期字体
var availableWidth = Math.Max(1, Bounds.Width - rootPadding.Left - rootPadding.Right);
var dateGroupEstimatedWidth = dateFontByScale * 0.6 * 3 + DateGroup.Spacing * 2;
var metaStackEstimatedWidth = classCountFontByScale * 0.6 * 4 + MetaStack.Spacing;
var headerColumnSpacing = Math.Clamp(10 * scale, 4, 16);
var totalHeaderNeed = dateGroupEstimatedWidth + headerColumnSpacing + metaStackEstimatedWidth;
var dateFont = dateFontByScale;
if (totalHeaderNeed > availableWidth)
{
var shrinkRatio = availableWidth / totalHeaderNeed;
dateFont = Math.Max(20, dateFontByScale * shrinkRatio);
}
// 为 HeaderGrid 左列设置最小宽度,防止被压缩至零
var minDateColumnWidth = dateFont * 0.6 * 3 + DateGroup.Spacing * 2;
HeaderGrid.ColumnDefinitions[0].MinWidth = minDateColumnWidth;
MonthTextBlock.FontSize = dateFont;
DayTextBlock.FontSize = dateFont;
SlashTextBlock.FontSize = dateFont;
@@ -940,8 +961,8 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
ClassCountTextBlock.Foreground = CreateBrush(_isNightVisual ? "#8D95A4" : "#738095");
StatusTextBlock.Foreground = CreateBrush(_isNightVisual ? "#9AA2B1" : "#4B5565");
WeekdayTextBlock.FontSize = Math.Clamp(34 * scale, 13, 32);
ClassCountTextBlock.FontSize = Math.Clamp(40 * scale, 14, 36);
WeekdayTextBlock.FontSize = weekdayFontByScale;
ClassCountTextBlock.FontSize = classCountFontByScale;
StatusTextBlock.FontSize = Math.Clamp(30 * scale, 12, 30);
WeekdayTextBlock.FontWeight = ToVariableWeight(Lerp(560, 700, Math.Clamp((scale - 0.60) / 1.2, 0, 1)));

View File

@@ -704,6 +704,24 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
ExtraNewsItemsPanel.Spacing = Math.Clamp(6 * scale, 3, 10);
ApplyNightModeVisual();
var headerHeight = refreshHeight;
var newsItemHeight = Math.Max(imageHeight, mainNewsMinHeight);
var requiredHeight = verticalPadding * 2
+ headerHeight
+ rowSpacing
+ newsItemHeight
+ rowSpacing
+ newsItemHeight;
if (_extraNewsRows.Count > 0)
{
var extraSpacing = ExtraNewsItemsPanel.Spacing * (_extraNewsRows.Count - 1);
requiredHeight += rowSpacing + extraSpacing + _extraNewsRows.Count * newsItemHeight;
}
this.MinHeight = requiredHeight;
}
private void UpdateRefreshButtonState()
@@ -842,6 +860,11 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
oldBitmap?.Dispose();
_newsBitmaps[index] = bitmap;
imageControl.Source = bitmap;
if (bitmap != null)
{
InvalidateMeasure();
}
}
private void DisposeNewsBitmaps()

View File

@@ -40,13 +40,12 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
private WhiteboardToolMode _toolMode = WhiteboardToolMode.Pen;
private bool? _isNightModeApplied;
private SKColor _selectedInkColor = SKColors.Black;
private bool _isUserCustomColor;
private float _selectedInkThickness = 2.5f;
private string _componentId = BuiltInComponentIds.DesktopWhiteboard;
private string _placementId = string.Empty;
private int _noteRetentionDays = WhiteboardNoteRetentionPolicy.DefaultDays;
private bool _isApplyingPersistedSnapshot;
private bool? _lastBitmapCacheEnabled;
private int _lastBitmapCacheSize;
private bool _noteDirty;
private int _noteLoadRevision;
private bool _disposed;
@@ -121,10 +120,11 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
settings.IgnorePressure = true;
settings.InkThickness = _selectedInkThickness;
settings.EraserSize = new Size(20, 20);
settings.IsBitmapCacheEnabled = true;
settings.MaxBitmapCacheSize = 2048;
InkCanvas.StrokeCollected += OnInkCanvasStrokeCollected;
InkCanvas.PointerReleased += OnInkCanvasPointerReleased;
InkCanvas.PointerCaptureLost += OnInkCanvasPointerCaptureLost;
UpdateInkCanvasCacheSettings(forceRefresh: true);
}
public void ApplyCellSize(double cellSize)
@@ -158,7 +158,6 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
var settings = InkCanvas.AvaloniaSkiaInkCanvas.Settings;
var eraserSize = Math.Clamp(_currentCellSize * 0.42, 12, 44);
settings.EraserSize = new Size(eraserSize, eraserSize);
UpdateInkCanvasCacheSettings(forceRefresh: false);
}
private void ApplyThemeVisual(bool force)
@@ -169,6 +168,7 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
return;
}
var wasNightMode = _isNightModeApplied;
_isNightModeApplied = isNightMode;
RootBorder.Background = new SolidColorBrush(isNightMode ? Color.Parse("#FF181B22") : Color.Parse("#FFF1F4F9"));
@@ -177,9 +177,39 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
ToolbarBorder.Background = new SolidColorBrush(isNightMode ? Color.Parse("#1AFFFFFF") : Color.Parse("#E6FFFFFF"));
ToolbarBorder.BorderBrush = new SolidColorBrush(isNightMode ? Color.Parse("#26FFFFFF") : Color.Parse("#16000000"));
ApplyThemeDefaultInkColor(isNightMode, wasNightMode);
RefreshToolButtonVisuals();
}
private void ApplyThemeDefaultInkColor(bool isNightMode, bool? wasNightMode)
{
if (_isUserCustomColor || wasNightMode == isNightMode)
{
return;
}
var oldDefault = wasNightMode == true ? SKColors.White : SKColors.Black;
var newDefault = isNightMode ? SKColors.White : SKColors.Black;
if (_selectedInkColor == oldDefault)
{
_selectedInkColor = newDefault;
if (_toolMode == WhiteboardToolMode.Pen)
{
InkCanvas.AvaloniaSkiaInkCanvas.Settings.InkColor = _selectedInkColor;
}
if (InkColorPicker is not null)
{
InkColorPicker.Color = new Color(
_selectedInkColor.Alpha,
_selectedInkColor.Red,
_selectedInkColor.Green,
_selectedInkColor.Blue);
}
}
}
public void SetComponentPlacementContext(string componentId, string? placementId)
{
var nextComponentId = string.IsNullOrWhiteSpace(componentId)
@@ -431,7 +461,9 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
private void OnColorPickerColorChanged(object? sender, ColorChangedEventArgs e)
{
var color = e.NewColor;
SetInkColor(new SKColor(color.R, color.G, color.B, color.A));
var skColor = new SKColor(color.R, color.G, color.B, color.A);
_isUserCustomColor = skColor != SKColors.Black && skColor != SKColors.White;
SetInkColor(skColor);
}
private void OnInkThicknessSliderValueChanged(object? sender, RangeBaseValueChangedEventArgs e)
@@ -713,7 +745,8 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
InkCanvas.AvaloniaSkiaInkCanvas.AddStaticStroke(staticStroke);
}
UpdateInkCanvasCacheSettings(forceRefresh: true);
InkCanvas.AvaloniaSkiaInkCanvas.UpdateBitmapCache();
InkCanvas.InvalidateVisual();
}
private static InkStylusPoint ConvertStylusPoint(WhiteboardStylusPointSnapshot point)
@@ -766,7 +799,9 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
}
}
UpdateInkCanvasCacheSettings(forceRefresh: true);
InkCanvas.AvaloniaSkiaInkCanvas.UseBitmapCache(false);
InkCanvas.AvaloniaSkiaInkCanvas.InvalidateBitmapCache();
InkCanvas.InvalidateVisual();
}
private bool HasValidPersistenceContext()
@@ -784,47 +819,4 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
return Array.Empty<InkStylusPoint>();
}
private void UpdateInkCanvasCacheSettings(bool forceRefresh)
{
var renderScaling = TopLevel.GetTopLevel(this)?.RenderScaling ?? 1d;
var widthPx = Math.Max(1d, CanvasBorder.Bounds.Width * renderScaling);
var heightPx = Math.Max(1d, CanvasBorder.Bounds.Height * renderScaling);
var longestSide = Math.Max(widthPx, heightPx);
var area = widthPx * heightPx;
var cacheEnabled = longestSide <= 1536d && area <= 1_400_000d;
var cacheSize = (int)Math.Clamp(Math.Ceiling(longestSide), 384d, 1536d);
if (!forceRefresh &&
_lastBitmapCacheEnabled == cacheEnabled &&
_lastBitmapCacheSize == cacheSize)
{
return;
}
_lastBitmapCacheEnabled = cacheEnabled;
_lastBitmapCacheSize = cacheSize;
var settings = InkCanvas.AvaloniaSkiaInkCanvas.Settings;
settings.IsBitmapCacheEnabled = cacheEnabled;
settings.MaxBitmapCacheSize = cacheSize;
try
{
InkCanvas.AvaloniaSkiaInkCanvas.UseBitmapCache(cacheEnabled);
if (cacheEnabled)
{
InkCanvas.AvaloniaSkiaInkCanvas.UpdateBitmapCache();
}
else
{
InkCanvas.AvaloniaSkiaInkCanvas.InvalidateBitmapCache();
InkCanvas.InvalidateVisual();
}
}
catch
{
// Keep drawing available even if the underlying cache backend rejects the cache update.
}
}
}

View File

@@ -1,194 +1,217 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:LanMountainDesktop.ViewModels"
xmlns:fi="using:FluentIcons.Avalonia"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
xmlns:converters="using:Avalonia.Data.Converters"
x:Class="LanMountainDesktop.Views.FusedDesktopComponentLibraryControl"
x:DataType="vm:ComponentLibraryWindowViewModel">
<UserControl.Styles>
<!-- 分类列表项样式 -->
<!-- 分类列表项样式 - 遵循 Fluent NavigationView 风格 -->
<Style Selector="ListBoxItem.category-item">
<Setter Property="Padding" Value="0"/>
<Setter Property="Margin" Value="0,0,0,4"/>
<Setter Property="Margin" Value="0,2"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}"/>
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="Background" Duration="0:0:0.083" Easing="0.05,0.75,0.10,1.00"/>
</Transitions>
</Setter>
</Style>
<Style Selector="ListBoxItem.category-item:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}"/>
</Style>
<Style Selector="ListBoxItem.category-item:selected /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemSelectedBackgroundBrush}"/>
</Style>
<Style Selector="ListBoxItem.category-item:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SubtleFillColorTertiaryBrush}"/>
</Style>
<!-- 分类项内容容器 - 默认状态 -->
<Style Selector="Border.category-content">
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemBackgroundBrush}"/>
<Setter Property="Padding" Value="12,10"/>
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusMd}"/>
</Style>
<Style Selector="ListBoxItem.category-item:selected Border.category-content">
<Setter Property="Background" Value="{DynamicResource AdaptiveAccentBrush}"/>
</Style>
<Style Selector="ListBoxItem.category-item:pointerover Border.category-content">
<Setter Property="Opacity" Value="0.9"/>
</Style>
<!-- 分类项图标和文字 - 默认状态 -->
<Style Selector="ListBoxItem.category-item fi|SymbolIcon.category-icon">
<!-- 分类项图标和文字 -->
<Style Selector="ListBoxItem.category-item fi|FluentIcon.category-icon">
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}"/>
</Style>
<Style Selector="ListBoxItem.category-item:selected fi|SymbolIcon.category-icon">
<Setter Property="Foreground" Value="{DynamicResource AdaptiveOnAccentBrush}"/>
<Style Selector="ListBoxItem.category-item:selected fi|FluentIcon.category-icon">
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}"/>
</Style>
<Style Selector="ListBoxItem.category-item TextBlock.category-text">
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}"/>
</Style>
<Style Selector="ListBoxItem.category-item:selected TextBlock.category-text">
<Setter Property="Foreground" Value="{DynamicResource AdaptiveOnAccentBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
</UserControl.Styles>
<Grid ColumnDefinitions="280,*"
ColumnSpacing="16"
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="0"
Margin="0">
<!-- 分类列表 (左侧) -->
<Border Classes="surface-translucent-panel"
CornerRadius="{DynamicResource DesignCornerRadiusLg}"
Padding="12">
<ListBox x:Name="CategoryListBox"
Background="Transparent"
BorderThickness="0"
SelectionChanged="OnCategorySelectionChanged"
ItemsSource="{Binding Categories}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:ComponentLibraryCategoryViewModel">
<Border Classes="category-content">
<!-- 左侧导航列 - 分类列表 + 底部"查找更多组件" -->
<Border Width="280"
Background="Transparent">
<Grid RowDefinitions="*,Auto">
<!-- 分类列表 -->
<ListBox x:Name="CategoryListBox"
Grid.Row="0"
Background="Transparent"
BorderThickness="0"
Margin="8,8,4,0"
SelectionChanged="OnCategorySelectionChanged"
ItemsSource="{Binding Categories}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:ComponentLibraryCategoryViewModel">
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="12">
<fi:SymbolIcon Symbol="{Binding Icon}"
ColumnSpacing="12"
Margin="12,10">
<fi:FluentIcon Icon="{Binding Icon}"
IconVariant="Regular"
FontSize="20"
FontSize="18"
Classes="category-icon"/>
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
FontSize="14"
FontWeight="SemiBold"
Classes="category-text"
Text="{Binding Title}"/>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- 底部"查找更多组件" - 在左侧导航列底部 -->
<StackPanel Grid.Row="1"
Margin="12,8,8,12">
<Border Height="1"
Background="{DynamicResource AdaptiveGlassPanelBorderBrush}"
Opacity="0.4"
Margin="0,0,0,8"/>
<Button Classes="hyperlink"
HorizontalAlignment="Left"
Click="OnFindMoreComponentsClick">
<StackPanel Orientation="Horizontal" Spacing="6">
<fi:FluentIcon Icon="Globe" IconVariant="Regular" FontSize="14"/>
<TextBlock Text="查找更多组件"/>
</StackPanel>
</Button>
</StackPanel>
</Grid>
</Border>
<!-- 组件预览区 (右侧) -->
<!-- 右侧内容区与左侧的分隔线 -->
<Border Grid.Column="1"
Classes="surface-translucent-strong"
CornerRadius="{DynamicResource DesignCornerRadiusLg}"
Padding="24">
<Panel>
Width="1"
HorizontalAlignment="Left"
Background="{DynamicResource AdaptiveGlassPanelBorderBrush}"
Opacity="0.5"/>
<!-- 组件预览区 (右侧) -->
<ScrollViewer Grid.Column="1"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="16,8,12,8"
Spacing="0">
<!-- 有选中组件时的显示 -->
<Grid RowDefinitions="Auto,*,Auto"
IsVisible="{Binding SelectedComponent, Converter={x:Static converters:ObjectConverters.IsNotNull}}">
<!-- 组件标题 -->
<TextBlock Grid.Row="0"
FontSize="28"
FontWeight="Bold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="{Binding SelectedComponent.DisplayName}"
Margin="0,0,0,20"/>
<Panel IsVisible="{Binding SelectedComponent, Converter={x:Static converters:ObjectConverters.IsNotNull}}">
<!-- 预览区域 -->
<Border Grid.Row="1"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
BorderThickness="1"
Padding="20"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Grid Width="400"
Height="300">
<!-- 预览图片 -->
<Image Source="{Binding SelectedComponent.PreviewBitmap}"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center"
RenderOptions.BitmapInterpolationMode="HighQuality"
IsVisible="{Binding SelectedComponent.IsPreviewReady}"/>
<!-- 组件展示面板 - 有独立背景色,与窗口背景形成层级分界 -->
<Border Classes="surface-translucent-panel"
CornerRadius="{DynamicResource DesignCornerRadiusLg}"
Padding="20">
<StackPanel Spacing="16">
<!-- 组件标题 -->
<TextBlock FontSize="28"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="{Binding SelectedComponent.DisplayName}"/>
<!-- 加载中状态 -->
<Border IsVisible="{Binding SelectedComponent.IsPreviewPending}"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12">
<ProgressBar Width="120"
IsIndeterminate="True"/>
<TextBlock HorizontalAlignment="Center"
TextAlignment="Center"
FontSize="14"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="{Binding SelectedComponent.PreviewStatusText}"/>
</StackPanel>
<!-- 固定大小的预览卡片 -->
<Border CornerRadius="{DynamicResource DesignCornerRadiusSm}"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
BorderThickness="1"
Width="420"
Height="300"
HorizontalAlignment="Center">
<Grid Margin="16">
<!-- 预览图片 -->
<Image Source="{Binding SelectedComponent.PreviewBitmap}"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center"
RenderOptions.BitmapInterpolationMode="HighQuality"
IsVisible="{Binding SelectedComponent.IsPreviewReady}"/>
<!-- 加载中状态 -->
<Border IsVisible="{Binding SelectedComponent.IsPreviewPending}"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12">
<ProgressBar Width="120"
IsIndeterminate="True"/>
<TextBlock HorizontalAlignment="Center"
TextAlignment="Center"
FontSize="14"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="{Binding SelectedComponent.PreviewStatusText}"/>
</StackPanel>
</Border>
<!-- 失败状态 -->
<Border IsVisible="{Binding SelectedComponent.IsPreviewFailed}"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="8">
<fi:FluentIcon Icon="ImageOff"
IconVariant="Regular"
FontSize="48"
Opacity="0.5"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"/>
<TextBlock HorizontalAlignment="Center"
TextAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="{Binding SelectedComponent.PreviewStatusText}"/>
<TextBlock HorizontalAlignment="Center"
TextAlignment="Center"
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="{Binding SelectedComponent.PreviewErrorMessage}"/>
</StackPanel>
</Border>
</Grid>
</Border>
<!-- 失败状态 -->
<Border IsVisible="{Binding SelectedComponent.IsPreviewFailed}"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="8">
<fi:SymbolIcon Symbol="ImageOff"
FontSize="48"
Opacity="0.5"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"/>
<TextBlock HorizontalAlignment="Center"
TextAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="{Binding SelectedComponent.PreviewStatusText}"/>
<TextBlock HorizontalAlignment="Center"
TextAlignment="Center"
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="{Binding SelectedComponent.PreviewErrorMessage}"/>
<!-- "添加小组件"按钮 - 在面板内居中,使用主题强调色 -->
<Button HorizontalAlignment="Center"
Classes="accent"
Padding="24,10"
Tag="{Binding SelectedComponent.ComponentId}"
Click="OnAddComponentClick">
<StackPanel Orientation="Horizontal" Spacing="8">
<fi:FluentIcon Icon="Add" IconVariant="Regular" FontSize="16"/>
<TextBlock Text="添加小组件" FontWeight="SemiBold"/>
</StackPanel>
</Border>
</Grid>
</Button>
</StackPanel>
</Border>
<!-- 底部操作区 -->
<Grid Grid.Row="2"
ColumnDefinitions="*,Auto"
Margin="0,24,0,0">
<TextBlock Grid.Column="0"
VerticalAlignment="Center"
FontSize="14"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="{Binding SelectedComponent.ComponentId, StringFormat='组件 ID: {0}'}"/>
<Button Grid.Column="1"
Classes="accent"
Padding="20,12"
Tag="{Binding SelectedComponent.ComponentId}"
Click="OnAddComponentClick">
<StackPanel Orientation="Horizontal" Spacing="8">
<fi:SymbolIcon Symbol="Add" FontSize="16"/>
<TextBlock Text="添加到桌面" FontWeight="SemiBold"/>
</StackPanel>
</Button>
</Grid>
</Grid>
</Panel>
<!-- 空状态 -->
<Grid IsVisible="{Binding SelectedComponent, Converter={x:Static converters:ObjectConverters.IsNull}}"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<StackPanel Spacing="16" HorizontalAlignment="Center">
<fi:SymbolIcon Symbol="Apps"
VerticalAlignment="Center"
MinHeight="400">
<StackPanel Spacing="16" HorizontalAlignment="Center"
VerticalAlignment="Center">
<fi:FluentIcon Icon="Apps"
IconVariant="Regular"
FontSize="64"
Opacity="0.3"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"/>
@@ -198,7 +221,7 @@
Text="请从左侧选择一个组件"/>
</StackPanel>
</Grid>
</Panel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</UserControl>

View File

@@ -4,6 +4,7 @@ using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using FluentIcons.Common;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Services;
@@ -29,6 +30,8 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService();
private readonly ICalculatorDataService _calculatorDataService = new CalculatorDataService();
private static readonly LocalizationService _localizationService = new();
public FusedDesktopComponentLibraryControl()
{
InitializeComponent();
@@ -76,26 +79,15 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
{
_viewModel.Categories.Clear();
var languageCode = _settingsFacade.Region.Get().LanguageCode;
// 添加"全部组件"分类
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
"all",
"全部组件",
L(languageCode, "component_category.all", "All"),
Symbol.Apps,
Array.Empty<ComponentLibraryItemViewModel>()));
var categoryMap = new Dictionary<string, (string Display, Symbol Icon)>
{
{ "clock", ("时钟", Symbol.Clock) },
{ "date", ("日历", Symbol.CalendarDate) },
{ "weather", ("天气", Symbol.WeatherSunny) },
{ "board", ("画板", Symbol.Edit) },
{ "media", ("媒体", Symbol.Play) },
{ "info", ("资讯", Symbol.News) },
{ "calculator", ("工具", Symbol.Calculator) },
{ "study", ("学习", Symbol.Hourglass) },
{ "file", ("文件", Symbol.Folder) }
};
var usedCategories = _allDefinitions
.Select(d => d.Category)
.Distinct()
@@ -103,23 +95,62 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
foreach (var cat in usedCategories)
{
if (categoryMap.TryGetValue(cat.ToLower(), out var info))
{
var categoryComponents = _allDefinitions
.Where(d => string.Equals(d.Category, cat, StringComparison.OrdinalIgnoreCase))
.OrderBy(d => d.DisplayName)
.Select(d => CreateComponentItem(d))
.ToArray();
var icon = ResolveCategoryIcon(cat);
var title = GetLocalizedCategoryTitle(languageCode, cat);
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
cat,
info.Display,
info.Icon,
categoryComponents));
}
var categoryComponents = _allDefinitions
.Where(d => string.Equals(d.Category, cat, StringComparison.OrdinalIgnoreCase))
.OrderBy(d => d.DisplayName)
.Select(d => CreateComponentItem(d))
.ToArray();
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
cat,
title,
icon,
categoryComponents));
}
}
/// <summary>
/// 分类图标映射 - 与阑山桌面 Dock 栏组件库 (MainWindow.ComponentSystem) 保持一致
/// </summary>
private static Symbol ResolveCategoryIcon(string categoryId)
{
if (string.Equals(categoryId, "Clock", StringComparison.OrdinalIgnoreCase)) return Symbol.Clock;
if (string.Equals(categoryId, "Date", StringComparison.OrdinalIgnoreCase)) return Symbol.CalendarDate;
if (string.Equals(categoryId, "Weather", StringComparison.OrdinalIgnoreCase)) return Symbol.WeatherSunny;
if (string.Equals(categoryId, "Board", StringComparison.OrdinalIgnoreCase)) return Symbol.Edit;
if (string.Equals(categoryId, "Media", StringComparison.OrdinalIgnoreCase)) return Symbol.Play;
if (string.Equals(categoryId, "Info", StringComparison.OrdinalIgnoreCase)) return Symbol.Apps;
if (string.Equals(categoryId, "Calculator", StringComparison.OrdinalIgnoreCase)) return Symbol.Calculator;
if (string.Equals(categoryId, "Study", StringComparison.OrdinalIgnoreCase)) return Symbol.Hourglass;
if (string.Equals(categoryId, "File", StringComparison.OrdinalIgnoreCase)) return Symbol.Folder;
return Symbol.Apps;
}
/// <summary>
/// 分类本地化标题 - 与阑山桌面 Dock 栏组件库 (MainWindow.ComponentSystem) 保持一致
/// </summary>
private string GetLocalizedCategoryTitle(string languageCode, string categoryId)
{
if (string.Equals(categoryId, "Clock", StringComparison.OrdinalIgnoreCase)) return L(languageCode, "component_category.clock", "Clock");
if (string.Equals(categoryId, "Date", StringComparison.OrdinalIgnoreCase)) return L(languageCode, "component_category.date", "Calendar");
if (string.Equals(categoryId, "Weather", StringComparison.OrdinalIgnoreCase)) return L(languageCode, "component_category.weather", "Weather");
if (string.Equals(categoryId, "Board", StringComparison.OrdinalIgnoreCase)) return L(languageCode, "component_category.board", "Board");
if (string.Equals(categoryId, "Media", StringComparison.OrdinalIgnoreCase)) return L(languageCode, "component_category.media", "Media");
if (string.Equals(categoryId, "Info", StringComparison.OrdinalIgnoreCase)) return L(languageCode, "component_category.info", "Info");
if (string.Equals(categoryId, "Calculator", StringComparison.OrdinalIgnoreCase)) return L(languageCode, "component_category.calculator", "Calculator");
if (string.Equals(categoryId, "Study", StringComparison.OrdinalIgnoreCase)) return L(languageCode, "component_category.study", "Study");
if (string.Equals(categoryId, "File", StringComparison.OrdinalIgnoreCase)) return L(languageCode, "component_category.file", "File");
return categoryId;
}
private string L(string languageCode, string key, string fallback)
{
return _localizationService.GetString(languageCode, key, fallback);
}
private ComponentLibraryItemViewModel CreateComponentItem(DesktopComponentDefinition definition)
{
var previewKey = ComponentPreviewKey.ForComponentType(
@@ -221,4 +252,22 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
AddComponentRequested?.Invoke(this, componentId);
}
}
private void OnFindMoreComponentsClick(object? sender, RoutedEventArgs e)
{
// 打开设置窗口并导航到插件目录页面
if (Application.Current is App app && app.SettingsWindowService is { } settingsWindowService)
{
var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow;
var request = new SettingsWindowOpenRequest(
Source: "FusedDesktopComponentLibrary",
Owner: mainWindow,
PageId: "plugin-catalog");
settingsWindowService.Open(request);
}
// 关闭所在窗口
var window = this.FindAncestorOfType<Window>();
window?.Close();
}
}

View File

@@ -1,69 +1,73 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:LanMountainDesktop.Views"
xmlns:fi="using:FluentIcons.Avalonia"
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
x:Class="LanMountainDesktop.Views.FusedDesktopComponentLibraryWindow"
Width="860" Height="620"
MinWidth="600" MinHeight="500"
CanResize="True"
WindowStartupLocation="CenterScreen"
SystemDecorations="Full"
SystemDecorations="BorderOnly"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="NoChrome"
ExtendClientAreaTitleBarHeightHint="-1"
ExtendClientAreaTitleBarHeightHint="48"
Background="Transparent"
TransparencyLevelHint="Mica"
Title="添加小组件">
<Panel>
<!-- 背景磨砂效果 -->
<Border Background="{DynamicResource AdaptiveSurfaceLowBrush}"
Opacity="0.85" />
<Grid x:Name="RootGrid"
Classes="settings-scope"
Background="{DynamicResource AdaptiveSettingsWindowBackgroundBrush}"
RowDefinitions="Auto,*">
<!-- 自定义标题栏 - 与 SettingsWindow 风格一致 -->
<Border x:Name="WindowTitleBarHost"
Height="48"
Padding="12,0,12,0"
Background="{DynamicResource AdaptiveSettingsWindowBackgroundBrush}"
BorderBrush="{DynamicResource AdaptiveSettingsWindowBorderBrush}"
BorderThickness="0,0,0,1"
PointerPressed="OnWindowTitleBarPointerPressed">
<Grid ColumnDefinitions="Auto,Auto,*,Auto"
ColumnSpacing="8"
VerticalAlignment="Center">
<fi:FluentIcon x:Name="WindowBrandIcon"
Icon="Apps"
IconVariant="Filled"
FontSize="16"
IsHitTestVisible="False"
VerticalAlignment="Center" />
<Grid RowDefinitions="Auto,*,Auto">
<!-- 自定义标题栏 -->
<Border Background="Transparent"
IsHitTestVisible="True"
Padding="20,16">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Spacing="6" VerticalAlignment="Center">
<TextBlock Text="添加小组件"
FontWeight="SemiBold"
FontSize="20"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock Text="将精美组件放置在您的系统桌面上(负一屏)"
Opacity="0.6"
FontSize="13"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
</StackPanel>
<TextBlock x:Name="WindowTitleTextBlock"
Grid.Column="1"
FontSize="12"
FontWeight="SemiBold"
IsHitTestVisible="False"
Text="添加小组件" />
<Button Grid.Column="1"
Width="36" Height="36"
Padding="0"
CornerRadius="{DynamicResource DesignCornerRadiusSm}"
BorderThickness="0"
Click="OnCloseClick">
<fi:SymbolIcon Symbol="Dismiss" FontSize="18" />
</Button>
</Grid>
</Border>
<TextBlock Grid.Column="2"
FontSize="12"
Opacity="0.6"
IsHitTestVisible="False"
VerticalAlignment="Center"
Text="将精美组件放置在您的系统桌面上(负一屏)" />
<!-- 组件库控件 -->
<controls:FusedDesktopComponentLibraryControl x:Name="LibraryControl"
Grid.Row="1" />
<!-- 底部查找更多组件链接 -->
<Border Grid.Row="2"
Background="Transparent"
Padding="20,12">
<Button Classes="hyperlink"
HorizontalAlignment="Center"
Click="OnFindMoreComponentsClick">
<StackPanel Orientation="Horizontal" Spacing="4">
<fi:SymbolIcon Symbol="Globe" FontSize="14" />
<TextBlock Text="查找更多组件" />
</StackPanel>
<Button x:Name="CloseWindowButton"
Grid.Column="3"
Width="40"
Height="32"
Padding="0"
Background="Transparent"
BorderThickness="0"
Click="OnCloseClick">
<fi:FluentIcon Icon="Dismiss"
IconVariant="Regular"
FontSize="16" />
</Button>
</Border>
</Grid>
</Panel>
</Grid>
</Border>
<!-- 组件库控件 -->
<controls:FusedDesktopComponentLibraryControl x:Name="LibraryControl"
Grid.Row="1"
Margin="12,8,16,8" />
</Grid>
</Window>

View File

@@ -1,6 +1,7 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Services;
@@ -103,23 +104,11 @@ public partial class FusedDesktopComponentLibraryWindow : Window
Close();
}
/// <summary>
/// 查找更多组件链接点击处理 - 打开设置窗口的插件目录页面
/// </summary>
private void OnFindMoreComponentsClick(object? sender, RoutedEventArgs e)
private void OnWindowTitleBarPointerPressed(object? sender, PointerPressedEventArgs e)
{
// 关闭当前窗口
Close();
// 打开设置窗口并导航到插件目录页面
if (Application.Current is App app && app.SettingsWindowService is { } settingsWindowService)
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow;
var request = new SettingsWindowOpenRequest(
Source: "FusedDesktopComponentLibrary",
Owner: mainWindow,
PageId: "plugin-catalog");
settingsWindowService.Open(request);
BeginMoveDrag(e);
}
}

683
design.md Normal file
View File

@@ -0,0 +1,683 @@
# UI Design System Guide (design.md)
> **目标**: 让 AI 正确使用 Fluent Avalonia / Fluent Icons / Material Avalonia避免窗口套窗口、容器套容器
>
> **最后更新**: 2026-04-11
---
## 一句话总结
**主界面用 Fluent + FluentIcon编辑器用 Material + MaterialIcon永远不要混用保持扁平结构。**
---
## 1. 技术栈与职责
### 1.1 库清单
| 库 | 包名 | 什么时候用 |
|---|------|----------|
| **FluentAvaloniaUI** | `FluentAvaloniaUI` | 主界面、设置页、导航 |
| **FluentIcons.Avalonia.Fluent** | `FluentIcons.Avalonia.Fluent` | 主界面图标(**首选**|
| **FluentIcons.Avalonia** | `FluentIcons.Avalonia` | 旧图标兼容SymbolIcon|
| **Material.Icons.Avalonia** | `Material.Icons.Avalonia` | 编辑器图标(**仅限 ComponentEditorWindow**|
| **Material.Avalonia** | `Material.Avalonia` | MD3 主题(**仅限 ComponentEditorWindow**|
### 1.2 初始化顺序App.axaml
```xml
<Application.Styles>
<sty:FluentAvaloniaTheme /> <!-- 第 1 位:基础 Win11 风格 -->
<mi:MaterialIconStyles /> <!-- 第 2 位:全局注册 Material Icons 样式 -->
<!-- 项目自定义样式 -->
<StyleInclude Source="avares://LanMountainDesktop/Styles/FlutermotionToken.axaml" />
<StyleInclude Source="avares://LanMountainDesktop/Styles/GlassModule.axaml" />
<StyleInclude Source="avares://LanMountainDesktop/Styles/SettingsAnimations.axaml" />
<StyleInclude Source="avares://LanMountainDesktop/Styles/SettingsCardStyles.axaml" />
<StyleInclude Source="avares://LanMountainDesktop/Styles/NavigationStyles.axaml" />
</Application.Styles>
```
---
## 2. 命名空间速查表
**复制粘贴用:**
```xml
<!-- 必需 -->
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
<!-- FluentAvaloniaUI 控件(主界面/设置页必需)-->
xmlns:ui="using:FluentAvalonia.UI.Controls"
<!-- Fluent Icons - 新版推荐(主界面首选)-->
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
<!-- Fluent Icons - 旧版兼容(已有代码维护用)-->
<!-- xmlns:fi-legacy="using:FluentIcons.Avalonia" -->
<!-- Material Icons仅 ComponentEditorWindow 使用)-->
<!-- xmlns:mi="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" -->
<!-- Material Theme仅 ComponentEditorWindow 使用)-->
<!-- xmlns:themes="clr-namespace:Material.Styles.Themes;assembly=Material.Styles" -->
<!-- 项目控件 -->
xmlns:local="using:LanMountainDesktop.Controls"
xmlns:comp="using:LanMountainDesktop.Views.Components"
```
---
## 3. 图标系统
### 3.1 选择决策树
```
你在写什么?
├─ 设置页面 / 主界面 / 桌面组件?
│ └─ 用 FluentIconfi:FluentIcon
│ ├─ 需要 Filled/Regular 切换?→ IconVariant="Filled" 或 "Regular"
│ └─ 简单静态图标?→ 也用 FluentIcon不用 SymbolIcon
├─ ComponentEditorWindow 及其子页面?
│ └─ 用 MaterialIconmi:MaterialIcon
└─ 其他情况?
└─ 默认 FluentIcon
```
### 3.2 FluentIcon 使用方法
```xml
<fi:FluentIcon Icon="Settings" <!-- -->
IconVariant="Filled" <!-- Filled | Regular -->
Classes="icon-m" /> <!-- icon-s(12px) | icon-m(16px) | icon-l(20px) -->
```
**常用图标名称:**
- 导航类:`Home`, `Settings`, `Navigation`, `ArrowLeft`, `ChevronRight`, `Dismiss`
- 操作类:`Add`, `Delete`, `Edit`, `Save`, `Refresh`, `Sync`, `ArrowSync`
- 状态类:`Info`, `Warning`, `ErrorBadge`, `CheckmarkCircle`
- 外观类:`ThemeLightDark`, `ColorBackground`, `Appearance`
### 3.3 MaterialIcon 使用方法(仅限编辑器)
```xml
<mi:MaterialIcon Kind="Close" <!-- -->
Width="24"
Height="24"
Foreground="{DynamicResource EditorPrimaryBrush}" />
```
**常用 Kind 值:**
- 操作:`Close`, `Check`, `Pencil`, `Delete`, `Settings`, `Plus`
- 导航:`ArrowLeft`, `ArrowRight`, `Home`, `Menu`
- 系统:`Power`, `Lock`, `ExitToApp`, `Refresh`, `WeatherNight`
### 3.4 ❌ 禁止事项
```xml
<!-- ❌ 错误:同一区域混用两种图标库 -->
<StackPanel>
<fi:FluentIcon Icon="Home" /> <!-- Fluent -->
<mi:MaterialIcon Kind="Settings" /> <!-- Material -->
</StackPanel>
<!-- ❌ 错误:硬编码尺寸 -->
<fi:FluentIcon Icon="Settings" FontSize="18" />
<!-- ✅ 正确:使用预定义 class -->
<fi:FluentIcon Icon="Settings" Classes="icon-m" />
```
---
## 4. 容器嵌套规范(核心!)
### 4.1 最大深度限制
| 场景 | 最大层数 | 从哪里开始数 |
|-----|---------|------------|
| 普通页面 | **≤ 4 层** | Window/UserControl → ... → 叶子节点 |
| Popup/Dialog | **≤ 3 层** | Border → Content |
| 列表项/DataTemplate | **≤ 3 层** | Root → ... → 元素 |
| MainWindow 桌面布局 | **≤ 6 层** | 特殊允许(多层叠加需求)|
### 4.2 如何数层级?
从根元素到目标元素经过的容器标签数:
```xml
<UserControl> <!-- 层级 0-->
<Border> <!-- 层级 1 -->
<Grid> <!-- 层级 2 -->
<StackPanel> <!-- 层级 3 -->
<Button> <!-- 层级 4叶子节点✅ OK -->
</Button>
</StackPanel>
</Grid>
</Border>
</UserControl>
```
### 4.3 推荐的标准结构
#### 结构 A标准设置页面3-4 层)
```xml
<UserControl>
<ScrollViewer> <!-- 层 1 -->
<StackPanel Spacing="24"> <!-- 层 2 -->
<Border Classes="settings-section-card"> <!-- 层 3 -->
<StackPanel Spacing="16"> <!-- 层 4 ✅ -->
<!-- 内容 -->
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</UserControl>
```
#### 结构 B卡片/面板组件3 层)
```xml
<Border CornerRadius="..." Padding="..."> <!-- 层 1-->
<Grid RowDefinitions="Auto,*"> <!-- 层 2 -->
<Grid ColumnDefinitions="Auto,*,Auto"> <!-- 层 3a: Header -->
<fi:FluentIcon Classes="icon-l" />
<TextBlock Grid.Column="1" />
<ContentPresenter Grid.Column="2" />
</Grid>
<ContentControl Grid.Row="1" /> <!-- 层 3b: Body ✅ -->
</Grid>
</Border>
```
#### 结构 C列表项2-3 层)
```xml
<Border Classes="list-item"> <!-- 层 1 -->
<Grid ColumnDefinitions="Auto,*,Auto"> <!-- 层 2 -->
<Border Classes="icon-host"> <!-- 层 3a -->
<fi:FluentIcon Classes="icon-m" />
</Border>
<StackPanel Grid.Column="1" Spacing="4"> <!-- 层 3b -->
<TextBlock Classes="title" />
<TextBlock Classes="subtitle" />
</StackPanel>
<Button Grid.Column="2">...</Button> <!-- 层 3c ✅ -->
</Grid>
</Border>
```
### 4.4 ❌ 反模式:过度嵌套
```xml
<!-- ❌ 错误7 层嵌套(实际项目中出现的反面教材)-->
<Grid> <!-- 1 -->
<Grid> <!-- 2 -->
<Border> <!-- 3 -->
<Grid> <!-- 4 -->
<Border> <!-- 5 -->
<Grid> <!-- 6 -->
<Border> <!-- 7 ❌ 太深了!-->
<Button Content="Click" />
</Border>
</Grid>
</Border>
</Grid>
</Border>
</Grid>
</Grid>
<!-- ✅ 重构后2 层 -->
<Grid HorizontalAlignment="Center"
VerticalAlignment="Center">
<Button Content="Click"
Padding="16,8" />
</Grid>
```
### 4.5 何时可以超过限制?
只有这 3 种情况:
1. **MainWindow 桌面布局**(壁纸层 + 组件层 + 拖拽层 + 任务栏)
2. **需要独立动画层**Transform/Opacity 动画需要单独容器)
3. **复杂 Popup 内部**
**必须加注释说明原因:**
```xml
<!--
允许深层嵌套原因:桌面渲染需要支持以下视觉层级
- DesktopWallpaperLayer壁纸背景
- DesktopPagesContainer桌面分页
- LauncherPagePanel启动器面板
- Canvas 拖拽层
- BottomTaskbarContainer任务栏
-->
<Grid x:Name="DesktopHost">
...
</Grid>
```
---
## 5. 窗口 vs UserControl vs Border
### 5.1 什么时候用什么?
| 需求 | 用什么 | 示例 |
|-----|-------|------|
| 独立窗口(有标题栏、可拖动、任务栏可见)| **Window** | SettingsWindow, ComponentEditorWindow |
| 可复用的 UI 组件块 | **UserControl** | SettingsOptionCard, ClockWidget |
| 视觉上的卡片/面板/容器 | **Border** | 设置分区卡片、弹出面板 |
### 5.2 ❌ 禁止:窗口套窗口
```csharp
// ❌ 错误:在 XAML 中实例化另一个 Window
// <local:SettingsWindow Visibility="Visible" />
// ✅ 正确:通过代码显示独立窗口
var settings = new SettingsWindow { Owner = this };
settings.Show();
```
### 5.3 ❌ 禁止:把 Window 当 UserControl 用
```xml
<!-- ❌ 错误MainWindow 内部嵌入了一个本应是独立窗口的东西 -->
<Window x:Class="MainWindow">
<Grid>
<Border x:Name="ComponentLibraryWindow" <!-- Window -->
Width="620"
Height="320"
CornerRadius="36">
<!-- 组件库内容 -->
</Border>
</Grid>
</Window>
```
**如果它不是真正的操作系统窗口,就用 Border 或 UserControl。**
---
## 6. 颜色与资源使用规范
### 6.1 必须使用 DynamicResource
```xml
<!-- ❌ 错误:硬编码颜色 -->
<TextBlock Foreground="#FF1D1B20" />
<Border Background="#FFF3EDF7" />
<Button Background="#FF6750A4" />
<!-- ✅ 正确:使用动态资源 -->
<TextBlock Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}" />
<Button Background="{DynamicResource AccentBrush}" />
```
### 6.2 常用资源键速查
| 资源键 | 用途 | 示例场景 |
|--------|------|---------|
| `AdaptiveTextPrimaryBrush` | 主要文本 | 标题、正文 |
| `AdaptiveTextSecondaryBrush` | 次要文本 | 描述文字、提示 |
| `AdaptiveSurfaceRaisedBrush` | 抬高表面 | 卡片背景、面板 |
| `AdaptiveSurfaceOverlayBrush` | 覆盖层 | 遮罩、弹窗背景 |
| `AccentBrush` | 强调色 | 主按钮、选中态 |
| `AppFontFamily` | 应用字体 | 全局字体设置 |
### 6.3 圆角 Token强制
```xml
<!-- ❌ 错误:硬编码圆角 -->
<Border CornerRadius="8" />
<Button CornerRadius="12" />
<!-- ❌ 错误:桌面组件根容器用了非组件级 Token -->
<Border CornerRadius="{DynamicResource DesignCornerRadiusMd}" />
<!-- ✅ 正确 -->
<!-- 桌面组件Widget根容器必须且只能用这个 -->
<Border CornerRadius="{DynamicResource DesignCornerRadiusComponent}" />
<!-- 内部元素按层级选择 -->
<Border CornerRadius="{DynamicResource DesignCornerRadiusSm}" /> <!---->
<Border CornerRadius="{DynamicResource DesignCornerRadiusMd}" /> <!---->
<Border CornerRadius="{DynamicResource DesignCornerRadiusLg}" /> <!---->
```
---
## 7. FluentAvaloniaUI 控件用法
### 7.1 NavigationView设置页导航
```xml
<ui:NavigationView x:Name="RootNav"
PaneDisplayMode="Auto" <!-- -->
OpenPaneLength="283" <!-- Win11 标准宽度 -->
IsSettingsVisible="False" <!-- 隐藏默认设置按钮 -->
IsBackButtonVisible="False"
SelectionChanged="OnNavChanged">
<ui:NavigationView.Resources>
<!-- 移除默认背景色 -->
<SolidColorBrush x:Key="NavigationViewContentBackground" Color="Transparent" />
<SolidColorBrush x:Key="NavigationViewContentGridBorderBrush" Color="Transparent" />
</ui:NavigationView.Resources>
<Grid Margin="12,0,16,16">
<ui:Frame x:Name="ContentFrame" />
</Grid>
</ui:NavigationView>
```
### 7.2 Frame页面导航容器
```xml
<ui:Frame x:Name="ContentFrame" />
```
```csharp
// C# 代码中导航
ContentFrame.Navigate(typeof(SettingsHomePage));
// 或绑定
ContentFrame.SourcePageType = typeof(SettingsHomePage);
```
### 7.3 InfoBar内联通知条
```xml
<ui:InfoBar Title="发现新版本"
Message="v2.0.0 可用"
Severity="Informational" <!-- Informational | Warning | Error | Success -->
IsOpen="True"
ActionButtonText="立即更新"
ActionButtonClick="OnUpdateClick"
CloseButtonClick="OnDismissClick" />
```
### 7.4 ContentDialog模态对话框
```csharp
var dialog = new ContentDialog
{
Title = "确认删除",
Content = "确定要删除吗?此操作不可撤销。",
PrimaryButtonText = "删除",
CloseButtonText = "取消",
DefaultButton = ContentDialogButton.Primary
};
var result = await dialog.ShowAsync(this);
if (result == ContentDialogResult.Primary)
{
}
```
---
## 8. Material.Avalonia 使用规范(严格限制!)
### 8.1 ⚠️ 只能在这里用
**✅ 允许:**
- `ComponentEditorWindow.axaml`
- ComponentEditorWindow 的子编辑页面
**❌ 禁止:**
- MainWindow
- SettingsWindow
- NotificationWindow
- 任何桌面组件Widget
- 任何其他地方
### 8.2 如何在 ComponentEditorWindow 中启用
```xml
<Window ...>
<Window.Resources>
<!-- MD3 色板(仅此窗口有效)-->
<SolidColorBrush x:Key="EditorPrimaryBrush" Color="#FF6750A4" />
<SolidColorBrush x:Key="EditorSurfaceContainerBrush" Color="#FFF3EDF7" />
<SolidColorBrush x:Key="EditorOnPrimaryBrush" Color="#FFFFFFFF" />
</Window.Resources>
<Window.Styles>
<!-- 加载 Material 主题(只影响此窗口)-->
<themes:CustomMaterialTheme BaseTheme="Light"
PrimaryColor="#6750A4"
SecondaryColor="#625B71" />
<!-- 项目自定义覆盖 -->
<StyleInclude Source="avares://LanMountainDesktop/Styles/ComponentEditorThemeResources.axaml" />
</Window.Styles>
</Window>
```
### 8.3 MD3 组件示例
#### FAB 按钮(浮动操作按钮)
```xml
<Button x:Name="SaveFAB"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Margin="28"
Width="64" Height="64"
Background="{DynamicResource EditorPrimaryBrush}"
Foreground="{DynamicResource EditorOnPrimaryBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
Click="OnSaveClick">
<mi:MaterialIcon Kind="Check" Width="32" Height="32" />
</Button>
```
#### Top App Bar顶栏
```xml
<Border Background="{DynamicResource EditorTopAppBarBackgroundBrush}"
Padding="24,16">
<Grid ColumnDefinitions="Auto,*,Auto">
<mi:MaterialIcon Kind="Widgets"
Width="28" Height="28"
Foreground="{DynamicResource EditorPrimaryBrush}" />
<TextBlock Grid.Column="1"
FontSize="20" FontWeight="SemiBold"
Text="组件编辑器"
Margin="16,0,0,0" />
<Button Grid.Column="2" Click="OnCloseClick">
<mi:MaterialIcon Kind="Close" Width="24" Height="24" />
</Button>
</Grid>
</Border>
```
---
## 9. 实战代码模板
### 模板 1新建设置页面
文件位置:`Views/Settings/YourPageName.axaml`
```xml
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
xmlns:local="using:LanMountainDesktop.Controls"
x:Class="LanMountainDesktop.Views.Settings.YourPageName">
<ScrollViewer Padding="24,0,24,24">
<StackPanel Spacing="24">
<!-- 分区卡片 -->
<Border Classes="settings-section-card">
<StackPanel Spacing="16">
<!-- 分区标题 -->
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="12">
<fi:FluentIcon Icon="YourSectionIcon"
IconVariant="Filled"
Classes="icon-l" />
<TextBlock Grid.Column="1"
Classes="settings-section-title"
Text="分区标题" />
</Grid>
<!-- 选项卡 1 -->
<local:SettingsOptionCard Icon="OptionIcon1"
Title="选项标题"
Title="选项描述">
<local:SettingsOptionCard.ActionContent>
<ToggleSwitch IsChecked="{Binding YourProperty, Mode=TwoWay}" />
</local:SettingsOptionCard.ActionContent>
</local:SettingsOptionCard>
<!-- 选项卡 2 -->
<local:SettingsOptionCard Icon="OptionIcon2"
Title="另一个选项"
Title="描述信息" />
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</UserControl>
```
**嵌套检查:** ScrollViewer(1) > StackPanel(2) > Border(3) > StackPanel(4) > Items(5) ✅ (因为有 ScrollViewer 容器5 层可接受)
### 模板 2新建桌面小组件
文件位置:`Views/Components/YourWidget.axaml`
```xml
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="LanMountainDesktop.Views.Components.YourWidget">
<Border Classes="desktop-widget-root"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
Padding="16,12">
<Grid RowDefinitions="Auto,*" RowSpacing="8">
<!-- 标题区 -->
<TextBlock x:Name="TitleTextBlock"
FontSize="14"
FontWeight="SemiBold" />
<!-- 内容区 -->
<ContentControl Grid.Row="1" />
</Grid>
</Border>
</UserControl>
```
**嵌套检查:** Border(1) > Grid(2) > Elements(3) ✅ 完美!
### 模板 3新建独立窗口
```xml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
x:Class="LanMountainDesktop.Views.YourWindow"
Width="800"
Height="600"
MinWidth="400"
MinHeight="300"
CanResize="True"
SystemDecorations="BorderOnly"
Background="Transparent"
Title="窗口标题">
<Grid RowDefinitions="Auto,*">
<!-- 自定义标题栏 -->
<Border Height="48"
Padding="12,0"
PointerPressed="OnTitleBarPressed">
<Grid ColumnDefinitions="Auto,*,Auto">
<fi:FluentIcon Icon="WindowIcon"
IconVariant="Filled"
Classes="icon-m" />
<TextBlock Grid.Column="1"
Text="{Binding Title}"
VerticalAlignment="Center" />
<Button Grid.Column="2"
Click="OnCloseClick">
<fi:FluentIcon Icon="Dismiss" IconVariant="Regular" />
</Button>
</Grid>
</Border>
<!-- 主内容区 -->
<ContentControl Grid.Row="1"
Content="{Binding Content}" />
</Grid>
</Window>
```
**嵌套检查:** Grid(1) > Border(2) > Grid(3) > Elements(4) ✅
---
## 10. AI 编码检查清单
### 写代码前问自己
- [ ] 这个文件是设置页/主界面?→ 用 Fluent + FluentIcon
- [ ] 这个文件是 ComponentEditorWindow→ 用 Material + MaterialIcon
- [ ] 我用了正确的命名空间吗?(见第 2 节速查表)
- [ ] 图标用了 Classes="icon-s/m/l" 而非硬编码 FontSize 吗?
### 写完代码后检查
- [ ] 数一下最大嵌套深度(见第 4.2 节)
- [ ] 有没有硬编码颜色值?(应该都用 DynamicResource
- [ ] 有没有硬编码 CornerRadius应该用 DesignCornerRadiusXxx
- [ ] 有没有在同一区域混用 FluentIcon 和 MaterialIcon
- [ ] 是不是不小心写了 `<local:SomeWindow>` 在另一个 Window 里?
- [ ] 是不是连续写了 Border > Grid > Border > Grid 可以合并?
### 如果审查别人代码
- [ ] 发现窗口套窗口了吗?
- [ ] 发现超过 4 层的无意义嵌套了吗?(没有注释说明原因的话)
- [ ] 发现 Fluent 和 Material 控件混在同一区域了吗?
- [ ] 发现应该用 DynamicResource 的地方硬编码了吗?
---
## 附录:常见错误快速修复
| 错误现象 | 问题原因 | 修复方法 |
|---------|---------|---------|
| 图标不显示或大小不对 | 用了错误的命名空间或硬编码尺寸 | 改用 `fi:FluentIcon` + `Classes="icon-m"` |
| 圆角在设置里改了但没生效 | 硬编码了 CornerRadius | 改用 `{DynamicResource DesignCornerRadiusComponent}` |
| 深色模式下颜色刺眼 | 硬编码了颜色值 | 改用 `{DynamicResource AdaptiveTextPrimaryBrush}` 等 |
| 设置页风格和其他窗口不一致 | 混用了 Material 控件 | 统一用 FluentAvaloniaUI 控件 |
| 性能差/渲染慢 | 嵌套太深(>6 层)| 扁平化结构,合并多余容器 |
| 弹窗显示位置/大小异常 | 把 Window 当成 UserControl 嵌套了 | 改为代码中 `.Show()` 显示 |
---
**相关文档:**
- [VISUAL_SPEC.md](./VISUAL_SPEC.md) - 视觉规范总纲
- [CORNER_RADIUS_SPEC.md](./CORNER_RADIUS_SPEC.md) - 圆角详细规范
- AGENTS.md - AI 强制规则