diff --git a/backend/app/api/v1/endpoints/dashboard.py b/backend/app/api/v1/endpoints/dashboard.py index cdcfbee..eb0f501 100644 --- a/backend/app/api/v1/endpoints/dashboard.py +++ b/backend/app/api/v1/endpoints/dashboard.py @@ -1,8 +1,11 @@ """Dashboard API""" -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Query from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db -from app.services.dashboard_service import get_dashboard_stats, DashboardStats +from app.services.dashboard_service import ( + get_dashboard_stats, DashboardStats, + get_recent_activity, RecentActivity, +) router = APIRouter(prefix="/dashboard", tags=["管理看板"]) @@ -10,3 +13,12 @@ router = APIRouter(prefix="/dashboard", tags=["管理看板"]) @router.get("/stats", response_model=DashboardStats) async def dashboard_stats(db: AsyncSession = Depends(get_db)): return await get_dashboard_stats(db) + + +@router.get("/recent-activity", response_model=list[RecentActivity]) +async def recent_activity( + limit: int = Query(10, ge=1, le=50), + db: AsyncSession = Depends(get_db), +): + """最近任务动态 — 看板活动时间线""" + return await get_recent_activity(db, limit) diff --git a/backend/app/services/dashboard_service.py b/backend/app/services/dashboard_service.py index 793b051..8786908 100644 --- a/backend/app/services/dashboard_service.py +++ b/backend/app/services/dashboard_service.py @@ -5,19 +5,38 @@ from pydantic import BaseModel class DashboardStats(BaseModel): + # 产品 products_total: int products_pending: int products_in_progress: int products_completed: int + # 任务 tasks_total: int tasks_pending: int tasks_in_progress: int tasks_completed: int + tasks_rejected: int + tasks_rework: int + # 通知 + unread_notifications: int = 0 + + +class RecentActivity(BaseModel): + action: str + task_name: str + operator: str + product_sn: str + time: str + remark: str | None = None async def get_dashboard_stats(db: AsyncSession) -> DashboardStats: from app.models.product import Product - from app.models.task import Task, TASK_STATUS_PENDING, TASK_STATUS_WIP, TASK_STATUS_COMPLETED + from app.models.task import ( + Task, TASK_STATUS_PENDING, TASK_STATUS_WIP, TASK_STATUS_COMPLETED, + TASK_STATUS_REJECTED, TASK_STATUS_ARCHIVED, + ) + from app.models.notification import Notification p_total = await db.scalar(select(func.count(Product.id))) p_pending = await db.scalar(select(func.count(Product.id)).where(Product.status == "pending")) @@ -28,6 +47,12 @@ async def get_dashboard_stats(db: AsyncSession) -> DashboardStats: t_pending = await db.scalar(select(func.count(Task.id)).where(Task.status == TASK_STATUS_PENDING)) t_progress = await db.scalar(select(func.count(Task.id)).where(Task.status == TASK_STATUS_WIP)) t_done = await db.scalar(select(func.count(Task.id)).where(Task.status == TASK_STATUS_COMPLETED)) + t_rejected = await db.scalar(select(func.count(Task.id)).where(Task.status == TASK_STATUS_REJECTED)) + t_rework = await db.scalar(select(func.count(Task.id)).where(Task.is_rework.is_(True))) + + unread = await db.scalar( + select(func.count(Notification.id)).where(Notification.is_read.is_(False)) + ) return DashboardStats( products_total=p_total or 0, @@ -38,4 +63,50 @@ async def get_dashboard_stats(db: AsyncSession) -> DashboardStats: tasks_pending=t_pending or 0, tasks_in_progress=t_progress or 0, tasks_completed=t_done or 0, + tasks_rejected=t_rejected or 0, + tasks_rework=t_rework or 0, + unread_notifications=unread or 0, ) + + +async def get_recent_activity(db: AsyncSession, limit: int = 10) -> list[RecentActivity]: + from app.models.task_log import TaskLog + from app.models.task import Task + from app.models.product import Product + + stmt = ( + select(TaskLog, Task.task_name, Product.serial_number) + .join(Task, TaskLog.task_id == Task.id) + .join(Product, Task.product_id == Product.id) + .order_by(TaskLog.created_at.desc()) + .limit(limit) + ) + result = await db.execute(stmt) + rows = result.all() + + activities: list[RecentActivity] = [] + for log, task_name, product_sn in rows: + action_label = _action_label(log.action_type) + time_str = log.created_at.strftime("%m-%d %H:%M") if log.created_at else "" + activities.append(RecentActivity( + action=action_label, + task_name=task_name or "", + operator=log.operator_id or "系统", + product_sn=product_sn or "", + time=time_str, + remark=log.remark, + )) + return activities + + +def _action_label(action_type: str) -> str: + labels = { + "create": "创建任务", + "receive": "确认接收", + "complete": "完成任务", + "transfer": "完工转交", + "reject": "品质驳回", + "end": "结束分支", + "recall": "撤回转交", + } + return labels.get(action_type, action_type) diff --git a/frontend/src/pages/admin/AdminDashboard.tsx b/frontend/src/pages/admin/AdminDashboard.tsx index 004a9a2..262c3e0 100644 --- a/frontend/src/pages/admin/AdminDashboard.tsx +++ b/frontend/src/pages/admin/AdminDashboard.tsx @@ -1,80 +1,261 @@ import { useEffect, useState } from "react"; -import { Package, ClipboardList, Loader2, AlertCircle } from "lucide-react"; -import { fetchDashboardStats, type DashboardStats } from "../../services/dashboardApi"; +import { + Package, ClipboardList, Bell, TrendingUp, AlertTriangle, + RefreshCw, Loader2, AlertCircle, Plus, ArrowRight, +} from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { + fetchDashboardStats, fetchRecentActivity, + type DashboardStats, type RecentActivity, +} from "../../services/dashboardApi"; -function StatCard({ - label, - total, - pending, - progress, - done, - icon: Icon, -}: { - label: string; - total: number; - pending: number; - progress: number; - done: number; - icon: React.ComponentType<{ className?: string }>; -}) { +// ─── 小卡片 ─────────────────────────────────────────────── +function MiniStat({ value, label, color }: { value: number; label: string; color: string }) { return ( -
{label}
+PC端与移动端共享同一后台数据
++ 产品 = 物理实体(身份证)| 任务 = 工序节点(流转步骤) +
+{stats.products_total} 个
+已登记的物理产品数量
+{stats.tasks_total} 个
+产品下所有工序任务汇总
+{stats.tasks_rejected + stats.tasks_rework}
+驳回/返工
+{stats.unread_notifications}
+未读通知
++ {stats.tasks_total > 0 ? Math.round((stats.tasks_completed / stats.tasks_total) * 100) : 0}% +
+{stats.tasks_completed}/{stats.tasks_total} 已完成
+
+ 💡 怎么理解?
+ 一个产品从创建到入库,会经过多道
+ 工序(装配→接线→质检…)。
+ 每道工序就是一个任务,由不同工人完成。
+ 所以任务数 ≥ 产品数是正常的。
+