refactor(效能分析): 人员视图改为数据切片瀑布流,告别横向滚动
- 设备按 CHUNK_SIZE=6 切块,向下追加渲染多个 ECharts 图(垂直瀑布流) - capabilityOption 改为 getCapabilityOption 工厂函数,每块独立构建 Option - 去除横向 overflow 与分页器,网页原生向下延伸 - grid.top 加大到 80 避免图例遮挡,X 轴标签简化(序列号+身份证) - 点击柱子下钻支持块内索引→全局设备映射,Tooltip 读取对应块数据
This commit is contained in:
@ -59,7 +59,7 @@ function alignName(name: string) {
|
||||
return name.length === 2 ? name[0] + " " + name[1] : name;
|
||||
}
|
||||
|
||||
const CAP_PAGE_SIZE = 20; // 人员视图每页设备数
|
||||
const CHUNK_SIZE = 6; // 人员视图瀑布流每块设备数
|
||||
|
||||
export default function AnalyticsDashboard() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@ -92,8 +92,6 @@ export default function AnalyticsDashboard() {
|
||||
const [flowLoading, setFlowLoading] = useState(false);
|
||||
// 📅 设备生产总天数口径:自然天 / 工作日
|
||||
const [totalDaysMode, setTotalDaysMode] = useState<"natural" | "workdays">("natural");
|
||||
// 📄 人员视图分页
|
||||
const [capPage, setCapPage] = useState(0);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// ─── 弹窗下钻:设备备注(all=true 表示流转图点击,展示全部) ──
|
||||
@ -202,12 +200,13 @@ export default function AnalyticsDashboard() {
|
||||
}, [recordQuery, assigneeIds]);
|
||||
|
||||
// 能力图谱:点柱子 → 按当前人员筛选过滤备注
|
||||
const handleZrClick = useCallback((chart: any, e: any) => {
|
||||
const handleZrClick = useCallback((chart: any, e: any, chunkStart = 0) => {
|
||||
const coord = chart.convertFromPixel({ seriesIndex: 0 }, [e.offsetX, e.offsetY]);
|
||||
if (!coord || coord.length < 1) return;
|
||||
const dataIndex = Math.round(coord[0]);
|
||||
if (dataIndex < 0 || dataIndex >= categoriesRef.current.length) return;
|
||||
const sn = categoriesRef.current[dataIndex];
|
||||
const globalIdx = chunkStart + dataIndex; // 瀑布流块内索引 → 全局设备索引
|
||||
if (globalIdx < 0 || globalIdx >= categoriesRef.current.length) return;
|
||||
const sn = categoriesRef.current[globalIdx];
|
||||
if (sn) setRecordQuery({ sn, all: false });
|
||||
}, []);
|
||||
|
||||
@ -228,22 +227,32 @@ export default function AnalyticsDashboard() {
|
||||
setProductSns([]);
|
||||
};
|
||||
|
||||
// ─── 柱状图:人员视图(X=设备身份证,系列=人员,居中紧凑,消除幽灵占位) ──
|
||||
const capabilityOption = useMemo<EChartsCoreOption>(() => {
|
||||
const allCategories = capability?.categories ?? [];
|
||||
const allDevices = capability?.devices ?? [];
|
||||
const allSeries = capability?.series ?? [];
|
||||
// 🔧 分页切片:每页只显示 CAP_PAGE_SIZE 台设备,避免超宽无滚动
|
||||
const start = capPage * CAP_PAGE_SIZE;
|
||||
const end = Math.min(start + CAP_PAGE_SIZE, allCategories.length);
|
||||
const categories = allCategories.slice(start, end);
|
||||
const devices = allDevices.slice(start, end);
|
||||
const series = allSeries.map((s) => ({ ...s, data: s.data.slice(start, end) }));
|
||||
// 每台设备上真正产生耗时的人员 seriesIndex 数组
|
||||
const activePerDevice = devices.map((_, devIdx) =>
|
||||
series
|
||||
// ─── 人员视图:设备按 CHUNK_SIZE 切块(瀑布流,向下追加渲染) ──
|
||||
const capabilityChunks = useMemo(() => {
|
||||
if (!capability || !capability.devices?.length) return [];
|
||||
const chunks = [];
|
||||
const total = capability.devices.length;
|
||||
for (let i = 0; i < total; i += CHUNK_SIZE) {
|
||||
chunks.push({
|
||||
start: i, // 该块在全部设备中的起始索引(点击下钻用)
|
||||
devices: capability.devices.slice(i, i + CHUNK_SIZE),
|
||||
categories: capability.categories.slice(i, i + CHUNK_SIZE),
|
||||
series: capability.series.map(s => ({
|
||||
name: s.name,
|
||||
data: s.data.slice(i, i + CHUNK_SIZE),
|
||||
})),
|
||||
});
|
||||
}
|
||||
return chunks;
|
||||
}, [capability]);
|
||||
|
||||
// ─── 单个块(6台设备)的 ECharts Option 工厂函数(无横向滚动,向下追加) ──
|
||||
const getCapabilityOption = (chunkDevices: any[], chunkCategories: string[], chunkSeries: any[]): EChartsCoreOption => {
|
||||
// 该块内每台设备上真正产生耗时的人员 seriesIndex
|
||||
const activePerDevice = chunkDevices.map((_, devIdx) =>
|
||||
chunkSeries
|
||||
.map((s, sIdx) => (s.data[devIdx] && s.data[devIdx].value != null ? sIdx : -1))
|
||||
.filter((idx) => idx !== -1),
|
||||
.filter(idx => idx !== -1),
|
||||
);
|
||||
return {
|
||||
tooltip: {
|
||||
@ -253,8 +262,8 @@ export default function AnalyticsDashboard() {
|
||||
formatter: (p: any) => {
|
||||
const items = Array.isArray(p) ? p : [p];
|
||||
const idx = items[0]?.dataIndex ?? 0;
|
||||
const sn = categories[idx] ?? "—";
|
||||
const dev = devices[idx];
|
||||
const sn = chunkCategories[idx] ?? "—";
|
||||
const dev = chunkDevices[idx];
|
||||
const ext = dev?.external_serial;
|
||||
const rows = items
|
||||
.filter((it: any) => (it.data?.status ?? "—") !== "—")
|
||||
@ -278,16 +287,14 @@ export default function AnalyticsDashboard() {
|
||||
},
|
||||
},
|
||||
legend: { top: 0, type: "scroll" },
|
||||
grid: { left: 48, right: 24, top: 60, bottom: 96 },
|
||||
grid: { left: 48, right: 24, top: 80, bottom: 48 }, // top 加大,避免图例遮挡柱子
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: devices.map((d) =>
|
||||
[d.material_name, d.spec_model, d.external_serial, d.product_sn].filter(Boolean).join("\n"),
|
||||
),
|
||||
axisLabel: { fontSize: 10, interval: 0, lineHeight: 13 },
|
||||
data: chunkDevices.map((d) => (d.external_serial ? `${d.external_serial}\n${d.product_sn}` : d.product_sn)),
|
||||
axisLabel: { fontSize: 11, interval: 0, color: "#666", lineHeight: 14 },
|
||||
},
|
||||
yAxis: { type: "value", name: "耗时(小时)" },
|
||||
series: series.map((s, sIdx) => ({
|
||||
series: chunkSeries.map((s, sIdx) => ({
|
||||
name: s.name,
|
||||
type: "custom",
|
||||
encode: { x: 0, y: 1 },
|
||||
@ -306,44 +313,28 @@ export default function AnalyticsDashboard() {
|
||||
const localIndex = activeSeries.indexOf(sIdx);
|
||||
if (localIndex === -1) return;
|
||||
|
||||
const barWidth = 24; // 黄金粗细,绝不妥协
|
||||
const gap = 6; // 紧凑的柱间距
|
||||
const barWidth = 24;
|
||||
const gap = 6;
|
||||
const totalWidth = activeSeries.length * barWidth + (activeSeries.length - 1) * gap;
|
||||
|
||||
// 核心:彻底消除幽灵占位,让存活的柱子绝对居中对齐
|
||||
const centerX = api.coord([devIdx, 0])[0];
|
||||
const x = centerX - totalWidth / 2 + localIndex * (barWidth + gap);
|
||||
|
||||
const valY = api.coord([devIdx, val])[1];
|
||||
const y0 = api.coord([devIdx, 0])[1];
|
||||
const height = Math.max(y0 - valY, 3); // 至少 3px,0 值/极小值也有柱子
|
||||
const height = Math.max(y0 - valY, 3);
|
||||
const y = y0 - height;
|
||||
|
||||
return {
|
||||
type: "rect",
|
||||
shape: { x, y, width: barWidth, height, r: [3, 3, 0, 0] },
|
||||
style: api.style(),
|
||||
};
|
||||
},
|
||||
data: s.data.map((d, i) => {
|
||||
data: s.data.map((d: any, i: number) => {
|
||||
if (d.value == null) return null;
|
||||
return {
|
||||
...d,
|
||||
value: [i, d.value], // custom 必须的 [x, y] 坐标格式
|
||||
actualValue: d.value, // 供 tooltip 读取的真实值
|
||||
};
|
||||
return { ...d, value: [i, d.value], actualValue: d.value };
|
||||
}),
|
||||
})),
|
||||
};
|
||||
}, [capability, capPage]);
|
||||
|
||||
// 分页信息
|
||||
const capTotalPages = capability ? Math.max(1, Math.ceil(capability.devices.length / CAP_PAGE_SIZE)) : 0;
|
||||
const capViewCount = capability
|
||||
? Math.min(CAP_PAGE_SIZE, Math.max(0, capability.devices.length - capPage * CAP_PAGE_SIZE))
|
||||
: 0;
|
||||
// 查询/筛选变化后回到第 1 页
|
||||
useEffect(() => { setCapPage(0); }, [capability]);
|
||||
};
|
||||
|
||||
// ─── 有数据的日期集合(用于日历蓝点) ──
|
||||
const activityDates = useMemo(() => {
|
||||
@ -782,22 +773,18 @@ export default function AnalyticsDashboard() {
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full overflow-x-auto pb-2 custom-scrollbar">
|
||||
<div style={{ minWidth: Math.max(1, capViewCount) * 140 }}>
|
||||
<BaseEChart
|
||||
option={capabilityOption}
|
||||
height={420}
|
||||
onZrClick={handleZrClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* 分页器:设备多时分页查看,避免超宽无滚动 */}
|
||||
{capTotalPages > 1 && (
|
||||
<div className="mt-3 flex items-center justify-center gap-2">
|
||||
<Button size="small" disabled={capPage === 0} onClick={() => setCapPage(p => Math.max(0, p - 1))}>上一页</Button>
|
||||
<span className="text-xs text-gray-400">{capPage + 1} / {capTotalPages} 页 · 每页 {CAP_PAGE_SIZE} 台</span>
|
||||
<Button size="small" disabled={capPage >= capTotalPages - 1} onClick={() => setCapPage(p => Math.min(capTotalPages - 1, p + 1))}>下一页</Button>
|
||||
/* 🔧 瀑布流:设备按 CHUNK_SIZE 切块向下追加渲染,告别横向滚动 */
|
||||
<div className="flex flex-col gap-12 pt-4">
|
||||
{capabilityChunks.map((chunk, idx) => (
|
||||
<div key={idx} className="relative">
|
||||
{idx > 0 && <div className="absolute -top-6 left-0 w-full border-t border-dashed border-gray-200" />}
|
||||
<BaseEChart
|
||||
option={getCapabilityOption(chunk.devices, chunk.categories, chunk.series)}
|
||||
height={380}
|
||||
onZrClick={(chart: any, e: any) => handleZrClick(chart, e, chunk.start)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user