Files
EmailBill/Service/IconSearch/IconifyApiService.cs
SunCheng 9921cd5fdf 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
2026-02-16 21:55:38 +08:00

118 lines
3.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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} 次");
}
}