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 @@