feat(效能分析): 人员视图也支持自然天/工作日耗时切换

- capability 接口加 mode 参数,natural 用自然小时、workdays 用工作小时
- 人员视图柱状图随顶部按钮切换,排除休息日的耗时
- 切换按钮自动重新请求 capability 与 flow
This commit is contained in:
2026-08-28 15:38:32 +08:00
parent 4447a1f52d
commit 907473b18c
4 changed files with 13 additions and 5 deletions

View File

@ -21,6 +21,7 @@ async def capability_profile(
spec_models: str | None = Query(None, description="规格型号,逗号分隔(可选)"), spec_models: str | None = Query(None, description="规格型号,逗号分隔(可选)"),
since: str | None = Query(None, description="起始日期 ISO"), since: str | None = Query(None, description="起始日期 ISO"),
until: str | None = Query(None, description="截止日期 ISO"), until: str | None = Query(None, description="截止日期 ISO"),
mode: str = Query("workdays", description="耗时口径: workdays(工作小时,默认) / natural(自然小时)"),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
"""个人能力图谱 — X 轴=设备身份证,分组柱状图(单台设备总耗时)。""" """个人能力图谱 — X 轴=设备身份证,分组柱状图(单台设备总耗时)。"""
@ -30,7 +31,7 @@ async def capability_profile(
specs = [s.strip() for s in spec_models.split(",") if s.strip()] if spec_models else None specs = [s.strip() for s in spec_models.split(",") if s.strip()] if spec_models else None
return await get_capability_profile( return await get_capability_profile(
db, assignee_ids=ids, spec_models=specs, db, assignee_ids=ids, spec_models=specs,
since=since_dt, until=until_dt, since=since_dt, until=until_dt, mode=mode,
) )

View File

@ -116,10 +116,13 @@ def _to_bj(dt: datetime | None) -> datetime | None:
return dt return dt
def _duration_hours(start: datetime | None, end: datetime, holidays: set = None) -> float: def _duration_hours(start: datetime | None, end: datetime, holidays: set = None, mode: str = "workdays") -> float:
"""计算单台耗时(工作小时,排除周末/节假日),无开始时间返回 0。""" """计算单台耗时(小时),无开始时间返回 0。
mode=workdays: 排除周末/节假日的工作小时mode=natural: 自然小时。"""
if not start: if not start:
return 0.0 return 0.0
if mode == "natural":
return (end - start).total_seconds() / 3600
from app.core.time_utils import working_duration_hours from app.core.time_utils import working_duration_hours
return working_duration_hours(start, end, holidays) return working_duration_hours(start, end, holidays)
@ -134,6 +137,7 @@ async def get_capability_profile(
spec_models: list[str] | None = None, spec_models: list[str] | None = None,
since: datetime | None = None, since: datetime | None = None,
until: datetime | None = None, until: datetime | None = None,
mode: str = "workdays",
) -> CapabilityResponse: ) -> CapabilityResponse:
""" """
个人能力图谱单机颗粒度X 轴 = 设备身份证,每个负责人一条柱状 series。 个人能力图谱单机颗粒度X 轴 = 设备身份证,每个负责人一条柱状 series。
@ -204,7 +208,7 @@ async def get_capability_profile(
"hours": 0.0, "prio": 9, "hours": 0.0, "prio": 9,
"first_start": None, "last_completed": None, "first_start": None, "last_completed": None,
}) })
entry["hours"] += _duration_hours(start, end, holidays) entry["hours"] += _duration_hours(start, end, holidays, mode)
if start and (entry["first_start"] is None or start < entry["first_start"]): if start and (entry["first_start"] is None or start < entry["first_start"]):
entry["first_start"] = start entry["first_start"] = start
completed_dt = _to_bj(row[4]) if row[4] else None completed_dt = _to_bj(row[4]) if row[4] else None

View File

@ -120,8 +120,9 @@ export default function AnalyticsDashboard() {
q.since = range[0].startOf("day").toISOString(); q.since = range[0].startOf("day").toISOString();
q.until = range[1].endOf("day").toISOString(); q.until = range[1].endOf("day").toISOString();
} }
q.mode = totalDaysMode; // 自然天 / 工作日,控制人员视图耗时口径
return q; return q;
}, [assigneeIds, specModels, range]); }, [assigneeIds, specModels, range, totalDaysMode]);
const buildFlowQuery = useCallback((): FlowQuery => { const buildFlowQuery = useCallback((): FlowQuery => {
const q: FlowQuery = {}; const q: FlowQuery = {};

View File

@ -98,6 +98,7 @@ export interface CapabilityQuery {
spec_models?: string[]; spec_models?: string[];
since?: string; since?: string;
until?: string; until?: string;
mode?: "natural" | "workdays";
} }
export interface FlowQuery { export interface FlowQuery {
@ -116,6 +117,7 @@ export async function fetchCapabilityProfile(query: CapabilityQuery): Promise<Ca
if (query.spec_models?.length) params.spec_models = query.spec_models.join(","); if (query.spec_models?.length) params.spec_models = query.spec_models.join(",");
if (query.since) params.since = query.since; if (query.since) params.since = query.since;
if (query.until) params.until = query.until; if (query.until) params.until = query.until;
if (query.mode) params.mode = query.mode;
const { data } = await api.get<CapabilityResponse>("/analytics/capability", { params }); const { data } = await api.get<CapabilityResponse>("/analytics/capability", { params });
return data; return data;
} }