| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>网关部署 - IPole</title>
- <style>
- *{margin:0;padding:0;box-sizing:border-box}
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f5f7fa;color:#333;min-height:100vh}
- .header{background:#1a73e8;color:#fff;padding:16px 24px;font-size:18px;font-weight:600}
- .container{max-width:640px;margin:24px auto;padding:0 16px}
- .card{background:#fff;border-radius:8px;padding:24px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.1)}
- .card h2{font-size:16px;margin-bottom:16px}
- .info-row{display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #eee;font-size:14px}
- .info-row:last-child{border-bottom:none}
- .info-label{color:#888}
- .upload-area{border:2px dashed #ccc;border-radius:8px;padding:40px;text-align:center;cursor:pointer;transition:border-color .2s}
- .upload-area:hover,.upload-area.dragover{border-color:#1a73e8}
- .upload-area p{color:#888;margin-top:8px;font-size:13px}
- .progress-bar{height:8px;background:#e8eaed;border-radius:4px;overflow:hidden;margin-top:12px}
- .progress-fill{height:100%;background:#1a73e8;transition:width .3s;width:0}
- .btn{display:inline-block;padding:10px 24px;background:#1a73e8;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:14px}
- .btn:disabled{opacity:.5;cursor:not-allowed}
- .check-item{display:flex;align-items:center;padding:8px 0;border-bottom:1px solid #eee;font-size:14px}
- .check-item:last-child{border-bottom:none}
- .check-icon{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;margin-right:12px;font-size:12px;flex-shrink:0}
- .check-ok{background:#e6f4ea;color:#1e8e3e}
- .check-fail{background:#fce8e6;color:#d93025}
- .check-pending{background:#e8eaed;color:#888}
- .check-detail{color:#888;font-size:12px;margin-left:auto}
- .status-msg{padding:12px;border-radius:4px;text-align:center;font-size:14px;margin-top:12px}
- .status-ok{background:#e6f4ea;color:#1e8e3e}
- .status-fail{background:#fce8e6;color:#d93025}
- .status-progress{background:#e8f0fe;color:#1a73e8}
- .hidden{display:none}
- .error-msg{color:#d93025;font-size:13px;margin-top:4px}
- </style>
- </head>
- <body>
- <div class="header">网关部署工具</div>
- <div class="container">
- <div class="card" id="infoCard">
- <h2>系统信息</h2>
- <div class="info-row"><span class="info-label">当前版本</span><span id="currentVersion">--</span></div>
- <div class="info-row"><span class="info-label">上次部署</span><span id="lastDeploy">--</span></div>
- <div class="info-row"><span class="info-label">上次结果</span><span id="lastResult">--</span></div>
- </div>
- <div class="card" id="uploadCard">
- <h2>上传新版本</h2>
- <div class="upload-area" id="dropZone">
- <div style="font-size:36px">+</div>
- <p>拖拽或点击上传 ipole 可执行文件</p>
- <input type="file" id="fileInput" style="display:none">
- </div>
- <div id="fileInfo" class="hidden" style="margin-top:12px">
- <div class="info-row"><span class="info-label">文件名</span><span id="fileName">--</span></div>
- <div class="info-row"><span class="info-label">大小</span><span id="fileSize">--</span></div>
- </div>
- <div id="uploadProgress" class="hidden">
- <div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
- <p style="font-size:12px;color:#888;margin-top:4px" id="progressText">0%</p>
- </div>
- <button class="btn" id="deployBtn" style="margin-top:12px;width:100%" disabled>开始部署</button>
- <p id="uploadError" class="error-msg hidden"></p>
- </div>
- <div class="card hidden" id="statusCard">
- <h2 id="statusTitle">部署状态</h2>
- <div id="statusMsg" class="status-msg status-progress">正在部署...</div>
- <div id="checkList"></div>
- </div>
- <div class="card hidden" id="reconnectCard">
- <div class="status-msg status-progress">等待网关重启...<br><span style="font-size:12px">正在自动重连(每2秒)</span></div>
- </div>
- </div>
- <script>
- var selectedFile = null;
- var dropZone = document.getElementById('dropZone');
- var fileInput = document.getElementById('fileInput');
- var checkNames = {mqtt:'MQTT连接', redis:'Redis', serial:'串口', modbus:'设备采集', device_online:'设备在线'};
- // 加载系统信息
- fetch('/deploy/result').then(function(r){return r.json()}).then(function(data){
- if (data.version) {
- document.getElementById('currentVersion').textContent = data.version;
- var d = new Date(data.timestamp * 1000);
- document.getElementById('lastDeploy').textContent = d.toLocaleString();
- var ok = data.phase1 && data.phase1.success;
- document.getElementById('lastResult').textContent = ok ? '成功' : '失败';
- if (ok) document.getElementById('lastResult').style.color = '#1e8e3e';
- else document.getElementById('lastResult').style.color = '#d93025';
- }
- }).catch(function(){});
- // 文件选择
- dropZone.addEventListener('click', function(){ fileInput.click(); });
- dropZone.addEventListener('dragover', function(e){ e.preventDefault(); dropZone.classList.add('dragover'); });
- dropZone.addEventListener('dragleave', function(){ dropZone.classList.remove('dragover'); });
- dropZone.addEventListener('drop', function(e){
- e.preventDefault();
- dropZone.classList.remove('dragover');
- handleFile(e.dataTransfer.files[0]);
- });
- fileInput.addEventListener('change', function(){ handleFile(fileInput.files[0]); });
- function handleFile(file) {
- if (!file) return;
- selectedFile = file;
- document.getElementById('fileName').textContent = file.name;
- document.getElementById('fileSize').textContent = formatSize(file.size);
- document.getElementById('fileInfo').classList.remove('hidden');
- document.getElementById('deployBtn').disabled = false;
- document.getElementById('uploadError').classList.add('hidden');
- }
- function formatSize(bytes) {
- if (bytes < 1024) return bytes + ' B';
- if (bytes < 1048576) return (bytes/1024).toFixed(1) + ' KB';
- return (bytes/1048576).toFixed(1) + ' MB';
- }
- document.getElementById('deployBtn').addEventListener('click', function(){
- if (!selectedFile) return;
- var btn = this;
- btn.disabled = true;
- btn.textContent = '上传中...';
- var formData = new FormData();
- formData.append('file', selectedFile);
- var xhr = new XMLHttpRequest();
- xhr.open('POST', '/deploy/upload');
- xhr.upload.onprogress = function(e){
- if (e.lengthComputable){
- var pct = Math.round(e.loaded / e.total * 100);
- document.getElementById('uploadProgress').classList.remove('hidden');
- document.getElementById('progressFill').style.width = pct + '%';
- document.getElementById('progressText').textContent = pct + '%';
- }
- };
- xhr.onload = function(){
- try {
- var resp = JSON.parse(xhr.responseText);
- if (resp.success) {
- document.getElementById('uploadCard').classList.add('hidden');
- showReconnect();
- } else {
- showError(resp.error || '部署失败');
- }
- } catch(e) {
- showError('解析响应失败');
- }
- btn.disabled = false;
- btn.textContent = '开始部署';
- };
- xhr.onerror = function(){
- document.getElementById('uploadCard').classList.add('hidden');
- showReconnect();
- };
- xhr.send(formData);
- });
- function showReconnect(){
- document.getElementById('reconnectCard').classList.remove('hidden');
- pollReconnect();
- }
- function pollReconnect(){
- var attempts = 0;
- var interval = setInterval(function(){
- attempts++;
- fetch('/deploy/result')
- .then(function(r){ return r.json(); })
- .then(function(data){
- clearInterval(interval);
- document.getElementById('reconnectCard').classList.add('hidden');
- showHealthResult(data);
- })
- .catch(function(){
- if (attempts > 30){
- clearInterval(interval);
- document.getElementById('reconnectCard').innerHTML =
- '<div class="status-msg status-fail">连接超时,请检查网关是否正常启动</div>';
- }
- });
- }, 2000);
- }
- function showHealthResult(data){
- var card = document.getElementById('statusCard');
- card.classList.remove('hidden');
- var checks = (data.phase1 && data.phase1.checks) || [];
- var allOk = data.phase1 && data.phase1.success;
- document.getElementById('statusTitle').textContent = allOk ? '部署成功' : '部署失败';
- document.getElementById('statusMsg').className = 'status-msg ' + (allOk ? 'status-ok' : 'status-fail');
- document.getElementById('statusMsg').textContent = allOk ? 'Phase 1 健康检测全部通过' : 'Phase 1 健康检测失败';
- var list = document.getElementById('checkList');
- list.innerHTML = '';
- checks.forEach(function(c){
- var cls = c.Status === 'ok' ? 'check-ok' : 'check-fail';
- var icon = c.Status === 'ok' ? 'OK' : '!!';
- list.innerHTML += '<div class="check-item">' +
- '<span class="check-icon ' + cls + '">' + icon + '</span>' +
- '<span>' + (checkNames[c.Name] || c.Name) + '</span>' +
- '<span class="check-detail">' + c.Detail + '</span></div>';
- });
- if (data.phase2){
- var p2 = data.phase2;
- list.innerHTML += '<div style="margin-top:12px;padding-top:12px;border-top:2px solid #eee">' +
- '<p style="font-size:13px;color:#888;margin-bottom:8px">Phase 2 业务恢复确认</p></div>';
- (data.phase2.checks||[]).forEach(function(c){
- var cls = c.Status === 'ok' ? 'check-ok' : 'check-fail';
- list.innerHTML += '<div class="check-item">' +
- '<span class="check-icon ' + cls + '">' + (c.Status === 'ok' ? 'OK' : '!!') + '</span>' +
- '<span>' + (checkNames[c.Name] || c.Name) + '</span>' +
- '<span class="check-detail">' + c.Detail + '</span></div>';
- });
- }
- }
- function showError(msg){
- var el = document.getElementById('uploadError');
- el.textContent = msg;
- el.classList.remove('hidden');
- }
- </script>
- </body>
- </html>
|