add new contents
This commit is contained in:
@@ -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
|
## Running the code
|
||||||
|
|
||||||
Run `npm i` to install the dependencies.
|
Run `npm i` to install the dependencies.
|
||||||
|
|||||||
391
docs/数字孪生系统拆分实施方案.md
Normal file
391
docs/数字孪生系统拆分实施方案.md
Normal 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
|
||||||
|
**状态**:待审批
|
||||||
@@ -5,18 +5,19 @@ import { Toaster } from "./ui/sonner";
|
|||||||
|
|
||||||
const navigationItems = [
|
const navigationItems = [
|
||||||
{ path: "/", label: "工作台", icon: Factory },
|
{ path: "/", label: "工作台", icon: Factory },
|
||||||
{ path: "/furnace-diagnosis", label: "高炉诊断", icon: Brain },
|
{ path: "/steel-dashboard", label: "钢铁监控(钢铁)", icon: Monitor },
|
||||||
{ path: "/intelligent-converter", label: "智能转炉", icon: Flame },
|
{ 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: "/quality-tracing", label: "质量溯源", icon: TrendingUp },
|
||||||
{ path: "/monitoring", label: "监控中心", icon: MonitorPlay },
|
{ path: "/monitoring", label: "监控中心", icon: MonitorPlay },
|
||||||
{ path: "/industrial-dashboard", label: "监控大屏", icon: Monitor },
|
|
||||||
{ path: "/knowledge-graph", label: "知识图谱", icon: Network },
|
{ path: "/knowledge-graph", label: "知识图谱", icon: Network },
|
||||||
{ path: "/knowledge-fusion", label: "知识融合", icon: GitMerge },
|
{ path: "/knowledge-fusion", label: "知识融合", icon: GitMerge },
|
||||||
{ path: "/entity-extraction", label: "实体抽取", icon: Scan },
|
{ path: "/entity-extraction", label: "实体抽取", icon: Scan },
|
||||||
{ path: "/rag-system", label: "智能检索", icon: Search },
|
{ path: "/rag-system", label: "智能检索", icon: Search },
|
||||||
{ path: "/prompt-engineering", label: "Prompt工程", icon: Sparkles },
|
{ path: "/prompt-engineering", label: "Prompt工程", icon: Sparkles },
|
||||||
{ path: "/model-management", label: "模型管理", icon: Cpu },
|
{ path: "/model-management", label: "模型管理", icon: Cpu },
|
||||||
{ path: "/human-machine", label: "人机协同", icon: Eye },
|
|
||||||
{ path: "/digital-twin", label: "数字孪生", icon: Box },
|
{ path: "/digital-twin", label: "数字孪生", icon: Box },
|
||||||
{ path: "/edge-cloud-sync", label: "边缘云同步", icon: Cloud },
|
{ path: "/edge-cloud-sync", label: "边缘云同步", icon: Cloud },
|
||||||
{ path: "/data-collection", label: "数据采集", icon: Database },
|
{ path: "/data-collection", label: "数据采集", icon: Database },
|
||||||
|
|||||||
84
src/app/components/shared/scene/CameraControls.ts
Normal file
84
src/app/components/shared/scene/CameraControls.ts
Normal 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() {
|
||||||
|
}
|
||||||
179
src/app/components/shared/scene/SceneSetup.ts
Normal file
179
src/app/components/shared/scene/SceneSetup.ts
Normal 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]);
|
||||||
|
}
|
||||||
57
src/app/components/shared/types.ts
Normal file
57
src/app/components/shared/types.ts
Normal 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: "已停止",
|
||||||
|
};
|
||||||
75
src/app/components/shared/ui/DevicePanel.tsx
Normal file
75
src/app/components/shared/ui/DevicePanel.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
1279
src/app/pages/alumina/AluminaDashboard.tsx
Normal file
1279
src/app/pages/alumina/AluminaDashboard.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -446,7 +446,7 @@ export function HumanMachineCollaboration() {
|
|||||||
<Cpu className="w-6 h-6 text-cyan-400" />
|
<Cpu className="w-6 h-6 text-cyan-400" />
|
||||||
</div>
|
</div>
|
||||||
<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>
|
<p className="text-sm text-slate-400">基于数字孪生的实时监控与智能交互</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
84
src/app/pages/alumina/devices/types.ts
Normal file
84
src/app/pages/alumina/devices/types.ts
Normal 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" },
|
||||||
|
];
|
||||||
@@ -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 { 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 { AreaChart, Area, BarChart, Bar, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
|
||||||
import { motion, AnimatePresence } from "motion/react";
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
import { StatCard } from "../components/StatCard";
|
import { StatCard } from "../../components/StatCard";
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
593
src/app/pages/common/EntityExtraction.tsx
Normal file
593
src/app/pages/common/EntityExtraction.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 { GitMerge, CheckCircle, XCircle, AlertCircle, RefreshCw, Download, TrendingUp, Activity, Clock, Zap, Target } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
const pendingEntities = [
|
const steelPendingEntities = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
entityA: "磨煤机跳闸",
|
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:45", entityA: "省煤器堵灰", entityB: "省煤器堵塞", action: "已合并", operator: "系统" },
|
||||||
{ time: "10:30", entityA: "空预器漏风", entityB: "空气预热器漏风", action: "已合并", operator: "系统" },
|
{ time: "10:30", entityA: "空预器漏风", entityB: "空气预热器漏风", action: "已合并", operator: "系统" },
|
||||||
{ time: "10:15", entityA: "炉膛负压高", entityB: "炉膛负压低", action: "已拒绝", operator: "人工" },
|
{ time: "10:15", entityA: "炉膛负压高", entityB: "炉膛负压低", action: "已拒绝", operator: "人工" },
|
||||||
{ time: "10:00", 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,
|
pending: 156,
|
||||||
merged: 142,
|
merged: 142,
|
||||||
rejected: 14,
|
rejected: 14,
|
||||||
@@ -73,11 +135,28 @@ const fusionStats = {
|
|||||||
manualReview: 14,
|
manualReview: 14,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const aluminaFusionStats = {
|
||||||
|
pending: 89,
|
||||||
|
merged: 76,
|
||||||
|
rejected: 13,
|
||||||
|
autoMerged: 65,
|
||||||
|
manualReview: 11,
|
||||||
|
};
|
||||||
|
|
||||||
export function KnowledgeFusion() {
|
export function KnowledgeFusion() {
|
||||||
const [selectedEntity, setSelectedEntity] = useState(pendingEntities[1]);
|
const [selectedEntity, setSelectedEntity] = useState(steelPendingEntities[1]);
|
||||||
const [processingId, setProcessingId] = useState<number | null>(null);
|
const [processingId, setProcessingId] = useState<number | null>(null);
|
||||||
const [mergeAnimatingId, setMergeAnimatingId] = useState<number | null>(null);
|
const [mergeAnimatingId, setMergeAnimatingId] = useState<number | null>(null);
|
||||||
const [statsPulse, setStatsPulse] = useState(false);
|
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
|
// Pulse animation for stats when entity changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -134,6 +213,26 @@ export function KnowledgeFusion() {
|
|||||||
<p className="text-sm text-slate-400">基于余弦距离与Jaccard系数的智能实体融合</p>
|
<p className="text-sm text-slate-400">基于余弦距离与Jaccard系数的智能实体融合</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<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
|
<motion.button
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
@@ -176,11 +275,11 @@ export function KnowledgeFusion() {
|
|||||||
{/* Fusion Statistics */}
|
{/* Fusion Statistics */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
<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: currentFusionStats.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: currentFusionStats.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: currentFusionStats.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: currentFusionStats.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.manualReview, color: "from-yellow-500 to-orange-500", icon: AlertCircle },
|
||||||
].map((stat, index) => (
|
].map((stat, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={stat.label}
|
key={stat.label}
|
||||||
@@ -230,7 +329,7 @@ export function KnowledgeFusion() {
|
|||||||
{/* Pending Entities List */}
|
{/* Pending Entities List */}
|
||||||
<div className="lg:col-span-2 space-y-4">
|
<div className="lg:col-span-2 space-y-4">
|
||||||
<h2 className="text-lg font-semibold text-white">待融合实体列表</h2>
|
<h2 className="text-lg font-semibold text-white">待融合实体列表</h2>
|
||||||
{pendingEntities.map((entity, index) => (
|
{currentPendingEntities.map((entity: typeof steelPendingEntities[0], index: number) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={entity.id}
|
key={entity.id}
|
||||||
initial={{ opacity: 0, x: -20 }}
|
initial={{ opacity: 0, x: -20 }}
|
||||||
@@ -537,7 +636,7 @@ export function KnowledgeFusion() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{fusionHistory.map((item, index) => (
|
{currentFusionHistory.map((item: typeof steelFusionHistory[0], index: number) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={index}
|
key={index}
|
||||||
initial={{ opacity: 0, y: 10, x: -10 }}
|
initial={{ opacity: 0, y: 10, x: -10 }}
|
||||||
@@ -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 { Search, Network, ZoomIn, ZoomOut, Maximize2, Plus, Download, Trash2, Edit, Eye } from "lucide-react";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import { toast } from "sonner";
|
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";
|
import * as d3 from "d3";
|
||||||
|
|
||||||
interface GraphNode extends d3.SimulationNodeDatum {
|
interface GraphNode extends d3.SimulationNodeDatum {
|
||||||
id: number;
|
id: number;
|
||||||
label: string;
|
label: string;
|
||||||
type: "fault" | "symptom" | "cause" | "solution" | "prevention";
|
type: string;
|
||||||
description: string;
|
description: string;
|
||||||
connections: number;
|
connections: number;
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ interface GraphEdge extends d3.SimulationLinkDatum<GraphNode> {
|
|||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodeTypeConfig = {
|
const steelNodeTypeConfig = {
|
||||||
fault: { label: "故障", color: "#ef4444", gradient: "from-red-500 to-orange-500" },
|
fault: { label: "故障", color: "#ef4444", gradient: "from-red-500 to-orange-500" },
|
||||||
symptom: { label: "症状", color: "#f59e0b", gradient: "from-yellow-500 to-amber-500" },
|
symptom: { label: "症状", color: "#f59e0b", gradient: "from-yellow-500 to-amber-500" },
|
||||||
cause: { label: "原因", color: "#3b82f6", gradient: "from-blue-500 to-cyan-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" },
|
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[] = [
|
const initialNodes: GraphNode[] = [
|
||||||
{ id: 1, label: "给煤机故障", type: "fault", description: "给煤机无法正常启动或运行中断", connections: 4 },
|
{ id: 1, label: "给煤机故障", type: "fault", description: "给煤机无法正常启动或运行中断", connections: 4 },
|
||||||
{ id: 2, label: "跳闸", type: "symptom", description: "设备意外停机保护动作", connections: 3 },
|
{ id: 2, label: "跳闸", type: "symptom", description: "设备意外停机保护动作", connections: 3 },
|
||||||
@@ -55,13 +63,49 @@ const initialEdges: GraphEdge[] = [
|
|||||||
{ source: 12, target: 7, label: "导致" },
|
{ 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,067" },
|
||||||
{ label: "关系总数", value: "1,249" },
|
{ label: "关系总数", value: "1,249" },
|
||||||
{ label: "故障类型", value: "45种" },
|
{ label: "故障类型", value: "45种" },
|
||||||
{ label: "图谱类型", value: "钢铁" },
|
{ label: "图谱类型", value: "钢铁" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const statsDataAlumina = [
|
||||||
|
{ label: "实体总数", value: "2,458" },
|
||||||
|
{ label: "关系总数", value: "3,102" },
|
||||||
|
{ label: "工序覆盖", value: "6大工序" },
|
||||||
|
{ label: "图谱类型", value: "铝冶炼" },
|
||||||
|
];
|
||||||
|
|
||||||
const queryHistoryData = [
|
const queryHistoryData = [
|
||||||
{ query: "给煤机皮带跑偏", result: "已找到5个相关节点", timestamp: "10:30" },
|
{ query: "给煤机皮带跑偏", result: "已找到5个相关节点", timestamp: "10:30" },
|
||||||
{ query: "过热器故障", result: "已找到3个相关节点", timestamp: "10:25" },
|
{ query: "过热器故障", result: "已找到3个相关节点", timestamp: "10:25" },
|
||||||
@@ -76,12 +120,18 @@ export function KnowledgeGraph() {
|
|||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
const [hoveredNode, setHoveredNode] = useState<number | null>(null);
|
const [hoveredNode, setHoveredNode] = useState<number | null>(null);
|
||||||
const [highlightedNodes, setHighlightedNodes] = useState<Set<number>>(new Set());
|
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 [simulation, setSimulation] = useState<d3.Simulation<any, any> | null>(null);
|
||||||
|
const [industry, setIndustry] = useState<"steel" | "alumina">("steel");
|
||||||
|
|
||||||
const svgRef = useRef<SVGSVGElement>(null);
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(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(() => {
|
const initGraph = useCallback(() => {
|
||||||
if (!svgRef.current || !containerRef.current) return;
|
if (!svgRef.current || !containerRef.current) return;
|
||||||
|
|
||||||
@@ -104,8 +154,9 @@ export function KnowledgeGraph() {
|
|||||||
svg.call(zoom);
|
svg.call(zoom);
|
||||||
|
|
||||||
const defs = svg.append("defs");
|
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")
|
const gradient = defs.append("radialGradient")
|
||||||
.attr("id", `gradient-${type}`)
|
.attr("id", `gradient-${type}`)
|
||||||
.attr("cx", "30%")
|
.attr("cx", "30%")
|
||||||
@@ -122,9 +173,11 @@ export function KnowledgeGraph() {
|
|||||||
.attr("stop-opacity", 0.4);
|
.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 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 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;
|
const targetId = typeof e.target === 'number' ? e.target : (e.target as GraphNode).id;
|
||||||
return filteredNodeIds.has(sourceId) && filteredNodeIds.has(targetId);
|
return filteredNodeIds.has(sourceId) && filteredNodeIds.has(targetId);
|
||||||
@@ -189,7 +242,7 @@ export function KnowledgeGraph() {
|
|||||||
nodeGroup.append("circle")
|
nodeGroup.append("circle")
|
||||||
.attr("r", 40)
|
.attr("r", 40)
|
||||||
.attr("fill", (d: any) => `url(#gradient-${d.type})`)
|
.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-width", 3)
|
||||||
.attr("stroke-opacity", 0.8);
|
.attr("stroke-opacity", 0.8);
|
||||||
|
|
||||||
@@ -253,7 +306,7 @@ export function KnowledgeGraph() {
|
|||||||
.transition()
|
.transition()
|
||||||
.duration(300)
|
.duration(300)
|
||||||
.attr("stroke-width", 4)
|
.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 {
|
} else {
|
||||||
nodeG.select("circle")
|
nodeG.select("circle")
|
||||||
.transition()
|
.transition()
|
||||||
@@ -274,7 +327,7 @@ export function KnowledgeGraph() {
|
|||||||
.transition()
|
.transition()
|
||||||
.duration(300)
|
.duration(300)
|
||||||
.attr("stroke-width", 3)
|
.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);
|
.attr("opacity", 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -294,11 +347,11 @@ export function KnowledgeGraph() {
|
|||||||
|
|
||||||
svg.call(zoom.transform, d3.zoomIdentity.translate(0, 0).scale(1));
|
svg.call(zoom.transform, d3.zoomIdentity.translate(0, 0).scale(1));
|
||||||
|
|
||||||
}, [typeFilter]);
|
}, [typeFilter, industry]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initGraph();
|
initGraph();
|
||||||
}, []);
|
}, [industry, initGraph]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!simulation) return;
|
if (!simulation) return;
|
||||||
@@ -427,7 +480,34 @@ export function KnowledgeGraph() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-white mb-1">工业知识图谱</h1>
|
<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>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<motion.button
|
<motion.button
|
||||||
@@ -452,7 +532,7 @@ export function KnowledgeGraph() {
|
|||||||
>
|
>
|
||||||
<h3 className="font-semibold text-white mb-4">图谱统计概览</h3>
|
<h3 className="font-semibold text-white mb-4">图谱统计概览</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{statsData.map((stat) => (
|
{currentStatsData.map((stat) => (
|
||||||
<div key={stat.label} className="flex justify-between items-center">
|
<div key={stat.label} className="flex justify-between items-center">
|
||||||
<span className="text-sm text-slate-400">{stat.label}</span>
|
<span className="text-sm text-slate-400">{stat.label}</span>
|
||||||
<span className="text-lg font-bold text-white">{stat.value}</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>
|
<h3 className="font-semibold text-white mb-4">节点类型筛选</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{Object.entries(nodeTypeConfig).map(([type, config]) => (
|
{Object.entries(currentNodeTypeConfig).map(([type, config]) => (
|
||||||
<motion.button
|
<motion.button
|
||||||
key={type}
|
key={type}
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
@@ -668,7 +748,7 @@ export function KnowledgeGraph() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-6 py-4 border-t border-slate-800/50 bg-slate-900/30 flex items-center gap-6 flex-wrap">
|
<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
|
<div
|
||||||
key={type}
|
key={type}
|
||||||
className={`flex items-center gap-2 transition-opacity ${
|
className={`flex items-center gap-2 transition-opacity ${
|
||||||
@@ -683,10 +763,10 @@ export function KnowledgeGraph() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="ml-auto text-xs text-slate-500">
|
<div className="ml-auto text-xs text-slate-500">
|
||||||
当前显示 {initialNodes.filter((n) => typeFilter.has(n.type)).length} 个节点 |{" "}
|
当前显示 {currentNodes.filter((n) => typeFilter.has(n.type)).length} 个节点 |{" "}
|
||||||
{initialEdges.filter(
|
{currentEdges.filter(
|
||||||
(e) => typeFilter.has(initialNodes.find((n) => n.id === e.source)?.type || "") &&
|
(e) => typeFilter.has(currentNodes.find((n) => n.id === e.source)?.type || "") &&
|
||||||
typeFilter.has(initialNodes.find((n) => n.id === e.target)?.type || "")
|
typeFilter.has(currentNodes.find((n) => n.id === e.target)?.type || "")
|
||||||
).length} 条关系
|
).length} 条关系
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -709,10 +789,10 @@ export function KnowledgeGraph() {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div
|
<div
|
||||||
className={`inline-flex px-3 py-1 rounded-full bg-gradient-to-br ${
|
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`}
|
} text-white text-sm`}
|
||||||
>
|
>
|
||||||
{nodeTypeConfig[selectedNode.type].label}
|
{currentNodeTypeConfig[selectedNode.type]?.label || selectedNode.type}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-slate-400">
|
<div className="text-sm text-slate-400">
|
||||||
关联节点: {getConnectedNodes(selectedNode.id).length} 个
|
关联节点: {getConnectedNodes(selectedNode.id).length} 个
|
||||||
@@ -4,7 +4,7 @@ import { LineChart, Line, BarChart, Bar, AreaChart, Area, XAxis, YAxis, Cartesia
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
const processStages = [
|
const steelProcessStages = [
|
||||||
{ name: "炼铁", status: "normal", load: 85, temp: 1250 },
|
{ name: "炼铁", status: "normal", load: 85, temp: 1250 },
|
||||||
{ name: "炼钢", status: "normal", load: 92, temp: 1650 },
|
{ name: "炼钢", status: "normal", load: 92, temp: 1650 },
|
||||||
{ name: "连铸", status: "normal", load: 88, temp: 1520 },
|
{ name: "连铸", status: "normal", load: 88, temp: 1520 },
|
||||||
@@ -12,7 +12,15 @@ const processStages = [
|
|||||||
{ name: "冷轧", status: "normal", load: 78, temp: 850 },
|
{ 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: "08:00", t1: 1245, t2: 1648, t3: 1518 },
|
||||||
{ time: "10:00", t1: 1248, t2: 1650, t3: 1520 },
|
{ time: "10:00", t1: 1248, t2: 1650, t3: 1520 },
|
||||||
{ time: "12:00", t1: 1250, t2: 1652, t3: 1522 },
|
{ time: "12:00", t1: 1250, t2: 1652, t3: 1522 },
|
||||||
@@ -20,7 +28,15 @@ const temperatureTrend = [
|
|||||||
{ time: "16:00", t1: 1250, t2: 1650, t3: 1520 },
|
{ 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: "00", value: 820 },
|
||||||
{ hour: "04", value: 780 },
|
{ hour: "04", value: 780 },
|
||||||
{ hour: "08", value: 950 },
|
{ hour: "08", value: 950 },
|
||||||
@@ -29,14 +45,30 @@ const productionData = [
|
|||||||
{ hour: "20", value: 890 },
|
{ 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: 1, type: "warning", message: "高炉1#冷却水温差偏高", time: "10:35", handled: false },
|
||||||
{ id: 2, type: "info", message: "连铸2#定期维护提醒", time: "10:20", 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: 3, type: "success", message: "炼钢3#工艺优化已完成", time: "10:15", handled: true },
|
||||||
{ id: 4, type: "warning", message: "热轧设备温度波动", time: "10:05", 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: "高炉 #1", status: "running", efficiency: 94, uptime: "99.2%" },
|
||||||
{ name: "高炉 #2", status: "running", efficiency: 92, uptime: "98.8%" },
|
{ name: "高炉 #2", status: "running", efficiency: 92, uptime: "98.8%" },
|
||||||
{ name: "转炉 #1", status: "running", efficiency: 96, uptime: "99.5%" },
|
{ name: "转炉 #1", status: "running", efficiency: 96, uptime: "99.5%" },
|
||||||
@@ -45,11 +77,34 @@ const equipmentStatus = [
|
|||||||
{ name: "连铸 #2", status: "running", efficiency: 93, uptime: "99.1%" },
|
{ 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() {
|
export function MonitoringCenter() {
|
||||||
const [currentTime, setCurrentTime] = useState(new Date());
|
const [currentTime, setCurrentTime] = useState(new Date());
|
||||||
const [alertsList, setAlertsList] = useState(alerts);
|
const [alertsList, setAlertsList] = useState(steelAlerts);
|
||||||
const [processData, setProcessData] = useState(processStages);
|
const [processData, setProcessData] = useState(steelProcessStages);
|
||||||
const [tempTrendData, setTempTrendData] = useState(temperatureTrend);
|
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
|
// Update current time
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -119,6 +174,28 @@ export function MonitoringCenter() {
|
|||||||
<h1 className="text-2xl font-bold text-white mb-1">可视化监控中心</h1>
|
<h1 className="text-2xl font-bold text-white mb-1">可视化监控中心</h1>
|
||||||
<p className="text-sm text-slate-400">全厂实时状态监控与数据分析</p>
|
<p className="text-sm text-slate-400">全厂实时状态监控与数据分析</p>
|
||||||
</div>
|
</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">
|
<div className="flex items-center gap-3">
|
||||||
<motion.button
|
<motion.button
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
@@ -387,7 +464,7 @@ export function MonitoringCenter() {
|
|||||||
<span className="text-sm text-slate-400">单位: 吨/小时</span>
|
<span className="text-sm text-slate-400">单位: 吨/小时</span>
|
||||||
</div>
|
</div>
|
||||||
<ResponsiveContainer width="100%" height={200}>
|
<ResponsiveContainer width="100%" height={200}>
|
||||||
<BarChart data={productionData}>
|
<BarChart data={currentProductionData}>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#334155" opacity={0.3} />
|
<CartesianGrid strokeDasharray="3 3" stroke="#334155" opacity={0.3} />
|
||||||
<XAxis dataKey="hour" stroke="#94a3b8" fontSize={12} />
|
<XAxis dataKey="hour" stroke="#94a3b8" fontSize={12} />
|
||||||
<YAxis 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>
|
<h2 className="text-lg font-semibold text-white mb-4">设备状态</h2>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{equipmentStatus.map((equipment, index) => (
|
{currentEquipmentStatus.map((equipment: typeof steelEquipmentStatus[0], index: number) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="p-3 rounded-lg bg-slate-800/50 border border-slate-700/50"
|
className="p-3 rounded-lg bg-slate-800/50 border border-slate-700/50"
|
||||||
@@ -3,7 +3,7 @@ import { motion, AnimatePresence } from "motion/react";
|
|||||||
import { RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar, ResponsiveContainer } from "recharts";
|
import { RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar, ResponsiveContainer } from "recharts";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Progress } from "../components/ui/progress";
|
import { Progress } from "../../components/ui/progress";
|
||||||
|
|
||||||
const currentParams = [
|
const currentParams = [
|
||||||
{ label: "铝硅比 A/S", value: "8.5", unit: "" },
|
{ label: "铝硅比 A/S", value: "8.5", unit: "" },
|
||||||
@@ -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 { Sparkles, Save, Play, Download, Copy, Plus, Code2, FileText, Hash, Zap, RefreshCw, Check } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
const templates = [
|
const steelTemplates = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "高炉故障诊断标准Prompt",
|
name: "高炉故障诊断标准Prompt",
|
||||||
category: "故障诊断",
|
category: "故障诊断",
|
||||||
|
industry: "steel",
|
||||||
description: "用于高炉故障诊断的标准提示词模板",
|
description: "用于高炉故障诊断的标准提示词模板",
|
||||||
content: `你是一位钢铁行业的资深工程师,擅长高炉故障诊断。
|
content: `你是一位钢铁行业的资深工程师,擅长高炉故障诊断。
|
||||||
|
|
||||||
@@ -29,9 +30,61 @@ const templates = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
name: "工艺优化建议Prompt",
|
name: "转炉炼钢优化Prompt",
|
||||||
category: "工艺优化",
|
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: `你是有色冶金领域的工艺专家,专注于氧化铝生产优化。
|
content: `你是有色冶金领域的工艺专家,专注于氧化铝生产优化。
|
||||||
|
|
||||||
**当前工况:**
|
**当前工况:**
|
||||||
@@ -52,26 +105,49 @@ const templates = [
|
|||||||
usage: 892,
|
usage: 892,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 5,
|
||||||
name: "质量分析标准Prompt",
|
name: "电解槽控制Prompt",
|
||||||
category: "质量分析",
|
category: "故障诊断",
|
||||||
description: "产品质量异常分析模板",
|
industry: "alumina",
|
||||||
content: `你是质量管理专家,负责钢铁产品质量溯源分析。
|
description: "电解槽运行状态监控与故障诊断",
|
||||||
|
content: `你是电解铝工艺专家,精通电解槽运行控制。
|
||||||
|
|
||||||
**产品信息:**
|
**实时数据:**
|
||||||
- 批号:{batch_number}
|
- 槽电压:{cell_voltage}
|
||||||
- 质量异常:{quality_issue}
|
- 系列电流:{series_current}
|
||||||
- 发现工序:{discovery_stage}
|
- 电解质温度:{electrolyte_temp}
|
||||||
|
- 分子比:{molecular_ratio}
|
||||||
|
|
||||||
**分析任务:**
|
**诊断要求:**
|
||||||
1. 溯源可能的问题工序
|
1. 分析当前电解状态
|
||||||
2. 分析根本原因
|
2. 识别异常参数
|
||||||
3. 提供改进措施
|
3. 给出调整建议
|
||||||
4. 给出预防建议
|
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"],
|
- 设备状态:{equipment_status}
|
||||||
usage: 567,
|
- 工艺参数:{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() {
|
export function PromptEngineering() {
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState(templates[0]);
|
const [selectedTemplate, setSelectedTemplate] = useState(steelTemplates[0]);
|
||||||
const [editedContent, setEditedContent] = useState(selectedTemplate.content);
|
const [editedContent, setEditedContent] = useState(selectedTemplate.content);
|
||||||
const [selectedCategory, setSelectedCategory] = useState("全部");
|
const [selectedCategory, setSelectedCategory] = useState("全部");
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
@@ -136,10 +212,13 @@ export function PromptEngineering() {
|
|||||||
const [isTestRunning, setIsTestRunning] = useState(false);
|
const [isTestRunning, setIsTestRunning] = useState(false);
|
||||||
const [testResult, setTestResult] = useState<string | null>(null);
|
const [testResult, setTestResult] = useState<string | null>(null);
|
||||||
const [testProgress, setTestProgress] = useState(0);
|
const [testProgress, setTestProgress] = useState(0);
|
||||||
|
const [industry, setIndustry] = useState<"steel" | "alumina">("steel");
|
||||||
|
|
||||||
|
const currentTemplates = industry === "steel" ? steelTemplates : aluminaTemplates;
|
||||||
|
|
||||||
const filteredTemplates = selectedCategory === "全部"
|
const filteredTemplates = selectedCategory === "全部"
|
||||||
? templates
|
? currentTemplates
|
||||||
: templates.filter(t => t.category === selectedCategory);
|
: currentTemplates.filter((t: typeof steelTemplates[0]) => t.category === selectedCategory);
|
||||||
|
|
||||||
// Typing effect for template preview
|
// Typing effect for template preview
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -159,6 +238,15 @@ export function PromptEngineering() {
|
|||||||
}
|
}
|
||||||
}, [selectedTemplate.id, isEditing]);
|
}, [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
|
// Simulate test running process
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isTestRunning) {
|
if (isTestRunning) {
|
||||||
@@ -220,6 +308,30 @@ export function PromptEngineering() {
|
|||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</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 */}
|
{/* Category Filter */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
@@ -3,7 +3,7 @@ import { Search, CheckCircle, AlertTriangle, ArrowRight, FileText, BarChart3, Tr
|
|||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
const processSteps = [
|
const steelProcessSteps = [
|
||||||
{ name: "炼铁", status: "normal", icon: "🔥", temp: 1450, efficiency: 98 },
|
{ name: "炼铁", status: "normal", icon: "🔥", temp: 1450, efficiency: 98 },
|
||||||
{ name: "炼钢", status: "normal", icon: "⚙️", temp: 1650, efficiency: 97 },
|
{ name: "炼钢", status: "normal", icon: "⚙️", temp: 1650, efficiency: 97 },
|
||||||
{ name: "连铸", status: "warning", icon: "💧", temp: 1520, efficiency: 85 },
|
{ name: "连铸", status: "warning", icon: "💧", temp: 1520, efficiency: 85 },
|
||||||
@@ -11,6 +11,56 @@ const processSteps = [
|
|||||||
{ name: "冷轧", status: "normal", icon: "❄️", temp: 20, efficiency: 99 },
|
{ 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() {
|
export function QualityTracing() {
|
||||||
const [batchNumber, setBatchNumber] = useState("PL20260403001");
|
const [batchNumber, setBatchNumber] = useState("PL20260403001");
|
||||||
const [showResult, setShowResult] = useState(true);
|
const [showResult, setShowResult] = useState(true);
|
||||||
@@ -18,8 +68,12 @@ export function QualityTracing() {
|
|||||||
const [activeStep, setActiveStep] = useState<number | null>(null);
|
const [activeStep, setActiveStep] = useState<number | null>(null);
|
||||||
const [flowProgress, setFlowProgress] = useState(0);
|
const [flowProgress, setFlowProgress] = useState(0);
|
||||||
const [particles, setParticles] = useState<Array<{id: number, x: number, y: number, delay: number}>>([]);
|
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 svgRef = useRef<SVGSVGElement>(null);
|
||||||
|
|
||||||
|
const currentProcessSteps = industry === "steel" ? steelProcessSteps : aluminaProcessSteps;
|
||||||
|
const currentTracingResults = industry === "steel" ? steelTracingResults : aluminaTracingResults;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showResult) return;
|
if (!showResult) return;
|
||||||
|
|
||||||
@@ -68,6 +122,28 @@ export function QualityTracing() {
|
|||||||
<h1 className="text-2xl font-bold text-white mb-1">质量溯源分析智能体</h1>
|
<h1 className="text-2xl font-bold text-white mb-1">质量溯源分析智能体</h1>
|
||||||
<p className="text-sm text-slate-400">全流程产品质量追溯与分析</p>
|
<p className="text-sm text-slate-400">全流程产品质量追溯与分析</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Search Section */}
|
{/* Search Section */}
|
||||||
@@ -124,73 +200,59 @@ export function QualityTracing() {
|
|||||||
<h2 className="font-semibold text-white mb-4">溯源结果</h2>
|
<h2 className="font-semibold text-white mb-4">溯源结果</h2>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* First Warning */}
|
{currentTracingResults.map((result, index) => (
|
||||||
<div className="p-6 rounded-xl bg-gradient-to-br from-yellow-900/20 to-orange-900/20 border border-yellow-500/30">
|
<div
|
||||||
<div className="flex items-start gap-4">
|
key={index}
|
||||||
<div className="p-3 rounded-full bg-yellow-500/20">
|
className={`p-6 rounded-xl border ${
|
||||||
<AlertTriangle className="w-6 h-6 text-yellow-400" />
|
result.type === "warning"
|
||||||
</div>
|
? "bg-gradient-to-br from-yellow-900/20 to-orange-900/20 border-yellow-500/30"
|
||||||
<div className="flex-1">
|
: "bg-gradient-to-br from-red-900/20 to-orange-900/20 border-red-500/30"
|
||||||
<h3 className="text-lg font-semibold text-yellow-400 mb-2">
|
}`}
|
||||||
⚠️ 发现质量异常 - 抗拉强度偏低
|
>
|
||||||
</h3>
|
<div className="flex items-start gap-4">
|
||||||
<div className="space-y-2 text-sm text-slate-300">
|
<div className={`p-3 rounded-full ${
|
||||||
<div className="flex items-center gap-2">
|
result.type === "warning" ? "bg-yellow-500/20" : "bg-red-500/20"
|
||||||
<span className="text-slate-400">产品批号:</span>
|
}`}>
|
||||||
<span className="font-medium text-white">{batchNumber}</span>
|
{result.type === "warning" ? (
|
||||||
</div>
|
<AlertTriangle className="w-6 h-6 text-yellow-400" />
|
||||||
<div className="flex items-center gap-2">
|
) : (
|
||||||
<span className="text-slate-400">检测时间:</span>
|
<TrendingDown className="w-6 h-6 text-red-400" />
|
||||||
<span>2026-04-03 14:30</span>
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex-1">
|
||||||
<span className="text-slate-400">异常指标:</span>
|
<h3 className={`text-lg font-semibold mb-2 ${
|
||||||
<span className="text-yellow-400">抗拉强度 458 MPa (标准值: ≥480 MPa)</span>
|
result.type === "warning" ? "text-yellow-400" : "text-red-400"
|
||||||
</div>
|
}`}>
|
||||||
<div className="flex items-center gap-2">
|
{result.title}
|
||||||
<span className="text-slate-400">超标程度:</span>
|
</h3>
|
||||||
<span className="text-red-400">偏低 4.6%</span>
|
<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">{result.batchNumber}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-slate-400">检测时间:</span>
|
||||||
|
<span>{result.time}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-slate-400">异常指标:</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">{result.deviation}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-slate-400">可能原因:</span>
|
||||||
|
<span className="text-orange-400">{result.reason}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
|
||||||
<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-red-400">延伸率 18.2% (标准值: ≥22%)</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-slate-400">超标程度:</span>
|
|
||||||
<span className="text-red-400">偏低 17.3%</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-slate-400">可能原因:</span>
|
|
||||||
<span className="text-orange-400">连铸段冷却速度过快</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -269,7 +331,7 @@ export function QualityTracing() {
|
|||||||
|
|
||||||
{/* Process Steps */}
|
{/* Process Steps */}
|
||||||
<div className="relative flex justify-between items-start">
|
<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 colors = getStepColor(step.status);
|
||||||
const isActive = activeStep === index;
|
const isActive = activeStep === index;
|
||||||
const isAnimated = animatedPath.includes(index);
|
const isAnimated = animatedPath.includes(index);
|
||||||
@@ -389,11 +451,11 @@ export function QualityTracing() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Connection Line */}
|
{/* Connection Line */}
|
||||||
{index < processSteps.length - 1 && (
|
{index < currentProcessSteps.length - 1 && (
|
||||||
<div className="absolute top-14 left-1/2 w-full h-1">
|
<div className="absolute top-14 left-1/2 w-full h-1">
|
||||||
<svg className="w-full h-full" preserveAspectRatio="none">
|
<svg className="w-full h-full" preserveAspectRatio="none">
|
||||||
<motion.path
|
<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"
|
fill="none"
|
||||||
stroke="url(#flowGradient)"
|
stroke="url(#flowGradient)"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
@@ -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 { Search, Database, FileText, Sparkles, TrendingUp, CheckCircle, Loader2, Brain, Network, Layers, ChevronRight } from "lucide-react";
|
||||||
import { motion, AnimatePresence } from "motion/react";
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Progress } from "../components/ui/progress";
|
import { Progress } from "../../components/ui/progress";
|
||||||
|
|
||||||
const searchStrategies = [
|
const searchStrategies = [
|
||||||
{ value: "bm25", label: "BM25" },
|
{ value: "bm25", label: "BM25" },
|
||||||
@@ -16,7 +16,7 @@ const knowledgeBases = [
|
|||||||
{ value: "nonferrous", label: "有色冶金" },
|
{ value: "nonferrous", label: "有色冶金" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const retrievalResults = [
|
const steelRetrievalResults = [
|
||||||
{
|
{
|
||||||
relevance: 0.92,
|
relevance: 0.92,
|
||||||
source: "高炉故障知识图谱",
|
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() {
|
export function RAGSystem() {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [strategy, setStrategy] = useState("hybrid");
|
const [strategy, setStrategy] = useState("hybrid");
|
||||||
const [knowledgeBase, setKnowledgeBase] = useState("all");
|
const [industry, setIndustry] = useState<"steel" | "alumina">("steel");
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
const [showResults, setShowResults] = useState(false);
|
const [showResults, setShowResults] = useState(false);
|
||||||
const [generatedAnswer, setGeneratedAnswer] = useState("");
|
const [generatedAnswer, setGeneratedAnswer] = useState("");
|
||||||
const [currentResults, setCurrentResults] = useState(retrievalResults);
|
const [currentResults, setCurrentResults] = useState(steelRetrievalResults);
|
||||||
const [searchProgress, setSearchProgress] = useState(0);
|
const [searchProgress, setSearchProgress] = useState(0);
|
||||||
const [currentPhase, setCurrentPhase] = useState<"query" | "retrieval" | "rerank" | "generate">("query");
|
const [currentPhase, setCurrentPhase] = useState<"query" | "retrieval" | "rerank" | "generate">("query");
|
||||||
const [typingText, setTypingText] = useState("");
|
const [typingText, setTypingText] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (industry === "steel") {
|
||||||
|
setCurrentResults(steelRetrievalResults);
|
||||||
|
} else {
|
||||||
|
setCurrentResults(aluminaRetrievalResults);
|
||||||
|
}
|
||||||
|
}, [industry]);
|
||||||
|
|
||||||
// Typing effect for generated answer
|
// Typing effect for generated answer
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (generatedAnswer && showResults) {
|
if (generatedAnswer && showResults) {
|
||||||
@@ -106,18 +144,15 @@ export function RAGSystem() {
|
|||||||
setCurrentPhase("generate");
|
setCurrentPhase("generate");
|
||||||
|
|
||||||
// Generate mock answer based on query
|
// Generate mock answer based on query
|
||||||
const answers = [
|
const answers = industry === "steel" ? steelAnswers : aluminaAnswers;
|
||||||
"目前炉况基本顺行,但请注意冷却壁水温差有上升趋势(当前+3.5°C),建议检查冷却水压力。根据历史数据分析和相关案例,水温差持续上升可能预示冷却系统存在堵塞风险,应立即进行检查。建议参考操作手册中的维护规程,进行系统性排查。",
|
|
||||||
"根据检索到的知识库信息,该问题涉及到多个方面。首先从实时监控数据来看,系统运行基本正常。其次结合历史案例分析,建议关注关键参数的变化趋势,及时采取预防措施。",
|
|
||||||
"基于RAG增强检索,我为您找到了相关的解决方案和案例。建议结合当前实际情况,参考历史经验进行处理。同时建议定期维护相关设备,降低故障发生概率。"
|
|
||||||
];
|
|
||||||
|
|
||||||
setGeneratedAnswer(answers[Math.floor(Math.random() * answers.length)]);
|
setGeneratedAnswer(answers[Math.floor(Math.random() * answers.length)]);
|
||||||
setShowResults(true);
|
setShowResults(true);
|
||||||
setIsSearching(false);
|
setIsSearching(false);
|
||||||
|
|
||||||
// Randomize result relevance
|
// Randomize result relevance
|
||||||
setCurrentResults(retrievalResults.map(r => ({
|
const baseResults = industry === "steel" ? steelRetrievalResults : aluminaRetrievalResults;
|
||||||
|
setCurrentResults(baseResults.map((r: typeof steelRetrievalResults[0]) => ({
|
||||||
...r,
|
...r,
|
||||||
relevance: 0.75 + Math.random() * 0.2
|
relevance: 0.75 + Math.random() * 0.2
|
||||||
})));
|
})));
|
||||||
@@ -211,19 +246,36 @@ export function RAGSystem() {
|
|||||||
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<label className="text-sm text-slate-400 mb-2 block">知识库范围</label>
|
<label className="text-sm text-slate-400 mb-2 block">知识库范围</label>
|
||||||
<select
|
<div className="flex gap-2">
|
||||||
value={knowledgeBase}
|
<button
|
||||||
onChange={(e) => {
|
onClick={() => {
|
||||||
setKnowledgeBase(e.target.value);
|
setIndustry("steel");
|
||||||
toast.info(`已切换到 ${knowledgeBases.find(k => k.value === e.target.value)?.label}`, { duration: 1500 });
|
toast.info("已切换到钢铁行业知识库", { duration: 1500 });
|
||||||
}}
|
}}
|
||||||
disabled={isSearching}
|
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"
|
||||||
{knowledgeBases.map(k => (
|
? "bg-orange-500/20 text-orange-400 border border-orange-500/50"
|
||||||
<option key={k.value} value={k.value}>{k.label}</option>
|
: "bg-slate-800/50 text-slate-400 border border-slate-700/50 hover:bg-slate-800"
|
||||||
))}
|
} disabled:opacity-50`}
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ export function FurnaceDiagnosis() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-white mb-1 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-white mb-1 flex items-center gap-2">
|
||||||
高炉故障诊断智能体
|
高炉故障诊断智能体(钢铁)
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{ rotate: 360 }}
|
animate={{ rotate: 360 }}
|
||||||
transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
|
transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
|
||||||
@@ -68,7 +68,7 @@ export function IntelligentConverter() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-white mb-1 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-white mb-1 flex items-center gap-2">
|
||||||
智能转炉控制模型
|
智能转炉控制模型(钢铁)
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{ scale: [1, 1.3, 1], rotate: [0, 180, 360] }}
|
animate={{ scale: [1, 1.3, 1], rotate: [0, 180, 360] }}
|
||||||
transition={{ duration: 3, repeat: Infinity }}
|
transition={{ duration: 3, repeat: Infinity }}
|
||||||
865
src/app/pages/steel/SteelDashboard.tsx
Normal file
865
src/app/pages/steel/SteelDashboard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
72
src/app/pages/steel/devices/types.ts
Normal file
72
src/app/pages/steel/devices/types.ts
Normal 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" },
|
||||||
|
];
|
||||||
@@ -1,24 +1,25 @@
|
|||||||
import { createBrowserRouter } from "react-router";
|
import { createBrowserRouter } from "react-router";
|
||||||
import { RootLayout } from "./components/RootLayout";
|
import { RootLayout } from "./components/RootLayout";
|
||||||
import { Dashboard } from "./pages/Dashboard";
|
import { Dashboard } from "./pages/common/Dashboard";
|
||||||
import { FurnaceDiagnosis } from "./pages/FurnaceDiagnosis";
|
import { FurnaceDiagnosis } from "./pages/steel/FurnaceDiagnosis";
|
||||||
import { KnowledgeGraph } from "./pages/KnowledgeGraph";
|
import { KnowledgeGraph } from "./pages/common/KnowledgeGraph";
|
||||||
import { RAGSystem } from "./pages/RAGSystem";
|
import { RAGSystem } from "./pages/common/RAGSystem";
|
||||||
import { MonitoringCenter } from "./pages/MonitoringCenter";
|
import { MonitoringCenter } from "./pages/common/MonitoringCenter";
|
||||||
import { QualityTracing } from "./pages/QualityTracing";
|
import { QualityTracing } from "./pages/common/QualityTracing";
|
||||||
import { ProcessOptimization } from "./pages/ProcessOptimization";
|
import { ProcessOptimization } from "./pages/common/ProcessOptimization";
|
||||||
import { ModelManagement } from "./pages/ModelManagement";
|
import { ModelManagement } from "./pages/common/ModelManagement";
|
||||||
import { DigitalTwin } from "./pages/DigitalTwin";
|
import { DigitalTwin } from "./pages/common/DigitalTwin";
|
||||||
import { DataCollection } from "./pages/DataCollection";
|
import { DataCollection } from "./pages/common/DataCollection";
|
||||||
import { DataGovernanceStandard } from "./pages/DataGovernanceStandard";
|
import { DataGovernanceStandard } from "./pages/common/DataGovernanceStandard";
|
||||||
import { PromptEngineering } from "./pages/PromptEngineering";
|
import { PromptEngineering } from "./pages/common/PromptEngineering";
|
||||||
import { HumanMachineCollaboration } from "./pages/HumanMachineCollaboration";
|
import { HumanMachineCollaboration } from "./pages/alumina/HumanMachineCollaboration";
|
||||||
import { IntelligentConverter } from "./pages/IntelligentConverter";
|
import { IntelligentConverter } from "./pages/steel/IntelligentConverter";
|
||||||
import { EconomicAnalysis } from "./pages/EconomicAnalysis";
|
import { EconomicAnalysis } from "./pages/common/EconomicAnalysis";
|
||||||
import { KnowledgeFusion } from "./pages/KnowledgeFusion";
|
import { KnowledgeFusion } from "./pages/common/KnowledgeFusion";
|
||||||
import { EntityExtraction } from "./pages/EntityExtraction";
|
import { EntityExtraction } from "./pages/common/EntityExtraction";
|
||||||
import { EdgeCloudSync } from "./pages/EdgeCloudSync";
|
import { EdgeCloudSync } from "./pages/common/EdgeCloudSync";
|
||||||
import { IndustrialDashboard } from "./pages/IndustrialDashboard";
|
import AluminaDashboard from "./pages/alumina/AluminaDashboard";
|
||||||
|
import SteelDashboard from "./pages/steel/SteelDashboard";
|
||||||
|
|
||||||
export const router = createBrowserRouter([
|
export const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@@ -43,7 +44,8 @@ export const router = createBrowserRouter([
|
|||||||
{ path: "human-machine", Component: HumanMachineCollaboration },
|
{ path: "human-machine", Component: HumanMachineCollaboration },
|
||||||
{ path: "intelligent-converter", Component: IntelligentConverter },
|
{ path: "intelligent-converter", Component: IntelligentConverter },
|
||||||
{ path: "economic-analysis", Component: EconomicAnalysis },
|
{ path: "economic-analysis", Component: EconomicAnalysis },
|
||||||
{ path: "industrial-dashboard", Component: IndustrialDashboard },
|
{ path: "alumina-dashboard", Component: AluminaDashboard },
|
||||||
|
{ path: "steel-dashboard", Component: SteelDashboard },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user