diff --git a/background.js b/background.js index 11f4b90..9b450f3 100644 --- a/background.js +++ b/background.js @@ -5250,6 +5250,16 @@ async function clearAndBroadcastAccountRunHistory(stateOverride = null) { return result; } +async function deleteAndBroadcastAccountRunHistoryRecords(recordIds = [], stateOverride = null) { + if (!accountRunHistoryHelpers?.deleteAccountRunHistoryRecords) { + return { deletedCount: 0, remainingCount: 0 }; + } + + const result = await accountRunHistoryHelpers.deleteAccountRunHistoryRecords(recordIds, stateOverride); + await broadcastAccountRunHistoryUpdate(); + return result; +} + const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoRunController({ addLog, appendAccountRunRecord: (...args) => appendAndBroadcastAccountRunRecord(...args), @@ -5957,6 +5967,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter cancelScheduledAutoRun, checkIcloudSession, clearAccountRunHistory: (...args) => clearAndBroadcastAccountRunHistory(...args), + deleteAccountRunHistoryRecords: (...args) => deleteAndBroadcastAccountRunHistoryRecords(...args), clearAutoRunTimerAlarm, clearLuckmailRuntimeState, clearStopRequest, diff --git a/background/account-run-history.js b/background/account-run-history.js index 28404ab..a855907 100644 --- a/background/account-run-history.js +++ b/background/account-run-history.js @@ -254,6 +254,30 @@ return normalizeAccountRunHistory(nextHistory); } + function deleteAccountRunHistoryEntries(history, recordIds = []) { + const normalizedHistory = normalizeAccountRunHistory(history); + const normalizedIds = Array.isArray(recordIds) + ? recordIds + .map((value) => String(value || '').trim().toLowerCase()) + .filter(Boolean) + : []; + + if (!normalizedIds.length) { + return { + deletedCount: 0, + nextHistory: normalizedHistory, + }; + } + + const selectedIds = new Set(normalizedIds); + const nextHistory = normalizedHistory.filter((record) => !selectedIds.has(buildRecordId(record.recordId || record.email))); + + return { + deletedCount: normalizedHistory.length - nextHistory.length, + nextHistory: normalizeAccountRunHistory(nextHistory), + }; + } + async function appendAccountRunHistoryRecord(status, stateOverride = null, reason = '') { const state = stateOverride || await getState(); const record = buildAccountRunHistoryRecord(state, status, reason); @@ -384,12 +408,42 @@ }; } + async function deleteAccountRunHistoryRecords(recordIds = [], stateOverride = null) { + const state = stateOverride || await getState(); + const history = await getPersistedAccountRunHistory(); + const { deletedCount, nextHistory } = deleteAccountRunHistoryEntries(history, recordIds); + + if (!deletedCount) { + return { + deletedCount: 0, + remainingCount: history.length, + }; + } + + await setPersistedAccountRunHistory(nextHistory); + + try { + const filePath = await syncAccountRunHistorySnapshot(nextHistory, state); + if (filePath) { + await addLog(`账号记录快照已同步到本地:${filePath}`, 'info'); + } + } catch (err) { + await addLog(getErrorMessage(err), 'warn'); + } + + return { + deletedCount, + remainingCount: nextHistory.length, + }; + } + return { appendAccountRunRecord, appendAccountRunHistoryRecord, buildAccountRunHistoryRecord, buildAccountRunHistorySnapshotPayload, clearAccountRunHistory, + deleteAccountRunHistoryRecords, getPersistedAccountRunHistory, normalizeAccountRunHistory, normalizeAccountRunHistoryRecord, diff --git a/background/message-router.js b/background/message-router.js index 8eeea3f..bdcd0be 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -13,6 +13,7 @@ cancelScheduledAutoRun, checkIcloudSession, clearAccountRunHistory, + deleteAccountRunHistoryRecords, clearAutoRunTimerAlarm, clearLuckmailRuntimeState, clearStopRequest, @@ -300,6 +301,19 @@ return { ok: true, ...result }; } + case 'DELETE_ACCOUNT_RUN_HISTORY_RECORDS': { + const state = await getState(); + if (isAutoRunLockedState(state)) { + throw new Error('自动流程运行中,当前不能删除邮箱记录。'); + } + if (typeof deleteAccountRunHistoryRecords !== 'function') { + return { ok: true, deletedCount: 0, remainingCount: 0 }; + } + const recordIds = Array.isArray(message.payload?.recordIds) ? message.payload.recordIds : []; + const result = await deleteAccountRunHistoryRecords(recordIds, state); + return { ok: true, ...result }; + } + case 'EXECUTE_STEP': { clearStopRequest(); if (message.source === 'sidepanel') { diff --git a/sidepanel/account-records-manager.js b/sidepanel/account-records-manager.js index 001f464..9621523 100644 --- a/sidepanel/account-records-manager.js +++ b/sidepanel/account-records-manager.js @@ -11,7 +11,44 @@ const displayTimeZone = constants.displayTimeZone || 'Asia/Shanghai'; const pageSize = Math.max(1, Math.floor(Number(constants.pageSize) || 10)); + const FILTER_CONFIG = { + all: { + label: '总', + className: '', + matches: () => true, + metaLabel: '全部', + }, + success: { + label: '成', + className: 'is-success', + matches: (record) => record.finalStatus === 'success', + metaLabel: '成功', + }, + failed: { + label: '失', + className: 'is-failed', + matches: (record) => record.finalStatus === 'failed', + metaLabel: '失败', + }, + stopped: { + label: '停', + className: 'is-stopped', + matches: (record) => record.finalStatus === 'stopped', + metaLabel: '停止', + }, + retry: { + label: '重试', + className: 'is-retry', + matches: (record) => normalizeRetryCount(record.retryCount) > 0, + metaLabel: '重试', + }, + }; + let currentPage = 1; + let activeFilter = 'all'; + let selectionMode = false; + let eventsBound = false; + const selectedRecordIds = new Set(); function escapeHtml(value) { if (typeof helpers.escapeHtml === 'function') { @@ -25,6 +62,17 @@ return Number.isFinite(timestamp) ? timestamp : 0; } + function normalizeRetryCount(value) { + const count = Math.floor(Number(value) || 0); + return count > 0 ? count : 0; + } + + function buildRecordId(record = {}) { + return String(record.recordId || record.email || '') + .trim() + .toLowerCase(); + } + function getAccountRunRecords(currentState = state.getLatestState()) { return (Array.isArray(currentState?.accountRunHistory) ? currentState.accountRunHistory : []) .filter((item) => item && typeof item === 'object') @@ -34,6 +82,7 @@ function summarizeAccountRunHistory(records = []) { return records.reduce((summary, record) => { + const retryCount = normalizeRetryCount(record.retryCount); summary.total += 1; if (record.finalStatus === 'success') { summary.success += 1; @@ -42,13 +91,17 @@ } else if (record.finalStatus === 'stopped') { summary.stopped += 1; } - summary.retryTotal += Math.max(0, Math.floor(Number(record.retryCount) || 0)); + if (retryCount > 0) { + summary.retryRecordCount += 1; + } + summary.retryTotal += retryCount; return summary; }, { total: 0, success: 0, failed: 0, stopped: 0, + retryRecordCount: 0, retryTotal: 0, }); } @@ -60,7 +113,7 @@ } const now = new Date(); - const sameYear = date.getFullYear() == now.getFullYear(); + const sameYear = date.getFullYear() === now.getFullYear(); const sameDay = date.toDateString() === now.toDateString(); if (sameDay) { @@ -101,31 +154,165 @@ return String(record.failureLabel || '').trim() || '流程失败'; } - function createStatChip(label, value, className = '') { - return `${escapeHtml(String(value))}${escapeHtml(label)}`; + function getFilterConfig(filterKey = activeFilter) { + return FILTER_CONFIG[filterKey] || FILTER_CONFIG.all; } - function updateHeader(records) { - if (!dom.accountRecordsMeta || !dom.accountRecordsStats) { + function getFilteredRecords(records = []) { + const filterConfig = getFilterConfig(activeFilter); + return records.filter((record) => filterConfig.matches(record)); + } + + function pruneSelectedRecordIds(records = []) { + const availableIds = new Set(records.map((record) => buildRecordId(record)).filter(Boolean)); + for (const recordId of Array.from(selectedRecordIds)) { + if (!availableIds.has(recordId)) { + selectedRecordIds.delete(recordId); + } + } + } + + function setNodeHidden(node, hidden) { + if (node) { + node.hidden = Boolean(hidden); + } + } + + function setNodeDisabled(node, disabled) { + if (node) { + node.disabled = Boolean(disabled); + } + } + + function toggleNodeClass(node, className, enabled) { + if (!node || !className) { + return; + } + if (node.classList && typeof node.classList.toggle === 'function') { + node.classList.toggle(className, Boolean(enabled)); + } + } + + function setNodeText(node, value) { + if (node) { + node.textContent = String(value || ''); + } + } + + function setNodeAttr(node, name, value) { + if (!node || !name) { + return; + } + if (typeof node.setAttribute === 'function') { + node.setAttribute(name, String(value)); + return; + } + node[name] = value; + } + + function getDatasetValue(node, attrName) { + if (!node || !attrName) { + return ''; + } + + if (typeof node.getAttribute === 'function') { + return String(node.getAttribute(attrName) || ''); + } + + const dataKey = attrName + .replace(/^data-/, '') + .replace(/-([a-z])/g, (_, char) => char.toUpperCase()); + return String(node.dataset?.[dataKey] || ''); + } + + function findClosest(target, selector) { + if (!target || typeof target.closest !== 'function') { + return null; + } + try { + return target.closest(selector); + } catch { + return null; + } + } + + function createStatChip(filterKey, value) { + const filterConfig = getFilterConfig(filterKey); + const classNames = [ + 'account-records-stat', + filterConfig.className, + activeFilter === filterKey ? 'is-active' : '', + ].filter(Boolean).join(' '); + + return ` + + `; + } + + function updateHeader(allRecords, filteredRecords) { + if (!dom.accountRecordsMeta) { return; } - if (!records.length) { + if (!allRecords.length) { dom.accountRecordsMeta.textContent = '暂无邮箱记录'; - } else { - dom.accountRecordsMeta.textContent = `共 ${records.length} 条,最近更新于 ${formatAccountRecordTime(records[0]?.finishedAt)}`; + return; } - const summary = summarizeAccountRunHistory(records); + const latestTime = formatAccountRecordTime(allRecords[0]?.finishedAt); + let metaText = `共 ${allRecords.length} 条,最近更新于 ${latestTime}`; + + if (activeFilter !== 'all') { + metaText = `共 ${allRecords.length} 条,当前筛选 ${getFilterConfig(activeFilter).metaLabel} ${filteredRecords.length} 条,最近更新于 ${latestTime}`; + } + + if (selectionMode) { + metaText += `,已选 ${selectedRecordIds.size} 条`; + } + + dom.accountRecordsMeta.textContent = metaText; + } + + function updateStats(allRecords) { + if (!dom.accountRecordsStats) { + return; + } + + const summary = summarizeAccountRunHistory(allRecords); dom.accountRecordsStats.innerHTML = [ - createStatChip('总', summary.total), - createStatChip('成', summary.success, 'is-success'), - createStatChip('失', summary.failed, 'is-failed'), - createStatChip('停', summary.stopped, 'is-stopped'), - createStatChip('重试', summary.retryTotal, 'is-retry'), + createStatChip('all', summary.total), + createStatChip('success', summary.success), + createStatChip('failed', summary.failed), + createStatChip('stopped', summary.stopped), + createStatChip('retry', summary.retryTotal), ].join(''); } + function updateToolbarState(allRecords) { + const totalRecords = allRecords.length; + setNodeDisabled(dom.btnClearAccountRecords, totalRecords === 0); + setNodeDisabled(dom.btnToggleAccountRecordsSelection, totalRecords === 0); + setNodeHidden(dom.btnClearAccountRecords, selectionMode); + toggleNodeClass(dom.btnToggleAccountRecordsSelection, 'is-active', selectionMode); + setNodeAttr(dom.btnToggleAccountRecordsSelection, 'aria-pressed', selectionMode ? 'true' : 'false'); + setNodeText(dom.btnToggleAccountRecordsSelection, selectionMode ? '取消多选' : '多选'); + + const selectedCount = selectedRecordIds.size; + setNodeHidden(dom.btnDeleteSelectedAccountRecords, !selectionMode); + setNodeDisabled(dom.btnDeleteSelectedAccountRecords, selectedCount === 0); + setNodeText( + dom.btnDeleteSelectedAccountRecords, + selectedCount > 0 ? `删除选中(${selectedCount})` : '删除选中' + ); + } + function updatePagination(totalRecords) { const totalPages = totalRecords > 0 ? Math.ceil(totalRecords / pageSize) : 0; if (totalPages === 0) { @@ -136,44 +323,73 @@ currentPage = 1; } - if (dom.accountRecordsPageLabel) { - dom.accountRecordsPageLabel.textContent = totalPages > 0 ? `${currentPage} / ${totalPages}` : '0 / 0'; - } - if (dom.btnAccountRecordsPrev) { - dom.btnAccountRecordsPrev.disabled = totalPages <= 1 || currentPage <= 1; - } - if (dom.btnAccountRecordsNext) { - dom.btnAccountRecordsNext.disabled = totalPages <= 1 || currentPage >= totalPages; - } - if (dom.btnClearAccountRecords) { - dom.btnClearAccountRecords.disabled = totalRecords === 0; - } + setNodeText(dom.accountRecordsPageLabel, totalPages > 0 ? `${currentPage} / ${totalPages}` : '0 / 0'); + setNodeDisabled(dom.btnAccountRecordsPrev, totalPages <= 1 || currentPage <= 1); + setNodeDisabled(dom.btnAccountRecordsNext, totalPages <= 1 || currentPage >= totalPages); return totalPages; } - function renderRecordList(records = []) { + function renderEmptyState(allRecords) { if (!dom.accountRecordsList) { return; } - const totalPages = updatePagination(records.length); - if (!records.length) { - dom.accountRecordsList.innerHTML = '