From 00fae01eed9cfdcbb06088479ec90a6a4923b593 Mon Sep 17 00:00:00 2001
From: duxingchen
Date: Wed, 12 Aug 2026 13:35:23 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20=E7=9C=8B=E6=9D=BF4=E9=A1=B9=E4=BD=93?=
=?UTF-8?q?=E9=AA=8C=E4=BF=AE=E5=A4=8D=20=E2=80=94=20=E6=97=B6=E9=97=B4/?=
=?UTF-8?q?=E8=BA=AB=E4=BB=BD=E8=AF=81/=E6=97=A5=E6=9C=9F=E7=AD=9B?=
=?UTF-8?q?=E9=80=89/=E9=80=9A=E7=9F=A5=E8=B7=B3=E8=BD=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
1. 完整16位产品身份证号显示(不再截断)
2. 日期筛选栏: 今天 | 近7天 | 近30天 | 全部
- 后端 recent-activity 支持 since/until ISO参数
- 默认展示今天动态,可按时间范围切换
3. 操作人兜底: TaskLog无operator_id时用任务assignee_id
4. 未读通知可点击跳转通知页面 + 快捷入口增加通知中心
---
backend/app/api/v1/endpoints/dashboard.py | 9 +-
backend/app/services/dashboard_service.py | 37 +++--
frontend/src/pages/admin/AdminDashboard.tsx | 163 +++++++++++++-------
frontend/src/services/dashboardApi.ts | 11 +-
4 files changed, 151 insertions(+), 69 deletions(-)
diff --git a/backend/app/api/v1/endpoints/dashboard.py b/backend/app/api/v1/endpoints/dashboard.py
index eb0f501..7ec55fc 100644
--- a/backend/app/api/v1/endpoints/dashboard.py
+++ b/backend/app/api/v1/endpoints/dashboard.py
@@ -1,4 +1,5 @@
"""Dashboard API"""
+from datetime import datetime
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
@@ -18,7 +19,11 @@ async def dashboard_stats(db: AsyncSession = Depends(get_db)):
@router.get("/recent-activity", response_model=list[RecentActivity])
async def recent_activity(
limit: int = Query(10, ge=1, le=50),
+ since: str | None = Query(None, description="起始日期 ISO格式 如 2026-08-12T00:00:00"),
+ until: str | None = Query(None, description="截止日期 ISO格式"),
db: AsyncSession = Depends(get_db),
):
- """最近任务动态 — 看板活动时间线"""
- return await get_recent_activity(db, limit)
+ """最近任务动态 — 支持日期范围筛选"""
+ since_dt = datetime.fromisoformat(since) if since else None
+ until_dt = datetime.fromisoformat(until) if until else None
+ return await get_recent_activity(db, limit, since=since_dt, until=until_dt)
diff --git a/backend/app/services/dashboard_service.py b/backend/app/services/dashboard_service.py
index f2fe875..6fe78fa 100644
--- a/backend/app/services/dashboard_service.py
+++ b/backend/app/services/dashboard_service.py
@@ -1,4 +1,5 @@
"""Dashboard 统计服务"""
+from datetime import datetime
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel
@@ -69,31 +70,43 @@ async def get_dashboard_stats(db: AsyncSession) -> DashboardStats:
)
-async def get_recent_activity(db: AsyncSession, limit: int = 10) -> list[RecentActivity]:
+async def get_recent_activity(
+ db: AsyncSession, limit: int = 10,
+ since: datetime | None = None, until: datetime | None = None,
+) -> list[RecentActivity]:
from app.models.task_log import TaskLog
from app.models.task import Task
from app.models.product import Product
from app.core.time_utils import BEIJING_TZ
stmt = (
- select(TaskLog, Task.task_name, Product.serial_number)
+ select(TaskLog, Task.task_name, Product.serial_number, Task.assignee_id)
.join(Task, TaskLog.task_id == Task.id)
.join(Product, Task.product_id == Product.id)
- .order_by(TaskLog.created_at.desc())
- .limit(limit)
)
+ if since:
+ stmt = stmt.where(TaskLog.created_at >= since)
+ if until:
+ stmt = stmt.where(TaskLog.created_at <= until)
+ stmt = stmt.order_by(TaskLog.created_at.desc()).limit(limit)
result = await db.execute(stmt)
rows = result.all()
- # 收集 operator_id → 批量翻译中文姓名
- operator_ids = list({row[0].operator_id for row in rows if row[0].operator_id})
+ # 收集 operator_id + 兜底 assignee_id → 批量翻译中文姓名
+ raw_ids: set[str] = set()
+ for row in rows:
+ op = row[0].operator_id
+ if op:
+ raw_ids.add(op)
+ elif row[3]: # assignee_id 兜底
+ raw_ids.add(row[3])
name_map: dict[str, str] = {}
- if operator_ids:
+ if raw_ids:
from app.services.mom_cache import get_display_names
- name_map = get_display_names(operator_ids)
+ name_map = get_display_names(list(raw_ids))
activities: list[RecentActivity] = []
- for log, task_name, product_sn in rows:
+ for log, task_name, product_sn, task_assignee in rows:
action_label = _action_label(log.action_type)
# 强制转北京时间显示
t = log.created_at
@@ -105,9 +118,9 @@ async def get_recent_activity(db: AsyncSession, limit: int = 10) -> list[RecentA
time_str = t.strftime("%m-%d %H:%M")
else:
time_str = ""
- # operator_id: 优先显示中文姓名 → 英文用户名兜底 → 无记录时为空
- op = log.operator_id
- op_display = name_map.get(op, op or "")
+ # operator_id: 优先显示中文姓名 → 英文用户名兜底 → 任务负责人兜底 → 空
+ op = log.operator_id or task_assignee or ""
+ op_display = name_map.get(op, op)
activities.append(RecentActivity(
action=action_label,
task_name=task_name or "",
diff --git a/frontend/src/pages/admin/AdminDashboard.tsx b/frontend/src/pages/admin/AdminDashboard.tsx
index 262c3e0..4172808 100644
--- a/frontend/src/pages/admin/AdminDashboard.tsx
+++ b/frontend/src/pages/admin/AdminDashboard.tsx
@@ -1,7 +1,7 @@
-import { useEffect, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
import {
Package, ClipboardList, Bell, TrendingUp, AlertTriangle,
- RefreshCw, Loader2, AlertCircle, Plus, ArrowRight,
+ RefreshCw, Loader2, AlertCircle, Plus, ArrowRight, Calendar,
} from "lucide-react";
import { useNavigate } from "react-router-dom";
import {
@@ -9,14 +9,22 @@ import {
type DashboardStats, type RecentActivity,
} from "../../services/dashboardApi";
-// ─── 小卡片 ───────────────────────────────────────────────
-function MiniStat({ value, label, color }: { value: number; label: string; color: string }) {
- return (
-
- );
+// ─── 日期筛选选项 ─────────────────────────────────────────
+type DateRange = "today" | "7d" | "30d" | "all";
+const DATE_OPTIONS: { key: DateRange; label: string }[] = [
+ { key: "today", label: "今天" },
+ { key: "7d", label: "近7天" },
+ { key: "30d", label: "近30天" },
+ { key: "all", label: "全部" },
+];
+
+function getSince(key: DateRange): string | undefined {
+ if (key === "all") return undefined;
+ const now = new Date();
+ now.setHours(0, 0, 0, 0);
+ if (key === "today") return now.toISOString();
+ now.setDate(now.getDate() - (key === "7d" ? 7 : 30));
+ return now.toISOString();
}
// ─── 进度条 ───────────────────────────────────────────────
@@ -65,11 +73,14 @@ function ActivityItem({ a }: { a: RecentActivity }) {
{a.action}
{a.task_name}
+ {a.remark && (
+ — {a.remark.slice(0, 30)}{a.remark.length > 30 ? "…" : ""}
+ )}
- {a.operator}
+ {a.operator || "—"}
·
- {a.product_sn.slice(0, 8)}…
+ {a.product_sn}
{a.time}
@@ -83,17 +94,26 @@ export default function AdminDashboard() {
const [activity, setActivity] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
+ const [dateRange, setDateRange] = useState("today");
const navigate = useNavigate();
- useEffect(() => {
+ const loadData = (dr: DateRange) => {
+ const since = getSince(dr);
Promise.all([
fetchDashboardStats(),
- fetchRecentActivity(8),
+ fetchRecentActivity(15, since),
])
- .then(([s, a]) => { setStats(s); setActivity(a); })
- .catch(() => setError("加载统计数据失败,请确认后端已启动"))
+ .then(([s, a]) => { setStats(s); setActivity(a); setError(null); })
+ .catch(() => setError("加载失败,请确认后端已启动"))
.finally(() => setLoading(false));
- }, []);
+ };
+
+ useEffect(() => { loadData(dateRange); }, [dateRange]);
+
+ const handleDateChange = (dr: DateRange) => {
+ setLoading(true);
+ setDateRange(dr);
+ };
// ── 加载态 ──
if (loading) {
@@ -109,6 +129,8 @@ export default function AdminDashboard() {
return (
{error || "数据为空"}
+
);
}
@@ -116,17 +138,34 @@ export default function AdminDashboard() {
return (
{/* ═══ 页头 ═══ */}
-
+
📊 生产管理看板
产品 = 物理实体(身份证)| 任务 = 工序节点(流转步骤)
-
+
+ {/* 日期筛选 */}
+
+ {DATE_OPTIONS.map(opt => (
+
+ ))}
+
+
+
{/* ═══ 第1行:4 张概览卡片 ═══ */}
@@ -155,7 +194,7 @@ export default function AdminDashboard() {
total={stats.tasks_total} labels={["待接收", "进行中", "已完成"]} />
- {/* 品质 & 通知 */}
+ {/* 品质 & 通知 — 通知可点击 */}
@@ -166,15 +205,27 @@ export default function AdminDashboard() {
{stats.tasks_rejected + stats.tasks_rework}
驳回/返工
-
+
+
未读通知 ↗
+
-
-
-
+
+
{stats.tasks_rejected}
+
已驳回
+
+
+
{stats.tasks_rework}
+
返工中
+
+
+
{stats.unread_notifications}
+
未读消息
+
@@ -190,15 +241,12 @@ export default function AdminDashboard() {
{stats.tasks_completed}/{stats.tasks_total} 已完成
- {/* 简易环形图 */}
-
-
-
+
@@ -207,11 +255,18 @@ export default function AdminDashboard() {
{/* 最近动态 */}
-
🕐 最近流转动态
- 最新 8 条
+
+ 最近流转动态
+
+
+ {dateRange === "today" ? "今天" : dateRange === "7d" ? "近7天" : dateRange === "30d" ? "近30天" : "全部"}
+ · {activity.length} 条
+
{activity.length === 0 ? (
-
暂无流转记录
+
+ {dateRange === "today" ? "今天暂无流转记录" : "该时间段暂无流转记录"}
+
) : (
{activity.map((a, i) =>
)}
@@ -225,7 +280,7 @@ export default function AdminDashboard() {
+
-
- {/* 说明卡片 */}
- 💡 怎么理解?
- 一个产品从创建到入库,会经过多道
- 工序(装配→接线→质检…)。
- 每道工序就是一个任务,由不同工人完成。
- 所以任务数 ≥ 产品数是正常的。
+ 💡 一个产品 = 多道工序
+ 产品从创建到入库,经过装配→接线→质检等多道工序。
+ 每个工序就是一个任务,
+ 由不同工人完成。任务数 ≥ 产品数是正常的。
diff --git a/frontend/src/services/dashboardApi.ts b/frontend/src/services/dashboardApi.ts
index 4ebcc46..361f4b1 100644
--- a/frontend/src/services/dashboardApi.ts
+++ b/frontend/src/services/dashboardApi.ts
@@ -28,7 +28,14 @@ export async function fetchDashboardStats(): Promise
{
return data;
}
-export async function fetchRecentActivity(limit = 10): Promise {
- const { data } = await api.get("/dashboard/recent-activity", { params: { limit } });
+export async function fetchRecentActivity(
+ limit = 10,
+ since?: string,
+ until?: string,
+): Promise {
+ const params: Record = { limit };
+ if (since) params.since = since;
+ if (until) params.until = until;
+ const { data } = await api.get("/dashboard/recent-activity", { params });
return data;
}