Files
EmailBill/.doc/ICON_SEARCH_BUG_FIX.md
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

214 lines
5.1 KiB
Markdown
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.
# Bug 修复报告:图标搜索 API 调用问题
**日期**: 2026-02-16
**严重程度**: 高(阻止功能使用)
**状态**: ✅ 已修复
## 问题描述
用户在前端调用图标搜索 API 时遇到 400 错误:
```json
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"request": [
"The request field is required."
],
"$.keywords": [
"The JSON value could not be converted to System.Collections.Generic.List`1[System.String]..."
]
}
}
```
## 根本原因
`Web/src/views/ClassificationEdit.vue` 中,`searchIcons` API 调用传递了错误的参数类型。
### 错误代码(第 377-387 行)
```javascript
const { success: keywordsSuccess, data: keywords } = await generateSearchKeywords(category.name)
if (!keywordsSuccess || !keywords || keywords.length === 0) {
showToast('生成搜索关键字失败')
return
}
// ❌ 错误keywords 是 SearchKeywordsResponse 对象,不是数组
const { success: iconsSuccess, data: icons } = await searchIcons(keywords)
```
### 问题分析
1. `generateSearchKeywords()` 返回的 `data``SearchKeywordsResponse` 对象:
```javascript
{
keywords: ["food", "restaurant", "dining"]
}
```
2. 代码错误地将整个对象传递给 `searchIcons()`
```javascript
// 实际发送的请求体
{
keywords: {
keywords: ["food", "restaurant"]
}
}
```
3. 后端期望的格式:
```javascript
{
keywords: ["food", "restaurant"] // 数组,不是对象
}
```
## 修复方案
### 修复后的代码
```javascript
const { success: keywordsSuccess, data: keywordsResponse } = await generateSearchKeywords(category.name)
if (!keywordsSuccess || !keywordsResponse || !keywordsResponse.keywords || keywordsResponse.keywords.length === 0) {
showToast('生成搜索关键字失败')
return
}
// ✅ 正确:提取 keywords 数组
const { success: iconsSuccess, data: icons } = await searchIcons(keywordsResponse.keywords)
```
### 关键变更
1. 重命名变量:`data: keywords` → `data: keywordsResponse`(更清晰)
2. 访问嵌套属性:`keywordsResponse.keywords`
3. 更新验证逻辑:检查 `keywordsResponse.keywords` 是否存在
## 影响范围
- **受影响文件**: `Web/src/views/ClassificationEdit.vue`
- **受影响功能**: 分类图标选择功能
- **用户影响**: 无法为分类选择 Iconify 图标
## 测试验证
### 1. 单元测试
已有的 130 个测试用例验证后端 API 正确性:
- ✅ IconController 集成测试通过
- ✅ Service 层单元测试通过
### 2. 手动测试步骤
```bash
# 1. 启动后端
cd WebApi
dotnet run
# 2. 启动前端
cd Web
pnpm dev
# 3. 测试流程
# - 访问分类管理页面
# - 点击"选择图标"按钮
# - 验证图标选择器正常打开
# - 搜索并选择图标
# - 确认图标正确保存
```
### 3. API 测试脚本
参见 `.doc/test-icon-api.sh` 脚本:
```bash
# 测试搜索图标 API
curl -X POST http://localhost:5071/api/icons/search \
-H "Content-Type: application/json" \
-d '{"keywords": ["food", "restaurant"]}'
# 预期响应
{
"success": true,
"data": [
{
"collectionName": "mdi",
"iconName": "food",
"iconIdentifier": "mdi:food"
},
...
]
}
```
## 预防措施
### 1. 类型安全改进
考虑将前端 API 客户端迁移到 TypeScript
```typescript
interface SearchKeywordsResponse {
keywords: string[]
}
export const generateSearchKeywords = async (categoryName: string): Promise<ApiResponse<SearchKeywordsResponse>> => {
// TypeScript 会在编译时捕获类型错误
}
```
### 2. API 客户端注释改进
更新 `Web/src/api/icons.js` 的 JSDoc
```javascript
/**
* 生成搜索关键字
* @param {string} categoryName - 分类名称
* @returns {Promise<{success: boolean, data: {keywords: string[]}}>}
* 注意: data 是对象,包含 keywords 数组字段
*/
```
### 3. 单元测试补充
为前端组件添加单元测试,验证 API 调用参数:
```javascript
// ClassificationEdit.spec.js
describe('ClassificationEdit - Icon Selection', () => {
it('should pass keywords array to searchIcons', async () => {
const mockKeywords = { keywords: ['food', 'restaurant'] }
generateSearchKeywords.mockResolvedValue({ success: true, data: mockKeywords })
await openIconSelector(category)
expect(searchIcons).toHaveBeenCalledWith(['food', 'restaurant'])
})
})
```
## 相关文档
- **API 文档**: `.doc/ICONIFY_INTEGRATION.md`
- **任务列表**: `openspec/changes/icon-search-integration/tasks.md`
- **测试脚本**: `.doc/test-icon-api.sh`
## 经验教训
1. **响应结构验证**: 在使用 API 响应数据前,应验证数据结构
2. **变量命名清晰**: 使用清晰的变量名(如 `keywordsResponse` 而非 `keywords`
3. **类型安全**: TypeScript 可以在编译时捕获此类错误
4. **测试覆盖**: 需要为前端组件添加集成测试
---
**修复者**: AI Assistant
**审核者**: 待审核
**最后更新**: 2026-02-16