feat: 添加自动运行选择对话框,支持继续当前流程或重新开始,优化用户体验

This commit is contained in:
QLHazyCoder
2026-04-08 17:41:00 +08:00
parent cf3f0f1665
commit ce78a099d6
5 changed files with 329 additions and 77 deletions
+73
View File
@@ -677,6 +677,79 @@ header {
.log-line { animation: fadeIn 120ms ease-out; }
/* ============================================================
Modal
============================================================ */
.modal-overlay {
position: fixed;
inset: 0;
z-index: 1200;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
background: rgba(15, 17, 23, 0.32);
backdrop-filter: blur(2px);
}
.modal-overlay[hidden] {
display: none !important;
}
.modal-card {
width: min(100%, 320px);
background: var(--bg-base);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: var(--shadow-md), 0 18px 36px rgba(0, 0, 0, 0.18);
padding: 14px;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
}
.modal-title {
font-size: 14px;
font-weight: 700;
color: var(--text-primary);
}
.modal-close {
width: 26px;
height: 26px;
border: none;
border-radius: 999px;
background: transparent;
color: var(--text-muted);
cursor: pointer;
font-size: 18px;
line-height: 1;
transition: all var(--transition);
}
.modal-close:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.modal-message {
font-size: 13px;
line-height: 1.55;
color: var(--text-secondary);
margin-bottom: 14px;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
/* ============================================================
Toast Notifications
============================================================ */
+15
View File
@@ -174,6 +174,21 @@
<div id="log-area"></div>
</section>
<div id="auto-start-modal" class="modal-overlay" hidden>
<div class="modal-card">
<div class="modal-header">
<span class="modal-title">启动自动</span>
<button id="btn-auto-start-close" class="modal-close" type="button" aria-label="关闭">×</button>
</div>
<p id="auto-start-message" class="modal-message">检测到已有流程进度,选择继续当前还是重新开始。</p>
<div class="modal-actions">
<button id="btn-auto-start-cancel" class="btn btn-ghost btn-sm" type="button">取消</button>
<button id="btn-auto-start-restart" class="btn btn-outline btn-sm" type="button">重新开始</button>
<button id="btn-auto-start-continue" class="btn btn-primary btn-sm" type="button">继续当前</button>
</div>
</div>
</div>
<div id="toast-container"></div>
<script src="sidepanel.js"></script>
</body>
+96 -12
View File
@@ -36,6 +36,12 @@ const rowInbucketMailbox = document.getElementById('row-inbucket-mailbox');
const inputInbucketMailbox = document.getElementById('input-inbucket-mailbox');
const inputRunCount = document.getElementById('input-run-count');
const inputAutoSkipFailures = document.getElementById('input-auto-skip-failures');
const autoStartModal = document.getElementById('auto-start-modal');
const autoStartMessage = document.getElementById('auto-start-message');
const btnAutoStartClose = document.getElementById('btn-auto-start-close');
const btnAutoStartCancel = document.getElementById('btn-auto-start-cancel');
const btnAutoStartRestart = document.getElementById('btn-auto-start-restart');
const btnAutoStartContinue = document.getElementById('btn-auto-start-continue');
const STEP_DEFAULT_STATUSES = {
1: 'pending',
2: 'pending',
@@ -60,6 +66,7 @@ let currentAutoRun = {
let settingsDirty = false;
let settingsSaveInFlight = false;
let settingsAutoSaveTimer = null;
let autoStartChoiceResolver = null;
const EYE_OPEN_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>';
const EYE_CLOSED_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.94 10.94 0 0 1 12 19C5 19 1 12 1 12a21.77 21.77 0 0 1 5.06-6.94"/><path d="M9.9 4.24A10.94 10.94 0 0 1 12 5c7 0 11 7 11 7a21.86 21.86 0 0 1-2.16 3.19"/><path d="M1 1l22 22"/><path d="M14.12 14.12a3 3 0 1 1-4.24-4.24"/></svg>';
@@ -103,6 +110,33 @@ function dismissToast(toast) {
toast.addEventListener('animationend', () => toast.remove());
}
function resolveAutoStartChoice(choice) {
if (autoStartChoiceResolver) {
autoStartChoiceResolver(choice);
autoStartChoiceResolver = null;
}
if (autoStartModal) {
autoStartModal.hidden = true;
}
}
function openAutoStartChoiceDialog(startStep) {
if (!autoStartModal) {
return Promise.resolve('restart');
}
if (autoStartChoiceResolver) {
resolveAutoStartChoice(null);
}
autoStartMessage.textContent = `检测到当前已有流程进度。继续当前会从步骤 ${startStep} 开始自动执行,重新开始会清空当前流程进度并从步骤 1 新开一轮。`;
autoStartModal.hidden = false;
return new Promise((resolve) => {
autoStartChoiceResolver = resolve;
});
}
function isDoneStatus(status) {
return status === 'completed' || status === 'manual_completed' || status === 'skipped';
}
@@ -111,6 +145,25 @@ function getStepStatuses(state = latestState) {
return { ...STEP_DEFAULT_STATUSES, ...(state?.stepStatuses || {}) };
}
function getFirstUnfinishedStep(state = latestState) {
const statuses = getStepStatuses(state);
for (let step = 1; step <= 9; step++) {
if (!isDoneStatus(statuses[step])) {
return step;
}
}
return null;
}
function hasSavedProgress(state = latestState) {
const statuses = getStepStatuses(state);
return Object.values(statuses).some((status) => status !== 'pending');
}
function shouldOfferAutoModeChoice(state = latestState) {
return hasSavedProgress(state) && getFirstUnfinishedStep(state) !== null;
}
function syncLatestState(nextState) {
const mergedStepStatuses = nextState?.stepStatuses
? { ...STEP_DEFAULT_STATUSES, ...(latestState?.stepStatuses || {}), ...nextState.stepStatuses }
@@ -681,20 +734,51 @@ btnStop.addEventListener('click', async () => {
showToast('正在停止当前流程...', 'warn', 2000);
});
autoStartModal?.addEventListener('click', (event) => {
if (event.target === autoStartModal) {
resolveAutoStartChoice(null);
}
});
btnAutoStartClose?.addEventListener('click', () => resolveAutoStartChoice(null));
btnAutoStartCancel?.addEventListener('click', () => resolveAutoStartChoice(null));
btnAutoStartRestart?.addEventListener('click', () => resolveAutoStartChoice('restart'));
btnAutoStartContinue?.addEventListener('click', () => resolveAutoStartChoice('continue'));
// Auto Run
btnAutoRun.addEventListener('click', async () => {
const totalRuns = parseInt(inputRunCount.value) || 1;
btnAutoRun.disabled = true;
inputRunCount.disabled = true;
btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> 运行中...';
await chrome.runtime.sendMessage({
type: 'AUTO_RUN',
source: 'sidepanel',
payload: {
totalRuns,
autoRunSkipFailures: inputAutoSkipFailures.checked,
},
});
try {
const totalRuns = parseInt(inputRunCount.value) || 1;
let mode = 'restart';
if (shouldOfferAutoModeChoice()) {
const startStep = getFirstUnfinishedStep();
const choice = await openAutoStartChoiceDialog(startStep);
if (!choice) {
return;
}
mode = choice;
}
btnAutoRun.disabled = true;
inputRunCount.disabled = true;
btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> 运行中...';
const response = await chrome.runtime.sendMessage({
type: 'AUTO_RUN',
source: 'sidepanel',
payload: {
totalRuns,
autoRunSkipFailures: inputAutoSkipFailures.checked,
mode,
},
});
if (response?.error) {
throw new Error(response.error);
}
} catch (err) {
setDefaultAutoRunButton();
inputRunCount.disabled = false;
showToast(err.message, 'error');
}
});
btnAutoContinue.addEventListener('click', async () => {