diff --git a/frontend/src/pages/admin/AnalyticsDashboard.tsx b/frontend/src/pages/admin/AnalyticsDashboard.tsx index 8258c1a..6a8cd23 100644 --- a/frontend/src/pages/admin/AnalyticsDashboard.tsx +++ b/frontend/src/pages/admin/AnalyticsDashboard.tsx @@ -2,7 +2,7 @@ * 个人能力图谱:X 轴 = 设备身份证,单机耗时对比,支持点击柱子下钻备注弹窗(含照片)。 */ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useSearchParams } from "react-router-dom"; -import { DatePicker, Tabs, Select, Input, Button, Empty, Modal, Timeline, Image } from "antd"; +import { DatePicker, Tabs, Select, Button, Empty, Modal, Timeline, Image } from "antd"; import { Users, GitBranch, RefreshCw, Loader2, AlertCircle } from "lucide-react"; import dayjs, { type Dayjs } from "dayjs"; import type { EChartsCoreOption } from "echarts/core"; @@ -54,6 +54,11 @@ function imageUrl(u: string) { return base + path; } +// ─── 中文姓名对齐:两字名字中间插入全角空格,与三字名视觉对齐 ── +function alignName(name: string) { + return name.length === 2 ? name[0] + " " + name[1] : name; +} + export default function AnalyticsDashboard() { const [searchParams, setSearchParams] = useSearchParams(); @@ -65,13 +70,14 @@ export default function AnalyticsDashboard() { const [specModels, setSpecModels] = useState(() => (searchParams.get("spec_models") || searchParams.get("spec_model") || "").split(",").filter(Boolean), ); - const [productSn, setProductSn] = useState(() => - searchParams.get("product_sn") || searchParams.get("sn") || "", + const [productSns, setProductSns] = useState(() => + (searchParams.get("product_sns") || searchParams.get("product_sn") || searchParams.get("sn") || "") + .split(",").filter(Boolean), ); const [activeTab, setActiveTab] = useState("capability"); // ─── 下拉选项 + 数据 ───────────────────────────────────── - const [options, setOptions] = useState({ assignees: [], spec_models: [] }); + const [options, setOptions] = useState({ assignees: [], spec_models: [], devices: [] }); const [capability, setCapability] = useState(null); const [capabilityLoading, setCapabilityLoading] = useState(false); const [flow, setFlow] = useState(null); @@ -93,9 +99,9 @@ export default function AnalyticsDashboard() { const params = new URLSearchParams(); if (assigneeIds.length) params.set("assignee_id", assigneeIds.join(",")); if (specModels.length) params.set("spec_models", specModels.join(",")); - if (productSn.trim()) params.set("product_sn", productSn.trim()); + if (productSns.length) params.set("product_sns", productSns.join(",")); setSearchParams(params, { replace: true }); - }, [assigneeIds, specModels, productSn, setSearchParams]); + }, [assigneeIds, specModels, productSns, setSearchParams]); // ─── 构建查询 ──────────────────────────────────────────── const buildCapabilityQuery = useCallback((): CapabilityQuery => { @@ -111,11 +117,10 @@ export default function AnalyticsDashboard() { const buildFlowQuery = useCallback((): FlowQuery => { const q: FlowQuery = {}; - const sns = productSn.split(/[,,\s]+/).filter(Boolean); - if (sns.length) q.product_sns = sns; + if (productSns.length) q.product_sns = productSns; if (specModels.length) q.spec_models = specModels; return q; - }, [productSn, specModels]); + }, [productSns, specModels]); // ─── 能力图谱(需选择人员才发起) ── const loadCapability = useCallback(async () => { @@ -135,7 +140,7 @@ export default function AnalyticsDashboard() { // ─── 流转对比(需选型号或输入身份证才发起) ── const loadFlow = useCallback(async () => { - if (specModels.length === 0 && productSn.trim().length === 0) { + if (specModels.length === 0 && productSns.length === 0) { setFlow(null); return; } @@ -147,7 +152,7 @@ export default function AnalyticsDashboard() { } finally { setFlowLoading(false); } - }, [specModels, productSn, buildFlowQuery]); + }, [specModels, productSns, buildFlowQuery]); // ─── 下拉选项(动态联动:随 assigneeIds/specModels 变化重新拉取) ── useEffect(() => { @@ -206,7 +211,7 @@ export default function AnalyticsDashboard() { setRange(null); setAssigneeIds([]); setSpecModels([]); - setProductSn(""); + setProductSns([]); }; // ─── 柱状图:个人能力图谱(X=设备身份证,系列=人员,并排对比) ── @@ -505,10 +510,35 @@ export default function AnalyticsDashboard() { placeholder="人员(可多选)" value={assigneeIds} onChange={setAssigneeIds} - options={options.assignees.map((a) => ({ value: a.id, label: `${a.name} (${a.id})` }))} + options={options.assignees.map((a) => ({ value: a.id, label: alignName(a.name) }))} style={{ minWidth: 160, maxWidth: 240 }} - maxTagCount={1} - maxTagTextLength={4} + maxTagCount="responsive" + popupMatchSelectWidth={false} + dropdownRender={() => ( +
+
+ 共 {options.assignees.length} 人 +
+ + +
+
+
+ {options.assignees.map((a) => { + const checked = assigneeIds.includes(a.id); + return ( +
setAssigneeIds((prev) => (checked ? prev.filter((id) => id !== a.id) : [...prev, a.id]))} + className={`cursor-pointer rounded-md px-2 py-1.5 text-center text-sm transition-colors ${checked ? "bg-blue-50 font-medium text-blue-700" : "bg-gray-50 text-gray-600 hover:bg-gray-100"}`} + > + {alignName(a.name)} +
+ ); + })} +
+
+ )} /> setProductSn(e.target.value)} - style={{ width: 200 }} + size="small" + placeholder="搜索或选择设备..." + value={productSns} + onChange={setProductSns} + popupMatchSelectWidth={false} + style={{ minWidth: 200 }} + styles={{ popup: { root: { minWidth: 440 } } }} + options={options.devices.map((d) => ({ + value: d.product_sn, + label: d.external_serial ? `${d.external_serial} (${d.product_sn})` : d.product_sn, + data: d, + }))} + optionRender={(oriOption: any) => { + const d = oriOption.data?.data; + if (!d) return oriOption.label; + return ( +
+
+ {d.material_name || "未知产品"}{" "} + ({d.spec_model || "无型号"}) +
+
+ + S/N: {d.external_serial || "无"} + + ID: {d.product_sn} +
+
+ ); + }} />