From f3cfed8fcc6229b9af6605a0c2b7dd26beb461e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Fri, 17 Apr 2026 18:01:53 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 71 +++++ web/static/i18n/en-US.json | 3 + web/static/i18n/zh-CN.json | 3 + web/static/js/tasks.js | 543 +++++++++++++++++++++++-------------- web/templates/index.html | 21 +- 5 files changed, 420 insertions(+), 221 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index 17c2d980..44f2aa32 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -9138,6 +9138,77 @@ header { min-width: 0; } +/* Inline editable value — hover hint */ +.bq-inline-editable { + cursor: pointer; + border-radius: 4px; + padding: 2px 6px; + margin: -2px -6px; + transition: background 0.15s; + display: inline-flex; + align-items: center; + gap: 6px; +} +.bq-inline-editable:hover { + background: var(--bg-secondary); +} +.bq-inline-editable::after { + content: '\270E'; + font-size: 11px; + color: var(--text-secondary); + opacity: 0; + transition: opacity 0.15s; + flex-shrink: 0; +} +.bq-inline-editable:hover::after { + opacity: 0.5; +} + +/* Inline edit controls (replaces value in-place) */ +.bq-inline-edit-controls { + display: inline-flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} +.bq-inline-edit-controls input[type="text"], +.bq-inline-edit-controls select { + padding: 4px 8px; + border-radius: 6px; + border: 1.5px solid var(--accent-color); + font-size: 13px; + outline: none; + background: var(--bg-primary); + color: var(--text-primary); + transition: border-color 0.15s, box-shadow 0.15s; +} +.bq-inline-edit-controls input[type="text"]:focus, +.bq-inline-edit-controls select:focus { + box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.15); +} +/* Task inline edit textarea */ +.batch-task-inline-edit { + flex: 1; + min-width: 0; +} +.batch-task-inline-edit textarea { + width: 100%; + min-height: 56px; + padding: 8px 10px; + border: 1.5px solid var(--accent-color); + border-radius: 6px; + font-size: 0.875rem; + resize: vertical; + font-family: inherit; + background: var(--bg-primary); + color: var(--text-primary); + outline: none; + box-sizing: border-box; + transition: border-color 0.15s, box-shadow 0.15s; +} +.batch-task-inline-edit textarea:focus { + box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.15); +} @media (max-width: 520px) { .bq-kv, .bq-kv--block { diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index a477b1ef..a9ecfcbc 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -284,6 +284,8 @@ "pauseQueueConfirm": "Pause this batch queue? The current task will be stopped; remaining tasks will stay pending.", "deleteQueueConfirm": "Delete this batch queue? This cannot be undone.", "deleteQueueFailed": "Failed to delete batch queue", + "rerunQueueConfirm": "Rerun this batch queue? All tasks will be reset to pending and re-executed.", + "rerunQueueFailed": "Failed to rerun batch queue", "batchQueueTitle": "Batch task queue", "batchQueueUntitled": "Untitled queue", "resumeExecute": "Resume", @@ -1542,6 +1544,7 @@ "startExecuteNow": "Run now (one round)", "startExecuteNowConfirm": "This is a Cron queue. Clicking Start will run the current round immediately instead of waiting for the next Cron time. Continue?", "pauseQueue": "Pause queue", + "rerunQueue": "Rerun", "deleteQueue": "Delete queue", "queueTitle": "Task title", "role": "Role", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 6d448740..9b8679da 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -284,6 +284,8 @@ "pauseQueueConfirm": "确定要暂停这个批量任务队列吗?当前正在执行的任务将被停止,后续任务将保留待执行状态。", "deleteQueueConfirm": "确定要删除这个批量任务队列吗?此操作不可恢复。", "deleteQueueFailed": "删除批量任务队列失败", + "rerunQueueConfirm": "确定要重跑一轮吗?所有子任务将被重置为待执行状态并重新开始执行。", + "rerunQueueFailed": "重跑批量任务失败", "batchQueueTitle": "批量任务队列", "batchQueueUntitled": "未命名队列", "resumeExecute": "继续执行", @@ -1542,6 +1544,7 @@ "startExecuteNow": "立即执行一轮", "startExecuteNowConfirm": "这是 Cron 队列,点击后会立即执行当前这一轮,不会等待下次 Cron 时间。确定立即执行吗?", "pauseQueue": "暂停队列", + "rerunQueue": "重跑一轮", "deleteQueue": "删除队列", "queueTitle": "任务标题", "role": "角色", diff --git a/web/static/js/tasks.js b/web/static/js/tasks.js index f7ad996a..822800ca 100644 --- a/web/static/js/tasks.js +++ b/web/static/js/tasks.js @@ -1309,6 +1309,11 @@ async function showBatchQueueDetail(queueId) { : _t('batchQueueDetailModal.startExecute'); } } + const rerunBtn = document.getElementById('batch-queue-rerun-btn'); + if (rerunBtn) { + // 已完成或已取消状态显示"重跑一轮" + rerunBtn.style.display = (queue.status === 'completed' || queue.status === 'cancelled') ? 'inline-block' : 'none'; + } if (pauseBtn) { // running状态显示"暂停队列" pauseBtn.style.display = queue.status === 'running' ? 'inline-block' : 'none'; @@ -1376,41 +1381,10 @@ async function showBatchQueueDetail(queueId) { ${showProgressNoteInModal ? `

${escapeHtml(pres.progressNote)}

` : ''}
-
${escapeHtml(_t('batchQueueDetailModal.queueTitle'))}${escapeHtml(queue.title || _t('tasks.batchQueueUntitled'))}${allowSubtaskMutation ? ` ` : ''}
- -
${escapeHtml(_t('batchQueueDetailModal.role'))}${roleLineVal}
-
${escapeHtml(_t('batchImportModal.agentMode'))}${escapeHtml(agentModeText)}
-
${escapeHtml(_t('batchImportModal.scheduleMode'))}${scheduleDetail}${allowSubtaskMutation ? ` ` : ''}
- +
${escapeHtml(_t('batchQueueDetailModal.queueTitle'))}${allowSubtaskMutation ? `${escapeHtml(queue.title || _t('tasks.batchQueueUntitled'))}` : escapeHtml(queue.title || _t('tasks.batchQueueUntitled'))}
+
${escapeHtml(_t('batchQueueDetailModal.role'))}${allowSubtaskMutation ? `${roleLineVal}` : roleLineVal}
+
${escapeHtml(_t('batchImportModal.agentMode'))}${allowSubtaskMutation ? `${escapeHtml(agentModeText)}` : escapeHtml(agentModeText)}
+
${escapeHtml(_t('batchImportModal.scheduleMode'))}${allowSubtaskMutation ? `${scheduleDetail}` : scheduleDetail}
${escapeHtml(_t('batchQueueDetailModal.taskTotal'))}${queue.tasks.length}
${queue.scheduleMode === 'cron' ? `
${escapeHtml(_t('batchQueueDetailModal.scheduleCronAuto'))}
` : ''}
@@ -1555,6 +1529,36 @@ async function pauseBatchQueue() { } } +// 重跑批量任务队列 +async function rerunBatchQueue() { + const queueId = batchQueuesState.currentQueueId; + if (!queueId) return; + + if (!confirm(_t('tasks.rerunQueueConfirm'))) { + return; + } + const btn = document.getElementById('batch-queue-rerun-btn'); + if (btn) { btn.disabled = true; } + try { + const response = await apiFetch(`/api/batch-tasks/${queueId}/rerun`, { + method: 'POST', + }); + + if (!response.ok) { + const result = await response.json().catch(() => ({})); + throw new Error(result.error || _t('tasks.rerunQueueFailed')); + } + + showBatchQueueDetail(queueId); + refreshBatchQueues(); + } catch (error) { + console.error('重跑批量任务失败:', error); + alert(_t('tasks.rerunQueueFailed') + ': ' + error.message); + } finally { + if (btn) { btn.disabled = false; } + } +} + // 删除批量任务队列(从详情模态框) async function deleteBatchQueue() { const queueId = batchQueuesState.currentQueueId; @@ -1633,15 +1637,14 @@ function startBatchQueueRefresh(queueId) { } batchQueuesState.refreshInterval = setInterval(() => { - // 如果编辑或添加任务的模态框正在打开,跳过本次刷新防止丢失编辑内容 - const editModal = document.getElementById('edit-batch-task-modal'); + // 如果有内联编辑或添加任务模态框正在打开,跳过本次刷新防止丢失编辑内容 const addModal = document.getElementById('add-batch-task-modal'); - const editScheduleRow = document.getElementById('bq-edit-schedule-row'); - const editMetadataRow = document.getElementById('bq-edit-metadata-row'); - if ((editModal && editModal.style.display === 'block') || - (addModal && addModal.style.display === 'block') || - (editScheduleRow && editScheduleRow.style.display !== 'none') || - (editMetadataRow && editMetadataRow.style.display !== 'none')) { + const content = document.getElementById('batch-queue-detail-content'); + const hasInlineEdit = content && ( + content.querySelector('.bq-inline-edit-controls') || + content.querySelector('.batch-task-inline-edit') + ); + if ((addModal && addModal.style.display === 'block') || hasInlineEdit) { return; } if (batchQueuesState.currentQueueId === queueId) { @@ -1678,157 +1681,96 @@ function viewBatchTaskConversation(conversationId) { window.location.hash = `chat?conversation=${conversationId}`; } -// 编辑批量任务的状态 -const editBatchTaskState = { - queueId: null, - taskId: null, - _escHandler: null, - _saveHandler: null -}; - -// 从元素获取任务信息并打开编辑模态框 +// --- 内联编辑:任务消息 --- +// 从元素获取任务信息并启动内联编辑 function editBatchTaskFromElement(button) { const taskItem = button.closest('.batch-task-item'); - if (!taskItem) { - console.error('无法找到任务项元素'); - return; - } - + if (!taskItem) return; + const queueId = taskItem.getAttribute('data-queue-id'); const taskId = taskItem.getAttribute('data-task-id'); const taskMessage = taskItem.getAttribute('data-task-message'); - - if (!queueId || !taskId) { - console.error('任务信息不完整'); - return; - } - + if (!queueId || !taskId) return; + // 解码HTML实体 const decodedMessage = taskMessage .replace(/'/g, "'") .replace(/"/g, '"') .replace(/\\n/g, '\n'); - - editBatchTask(queueId, taskId, decodedMessage); + + // 找到 .batch-task-message 和 header 中的按钮 + const msgSpan = taskItem.querySelector('.batch-task-message'); + const header = taskItem.querySelector('.batch-task-header'); + if (!msgSpan || !header) return; + + // 隐藏编辑/删除按钮 + header.querySelectorAll('.batch-task-edit-btn, .batch-task-delete-btn').forEach(b => b.style.display = 'none'); + + // 替换消息为内联编辑区域 + const editDiv = document.createElement('div'); + editDiv.className = 'batch-task-inline-edit'; + editDiv.innerHTML = ``; + msgSpan.style.display = 'none'; + msgSpan.parentNode.insertBefore(editDiv, msgSpan.nextSibling); + + const textarea = editDiv.querySelector('textarea'); + if (textarea) { + let taskCancelled = false; + textarea.focus(); + textarea.setSelectionRange(textarea.value.length, textarea.value.length); + textarea.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + taskCancelled = true; + cancelInlineTask(); + } + }); + textarea.addEventListener('blur', () => { + if (!taskCancelled) saveInlineTask(queueId, taskId); + }); + } } -// 打开编辑批量任务模态框 -function editBatchTask(queueId, taskId, currentMessage) { - editBatchTaskState.queueId = queueId; - editBatchTaskState.taskId = taskId; - - const modal = document.getElementById('edit-batch-task-modal'); - const messageInput = document.getElementById('edit-task-message'); - - if (!modal || !messageInput) { - console.error('编辑任务模态框元素不存在'); - return; - } - - messageInput.value = currentMessage; - modal.style.display = 'block'; - - // 聚焦到输入框 - setTimeout(() => { - messageInput.focus(); - messageInput.select(); - }, 100); - - // 清理旧的事件监听器(防止泄漏) - if (editBatchTaskState._escHandler) { - document.removeEventListener('keydown', editBatchTaskState._escHandler); - } - if (editBatchTaskState._saveHandler) { - messageInput.removeEventListener('keydown', editBatchTaskState._saveHandler); - } - - // 添加ESC键监听 - editBatchTaskState._escHandler = (e) => { - if (e.key === 'Escape') { - closeEditBatchTaskModal(); - } - }; - document.addEventListener('keydown', editBatchTaskState._escHandler); - - // 添加Enter+Ctrl/Cmd保存功能 - editBatchTaskState._saveHandler = (e) => { - if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { - e.preventDefault(); - saveBatchTask(); - } - }; - messageInput.addEventListener('keydown', editBatchTaskState._saveHandler); +function cancelInlineTask() { + // 刷新整个详情来还原 + const queueId = batchQueuesState.currentQueueId; + if (queueId) showBatchQueueDetail(queueId); } -// 关闭编辑批量任务模态框 -function closeEditBatchTaskModal() { - const modal = document.getElementById('edit-batch-task-modal'); - if (modal) { - modal.style.display = 'none'; - } - // 清理事件监听器 - if (editBatchTaskState._escHandler) { - document.removeEventListener('keydown', editBatchTaskState._escHandler); - editBatchTaskState._escHandler = null; - } - if (editBatchTaskState._saveHandler) { - const messageInput = document.getElementById('edit-task-message'); - if (messageInput) { - messageInput.removeEventListener('keydown', editBatchTaskState._saveHandler); - } - editBatchTaskState._saveHandler = null; - } - editBatchTaskState.queueId = null; - editBatchTaskState.taskId = null; -} +async function saveInlineTask(queueId, taskId) { + if (_bqInlineSaving) return; + _bqInlineSaving = true; + const textarea = document.getElementById(`bq-task-edit-${taskId}`); + if (!textarea) { _bqInlineSaving = false; return; } -// 保存批量任务 -async function saveBatchTask() { - const queueId = editBatchTaskState.queueId; - const taskId = editBatchTaskState.taskId; - const messageInput = document.getElementById('edit-task-message'); - - if (!queueId || !taskId) { - alert(_t('tasks.taskIncomplete')); - return; - } - - if (!messageInput) { - alert(_t('tasks.cannotGetTaskMessageInput')); - return; - } - - const message = messageInput.value.trim(); + const message = textarea.value.trim(); if (!message) { + _bqInlineSaving = false; alert(_t('tasks.taskMessageRequired')); return; } - + try { const response = await apiFetch(`/api/batch-tasks/${queueId}/tasks/${taskId}`, { method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ message: message }), + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message }), }); - + if (!response.ok) { const result = await response.json().catch(() => ({})); throw new Error(result.error || _t('tasks.updateTaskFailed')); } - - // 关闭编辑模态框 - closeEditBatchTaskModal(); - + + _bqInlineSaving = false; // 刷新队列详情 if (batchQueuesState.currentQueueId === queueId) { showBatchQueueDetail(queueId); } - + // 刷新队列列表 refreshBatchQueues(); } catch (error) { + _bqInlineSaving = false; console.error('保存任务失败:', error); alert(_t('tasks.saveTaskFailed') + ': ' + error.message); } @@ -2046,26 +1988,54 @@ async function updateBatchQueueScheduleEnabled(enabled) { } } -// --- 元数据(标题/角色)内联编辑 --- -function showEditMetadataInline() { - // 关闭调度编辑行(互斥) - const schedRow = document.getElementById('bq-edit-schedule-row'); - if (schedRow) schedRow.style.display = 'none'; - const row = document.getElementById('bq-edit-metadata-row'); - if (row) row.style.display = ''; +// --- 内联编辑:取消所有正在编辑的内联区域 --- +function cancelAllInlineEdits() { + _bqInlineSaving = true; // 防止 blur 触发保存 + const queueId = batchQueuesState.currentQueueId; + if (queueId) showBatchQueueDetail(queueId); + _bqInlineSaving = false; } -function hideEditMetadataInline() { - const row = document.getElementById('bq-edit-metadata-row'); - if (row) row.style.display = 'none'; -} -async function saveEditMetadata() { + +// --- 内联编辑:标题 --- +let _bqInlineSaving = false; +function startInlineEditTitle() { + const container = document.getElementById('bq-title-val'); + if (!container) return; const queueId = batchQueuesState.currentQueueId; if (!queueId) return; - const titleInput = document.getElementById('bq-edit-title'); - const roleInput = document.getElementById('bq-edit-role'); - const title = titleInput ? titleInput.value.trim() : ''; - const role = roleInput ? roleInput.value.trim() : ''; + const currentTitle = (container.querySelector('.bq-inline-editable') || container).textContent.trim(); + const untitledText = _t('tasks.batchQueueUntitled'); + const val = currentTitle === untitledText ? '' : currentTitle; + container.innerHTML = ` + + `; + const inp = document.getElementById('bq-edit-title'); + if (inp) { + inp.focus(); + inp.select(); + let cancelled = false; + inp.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { e.preventDefault(); inp.blur(); } + if (e.key === 'Escape') { cancelled = true; cancelAllInlineEdits(); } + }); + inp.addEventListener('blur', () => { + if (cancelled) return; + saveInlineTitle(); + }); + } +} +async function saveInlineTitle() { + if (_bqInlineSaving) return; + _bqInlineSaving = true; + const queueId = batchQueuesState.currentQueueId; + if (!queueId) { _bqInlineSaving = false; return; } + const inp = document.getElementById('bq-edit-title'); + const title = inp ? inp.value.trim() : ''; try { + // 获取当前角色(保持不变) + const detailResp = await apiFetch(`/api/batch-tasks/${queueId}`); + const detail = await detailResp.json(); + const role = detail.queue ? (detail.queue.role || '') : ''; const response = await apiFetch(`/api/batch-tasks/${queueId}/metadata`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, @@ -2075,9 +2045,131 @@ async function saveEditMetadata() { const result = await response.json().catch(() => ({})); throw new Error(result.error || _t('tasks.updateTaskFailed')); } + _bqInlineSaving = false; showBatchQueueDetail(queueId); refreshBatchQueues(); } catch (e) { + _bqInlineSaving = false; + console.error(e); + alert(e.message); + } +} + +// --- 内联编辑:角色 --- +function startInlineEditRole() { + const container = document.getElementById('bq-role-val'); + if (!container) return; + const queueId = batchQueuesState.currentQueueId; + if (!queueId) return; + // 获取当前详情中角色名 — 从 layout 的 data 中无法获取,故使用 API 拉取 + apiFetch(`/api/batch-tasks/${queueId}`).then(r => r.json()).then(detail => { + const queue = detail.queue; + const currentRole = queue.role || ''; + const roles = (Array.isArray(batchQueuesState.loadedRoles) ? batchQueuesState.loadedRoles : []).filter(r => r.name !== '默认' && r.enabled !== false).sort((a, b) => (a.name || '').localeCompare(b.name || '', 'zh-CN')); + const currentInList = !currentRole || roles.some(r => r.name === currentRole); + const orphanOpt = !currentInList ? `` : ''; + const opts = roles.map(r => ``).join(''); + container.innerHTML = ` + + `; + const sel = document.getElementById('bq-edit-role'); + if (sel) { + sel.focus(); + let cancelled = false; + sel.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { cancelled = true; cancelAllInlineEdits(); } + }); + sel.addEventListener('change', () => { if (!cancelled) saveInlineRole(); }); + sel.addEventListener('blur', () => { if (!cancelled) saveInlineRole(); }); + } + }); +} +async function saveInlineRole() { + if (_bqInlineSaving) return; + _bqInlineSaving = true; + const queueId = batchQueuesState.currentQueueId; + if (!queueId) { _bqInlineSaving = false; return; } + const sel = document.getElementById('bq-edit-role'); + const role = sel ? sel.value.trim() : ''; + try { + const detailResp = await apiFetch(`/api/batch-tasks/${queueId}`); + const detail = await detailResp.json(); + const title = detail.queue ? (detail.queue.title || '') : ''; + const response = await apiFetch(`/api/batch-tasks/${queueId}/metadata`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title, role }), + }); + if (!response.ok) { + const result = await response.json().catch(() => ({})); + throw new Error(result.error || _t('tasks.updateTaskFailed')); + } + _bqInlineSaving = false; + showBatchQueueDetail(queueId); + refreshBatchQueues(); + } catch (e) { + _bqInlineSaving = false; + console.error(e); + alert(e.message); + } +} + +// --- 内联编辑:代理模式 --- +function startInlineEditAgentMode() { + const container = document.getElementById('bq-agentmode-val'); + if (!container) return; + const queueId = batchQueuesState.currentQueueId; + if (!queueId) return; + apiFetch(`/api/batch-tasks/${queueId}`).then(r => r.json()).then(detail => { + const queue = detail.queue; + const currentMode = queue.agentMode || 'single'; + container.innerHTML = ` + + `; + const sel = document.getElementById('bq-edit-agentmode'); + if (sel) { + sel.focus(); + let cancelled = false; + sel.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { cancelled = true; cancelAllInlineEdits(); } + }); + sel.addEventListener('change', () => { if (!cancelled) saveInlineAgentMode(); }); + sel.addEventListener('blur', () => { if (!cancelled) saveInlineAgentMode(); }); + } + }); +} +async function saveInlineAgentMode() { + if (_bqInlineSaving) return; + _bqInlineSaving = true; + const queueId = batchQueuesState.currentQueueId; + if (!queueId) { _bqInlineSaving = false; return; } + const sel = document.getElementById('bq-edit-agentmode'); + const agentMode = sel ? sel.value : 'single'; + try { + const detailResp = await apiFetch(`/api/batch-tasks/${queueId}`); + const detail = await detailResp.json(); + const title = detail.queue ? (detail.queue.title || '') : ''; + const role = detail.queue ? (detail.queue.role || '') : ''; + const response = await apiFetch(`/api/batch-tasks/${queueId}/metadata`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title, role, agentMode }), + }); + if (!response.ok) { + const result = await response.json().catch(() => ({})); + throw new Error(result.error || _t('tasks.updateTaskFailed')); + } + _bqInlineSaving = false; + showBatchQueueDetail(queueId); + refreshBatchQueues(); + } catch (e) { + _bqInlineSaving = false; console.error(e); alert(e.message); } @@ -2119,38 +2211,82 @@ async function retryBatchTask(queueId, taskId) { } } -// --- 调度配置内联编辑 --- -function showEditScheduleInline() { - // 关闭元数据编辑行(互斥) - const metaRow = document.getElementById('bq-edit-metadata-row'); - if (metaRow) metaRow.style.display = 'none'; - const row = document.getElementById('bq-edit-schedule-row'); - if (row) row.style.display = ''; +// --- 内联编辑:调度配置 --- +function startInlineEditSchedule() { + const container = document.getElementById('bq-schedule-val'); + if (!container) return; + const queueId = batchQueuesState.currentQueueId; + if (!queueId) return; + apiFetch(`/api/batch-tasks/${queueId}`).then(r => r.json()).then(detail => { + const queue = detail.queue; + const isCron = queue.scheduleMode === 'cron'; + container.innerHTML = ` + + + `; + let schedCancelled = false; + const sel = document.getElementById('bq-edit-schedule-mode'); + const cronInp = document.getElementById('bq-edit-cron-expr'); + if (sel) { + sel.focus(); + sel.addEventListener('keydown', (e) => { if (e.key === 'Escape') { schedCancelled = true; cancelAllInlineEdits(); } }); + sel.addEventListener('change', () => { + toggleInlineScheduleCron(); + // 切到 manual 时直接保存;切到 cron 时等用户输入表达式后 blur 保存 + if (sel.value !== 'cron' && !schedCancelled) saveInlineSchedule(); + }); + sel.addEventListener('blur', (e) => { + // 如果焦点移到了 cron 输入框,不触发保存 + setTimeout(() => { + const active = document.activeElement; + if (active && (active.id === 'bq-edit-cron-expr' || active.id === 'bq-edit-schedule-mode')) return; + if (!schedCancelled) saveInlineSchedule(); + }, 100); + }); + } + if (cronInp) { + cronInp.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { e.preventDefault(); cronInp.blur(); } + if (e.key === 'Escape') { schedCancelled = true; cancelAllInlineEdits(); } + }); + cronInp.addEventListener('blur', () => { + setTimeout(() => { + const active = document.activeElement; + if (active && (active.id === 'bq-edit-cron-expr' || active.id === 'bq-edit-schedule-mode')) return; + if (!schedCancelled) saveInlineSchedule(); + }, 100); + }); + } + }); } -function hideEditScheduleInline() { - const row = document.getElementById('bq-edit-schedule-row'); - if (row) row.style.display = 'none'; -} -function toggleEditScheduleCronInput() { +function toggleInlineScheduleCron() { const modeSelect = document.getElementById('bq-edit-schedule-mode'); const cronInput = document.getElementById('bq-edit-cron-expr'); if (modeSelect && cronInput) { cronInput.style.display = modeSelect.value === 'cron' ? '' : 'none'; + if (modeSelect.value === 'cron') cronInput.focus(); } } -async function saveEditSchedule() { +async function saveInlineSchedule() { + if (_bqInlineSaving) return; + _bqInlineSaving = true; const queueId = batchQueuesState.currentQueueId; - if (!queueId) return; + if (!queueId) { _bqInlineSaving = false; return; } const modeSelect = document.getElementById('bq-edit-schedule-mode'); const cronInput = document.getElementById('bq-edit-cron-expr'); - if (!modeSelect) return; + if (!modeSelect) { _bqInlineSaving = false; return; } const scheduleMode = modeSelect.value; const cronExpr = cronInput ? cronInput.value.trim() : ''; if (scheduleMode === 'cron' && !cronExpr) { + _bqInlineSaving = false; alert(_t('batchImportModal.cronExprRequired')); return; } if (scheduleMode === 'cron' && !/^\S+\s+\S+\s+\S+\s+\S+\s+\S+$/.test(cronExpr)) { + _bqInlineSaving = false; alert(_t('batchImportModal.cronExprInvalid') || 'Cron 表达式格式错误,需要 5 段(分 时 日 月 周)'); return; } @@ -2164,9 +2300,11 @@ async function saveEditSchedule() { const result = await response.json().catch(() => ({})); throw new Error(result.error || _t('batchQueueDetailModal.editScheduleError')); } + _bqInlineSaving = false; showBatchQueueDetail(queueId); refreshBatchQueues(); } catch (e) { + _bqInlineSaving = false; console.error(e); alert(_t('batchQueueDetailModal.editScheduleError') + ': ' + e.message); } @@ -2179,14 +2317,14 @@ window.createBatchQueue = createBatchQueue; window.showBatchQueueDetail = showBatchQueueDetail; window.startBatchQueue = startBatchQueue; window.pauseBatchQueue = pauseBatchQueue; +window.rerunBatchQueue = rerunBatchQueue; window.deleteBatchQueue = deleteBatchQueue; window.closeBatchQueueDetailModal = closeBatchQueueDetailModal; window.refreshBatchQueues = refreshBatchQueues; window.viewBatchTaskConversation = viewBatchTaskConversation; -window.editBatchTask = editBatchTask; window.editBatchTaskFromElement = editBatchTaskFromElement; -window.closeEditBatchTaskModal = closeEditBatchTaskModal; -window.saveBatchTask = saveBatchTask; +window.cancelInlineTask = cancelInlineTask; +window.saveInlineTask = saveInlineTask; window.filterBatchQueues = filterBatchQueues; window.goBatchQueuesPage = goBatchQueuesPage; window.changeBatchQueuesPageSize = changeBatchQueuesPageSize; @@ -2197,14 +2335,17 @@ window.deleteBatchTaskFromElement = deleteBatchTaskFromElement; window.deleteBatchQueueFromList = deleteBatchQueueFromList; window.handleBatchScheduleModeChange = handleBatchScheduleModeChange; window.updateBatchQueueScheduleEnabled = updateBatchQueueScheduleEnabled; -window.showEditMetadataInline = showEditMetadataInline; -window.hideEditMetadataInline = hideEditMetadataInline; -window.saveEditMetadata = saveEditMetadata; +window.cancelAllInlineEdits = cancelAllInlineEdits; +window.startInlineEditTitle = startInlineEditTitle; +window.saveInlineTitle = saveInlineTitle; +window.startInlineEditRole = startInlineEditRole; +window.saveInlineRole = saveInlineRole; +window.startInlineEditAgentMode = startInlineEditAgentMode; +window.saveInlineAgentMode = saveInlineAgentMode; window.retryBatchTask = retryBatchTask; -window.showEditScheduleInline = showEditScheduleInline; -window.hideEditScheduleInline = hideEditScheduleInline; -window.toggleEditScheduleCronInput = toggleEditScheduleCronInput; -window.saveEditSchedule = saveEditSchedule; +window.startInlineEditSchedule = startInlineEditSchedule; +window.toggleInlineScheduleCron = toggleInlineScheduleCron; +window.saveInlineSchedule = saveInlineSchedule; // 语言切换后,列表/分页/详情弹窗由 JS 渲染的文案需用当前语言重绘(applyTranslations 不会处理 innerHTML 内容) document.addEventListener('languagechange', function () { diff --git a/web/templates/index.html b/web/templates/index.html index a9eddc2e..c1d24ee8 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -2395,6 +2395,7 @@ version: 1.0.0
@@ -2407,26 +2408,6 @@ version: 1.0.0
- - -