更新后端服务UI
客户端重构部分界面UI
This commit is contained in:
@@ -25,6 +25,13 @@ internal static class ModernUi
|
||||
internal static SolidColorBrush Danger { get; } = Brush("#C42B1C");
|
||||
internal static SolidColorBrush Info { get; } = Brush("#0078D4");
|
||||
|
||||
// Hover-specific brushes — updated by SetPalette so they track theme changes.
|
||||
internal static SolidColorBrush HoverSurface { get; } = Brush("#EBEBEB");
|
||||
internal static SolidColorBrush HoverAccentSoft { get; } = Brush("#D0EAFF");
|
||||
internal static SolidColorBrush PrimaryHover { get; } = Brush("#005AA3");
|
||||
internal static SolidColorBrush PrimaryPressed { get; } = Brush("#004F8E");
|
||||
internal static SolidColorBrush Transparent { get; } = Brush("#00FFFFFF");
|
||||
|
||||
internal static void SetPalette(bool dark)
|
||||
{
|
||||
SetBrush(AppBackground, dark ? "#202020" : "#F7F7F7");
|
||||
@@ -41,6 +48,12 @@ internal static class ModernUi
|
||||
SetBrush(Bronze, dark ? "#F7B189" : "#CA5010");
|
||||
SetBrush(Danger, dark ? "#F1707B" : "#C42B1C");
|
||||
SetBrush(Info, dark ? "#60CDFF" : "#0078D4");
|
||||
// Hover brushes
|
||||
SetBrush(HoverSurface, dark ? "#3A3A3A" : "#EBEBEB");
|
||||
SetBrush(HoverAccentSoft, dark ? "#0D4F78" : "#D0EAFF");
|
||||
SetBrush(PrimaryHover, dark ? "#4BB8F0" : "#005AA3");
|
||||
SetBrush(PrimaryPressed, dark ? "#38A8E5" : "#004F8E");
|
||||
// Transparent is always the same — no update needed
|
||||
}
|
||||
|
||||
internal static SolidColorBrush Brush(string hex)
|
||||
@@ -138,16 +151,40 @@ internal static class ModernUi
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the WinUI 3 Button ControlTemplate's PointerOver/Pressed theme resource keys
|
||||
/// at the instance level, so VSM animations still run but with the correct custom colors.
|
||||
/// Call this after constructing a Button whose Background/BorderBrush were set manually.
|
||||
/// </summary>
|
||||
internal static void ApplyHoverResources(
|
||||
Button button,
|
||||
Brush hoverBackground,
|
||||
Brush hoverBorder,
|
||||
Brush? pressedBackground = null,
|
||||
Brush? pressedBorder = null,
|
||||
Brush? hoverForeground = null)
|
||||
{
|
||||
button.Resources["ButtonBackgroundPointerOver"] = hoverBackground;
|
||||
button.Resources["ButtonBorderBrushPointerOver"] = hoverBorder;
|
||||
button.Resources["ButtonBackgroundPressed"] = pressedBackground ?? hoverBackground;
|
||||
button.Resources["ButtonBorderBrushPressed"] = pressedBorder ?? hoverBorder;
|
||||
if (hoverForeground is not null)
|
||||
{
|
||||
button.Resources["ButtonForegroundPointerOver"] = hoverForeground;
|
||||
button.Resources["ButtonForegroundPressed"] = hoverForeground;
|
||||
}
|
||||
}
|
||||
|
||||
internal static Button IconButton(string glyph, string tooltip, Action? click = null)
|
||||
{
|
||||
var button = new Button
|
||||
{
|
||||
Width = 40,
|
||||
Width = 36,
|
||||
Height = 36,
|
||||
Padding = new Thickness(0),
|
||||
CornerRadius = new CornerRadius(6),
|
||||
Background = Brush("#00FFFFFF"),
|
||||
BorderBrush = Brush("#00FFFFFF"),
|
||||
Background = Transparent,
|
||||
BorderBrush = Transparent,
|
||||
Content = new FontIcon
|
||||
{
|
||||
Glyph = glyph,
|
||||
@@ -155,6 +192,7 @@ internal static class ModernUi
|
||||
Foreground = TextSecondary
|
||||
}
|
||||
};
|
||||
ApplyHoverResources(button, SurfaceAlt, Stroke, HoverSurface, StrokeStrong);
|
||||
ToolTipService.SetToolTip(button, tooltip);
|
||||
AutomationProperties.SetName(button, tooltip);
|
||||
if (click is not null)
|
||||
@@ -167,6 +205,7 @@ internal static class ModernUi
|
||||
|
||||
internal static Button PillButton(string text, string glyph, Action? click = null, bool primary = false)
|
||||
{
|
||||
var fgBrush = primary ? Brush("#FFFFFFFF") : TextPrimary;
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
@@ -175,8 +214,8 @@ internal static class ModernUi
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Children =
|
||||
{
|
||||
new FontIcon { Glyph = glyph, FontSize = 15 },
|
||||
Text(text, 14, FontWeights.SemiBold, primary ? Brush("#FFFFFFFF") : TextPrimary)
|
||||
new FontIcon { Glyph = glyph, FontSize = 15, Foreground = fgBrush },
|
||||
Text(text, 14, FontWeights.SemiBold, fgBrush)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -184,12 +223,22 @@ internal static class ModernUi
|
||||
{
|
||||
Padding = new Thickness(14, 8, 14, 8),
|
||||
CornerRadius = new CornerRadius(6),
|
||||
Background = primary ? Accent : Brush("#00FFFFFF"),
|
||||
Foreground = primary ? Brush("#FFFFFFFF") : TextPrimary,
|
||||
Background = primary ? Accent : Transparent,
|
||||
Foreground = fgBrush,
|
||||
BorderBrush = primary ? Accent : Stroke,
|
||||
BorderThickness = new Thickness(1),
|
||||
Content = panel
|
||||
};
|
||||
|
||||
if (primary)
|
||||
{
|
||||
ApplyHoverResources(button, PrimaryHover, PrimaryHover, PrimaryPressed, PrimaryPressed, Brush("#FFFFFFFF"));
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyHoverResources(button, SurfaceAlt, StrokeStrong, HoverSurface, StrokeStrong);
|
||||
}
|
||||
|
||||
ToolTipService.SetToolTip(button, text);
|
||||
AutomationProperties.SetName(button, text);
|
||||
if (click is not null)
|
||||
|
||||
@@ -208,6 +208,7 @@ public sealed class HomePage : Page
|
||||
BorderThickness = new Thickness(1),
|
||||
Content = new Border { Padding = new Thickness(16, 12, 16, 12), Child = grid }
|
||||
};
|
||||
ModernUi.ApplyHoverResources(button, ModernUi.SurfaceAlt, ModernUi.StrokeStrong, ModernUi.HoverSurface, ModernUi.StrokeStrong);
|
||||
button.Click += async (_, _) => await ShowAnnouncementDialogAsync();
|
||||
ToolTipService.SetToolTip(button, AppLocalizer.T("查看完整公告", "View full announcement"));
|
||||
return button;
|
||||
@@ -663,6 +664,7 @@ public sealed class HomePage : Page
|
||||
BorderThickness = new Thickness(1),
|
||||
Content = grid
|
||||
};
|
||||
ModernUi.ApplyHoverResources(button, ModernUi.HoverSurface, ModernUi.StrokeStrong, ModernUi.Stroke, ModernUi.StrokeStrong);
|
||||
ToolTipService.SetToolTip(button, $"{ToolText.Name(module)}\n{ToolText.Description(module)}");
|
||||
button.Click += (_, _) => _openTool(module);
|
||||
return button;
|
||||
@@ -707,6 +709,7 @@ public sealed class HomePage : Page
|
||||
BorderThickness = new Thickness(1),
|
||||
Content = grid
|
||||
};
|
||||
ModernUi.ApplyHoverResources(button, ModernUi.HoverSurface, ModernUi.StrokeStrong, ModernUi.Stroke, ModernUi.StrokeStrong);
|
||||
ToolTipService.SetToolTip(button, label);
|
||||
button.Click += (_, _) => _openToolbox(label);
|
||||
return button;
|
||||
|
||||
@@ -293,13 +293,14 @@ public sealed class SettingsPage : Page
|
||||
},
|
||||
new StackPanel
|
||||
{
|
||||
Spacing = 10,
|
||||
Spacing = 8,
|
||||
Children =
|
||||
{
|
||||
_cpuStrip,
|
||||
_memoryStrip,
|
||||
BuildControlStatusLine("\uE950", AppLocalizer.T("处理器", "Processor"), _controlProcessorStatus),
|
||||
BuildControlStatusLine("\uE823", AppLocalizer.T("运行", "Uptime"), _controlUptimeStatus)
|
||||
new Border { Height = 1, Background = ModernUi.Stroke, Margin = new Thickness(0, 2, 0, 2) },
|
||||
BuildControlStatusLine("", AppLocalizer.T("处理器", "Processor"), _controlProcessorStatus),
|
||||
BuildControlStatusLine("", AppLocalizer.T("运行", "Uptime"), _controlUptimeStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -535,12 +536,13 @@ public sealed class SettingsPage : Page
|
||||
MinHeight = 38,
|
||||
Padding = new Thickness(7, 4, 7, 4),
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Background = ModernUi.Brush("#00FFFFFF"),
|
||||
BorderBrush = ModernUi.Brush("#00FFFFFF"),
|
||||
Background = ModernUi.Transparent,
|
||||
BorderBrush = ModernUi.Transparent,
|
||||
BorderThickness = new Thickness(1),
|
||||
Content = grid,
|
||||
Tag = key
|
||||
};
|
||||
ModernUi.ApplyHoverResources(button, ModernUi.SurfaceAlt, ModernUi.Stroke, ModernUi.HoverSurface, ModernUi.StrokeStrong);
|
||||
button.Click += (_, _) => ShowSettingsPage(key);
|
||||
AutomationProperties.SetName(button, title);
|
||||
return button;
|
||||
@@ -563,8 +565,15 @@ public sealed class SettingsPage : Page
|
||||
foreach (var pair in _settingsPageButtons)
|
||||
{
|
||||
var selected = string.Equals(pair.Key, _activeSettingsPage, StringComparison.OrdinalIgnoreCase);
|
||||
pair.Value.Background = selected ? ModernUi.AccentSoft : ModernUi.Brush("#00FFFFFF");
|
||||
pair.Value.BorderBrush = selected ? ModernUi.Accent : ModernUi.Brush("#00FFFFFF");
|
||||
pair.Value.Background = selected ? ModernUi.AccentSoft : ModernUi.Transparent;
|
||||
pair.Value.BorderBrush = selected ? ModernUi.Accent : ModernUi.Transparent;
|
||||
// Keep hover resources in sync with selected state
|
||||
ModernUi.ApplyHoverResources(
|
||||
pair.Value,
|
||||
selected ? ModernUi.HoverAccentSoft : ModernUi.SurfaceAlt,
|
||||
selected ? ModernUi.Accent : ModernUi.Stroke,
|
||||
selected ? ModernUi.HoverAccentSoft : ModernUi.HoverSurface,
|
||||
selected ? ModernUi.Accent : ModernUi.StrokeStrong);
|
||||
if (pair.Value.Content is Grid grid && grid.Children.FirstOrDefault() is Border iconTile)
|
||||
{
|
||||
iconTile.Background = selected ? ModernUi.AccentSoft : ModernUi.SurfaceAlt;
|
||||
@@ -902,19 +911,27 @@ public sealed class SettingsPage : Page
|
||||
private UIElement BuildActionRow(string glyph, string title, UIElement value, Func<Task> action)
|
||||
{
|
||||
var row = BaseRow(glyph, title, value);
|
||||
var chevron = new FontIcon { Glyph = "", FontSize = 14, Foreground = ModernUi.TextSecondary, VerticalAlignment = VerticalAlignment.Center };
|
||||
row.Children.Add(chevron);
|
||||
Grid.SetColumn(chevron, 2);
|
||||
|
||||
var tooltip = AppLocalizer.T($"打开 {title}", $"Open {title}");
|
||||
var button = ModernUi.IconButton("\uE974", tooltip, async () => await action());
|
||||
button.Width = 42;
|
||||
button.Height = 38;
|
||||
button.MinWidth = 42;
|
||||
button.MinHeight = 38;
|
||||
button.VerticalAlignment = VerticalAlignment.Center;
|
||||
button.HorizontalAlignment = HorizontalAlignment.Right;
|
||||
ToolTipService.SetToolTip(button, tooltip);
|
||||
AutomationProperties.SetName(button, tooltip);
|
||||
row.Children.Add(button);
|
||||
Grid.SetColumn(button, 2);
|
||||
return row;
|
||||
var rowButton = new Button
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
HorizontalContentAlignment = HorizontalAlignment.Stretch,
|
||||
Padding = new Thickness(0),
|
||||
CornerRadius = new CornerRadius(6),
|
||||
Background = ModernUi.Transparent,
|
||||
BorderBrush = ModernUi.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
Content = row
|
||||
};
|
||||
ModernUi.ApplyHoverResources(rowButton, ModernUi.SurfaceAlt, ModernUi.Transparent, ModernUi.HoverSurface, ModernUi.Transparent);
|
||||
rowButton.Click += async (_, _) => await action();
|
||||
ToolTipService.SetToolTip(rowButton, tooltip);
|
||||
AutomationProperties.SetName(rowButton, tooltip);
|
||||
return rowButton;
|
||||
}
|
||||
|
||||
private UIElement BuildToggleRow(string glyph, string title, string subtitle, ToggleSwitch toggle)
|
||||
|
||||
@@ -342,13 +342,13 @@ public sealed class ToolboxPage : Page
|
||||
|
||||
var grid = new Grid { ColumnSpacing = 20, RowSpacing = 12 };
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition());
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(0.72, GridUnitType.Star) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(0.85, GridUnitType.Star) });
|
||||
grid.Children.Add(title);
|
||||
Grid.SetColumn(right, 1);
|
||||
grid.Children.Add(right);
|
||||
grid.SizeChanged += (_, e) =>
|
||||
{
|
||||
var narrow = e.NewSize.Width < 940;
|
||||
var narrow = e.NewSize.Width < 860;
|
||||
grid.ColumnDefinitions.Clear();
|
||||
grid.RowDefinitions.Clear();
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition());
|
||||
@@ -361,7 +361,7 @@ public sealed class ToolboxPage : Page
|
||||
}
|
||||
else
|
||||
{
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(0.72, GridUnitType.Star) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(0.85, GridUnitType.Star) });
|
||||
Grid.SetColumn(right, 1);
|
||||
Grid.SetRow(right, 0);
|
||||
}
|
||||
@@ -708,6 +708,12 @@ public sealed class ToolboxPage : Page
|
||||
count,
|
||||
ToolText.CategoryGlyph(category))
|
||||
};
|
||||
ModernUi.ApplyHoverResources(
|
||||
button,
|
||||
selected ? ModernUi.HoverAccentSoft : ModernUi.HoverSurface,
|
||||
selected ? ModernUi.Accent : ModernUi.StrokeStrong,
|
||||
selected ? ModernUi.HoverAccentSoft : ModernUi.SurfaceAlt,
|
||||
selected ? ModernUi.Accent : ModernUi.StrokeStrong);
|
||||
|
||||
button.Click += (_, _) =>
|
||||
{
|
||||
@@ -780,6 +786,12 @@ public sealed class ToolboxPage : Page
|
||||
}
|
||||
}
|
||||
};
|
||||
ModernUi.ApplyHoverResources(
|
||||
button,
|
||||
selected ? ModernUi.HoverAccentSoft : ModernUi.HoverSurface,
|
||||
selected ? ModernUi.Accent : ModernUi.StrokeStrong,
|
||||
selected ? ModernUi.HoverAccentSoft : ModernUi.SurfaceAlt,
|
||||
selected ? ModernUi.Accent : ModernUi.StrokeStrong);
|
||||
button.Click += async (_, _) =>
|
||||
{
|
||||
_selectedScope = scope;
|
||||
@@ -942,11 +954,13 @@ public sealed class ToolboxPage : Page
|
||||
HorizontalContentAlignment = HorizontalAlignment.Stretch,
|
||||
VerticalContentAlignment = VerticalAlignment.Stretch,
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Background = ModernUi.Brush("#00FFFFFF"),
|
||||
BorderBrush = ModernUi.Brush("#00FFFFFF"),
|
||||
Background = ModernUi.Transparent,
|
||||
BorderBrush = ModernUi.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
ContextFlyout = BuildToolFlyout(module, favorite)
|
||||
};
|
||||
// Suppress the built-in template hover overlay on the transparent inner button
|
||||
ModernUi.ApplyHoverResources(button, ModernUi.Transparent, ModernUi.Transparent, ModernUi.Transparent, ModernUi.Transparent);
|
||||
grid.IsHitTestVisible = false;
|
||||
ToolTipService.SetToolTip(button, $"{viewModel.Name}\n{viewModel.Description}");
|
||||
AutomationProperties.SetName(button, viewModel.Name);
|
||||
@@ -958,14 +972,20 @@ public sealed class ToolboxPage : Page
|
||||
favoriteButton.VerticalAlignment = VerticalAlignment.Top;
|
||||
favoriteButton.Margin = new Thickness(0, 12, 12, 0);
|
||||
Canvas.SetZIndex(favoriteButton, 2);
|
||||
return new Border
|
||||
|
||||
var normalBg = ModernUi.Surface;
|
||||
var normalBorder = favorite ? ModernUi.Bronze : IsRisky(module) ? ModernUi.Danger : ModernUi.Stroke;
|
||||
var hoverBg = ModernUi.HoverSurface;
|
||||
var hoverBorder = favorite ? ModernUi.Bronze : IsRisky(module) ? ModernUi.Danger : ModernUi.StrokeStrong;
|
||||
|
||||
var outerBorder = new Border
|
||||
{
|
||||
Width = buttonWidth,
|
||||
Height = compact ? 170 : 236,
|
||||
Margin = new Thickness(7),
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Background = ModernUi.Surface,
|
||||
BorderBrush = favorite ? ModernUi.Bronze : IsRisky(module) ? ModernUi.Danger : ModernUi.Stroke,
|
||||
Background = normalBg,
|
||||
BorderBrush = normalBorder,
|
||||
BorderThickness = new Thickness(1),
|
||||
Child = new Grid
|
||||
{
|
||||
@@ -973,6 +993,17 @@ public sealed class ToolboxPage : Page
|
||||
},
|
||||
ContextFlyout = BuildToolFlyout(module, favorite)
|
||||
};
|
||||
outerBorder.PointerEntered += (_, _) =>
|
||||
{
|
||||
outerBorder.Background = hoverBg;
|
||||
outerBorder.BorderBrush = hoverBorder;
|
||||
};
|
||||
outerBorder.PointerExited += (_, _) =>
|
||||
{
|
||||
outerBorder.Background = normalBg;
|
||||
outerBorder.BorderBrush = normalBorder;
|
||||
};
|
||||
return outerBorder;
|
||||
}
|
||||
|
||||
private MenuFlyout BuildToolFlyout(IToolModule module, bool favorite)
|
||||
|
||||
@@ -206,6 +206,12 @@ public abstract partial class AdaptiveToolPage
|
||||
|
||||
private Border BuildCompactGalleryInputCard(IToolModule module)
|
||||
{
|
||||
// Auto-load tools with no user input: collapse to a minimal reload card
|
||||
if (_spec.AutoRunOnOpen && _spec.PrimaryInput is ToolPrimaryInputKind.None)
|
||||
{
|
||||
return BuildAutoLoadInputCard(module);
|
||||
}
|
||||
|
||||
var panel = new StackPanel { Spacing = _spec.PageDensity == ToolPageDensity.Compact ? 10 : 14 };
|
||||
panel.Children.Add(BuildGallerySectionHeader(
|
||||
AppLocalizer.T("输入", "Input"),
|
||||
@@ -660,4 +666,59 @@ public abstract partial class AdaptiveToolPage
|
||||
_ => AppLocalizer.T("文本", "Text")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimal input card for auto-load tools that have no user-supplied input.
|
||||
/// Shows only the tool description, a reload button, and a cancel button.
|
||||
/// </summary>
|
||||
private Border BuildAutoLoadInputCard(IToolModule module)
|
||||
{
|
||||
var reloadButton = ModernUi.PillButton(
|
||||
AppLocalizer.T("重新加载", "Reload"),
|
||||
"",
|
||||
async () => await RunCurrentToolAsync(),
|
||||
primary: true);
|
||||
|
||||
var cancelButton = ModernUi.PillButton(
|
||||
AppLocalizer.T("取消", "Cancel"),
|
||||
"",
|
||||
() => CancelRunningTool());
|
||||
|
||||
var actions = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Spacing = 10,
|
||||
Children = { reloadButton, cancelButton }
|
||||
};
|
||||
|
||||
var grid = new Grid { ColumnSpacing = 12 };
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition());
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
grid.Children.Add(ModernUi.IconTile(module.Metadata.IconGlyph, 38, ModernUi.AccentSoft, ModernUi.Accent, 17));
|
||||
|
||||
var text = new StackPanel
|
||||
{
|
||||
Spacing = 2,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Children =
|
||||
{
|
||||
ModernUi.Text(AppLocalizer.T("数据源", "Data source"), 11, FontWeights.SemiBold, ModernUi.Accent, maxLines: 1),
|
||||
ModernUi.Text(AppLocalizer.T("自动加载", "Auto load"), 14, FontWeights.SemiBold, maxLines: 1),
|
||||
ModernUi.Text(CompactInputDescription(module), 12, foreground: ModernUi.TextSecondary, maxLines: 2)
|
||||
}
|
||||
};
|
||||
Grid.SetColumn(text, 1);
|
||||
grid.Children.Add(text);
|
||||
|
||||
Grid.SetColumn(_progressRing, 2);
|
||||
grid.Children.Add(_progressRing);
|
||||
|
||||
return ModernUi.Card(new StackPanel
|
||||
{
|
||||
Spacing = 12,
|
||||
Children = { grid, actions }
|
||||
}, new Thickness(14), radius: 8);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,18 @@ public abstract partial class AdaptiveToolPage
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ranked list / news / hotboard tools get a dedicated renderer before generic JSON
|
||||
var rankedListToolIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"movie_box_office", "baidu_hot", "bili_hot", "zhihu_hot", "weibo_hot",
|
||||
"cctv_news", "tech_news", "football_news", "history_today",
|
||||
"earthquake_info", "gold_price", "oil_price", "hotboard"
|
||||
};
|
||||
if (rankedListToolIds.Contains(module.Id) || _spec.Result == ToolResultKind.RankedList || _spec.Result == ToolResultKind.NewsCards)
|
||||
{
|
||||
if (TryRenderRankedList(module, output, lines)) return true;
|
||||
}
|
||||
|
||||
if (TryRenderJson(output, module))
|
||||
{
|
||||
return true;
|
||||
@@ -637,4 +649,194 @@ public abstract partial class AdaptiveToolPage
|
||||
_ => "结果摘要"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders ranked/hotboard/news results with a dedicated rank-badge card layout.
|
||||
/// Handles both JSON array output and numbered-text-line output (e.g. "1. Title · meta").
|
||||
/// </summary>
|
||||
private bool TryRenderRankedList(IToolModule module, string output, IReadOnlyList<string> lines)
|
||||
{
|
||||
// Try JSON array first
|
||||
var trimmed = output.TrimStart();
|
||||
if (trimmed.StartsWith('['))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(output);
|
||||
if (document.RootElement.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var items = document.RootElement.EnumerateArray().Take(50).ToList();
|
||||
if (items.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_resultCards.Children.Add(BuildInfoGrid([
|
||||
(AppLocalizer.T("条目数", "Items"), items.Count.ToString()),
|
||||
(AppLocalizer.T("类型", "Kind"), AppLocalizer.T("榜单 / 资讯", "Ranked list"))
|
||||
]));
|
||||
|
||||
for (var index = 0; index < items.Count; index++)
|
||||
{
|
||||
var item = items[index];
|
||||
var rank = index + 1;
|
||||
var title = FirstJsonString(item, "title", "name", "text", "subject", "headline") ?? $"#{rank}";
|
||||
var meta = FirstJsonString(item, "score", "hot", "value", "heat", "count", "view", "desc", "description", "meta", "category", "type");
|
||||
var url = FirstJsonString(item, "url", "link", "href");
|
||||
|
||||
// Some APIs return an explicit rank field
|
||||
if (item.TryGetProperty("rank", out var rankElem) && rankElem.TryGetInt32(out var explicitRank))
|
||||
{
|
||||
rank = explicitRank;
|
||||
}
|
||||
|
||||
_resultCards.Children.Add(BuildRankedItemCard(rank, title, meta, url));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fall through to text parsing
|
||||
}
|
||||
}
|
||||
|
||||
// Try numbered text lines: "1. Title · meta", "1、Title", "#1 Title"
|
||||
var rankedLines = lines
|
||||
.Select(ParseRankedLine)
|
||||
.Where(item => item.HasValue)
|
||||
.Select(item => item!.Value)
|
||||
.Take(50)
|
||||
.ToList();
|
||||
|
||||
if (rankedLines.Count < 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_resultCards.Children.Add(BuildInfoGrid([
|
||||
(AppLocalizer.T("条目数", "Items"), rankedLines.Count.ToString()),
|
||||
(AppLocalizer.T("来源", "Source"), AppLocalizer.T("实时榜单", "Live ranking"))
|
||||
]));
|
||||
|
||||
foreach (var item in rankedLines)
|
||||
{
|
||||
_resultCards.Children.Add(BuildRankedItemCard(item.Rank, item.Title, item.Meta, null));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static (int Rank, string Title, string? Meta)? ParseRankedLine(string line)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line)) return null;
|
||||
|
||||
// Formats: "1. Title · meta", "1. Title", "#1 Title", "1、Title"
|
||||
var match = Regex.Match(line.Trim(), @"^(?:#?(\d+)[.、\s]+|(\d+)\.\s*)(.+)$");
|
||||
if (!match.Success) return null;
|
||||
|
||||
var rankStr = match.Groups[1].Value.Length > 0 ? match.Groups[1].Value : match.Groups[2].Value;
|
||||
if (!int.TryParse(rankStr, out var rank)) return null;
|
||||
|
||||
var rest = match.Groups[3].Value.Trim();
|
||||
string? meta = null;
|
||||
string title = rest;
|
||||
|
||||
// Split "Title · meta" or "Title - meta" or "Title | meta"
|
||||
var sep = rest.IndexOfAny([' ']);
|
||||
var dotSep = rest.IndexOf(" · ", StringComparison.Ordinal);
|
||||
var dashSep = rest.IndexOf(" - ", StringComparison.Ordinal);
|
||||
var pipeSep = rest.IndexOf(" | ", StringComparison.Ordinal);
|
||||
var tabSep = rest.IndexOf('\t');
|
||||
|
||||
var splitAt = new[] { dotSep, dashSep, pipeSep, tabSep }.Where(pos => pos > 0).OrderBy(pos => pos).FirstOrDefault(-1);
|
||||
if (splitAt > 0)
|
||||
{
|
||||
title = rest[..splitAt].Trim();
|
||||
meta = rest[(splitAt + 1)..].TrimStart(' ', '-', '·', '|', '\t').Trim();
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(title) ? null : (rank, title, string.IsNullOrWhiteSpace(meta) ? null : meta);
|
||||
}
|
||||
|
||||
private static string? FirstJsonString(JsonElement element, params string[] keys)
|
||||
{
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (element.TryGetProperty(key, out var prop) && prop.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var value = prop.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(value)) return value;
|
||||
}
|
||||
|
||||
// Also try camelCase variants with number suffixes
|
||||
if (element.TryGetProperty(key.ToLowerInvariant(), out var lowerProp) && lowerProp.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var value = lowerProp.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(value)) return value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Border BuildRankedItemCard(int rank, string title, string? meta, string? url)
|
||||
{
|
||||
// Rank badge color: gold/silver/bronze for top 3, default for rest
|
||||
var rankFg = rank switch
|
||||
{
|
||||
1 => ModernUi.Brush("#CA5010"), // gold-ish / bronze
|
||||
2 => ModernUi.TextSecondary,
|
||||
3 => ModernUi.TextSecondary,
|
||||
_ => ModernUi.TextSecondary
|
||||
};
|
||||
var rankBg = rank switch
|
||||
{
|
||||
1 => ModernUi.AccentSoft,
|
||||
2 => ModernUi.SurfaceAlt,
|
||||
3 => ModernUi.SurfaceAlt,
|
||||
_ => ModernUi.SurfaceAlt
|
||||
};
|
||||
|
||||
var rankBadge = ModernUi.Badge(rank.ToString(), rank == 1 ? ModernUi.Accent : rankFg, rankBg);
|
||||
rankBadge.MinWidth = 32;
|
||||
rankBadge.HorizontalAlignment = HorizontalAlignment.Center;
|
||||
|
||||
var text = new StackPanel
|
||||
{
|
||||
Spacing = 2,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Children =
|
||||
{
|
||||
ModernUi.Text(ModernUi.Ellipsize(title, 100), 14, FontWeights.SemiBold, maxLines: 1),
|
||||
}
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(meta))
|
||||
{
|
||||
text.Children.Add(ModernUi.Text(ModernUi.Ellipsize(meta, 80), 12, foreground: ModernUi.TextSecondary, maxLines: 1));
|
||||
}
|
||||
|
||||
var row = new Grid { ColumnSpacing = 12, MinHeight = 40 };
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(36) });
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition());
|
||||
|
||||
row.Children.Add(rankBadge);
|
||||
Grid.SetColumn(text, 1);
|
||||
row.Children.Add(text);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
var linkButton = ModernUi.IconButton("", AppLocalizer.T("打开链接", "Open link"),
|
||||
async () => await Windows.System.Launcher.LaunchUriAsync(new Uri(url!)));
|
||||
Grid.SetColumn(linkButton, 2);
|
||||
row.Children.Add(linkButton);
|
||||
}
|
||||
|
||||
return ModernUi.Card(row, new Thickness(10, 8, 10, 8), radius: 8, background: ModernUi.SurfaceAlt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -946,8 +946,8 @@ public abstract partial class AdaptiveToolPage : ToolPageBase
|
||||
var wrap = new VariableSizedWrapGrid
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
ItemWidth = 190,
|
||||
ItemHeight = 74
|
||||
ItemWidth = 200,
|
||||
ItemHeight = 60
|
||||
};
|
||||
wrap.Children.Add(_numberBox);
|
||||
if (_secondaryNumberBox.Visibility == Visibility.Visible)
|
||||
|
||||
Reference in New Issue
Block a user