feat: 新增效能分析看板(个人能力图谱 + 设备人员耗时对比)
- 前端:AnalyticsDashboard 页面 + BaseEChart 封装 + analyticsApi,路由与导航 - 后端:/analytics 系列接口(capability/flow/options/device-records) - 能力图谱单机颗粒度、流转对比按人堆叠识别瓶颈、点击下钻备注、筛选动态联动 - 依赖:引入 echarts,移除 echarts-for-react
This commit is contained in:
80
frontend/src/components/BaseEChart.tsx
Normal file
80
frontend/src/components/BaseEChart.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
/** 轻量级 ECharts 封装 — 原生 echarts/core + ResizeObserver 响应式缩放
|
||||
* 支持 onEvents(常规事件)与 onZrClick(ZRender 底层点击,扩大热区)。 */
|
||||
import { useEffect, useRef } from "react";
|
||||
import * as echarts from "echarts/core";
|
||||
import { LineChart, BarChart } from "echarts/charts";
|
||||
import {
|
||||
GridComponent, TooltipComponent, LegendComponent, DataZoomComponent,
|
||||
} from "echarts/components";
|
||||
import { CanvasRenderer } from "echarts/renderers";
|
||||
import type { EChartsCoreOption } from "echarts/core";
|
||||
|
||||
// 按需注册(新增图表/组件时在此追加,避免全量打包)
|
||||
echarts.use([
|
||||
LineChart,
|
||||
BarChart,
|
||||
GridComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
DataZoomComponent,
|
||||
CanvasRenderer,
|
||||
]);
|
||||
|
||||
type EChartsInstance = ReturnType<typeof echarts.init>;
|
||||
|
||||
interface BaseEChartProps {
|
||||
option: EChartsCoreOption;
|
||||
height?: number | string;
|
||||
className?: string;
|
||||
onEvents?: Record<string, (params: any) => void>;
|
||||
onZrClick?: (chart: EChartsInstance, event: any) => void;
|
||||
}
|
||||
|
||||
export default function BaseEChart({ option, height = 380, className, onEvents, onZrClick }: BaseEChartProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const chartRef = useRef<EChartsInstance | null>(null);
|
||||
const eventsRef = useRef(onEvents);
|
||||
eventsRef.current = onEvents;
|
||||
const onZrClickRef = useRef(onZrClick);
|
||||
onZrClickRef.current = onZrClick;
|
||||
|
||||
// 初始化(仅一次):init → setOption → 注册事件 → ResizeObserver
|
||||
useEffect(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const chart = echarts.init(el);
|
||||
chartRef.current = chart;
|
||||
chart.setOption(option);
|
||||
|
||||
// 常规 ECharts 事件(读 eventsRef,保证始终用最新 handler)
|
||||
Object.keys(eventsRef.current ?? {}).forEach((event) => {
|
||||
chart.on(event, (params: any) => {
|
||||
eventsRef.current?.[event]?.(params);
|
||||
});
|
||||
});
|
||||
|
||||
// ZRender 底层点击(点击列阴影/背景也能触发,热区更大)
|
||||
chart.getZr().on("click", (e: any) => {
|
||||
onZrClickRef.current?.(chart, e);
|
||||
});
|
||||
|
||||
const observer = new ResizeObserver(() => chart.resize());
|
||||
observer.observe(el);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
chart.dispose();
|
||||
chartRef.current = null;
|
||||
};
|
||||
// 仅挂载时执行一次;option 更新交给下面的 effect
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// option 更新:整表替换,避免 merge 残留旧 series
|
||||
useEffect(() => {
|
||||
chartRef.current?.setOption(option, { notMerge: true });
|
||||
}, [option]);
|
||||
|
||||
return <div ref={containerRef} className={className} style={{ width: "100%", height }} />;
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { NavLink, Outlet, useLocation, useNavigate, Navigate } from "react-router-dom";
|
||||
import { QrCode, Package, ArrowLeft, LayoutDashboard, Smartphone, GitBranch, LogOut, User, Users } from "lucide-react";
|
||||
import { QrCode, Package, ArrowLeft, LayoutDashboard, Smartphone, GitBranch, LogOut, User, Users, BarChart3 } from "lucide-react";
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
|
||||
const MENU = [
|
||||
@ -27,6 +27,12 @@ const MENU = [
|
||||
icon: Users,
|
||||
description: "按负责人查看在制品设备分布",
|
||||
},
|
||||
{
|
||||
title: "效能分析",
|
||||
path: "/admin/analytics",
|
||||
icon: BarChart3,
|
||||
description: "人员效能 / 设备流转 ECharts 可视化",
|
||||
},
|
||||
];
|
||||
|
||||
export default function AdminLayout() {
|
||||
|
||||
Reference in New Issue
Block a user