diff --git a/backend/app/api/v1/__init__.py b/backend/app/api/v1/__init__.py index c7705e07..e601b4de 100644 --- a/backend/app/api/v1/__init__.py +++ b/backend/app/api/v1/__init__.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from app.api.v1 import auth, user, datasource, metadata, classification, project, task, report +from app.api.v1 import auth, user, datasource, metadata, classification, project, task, report, dashboard api_router = APIRouter() api_router.include_router(auth.router, prefix="/auth", tags=["认证"]) @@ -11,3 +11,4 @@ api_router.include_router(classification.router, prefix="/classifications", tags api_router.include_router(project.router, prefix="/projects", tags=["项目管理"]) api_router.include_router(task.router, prefix="/tasks", tags=["任务管理"]) api_router.include_router(report.router, prefix="/reports", tags=["报告管理"]) +api_router.include_router(dashboard.router, prefix="/dashboard", tags=["仪表盘"]) diff --git a/backend/app/api/v1/dashboard.py b/backend/app/api/v1/dashboard.py new file mode 100644 index 00000000..f9a60f63 --- /dev/null +++ b/backend/app/api/v1/dashboard.py @@ -0,0 +1,113 @@ +from typing import Optional +from fastapi import APIRouter, Depends, Query +from sqlalchemy.orm import Session +from sqlalchemy import func + +from app.core.database import get_db +from app.models.user import User +from app.models.metadata import DataSource, DataTable, DataColumn +from app.models.project import ClassificationResult, ClassificationProject +from app.models.classification import Category, DataLevel +from app.schemas.common import ResponseModel +from app.api.deps import get_current_user + +router = APIRouter() + + +@router.get("/stats") +def get_dashboard_stats( + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """Dashboard overview statistics based on real DB data.""" + data_sources = db.query(DataSource).count() + tables = db.query(DataTable).count() + columns = db.query(DataColumn).count() + labeled = db.query(ClassificationResult).count() + sensitive = db.query(ClassificationResult).join(DataLevel).filter( + DataLevel.code.in_(['L4', 'L5']) + ).count() + projects = db.query(ClassificationProject).count() + + return ResponseModel(data={ + "data_sources": data_sources, + "tables": tables, + "columns": columns, + "labeled": labeled, + "sensitive": sensitive, + "projects": projects, + }) + + +@router.get("/distribution") +def get_dashboard_distribution( + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """Distribution data for charts based on real DB data.""" + # Level distribution + level_dist = db.query(DataLevel.name, DataLevel.code, DataLevel.color, func.count(ClassificationResult.id)).\ + join(ClassificationResult, DataLevel.id == ClassificationResult.level_id).\ + group_by(DataLevel.id).\ + order_by(DataLevel.sort_order).all() + + # Category distribution + category_dist = db.query(Category.name, func.count(ClassificationResult.id)).\ + join(ClassificationResult, Category.id == ClassificationResult.category_id).\ + group_by(Category.id).\ + order_by(func.count(ClassificationResult.id).desc()).limit(8).all() + + # Source distribution + source_dist = db.query(ClassificationResult.source, func.count(ClassificationResult.id)).\ + group_by(ClassificationResult.source).all() + + # Project progress + projects = db.query(ClassificationProject).all() + project_progress = [] + for p in projects: + total = db.query(ClassificationResult).filter(ClassificationResult.project_id == p.id).count() + reviewed = db.query(ClassificationResult).filter( + ClassificationResult.project_id == p.id, + ClassificationResult.status == 'reviewed', + ).count() + project_progress.append({ + "id": p.id, + "name": p.name, + "status": p.status, + "progress": round(reviewed / total * 100) if total else 0, + "planned_end": p.planned_end.isoformat() if p.planned_end else None, + }) + + # Heatmap: source vs level + sources = db.query(DataSource).order_by(DataSource.id).limit(8).all() + levels = db.query(DataLevel).order_by(DataLevel.sort_order).all() + heatmap = [] + for si, source in enumerate(sources): + for li, level in enumerate(levels): + count = db.query(func.count(ClassificationResult.id)).\ + join(DataColumn, ClassificationResult.column_id == DataColumn.id).\ + join(DataTable, DataColumn.table_id == DataTable.id).\ + join(DataSource, DataTable.database_id == DataSource.id).\ + filter(DataSource.id == source.id, ClassificationResult.level_id == level.id).scalar() + heatmap.append({ + "source_name": source.name, + "level_code": level.code, + "count": count or 0, + }) + + return ResponseModel(data={ + "level_distribution": [ + {"name": name, "code": code, "color": color, "count": count} + for name, code, color, count in level_dist + ], + "category_distribution": [ + {"name": name, "count": count} + for name, count in category_dist + ], + "source_distribution": [ + {"source": src, "count": count} + for src, count in source_dist + ], + "project_progress": project_progress, + "heatmap": heatmap, + }) diff --git a/backend/app/api/v1/report.py b/backend/app/api/v1/report.py index a6b7c0d6..4bb5f2fb 100644 --- a/backend/app/api/v1/report.py +++ b/backend/app/api/v1/report.py @@ -1,14 +1,46 @@ from fastapi import APIRouter, Depends, Response from sqlalchemy.orm import Session +from sqlalchemy import func from app.core.database import get_db from app.models.user import User -from app.services import report_service +from app.models.project import ClassificationResult, ClassificationProject +from app.models.classification import Category, DataLevel +from app.schemas.common import ResponseModel from app.api.deps import get_current_user +from app.services import report_service router = APIRouter() +@router.get("/stats") +def get_report_stats( + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """Global report statistics.""" + total = db.query(ClassificationResult).count() + auto = db.query(ClassificationResult).filter(ClassificationResult.source == 'auto').count() + manual = db.query(ClassificationResult).filter(ClassificationResult.source == 'manual').count() + reviewed = db.query(ClassificationResult).filter(ClassificationResult.status == 'reviewed').count() + + # Level distribution + level_dist = db.query(DataLevel.name, DataLevel.code, func.count(ClassificationResult.id)).\ + join(ClassificationResult, DataLevel.id == ClassificationResult.level_id).\ + group_by(DataLevel.id).order_by(DataLevel.sort_order).all() + + return ResponseModel(data={ + "total": total, + "auto": auto, + "manual": manual, + "reviewed": reviewed, + "level_distribution": [ + {"name": name, "code": code, "count": count} + for name, code, count in level_dist + ], + }) + + @router.get("/projects/{project_id}/download") def download_report( project_id: int, diff --git a/backend/app/models/project.py b/backend/app/models/project.py index f5a2e83b..43e90258 100644 --- a/backend/app/models/project.py +++ b/backend/app/models/project.py @@ -48,6 +48,7 @@ class ClassificationProject(Base): created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + template = relationship("ClassificationTemplate") tasks = relationship("ClassificationTask", back_populates="project", cascade="all, delete-orphan") results = relationship("ClassificationResult", back_populates="project", cascade="all, delete-orphan") diff --git a/frontend/src/api/dashboard.ts b/frontend/src/api/dashboard.ts new file mode 100644 index 00000000..745bb884 --- /dev/null +++ b/frontend/src/api/dashboard.ts @@ -0,0 +1,57 @@ +import request from './request' + +export interface DashboardStats { + data_sources: number + tables: number + columns: number + labeled: number + sensitive: number + projects: number +} + +export interface LevelDistItem { + name: string + code: string + color: string + count: number +} + +export interface CategoryDistItem { + name: string + count: number +} + +export interface SourceDistItem { + source: string + count: number +} + +export interface ProjectProgressItem { + id: number + name: string + status: string + progress: number + planned_end: string | null +} + +export interface HeatmapItem { + source_name: string + level_code: string + count: number +} + +export interface DashboardDistribution { + level_distribution: LevelDistItem[] + category_distribution: CategoryDistItem[] + source_distribution: SourceDistItem[] + project_progress: ProjectProgressItem[] + heatmap: HeatmapItem[] +} + +export function getDashboardStats() { + return request.get('/dashboard/stats') +} + +export function getDashboardDistribution() { + return request.get('/dashboard/distribution') +} diff --git a/frontend/src/api/report.ts b/frontend/src/api/report.ts new file mode 100644 index 00000000..3da52c07 --- /dev/null +++ b/frontend/src/api/report.ts @@ -0,0 +1,31 @@ +import request from './request' + +export interface ReportStats { + total: number + auto: number + manual: number + reviewed: number + level_distribution: { + name: string + code: string + count: number + }[] +} + +export function getReportStats() { + return request.get('/reports/stats') +} + +export function downloadReport(projectId: number) { + const token = localStorage.getItem('pdg_token') + const url = `/api/v1/reports/projects/${projectId}/download` + const a = document.createElement('a') + a.href = url + a.download = `report_project_${projectId}.docx` + if (token) { + a.setAttribute('data-token', token) + } + document.body.appendChild(a) + a.click() + document.body.removeChild(a) +} diff --git a/frontend/src/views/dashboard/Dashboard.vue b/frontend/src/views/dashboard/Dashboard.vue index b730133a..b38a09a1 100644 --- a/frontend/src/views/dashboard/Dashboard.vue +++ b/frontend/src/views/dashboard/Dashboard.vue @@ -9,7 +9,7 @@
-
{{ stats.dataSources }}
+
{{ stats.data_sources }}
数据源
@@ -98,27 +98,35 @@