From 5119ca775b26255383d3bfd0aa5f6705deb7490d Mon Sep 17 00:00:00 2001 From: hiderfong Date: Thu, 23 Apr 2026 10:46:51 +0800 Subject: [PATCH] fix: classification results empty and res.data access issues Backend: - Add GET /classifications/results endpoint with project/level/keyword filters - Add column relationship to ClassificationResult model - Fix test data generator to fetch column IDs from DB after bulk insert Frontend: - Fix request.ts interceptor to return full response body (keep total/pagination) - Fix all pages to use res.data instead of res - Add getClassificationResults API in classification.ts - Implement fetchData in Classification.vue with proper filtering and pagination - Fix same res.data issue in Category.vue, Metadata.vue, Project.vue, DataSource.vue, Dashboard.vue, Task.vue --- backend/app/api/v1/classification.py | 48 +++++++++++++++++++ backend/app/models/project.py | 1 + backend/scripts/generate_test_data.py | 4 +- frontend/src/api/classification.ts | 31 ++++++++++++ frontend/src/api/request.ts | 2 +- frontend/src/views/category/Category.vue | 6 +-- .../views/classification/Classification.vue | 22 ++++----- frontend/src/views/metadata/Metadata.vue | 2 +- frontend/src/views/task/Task.vue | 4 +- 9 files changed, 100 insertions(+), 20 deletions(-) diff --git a/backend/app/api/v1/classification.py b/backend/app/api/v1/classification.py index c3b12b72..9f0d530c 100644 --- a/backend/app/api/v1/classification.py +++ b/backend/app/api/v1/classification.py @@ -151,6 +151,54 @@ def list_templates( return ResponseModel(data=[TemplateOut.model_validate(i) for i in items]) +@router.get("/results", response_model=ListResponse) +def list_results( + project_id: Optional[int] = Query(None), + level_id: Optional[int] = Query(None), + keyword: Optional[str] = Query(None), + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=500), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + from app.services.project_service import list_results as _list_results + items, total = _list_results(db, project_id=project_id, keyword=keyword, page=page, page_size=page_size) + + data = [] + for r in items: + col = r.column + table = col.table if col else None + database = table.database if table else None + source = database.source if database else None + + # Filter by level_id if specified + if level_id and r.level_id != level_id: + continue + + data.append({ + "id": r.id, + "project_id": r.project_id, + "column_id": col.id if col else None, + "column_name": col.name if col else None, + "data_type": col.data_type if col else None, + "comment": col.comment if col else None, + "table_name": table.name if table else None, + "database_name": database.name if database else None, + "source_name": source.name if source else None, + "category_id": r.category_id, + "category_name": r.category.name if r.category else None, + "level_id": r.level_id, + "level_name": r.level.name if r.level else None, + "level_color": r.level.color if r.level else None, + "source": r.source, + "confidence": r.confidence, + "status": r.status, + "created_at": r.created_at.isoformat() if r.created_at else None, + }) + + return ListResponse(data=data, total=total, page=page, page_size=page_size) + + @router.post("/auto-classify/{project_id}") def auto_classify( project_id: int, diff --git a/backend/app/models/project.py b/backend/app/models/project.py index 6dce15d9..f5a2e83b 100644 --- a/backend/app/models/project.py +++ b/backend/app/models/project.py @@ -92,6 +92,7 @@ class ClassificationResult(Base): updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) project = relationship("ClassificationProject", back_populates="results") + column = relationship("DataColumn") category = relationship("Category") level = relationship("DataLevel") diff --git a/backend/scripts/generate_test_data.py b/backend/scripts/generate_test_data.py index 621d5a00..27bc86bb 100644 --- a/backend/scripts/generate_test_data.py +++ b/backend/scripts/generate_test_data.py @@ -417,7 +417,9 @@ print(f" Created {len(projects)} projects") # ============================================================ print("Generating classification results...") -all_col_ids = [c.id for c in all_columns] +# Re-fetch column IDs from DB since bulk_save_objects doesn't populate object IDs +col_rows = db.query(DataColumn.id).all() +all_col_ids = [c[0] for c in col_rows] random.shuffle(all_col_ids) result_batch = [] diff --git a/frontend/src/api/classification.ts b/frontend/src/api/classification.ts index 285a839f..2edfa4b6 100644 --- a/frontend/src/api/classification.ts +++ b/frontend/src/api/classification.ts @@ -76,3 +76,34 @@ export function deleteRule(id: number) { export function getTemplates() { return request.get('/classifications/templates') } + +export interface ClassificationResultItem { + id: number + project_id: number + column_id?: number + column_name?: string + data_type?: string + comment?: string + table_name?: string + database_name?: string + source_name?: string + category_id?: number + category_name?: string + level_id?: number + level_name?: string + level_color?: string + source: string + confidence: number + status: string + created_at?: string +} + +export function getClassificationResults(params: { + project_id?: number + level_id?: number + keyword?: string + page?: number + page_size?: number +}) { + return request.get('/classifications/results', { params }) +} diff --git a/frontend/src/api/request.ts b/frontend/src/api/request.ts index 5d627ac4..bbffed5c 100644 --- a/frontend/src/api/request.ts +++ b/frontend/src/api/request.ts @@ -26,7 +26,7 @@ request.interceptors.response.use( ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } - return res.data + return res }, (error: AxiosError) => { const status = error.response?.status diff --git a/frontend/src/views/category/Category.vue b/frontend/src/views/category/Category.vue index 4571844c..a7f45693 100644 --- a/frontend/src/views/category/Category.vue +++ b/frontend/src/views/category/Category.vue @@ -236,7 +236,7 @@ const ruleRules = { async function fetchLevels() { try { const res: any = await getDataLevels() - levels.value = res || [] + levels.value = res.data || [] } catch (e: any) { ElMessage.error(e?.message || '加载分级失败') } @@ -245,7 +245,7 @@ async function fetchLevels() { async function fetchCategories() { try { const res: any = await getCategoryTree() - categoryTree.value = res || [] + categoryTree.value = res.data || [] } catch (e: any) { ElMessage.error(e?.message || '加载分类失败') } @@ -267,7 +267,7 @@ async function fetchRules() { async function fetchTemplates() { try { const res: any = await getTemplates() - templates.value = res || [] + templates.value = res.data || [] } catch (e: any) { // ignore } diff --git a/frontend/src/views/classification/Classification.vue b/frontend/src/views/classification/Classification.vue index 7659e7ff..ba93df57 100644 --- a/frontend/src/views/classification/Classification.vue +++ b/frontend/src/views/classification/Classification.vue @@ -75,8 +75,7 @@ import { ref, onMounted } from 'vue' import { ElMessage } from 'element-plus' import { Search } from '@element-plus/icons-vue' import { getProjects } from '@/api/project' -import { getDataLevels } from '@/api/classification' -import { getTaskItems } from '@/api/task' +import { getDataLevels, getClassificationResults } from '@/api/classification' const loading = ref(false) const resultList = ref([]) @@ -100,16 +99,15 @@ function confidenceClass(v: number) { async function fetchData() { loading.value = true try { - if (!filterProjectId.value) { - resultList.value = [] - total.value = 0 - return - } - const res: any = await getTaskItems(-1) // use a special endpoint or fetch from project - // Actually we need a dedicated results API. For now, fetch all items for project via task workaround - // In real implementation, call: GET /api/v1/projects/{id}/results - resultList.value = [] - total.value = 0 + const res: any = await getClassificationResults({ + project_id: filterProjectId.value || undefined, + level_id: filterLevelId.value || undefined, + keyword: filterKeyword.value || undefined, + page: page.value, + page_size: pageSize.value, + }) + resultList.value = res.data || [] + total.value = res.total || 0 } catch (e: any) { ElMessage.error(e?.message || '加载失败') } finally { diff --git a/frontend/src/views/metadata/Metadata.vue b/frontend/src/views/metadata/Metadata.vue index 4e35b15b..725ce60d 100644 --- a/frontend/src/views/metadata/Metadata.vue +++ b/frontend/src/views/metadata/Metadata.vue @@ -118,7 +118,7 @@ function filterNode(value: string, data: TreeNode) { async function fetchTree() { try { const res: any = await getMetadataTree() - treeData.value = res || [] + treeData.value = res.data || [] } catch (e: any) { ElMessage.error(e?.message || '加载失败') } diff --git a/frontend/src/views/task/Task.vue b/frontend/src/views/task/Task.vue index d3d4e95c..7d7cfc90 100644 --- a/frontend/src/views/task/Task.vue +++ b/frontend/src/views/task/Task.vue @@ -150,7 +150,7 @@ const filteredLabelItems = computed(() => { async function fetchData() { try { const res: any = await getMyTasks() - tasks.value = res || [] + tasks.value = res.data || [] } catch (e: any) { ElMessage.error(e?.message || '加载失败') } @@ -186,7 +186,7 @@ async function openLabel(task: TaskItem) { } try { const res: any = await getTaskItems(task.id) - labelItems.value = (res || []).map((item: any) => ({ + labelItems.value = (res.data || []).map((item: any) => ({ ...item, _category_id: item.category_id, _level_id: item.level_id,