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:
caoqianming 2026-03-25 13:05:23 +08:00
parent 0077a4ad90
commit 757d6f9a3a
7 changed files with 90 additions and 2 deletions

View File

@ -410,6 +410,58 @@ pub async fn stop_auto_unit(
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(
State(state): State<AppState>,
Path(unit_id): Path<Uuid>,

View File

@ -235,6 +235,14 @@ fn build_router(state: AppState) -> Router {
"/api/control/unit/{unit_id}/stop-auto",
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(
"/api/control/unit/{unit_id}/ack-fault",
post(handler::control::ack_fault_unit),

View File

@ -3,6 +3,10 @@
<aside class="ops-unit-sidebar">
<div class="panel-head">
<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 class="list ops-unit-list" id="opsUnitList"></div>
</aside>

View File

@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>PLC Control</title>
<link rel="stylesheet" href="/ui/styles.css?v=20260325c" />
<link rel="stylesheet" href="/ui/styles.css?v=20260325d" />
</head>
<body>
<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/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>
</html>

View File

@ -4,6 +4,8 @@ export const dom = {
statusText: byId("statusText"),
wsDot: byId("wsDot"),
wsLabel: byId("wsLabel"),
batchStartAutoBtn: byId("batchStartAutoBtn"),
batchStopAutoBtn: byId("batchStopAutoBtn"),
tabOps: byId("tabOps"),
tabConfig: byId("tabConfig"),
opsUnitList: byId("opsUnitList"),

View File

@ -190,6 +190,18 @@ function renderOpsEquipments(equipments) {
export function startOps() {
renderOpsUnits();
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. */

View File

@ -323,6 +323,16 @@ body {
padding: 7px 12px;
border-bottom: 1px solid var(--border-light);
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 {