feat: 效能分析X轴四行标签/flow分块/自然工作日即时切换/时间快捷选项

This commit is contained in:
2026-09-02 10:39:01 +08:00
parent 3c6c8c0c53
commit 06337fc8eb
2 changed files with 182 additions and 73 deletions

View File

@ -12,7 +12,7 @@ import BaseEChart from "../../components/BaseEChart";
import { import {
fetchCapabilityProfile, fetchFlowData, fetchAnalyticsOptions, fetchDeviceRecords, fetchCapabilityProfile, fetchFlowData, fetchAnalyticsOptions, fetchDeviceRecords,
type CapabilityResponse, type FlowResponse, type AnalyticsOptions, type CapabilityResponse, type FlowResponse, type AnalyticsOptions,
type CapabilityQuery, type FlowQuery, type DeviceRecord, type CapabilityQuery, type FlowQuery, type DeviceRecord, type FlowDevice, type FlowInterval,
} from "../../services/analyticsApi"; } from "../../services/analyticsApi";
dayjs.locale("zh-cn"); dayjs.locale("zh-cn");
@ -66,12 +66,29 @@ function alignName(name: string) {
} }
const CHUNK_SIZE = 6; // 人员视图瀑布流每块设备数 const CHUNK_SIZE = 6; // 人员视图瀑布流每块设备数
const FLOW_CHUNK_SIZE = 8; // 轨迹流转瀑布流每块设备数
// ⏰ 时间筛选(今天 / 近7天 / 近30天 / 自定义)
type DateRangeKey = "today" | "7d" | "30d" | "custom";
function rangeToParams(key: DateRangeKey, customRange: [Dayjs, Dayjs] | null) {
if (key === "custom" && customRange) {
return {
since: customRange[0].startOf("day").toISOString(),
until: customRange[1].endOf("day").toISOString(),
};
}
const since = dayjs().startOf("day");
if (key === "7d") return { since: since.subtract(7, "day").toISOString() };
if (key === "30d") return { since: since.subtract(30, "day").toISOString() };
return { since: since.toISOString() };
}
export default function AnalyticsDashboard() { export default function AnalyticsDashboard() {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
// ─── 筛选状态(从 URL 懒初始化) ───────────────────────── // ─── 筛选状态(从 URL 懒初始化) ─────────────────────────
const [range, setRange] = useState<[Dayjs, Dayjs] | null>(null); const [dateKey, setDateKey] = useState<DateRangeKey>("today");
const [customRange, setCustomRange] = useState<[Dayjs, Dayjs] | null>(null);
const [assigneeIds, setAssigneeIds] = useState<string[]>(() => const [assigneeIds, setAssigneeIds] = useState<string[]>(() =>
(searchParams.get("assignee_id") || "").split(",").filter(Boolean), (searchParams.get("assignee_id") || "").split(",").filter(Boolean),
); );
@ -124,21 +141,23 @@ export default function AnalyticsDashboard() {
const q: CapabilityQuery = {}; const q: CapabilityQuery = {};
if (assigneeIds.length) q.assignee_ids = assigneeIds; if (assigneeIds.length) q.assignee_ids = assigneeIds;
if (specModels.length) q.spec_models = specModels; if (specModels.length) q.spec_models = specModels;
if (range) { const { since, until } = rangeToParams(dateKey, customRange);
q.since = range[0].startOf("day").toISOString(); if (since) q.since = since;
q.until = range[1].endOf("day").toISOString(); if (until) q.until = until;
}
q.mode = totalDaysMode; // 自然天 / 工作日,控制人员视图耗时口径 q.mode = totalDaysMode; // 自然天 / 工作日,控制人员视图耗时口径
return q; return q;
}, [assigneeIds, specModels, range, totalDaysMode]); }, [assigneeIds, specModels, dateKey, customRange, totalDaysMode]);
const buildFlowQuery = useCallback((): FlowQuery => { const buildFlowQuery = useCallback((): FlowQuery => {
const q: FlowQuery = {}; const q: FlowQuery = {};
if (productSns.length) q.product_sns = productSns; if (productSns.length) q.product_sns = productSns;
if (specModels.length) q.spec_models = specModels; if (specModels.length) q.spec_models = specModels;
q.mode = totalDaysMode; // 自然天 / 工作日,控制轨迹流转图时间口径 const { since, until } = rangeToParams(dateKey, customRange);
if (since) q.since = since;
if (until) q.until = until;
// flow 后端同时返回自然/工作日两套数据,切换由前端 isWorkday 即时映射,无需重新请求
return q; return q;
}, [productSns, specModels, totalDaysMode]); }, [productSns, specModels, dateKey, customRange]);
// ─── 能力图谱(需选择人员才发起) ── // ─── 能力图谱(需选择人员才发起) ──
const loadCapability = useCallback(async () => { const loadCapability = useCallback(async () => {
@ -216,18 +235,20 @@ export default function AnalyticsDashboard() {
if (sn) setRecordQuery({ sn, all: false }); if (sn) setRecordQuery({ sn, all: false });
}, []); }, []);
// 流转对比:点柱子/背景 → 展示该设备全生命周期所有备注(不过滤人员) // 流转对比:点柱子/背景 → 展示该设备全生命周期所有备注(不过滤人员);分块渲染需换算全局下标
const handleFlowZrClick = useCallback((chart: any, e: any) => { const handleFlowZrClick = useCallback((chart: any, e: any, chunkStart: number) => {
const coord = chart.convertFromPixel({ seriesIndex: 0 }, [e.offsetX, e.offsetY]); const coord = chart.convertFromPixel({ seriesIndex: 0 }, [e.offsetX, e.offsetY]);
if (!coord || coord.length < 1) return; if (!coord || coord.length < 1) return;
const dataIndex = Math.round(coord[0]); const dataIndex = Math.round(coord[0]);
if (dataIndex < 0 || dataIndex >= flowSnRef.current.length) return; const globalIdx = chunkStart + dataIndex;
const sn = flowSnRef.current[dataIndex]; if (globalIdx < 0 || globalIdx >= flowSnRef.current.length) return;
const sn = flowSnRef.current[globalIdx];
if (sn) setRecordQuery({ sn, all: true }); if (sn) setRecordQuery({ sn, all: true });
}, []); }, []);
const handleReset = () => { const handleReset = () => {
setRange(null); setDateKey("today");
setCustomRange(null);
setAssigneeIds([]); setAssigneeIds([]);
setSpecModels([]); setSpecModels([]);
setProductSns([]); setProductSns([]);
@ -286,7 +307,8 @@ export default function AnalyticsDashboard() {
}) })
.join("<br/>"); .join("<br/>");
return ( return (
`设备身份证:${sn}` + `产品名称:${dev?.material_name ?? "—"}` +
`<br/>设备身份证:${sn}` +
(ext ? `<br/>产品序列号:${ext}` : "") + (ext ? `<br/>产品序列号:${ext}` : "") +
`<br/>规格型号:${dev?.spec_model ?? "—"}<br/>${rows}` `<br/>规格型号:${dev?.spec_model ?? "—"}<br/>${rows}`
); );
@ -296,8 +318,16 @@ export default function AnalyticsDashboard() {
grid: { left: 48, right: 24, top: 80, bottom: 48 }, // top 加大,避免图例遮挡柱子 grid: { left: 48, right: 24, top: 80, bottom: 48 }, // top 加大,避免图例遮挡柱子
xAxis: { xAxis: {
type: "category", type: "category",
data: chunkDevices.map((d) => (d.external_serial ? `${d.external_serial}\n${d.product_sn}` : d.product_sn)), data: chunkDevices.map((d) => d.product_sn),
axisLabel: { fontSize: 11, interval: 0, color: "#666", lineHeight: 14 }, axisLabel: {
fontSize: 11, interval: 0, color: "#666", lineHeight: 14,
// 🔧 标签改为「产品名称(截断) + 尾号4位」,避免 16 位身份证重叠
formatter: (sn: string) => {
const dev = chunkDevices.find((d) => d.product_sn === sn);
const name = dev?.material_name ? dev.material_name.slice(0, 6) : sn.slice(0, 6);
return `${name} ${sn.slice(-4)}`;
},
},
}, },
yAxis: { type: "value", name: "耗时(小时)" }, yAxis: { type: "value", name: "耗时(小时)" },
series: chunkSeries.map((s, sIdx) => ({ series: chunkSeries.map((s, sIdx) => ({
@ -354,30 +384,54 @@ export default function AnalyticsDashboard() {
return set; return set;
}, [capability]); }, [capability]);
// ─── 生命周期时间轴(主干延续 + 分支贴两侧 + 上帝视角 Tooltip) ── // ─── 轨迹流转:按设备分块(每块 FLOW_CHUNK_SIZE 台),每块一个 ECharts 实例 ──
const flowOption = useMemo<EChartsCoreOption>(() => { const flowChunks = useMemo(() => {
const devices = flow?.devices ?? []; if (!flow) return [];
const series = flow?.series ?? []; const chunks: { devices: FlowDevice[]; series: { name: string; data: FlowInterval[] }[]; startIndex: number }[] = [];
for (let i = 0; i < flow.devices.length; i += FLOW_CHUNK_SIZE) {
const devSlice = flow.devices.slice(i, i + FLOW_CHUNK_SIZE);
const idSet = new Set(devSlice.map((_, j) => i + j));
const seriesSlice = flow.series
.map((s) => ({
name: s.name,
data: s.data
.filter((it) => idSet.has(it[0]))
.map((it) => [it[0] - i, ...it.slice(1)] as FlowInterval),
}))
.filter((s) => s.data.length > 0);
chunks.push({ devices: devSlice, series: seriesSlice, startIndex: i });
}
return chunks;
}, [flow]);
// ─── 单个块的 ECharts Option 工厂(isWorkday 切换自然/工作日偏移,即时重绘) ──
const getFlowChunkOption = (
chunkDevices: FlowDevice[],
chunkSeries: { name: string; data: FlowInterval[] }[],
isWorkday: boolean,
): EChartsCoreOption => {
// 分支配色(与人员一一对应,用于柱子和 tooltip 色点) // 分支配色(与人员一一对应,用于柱子和 tooltip 色点)
const BRANCH_COLORS = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6", "#06b6d4", "#ec4899", "#84cc16"]; const BRANCH_COLORS = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6", "#06b6d4", "#ec4899", "#84cc16"];
const colorMap: Record<string, string> = {}; const colorMap: Record<string, string> = {};
series.forEach((s, index) => { chunkSeries.forEach((s, index) => {
colorMap[s.name] = BRANCH_COLORS[index % BRANCH_COLORS.length]; colorMap[s.name] = BRANCH_COLORS[index % BRANCH_COLORS.length];
}); });
// 1. 预计算每台设备的统计:累计投入、空闲时长、人员排序 // 1. 预计算每台设备的统计:累计投入、空闲时长、人员排序(按 isWorkday 选偏移/时长)
const deviceStats = devices.map((d, idx) => { const deviceStats = chunkDevices.map((d, idx) => {
let totalInputHours = 0; let totalInputHours = 0;
const personHours: Record<string, number> = {}; const personHours: Record<string, number> = {};
const intervals: [number, number][] = []; const intervals: [number, number][] = [];
series.forEach((s) => { chunkSeries.forEach((s) => {
s.data.forEach((task: any) => { s.data.forEach((task) => {
if (task[0] === idx) { if (task[0] === idx) {
totalInputHours += task[4]; const dur = isWorkday ? task[8] : task[7];
personHours[s.name] = (personHours[s.name] || 0) + task[4]; const so = isWorkday ? task[5] : task[3];
intervals.push([task[1], task[2]]); const eo = isWorkday ? task[6] : task[4];
totalInputHours += dur;
personHours[s.name] = (personHours[s.name] || 0) + dur;
intervals.push([so, eo]);
} }
}); });
}); });
@ -395,36 +449,48 @@ export default function AnalyticsDashboard() {
mergedEnd = e; mergedEnd = e;
} }
} }
const idleTime = Math.max((d.lead_time || 0) - covered, 0); const leadMax = intervals.reduce((m, it) => Math.max(m, it[1]), 0);
const idleTime = Math.max(leadMax - covered, 0);
const sortedPeople = Object.entries(personHours).sort((a, b) => b[1] - a[1]); const sortedPeople = Object.entries(personHours).sort((a, b) => b[1] - a[1]);
return { return {
leadTime: d.lead_time || 0, leadTime: leadMax,
totalInputHours, totalInputHours,
idleTime, idleTime,
sortedPeople, sortedPeople,
}; };
}); });
// 背景主干高度:所选模式下的总生命周期(严丝合缝,消除自然空闲断层)
const leadByMode = chunkDevices.map((d, idx) => {
let maxEnd = 0;
chunkSeries.forEach((s) => s.data.forEach((t) => {
if (t[0] === idx) {
const e = isWorkday ? t[6] : t[4];
if (e > maxEnd) maxEnd = e;
}
}));
return Math.max(maxEnd, 1);
});
return { return {
tooltip: { tooltip: {
trigger: "axis", trigger: "axis",
axisPointer: { type: "shadow", shadowStyle: { color: "rgba(0,0,0,0.05)" } }, axisPointer: { type: "shadow", shadowStyle: { color: "rgba(0,0,0,0.05)" } },
formatter: (params: any) => { formatter: (params: any) => {
const deviceIndex = params[0].dataIndex; const deviceIndex = params[0].dataIndex;
const device = devices[deviceIndex]; const device = chunkDevices[deviceIndex];
const stats = deviceStats[deviceIndex]; const stats = deviceStats[deviceIndex];
if (!device || !stats) return ""; if (!device || !stats) return "";
let html = `<div style="font-size:14px;font-weight:bold;color:#1f2937;margin-bottom:4px;">${device.spec_model || "未知型号"}</div>`; let html = `<div style="font-size:14px;font-weight:bold;color:#1f2937;margin-bottom:4px;">${device.material_name || "未知产品"}(${device.spec_model || "未知型号"})</div>`;
html += `<div style="color:#6b7280;font-size:12px;margin-bottom:8px;">身份证: ${device.product_sn}</div>`; html += `<div style="color:#6b7280;font-size:12px;margin-bottom:8px;">身份证: ${device.product_sn}</div>`;
html += `<hr style="margin:8px 0;border-color:#e5e7eb" />`; html += `<hr style="margin:8px 0;border-color:#e5e7eb" />`;
if (device.started_at) { if (device.started_at) {
html += `<div style="display:flex;justify-content:space-between;margin-bottom:4px;"><span>开始时间:</span><b style="color:#111827">${device.started_at}</b></div>`; html += `<div style="display:flex;justify-content:space-between;margin-bottom:4px;"><span>开始时间:</span><b style="color:#111827">${device.started_at}</b></div>`;
} }
html += `<div style="display:flex;justify-content:space-between;margin-bottom:4px;"><span>实际流转周期:</span><b style="color:#111827">${stats.leadTime} h</b></div>`; html += `<div style="display:flex;justify-content:space-between;margin-bottom:4px;"><span>实际流转周期:</span><b style="color:#111827">${stats.leadTime.toFixed(1)} h</b></div>`;
html += `<div style="display:flex;justify-content:space-between;margin-bottom:4px;"><span>累计投入工时:</span><b style="color:#2563eb">${stats.totalInputHours.toFixed(1)} h</b></div>`; html += `<div style="display:flex;justify-content:space-between;margin-bottom:4px;"><span>累计投入工时:</span><b style="color:#2563eb">${stats.totalInputHours.toFixed(1)} h</b></div>`;
html += `<div style="display:flex;justify-content:space-between;margin-bottom:12px;"><span>中间空闲时长:</span><b style="color:#f59e0b">${stats.idleTime.toFixed(1)} h</b></div>`; html += `<div style="display:flex;justify-content:space-between;margin-bottom:12px;"><span>中间空闲时长:</span><b style="color:#f59e0b">${stats.idleTime.toFixed(1)} h</b></div>`;
@ -442,14 +508,24 @@ export default function AnalyticsDashboard() {
return html; return html;
}, },
}, },
legend: { top: 0, type: "scroll", data: series.map((s) => s.name) }, legend: { top: 0, type: "scroll", data: chunkSeries.map((s) => s.name) },
grid: { left: 48, right: 24, top: 60, bottom: 96 }, grid: { left: 48, right: 24, top: 60, bottom: 96 },
xAxis: { xAxis: {
type: "category", type: "category",
data: devices.map((d) => data: chunkDevices.map((d) => d.product_sn),
[d.material_name, d.spec_model, d.external_serial, d.product_sn].filter(Boolean).join("\n"), axisLabel: {
), fontSize: 11, interval: 0, color: '#666', lineHeight: 15, width: 120, overflow: 'break' as const,
axisLabel: { fontSize: 11, interval: 0, color: '#666', lineHeight: 14 }, // 🔧 X 轴四行展示:产品名称 / 规格型号 / 业务序列号(若有) / 身份证
formatter: (sn: string) => {
const dev = chunkDevices.find((d) => d.product_sn === sn);
if (!dev) return sn;
const lines = [dev.material_name || "—"];
if (dev.spec_model) lines.push(dev.spec_model);
if (dev.external_serial) lines.push(dev.external_serial);
lines.push(dev.product_sn);
return lines.join("\n");
},
},
axisLine: { lineStyle: { color: '#ddd' } }, axisLine: { lineStyle: { color: '#ddd' } },
axisTick: { show: false } axisTick: { show: false }
}, },
@ -461,7 +537,7 @@ export default function AnalyticsDashboard() {
splitLine: { lineStyle: { type: 'dashed', color: '#f3f4f6' } } splitLine: { lineStyle: { type: 'dashed', color: '#f3f4f6' } }
}, },
series: [ series: [
// 1. 背景主干 (The Trunk) - 连贯的浅灰底 // 1. 背景主干 (The Trunk) - 连贯的浅灰底(所选模式生命周期)
{ {
name: "总生命周期", name: "总生命周期",
type: "bar" as const, type: "bar" as const,
@ -477,24 +553,25 @@ export default function AnalyticsDashboard() {
value: { fontSize: 14, fontWeight: "bold", color: "#1f2937" }, value: { fontSize: 14, fontWeight: "bold", color: "#1f2937" },
}, },
}, },
data: devices.map((d) => d.lead_time || 0), data: leadByMode,
}, },
// 2. 任务分支 (The Branches) - 贴主干两侧 // 2. 任务分支 (The Branches) - 贴主干两侧(按模式偏移,紧凑贴合)
...series.map((s, index) => ({ ...chunkSeries.map((s, index) => ({
name: s.name, name: s.name,
type: "custom" as const, type: "custom" as const,
z: 2, z: 2,
itemStyle: { color: BRANCH_COLORS[index % BRANCH_COLORS.length] }, itemStyle: { color: BRANCH_COLORS[index % BRANCH_COLORS.length] },
dimensions: ['device', 'start', 'end', 'task', 'duration', 'is_main'], dimensions: ['device', 'task', 'is_main', 'start_nat', 'end_nat', 'start_work', 'end_work', 'dur_total', 'dur_work'],
encode: { x: 0, y: [1, 2] }, encode: { x: 0, y: [3, 4] },
renderItem: (params: any, api: any) => { renderItem: (params: any, api: any) => {
const categoryIndex = api.value(0); const categoryIndex = api.value(0);
const rawData = s.data[params.dataIndex]; // [device, start, end, task, duration, is_main] const rawData = s.data[params.dataIndex]; // [idx, task, is_main, start_nat, end_nat, start_work, end_work, dur_total, dur_work]
const startOff = isWorkday ? rawData[5] : rawData[3];
const start = api.coord([categoryIndex, rawData[1]]); const endOff = isWorkday ? rawData[6] : rawData[4];
const end = api.coord([categoryIndex, rawData[2]]); const isMain = rawData[2] === 1;
const isMain = rawData[5] === 1; // 解析后端传来的主次标识
const start = api.coord([categoryIndex, startOff]);
const end = api.coord([categoryIndex, endOff]);
const trunkWidth = 32; // 灰色背景主干的宽度 const trunkWidth = 32; // 灰色背景主干的宽度
const blockWidth = 20; const blockWidth = 20;
@ -526,7 +603,7 @@ export default function AnalyticsDashboard() {
})), })),
], ],
}; };
}, [flow]); };
const capabilityEmpty = !capability || capability.series.length === 0; const capabilityEmpty = !capability || capability.series.length === 0;
const flowEmpty = !flow || flow.devices.length === 0; const flowEmpty = !flow || flow.devices.length === 0;
@ -544,25 +621,40 @@ export default function AnalyticsDashboard() {
{/* 顶部统一筛选栏 */} {/* 顶部统一筛选栏 */}
<div className="rounded-xl bg-white p-4 shadow-sm"> <div className="rounded-xl bg-white p-4 shadow-sm">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<RangePicker {/* ⏰ 时间快捷选项:今天 / 近7天 / 近30天 / 自定义 */}
<Radio.Group
value={dateKey}
onChange={e => { setDateKey(e.target.value); setCustomRange(null); }}
size="small" size="small"
value={range as any} optionType="button"
onChange={(dates) => setRange(dates as [Dayjs, Dayjs] | null)} buttonStyle="solid"
disabledDate={(d) => d.isAfter(dayjs(), "day")} >
cellRender={(current, info) => { <Radio.Button value="today">今天</Radio.Button>
if (info.type !== "date") return info.originNode; <Radio.Button value="7d">近7天</Radio.Button>
const day = current as Dayjs; <Radio.Button value="30d">近30天</Radio.Button>
const hasDot = activityDates.has(day.format("YYYY-MM-DD")); <Radio.Button value="custom">自定义</Radio.Button>
return ( </Radio.Group>
<div className="flex flex-col items-center"> {dateKey === "custom" && (
<span>{day.date()}</span> <RangePicker
{hasDot && <span className="h-1 w-1 rounded-full bg-blue-500" />} size="small"
</div> value={customRange as any}
); onChange={dates => setCustomRange(dates as [Dayjs, Dayjs] | null)}
}} disabledDate={(d) => d.isAfter(dayjs(), "day")}
style={{ width: 240 }} cellRender={(current, info) => {
placeholder={["开始日期", "结束日期"]} if (info.type !== "date") return info.originNode;
/> const day = current as Dayjs;
const hasDot = activityDates.has(day.format("YYYY-MM-DD"));
return (
<div className="flex flex-col items-center">
<span>{day.date()}</span>
{hasDot && <span className="h-1 w-1 rounded-full bg-blue-500" />}
</div>
);
}}
style={{ width: 240 }}
placeholder={["开始日期", "结束日期"]}
/>
)}
<Select <Select
mode="multiple" mode="multiple"
size="small" size="small"
@ -830,7 +922,19 @@ export default function AnalyticsDashboard() {
/> />
</div> </div>
) : ( ) : (
<BaseEChart option={flowOption} height={380} onZrClick={handleFlowZrClick} /> /* 🔧 瀑布流:每块 FLOW_CHUNK_SIZE 台设备一个图表,向下追加;isWorkday 即时映射自然/工作日 */
<div className="flex flex-col gap-12 pt-4">
{flowChunks.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={getFlowChunkOption(chunk.devices, chunk.series, totalDaysMode === "workdays")}
height={380}
onZrClick={(chart: any, e: any) => handleFlowZrClick(chart, e, chunk.startIndex)}
/>
</div>
))}
</div>
)} )}
</div> </div>
), ),

