safesight-control/internal/web/ui/templates/config_versions.html

129 lines
4.6 KiB
HTML

{{define "config_versions"}}
{{template "device_header" .}}
<div class="card">
<div class="section-title">
<div>
<h2>配置历史</h2>
<div class="muted small">每次下发配置时自动保存的快照,可对比任意两个版本。</div>
</div>
<div class="actions">
<a class="btn ghost" href="/devices/{{.Device.DeviceID}}">← 返回设备</a>
</div>
</div>
{{if .ConfigVersions}}
<div style="margin-bottom:16px">
<span style="font-size:12px;color:var(--muted);margin-right:8px">对比:</span>
<select id="version-a" style="width:200px;font-size:12px">
<option value="">选择版本 A</option>
{{range .ConfigVersions}}
<option value="{{.ID}}">{{.CreatedAt}} · {{shortID .ConfigID}}</option>
{{end}}
</select>
<span style="margin:0 8px;color:var(--muted)">vs</span>
<select id="version-b" style="width:200px;font-size:12px">
<option value="">选择版本 B</option>
{{range .ConfigVersions}}
<option value="{{.ID}}">{{.CreatedAt}} · {{shortID .ConfigID}}</option>
{{end}}
</select>
</div>
<div id="diff-result" style="display:none">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
<div>
<div style="font-size:11px;color:var(--muted);margin-bottom:4px" id="label-a"></div>
<pre id="diff-a" style="height:400px;overflow:auto;font-size:11px;line-height:1.5;margin:0"></pre>
</div>
<div>
<div style="font-size:11px;color:var(--muted);margin-bottom:4px" id="label-b"></div>
<pre id="diff-b" style="height:400px;overflow:auto;font-size:11px;line-height:1.5;margin:0"></pre>
</div>
</div>
</div>
<div class="table-wrap" style="margin-top:16px">
<table>
<thead><tr><th>时间</th><th>配置 ID</th><th>大小</th></tr></thead>
<tbody>
{{range .ConfigVersions}}
<tr>
<td class="mono small">{{.CreatedAt}}</td>
<td class="mono small">{{shortID .ConfigID}}</td>
<td class="small">{{len .ConfigJSON}} 字符</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<div class="empty-state">
<div class="empty-title">暂无配置历史</div>
<div class="muted">通过管控台或部署向导下发配置后,快照将自动保存在这里。</div>
</div>
{{end}}
</div>
<script>
(function(){
var selA = document.getElementById("version-a");
var selB = document.getElementById("version-b");
var diffResult = document.getElementById("diff-result");
if (!selA || !selB) return;
function formatJSON(s) {
try {
return JSON.stringify(JSON.parse(s), null, 2);
} catch(e) {
return s;
}
}
function compare() {
var a = selA.value, b = selB.value;
if (!a || !b) { diffResult.style.display = "none"; return; }
var labelA = selA.options[selA.selectedIndex].text;
var labelB = selB.options[selB.selectedIndex].text;
document.getElementById("label-a").textContent = labelA;
document.getElementById("label-b").textContent = labelB;
Promise.all([
fetch("/devices/{{.Device.DeviceID}}/config-versions/"+a).then(function(r){return r.json()}),
fetch("/devices/{{.Device.DeviceID}}/config-versions/"+b).then(function(r){return r.json()})
]).then(function(results){
var linesA = formatJSON(results[0].config_json).split("\n");
var linesB = formatJSON(results[1].config_json).split("\n");
var maxLen = Math.max(linesA.length, linesB.length);
var outA = [], outB = [];
for (var i = 0; i < maxLen; i++) {
var la = linesA[i] || "", lb = linesB[i] || "";
if (la === lb) {
outA.push(la);
outB.push(lb);
} else {
outA.push('\x1b[41m' + la + '\x1b[0m');
outB.push('\x1b[42m' + lb + '\x1b[0m');
}
}
var preA = document.getElementById("diff-a");
var preB = document.getElementById("diff-b");
function render(lines) {
return lines.map(function(l){
if (l.indexOf('\x1b[41m') >= 0) return '<span style="background:rgba(220,70,70,.25);display:block">' + l.replace(/\x1b\[41m/g,"").replace(/\x1b\[0m/g,"") + '</span>';
if (l.indexOf('\x1b[42m') >= 0) return '<span style="background:rgba(70,180,70,.25);display:block">' + l.replace(/\x1b\[42m/g,"").replace(/\x1b\[0m/g,"") + '</span>';
return '<span style="display:block">' + l + '</span>';
}).join("");
}
preA.innerHTML = render(outA);
preB.innerHTML = render(outB);
diffResult.style.display = "";
});
}
selA.addEventListener("change", compare);
selB.addEventListener("change", compare);
})();
</script>
{{end}}