add new contents

This commit is contained in:
2026-04-08 23:50:01 +08:00
parent 38eef9e645
commit 05a9313493
32 changed files with 4353 additions and 2369 deletions

View File

@@ -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.

View File

@@ -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<HTMLDivElement>;
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
**状态**:待审批

View File

@@ -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 },

View File

@@ -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() {
}

View File

@@ -0,0 +1,179 @@
import { useEffect, useRef, useCallback } from "react";
import * as THREE from "three";
export interface SceneSetupOptions {
containerRef: React.RefObject<HTMLDivElement | null>;
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<SceneContext | null>(null);
const animationFrameRef = useRef<number>(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]);
}

View File

@@ -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<DeviceStatus, string> = {
running: "运行中",
warning: "预警",
stopped: "已停止",
};

View File

@@ -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 (
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
className="bg-slate-800/90 backdrop-blur-xl rounded-2xl border border-slate-700/50 p-5"
>
<div className="flex items-center justify-between mb-4">
<h3 className="text-white font-semibold text-lg">{device.name}</h3>
<button
onClick={onClose}
className="p-1.5 rounded-lg hover:bg-slate-700/50 transition-colors"
>
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<div className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-gradient-to-r ${status.bg} border ${status.border}`}>
<div className={`w-2 h-2 rounded-full animate-pulse ${status.dot}`} />
<span className={`text-sm font-medium ${status.text}`}>{texts[device.status]}</span>
</div>
<div className="grid grid-cols-2 gap-3 mt-5">
<div className="bg-slate-900/50 rounded-xl p-3">
<div className="flex items-center gap-2 text-red-400 mb-1">
<Thermometer className="w-4 h-4" />
<span className="text-xs"></span>
</div>
<div className="text-white font-mono font-semibold">{device.temperature}°C</div>
</div>
<div className="bg-slate-900/50 rounded-xl p-3">
<div className="flex items-center gap-2 text-blue-400 mb-1">
<Gauge className="w-4 h-4" />
<span className="text-xs"></span>
</div>
<div className="text-white font-mono font-semibold">{device.pressure} MPa</div>
</div>
<div className="bg-slate-900/50 rounded-xl p-3">
<div className="flex items-center gap-2 text-cyan-400 mb-1">
<Droplets className="w-4 h-4" />
<span className="text-xs"></span>
</div>
<div className="text-white font-mono font-semibold">{device.flow} m³/h</div>
</div>
<div className="bg-slate-900/50 rounded-xl p-3">
<div className="flex items-center gap-2 text-emerald-400 mb-1">
<Activity className="w-4 h-4" />
<span className="text-xs"></span>
</div>
<div className="text-white font-mono font-semibold">{device.efficiency}%</div>
</div>
</div>
</motion.div>
);
}

View File

@@ -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<Entity[]>(initialEntities);
const [relations, setRelations] = useState<Relation[]>(initialRelations);
const [isTraining, setIsTraining] = useState(false);
const [isExtracting, setIsExtracting] = useState(false);
const [trainingProgress, setTrainingProgress] = useState(0);
const [metrics, setMetrics] = useState<TrainingMetrics>({ 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 (
<motion.span
key={index}
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className={`${config.bgColor} ${config.borderColor} border rounded px-1 mx-0.5`}
>
<span className={`${config.textColor} font-medium`}>{part.text}</span>
</motion.span>
);
}
return <span key={index} className="text-slate-300">{part.text}</span>;
});
};
return (
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white mb-1">BERT-BiLSTM-CRF </h1>
<p className="text-sm text-slate-400"></p>
</div>
<div className="flex items-center gap-3">
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-slate-800/50"
>
<Brain className="w-4 h-4 text-purple-400" />
<span className="text-sm text-slate-300"></span>
<motion.div
animate={{ opacity: [0.5, 1, 0.5] }}
transition={{ duration: 1.5, repeat: Infinity }}
className="w-2 h-2 rounded-full bg-green-400"
/>
</motion.div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="rounded-xl bg-slate-900/50 border border-slate-800/50 backdrop-blur-sm p-6"
>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
<Layers className="w-5 h-5 text-blue-400" />
</h2>
</div>
<div className="flex items-center justify-between gap-2 overflow-x-auto pb-2">
{[
{ 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) => (
<div key={step.label} className="flex items-center">
<motion.div
whileHover={{ scale: 1.05, y: -3 }}
className={`flex flex-col items-center gap-2 px-4 py-3 rounded-lg bg-gradient-to-br ${step.color} min-w-[100px]`}
>
<step.icon className="w-6 h-6 text-white" />
<span className="text-xs font-medium text-white">{step.label}</span>
</motion.div>
{index < 4 && (
<motion.div
animate={{ x: [0, 5, 0] }}
transition={{ duration: 1, repeat: Infinity }}
className="mx-1"
>
<ArrowRight className="w-4 h-4 text-slate-400" />
</motion.div>
)}
</div>
))}
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="rounded-xl bg-slate-900/50 border border-slate-800/50 backdrop-blur-sm p-6"
>
<div className="flex items-center gap-4 mb-4">
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
<FileText className="w-5 h-5 text-cyan-400" />
</h2>
<div className="flex-1 h-px bg-slate-700" />
</div>
<div className="mb-4">
<label className="text-sm text-slate-400 mb-2 block"></label>
<div className="flex flex-wrap gap-2">
{sampleTexts.map((text, index) => (
<motion.button
key={index}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => 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)}...
</motion.button>
))}
</div>
</div>
<div className="relative">
<textarea
value={inputText}
onChange={(e) => { setInputText(e.target.value); setShowResult(false); }}
placeholder="请输入需要抽取的文本内容..."
className="w-full h-32 px-4 py-3 rounded-lg bg-slate-800/50 border border-slate-700/50 text-white placeholder-slate-500 resize-none focus:outline-none focus:border-blue-500/50 focus:ring-2 focus:ring-blue-500/20 transition-all"
/>
<AnimatePresence>
{isExtracting && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 flex items-center justify-center bg-slate-900/80 rounded-lg"
>
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full"
/>
</motion.div>
)}
</AnimatePresence>
</div>
<div className="flex items-center gap-3 mt-4">
<motion.button
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
onClick={handleExtract}
disabled={isExtracting}
className="flex items-center gap-2 px-5 py-2.5 rounded-lg bg-gradient-to-r from-blue-500 to-cyan-500 text-white font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
{isExtracting ? (
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
>
<Loader2 className="w-4 h-4" />
</motion.div>
) : (
<Target className="w-4 h-4" />
)}
{isExtracting ? "抽取中..." : "开始抽取"}
</motion.button>
</div>
</motion.div>
<AnimatePresence>
{showResult && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="rounded-xl bg-slate-900/50 border border-slate-800/50 backdrop-blur-sm p-6"
>
<div className="flex items-center gap-4 mb-4">
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
<CheckCircle2 className="w-5 h-5 text-green-400" />
</h2>
<motion.span
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="flex items-center gap-1 text-xs px-2 py-1 rounded bg-green-500/20 text-green-400"
>
<CheckCircle2 className="w-3 h-3" />
</motion.span>
</div>
<div className="mb-4 p-4 rounded-lg bg-slate-800/50">
<label className="text-xs text-slate-500 mb-2 block"></label>
<p className="text-base leading-relaxed">{renderHighlightedText()}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h3 className="text-sm font-medium text-white mb-3 flex items-center gap-2">
<Network className="w-4 h-4 text-orange-400" />
({entities.length})
</h3>
<div className="space-y-2">
<AnimatePresence>
{entities.map((entity, index) => (
<motion.div
key={`${entity.text}-${index}`}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.1 }}
className={`flex items-center justify-between p-2 rounded-lg ${entityTypes[entity.type].bgColor} border ${entityTypes[entity.type].borderColor}`}
>
<span className="text-sm text-white font-mono">{entity.text}</span>
<span className={`text-xs px-2 py-0.5 rounded ${entityTypes[entity.type].bgColor} ${entityTypes[entity.type].textColor}`}>
{entityTypes[entity.type].label}
</span>
</motion.div>
))}
</AnimatePresence>
</div>
</div>
<div>
<h3 className="text-sm font-medium text-white mb-3 flex items-center gap-2">
<ArrowRight className="w-4 h-4 text-purple-400" />
({relations.length})
</h3>
<div className="space-y-2">
<AnimatePresence>
{relations.map((relation, index) => (
<motion.div
key={`${relation.source}-${relation.target}-${index}`}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.1 }}
className="flex items-center gap-2 p-2 rounded-lg bg-slate-800/50 border border-slate-700/50"
>
<span className="text-sm text-cyan-400 font-mono">{relation.source}</span>
<motion.span
animate={{ opacity: [0.5, 1, 0.5] }}
transition={{ duration: 1.5, repeat: Infinity }}
className="text-xs px-2 py-0.5 rounded bg-purple-500/20 text-purple-400"
>
{relation.type}
</motion.span>
<ArrowRight className="w-3 h-3 text-slate-500" />
<span className="text-sm text-cyan-400 font-mono">{relation.target}</span>
</motion.div>
))}
</AnimatePresence>
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
<div className="space-y-6">
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 }}
className="rounded-xl bg-slate-900/50 border border-slate-800/50 backdrop-blur-sm p-6"
>
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-green-400" />
</h2>
<div className="mb-4">
<div className="flex items-center justify-between text-sm mb-2">
<span className="text-slate-400"></span>
<span className="text-white font-medium">{Math.round(trainingProgress)}%</span>
</div>
<div className="h-3 bg-slate-800 rounded-full overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${trainingProgress}%` }}
className="h-full bg-gradient-to-r from-blue-500 to-cyan-500 rounded-full relative"
>
<motion.div
className="absolute inset-0 bg-white/30"
animate={{ x: ["-100%", "200%"] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
/>
</motion.div>
</div>
<div className="text-xs text-slate-500 mt-1">
Epoch {metrics.epoch}/{metrics.totalEpochs}
</div>
</div>
<div className="grid grid-cols-2 gap-3 mb-4">
{[
{ label: "Loss", value: metrics.loss.toFixed(3), color: "text-red-400" },
{ label: "F1", value: metrics.f1.toFixed(2), color: "text-green-400" },
{ label: "Precision", value: metrics.precision.toFixed(2), color: "text-blue-400" },
{ label: "Recall", value: metrics.recall.toFixed(2), color: "text-yellow-400" },
].map((metric) => (
<motion.div
key={metric.label}
animate={isTraining ? { scale: [1, 1.02, 1] } : {}}
transition={{ duration: 2, repeat: Infinity }}
className="p-3 rounded-lg bg-slate-800/50"
>
<div className="text-xs text-slate-500 mb-1">{metric.label}</div>
<div className={`text-lg font-bold ${metric.color}`}>{metric.value}</div>
</motion.div>
))}
</div>
<motion.button
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
onClick={handleTrain}
className={`w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium ${
isTraining
? "bg-red-500/20 text-red-400 border border-red-500/50"
: "bg-green-500/20 text-green-400 border border-green-500/50"
}`}
>
{isTraining ? (
<>
<Pause className="w-4 h-4" />
</>
) : (
<>
<Play className="w-4 h-4" />
</>
)}
</motion.button>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3 }}
className="rounded-xl bg-slate-900/50 border border-slate-800/50 backdrop-blur-sm p-6"
>
<h2 className="text-lg font-semibold text-white mb-4"></h2>
<div className="space-y-2">
{Object.entries(entityTypes).map(([key, config]) => (
<motion.div
key={key}
whileHover={{ scale: 1.02, x: 3 }}
className={`flex items-center justify-between p-2 rounded-lg ${config.bgColor} border ${config.borderColor}`}
>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded" style={{ backgroundColor: config.color }} />
<span className="text-sm text-white">{config.label}</span>
</div>
<span className={`text-xs ${config.textColor}`}>
{entities.filter(e => e.type === key).length}
</span>
</motion.div>
))}
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.4 }}
className="rounded-xl bg-slate-900/50 border border-slate-800/50 backdrop-blur-sm p-6"
>
<h2 className="text-lg font-semibold text-white mb-4"></h2>
<div className="space-y-2">
{relationTypes.map((type, index) => (
<motion.div
key={type}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 + index * 0.05 }}
className="flex items-center gap-2 p-2 rounded-lg bg-slate-800/50"
>
<ArrowRight className="w-4 h-4 text-purple-400" />
<span className="text-sm text-white">{type}</span>
<span className="ml-auto text-xs text-slate-500">
{relations.filter(r => r.type === type).length}
</span>
</motion.div>
))}
</div>
</motion.div>
</div>
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -446,7 +446,7 @@ export function HumanMachineCollaboration() {
<Cpu className="w-6 h-6 text-cyan-400" />
</div>
<div>
<h1 className="text-2xl font-bold text-white">VR系统</h1>
<h1 className="text-2xl font-bold text-white">VR系统()</h1>
<p className="text-sm text-slate-400"></p>
</div>
</div>

View File

@@ -0,0 +1,84 @@
import type { DeviceData, DeviceStatus, IndustryType } from "../../../components/shared/types";
export type AluminaDeviceType =
| "reactor"
| "tank"
| "pump"
| "furnace"
| "heatExchanger"
| "storage"
| "slurryTank"
| "pressureVessel"
| "settlingTank"
| "decompositionTower"
| "evaporator"
| "calcinationFurnace";
export interface AluminaDeviceData {
id: string;
name: string;
type: AluminaDeviceType;
industry: "alumina";
position: [number, number, number];
status: "running" | "warning" | "stopped";
temperature: number;
pressure: number;
flow: number;
efficiency: number;
color: number;
}
export type { DeviceData } from "../../../components/shared/types";
export const aluminaStatusColors = {
running: {
bg: "from-cyan-500/20 to-blue-500/20",
border: "border-cyan-500/50",
text: "text-cyan-400",
dot: "bg-cyan-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 aluminaStatusText = {
running: "运行中",
warning: "预警",
stopped: "已停止",
};
export const aluminaDevices: AluminaDeviceData[] = [
{ id: "reactor-1", name: "溶出反应器 #1", type: "reactor", industry: "alumina", position: [-8, 3, 0], status: "running", temperature: 258, pressure: 4.5, flow: 125, efficiency: 96.2, color: 0x06b6d4 },
{ id: "reactor-2", name: "溶出反应器 #2", type: "reactor", industry: "alumina", position: [-5.5, 3, 0], status: "running", temperature: 255, pressure: 4.3, flow: 118, efficiency: 94.8, color: 0x06b6d4 },
{ id: "slurry-tank-1", name: "泥浆槽 #1", type: "slurryTank", industry: "alumina", position: [-3, 1.5, 0], status: "running", temperature: 85, pressure: 0.5, flow: 95, efficiency: 93.5, color: 0x8b5cf6 },
{ id: "slurry-tank-2", name: "泥浆槽 #2", type: "slurryTank", industry: "alumina", position: [-3, 1.5, 2.5], status: "running", temperature: 82, pressure: 0.5, flow: 90, efficiency: 92.8, color: 0x8b5cf6 },
{ id: "pressure-vessel-1", name: "压力容器 #1", type: "pressureVessel", industry: "alumina", position: [0, 2, 0], status: "running", temperature: 150, pressure: 3.2, flow: 110, efficiency: 95.1, color: 0xdc2626 },
{ id: "pressure-vessel-2", name: "压力容器 #2", type: "pressureVessel", industry: "alumina", position: [0, 2, 2.5], status: "warning", temperature: 165, pressure: 3.5, flow: 98, efficiency: 91.2, color: 0xf59e0b },
{ id: "tank-1", name: "沉降槽 #1", type: "tank", industry: "alumina", position: [3, 2, 0], status: "running", temperature: 98, pressure: 0.8, flow: 85, efficiency: 92.5, color: 0x10b981 },
{ id: "tank-2", name: "沉降槽 #2", type: "tank", industry: "alumina", position: [3, 2, 2.5], status: "warning", temperature: 105, pressure: 0.9, flow: 72, efficiency: 87.3, color: 0xf59e0b },
{ id: "settling-tank-dt", name: "沉降槽 DT", type: "settlingTank", industry: "alumina", position: [3, 2, -2.5], status: "running", temperature: 95, pressure: 0.7, flow: 88, efficiency: 93.8, color: 0x10b981 },
{ id: "decomposition-tower-1", name: "分解塔 #1", type: "decompositionTower", industry: "alumina", position: [6, 3, 0], status: "running", temperature: 200, pressure: 1.5, flow: 75, efficiency: 94.5, color: 0x737373 },
{ id: "evaporator-1", name: "蒸发器 #1", type: "evaporator", industry: "alumina", position: [6, 3, 2.5], status: "running", temperature: 180, pressure: 1.2, flow: 95, efficiency: 95.8, color: 0x64748b },
{ id: "calcination-furnace-1", name: "煅烧炉 #1", type: "calcinationFurnace", industry: "alumina", position: [9, 2.5, 0], status: "running", temperature: 1200, pressure: 0.5, flow: 45, efficiency: 94.2, color: 0x78350f },
{ id: "pump-1", name: "循环泵 #1", type: "pump", industry: "alumina", position: [-6, -0.5, 0], status: "running", temperature: 65, pressure: 2.5, flow: 180, efficiency: 91.8, color: 0x8b5cf6 },
{ id: "pump-2", name: "循环泵 #2", type: "pump", industry: "alumina", position: [-3.5, -0.5, 0], status: "running", temperature: 68, pressure: 2.3, flow: 175, efficiency: 90.5, color: 0x8b5cf6 },
{ id: "heat-exchanger", name: "换热器 #1", type: "heatExchanger", industry: "alumina", position: [6, 0, 0], status: "running", temperature: 180, pressure: 1.2, flow: 95, efficiency: 95.8, color: 0x0891b2 },
{ id: "storage-1", name: "原料储罐 #1", type: "storage", industry: "alumina", position: [9, 2.5, -2.5], status: "running", temperature: 45, pressure: 0.3, flow: 60, efficiency: 98.1, color: 0x3b82f6 },
{ id: "furnace-1", name: "焙烧炉 #1", type: "furnace", industry: "alumina", position: [9, 2.5, 2.5], status: "running", temperature: 1200, pressure: 0.5, flow: 45, efficiency: 94.2, color: 0xef4444 },
];
export const aluminaAlerts = [
{ id: 1, level: "warning" as const, message: "压力容器 #2 温度异常偏高", time: "10:23:45" },
{ id: 2, level: "warning" as const, message: "沉降槽 #2 液位低于阈值", time: "10:15:30" },
{ id: 3, level: "info" as const, message: "循环泵 #1 完成例行检修", time: "09:45:00" },
];

View File

@@ -1,7 +1,7 @@
import { Users, MessageSquare, Cpu, Zap, TrendingUp, AlertTriangle, Activity, RefreshCw, Clock, ArrowRight, Sparkles, Brain, Database, Network, Eye, Settings, Bell, User, Factory, Layers, GitBranch, FileText, Cloud, Cpu as CpuIcon } from "lucide-react";
import { AreaChart, Area, BarChart, Bar, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
import { motion, AnimatePresence } from "motion/react";
import { StatCard } from "../components/StatCard";
import { StatCard } from "../../components/StatCard";
import { useState, useEffect, useCallback } from "react";
import { toast } from "sonner";
import { useNavigate } from "react-router";

View File

@@ -0,0 +1,593 @@
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";
type CommonEntityType = "fault" | "symptom" | "cause" | "solution" | "prevention";
type SteelEntityType = "blastFurnace" | "steelmaking" | "rolling" | "processParam" | "defectType";
type AluminaEntityType = "electrolytic" | "aluminaQuality" | "dissolution" | "effectType" | "qualityIndex";
type EntityType = CommonEntityType | SteelEntityType | AluminaEntityType;
interface Entity {
text: string;
type: EntityType;
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;
}
type IndustryType = "common" | "steel" | "alumina";
const commonEntityTypes = {
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 steelEntityTypes = {
blastFurnace: { label: "高炉参数", color: "#f97316", bgColor: "bg-orange-500/20", borderColor: "border-orange-500/50", textColor: "text-orange-400" },
steelmaking: { label: "炼钢参数", color: "#3b82f6", bgColor: "bg-blue-500/20", borderColor: "border-blue-500/50", textColor: "text-blue-400" },
rolling: { label: "轧制缺陷", color: "#8b5cf6", bgColor: "bg-violet-500/20", borderColor: "border-violet-500/50", textColor: "text-violet-400" },
processParam: { label: "工艺指标", color: "#06b6d4", bgColor: "bg-cyan-500/20", borderColor: "border-cyan-500/50", textColor: "text-cyan-400" },
defectType: { label: "缺陷类型", color: "#ef4444", bgColor: "bg-red-500/20", borderColor: "border-red-500/50", textColor: "text-red-400" },
};
const aluminaEntityTypes = {
electrolytic: { label: "电解参数", color: "#0d9488", bgColor: "bg-teal-500/20", borderColor: "border-teal-500/50", textColor: "text-teal-400" },
aluminaQuality: { label: "氧化铝指标", color: "#8b5cf6", bgColor: "bg-purple-500/20", borderColor: "border-purple-500/50", textColor: "text-purple-400" },
dissolution: { label: "溶解参数", color: "#0ea5e9", bgColor: "bg-sky-500/20", borderColor: "border-sky-500/50", textColor: "text-sky-400" },
effectType: { label: "效应类型", color: "#f59e0b", bgColor: "bg-amber-500/20", borderColor: "border-amber-500/50", textColor: "text-amber-400" },
qualityIndex: { label: "质量指标", color: "#10b981", bgColor: "bg-emerald-500/20", borderColor: "border-emerald-500/50", textColor: "text-emerald-400" },
};
const relationTypes = ["表现", "由于", "导致", "对策", "预防"];
const sampleTexts = {
common: [
"给煤机表现出跳闸故障现象,由于皮带松紧不当导致设备停机。",
"电机过热会导致停机故障,对策是检查散热系统并清理风扇。",
"冷却壁水温差上升可能预示堵塞,预防措施是定期检查冷却水系统。",
],
steel: [
"高炉有效容积利用系数下降至3.8,可能由焦比升高或风温不足引起。",
"转炉炼钢过程中钢水温度达到1650℃精炼阶段需要控制碳含量。",
"热轧带钢出现边部裂纹,原因是轧制温度偏低或压下量过大。",
],
alumina: [
"电解槽槽电压升高至4.2V,阳极效应系数增加,需要调整分子比。",
"氧化铝含量下降至97.5%,溶出率偏低需要检查拜耳法工艺参数。",
"晶种分解效率波动分解率下降至48%,可能与精液浓度有关。",
],
};
const initialEntities = {
common: [
{ text: "给煤机", type: "fault" as EntityType, start: 0, end: 3 },
{ text: "跳闸", type: "symptom" as EntityType, start: 5, end: 7 },
{ text: "皮带松紧不当", type: "cause" as EntityType, start: 12, end: 18 },
],
steel: [
{ text: "高炉有效容积利用系数", type: "blastFurnace" as EntityType, start: 0, end: 10 },
{ text: "焦比升高", type: "processParam" as EntityType, start: 22, end: 27 },
{ text: "风温不足", type: "cause" as EntityType, start: 30, end: 34 },
],
alumina: [
{ text: "槽电压", type: "electrolytic" as EntityType, start: 0, end: 3 },
{ text: "4.2V", type: "processParam" as EntityType, start: 6, end: 10 },
{ text: "阳极效应系数", type: "effectType" as EntityType, start: 12, end: 18 },
],
};
const initialRelations = {
common: [
{ source: "给煤机", target: "跳闸", type: "表现" as const },
{ source: "皮带松紧不当", target: "跳闸", type: "导致" as const },
],
steel: [
{ source: "焦比升高", target: "高炉有效容积利用系数", type: "导致" as const },
{ source: "风温不足", target: "高炉有效容积利用系数", type: "导致" as const },
],
alumina: [
{ source: "槽电压", target: "4.2V", type: "表现" as const },
{ source: "分子比", target: "阳极效应系数", type: "对策" as const },
],
};
export function EntityExtraction() {
const [inputText, setInputText] = useState(sampleTexts.common[0]);
const [entities, setEntities] = useState<Entity[]>(initialEntities.common);
const [relations, setRelations] = useState<Relation[]>(initialRelations.common);
const [isTraining, setIsTraining] = useState(false);
const [isExtracting, setIsExtracting] = useState(false);
const [trainingProgress, setTrainingProgress] = useState(0);
const [metrics, setMetrics] = useState<TrainingMetrics>({ 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 [industry, setIndustry] = useState<IndustryType>("common");
const currentEntityTypes = industry === "common" ? commonEntityTypes :
industry === "steel" ? steelEntityTypes : aluminaEntityTypes;
useEffect(() => {
setEntities(initialEntities[industry]);
setRelations(initialRelations[industry]);
setInputText(sampleTexts[industry][0]);
setShowResult(false);
}, [industry]);
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[] = industry === "common" ? [
{ text: "给煤机", type: "fault" as EntityType, start: 0, end: 3 },
{ text: "跳闸", type: "symptom" as EntityType, start: 5, end: 7 },
{ text: "皮带松紧不当", type: "cause" as EntityType, start: 12, end: 18 },
{ text: "设备停机", type: "symptom" as EntityType, start: 23, end: 27 },
] : industry === "steel" ? [
{ text: "高炉有效容积利用系数", type: "blastFurnace" as EntityType, start: 0, end: 10 },
{ text: "3.8", type: "processParam" as EntityType, start: 14, end: 16 },
{ text: "焦比升高", type: "processParam" as EntityType, start: 22, end: 27 },
] : [
{ text: "槽电压", type: "electrolytic" as EntityType, start: 0, end: 3 },
{ text: "4.2V", type: "processParam" as EntityType, start: 6, end: 10 },
{ text: "阳极效应系数", type: "effectType" as EntityType, start: 12, end: 18 },
];
const newRelations: Relation[] = industry === "common" ? [
{ source: "给煤机", target: "跳闸", type: "表现" },
{ source: "皮带松紧不当", target: "跳闸", type: "导致" },
{ source: "给煤机", target: "设备停机", type: "表现" },
] : industry === "steel" ? [
{ source: "焦比升高", target: "高炉有效容积利用系数", type: "导致" },
] : [
{ source: "槽电压", target: "4.2V", type: "表现" },
];
setEntities(newEntities);
setRelations(newRelations);
setShowResult(true);
return "实体抽取完成";
},
error: "抽取失败",
}
);
};
const handleTrain = () => {
setIsTraining(true);
setTrainingProgress(0);
const interval = setInterval(() => {
setTrainingProgress((prev) => {
if (prev >= 100) {
clearInterval(interval);
setIsTraining(false);
setMetrics({ epoch: 100, totalEpochs: 100, loss: 0.018, f1: 0.94, precision: 0.95, recall: 0.93 });
toast.success("模型训练完成!");
return 100;
}
return prev + 2;
});
}, 100);
};
const renderHighlightedText = () => {
if (!inputText) return null;
const sortedEntities = [...entities].sort((a, b) => a.start - b.start);
const parts: React.ReactNode[] = [];
let lastIndex = 0;
sortedEntities.forEach((entity, idx) => {
if (entity.start > lastIndex) {
parts.push(
<span key={`text-${idx}`} className="text-slate-300">
{inputText.slice(lastIndex, entity.start)}
</span>
);
}
const entityTypeConfig = currentEntityTypes[entity.type as keyof typeof currentEntityTypes];
parts.push(
<span
key={`entity-${idx}`}
className={`px-1.5 py-0.5 rounded ${entityTypeConfig?.bgColor || "bg-slate-700"} ${entityTypeConfig?.borderColor || "border-slate-600"} border`}
title={entityTypeConfig?.label || entity.type}
>
<span className={entityTypeConfig?.textColor || "text-white"}>{entity.text}</span>
</span>
);
lastIndex = entity.end;
});
if (lastIndex < inputText.length) {
parts.push(
<span key="text-last" className="text-slate-300">
{inputText.slice(lastIndex)}
</span>
);
}
return parts;
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 p-6">
<div className="max-w-7xl mx-auto">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<div className="flex items-center gap-3 mb-2">
<div className="p-3 rounded-xl bg-gradient-to-br from-blue-500/20 to-purple-500/20 border border-blue-500/30">
<Brain className="w-8 h-8 text-blue-400" />
</div>
<div>
<h1 className="text-3xl font-bold text-white"></h1>
<p className="text-slate-400"></p>
</div>
</div>
</motion.div>
<div className="mb-6 flex gap-2">
{(["common", "steel", "alumina"] as IndustryType[]).map((ind) => (
<button
key={ind}
onClick={() => setIndustry(ind)}
className={`px-4 py-2 rounded-lg font-medium transition-all ${
industry === ind
? ind === "common" ? "bg-blue-500/20 text-blue-400 border border-blue-500/50" :
ind === "steel" ? "bg-orange-500/20 text-orange-400 border border-orange-500/50" :
"bg-teal-500/20 text-teal-400 border border-teal-500/50"
: "bg-slate-800/50 text-slate-400 border border-slate-700/50 hover:bg-slate-700/50"
}`}
>
{ind === "common" ? "通用" : ind === "steel" ? "钢铁行业" : "铝冶炼行业"}
</button>
))}
</div>
<div className="mb-6 flex gap-2">
{(["extract", "train"] as const).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-4 py-2 rounded-lg font-medium transition-all ${
activeTab === tab
? "bg-blue-500/20 text-blue-400 border border-blue-500/50"
: "bg-slate-800/50 text-slate-400 border border-slate-700/50 hover:bg-slate-700/50"
}`}
>
{tab === "extract" ? "实体抽取" : "模型训练"}
</button>
))}
</div>
{activeTab === "extract" ? (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className="rounded-xl bg-slate-900/50 border border-slate-800/50 backdrop-blur-sm p-6"
>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
<FileText className="w-5 h-5 text-blue-400" />
</h2>
<button
onClick={() => setInputText(sampleTexts[industry][Math.floor(Math.random() * sampleTexts[industry].length)])}
className="text-sm text-slate-400 hover:text-white flex items-center gap-1"
>
<RotateCw className="w-4 h-4" />
</button>
</div>
<textarea
value={inputText}
onChange={(e) => setInputText(e.target.value)}
className="w-full h-40 bg-slate-800/50 border border-slate-700/50 rounded-lg p-4 text-slate-200 resize-none focus:outline-none focus:border-blue-500/50"
placeholder="请输入需要分析的文本..."
/>
<div className="mt-4">
<h3 className="text-sm font-medium text-slate-400 mb-2"></h3>
<div className="flex flex-wrap gap-2">
{Object.entries(currentEntityTypes).map(([key, config]) => (
<div
key={key}
className={`px-3 py-1 rounded-full text-sm ${config.bgColor} border ${config.borderColor}`}
>
<span className={config.textColor}>{config.label}</span>
</div>
))}
</div>
</div>
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={handleExtract}
disabled={isExtracting}
className="mt-6 w-full py-3 rounded-lg bg-gradient-to-r from-blue-500 to-purple-500 text-white font-medium flex items-center justify-center gap-2 disabled:opacity-50"
>
{isExtracting ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
...
</>
) : (
<>
<Play className="w-5 h-5" />
</>
)}
</motion.button>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
className="rounded-xl bg-slate-900/50 border border-slate-800/50 backdrop-blur-sm p-6"
>
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<Target className="w-5 h-5 text-purple-400" />
</h2>
<AnimatePresence mode="wait">
{showResult ? (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
>
<div className="mb-6 p-4 bg-slate-800/50 rounded-lg border border-slate-700/50">
<h3 className="text-sm font-medium text-slate-400 mb-2"></h3>
<p className="text-slate-200 leading-relaxed">{renderHighlightedText()}</p>
</div>
<div className="mb-6">
<h3 className="text-sm font-medium text-slate-400 mb-3"></h3>
<div className="space-y-2">
{entities.map((entity, idx) => {
const config = currentEntityTypes[entity.type as keyof typeof currentEntityTypes];
return (
<div
key={idx}
className={`p-3 rounded-lg ${config?.bgColor || "bg-slate-800"} border ${config?.borderColor || "border-slate-700"} flex items-center gap-3`}
>
<span className={`font-semibold ${config?.textColor || "text-white"}`}>
{entity.text}
</span>
<ArrowRight className="w-4 h-4 text-slate-500" />
<span className={`text-sm ${config?.textColor || "text-white"}`}>
{config?.label || entity.type}
</span>
</div>
);
})}
</div>
</div>
<div>
<h3 className="text-sm font-medium text-slate-400 mb-3"></h3>
<div className="space-y-2">
{relations.map((rel, idx) => (
<div
key={idx}
className="p-3 rounded-lg bg-slate-800/50 border border-slate-700/50 flex items-center gap-3"
>
<span className="text-blue-400 font-medium">{rel.source}</span>
<span className="text-slate-500">-</span>
<span className="text-purple-400 text-sm px-2 py-0.5 bg-purple-500/20 rounded">
{rel.type}
</span>
<span className="text-slate-500">-</span>
<span className="text-green-400 font-medium">{rel.target}</span>
</div>
))}
</div>
</div>
</motion.div>
) : (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex flex-col items-center justify-center h-64 text-slate-500"
>
<Target className="w-12 h-12 mb-4" />
<p>"开始抽取"</p>
</motion.div>
)}
</AnimatePresence>
</motion.div>
</div>
) : (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="rounded-xl bg-slate-900/50 border border-slate-800/50 backdrop-blur-sm p-6"
>
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
<Brain className="w-5 h-5 text-purple-400" />
</h2>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 text-sm text-slate-400">
<Database className="w-4 h-4" />
<span>训练样本: 12,580</span>
</div>
<div className="flex items-center gap-2 text-sm text-slate-400">
<Network className="w-4 h-4" />
<span>模型: BERT-BiLSTM-CRF</span>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div className="p-4 rounded-lg bg-slate-800/50 border border-slate-700/50">
<div className="flex items-center justify-between mb-2">
<span className="text-slate-400"></span>
<span className="text-white font-medium">{trainingProgress}%</span>
</div>
<div className="h-2 bg-slate-700 rounded-full overflow-hidden">
<motion.div
className="h-full bg-gradient-to-r from-blue-500 to-purple-500"
initial={{ width: 0 }}
animate={{ width: `${trainingProgress}%` }}
/>
</div>
<div className="mt-2 text-sm text-slate-500">
Epoch {metrics.epoch} / {metrics.totalEpochs}
</div>
</div>
<div className="p-4 rounded-lg bg-slate-800/50 border border-slate-700/50">
<div className="flex items-center justify-between mb-2">
<span className="text-slate-400">Loss</span>
<span className="text-white font-medium">{metrics.loss.toFixed(3)}</span>
</div>
<div className="flex items-center gap-4 mt-4">
<div>
<div className="text-2xl font-bold text-green-400">{metrics.f1.toFixed(2)}</div>
<div className="text-sm text-slate-500">F1 Score</div>
</div>
<div>
<div className="text-2xl font-bold text-blue-400">{metrics.precision.toFixed(2)}</div>
<div className="text-sm text-slate-500">Precision</div>
</div>
<div>
<div className="text-2xl font-bold text-orange-400">{metrics.recall.toFixed(2)}</div>
<div className="text-sm text-slate-500">Recall</div>
</div>
</div>
</div>
</div>
<div className="p-4 rounded-lg bg-slate-800/50 border border-slate-700/50 mb-6">
<h3 className="text-slate-400 mb-4"></h3>
<div className="font-mono text-sm text-slate-500 space-y-1">
<div>[2026-04-08 10:23:15] Loading training data...</div>
<div>[2026-04-08 10:23:16] Loaded 12,580 samples</div>
<div>[2026-04-08 10:23:17] Initializing BERT-BiLSTM-CRF model...</div>
<div className="text-green-400">[2026-04-08 10:23:20] Model initialized successfully</div>
<div>[2026-04-08 10:23:20] Starting training loop...</div>
</div>
</div>
<div className="flex gap-4">
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={handleTrain}
disabled={isTraining}
className="flex-1 py-3 rounded-lg bg-gradient-to-r from-blue-500 to-purple-500 text-white font-medium flex items-center justify-center gap-2 disabled:opacity-50"
>
{isTraining ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
...
</>
) : (
<>
<Play className="w-5 h-5" />
</>
)}
</motion.button>
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="px-6 py-3 rounded-lg bg-slate-800/50 border border-slate-700/50 text-white font-medium flex items-center justify-center gap-2 hover:bg-slate-700/50"
>
<Settings className="w-5 h-5" />
</motion.button>
</div>
</motion.div>
)}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="mt-6 grid grid-cols-1 md:grid-cols-3 gap-4"
>
<div className="rounded-xl bg-slate-900/50 border border-slate-800/50 backdrop-blur-sm p-4">
<div className="flex items-center gap-3 mb-2">
<div className="p-2 rounded-lg bg-blue-500/20">
<Zap className="w-5 h-5 text-blue-400" />
</div>
<span className="text-slate-400"></span>
</div>
<div className="text-2xl font-bold text-white">94.2%</div>
<div className="text-sm text-green-400"> 2.3%</div>
</div>
<div className="rounded-xl bg-slate-900/50 border border-slate-800/50 backdrop-blur-sm p-4">
<div className="flex items-center gap-3 mb-2">
<div className="p-2 rounded-lg bg-purple-500/20">
<TrendingUp className="w-5 h-5 text-purple-400" />
</div>
<span className="text-slate-400"></span>
</div>
<div className="text-2xl font-bold text-white">12,580</div>
<div className="text-sm text-slate-500"></div>
</div>
<div className="rounded-xl bg-slate-900/50 border border-slate-800/50 backdrop-blur-sm p-4">
<div className="flex items-center gap-3 mb-2">
<div className="p-2 rounded-lg bg-green-500/20">
<CheckCircle2 className="w-5 h-5 text-green-400" />
</div>
<span className="text-slate-400"></span>
</div>
<div className="text-2xl font-bold text-white">{Object.keys(currentEntityTypes).length}</div>
<div className="text-sm text-slate-500"></div>
</div>
</motion.div>
</div>
</div>
);
}

View File

@@ -3,7 +3,7 @@ import { motion, AnimatePresence } from "motion/react";
import { GitMerge, CheckCircle, XCircle, AlertCircle, RefreshCw, Download, TrendingUp, Activity, Clock, Zap, Target } from "lucide-react";
import { toast } from "sonner";
const pendingEntities = [
const steelPendingEntities = [
{
id: 1,
entityA: "磨煤机跳闸",
@@ -58,14 +58,76 @@ const pendingEntities = [
},
];
const fusionHistory = [
const aluminaPendingEntities = [
{
id: 1,
entityA: "槽电压升高",
entityB: "电解槽电压异常",
cosineDistance: 0.85,
jaccardCoefficient: 0.72,
recommendation: "建议合并",
confidence: 88,
attributes: {
entityA: ["电解参数", "电压异常", "能耗增加"],
entityB: ["电解状态", "电压异常", "生产异常"],
},
},
{
id: 2,
entityA: "分子比偏高",
entityB: "电解质分子比升高",
cosineDistance: 0.91,
jaccardCoefficient: 0.85,
recommendation: "建议合并",
confidence: 94,
attributes: {
entityA: ["电解质成分", "分子比异常", "工艺参数"],
entityB: ["电解质状态", "分子比变化", "参数调整"],
},
},
{
id: 3,
entityA: "效应系数增加",
entityB: "阳极效应频发",
cosineDistance: 0.78,
jaccardCoefficient: 0.65,
recommendation: "不匹配",
confidence: 72,
attributes: {
entityA: ["效应指标", "频次异常", "工况波动"],
entityB: ["阳极问题", "效应发生", "质量影响"],
},
},
{
id: 4,
entityA: "铝液温度偏高",
entityB: "电解温度超限",
cosineDistance: 0.89,
jaccardCoefficient: 0.80,
recommendation: "建议合并",
confidence: 90,
attributes: {
entityA: ["温度参数", "温度异常", "热平衡"],
entityB: ["温度状态", "超限报警", "工况异常"],
},
},
];
const steelFusionHistory = [
{ time: "10:45", entityA: "省煤器堵灰", entityB: "省煤器堵塞", action: "已合并", operator: "系统" },
{ time: "10:30", entityA: "空预器漏风", entityB: "空气预热器漏风", action: "已合并", operator: "系统" },
{ time: "10:15", entityA: "炉膛负压高", entityB: "炉膛负压低", action: "已拒绝", operator: "人工" },
{ time: "10:00", entityA: "给水温度低", entityB: "给水温度偏低", action: "已合并", operator: "系统" },
];
const fusionStats = {
const aluminaFusionHistory = [
{ time: "14:30", entityA: "极距增大", entityB: "极距异常", action: "已合并", operator: "系统" },
{ time: "14:15", entityA: "电解质水平下降", entityB: "电解质偏少", action: "已合并", operator: "系统" },
{ time: "13:45", entityA: "阳极消耗不均", entityB: "阳极异常消耗", action: "已拒绝", operator: "人工" },
{ time: "13:30", entityA: "壳面破损", entityB: "炉膛壳面损坏", action: "已合并", operator: "系统" },
];
const steelFusionStats = {
pending: 156,
merged: 142,
rejected: 14,
@@ -73,11 +135,28 @@ const fusionStats = {
manualReview: 14,
};
const aluminaFusionStats = {
pending: 89,
merged: 76,
rejected: 13,
autoMerged: 65,
manualReview: 11,
};
export function KnowledgeFusion() {
const [selectedEntity, setSelectedEntity] = useState(pendingEntities[1]);
const [selectedEntity, setSelectedEntity] = useState(steelPendingEntities[1]);
const [processingId, setProcessingId] = useState<number | null>(null);
const [mergeAnimatingId, setMergeAnimatingId] = useState<number | null>(null);
const [statsPulse, setStatsPulse] = useState(false);
const [industry, setIndustry] = useState<"steel" | "alumina">("steel");
const currentPendingEntities = industry === "steel" ? steelPendingEntities : aluminaPendingEntities;
const currentFusionHistory = industry === "steel" ? steelFusionHistory : aluminaFusionHistory;
const currentFusionStats = industry === "steel" ? steelFusionStats : aluminaFusionStats;
useEffect(() => {
setSelectedEntity(currentPendingEntities[1]);
}, [industry, currentPendingEntities]);
// Pulse animation for stats when entity changes
useEffect(() => {
@@ -134,6 +213,26 @@ export function KnowledgeFusion() {
<p className="text-sm text-slate-400">Jaccard系数的智能实体融合</p>
</div>
<div className="flex gap-3">
<button
onClick={() => setIndustry("steel")}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
industry === "steel"
? "bg-orange-500/20 text-orange-400 border border-orange-500/50"
: "bg-slate-800/50 text-slate-400 border border-slate-700/50 hover:bg-slate-800"
}`}
>
</button>
<button
onClick={() => setIndustry("alumina")}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
industry === "alumina"
? "bg-cyan-500/20 text-cyan-400 border border-cyan-500/50"
: "bg-slate-800/50 text-slate-400 border border-slate-700/50 hover:bg-slate-800"
}`}
>
</button>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
@@ -176,11 +275,11 @@ export function KnowledgeFusion() {
{/* Fusion Statistics */}
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
{[
{ label: "待融合实体", value: fusionStats.pending, color: "from-blue-500 to-cyan-500", icon: AlertCircle },
{ label: "已融合", value: fusionStats.merged, color: "from-green-500 to-emerald-500", icon: CheckCircle },
{ label: "已拒绝", value: fusionStats.rejected, color: "from-red-500 to-pink-500", icon: XCircle },
{ label: "自动合并", value: fusionStats.autoMerged, color: "from-purple-500 to-indigo-500", icon: RefreshCw },
{ label: "待确认", value: fusionStats.manualReview, color: "from-yellow-500 to-orange-500", icon: AlertCircle },
{ label: "待融合实体", value: currentFusionStats.pending, color: "from-blue-500 to-cyan-500", icon: AlertCircle },
{ label: "已融合", value: currentFusionStats.merged, color: "from-green-500 to-emerald-500", icon: CheckCircle },
{ label: "已拒绝", value: currentFusionStats.rejected, color: "from-red-500 to-pink-500", icon: XCircle },
{ label: "自动合并", value: currentFusionStats.autoMerged, color: "from-purple-500 to-indigo-500", icon: RefreshCw },
{ label: "待确认", value: currentFusionStats.manualReview, color: "from-yellow-500 to-orange-500", icon: AlertCircle },
].map((stat, index) => (
<motion.div
key={stat.label}
@@ -230,7 +329,7 @@ export function KnowledgeFusion() {
{/* Pending Entities List */}
<div className="lg:col-span-2 space-y-4">
<h2 className="text-lg font-semibold text-white"></h2>
{pendingEntities.map((entity, index) => (
{currentPendingEntities.map((entity: typeof steelPendingEntities[0], index: number) => (
<motion.div
key={entity.id}
initial={{ opacity: 0, x: -20 }}
@@ -537,7 +636,7 @@ export function KnowledgeFusion() {
</motion.div>
</div>
<div className="space-y-3">
{fusionHistory.map((item, index) => (
{currentFusionHistory.map((item: typeof steelFusionHistory[0], index: number) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 10, x: -10 }}

View File

@@ -2,13 +2,13 @@ import { useState, useRef, useEffect, useCallback } from "react";
import { Search, Network, ZoomIn, ZoomOut, Maximize2, Plus, Download, Trash2, Edit, Eye } from "lucide-react";
import { motion } from "motion/react";
import { toast } from "sonner";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../components/ui/dialog";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../../components/ui/dialog";
import * as d3 from "d3";
interface GraphNode extends d3.SimulationNodeDatum {
id: number;
label: string;
type: "fault" | "symptom" | "cause" | "solution" | "prevention";
type: string;
description: string;
connections: number;
}
@@ -19,7 +19,7 @@ interface GraphEdge extends d3.SimulationLinkDatum<GraphNode> {
label: string;
}
const nodeTypeConfig = {
const steelNodeTypeConfig = {
fault: { label: "故障", color: "#ef4444", gradient: "from-red-500 to-orange-500" },
symptom: { label: "症状", color: "#f59e0b", gradient: "from-yellow-500 to-amber-500" },
cause: { label: "原因", color: "#3b82f6", gradient: "from-blue-500 to-cyan-500" },
@@ -27,6 +27,14 @@ const nodeTypeConfig = {
prevention: { label: "预防措施", color: "#a855f7", gradient: "from-purple-500 to-pink-500" },
};
const aluminaNodeTypeConfig = {
equipment: { label: "设备", color: "#0ea5e9", gradient: "from-sky-500 to-blue-500" },
material: { label: "原料", color: "#22c55e", gradient: "from-green-500 to-emerald-500" },
parameter: { label: "工艺参数", color: "#f59e0b", gradient: "from-amber-500 to-orange-500" },
quality: { label: "质量指标", color: "#a855f7", gradient: "from-purple-500 to-pink-500" },
process: { label: "工序", color: "#ec4899", gradient: "from-pink-500 to-rose-500" },
};
const initialNodes: GraphNode[] = [
{ id: 1, label: "给煤机故障", type: "fault", description: "给煤机无法正常启动或运行中断", connections: 4 },
{ id: 2, label: "跳闸", type: "symptom", description: "设备意外停机保护动作", connections: 3 },
@@ -55,13 +63,49 @@ const initialEdges: GraphEdge[] = [
{ source: 12, target: 7, label: "导致" },
];
const statsData = [
const aluminaNodes: GraphNode[] = [
{ id: 1, label: "溶出釜", type: "equipment", description: "高压溶出铝土矿的核心设备", connections: 5 },
{ id: 2, label: "铝土矿", type: "material", description: "主要原料铝硅比A/S=8.5", connections: 4 },
{ id: 3, label: "配碱量", type: "parameter", description: "碱液与矿石比例210kg/t", connections: 3 },
{ id: 4, label: "溶出率", type: "quality", description: "目标溶出率≥95%", connections: 4 },
{ id: 5, label: "溶出工序", type: "process", description: "高压碱液溶出铝土矿", connections: 3 },
{ id: 6, label: "沉降槽", type: "equipment", description: "分离赤泥和铝酸钠溶液", connections: 4 },
{ id: 7, label: "种子分解", type: "process", description: "添加晶种分解氢氧化铝", connections: 3 },
{ id: 8, label: "蒸发器", type: "equipment", description: "浓缩母液的设备", connections: 3 },
{ id: 9, label: "煅烧炉", type: "equipment", description: "焙烧氢氧化铝成氧化铝", connections: 4 },
{ id: 10, label: "电解槽", type: "equipment", description: "电解生产金属铝的核心设备", connections: 5 },
{ id: 11, label: "槽电压", type: "parameter", description: "正常范围3.8-4.2V", connections: 4 },
{ id: 12, label: "分子比", type: "parameter", description: "电解质分子比2.2-2.4", connections: 3 },
];
const aluminaEdges: GraphEdge[] = [
{ source: 1, target: 5, label: "属于" },
{ source: 2, target: 1, label: "输入" },
{ source: 3, target: 1, label: "作用于" },
{ source: 1, target: 4, label: "产出" },
{ source: 4, target: 6, label: "输送至" },
{ source: 6, target: 7, label: "进入" },
{ source: 7, target: 8, label: "产出" },
{ source: 8, target: 9, label: "输送至" },
{ source: 9, target: 10, label: "原料" },
{ source: 10, target: 11, label: "监测" },
{ source: 10, target: 12, label: "影响" },
];
const statsDataSteel = [
{ label: "实体总数", value: "1,067" },
{ label: "关系总数", value: "1,249" },
{ label: "故障类型", value: "45种" },
{ label: "图谱类型", value: "钢铁" },
];
const statsDataAlumina = [
{ label: "实体总数", value: "2,458" },
{ label: "关系总数", value: "3,102" },
{ label: "工序覆盖", value: "6大工序" },
{ label: "图谱类型", value: "铝冶炼" },
];
const queryHistoryData = [
{ query: "给煤机皮带跑偏", result: "已找到5个相关节点", timestamp: "10:30" },
{ query: "过热器故障", result: "已找到3个相关节点", timestamp: "10:25" },
@@ -76,12 +120,18 @@ export function KnowledgeGraph() {
const [isSearching, setIsSearching] = useState(false);
const [hoveredNode, setHoveredNode] = useState<number | null>(null);
const [highlightedNodes, setHighlightedNodes] = useState<Set<number>>(new Set());
const [typeFilter, setTypeFilter] = useState<Set<string>>(new Set(Object.keys(nodeTypeConfig)));
const [typeFilter, setTypeFilter] = useState<Set<string>>(new Set(Object.keys(steelNodeTypeConfig)));
const [simulation, setSimulation] = useState<d3.Simulation<any, any> | null>(null);
const [industry, setIndustry] = useState<"steel" | "alumina">("steel");
const svgRef = useRef<SVGSVGElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const currentStatsData = industry === "steel" ? statsDataSteel : statsDataAlumina;
const currentNodes = industry === "steel" ? initialNodes : aluminaNodes;
const currentEdges = industry === "steel" ? initialEdges : aluminaEdges;
const currentNodeTypeConfig = industry === "steel" ? steelNodeTypeConfig : aluminaNodeTypeConfig;
const initGraph = useCallback(() => {
if (!svgRef.current || !containerRef.current) return;
@@ -104,8 +154,9 @@ export function KnowledgeGraph() {
svg.call(zoom);
const defs = svg.append("defs");
const configToUse = industry === "steel" ? steelNodeTypeConfig : aluminaNodeTypeConfig;
Object.entries(nodeTypeConfig).forEach(([type, config]) => {
Object.entries(configToUse).forEach(([type, config]) => {
const gradient = defs.append("radialGradient")
.attr("id", `gradient-${type}`)
.attr("cx", "30%")
@@ -122,9 +173,11 @@ export function KnowledgeGraph() {
.attr("stop-opacity", 0.4);
});
const filteredNodes = initialNodes.filter(n => typeFilter.has(n.type));
const nodesToUse = industry === "steel" ? initialNodes : aluminaNodes;
const edgesToUse = industry === "steel" ? initialEdges : aluminaEdges;
const filteredNodes = nodesToUse.filter(n => typeFilter.has(n.type));
const filteredNodeIds = new Set(filteredNodes.map(n => n.id));
const filteredEdges = initialEdges.filter(e => {
const filteredEdges = edgesToUse.filter(e => {
const sourceId = typeof e.source === 'number' ? e.source : (e.source as GraphNode).id;
const targetId = typeof e.target === 'number' ? e.target : (e.target as GraphNode).id;
return filteredNodeIds.has(sourceId) && filteredNodeIds.has(targetId);
@@ -189,7 +242,7 @@ export function KnowledgeGraph() {
nodeGroup.append("circle")
.attr("r", 40)
.attr("fill", (d: any) => `url(#gradient-${d.type})`)
.attr("stroke", (d: any) => nodeTypeConfig[d.type as keyof typeof nodeTypeConfig].color)
.attr("stroke", (d: any) => configToUse[d.type as keyof typeof configToUse]?.color || "#fff")
.attr("stroke-width", 3)
.attr("stroke-opacity", 0.8);
@@ -253,7 +306,7 @@ export function KnowledgeGraph() {
.transition()
.duration(300)
.attr("stroke-width", 4)
.attr("stroke", nodeTypeConfig[n.type as keyof typeof nodeTypeConfig].color);
.attr("stroke", configToUse[n.type as keyof typeof configToUse]?.color || "#fff");
} else {
nodeG.select("circle")
.transition()
@@ -274,7 +327,7 @@ export function KnowledgeGraph() {
.transition()
.duration(300)
.attr("stroke-width", 3)
.attr("stroke", (d: any) => nodeTypeConfig[d.type as keyof typeof nodeTypeConfig].color)
.attr("stroke", (d: any) => configToUse[d.type as keyof typeof configToUse]?.color || "#fff")
.attr("opacity", 1);
});
@@ -294,11 +347,11 @@ export function KnowledgeGraph() {
svg.call(zoom.transform, d3.zoomIdentity.translate(0, 0).scale(1));
}, [typeFilter]);
}, [typeFilter, industry]);
useEffect(() => {
initGraph();
}, []);
}, [industry, initGraph]);
useEffect(() => {
if (!simulation) return;
@@ -427,7 +480,34 @@ export function KnowledgeGraph() {
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white mb-1"></h1>
<p className="text-sm text-slate-400"></p>
<div className="flex items-center gap-3 mt-2">
<button
onClick={() => {
setIndustry("steel");
setTypeFilter(new Set(Object.keys(steelNodeTypeConfig)));
}}
className={`px-3 py-1 rounded-lg text-sm font-medium transition-all ${
industry === "steel"
? "bg-orange-500/20 text-orange-400 border border-orange-500/50"
: "bg-slate-800/50 text-slate-400 border border-slate-700/50 hover:bg-slate-800"
}`}
>
</button>
<button
onClick={() => {
setIndustry("alumina");
setTypeFilter(new Set(Object.keys(aluminaNodeTypeConfig)));
}}
className={`px-3 py-1 rounded-lg text-sm font-medium transition-all ${
industry === "alumina"
? "bg-cyan-500/20 text-cyan-400 border border-cyan-500/50"
: "bg-slate-800/50 text-slate-400 border border-slate-700/50 hover:bg-slate-800"
}`}
>
</button>
</div>
</div>
<div className="flex items-center gap-2">
<motion.button
@@ -452,7 +532,7 @@ export function KnowledgeGraph() {
>
<h3 className="font-semibold text-white mb-4"></h3>
<div className="space-y-3">
{statsData.map((stat) => (
{currentStatsData.map((stat) => (
<div key={stat.label} className="flex justify-between items-center">
<span className="text-sm text-slate-400">{stat.label}</span>
<span className="text-lg font-bold text-white">{stat.value}</span>
@@ -499,7 +579,7 @@ export function KnowledgeGraph() {
>
<h3 className="font-semibold text-white mb-4"></h3>
<div className="space-y-2">
{Object.entries(nodeTypeConfig).map(([type, config]) => (
{Object.entries(currentNodeTypeConfig).map(([type, config]) => (
<motion.button
key={type}
whileHover={{ scale: 1.02 }}
@@ -668,7 +748,7 @@ export function KnowledgeGraph() {
</div>
<div className="px-6 py-4 border-t border-slate-800/50 bg-slate-900/30 flex items-center gap-6 flex-wrap">
{Object.entries(nodeTypeConfig).map(([type, config]) => (
{Object.entries(currentNodeTypeConfig).map(([type, config]) => (
<div
key={type}
className={`flex items-center gap-2 transition-opacity ${
@@ -683,10 +763,10 @@ export function KnowledgeGraph() {
</div>
))}
<div className="ml-auto text-xs text-slate-500">
{initialNodes.filter((n) => typeFilter.has(n.type)).length} |{" "}
{initialEdges.filter(
(e) => typeFilter.has(initialNodes.find((n) => n.id === e.source)?.type || "") &&
typeFilter.has(initialNodes.find((n) => n.id === e.target)?.type || "")
{currentNodes.filter((n) => typeFilter.has(n.type)).length} |{" "}
{currentEdges.filter(
(e) => typeFilter.has(currentNodes.find((n) => n.id === e.source)?.type || "") &&
typeFilter.has(currentNodes.find((n) => n.id === e.target)?.type || "")
).length}
</div>
</div>
@@ -709,10 +789,10 @@ export function KnowledgeGraph() {
<div className="flex items-center gap-3">
<div
className={`inline-flex px-3 py-1 rounded-full bg-gradient-to-br ${
nodeTypeConfig[selectedNode.type].gradient
currentNodeTypeConfig[selectedNode.type]?.gradient || "from-gray-500 to-gray-600"
} text-white text-sm`}
>
{nodeTypeConfig[selectedNode.type].label}
{currentNodeTypeConfig[selectedNode.type]?.label || selectedNode.type}
</div>
<div className="text-sm text-slate-400">
: {getConnectedNodes(selectedNode.id).length}

View File

@@ -4,7 +4,7 @@ import { LineChart, Line, BarChart, Bar, AreaChart, Area, XAxis, YAxis, Cartesia
import { useState, useEffect, useRef } from "react";
import { toast } from "sonner";
const processStages = [
const steelProcessStages = [
{ name: "炼铁", status: "normal", load: 85, temp: 1250 },
{ name: "炼钢", status: "normal", load: 92, temp: 1650 },
{ name: "连铸", status: "normal", load: 88, temp: 1520 },
@@ -12,7 +12,15 @@ const processStages = [
{ name: "冷轧", status: "normal", load: 78, temp: 850 },
];
const temperatureTrend = [
const aluminaProcessStages = [
{ name: "铝土矿破碎", status: "normal", load: 82, temp: 25 },
{ name: "拜耳法溶出", status: "normal", load: 88, temp: 260 },
{ name: "沉降分离", status: "normal", load: 85, temp: 100 },
{ name: "晶种分解", status: "warning", load: 78, temp: 80 },
{ name: "焙烧", status: "normal", load: 90, temp: 1100 },
];
const steelTemperatureTrend = [
{ time: "08:00", t1: 1245, t2: 1648, t3: 1518 },
{ time: "10:00", t1: 1248, t2: 1650, t3: 1520 },
{ time: "12:00", t1: 1250, t2: 1652, t3: 1522 },
@@ -20,7 +28,15 @@ const temperatureTrend = [
{ time: "16:00", t1: 1250, t2: 1650, t3: 1520 },
];
const productionData = [
const aluminaTemperatureTrend = [
{ time: "08:00", t1: 24, t2: 258, t3: 98 },
{ time: "10:00", t1: 25, t2: 260, t3: 100 },
{ time: "12:00", t1: 26, t2: 262, t3: 102 },
{ time: "14:00", t1: 25, t2: 259, t3: 99 },
{ time: "16:00", t1: 25, t2: 260, t3: 100 },
];
const steelProductionData = [
{ hour: "00", value: 820 },
{ hour: "04", value: 780 },
{ hour: "08", value: 950 },
@@ -29,14 +45,30 @@ const productionData = [
{ hour: "20", value: 890 },
];
const alerts = [
const aluminaProductionData = [
{ hour: "00", value: 420 },
{ hour: "04", value: 380 },
{ hour: "08", value: 480 },
{ hour: "12", value: 520 },
{ hour: "16", value: 490 },
{ hour: "20", value: 450 },
];
const steelAlerts = [
{ id: 1, type: "warning", message: "高炉1#冷却水温差偏高", time: "10:35", handled: false },
{ id: 2, type: "info", message: "连铸2#定期维护提醒", time: "10:20", handled: false },
{ id: 3, type: "success", message: "炼钢3#工艺优化已完成", time: "10:15", handled: true },
{ id: 4, type: "warning", message: "热轧设备温度波动", time: "10:05", handled: true },
];
const equipmentStatus = [
const aluminaAlerts = [
{ id: 1, type: "warning", message: "分解槽搅拌强度偏低", time: "10:35", handled: false },
{ id: 2, type: "info", message: "蒸发器定期维护提醒", time: "10:20", handled: false },
{ id: 3, type: "success", message: "铝酸钠溶液浓度优化完成", time: "10:15", handled: true },
{ id: 4, type: "warning", message: "晶种分解效率波动", time: "10:05", handled: true },
];
const steelEquipmentStatus = [
{ name: "高炉 #1", status: "running", efficiency: 94, uptime: "99.2%" },
{ name: "高炉 #2", status: "running", efficiency: 92, uptime: "98.8%" },
{ name: "转炉 #1", status: "running", efficiency: 96, uptime: "99.5%" },
@@ -45,11 +77,34 @@ const equipmentStatus = [
{ name: "连铸 #2", status: "running", efficiency: 93, uptime: "99.1%" },
];
const aluminaEquipmentStatus = [
{ name: "磨机 #1", status: "running", efficiency: 94, uptime: "99.2%" },
{ name: "磨机 #2", status: "running", efficiency: 92, uptime: "98.8%" },
{ name: "溶出器 #1", status: "running", efficiency: 96, uptime: "99.5%" },
{ name: "溶出器 #2", status: "running", efficiency: 91, uptime: "98.3%" },
{ name: "沉降槽 #1", status: "running", efficiency: 93, uptime: "99.1%" },
{ name: "分解槽 #1", status: "maintenance", efficiency: 0, uptime: "—" },
];
export function MonitoringCenter() {
const [currentTime, setCurrentTime] = useState(new Date());
const [alertsList, setAlertsList] = useState(alerts);
const [processData, setProcessData] = useState(processStages);
const [tempTrendData, setTempTrendData] = useState(temperatureTrend);
const [alertsList, setAlertsList] = useState(steelAlerts);
const [processData, setProcessData] = useState(steelProcessStages);
const [tempTrendData, setTempTrendData] = useState(steelTemperatureTrend);
const [industry, setIndustry] = useState<"steel" | "alumina">("steel");
const currentProductionData = industry === "steel" ? steelProductionData : aluminaProductionData;
const currentEquipmentStatus = industry === "steel" ? steelEquipmentStatus : aluminaEquipmentStatus;
const currentProcessStages = industry === "steel" ? steelProcessStages : aluminaProcessStages;
const currentTempTrend = industry === "steel" ? steelTemperatureTrend : aluminaTemperatureTrend;
const currentAlerts = industry === "steel" ? steelAlerts : aluminaAlerts;
// Reset data when industry changes
useEffect(() => {
setProcessData(currentProcessStages);
setTempTrendData(currentTempTrend);
setAlertsList(currentAlerts);
}, [industry, currentProcessStages, currentTempTrend, currentAlerts]);
// Update current time
useEffect(() => {
@@ -119,6 +174,28 @@ export function MonitoringCenter() {
<h1 className="text-2xl font-bold text-white mb-1"></h1>
<p className="text-sm text-slate-400"></p>
</div>
<div className="flex gap-3">
<button
onClick={() => setIndustry("steel")}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
industry === "steel"
? "bg-orange-500/20 text-orange-400 border border-orange-500/50"
: "bg-slate-800/50 text-slate-400 border border-slate-700/50 hover:bg-slate-800"
}`}
>
</button>
<button
onClick={() => setIndustry("alumina")}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
industry === "alumina"
? "bg-cyan-500/20 text-cyan-400 border border-cyan-500/50"
: "bg-slate-800/50 text-slate-400 border border-slate-700/50 hover:bg-slate-800"
}`}
>
</button>
</div>
<div className="flex items-center gap-3">
<motion.button
whileHover={{ scale: 1.05 }}
@@ -387,7 +464,7 @@ export function MonitoringCenter() {
<span className="text-sm text-slate-400">单位: /</span>
</div>
<ResponsiveContainer width="100%" height={200}>
<BarChart data={productionData}>
<BarChart data={currentProductionData}>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" opacity={0.3} />
<XAxis dataKey="hour" stroke="#94a3b8" fontSize={12} />
<YAxis stroke="#94a3b8" fontSize={12} />
@@ -516,7 +593,7 @@ export function MonitoringCenter() {
<h2 className="text-lg font-semibold text-white mb-4"></h2>
<div className="space-y-3">
{equipmentStatus.map((equipment, index) => (
{currentEquipmentStatus.map((equipment: typeof steelEquipmentStatus[0], index: number) => (
<div
key={index}
className="p-3 rounded-lg bg-slate-800/50 border border-slate-700/50"

View File

@@ -3,7 +3,7 @@ import { motion, AnimatePresence } from "motion/react";
import { RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar, ResponsiveContainer } from "recharts";
import { useState } from "react";
import { toast } from "sonner";
import { Progress } from "../components/ui/progress";
import { Progress } from "../../components/ui/progress";
const currentParams = [
{ label: "铝硅比 A/S", value: "8.5", unit: "" },

View File

@@ -3,11 +3,12 @@ import { motion, AnimatePresence } from "motion/react";
import { Sparkles, Save, Play, Download, Copy, Plus, Code2, FileText, Hash, Zap, RefreshCw, Check } from "lucide-react";
import { toast } from "sonner";
const templates = [
const steelTemplates = [
{
id: 1,
name: "高炉故障诊断标准Prompt",
category: "故障诊断",
industry: "steel",
description: "用于高炉故障诊断的标准提示词模板",
content: `你是一位钢铁行业的资深工程师,擅长高炉故障诊断。
@@ -29,9 +30,61 @@ const templates = [
},
{
id: 2,
name: "工艺优化建议Prompt",
name: "转炉炼钢优化Prompt",
category: "工艺优化",
description: "氧化铝工艺参数优化建议生成模板",
industry: "steel",
description: "转炉炼钢工艺参数优化建议",
content: `你是钢铁行业的转炉炼钢专家,精通转炉控制优化。
****
- {hot_metal_temp}
- {carbon_content}
- {oxygen_content}
- {refining_time}
****
{optimization_target}
1.
2.
3.
4. `,
variables: ["hot_metal_temp", "carbon_content", "oxygen_content", "refining_time", "optimization_target"],
usage: 756,
},
{
id: 3,
name: "钢铁质量溯源Prompt",
category: "质量分析",
industry: "steel",
description: "钢铁产品质量异常溯源分析",
content: `你是质量管理专家,负责钢铁产品质量溯源分析。
****
- {batch_number}
- {quality_issue}
- {discovery_stage}
****
1.
2.
3.
4.
`,
variables: ["batch_number", "quality_issue", "discovery_stage"],
usage: 567,
},
];
const aluminaTemplates = [
{
id: 4,
name: "氧化铝工艺优化Prompt",
category: "工艺优化",
industry: "alumina",
description: "氧化铝生产参数优化建议生成模板",
content: `你是有色冶金领域的工艺专家,专注于氧化铝生产优化。
****
@@ -52,26 +105,49 @@ const templates = [
usage: 892,
},
{
id: 3,
name: "质量分析标准Prompt",
category: "质量分析",
description: "产品质量异常分析模板",
content: `你是质量管理专家,负责钢铁产品质量溯源分析。
id: 5,
name: "电解槽控制Prompt",
category: "故障诊断",
industry: "alumina",
description: "电解槽运行状态监控与故障诊断",
content: `你是电解铝工艺专家,精通电解槽运行控制。
****
- {batch_number}
- {quality_issue}
- {discovery_stage}
****
- {cell_voltage}
- {series_current}
- {electrolyte_temp}
- {molecular_ratio}
****
1.
2.
3.
4.
****
1.
2.
3.
4. `,
variables: ["cell_voltage", "series_current", "electrolyte_temp", "molecular_ratio"],
usage: 1089,
},
{
id: 6,
name: "铝冶炼人机协同Prompt",
category: "人机协同",
industry: "alumina",
description: "电解铝AR辅助人机协同交互",
content: `你是电解铝生产的人机协同助手提供AR增强现实支持。
`,
variables: ["batch_number", "quality_issue", "discovery_stage"],
usage: 567,
****
- {equipment_status}
- {process_parameters}
- {operation_suggestion}
****
1.
2.
3.
4.
AR交互界面和智能辅助建议`,
variables: ["equipment_status", "process_parameters", "operation_suggestion"],
usage: 654,
},
];
@@ -127,7 +203,7 @@ function TemplatePreview({ text }: { text: string }) {
}
export function PromptEngineering() {
const [selectedTemplate, setSelectedTemplate] = useState(templates[0]);
const [selectedTemplate, setSelectedTemplate] = useState(steelTemplates[0]);
const [editedContent, setEditedContent] = useState(selectedTemplate.content);
const [selectedCategory, setSelectedCategory] = useState("全部");
const [isEditing, setIsEditing] = useState(false);
@@ -136,10 +212,13 @@ export function PromptEngineering() {
const [isTestRunning, setIsTestRunning] = useState(false);
const [testResult, setTestResult] = useState<string | null>(null);
const [testProgress, setTestProgress] = useState(0);
const [industry, setIndustry] = useState<"steel" | "alumina">("steel");
const currentTemplates = industry === "steel" ? steelTemplates : aluminaTemplates;
const filteredTemplates = selectedCategory === "全部"
? templates
: templates.filter(t => t.category === selectedCategory);
? currentTemplates
: currentTemplates.filter((t: typeof steelTemplates[0]) => t.category === selectedCategory);
// Typing effect for template preview
useEffect(() => {
@@ -159,6 +238,15 @@ export function PromptEngineering() {
}
}, [selectedTemplate.id, isEditing]);
// Update selected template when industry changes
useEffect(() => {
const newTemplates = industry === "steel" ? steelTemplates : aluminaTemplates;
const firstTemplate = newTemplates[0];
setSelectedTemplate(firstTemplate);
setEditedContent(firstTemplate.content);
setIsEditing(false);
}, [industry]);
// Simulate test running process
useEffect(() => {
if (isTestRunning) {
@@ -220,6 +308,30 @@ export function PromptEngineering() {
</motion.button>
</div>
{/* Industry Tabs */}
<div className="flex gap-3">
<button
onClick={() => setIndustry("steel")}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
industry === "steel"
? "bg-orange-500/20 text-orange-400 border border-orange-500/50"
: "bg-slate-800/50 text-slate-400 border border-slate-700/50 hover:bg-slate-800"
}`}
>
</button>
<button
onClick={() => setIndustry("alumina")}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
industry === "alumina"
? "bg-cyan-500/20 text-cyan-400 border border-cyan-500/50"
: "bg-slate-800/50 text-slate-400 border border-slate-700/50 hover:bg-slate-800"
}`}
>
</button>
</div>
{/* Category Filter */}
<motion.div
initial={{ opacity: 0, y: 20 }}

View File

@@ -3,7 +3,7 @@ import { Search, CheckCircle, AlertTriangle, ArrowRight, FileText, BarChart3, Tr
import { motion } from "motion/react";
import { toast } from "sonner";
const processSteps = [
const steelProcessSteps = [
{ name: "炼铁", status: "normal", icon: "🔥", temp: 1450, efficiency: 98 },
{ name: "炼钢", status: "normal", icon: "⚙️", temp: 1650, efficiency: 97 },
{ name: "连铸", status: "warning", icon: "💧", temp: 1520, efficiency: 85 },
@@ -11,6 +11,56 @@ const processSteps = [
{ name: "冷轧", status: "normal", icon: "❄️", temp: 20, efficiency: 99 },
];
const aluminaProcessSteps = [
{ name: "铝土矿破碎", status: "normal", icon: "🪨", temp: 25, efficiency: 96 },
{ name: "拜耳法溶出", status: "normal", icon: "⚗️", temp: 260, efficiency: 94 },
{ name: "沉降分离", status: "normal", icon: "🫗", temp: 100, efficiency: 92 },
{ name: "晶种分解", status: "warning", icon: "💎", temp: 80, efficiency: 88 },
{ name: "焙烧", status: "normal", icon: "🔥", temp: 1100, efficiency: 95 },
];
const steelTracingResults = [
{
type: "warning" as const,
title: "⚠️ 发现质量异常 - 抗拉强度偏低",
batchNumber: "PL20260403001",
time: "2026-04-03 14:30",
indicator: "抗拉强度 458 MPa (标准值: ≥480 MPa)",
deviation: "偏低 4.6%",
reason: "连铸段冷却速度过快",
},
{
type: "error" as const,
title: "🔴 发现质量异常 - 延伸率不合格",
batchNumber: "PL20260403001",
time: "2026-04-03 14:30",
indicator: "延伸率 18.2% (标准值: ≥22%)",
deviation: "偏低 17.3%",
reason: "热轧温度控制不当",
},
];
const aluminaTracingResults = [
{
type: "warning" as const,
title: "⚠️ 发现质量异常 - 氧化铝纯度偏低",
batchNumber: "AL20260403001",
time: "2026-04-03 14:30",
indicator: "氧化铝纯度 97.2% (标准值: ≥98%)",
deviation: "偏低 0.8%",
reason: "拜耳法溶出条件控制不当",
},
{
type: "error" as const,
title: "🔴 发现质量异常 - 粒度不合格",
batchNumber: "AL20260403001",
time: "2026-04-03 14:30",
indicator: "粒度 -45μm 占比 18.2% (标准值: ≥85%)",
deviation: "偏低 66.8%",
reason: "晶种分解工艺参数异常",
},
];
export function QualityTracing() {
const [batchNumber, setBatchNumber] = useState("PL20260403001");
const [showResult, setShowResult] = useState(true);
@@ -18,8 +68,12 @@ export function QualityTracing() {
const [activeStep, setActiveStep] = useState<number | null>(null);
const [flowProgress, setFlowProgress] = useState(0);
const [particles, setParticles] = useState<Array<{id: number, x: number, y: number, delay: number}>>([]);
const [industry, setIndustry] = useState<"steel" | "alumina">("steel");
const svgRef = useRef<SVGSVGElement>(null);
const currentProcessSteps = industry === "steel" ? steelProcessSteps : aluminaProcessSteps;
const currentTracingResults = industry === "steel" ? steelTracingResults : aluminaTracingResults;
useEffect(() => {
if (!showResult) return;
@@ -68,6 +122,28 @@ export function QualityTracing() {
<h1 className="text-2xl font-bold text-white mb-1"></h1>
<p className="text-sm text-slate-400"></p>
</div>
<div className="flex gap-3">
<button
onClick={() => setIndustry("steel")}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
industry === "steel"
? "bg-orange-500/20 text-orange-400 border border-orange-500/50"
: "bg-slate-800/50 text-slate-400 border border-slate-700/50 hover:bg-slate-800"
}`}
>
</button>
<button
onClick={() => setIndustry("alumina")}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
industry === "alumina"
? "bg-cyan-500/20 text-cyan-400 border border-cyan-500/50"
: "bg-slate-800/50 text-slate-400 border border-slate-700/50 hover:bg-slate-800"
}`}
>
</button>
</div>
</div>
{/* Search Section */}
@@ -124,73 +200,59 @@ export function QualityTracing() {
<h2 className="font-semibold text-white mb-4"></h2>
<div className="space-y-4">
{/* First Warning */}
<div className="p-6 rounded-xl bg-gradient-to-br from-yellow-900/20 to-orange-900/20 border border-yellow-500/30">
{currentTracingResults.map((result, index) => (
<div
key={index}
className={`p-6 rounded-xl border ${
result.type === "warning"
? "bg-gradient-to-br from-yellow-900/20 to-orange-900/20 border-yellow-500/30"
: "bg-gradient-to-br from-red-900/20 to-orange-900/20 border-red-500/30"
}`}
>
<div className="flex items-start gap-4">
<div className="p-3 rounded-full bg-yellow-500/20">
<div className={`p-3 rounded-full ${
result.type === "warning" ? "bg-yellow-500/20" : "bg-red-500/20"
}`}>
{result.type === "warning" ? (
<AlertTriangle className="w-6 h-6 text-yellow-400" />
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-yellow-400 mb-2">
-
</h3>
<div className="space-y-2 text-sm text-slate-300">
<div className="flex items-center gap-2">
<span className="text-slate-400">:</span>
<span className="font-medium text-white">{batchNumber}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-slate-400">:</span>
<span>2026-04-03 14:30</span>
</div>
<div className="flex items-center gap-2">
<span className="text-slate-400">:</span>
<span className="text-yellow-400"> 458 MPa (: 480 MPa)</span>
</div>
<div className="flex items-center gap-2">
<span className="text-slate-400">:</span>
<span className="text-red-400"> 4.6%</span>
</div>
</div>
</div>
</div>
</div>
{/* Second Warning */}
<div className="p-6 rounded-xl bg-gradient-to-br from-red-900/20 to-orange-900/20 border border-red-500/30">
<div className="flex items-start gap-4">
<div className="p-3 rounded-full bg-red-500/20">
) : (
<TrendingDown className="w-6 h-6 text-red-400" />
)}
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-red-400 mb-2">
🔴 -
<h3 className={`text-lg font-semibold mb-2 ${
result.type === "warning" ? "text-yellow-400" : "text-red-400"
}`}>
{result.title}
</h3>
<div className="space-y-2 text-sm text-slate-300">
<div className="flex items-center gap-2">
<span className="text-slate-400">:</span>
<span className="font-medium text-white">{batchNumber}</span>
<span className="font-medium text-white">{result.batchNumber}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-slate-400">:</span>
<span>2026-04-03 14:30</span>
<span>{result.time}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-slate-400">:</span>
<span className="text-red-400"> 18.2% (: 22%)</span>
<span className={result.type === "warning" ? "text-yellow-400" : "text-red-400"}>
{result.indicator}
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-slate-400">:</span>
<span className="text-red-400"> 17.3%</span>
<span className="text-red-400">{result.deviation}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-slate-400">:</span>
<span className="text-orange-400"></span>
<span className="text-orange-400">{result.reason}</span>
</div>
</div>
</div>
</div>
</div>
))}
</div>
</motion.div>
@@ -269,7 +331,7 @@ export function QualityTracing() {
{/* Process Steps */}
<div className="relative flex justify-between items-start">
{processSteps.map((step, index) => {
{currentProcessSteps.map((step: typeof steelProcessSteps[0], index: number) => {
const colors = getStepColor(step.status);
const isActive = activeStep === index;
const isAnimated = animatedPath.includes(index);
@@ -389,11 +451,11 @@ export function QualityTracing() {
</div>
{/* Connection Line */}
{index < processSteps.length - 1 && (
{index < currentProcessSteps.length - 1 && (
<div className="absolute top-14 left-1/2 w-full h-1">
<svg className="w-full h-full" preserveAspectRatio="none">
<motion.path
d={`M 0 0 L ${100 / (processSteps.length - 1) * 2}% 0`}
d={`M 0 0 L ${100 / (currentProcessSteps.length - 1) * 2}% 0`}
fill="none"
stroke="url(#flowGradient)"
strokeWidth="2"

View File

@@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
import { Search, Database, FileText, Sparkles, TrendingUp, CheckCircle, Loader2, Brain, Network, Layers, ChevronRight } from "lucide-react";
import { motion, AnimatePresence } from "motion/react";
import { toast } from "sonner";
import { Progress } from "../components/ui/progress";
import { Progress } from "../../components/ui/progress";
const searchStrategies = [
{ value: "bm25", label: "BM25" },
@@ -16,7 +16,7 @@ const knowledgeBases = [
{ value: "nonferrous", label: "有色冶金" },
];
const retrievalResults = [
const steelRetrievalResults = [
{
relevance: 0.92,
source: "高炉故障知识图谱",
@@ -34,18 +34,56 @@ const retrievalResults = [
},
];
const aluminaRetrievalResults = [
{
relevance: 0.91,
source: "电解铝知识图谱",
content: "槽电压异常升高可能原因包括:阳极消耗不均、电解质水平下降、极距增大。建议检查阳极状态和电解质成分,及时调整工艺参数。"
},
{
relevance: 0.85,
source: "故障案例库",
content: "2025年4月案例某电解槽系列电流波动经检查发现阳极导杆接触不良清理紧固后恢复正常。建议定期检查导杆连接状态。"
},
{
relevance: 0.79,
source: "操作手册",
content: "电解槽维护规程每日检查槽电压和系列电流每班测量电解质水平和温度每周分析分子比变化。正常槽电压范围3.8-4.2V。"
},
];
const steelAnswers = [
"目前高炉炉况基本顺行,但请注意冷却壁水温差有上升趋势(当前+3.5°C建议检查冷却水压力。根据历史数据分析和相关案例水温差持续上升可能预示冷却系统存在堵塞风险应立即进行检查。建议参考操作手册中的维护规程进行系统性排查。",
"根据高炉实时监控数据,系统运行基本正常。从热负荷分布来看,当前处于稳定状态。建议持续关注焦比和透气性指标的变化趋势。",
"基于RAG增强检索为您找到高炉运行相关的优化建议。保持当前风量和风温配置定期监控炉身各段温度分布可有效预防异常工况发生。"
];
const aluminaAnswers = [
"目前电解槽系列运行基本稳定,但需要注意槽电压有上升趋势。建议检查阳极状态和电解质水平,及时调整极距。根据历史数据分析,槽电压持续升高可能导致能耗增加。",
"根据电解铝实时监控数据系列电流波动在正常范围内系列电流320KA。建议关注分子比变化保持在2.2-2.4范围内有利于电解效率。",
"基于电解铝知识图谱为您找到以下优化建议保持合理的电解质水平18-22cm定期检查阳极消耗状态可有效降低效应发生概率。"
];
export function RAGSystem() {
const [query, setQuery] = useState("");
const [strategy, setStrategy] = useState("hybrid");
const [knowledgeBase, setKnowledgeBase] = useState("all");
const [industry, setIndustry] = useState<"steel" | "alumina">("steel");
const [isSearching, setIsSearching] = useState(false);
const [showResults, setShowResults] = useState(false);
const [generatedAnswer, setGeneratedAnswer] = useState("");
const [currentResults, setCurrentResults] = useState(retrievalResults);
const [currentResults, setCurrentResults] = useState(steelRetrievalResults);
const [searchProgress, setSearchProgress] = useState(0);
const [currentPhase, setCurrentPhase] = useState<"query" | "retrieval" | "rerank" | "generate">("query");
const [typingText, setTypingText] = useState("");
useEffect(() => {
if (industry === "steel") {
setCurrentResults(steelRetrievalResults);
} else {
setCurrentResults(aluminaRetrievalResults);
}
}, [industry]);
// Typing effect for generated answer
useEffect(() => {
if (generatedAnswer && showResults) {
@@ -106,18 +144,15 @@ export function RAGSystem() {
setCurrentPhase("generate");
// Generate mock answer based on query
const answers = [
"目前炉况基本顺行,但请注意冷却壁水温差有上升趋势(当前+3.5°C建议检查冷却水压力。根据历史数据分析和相关案例水温差持续上升可能预示冷却系统存在堵塞风险应立即进行检查。建议参考操作手册中的维护规程进行系统性排查。",
"根据检索到的知识库信息,该问题涉及到多个方面。首先从实时监控数据来看,系统运行基本正常。其次结合历史案例分析,建议关注关键参数的变化趋势,及时采取预防措施。",
"基于RAG增强检索我为您找到了相关的解决方案和案例。建议结合当前实际情况参考历史经验进行处理。同时建议定期维护相关设备降低故障发生概率。"
];
const answers = industry === "steel" ? steelAnswers : aluminaAnswers;
setGeneratedAnswer(answers[Math.floor(Math.random() * answers.length)]);
setShowResults(true);
setIsSearching(false);
// Randomize result relevance
setCurrentResults(retrievalResults.map(r => ({
const baseResults = industry === "steel" ? steelRetrievalResults : aluminaRetrievalResults;
setCurrentResults(baseResults.map((r: typeof steelRetrievalResults[0]) => ({
...r,
relevance: 0.75 + Math.random() * 0.2
})));
@@ -211,19 +246,36 @@ export function RAGSystem() {
<div className="flex-1">
<label className="text-sm text-slate-400 mb-2 block"></label>
<select
value={knowledgeBase}
onChange={(e) => {
setKnowledgeBase(e.target.value);
toast.info(`已切换到 ${knowledgeBases.find(k => k.value === e.target.value)?.label}`, { duration: 1500 });
<div className="flex gap-2">
<button
onClick={() => {
setIndustry("steel");
toast.info("已切换到钢铁行业知识库", { duration: 1500 });
}}
disabled={isSearching}
className="w-full px-4 py-2 rounded-lg bg-slate-800/50 border border-slate-700/50 text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 disabled:opacity-50"
className={`flex-1 px-3 py-2 rounded-lg text-sm font-medium transition-all ${
industry === "steel"
? "bg-orange-500/20 text-orange-400 border border-orange-500/50"
: "bg-slate-800/50 text-slate-400 border border-slate-700/50 hover:bg-slate-800"
} disabled:opacity-50`}
>
{knowledgeBases.map(k => (
<option key={k.value} value={k.value}>{k.label}</option>
))}
</select>
</button>
<button
onClick={() => {
setIndustry("alumina");
toast.info("已切换到铝冶炼知识库", { duration: 1500 });
}}
disabled={isSearching}
className={`flex-1 px-3 py-2 rounded-lg text-sm font-medium transition-all ${
industry === "alumina"
? "bg-cyan-500/20 text-cyan-400 border border-cyan-500/50"
: "bg-slate-800/50 text-slate-400 border border-slate-700/50 hover:bg-slate-800"
} disabled:opacity-50`}
>
</button>
</div>
</div>
</div>

View File

@@ -166,7 +166,7 @@ export function FurnaceDiagnosis() {
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white mb-1 flex items-center gap-2">
()
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 2, repeat: Infinity, ease: "linear" }}

View File

@@ -68,7 +68,7 @@ export function IntelligentConverter() {
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white mb-1 flex items-center gap-2">
()
<motion.div
animate={{ scale: [1, 1.3, 1], rotate: [0, 180, 360] }}
transition={{ duration: 3, repeat: Infinity }}

View File

@@ -0,0 +1,865 @@
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
import { motion } from "framer-motion";
import { toast } from "sonner";
import {
Activity,
AlertTriangle,
CheckCircle,
Zap,
Thermometer,
Droplets,
Gauge,
Factory,
Box,
Cpu,
Bell,
Play,
Pause,
RotateCcw,
Maximize2,
Flame,
} from "lucide-react";
import * as THREE from "three";
import { DevicePanel } from "../../components/shared/ui/DevicePanel";
import type { DeviceData } from "./devices/types";
import { steelDevices, steelAlerts, steelStatusColors, steelStatusText } from "./devices/types";
interface SceneState {
scene: THREE.Scene;
camera: THREE.PerspectiveCamera;
renderer: THREE.WebGLRenderer;
devices3D: Map<string, THREE.Group>;
isPlaying: boolean;
animationId: number;
}
function createBlastFurnace(color: number): THREE.Group {
const group = new THREE.Group();
const bodyMaterial = new THREE.MeshStandardMaterial({
color: 0x7f1d1d,
emissive: color,
emissiveIntensity: 0.15,
roughness: 0.6,
metalness: 0.3,
});
const bodyGeometry = new THREE.CylinderGeometry(1.75, 2, 5.6, 32);
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.castShadow = true;
body.receiveShadow = true;
group.add(body);
const throatGeometry = new THREE.CylinderGeometry(1.54, 1.75, 1.05, 32);
const throat = new THREE.Mesh(throatGeometry, bodyMaterial);
throat.position.y = 3.325;
throat.castShadow = true;
group.add(throat);
const topGeometry = new THREE.CylinderGeometry(1.26, 1.54, 0.7, 32);
const top = new THREE.Mesh(topGeometry, bodyMaterial);
top.position.y = 4.2;
group.add(top);
const hearthGeometry = new THREE.CylinderGeometry(2, 1.75, 1.4, 32);
const hearthMaterial = new THREE.MeshStandardMaterial({
color: 0x451a03,
roughness: 0.7,
metalness: 0.2,
});
const hearth = new THREE.Mesh(hearthGeometry, hearthMaterial);
hearth.position.y = -3.5;
hearth.castShadow = true;
group.add(hearth);
const tuyereMaterial = new THREE.MeshStandardMaterial({
color: 0x6b7280,
roughness: 0.3,
metalness: 0.8,
});
for (let i = 0; i < 4; i++) {
const angle = (i * Math.PI) / 2;
const tuyereGeometry = new THREE.CylinderGeometry(0.14, 0.175, 0.42, 16);
const tuyere = new THREE.Mesh(tuyereGeometry, tuyereMaterial);
tuyere.position.set(Math.cos(angle) * 1.61, -1.4, Math.sin(angle) * 1.61);
tuyere.rotation.z = Math.PI / 4;
tuyere.castShadow = true;
group.add(tuyere);
}
const hearthBottomGeometry = new THREE.CylinderGeometry(1.75, 1.75, 0.35, 32);
const hearthBottom = new THREE.Mesh(hearthBottomGeometry, hearthMaterial);
hearthBottom.position.y = -4.2;
group.add(hearthBottom);
return group;
}
function createHotBlastStove(color: number): THREE.Group {
const group = new THREE.Group();
const bodyMaterial = new THREE.MeshStandardMaterial({
color: 0x9a3412,
emissive: color,
emissiveIntensity: 0.2,
roughness: 0.5,
metalness: 0.3,
});
const bodyGeometry = new THREE.CylinderGeometry(1.05, 1.26, 4.9, 32);
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.castShadow = true;
body.receiveShadow = true;
group.add(body);
const domeGeometry = new THREE.SphereGeometry(1.05, 32, 16, 0, Math.PI * 2, 0, Math.PI / 2);
const dome = new THREE.Mesh(domeGeometry, bodyMaterial);
dome.position.y = 2.45;
dome.castShadow = true;
group.add(dome);
const checkerMaterial = new THREE.MeshStandardMaterial({
color: 0x78350f,
roughness: 0.7,
metalness: 0.2,
});
const checkerGeometry = new THREE.BoxGeometry(0.84, 0.56, 0.84);
for (let i = 0; i < 4; i++) {
const checker = new THREE.Mesh(checkerGeometry, checkerMaterial);
checker.position.y = -0.7 + i * 1.05;
checker.castShadow = true;
group.add(checker);
}
const chimneyGeometry = new THREE.CylinderGeometry(0.28, 0.35, 1.4, 16);
const chimney = new THREE.Mesh(chimneyGeometry, bodyMaterial);
chimney.position.set(0.42, 3.15, 0);
chimney.castShadow = true;
group.add(chimney);
const pipeMaterial = new THREE.MeshStandardMaterial({
color: 0x6b7280,
roughness: 0.4,
metalness: 0.7,
});
const pipeGeometry = new THREE.CylinderGeometry(0.105, 0.105, 1.05, 16);
const pipe1 = new THREE.Mesh(pipeGeometry, pipeMaterial);
pipe1.position.set(0.91, 1.4, 0);
pipe1.rotation.z = Math.PI / 3;
group.add(pipe1);
const pipe2 = new THREE.Mesh(pipeGeometry, pipeMaterial);
pipe2.position.set(-0.91, 1.4, 0);
pipe2.rotation.z = -Math.PI / 3;
group.add(pipe2);
return group;
}
function createAirBlower(color: number): THREE.Group {
const group = new THREE.Group();
const housingMaterial = new THREE.MeshStandardMaterial({
color: 0x1e40af,
emissive: color,
emissiveIntensity: 0.15,
roughness: 0.4,
metalness: 0.6,
});
const housingGeometry = new THREE.CylinderGeometry(0.84, 0.84, 1.75, 32);
const housing = new THREE.Mesh(housingGeometry, housingMaterial);
housing.castShadow = true;
group.add(housing);
const motorGeometry = new THREE.CylinderGeometry(0.49, 0.49, 1.26, 16);
const motorMaterial = new THREE.MeshStandardMaterial({
color: 0x374151,
roughness: 0.4,
metalness: 0.6,
});
const motor = new THREE.Mesh(motorGeometry, motorMaterial);
motor.position.y = 1.4;
motor.castShadow = true;
group.add(motor);
const impellerMaterial = new THREE.MeshStandardMaterial({
color: 0x60a5fa,
roughness: 0.3,
metalness: 0.7,
});
const impellerGeometry = new THREE.BoxGeometry(1.4, 0.21, 0.56);
for (let i = 0; i < 6; i++) {
const angle = (i * Math.PI) / 3;
const impeller = new THREE.Mesh(impellerGeometry, impellerMaterial);
impeller.position.y = 0.35 - i * 0.105;
impeller.rotation.y = angle;
impeller.castShadow = true;
group.add(impeller);
}
const baseGeometry = new THREE.BoxGeometry(1.75, 0.28, 1.4);
const base = new THREE.Mesh(baseGeometry, motorMaterial);
base.position.y = -1.015;
base.castShadow = true;
group.add(base);
const inletGeometry = new THREE.CylinderGeometry(0.4, 0.4, 0.6, 16);
const inlet = new THREE.Mesh(inletGeometry, housingMaterial);
inlet.position.set(0, 0.8, 1.1);
inlet.rotation.x = Math.PI / 2;
group.add(inlet);
const outletGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.6, 16);
const outlet = new THREE.Mesh(outletGeometry, housingMaterial);
outlet.position.set(0, 0.8, -1.1);
outlet.rotation.x = -Math.PI / 2;
group.add(outlet);
return group;
}
function createCoolingWall(color: number): THREE.Group {
const group = new THREE.Group();
const plateMaterial = new THREE.MeshStandardMaterial({
color: 0x0c4a6e,
emissive: color,
emissiveIntensity: 0.1,
roughness: 0.3,
metalness: 0.7,
});
const plateGeometry = new THREE.BoxGeometry(0.3, 5, 3);
const plate = new THREE.Mesh(plateGeometry, plateMaterial);
plate.castShadow = true;
plate.receiveShadow = true;
group.add(plate);
const pipeMaterial = new THREE.MeshStandardMaterial({
color: 0x0891b2,
roughness: 0.2,
metalness: 0.9,
});
for (let i = 0; i < 5; i++) {
const pipeGeometry = new THREE.CylinderGeometry(0.08, 0.08, 3.2, 16);
const pipe = new THREE.Mesh(pipeGeometry, pipeMaterial);
pipe.position.set(0.2, -2 + i * 1, 0);
pipe.castShadow = true;
group.add(pipe);
}
const bracketMaterial = new THREE.MeshStandardMaterial({
color: 0x57534e,
roughness: 0.5,
metalness: 0.5,
});
for (let i = 0; i < 2; i++) {
const bracketGeometry = new THREE.BoxGeometry(0.5, 0.2, 0.2);
const bracket = new THREE.Mesh(bracketGeometry, bracketMaterial);
bracket.position.set(0.25, 2 - i * 4, 1.4);
group.add(bracket);
const bracket2 = new THREE.Mesh(bracketGeometry, bracketMaterial);
bracket2.position.set(0.25, 2 - i * 4, -1.4);
group.add(bracket2);
}
return group;
}
function createCoalMill(color: number): THREE.Group {
const group = new THREE.Group();
const bodyMaterial = new THREE.MeshStandardMaterial({
color: 0x374151,
emissive: color,
emissiveIntensity: 0.1,
roughness: 0.5,
metalness: 0.5,
});
const bodyGeometry = new THREE.CylinderGeometry(0.7, 0.7, 2.1, 32);
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.castShadow = true;
group.add(body);
const rollerMaterial = new THREE.MeshStandardMaterial({
color: 0x6b7280,
roughness: 0.4,
metalness: 0.7,
});
for (let i = 0; i < 3; i++) {
const angle = (i * Math.PI * 2) / 3;
const rollerGeometry = new THREE.CylinderGeometry(0.21, 0.21, 0.28, 16);
const roller = new THREE.Mesh(rollerGeometry, rollerMaterial);
roller.position.set(Math.cos(angle) * 0.42, 0, Math.sin(angle) * 0.42);
roller.castShadow = true;
group.add(roller);
}
const shaftGeometry = new THREE.CylinderGeometry(0.07, 0.07, 1.4, 16);
const shaft = new THREE.Mesh(shaftGeometry, rollerMaterial);
shaft.position.y = 1.19;
group.add(shaft);
const feedGeometry = new THREE.CylinderGeometry(0.175, 0.175, 0.56, 16);
const feed = new THREE.Mesh(feedGeometry, bodyMaterial);
feed.position.y = 1.75;
feed.castShadow = true;
group.add(feed);
const dischargeGeometry = new THREE.CylinderGeometry(0.14, 0.14, 0.42, 16);
const discharge = new THREE.Mesh(dischargeGeometry, bodyMaterial);
discharge.position.set(0.56, -0.35, 0);
discharge.rotation.z = Math.PI / 2;
group.add(discharge);
const baseGeometry = new THREE.BoxGeometry(1.4, 0.21, 1.4);
const base = new THREE.Mesh(baseGeometry, rollerMaterial);
base.position.y = -1.155;
base.castShadow = true;
group.add(base);
return group;
}
function createFeedConveyor(color: number): THREE.Group {
const group = new THREE.Group();
const frameMaterial = new THREE.MeshStandardMaterial({
color: 0x57534e,
roughness: 0.5,
metalness: 0.5,
});
const frameGeometry = new THREE.BoxGeometry(2.8, 0.07, 0.42);
const frame = new THREE.Mesh(frameGeometry, frameMaterial);
frame.castShadow = true;
frame.receiveShadow = true;
group.add(frame);
const legGeometry = new THREE.BoxGeometry(0.07, 1.05, 0.07);
for (let i = 0; i < 4; i++) {
const x = -1.05 + i * 0.7;
const leg = new THREE.Mesh(legGeometry, frameMaterial);
leg.position.set(x, -0.525, 0);
leg.castShadow = true;
group.add(leg);
}
const beltMaterial = new THREE.MeshStandardMaterial({
color: 0x1f2937,
roughness: 0.8,
metalness: 0.2,
});
const beltGeometry = new THREE.BoxGeometry(2.66, 0.035, 0.35);
const belt = new THREE.Mesh(beltGeometry, beltMaterial);
belt.position.y = 0.056;
group.add(belt);
const rollerMaterial = new THREE.MeshStandardMaterial({
color: 0x9ca3af,
roughness: 0.3,
metalness: 0.8,
});
const rollerGeometry = new THREE.CylinderGeometry(0.105, 0.105, 0.35, 16);
const roller1 = new THREE.Mesh(rollerGeometry, rollerMaterial);
roller1.position.set(-1.19, 0, 0);
roller1.rotation.x = Math.PI / 2;
roller1.castShadow = true;
group.add(roller1);
const roller2 = new THREE.Mesh(rollerGeometry, rollerMaterial);
roller2.position.set(1.19, 0, 0);
roller2.rotation.x = Math.PI / 2;
roller2.castShadow = true;
group.add(roller2);
const hopperMaterial = new THREE.MeshStandardMaterial({
color: 0x78716c,
roughness: 0.5,
metalness: 0.5,
});
const hopperGeometry = new THREE.BoxGeometry(0.56, 0.42, 0.42);
const hopper = new THREE.Mesh(hopperGeometry, hopperMaterial);
hopper.position.set(-1.19, 0.35, 0);
hopper.castShadow = true;
group.add(hopper);
return group;
}
function createSteelPipeSystem(): THREE.Group {
const group = new THREE.Group();
const pipeMaterial = new THREE.MeshStandardMaterial({
color: 0x525252,
roughness: 0.4,
metalness: 0.7,
});
const pipe1Geometry = new THREE.CylinderGeometry(0.07, 0.07, 5.6, 16);
const pipe1 = new THREE.Mesh(pipe1Geometry, pipeMaterial);
pipe1.position.set(-2.1, 5.6, 0);
pipe1.rotation.z = Math.PI / 2;
group.add(pipe1);
const pipe2Geometry = new THREE.CylinderGeometry(0.056, 0.056, 3.5, 16);
const pipe2 = new THREE.Mesh(pipe2Geometry, pipeMaterial);
pipe2.position.set(-1.4, 3.5, 0);
group.add(pipe2);
const pipe3Geometry = new THREE.CylinderGeometry(0.056, 0.056, 4.2, 16);
const pipe3 = new THREE.Mesh(pipe3Geometry, pipeMaterial);
pipe3.position.set(2.1, 2.1, 0);
pipe3.rotation.z = Math.PI / 2;
group.add(pipe3);
return group;
}
const deviceCreators: Record<string, (color: number) => THREE.Group> = {
blastFurnace: createBlastFurnace,
hotBlastStove: createHotBlastStove,
airBlower: createAirBlower,
coolingWall: createCoolingWall,
coalMill: createCoalMill,
feedConveyor: createFeedConveyor,
};
export default function SteelDashboard() {
const containerRef = useRef<HTMLDivElement>(null);
const sceneRef = useRef<SceneState | null>(null);
const [selectedDevice, setSelectedDevice] = useState<DeviceData | null>(null);
const [isPlaying, setIsPlaying] = useState(true);
const [autoRotate, setAutoRotate] = useState(true);
const [cameraAngle, setCameraAngle] = useState(0);
const deviceList = useMemo(() => steelDevices, []);
const createDeviceMesh = useCallback((device: DeviceData): THREE.Group => {
const creator = deviceCreators[device.type];
if (!creator) {
console.warn(`Unknown device type: ${device.type}`);
return new THREE.Group();
}
const group = creator(device.color);
const [x, y, z] = device.position;
group.position.set(x, y, z);
group.userData.deviceId = device.id;
return group;
}, []);
const onMouseClick = useCallback(
(event: MouseEvent) => {
if (!sceneRef.current || !containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
const y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(new THREE.Vector2(x, y), sceneRef.current.camera);
const allObjects: THREE.Object3D[] = [];
sceneRef.current.devices3D.forEach((group) => {
group.traverse((obj) => {
if (obj instanceof THREE.Mesh) {
allObjects.push(obj);
}
});
});
const intersects = raycaster.intersectObjects(allObjects);
if (intersects.length > 0) {
let obj: THREE.Object3D | null = intersects[0].object;
while (obj && !obj.userData.deviceId) {
obj = obj.parent;
}
if (obj?.userData.deviceId) {
const device = deviceList.find((d) => d.id === obj!.userData.deviceId);
if (device) {
setSelectedDevice(device);
toast.info(`选中: ${device.name}`, { duration: 1500 });
}
}
}
},
[deviceList]
);
useEffect(() => {
if (!containerRef.current || sceneRef.current) return;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0f1a);
const camera = new THREE.PerspectiveCamera(50, containerRef.current.clientWidth / containerRef.current.clientHeight, 0.1, 1000);
camera.position.set(15, 12, 20);
camera.lookAt(0, 2, 0);
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(0xf97316, 0.6, 40);
fillLight1.position.set(-15, 10, 10);
scene.add(fillLight1);
const fillLight2 = new THREE.PointLight(0xdc2626, 0.5, 40);
fillLight2.position.set(15, 10, -10);
scene.add(fillLight2);
const floorMaterial = new THREE.MeshStandardMaterial({
color: 0x1e293b,
roughness: 0.8,
});
const floorGeometry = new THREE.PlaneGeometry(50, 50);
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(50, 50, 0x334155, 0x1e293b);
gridHelper.position.y = 0;
scene.add(gridHelper);
const pipeSystem = createSteelPipeSystem();
scene.add(pipeSystem);
const devices3D = new Map<string, THREE.Group>();
deviceList.forEach((device) => {
const group = createDeviceMesh(device);
scene.add(group);
devices3D.set(device.id, group);
});
sceneRef.current = {
scene,
camera,
renderer,
devices3D,
isPlaying: true,
animationId: 0,
};
let localCameraAngle = 0;
const animate = () => {
if (!sceneRef.current) return;
const { scene, camera, renderer, devices3D, isPlaying } = sceneRef.current;
if (isPlaying && autoRotate) {
localCameraAngle += 0.003;
camera.position.x = Math.sin(localCameraAngle) * 16;
camera.position.z = Math.cos(localCameraAngle) * 16;
camera.position.y = 12 + Math.sin(Date.now() * 0.0002) * 1.5;
camera.lookAt(0, 0, 0);
setCameraAngle(localCameraAngle);
}
deviceList.forEach((device) => {
const group = devices3D.get(device.id);
if (group && device.status === "running") {
if (device.type === "airBlower" && group.children[3]) {
group.children[3].rotation.y += 0.05;
}
if (device.type === "coalMill" && group.children[4]) {
group.children[4].rotation.y += 0.03;
}
if (device.type === "feedConveyor") {
group.children.forEach((child) => {
if (child instanceof THREE.Mesh && child.geometry && child.geometry.type === "BoxGeometry" && child.geometry.parameters.width > 3) {
child.position.x = 0.5 - ((Date.now() % 3000) / 3000) * 0.3;
}
});
}
}
});
sceneRef.current.animationId = requestAnimationFrame(animate);
renderer.render(scene, camera);
};
animate();
renderer.domElement.addEventListener("click", onMouseClick);
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);
if (sceneRef.current) {
cancelAnimationFrame(sceneRef.current.animationId);
renderer.domElement.removeEventListener("click", onMouseClick);
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, createDeviceMesh, deviceList, onMouseClick, autoRotate]);
useEffect(() => {
if (sceneRef.current) {
sceneRef.current.isPlaying = isPlaying;
}
}, [isPlaying]);
const runningCount = deviceList.filter((d) => d.status === "running").length;
const warningCount = deviceList.filter((d) => d.status === "warning").length;
const avgEfficiency = (deviceList.reduce((acc, d) => acc + d.efficiency, 0) / deviceList.length).toFixed(1);
const alerts = steelAlerts;
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 relative overflow-hidden">
<div ref={containerRef} className="absolute inset-0" />
<div className="absolute top-6 left-6 z-10">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-slate-800/80 backdrop-blur-xl rounded-2xl border border-slate-700/50 px-5 py-3"
>
<div className="flex items-center gap-3">
<div className="p-2.5 rounded-xl bg-gradient-to-br from-orange-500/20 to-red-500/20 border border-orange-500/30">
<Factory className="w-6 h-6 text-orange-400" />
</div>
<div>
<h1 className="text-xl font-bold text-white">()</h1>
<p className="text-xs text-slate-400"></p>
</div>
</div>
</motion.div>
</div>
<div className="absolute top-6 right-6 z-10">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="flex items-center gap-3"
>
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-orange-500/20 border border-orange-500/30">
<div className="w-2 h-2 rounded-full bg-orange-400 animate-pulse" />
<span className="text-orange-400 text-sm font-medium"></span>
</div>
<div className="flex items-center gap-1.5 bg-slate-800/80 backdrop-blur-xl rounded-full border border-slate-700/50 p-1">
<button
onClick={() => setIsPlaying(!isPlaying)}
className={`p-2 rounded-full transition-colors ${isPlaying ? "bg-orange-500/20 text-orange-400" : "bg-slate-700/50 text-slate-400 hover:text-white"}`}
>
{isPlaying ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
</button>
<button
onClick={() => setAutoRotate(!autoRotate)}
className={`p-2 rounded-full transition-colors ${autoRotate ? "bg-orange-500/20 text-orange-400" : "bg-slate-700/50 text-slate-400 hover:text-white"}`}
>
<RotateCcw className="w-4 h-4" />
</button>
<button className="p-2 rounded-full bg-slate-700/50 text-slate-400 hover:text-white transition-colors">
<Maximize2 className="w-4 h-4" />
</button>
</div>
</motion.div>
</div>
<div className="absolute bottom-4 left-6 z-10">
<div className="w-80 bg-slate-800/80 backdrop-blur-xl rounded-2xl border border-slate-700/50 p-3">
<div className="flex items-center justify-between mb-2">
<h2 className="text-white font-semibold flex items-center gap-2">
<Box className="w-4 h-4 text-orange-400" />
</h2>
<span className="text-xs text-slate-400">{deviceList.length} </span>
</div>
<div className="grid grid-cols-2 gap-1.5 max-h-32 overflow-y-auto pr-1">
{deviceList.map((device) => (
<motion.div
key={device.id}
whileHover={{ scale: 1.02, x: 5 }}
whileTap={{ scale: 0.98 }}
onClick={() => {
setSelectedDevice(device);
toast.info(`选中: ${device.name}`, { duration: 1500 });
}}
className={`p-1.5 rounded-lg border cursor-pointer transition-all ${
selectedDevice?.id === device.id
? "bg-orange-500/20 border-orange-500/50"
: device.status === "warning"
? "bg-amber-500/10 border-amber-500/30 hover:border-amber-500/50"
: "bg-slate-700/50 border-slate-600/50 hover:border-orange-500/50"
}`}
>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-1">
<div
className="w-1.5 h-1.5 rounded-full animate-pulse"
style={{
backgroundColor: device.status === "running" ? "#f97316" : device.status === "warning" ? "#f59e0b" : "#6b7280",
}}
/>
<span className="text-white text-[10px] font-medium">{device.name}</span>
</div>
<span
className={`text-[10px] px-1 py-0.5 rounded-full ${
device.status === "running" ? "bg-orange-500/20 text-orange-400" : device.status === "warning" ? "bg-amber-500/20 text-amber-400" : "bg-slate-500/20 text-slate-400"
}`}
>
{device.status === "running" ? "运行" : device.status === "warning" ? "预警" : "停止"}
</span>
</div>
<div className="grid grid-cols-3 gap-0.5 text-[10px]">
<div className="text-center p-0.5 rounded bg-slate-900/50">
<Thermometer className="w-2.5 h-2.5 text-red-400 mx-auto mb-0.5" />
<div className="text-white font-mono text-[9px]">{device.temperature}°C</div>
</div>
<div className="text-center p-0.5 rounded bg-slate-900/50">
<Gauge className="w-2.5 h-2.5 text-blue-400 mx-auto mb-0.5" />
<div className="text-white font-mono text-[9px]">{device.pressure}</div>
</div>
<div className="text-center p-0.5 rounded bg-slate-900/50">
<Activity className="w-2.5 h-2.5 text-orange-400 mx-auto mb-0.5" />
<div className="text-white font-mono text-[9px]">{device.efficiency}%</div>
</div>
</div>
</motion.div>
))}
</div>
</div>
</div>
<div className="absolute bottom-6 right-6 z-10">
<div className="w-72 space-y-2">
<div className="bg-slate-800/80 backdrop-blur-xl rounded-2xl border border-slate-700/50 p-3">
<h2 className="text-white font-semibold mb-2 flex items-center gap-2">
<Activity className="w-4 h-4 text-emerald-400" />
</h2>
<div className="grid grid-cols-2 gap-2">
<div className="bg-gradient-to-br from-orange-500/20 to-red-500/20 rounded-lg p-2 border border-orange-500/30">
<Droplets className="w-3 h-3 text-orange-400 mb-0.5" />
<div className="text-lg font-bold text-white">{avgEfficiency}%</div>
<div className="text-slate-400 text-[10px]"></div>
</div>
<div className="bg-gradient-to-br from-emerald-500/20 to-teal-500/20 rounded-lg p-2 border border-emerald-500/30">
<CheckCircle className="w-3 h-3 text-emerald-400 mb-0.5" />
<div className="text-lg font-bold text-white">
{runningCount}/{deviceList.length}
</div>
<div className="text-slate-400 text-[10px]"></div>
</div>
<div className="bg-gradient-to-br from-amber-500/20 to-yellow-500/20 rounded-lg p-2 border border-amber-500/30">
<Zap className="w-3 h-3 text-amber-400 mb-0.5" />
<div className="text-lg font-bold text-white">{warningCount}</div>
<div className="text-slate-400 text-[10px]"></div>
</div>
<div className="bg-gradient-to-br from-violet-500/20 to-purple-500/20 rounded-lg p-2 border border-violet-500/30">
<Cpu className="w-3 h-3 text-violet-400 mb-0.5" />
<div className="text-lg font-bold text-white">8500</div>
<div className="text-slate-400 text-[10px]"></div>
</div>
</div>
</div>
<div className="bg-slate-800/80 backdrop-blur-xl rounded-2xl border border-slate-700/50 p-3">
<h2 className="text-white font-semibold mb-2 flex items-center gap-2">
<Bell className="w-4 h-4 text-amber-400" />
<span className="ml-auto text-[10px] text-slate-400 bg-slate-700/50 px-1.5 py-0.5 rounded-full">{alerts.length} </span>
</h2>
<div className="space-y-1.5 max-h-20 overflow-y-auto">
{alerts.map((alert) => (
<div
key={alert.id}
className={`p-1.5 rounded-lg border text-xs ${
alert.level === "warning" ? "bg-amber-500/10 border-amber-500/30" : "bg-blue-500/10 border-blue-500/30"
}`}
>
<div className="flex items-start gap-2">
<AlertTriangle
className={`w-3 h-3 mt-0.5 ${alert.level === "warning" ? "text-amber-400" : "text-blue-400"}`}
/>
<div className="flex-1 min-w-0">
<p className="text-white text-[10px]">{alert.message}</p>
<p className="text-slate-500 text-[10px] mt-0.5">{alert.time}</p>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
{selectedDevice && (
<div className="absolute top-1/2 right-6 transform -translate-y-1/2 z-20">
<DevicePanel
device={selectedDevice}
onClose={() => setSelectedDevice(null)}
statusColors={steelStatusColors}
texts={steelStatusText}
/>
</div>
)}
<div className="absolute bottom-6 left-6 z-10">
<div className="text-xs text-slate-500 bg-slate-800/60 backdrop-blur-sm rounded-lg px-3 py-1.5">
| |
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,72 @@
import type { DeviceData } from "../../../components/shared/types";
export type SteelDeviceType =
| "blastFurnace"
| "hotBlastStove"
| "airBlower"
| "coolingWall"
| "coalMill"
| "feedConveyor";
export interface SteelDeviceData {
id: string;
name: string;
type: SteelDeviceType;
industry: "steel";
position: [number, number, number];
status: "running" | "warning" | "stopped";
temperature: number;
pressure: number;
flow: number;
efficiency: number;
color: number;
}
export const steelStatusColors = {
running: {
bg: "from-orange-500/20 to-red-500/20",
border: "border-orange-500/50",
text: "text-orange-400",
dot: "bg-orange-400",
},
warning: {
bg: "from-amber-500/20 to-yellow-500/20",
border: "border-amber-500/50",
text: "text-amber-400",
dot: "bg-amber-400",
},
stopped: {
bg: "from-slate-500/20 to-gray-500/20",
border: "border-slate-500/50",
text: "text-slate-400",
dot: "bg-slate-400",
},
};
export const steelStatusText = {
running: "运行中",
warning: "预警",
stopped: "已停止",
};
export type { DeviceData } from "../../../components/shared/types";
export const steelDevices: SteelDeviceData[] = [
{ id: "bf-1", name: "1号高炉", type: "blastFurnace", industry: "steel", position: [0, 5, 0], status: "running", temperature: 1450, pressure: 0.35, flow: 1800, efficiency: 92.5, color: 0xdc2626 },
{ id: "bf-2", name: "2号高炉", type: "blastFurnace", industry: "steel", position: [6, 5, 0], status: "running", temperature: 1420, pressure: 0.32, flow: 1750, efficiency: 91.8, color: 0xb91c1c },
{ id: "hbs-1", name: "1号热风炉", type: "hotBlastStove", industry: "steel", position: [-5, 4, 0], status: "running", temperature: 1250, pressure: 0.25, flow: 500, efficiency: 88.5, color: 0xf97316 },
{ id: "hbs-2", name: "2号热风炉", type: "hotBlastStove", industry: "steel", position: [-5, 4, 3], status: "running", temperature: 1280, pressure: 0.28, flow: 520, efficiency: 89.2, color: 0xea580c },
{ id: "hbs-3", name: "3号热风炉", type: "hotBlastStove", industry: "steel", position: [-5, 4, -3], status: "warning", temperature: 1150, pressure: 0.22, flow: 450, efficiency: 85.0, color: 0xf59e0b },
{ id: "ab-1", name: "1号鼓风机", type: "airBlower", industry: "steel", position: [-8, 2, 0], status: "running", temperature: 80, pressure: 0.6, flow: 2500, efficiency: 94.2, color: 0x3b82f6 },
{ id: "ab-2", name: "2号鼓风机", type: "airBlower", industry: "steel", position: [-8, 2, 3], status: "running", temperature: 75, pressure: 0.55, flow: 2400, efficiency: 93.8, color: 0x2563eb },
{ id: "cm-1", name: "1号磨煤机", type: "coalMill", industry: "steel", position: [3, 2, 4], status: "running", temperature: 120, pressure: 0.15, flow: 85, efficiency: 90.5, color: 0x475569 },
{ id: "cm-2", name: "2号磨煤机", type: "coalMill", industry: "steel", position: [3, 2, -4], status: "warning", temperature: 145, pressure: 0.18, flow: 72, efficiency: 86.2, color: 0xf59e0b },
{ id: "fc-1", name: "1号给煤机", type: "feedConveyor", industry: "steel", position: [0, 1, 4], status: "running", temperature: 45, pressure: 0.08, flow: 120, efficiency: 95.8, color: 0x78716c },
{ id: "fc-2", name: "2号给煤机", type: "feedConveyor", industry: "steel", position: [0, 1, -4], status: "running", temperature: 42, pressure: 0.07, flow: 115, efficiency: 96.1, color: 0x57534e },
];
export const steelAlerts = [
{ id: 1, level: "warning" as const, message: "3号热风炉温度异常偏低", time: "14:23:45" },
{ id: 2, level: "warning" as const, message: "2号磨煤机温度过高", time: "14:15:30" },
{ id: 3, level: "info" as const, message: "1号高炉完成例行检修", time: "13:45:00" },
];

View File

@@ -1,24 +1,25 @@
import { createBrowserRouter } from "react-router";
import { RootLayout } from "./components/RootLayout";
import { Dashboard } from "./pages/Dashboard";
import { FurnaceDiagnosis } from "./pages/FurnaceDiagnosis";
import { KnowledgeGraph } from "./pages/KnowledgeGraph";
import { RAGSystem } from "./pages/RAGSystem";
import { MonitoringCenter } from "./pages/MonitoringCenter";
import { QualityTracing } from "./pages/QualityTracing";
import { ProcessOptimization } from "./pages/ProcessOptimization";
import { ModelManagement } from "./pages/ModelManagement";
import { DigitalTwin } from "./pages/DigitalTwin";
import { DataCollection } from "./pages/DataCollection";
import { DataGovernanceStandard } from "./pages/DataGovernanceStandard";
import { PromptEngineering } from "./pages/PromptEngineering";
import { HumanMachineCollaboration } from "./pages/HumanMachineCollaboration";
import { IntelligentConverter } from "./pages/IntelligentConverter";
import { EconomicAnalysis } from "./pages/EconomicAnalysis";
import { KnowledgeFusion } from "./pages/KnowledgeFusion";
import { EntityExtraction } from "./pages/EntityExtraction";
import { EdgeCloudSync } from "./pages/EdgeCloudSync";
import { IndustrialDashboard } from "./pages/IndustrialDashboard";
import { Dashboard } from "./pages/common/Dashboard";
import { FurnaceDiagnosis } from "./pages/steel/FurnaceDiagnosis";
import { KnowledgeGraph } from "./pages/common/KnowledgeGraph";
import { RAGSystem } from "./pages/common/RAGSystem";
import { MonitoringCenter } from "./pages/common/MonitoringCenter";
import { QualityTracing } from "./pages/common/QualityTracing";
import { ProcessOptimization } from "./pages/common/ProcessOptimization";
import { ModelManagement } from "./pages/common/ModelManagement";
import { DigitalTwin } from "./pages/common/DigitalTwin";
import { DataCollection } from "./pages/common/DataCollection";
import { DataGovernanceStandard } from "./pages/common/DataGovernanceStandard";
import { PromptEngineering } from "./pages/common/PromptEngineering";
import { HumanMachineCollaboration } from "./pages/alumina/HumanMachineCollaboration";
import { IntelligentConverter } from "./pages/steel/IntelligentConverter";
import { EconomicAnalysis } from "./pages/common/EconomicAnalysis";
import { KnowledgeFusion } from "./pages/common/KnowledgeFusion";
import { EntityExtraction } from "./pages/common/EntityExtraction";
import { EdgeCloudSync } from "./pages/common/EdgeCloudSync";
import AluminaDashboard from "./pages/alumina/AluminaDashboard";
import SteelDashboard from "./pages/steel/SteelDashboard";
export const router = createBrowserRouter([
{
@@ -43,7 +44,8 @@ export const router = createBrowserRouter([
{ path: "human-machine", Component: HumanMachineCollaboration },
{ path: "intelligent-converter", Component: IntelligentConverter },
{ path: "economic-analysis", Component: EconomicAnalysis },
{ path: "industrial-dashboard", Component: IndustrialDashboard },
{ path: "alumina-dashboard", Component: AluminaDashboard },
{ path: "steel-dashboard", Component: SteelDashboard },
],
},
]);