This commit is contained in:
@@ -131,7 +131,7 @@
|
||||
<label>Tiêu đề widget (tùy chọn)</label>
|
||||
<input data-field="title" type="text" value="${escapeHtml(widget.title || "")}" />
|
||||
</div>
|
||||
<p class="mutedNote">Tạm dừng / tiếp tục mission đang chạy trên robot.</p>`;
|
||||
<p class="mutedNote">Tạm dừng / tiếp tục / hủy mission đang chạy trên robot.</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,9 +206,14 @@
|
||||
const paused = state === "paused" || snap?.runner?.paused;
|
||||
const running = state === "running" || paused;
|
||||
bodyEl.innerHTML = `
|
||||
<button type="button" class="dashboardPauseBtn ${paused ? "is-paused" : ""}" data-pause-action="${paused ? "continue" : "pause"}" ${running ? "" : "disabled"}>
|
||||
${paused ? "Continue" : "Pause"}
|
||||
</button>
|
||||
<div class="dashboardRunnerControls">
|
||||
<button type="button" class="dashboardPauseBtn ${paused ? "is-paused" : ""}" data-pause-action="${paused ? "continue" : "pause"}" ${running ? "" : "disabled"}>
|
||||
${paused ? "Continue" : "Pause"}
|
||||
</button>
|
||||
<button type="button" class="dashboardCancelBtn" data-cancel-mission ${running ? "" : "disabled"}>
|
||||
Hủy mission
|
||||
</button>
|
||||
</div>
|
||||
<p class="mutedNote dashboardWidgetHint">${running ? (paused ? "Mission đang tạm dừng" : "Mission đang chạy") : "Không có mission đang chạy"}</p>`;
|
||||
bodyEl.querySelector("[data-pause-action]")?.addEventListener("click", async (evt) => {
|
||||
const action = evt.currentTarget.dataset.pauseAction;
|
||||
@@ -219,6 +224,13 @@
|
||||
alert(e.message);
|
||||
}
|
||||
});
|
||||
bodyEl.querySelector("[data-cancel-mission]")?.addEventListener("click", async () => {
|
||||
try {
|
||||
await missions()?.cancelRunner?.();
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderWidget(widget) {
|
||||
|
||||
@@ -547,7 +547,10 @@
|
||||
<div class="cardTitle">Mission queue</div>
|
||||
<div class="cardSub">Thêm mission bằng biểu tượng queue — robot chạy theo thứ tự từ trên xuống.</div>
|
||||
</div>
|
||||
<button id="missionQueueClearBtn" type="button" class="btn subtle danger">Xóa queue</button>
|
||||
<div class="missionQueueCardActions">
|
||||
<button id="missionQueueCancelBtn" type="button" class="btn subtle danger" title="Hủy mission đang chạy">Hủy chạy</button>
|
||||
<button id="missionQueueClearBtn" type="button" class="btn subtle danger">Xóa queue</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cardBody">
|
||||
<div id="missionQueueRunner" class="missionQueueRunner mutedNote">—</div>
|
||||
|
||||
@@ -473,6 +473,7 @@
|
||||
executing: "Đang chạy",
|
||||
completed: "Xong",
|
||||
failed: "Lỗi",
|
||||
cancelled: "Đã hủy",
|
||||
};
|
||||
return map[status] || status;
|
||||
}
|
||||
@@ -639,6 +640,12 @@
|
||||
await refreshQueue();
|
||||
}
|
||||
|
||||
async function cancelRunner() {
|
||||
if (!confirm("Hủy mission đang chạy? (thoát loop và dừng ngay)")) return;
|
||||
await missionApi("/api/mission_queue/cancel", { method: "POST", body: "{}" });
|
||||
await refreshQueue();
|
||||
}
|
||||
|
||||
function openQueueDialog(missionId) {
|
||||
const mission = findMission(missionId);
|
||||
if (!mission) return;
|
||||
@@ -850,15 +857,19 @@
|
||||
|
||||
row.innerHTML = `
|
||||
<div class="missionDragHandle" draggable="true" title="Kéo để sắp xếp" aria-label="Kéo để sắp xếp">↕</div>
|
||||
<div class="missionActionMain">
|
||||
<div class="missionActionLabelRow">
|
||||
<span class="missionActionIcon ${iconClass}">${iconChar}</span>
|
||||
<span class="missionActionLabel">${escapeHtml(action.label)}</span>
|
||||
<div class="missionActionTop">
|
||||
<div class="missionActionMain">
|
||||
<div class="missionActionLabelRow">
|
||||
<span class="missionActionIcon ${iconClass}">${iconChar}</span>
|
||||
<span class="missionActionLabel">${escapeHtml(action.label)}</span>
|
||||
</div>
|
||||
<div class="missionActionSummary">${escapeHtml(actionSummary(action))}</div>
|
||||
</div>
|
||||
<div class="missionActionSummary">${escapeHtml(actionSummary(action))}</div>
|
||||
</div>
|
||||
<button type="button" class="iconBtn" data-config="${action.id}" title="Cấu hình">⚙</button>
|
||||
<button type="button" class="iconBtn danger" data-remove="${action.id}" title="Xóa">×</button>`;
|
||||
<div class="missionActionBtns">
|
||||
<button type="button" class="iconBtn" data-config="${action.id}" title="Cấu hình">⚙</button>
|
||||
<button type="button" class="iconBtn danger" data-remove="${action.id}" title="Xóa">×</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
if (action.type === "loop" && Array.isArray(action.children)) {
|
||||
const loop = document.createElement("div");
|
||||
@@ -876,11 +887,15 @@
|
||||
renderActionRows(action.children, `${listPath}.${action.id}`, drop);
|
||||
}
|
||||
loop.appendChild(drop);
|
||||
row.querySelector(".missionActionMain").appendChild(loop);
|
||||
row.appendChild(loop);
|
||||
}
|
||||
|
||||
row.querySelector("[data-config]").addEventListener("click", () => openActionConfig(action.id));
|
||||
row.querySelector("[data-remove]").addEventListener("click", () => {
|
||||
row.querySelector("[data-config]").addEventListener("click", (evt) => {
|
||||
evt.stopPropagation();
|
||||
openActionConfig(action.id);
|
||||
});
|
||||
row.querySelector("[data-remove]").addEventListener("click", (evt) => {
|
||||
evt.stopPropagation();
|
||||
removeActionFromTree(action.id);
|
||||
renderMissionEditor();
|
||||
});
|
||||
@@ -1308,6 +1323,9 @@
|
||||
});
|
||||
|
||||
el("missionQueueClearBtn")?.addEventListener("click", clearQueue);
|
||||
el("missionQueueCancelBtn")?.addEventListener("click", () => {
|
||||
cancelRunner().catch((e) => alert(e.message));
|
||||
});
|
||||
el("missionQueueForm")?.addEventListener("submit", submitQueueDialog);
|
||||
}
|
||||
|
||||
@@ -1327,6 +1345,7 @@
|
||||
enqueueMission,
|
||||
pauseRunner,
|
||||
continueRunner,
|
||||
cancelRunner,
|
||||
refreshQueue,
|
||||
clearQueue,
|
||||
getQueueSnapshot,
|
||||
|
||||
@@ -622,6 +622,9 @@ canvas {
|
||||
.missionQueueStatus.executing { background: #dbeafe; color: #1d4ed8; }
|
||||
.missionQueueStatus.completed { background: #d1fae5; color: #047857; }
|
||||
.missionQueueStatus.failed { background: #fee2e2; color: #b91c1c; }
|
||||
.missionQueueStatus.cancelled { background: #f3f4f6; color: #6b7280; }
|
||||
.missionQueueItem.status-cancelled { opacity: 0.72; }
|
||||
.missionQueueCardActions { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.missionQueueRunner {
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
@@ -730,7 +733,7 @@ canvas {
|
||||
|
||||
.missionActionRow {
|
||||
display: grid;
|
||||
grid-template-columns: 32px 1fr auto auto;
|
||||
grid-template-columns: 32px 1fr;
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
padding: 10px 12px;
|
||||
@@ -739,6 +742,28 @@ canvas {
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
.missionActionTop {
|
||||
grid-column: 2;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.missionActionBtns {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.missionLoopBlock {
|
||||
grid-column: 2;
|
||||
margin-top: 0;
|
||||
border-radius: 10px;
|
||||
border: 1px dashed rgba(124, 58, 237, 0.35);
|
||||
background: rgba(124, 58, 237, 0.04);
|
||||
padding: 10px;
|
||||
}
|
||||
.missionActionRow.dragging { opacity: 0.45; }
|
||||
.missionActionRow.dropBefore { box-shadow: inset 0 3px 0 var(--accent); }
|
||||
.missionActionRow.dropAfter { box-shadow: inset 0 -3px 0 var(--accent); }
|
||||
@@ -789,13 +814,6 @@ canvas {
|
||||
.iconBtn:hover { border-color: rgba(37, 99, 235, 0.35); color: var(--accent); background: #eff6ff; }
|
||||
.iconBtn.danger:hover { border-color: rgba(239, 68, 68, 0.35); color: var(--danger); background: #fef2f2; }
|
||||
|
||||
.missionLoopBlock {
|
||||
margin-top: 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px dashed rgba(124, 58, 237, 0.35);
|
||||
background: rgba(124, 58, 237, 0.04);
|
||||
padding: 10px;
|
||||
}
|
||||
.missionLoopLabel {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
@@ -950,6 +968,21 @@ canvas {
|
||||
}
|
||||
.dashboardPauseBtn.is-paused { background: #ecfdf5; color: #047857; }
|
||||
.dashboardPauseBtn:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
.dashboardRunnerControls { display: grid; gap: 8px; }
|
||||
.dashboardCancelBtn {
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
border: 1px solid rgba(239, 68, 68, 0.35);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
background: #fef2f2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
.dashboardCancelBtn:hover:not(:disabled) { background: #fee2e2; }
|
||||
.dashboardCancelBtn:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
.dashboardInfoCard .dashboardInfoGrid { display: grid; gap: 8px; }
|
||||
.dashboardEmpty { text-align: center; padding: 12px 0 0; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user