diff --git a/README.md b/README.md index 9d012af0..73517907 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,4 @@ - # 生成酷炫页面功能 - - This is a code bundle for 生成酷炫页面功能. The original project is available at https://www.figma.com/design/bjl07QIZTT9LhNxXMRXIGO/%E7%94%9F%E6%88%90%E9%85%B7%E7%82%AB%E9%A1%B5%E9%9D%A2%E5%8A%9F%E8%83%BD. - ## Running the code Run `npm i` to install the dependencies. diff --git a/docs/数字孪生系统拆分实施方案.md b/docs/数字孪生系统拆分实施方案.md new file mode 100644 index 00000000..4a674da6 --- /dev/null +++ b/docs/数字孪生系统拆分实施方案.md @@ -0,0 +1,391 @@ +# 数字孪生系统场景拆分实施方案 + +## 一、现状分析 + +### 1.1 现有代码结构 + +| 文件 | 行数 | 功能 | 所属场景 | +|------|------|------|----------| +| `IndustrialDashboard.tsx` | 1627行 | 监控大屏 + 3D模型渲染 | 氧化铝 | +| `DigitalTwin.tsx` | 1462行 | 数字孪生制作系统 | 氧化铝 | + +### 1.2 现有设备模型 + +**IndustrialDashboard.tsx (13个模型函数)**: +``` +createReactor - 溶出反应器 +createTank - 沉降槽 +createPump - 循环泵 +createFurnace - 焙烧炉 +createHeatExchanger - 换热器 +createStorage - 原料储罐 +createSlurryTank - 泥浆槽(来自DigitalTwin) +createPressureVessel - 压力容器(来自DigitalTwin) +createSettlingTankDT - 沉降槽DT版(来自DigitalTwin) +createDecompositionTower - 分解塔(来自DigitalTwin) +createEvaporator - 蒸发器(来自DigitalTwin) +createCalcinationFurnace - 煅烧炉(来自DigitalTwin) +createPipeSystem - 管道系统 +``` + +**DigitalTwin.tsx (7个模型函数)**: +``` +createSlurryTank - 泥浆槽 +createPressureVessel - 压力容器 +createSettlingTank - 沉降槽 +createDecompositionTower - 分解塔 +createEvaporator - 蒸发器 +createCalcinationFurnace - 煅烧炉 +createPipingSystem - 管道系统 +``` + +### 1.3 问题总结 + +1. **场景混淆**:文档描述钢铁+有色冶金两个行业,但代码只有氧化铝 +2. **代码重复**:DigitalTwin的模型被复制到IndustrialDashboard +3. **设备冗余**:同一个设备有多个版本(如SettlingTank vs SettlingTankDT) +4. **维护困难**:修改一个模型需要改多处 + +--- + +## 二、拆分方案 + +### 2.1 目标架构 + +``` +src/app/ +├── components/ +│ └── shared/ # ⭐ 新建:共享组件 +│ ├── scene/ # 共享3D场景组件 +│ │ ├── SceneSetup.tsx # 相机、灯光、地面 +│ │ ├── CameraControls.tsx # 相机控制逻辑 +│ │ └── PipeSystem.tsx # 管道系统组件 +│ ├── models/ # 共享3D模型 +│ │ ├── base/ # 基础模型(反应器、储罐等) +│ │ └── pipes/ # 管道连接件 +│ └── ui/ # 共享UI组件 +│ ├── DevicePanel.tsx # 设备详情面板 +│ ├── StatusBar.tsx # 状态栏 +│ └── AlertList.tsx # 告警列表 +│ +├── pages/ +│ ├── alumina/ # ⭐ 新建:铝冶炼场景 +│ │ ├── AluminaDashboard.tsx # 氧化铝监控大屏(改造自IndustrialDashboard) +│ │ ├── AluminaDigitalTwin.tsx # 氧化铝数字孪生(改造自DigitalTwin) +│ │ └── devices/ # 铝冶炼专用设备模型 +│ │ ├── SlurryTank.ts +│ │ ├── PressureVessel.ts +│ │ ├── SettlingTank.ts +│ │ ├── DecompositionTower.ts +│ │ ├── Evaporator.ts +│ │ └── CalcinationFurnace.ts +│ │ +│ ├── steel/ # ⭐ 新建:钢铁场景 +│ │ ├── SteelDashboard.tsx # 钢铁监控大屏 +│ │ ├── SteelDigitalTwin.tsx # 钢铁数字孪生 +│ │ └── devices/ # 钢铁专用设备模型 +│ │ ├── BlastFurnace.ts # 高炉 +│ │ ├── HotBlastStove.ts # 热风炉 +│ │ ├── AirBlower.ts # 鼓风机 +│ │ ├── CoolingWall.ts # 冷却壁 +│ │ ├── CoalMill.ts # 磨煤机 +│ │ └── FeedConveyor.ts # 给煤机 +│ │ +│ └── legacy/ # ⭐ 保留:旧文件(可后续删除) +│ ├── IndustrialDashboard.tsx +│ └── DigitalTwin.tsx +``` + +### 2.2 共享模块设计 + +#### 2.2.1 共享3D场景组件 + +**SceneSetup.tsx** - 场景初始化 +```typescript +interface SceneSetupProps { + containerRef: RefObject; + cameraPosition?: [number, number, number]; + lookAt?: [number, number, number]; + autoRotate?: boolean; + onSceneReady?: (scene: THREE.Scene, camera: THREE.PerspectiveCamera) => void; +} +``` + +**CameraControls.tsx** - 相机控制 +```typescript +interface CameraControlsProps { + camera: THREE.PerspectiveCamera; + autoRotate: boolean; + onToggleRotate?: () => void; + onResetView?: () => void; +} +``` + +#### 2.2.2 共享UI组件 + +**DevicePanel.tsx** - 设备详情面板 +```typescript +interface DevicePanelProps { + device: DeviceData | null; + onClose: () => void; +} +``` + +**AlertList.tsx** - 告警列表 +```typescript +interface AlertListProps { + alerts: AlertItem[]; + onAlertClick?: (alert: AlertItem) => void; +} +``` + +### 2.3 场景专用模块设计 + +#### 2.3.1 铝冶炼场景设备模型 + +| 设备 | 文件 | 描述 | +|------|------|------| +| 泥浆槽 | `SlurryTank.ts` | 带搅拌器和叶轮的圆柱形槽体 | +| 压力容器 | `PressureVessel.ts` | 带圆顶和管道的压力容器 | +| 沉降槽 | `SettlingTank.ts` | 带锥形底部的沉降槽 | +| 分解塔 | `DecompositionTower.ts` | 带夹套和盘管的塔式设备 | +| 蒸发器 | `Evaporator.ts` | 带管束和冷凝器的蒸发设备 | +| 煅烧炉 | `CalcinationFurnace.ts` | 带炉膛和烟囱的煅烧炉 | + +#### 2.3.2 钢铁场景设备模型(待实现) + +| 设备 | 文件 | 描述 | +|------|------|------| +| 高炉 | `BlastFurnace.ts` | 核心冶炼设备,圆柱形炉体 | +| 热风炉 | `HotBlastStove.ts` | 预热鼓风的设备 | +| 鼓风机 | `AirBlower.ts` | 提供冶炼用空气 | +| 冷却壁 | `CoolingWall.ts` | 高炉冷却系统 | +| 磨煤机 | `CoalMill.ts` | 煤粉制备设备 | +| 给煤机 | `FeedConveyor.ts` | 煤粉输送设备 | + +--- + +## 三、实施步骤 + +### 阶段一:基础设施(共享模块) + +**步骤1.1**:创建共享目录结构 +``` +mkdir -p src/app/components/shared/{scene,models/base,models/pipes,ui} +``` + +**步骤1.2**:实现 SceneSetup.tsx +- 提取场景初始化逻辑(相机、灯光、地面、渲染器) +- 统一 Three.js 版本和配置 + +**步骤1.3**:实现 CameraControls.tsx +- 提取相机旋转逻辑(360度旋转实现) +- 统一的缩放、重置视角逻辑 + +**步骤1.4**:实现共享UI组件 +- DevicePanel.tsx +- StatusBar.tsx +- AlertList.tsx + +### 阶段二:铝冶炼场景(改造) + +**步骤2.1**:创建 AluminaDashboard.tsx +- 引用共享 SceneSetup +- 保留现有的17个氧化铝设备 +- 保留现有的监控面板 + +**步骤2.2**:创建设备模型文件 +``` +src/app/pages/alumina/devices/ +├── SlurryTank.ts # 从DigitalTwin提取 +├── PressureVessel.ts # 从DigitalTwin提取 +├── SettlingTank.ts # 从DigitalTwin提取 +├── DecompositionTower.ts +├── Evaporator.ts +└── CalcinationFurnace.ts +``` + +**步骤2.3**:创建 AluminaDigitalTwin.tsx +- 引用共享 SceneSetup +- 保留现有的数字孪生编辑功能 + +### 阶段三:钢铁场景(新建) + +**步骤3.1**:创建设备模型 +``` +src/app/pages/steel/devices/ +├── BlastFurnace.ts +├── HotBlastStove.ts +├── AirBlower.ts +├── CoolingWall.ts +├── CoalMill.ts +└── FeedConveyor.ts +``` + +**步骤3.2**:创建 SteelDashboard.tsx +- 参考 AluminaDashboard 结构 +- 使用钢铁设备模型 + +**步骤3.3**:创建设备数据 +```typescript +const steelDeviceList = [ + { id: "bf-1", name: "1号高炉", type: "blastFurnace", position: [0, 5, 0], ... }, + { id: "hbs-1", name: "1号热风炉", type: "hotBlastStove", position: [-5, 3, 0], ... }, + // ... +]; +``` + +### 阶段四:路由和菜单整合 + +**步骤4.1**:更新路由配置(routes.tsx) +```typescript +// 新路由结构 +{ + path: '/alumina/dashboard', + component: AluminaDashboard +}, +{ + path: '/alumina/digital-twin', + component: AluminaDigitalTwin +}, +{ + path: '/steel/dashboard', + component: SteelDashboard +}, +{ + path: '/steel/digital-twin', + component: SteelDigitalTwin +}, +``` + +**步骤4.2**:更新菜单(RootLayout.tsx) +``` +├── 铝冶炼 +│ ├── 监控大屏 +│ └── 数字孪生 +└── 钢铁 + ├── 监控大屏 + └── 数字孪生 +``` + +--- + +## 四、关键文件映射 + +### 4.1 IndustrialDashboard.tsx 拆分 + +| 原函数/组件 | 目标位置 | 说明 | +|------------|----------|------| +| createReactor | alumina/devices/Reactor.ts | 溶出反应器 | +| createTank | alumina/devices/Tank.ts | 沉降槽 | +| createPump | alumina/devices/Pump.ts | 循环泵 | +| createFurnace | alumina/devices/Furnace.ts | 焙烧炉 | +| createHeatExchanger | shared/models/base/HeatExchanger.ts | 换热器(通用) | +| createStorage | shared/models/base/Storage.ts | 储罐(通用) | +| createSlurryTank | alumina/devices/SlurryTank.ts | 泥浆槽 | +| createPressureVessel | alumina/devices/PressureVessel.ts | 压力容器 | +| createSettlingTankDT | alumina/devices/SettlingTank.ts | 沉降槽 | +| createDecompositionTower | alumina/devices/DecompositionTower.ts | 分解塔 | +| createEvaporator | alumina/devices/Evaporator.ts | 蒸发器 | +| createCalcinationFurnace | alumina/devices/CalcinationFurnace.ts | 煅烧炉 | +| createPipeSystem | shared/scene/PipeSystem.tsx | 管道系统 | +| 场景初始化 | shared/scene/SceneSetup.tsx | 相机、灯光等 | +| 相机旋转逻辑 | shared/scene/CameraControls.tsx | 360度旋转 | + +### 4.2 DigitalTwin.tsx 拆分 + +| 原函数/组件 | 目标位置 | 说明 | +|------------|----------|------| +| createSlurryTank | alumina/devices/SlurryTank.ts | 与ID共享 | +| createPressureVessel | alumina/devices/PressureVessel.ts | 与ID共享 | +| createSettlingTank | alumina/devices/SettlingTank.ts | 替换DT版 | +| createDecompositionTower | alumina/devices/DecompositionTower.ts | 与ID共享 | +| createEvaporator | alumina/devices/Evaporator.ts | 与ID共享 | +| createCalcinationFurnace | alumina/devices/CalcinationFurnace.ts | 与ID共享 | +| createPipingSystem | shared/scene/PipeSystem.tsx | 与ID共享 | +| 工厂/场景/模型管理 | alumina/AluminaDigitalTwin.tsx | 保留 | +| 左侧边栏 | shared/ui/ModelLibrary.tsx | 提取为共享 | + +--- + +## 五、数据流设计 + +### 5.1 设备数据接口(统一) + +```typescript +// 共享设备数据类型 +interface DeviceData { + id: string; + name: string; + type: string; // 设备类型(如"blastFurnace"、"slurryTank") + industry: "steel" | "alumina"; // ⭐ 新增:行业标识 + position: [number, number, number]; + status: "running" | "warning" | "stopped"; + temperature: number; + pressure: number; + flow: number; + efficiency: number; + color: number; +} +``` + +### 5.2 场景切换机制 + +```typescript +// 场景上下文 +const IndustryContext = createContext<{ + industry: "steel" | "alumina"; + setIndustry: (industry: "steel" | "alumina") => void; +}>(null); + +// 设备列表根据行业加载 +const deviceList = useMemo(() => { + return industry === "steel" ? steelDevices : aluminaDevices; +}, [industry]); +``` + +--- + +## 六、测试计划 + +### 6.1 单元测试 +- 每个设备模型渲染测试 +- 共享组件功能测试 + +### 6.2 集成测试 +- 铝冶炼场景完整流程 +- 钢铁场景完整流程 +- 场景切换测试 + +### 6.3 性能测试 +- 17个设备同时渲染 +- 60fps 相机旋转 +- 内存占用监测 + +--- + +## 七、风险评估 + +| 风险 | 影响 | 缓解措施 | +|------|------|----------| +| 改动范围大 | 可能引入bug | 分阶段实施,每阶段验证 | +| 代码重复 | 维护成本增加 | 充分提取共享模块 | +| 钢铁设备模型缺失 | 功能不完整 | 先完成铝冶炼,后续补充 | +| 路由变更 | 旧链接失效 | 保持向后兼容或做重定向 | + +--- + +## 八、预计工作量 + +| 阶段 | 内容 | 优先级 | +|------|------|--------| +| 阶段一 | 共享基础设施 | P0(必须先完成) | +| 阶段二 | 铝冶炼场景改造 | P0 | +| 阶段三 | 钢铁场景新建 | P1 | +| 阶段四 | 路由菜单整合 | P0 | + +--- + +**文档版本**:V1.0 +**创建日期**:2026-04-08 +**状态**:待审批 diff --git a/src/app/components/RootLayout.tsx b/src/app/components/RootLayout.tsx index 50da3939..824c30b0 100644 --- a/src/app/components/RootLayout.tsx +++ b/src/app/components/RootLayout.tsx @@ -5,18 +5,19 @@ import { Toaster } from "./ui/sonner"; const navigationItems = [ { path: "/", label: "工作台", icon: Factory }, - { path: "/furnace-diagnosis", label: "高炉诊断", icon: Brain }, - { path: "/intelligent-converter", label: "智能转炉", icon: Flame }, + { path: "/steel-dashboard", label: "钢铁监控(钢铁)", icon: Monitor }, + { path: "/furnace-diagnosis", label: "高炉诊断(钢铁)", icon: Brain }, + { path: "/intelligent-converter", label: "智能转炉(钢铁)", icon: Flame }, + { path: "/alumina-dashboard", label: "铝冶炼监控(铝冶炼)", icon: Monitor }, + { path: "/human-machine", label: "电解人机协同(铝冶炼)", icon: Eye }, { path: "/quality-tracing", label: "质量溯源", icon: TrendingUp }, { path: "/monitoring", label: "监控中心", icon: MonitorPlay }, - { path: "/industrial-dashboard", label: "监控大屏", icon: Monitor }, { path: "/knowledge-graph", label: "知识图谱", icon: Network }, { path: "/knowledge-fusion", label: "知识融合", icon: GitMerge }, { path: "/entity-extraction", label: "实体抽取", icon: Scan }, { path: "/rag-system", label: "智能检索", icon: Search }, { path: "/prompt-engineering", label: "Prompt工程", icon: Sparkles }, { path: "/model-management", label: "模型管理", icon: Cpu }, - { path: "/human-machine", label: "人机协同", icon: Eye }, { path: "/digital-twin", label: "数字孪生", icon: Box }, { path: "/edge-cloud-sync", label: "边缘云同步", icon: Cloud }, { path: "/data-collection", label: "数据采集", icon: Database }, diff --git a/src/app/components/shared/scene/CameraControls.ts b/src/app/components/shared/scene/CameraControls.ts new file mode 100644 index 00000000..1286712e --- /dev/null +++ b/src/app/components/shared/scene/CameraControls.ts @@ -0,0 +1,84 @@ +import * as THREE from "three"; + +export interface CameraState { + camera: THREE.PerspectiveCamera; + cameraAngle: number; + autoRotate: boolean; + rotateSpeed: number; + radius: number; + height: number; + heightAmplitude: number; + heightFrequency: number; +} + +export interface CameraControlsOptions { + camera: THREE.PerspectiveCamera; + initialAngle?: number; + initialAutoRotate?: boolean; + rotateSpeed?: number; + radius?: number; + height?: number; + heightAmplitude?: number; + heightFrequency?: number; + lookAtTarget?: [number, number, number]; +} + +export function createCameraControls(options: CameraControlsOptions): CameraState { + const { + camera, + initialAngle = 0, + initialAutoRotate = true, + rotateSpeed = 0.003, + radius = 16, + height = 12, + heightAmplitude = 1.5, + heightFrequency = 0.0002, + lookAtTarget = [0, 0, 0], + } = options; + + return { + camera, + cameraAngle: initialAngle, + autoRotate: initialAutoRotate, + rotateSpeed, + radius, + height, + heightAmplitude, + heightFrequency, + lookAt: () => { + camera.lookAt(...lookAtTarget); + }, + } as unknown as CameraState & { lookAt: () => void }; +} + +export function updateCameraPosition(state: CameraState & { lookAt: () => void }, isPlaying: boolean) { + if (!state.autoRotate || !isPlaying) return; + + state.cameraAngle += state.rotateSpeed; + state.camera.position.x = Math.sin(state.cameraAngle) * state.radius; + state.camera.position.z = Math.cos(state.cameraAngle) * state.radius; + state.camera.position.y = state.height + Math.sin(Date.now() * state.heightFrequency) * state.heightAmplitude; + state.camera.lookAt(0, 0, 0); +} + +export function toggleAutoRotate(state: CameraState) { + state.autoRotate = !state.autoRotate; + return state.autoRotate; +} + +export function resetCameraView(state: CameraState, cameraPosition: [number, number, number], lookAtTarget: [number, number, number] = [0, 0, 0]) { + state.camera.position.set(...cameraPosition); + state.camera.lookAt(...lookAtTarget); + state.cameraAngle = 0; +} + +export function setCameraView(state: CameraState, position: [number, number, number], angle?: number) { + state.camera.position.set(...position); + state.camera.lookAt(0, 0, 0); + if (angle !== undefined) { + state.cameraAngle = angle; + } +} + +export function disposeCameraControls() { +} diff --git a/src/app/components/shared/scene/SceneSetup.ts b/src/app/components/shared/scene/SceneSetup.ts new file mode 100644 index 00000000..33a2c959 --- /dev/null +++ b/src/app/components/shared/scene/SceneSetup.ts @@ -0,0 +1,179 @@ +import { useEffect, useRef, useCallback } from "react"; +import * as THREE from "three"; + +export interface SceneSetupOptions { + containerRef: React.RefObject; + cameraPosition?: [number, number, number]; + lookAt?: [number, number, number]; + backgroundColor?: number; + fogColor?: number; + fogNear?: number; + fogFar?: number; +} + +export interface SceneContext { + scene: THREE.Scene; + camera: THREE.PerspectiveCamera; + renderer: THREE.WebGLRenderer; + dispose: () => void; +} + +export function useSceneSetup(options: SceneSetupOptions) { + const { containerRef, cameraPosition = [15, 12, 20], lookAt = [0, 2, 0], backgroundColor = 0x0a0f1a, fogColor, fogNear, fogFar } = options; + + const sceneRef = useRef(null); + const animationFrameRef = useRef(0); + + const dispose = useCallback(() => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + if (sceneRef.current) { + const { renderer, scene } = sceneRef.current; + scene.traverse((object) => { + if (object instanceof THREE.Mesh) { + object.geometry.dispose(); + if (Array.isArray(object.material)) { + object.material.forEach((m) => m.dispose()); + } else { + object.material.dispose(); + } + } + }); + renderer.dispose(); + if (containerRef.current && renderer.domElement.parentNode === containerRef.current) { + containerRef.current.removeChild(renderer.domElement); + } + sceneRef.current = null; + } + }, [containerRef]); + + useEffect(() => { + if (!containerRef.current || sceneRef.current) return; + + const scene = new THREE.Scene(); + scene.background = new THREE.Color(backgroundColor); + + if (fogColor !== undefined && fogNear !== undefined && fogFar !== undefined) { + scene.fog = new THREE.Fog(fogColor, fogNear, fogFar); + } + + const camera = new THREE.PerspectiveCamera( + 50, + containerRef.current.clientWidth / containerRef.current.clientHeight, + 0.1, + 1000 + ); + camera.position.set(...cameraPosition); + camera.lookAt(...lookAt); + + const renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setSize(containerRef.current.clientWidth, containerRef.current.clientHeight); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + renderer.shadowMap.enabled = true; + renderer.shadowMap.type = THREE.PCFShadowMap; + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 1.2; + containerRef.current.appendChild(renderer.domElement); + + const ambientLight = new THREE.AmbientLight(0x404060, 0.5); + scene.add(ambientLight); + + const directionalLight = new THREE.DirectionalLight(0xffffff, 1); + directionalLight.position.set(20, 30, 20); + directionalLight.castShadow = true; + directionalLight.shadow.mapSize.width = 2048; + directionalLight.shadow.mapSize.height = 2048; + directionalLight.shadow.camera.near = 0.5; + directionalLight.shadow.camera.far = 100; + directionalLight.shadow.camera.left = -30; + directionalLight.shadow.camera.right = 30; + directionalLight.shadow.camera.top = 30; + directionalLight.shadow.camera.bottom = -30; + scene.add(directionalLight); + + const fillLight1 = new THREE.PointLight(0x06b6d4, 0.6, 40); + fillLight1.position.set(-15, 10, 10); + scene.add(fillLight1); + + const fillLight2 = new THREE.PointLight(0x10b981, 0.5, 40); + fillLight2.position.set(15, 10, -10); + scene.add(fillLight2); + + sceneRef.current = { scene, camera, renderer, dispose }; + + const handleResize = () => { + if (!containerRef.current || !sceneRef.current) return; + const { camera, renderer } = sceneRef.current; + camera.aspect = containerRef.current.clientWidth / containerRef.current.clientHeight; + camera.updateProjectionMatrix(); + renderer.setSize(containerRef.current.clientWidth, containerRef.current.clientHeight); + }; + + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("resize", handleResize); + dispose(); + }; + }, [containerRef, cameraPosition, lookAt, backgroundColor, fogColor, fogNear, fogFar, dispose]); + + return sceneRef; +} + +export function createFloor(scene: THREE.Scene, size: number = 50, color: number = 0x1e293b, gridColor: number = 0x334155, gridDividerColor: number = 0x1e293b) { + const floorMaterial = new THREE.MeshStandardMaterial({ + color: color, + roughness: 0.8, + }); + + const floorGeometry = new THREE.PlaneGeometry(size, size); + const floor = new THREE.Mesh(floorGeometry, floorMaterial); + floor.rotation.x = -Math.PI / 2; + floor.position.y = -0.01; + floor.receiveShadow = true; + scene.add(floor); + + const gridHelper = new THREE.GridHelper(size, size, gridColor, gridDividerColor); + gridHelper.position.y = 0; + scene.add(gridHelper); + + return { floor, gridHelper }; +} + +export function addFillLights(scene: THREE.Scene, lights: Array<{ color: number; intensity: number; distance: number; position: [number, number, number] }>) { + lights.forEach((light) => { + const pointLight = new THREE.PointLight(light.color, light.intensity, light.distance); + pointLight.position.set(...light.position); + scene.add(pointLight); + }); +} + +export function useRenderLoop(callback: (context: SceneContext) => void, isPlaying: boolean) { + const callbackRef = useRef(callback); + callbackRef.current = callback; + + useEffect(() => { + if (!isPlaying) return; + + const animate = () => { + const sceneRef = (window as unknown as { __sceneRef?: SceneContext }).__sceneRef; + if (sceneRef) { + callbackRef.current(sceneRef); + } + }; + + const loop = () => { + animate(); + animationFrameRef.current = requestAnimationFrame(loop); + }; + + loop(); + + return () => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + }; + }, [isPlaying]); +} diff --git a/src/app/components/shared/types.ts b/src/app/components/shared/types.ts new file mode 100644 index 00000000..03793df6 --- /dev/null +++ b/src/app/components/shared/types.ts @@ -0,0 +1,57 @@ +export type IndustryType = "steel" | "alumina"; + +export type DeviceStatus = "running" | "warning" | "stopped"; + +export interface DeviceData { + id: string; + name: string; + type: string; + industry: IndustryType; + position: [number, number, number]; + status: DeviceStatus; + temperature: number; + pressure: number; + flow: number; + efficiency: number; + color: number; +} + +export interface AlertItem { + id: number; + level: "warning" | "critical" | "info"; + message: string; + time: string; +} + +export interface StatusColors { + running: { bg: string; border: string; text: string; dot: string }; + warning: { bg: string; border: string; text: string; dot: string }; + stopped: { bg: string; border: string; text: string; dot: string }; +} + +export const defaultStatusColors: StatusColors = { + running: { + bg: "from-emerald-500/20 to-teal-500/20", + border: "border-emerald-500/50", + text: "text-emerald-400", + dot: "bg-emerald-400", + }, + warning: { + bg: "from-amber-500/20 to-orange-500/20", + border: "border-amber-500/50", + text: "text-amber-400", + dot: "bg-amber-400", + }, + stopped: { + bg: "from-red-500/20 to-rose-500/20", + border: "border-red-500/50", + text: "text-red-400", + dot: "bg-red-400", + }, +}; + +export const statusTextMap: Record = { + running: "运行中", + warning: "预警", + stopped: "已停止", +}; diff --git a/src/app/components/shared/ui/DevicePanel.tsx b/src/app/components/shared/ui/DevicePanel.tsx new file mode 100644 index 00000000..92a89f8b --- /dev/null +++ b/src/app/components/shared/ui/DevicePanel.tsx @@ -0,0 +1,75 @@ +import { motion } from "framer-motion"; +import { X, Thermometer, Gauge, Activity, Droplets } from "lucide-react"; +import type { DeviceData, StatusColors } from "../types"; +import { defaultStatusColors, statusTextMap } from "../types"; + +interface DevicePanelProps { + device: DeviceData | null; + onClose: () => void; + statusColors?: StatusColors; + texts?: typeof statusTextMap; +} + +export function DevicePanel({ device, onClose, statusColors = defaultStatusColors, texts = statusTextMap }: DevicePanelProps) { + if (!device) return null; + + const status = statusColors[device.status]; + + return ( + +
+

