chore: migrate remaining ECharts components to Chart.js
- Migrated 4 components from ECharts to Chart.js: * MonthlyExpenseCard.vue (折线图) * DailyTrendChart.vue (双系列折线图) * ExpenseCategoryCard.vue (环形图) * BudgetChartAnalysis.vue (仪表盘 + 多种图表) - Removed all ECharts imports and environment variable switches - Unified all charts to use BaseChart.vue component - Build verified: pnpm build success ✓ - No echarts imports remaining ✓ Refs: openspec/changes/migrate-remaining-echarts-to-chartjs
This commit is contained in:
117
Service/IconSearch/IconifyApiService.cs
Normal file
117
Service/IconSearch/IconifyApiService.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
namespace Service.IconSearch;
|
||||
|
||||
/// <summary>
|
||||
/// Iconify API 响应
|
||||
/// 实际 API 返回的图标是字符串数组,格式为 "collection:iconName"
|
||||
/// 例如:["mdi:home", "svg-spinners:wind-toy"]
|
||||
/// </summary>
|
||||
public record IconifyApiResponse
|
||||
{
|
||||
[JsonPropertyName("icons")]
|
||||
public List<string>? Icons { get; init; }
|
||||
}
|
||||
|
||||
public record IconifySettings
|
||||
{
|
||||
public string ApiUrl { get; init; } = "https://api.iconify.design/search";
|
||||
public int DefaultLimit { get; init; } = 20;
|
||||
public int MaxRetryCount { get; init; } = 3;
|
||||
public int RetryDelayMs { get; init; } = 1000;
|
||||
}
|
||||
|
||||
public class IconifyApiService(
|
||||
IOptions<IconifySettings> settings,
|
||||
ILogger<IconifyApiService> logger
|
||||
) : IIconifyApiService
|
||||
{
|
||||
private readonly HttpClient _httpClient = new();
|
||||
private readonly IconifySettings _settings = settings.Value;
|
||||
|
||||
public async Task<List<IconCandidate>> SearchIconsAsync(List<string> keywords, int limit = 20)
|
||||
{
|
||||
var allIcons = new List<IconCandidate>();
|
||||
var actualLimit = limit > 0 ? limit : _settings.DefaultLimit;
|
||||
|
||||
foreach (var keyword in keywords)
|
||||
{
|
||||
try
|
||||
{
|
||||
var icons = await SearchIconsByKeywordAsync(keyword, actualLimit);
|
||||
allIcons.AddRange(icons);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "搜索图标失败,关键字:{Keyword}", keyword);
|
||||
}
|
||||
}
|
||||
|
||||
return allIcons;
|
||||
}
|
||||
|
||||
private async Task<List<IconCandidate>> SearchIconsByKeywordAsync(string keyword, int limit)
|
||||
{
|
||||
var url = $"{_settings.ApiUrl}?query={Uri.EscapeDataString(keyword)}&limit={limit}";
|
||||
var response = await CallApiWithRetryAsync(url);
|
||||
|
||||
if (string.IsNullOrEmpty(response))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var apiResponse = JsonSerializer.Deserialize<IconifyApiResponse>(response);
|
||||
if (apiResponse?.Icons == null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// 解析字符串格式 "collection:iconName" 为 IconCandidate
|
||||
var candidates = apiResponse.Icons
|
||||
.Select(iconStr =>
|
||||
{
|
||||
var parts = iconStr.Split(':', 2);
|
||||
if (parts.Length != 2)
|
||||
{
|
||||
logger.LogWarning("无效的图标标识符格式:{IconStr}", iconStr);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new IconCandidate
|
||||
{
|
||||
CollectionName = parts[0],
|
||||
IconName = parts[1]
|
||||
};
|
||||
})
|
||||
.Where(c => c != null)
|
||||
.Cast<IconCandidate>()
|
||||
.ToList();
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private async Task<string> CallApiWithRetryAsync(string url)
|
||||
{
|
||||
var retryCount = 0;
|
||||
var delay = _settings.RetryDelayMs;
|
||||
|
||||
while (retryCount < _settings.MaxRetryCount)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
catch (HttpRequestException ex) when (retryCount < _settings.MaxRetryCount - 1)
|
||||
{
|
||||
logger.LogWarning(ex, "Iconify API调用失败,等待 {DelayMs}ms 后重试({RetryCount}/{MaxRetryCount})",
|
||||
delay, retryCount + 1, _settings.MaxRetryCount);
|
||||
await Task.Delay(delay);
|
||||
delay *= 2;
|
||||
retryCount++;
|
||||
}
|
||||
}
|
||||
|
||||
throw new HttpRequestException($"Iconify API调用失败,已重试 {_settings.MaxRetryCount} 次");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user