feat(analytics): 筛选器体验升级(人名网格化/规格物料名/设备卡片下拉)
- 人员选择器 4 列网格 + 两字姓名对齐 + 全选/全不选 - 规格型号 2 列网格 + 物料名展示 + 全选/全不选 - 设备输入框改为卡片式下拉器(物料名/S/N/完整身份证) - productSn(string) 改为 productSns(string[]) 多选
This commit is contained in:
@ -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<string[]>(() =>
|
||||
(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<string[]>(() =>
|
||||
(searchParams.get("product_sns") || searchParams.get("product_sn") || searchParams.get("sn") || "")
|
||||
.split(",").filter(Boolean),
|
||||
);
|
||||
const [activeTab, setActiveTab] = useState("capability");
|
||||
|
||||
// ─── 下拉选项 + 数据 ─────────────────────────────────────
|
||||
const [options, setOptions] = useState<AnalyticsOptions>({ assignees: [], spec_models: [] });
|
||||
const [options, setOptions] = useState<AnalyticsOptions>({ assignees: [], spec_models: [], devices: [] });
|
||||
const [capability, setCapability] = useState<CapabilityResponse | null>(null);
|
||||
const [capabilityLoading, setCapabilityLoading] = useState(false);
|
||||
const [flow, setFlow] = useState<FlowResponse | null>(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={() => (
|
||||
<div className="w-[400px] p-3">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<span className="text-xs text-gray-400">共 {options.assignees.length} 人</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button size="small" type="link" onClick={(e) => { e.stopPropagation(); setAssigneeIds(options.assignees.map((a) => a.id)); }}>全选</Button>
|
||||
<Button size="small" type="link" onClick={(e) => { e.stopPropagation(); setAssigneeIds([]); }}>全不选</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{options.assignees.map((a) => {
|
||||
const checked = assigneeIds.includes(a.id);
|
||||
return (
|
||||
<div
|
||||
key={a.id}
|
||||
onClick={() => 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)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Select
|
||||
mode="multiple"
|
||||
@ -517,18 +547,74 @@ export default function AnalyticsDashboard() {
|
||||
placeholder="规格型号"
|
||||
value={specModels}
|
||||
onChange={setSpecModels}
|
||||
options={options.spec_models.map((s) => ({ value: s, label: s }))}
|
||||
options={options.spec_models.map((s) => ({
|
||||
value: s.spec_model,
|
||||
label: s.material_name ? `${s.material_name} (${s.spec_model})` : s.spec_model,
|
||||
}))}
|
||||
style={{ minWidth: 160, maxWidth: 240 }}
|
||||
maxTagCount={1}
|
||||
maxTagTextLength={8}
|
||||
maxTagCount="responsive"
|
||||
popupMatchSelectWidth={false}
|
||||
dropdownRender={() => (
|
||||
<div className="w-[460px] p-3">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<span className="text-xs text-gray-400">共 {options.spec_models.length} 个型号</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button size="small" type="link" onClick={(e) => { e.stopPropagation(); setSpecModels(options.spec_models.map((s) => s.spec_model)); }}>全选</Button>
|
||||
<Button size="small" type="link" onClick={(e) => { e.stopPropagation(); setSpecModels([]); }}>全不选</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{options.spec_models.map((s) => {
|
||||
const checked = specModels.includes(s.spec_model);
|
||||
return (
|
||||
<div
|
||||
key={s.spec_model}
|
||||
onClick={() => setSpecModels((prev) => (checked ? prev.filter((m) => m !== s.spec_model) : [...prev, s.spec_model]))}
|
||||
className={`cursor-pointer rounded-md px-2 py-1.5 transition-colors ${checked ? "bg-blue-50 font-medium text-blue-700" : "bg-gray-50 text-gray-600 hover:bg-gray-100"}`}
|
||||
>
|
||||
<div className="truncate text-sm">{s.material_name || "未知产品"}</div>
|
||||
<div className="truncate text-xs text-gray-400">{s.spec_model}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
size="small"
|
||||
<Select
|
||||
mode="multiple"
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder="设备身份证(可多个,逗号分隔)"
|
||||
value={productSn}
|
||||
onChange={(e) => 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 (
|
||||
<div className="flex flex-col border-b border-gray-50 py-2">
|
||||
<div className="text-sm font-bold text-gray-800">
|
||||
{d.material_name || "未知产品"}{" "}
|
||||
<span className="text-xs font-normal text-gray-500">({d.spec_model || "无型号"})</span>
|
||||
</div>
|
||||
<div className="mt-1 flex items-center justify-between text-xs">
|
||||
<span className="text-gray-400">
|
||||
S/N: <span className="text-gray-600">{d.external_serial || "无"}</span>
|
||||
</span>
|
||||
<span className="font-mono text-xs text-gray-500">ID: {d.product_sn}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Button size="small" type="primary" onClick={() => { loadCapability(); loadFlow(); }}>
|
||||
查询
|
||||
@ -601,7 +687,7 @@ export default function AnalyticsDashboard() {
|
||||
) : flowEmpty ? (
|
||||
<div className="flex h-[380px] items-center justify-center">
|
||||
<Empty
|
||||
description={specModels.length === 0 && productSn.trim().length === 0 ? "请先选择规格型号或输入设备身份证查看流转" : "暂无数据"}
|
||||
description={specModels.length === 0 && productSns.length === 0 ? "请先选择规格型号或输入设备身份证查看流转" : "暂无数据"}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user