"use client"; import { NavigationLink } from "@components/loading/NavigationLink"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { FiAlertCircle, FiAlertTriangle, FiBox, FiCheckCircle, FiRefreshCw, } from "react-icons/fi"; import { useProject } from "@/contexts/ProjectContext"; import { getRoundedCurrentTimelineMinutes } from "@components/olmap/core/Controls/timelineTime"; import { useTimelineTimeConfig } from "@components/olmap/core/Controls/useTimelineTimeConfig"; import { ThreeDimensionalControls, type ControlTab, } from "./ThreeDimensionalControls"; import { ThreeDimensionalTimeline } from "./ThreeDimensionalTimeline"; import { fetchPressureDevices, fetchSceneFrame, supportsThreeDimensionalScene, type PressureDevice, type SceneFrame, type SceneModelIndex, ZJB_PROJECT_CODE, ZJB_SCENE_MODEL_ID, } from "./sceneData"; import { isSceneRuntimeMessage, SCENE_CHANNEL, SCENE_PROTOCOL_VERSION, type SceneAssetSelection, type SceneCommand, type SceneHostMessage, type SceneRuntimeState, } from "./sceneProtocol"; const SCENE_URL = "/three-dimensional/zjb/v29/preview.html?host=platform2&v=36-resumable-cache"; const SCENE_LOAD_TIMEOUT_MS = 120_000; const initialRuntimeState: SceneRuntimeState = { mode: "network", status: "正在载入三维模型", contextVisible: true, roofVisible: true, displayMode: "global", style: { scale: 6, mode: "uniform", color: "#098ed0", missingColor: "#89949d", lowColor: "#2b83ba", highColor: "#e66c37", opacity: 1, roughness: 0.3, metalness: 0.22, nodes: true, direction: "none", arrowColor: "#f2b447", autoRange: true, min: 0, max: 3, }, styleSummary: {}, appearance: { preset: "day", exposure: 0.95, contextOpacity: 0.4, shadows: true, effects: true, quality: "standard", }, camera: { active: null, note: "正在准备观察位置", views: [] }, }; const emptyFrame: SceneFrame = { selectedTime: "", resultTime: null, payload: null, stats: { simulationNodes: 0, simulationLinks: 0, scadaOverrides: 0, missingNodes: 0, missingLinks: 0, ignoredElements: 0, }, warnings: [], }; type SceneHostPayload = | Pick, "type" | "payload"> | Pick, "type"> | Pick, "type" | "command">; const formatDateTime = (value: Date | string) => new Intl.DateTimeFormat("zh-CN", { timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false, }).format(typeof value === "string" ? new Date(value) : value); const toQueryTime = (selectedDate: Date, currentTime: number) => { const queryTime = new Date(selectedDate); queryTime.setHours(Math.floor(currentTime / 60), currentTime % 60, 0, 0); return queryTime; }; export default function ThreeDimensionalScene() { const project = useProject(); const projectCode = project?.networkName?.trim().toLowerCase() ?? ""; const isZjbProject = supportsThreeDimensionalScene(projectCode); const iframeRef = useRef(null); const frameRevisionRef = useRef(0); const { durationMinutes, stepMinutes } = useTimelineTimeConfig(); const [selectedDate, setSelectedDate] = useState(() => new Date()); const [currentTime, setCurrentTime] = useState(() => getRoundedCurrentTimelineMinutes(), ); const [model, setModel] = useState(null); const [runtimeState, setRuntimeState] = useState(initialRuntimeState); const [selection, setSelection] = useState(null); const [controlsOpen, setControlsOpen] = useState(true); const [controlTab, setControlTab] = useState("scene"); const [timelineOpen, setTimelineOpen] = useState(true); const [pressureDevices, setPressureDevices] = useState([]); const [pressureMappingWarning, setPressureMappingWarning] = useState(null); const [frame, setFrame] = useState(emptyFrame); const [loading, setLoading] = useState(false); const [loadError, setLoadError] = useState(null); const [sceneError, setSceneError] = useState(null); const [sceneLoading, setSceneLoading] = useState(true); const [sceneResourceError, setSceneResourceError] = useState(false); const [sceneStarted, setSceneStarted] = useState(false); const [refreshVersion, setRefreshVersion] = useState(0); const [sceneKey, setSceneKey] = useState(0); const resolvedCurrentTime = useMemo(() => { const bounded = Math.min(durationMinutes, Math.max(0, currentTime)); return Math.floor(bounded / stepMinutes) * stepMinutes; }, [currentTime, durationMinutes, stepMinutes]); const postToScene = useCallback((message: SceneHostPayload) => { iframeRef.current?.contentWindow?.postMessage( { channel: SCENE_CHANNEL, version: SCENE_PROTOCOL_VERSION, projectCode: ZJB_PROJECT_CODE, modelId: ZJB_SCENE_MODEL_ID, ...message, } satisfies SceneHostMessage, window.location.origin, ); }, []); const sendCommand = useCallback( (command: SceneCommand) => postToScene({ type: "command", command }), [postToScene], ); const retryScene = useCallback(() => { setModel(null); setSelection(null); setRuntimeState(initialRuntimeState); setSceneError(null); setSceneLoading(true); setSceneResourceError(false); setSceneStarted(false); setSceneKey((value) => value + 1); }, []); useEffect(() => { if (!isZjbProject) return; const onMessage = (event: MessageEvent) => { if ( event.origin !== window.location.origin || event.source !== iframeRef.current?.contentWindow || !isSceneRuntimeMessage(event.data) || event.data.projectCode !== ZJB_PROJECT_CODE || event.data.modelId !== ZJB_SCENE_MODEL_ID ) { return; } const message = event.data; if (message.type === "ready") { if (!Array.isArray(message.nodeIds) || !Array.isArray(message.linkIds) || !message.state) { setSceneError("三维场景返回了无效的模型索引。"); return; } setModel({ modelId: message.modelId, nodeIds: new Set(message.nodeIds.map(String)), linkIds: new Set(message.linkIds.map(String)), }); setRuntimeState(message.state); setSceneError(null); setSceneLoading(false); setSceneResourceError(false); return; } if (message.type === "loading") { if (typeof message.active === "boolean") { setSceneLoading(message.active); setSceneStarted(true); setSceneResourceError(message.failed === true); if (message.active) setSceneError(null); } return; } if (message.type === "scene-state") { setRuntimeState(message.state); setSceneError(null); } else if (message.type === "selection-changed") { setSelection(message.selection); if (message.selection) { setControlsOpen(true); setControlTab("properties"); } } else if (message.type === "error") { setSceneError(message.message || "三维场景执行命令失败。"); } else if (message.type === "results-applied") { setSceneError(null); } else if (message.type === "results-cleared") { setSceneError(null); } }; window.addEventListener("message", onMessage); return () => window.removeEventListener("message", onMessage); }, [isZjbProject, sceneKey]); useEffect(() => { // Once the viewer starts, its per-request inactivity timeout owns recovery. // A long but progressing first download should not hit a total-load deadline. if (!isZjbProject || model || !sceneLoading || sceneStarted) return; const timerId = window.setTimeout(() => { setSceneError("三维模型载入超时,请检查静态资源后重试。"); }, SCENE_LOAD_TIMEOUT_MS); return () => window.clearTimeout(timerId); }, [isZjbProject, model, sceneKey, sceneLoading, sceneStarted]); useEffect(() => { if (!isZjbProject) return; const controller = new AbortController(); fetchPressureDevices(controller.signal) .then((devices) => { setPressureDevices(devices); setPressureMappingWarning(null); }) .catch((error: unknown) => { if (controller.signal.aborted) return; setPressureDevices([]); setPressureMappingWarning( error instanceof Error ? `压力测点映射不可用:${error.message}` : "压力测点映射不可用。", ); }); return () => controller.abort(); }, [isZjbProject]); const queryTime = useMemo( () => toQueryTime(selectedDate, resolvedCurrentTime), [resolvedCurrentTime, selectedDate], ); useEffect(() => { if (!isZjbProject || !model) return; const revision = frameRevisionRef.current + 1; frameRevisionRef.current = revision; const controller = new AbortController(); const timerId = window.setTimeout(() => { setLoading(true); setLoadError(null); fetchSceneFrame({ queryTime, model, pressureDevices, signal: controller.signal }) .then((nextFrame) => { if (controller.signal.aborted || revision !== frameRevisionRef.current) return; setFrame(nextFrame); postToScene( nextFrame.payload ? { type: "results", payload: nextFrame.payload } : { type: "clear-results" }, ); }) .catch((error: unknown) => { if (controller.signal.aborted || revision !== frameRevisionRef.current) return; setFrame({ ...emptyFrame, selectedTime: queryTime.toISOString() }); postToScene({ type: "clear-results" }); setLoadError(error instanceof Error ? error.message : "当前时间帧加载失败。"); }) .finally(() => { if (!controller.signal.aborted && revision === frameRevisionRef.current) setLoading(false); }); }, 180); return () => { window.clearTimeout(timerId); controller.abort(); }; }, [isZjbProject, model, postToScene, pressureDevices, queryTime, refreshVersion]); if (!isZjbProject) { return (

当前项目未配置三维场景

目前仅高铁湛江北站项目提供三维模型。请切换到 ZJB 项目后再打开此页面。

返回管网在线模拟
); } const hasFrame = frame.payload !== null; const frameStatusText = loading ? "正在读取时间帧" : loadError ? "数据请求失败" : hasFrame ? "时间帧已应用" : "该时刻无模拟结果"; const dataWarnings = [pressureMappingWarning, ...frame.warnings].filter( (warning): warning is string => Boolean(warning), ); return (