- 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
118 lines
3.6 KiB
C#
118 lines
3.6 KiB
C#
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} 次");
|
||
}
|
||
}
|