开放式PLC编程环境
这是一个开放式PLC编程环境的自定义组件,提供了一个面向智能仓储堆垛机控制的开放式PLC编程环境。该组件采用苹果科技风格设计,支持多厂商PLC硬件,具有直观的界面和丰富的功能。
功能特点
- 多语言编程支持:梯形图(LD)、结构化文本(ST)和功能块图(FBD)三种PLC编程语言
- 多厂商硬件兼容:支持西门子、AB、三菱、欧姆龙和施耐德等主流PLC厂商
- 苹果科技风格界面:简洁美观的UI设计,符合现代工业审美
- 专业编程工具:集成Monaco编辑器,提供代码高亮、自动完成和错误检查等功能
- 实时变量监控:在线查看和跟踪PLC变量状态变化
- 堆垛机可视化:直观展示堆垛机运行状态和位置信息
- 告警管理系统:实时显示系统告警信息和处理状态
- 自适应布局:响应式设计,适应不同屏幕尺寸
- 动态交互效果:流畅的动画效果,提升用户体验
界面区域说明
组件包含以下主要功能区域:
- 顶部工具栏:包含编程语言选择、PLC厂商选择和文件操作选项
- 编程区域:
- 梯形图编辑器:可视化梯形图编程环境
- 文本编辑器:用于ST语言和FBD编程
- 标签页管理:多程序文件的标签页切换
- 变量监控区:
- 变量列表:显示和筛选当前PLC变量
- 实时值更新:动态显示变量的当前值
- 堆垛机状态区:
- 可视化动画:显示堆垛机位置和动作
- 实时指标:当前速度、位置和载重等关键参数
- 告警信息区:
- 告警显示:实时系统告警和错误信息
- 告警处理:告警确认和处理功能
连接实际硬件
要将组件连接到实际的PLC硬件,请按照以下步骤操作:
- 点击顶部菜单按钮,打开硬件配置对话框
- 选择相应的PLC型号和通信参数
- 配置I/O模块和通信地址
- 保存配置后重新连接
组件默认使用模拟数据。要连接实际硬件,需要修改script.js
中的initVariableSimulation
函数,实现与实际PLC的通信。
编程示例
组件内置了几个堆垛机控制的编程示例:
- 主程序:主控制循环和基本功能
- 子程序1:堆垛机位置控制逻辑
- 配置:系统参数和硬件配置
这些示例可以作为开发实际应用程序的起点。
自定义选项
您可以通过修改组件代码来自定义以下内容:
- 界面主题:在CSS中修改颜色变量,调整组件的视觉风格
- 编程功能:添加新的编程工具或语言支持
- 变量监控:调整变量的显示方式和更新频率
- 堆垛机动画:根据实际设备特性调整可视化效果
- 告警规则:定制告警触发条件和处理流程
浏览器兼容性
该组件使用了现代Web技术,建议在以下浏览器版本中使用:
- Chrome 60+
- Firefox 55+
- Safari 11+
- Edge 16+
注意事项
- 组件默认使用模拟数据,实际应用时需要连接到真实的PLC数据源
- 为保证最佳性能,请在实际部署环境中优化数据刷新频率
- 使用前请确认所选PLC厂商的通信驱动是否可用
项目结构
效果展示
源码
index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开放式PLC编程环境</title>
<link rel="stylesheet" href="styles.css">
<!-- 添加Monaco编辑器 -->
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/loader.js"></script>
</head>
<body>
<div id="plc-programming-environment">
<header class="ppe-header">
<div class="ppe-logo">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 7V17C21 19.2091 19.2091 21 17 21H7C4.79086 21 3 19.2091 3 17V7C3 4.79086 4.79086 3 7 3H17C19.2091 3 21 4.79086 21 7Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<path d="M7 9H10M7 12H13M7 15H10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<path d="M15 9L17 9M15 12L17 12M15 15L17 15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<span>智能仓储堆垛机控制</span>
</div>
<div class="ppe-actions">
<button class="ppe-btn ppe-btn-primary" id="deploy-btn">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14"></path>
<path d="m12 5 7 7-7 7"></path>
</svg>
部署程序
</button>
<button class="ppe-btn" id="menu-btn">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 18H21M3 12H21M3 6H21" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
菜单
</button>
</div>
</header>
<div class="ppe-content">
<!-- 工具栏 -->
<div class="ppe-toolbar">
<div class="ppe-toolbar-group">
<button class="ppe-tool-btn active" data-mode="ladder">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 5v14M20 5v14M4 12h16M8 5v7M16 12v7"></path>
</svg>
梯形图
</button>
<button class="ppe-tool-btn" data-mode="st">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 3a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3H6a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3V6a3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3h12a3 3 0 0 0 3-3 3 3 0 0 0-3-3z"></path>
</svg>
ST语言
</button>
<button class="ppe-tool-btn" data-mode="fbd">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="8" width="6" height="8" rx="2"></rect>
<rect x="15" y="8" width="6" height="8" rx="2"></rect>
<path d="M9 12h6"></path>
</svg>
功能块
</button>
</div>
<div class="ppe-toolbar-group">
<select id="plc-vendor" class="ppe-select">
<option value="siemens">西门子S7</option>
<option value="ab">AB CompactLogix</option>
<option value="mitsubishi">三菱FX5U</option>
<option value="omron">欧姆龙NX</option>
<option value="schneider">施耐德M340</option>
</select>
</div>
<div class="ppe-toolbar-group">
<button class="ppe-tool-btn" id="save-btn">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
<polyline points="17 21 17 13 7 13 7 21"></polyline>
<polyline points="7 3 7 8 15 8"></polyline>
</svg>
保存
</button>
<button class="ppe-tool-btn" id="download-btn">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
下载
</button>
</div>
</div>
<!-- 主面板区域 -->
<div class="ppe-main-panels">
<!-- 编程区域 -->
<div class="ppe-panel ppe-editor-panel">
<div class="ppe-panel-header">
<h2>程序编辑区</h2>
<div class="ppe-panel-actions">
<button class="ppe-btn ppe-btn-sm" id="verify-btn">验证</button>
<button class="ppe-btn ppe-btn-sm" id="format-btn">格式化</button>
</div>
</div>
<div class="ppe-panel-body">
<div class="ppe-tabs">
<div class="ppe-tab active" data-tab="main">主程序</div>
<div class="ppe-tab" data-tab="subr1">子程序1</div>
<div class="ppe-tab" data-tab="config">配置</div>
<div class="ppe-tab-add">+</div>
</div>
<div class="ppe-editor-container">
<div id="monaco-editor" class="ppe-editor-area"></div>
<div class="ppe-ladder-editor" style="display: none;">
<div class="ppe-ladder-toolbar">
<button class="ppe-ladder-btn" data-element="contact-no">常开触点</button>
<button class="ppe-ladder-btn" data-element="contact-nc">常闭触点</button>
<button class="ppe-ladder-btn" data-element="coil">线圈</button>
<button class="ppe-ladder-btn" data-element="timer">定时器</button>
<button class="ppe-ladder-btn" data-element="counter">计数器</button>
</div>
<div class="ppe-ladder-grid" id="ladder-grid">
<!-- 梯形图网格区域 -->
</div>
</div>
</div>
</div>
</div>
<!-- 右侧面板 -->
<div class="ppe-side-panels">
<!-- 变量监控面板 -->
<div class="ppe-panel ppe-variables-panel">
<div class="ppe-panel-header">
<h2>变量监控</h2>
<div class="ppe-panel-actions">
<button class="ppe-btn ppe-btn-sm" id="refresh-vars-btn">刷新</button>
</div>
</div>
<div class="ppe-panel-body">
<div class="ppe-search-bar">
<input type="text" placeholder="搜索变量..." class="ppe-search-input">
</div>
<div class="ppe-variables-list">
<div class="ppe-variable-item">
<div class="ppe-variable-name">S_StartButton</div>
<div class="ppe-variable-type">BOOL</div>
<div class="ppe-variable-value">TRUE</div>
</div>
<div class="ppe-variable-item">
<div class="ppe-variable-name">S_StopButton</div>
<div class="ppe-variable-type">BOOL</div>
<div class="ppe-variable-value">FALSE</div>
</div>
<div class="ppe-variable-item">
<div class="ppe-variable-name">Position_X</div>
<div class="ppe-variable-type">REAL</div>
<div class="ppe-variable-value">145.32</div>
</div>
<div class="ppe-variable-item">
<div class="ppe-variable-name">Position_Y</div>
<div class="ppe-variable-type">REAL</div>
<div class="ppe-variable-value">87.65</div>
</div>
<div class="ppe-variable-item">
<div class="ppe-variable-name">Position_Z</div>
<div class="ppe-variable-type">REAL</div>
<div class="ppe-variable-value">22.41</div>
</div>
<div class="ppe-variable-item">
<div class="ppe-variable-name">Speed_X</div>
<div class="ppe-variable-type">REAL</div>
<div class="ppe-variable-value">0.75</div>
</div>
<div class="ppe-variable-item">
<div class="ppe-variable-name">CurrentShelf</div>
<div class="ppe-variable-type">INT</div>
<div class="ppe-variable-value">12</div>
</div>
<div class="ppe-variable-item">
<div class="ppe-variable-name">TargetShelf</div>
<div class="ppe-variable-type">INT</div>
<div class="ppe-variable-value">18</div>
</div>
</div>
</div>
</div>
<!-- 堆垛机监控面板 -->
<div class="ppe-panel ppe-stacker-panel">
<div class="ppe-panel-header">
<h2>堆垛机状态</h2>
<span class="ppe-status-badge status-normal">运行中</span>
</div>
<div class="ppe-panel-body">
<div class="ppe-stacker-animation">
<div class="ppe-storage-rack">
<!-- 储物架动画区域 -->
</div>
<div class="ppe-stacker" id="stacker-animation">
<!-- 堆垛机动画 -->
</div>
</div>
<div class="ppe-stacker-metrics">
<div class="ppe-metric">
<div class="ppe-metric-value">5.4 m/s</div>
<div class="ppe-metric-label">当前速度</div>
</div>
<div class="ppe-metric">
<div class="ppe-metric-value">B12-34</div>
<div class="ppe-metric-label">当前位置</div>
</div>
<div class="ppe-metric">
<div class="ppe-metric-value">45 kg</div>
<div class="ppe-metric-label">载重</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 底部告警区域 -->
<div class="ppe-panel ppe-alerts-panel">
<div class="ppe-panel-header">
<h2>系统告警</h2>
<div class="ppe-badge">2</div>
</div>
<div class="ppe-panel-body">
<div id="alerts-list" class="ppe-alerts-list">
<div class="ppe-alert-item alert-warning">
<div class="ppe-alert-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
</div>
<div class="ppe-alert-content">
<div class="ppe-alert-message">堆垛机X轴接近极限位置,请检查程序逻辑</div>
<div class="ppe-alert-time">10:45:21</div>
</div>
<div class="ppe-alert-actions">
<button class="ppe-alert-btn">查看</button>
</div>
</div>
<div class="ppe-alert-item alert-error">
<div class="ppe-alert-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
</div>
<div class="ppe-alert-content">
<div class="ppe-alert-message">PLC通信中断,请检查网络连接</div>
<div class="ppe-alert-time">10:42:53</div>
</div>
<div class="ppe-alert-actions">
<button class="ppe-alert-btn">查看</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 模态框 - 硬件配置 -->
<div id="hardware-config-modal" class="ppe-modal">
<div class="ppe-modal-content">
<div class="ppe-modal-header">
<h3>PLC硬件配置</h3>
<button class="ppe-modal-close">×</button>
</div>
<div class="ppe-modal-body">
<div class="ppe-form-group">
<label>PLC型号</label>
<select class="ppe-select">
<option>西门子 S7-1200</option>
<option>西门子 S7-1500</option>
<option>AB CompactLogix 5380</option>
<option>AB ControlLogix 5580</option>
<option>三菱 FX5U-32M</option>
</select>
</div>
<div class="ppe-form-group">
<label>IP地址</label>
<input type="text" class="ppe-input" value="192.168.1.100">
</div>
<div class="ppe-form-group">
<label>通信端口</label>
<input type="number" class="ppe-input" value="102">
</div>
<div class="ppe-form-group">
<label>刷新率 (ms)</label>
<input type="number" class="ppe-input" value="100">
</div>
<div class="ppe-hardware-modules">
<h4>I/O模块</h4>
<div class="ppe-module-list">
<div class="ppe-module-item">
<div class="ppe-module-header">
<span>数字量输入模块 DI16</span>
<span>Slot 1</span>
</div>
<div class="ppe-module-body">16通道,24V DC</div>
</div>
<div class="ppe-module-item">
<div class="ppe-module-header">
<span>数字量输出模块 DO16</span>
<span>Slot 2</span>
</div>
<div class="ppe-module-body">16通道,继电器输出</div>
</div>
<div class="ppe-module-item">
<div class="ppe-module-header">
<span>模拟量输入模块 AI8</span>
<span>Slot 3</span>
</div>
<div class="ppe-module-body">8通道,±10V/4-20mA</div>
</div>
<div class="ppe-module-item">
<div class="ppe-module-header">
<span>模拟量输出模块 AO4</span>
<span>Slot 4</span>
</div>
<div class="ppe-module-body">4通道,±10V/4-20mA</div>
</div>
</div>
<button class="ppe-btn ppe-btn-sm ppe-btn-add">添加模块</button>
</div>
</div>
<div class="ppe-modal-footer">
<button class="ppe-btn ppe-btn-secondary modal-close-btn">取消</button>
<button class="ppe-btn ppe-btn-primary">保存配置</button>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
styles.css
/* 开放式PLC编程环境 - 苹果科技风格 */
:root {
/* 颜色变量 - 苹果风格 */
--background: #F5F5F7;
--card-bg: #FFFFFF;
--primary-text: #1D1D1F;
--secondary-text: #86868B;
--accent-blue: #0066CC;
--accent-green: #34C759;
--accent-orange: #FF9500;
--accent-red: #FF3B30;
--accent-purple: #5E5CE6;
--border-color: #D2D2D7;
--grid-color: #E8E8ED;
/* 阴影 */
--card-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
--header-shadow: 0 1px 5px rgba(0, 0, 0, 0.05);
/* 尺寸变量 */
--header-height: 60px;
--toolbar-height: 48px;
--panels-gap: 16px;
--border-radius: 10px;
--input-height: 32px;
}
/* 基础样式 */
#plc-programming-environment {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif;
color: var(--primary-text);
background-color: var(--background);
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
position: relative;
box-sizing: border-box;
margin: 0;
padding: 0;
overflow: hidden;
}
#plc-programming-environment * {
box-sizing: border-box;
}
/* 顶部导航栏 */
.ppe-header {
height: var(--header-height);
background-color: var(--card-bg);
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
box-shadow: var(--header-shadow);
z-index: 10;
}
.ppe-logo {
display: flex;
align-items: center;
gap: 10px;
font-weight: 500;
}
.ppe-logo svg {
color: var(--accent-blue);
}
.ppe-actions {
display: flex;
gap: 12px;
}
/* 主内容区域 */
.ppe-content {
display: flex;
flex-direction: column;
flex: 1;
height: calc(100% - var(--header-height));
overflow: hidden;
}
/* 工具栏 */
.ppe-toolbar {
height: var(--toolbar-height);
background-color: var(--card-bg);
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
padding: 0 16px;
gap: 20px;
}
.ppe-toolbar-group {
display: flex;
align-items: center;
gap: 8px;
}
.ppe-toolbar-group:not(:last-child) {
padding-right: 20px;
border-right: 1px solid var(--border-color);
}
.ppe-tool-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
border-radius: 6px;
border: none;
background-color: transparent;
color: var(--primary-text);
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.ppe-tool-btn:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.ppe-tool-btn.active {
background-color: rgba(0, 102, 204, 0.1);
color: var(--accent-blue);
}
.ppe-tool-btn svg {
color: var(--secondary-text);
}
.ppe-tool-btn.active svg {
color: var(--accent-blue);
}
/* 选择器样式 */
.ppe-select {
height: var(--input-height);
padding: 0 12px;
border-radius: 6px;
border: 1px solid var(--border-color);
background-color: var(--card-bg);
color: var(--primary-text);
font-size: 13px;
outline: none;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2386868B' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 32px;
}
.ppe-select:hover {
border-color: var(--secondary-text);
}
.ppe-select:focus {
border-color: var(--accent-blue);
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2);
}
/* 主面板区域 - 使用Grid布局 */
.ppe-main-panels {
display: grid;
grid-template-columns: 1fr 320px;
grid-gap: var(--panels-gap);
padding: var(--panels-gap);
flex: 1;
overflow: hidden;
}
/* 面板样式 */
.ppe-panel {
background-color: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--card-shadow);
display: flex;
flex-direction: column;
overflow: hidden;
}
.ppe-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
}
.ppe-panel-header h2 {
margin: 0;
font-size: 14px;
font-weight: 600;
}
.ppe-panel-actions {
display: flex;
gap: 8px;
}
.ppe-panel-body {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* 编辑器面板 */
.ppe-editor-panel {
height: 100%;
}
/* 标签页样式 */
.ppe-tabs {
display: flex;
border-bottom: 1px solid var(--border-color);
background-color: #FAFAFA;
}
.ppe-tab {
padding: 8px 16px;
font-size: 13px;
cursor: pointer;
border-right: 1px solid var(--border-color);
transition: background-color 0.2s;
}
.ppe-tab:hover {
background-color: rgba(0, 0, 0, 0.02);
}
.ppe-tab.active {
background-color: var(--card-bg);
border-bottom: 2px solid var(--accent-blue);
}
.ppe-tab-add {
padding: 8px 12px;
font-size: 14px;
cursor: pointer;
color: var(--secondary-text);
display: flex;
align-items: center;
justify-content: center;
}
.ppe-tab-add:hover {
color: var(--accent-blue);
background-color: rgba(0, 0, 0, 0.02);
}
/* 编辑器容器 */
.ppe-editor-container {
flex: 1;
overflow: hidden;
position: relative;
}
.ppe-editor-area {
height: 100%;
width: 100%;
}
/* 梯形图编辑器 */
.ppe-ladder-editor {
display: flex;
flex-direction: column;
height: 100%;
max-height: 100%;
}
.ppe-ladder-toolbar {
display: flex;
padding: 8px;
border-bottom: 1px solid var(--border-color);
gap: 8px;
background-color: var(--card-bg);
flex-shrink: 0;
}
.ppe-ladder-btn {
padding: 4px 8px;
font-size: 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--card-bg);
cursor: pointer;
transition: all 0.2s;
}
.ppe-ladder-btn:hover {
background-color: var(--background);
border-color: var(--secondary-text);
}
.ppe-ladder-grid {
flex: 1;
overflow-y: auto;
overflow-x: auto;
background-color: var(--card-bg);
background-image:
linear-gradient(var(--grid-color) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
background-size: 20px 20px;
padding: 8px;
max-height: 400px; /* 设置最大高度为400px */
position: relative;
}
/* 梯形图元素样式 */
.ladder-element {
position: absolute;
width: 100px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.ladder-function-block {
padding: 5px 10px;
border: 1px solid var(--primary-text);
border-radius: 4px;
background-color: var(--background);
font-size: 12px;
min-width: 80px;
text-align: center;
}
.ladder-label {
position: absolute;
font-size: 10px;
top: -15px;
width: 100%;
text-align: center;
color: var(--secondary-text);
}
/* 右侧面板 */
.ppe-side-panels {
display: grid;
grid-template-rows: 1fr 1fr;
grid-gap: var(--panels-gap);
height: 100%;
max-height: 100%;
}
/* 变量监控面板 */
.ppe-variables-panel {
max-height: 100%;
}
.ppe-search-bar {
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
}
.ppe-search-input {
width: 100%;
height: var(--input-height);
padding: 0 12px;
border-radius: 6px;
border: 1px solid var(--border-color);
background-color: var(--background);
font-size: 13px;
outline: none;
}
.ppe-search-input:focus {
border-color: var(--accent-blue);
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2);
}
.ppe-variables-list {
overflow-y: auto;
padding: 8px;
max-height: calc(100% - 57px);
}
.ppe-variable-item {
display: flex;
padding: 8px 12px;
border-radius: 6px;
background-color: var(--background);
margin-bottom: 6px;
transition: all 0.2s;
}
.ppe-variable-item:hover {
background-color: rgba(0, 102, 204, 0.05);
}
.ppe-variable-name {
flex: 3;
font-size: 13px;
font-weight: 500;
}
.ppe-variable-type {
flex: 1;
font-size: 12px;
color: var(--secondary-text);
text-align: center;
}
.ppe-variable-value {
flex: 1;
font-size: 13px;
text-align: right;
font-weight: 500;
color: var(--accent-blue);
}
/* 堆垛机状态面板 */
.ppe-stacker-panel {
display: flex;
flex-direction: column;
}
.ppe-stacker-animation {
flex: 1;
position: relative;
background-color: #F0F2F5;
overflow: hidden;
min-height: 140px;
}
.ppe-storage-rack {
position: absolute;
top: 10px;
left: 10px;
right: 10px;
bottom: 10px;
background-image: repeating-linear-gradient(
to right,
rgba(0, 0, 0, 0.1) 0px,
rgba(0, 0, 0, 0.1) 1px,
transparent 1px,
transparent 40px
),
repeating-linear-gradient(
to bottom,
rgba(0, 0, 0, 0.1) 0px,
rgba(0, 0, 0, 0.1) 1px,
transparent 1px,
transparent 40px
);
border: 1px solid rgba(0, 0, 0, 0.2);
}
.ppe-stacker {
position: absolute;
width: 30px;
height: 40px;
background-color: var(--accent-blue);
border-radius: 4px;
left: 150px;
top: 60px;
transition: all 0.5s cubic-bezier(0.22, 1, 0.36, 1);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.ppe-stacker:before {
content: '';
position: absolute;
width: 10px;
height: 5px;
background-color: #FFD700;
bottom: 0;
left: 10px;
border-radius: 2px 2px 0 0;
transform-origin: bottom center;
animation: move-fork 2s ease-in-out infinite;
}
@keyframes move-fork {
0%, 100% { transform: scaleY(1); }
50% { transform: scaleY(2); }
}
.ppe-stacker-metrics {
display: flex;
justify-content: space-between;
padding: 12px 16px;
background-color: var(--card-bg);
border-top: 1px solid var(--border-color);
}
.ppe-metric {
text-align: center;
flex: 1;
}
.ppe-metric-value {
font-size: 16px;
font-weight: 600;
}
.ppe-metric-label {
font-size: 12px;
color: var(--secondary-text);
margin-top: 2px;
}
/* 按钮样式 */
.ppe-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border-radius: 6px;
border: 1px solid var(--border-color);
background-color: var(--card-bg);
color: var(--primary-text);
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.ppe-btn:hover {
background-color: var(--background);
}
.ppe-btn-sm {
padding: 4px 10px;
font-size: 13px;
}
.ppe-btn-primary {
background-color: var(--accent-blue);
border-color: var(--accent-blue);
color: white;
}
.ppe-btn-primary:hover {
background-color: #0055B3;
border-color: #0055B3;
}
.ppe-btn-secondary {
background-color: var(--background);
}
.ppe-btn-add {
background-color: transparent;
color: var(--accent-blue);
border-style: dashed;
}
.ppe-btn-add:hover {
background-color: rgba(0, 102, 204, 0.05);
}
/* 告警面板 */
.ppe-alerts-panel {
margin: 0 var(--panels-gap) var(--panels-gap);
height: 120px;
flex-shrink: 0;
}
.ppe-alerts-list {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
overflow-y: auto;
max-height: 100%;
}
/* 告警项 */
.ppe-alert-item {
display: flex;
align-items: center;
gap: 10px;
background-color: var(--background);
border-radius: 6px;
padding: 10px 12px;
}
.alert-warning {
border-left: 3px solid var(--accent-orange);
}
.alert-error {
border-left: 3px solid var(--accent-red);
}
.alert-info {
border-left: 3px solid var(--accent-blue);
}
.ppe-alert-icon {
color: var(--accent-orange);
}
.alert-error .ppe-alert-icon {
color: var(--accent-red);
}
.alert-info .ppe-alert-icon {
color: var(--accent-blue);
}
.ppe-alert-content {
flex: 1;
}
.ppe-alert-message {
font-size: 13px;
}
.ppe-alert-time {
font-size: 12px;
color: var(--secondary-text);
margin-top: 2px;
}
.ppe-alert-actions {
flex-shrink: 0;
}
.ppe-alert-btn {
padding: 3px 8px;
border-radius: 4px;
font-size: 12px;
border: 1px solid var(--border-color);
background-color: transparent;
color: var(--accent-blue);
cursor: pointer;
}
.ppe-alert-btn:hover {
background-color: var(--accent-blue);
color: white;
}
/* 状态徽章 */
.ppe-status-badge, .ppe-badge {
font-size: 12px;
font-weight: 500;
padding: 2px 8px;
border-radius: 10px;
}
.ppe-badge {
background-color: var(--accent-red);
color: white;
min-width: 24px;
text-align: center;
}
.status-normal {
background-color: rgba(52, 199, 89, 0.1);
color: var(--accent-green);
}
.status-warning {
background-color: rgba(255, 149, 0, 0.1);
color: var(--accent-orange);
}
.status-error {
background-color: rgba(255, 59, 48, 0.1);
color: var(--accent-red);
}
/* 输入和表单样式 */
.ppe-input {
height: var(--input-height);
padding: 0 12px;
border-radius: 6px;
border: 1px solid var(--border-color);
width: 100%;
font-size: 13px;
outline: none;
}
.ppe-input:focus {
border-color: var(--accent-blue);
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2);
}
.ppe-form-group {
margin-bottom: 16px;
}
.ppe-form-group label {
display: block;
font-size: 13px;
font-weight: 500;
margin-bottom: 6px;
}
/* 模态框 */
.ppe-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.ppe-modal.active {
display: flex;
}
.ppe-modal-content {
background-color: var(--card-bg);
border-radius: var(--border-radius);
width: 90%;
max-width: 600px;
max-height: 90vh;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.ppe-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid var(--border-color);
}
.ppe-modal-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.ppe-modal-close {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: var(--secondary-text);
}
.ppe-modal-body {
padding: 20px;
overflow-y: auto;
flex: 1;
}
.ppe-modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 15px 20px;
border-top: 1px solid var(--border-color);
}
/* 硬件模块列表 */
.ppe-hardware-modules {
margin-top: 20px;
}
.ppe-hardware-modules h4 {
font-size: 14px;
margin-bottom: 10px;
}
.ppe-module-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
}
.ppe-module-item {
border: 1px solid var(--border-color);
border-radius: 6px;
overflow: hidden;
}
.ppe-module-header {
padding: 8px 12px;
background-color: var(--background);
display: flex;
justify-content: space-between;
font-size: 13px;
font-weight: 500;
}
.ppe-module-body {
padding: 8px 12px;
font-size: 12px;
color: var(--secondary-text);
}
/* 动画效果 */
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.animate-pulse {
animation: pulse 2s infinite;
}
.animate-blink {
animation: blink 1.5s infinite;
}
/* 响应式布局调整 */
@media (max-width: 992px) {
.ppe-main-panels {
grid-template-columns: 1fr;
grid-template-rows: 1fr auto;
}
.ppe-side-panels {
grid-template-rows: auto auto;
}
.ppe-stacker-panel {
min-height: 250px;
}
}
@media (max-width: 768px) {
.ppe-toolbar {
overflow-x: auto;
justify-content: flex-start;
}
.ppe-toolbar-group {
flex-shrink: 0;
}
}
script.js
// 开放式PLC编程环境 - JavaScript
// 全局变量
let editor = null; // Monaco编辑器实例
let currentMode = 'ladder'; // 当前编程模式:ladder(梯形图), st(结构化文本), fbd(功能块)
let currentPLCVendor = 'siemens'; // 当前选择的PLC厂商
let simulationRunning = true; // 模拟运行状态
let stackerPosition = { x: 150, y: 60 }; // 堆垛机位置
let alertHistory = []; // 告警历史记录
let variableValues = {}; // 变量值记录
let plcConnected = true; // PLC连接状态
// 初始示例代码 - ST语言
const sampleSTCode = `// 堆垛机控制程序 - 结构化文本
PROGRAM Main
VAR
S_StartButton AT %I0.0 : BOOL; // 启动按钮
S_StopButton AT %I0.1 : BOOL; // 停止按钮
S_ResetButton AT %I0.2 : BOOL; // 复位按钮
S_EmergencyStop AT %I0.3 : BOOL; // 紧急停止按钮
Position_X AT %MW10 : REAL; // X轴位置
Position_Y AT %MW14 : REAL; // Y轴位置
Position_Z AT %MW18 : REAL; // Z轴位置
Speed_X AT %MW22 : REAL; // X轴速度
Speed_Y AT %MW26 : REAL; // Y轴速度
Speed_Z AT %MW30 : REAL; // Z轴速度
CurrentShelf AT %MW34 : INT; // 当前货架位置
TargetShelf AT %MW36 : INT; // 目标货架位置
RunStatus AT %Q0.0 : BOOL; // 运行状态
FaultStatus AT %Q0.1 : BOOL; // 故障状态
PLC_Cycle : TIME := T#10MS; // PLC周期时间
END_VAR
// 主程序循环
RunStatus := S_StartButton AND NOT S_StopButton AND NOT S_EmergencyStop;
FaultStatus := S_EmergencyStop OR (Position_X > 2000.0);
// 如果系统处于运行状态,执行堆垛机控制
IF RunStatus THEN
// 简单移动逻辑 - 向目标位置移动
IF CurrentShelf <> TargetShelf THEN
// 计算目标位置坐标
Position_X := Position_X + Speed_X * PLC_Cycle;
Position_Y := Position_Y + Speed_Y * PLC_Cycle;
// 检查是否到达目标位置
IF ABS(Position_X - TargetShelf * 100.0) < 1.0 THEN
CurrentShelf := TargetShelf;
Speed_X := 0.0;
Speed_Y := 0.0;
END_IF;
END_IF;
END_IF;
// 复位操作
IF S_ResetButton THEN
Position_X := 0.0;
Position_Y := 0.0;
Position_Z := 0.0;
Speed_X := 0.0;
Speed_Y := 0.0;
Speed_Z := 0.0;
CurrentShelf := 0;
END_IF;
END_PROGRAM`;
// 梯形图示例数据
const ladderData = [
{ type: 'rung_start', x: 0, y: 0 },
{ type: 'contact_no', x: 1, y: 0, label: 'S_StartButton' },
{ type: 'contact_nc', x: 2, y: 0, label: 'S_StopButton' },
{ type: 'contact_nc', x: 3, y: 0, label: 'S_EmergencyStop' },
{ type: 'coil', x: 4, y: 0, label: 'RunStatus' },
{ type: 'rung_end', x: 5, y: 0 },
{ type: 'rung_start', x: 0, y: 1 },
{ type: 'contact_no', x: 1, y: 1, label: 'S_EmergencyStop' },
{ type: 'coil', x: 4, y: 1, label: 'FaultStatus' },
{ type: 'rung_end', x: 5, y: 1 },
{ type: 'rung_start', x: 0, y: 2 },
{ type: 'contact_no', x: 1, y: 2, label: 'RunStatus' },
{ type: 'contact_no', x: 2, y: 2, label: 'TargetNotReached' },
{ type: 'function_block', x: 3, y: 2, label: 'MOVE', inputs: ['Speed_X', 'PLC_Cycle'], outputs: ['Position_X'] },
{ type: 'rung_end', x: 5, y: 2 },
{ type: 'rung_start', x: 0, y: 3 },
{ type: 'contact_no', x: 1, y: 3, label: 'S_ResetButton' },
{ type: 'function_block', x: 3, y: 3, label: 'RESET', inputs: [], outputs: ['Position_X', 'Position_Y', 'Position_Z'] },
{ type: 'rung_end', x: 5, y: 3 },
];
// 初始化函数
function initPLCProgrammingEnvironment() {
console.log('初始化PLC编程环境...');
// 初始化Monaco编辑器
initMonacoEditor();
// 初始化梯形图编辑器
initLadderEditor();
// 初始化事件监听器
setupEventListeners();
// 初始化堆垛机动画
initStackerAnimation();
// 初始化变量模拟
initVariableSimulation();
// 更新界面状态
updateUIState();
console.log('PLC编程环境初始化完成');
}
// 初始化Monaco编辑器
function initMonacoEditor() {
require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs' }});
require(['vs/editor/editor.main'], function() {
// 注册PLC编程语言
monaco.languages.register({ id: 'structuredtext' });
// 配置语法高亮
monaco.languages.setMonarchTokensProvider('structuredtext', {
defaultToken: '',
ignoreCase: true,
tokenizer: {
root: [
[/\/\/.*$/, 'comment'],
[/\b(PROGRAM|END_PROGRAM|VAR|END_VAR|IF|THEN|ELSE|ELSIF|END_IF|FOR|TO|BY|DO|END_FOR|WHILE|END_WHILE|REPEAT|UNTIL|END_REPEAT|CASE|OF|END_CASE|RETURN|EXIT)\b/, 'keyword'],
[/\b(BOOL|INT|DINT|REAL|TIME|STRING|ARRAY|STRUCT|END_STRUCT)\b/, 'type'],
[/\b(TRUE|FALSE|NULL)\b/, 'constant'],
[/\b(AT|ANY|FROM)\b/, 'modifier'],
[/\b(ABS|SQRT|LOG|EXP|SIN|COS|TAN|ASIN|ACOS|ATAN|ADD|SUB|MUL|DIV|MOD)\b/, 'function'],
[/\b(AND|OR|NOT|XOR|GT|GE|LT|LE|EQ|NE)\b/, 'operator'],
[/\b\d+\.\d*([eE][-+]?\d+)?\b/, 'number.float'],
[/\b\d+\b/, 'number'],
[/T#[0-9]+[smhd]/, 'timevalue'],
[/\%[IQM][XBWDL]?[0-9\.]+/, 'address'],
[/"([^"\\]|\\.)*$/, 'string.invalid'],
[/"/, { token: 'string.quote', bracket: '@open', next: '@string' }],
],
string: [
[/[^\\"]+/, 'string'],
[/\\./, 'string.escape'],
[/"/, { token: 'string.quote', bracket: '@close', next: '@pop' }]
]
}
});
// 创建编辑器
editor = monaco.editor.create(document.getElementById('monaco-editor'), {
value: sampleSTCode,
language: 'structuredtext',
theme: 'vs-light',
automaticLayout: true,
minimap: {
enabled: false
}
});
// 隐藏编辑器,显示梯形图编辑器
document.getElementById('monaco-editor').style.display = 'none';
document.querySelector('.ppe-ladder-editor').style.display = 'flex';
});
}
// 初始化梯形图编辑器
function initLadderEditor() {
const ladderGrid = document.getElementById('ladder-grid');
// 清空现有内容
ladderGrid.innerHTML = '';
// 创建网格背景
const gridSize = 20;
const cols = 10;
const rows = 15;
// 绘制梯形图
ladderData.forEach(element => {
const elementDiv = document.createElement('div');
elementDiv.className = `ladder-element ladder-${element.type}`;
elementDiv.style.left = `${element.x * gridSize * 5}px`;
elementDiv.style.top = `${element.y * gridSize * 3}px`;
if (element.label) {
const labelDiv = document.createElement('div');
labelDiv.className = 'ladder-label';
labelDiv.textContent = element.label;
elementDiv.appendChild(labelDiv);
}
// 根据类型设置具体内容
if (element.type === 'contact_no') {
elementDiv.innerHTML += `
<svg viewBox="0 0 50 20" xmlns="http://www.w3.org/2000/svg">
<line x1="0" y1="10" x2="15" y2="10" stroke="black" />
<line x1="35" y1="10" x2="50" y2="10" stroke="black" />
<line x1="15" y1="0" x2="15" y2="20" stroke="black" />
<line x1="35" y1="0" x2="35" y2="20" stroke="black" />
</svg>
`;
} else if (element.type === 'contact_nc') {
elementDiv.innerHTML += `
<svg viewBox="0 0 50 20" xmlns="http://www.w3.org/2000/svg">
<line x1="0" y1="10" x2="15" y2="10" stroke="black" />
<line x1="35" y1="10" x2="50" y2="10" stroke="black" />
<line x1="15" y1="0" x2="15" y2="20" stroke="black" />
<line x1="35" y1="0" x2="35" y2="20" stroke="black" />
<line x1="15" y1="5" x2="35" y2="5" stroke="black" />
</svg>
`;
} else if (element.type === 'coil') {
elementDiv.innerHTML += `
<svg viewBox="0 0 50 20" xmlns="http://www.w3.org/2000/svg">
<line x1="0" y1="10" x2="10" y2="10" stroke="black" />
<line x1="40" y1="10" x2="50" y2="10" stroke="black" />
<circle cx="25" cy="10" r="15" fill="none" stroke="black" />
</svg>
`;
} else if (element.type === 'function_block') {
const blockDiv = document.createElement('div');
blockDiv.className = 'ladder-function-block';
blockDiv.textContent = element.label || 'FUNC';
elementDiv.appendChild(blockDiv);
} else if (element.type === 'rung_start') {
elementDiv.innerHTML += `
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<line x1="0" y1="10" x2="20" y2="10" stroke="black" stroke-width="2" />
</svg>
`;
} else if (element.type === 'rung_end') {
elementDiv.innerHTML += `
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<line x1="0" y1="10" x2="20" y2="10" stroke="black" stroke-width="2" />
</svg>
`;
}
ladderGrid.appendChild(elementDiv);
});
}
// 初始化事件监听器
function setupEventListeners() {
// 编程模式切换
document.querySelectorAll('.ppe-tool-btn[data-mode]').forEach(btn => {
btn.addEventListener('click', function() {
const mode = this.getAttribute('data-mode');
switchProgrammingMode(mode);
});
});
// PLC厂商选择
document.getElementById('plc-vendor').addEventListener('change', function() {
currentPLCVendor = this.value;
console.log(`切换到PLC厂商: ${currentPLCVendor}`);
});
// 保存按钮
document.getElementById('save-btn').addEventListener('click', function() {
saveProgram();
});
// 下载按钮
document.getElementById('download-btn').addEventListener('click', function() {
downloadProgram();
});
// 验证按钮
document.getElementById('verify-btn').addEventListener('click', function() {
verifyProgram();
});
// 格式化按钮
document.getElementById('format-btn').addEventListener('click', function() {
formatProgram();
});
// 部署按钮
document.getElementById('deploy-btn').addEventListener('click', function() {
deployProgram();
});
// 标签页切换
document.querySelectorAll('.ppe-tab').forEach(tab => {
tab.addEventListener('click', function() {
const tabId = this.getAttribute('data-tab');
switchTab(tabId);
});
});
// 刷新变量按钮
document.getElementById('refresh-vars-btn').addEventListener('click', function() {
refreshVariables();
});
// 变量搜索功能
document.querySelector('.ppe-search-input').addEventListener('input', function() {
filterVariables(this.value);
});
// 硬件配置模态框
document.getElementById('menu-btn').addEventListener('click', function() {
openHardwareConfig();
});
// 关闭模态框
document.querySelector('.ppe-modal-close').addEventListener('click', function() {
closeModal('hardware-config-modal');
});
document.querySelector('.modal-close-btn').addEventListener('click', function() {
closeModal('hardware-config-modal');
});
}
// 切换编程模式
function switchProgrammingMode(mode) {
// 更新全局变量
currentMode = mode;
// 更新按钮状态
document.querySelectorAll('.ppe-tool-btn[data-mode]').forEach(btn => {
btn.classList.remove('active');
});
document.querySelector(`.ppe-tool-btn[data-mode="${mode}"]`).classList.add('active');
// 显示相应的编辑器
if (mode === 'ladder') {
document.getElementById('monaco-editor').style.display = 'none';
document.querySelector('.ppe-ladder-editor').style.display = 'flex';
} else {
document.getElementById('monaco-editor').style.display = 'block';
document.querySelector('.ppe-ladder-editor').style.display = 'none';
// 更新Monaco编辑器语言
if (editor) {
if (mode === 'st') {
monaco.editor.setModelLanguage(editor.getModel(), 'structuredtext');
} else {
monaco.editor.setModelLanguage(editor.getModel(), 'plaintext');
}
}
}
console.log(`切换到${mode}编程模式`);
}
// 初始化堆垛机动画
function initStackerAnimation() {
const stacker = document.getElementById('stacker-animation');
if (!stacker) return;
// 设置初始位置
updateStackerPosition();
// 设置动画
setInterval(() => {
if (simulationRunning) {
// 随机移动堆垛机
const newX = Math.max(10, Math.min(280, stackerPosition.x + (Math.random() * 40 - 20)));
const newY = Math.max(10, Math.min(120, stackerPosition.y + (Math.random() * 20 - 10)));
// 缓慢更新位置
stackerPosition.x = stackerPosition.x + (newX - stackerPosition.x) * 0.1;
stackerPosition.y = stackerPosition.y + (newY - stackerPosition.y) * 0.1;
updateStackerPosition();
updateStackerMetrics();
}
}, 500);
}
// 更新堆垛机位置
function updateStackerPosition() {
const stacker = document.getElementById('stacker-animation');
if (!stacker) return;
stacker.style.left = `${stackerPosition.x}px`;
stacker.style.top = `${stackerPosition.y}px`;
}
// 更新堆垛机指标
function updateStackerMetrics() {
// 更新速度指标
const speedElement = document.querySelector('.ppe-stacker-metrics .ppe-metric:nth-child(1) .ppe-metric-value');
if (speedElement) {
const speed = (Math.random() * 2 + 4).toFixed(1);
speedElement.textContent = `${speed} m/s`;
}
// 更新位置指标
const positionElement = document.querySelector('.ppe-stacker-metrics .ppe-metric:nth-child(2) .ppe-metric-value');
if (positionElement) {
const shelfX = String.fromCharCode(65 + Math.floor(stackerPosition.x / 40));
const shelfY = Math.floor(stackerPosition.y / 30) + 10;
positionElement.textContent = `${shelfX}${shelfY}`;
}
// 更新载重指标
const loadElement = document.querySelector('.ppe-stacker-metrics .ppe-metric:nth-child(3) .ppe-metric-value');
if (loadElement) {
const load = Math.floor(Math.random() * 30 + 30);
loadElement.textContent = `${load} kg`;
}
}
// 初始化变量模拟
function initVariableSimulation() {
// 初始化变量值
variableValues = {
'S_StartButton': true,
'S_StopButton': false,
'S_ResetButton': false,
'S_EmergencyStop': false,
'Position_X': 145.32,
'Position_Y': 87.65,
'Position_Z': 22.41,
'Speed_X': 0.75,
'Speed_Y': 0.5,
'Speed_Z': 0.3,
'CurrentShelf': 12,
'TargetShelf': 18,
'RunStatus': true,
'FaultStatus': false
};
// 定期更新变量值
setInterval(() => {
if (simulationRunning) {
// 随机更新一些变量
variableValues.Position_X += (Math.random() * 2 - 1) * 0.5;
variableValues.Position_Y += (Math.random() * 2 - 1) * 0.3;
variableValues.Position_Z += (Math.random() * 2 - 1) * 0.1;
variableValues.Speed_X = Math.max(0, Math.min(2, variableValues.Speed_X + (Math.random() * 0.2 - 0.1)));
// 检测异常
if (variableValues.Position_X > 200 && Math.random() < 0.05) {
addAlert('warning', '堆垛机X轴接近极限位置,请检查程序逻辑');
}
// 随机PLC连接状态
if (Math.random() < 0.01) {
plcConnected = !plcConnected;
if (!plcConnected) {
addAlert('error', 'PLC通信中断,请检查网络连接');
} else {
addAlert('info', 'PLC通信已恢复');
}
updateUIState();
}
// 更新变量显示
updateVariableDisplay();
}
}, 2000);
}
// 更新变量显示
function updateVariableDisplay() {
document.querySelectorAll('.ppe-variable-item').forEach(item => {
const name = item.querySelector('.ppe-variable-name').textContent;
const valueElement = item.querySelector('.ppe-variable-value');
if (name in variableValues) {
const value = variableValues[name];
if (typeof value === 'boolean') {
valueElement.textContent = value ? 'TRUE' : 'FALSE';
} else if (typeof value === 'number') {
if (Number.isInteger(value)) {
valueElement.textContent = value.toString();
} else {
valueElement.textContent = value.toFixed(2);
}
} else {
valueElement.textContent = value.toString();
}
}
});
}
// 过滤变量
function filterVariables(query) {
if (!query) {
// 显示所有变量
document.querySelectorAll('.ppe-variable-item').forEach(item => {
item.style.display = 'flex';
});
return;
}
query = query.toLowerCase();
document.querySelectorAll('.ppe-variable-item').forEach(item => {
const name = item.querySelector('.ppe-variable-name').textContent.toLowerCase();
if (name.includes(query)) {
item.style.display = 'flex';
} else {
item.style.display = 'none';
}
});
}
// 刷新变量
function refreshVariables() {
console.log('刷新变量...');
// 添加闪烁动画效果
document.querySelectorAll('.ppe-variable-item').forEach(item => {
item.classList.add('animate-blink');
setTimeout(() => {
item.classList.remove('animate-blink');
}, 1000);
});
updateVariableDisplay();
}
// 添加告警
function addAlert(type, message) {
const alert = {
id: Date.now(),
type: type,
message: message,
time: new Date()
};
// 添加到告警历史
alertHistory.unshift(alert);
// 限制最大历史记录数
if (alertHistory.length > 5) {
alertHistory.pop();
}
// 更新告警显示
updateAlertDisplay();
// 更新告警徽章
const badge = document.querySelector('.ppe-badge');
if (badge) {
badge.textContent = alertHistory.length;
}
}
// 更新告警显示
function updateAlertDisplay() {
const alertsList = document.getElementById('alerts-list');
if (!alertsList) return;
// 清空当前告警
alertsList.innerHTML = '';
// 添加告警
alertHistory.forEach(alert => {
let alertClass = 'alert-info';
let iconSvg = '';
if (alert.type === 'warning') {
alertClass = 'alert-warning';
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>';
} else if (alert.type === 'error') {
alertClass = 'alert-error';
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>';
} else {
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>';
}
// 格式化时间
const time = new Intl.DateTimeFormat('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).format(alert.time);
const alertHtml = `
<div class="ppe-alert-item ${alertClass}">
<div class="ppe-alert-icon">
${iconSvg}
</div>
<div class="ppe-alert-content">
<div class="ppe-alert-message">${alert.message}</div>
<div class="ppe-alert-time">${time}</div>
</div>
<div class="ppe-alert-actions">
<button class="ppe-alert-btn">查看</button>
</div>
</div>
`;
alertsList.innerHTML += alertHtml;
});
// 添加查看按钮事件
document.querySelectorAll('.ppe-alert-btn').forEach((btn, index) => {
btn.addEventListener('click', function() {
console.log(`查看告警: ${alertHistory[index].message}`);
// 这里可以添加弹出详情的逻辑
});
});
}
// 保存程序
function saveProgram() {
console.log('保存程序...');
addAlert('info', '程序已保存');
}
// 下载程序
function downloadProgram() {
console.log('下载程序...');
let content = '';
let filename = '';
if (currentMode === 'ladder') {
content = JSON.stringify(ladderData, null, 2);
filename = 'ladder_program.json';
} else {
if (editor) {
content = editor.getValue();
filename = currentMode === 'st' ? 'program.st' : 'program.txt';
}
}
if (content) {
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
addAlert('info', `程序已下载为 ${filename}`);
}
}
// 验证程序
function verifyProgram() {
console.log('验证程序...');
// 模拟验证过程
setTimeout(() => {
if (Math.random() < 0.7) {
addAlert('info', '程序验证通过');
} else {
addAlert('warning', '程序验证发现潜在问题');
}
}, 500);
}
// 格式化程序
function formatProgram() {
console.log('格式化程序...');
if (currentMode !== 'ladder' && editor) {
// 模拟格式化过程
editor.getAction('editor.action.formatDocument').run();
addAlert('info', '程序已格式化');
} else {
addAlert('info', '梯形图程序无需格式化');
}
}
// 部署程序
function deployProgram() {
console.log('部署程序...');
// 检查PLC连接状态
if (!plcConnected) {
addAlert('error', '无法部署程序,PLC未连接');
return;
}
// 模拟部署过程
const deployBtn = document.getElementById('deploy-btn');
if (deployBtn) {
deployBtn.disabled = true;
deployBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" class="animate-spin"></svg> 部署中...';
}
setTimeout(() => {
if (deployBtn) {
deployBtn.disabled = false;
deployBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg> 部署程序';
}
if (Math.random() < 0.9) {
addAlert('info', '程序已成功部署到PLC');
} else {
addAlert('error', '程序部署失败,请检查PLC连接');
}
}, 2000);
}
// 切换标签页
function switchTab(tabId) {
document.querySelectorAll('.ppe-tab').forEach(tab => {
tab.classList.remove('active');
});
document.querySelector(`.ppe-tab[data-tab="${tabId}"]`).classList.add('active');
console.log(`切换到标签页: ${tabId}`);
// 这里可以添加加载不同标签页内容的逻辑
}
// 打开硬件配置对话框
function openHardwareConfig() {
document.getElementById('hardware-config-modal').style.display = 'flex';
}
// 关闭模态框
function closeModal(modalId) {
document.getElementById(modalId).style.display = 'none';
}
// 更新UI状态
function updateUIState() {
// 更新PLC连接状态
const statusBadge = document.querySelector('.ppe-status-badge');
if (statusBadge) {
if (plcConnected) {
statusBadge.className = 'ppe-status-badge status-normal';
statusBadge.textContent = '运行中';
} else {
statusBadge.className = 'ppe-status-badge status-error';
statusBadge.textContent = '已断开';
}
}
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
initPLCProgrammingEnvironment();
});
// 导出全局对象
window.plcProgramming = {
switchMode: switchProgrammingMode,
deployProgram: deployProgram,
refreshVariables: refreshVariables,
addAlert: addAlert
};