import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; import timezone from "dayjs/plugin/timezone"; import config from "@/config/config"; import { apiFetch } from "@/lib/apiFetch"; import { useProjectStore } from "@/store/projectStore"; dayjs.extend(utc); dayjs.extend(timezone); export type SimulationRange = { from: Date; to: Date }; export const parseHydraulicStepSeconds = (value: unknown): number | undefined => { if (typeof value !== "string" || !/^\d+:\d{1,2}(:\d{1,2})?$/.test(value.trim())) { return undefined; } const [hours, minutes, seconds = 0] = value.trim().split(":").map(Number); const step = hours * 3600 + minutes * 60 + seconds; return minutes < 60 && seconds < 60 && step > 0 ? step : undefined; }; /** 与地图时间轴一致,以北京时间零点为基准向外取整,跨日仍保持同一步长。 */ export const alignSimulationRange = ( range: SimulationRange, stepSeconds: number, ): SimulationRange => { if (!Number.isFinite(stepSeconds) || stepSeconds <= 0 || !Number.isFinite(range.from.getTime()) || !Number.isFinite(range.to.getTime())) { throw new Error("查询时间或水力时间步长无效"); } const start = Math.min(range.from.getTime(), range.to.getTime()); const end = Math.max(range.from.getTime(), range.to.getTime()); const origin = dayjs(start).tz("Asia/Shanghai").startOf("day").valueOf(); const step = stepSeconds * 1000; const from = origin + Math.floor((start - origin) / step) * step; const to = Math.max(from + step, origin + Math.ceil((end - origin) / step) * step); return { from: new Date(from), to: new Date(to) }; }; /** 复用同步在线模拟接口,成功响应表示计算和保存已完成;不自动重试写请求。 */ export const runOnlineSimulation = async ( range: SimulationRange, signal?: AbortSignal, ): Promise<{ range: SimulationRange; stepSeconds: number }> => { const projectId = useProjectStore.getState().currentProjectId; if (!projectId) throw new Error("请先选择项目,未启动在线模拟"); const projectHeaders = { "X-Project-Id": projectId }; const timeResponse = await apiFetch( `${config.BACKEND_URL}/api/v1/network-options/time`, { signal, headers: projectHeaders, projectHeaderMode: "omit", }, ); if (!timeResponse.ok) throw new Error("无法读取管网时间步长,未启动在线模拟"); const payload = await timeResponse.json(); const stepSeconds = parseHydraulicStepSeconds( (payload?.times ?? payload)?.["HYDRAULIC TIMESTEP"], ); if (!stepSeconds) throw new Error("管网水力时间步长无效,未启动在线模拟"); if (signal?.aborted || useProjectStore.getState().currentProjectId !== projectId) { throw new Error("项目已切换或面板已关闭,未启动在线模拟"); } const alignedRange = alignSimulationRange(range, stepSeconds); const response = await apiFetch(`${config.BACKEND_URL}/api/v1/simulation-runs`, { method: "POST", headers: { ...projectHeaders, "Content-Type": "application/json" }, projectHeaderMode: "omit", // 后端循环的结束时间不包含在计算内,多算一个步长以覆盖图表右端点。 body: JSON.stringify({ start_time: alignedRange.from.toISOString(), duration: Math.ceil((alignedRange.to.getTime() - alignedRange.from.getTime() + stepSeconds * 1000) / 60000), }), signal, }); const result = await response.json().catch(() => null); if (!response.ok || result?.status !== "success") { throw new Error(typeof result?.detail === "string" ? result.detail : "在线模拟未确认完成,请刷新查看已生成的数据"); } return { range: alignedRange, stepSeconds }; };