fix: separate runtime refresh from config reload
This commit is contained in:
109
web/src/main.js
109
web/src/main.js
@@ -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 = {}) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user