fix: separate runtime refresh from config reload

This commit is contained in:
Yoilun
2026-04-29 15:53:01 +08:00
parent c81a20b2ea
commit 08c5d2e955
2 changed files with 107 additions and 15 deletions

View File

@@ -25,6 +25,8 @@ const state = {
image: null,
imageUrl: null,
status: "正在连接后端...",
configDirty: false,
calibrationDirty: false,
};
const app = document.querySelector("#app");
@@ -49,18 +51,17 @@ app.innerHTML = `
<main class="main">
<section class="status-line">
<span id="statusText"></span>
<button id="refreshAll" type="button">刷新</button>
<button id="refreshRuntimeData" type="button">刷新运行数据</button>
</section>
<section id="calibrationView" class="view">
<section class="toolbar">
<label>
RTSP 地址
RTSP 地址(用于抓帧)
<input id="rtspUrl" type="text" placeholder="rtsp://user:password@camera-ip:554/stream">
</label>
<button id="saveConfig" type="button">保存配置和标定</button>
<button id="captureSnapshot" type="button">抓取一帧</button>
<button id="saveCalibration" type="button">保存标定</button>
<button id="saveCalibration" type="button">保存标定</button>
</section>
<section class="calibration-grid">
@@ -70,7 +71,7 @@ app.innerHTML = `
<div class="button-stack">
<button id="undoPoint" type="button">撤销点</button>
<button id="clearRegion" type="button">清空当前区域</button>
<button id="loadConfigPolygons" type="button">载入当前配置区域</button>
<button id="loadConfigPolygons" type="button">放弃草稿并载入已保存标定</button>
</div>
</aside>
<section class="canvas-panel">
@@ -93,6 +94,10 @@ app.innerHTML = `
<section id="settingsView" class="view hidden">
<section class="settings-grid">
<label>
RTSP 地址
<input id="settingsRtspUrl" type="text" placeholder="rtsp://user:password@camera-ip:554/stream">
</label>
<label>
Camera ID
<input id="cameraId" type="text">
@@ -110,6 +115,10 @@ app.innerHTML = `
<input id="trashWindow" type="number" min="1">
</label>
</section>
<section class="settings-actions">
<button id="saveConfig" type="button">保存运行配置</button>
<button id="reloadConfig" type="button">重新载入后端配置</button>
</section>
<pre id="configPreview" class="config-preview"></pre>
</section>
</main>
@@ -121,6 +130,7 @@ const els = {
canvas: document.querySelector("#canvas"),
regionList: document.querySelector("#regionList"),
rtspUrl: document.querySelector("#rtspUrl"),
settingsRtspUrl: document.querySelector("#settingsRtspUrl"),
cameraId: document.querySelector("#cameraId"),
timezone: document.querySelector("#timezone"),
maxDwell: document.querySelector("#maxDwell"),
@@ -136,15 +146,16 @@ function boot() {
wireEvents();
loadDraftPolygons();
renderRegionList();
refreshAll();
loadInitialData();
}
function wireEvents() {
document.querySelectorAll(".tabs button").forEach((button) => {
button.addEventListener("click", () => setTab(button.dataset.tab));
});
document.querySelector("#refreshAll").addEventListener("click", refreshAll);
document.querySelector("#refreshRuntimeData").addEventListener("click", refreshRuntimeData);
document.querySelector("#saveConfig").addEventListener("click", saveConfig);
document.querySelector("#reloadConfig").addEventListener("click", reloadConfig);
document.querySelector("#captureSnapshot").addEventListener("click", captureSnapshot);
document.querySelector("#saveCalibration").addEventListener("click", saveCalibration);
document.querySelector("#undoPoint").addEventListener("click", undoPoint);
@@ -152,11 +163,23 @@ function wireEvents() {
document.querySelector("#loadConfigPolygons").addEventListener("click", loadPolygonsFromConfig);
els.canvas.addEventListener("click", addPoint);
window.addEventListener("resize", drawCanvas);
[els.rtspUrl, els.settingsRtspUrl, els.cameraId, els.timezone, els.maxDwell, els.trashWindow].forEach((input) => {
input.addEventListener("input", () => {
state.configDirty = true;
if (input === els.rtspUrl) {
els.settingsRtspUrl.value = els.rtspUrl.value;
}
if (input === els.settingsRtspUrl) {
els.rtspUrl.value = els.settingsRtspUrl.value;
}
renderConfigPreview();
});
});
}
async function refreshAll() {
async function loadInitialData() {
try {
setStatus("正在读取配置和事件...");
setStatus("正在读取配置和运行数据...");
const [config, summary, events] = await Promise.all([
apiJson("/api/manage/config"),
apiJson("/api/manage/summary"),
@@ -166,6 +189,7 @@ async function refreshAll() {
state.summary = summary;
state.events = events.items || [];
fillForm();
state.configDirty = false;
if (!hasAnyPolygon()) {
loadPolygonsFromConfig(false);
}
@@ -176,23 +200,54 @@ async function refreshAll() {
}
}
async function refreshRuntimeData() {
try {
setStatus("正在刷新运行数据...");
const [summary, events] = await Promise.all([
apiJson("/api/manage/summary"),
apiJson("/api/manage/events?limit=200"),
]);
state.summary = summary;
state.events = events.items || [];
render();
setStatus("运行数据已刷新");
} catch (error) {
setStatus(`刷新运行数据失败:${error.message}`);
}
}
async function reloadConfig() {
if (state.configDirty && !window.confirm("当前运行配置有未保存修改。确认放弃修改并重新载入后端配置?")) {
return;
}
try {
setStatus("正在重新载入后端配置...");
state.config = await apiJson("/api/manage/config");
fillForm();
state.configDirty = false;
render();
setStatus("后端配置已重新载入");
} catch (error) {
setStatus(`重新载入配置失败:${error.message}`);
}
}
async function saveConfig() {
try {
const payload = {
camera_id: els.cameraId.value.trim(),
timezone: els.timezone.value.trim(),
rtsp_url: els.rtspUrl.value.trim(),
rtsp_url: els.settingsRtspUrl.value.trim(),
thresholds: {
max_dwell_seconds: Number(els.maxDwell.value),
trash_confirmation_seconds: Number(els.trashWindow.value),
},
};
state.config = await apiJson("/api/manage/config", {method: "PUT", body: payload});
const calibrationSaved = await persistCalibration({requireAny: false});
saveDraftPolygons();
state.configDirty = false;
fillForm();
renderConfigPreview();
setStatus(calibrationSaved ? "配置和标定已保存" : "配置已保存;当前没有可保存的标定点");
setStatus("运行配置已保存");
} catch (error) {
setStatus(`保存配置失败:${error.message}`);
}
@@ -233,6 +288,7 @@ async function saveCalibration() {
try {
const saved = await persistCalibration({requireAny: true});
if (saved) {
state.calibrationDirty = false;
saveDraftPolygons();
render();
setStatus("标定已保存到项目配置");
@@ -277,6 +333,7 @@ function setTab(tab) {
function fillForm() {
const config = state.config || {};
els.rtspUrl.value = config.stream?.rtsp_url || "";
els.settingsRtspUrl.value = config.stream?.rtsp_url || "";
els.cameraId.value = config.camera_id || "";
els.timezone.value = config.timezone || "";
els.maxDwell.value = config.thresholds?.max_dwell_seconds || 10800;
@@ -287,6 +344,9 @@ function loadPolygonsFromConfig(updateStatus = true) {
if (!state.config) {
return;
}
if (updateStatus && state.calibrationDirty && !window.confirm("当前标定有未保存草稿。确认放弃草稿并载入已保存标定?")) {
return;
}
for (const zone of state.config.zones || []) {
if (zone.id && Array.isArray(zone.polygon)) {
state.polygons[zone.id] = zone.polygon.map(([x, y]) => ({x, y}));
@@ -295,6 +355,7 @@ function loadPolygonsFromConfig(updateStatus = true) {
if (Array.isArray(state.config.trash?.roi)) {
state.polygons.trash = state.config.trash.roi.map(([x, y]) => ({x, y}));
}
state.calibrationDirty = false;
saveDraftPolygons();
render();
if (updateStatus) {
@@ -342,6 +403,7 @@ function addPoint(event) {
const x = clamp(rawX / imageRect.width);
const y = clamp(rawY / imageRect.height);
state.polygons[state.activeRegion].push({x: round(x), y: round(y)});
state.calibrationDirty = true;
saveDraftPolygons();
render();
}
@@ -370,12 +432,14 @@ function getCanvasImageRect() {
function undoPoint() {
state.polygons[state.activeRegion].pop();
state.calibrationDirty = true;
saveDraftPolygons();
render();
}
function clearRegion() {
state.polygons[state.activeRegion] = [];
state.calibrationDirty = true;
saveDraftPolygons();
render();
}
@@ -526,7 +590,24 @@ function renderEvents() {
}
function renderConfigPreview() {
els.configPreview.textContent = JSON.stringify(state.config || {}, null, 2);
const preview = {
...(state.config || {}),
stream: {
...((state.config || {}).stream || {}),
rtsp_url: els.settingsRtspUrl.value,
},
camera_id: els.cameraId.value,
timezone: els.timezone.value,
thresholds: {
max_dwell_seconds: Number(els.maxDwell.value || 0),
trash_confirmation_seconds: Number(els.trashWindow.value || 0),
},
ui_state: {
config_dirty: state.configDirty,
calibration_dirty: state.calibrationDirty,
},
};
els.configPreview.textContent = JSON.stringify(preview, null, 2);
}
async function apiJson(path, options = {}) {

View File

@@ -113,7 +113,7 @@ label {
.toolbar {
display: grid;
grid-template-columns: minmax(360px, 1fr) auto auto auto;
grid-template-columns: minmax(360px, 1fr) auto auto;
gap: 12px;
align-items: end;
margin-bottom: 12px;
@@ -263,6 +263,17 @@ th {
border-radius: 8px;
}
.settings-actions {
display: flex;
gap: 10px;
justify-content: flex-start;
margin-bottom: 12px;
padding: 12px;
background: #ffffff;
border: 1px solid #d8dee5;
border-radius: 8px;
}
.config-preview {
margin: 0;
padding: 12px;