feat(control): batch start/stop auto control for all enabled units
Backend: - POST /api/control/unit/batch-start-auto — starts auto on all enabled units that are not fault/comm locked and not already running auto - POST /api/control/unit/batch-stop-auto — stops auto on all units Frontend (ops view): - Add "全部启动" / "全部停止" buttons in the unit sidebar header Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0077a4ad90
commit
757d6f9a3a
|
|
@ -410,6 +410,58 @@ pub async fn stop_auto_unit(
|
||||||
Ok(Json(json!({ "ok_msg": "Auto control stopped", "unit_id": unit_id })))
|
Ok(Json(json!({ "ok_msg": "Auto control stopped", "unit_id": unit_id })))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn batch_start_auto(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Result<impl IntoResponse, ApiErr> {
|
||||||
|
let units = crate::service::get_all_enabled_units(&state.pool).await?;
|
||||||
|
let mut started = Vec::new();
|
||||||
|
let mut skipped = Vec::new();
|
||||||
|
|
||||||
|
for unit in units {
|
||||||
|
let mut runtime = state.control_runtime.get_or_init(unit.id).await;
|
||||||
|
if runtime.auto_enabled {
|
||||||
|
skipped.push(unit.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if runtime.fault_locked || runtime.comm_locked {
|
||||||
|
skipped.push(unit.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
runtime.auto_enabled = true;
|
||||||
|
runtime.state = crate::control::runtime::UnitRuntimeState::Stopped;
|
||||||
|
runtime.current_stop_elapsed_sec = 0;
|
||||||
|
state.control_runtime.upsert(runtime).await;
|
||||||
|
let _ = state
|
||||||
|
.event_manager
|
||||||
|
.send(crate::event::AppEvent::AutoControlStarted { unit_id: unit.id });
|
||||||
|
started.push(unit.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Json(json!({ "started": started, "skipped": skipped })))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn batch_stop_auto(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Result<impl IntoResponse, ApiErr> {
|
||||||
|
let units = crate::service::get_all_enabled_units(&state.pool).await?;
|
||||||
|
let mut stopped = Vec::new();
|
||||||
|
|
||||||
|
for unit in units {
|
||||||
|
let mut runtime = state.control_runtime.get_or_init(unit.id).await;
|
||||||
|
if !runtime.auto_enabled {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
runtime.auto_enabled = false;
|
||||||
|
state.control_runtime.upsert(runtime).await;
|
||||||
|
let _ = state
|
||||||
|
.event_manager
|
||||||
|
.send(crate::event::AppEvent::AutoControlStopped { unit_id: unit.id });
|
||||||
|
stopped.push(unit.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Json(json!({ "stopped": stopped })))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn ack_fault_unit(
|
pub async fn ack_fault_unit(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(unit_id): Path<Uuid>,
|
Path(unit_id): Path<Uuid>,
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,14 @@ fn build_router(state: AppState) -> Router {
|
||||||
"/api/control/unit/{unit_id}/stop-auto",
|
"/api/control/unit/{unit_id}/stop-auto",
|
||||||
post(handler::control::stop_auto_unit),
|
post(handler::control::stop_auto_unit),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/control/unit/batch-start-auto",
|
||||||
|
post(handler::control::batch_start_auto),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/control/unit/batch-stop-auto",
|
||||||
|
post(handler::control::batch_stop_auto),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/control/unit/{unit_id}/ack-fault",
|
"/api/control/unit/{unit_id}/ack-fault",
|
||||||
post(handler::control::ack_fault_unit),
|
post(handler::control::ack_fault_unit),
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@
|
||||||
<aside class="ops-unit-sidebar">
|
<aside class="ops-unit-sidebar">
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<h2>控制单元</h2>
|
<h2>控制单元</h2>
|
||||||
|
<div class="ops-batch-actions">
|
||||||
|
<button type="button" class="secondary" id="batchStartAutoBtn" title="启动所有未锁定单元的自动控制">全部启动</button>
|
||||||
|
<button type="button" class="danger" id="batchStopAutoBtn" title="停止所有单元的自动控制">全部停止</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="list ops-unit-list" id="opsUnitList"></div>
|
<div class="list ops-unit-list" id="opsUnitList"></div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>PLC Control</title>
|
<title>PLC Control</title>
|
||||||
<link rel="stylesheet" href="/ui/styles.css?v=20260325c" />
|
<link rel="stylesheet" href="/ui/styles.css?v=20260325d" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div data-partial="/ui/html/topbar.html"></div>
|
<div data-partial="/ui/html/topbar.html"></div>
|
||||||
|
|
@ -22,6 +22,6 @@
|
||||||
<div data-partial="/ui/html/modals.html"></div>
|
<div data-partial="/ui/html/modals.html"></div>
|
||||||
<div data-partial="/ui/html/api-doc-drawer.html"></div>
|
<div data-partial="/ui/html/api-doc-drawer.html"></div>
|
||||||
|
|
||||||
<script type="module" src="/ui/js/index.js?v=20260325c"></script>
|
<script type="module" src="/ui/js/index.js?v=20260325d"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ export const dom = {
|
||||||
statusText: byId("statusText"),
|
statusText: byId("statusText"),
|
||||||
wsDot: byId("wsDot"),
|
wsDot: byId("wsDot"),
|
||||||
wsLabel: byId("wsLabel"),
|
wsLabel: byId("wsLabel"),
|
||||||
|
batchStartAutoBtn: byId("batchStartAutoBtn"),
|
||||||
|
batchStopAutoBtn: byId("batchStopAutoBtn"),
|
||||||
tabOps: byId("tabOps"),
|
tabOps: byId("tabOps"),
|
||||||
tabConfig: byId("tabConfig"),
|
tabConfig: byId("tabConfig"),
|
||||||
opsUnitList: byId("opsUnitList"),
|
opsUnitList: byId("opsUnitList"),
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,18 @@ function renderOpsEquipments(equipments) {
|
||||||
export function startOps() {
|
export function startOps() {
|
||||||
renderOpsUnits();
|
renderOpsUnits();
|
||||||
loadAllEquipmentCards();
|
loadAllEquipmentCards();
|
||||||
|
|
||||||
|
dom.batchStartAutoBtn?.addEventListener("click", () => {
|
||||||
|
apiFetch("/api/control/unit/batch-start-auto", { method: "POST" })
|
||||||
|
.then(() => loadUnits())
|
||||||
|
.catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.batchStopAutoBtn?.addEventListener("click", () => {
|
||||||
|
apiFetch("/api/control/unit/batch-stop-auto", { method: "POST" })
|
||||||
|
.then(() => loadUnits())
|
||||||
|
.catch(() => {});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Called by WS handler when a unit's runtime changes — syncs manual button disabled state. */
|
/** Called by WS handler when a unit's runtime changes — syncs manual button disabled state. */
|
||||||
|
|
|
||||||
|
|
@ -323,6 +323,16 @@ body {
|
||||||
padding: 7px 12px;
|
padding: 7px 12px;
|
||||||
border-bottom: 1px solid var(--border-light);
|
border-bottom: 1px solid var(--border-light);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.ops-batch-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.ops-batch-actions button {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2, h3 {
|
h2, h3 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue