deploy.html 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>网关部署 - IPole</title>
  7. <style>
  8. *{margin:0;padding:0;box-sizing:border-box}
  9. body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f5f7fa;color:#333;min-height:100vh}
  10. .header{background:#1a73e8;color:#fff;padding:16px 24px;font-size:18px;font-weight:600}
  11. .container{max-width:640px;margin:24px auto;padding:0 16px}
  12. .card{background:#fff;border-radius:8px;padding:24px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.1)}
  13. .card h2{font-size:16px;margin-bottom:16px}
  14. .info-row{display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #eee;font-size:14px}
  15. .info-row:last-child{border-bottom:none}
  16. .info-label{color:#888}
  17. .upload-area{border:2px dashed #ccc;border-radius:8px;padding:40px;text-align:center;cursor:pointer;transition:border-color .2s}
  18. .upload-area:hover,.upload-area.dragover{border-color:#1a73e8}
  19. .upload-area p{color:#888;margin-top:8px;font-size:13px}
  20. .progress-bar{height:8px;background:#e8eaed;border-radius:4px;overflow:hidden;margin-top:12px}
  21. .progress-fill{height:100%;background:#1a73e8;transition:width .3s;width:0}
  22. .btn{display:inline-block;padding:10px 24px;background:#1a73e8;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:14px}
  23. .btn:disabled{opacity:.5;cursor:not-allowed}
  24. .check-item{display:flex;align-items:center;padding:8px 0;border-bottom:1px solid #eee;font-size:14px}
  25. .check-item:last-child{border-bottom:none}
  26. .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}
  27. .check-ok{background:#e6f4ea;color:#1e8e3e}
  28. .check-fail{background:#fce8e6;color:#d93025}
  29. .check-pending{background:#e8eaed;color:#888}
  30. .check-detail{color:#888;font-size:12px;margin-left:auto}
  31. .status-msg{padding:12px;border-radius:4px;text-align:center;font-size:14px;margin-top:12px}
  32. .status-ok{background:#e6f4ea;color:#1e8e3e}
  33. .status-fail{background:#fce8e6;color:#d93025}
  34. .status-progress{background:#e8f0fe;color:#1a73e8}
  35. .hidden{display:none}
  36. .error-msg{color:#d93025;font-size:13px;margin-top:4px}
  37. </style>
  38. </head>
  39. <body>
  40. <div class="header">网关部署工具</div>
  41. <div class="container">
  42. <div class="card" id="infoCard">
  43. <h2>系统信息</h2>
  44. <div class="info-row"><span class="info-label">当前版本</span><span id="currentVersion">--</span></div>
  45. <div class="info-row"><span class="info-label">上次部署</span><span id="lastDeploy">--</span></div>
  46. <div class="info-row"><span class="info-label">上次结果</span><span id="lastResult">--</span></div>
  47. </div>
  48. <div class="card" id="uploadCard">
  49. <h2>上传新版本</h2>
  50. <div class="upload-area" id="dropZone">
  51. <div style="font-size:36px">+</div>
  52. <p>拖拽或点击上传 ipole 可执行文件</p>
  53. <input type="file" id="fileInput" style="display:none">
  54. </div>
  55. <div id="fileInfo" class="hidden" style="margin-top:12px">
  56. <div class="info-row"><span class="info-label">文件名</span><span id="fileName">--</span></div>
  57. <div class="info-row"><span class="info-label">大小</span><span id="fileSize">--</span></div>
  58. </div>
  59. <div id="uploadProgress" class="hidden">
  60. <div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
  61. <p style="font-size:12px;color:#888;margin-top:4px" id="progressText">0%</p>
  62. </div>
  63. <button class="btn" id="deployBtn" style="margin-top:12px;width:100%" disabled>开始部署</button>
  64. <p id="uploadError" class="error-msg hidden"></p>
  65. </div>
  66. <div class="card hidden" id="statusCard">
  67. <h2 id="statusTitle">部署状态</h2>
  68. <div id="statusMsg" class="status-msg status-progress">正在部署...</div>
  69. <div id="checkList"></div>
  70. </div>
  71. <div class="card hidden" id="reconnectCard">
  72. <div class="status-msg status-progress">等待网关重启...<br><span style="font-size:12px">正在自动重连(每2秒)</span></div>
  73. </div>
  74. </div>
  75. <script>
  76. var selectedFile = null;
  77. var dropZone = document.getElementById('dropZone');
  78. var fileInput = document.getElementById('fileInput');
  79. var checkNames = {mqtt:'MQTT连接', redis:'Redis', serial:'串口', modbus:'设备采集', device_online:'设备在线'};
  80. // 加载系统信息
  81. fetch('/deploy/result').then(function(r){return r.json()}).then(function(data){
  82. if (data.version) {
  83. document.getElementById('currentVersion').textContent = data.version;
  84. var d = new Date(data.timestamp * 1000);
  85. document.getElementById('lastDeploy').textContent = d.toLocaleString();
  86. var ok = data.phase1 && data.phase1.success;
  87. document.getElementById('lastResult').textContent = ok ? '成功' : '失败';
  88. if (ok) document.getElementById('lastResult').style.color = '#1e8e3e';
  89. else document.getElementById('lastResult').style.color = '#d93025';
  90. }
  91. }).catch(function(){});
  92. // 文件选择
  93. dropZone.addEventListener('click', function(){ fileInput.click(); });
  94. dropZone.addEventListener('dragover', function(e){ e.preventDefault(); dropZone.classList.add('dragover'); });
  95. dropZone.addEventListener('dragleave', function(){ dropZone.classList.remove('dragover'); });
  96. dropZone.addEventListener('drop', function(e){
  97. e.preventDefault();
  98. dropZone.classList.remove('dragover');
  99. handleFile(e.dataTransfer.files[0]);
  100. });
  101. fileInput.addEventListener('change', function(){ handleFile(fileInput.files[0]); });
  102. function handleFile(file) {
  103. if (!file) return;
  104. selectedFile = file;
  105. document.getElementById('fileName').textContent = file.name;
  106. document.getElementById('fileSize').textContent = formatSize(file.size);
  107. document.getElementById('fileInfo').classList.remove('hidden');
  108. document.getElementById('deployBtn').disabled = false;
  109. document.getElementById('uploadError').classList.add('hidden');
  110. }
  111. function formatSize(bytes) {
  112. if (bytes < 1024) return bytes + ' B';
  113. if (bytes < 1048576) return (bytes/1024).toFixed(1) + ' KB';
  114. return (bytes/1048576).toFixed(1) + ' MB';
  115. }
  116. document.getElementById('deployBtn').addEventListener('click', function(){
  117. if (!selectedFile) return;
  118. var btn = this;
  119. btn.disabled = true;
  120. btn.textContent = '上传中...';
  121. var formData = new FormData();
  122. formData.append('file', selectedFile);
  123. var xhr = new XMLHttpRequest();
  124. xhr.open('POST', '/deploy/upload');
  125. xhr.upload.onprogress = function(e){
  126. if (e.lengthComputable){
  127. var pct = Math.round(e.loaded / e.total * 100);
  128. document.getElementById('uploadProgress').classList.remove('hidden');
  129. document.getElementById('progressFill').style.width = pct + '%';
  130. document.getElementById('progressText').textContent = pct + '%';
  131. }
  132. };
  133. xhr.onload = function(){
  134. try {
  135. var resp = JSON.parse(xhr.responseText);
  136. if (resp.success) {
  137. document.getElementById('uploadCard').classList.add('hidden');
  138. showReconnect();
  139. } else {
  140. showError(resp.error || '部署失败');
  141. }
  142. } catch(e) {
  143. showError('解析响应失败');
  144. }
  145. btn.disabled = false;
  146. btn.textContent = '开始部署';
  147. };
  148. xhr.onerror = function(){
  149. document.getElementById('uploadCard').classList.add('hidden');
  150. showReconnect();
  151. };
  152. xhr.send(formData);
  153. });
  154. function showReconnect(){
  155. document.getElementById('reconnectCard').classList.remove('hidden');
  156. pollReconnect();
  157. }
  158. function pollReconnect(){
  159. var attempts = 0;
  160. var interval = setInterval(function(){
  161. attempts++;
  162. fetch('/deploy/result')
  163. .then(function(r){ return r.json(); })
  164. .then(function(data){
  165. clearInterval(interval);
  166. document.getElementById('reconnectCard').classList.add('hidden');
  167. showHealthResult(data);
  168. })
  169. .catch(function(){
  170. if (attempts > 30){
  171. clearInterval(interval);
  172. document.getElementById('reconnectCard').innerHTML =
  173. '<div class="status-msg status-fail">连接超时,请检查网关是否正常启动</div>';
  174. }
  175. });
  176. }, 2000);
  177. }
  178. function showHealthResult(data){
  179. var card = document.getElementById('statusCard');
  180. card.classList.remove('hidden');
  181. var checks = (data.phase1 && data.phase1.checks) || [];
  182. var allOk = data.phase1 && data.phase1.success;
  183. document.getElementById('statusTitle').textContent = allOk ? '部署成功' : '部署失败';
  184. document.getElementById('statusMsg').className = 'status-msg ' + (allOk ? 'status-ok' : 'status-fail');
  185. document.getElementById('statusMsg').textContent = allOk ? 'Phase 1 健康检测全部通过' : 'Phase 1 健康检测失败';
  186. var list = document.getElementById('checkList');
  187. list.innerHTML = '';
  188. checks.forEach(function(c){
  189. var cls = c.Status === 'ok' ? 'check-ok' : 'check-fail';
  190. var icon = c.Status === 'ok' ? 'OK' : '!!';
  191. list.innerHTML += '<div class="check-item">' +
  192. '<span class="check-icon ' + cls + '">' + icon + '</span>' +
  193. '<span>' + (checkNames[c.Name] || c.Name) + '</span>' +
  194. '<span class="check-detail">' + c.Detail + '</span></div>';
  195. });
  196. if (data.phase2){
  197. var p2 = data.phase2;
  198. list.innerHTML += '<div style="margin-top:12px;padding-top:12px;border-top:2px solid #eee">' +
  199. '<p style="font-size:13px;color:#888;margin-bottom:8px">Phase 2 业务恢复确认</p></div>';
  200. (data.phase2.checks||[]).forEach(function(c){
  201. var cls = c.Status === 'ok' ? 'check-ok' : 'check-fail';
  202. list.innerHTML += '<div class="check-item">' +
  203. '<span class="check-icon ' + cls + '">' + (c.Status === 'ok' ? 'OK' : '!!') + '</span>' +
  204. '<span>' + (checkNames[c.Name] || c.Name) + '</span>' +
  205. '<span class="check-detail">' + c.Detail + '</span></div>';
  206. });
  207. }
  208. }
  209. function showError(msg){
  210. var el = document.getElementById('uploadError');
  211. el.textContent = msg;
  212. el.classList.remove('hidden');
  213. }
  214. </script>
  215. </body>
  216. </html>