feat(sidepanel): implement multi-select and delete functionality for account records
- Added a new feature to allow users to select multiple account records and delete them. - Introduced a filter system to categorize account records based on their status (success, failed, stopped, retry). - Enhanced the UI with a toolbar for selection and deletion actions, including a confirmation modal for deletions. - Updated the CSS for better styling of the new toolbar and selection states. - Implemented backend support for deleting selected records and syncing the remaining records. - Added unit tests to ensure the new functionality works as expected.
This commit is contained in:
@@ -5250,6 +5250,16 @@ async function clearAndBroadcastAccountRunHistory(stateOverride = null) {
|
|||||||
return result;
|
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({
|
const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoRunController({
|
||||||
addLog,
|
addLog,
|
||||||
appendAccountRunRecord: (...args) => appendAndBroadcastAccountRunRecord(...args),
|
appendAccountRunRecord: (...args) => appendAndBroadcastAccountRunRecord(...args),
|
||||||
@@ -5957,6 +5967,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
|
|||||||
cancelScheduledAutoRun,
|
cancelScheduledAutoRun,
|
||||||
checkIcloudSession,
|
checkIcloudSession,
|
||||||
clearAccountRunHistory: (...args) => clearAndBroadcastAccountRunHistory(...args),
|
clearAccountRunHistory: (...args) => clearAndBroadcastAccountRunHistory(...args),
|
||||||
|
deleteAccountRunHistoryRecords: (...args) => deleteAndBroadcastAccountRunHistoryRecords(...args),
|
||||||
clearAutoRunTimerAlarm,
|
clearAutoRunTimerAlarm,
|
||||||
clearLuckmailRuntimeState,
|
clearLuckmailRuntimeState,
|
||||||
clearStopRequest,
|
clearStopRequest,
|
||||||
|
|||||||
@@ -254,6 +254,30 @@
|
|||||||
return normalizeAccountRunHistory(nextHistory);
|
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 = '') {
|
async function appendAccountRunHistoryRecord(status, stateOverride = null, reason = '') {
|
||||||
const state = stateOverride || await getState();
|
const state = stateOverride || await getState();
|
||||||
const record = buildAccountRunHistoryRecord(state, status, reason);
|
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 {
|
return {
|
||||||
appendAccountRunRecord,
|
appendAccountRunRecord,
|
||||||
appendAccountRunHistoryRecord,
|
appendAccountRunHistoryRecord,
|
||||||
buildAccountRunHistoryRecord,
|
buildAccountRunHistoryRecord,
|
||||||
buildAccountRunHistorySnapshotPayload,
|
buildAccountRunHistorySnapshotPayload,
|
||||||
clearAccountRunHistory,
|
clearAccountRunHistory,
|
||||||
|
deleteAccountRunHistoryRecords,
|
||||||
getPersistedAccountRunHistory,
|
getPersistedAccountRunHistory,
|
||||||
normalizeAccountRunHistory,
|
normalizeAccountRunHistory,
|
||||||
normalizeAccountRunHistoryRecord,
|
normalizeAccountRunHistoryRecord,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
cancelScheduledAutoRun,
|
cancelScheduledAutoRun,
|
||||||
checkIcloudSession,
|
checkIcloudSession,
|
||||||
clearAccountRunHistory,
|
clearAccountRunHistory,
|
||||||
|
deleteAccountRunHistoryRecords,
|
||||||
clearAutoRunTimerAlarm,
|
clearAutoRunTimerAlarm,
|
||||||
clearLuckmailRuntimeState,
|
clearLuckmailRuntimeState,
|
||||||
clearStopRequest,
|
clearStopRequest,
|
||||||
@@ -300,6 +301,19 @@
|
|||||||
return { ok: true, ...result };
|
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': {
|
case 'EXECUTE_STEP': {
|
||||||
clearStopRequest();
|
clearStopRequest();
|
||||||
if (message.source === 'sidepanel') {
|
if (message.source === 'sidepanel') {
|
||||||
|
|||||||
@@ -11,7 +11,44 @@
|
|||||||
const displayTimeZone = constants.displayTimeZone || 'Asia/Shanghai';
|
const displayTimeZone = constants.displayTimeZone || 'Asia/Shanghai';
|
||||||
const pageSize = Math.max(1, Math.floor(Number(constants.pageSize) || 10));
|
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 currentPage = 1;
|
||||||
|
let activeFilter = 'all';
|
||||||
|
let selectionMode = false;
|
||||||
|
let eventsBound = false;
|
||||||
|
const selectedRecordIds = new Set();
|
||||||
|
|
||||||
function escapeHtml(value) {
|
function escapeHtml(value) {
|
||||||
if (typeof helpers.escapeHtml === 'function') {
|
if (typeof helpers.escapeHtml === 'function') {
|
||||||
@@ -25,6 +62,17 @@
|
|||||||
return Number.isFinite(timestamp) ? timestamp : 0;
|
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()) {
|
function getAccountRunRecords(currentState = state.getLatestState()) {
|
||||||
return (Array.isArray(currentState?.accountRunHistory) ? currentState.accountRunHistory : [])
|
return (Array.isArray(currentState?.accountRunHistory) ? currentState.accountRunHistory : [])
|
||||||
.filter((item) => item && typeof item === 'object')
|
.filter((item) => item && typeof item === 'object')
|
||||||
@@ -34,6 +82,7 @@
|
|||||||
|
|
||||||
function summarizeAccountRunHistory(records = []) {
|
function summarizeAccountRunHistory(records = []) {
|
||||||
return records.reduce((summary, record) => {
|
return records.reduce((summary, record) => {
|
||||||
|
const retryCount = normalizeRetryCount(record.retryCount);
|
||||||
summary.total += 1;
|
summary.total += 1;
|
||||||
if (record.finalStatus === 'success') {
|
if (record.finalStatus === 'success') {
|
||||||
summary.success += 1;
|
summary.success += 1;
|
||||||
@@ -42,13 +91,17 @@
|
|||||||
} else if (record.finalStatus === 'stopped') {
|
} else if (record.finalStatus === 'stopped') {
|
||||||
summary.stopped += 1;
|
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;
|
return summary;
|
||||||
}, {
|
}, {
|
||||||
total: 0,
|
total: 0,
|
||||||
success: 0,
|
success: 0,
|
||||||
failed: 0,
|
failed: 0,
|
||||||
stopped: 0,
|
stopped: 0,
|
||||||
|
retryRecordCount: 0,
|
||||||
retryTotal: 0,
|
retryTotal: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -60,7 +113,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const sameYear = date.getFullYear() == now.getFullYear();
|
const sameYear = date.getFullYear() === now.getFullYear();
|
||||||
const sameDay = date.toDateString() === now.toDateString();
|
const sameDay = date.toDateString() === now.toDateString();
|
||||||
|
|
||||||
if (sameDay) {
|
if (sameDay) {
|
||||||
@@ -101,31 +154,165 @@
|
|||||||
return String(record.failureLabel || '').trim() || '流程失败';
|
return String(record.failureLabel || '').trim() || '流程失败';
|
||||||
}
|
}
|
||||||
|
|
||||||
function createStatChip(label, value, className = '') {
|
function getFilterConfig(filterKey = activeFilter) {
|
||||||
return `<span class="account-records-stat${className ? ` ${className}` : ''}"><strong>${escapeHtml(String(value))}</strong>${escapeHtml(label)}</span>`;
|
return FILTER_CONFIG[filterKey] || FILTER_CONFIG.all;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateHeader(records) {
|
function getFilteredRecords(records = []) {
|
||||||
if (!dom.accountRecordsMeta || !dom.accountRecordsStats) {
|
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 `
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="${classNames}"
|
||||||
|
data-account-record-filter="${escapeHtml(filterKey)}"
|
||||||
|
aria-pressed="${activeFilter === filterKey ? 'true' : 'false'}"
|
||||||
|
>
|
||||||
|
<strong>${escapeHtml(String(value))}</strong>${escapeHtml(filterConfig.label)}
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHeader(allRecords, filteredRecords) {
|
||||||
|
if (!dom.accountRecordsMeta) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!records.length) {
|
if (!allRecords.length) {
|
||||||
dom.accountRecordsMeta.textContent = '暂无邮箱记录';
|
dom.accountRecordsMeta.textContent = '暂无邮箱记录';
|
||||||
} else {
|
return;
|
||||||
dom.accountRecordsMeta.textContent = `共 ${records.length} 条,最近更新于 ${formatAccountRecordTime(records[0]?.finishedAt)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = [
|
dom.accountRecordsStats.innerHTML = [
|
||||||
createStatChip('总', summary.total),
|
createStatChip('all', summary.total),
|
||||||
createStatChip('成', summary.success, 'is-success'),
|
createStatChip('success', summary.success),
|
||||||
createStatChip('失', summary.failed, 'is-failed'),
|
createStatChip('failed', summary.failed),
|
||||||
createStatChip('停', summary.stopped, 'is-stopped'),
|
createStatChip('stopped', summary.stopped),
|
||||||
createStatChip('重试', summary.retryTotal, 'is-retry'),
|
createStatChip('retry', summary.retryTotal),
|
||||||
].join('');
|
].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) {
|
function updatePagination(totalRecords) {
|
||||||
const totalPages = totalRecords > 0 ? Math.ceil(totalRecords / pageSize) : 0;
|
const totalPages = totalRecords > 0 ? Math.ceil(totalRecords / pageSize) : 0;
|
||||||
if (totalPages === 0) {
|
if (totalPages === 0) {
|
||||||
@@ -136,44 +323,73 @@
|
|||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dom.accountRecordsPageLabel) {
|
setNodeText(dom.accountRecordsPageLabel, totalPages > 0 ? `${currentPage} / ${totalPages}` : '0 / 0');
|
||||||
dom.accountRecordsPageLabel.textContent = totalPages > 0 ? `${currentPage} / ${totalPages}` : '0 / 0';
|
setNodeDisabled(dom.btnAccountRecordsPrev, totalPages <= 1 || currentPage <= 1);
|
||||||
}
|
setNodeDisabled(dom.btnAccountRecordsNext, totalPages <= 1 || currentPage >= totalPages);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalPages;
|
return totalPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRecordList(records = []) {
|
function renderEmptyState(allRecords) {
|
||||||
if (!dom.accountRecordsList) {
|
if (!dom.accountRecordsList) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalPages = updatePagination(records.length);
|
const message = allRecords.length
|
||||||
if (!records.length) {
|
? `当前筛选“${getFilterConfig(activeFilter).metaLabel}”下暂无记录`
|
||||||
dom.accountRecordsList.innerHTML = '<div class="account-records-empty">暂无邮箱记录。</div>';
|
: '暂无邮箱记录';
|
||||||
|
dom.accountRecordsList.innerHTML = `<div class="account-records-empty">${escapeHtml(message)}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRecordList(allRecords, filteredRecords) {
|
||||||
|
if (!dom.accountRecordsList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalPages = updatePagination(filteredRecords.length);
|
||||||
|
if (!filteredRecords.length) {
|
||||||
|
renderEmptyState(allRecords);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const startIndex = (currentPage - 1) * pageSize;
|
const startIndex = (currentPage - 1) * pageSize;
|
||||||
const visibleRecords = records.slice(startIndex, startIndex + pageSize);
|
const visibleRecords = filteredRecords.slice(startIndex, startIndex + pageSize);
|
||||||
|
|
||||||
dom.accountRecordsList.innerHTML = visibleRecords.map((record) => {
|
dom.accountRecordsList.innerHTML = visibleRecords.map((record) => {
|
||||||
|
const recordId = buildRecordId(record);
|
||||||
const statusMeta = getStatusMeta(record);
|
const statusMeta = getStatusMeta(record);
|
||||||
const summaryText = getRecordSummaryText(record);
|
const summaryText = getRecordSummaryText(record);
|
||||||
const retryCount = Math.max(0, Math.floor(Number(record.retryCount) || 0));
|
const retryCount = normalizeRetryCount(record.retryCount);
|
||||||
|
const isSelected = selectedRecordIds.has(recordId);
|
||||||
|
const itemClassNames = [
|
||||||
|
'account-record-item',
|
||||||
|
`is-${statusMeta.kind}`,
|
||||||
|
selectionMode ? 'is-selectable' : '',
|
||||||
|
isSelected ? 'is-selected' : '',
|
||||||
|
].filter(Boolean).join(' ');
|
||||||
|
const selectionMarkup = selectionMode
|
||||||
|
? `
|
||||||
|
<label class="account-record-item-check" data-account-record-toggle="${escapeHtml(recordId)}">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
data-account-record-checkbox="${escapeHtml(recordId)}"
|
||||||
|
${isSelected ? 'checked' : ''}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
`
|
||||||
|
: '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="account-record-item is-${escapeHtml(statusMeta.kind)}" title="${escapeHtml(String(record.email || ''))}">
|
<div
|
||||||
|
class="${itemClassNames}"
|
||||||
|
data-account-record-id="${escapeHtml(recordId)}"
|
||||||
|
title="${escapeHtml(String(record.email || ''))}"
|
||||||
|
>
|
||||||
<div class="account-record-item-top">
|
<div class="account-record-item-top">
|
||||||
<div class="account-record-item-email mono">${escapeHtml(String(record.email || '').trim() || '(空邮箱)')}</div>
|
<div class="account-record-item-email-row">
|
||||||
|
${selectionMarkup}
|
||||||
|
<div class="account-record-item-email mono">${escapeHtml(String(record.email || '').trim() || '(空邮箱)')}</div>
|
||||||
|
</div>
|
||||||
<div class="account-record-item-side">
|
<div class="account-record-item-side">
|
||||||
<span class="account-record-item-status">${escapeHtml(statusMeta.label)}</span>
|
<span class="account-record-item-status">${escapeHtml(statusMeta.label)}</span>
|
||||||
<span class="account-record-item-time mono">${escapeHtml(formatAccountRecordTime(record.finishedAt))}</span>
|
<span class="account-record-item-time mono">${escapeHtml(formatAccountRecordTime(record.finishedAt))}</span>
|
||||||
@@ -187,27 +403,67 @@
|
|||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
if (totalPages <= 1 && dom.accountRecordsPageLabel) {
|
if (totalPages <= 1) {
|
||||||
dom.accountRecordsPageLabel.textContent = '1 / 1';
|
setNodeText(dom.accountRecordsPageLabel, '1 / 1');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(currentState = state.getLatestState()) {
|
function render(currentState = state.getLatestState()) {
|
||||||
const records = getAccountRunRecords(currentState);
|
const allRecords = getAccountRunRecords(currentState);
|
||||||
updateHeader(records);
|
pruneSelectedRecordIds(allRecords);
|
||||||
renderRecordList(records);
|
|
||||||
|
if (!allRecords.length) {
|
||||||
|
selectionMode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredRecords = getFilteredRecords(allRecords);
|
||||||
|
updateHeader(allRecords, filteredRecords);
|
||||||
|
updateStats(allRecords);
|
||||||
|
updateToolbarState(allRecords);
|
||||||
|
renderRecordList(allRecords, filteredRecords);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPanel() {
|
function openPanel() {
|
||||||
if (dom.accountRecordsOverlay) {
|
setNodeHidden(dom.accountRecordsOverlay, false);
|
||||||
dom.accountRecordsOverlay.hidden = false;
|
|
||||||
}
|
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
function closePanel() {
|
function closePanel() {
|
||||||
if (dom.accountRecordsOverlay) {
|
setNodeHidden(dom.accountRecordsOverlay, true);
|
||||||
dom.accountRecordsOverlay.hidden = true;
|
}
|
||||||
|
|
||||||
|
function resetSelection() {
|
||||||
|
selectedRecordIds.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectionMode(nextValue) {
|
||||||
|
const nextSelectionMode = Boolean(nextValue);
|
||||||
|
if (!nextSelectionMode) {
|
||||||
|
resetSelection();
|
||||||
|
}
|
||||||
|
selectionMode = nextSelectionMode;
|
||||||
|
currentPage = 1;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSelectionMode() {
|
||||||
|
setSelectionMode(!selectionMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRecordSelection(recordId, forceSelected = null) {
|
||||||
|
const normalizedRecordId = String(recordId || '').trim().toLowerCase();
|
||||||
|
if (!selectionMode || !normalizedRecordId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldSelect = forceSelected === null
|
||||||
|
? !selectedRecordIds.has(normalizedRecordId)
|
||||||
|
: Boolean(forceSelected);
|
||||||
|
|
||||||
|
if (shouldSelect) {
|
||||||
|
selectedRecordIds.add(normalizedRecordId);
|
||||||
|
} else {
|
||||||
|
selectedRecordIds.delete(normalizedRecordId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,12 +492,98 @@
|
|||||||
throw new Error(response.error);
|
throw new Error(response.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activeFilter = 'all';
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
|
selectionMode = false;
|
||||||
|
resetSelection();
|
||||||
state.syncLatestState({ accountRunHistory: [] });
|
state.syncLatestState({ accountRunHistory: [] });
|
||||||
helpers.showToast?.(`已清理 ${Math.max(0, Number(response?.clearedCount) || 0)} 条邮箱记录`, 'success', 2200);
|
helpers.showToast?.(`已清理 ${Math.max(0, Number(response?.clearedCount) || 0)} 条邮箱记录。`, 'success', 2200);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteSelectedRecords() {
|
||||||
|
const recordIds = Array.from(selectedRecordIds).filter(Boolean);
|
||||||
|
if (!recordIds.length) {
|
||||||
|
helpers.showToast?.('请先勾选要删除的邮箱记录。', 'warn', 1800);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmed = await helpers.openConfirmModal({
|
||||||
|
title: '删除选中记录',
|
||||||
|
message: `确认删除选中的 ${recordIds.length} 条邮箱记录吗?该操作会同步更新本地 helper 快照。`,
|
||||||
|
confirmLabel: '确认删除',
|
||||||
|
confirmVariant: 'btn-danger',
|
||||||
|
});
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await runtime.sendMessage({
|
||||||
|
type: 'DELETE_ACCOUNT_RUN_HISTORY_RECORDS',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: {
|
||||||
|
recordIds,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (response?.error) {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingRecords = getAccountRunRecords();
|
||||||
|
const selectedIds = new Set(recordIds);
|
||||||
|
const nextRecords = existingRecords.filter((record) => !selectedIds.has(buildRecordId(record)));
|
||||||
|
|
||||||
|
resetSelection();
|
||||||
|
state.syncLatestState({ accountRunHistory: nextRecords });
|
||||||
|
helpers.showToast?.(`已删除 ${Math.max(0, Number(response?.deletedCount) || 0)} 条邮箱记录。`, 'success', 2200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStatsClick(event) {
|
||||||
|
const filterNode = findClosest(event?.target, '[data-account-record-filter]');
|
||||||
|
if (!filterNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextFilter = getDatasetValue(filterNode, 'data-account-record-filter');
|
||||||
|
if (!FILTER_CONFIG[nextFilter]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeFilter = activeFilter === nextFilter && nextFilter !== 'all'
|
||||||
|
? 'all'
|
||||||
|
: nextFilter;
|
||||||
|
currentPage = 1;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRecordListClick(event) {
|
||||||
|
if (!selectionMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleNode = findClosest(event?.target, '[data-account-record-toggle]');
|
||||||
|
if (toggleNode) {
|
||||||
|
const recordId = getDatasetValue(toggleNode, 'data-account-record-toggle');
|
||||||
|
const explicitChecked = typeof event?.target?.checked === 'boolean' ? event.target.checked : null;
|
||||||
|
toggleRecordSelection(recordId, explicitChecked);
|
||||||
|
render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recordNode = findClosest(event?.target, '[data-account-record-id]');
|
||||||
|
if (!recordNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleRecordSelection(getDatasetValue(recordNode, 'data-account-record-id'));
|
||||||
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindEvents() {
|
function bindEvents() {
|
||||||
|
if (eventsBound) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
eventsBound = true;
|
||||||
|
|
||||||
dom.btnOpenAccountRecords?.addEventListener('click', () => {
|
dom.btnOpenAccountRecords?.addEventListener('click', () => {
|
||||||
openPanel();
|
openPanel();
|
||||||
});
|
});
|
||||||
@@ -253,6 +595,12 @@
|
|||||||
closePanel();
|
closePanel();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
dom.accountRecordsStats?.addEventListener('click', (event) => {
|
||||||
|
handleStatsClick(event);
|
||||||
|
});
|
||||||
|
dom.accountRecordsList?.addEventListener('click', (event) => {
|
||||||
|
handleRecordListClick(event);
|
||||||
|
});
|
||||||
dom.btnAccountRecordsPrev?.addEventListener('click', () => {
|
dom.btnAccountRecordsPrev?.addEventListener('click', () => {
|
||||||
if (currentPage <= 1) {
|
if (currentPage <= 1) {
|
||||||
return;
|
return;
|
||||||
@@ -264,26 +612,45 @@
|
|||||||
currentPage += 1;
|
currentPage += 1;
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
dom.btnClearAccountRecords?.addEventListener('click', () => {
|
dom.btnToggleAccountRecordsSelection?.addEventListener('click', () => {
|
||||||
clearRecords().catch((error) => {
|
toggleSelectionMode();
|
||||||
|
});
|
||||||
|
dom.btnDeleteSelectedAccountRecords?.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
await deleteSelectedRecords();
|
||||||
|
} catch (error) {
|
||||||
|
helpers.showToast?.(`删除邮箱记录失败:${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dom.btnClearAccountRecords?.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
await clearRecords();
|
||||||
|
} catch (error) {
|
||||||
helpers.showToast?.(`清理邮箱记录失败:${error.message}`, 'error');
|
helpers.showToast?.(`清理邮箱记录失败:${error.message}`, 'error');
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
|
activeFilter = 'all';
|
||||||
|
selectionMode = false;
|
||||||
|
resetSelection();
|
||||||
closePanel();
|
closePanel();
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
bindEvents,
|
bindEvents,
|
||||||
|
clearRecords,
|
||||||
closePanel,
|
closePanel,
|
||||||
|
deleteSelectedRecords,
|
||||||
openPanel,
|
openPanel,
|
||||||
render,
|
render,
|
||||||
reset,
|
reset,
|
||||||
|
setSelectionMode,
|
||||||
summarizeAccountRunHistory,
|
summarizeAccountRunHistory,
|
||||||
|
toggleSelectionMode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+105
-7
@@ -27,6 +27,8 @@
|
|||||||
--green-soft: rgba(22, 163, 74, 0.08);
|
--green-soft: rgba(22, 163, 74, 0.08);
|
||||||
--orange: #ea580c;
|
--orange: #ea580c;
|
||||||
--orange-soft: rgba(234, 88, 12, 0.08);
|
--orange-soft: rgba(234, 88, 12, 0.08);
|
||||||
|
--amber: #d97706;
|
||||||
|
--amber-soft: rgba(217, 119, 6, 0.08);
|
||||||
--red: #dc2626;
|
--red: #dc2626;
|
||||||
--red-soft: rgba(220, 38, 38, 0.08);
|
--red-soft: rgba(220, 38, 38, 0.08);
|
||||||
--cyan: #0891b2;
|
--cyan: #0891b2;
|
||||||
@@ -62,6 +64,8 @@
|
|||||||
--green-soft: rgba(34, 197, 94, 0.12);
|
--green-soft: rgba(34, 197, 94, 0.12);
|
||||||
--orange: #f97316;
|
--orange: #f97316;
|
||||||
--orange-soft: rgba(249, 115, 22, 0.12);
|
--orange-soft: rgba(249, 115, 22, 0.12);
|
||||||
|
--amber: #f59e0b;
|
||||||
|
--amber-soft: rgba(245, 158, 11, 0.12);
|
||||||
--red: #ef4444;
|
--red: #ef4444;
|
||||||
--red-soft: rgba(239, 68, 68, 0.12);
|
--red-soft: rgba(239, 68, 68, 0.12);
|
||||||
--cyan: #06b6d4;
|
--cyan: #06b6d4;
|
||||||
@@ -1788,24 +1792,55 @@ header {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-records-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.account-records-stats {
|
.account-records-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-records-toolbar-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 4px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-records-stats::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-records-stat {
|
.account-records-stat {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 3px;
|
||||||
padding: 2px 8px;
|
padding: 2px 7px;
|
||||||
border-radius: 999px;
|
|
||||||
border: 1px solid var(--border-subtle);
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: 999px;
|
||||||
background: color-mix(in srgb, var(--bg-base) 86%, transparent);
|
background: color-mix(in srgb, var(--bg-base) 86%, transparent);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-family: inherit;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-secondary);
|
white-space: nowrap;
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background var(--transition), border-color var(--transition), color var(--transition), transform var(--transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-records-stat strong {
|
.account-records-stat strong {
|
||||||
@@ -1813,6 +1848,17 @@ header {
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-records-stat:hover:not(:disabled) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
border-color: color-mix(in srgb, var(--blue) 20%, var(--border));
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-records-stat.is-active {
|
||||||
|
border-color: color-mix(in srgb, var(--blue) 24%, var(--border));
|
||||||
|
box-shadow: 0 0 0 1px color-mix(in srgb, var(--blue) 12%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.account-records-stat.is-success {
|
.account-records-stat.is-success {
|
||||||
color: var(--green);
|
color: var(--green);
|
||||||
background: var(--green-soft);
|
background: var(--green-soft);
|
||||||
@@ -1837,6 +1883,16 @@ header {
|
|||||||
border-color: color-mix(in srgb, var(--amber) 16%, var(--border));
|
border-color: color-mix(in srgb, var(--amber) 16%, var(--border));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-records-toolbar-actions .btn.is-active {
|
||||||
|
background: var(--blue-soft);
|
||||||
|
border-color: color-mix(in srgb, var(--blue) 22%, var(--border));
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-records-toolbar-actions .btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.account-records-list {
|
.account-records-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -1875,6 +1931,22 @@ header {
|
|||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
background: color-mix(in srgb, var(--bg-base) 90%, transparent);
|
background: color-mix(in srgb, var(--bg-base) 90%, transparent);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
|
transition: border-color var(--transition), background var(--transition), box-shadow var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-record-item.is-selectable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-record-item.is-selectable:hover {
|
||||||
|
border-color: color-mix(in srgb, var(--blue) 18%, var(--border));
|
||||||
|
background: color-mix(in srgb, var(--bg-base) 82%, var(--blue-soft));
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-record-item.is-selected {
|
||||||
|
border-color: color-mix(in srgb, var(--blue) 26%, var(--border));
|
||||||
|
background: color-mix(in srgb, var(--bg-base) 78%, var(--blue-soft));
|
||||||
|
box-shadow: var(--shadow-sm), 0 0 0 1px color-mix(in srgb, var(--blue) 10%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-record-item-top,
|
.account-record-item-top,
|
||||||
@@ -1885,6 +1957,32 @@ header {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-record-item-email-row {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-record-item-check {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-record-item-check input {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin: 0;
|
||||||
|
accent-color: var(--blue);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.account-record-item-email {
|
.account-record-item-email {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -2055,7 +2153,7 @@ header {
|
|||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 1200;
|
z-index: 1400;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -575,11 +575,17 @@
|
|||||||
<span id="account-records-meta" class="account-records-panel-meta">暂无邮箱记录</span>
|
<span id="account-records-meta" class="account-records-panel-meta">暂无邮箱记录</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="account-records-panel-actions">
|
<div class="account-records-panel-actions">
|
||||||
<button id="btn-clear-account-records" class="btn btn-ghost btn-xs" type="button">清理记录</button>
|
|
||||||
<button id="btn-close-account-records" class="modal-close" type="button" aria-label="关闭">×</button>
|
<button id="btn-close-account-records" class="modal-close" type="button" aria-label="关闭">×</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="account-records-stats" class="account-records-stats"></div>
|
<div class="account-records-toolbar">
|
||||||
|
<div id="account-records-stats" class="account-records-stats" role="group" aria-label="邮箱记录筛选"></div>
|
||||||
|
<div class="account-records-toolbar-actions">
|
||||||
|
<button id="btn-toggle-account-records-selection" class="btn btn-ghost btn-xs" type="button">多选</button>
|
||||||
|
<button id="btn-delete-selected-account-records" class="btn btn-ghost btn-xs" type="button" hidden disabled>删除选中</button>
|
||||||
|
<button id="btn-clear-account-records" class="btn btn-ghost btn-xs" type="button">清理记录</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="account-records-list" class="account-records-list"></div>
|
<div id="account-records-list" class="account-records-list"></div>
|
||||||
<div class="account-records-pagination">
|
<div class="account-records-pagination">
|
||||||
<button id="btn-account-records-prev" class="btn btn-outline btn-xs" type="button">上一页</button>
|
<button id="btn-account-records-prev" class="btn btn-outline btn-xs" type="button">上一页</button>
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ const btnAccountRecordsPrev = document.getElementById('btn-account-records-prev'
|
|||||||
const btnAccountRecordsNext = document.getElementById('btn-account-records-next');
|
const btnAccountRecordsNext = document.getElementById('btn-account-records-next');
|
||||||
const btnCloseAccountRecords = document.getElementById('btn-close-account-records');
|
const btnCloseAccountRecords = document.getElementById('btn-close-account-records');
|
||||||
const btnClearAccountRecords = document.getElementById('btn-clear-account-records');
|
const btnClearAccountRecords = document.getElementById('btn-clear-account-records');
|
||||||
|
const btnToggleAccountRecordsSelection = document.getElementById('btn-toggle-account-records-selection');
|
||||||
|
const btnDeleteSelectedAccountRecords = document.getElementById('btn-delete-selected-account-records');
|
||||||
const updateSection = document.getElementById('update-section');
|
const updateSection = document.getElementById('update-section');
|
||||||
const btnRepoHome = document.getElementById('btn-repo-home');
|
const btnRepoHome = document.getElementById('btn-repo-home');
|
||||||
const extensionUpdateStatus = document.getElementById('extension-update-status');
|
const extensionUpdateStatus = document.getElementById('extension-update-status');
|
||||||
@@ -2978,8 +2980,10 @@ const accountRecordsManager = window.SidepanelAccountRecordsManager?.createAccou
|
|||||||
btnAccountRecordsNext,
|
btnAccountRecordsNext,
|
||||||
btnAccountRecordsPrev,
|
btnAccountRecordsPrev,
|
||||||
btnClearAccountRecords,
|
btnClearAccountRecords,
|
||||||
|
btnDeleteSelectedAccountRecords,
|
||||||
btnCloseAccountRecords,
|
btnCloseAccountRecords,
|
||||||
btnOpenAccountRecords,
|
btnOpenAccountRecords,
|
||||||
|
btnToggleAccountRecordsSelection,
|
||||||
},
|
},
|
||||||
helpers: {
|
helpers: {
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
|
|||||||
@@ -203,3 +203,103 @@ test('account run history helper clears persisted records and syncs full snapsho
|
|||||||
});
|
});
|
||||||
assert.equal(logs[0].message, '账号记录快照已同步到本地:C:/tmp/account-run-history.json');
|
assert.equal(logs[0].message, '账号记录快照已同步到本地:C:/tmp/account-run-history.json');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('account run history helper deletes selected records and syncs remaining snapshot payload', async () => {
|
||||||
|
const source = fs.readFileSync('background/account-run-history.js', 'utf8');
|
||||||
|
const globalScope = {};
|
||||||
|
const api = new Function('self', `${source}; return self.MultiPageBackgroundAccountRunHistory;`)(globalScope);
|
||||||
|
|
||||||
|
let storedHistory = [
|
||||||
|
{
|
||||||
|
recordId: 'keep@example.com',
|
||||||
|
email: 'keep@example.com',
|
||||||
|
password: 'secret',
|
||||||
|
finalStatus: 'success',
|
||||||
|
finishedAt: '2026-04-17T01:10:00.000Z',
|
||||||
|
retryCount: 0,
|
||||||
|
failureLabel: '流程完成',
|
||||||
|
failureDetail: '',
|
||||||
|
failedStep: null,
|
||||||
|
source: 'manual',
|
||||||
|
autoRunContext: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
recordId: 'remove@example.com',
|
||||||
|
email: 'remove@example.com',
|
||||||
|
password: 'secret',
|
||||||
|
finalStatus: 'failed',
|
||||||
|
finishedAt: '2026-04-17T01:00:00.000Z',
|
||||||
|
retryCount: 2,
|
||||||
|
failureLabel: '步骤 8 失败',
|
||||||
|
failureDetail: '步骤 8:认证页异常',
|
||||||
|
failedStep: 8,
|
||||||
|
source: 'auto',
|
||||||
|
autoRunContext: {
|
||||||
|
currentRun: 1,
|
||||||
|
totalRuns: 5,
|
||||||
|
attemptRun: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const fetchCalls = [];
|
||||||
|
global.fetch = async (url, options = {}) => {
|
||||||
|
fetchCalls.push({
|
||||||
|
url,
|
||||||
|
options,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
ok: true,
|
||||||
|
filePath: 'C:/tmp/account-run-history.json',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const logs = [];
|
||||||
|
const helpers = api.createAccountRunHistoryHelpers({
|
||||||
|
ACCOUNT_RUN_HISTORY_STORAGE_KEY: 'accountRunHistory',
|
||||||
|
addLog: async (message, level) => {
|
||||||
|
logs.push({ message, level });
|
||||||
|
},
|
||||||
|
buildLocalHelperEndpoint: (baseUrl, path) => `${baseUrl}${path}`,
|
||||||
|
chrome: {
|
||||||
|
storage: {
|
||||||
|
local: {
|
||||||
|
get: async () => ({ accountRunHistory: storedHistory }),
|
||||||
|
set: async (payload) => {
|
||||||
|
storedHistory = payload.accountRunHistory;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||||
|
getState: async () => ({
|
||||||
|
accountRunHistoryTextEnabled: true,
|
||||||
|
accountRunHistoryHelperBaseUrl: 'http://127.0.0.1:17373',
|
||||||
|
}),
|
||||||
|
normalizeAccountRunHistoryHelperBaseUrl: (value) => String(value || '').trim(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await helpers.deleteAccountRunHistoryRecords(['remove@example.com']);
|
||||||
|
assert.deepStrictEqual(result, {
|
||||||
|
deletedCount: 1,
|
||||||
|
remainingCount: 1,
|
||||||
|
});
|
||||||
|
assert.equal(storedHistory.length, 1);
|
||||||
|
assert.equal(storedHistory[0].email, 'keep@example.com');
|
||||||
|
assert.equal(fetchCalls.length, 1);
|
||||||
|
assert.equal(fetchCalls[0].url, 'http://127.0.0.1:17373/sync-account-run-records');
|
||||||
|
assert.deepStrictEqual(JSON.parse(fetchCalls[0].options.body), {
|
||||||
|
generatedAt: JSON.parse(fetchCalls[0].options.body).generatedAt,
|
||||||
|
summary: {
|
||||||
|
total: 1,
|
||||||
|
success: 1,
|
||||||
|
failed: 0,
|
||||||
|
stopped: 0,
|
||||||
|
retryTotal: 0,
|
||||||
|
},
|
||||||
|
records: storedHistory,
|
||||||
|
});
|
||||||
|
assert.equal(logs[0].message, '账号记录快照已同步到本地:C:/tmp/account-run-history.json');
|
||||||
|
});
|
||||||
|
|||||||
@@ -48,30 +48,86 @@ function extractFunction(name) {
|
|||||||
return sidepanelSource.slice(start, end);
|
return sidepanelSource.slice(start, end);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createButton() {
|
function createClassList() {
|
||||||
|
const classNames = new Set();
|
||||||
return {
|
return {
|
||||||
disabled: false,
|
add(...values) {
|
||||||
textContent: '',
|
values.forEach((value) => classNames.add(String(value)));
|
||||||
hidden: false,
|
},
|
||||||
listeners: {},
|
remove(...values) {
|
||||||
addEventListener(type, handler) {
|
values.forEach((value) => classNames.delete(String(value)));
|
||||||
this.listeners[type] = handler;
|
},
|
||||||
|
toggle(value, force) {
|
||||||
|
const key = String(value);
|
||||||
|
if (force === undefined) {
|
||||||
|
if (classNames.has(key)) {
|
||||||
|
classNames.delete(key);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
classNames.add(key);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (force) {
|
||||||
|
classNames.add(key);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
classNames.delete(key);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
contains(value) {
|
||||||
|
return classNames.has(String(value));
|
||||||
|
},
|
||||||
|
toString() {
|
||||||
|
return Array.from(classNames).join(' ');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createContainer() {
|
function createNode(initial = {}) {
|
||||||
return {
|
return {
|
||||||
innerHTML: '',
|
innerHTML: '',
|
||||||
textContent: '',
|
textContent: '',
|
||||||
hidden: true,
|
hidden: false,
|
||||||
|
disabled: false,
|
||||||
listeners: {},
|
listeners: {},
|
||||||
|
attributes: {},
|
||||||
|
dataset: {},
|
||||||
|
classList: createClassList(),
|
||||||
addEventListener(type, handler) {
|
addEventListener(type, handler) {
|
||||||
this.listeners[type] = handler;
|
this.listeners[type] = handler;
|
||||||
},
|
},
|
||||||
|
setAttribute(name, value) {
|
||||||
|
this.attributes[name] = String(value);
|
||||||
|
},
|
||||||
|
getAttribute(name) {
|
||||||
|
return this.attributes[name];
|
||||||
|
},
|
||||||
|
...initial,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createClosestTarget(matches = {}, checked) {
|
||||||
|
return {
|
||||||
|
checked,
|
||||||
|
closest(selector) {
|
||||||
|
return matches[selector] || null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDataNode(attrName, attrValue) {
|
||||||
|
return {
|
||||||
|
dataset: {},
|
||||||
|
getAttribute(name) {
|
||||||
|
return name === attrName ? attrValue : '';
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function flushPromises() {
|
||||||
|
await new Promise((resolve) => setImmediate(resolve));
|
||||||
|
}
|
||||||
|
|
||||||
test('sidepanel html contains account records overlay and manager script', () => {
|
test('sidepanel html contains account records overlay and manager script', () => {
|
||||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||||
const managerIndex = html.indexOf('<script src="account-records-manager.js"></script>');
|
const managerIndex = html.indexOf('<script src="account-records-manager.js"></script>');
|
||||||
@@ -82,12 +138,24 @@ test('sidepanel html contains account records overlay and manager script', () =>
|
|||||||
assert.match(html, /id="account-records-list"/);
|
assert.match(html, /id="account-records-list"/);
|
||||||
assert.match(html, /id="account-records-stats"/);
|
assert.match(html, /id="account-records-stats"/);
|
||||||
assert.match(html, /id="btn-clear-account-records"/);
|
assert.match(html, /id="btn-clear-account-records"/);
|
||||||
|
assert.match(html, /id="btn-toggle-account-records-selection"/);
|
||||||
|
assert.match(html, /id="btn-delete-selected-account-records"/);
|
||||||
assert.match(html, /id="input-sub2api-default-proxy"/);
|
assert.match(html, /id="input-sub2api-default-proxy"/);
|
||||||
assert.notEqual(managerIndex, -1);
|
assert.notEqual(managerIndex, -1);
|
||||||
assert.notEqual(sidepanelIndex, -1);
|
assert.notEqual(sidepanelIndex, -1);
|
||||||
assert.ok(managerIndex < sidepanelIndex);
|
assert.ok(managerIndex < sidepanelIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('sidepanel css keeps confirm modal above account records overlay', () => {
|
||||||
|
const css = fs.readFileSync('sidepanel/sidepanel.css', 'utf8');
|
||||||
|
const overlayMatch = css.match(/\.account-records-overlay\s*\{[\s\S]*?z-index:\s*(\d+);/);
|
||||||
|
const modalMatch = css.match(/\.modal-overlay\s*\{[\s\S]*?z-index:\s*(\d+);/);
|
||||||
|
|
||||||
|
assert.ok(overlayMatch, 'missing account records overlay z-index');
|
||||||
|
assert.ok(modalMatch, 'missing modal overlay z-index');
|
||||||
|
assert.ok(Number(modalMatch[1]) > Number(overlayMatch[1]));
|
||||||
|
});
|
||||||
|
|
||||||
test('sidepanel account records helper normalizes snapshot helper base url', () => {
|
test('sidepanel account records helper normalizes snapshot helper base url', () => {
|
||||||
const bundle = [
|
const bundle = [
|
||||||
extractFunction('normalizeAccountRunHistoryHelperBaseUrlValue'),
|
extractFunction('normalizeAccountRunHistoryHelperBaseUrlValue'),
|
||||||
@@ -105,48 +173,71 @@ return { normalizeAccountRunHistoryHelperBaseUrlValue };
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('account records manager exposes a factory and renders summarized paginated records', () => {
|
test('account records manager supports filter chips and partial multi-select delete', async () => {
|
||||||
const source = fs.readFileSync('sidepanel/account-records-manager.js', 'utf8');
|
const source = fs.readFileSync('sidepanel/account-records-manager.js', 'utf8');
|
||||||
const windowObject = {};
|
const windowObject = {};
|
||||||
|
|
||||||
const api = new Function('window', `${source}; return window.SidepanelAccountRecordsManager;`)(windowObject);
|
const api = new Function('window', `${source}; return window.SidepanelAccountRecordsManager;`)(windowObject);
|
||||||
|
|
||||||
assert.equal(typeof api?.createAccountRecordsManager, 'function');
|
assert.equal(typeof api?.createAccountRecordsManager, 'function');
|
||||||
|
|
||||||
const btnOpenAccountRecords = createButton();
|
let latestState = {
|
||||||
const btnCloseAccountRecords = createButton();
|
accountRunHistory: [
|
||||||
const btnClearAccountRecords = createButton();
|
{
|
||||||
const btnAccountRecordsPrev = createButton();
|
recordId: 'success@example.com',
|
||||||
const btnAccountRecordsNext = createButton();
|
email: 'success@example.com',
|
||||||
const overlay = createContainer();
|
password: 'secret',
|
||||||
const list = createContainer();
|
finalStatus: 'success',
|
||||||
const stats = createContainer();
|
finishedAt: '2026-04-17T04:31:00.000Z',
|
||||||
const meta = createContainer();
|
retryCount: 0,
|
||||||
const pageLabel = createContainer();
|
failureLabel: '流程完成',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
recordId: 'failed@example.com',
|
||||||
|
email: 'failed@example.com',
|
||||||
|
password: 'secret',
|
||||||
|
finalStatus: 'failed',
|
||||||
|
finishedAt: '2026-04-17T04:29:00.000Z',
|
||||||
|
retryCount: 2,
|
||||||
|
failureLabel: '出现手机号验证',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
recordId: 'stopped@example.com',
|
||||||
|
email: 'stopped@example.com',
|
||||||
|
password: 'secret',
|
||||||
|
finalStatus: 'stopped',
|
||||||
|
finishedAt: '2026-04-17T04:28:00.000Z',
|
||||||
|
retryCount: 1,
|
||||||
|
failureLabel: '流程已停止',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
const manager = api.createAccountRecordsManager({
|
const btnOpenAccountRecords = createNode();
|
||||||
|
const btnCloseAccountRecords = createNode();
|
||||||
|
const btnClearAccountRecords = createNode();
|
||||||
|
const btnToggleAccountRecordsSelection = createNode();
|
||||||
|
const btnDeleteSelectedAccountRecords = createNode({ hidden: true, disabled: true });
|
||||||
|
const btnAccountRecordsPrev = createNode();
|
||||||
|
const btnAccountRecordsNext = createNode();
|
||||||
|
const overlay = createNode();
|
||||||
|
const list = createNode();
|
||||||
|
const stats = createNode();
|
||||||
|
const meta = createNode();
|
||||||
|
const pageLabel = createNode();
|
||||||
|
const messages = [];
|
||||||
|
const toasts = [];
|
||||||
|
let manager = null;
|
||||||
|
|
||||||
|
manager = api.createAccountRecordsManager({
|
||||||
state: {
|
state: {
|
||||||
getLatestState: () => ({
|
getLatestState: () => latestState,
|
||||||
accountRunHistory: [
|
syncLatestState(nextState) {
|
||||||
{
|
latestState = {
|
||||||
email: 'success@example.com',
|
...latestState,
|
||||||
password: 'secret',
|
...(nextState || {}),
|
||||||
finalStatus: 'success',
|
};
|
||||||
finishedAt: '2026-04-17T04:31:00.000Z',
|
manager.render(latestState);
|
||||||
retryCount: 0,
|
},
|
||||||
failureLabel: '流程完成',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
email: 'failed@example.com',
|
|
||||||
password: 'secret',
|
|
||||||
finalStatus: 'failed',
|
|
||||||
finishedAt: '2026-04-17T04:29:00.000Z',
|
|
||||||
retryCount: 2,
|
|
||||||
failureLabel: '出现手机号验证',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
syncLatestState() {},
|
|
||||||
},
|
},
|
||||||
dom: {
|
dom: {
|
||||||
accountRecordsList: list,
|
accountRecordsList: list,
|
||||||
@@ -158,15 +249,33 @@ test('account records manager exposes a factory and renders summarized paginated
|
|||||||
btnAccountRecordsPrev,
|
btnAccountRecordsPrev,
|
||||||
btnClearAccountRecords,
|
btnClearAccountRecords,
|
||||||
btnCloseAccountRecords,
|
btnCloseAccountRecords,
|
||||||
|
btnDeleteSelectedAccountRecords,
|
||||||
btnOpenAccountRecords,
|
btnOpenAccountRecords,
|
||||||
|
btnToggleAccountRecordsSelection,
|
||||||
},
|
},
|
||||||
helpers: {
|
helpers: {
|
||||||
escapeHtml: (value) => String(value || ''),
|
escapeHtml: (value) => String(value || ''),
|
||||||
openConfirmModal: async () => true,
|
openConfirmModal: async () => true,
|
||||||
showToast() {},
|
showToast(message, tone) {
|
||||||
|
toasts.push({ message, tone });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
runtime: {
|
runtime: {
|
||||||
sendMessage: async () => ({ clearedCount: 2 }),
|
sendMessage: async (message) => {
|
||||||
|
messages.push(message);
|
||||||
|
if (message.type === 'DELETE_ACCOUNT_RUN_HISTORY_RECORDS') {
|
||||||
|
return {
|
||||||
|
deletedCount: message.payload.recordIds.length,
|
||||||
|
remainingCount: 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (message.type === 'CLEAR_ACCOUNT_RUN_HISTORY') {
|
||||||
|
return {
|
||||||
|
clearedCount: latestState.accountRunHistory.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
constants: {
|
constants: {
|
||||||
displayTimeZone: 'Asia/Shanghai',
|
displayTimeZone: 'Asia/Shanghai',
|
||||||
@@ -174,18 +283,59 @@ test('account records manager exposes a factory and renders summarized paginated
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(typeof manager.bindEvents, 'function');
|
|
||||||
assert.equal(typeof manager.render, 'function');
|
|
||||||
assert.equal(typeof manager.openPanel, 'function');
|
|
||||||
|
|
||||||
manager.bindEvents();
|
manager.bindEvents();
|
||||||
manager.render();
|
manager.render();
|
||||||
|
|
||||||
assert.match(meta.textContent, /共 2 条/);
|
assert.match(meta.textContent, /共 3 条/);
|
||||||
assert.match(stats.innerHTML, /重试/);
|
assert.match(stats.innerHTML, /data-account-record-filter="retry"/);
|
||||||
assert.match(list.innerHTML, /success@example\.com/);
|
assert.match(list.innerHTML, /success@example\.com/);
|
||||||
assert.match(list.innerHTML, /出现手机号验证/);
|
assert.match(list.innerHTML, /failed@example\.com/);
|
||||||
assert.match(list.innerHTML, /重试 2/);
|
|
||||||
assert.equal(pageLabel.textContent, '1 / 1');
|
assert.equal(pageLabel.textContent, '1 / 1');
|
||||||
assert.equal(btnClearAccountRecords.disabled, false);
|
assert.equal(btnDeleteSelectedAccountRecords.hidden, true);
|
||||||
|
|
||||||
|
stats.listeners.click({
|
||||||
|
target: createClosestTarget({
|
||||||
|
'[data-account-record-filter]': createDataNode('data-account-record-filter', 'retry'),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.match(meta.textContent, /当前筛选 重试 2 条/);
|
||||||
|
assert.doesNotMatch(list.innerHTML, /success@example\.com/);
|
||||||
|
assert.match(list.innerHTML, /failed@example\.com/);
|
||||||
|
assert.match(list.innerHTML, /stopped@example\.com/);
|
||||||
|
|
||||||
|
btnToggleAccountRecordsSelection.listeners.click();
|
||||||
|
|
||||||
|
assert.equal(btnDeleteSelectedAccountRecords.hidden, false);
|
||||||
|
assert.equal(btnClearAccountRecords.hidden, true);
|
||||||
|
assert.equal(btnDeleteSelectedAccountRecords.disabled, true);
|
||||||
|
assert.equal(btnToggleAccountRecordsSelection.textContent, '取消多选');
|
||||||
|
|
||||||
|
list.listeners.click({
|
||||||
|
target: createClosestTarget({
|
||||||
|
'[data-account-record-toggle]': null,
|
||||||
|
'[data-account-record-id]': createDataNode('data-account-record-id', 'failed@example.com'),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(btnDeleteSelectedAccountRecords.disabled, false);
|
||||||
|
assert.match(btnDeleteSelectedAccountRecords.textContent, /删除选中\(1\)/);
|
||||||
|
assert.match(list.innerHTML, /data-account-record-checkbox="failed@example\.com"[^>]*checked/);
|
||||||
|
|
||||||
|
await btnDeleteSelectedAccountRecords.listeners.click();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
assert.equal(messages.length, 1);
|
||||||
|
assert.equal(messages[0].type, 'DELETE_ACCOUNT_RUN_HISTORY_RECORDS');
|
||||||
|
assert.deepStrictEqual(messages[0].payload.recordIds, ['failed@example.com']);
|
||||||
|
assert.equal(latestState.accountRunHistory.length, 2);
|
||||||
|
assert.equal(latestState.accountRunHistory.some((item) => item.email === 'failed@example.com'), false);
|
||||||
|
assert.match(meta.textContent, /当前筛选 重试 1 条/);
|
||||||
|
assert.doesNotMatch(list.innerHTML, /failed@example\.com/);
|
||||||
|
assert.match(list.innerHTML, /stopped@example\.com/);
|
||||||
|
assert.equal(btnDeleteSelectedAccountRecords.disabled, true);
|
||||||
|
assert.deepStrictEqual(toasts.at(-1), {
|
||||||
|
message: '已删除 1 条邮箱记录。',
|
||||||
|
tone: 'success',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user