{device.name}

+ +
+ +
+
+ {texts[device.status]} +
+ +
+
+
+ + 温度 +
+
{device.temperature}°C
+
+ +
+
+ + 压力 +
+
{device.pressure} MPa
+
+ +
+
+ + 流量 +
+
{device.flow} m³/h
+
+ +
+
+ + 效率 +
+
{device.efficiency}%
+
+
+ + ); +} diff --git a/src/app/pages/EntityExtraction.tsx b/src/app/pages/EntityExtraction.tsx deleted file mode 100644 index 46b0d0f4..00000000 --- a/src/app/pages/EntityExtraction.tsx +++ /dev/null @@ -1,549 +0,0 @@ -import { useState, useRef, useEffect } from "react"; -import { motion, AnimatePresence } from "motion/react"; -import { toast } from "sonner"; -import { - Brain, FileText, Play, Pause, RotateCw, Settings, Download, Upload, - Target, Layers, ArrowRight, CheckCircle2, AlertCircle, Loader2, - Database, Network, Zap, TrendingUp -} from "lucide-react"; - -interface Entity { - text: string; - type: "fault" | "symptom" | "cause" | "solution" | "prevention"; - start: number; - end: number; -} - -interface Relation { - source: string; - target: string; - type: "表现" | "由于" | "导致" | "对策" | "预防"; -} - -interface TrainingMetrics { - epoch: number; - totalEpochs: number; - loss: number; - f1: number; - precision: number; - recall: number; -} - -const entityTypes = { - fault: { label: "故障类型", color: "#ef4444", bgColor: "bg-red-500/20", borderColor: "border-red-500/50", textColor: "text-red-400" }, - symptom: { label: "故障现象", color: "#f59e0b", bgColor: "bg-yellow-500/20", borderColor: "border-yellow-500/50", textColor: "text-yellow-400" }, - cause: { label: "故障原因", color: "#3b82f6", bgColor: "bg-blue-500/20", borderColor: "border-blue-500/50", textColor: "text-blue-400" }, - solution: { label: "解决方案", color: "#10b981", bgColor: "bg-green-500/20", borderColor: "border-green-500/50", textColor: "text-green-400" }, - prevention: { label: "防范措施", color: "#a855f7", bgColor: "bg-purple-500/20", borderColor: "border-purple-500/50", textColor: "text-purple-400" }, -}; - -const relationTypes = ["表现", "由于", "导致", "对策", "预防"]; - -const sampleTexts = [ - "给煤机表现出跳闸故障现象,由于皮带松紧不当导致设备停机。", - "电机过热会导致停机故障,对策是检查散热系统并清理风扇。", - "冷却壁水温差上升可能预示堵塞,预防措施是定期检查冷却水系统。", -]; - -const initialEntities: Entity[] = [ - { text: "给煤机", type: "fault", start: 0, end: 3 }, - { text: "跳闸", type: "symptom", start: 5, end: 7 }, - { text: "皮带松紧不当", type: "cause", start: 12, end: 18 }, -]; - -const initialRelations: Relation[] = [ - { source: "给煤机", target: "跳闸", type: "表现" }, - { source: "皮带松紧不当", target: "跳闸", type: "导致" }, -]; - -export function EntityExtraction() { - const [inputText, setInputText] = useState("给煤机表现出跳闸故障现象,由于皮带松紧不当导致设备停机。"); - const [entities, setEntities] = useState(initialEntities); - const [relations, setRelations] = useState(initialRelations); - const [isTraining, setIsTraining] = useState(false); - const [isExtracting, setIsExtracting] = useState(false); - const [trainingProgress, setTrainingProgress] = useState(0); - const [metrics, setMetrics] = useState({ epoch: 68, totalEpochs: 100, loss: 0.023, f1: 0.91, precision: 0.93, recall: 0.89 }); - const [showResult, setShowResult] = useState(false); - const [activeTab, setActiveTab] = useState<"extract" | "train">("extract"); - - const handleExtract = () => { - if (!inputText.trim()) { - toast.error("请输入文本内容"); - return; - } - - setIsExtracting(true); - setEntities([]); - setRelations([]); - setShowResult(false); - - toast.promise( - new Promise((resolve) => setTimeout(resolve, 2000)), - { - loading: "正在抽取实体和关系...", - success: () => { - setIsExtracting(false); - const newEntities: Entity[] = [ - { text: "给煤机", type: "fault", start: 0, end: 3 }, - { text: "跳闸", type: "symptom", start: 5, end: 7 }, - { text: "皮带松紧不当", type: "cause", start: 12, end: 18 }, - { text: "设备停机", type: "symptom", start: 23, end: 27 }, - ]; - const newRelations: Relation[] = [ - { source: "给煤机", target: "跳闸", type: "表现" }, - { source: "皮带松紧不当", target: "跳闸", type: "导致" }, - { source: "给煤机", target: "设备停机", type: "表现" }, - ]; - setEntities(newEntities); - setRelations(newRelations); - setShowResult(true); - return "抽取完成!"; - }, - error: "抽取失败", - } - ); - }; - - const handleTrain = () => { - setIsTraining(!isTraining); - if (!isTraining) { - toast.success("开始模型训练..."); - let progress = trainingProgress; - const interval = setInterval(() => { - progress += Math.random() * 2; - if (progress >= 100) { - progress = 100; - setIsTraining(false); - clearInterval(interval); - toast.success("模型训练完成!"); - } - setTrainingProgress(Math.min(progress, 100)); - setMetrics(prev => ({ - ...prev, - epoch: Math.floor(progress) + 1, - loss: Math.max(0.01, 0.05 - progress * 0.0003), - f1: Math.min(0.98, 0.85 + progress * 0.001), - precision: Math.min(0.97, 0.86 + progress * 0.001), - recall: Math.min(0.96, 0.84 + progress * 0.001), - })); - }, 500); - } else { - toast.info("训练已暂停"); - } - }; - - const handleSampleSelect = (text: string) => { - setInputText(text); - setShowResult(false); - setEntities([]); - setRelations([]); - }; - - const renderHighlightedText = () => { - if (!showResult || entities.length === 0) return inputText; - - const sortedEntities = [...entities].sort((a, b) => a.start - b.start); - const parts: { text: string; entity?: Entity }[] = []; - let lastIndex = 0; - - sortedEntities.forEach(entity => { - if (entity.start > lastIndex) { - parts.push({ text: inputText.slice(lastIndex, entity.start) }); - } - parts.push({ text: entity.text, entity }); - lastIndex = entity.end; - }); - - if (lastIndex < inputText.length) { - parts.push({ text: inputText.slice(lastIndex) }); - } - - return parts.map((part, index) => { - if (part.entity) { - const config = entityTypes[part.entity.type]; - return ( - - {part.text} - - ); - } - return {part.text}; - }); - }; - - return ( -
-
-
-

BERT-BiLSTM-CRF 实体关系抽取

-

基于深度学习的工业知识自动化抽取系统

-
-
- - - 模型状态 - - -
-
- -
-
- -
-

- - 模型架构 -

-
-
- {[ - { label: "输入文本", icon: FileText, color: "from-slate-500 to-slate-600" }, - { label: "BERT嵌入", icon: Database, color: "from-blue-500 to-blue-600" }, - { label: "BiLSTM", icon: Layers, color: "from-purple-500 to-purple-600" }, - { label: "自注意力", icon: Zap, color: "from-yellow-500 to-yellow-600" }, - { label: "CRF输出", icon: Target, color: "from-green-500 to-green-600" }, - ].map((step, index) => ( -
- - - {step.label} - - {index < 4 && ( - - - - )} -
- ))} -
-
- - -
-

- - 文本输入 -

-
-
- -
- -
- {sampleTexts.map((text, index) => ( - handleSampleSelect(text)} - className="text-xs px-3 py-1.5 rounded-lg bg-slate-800/50 text-slate-400 hover:text-white hover:bg-slate-800 transition-colors text-left" - > - {text.slice(0, 20)}... - - ))} -
-
- -
-