diff --git a/.gitignore b/.gitignore
index 9420128..2ff28cc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -63,7 +63,8 @@ BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
-artifacts/
+# 仅忽略仓库根目录的构建产物目录,避免误伤源码子目录(如 Agents/MarketAnalysis/Artifacts/)
+/artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
diff --git a/docs/design-system.md b/docs/design-system.md
index e83484f..46ca742 100644
--- a/docs/design-system.md
+++ b/docs/design-system.md
@@ -1,9 +1,17 @@
# MarketAssistant UI 设计系统
-> **版本**: 2.0
+> **版本**: 2.1
> **平台**: Avalonia 11.x 跨平台桌面应用
> **产品**: 金融市场数据终端 (A股 + 虚拟币)
> **设计方向**: 清晰专业风 — Bloomberg Terminal 的严谨 × 现代 SaaS 的优雅
+>
+> **v2.1 变更(与 design-prototype.html v5 的裁决)**:
+> 1. 深色背景以本文档色值为准(`bg-root #0A0E17` / `bg-surface #111722` / `bg-elevated #161C2A`),原型中更深的 `#080C14/#0E1320/#141B2A` 弃用;
+> 2. 导航活跃指示条采用**左侧 3px 竖条**,不采用原型的底部横条;选中项背景使用 `bg-selected` 浅色填充而非整块品牌蓝;
+> 3. 首页不放 K 线区,采用 Bento Grid(热门标的置左最宽 + 快讯 + 最近查看);K 线保留在资产详情页。原型中的"AI 市场概览横幅"待数据源就绪后再引入;
+> 4. 涨跌标签一律带约 12% 透明度背景(`BullishTagBackgroundBrush` 等),靠色块而非纯文字色区分语义;
+> 5. 响应式简化为两档:≥1100px 完整布局,<1100px 单列隐藏详情面板;
+> 6. TopBar 常驻:标题/返回 + A股/虚拟币分段切换器(复用 Ctrl+M 同一逻辑);行情 ticker 需真实指数数据源,无数据时整段隐藏。
---
diff --git a/src/MarketAssistant.Agents/Analysts/AIAgentFailureIsolation.cs b/src/MarketAssistant.Agents/Analysts/AIAgentFailureIsolation.cs
index 3ffb158..4ffb84a 100644
--- a/src/MarketAssistant.Agents/Analysts/AIAgentFailureIsolation.cs
+++ b/src/MarketAssistant.Agents/Analysts/AIAgentFailureIsolation.cs
@@ -1,7 +1,7 @@
-using System.Runtime.CompilerServices;
-using System.Text;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
+using System.Runtime.CompilerServices;
+using System.Text;
namespace MarketAssistant.Agents.Analysts;
diff --git a/src/MarketAssistant.App.Services/Agents/MarketAnalysis/Artifacts/AnalystArtifactStore.cs b/src/MarketAssistant.App.Services/Agents/MarketAnalysis/Artifacts/AnalystArtifactStore.cs
new file mode 100644
index 0000000..19573be
--- /dev/null
+++ b/src/MarketAssistant.App.Services/Agents/MarketAnalysis/Artifacts/AnalystArtifactStore.cs
@@ -0,0 +1,56 @@
+using System.Text;
+
+namespace MarketAssistant.Services.Agents.MarketAnalysis.Artifacts;
+
+///
+/// 分析师产物存储(P1-07):按 Run 隔离保存每位分析师的完整产物,
+/// 协调阶段只传摘要,全文通过工具按需读取。
+///
+public interface IAnalystArtifactStore
+{
+ /// 写入/覆盖指定 Run 下某分析师的产物全文。
+ Task SaveAsync(Guid runId, string analystName, string content, CancellationToken cancellationToken = default);
+
+ /// 读取指定 Run 下某分析师的产物全文;不存在时返回 null。
+ Task GetAsync(Guid runId, string analystName, CancellationToken cancellationToken = default);
+}
+
+///
+/// 基于文件系统的产物存储实现:%APPDATA%/MarketAssistant/analyst-artifacts/{runId}/{analystName}.md
+///
+public sealed class FileAnalystArtifactStore : IAnalystArtifactStore
+{
+ private readonly string _rootDirectory;
+
+ public FileAnalystArtifactStore(string rootDirectory)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(rootDirectory);
+ _rootDirectory = rootDirectory;
+ }
+
+ public async Task SaveAsync(Guid runId, string analystName, string content, CancellationToken cancellationToken = default)
+ {
+ var path = GetPath(runId, analystName);
+ Directory.CreateDirectory(Path.GetDirectoryName(path)!);
+ await File.WriteAllTextAsync(path, content ?? string.Empty, Encoding.UTF8, cancellationToken);
+ }
+
+ public async Task GetAsync(Guid runId, string analystName, CancellationToken cancellationToken = default)
+ {
+ var path = GetPath(runId, analystName);
+ if (!File.Exists(path))
+ return null;
+
+ return await File.ReadAllTextAsync(path, Encoding.UTF8, cancellationToken);
+ }
+
+ private string GetPath(Guid runId, string analystName)
+ {
+ // analystName 为 MAF Agent ASCII Name,仅允许安全字符
+ var safeName = string.Concat(analystName.Where(char.IsLetterOrDigit));
+ if (string.IsNullOrEmpty(safeName))
+ throw new ArgumentException("分析师名称不能为空", nameof(analystName));
+
+ return Path.Combine(_rootDirectory, runId.ToString("N"), $"{safeName}.md");
+ }
+}
\ No newline at end of file
diff --git a/src/MarketAssistant.App.Services/Applications/Assets/AShareAssetInfoService.cs b/src/MarketAssistant.App.Services/Applications/Assets/AShareAssetInfoService.cs
index a360777..a0b2bef 100644
--- a/src/MarketAssistant.App.Services/Applications/Assets/AShareAssetInfoService.cs
+++ b/src/MarketAssistant.App.Services/Applications/Assets/AShareAssetInfoService.cs
@@ -2,7 +2,6 @@
using MarketAssistant.DataProviders.AShare;
using MarketAssistant.Infrastructure.Core;
using Microsoft.Extensions.Logging;
-using System.Globalization;
namespace MarketAssistant.Applications.Assets;
@@ -84,7 +83,7 @@ public async Task GetAssetInfoAsync(string code, string market = "",
}
// 当前价格
- assetInfo.CurrentPrice = data.LastPrice.ToString(CultureInfo.InvariantCulture);
+ assetInfo.CurrentPrice = PriceFormatter.Format(data.LastPrice);
// 涨跌幅(CLS 的 change 为小数比率,如 -0.0082 表示 -0.82%)
var changeRatio = data.Change;
@@ -103,7 +102,7 @@ public async Task> GetHotAssetsAsync()
// HTTP 访问、GBK 解码与解析由 SinaFundFlowClient 负责;此处仅做业务映射。
try
{
- var items = await _sinaFundFlowClient.GetTopNetInflowAsync(8);
+ var items = await _sinaFundFlowClient.GetTopNetInflowAsync(12);
return items.Select(item =>
{
diff --git a/src/MarketAssistant.App.Services/Applications/Assets/CryptoAssetInfoService.cs b/src/MarketAssistant.App.Services/Applications/Assets/CryptoAssetInfoService.cs
index f5a5dd7..3efbe0f 100644
--- a/src/MarketAssistant.App.Services/Applications/Assets/CryptoAssetInfoService.cs
+++ b/src/MarketAssistant.App.Services/Applications/Assets/CryptoAssetInfoService.cs
@@ -82,7 +82,7 @@ public async Task GetAssetInfoAsync(string code, string market = "",
Name = ExtractBaseCurrency(ticker.Symbol), // 提取基础币种
MarketType = MarketType.Crypto,
Market = "Binance",
- CurrentPrice = FormatPrice(ticker.LastPrice),
+ CurrentPrice = PriceFormatter.Format(ticker.LastPrice),
ChangePercentage = FormatPercentage(ticker.PriceChangePercent),
Volume24h = FormatVolume(ticker.Volume),
MarketCap = await FetchMarketCapAsync(ExtractBaseCurrency(ticker.Symbol), cancellationToken)
@@ -109,7 +109,7 @@ public async Task> GetHotAssetsAsync()
// 稳定币列表(用于过滤稳定币互换交易对)
var stablecoins = new[] { "USDT", "USDC", "BUSD", "FDUSD", "DAI", "TUSD", "USDP" };
- // 筛选USDT交易对,排除稳定币互换(需同时满足:基础币种是稳定币 且 价格接近1.0),按24小时交易量排序,取前8个
+ // 筛选USDT交易对,排除稳定币互换(需同时满足:基础币种是稳定币 且 价格接近1.0),按24小时交易量排序,取前12个
var hotAssets = tickers
.Where(t => t.Symbol.EndsWith("USDT") && t.Symbol != "USDT")
// 过滤稳定币互换交易对(基础币种是稳定币 且 价格接近1.0,同时满足才过滤)
@@ -123,13 +123,13 @@ public async Task> GetHotAssetsAsync()
return !(isStablecoin && isPriceNearOne);
})
.OrderByDescending(t => t.QuoteVolume)
- .Take(8)
+ .Take(12)
.Select(t => new HotAsset
{
Name = ExtractBaseCurrency(t.Symbol),
Code = t.Symbol,
Market = "Binance",
- CurrentPrice = FormatPrice(t.LastPrice),
+ CurrentPrice = PriceFormatter.Format(t.LastPrice),
ChangePercentage = FormatOpenClosePercentage(t.OpenPrice, t.LastPrice),
MarketType = MarketType.Crypto,
MetricLabel = "交易量",
@@ -201,26 +201,6 @@ private async Task> GetSymbolsAsync(CancellationToken ca
return tradingSymbols;
}
- ///
- /// 格式化价格显示
- ///
- private string FormatPrice(decimal price)
- {
- // 根据价格大小选择精度
- if (price >= 1000)
- {
- return price.ToString("N2"); // 1000+ 显示2位小数
- }
- else if (price >= 1)
- {
- return price.ToString("N4"); // 1-1000 显示4位小数
- }
- else
- {
- return price.ToString("N6"); // <1 显示6位小数
- }
- }
-
///
/// 格式化百分比显示
///
diff --git a/src/MarketAssistant.App.Services/Applications/Assets/Models/AssetItem.cs b/src/MarketAssistant.App.Services/Applications/Assets/Models/AssetItem.cs
index ad3a4bd..8f3f412 100644
--- a/src/MarketAssistant.App.Services/Applications/Assets/Models/AssetItem.cs
+++ b/src/MarketAssistant.App.Services/Applications/Assets/Models/AssetItem.cs
@@ -15,6 +15,16 @@ public class AssetItem
///
public string Code { get; set; } = string.Empty;
+ ///
+ /// 当前价格(仅用于首页最近查看等场景的展示,历史记录不依赖)
+ ///
+ public string CurrentPrice { get; set; } = string.Empty;
+
+ ///
+ /// 涨跌幅百分比(如 "+1.26%",仅用于展示)
+ ///
+ public string ChangePercentage { get; set; } = string.Empty;
+
public override string ToString() => string.IsNullOrEmpty(Name) ? Code : $"{Name} ({Code})";
}
diff --git a/src/MarketAssistant.App.Services/Applications/Assets/Models/HotAsset.cs b/src/MarketAssistant.App.Services/Applications/Assets/Models/HotAsset.cs
index b457070..a02a212 100644
--- a/src/MarketAssistant.App.Services/Applications/Assets/Models/HotAsset.cs
+++ b/src/MarketAssistant.App.Services/Applications/Assets/Models/HotAsset.cs
@@ -62,6 +62,13 @@ public class HotAsset
///
public string FormattedMetric => FormatMetric(MetricValue);
+ ///
+ /// 行尾标签文本:A 股显示所属板块,加密货币显示核心指标(如"24h量 2.41B")
+ ///
+ public string TagText => MarketType == MarketType.Crypto
+ ? $"{MetricLabel} {FormattedMetric}"
+ : SectorName ?? string.Empty;
+
private static string FormatMetric(string? value)
{
if (string.IsNullOrWhiteSpace(value))
diff --git a/src/MarketAssistant.App.Services/Applications/Assets/PriceFormatter.cs b/src/MarketAssistant.App.Services/Applications/Assets/PriceFormatter.cs
new file mode 100644
index 0000000..3cd13f4
--- /dev/null
+++ b/src/MarketAssistant.App.Services/Applications/Assets/PriceFormatter.cs
@@ -0,0 +1,25 @@
+namespace MarketAssistant.Applications.Assets;
+
+///
+/// 价格展示格式化工具(按价格量级选择小数位),供资产信息服务与实时行情刷新共用。
+///
+public static class PriceFormatter
+{
+ ///
+ /// 按量级格式化价格:千及以上 2 位小数,1 及以上 4 位,小于 1 取 6 位(适配低价币)。
+ ///
+ public static string Format(decimal price)
+ {
+ if (price >= 1000)
+ {
+ return price.ToString("N2");
+ }
+
+ if (price >= 1)
+ {
+ return price.ToString("N4");
+ }
+
+ return price.ToString("N6");
+ }
+}
\ No newline at end of file
diff --git a/src/MarketAssistant.App.Services/Applications/Settings/UserSetting.cs b/src/MarketAssistant.App.Services/Applications/Settings/UserSetting.cs
index c3aa3a6..ffac5b5 100644
--- a/src/MarketAssistant.App.Services/Applications/Settings/UserSetting.cs
+++ b/src/MarketAssistant.App.Services/Applications/Settings/UserSetting.cs
@@ -45,17 +45,6 @@ public bool LoadKnowledge
set => SetProperty(ref _loadKnowledge, value);
}
- private bool _enableExperimentalTrading;
- ///
- /// 是否启用实验性自主交易。默认关闭;仅虚拟币等支持交易的市场生效。
- /// 开启即表示用户理解实盘风险、密钥与不可撤销操作。
- ///
- public bool EnableExperimentalTrading
- {
- get => _enableExperimentalTrading;
- set => SetProperty(ref _enableExperimentalTrading, value);
- }
-
[JsonIgnore]
public const string VectorCollectionName = "knowledge";
diff --git a/src/MarketAssistant.App/Assets/Images/wallet.svg b/src/MarketAssistant.App/Assets/Images/wallet.svg
new file mode 100644
index 0000000..13214bc
--- /dev/null
+++ b/src/MarketAssistant.App/Assets/Images/wallet.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/MarketAssistant.App/Assets/Raw/kline_chart.html b/src/MarketAssistant.App/Assets/Raw/kline_chart.html
index c548fcc..67672f6 100644
--- a/src/MarketAssistant.App/Assets/Raw/kline_chart.html
+++ b/src/MarketAssistant.App/Assets/Raw/kline_chart.html
@@ -5,6 +5,13 @@
股票K线图
-
+
+
+
+
+
+
diff --git a/src/MarketAssistant.App/Resources/Styles/CardStyles.axaml b/src/MarketAssistant.App/Resources/Styles/CardStyles.axaml
index 5d120c9..ebf13e1 100644
--- a/src/MarketAssistant.App/Resources/Styles/CardStyles.axaml
+++ b/src/MarketAssistant.App/Resources/Styles/CardStyles.axaml
@@ -1,6 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
@@ -40,8 +68,8 @@
-
-
+
+
@@ -52,22 +80,22 @@
diff --git a/src/MarketAssistant.App/Resources/Styles/Colors.axaml b/src/MarketAssistant.App/Resources/Styles/Colors.axaml
index a2c9407..f50d515 100644
--- a/src/MarketAssistant.App/Resources/Styles/Colors.axaml
+++ b/src/MarketAssistant.App/Resources/Styles/Colors.axaml
@@ -33,12 +33,12 @@
-
- #FAFAFA
+
+ #F5F6F8
#FFFFFF
#FFFFFF
- #EEEEEE
+ #F0F1F3
#212121
@@ -46,48 +46,79 @@
#BDBDBD
- #E0E0E0
- #EEEEEE
+ #E0E2E8
+ #EEF0F4
+ #EEF0F4
#F5F5F5
#EEEEEE
#E3F2FD
+ #1565C0
#40000000
#FEF2F2
+
+
+ #ECFDF5
+ #10B981
+ #065F46
+ #FFFBEB
+ #F59E0B
+ #92400E
+ #F0F9FF
+ #3B82F6
+ #1E40AF
+ #FEE2E2
+ #EF4444
+ #991B1B
-
- #121212
- #1E1E1E
+
+ #0A0E17
+ #111722
- #1E1E1E
- #383838
+ #111722
+ #1B2233
-
- #FFFFFF
- #B3B3B3
- #666666
+
+ #E4EAF5
+ #8894A8
+ #5A6680
- #3A3A3A
- #2C2C2C
+ #1E2636
+ #182032
- #2C2C2C
- #383838
- #1E3A5F
+ #161C2A
+ #1B2233
+ #262D3D
+ #42A5F5
#80000000
#2D1B1B
+
+
+ #1E10B981
+ #10B981
+ #34D399
+ #1EF59E0B
+ #F59E0B
+ #FBBF24
+ #1E3B82F6
+ #3B82F6
+ #93C5FD
+ #1EEF4444
+ #EF4444
+ #FCA5A5
@@ -121,9 +152,8 @@
-
+
-
diff --git a/src/MarketAssistant.App/Resources/Styles/Spacing.axaml b/src/MarketAssistant.App/Resources/Styles/Spacing.axaml
index 11bcb74..48f3563 100644
--- a/src/MarketAssistant.App/Resources/Styles/Spacing.axaml
+++ b/src/MarketAssistant.App/Resources/Styles/Spacing.axaml
@@ -54,6 +54,9 @@
12
11
10
+
+
+ JetBrains Mono, SF Mono, Consolas, Cascadia Mono
diff --git a/src/MarketAssistant.App/Resources/Styles/TextStyles.axaml b/src/MarketAssistant.App/Resources/Styles/TextStyles.axaml
index bdd3624..c7de234 100644
--- a/src/MarketAssistant.App/Resources/Styles/TextStyles.axaml
+++ b/src/MarketAssistant.App/Resources/Styles/TextStyles.axaml
@@ -109,9 +109,11 @@
+
diff --git a/src/MarketAssistant.App/ViewModels/AboutPageViewModel.cs b/src/MarketAssistant.App/ViewModels/AboutPageViewModel.cs
index 72bcc32..4bcd558 100644
--- a/src/MarketAssistant.App/ViewModels/AboutPageViewModel.cs
+++ b/src/MarketAssistant.App/ViewModels/AboutPageViewModel.cs
@@ -299,6 +299,7 @@ private void InitializeFeatureItems()
{
IconSource = "/Assets/Images/refresh.svg",
Title = "更新日志",
+ Description = "了解每个版本的功能更新与问题修复",
ButtonText = "查看",
Command = new RelayCommand(() => OpenUrl(AppInfo.ChangelogUrl))
});
@@ -307,6 +308,7 @@ private void InitializeFeatureItems()
{
IconSource = "/Assets/Images/globe.svg",
Title = "官方网站",
+ Description = "访问项目主页,获取最新动态与文档",
ButtonText = "查看",
Command = new RelayCommand(() => OpenUrl(AppInfo.OfficialWebsite))
});
@@ -315,6 +317,7 @@ private void InitializeFeatureItems()
{
IconSource = "/Assets/Images/feedback.svg",
Title = "意见反馈",
+ Description = "提交遇到的问题或功能建议",
ButtonText = "反馈",
Command = new RelayCommand(() => OpenUrl(AppInfo.FeedbackUrl))
});
@@ -323,6 +326,7 @@ private void InitializeFeatureItems()
{
IconSource = "/Assets/Images/license.svg",
Title = "许可证",
+ Description = "查看本应用的开源许可条款",
ButtonText = "查看",
Command = new RelayCommand(() => OpenUrl(AppInfo.LicenseUrl))
});
@@ -331,6 +335,7 @@ private void InitializeFeatureItems()
{
IconSource = "/Assets/Images/qq.svg",
Title = $"官方QQ群: {AppInfo.QQGroupNumber}",
+ Description = "加入社区,与其他用户交流使用心得",
ButtonText = "加入",
Command = new RelayCommand(() => OpenUrl(AppInfo.QQGroupUrl))
});
@@ -349,6 +354,11 @@ public class FeatureItem
///
public string Title { get; set; } = "";
+ ///
+ /// 功能项描述
+ ///
+ public string Description { get; set; } = "";
+
///
/// 按钮文本
///
diff --git a/src/MarketAssistant.App/ViewModels/AssetPageViewModel.cs b/src/MarketAssistant.App/ViewModels/AssetPageViewModel.cs
index 7358254..455b91e 100644
--- a/src/MarketAssistant.App/ViewModels/AssetPageViewModel.cs
+++ b/src/MarketAssistant.App/ViewModels/AssetPageViewModel.cs
@@ -3,6 +3,7 @@
using CommunityToolkit.Mvvm.Messaging;
using MarketAssistant.Applications.Charts;
using MarketAssistant.Applications.Charts.Models;
+using MarketAssistant.Applications.Assets;
using MarketAssistant.Infrastructure;
using MarketAssistant.Infrastructure.Core;
using MarketAssistant.DataProviders;
@@ -55,6 +56,16 @@ public partial class AssetPageViewModel : ViewModelBase, INavigationAware当前价展示文本(按量级格式化,适配低价币)
+ public string CurrentPriceText => PriceFormatter.Format(CurrentPrice);
+
+ /// 涨跌额展示文本(按量级格式化,适配低价币)
+ public string PriceChangeText => PriceFormatter.Format(PriceChange);
+
+ partial void OnCurrentPriceChanged(decimal value) => OnPropertyChanged(nameof(CurrentPriceText));
+
+ partial void OnPriceChangeChanged(decimal value) => OnPropertyChanged(nameof(PriceChangeText));
+
///
/// 计算属性用于UI绑定
///
diff --git a/src/MarketAssistant.App/ViewModels/AssetSelectionPageViewModel.cs b/src/MarketAssistant.App/ViewModels/AssetSelectionPageViewModel.cs
index 560a7d5..bc88671 100644
--- a/src/MarketAssistant.App/ViewModels/AssetSelectionPageViewModel.cs
+++ b/src/MarketAssistant.App/ViewModels/AssetSelectionPageViewModel.cs
@@ -140,14 +140,21 @@ public string CurrentInputContent
};
///
- /// 当前按钮文本
+ /// 当前按钮文本(随模式与市场类型切换,如"开始选股"/"开始选币")
///
- public string CurrentButtonText => SelectedMode?.ModeType switch
+ public string CurrentButtonText
{
- SelectionModeType.UserRequirement => "开始选股",
- SelectionModeType.NewsAnalysis => "基于新闻选股",
- _ => "开始分析"
- };
+ get
+ {
+ var noun = _marketContext.CurrentMarket == MarketType.Crypto ? "选币" : "选股";
+ return SelectedMode?.ModeType switch
+ {
+ SelectionModeType.UserRequirement => $"开始{noun}",
+ SelectionModeType.NewsAnalysis => $"基于新闻{noun}",
+ _ => "开始分析"
+ };
+ }
+ }
///
/// 推荐投资标的列表(用于UI绑定)
@@ -240,6 +247,7 @@ partial void OnSelectedModeChanged(SelectionModeItem? value)
OnPropertyChanged(nameof(CurrentInputContent));
OnPropertyChanged(nameof(CurrentPlaceholder));
OnPropertyChanged(nameof(CurrentButtonText));
+ OnPropertyChanged(nameof(CurrentButtonText));
OnPropertyChanged(nameof(IsInputAreaVisible));
OnPropertyChanged(nameof(IsQuickStrategyAreaVisible));
}
diff --git a/src/MarketAssistant.App/ViewModels/FavoritesPageViewModel.cs b/src/MarketAssistant.App/ViewModels/FavoritesPageViewModel.cs
index 48d2a48..ada77f5 100644
--- a/src/MarketAssistant.App/ViewModels/FavoritesPageViewModel.cs
+++ b/src/MarketAssistant.App/ViewModels/FavoritesPageViewModel.cs
@@ -299,7 +299,7 @@ private void FlushPendingPriceUpdates()
if (_assetIndex.TryGetValue(symbol, out var asset))
{
- asset.CurrentPrice = update.Price.ToString("G");
+ asset.CurrentPrice = PriceFormatter.Format(update.Price);
asset.ChangePercentage = $"{update.Change:F2}%";
}
}
diff --git a/src/MarketAssistant.App/ViewModels/Home/HomeSearchViewModel.cs b/src/MarketAssistant.App/ViewModels/Home/HomeSearchViewModel.cs
index f87b864..4171963 100644
--- a/src/MarketAssistant.App/ViewModels/Home/HomeSearchViewModel.cs
+++ b/src/MarketAssistant.App/ViewModels/Home/HomeSearchViewModel.cs
@@ -31,6 +31,12 @@ public partial class HomeSearchViewModel : ViewModelBase, IDisposable
[ObservableProperty]
private bool _isSearching;
+ ///
+ /// 当前键盘/鼠标高亮的结果项(Up/Down 仅移动高亮,导航由 Enter 或点击显式触发)
+ ///
+ [ObservableProperty]
+ private AssetItem? _selectedResult;
+
///
/// 搜索结果集合
///
@@ -73,43 +79,42 @@ partial void OnSearchQueryChanged(string value)
return;
}
- // 触发防抖搜索
- _ = Task.Run(async () =>
- {
- try
- {
- // 等待200毫秒
- await Task.Delay(DebounceDelayMs, cancellationToken);
+ // 触发防抖搜索:延迟与请求共用同一令牌,避免慢响应的旧查询覆盖新查询结果
+ _ = DebouncedSearchAsync(value, cancellationToken);
+ }
- // 如果没有被取消,执行搜索
- if (!cancellationToken.IsCancellationRequested)
- {
- await SearchAsync(value);
- }
- }
- catch (TaskCanceledException)
- {
- // 防抖被取消,正常情况,不记录日志
- Logger?.LogDebug("搜索防抖被取消,查询:{Query}", value);
- }
- catch (Exception ex)
- {
- Logger?.LogError(ex, "搜索资产时发生错误,查询:{Query}", value);
- }
- }, cancellationToken);
+ ///
+ /// 防抖后执行搜索
+ ///
+ private async Task DebouncedSearchAsync(string value, CancellationToken cancellationToken)
+ {
+ try
+ {
+ await Task.Delay(DebounceDelayMs, cancellationToken);
+ await SearchAsync(value, cancellationToken);
+ }
+ catch (OperationCanceledException)
+ {
+ // 防抖或请求被取消,正常情况,不记录日志
+ Logger?.LogDebug("搜索被取消,查询:{Query}", value);
+ }
+ catch (Exception ex)
+ {
+ Logger?.LogError(ex, "搜索资产时发生错误,查询:{Query}", value);
+ }
}
///
- /// 执行搜索
+ /// 执行搜索并刷新结果列表
///
- [RelayCommand]
- private async Task SearchAsync(string? query)
+ private async Task SearchAsync(string query, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(query))
{
IsSearching = false;
IsSearchResultVisible = false;
SearchResults.Clear();
+ SelectedResult = null;
Logger?.LogDebug("搜索查询为空,清空结果");
return;
}
@@ -119,13 +124,17 @@ private async Task SearchAsync(string? query)
await SafeExecuteAsync(async () =>
{
- var results = await HomeAssetService.SearchAssetAsync(query, CancellationToken.None);
+ var results = await HomeAssetService.SearchAssetAsync(query, cancellationToken);
+
+ // 请求期间查询已变化则丢弃本次结果
+ cancellationToken.ThrowIfCancellationRequested();
Logger?.LogInformation("搜索完成,找到 {Count} 个结果", results.Count);
// 确保在 UI 线程上更新集合
await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() =>
{
+ SelectedResult = null;
SearchResults.Clear();
foreach (var asset in results)
{
@@ -160,6 +169,30 @@ private void NavigateToAsset(AssetItem? asset)
AssetSelected?.Invoke(this, asset);
}
+ ///
+ /// 在结果列表中移动键盘高亮项,返回是否发生了移动。
+ /// 仅移动选中项不触发导航,导航由 Enter 或点击显式触发。
+ ///
+ public bool MoveSelection(int offset)
+ {
+ if (SearchResults.Count == 0)
+ {
+ return false;
+ }
+
+ var currentIndex = SelectedResult is null ? -1 : SearchResults.IndexOf(SelectedResult);
+ var newIndex = Math.Clamp(currentIndex + offset, 0, SearchResults.Count - 1);
+ var target = SearchResults[newIndex];
+
+ if (target == SelectedResult)
+ {
+ return false;
+ }
+
+ SelectedResult = target;
+ return true;
+ }
+
///
/// 清空搜索(包括文本和结果)
///
@@ -172,6 +205,7 @@ public void ClearSearch()
SearchQuery = string.Empty;
SearchResults.Clear();
+ SelectedResult = null;
IsSearchResultVisible = false;
IsSearching = false;
}
diff --git a/src/MarketAssistant.App/ViewModels/Home/RecentAssetsViewModel.cs b/src/MarketAssistant.App/ViewModels/Home/RecentAssetsViewModel.cs
index 8a601cd..80d3779 100644
--- a/src/MarketAssistant.App/ViewModels/Home/RecentAssetsViewModel.cs
+++ b/src/MarketAssistant.App/ViewModels/Home/RecentAssetsViewModel.cs
@@ -1,4 +1,5 @@
using CommunityToolkit.Mvvm.Input;
+using MarketAssistant.Applications.Assets;
using MarketAssistant.Applications.Assets.Models;
using MarketAssistant.Applications.History;
using MarketAssistant.Applications.Home;
@@ -24,6 +25,9 @@ public partial class RecentAssetsViewModel : ViewModelBase, IDisposable
private IHomeAssetService HomeAssetService =>
_serviceProvider.GetRequiredKeyedService(_marketContext.CurrentMarket);
+ private IAssetInfoService AssetInfoService =>
+ _serviceProvider.GetRequiredKeyedService(_marketContext.CurrentMarket);
+
///
/// 最近查看资产集合
///
@@ -68,6 +72,9 @@ await SafeExecuteAsync(async () =>
{
var recentAssets = await HistoryService.GetHistoryAsync();
+ // 并行补全实时价格/涨跌幅(单个失败仅留空,不影响整体加载)
+ await Task.WhenAll(recentAssets.Select(EnrichWithQuoteAsync));
+
RecentAssets.Clear();
foreach (var asset in recentAssets)
{
@@ -76,6 +83,23 @@ await SafeExecuteAsync(async () =>
}, "加载最近查看资产");
}
+ ///
+ /// 补全单个资产的实时价格与涨跌幅
+ ///
+ private async Task EnrichWithQuoteAsync(AssetItem asset)
+ {
+ try
+ {
+ var info = await AssetInfoService.GetAssetInfoAsync(asset.Code);
+ asset.CurrentPrice = info.CurrentPrice;
+ asset.ChangePercentage = info.ChangePercentage;
+ }
+ catch (Exception ex)
+ {
+ Logger?.LogWarning(ex, "补全最近查看资产行情失败: {Code}", asset.Code);
+ }
+ }
+
///
/// 添加资产到最近查看
///
diff --git a/src/MarketAssistant.App/ViewModels/IndexTickerItemViewModel.cs b/src/MarketAssistant.App/ViewModels/IndexTickerItemViewModel.cs
new file mode 100644
index 0000000..9ce251b
--- /dev/null
+++ b/src/MarketAssistant.App/ViewModels/IndexTickerItemViewModel.cs
@@ -0,0 +1,41 @@
+namespace MarketAssistant.ViewModels;
+
+///
+/// 顶栏行情条单项(当前为模拟数据,待接入真实指数服务后替换)
+///
+public class IndexTickerItemViewModel
+{
+ ///
+ /// 指数名称(如"上证"、"BTC")
+ ///
+ public string Name { get; }
+
+ ///
+ /// 最新点位/价格
+ ///
+ public string Price { get; }
+
+ ///
+ /// 涨跌幅文本(含正负号)
+ ///
+ public string ChangeText { get; }
+
+ ///
+ /// 是否上涨(决定涨跌配色)
+ ///
+ public bool IsUp { get; }
+
+ ///
+ /// 是否下跌(决定涨跌配色)
+ ///
+ public bool IsDown { get; }
+
+ public IndexTickerItemViewModel(string name, string price, string changeText, bool isUp)
+ {
+ Name = name;
+ Price = price;
+ ChangeText = changeText;
+ IsUp = isUp;
+ IsDown = !isUp;
+ }
+}
\ No newline at end of file
diff --git a/src/MarketAssistant.App/ViewModels/MCPConfigPageViewModel.cs b/src/MarketAssistant.App/ViewModels/MCPConfigPageViewModel.cs
index ab9048b..4f0c04f 100644
--- a/src/MarketAssistant.App/ViewModels/MCPConfigPageViewModel.cs
+++ b/src/MarketAssistant.App/ViewModels/MCPConfigPageViewModel.cs
@@ -49,6 +49,28 @@ public partial class MCPConfigPageViewModel : ViewModelBase, INavigationAware
[ObservableProperty]
private string _transportType = "stdio";
+ ///
+ /// 是否为 stdio 传输类型(控制参数与环境变量输入区的可见性)
+ ///
+ public bool IsStdio => TransportType == "stdio";
+
+ ///
+ /// 命令/URL 输入框占位提示,随传输类型切换
+ ///
+ public string CommandPlaceholder => TransportType switch
+ {
+ "stdio" => "请输入命令,如 npx",
+ "sse" => "请输入 URL,如 http://localhost:3000/sse",
+ _ => "请输入 URL,如 http://localhost:3000/mcp"
+ };
+
+ ///
+ /// 命令/URL 输入框下方说明文字,随传输类型切换
+ ///
+ public string CommandHint => TransportType == "stdio"
+ ? "提示:命令参数与环境变量请在下方对应输入框填写"
+ : $"提示:URL 示例 http://localhost:3000/{(TransportType == "sse" ? "sse" : "mcp")}";
+
[ObservableProperty]
private string _command = string.Empty;
@@ -67,8 +89,21 @@ public partial class MCPConfigPageViewModel : ViewModelBase, INavigationAware
[ObservableProperty]
private ObservableCollection _toolItems = new();
+ // 表单校验错误信息(保存/测试时触发,修正后即时清除)
+ [ObservableProperty]
+ private string? _nameError;
+
+ [ObservableProperty]
+ private string? _commandError;
+
private MCPServerConfig? _editingConfig;
+ // 正在编辑的列表源配置(用于切换确认被拒时回退选中项)
+ private MCPServerConfig? _editingSource;
+
+ // 回退选中项期间抑制选择变更处理,避免递归触发确认
+ private bool _suppressSelectionChanged;
+
public MCPConfigPageViewModel(
MCPServerConfigService configService,
INotificationService notificationService,
@@ -115,6 +150,7 @@ private void AddServer()
{
// 清空选中项,避免与编辑状态冲突
SelectedConfig = null;
+ _editingSource = null;
_editingConfig = new MCPServerConfig
{
@@ -135,6 +171,8 @@ private void EditServer()
{
if (SelectedConfig == null) return;
+ _editingSource = SelectedConfig;
+
// 手动复制配置
_editingConfig = new MCPServerConfig
{
@@ -147,7 +185,8 @@ private void EditServer()
IsEnabled = SelectedConfig.IsEnabled,
EnvironmentVariables = new Dictionary(SelectedConfig.EnvironmentVariables),
Category = SelectedConfig.Category,
- AllowedTools = [.. SelectedConfig.AllowedTools]
+ AllowedTools = [.. SelectedConfig.AllowedTools],
+ AllowAllTools = SelectedConfig.AllowAllTools
};
LoadConfigToUI(_editingConfig);
IsEditing = true;
@@ -161,16 +200,9 @@ private void SaveServer()
{
if (_editingConfig == null) return;
- // 验证必填字段
- if (string.IsNullOrWhiteSpace(Name))
- {
- _notificationService?.ShowWarning("请输入服务器名称");
- return;
- }
-
- if (string.IsNullOrWhiteSpace(Command))
+ // 校验必填字段
+ if (!ValidateForm())
{
- _notificationService?.ShowWarning("请输入命令或URL");
return;
}
@@ -187,6 +219,7 @@ private void SaveServer()
_mcpToolProvider.Invalidate();
IsEditing = false;
_editingConfig = null;
+ _editingSource = null;
_notificationService?.ShowSuccess("保存成功");
Logger?.LogInformation("MCP服务器配置已保存: {Name}", Name);
@@ -199,14 +232,36 @@ private void SaveServer()
}
///
- /// 取消编辑
+ /// 取消编辑(存在未保存修改时需用户确认)
///
[RelayCommand]
- private void CancelEdit()
+ private async Task CancelEdit()
+ {
+ if (HasUnsavedChanges())
+ {
+ var confirmed = await _dialogService.ShowConfirmationAsync(
+ "未保存的修改",
+ "当前有未保存的修改,确定放弃并退出编辑吗?",
+ "放弃修改",
+ "继续编辑");
+
+ if (!confirmed) return;
+ }
+
+ ForceCancelEdit();
+ }
+
+ ///
+ /// 直接退出编辑状态,不做脏检查(用于页面导航离开等自动取消场景)
+ ///
+ private void ForceCancelEdit()
{
SelectedConfig = null;
IsEditing = false;
_editingConfig = null;
+ _editingSource = null;
+ NameError = null;
+ CommandError = null;
}
///
@@ -217,16 +272,9 @@ private async Task TestConnection()
{
if (_editingConfig == null) return;
- // 验证必填字段
- if (string.IsNullOrWhiteSpace(Name))
+ // 校验必填字段
+ if (!ValidateForm())
{
- _notificationService?.ShowWarning("请输入服务器名称");
- return;
- }
-
- if (string.IsNullOrWhiteSpace(Command))
- {
- _notificationService?.ShowWarning("请输入命令或URL");
return;
}
@@ -313,6 +361,8 @@ private async Task DeleteServer()
LoadServerConfigs();
_mcpToolProvider.Invalidate();
IsEditing = false;
+ _editingConfig = null;
+ _editingSource = null;
_notificationService?.ShowSuccess("删除成功");
}
}
@@ -396,6 +446,10 @@ private void LoadConfigToUI(MCPServerConfig config)
{
EnvironmentVariablesText = string.Empty;
}
+
+ // 重新载入表单时清除历史校验错误
+ NameError = null;
+ CommandError = null;
}
///
@@ -439,20 +493,177 @@ private void SaveUIToConfig(MCPServerConfig config)
return result;
}
+ ///
+ /// 校验名称必填
+ ///
+ private void ValidateName()
+ {
+ NameError = string.IsNullOrWhiteSpace(Name) ? "请输入服务器名称" : null;
+ }
+
+ ///
+ /// 校验命令/URL必填及URL格式(sse/streamableHttp 时 Command 字段存储 URL)
+ ///
+ private void ValidateCommand()
+ {
+ if (string.IsNullOrWhiteSpace(Command))
+ {
+ CommandError = TransportType == "stdio" ? "请输入命令" : "请输入 URL";
+ }
+ else if (TransportType != "stdio" && !Uri.TryCreate(Command, UriKind.Absolute, out _))
+ {
+ CommandError = "URL 格式不正确,例如 http://localhost:3000/mcp";
+ }
+ else
+ {
+ CommandError = null;
+ }
+ }
+
+ ///
+ /// 校验表单必填项,返回是否全部通过
+ ///
+ private bool ValidateForm()
+ {
+ ValidateName();
+ ValidateCommand();
+ return NameError is null && CommandError is null;
+ }
+
+ partial void OnNameChanged(string value)
+ {
+ // 已显示错误时即时重新校验,便于用户修正后错误提示消失
+ if (NameError is not null)
+ {
+ ValidateName();
+ }
+ }
+
+ partial void OnCommandChanged(string value)
+ {
+ if (CommandError is not null)
+ {
+ ValidateCommand();
+ }
+ }
+
+ partial void OnTransportTypeChanged(string value)
+ {
+ OnPropertyChanged(nameof(IsStdio));
+ OnPropertyChanged(nameof(CommandPlaceholder));
+ OnPropertyChanged(nameof(CommandHint));
+ if (CommandError is not null)
+ {
+ ValidateCommand();
+ }
+ }
+
partial void OnSelectedConfigChanged(MCPServerConfig? value)
{
- if (value != null)
+ if (value is null || _suppressSelectionChanged)
+ {
+ return;
+ }
+
+ // 存在未保存修改时先确认,用户拒绝则回退选中项
+ if (IsEditing && HasUnsavedChanges() && !ReferenceEquals(value, _editingSource))
+ {
+ _ = HandleSelectionChangeAsync(value);
+ return;
+ }
+
+ EditServer();
+ }
+
+ ///
+ /// 处理带未保存修改的选中切换:确认丢弃后加载目标配置,否则回退选中项
+ ///
+ private async Task HandleSelectionChangeAsync(MCPServerConfig target)
+ {
+ try
+ {
+ var confirmed = await _dialogService.ShowConfirmationAsync(
+ "未保存的修改",
+ $"服务器「{(target.Name is { Length: > 0 } n ? n : target.Id)}」有未保存的修改,切换后将丢弃这些修改。是否继续?",
+ "丢弃并切换",
+ "继续编辑");
+
+ if (confirmed)
+ {
+ EditServer();
+ }
+ else
+ {
+ _suppressSelectionChanged = true;
+ try
+ {
+ SelectedConfig = _editingSource;
+ }
+ finally
+ {
+ _suppressSelectionChanged = false;
+ }
+ }
+ }
+ catch (Exception ex)
{
+ Logger?.LogError(ex, "处理MCP服务器切换确认失败");
EditServer();
}
}
+ ///
+ /// 判断当前表单是否存在未保存的修改
+ ///
+ private bool HasUnsavedChanges()
+ {
+ if (_editingConfig is null || !IsEditing)
+ {
+ return false;
+ }
+
+ return !string.Equals(Name, _editingConfig.Name, StringComparison.Ordinal)
+ || !string.Equals(Description, _editingConfig.Description, StringComparison.Ordinal)
+ || !string.Equals(TransportType, _editingConfig.TransportType, StringComparison.Ordinal)
+ || !string.Equals(Command, _editingConfig.Command, StringComparison.Ordinal)
+ || !string.Equals(Arguments, _editingConfig.Arguments, StringComparison.Ordinal)
+ || IsEnabled != _editingConfig.IsEnabled
+ || !AreDictionariesEqual(ParseEnvironmentVariables(), _editingConfig.EnvironmentVariables)
+ || !AreAllowedToolsEqual();
+ }
+
+ ///
+ /// 比较两个环境变量字典是否一致(忽略顺序)
+ ///
+ private static bool AreDictionariesEqual(Dictionary left, Dictionary right)
+ => left.Count == right.Count && left.All(kv => right.TryGetValue(kv.Key, out var value) && value == kv.Value);
+
+ ///
+ /// 比较当前勾选的工具白名单(含允许全部工具开关)与编辑配置是否一致
+ ///
+ private bool AreAllowedToolsEqual()
+ {
+ if (AllowAllTools != _editingConfig!.AllowAllTools)
+ {
+ return false;
+ }
+
+ if (AllowAllTools)
+ {
+ return true;
+ }
+
+ var selected = ToolItems.Where(item => item.IsSelected).Select(item => item.Name).ToList();
+ return selected.Count == _editingConfig.AllowedTools.Count
+ && selected.All(_editingConfig.AllowedTools.Contains);
+ }
+
public void OnNavigatedFrom()
{
// 离开页面时如果正在编辑但未保存,自动取消编辑状态
if (IsEditing)
{
- CancelEdit();
+ ForceCancelEdit();
}
}
diff --git a/src/MarketAssistant.App/ViewModels/MainWindowViewModel.cs b/src/MarketAssistant.App/ViewModels/MainWindowViewModel.cs
index dca50b9..b18f83b 100644
--- a/src/MarketAssistant.App/ViewModels/MainWindowViewModel.cs
+++ b/src/MarketAssistant.App/ViewModels/MainWindowViewModel.cs
@@ -1,10 +1,8 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
-using MarketAssistant.Applications.Settings;
using MarketAssistant.Services.Market;
using MarketAssistant.Services.Navigation;
using MarketAssistant.Services.Notification;
-using MarketAssistant.Services.Settings;
using MarketAssistant.ViewModels.Trading;
using Microsoft.Extensions.Logging;
using System.Collections.ObjectModel;
@@ -18,7 +16,6 @@ public partial class MainWindowViewModel : ViewModelBase
private readonly NavigationService _navigationService;
private readonly MarketContext _marketContext;
private readonly INotificationService _notificationService;
- private readonly IUserSettingService _userSettingService;
private bool _isSynchronizingNavigationSelection;
[ObservableProperty]
@@ -28,19 +25,40 @@ public partial class MainWindowViewModel : ViewModelBase
public bool CanGoBack => _navigationService.CanGoBack;
public string CurrentPageTitle => _navigationService.CurrentPage?.Title ?? string.Empty;
- public ObservableCollection NavigationItems { get; }
+ public ObservableCollection MainNavigationItems { get; }
+
+ public ObservableCollection BottomNavigationItems { get; }
+
+ ///
+ /// 顶栏行情条(当前为模拟数据,待接入真实指数服务)
+ ///
+ public ObservableCollection IndexTickers { get; }
+
+ ///
+ /// 行情条是否可见(无数据时整段隐藏,对齐设计系统裁决 #6)
+ ///
+ public bool HasIndexTickers => IndexTickers.Count > 0;
///
/// 当前市场类型显示文本
///
public string CurrentMarketText => _marketContext.CurrentMarket == MarketType.AShare ? "A股市场" : "虚拟币市场";
+ ///
+ /// 当前是否为 A 股市场(用于顶栏分段切换器视觉状态)
+ ///
+ public bool IsAShareMarket => _marketContext.CurrentMarket == MarketType.AShare;
+
+ ///
+ /// 当前是否为虚拟币市场(用于顶栏分段切换器视觉状态)
+ ///
+ public bool IsCryptoMarket => _marketContext.CurrentMarket == MarketType.Crypto;
+
public MainWindowViewModel(
IServiceProvider serviceProvider,
NavigationService navigationService,
MarketContext marketContext,
INotificationService notificationService,
- IUserSettingService userSettingService,
ILogger? logger = null)
: base(logger)
{
@@ -48,10 +66,12 @@ public MainWindowViewModel(
_navigationService = navigationService;
_marketContext = marketContext;
_notificationService = notificationService;
- _userSettingService = userSettingService;
- NavigationItems = new ObservableCollection();
+ MainNavigationItems = new ObservableCollection();
+ BottomNavigationItems = new ObservableCollection();
+ IndexTickers = new ObservableCollection();
RebuildNavigationItems();
+ RebuildIndexTickers();
// 监听导航服务属性变更
_navigationService.PropertyChanged += OnNavigationServicePropertyChanged;
@@ -60,38 +80,70 @@ public MainWindowViewModel(
SubscribeToMarketChanges(_marketContext);
// 默认导航到首页。SelectedNavigationItem 的变更回调负责实际导航,避免重复入栈。
- SelectedNavigationItem = NavigationItems[0];
+ SelectedNavigationItem = MainNavigationItems[0];
}
protected override void OnMarketChanged(MarketType newMarket)
{
OnPropertyChanged(nameof(CurrentMarketText));
+ OnPropertyChanged(nameof(IsAShareMarket));
+ OnPropertyChanged(nameof(IsCryptoMarket));
RebuildNavigationItems();
+ RebuildIndexTickers();
}
private void RebuildNavigationItems()
{
- NavigationItems.Clear();
+ MainNavigationItems.Clear();
+ BottomNavigationItems.Clear();
- NavigationItems.Add(new NavigationItemViewModel("首页", "avares://MarketAssistant/Assets/Images/tab_home.svg", "avares://MarketAssistant/Assets/Images/tab_home_on.svg", () => _serviceProvider.GetRequiredService()));
- NavigationItems.Add(new NavigationItemViewModel("收藏", "avares://MarketAssistant/Assets/Images/tab_favorites.svg", "avares://MarketAssistant/Assets/Images/tab_favorites_on.svg", () => _serviceProvider.GetRequiredService()));
- NavigationItems.Add(new NavigationItemViewModel("告警", "avares://MarketAssistant/Assets/Images/tab_alert.svg", "avares://MarketAssistant/Assets/Images/tab_alert_on.svg", () => _serviceProvider.GetRequiredService()));
- NavigationItems.Add(new NavigationItemViewModel("AI选股", "avares://MarketAssistant/Assets/Images/tab_analysis.svg", "avares://MarketAssistant/Assets/Images/tab_analysis_on.svg", () => _serviceProvider.GetRequiredService()));
- // 交易为实验功能:仅当前市场支持交易且用户在设置中显式开启时可见(默认关闭)
+ MainNavigationItems.Add(new NavigationItemViewModel("首页", "avares://MarketAssistant/Assets/Images/tab_home.svg", "avares://MarketAssistant/Assets/Images/tab_home_on.svg", () => _serviceProvider.GetRequiredService()));
+ MainNavigationItems.Add(new NavigationItemViewModel("收藏", "avares://MarketAssistant/Assets/Images/tab_favorites.svg", "avares://MarketAssistant/Assets/Images/tab_favorites_on.svg", () => _serviceProvider.GetRequiredService()));
+ MainNavigationItems.Add(new NavigationItemViewModel("告警", "avares://MarketAssistant/Assets/Images/tab_alert.svg", "avares://MarketAssistant/Assets/Images/tab_alert_on.svg", () => _serviceProvider.GetRequiredService()));
+ MainNavigationItems.Add(new NavigationItemViewModel("AI选股", "avares://MarketAssistant/Assets/Images/tab_analysis.svg", "avares://MarketAssistant/Assets/Images/tab_analysis_on.svg", () => _serviceProvider.GetRequiredService()));
+ // 交易入口跟随市场能力:虚拟币等支持交易的市场可见,A 股不可见
if (IsTradingVisible())
{
- NavigationItems.Add(new NavigationItemViewModel("交易", "avares://MarketAssistant/Assets/Images/tab_trading.svg", "avares://MarketAssistant/Assets/Images/tab_trading_on.svg", () => _serviceProvider.GetRequiredService()));
+ MainNavigationItems.Add(new NavigationItemViewModel("交易", "avares://MarketAssistant/Assets/Images/tab_trading.svg", "avares://MarketAssistant/Assets/Images/tab_trading_on.svg", () => _serviceProvider.GetRequiredService()));
+ }
+
+ // 底部固定:设置 / 关于(对齐原型 sidebar nav-bot)
+ BottomNavigationItems.Add(new NavigationItemViewModel("设置", "avares://MarketAssistant/Assets/Images/tab_settings.svg", "avares://MarketAssistant/Assets/Images/tab_settings_on.svg", () => _serviceProvider.GetRequiredService()));
+ BottomNavigationItems.Add(new NavigationItemViewModel("关于", "avares://MarketAssistant/Assets/Images/tab_about.svg", "avares://MarketAssistant/Assets/Images/tab_about_on.svg", () => _serviceProvider.GetRequiredService()));
+ }
+
+ ///
+ /// 按当前市场填充顶栏行情条。
+ /// 当前为示意数据。接入真实指数服务时仅需替换本方法:
+ /// 在 App.Services 对应市场模块(AShareMarketModule / CryptoMarketModule)注册
+ /// IIndexQuoteService(Keyed by MarketType),此处改为调用其接口即可,XAML 无需改动。
+ ///
+ private void RebuildIndexTickers()
+ {
+ IndexTickers.Clear();
+
+ if (_marketContext.CurrentMarket == MarketType.AShare)
+ {
+ IndexTickers.Add(new IndexTickerItemViewModel("上证", "3,387.52", "+1.24%", isUp: true));
+ IndexTickers.Add(new IndexTickerItemViewModel("深证", "10,892.41", "-0.38%", isUp: false));
+ IndexTickers.Add(new IndexTickerItemViewModel("创业", "2,156.30", "+0.82%", isUp: true));
+ }
+ else
+ {
+ // 加密市场展示指数类行情(与热门标的中的 BTC/ETH 等个币区分),避免信息重复
+ IndexTickers.Add(new IndexTickerItemViewModel("CoinDesk 20", "2,184.62", "+1.92%", isUp: true));
+ IndexTickers.Add(new IndexTickerItemViewModel("Bitwise 10", "4,356.18", "+2.35%", isUp: true));
+ IndexTickers.Add(new IndexTickerItemViewModel("DPI", "112.47", "-0.86%", isUp: false));
}
- NavigationItems.Add(new NavigationItemViewModel("设置", "avares://MarketAssistant/Assets/Images/tab_settings.svg", "avares://MarketAssistant/Assets/Images/tab_settings_on.svg", () => _serviceProvider.GetRequiredService()));
- NavigationItems.Add(new NavigationItemViewModel("关于", "avares://MarketAssistant/Assets/Images/tab_about.svg", "avares://MarketAssistant/Assets/Images/tab_about_on.svg", () => _serviceProvider.GetRequiredService()));
+
+ OnPropertyChanged(nameof(HasIndexTickers));
}
///
- /// 交易导航可见性:市场支持交易(如虚拟币)且用户显式开启实验开关;A 股始终不可见。
+ /// 交易导航可见性:仅当前市场支持交易时可见(如虚拟币);A 股始终不可见。
///
private bool IsTradingVisible()
- => _marketContext.CurrentCapability.SupportsTrading
- && _userSettingService.CurrentSetting.EnableExperimentalTrading;
+ => _marketContext.CurrentCapability.SupportsTrading;
private void OnNavigationServicePropertyChanged(object? sender, PropertyChangedEventArgs e)
{
@@ -111,7 +163,9 @@ private void OnNavigationServicePropertyChanged(object? sender, PropertyChangedE
_isSynchronizingNavigationSelection = true;
try
{
- SelectedNavigationItem = NavigationItems.FirstOrDefault(
+ SelectedNavigationItem = MainNavigationItems.FirstOrDefault(
+ item => item.Title == _navigationService.CurrentRootNavigationItemTitle)
+ ?? BottomNavigationItems.FirstOrDefault(
item => item.Title == _navigationService.CurrentRootNavigationItemTitle);
}
finally
@@ -142,6 +196,28 @@ private void ToggleMarket()
? MarketType.Crypto
: MarketType.AShare;
+ SwitchToMarket(newMarket);
+ }
+
+ ///
+ /// 按指定市场切换(顶栏分段切换器使用)
+ ///
+ /// 目标市场类型
+ [RelayCommand]
+ private void SwitchMarket(MarketType market)
+ {
+ // 已是目标市场则不重复切换刷新页面
+ if (_marketContext.CurrentMarket == market)
+ return;
+
+ SwitchToMarket(market);
+ }
+
+ ///
+ /// 执行市场切换、提示并刷新当前页面
+ ///
+ private void SwitchToMarket(MarketType newMarket)
+ {
_marketContext.SwitchMarket(newMarket);
// 显示切换提示
diff --git a/src/MarketAssistant.App/Views/Components/AnalysisReportView.axaml b/src/MarketAssistant.App/Views/Components/AnalysisReportView.axaml
index 8f68c9a..7d5692b 100644
--- a/src/MarketAssistant.App/Views/Components/AnalysisReportView.axaml
+++ b/src/MarketAssistant.App/Views/Components/AnalysisReportView.axaml
@@ -240,15 +240,15 @@
-
+ Foreground="{DynamicResource SuccessPanelTextBrush}"/>
@@ -266,15 +266,15 @@
-
+ Foreground="{DynamicResource DangerPanelTextBrush}"/>
@@ -318,7 +318,7 @@
-
+
-
+ Foreground="{DynamicResource WarningPanelTextBrush}"/>
diff --git a/src/MarketAssistant.App/Views/Components/ProgressDisplayView.axaml b/src/MarketAssistant.App/Views/Components/ProgressDisplayView.axaml
index 9b318f7..0366f87 100644
--- a/src/MarketAssistant.App/Views/Components/ProgressDisplayView.axaml
+++ b/src/MarketAssistant.App/Views/Components/ProgressDisplayView.axaml
@@ -107,14 +107,14 @@
-
diff --git a/src/MarketAssistant.App/Views/Pages/AboutPageView.axaml b/src/MarketAssistant.App/Views/Pages/AboutPageView.axaml
index 97b42f8..bb53068 100644
--- a/src/MarketAssistant.App/Views/Pages/AboutPageView.axaml
+++ b/src/MarketAssistant.App/Views/Pages/AboutPageView.axaml
@@ -33,6 +33,7 @@
Padding="0"
Width="{StaticResource SmallIconSize}"
Height="{StaticResource SmallIconSize}"
+ ToolTip.Tip="在 GitHub 上查看源码"
Command="{Binding OpenGitHubCommand}">
-
+
+
+
@@ -200,7 +204,7 @@
Margin="{StaticResource HorizontalMargin}">
-
diff --git a/src/MarketAssistant.App/Views/Pages/AssetPageView.axaml b/src/MarketAssistant.App/Views/Pages/AssetPageView.axaml
index 91903d7..5e73f72 100644
--- a/src/MarketAssistant.App/Views/Pages/AssetPageView.axaml
+++ b/src/MarketAssistant.App/Views/Pages/AssetPageView.axaml
@@ -40,6 +40,7 @@
Padding="{StaticResource TinyPadding}"
VerticalAlignment="Center">
@@ -48,7 +49,8 @@
-
@@ -132,7 +135,7 @@
VerticalAlignment="Center" />
-
diff --git a/src/MarketAssistant.App/Views/Pages/AssetSelectionPageView.axaml b/src/MarketAssistant.App/Views/Pages/AssetSelectionPageView.axaml
index 85c174b..af8758c 100644
--- a/src/MarketAssistant.App/Views/Pages/AssetSelectionPageView.axaml
+++ b/src/MarketAssistant.App/Views/Pages/AssetSelectionPageView.axaml
@@ -20,10 +20,49 @@
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
@@ -58,108 +94,90 @@
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+ TextWrapping="Wrap"/>
+
+ MinWidth="{StaticResource MediumButtonMinWidth}"/>
-
-
-
-
-
-
+
+
-
@@ -168,94 +186,172 @@
-
-
-
-
+
+ HorizontalAlignment="Left">
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
+ Foreground="{StaticResource PrimaryBrush}"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
-
-
+
-
-
+ Foreground="{DynamicResource InfoPanelTextBrush}"/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
+
-
-
+
diff --git a/src/MarketAssistant.App/Views/Pages/FavoritesPageView.axaml b/src/MarketAssistant.App/Views/Pages/FavoritesPageView.axaml
index 6f7bba1..33100da 100644
--- a/src/MarketAssistant.App/Views/Pages/FavoritesPageView.axaml
+++ b/src/MarketAssistant.App/Views/Pages/FavoritesPageView.axaml
@@ -20,158 +20,141 @@
+
+
+
+
+
+
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
+ VerticalAlignment="Center"/>
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/src/MarketAssistant.App/Views/Pages/HomePageView.axaml b/src/MarketAssistant.App/Views/Pages/HomePageView.axaml
index 974694a..7e04391 100644
--- a/src/MarketAssistant.App/Views/Pages/HomePageView.axaml
+++ b/src/MarketAssistant.App/Views/Pages/HomePageView.axaml
@@ -16,41 +16,80 @@
+
+
+
+
+
+
+
-
+
+
-
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ColumnDefinitions="3*,2*">
-
-
+
+
-
+ FontWeight="SemiBold"/>
-
-
+
+
-
+
-
+
+
-
+
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
\ No newline at end of file
diff --git a/src/MarketAssistant.App/Views/Pages/HomePageView.axaml.cs b/src/MarketAssistant.App/Views/Pages/HomePageView.axaml.cs
index a652af3..a40bb88 100644
--- a/src/MarketAssistant.App/Views/Pages/HomePageView.axaml.cs
+++ b/src/MarketAssistant.App/Views/Pages/HomePageView.axaml.cs
@@ -15,22 +15,66 @@ public HomePageView()
}
///
- /// 搜索结果项点击处理
+ /// 搜索结果项点击处理:激活所点资产并导航
///
- private void SearchResultItem_PointerPressed(object? sender, PointerPressedEventArgs e)
+ private void SearchResultItem_Tapped(object? sender, TappedEventArgs e)
{
- if (sender is Control control &&
- control.Tag is AssetItem selectedAsset &&
+ if (sender is Control { DataContext: AssetItem asset } &&
DataContext is HomePageViewModel viewModel)
{
- // 标记事件已处理,防止AutoCompleteBox处理
e.Handled = true;
- // 关闭下拉框
- viewModel.Search.IsSearchResultVisible = false;
+ // 执行导航(内部会关闭下拉框)
+ viewModel.Search.NavigateToAssetCommand.Execute(asset);
+ }
+ }
+
+ ///
+ /// 搜索框键盘处理:Esc 关闭结果、上下键移动高亮、回车跳转高亮结果。
+ /// 输入法组合期间回车由输入法消费,不会进入此处理,避免重复提交。
+ ///
+ private void StockSearchBox_KeyDown(object? sender, KeyEventArgs e)
+ {
+ if (DataContext is not HomePageViewModel viewModel)
+ {
+ return;
+ }
- // 执行导航
- viewModel.Search.NavigateToAssetCommand.Execute(selectedAsset);
+ var search = viewModel.Search;
+
+ switch (e.Key)
+ {
+ case Key.Escape:
+ if (search.IsSearchResultVisible)
+ {
+ search.IsSearchResultVisible = false;
+ e.Handled = true;
+ }
+ break;
+
+ case Key.Down:
+ if (search.IsSearchResultVisible && search.MoveSelection(1))
+ {
+ e.Handled = true;
+ }
+ break;
+
+ case Key.Up:
+ if (search.IsSearchResultVisible && search.MoveSelection(-1))
+ {
+ e.Handled = true;
+ }
+ break;
+
+ case Key.Enter:
+ if (search.IsSearchResultVisible &&
+ search.SelectedResult is AssetItem selectedAsset)
+ {
+ search.IsSearchResultVisible = false;
+ search.NavigateToAssetCommand.Execute(selectedAsset);
+ e.Handled = true;
+ }
+ break;
}
}
diff --git a/src/MarketAssistant.App/Views/Pages/MCPConfigPageView.axaml b/src/MarketAssistant.App/Views/Pages/MCPConfigPageView.axaml
index 5821523..f9bd178 100644
--- a/src/MarketAssistant.App/Views/Pages/MCPConfigPageView.axaml
+++ b/src/MarketAssistant.App/Views/Pages/MCPConfigPageView.axaml
@@ -12,7 +12,7 @@
- 280
+ 280
4,0
@@ -39,40 +39,46 @@
Background="{DynamicResource CardBackgroundBrush}">
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+ Margin="{StaticResource TinyBottomMargin}">
@@ -80,6 +86,7 @@
FontWeight="Bold"
Foreground="{DynamicResource TextPrimaryBrush}"
TextTrimming="CharacterEllipsis"/>
+
+
+
+
+
-
-
+
+
-
-
+
@@ -146,6 +161,10 @@
+
@@ -180,41 +199,24 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
-
+
-
-
+
-
-
-
-
-
-
-
-
-
-
+ Foreground="{StaticResource ErrorBrush}"/>
-
+
-
+
@@ -332,18 +332,14 @@
diff --git a/src/MarketAssistant.App/Views/Pages/MCPConfigPageView.axaml.cs b/src/MarketAssistant.App/Views/Pages/MCPConfigPageView.axaml.cs
index 20668b1..76e232d 100644
--- a/src/MarketAssistant.App/Views/Pages/MCPConfigPageView.axaml.cs
+++ b/src/MarketAssistant.App/Views/Pages/MCPConfigPageView.axaml.cs
@@ -1,42 +1,15 @@
using Avalonia.Controls;
-using Avalonia.Input;
-using Avalonia.Interactivity;
-using MarketAssistant.Applications.Settings;
-using MarketAssistant.ViewModels;
namespace MarketAssistant.Views.Pages;
+///
+/// MCP 服务器配置页。
+/// 列表选择、键盘导航由 ListBox 内建处理,页面仅承载视图结构。
+///
public partial class MCPConfigPageView : UserControl
{
public MCPConfigPageView()
{
InitializeComponent();
}
-
- ///
- /// 服务器项点击事件
- ///
- private void OnServerItemTapped(object? sender, RoutedEventArgs e)
- {
- SelectServer(sender);
- }
-
- private void OnServerItemKeyDown(object? sender, KeyEventArgs e)
- {
- if (e.Key is Key.Enter or Key.Space)
- {
- SelectServer(sender);
- e.Handled = true;
- }
- }
-
- private void SelectServer(object? sender)
- {
- if (sender is Border border &&
- border.Tag is MCPServerConfig config &&
- DataContext is MCPConfigPageViewModel viewModel)
- {
- viewModel.SelectedConfig = config;
- }
- }
}
diff --git a/src/MarketAssistant.App/Views/Pages/PriceAlertPageView.axaml b/src/MarketAssistant.App/Views/Pages/PriceAlertPageView.axaml
index 93a0eb3..ac7b3ab 100644
--- a/src/MarketAssistant.App/Views/Pages/PriceAlertPageView.axaml
+++ b/src/MarketAssistant.App/Views/Pages/PriceAlertPageView.axaml
@@ -314,31 +314,40 @@
Spacing="{StaticResource TinySpacing}"
IsVisible="{Binding Condition, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static alert:AlertCondition.PriceAbove}}">
-
+
-
+
-
+
-
+
@@ -350,6 +359,7 @@
diff --git a/src/MarketAssistant.App/Views/Pages/SettingsPageView.axaml b/src/MarketAssistant.App/Views/Pages/SettingsPageView.axaml
index 3b7c9a1..9041e4e 100644
--- a/src/MarketAssistant.App/Views/Pages/SettingsPageView.axaml
+++ b/src/MarketAssistant.App/Views/Pages/SettingsPageView.axaml
@@ -12,9 +12,28 @@
0,0,8,16
+
+ 16,12,16,12
+ 20,16,20,20
+
+
+
+
+
+
-
+
+
@@ -234,17 +255,17 @@
-
-
+
-
+
-
@@ -296,9 +317,160 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -343,7 +515,7 @@
HorizontalAlignment="Left"
Margin="{StaticResource TinyTopMargin}"/>
-
+
-
+
\ No newline at end of file
diff --git a/src/MarketAssistant.App/Views/Pages/Trading/TradeMonitorView.axaml b/src/MarketAssistant.App/Views/Pages/Trading/TradeMonitorView.axaml
index adf78b5..cb407b2 100644
--- a/src/MarketAssistant.App/Views/Pages/Trading/TradeMonitorView.axaml
+++ b/src/MarketAssistant.App/Views/Pages/Trading/TradeMonitorView.axaml
@@ -4,6 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:MarketAssistant.ViewModels.Trading"
xmlns:controls="using:MarketAssistant.Views.Controls"
+ xmlns:svg="clr-namespace:Avalonia.Svg.Skia;assembly=Svg.Controls.Skia.Avalonia"
xmlns:models="using:MarketAssistant.Trading.Models"
xmlns:abstractions="using:MarketAssistant.Trading.Abstractions"
xmlns:converts="using:MarketAssistant.Converters"
@@ -19,76 +20,83 @@
-
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
-
+
-
-
-
-
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
-
+
@@ -151,61 +159,8 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -276,8 +231,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -335,7 +339,6 @@
-
@@ -343,4 +346,4 @@
-
+
\ No newline at end of file
diff --git a/src/MarketAssistant.App/Views/Pages/Trading/TradingPageView.axaml b/src/MarketAssistant.App/Views/Pages/Trading/TradingPageView.axaml
index e5fd839..efe919d 100644
--- a/src/MarketAssistant.App/Views/Pages/Trading/TradingPageView.axaml
+++ b/src/MarketAssistant.App/Views/Pages/Trading/TradingPageView.axaml
@@ -3,77 +3,175 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:MarketAssistant.ViewModels.Trading"
- xmlns:controls="using:MarketAssistant.Views.Controls"
+ xmlns:converts="using:MarketAssistant.Converters"
xmlns:trading="using:MarketAssistant.Views.Pages.Trading"
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800"
x:Class="MarketAssistant.Views.Pages.Trading.TradingPageView"
x:DataType="vm:TradingPageViewModel">
-
-
-
-
-
-
-
-
+
+
+ 20,16,20,20
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+ Foreground="{DynamicResource TextSecondaryBrush}"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MarketAssistant.App/Views/Windows/MainWindow.axaml b/src/MarketAssistant.App/Views/Windows/MainWindow.axaml
index 291e663..9858655 100644
--- a/src/MarketAssistant.App/Views/Windows/MainWindow.axaml
+++ b/src/MarketAssistant.App/Views/Windows/MainWindow.axaml
@@ -42,9 +42,49 @@
Background="{DynamicResource CardBackgroundBrush}"
BorderBrush="{DynamicResource BorderBrush}"
BorderThickness="{StaticResource RightBorderThickness}">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ Padding="0"
+ ItemTemplate="{StaticResource NavRailItemTemplate}"/>
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MarketAssistant.DataProviders/AShare/EastMoneyNewsClient.cs b/src/MarketAssistant.DataProviders/AShare/EastMoneyNewsClient.cs
index b6f374b..49426e1 100644
--- a/src/MarketAssistant.DataProviders/AShare/EastMoneyNewsClient.cs
+++ b/src/MarketAssistant.DataProviders/AShare/EastMoneyNewsClient.cs
@@ -64,7 +64,7 @@ public async Task> SearchNewsAsync(
});
var param = Uri.EscapeDataString(payloadJson);
var url = $"search/jsonp?cb=jQuery¶m={param}";
-using var httpClient = _httpClientFactory.CreateClient("EastMoneySearch");
+ using var httpClient = _httpClientFactory.CreateClient("EastMoneySearch");
httpClient.Timeout = TimeSpan.FromSeconds(15);
using var request = new HttpRequestMessage(HttpMethod.Get, url);
diff --git a/src/MarketAssistant.Infrastructure/Factories/ModelDiscoveryService.cs b/src/MarketAssistant.Infrastructure/Factories/ModelDiscoveryService.cs
index 9038829..a176ceb 100644
--- a/src/MarketAssistant.Infrastructure/Factories/ModelDiscoveryService.cs
+++ b/src/MarketAssistant.Infrastructure/Factories/ModelDiscoveryService.cs
@@ -116,4 +116,4 @@ private static bool TryGetArray(JsonElement root, string propertyName, out JsonE
return false;
}
- }
+}
diff --git a/tests/Application/HomeSearchViewModelTest.cs b/tests/Application/HomeSearchViewModelTest.cs
new file mode 100644
index 0000000..5eb0278
--- /dev/null
+++ b/tests/Application/HomeSearchViewModelTest.cs
@@ -0,0 +1,129 @@
+using MarketAssistant.Applications.Assets.Models;
+using MarketAssistant.Applications.Settings;
+using MarketAssistant.Services.Market;
+using MarketAssistant.Services.Settings;
+using MarketAssistant.ViewModels.Home;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+
+namespace TestMarketAssistant.Application;
+
+///
+/// HomeSearchViewModel 单元测试:覆盖键盘高亮导航(MoveSelection)与清空逻辑
+///
+[TestClass]
+public class HomeSearchViewModelTest
+{
+ private static HomeSearchViewModel CreateViewModel()
+ {
+ var services = new ServiceCollection();
+ var userSettingService = new Mock();
+ userSettingService
+ .Setup(s => s.CurrentSetting)
+ .Returns(new UserSetting());
+ services.AddSingleton(userSettingService.Object);
+ var serviceProvider = services.BuildServiceProvider();
+ var marketContext = new MarketContext(userSettingService.Object, serviceProvider);
+
+ return new HomeSearchViewModel(
+ serviceProvider,
+ marketContext,
+ NullLogger.Instance);
+ }
+
+ private static List CreateResults(int count)
+ => Enumerable.Range(1, count)
+ .Select(i => new AssetItem { Name = $"资产{i}", Code = $"CODE{i:000}" })
+ .ToList();
+
+ [TestMethod]
+ public void MoveSelection_WithNoResults_ShouldReturnFalse()
+ {
+ var vm = CreateViewModel();
+
+ Assert.IsFalse(vm.MoveSelection(1));
+ Assert.IsNull(vm.SelectedResult);
+ }
+
+ [TestMethod]
+ public void MoveSelection_WithNoHighlight_DownShouldSelectFirstItem()
+ {
+ var vm = CreateViewModel();
+ foreach (var item in CreateResults(3))
+ {
+ vm.SearchResults.Add(item);
+ }
+
+ Assert.IsTrue(vm.MoveSelection(1));
+ Assert.AreSame(vm.SearchResults[0], vm.SelectedResult);
+ }
+
+ [TestMethod]
+ public void MoveSelection_AtLastItem_DownShouldStayAndReturnFalse()
+ {
+ var vm = CreateViewModel();
+ foreach (var item in CreateResults(2))
+ {
+ vm.SearchResults.Add(item);
+ }
+ vm.SelectedResult = vm.SearchResults[1];
+
+ Assert.IsFalse(vm.MoveSelection(1));
+ Assert.AreSame(vm.SearchResults[1], vm.SelectedResult);
+ }
+
+ [TestMethod]
+ public void MoveSelection_AtFirstItem_UpShouldStayAndReturnFalse()
+ {
+ var vm = CreateViewModel();
+ foreach (var item in CreateResults(2))
+ {
+ vm.SearchResults.Add(item);
+ }
+ vm.SelectedResult = vm.SearchResults[0];
+
+ Assert.IsFalse(vm.MoveSelection(-1));
+ Assert.AreSame(vm.SearchResults[0], vm.SelectedResult);
+ }
+
+ [TestMethod]
+ public void MoveSelection_SequentialDowns_ShouldMoveThroughList()
+ {
+ var vm = CreateViewModel();
+ foreach (var item in CreateResults(3))
+ {
+ vm.SearchResults.Add(item);
+ }
+
+ vm.MoveSelection(1);
+ Assert.AreSame(vm.SearchResults[0], vm.SelectedResult);
+
+ vm.MoveSelection(1);
+ Assert.AreSame(vm.SearchResults[1], vm.SelectedResult);
+
+ vm.MoveSelection(-1);
+ Assert.AreSame(vm.SearchResults[0], vm.SelectedResult);
+ }
+
+ [TestMethod]
+ public void ClearSearch_ShouldClearQueryResultsAndSelection()
+ {
+ var vm = CreateViewModel();
+ foreach (var item in CreateResults(2))
+ {
+ vm.SearchResults.Add(item);
+ }
+ vm.SelectedResult = vm.SearchResults[0];
+ vm.SearchQuery = "茅台";
+ vm.IsSearchResultVisible = true;
+
+ vm.ClearSearch();
+
+ Assert.AreEqual(string.Empty, vm.SearchQuery);
+ Assert.AreEqual(0, vm.SearchResults.Count);
+ Assert.IsNull(vm.SelectedResult);
+ Assert.IsFalse(vm.IsSearchResultVisible);
+ Assert.IsFalse(vm.IsSearching);
+ }
+}