A:完成提交
This commit is contained in:
1 parent
7daac20ddb
commit
b93bda3113
4 files changed
+5420
No files matched your search
@@ -0,0 +1,1264 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>通用PLC数据转换工具</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
||||
min-height: 100vh;
|
||||
color: #333;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 1.1em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.main-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.main-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 15px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 15px 35px rgba(0,0,0,0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.panel h3 {
|
||||
color: #4a5568;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.4em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: linear-gradient(45deg, #667eea, #764ba2);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
select, input, textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
select:focus, input:focus, textarea:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.grid-3 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.grid-4 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid-2, .grid-3, .grid-4 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
button {
|
||||
background: linear-gradient(45deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: linear-gradient(45deg, #718096, #2d3748);
|
||||
}
|
||||
|
||||
button.success {
|
||||
background: linear-gradient(45deg, #48bb78, #38a169);
|
||||
}
|
||||
|
||||
button.warning {
|
||||
background: linear-gradient(45deg, #ed8936, #dd6b20);
|
||||
}
|
||||
|
||||
.result-panel {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.result {
|
||||
background: linear-gradient(135deg, #f0fff4, #e6fffa);
|
||||
border: 2px solid #48bb78;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-top: 15px;
|
||||
font-family: 'Courier New', monospace;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: linear-gradient(135deg, #fed7d7, #ffeaea);
|
||||
border-color: #f56565;
|
||||
color: #c53030;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 12px 20px;
|
||||
cursor: pointer;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
border-bottom-color: #667eea;
|
||||
color: #667eea;
|
||||
background: #f7fafc;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.preset-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.preset-card {
|
||||
background: #f7fafc;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preset-card:hover {
|
||||
border-color: #667eea;
|
||||
background: #eef2ff;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.preset-card.active {
|
||||
border-color: #667eea;
|
||||
background: #eef2ff;
|
||||
}
|
||||
|
||||
.preset-card h4 {
|
||||
color: #4a5568;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.preset-card p {
|
||||
color: #718096;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.format-examples {
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: .4s;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #667eea;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
.bit-display {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
gap: 5px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.bit {
|
||||
background: #f1f5f9;
|
||||
border: 1px solid #cbd5e0;
|
||||
border-radius: 4px;
|
||||
padding: 8px 4px;
|
||||
text-align: center;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.bit.set {
|
||||
background: #48bb78;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.floating-help {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: linear-gradient(45deg, #667eea, #764ba2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.help-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.8);
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.help-content {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 30px;
|
||||
max-width: 800px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
margin: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🔧 通用PLC数据转换工具</h1>
|
||||
<p>支持所有主流PLC品牌和数据格式的专业转换工具</p>
|
||||
</div>
|
||||
|
||||
<div class="main-grid">
|
||||
<!-- 左侧:配置面板 -->
|
||||
<div class="panel">
|
||||
<h3><span class="icon">⚙️</span>转换配置</h3>
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab active" onclick="switchTab('basic')">基础转换</div>
|
||||
<div class="tab" onclick="switchTab('advanced')">高级选项</div>
|
||||
<div class="tab" onclick="switchTab('presets')">预设配置</div>
|
||||
</div>
|
||||
|
||||
<!-- 基础转换 -->
|
||||
<div id="basic" class="tab-content active">
|
||||
<div class="form-group">
|
||||
<label>PLC品牌</label>
|
||||
<select id="plcBrand" onchange="updatePresets()">
|
||||
<option value="omron">欧姆龙 (Omron)</option>
|
||||
<option value="siemens">西门子 (Siemens)</option>
|
||||
<option value="mitsubishi">三菱 (Mitsubishi)</option>
|
||||
<option value="ab">罗克韦尔 (Allen-Bradley)</option>
|
||||
<option value="schneider">施耐德 (Schneider)</option>
|
||||
<option value="delta">台达 (Delta)</option>
|
||||
<option value="keyence">基恩士 (Keyence)</option>
|
||||
<option value="panasonic">松下 (Panasonic)</option>
|
||||
<option value="custom">自定义</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="form-group">
|
||||
<label>源数据类型</label>
|
||||
<select id="sourceType">
|
||||
<option value="ushort">USHORT (16位)</option>
|
||||
<option value="short">SHORT (16位)</option>
|
||||
<option value="uint">UINT (32位)</option>
|
||||
<option value="int">INT (32位)</option>
|
||||
<option value="ulong">ULONG (64位)</option>
|
||||
<option value="long">LONG (64位)</option>
|
||||
<option value="byte">BYTE (8位)</option>
|
||||
<option value="bool">BOOL (1位)</option>
|
||||
<option value="real">REAL (32位浮点)</option>
|
||||
<option value="double">DOUBLE (64位浮点)</option>
|
||||
<option value="string">STRING</option>
|
||||
<option value="raw">原始数据</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>目标数据类型</label>
|
||||
<select id="targetType">
|
||||
<option value="int">INT (32位)</option>
|
||||
<option value="uint">UINT (32位)</option>
|
||||
<option value="short">SHORT (16位)</option>
|
||||
<option value="ushort">USHORT (16位)</option>
|
||||
<option value="long">LONG (64位)</option>
|
||||
<option value="ulong">ULONG (64位)</option>
|
||||
<option value="byte">BYTE (8位)</option>
|
||||
<option value="bool">BOOL (1位)</option>
|
||||
<option value="real">REAL (32位浮点)</option>
|
||||
<option value="double">DOUBLE (64位浮点)</option>
|
||||
<option value="string">STRING</option>
|
||||
<option value="hex">十六进制</option>
|
||||
<option value="binary">二进制</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-3">
|
||||
<div class="form-group">
|
||||
<label>字节序</label>
|
||||
<select id="byteOrder">
|
||||
<option value="ABCD">ABCD (大端序)</option>
|
||||
<option value="DCBA">DCBA (小端序)</option>
|
||||
<option value="BADC">BADC (混合序1)</option>
|
||||
<option value="CDAB">CDAB (混合序2)</option>
|
||||
<option value="auto">自动检测</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>位序</label>
|
||||
<select id="bitOrder">
|
||||
<option value="msb">MSB优先</option>
|
||||
<option value="lsb">LSB优先</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>数据格式</label>
|
||||
<select id="dataFormat">
|
||||
<option value="decimal">十进制</option>
|
||||
<option value="hex">十六进制</option>
|
||||
<option value="binary">二进制</option>
|
||||
<option value="array">数组</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 高级选项 -->
|
||||
<div id="advanced" class="tab-content">
|
||||
<div class="grid-2">
|
||||
<div class="form-group">
|
||||
<label>偏移量 (字节)</label>
|
||||
<input type="number" id="offset" value="0" min="0">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>数据长度限制</label>
|
||||
<input type="number" id="lengthLimit" value="0" min="0" placeholder="0=无限制">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="signedData" checked>
|
||||
有符号数据
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="reverseOrder">
|
||||
反转数组顺序
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="ignoreErrors">
|
||||
忽略转换错误
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>浮点数精度</label>
|
||||
<input type="number" id="precision" value="6" min="0" max="15">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>字符串编码</label>
|
||||
<select id="encoding">
|
||||
<option value="utf8">UTF-8</option>
|
||||
<option value="ascii">ASCII</option>
|
||||
<option value="gbk">GBK</option>
|
||||
<option value="unicode">Unicode</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预设配置 -->
|
||||
<div id="presets" class="tab-content">
|
||||
<div class="preset-grid" id="presetGrid">
|
||||
<!-- 预设配置将在这里动态生成 -->
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button onclick="saveCurrentPreset()">💾 保存当前配置</button>
|
||||
<button onclick="loadPreset()" class="secondary">📁 加载配置文件</button>
|
||||
<input type="file" id="fileInput" style="display:none" accept=".json">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button onclick="convertData()">🔄 转换数据</button>
|
||||
<button onclick="autoDetect()" class="warning">🔍 自动检测</button>
|
||||
<button onclick="clearAll()" class="secondary">🗑️ 清空</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:数据输入面板 -->
|
||||
<div class="panel">
|
||||
<h3><span class="icon">📥</span>数据输入</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label>原始数据</label>
|
||||
<textarea id="inputData" placeholder="输入你的PLC数据,支持多种格式:
|
||||
[256,0,256,0,256,0]
|
||||
256,0,256,0,256,0
|
||||
0x0100,0x0000,0x0100
|
||||
01000000 00000000...">[256,0,256,0,256,0,256,0,256,0]</textarea>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button onclick="loadSampleData()" class="secondary">📝 加载示例</button>
|
||||
<button onclick="importFromFile()" class="secondary">📂 从文件导入</button>
|
||||
<button onclick="formatInput()">🎨 格式化</button>
|
||||
</div>
|
||||
|
||||
<div class="format-examples">
|
||||
<strong>支持的输入格式:</strong><br>
|
||||
数组: [1,2,3] 或 1,2,3<br>
|
||||
十六进制: 0x01,0x02 或 01 02<br>
|
||||
二进制: 00000001 00000010<br>
|
||||
混合: 1 0x02 00000011
|
||||
</div>
|
||||
|
||||
<!-- 位编辑器 -->
|
||||
<div class="form-group">
|
||||
<label>位编辑器 <small>(点击切换位状态)</small></label>
|
||||
<div id="bitEditor" class="bit-display">
|
||||
<!-- 位显示将在这里动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 结果面板 -->
|
||||
<div class="panel result-panel">
|
||||
<h3><span class="icon">📤</span>转换结果</h3>
|
||||
|
||||
<div class="button-group">
|
||||
<button onclick="exportResult()" class="success">💾 导出结果</button>
|
||||
<button onclick="copyToClipboard()" class="success">📋 复制到剪贴板</button>
|
||||
<button onclick="showComparison()" class="secondary">🔍 显示对比</button>
|
||||
</div>
|
||||
|
||||
<div id="result"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 浮动帮助按钮 -->
|
||||
<div class="floating-help" onclick="toggleHelp()">❓</div>
|
||||
|
||||
<!-- 帮助面板 -->
|
||||
<div class="help-panel" id="helpPanel" onclick="toggleHelp()">
|
||||
<div class="help-content" onclick="event.stopPropagation()">
|
||||
<h2>📚 使用帮助</h2>
|
||||
<h3>PLC品牌特色:</h3>
|
||||
<ul>
|
||||
<li><strong>欧姆龙:</strong> 通常使用小端序,支持CJ/CP/CS系列</li>
|
||||
<li><strong>西门子:</strong> S7系列,大端序为主</li>
|
||||
<li><strong>三菱:</strong> FX/Q系列,混合字节序常见</li>
|
||||
<li><strong>AB:</strong> CompactLogix/ControlLogix,小端序</li>
|
||||
<li><strong>施耐德:</strong> Modicon系列,大端序</li>
|
||||
</ul>
|
||||
|
||||
<h3>数据类型说明:</h3>
|
||||
<ul>
|
||||
<li><strong>BOOL:</strong> 1位布尔值</li>
|
||||
<li><strong>BYTE:</strong> 8位无符号整数 (0-255)</li>
|
||||
<li><strong>USHORT:</strong> 16位无符号整数 (0-65535)</li>
|
||||
<li><strong>INT:</strong> 32位有符号整数</li>
|
||||
<li><strong>REAL:</strong> 32位IEEE754浮点数</li>
|
||||
</ul>
|
||||
|
||||
<h3>字节序说明:</h3>
|
||||
<ul>
|
||||
<li><strong>ABCD:</strong> 大端序,高字节在前</li>
|
||||
<li><strong>DCBA:</strong> 小端序,低字节在前</li>
|
||||
<li><strong>BADC/CDAB:</strong> 字节对调的混合模式</li>
|
||||
</ul>
|
||||
|
||||
<button onclick="toggleHelp()" style="margin-top: 20px;">关闭帮助</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 全局变量
|
||||
let currentPresets = {};
|
||||
let conversionHistory = [];
|
||||
|
||||
// PLC品牌预设配置
|
||||
const plcPresets = {
|
||||
omron: {
|
||||
name: "欧姆龙默认",
|
||||
byteOrder: "DCBA",
|
||||
bitOrder: "lsb",
|
||||
signed: true,
|
||||
description: "适用于CJ/CP/CS系列"
|
||||
},
|
||||
siemens: {
|
||||
name: "西门子默认",
|
||||
byteOrder: "ABCD",
|
||||
bitOrder: "msb",
|
||||
signed: true,
|
||||
description: "适用于S7-300/400/1500"
|
||||
},
|
||||
mitsubishi: {
|
||||
name: "三菱默认",
|
||||
byteOrder: "BADC",
|
||||
bitOrder: "lsb",
|
||||
signed: true,
|
||||
description: "适用于FX/Q系列"
|
||||
},
|
||||
ab: {
|
||||
name: "AB默认",
|
||||
byteOrder: "DCBA",
|
||||
bitOrder: "lsb",
|
||||
signed: true,
|
||||
description: "适用于CompactLogix"
|
||||
},
|
||||
schneider: {
|
||||
name: "施耐德默认",
|
||||
byteOrder: "ABCD",
|
||||
bitOrder: "msb",
|
||||
signed: true,
|
||||
description: "适用于Modicon系列"
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化
|
||||
window.onload = function() {
|
||||
updatePresets();
|
||||
updateBitEditor();
|
||||
loadSampleData();
|
||||
};
|
||||
|
||||
// 切换标签页
|
||||
function switchTab(tabName) {
|
||||
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||
|
||||
event.target.classList.add('active');
|
||||
document.getElementById(tabName).classList.add('active');
|
||||
}
|
||||
|
||||
// 更新预设配置
|
||||
function updatePresets() {
|
||||
const brand = document.getElementById('plcBrand').value;
|
||||
const presetGrid = document.getElementById('presetGrid');
|
||||
|
||||
presetGrid.innerHTML = '';
|
||||
|
||||
Object.entries(plcPresets).forEach(([key, preset]) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'preset-card';
|
||||
if (key === brand) card.classList.add('active');
|
||||
|
||||
card.innerHTML = `
|
||||
<h4>${preset.name}</h4>
|
||||
<p>${preset.description}</p>
|
||||
`;
|
||||
|
||||
card.onclick = () => applyPreset(key);
|
||||
presetGrid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// 应用预设配置
|
||||
function applyPreset(presetKey) {
|
||||
const preset = plcPresets[presetKey];
|
||||
if (!preset) return;
|
||||
|
||||
document.getElementById('byteOrder').value = preset.byteOrder;
|
||||
document.getElementById('bitOrder').value = preset.bitOrder;
|
||||
document.getElementById('signedData').checked = preset.signed;
|
||||
document.getElementById('plcBrand').value = presetKey;
|
||||
|
||||
updatePresets();
|
||||
}
|
||||
|
||||
// 加载示例数据
|
||||
function loadSampleData() {
|
||||
const samples = [
|
||||
"[256,0,256,0,256,0,256,0,256,0]",
|
||||
"0x0100,0x0000,0x0100,0x0000",
|
||||
"1,0,1,0,1,0,1,0,1,0",
|
||||
"00000001 00000000 00000001 00000000"
|
||||
];
|
||||
|
||||
const randomSample = samples[Math.floor(Math.random() * samples.length)];
|
||||
document.getElementById('inputData').value = randomSample;
|
||||
}
|
||||
|
||||
// 解析输入数据
|
||||
function parseInputData(input) {
|
||||
input = input.trim();
|
||||
|
||||
// 移除数组括号
|
||||
if (input.startsWith('[') && input.endsWith(']')) {
|
||||
input = input.slice(1, -1);
|
||||
}
|
||||
|
||||
// 分割数据
|
||||
let values = [];
|
||||
|
||||
if (input.includes(',')) {
|
||||
values = input.split(',');
|
||||
} else {
|
||||
values = input.split(/\s+/);
|
||||
}
|
||||
|
||||
// 解析每个值
|
||||
return values.map(val => {
|
||||
val = val.trim();
|
||||
if (val.startsWith('0x') || val.startsWith('0X')) {
|
||||
return parseInt(val, 16);
|
||||
} else if (/^[01]+$/.test(val) && val.length > 4) {
|
||||
return parseInt(val, 2);
|
||||
} else {
|
||||
return parseInt(val, 10);
|
||||
}
|
||||
}).filter(val => !isNaN(val));
|
||||
}
|
||||
|
||||
// 主要转换函数
|
||||
function convertData() {
|
||||
const inputData = document.getElementById('inputData').value;
|
||||
const sourceType = document.getElementById('sourceType').value;
|
||||
const targetType = document.getElementById('targetType').value;
|
||||
const byteOrder = document.getElementById('byteOrder').value;
|
||||
const signed = document.getElementById('signedData').checked;
|
||||
const resultDiv = document.getElementById('result');
|
||||
|
||||
try {
|
||||
const parsedData = parseInputData(inputData);
|
||||
if (parsedData.length === 0) {
|
||||
throw new Error('无法解析输入数据');
|
||||
}
|
||||
|
||||
let result;
|
||||
|
||||
// 根据转换类型进行处理
|
||||
if (sourceType === 'ushort' && targetType === 'int') {
|
||||
result = convertUshortToInt(parsedData, byteOrder, signed);
|
||||
} else if (sourceType === 'int' && targetType === 'ushort') {
|
||||
result = convertIntToUshort(parsedData, byteOrder);
|
||||
} else if (targetType === 'real') {
|
||||
result = convertToFloat(parsedData, byteOrder);
|
||||
} else if (targetType === 'double') {
|
||||
result = convertToDouble(parsedData, byteOrder);
|
||||
} else if (targetType === 'hex') {
|
||||
result = convertToHex(parsedData);
|
||||
} else if (targetType === 'binary') {
|
||||
result = convertToBinary(parsedData);
|
||||
} else if (targetType === 'bool') {
|
||||
result = convertToBool(parsedData);
|
||||
} else {
|
||||
result = convertGeneric(parsedData, sourceType, targetType, byteOrder, signed);
|
||||
}
|
||||
|
||||
displayResult(result, parsedData, sourceType, targetType, byteOrder);
|
||||
updateBitEditor(parsedData[0] || 0);
|
||||
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = `<div class="result error">转换错误: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 转换函数实现
|
||||
function convertUshortToInt(ushortArray, byteOrder, signed = true) {
|
||||
if (ushortArray.length % 2 !== 0) {
|
||||
throw new Error('USHORT到INT转换需要偶数个数据');
|
||||
}
|
||||
|
||||
const intArray = [];
|
||||
|
||||
for (let i = 0; i < ushortArray.length; i += 2) {
|
||||
const low = ushortArray[i];
|
||||
const high = ushortArray[i + 1];
|
||||
|
||||
let intValue;
|
||||
switch (byteOrder) {
|
||||
case 'ABCD':
|
||||
intValue = (high << 16) | low;
|
||||
break;
|
||||
case 'DCBA':
|
||||
intValue = (low << 16) | high;
|
||||
break;
|
||||
case 'BADC':
|
||||
intValue = ((low & 0xFF) << 24) | (((low >> 8) & 0xFF) << 16) |
|
||||
((high & 0xFF) << 8) | ((high >> 8) & 0xFF);
|
||||
break;
|
||||
case 'CDAB':
|
||||
intValue = ((high & 0xFF) << 24) | (((high >> 8) & 0xFF) << 16) |
|
||||
((low & 0xFF) << 8) | ((low >> 8) & 0xFF);
|
||||
break;
|
||||
default:
|
||||
intValue = (low << 16) | high;
|
||||
}
|
||||
|
||||
if (signed && intValue > 0x7FFFFFFF) {
|
||||
intValue = intValue - 0x100000000;
|
||||
}
|
||||
|
||||
intArray.push(intValue);
|
||||
}
|
||||
|
||||
return intArray;
|
||||
}
|
||||
|
||||
function convertIntToUshort(intArray, byteOrder) {
|
||||
const ushortArray = [];
|
||||
|
||||
intArray.forEach(intValue => {
|
||||
const uintValue = intValue >>> 0;
|
||||
|
||||
switch (byteOrder) {
|
||||
case 'ABCD':
|
||||
ushortArray.push(uintValue & 0xFFFF);
|
||||
ushortArray.push((uintValue >> 16) & 0xFFFF);
|
||||
break;
|
||||
case 'DCBA':
|
||||
ushortArray.push((uintValue >> 16) & 0xFFFF);
|
||||
ushortArray.push(uintValue & 0xFFFF);
|
||||
break;
|
||||
default:
|
||||
ushortArray.push((uintValue >> 16) & 0xFFFF);
|
||||
ushortArray.push(uintValue & 0xFFFF);
|
||||
}
|
||||
});
|
||||
|
||||
return ushortArray;
|
||||
}
|
||||
|
||||
function convertToFloat(data, byteOrder) {
|
||||
const floatArray = [];
|
||||
|
||||
for (let i = 0; i < data.length; i += 2) {
|
||||
if (i + 1 >= data.length) break;
|
||||
|
||||
const intValue = convertUshortToInt([data[i], data[i + 1]], byteOrder, false)[0];
|
||||
const buffer = new ArrayBuffer(4);
|
||||
const intView = new Uint32Array(buffer);
|
||||
const floatView = new Float32Array(buffer);
|
||||
|
||||
intView[0] = intValue;
|
||||
floatArray.push(floatView[0]);
|
||||
}
|
||||
|
||||
return floatArray;
|
||||
}
|
||||
|
||||
function convertToDouble(data, byteOrder) {
|
||||
const doubleArray = [];
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
if (i + 3 >= data.length) break;
|
||||
|
||||
// 64位双精度需要4个16位数据
|
||||
const part1 = convertUshortToInt([data[i], data[i + 1]], byteOrder, false)[0];
|
||||
const part2 = convertUshortToInt([data[i + 2], data[i + 3]], byteOrder, false)[0];
|
||||
|
||||
const buffer = new ArrayBuffer(8);
|
||||
const intView = new Uint32Array(buffer);
|
||||
const doubleView = new Float64Array(buffer);
|
||||
|
||||
if (byteOrder === 'ABCD' || byteOrder === 'BADC') {
|
||||
intView[1] = part1;
|
||||
intView[0] = part2;
|
||||
} else {
|
||||
intView[0] = part1;
|
||||
intView[1] = part2;
|
||||
}
|
||||
|
||||
doubleArray.push(doubleView[0]);
|
||||
}
|
||||
|
||||
return doubleArray;
|
||||
}
|
||||
|
||||
function convertToHex(data) {
|
||||
return data.map(val => '0x' + val.toString(16).toUpperCase().padStart(4, '0'));
|
||||
}
|
||||
|
||||
function convertToBinary(data) {
|
||||
return data.map(val => val.toString(2).padStart(16, '0'));
|
||||
}
|
||||
|
||||
function convertToBool(data) {
|
||||
return data.map(val => val !== 0);
|
||||
}
|
||||
|
||||
function convertGeneric(data, sourceType, targetType, byteOrder, signed) {
|
||||
// 通用转换逻辑
|
||||
return data.map(val => {
|
||||
switch (targetType) {
|
||||
case 'byte':
|
||||
return val & 0xFF;
|
||||
case 'short':
|
||||
return signed && val > 32767 ? val - 65536 : val;
|
||||
case 'ushort':
|
||||
return val & 0xFFFF;
|
||||
case 'uint':
|
||||
return val >>> 0;
|
||||
case 'long':
|
||||
case 'ulong':
|
||||
return val;
|
||||
default:
|
||||
return val;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 自动检测最佳配置
|
||||
function autoDetect() {
|
||||
const inputData = document.getElementById('inputData').value;
|
||||
const parsedData = parseInputData(inputData);
|
||||
|
||||
if (parsedData.length === 0) return;
|
||||
|
||||
const resultDiv = document.getElementById('result');
|
||||
let bestFormat = 'DCBA';
|
||||
let bestResults = {};
|
||||
|
||||
// 测试所有字节序格式
|
||||
const formats = ['ABCD', 'DCBA', 'BADC', 'CDAB'];
|
||||
|
||||
let analysisResult = '<div class="result"><strong>🔍 自动检测结果:</strong>\n\n';
|
||||
|
||||
formats.forEach(format => {
|
||||
try {
|
||||
if (parsedData.length % 2 === 0) {
|
||||
const converted = convertUshortToInt(parsedData, format);
|
||||
bestResults[format] = converted;
|
||||
|
||||
const score = analyzePattern(converted);
|
||||
analysisResult += `${format}: [${converted.slice(0, 5).join(', ')}${converted.length > 5 ? '...' : ''}] (评分: ${score})\n`;
|
||||
|
||||
if (score > (bestResults[bestFormat] ? analyzePattern(bestResults[bestFormat]) : 0)) {
|
||||
bestFormat = format;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
analysisResult += `${format}: 转换失败\n`;
|
||||
}
|
||||
});
|
||||
|
||||
analysisResult += `\n推荐使用: ${bestFormat} ✅\n`;
|
||||
analysisResult += '</div>';
|
||||
|
||||
// 应用最佳配置
|
||||
document.getElementById('byteOrder').value = bestFormat;
|
||||
|
||||
resultDiv.innerHTML = analysisResult;
|
||||
}
|
||||
|
||||
// 分析数据模式评分
|
||||
function analyzePattern(data) {
|
||||
let score = 0;
|
||||
|
||||
// 检查重复模式
|
||||
const uniqueValues = new Set(data);
|
||||
if (uniqueValues.size < data.length / 2) score += 10;
|
||||
|
||||
// 检查合理范围
|
||||
const maxVal = Math.max(...data);
|
||||
const minVal = Math.min(...data);
|
||||
if (maxVal < 10000 && minVal >= 0) score += 20;
|
||||
|
||||
// 检查序列模式
|
||||
let isSequential = true;
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
if (Math.abs(data[i] - data[i-1]) > 100) {
|
||||
isSequential = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isSequential) score += 15;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
// 显示转换结果
|
||||
function displayResult(result, originalData, sourceType, targetType, byteOrder) {
|
||||
const resultDiv = document.getElementById('result');
|
||||
const precision = parseInt(document.getElementById('precision').value);
|
||||
|
||||
let formattedResult;
|
||||
if (targetType === 'real' || targetType === 'double') {
|
||||
formattedResult = result.map(val => val.toFixed(precision)).join(', ');
|
||||
} else if (targetType === 'hex') {
|
||||
formattedResult = result.join(', ');
|
||||
} else if (targetType === 'binary') {
|
||||
formattedResult = result.join(' ');
|
||||
} else if (targetType === 'bool') {
|
||||
formattedResult = result.map(val => val ? 'True' : 'False').join(', ');
|
||||
} else {
|
||||
formattedResult = result.join(', ');
|
||||
}
|
||||
|
||||
const verification = verifyConversion(result, originalData, sourceType, targetType, byteOrder);
|
||||
|
||||
resultDiv.innerHTML = `<div class="result">
|
||||
<strong>✅ 转换成功:</strong>
|
||||
源数据 (${sourceType}): [${originalData.join(', ')}]
|
||||
转换结果 (${targetType}): [${formattedResult}]
|
||||
|
||||
<strong>转换参数:</strong>
|
||||
字节序: ${byteOrder}
|
||||
数据长度: ${originalData.length} → ${result.length}
|
||||
${verification}
|
||||
|
||||
<strong>详细分析:</strong>
|
||||
${getDetailedAnalysis(originalData, result, sourceType, targetType, byteOrder)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// 验证转换结果
|
||||
function verifyConversion(result, originalData, sourceType, targetType, byteOrder) {
|
||||
if (sourceType === 'ushort' && targetType === 'int' && originalData.length % 2 === 0) {
|
||||
try {
|
||||
const backToUshort = convertIntToUshort(result, byteOrder);
|
||||
const matches = JSON.stringify(backToUshort) === JSON.stringify(originalData);
|
||||
return `反向验证: ${matches ? '✅ 通过' : '❌ 失败'}`;
|
||||
} catch (error) {
|
||||
return `反向验证: ❌ 错误 - ${error.message}`;
|
||||
}
|
||||
}
|
||||
return '验证: ℹ️ 不适用此转换类型';
|
||||
}
|
||||
|
||||
// 获取详细分析
|
||||
function getDetailedAnalysis(originalData, result, sourceType, targetType, byteOrder) {
|
||||
let analysis = '';
|
||||
|
||||
if (sourceType === 'ushort' && targetType === 'int') {
|
||||
for (let i = 0; i < Math.min(originalData.length / 2, 3); i++) {
|
||||
const idx = i * 2;
|
||||
if (idx + 1 < originalData.length && i < result.length) {
|
||||
const low = originalData[idx];
|
||||
const high = originalData[idx + 1];
|
||||
const intVal = result[i];
|
||||
|
||||
analysis += `第${i + 1}组: [${low}, ${high}] → ${intVal} `;
|
||||
analysis += `(0x${low.toString(16).padStart(4, '0')}, 0x${high.toString(16).padStart(4, '0')} → 0x${(intVal >>> 0).toString(16).padStart(8, '0')})\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return analysis || '数据转换完成';
|
||||
}
|
||||
|
||||
// 更新位编辑器
|
||||
function updateBitEditor(value = 0) {
|
||||
const bitEditor = document.getElementById('bitEditor');
|
||||
bitEditor.innerHTML = '';
|
||||
|
||||
for (let i = 15; i >= 0; i--) {
|
||||
const bit = document.createElement('div');
|
||||
bit.className = 'bit';
|
||||
bit.textContent = ((value >> i) & 1).toString();
|
||||
bit.title = `位 ${i}`;
|
||||
|
||||
if ((value >> i) & 1) {
|
||||
bit.classList.add('set');
|
||||
}
|
||||
|
||||
bit.onclick = () => toggleBit(i);
|
||||
bitEditor.appendChild(bit);
|
||||
}
|
||||
}
|
||||
|
||||
// 切换位状态
|
||||
function toggleBit(bitIndex) {
|
||||
const inputData = document.getElementById('inputData').value;
|
||||
const parsedData = parseInputData(inputData);
|
||||
|
||||
if (parsedData.length > 0) {
|
||||
parsedData[0] ^= (1 << bitIndex);
|
||||
document.getElementById('inputData').value = '[' + parsedData.join(',') + ']';
|
||||
updateBitEditor(parsedData[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化输入
|
||||
function formatInput() {
|
||||
const input = document.getElementById('inputData').value;
|
||||
const parsed = parseInputData(input);
|
||||
document.getElementById('inputData').value = '[' + parsed.join(', ') + ']';
|
||||
}
|
||||
|
||||
// 复制到剪贴板
|
||||
function copyToClipboard() {
|
||||
const resultDiv = document.getElementById('result');
|
||||
if (resultDiv.textContent) {
|
||||
navigator.clipboard.writeText(resultDiv.textContent).then(() => {
|
||||
alert('结果已复制到剪贴板!');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 导出结果
|
||||
function exportResult() {
|
||||
const resultDiv = document.getElementById('result');
|
||||
if (!resultDiv.textContent) {
|
||||
alert('没有可导出的结果');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
timestamp: new Date().toISOString(),
|
||||
input: document.getElementById('inputData').value,
|
||||
result: resultDiv.textContent,
|
||||
config: {
|
||||
plcBrand: document.getElementById('plcBrand').value,
|
||||
sourceType: document.getElementById('sourceType').value,
|
||||
targetType: document.getElementById('targetType').value,
|
||||
byteOrder: document.getElementById('byteOrder').value
|
||||
}
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `plc_conversion_${new Date().getTime()}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// 清空所有数据
|
||||
function clearAll() {
|
||||
document.getElementById('inputData').value = '';
|
||||
document.getElementById('result').innerHTML = '';
|
||||
updateBitEditor(0);
|
||||
}
|
||||
|
||||
// 显示对比
|
||||
function showComparison() {
|
||||
// 实现数据对比功能
|
||||
alert('对比功能开发中...');
|
||||
}
|
||||
|
||||
// 切换帮助面板
|
||||
function toggleHelp() {
|
||||
const helpPanel = document.getElementById('helpPanel');
|
||||
helpPanel.style.display = helpPanel.style.display === 'flex' ? 'none' : 'flex';
|
||||
}
|
||||
|
||||
// 从文件导入
|
||||
function importFromFile() {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.txt,.csv,.json';
|
||||
input.onchange = function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
document.getElementById('inputData').value = e.target.result;
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
// 保存当前预设
|
||||
function saveCurrentPreset() {
|
||||
const name = prompt('请输入预设名称:');
|
||||
if (name) {
|
||||
const config = {
|
||||
name: name,
|
||||
plcBrand: document.getElementById('plcBrand').value,
|
||||
sourceType: document.getElementById('sourceType').value,
|
||||
targetType: document.getElementById('targetType').value,
|
||||
byteOrder: document.getElementById('byteOrder').value,
|
||||
signed: document.getElementById('signedData').checked
|
||||
};
|
||||
|
||||
localStorage.setItem('plc_preset_' + name, JSON.stringify(config));
|
||||
alert('预设已保存!');
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时自动转换示例数据
|
||||
setTimeout(() => {
|
||||
convertData();
|
||||
}, 500);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user