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 ( -
- {value} -

{label}

-
- ); +// ─── 日期筛选选项 ───────────────────────────────────────── +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} 已完成

- {/* 简易环形图 */} -
- - - 0 ? (stats.tasks_completed / stats.tasks_total) * 251 : 0} 251`} - strokeLinecap="round" /> - -
+ + + 0 ? (stats.tasks_completed / stats.tasks_total) * 251 : 0} 251`} + strokeLinecap="round" /> + @@ -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; }