feat: lazy-load device resource status via AJAX

This commit is contained in:
tian 2026-05-06 14:00:16 +08:00
parent 10c21d8fb8
commit 2289802dac
2 changed files with 92 additions and 48 deletions

View File

@ -610,6 +610,7 @@ func (u *UI) Routes() (chi.Router, error) {
r.Get("/system", u.pageSystem)
r.Get("/system/db-backup", u.pageSystemDBBackup)
r.Get("/resources", u.pageResources)
r.Get("/api/resources/device-status", u.apiResourceDeviceStatus)
r.Post("/system/db-restore", u.actionSystemDBRestore)
r.Get("/api/graph-node-types", u.apiGraphNodeTypes)
r.Get("/device-config", u.pageDeviceConfig)
@ -1418,14 +1419,31 @@ func (u *UI) pageResources(w http.ResponseWriter, r *http.Request) {
data.OnlineCount++
}
}
board := service.ResourceStatusBoard{}
if strings.TrimSpace(u.dbPath) != "" {
if store, err := storage.OpenSQLite(u.dbPath); err == nil {
resourcesRepo := storage.NewResourcesRepo(store.DB())
if items, err := resourcesRepo.List(); err == nil {
data.StandardResources = items
}
_ = store.Close()
}
}
u.render(w, r, "resources", data)
}
func (u *UI) apiResourceDeviceStatus(w http.ResponseWriter, r *http.Request) {
u.ensureDevicesLoaded()
devices := u.registry.GetDevices()
board := service.ResourceStatusBoard{
Summary: service.ResourceStatusSummary{Devices: len(devices)},
}
if strings.TrimSpace(u.dbPath) != "" {
if store, err := storage.OpenSQLite(u.dbPath); err == nil {
resourcesRepo := storage.NewResourcesRepo(store.DB())
if items, err := resourcesRepo.List(); err == nil {
board.Summary.StandardResources = len(items)
installed := map[string][]service.InstalledResourceStatus{}
for _, device := range data.Devices {
for _, device := range devices {
if device == nil || !device.Online {
continue
}
@ -1434,13 +1452,13 @@ func (u *UI) pageResources(w http.ResponseWriter, r *http.Request) {
installed[device.DeviceID] = items
}
}
board = service.BuildResourceStatusBoard(data.StandardResources, data.Devices, installed)
board = service.BuildResourceStatusBoard(items, devices, installed)
}
_ = store.Close()
}
}
data.ResourceStatusBoard = &board
u.render(w, r, "resources", data)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(board)
}
func (u *UI) actionResourceSync(w http.ResponseWriter, r *http.Request) {

View File

@ -1,5 +1,4 @@
{{define "resources"}}
{{$board := .ResourceStatusBoard}}
<div class="card">
<div class="section-title">
<div>
@ -7,7 +6,7 @@
<div class="form-hint">统一维护标准资源,设备侧通过任务同步。当前支持人脸库等资源类型。</div>
</div>
<div class="actions compact">
<form method="post" action="/ui/resources/sync">
<form method="post" action="/ui/resources/sync" id="sync-form">
{{range .Devices}}{{if .Online}}<input type="hidden" name="device_id" value="{{.DeviceID}}">{{end}}{{end}}
<input type="hidden" name="action" value="resource_sync_all">
<button class="btn" type="submit">同步全部资源</button>
@ -17,7 +16,7 @@
<div class="stats">
<div class="stat accent-teal">
<div class="k metric-label">{{icon "template"}}<span>标准资源总数</span></div>
<div class="v">{{if $board}}{{$board.Summary.StandardResources}}{{else}}0{{end}}</div>
<div class="v" id="stat-standard">{{len .StandardResources}}</div>
</div>
<div class="stat accent-green">
<div class="k metric-label">{{icon "devices"}}<span>在线设备数</span></div>
@ -25,15 +24,15 @@
</div>
<div class="stat accent-teal">
<div class="k metric-label">{{icon "assets"}}<span>完整设备数</span></div>
<div class="v">{{if $board}}{{$board.Summary.CompleteDevices}}{{else}}0{{end}}</div>
<div class="v" id="stat-complete">-</div>
</div>
<div class="stat accent-amber">
<div class="k metric-label">{{icon "warn"}}<span>缺失设备数</span></div>
<div class="v">{{if $board}}{{$board.Summary.MissingDevices}}{{else}}0{{end}}</div>
<div class="v" id="stat-missing">-</div>
</div>
<div class="stat accent-amber">
<div class="k metric-label">{{icon "service"}}<span>不一致设备数</span></div>
<div class="v">{{if $board}}{{$board.Summary.MismatchDevices}}{{else}}0{{end}}</div>
<div class="v" id="stat-mismatch">-</div>
</div>
</div>
</div>
@ -67,15 +66,15 @@
</div>
</div>
<div class="card">
<div class="card" id="device-status-card">
<div class="section-title">
<div>
<h2 class="title-with-icon">{{icon "devices"}}<span>设备资源状态</span></h2>
<div class="form-hint">按标准资源逐项比对设备已安装状态,缺失和不一致会直接标出。</div>
<div class="form-hint">按标准资源逐项比对设备已安装状态,缺失和不一致会直接标出。<span id="status-hint"></span></div>
</div>
</div>
<div class="table-wrap">
<table class="models-status-table">
<table class="models-status-table" id="device-status-table">
<thead>
<tr>
<th>设备</th>
@ -83,42 +82,69 @@
<th class="model-extra-col">非标资源</th>
</tr>
</thead>
<tbody>
{{if and $board (gt (len $board.Rows) 0)}}
{{range $board.Rows}}
<tr>
<td>
<div class="table-key">{{.DeviceName}}</div>
<div class="muted small mono">{{.DeviceID}}</div>
</td>
{{range .Cells}}
<td>
{{if eq .Status "ok"}}<span class="pill ok">一致</span>
{{else if eq .Status "mismatch"}}<span class="pill warn">不一致</span>
{{else}}<span class="pill bad">缺失</span>{{end}}
</td>
{{end}}
<td>
{{if gt .ExtraCount 0}}
<details class="mini-details">
<summary>{{.ExtraCount}} 个 · 更多</summary>
<div class="mini-details-body">
{{range .ExtraResources}}
<div class="mini-details-item mono">{{.Name}}</div>
{{end}}
</div>
</details>
{{else}}
<span class="muted">0</span>
{{end}}
</td>
</tr>
{{end}}
{{else}}
<tr><td colspan="99" class="muted">暂无设备资源状态。请先确保设备在线且 agent 实现了 GET /v1/resources/status 端点。</td></tr>
{{end}}
<tbody id="device-status-body">
<tr><td colspan="99" class="muted">正在加载设备资源状态…</td></tr>
</tbody>
</table>
</div>
</div>
<script>
(function() {
var cols = {{len .StandardResources}};
var typeLabels = {};
{{range .StandardResources}}
typeLabels["{{.Name}}"] = "{{resourceTypeLabel .ResourceType}}";
{{end}}
function pill(cls, text) {
var m = {"ok":"一致","mismatch":"不一致","missing":"缺失"};
return '<span class="pill ' + cls + '">' + (m[text]||text) + '</span>';
}
function renderBoard(board) {
document.getElementById("stat-complete").textContent = board.summary.complete_devices;
document.getElementById("stat-missing").textContent = board.summary.missing_devices;
document.getElementById("stat-mismatch").textContent = board.summary.mismatch_devices;
var body = document.getElementById("device-status-body");
if (!board.rows || board.rows.length === 0) {
body.innerHTML = '<tr><td colspan="99" class="muted">暂无设备资源状态。请先确保设备在线且 agent 实现了 GET /v1/resources/status 端点。</td></tr>';
return;
}
var html = '';
for (var i = 0; i < board.rows.length; i++) {
var row = board.rows[i];
html += '<tr>';
html += '<td><div class="table-key">' + esc(row.device_name) + '</div><div class="muted small mono">' + esc(row.device_id) + '</div></td>';
for (var j = 0; j < row.cells.length; j++) {
var c = row.cells[j];
html += '<td>' + pill(c.status === "ok" ? "ok" : c.status === "mismatch" ? "warn" : "bad", c.status) + '</td>';
}
html += '<td>';
if (row.extra_resource_count > 0) {
html += '<details class="mini-details"><summary>' + row.extra_resource_count + ' 个 · 更多</summary><div class="mini-details-body">';
for (var k = 0; k < row.extra_resources.length; k++) {
html += '<div class="mini-details-item mono">' + esc(row.extra_resources[k].name) + '</div>';
}
html += '</div></details>';
} else {
html += '<span class="muted">0</span>';
}
html += '</td></tr>';
}
body.innerHTML = html;
document.getElementById("status-hint").textContent = ' (已刷新)';
}
function esc(s) { return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
fetch('/ui/api/resources/device-status')
.then(function(r) { return r.json(); })
.then(renderBoard)
.catch(function(e) {
document.getElementById("device-status-body").innerHTML = '<tr><td colspan="99" class="muted">加载失败,请刷新页面重试。</td></tr>';
});
})();
</script>
{{end}}