diff --git a/backend/app/api/v1/endpoints/analytics.py b/backend/app/api/v1/endpoints/analytics.py index 0fa50d9..2e06f37 100644 --- a/backend/app/api/v1/endpoints/analytics.py +++ b/backend/app/api/v1/endpoints/analytics.py @@ -38,12 +38,13 @@ async def capability_profile( async def flow_compare( product_sns: str | None = Query(None, description="身份证,逗号分隔"), spec_models: str | None = Query(None, description="规格型号,逗号分隔(无 product_sns 时按型号取最近设备)"), + mode: str = Query("natural", description="时间口径: natural(自然小时) / workdays(工作小时,排除周末节假日)"), db: AsyncSession = Depends(get_db), ): """设备流转对比 — 每台设备各操作人耗时(堆叠柱状,按人堆叠,识别瓶颈)。""" sns = [s.strip() for s in product_sns.split(",") if s.strip()] if product_sns else None specs = [s.strip() for s in spec_models.split(",") if s.strip()] if spec_models else None - return await get_flow_compare(db, product_sns=sns, spec_models=specs) + return await get_flow_compare(db, product_sns=sns, spec_models=specs, mode=mode) @router.get("/options", response_model=AnalyticsOptions) diff --git a/backend/app/services/analytics_service.py b/backend/app/services/analytics_service.py index a64a45e..46a7832 100644 --- a/backend/app/services/analytics_service.py +++ b/backend/app/services/analytics_service.py @@ -280,6 +280,7 @@ async def get_flow_compare( db: AsyncSession, product_sns: list[str] | None = None, spec_models: list[str] | None = None, + mode: str = "natural", ) -> FlowResponse: """ 查询每台设备上各操作人的任务时间区间,拼装为「生命周期时间轴」区间图: @@ -426,10 +427,20 @@ async def get_flow_compare( # ── 按人分组,转为相对 T0 的小时偏移区间 ── by_assignee: dict[str, list[list]] = {} + # 工作日模式:把时间偏移换算为「排除周末/节假日的工作小时」,压缩休息日 + if mode == "workdays": + from app.core.time_utils import working_duration_hours as _wdh + from app.models.holiday import Holiday as _Holiday + hres = await db.execute(select(_Holiday.day)) + _holidays = {r[0] for r in hres} for it in intervals: t0 = t0_by_device[it["idx"]] - start_offset = round((it["start"] - t0).total_seconds() / 3600, 1) - end_offset = round((it["end"] - t0).total_seconds() / 3600, 1) + if mode == "workdays": + start_offset = _wdh(t0, it["start"], _holidays) + end_offset = _wdh(t0, it["end"], _holidays) + else: + start_offset = round((it["start"] - t0).total_seconds() / 3600, 1) + end_offset = round((it["end"] - t0).total_seconds() / 3600, 1) duration = round(end_offset - start_offset, 1) by_assignee.setdefault(it["assignee_id"], []).append( [it["idx"], start_offset, end_offset, it["task_name"], duration, it["is_main"]] diff --git a/frontend/src/pages/admin/AnalyticsDashboard.tsx b/frontend/src/pages/admin/AnalyticsDashboard.tsx index 08f8c39..c3c16a5 100644 --- a/frontend/src/pages/admin/AnalyticsDashboard.tsx +++ b/frontend/src/pages/admin/AnalyticsDashboard.tsx @@ -127,8 +127,9 @@ export default function AnalyticsDashboard() { const q: FlowQuery = {}; if (productSns.length) q.product_sns = productSns; if (specModels.length) q.spec_models = specModels; + q.mode = totalDaysMode; // 自然天 / 工作日,控制轨迹流转图时间口径 return q; - }, [productSns, specModels]); + }, [productSns, specModels, totalDaysMode]); // ─── 能力图谱(需选择人员才发起) ── const loadCapability = useCallback(async () => { diff --git a/frontend/src/services/analyticsApi.ts b/frontend/src/services/analyticsApi.ts index ff0a30b..f4e0150 100644 --- a/frontend/src/services/analyticsApi.ts +++ b/frontend/src/services/analyticsApi.ts @@ -103,6 +103,7 @@ export interface CapabilityQuery { export interface FlowQuery { product_sns?: string[]; spec_models?: string[]; + mode?: "natural" | "workdays"; } // ============================================================ @@ -123,6 +124,7 @@ export async function fetchFlowData(query: FlowQuery): Promise { const params: Record = {}; if (query.product_sns?.length) params.product_sns = query.product_sns.join(","); if (query.spec_models?.length) params.spec_models = query.spec_models.join(","); + if (query.mode) params.mode = query.mode; const { data } = await api.get("/analytics/flow", { params }); return data; }