View File

@ -45,8 +45,9 @@ export interface FlowDevice {
total_workdays: number; // 设备生产总天数(工作日) total_workdays: number; // 设备生产总天数(工作日)
} }
// 时间区间:deviceIndex, startOffset, endOffset, taskName, duration, isMain // 时间区间:deviceIndex, taskName, isMain,
export type FlowInterval = [number, number, number, string, number, number]; // startNat, endNat, startWork, endWork, durationTotal(自然), durationWork(工作日)
export type FlowInterval = [number, string, number, number, number, number, number, number, number];
export interface FlowSeries { export interface FlowSeries {
name: string; // 人员姓名 name: string; // 人员姓名
@ -105,6 +106,8 @@ export interface FlowQuery {
product_sns?: string[]; product_sns?: string[];
spec_models?: string[]; spec_models?: string[];
mode?: "natural" | "workdays"; mode?: "natural" | "workdays";
since?: string;
until?: string;
} }
// ============================================================ // ============================================================
@ -127,6 +130,8 @@ export async function fetchFlowData(query: FlowQuery): Promise<FlowResponse> {
if (query.product_sns?.length) params.product_sns = query.product_sns.join(","); if (query.product_sns?.length) params.product_sns = query.product_sns.join(",");
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.mode) params.mode = query.mode; if (query.mode) params.mode = query.mode;
if (query.since) params.since = query.since;
if (query.until) params.until = query.until;
const { data } = await api.get<FlowResponse>("/analytics/flow", { params }); const { data } = await api.get<FlowResponse>("/analytics/flow", { params });
return data; return data;
} }