|
|
@@ -8,15 +8,18 @@
|
|
|
<style>
|
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
|
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0f172a;color:#e2e8f0;min-height:100vh}
|
|
|
-.header{background:linear-gradient(135deg,#1e293b,#334155);padding:16px 24px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #475569}
|
|
|
+.header{background:linear-gradient(135deg,#1e293b,#334155);padding:16px 24px;display:flex;justify-content:center;align-items:center;border-bottom:1px solid #475569}
|
|
|
+.header-inner{max-width:1200px;width:100%;display:flex;justify-content:space-between;align-items:center}
|
|
|
.header h1{font-size:20px;color:#38bdf8}
|
|
|
.header .status{display:flex;gap:16px;align-items:center;font-size:13px}
|
|
|
.header .status .dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:4px}
|
|
|
.dot-green{background:#22c55e}.dot-red{background:#ef4444}.dot-yellow{background:#eab308}
|
|
|
-.tabs{display:flex;background:#1e293b;border-bottom:1px solid #334155}
|
|
|
+.tabs{display:flex;justify-content:center;background:#1e293b;border-bottom:1px solid #334155}
|
|
|
+.tabs-inner{max-width:1200px;width:100%;display:flex;justify-content:center}
|
|
|
.tab{padding:12px 24px;cursor:pointer;font-size:14px;color:#94a3b8;border-bottom:2px solid transparent;transition:all .2s}
|
|
|
.tab:hover{color:#e2e8f0}.tab.active{color:#38bdf8;border-bottom-color:#38bdf8}
|
|
|
.page{display:none;padding:20px 24px}.page.active{display:block}
|
|
|
+.page-inner{max-width:1200px;margin:0 auto}
|
|
|
/* Device Cards */
|
|
|
.device-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(380px,1fr));gap:16px;margin-bottom:24px}
|
|
|
.device-card{background:#1e293b;border-radius:12px;border:1px solid #334155;overflow:hidden}
|
|
|
@@ -32,7 +35,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b
|
|
|
.section-title{font-size:13px;color:#38bdf8;margin:12px 0 8px;padding-bottom:4px;border-bottom:1px solid #334155}
|
|
|
/* History Page */
|
|
|
.history-controls{background:#1e293b;border-radius:12px;padding:16px;margin-bottom:20px;border:1px solid #334155}
|
|
|
-.controls-row{display:flex;flex-wrap:wrap;gap:12px;align-items:end}
|
|
|
+.controls-row{display:flex;flex-wrap:wrap;gap:12px;align-items:end;justify-content:center}
|
|
|
.control-group{display:flex;flex-direction:column;gap:4px}
|
|
|
.control-group label{font-size:12px;color:#94a3b8}
|
|
|
.control-group input,.control-group select{background:#0f172a;border:1px solid #475569;color:#e2e8f0;padding:8px 12px;border-radius:6px;font-size:13px}
|
|
|
@@ -61,49 +64,56 @@ tr:hover td{background:#1e293b}
|
|
|
</head>
|
|
|
<body>
|
|
|
<div class="header">
|
|
|
- <h1>MPPT Solar Monitor - DM Series</h1>
|
|
|
- <div class="status">
|
|
|
- <span><span class="dot dot-green" id="conn-dot"></span> <span id="conn-status">Connecting...</span></span>
|
|
|
- <span>Devices: <strong id="dev-online">0</strong>/<strong id="dev-total">0</strong></span>
|
|
|
- <span>Records: <strong id="db-records">0</strong></span>
|
|
|
+ <div class="header-inner">
|
|
|
+ <h1>MPPT Solar Monitor - DM Series</h1>
|
|
|
+ <div class="status">
|
|
|
+ <span><span class="dot dot-green" id="conn-dot"></span> <span id="conn-status">Connecting...</span></span>
|
|
|
+ <span>Devices: <strong id="dev-online">0</strong>/<strong id="dev-total">0</strong></span>
|
|
|
+ <span>Records: <strong id="db-records">0</strong></span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="tabs">
|
|
|
- <div class="tab active" onclick="switchPage('realtime')">Real-time Monitor</div>
|
|
|
- <div class="tab" onclick="switchPage('history')">History Query</div>
|
|
|
- <div class="tab" onclick="switchPage('settings')">Device Settings</div>
|
|
|
+ <div class="tabs-inner">
|
|
|
+ <div class="tab active" onclick="switchPage('realtime')">Real-time Monitor</div>
|
|
|
+ <div class="tab" onclick="switchPage('history')">History Query</div>
|
|
|
+ <div class="tab" onclick="switchPage('settings')">Device Settings</div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Real-time Page -->
|
|
|
<div class="page active" id="page-realtime">
|
|
|
- <div class="device-grid" id="device-grid"></div>
|
|
|
+ <div class="page-inner">
|
|
|
+ <div class="device-grid" id="device-grid"></div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
<!-- History Page -->
|
|
|
<div class="page" id="page-history">
|
|
|
- <div class="history-controls">
|
|
|
- <div class="controls-row">
|
|
|
- <div class="control-group">
|
|
|
- <label>Device</label>
|
|
|
- <select id="hist-device"><option value="0">All Devices</option></select>
|
|
|
- </div>
|
|
|
- <div class="control-group">
|
|
|
- <label>Start Time</label>
|
|
|
- <input type="datetime-local" id="hist-start">
|
|
|
- </div>
|
|
|
- <div class="control-group">
|
|
|
- <label>End Time</label>
|
|
|
- <input type="datetime-local" id="hist-end">
|
|
|
- </div>
|
|
|
- <div class="control-group">
|
|
|
- <label>Page Size</label>
|
|
|
- <select id="hist-size">
|
|
|
- <option value="20">20</option><option value="50">50</option>
|
|
|
- <option value="100">100</option><option value="200">200</option>
|
|
|
- </select>
|
|
|
- </div>
|
|
|
- <div class="control-group">
|
|
|
- <label>Sort</label>
|
|
|
+ <div class="page-inner">
|
|
|
+ <div class="history-controls">
|
|
|
+ <div class="controls-row">
|
|
|
+ <div class="control-group">
|
|
|
+ <label>Device</label>
|
|
|
+ <select id="hist-device"><option value="0">All Devices</option></select>
|
|
|
+ </div>
|
|
|
+ <div class="control-group">
|
|
|
+ <label>Start Time</label>
|
|
|
+ <input type="datetime-local" id="hist-start">
|
|
|
+ </div>
|
|
|
+ <div class="control-group">
|
|
|
+ <label>End Time</label>
|
|
|
+ <input type="datetime-local" id="hist-end">
|
|
|
+ </div>
|
|
|
+ <div class="control-group">
|
|
|
+ <label>Page Size</label>
|
|
|
+ <select id="hist-size">
|
|
|
+ <option value="20">20</option><option value="50">50</option>
|
|
|
+ <option value="100">100</option><option value="200">200</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div class="control-group">
|
|
|
+ <label>Sort</label>
|
|
|
<select id="hist-sort"><option value="desc">Newest First</option><option value="asc">Oldest First</option></select>
|
|
|
</div>
|
|
|
<div class="control-group">
|
|
|
@@ -137,16 +147,18 @@ tr:hover td{background:#1e293b}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Settings Page -->
|
|
|
<div class="page" id="page-settings">
|
|
|
- <div class="history-controls">
|
|
|
- <h3 style="color:#38bdf8;margin-bottom:12px;font-size:16px">Device Configuration (Output Registers)</h3>
|
|
|
- <p style="color:#94a3b8;font-size:13px;margin-bottom:16px">Write parameters to MPPT device via Modbus function code 0x10. Changes take effect immediately.</p>
|
|
|
- <div class="controls-row">
|
|
|
- <div class="control-group">
|
|
|
- <label>Target Device</label>
|
|
|
+ <div class="page-inner">
|
|
|
+ <div class="history-controls">
|
|
|
+ <h3 style="color:#38bdf8;margin-bottom:12px;font-size:16px">Device Configuration (Output Registers)</h3>
|
|
|
+ <p style="color:#94a3b8;font-size:13px;margin-bottom:16px">Write parameters to MPPT device via Modbus function code 0x10. Changes take effect immediately.</p>
|
|
|
+ <div class="controls-row">
|
|
|
+ <div class="control-group">
|
|
|
+ <label>Target Device</label>
|
|
|
<select id="set-device"></select>
|
|
|
</div>
|
|
|
<div class="control-group">
|
|
|
@@ -171,22 +183,23 @@ tr:hover td{background:#1e293b}
|
|
|
</div>
|
|
|
<div id="param-info" style="margin-top:12px;padding:10px;background:#0f172a;border-radius:6px;font-size:12px;color:#94a3b8"></div>
|
|
|
<div id="write-result" style="margin-top:12px;padding:10px;border-radius:6px;display:none"></div>
|
|
|
- </div>
|
|
|
- <div class="table-container" style="margin-top:20px">
|
|
|
- <table>
|
|
|
- <thead><tr><th>Register</th><th>Address</th><th>Range</th><th>Description</th><th>Access</th></tr></thead>
|
|
|
- <tbody>
|
|
|
- <tr><td>Battery Type</td><td>40001 (0x0000)</td><td>0-5</td><td>0=胶体 1=密封 2=磷酸铁锂 3=三元锂电 4=自定义 5=加水</td><td style="color:#22c55e">R/W</td></tr>
|
|
|
- <tr><td>Battery Level</td><td>40002 (0x0001)</td><td>0-6</td><td>0=自动识别 1=12V 2=24V 3=36V 4=48V 5=60V 6=72V</td><td style="color:#22c55e">R/W</td></tr>
|
|
|
- <tr><td>Charge Current Limit</td><td>40003 (0x0002)</td><td>5-100</td><td>PV充电电流限制 (%)</td><td style="color:#22c55e">R/W</td></tr>
|
|
|
- <tr><td>Over-discharge Voltage</td><td>40004 (0x0003)</td><td>80-600</td><td>电池过放电压 (0.1V)</td><td style="color:#ef4444">Read Only</td></tr>
|
|
|
- <tr><td>Boost Voltage</td><td>40005 (0x0004)</td><td>80-600</td><td>电池提升电压 (0.1V)</td><td style="color:#ef4444">Read Only</td></tr>
|
|
|
- <tr><td>Full Charge Voltage</td><td>40006 (0x0005)</td><td>80-600</td><td>电池充满电压 (0.1V) - 仅自定义电池可写</td><td style="color:#eab308">R/W (Custom only)</td></tr>
|
|
|
- <tr><td>Recovery Voltage</td><td>40007 (0x0006)</td><td>80-600</td><td>电池过放恢复电压 (0.1V)</td><td style="color:#ef4444">Read Only</td></tr>
|
|
|
- <tr><td>485 Address</td><td>40008 (0x0007)</td><td>0-247</td><td>设备485通信地址</td><td style="color:#22c55e">R/W</td></tr>
|
|
|
- <tr><td>Charge Mode</td><td>40009 (0x0008)</td><td>0-1</td><td>0=MPPT 1=DCDC</td><td style="color:#22c55e">R/W</td></tr>
|
|
|
- </tbody>
|
|
|
- </table>
|
|
|
+ </div>
|
|
|
+ <div class="table-container" style="margin-top:20px">
|
|
|
+ <table>
|
|
|
+ <thead><tr><th>Register</th><th>Address</th><th>Range</th><th>Description</th><th>Access</th></tr></thead>
|
|
|
+ <tbody>
|
|
|
+ <tr><td>Battery Type</td><td>40001 (0x0000)</td><td>0-5</td><td>0=胶体 1=密封 2=磷酸铁锂 3=三元锂电 4=自定义 5=加水</td><td style="color:#22c55e">R/W</td></tr>
|
|
|
+ <tr><td>Battery Level</td><td>40002 (0x0001)</td><td>0-6</td><td>0=自动识别 1=12V 2=24V 3=36V 4=48V 5=60V 6=72V</td><td style="color:#22c55e">R/W</td></tr>
|
|
|
+ <tr><td>Charge Current Limit</td><td>40003 (0x0002)</td><td>5-100</td><td>PV充电电流限制 (%)</td><td style="color:#22c55e">R/W</td></tr>
|
|
|
+ <tr><td>Over-discharge Voltage</td><td>40004 (0x0003)</td><td>80-600</td><td>电池过放电压 (0.1V)</td><td style="color:#ef4444">Read Only</td></tr>
|
|
|
+ <tr><td>Boost Voltage</td><td>40005 (0x0004)</td><td>80-600</td><td>电池提升电压 (0.1V)</td><td style="color:#ef4444">Read Only</td></tr>
|
|
|
+ <tr><td>Full Charge Voltage</td><td>40006 (0x0005)</td><td>80-600</td><td>电池充满电压 (0.1V) - 仅自定义电池可写</td><td style="color:#eab308">R/W (Custom only)</td></tr>
|
|
|
+ <tr><td>Recovery Voltage</td><td>40007 (0x0006)</td><td>80-600</td><td>电池过放恢复电压 (0.1V)</td><td style="color:#ef4444">Read Only</td></tr>
|
|
|
+ <tr><td>485 Address</td><td>40008 (0x0007)</td><td>0-247</td><td>设备485通信地址</td><td style="color:#22c55e">R/W</td></tr>
|
|
|
+ <tr><td>Charge Mode</td><td>40009 (0x0008)</td><td>0-1</td><td>0=MPPT 1=DCDC</td><td style="color:#22c55e">R/W</td></tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
@@ -288,11 +301,17 @@ async function fetchStatus() {
|
|
|
} catch(e) {}
|
|
|
}
|
|
|
|
|
|
+// Helper: format Date to datetime-local string in local timezone
|
|
|
+function toLocalDatetime(d) {
|
|
|
+ const pad = n => String(n).padStart(2, '0');
|
|
|
+ return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
|
+}
|
|
|
+
|
|
|
function setQuickRange(hours) {
|
|
|
const now = new Date();
|
|
|
const start = new Date(now.getTime() - hours * 3600000);
|
|
|
- document.getElementById('hist-start').value = start.toISOString().slice(0, 16);
|
|
|
- document.getElementById('hist-end').value = now.toISOString().slice(0, 16);
|
|
|
+ document.getElementById('hist-start').value = toLocalDatetime(start);
|
|
|
+ document.getElementById('hist-end').value = toLocalDatetime(now);
|
|
|
loadHistory(1);
|
|
|
}
|
|
|
|
|
|
@@ -388,11 +407,15 @@ fetchRealtime();
|
|
|
fetchStatus();
|
|
|
setInterval(fetchRealtime, 2000);
|
|
|
setInterval(fetchStatus, 10000);
|
|
|
-// Set default time range
|
|
|
+// Set default time range: yesterday to today in China timezone (UTC+8)
|
|
|
const now = new Date();
|
|
|
-const start = new Date(now.getTime() - 24*3600000);
|
|
|
-document.getElementById('hist-end').value = now.toISOString().slice(0,16);
|
|
|
-document.getElementById('hist-start').value = start.toISOString().slice(0,16);
|
|
|
+// Convert to China timezone (UTC+8)
|
|
|
+const chinaOffset = 8 * 60;
|
|
|
+const localOffset = now.getTimezoneOffset();
|
|
|
+const chinaTime = new Date(now.getTime() + (chinaOffset + localOffset) * 60000);
|
|
|
+const yesterday = new Date(chinaTime.getTime() - 24*3600000);
|
|
|
+document.getElementById('hist-end').value = toLocalDatetime(chinaTime);
|
|
|
+document.getElementById('hist-start').value = toLocalDatetime(yesterday);
|
|
|
|
|
|
// ===== Settings Page Functions =====
|
|
|
const paramInfo = {
|