import GuJumpgate snapshot from GitHub
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
(function attachSidepanelAccountPoolUi(globalScope) {
|
||||
function createAccountPoolFormController(options = {}) {
|
||||
const {
|
||||
formShell = null,
|
||||
toggleButton = null,
|
||||
hiddenLabel = '添加账号',
|
||||
visibleLabel = '取消添加',
|
||||
onClear = null,
|
||||
onFocus = null,
|
||||
} = options;
|
||||
|
||||
let visible = false;
|
||||
|
||||
function sync() {
|
||||
if (formShell) {
|
||||
formShell.hidden = !visible;
|
||||
}
|
||||
if (toggleButton) {
|
||||
toggleButton.textContent = visible ? visibleLabel : hiddenLabel;
|
||||
toggleButton.setAttribute('aria-expanded', String(visible));
|
||||
}
|
||||
}
|
||||
|
||||
function setVisible(nextVisible, controlOptions = {}) {
|
||||
const {
|
||||
clearForm = false,
|
||||
focusField = false,
|
||||
} = controlOptions;
|
||||
|
||||
visible = Boolean(nextVisible);
|
||||
if (clearForm && typeof onClear === 'function') {
|
||||
onClear();
|
||||
}
|
||||
|
||||
sync();
|
||||
|
||||
if (visible && focusField && typeof onFocus === 'function') {
|
||||
onFocus();
|
||||
}
|
||||
}
|
||||
|
||||
function isVisible() {
|
||||
return visible;
|
||||
}
|
||||
|
||||
sync();
|
||||
|
||||
return {
|
||||
isVisible,
|
||||
setVisible,
|
||||
sync,
|
||||
};
|
||||
}
|
||||
|
||||
globalScope.SidepanelAccountPoolUi = {
|
||||
createAccountPoolFormController,
|
||||
};
|
||||
})(window);
|
||||
@@ -0,0 +1,843 @@
|
||||
(function attachSidepanelAccountRecordsManager(globalScope) {
|
||||
function createAccountRecordsManager(context = {}) {
|
||||
const {
|
||||
state,
|
||||
dom,
|
||||
helpers,
|
||||
runtime,
|
||||
constants = {},
|
||||
} = context;
|
||||
|
||||
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) => getRecordDisplayStatus(record) === 'success',
|
||||
metaLabel: '成功',
|
||||
},
|
||||
running: {
|
||||
label: '运行',
|
||||
className: 'is-running',
|
||||
matches: (record) => getRecordDisplayStatus(record) === 'running',
|
||||
metaLabel: '运行中',
|
||||
},
|
||||
failed: {
|
||||
label: '失',
|
||||
className: 'is-failed',
|
||||
matches: (record) => getRecordDisplayStatus(record) === 'failed',
|
||||
metaLabel: '失败',
|
||||
},
|
||||
stopped: {
|
||||
label: '停',
|
||||
className: 'is-stopped',
|
||||
matches: (record) => getRecordDisplayStatus(record) === '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') {
|
||||
return helpers.escapeHtml(String(value || ''));
|
||||
}
|
||||
return String(value || '');
|
||||
}
|
||||
|
||||
function normalizeTimestamp(value) {
|
||||
const timestamp = Date.parse(String(value || ''));
|
||||
return Number.isFinite(timestamp) ? timestamp : 0;
|
||||
}
|
||||
|
||||
function normalizeRetryCount(value) {
|
||||
const count = Math.floor(Number(value) || 0);
|
||||
return count > 0 ? count : 0;
|
||||
}
|
||||
|
||||
function buildRecordId(record = {}) {
|
||||
const rawRecordId = String(record.recordId || '').trim();
|
||||
if (rawRecordId) {
|
||||
return rawRecordId.toLowerCase();
|
||||
}
|
||||
const rawIdentifierType = String(record.accountIdentifierType || '').trim().toLowerCase();
|
||||
const hasPhoneOnlyIdentifier = !record.email && (
|
||||
record.phoneNumber
|
||||
|| record.phone
|
||||
|| record.number
|
||||
|| (record.accountIdentifier && !/@/.test(String(record.accountIdentifier || '')))
|
||||
);
|
||||
const identifierType = rawIdentifierType === 'phone'
|
||||
|| (!rawIdentifierType && hasPhoneOnlyIdentifier)
|
||||
? 'phone'
|
||||
: 'email';
|
||||
const identifier = String(
|
||||
record.accountIdentifier
|
||||
|| (identifierType === 'phone' ? (record.phoneNumber || record.phone || record.number || '') : (record.email || ''))
|
||||
|| ''
|
||||
).trim();
|
||||
if (!identifier) {
|
||||
return '';
|
||||
}
|
||||
return identifierType === 'phone'
|
||||
? `phone:${identifier.toLowerCase()}`
|
||||
: identifier.toLowerCase();
|
||||
}
|
||||
|
||||
function getRecordDisplayStatus(record = {}) {
|
||||
return String(record.displayStatus || record.finalStatus || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function isAutoRunRecordDisplayRunning(currentState = {}) {
|
||||
const phase = String(currentState.autoRunPhase || '').trim().toLowerCase();
|
||||
return Boolean(currentState.autoRunning)
|
||||
&& ['running', 'waiting_step', 'waiting_email', 'retrying'].includes(phase);
|
||||
}
|
||||
|
||||
function buildCurrentAccountRecordId(currentState = {}) {
|
||||
const accountIdentifierType = String(currentState.accountIdentifierType || '').trim().toLowerCase();
|
||||
const email = String(currentState.email || '').trim();
|
||||
const phoneNumber = String(
|
||||
currentState.signupPhoneNumber
|
||||
|| currentState.phoneNumber
|
||||
|| currentState.phone
|
||||
|| ''
|
||||
).trim();
|
||||
const accountIdentifier = String(
|
||||
currentState.accountIdentifier
|
||||
|| (accountIdentifierType === 'phone' ? phoneNumber : email)
|
||||
|| ''
|
||||
).trim();
|
||||
return buildRecordId({
|
||||
accountIdentifierType,
|
||||
accountIdentifier,
|
||||
email,
|
||||
phoneNumber,
|
||||
});
|
||||
}
|
||||
|
||||
function applyRunningDisplayState(record = {}, currentState = {}) {
|
||||
if (!isAutoRunRecordDisplayRunning(currentState)) {
|
||||
return record;
|
||||
}
|
||||
if (getRecordDisplayStatus(record) === 'success') {
|
||||
return record;
|
||||
}
|
||||
|
||||
const currentRecordId = buildCurrentAccountRecordId(currentState);
|
||||
if (!currentRecordId || buildRecordId(record) !== currentRecordId) {
|
||||
return record;
|
||||
}
|
||||
|
||||
return {
|
||||
...record,
|
||||
displayStatus: 'running',
|
||||
displaySummary: '正在运行',
|
||||
};
|
||||
}
|
||||
|
||||
function getRecordIdentifierType(record = {}) {
|
||||
const rawType = String(record.accountIdentifierType || '').trim().toLowerCase();
|
||||
if (rawType === 'phone') {
|
||||
return 'phone';
|
||||
}
|
||||
if (rawType === 'email') {
|
||||
return 'email';
|
||||
}
|
||||
if (!record.email && (record.phoneNumber || record.phone || record.number)) {
|
||||
return 'phone';
|
||||
}
|
||||
if (!record.email && record.accountIdentifier && !/@/.test(String(record.accountIdentifier || ''))) {
|
||||
return 'phone';
|
||||
}
|
||||
return 'email';
|
||||
}
|
||||
|
||||
function getRecordEmail(record = {}) {
|
||||
const identifierType = getRecordIdentifierType(record);
|
||||
return String(
|
||||
record.email
|
||||
|| (identifierType === 'email' ? record.accountIdentifier : '')
|
||||
|| ''
|
||||
).trim();
|
||||
}
|
||||
|
||||
function getRecordPhoneNumber(record = {}) {
|
||||
const identifierType = getRecordIdentifierType(record);
|
||||
return String(
|
||||
record.phoneNumber
|
||||
|| record.phone
|
||||
|| record.number
|
||||
|| (identifierType === 'phone' ? record.accountIdentifier : '')
|
||||
|| ''
|
||||
).trim();
|
||||
}
|
||||
|
||||
function getRecordPrimaryIdentifier(record = {}) {
|
||||
const identifierType = getRecordIdentifierType(record);
|
||||
const email = getRecordEmail(record);
|
||||
const phoneNumber = getRecordPhoneNumber(record);
|
||||
return identifierType === 'phone'
|
||||
? (phoneNumber || String(record.accountIdentifier || '').trim() || email)
|
||||
: (email || String(record.accountIdentifier || '').trim() || phoneNumber);
|
||||
}
|
||||
|
||||
function getRecordSecondaryIdentifier(record = {}) {
|
||||
const identifierType = getRecordIdentifierType(record);
|
||||
const email = getRecordEmail(record);
|
||||
const phoneNumber = getRecordPhoneNumber(record);
|
||||
if (identifierType === 'phone' && email) {
|
||||
return `邮箱 ${email}`;
|
||||
}
|
||||
if (identifierType !== 'phone' && phoneNumber) {
|
||||
return `绑定手机号 ${phoneNumber}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getRecordTitle(record = {}) {
|
||||
const primaryIdentifier = getRecordPrimaryIdentifier(record) || '(空账号)';
|
||||
const secondaryIdentifier = getRecordSecondaryIdentifier(record);
|
||||
return secondaryIdentifier
|
||||
? `${primaryIdentifier} / ${secondaryIdentifier}`
|
||||
: primaryIdentifier;
|
||||
}
|
||||
|
||||
function getAccountRunRecords(currentState = state.getLatestState()) {
|
||||
return (Array.isArray(currentState?.accountRunHistory) ? currentState.accountRunHistory : [])
|
||||
.filter((item) => item && typeof item === 'object')
|
||||
.slice()
|
||||
.sort((left, right) => normalizeTimestamp(right.finishedAt) - normalizeTimestamp(left.finishedAt))
|
||||
.map((record) => applyRunningDisplayState(record, currentState));
|
||||
}
|
||||
|
||||
function summarizeAccountRunHistory(records = []) {
|
||||
return records.reduce((summary, record) => {
|
||||
const retryCount = normalizeRetryCount(record.retryCount);
|
||||
const status = getRecordDisplayStatus(record);
|
||||
summary.total += 1;
|
||||
if (status === 'success') {
|
||||
summary.success += 1;
|
||||
} else if (status === 'running') {
|
||||
summary.running += 1;
|
||||
} else if (status === 'failed') {
|
||||
summary.failed += 1;
|
||||
} else if (status === 'stopped') {
|
||||
summary.stopped += 1;
|
||||
}
|
||||
if (retryCount > 0) {
|
||||
summary.retryRecordCount += 1;
|
||||
}
|
||||
summary.retryTotal += retryCount;
|
||||
return summary;
|
||||
}, {
|
||||
total: 0,
|
||||
success: 0,
|
||||
running: 0,
|
||||
failed: 0,
|
||||
stopped: 0,
|
||||
retryRecordCount: 0,
|
||||
retryTotal: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function formatAccountRecordTime(value) {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return '--:--';
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const sameYear = date.getFullYear() === now.getFullYear();
|
||||
const sameDay = date.toDateString() === now.toDateString();
|
||||
|
||||
if (sameDay) {
|
||||
return date.toLocaleTimeString('zh-CN', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
timeZone: displayTimeZone,
|
||||
});
|
||||
}
|
||||
|
||||
return date.toLocaleString('zh-CN', {
|
||||
hour12: false,
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
...(sameYear ? {} : { year: '2-digit' }),
|
||||
timeZone: displayTimeZone,
|
||||
}).replace(/\//g, '-');
|
||||
}
|
||||
|
||||
function getStatusMeta(record = {}) {
|
||||
const status = getRecordDisplayStatus(record);
|
||||
if (status === 'success') {
|
||||
return { kind: 'success', label: '成功' };
|
||||
}
|
||||
if (status === 'running') {
|
||||
return { kind: 'running', label: '正在运行' };
|
||||
}
|
||||
if (status === 'stopped') {
|
||||
return { kind: 'stopped', label: '停止' };
|
||||
}
|
||||
return { kind: 'failed', label: '失败' };
|
||||
}
|
||||
|
||||
function getRecordSummaryText(record = {}) {
|
||||
const status = getRecordDisplayStatus(record);
|
||||
if (record.displaySummary) {
|
||||
return String(record.displaySummary || '').trim();
|
||||
}
|
||||
if (status === 'success') {
|
||||
return '流程完成';
|
||||
}
|
||||
if (status === 'running') {
|
||||
return '正在运行';
|
||||
}
|
||||
|
||||
return String(record.failureDetail || record.reason || '').trim()
|
||||
|| String(record.failureLabel || '').trim()
|
||||
|| '流程失败';
|
||||
}
|
||||
|
||||
function getRecordTooltipText(record = {}, summaryText = '') {
|
||||
const recordTitle = getRecordTitle(record);
|
||||
const status = getRecordDisplayStatus(record);
|
||||
const detail = String(record.displaySummary || record.failureDetail || record.reason || '').trim();
|
||||
if (status === 'success' || status === 'running' || !detail || detail === recordTitle) {
|
||||
return recordTitle;
|
||||
}
|
||||
return `${recordTitle}\n${detail}`;
|
||||
}
|
||||
|
||||
function getFilterConfig(filterKey = activeFilter) {
|
||||
return FILTER_CONFIG[filterKey] || FILTER_CONFIG.all;
|
||||
}
|
||||
|
||||
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 `
|
||||
<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;
|
||||
}
|
||||
|
||||
if (!allRecords.length) {
|
||||
dom.accountRecordsMeta.textContent = '暂无账号记录';
|
||||
return;
|
||||
}
|
||||
|
||||
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('all', summary.total),
|
||||
createStatChip('running', summary.running),
|
||||
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) {
|
||||
currentPage = 1;
|
||||
} else if (currentPage > totalPages) {
|
||||
currentPage = totalPages;
|
||||
} else if (currentPage < 1) {
|
||||
currentPage = 1;
|
||||
}
|
||||
|
||||
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 renderEmptyState(allRecords) {
|
||||
if (!dom.accountRecordsList) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = allRecords.length
|
||||
? `当前筛选“${getFilterConfig(activeFilter).metaLabel}”下暂无记录`
|
||||
: '暂无账号记录';
|
||||
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;
|
||||
}
|
||||
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
const visibleRecords = filteredRecords.slice(startIndex, startIndex + pageSize);
|
||||
|
||||
dom.accountRecordsList.innerHTML = visibleRecords.map((record) => {
|
||||
const recordId = buildRecordId(record);
|
||||
const primaryIdentifier = getRecordPrimaryIdentifier(record) || '(空账号)';
|
||||
const secondaryIdentifier = getRecordSecondaryIdentifier(record);
|
||||
const statusMeta = getStatusMeta(record);
|
||||
const summaryText = getRecordSummaryText(record);
|
||||
const recordTitle = getRecordTooltipText(record, summaryText);
|
||||
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 `
|
||||
<div
|
||||
class="${itemClassNames}"
|
||||
data-account-record-id="${escapeHtml(recordId)}"
|
||||
title="${escapeHtml(recordTitle)}"
|
||||
>
|
||||
<div class="account-record-item-top">
|
||||
<div class="account-record-item-email-row">
|
||||
${selectionMarkup}
|
||||
<div class="account-record-item-identity">
|
||||
<div class="account-record-item-email mono">${escapeHtml(primaryIdentifier)}</div>
|
||||
${secondaryIdentifier ? `<div class="account-record-item-secondary mono">${escapeHtml(secondaryIdentifier)}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="account-record-item-side">
|
||||
<span class="account-record-item-status">${escapeHtml(statusMeta.label)}</span>
|
||||
<span class="account-record-item-time mono">${escapeHtml(formatAccountRecordTime(record.finishedAt))}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="account-record-item-bottom">
|
||||
<div class="account-record-item-summary">${escapeHtml(summaryText)}</div>
|
||||
<span class="account-record-item-retry mono">重试 ${escapeHtml(String(retryCount))}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
if (totalPages <= 1) {
|
||||
setNodeText(dom.accountRecordsPageLabel, '1 / 1');
|
||||
}
|
||||
}
|
||||
|
||||
function render(currentState = state.getLatestState()) {
|
||||
const allRecords = getAccountRunRecords(currentState);
|
||||
pruneSelectedRecordIds(allRecords);
|
||||
|
||||
if (!allRecords.length) {
|
||||
selectionMode = false;
|
||||
}
|
||||
|
||||
const filteredRecords = getFilteredRecords(allRecords);
|
||||
updateHeader(allRecords, filteredRecords);
|
||||
updateStats(allRecords);
|
||||
updateToolbarState(allRecords);
|
||||
renderRecordList(allRecords, filteredRecords);
|
||||
}
|
||||
|
||||
function openPanel() {
|
||||
setNodeHidden(dom.accountRecordsOverlay, false);
|
||||
render();
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
setNodeHidden(dom.accountRecordsOverlay, 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);
|
||||
}
|
||||
}
|
||||
|
||||
async function clearRecords() {
|
||||
const records = getAccountRunRecords();
|
||||
if (!records.length) {
|
||||
helpers.showToast?.('没有可清理的账号记录。', 'warn', 1800);
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await helpers.openConfirmModal({
|
||||
title: '清理账号记录',
|
||||
message: '确认清理当前全部账号记录吗?该操作会同时清空面板记录与本地同步快照。',
|
||||
confirmLabel: '确认清理',
|
||||
confirmVariant: 'btn-danger',
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'CLEAR_ACCOUNT_RUN_HISTORY',
|
||||
source: 'sidepanel',
|
||||
});
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
activeFilter = 'all';
|
||||
currentPage = 1;
|
||||
selectionMode = false;
|
||||
resetSelection();
|
||||
state.syncLatestState({ accountRunHistory: [] });
|
||||
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() {
|
||||
if (eventsBound) {
|
||||
return;
|
||||
}
|
||||
eventsBound = true;
|
||||
|
||||
dom.btnOpenAccountRecords?.addEventListener('click', () => {
|
||||
openPanel();
|
||||
});
|
||||
dom.btnCloseAccountRecords?.addEventListener('click', () => {
|
||||
closePanel();
|
||||
});
|
||||
dom.accountRecordsOverlay?.addEventListener('click', (event) => {
|
||||
if (event.target === dom.accountRecordsOverlay) {
|
||||
closePanel();
|
||||
}
|
||||
});
|
||||
dom.accountRecordsStats?.addEventListener('click', (event) => {
|
||||
handleStatsClick(event);
|
||||
});
|
||||
dom.accountRecordsList?.addEventListener('click', (event) => {
|
||||
handleRecordListClick(event);
|
||||
});
|
||||
dom.btnAccountRecordsPrev?.addEventListener('click', () => {
|
||||
if (currentPage <= 1) {
|
||||
return;
|
||||
}
|
||||
currentPage -= 1;
|
||||
render();
|
||||
});
|
||||
dom.btnAccountRecordsNext?.addEventListener('click', () => {
|
||||
currentPage += 1;
|
||||
render();
|
||||
});
|
||||
dom.btnToggleAccountRecordsSelection?.addEventListener('click', () => {
|
||||
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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function reset() {
|
||||
currentPage = 1;
|
||||
activeFilter = 'all';
|
||||
selectionMode = false;
|
||||
resetSelection();
|
||||
closePanel();
|
||||
render();
|
||||
}
|
||||
|
||||
return {
|
||||
bindEvents,
|
||||
clearRecords,
|
||||
closePanel,
|
||||
deleteSelectedRecords,
|
||||
openPanel,
|
||||
render,
|
||||
reset,
|
||||
setSelectionMode,
|
||||
summarizeAccountRunHistory,
|
||||
toggleSelectionMode,
|
||||
};
|
||||
}
|
||||
|
||||
globalScope.SidepanelAccountRecordsManager = {
|
||||
createAccountRecordsManager,
|
||||
};
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
@@ -0,0 +1,116 @@
|
||||
(() => {
|
||||
const PORTAL_BASE_URL = '';
|
||||
const CONTENT_SUMMARY_API_URL = '';
|
||||
const CACHE_KEY = 'multipage-contribution-content-summary-v1';
|
||||
const FETCH_TIMEOUT_MS = 6000;
|
||||
|
||||
function sanitizeItem(item = {}) {
|
||||
return {
|
||||
slug: String(item?.slug || '').trim(),
|
||||
title: String(item?.title || '').trim(),
|
||||
text: String(item?.text || '').trim(),
|
||||
isEnabled: Boolean(item?.is_enabled ?? item?.isEnabled),
|
||||
hasContent: Boolean(item?.has_content ?? item?.hasContent),
|
||||
isVisible: Boolean(item?.is_visible ?? item?.isVisible),
|
||||
updatedAt: String(item?.updated_at ?? item?.updatedAt ?? '').trim(),
|
||||
updatedAtDisplay: String(item?.updated_at_display ?? item?.updatedAtDisplay ?? '').trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function buildSnapshot(payload = {}) {
|
||||
const items = Array.isArray(payload?.items)
|
||||
? payload.items.map(sanitizeItem).filter((item) => item.slug)
|
||||
: [];
|
||||
const promptVersion = String(payload?.prompt_version || '').trim();
|
||||
const latestUpdatedAt = String(payload?.latest_updated_at || '').trim();
|
||||
const latestUpdatedAtDisplay = String(payload?.latest_updated_at_display || '').trim();
|
||||
const hasVisibleUpdates = Boolean(payload?.has_visible_updates) && Boolean(promptVersion);
|
||||
|
||||
return {
|
||||
status: hasVisibleUpdates ? 'update-available' : 'idle',
|
||||
promptVersion,
|
||||
hasVisibleUpdates,
|
||||
latestUpdatedAt,
|
||||
latestUpdatedAtDisplay,
|
||||
items,
|
||||
portalUrl: PORTAL_BASE_URL,
|
||||
apiUrl: CONTENT_SUMMARY_API_URL,
|
||||
checkedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function readCache() {
|
||||
try {
|
||||
const raw = localStorage.getItem(CACHE_KEY);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const snapshot = buildSnapshot({
|
||||
items: parsed.items,
|
||||
prompt_version: parsed.promptVersion,
|
||||
has_visible_updates: parsed.hasVisibleUpdates,
|
||||
latest_updated_at: parsed.latestUpdatedAt,
|
||||
latest_updated_at_display: parsed.latestUpdatedAtDisplay,
|
||||
});
|
||||
if (!Number.isFinite(parsed.checkedAt)) {
|
||||
return snapshot;
|
||||
}
|
||||
snapshot.checkedAt = parsed.checkedAt;
|
||||
return snapshot;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeCache(snapshot) {
|
||||
try {
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(snapshot));
|
||||
} catch (error) {
|
||||
// Ignore cache write failures.
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchContentSummary() {
|
||||
return buildSnapshot({});
|
||||
}
|
||||
|
||||
async function getContentUpdateSnapshot() {
|
||||
try {
|
||||
return await fetchContentSummary();
|
||||
} catch (error) {
|
||||
const cached = readCache();
|
||||
if (cached) {
|
||||
return {
|
||||
...cached,
|
||||
fromCache: true,
|
||||
errorMessage: error?.message || '内容摘要获取失败',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'error',
|
||||
promptVersion: '',
|
||||
hasVisibleUpdates: false,
|
||||
latestUpdatedAt: '',
|
||||
latestUpdatedAtDisplay: '',
|
||||
items: [],
|
||||
portalUrl: PORTAL_BASE_URL,
|
||||
apiUrl: CONTENT_SUMMARY_API_URL,
|
||||
checkedAt: Date.now(),
|
||||
errorMessage: error?.message || '内容摘要获取失败',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
window.SidepanelContributionContentService = {
|
||||
getContentUpdateSnapshot,
|
||||
portalUrl: PORTAL_BASE_URL,
|
||||
apiUrl: CONTENT_SUMMARY_API_URL,
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,503 @@
|
||||
(function attachSidepanelContributionMode(globalScope) {
|
||||
const ACTIVE_STATUSES = new Set(['started', 'waiting', 'processing']);
|
||||
const FINAL_STATUSES = new Set(['auto_approved', 'auto_rejected', 'expired', 'error']);
|
||||
const DEFAULT_COPY = '当前账号将用于支持项目维护。扩展会自动申请贡献登录地址并持续跟踪授权状态;如检测到回调地址,会自动提交,并继续等待服务端确认。';
|
||||
const CONTRIBUTION_SOURCE_CPA = 'cpa';
|
||||
const CONTRIBUTION_SOURCE_SUB2API = 'sub2api';
|
||||
const CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME = 'codex号池';
|
||||
|
||||
function createContributionModeManager(context = {}) {
|
||||
const {
|
||||
state,
|
||||
dom,
|
||||
helpers,
|
||||
runtime,
|
||||
constants = {},
|
||||
} = context;
|
||||
|
||||
const contributionPortalUrl = constants.contributionPortalUrl || '';
|
||||
const guideRepositoryUrl = constants.guideRepositoryUrl || 'https://github.com/FoundZiGu/GuJumpgate';
|
||||
const contributionUploadUrl = constants.contributionUploadUrl || '';
|
||||
const pollIntervalMs = Math.max(1500, Math.floor(Number(constants.pollIntervalMs) || 2500));
|
||||
|
||||
const hiddenRows = [
|
||||
dom.rowVpsUrl,
|
||||
dom.rowVpsPassword,
|
||||
dom.rowLocalCpaStep9Mode,
|
||||
dom.rowSub2ApiUrl,
|
||||
dom.rowSub2ApiEmail,
|
||||
dom.rowSub2ApiPassword,
|
||||
dom.rowSub2ApiGroup,
|
||||
dom.rowSub2ApiDefaultProxy,
|
||||
dom.rowCodex2ApiUrl,
|
||||
dom.rowCodex2ApiAdminKey,
|
||||
dom.rowCustomPassword,
|
||||
dom.rowAccountRunHistoryHelperBaseUrl,
|
||||
].filter(Boolean);
|
||||
|
||||
let actionInFlight = false;
|
||||
let pollInFlight = false;
|
||||
let pollTimer = null;
|
||||
|
||||
function getLatestState() {
|
||||
return state.getLatestState?.() || {};
|
||||
}
|
||||
|
||||
function normalizeString(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function normalizeStatus(value = '') {
|
||||
const normalized = normalizeString(value).toLowerCase();
|
||||
if (ACTIVE_STATUSES.has(normalized) || FINAL_STATUSES.has(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function normalizeCallbackStatus(value = '') {
|
||||
const normalized = normalizeString(value).toLowerCase();
|
||||
switch (normalized) {
|
||||
case 'waiting':
|
||||
case 'captured':
|
||||
case 'submitting':
|
||||
case 'submitted':
|
||||
case 'failed':
|
||||
case 'idle':
|
||||
return normalized;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeContributionSource(value = '') {
|
||||
const normalized = normalizeString(value).toLowerCase();
|
||||
return normalized === CONTRIBUTION_SOURCE_SUB2API
|
||||
? CONTRIBUTION_SOURCE_SUB2API
|
||||
: CONTRIBUTION_SOURCE_CPA;
|
||||
}
|
||||
|
||||
function getContributionSource(currentState = getLatestState()) {
|
||||
return normalizeContributionSource(currentState.contributionSource || currentState.panelMode);
|
||||
}
|
||||
|
||||
function getContributionSourceLabel(currentState = getLatestState()) {
|
||||
return getContributionSource(currentState) === CONTRIBUTION_SOURCE_SUB2API ? 'SUB2API' : 'CPA';
|
||||
}
|
||||
|
||||
function isContributionModeEnabled(currentState = getLatestState()) {
|
||||
return Boolean(currentState.contributionMode);
|
||||
}
|
||||
|
||||
function hasActiveContributionSession(currentState = getLatestState()) {
|
||||
const status = normalizeStatus(currentState.contributionStatus);
|
||||
return Boolean(normalizeString(currentState.contributionSessionId) && status && !FINAL_STATUSES.has(status));
|
||||
}
|
||||
|
||||
function isModeSwitchBlocked() {
|
||||
return Boolean(helpers.isModeSwitchBlocked?.(getLatestState()));
|
||||
}
|
||||
|
||||
function setContributionHidden(element, hidden) {
|
||||
element?.classList.toggle('is-contribution-hidden', hidden);
|
||||
}
|
||||
|
||||
function syncContributionRows(enabled) {
|
||||
hiddenRows.forEach((row) => {
|
||||
setContributionHidden(row, enabled);
|
||||
});
|
||||
}
|
||||
|
||||
function syncContributionButton(enabled, blocked) {
|
||||
if (!dom.btnContributionMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
dom.btnContributionMode.classList.toggle('is-active', enabled);
|
||||
dom.btnContributionMode.setAttribute('aria-pressed', String(enabled));
|
||||
dom.btnContributionMode.disabled = false;
|
||||
dom.btnContributionMode.title = '打开项目仓库说明页';
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollTimer) {
|
||||
clearTimeout(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function schedulePolling(delayMs = pollIntervalMs) {
|
||||
stopPolling();
|
||||
if (!isContributionModeEnabled() || !hasActiveContributionSession()) {
|
||||
return;
|
||||
}
|
||||
|
||||
pollTimer = setTimeout(() => {
|
||||
pollOnce({ silentError: true }).catch(() => {});
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
function ensurePolling() {
|
||||
if (!isContributionModeEnabled() || !hasActiveContributionSession()) {
|
||||
stopPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pollTimer && !pollInFlight) {
|
||||
schedulePolling(1200);
|
||||
}
|
||||
}
|
||||
|
||||
function getOauthStatusText(currentState = getLatestState()) {
|
||||
const status = normalizeStatus(currentState.contributionStatus);
|
||||
const hasAuthUrl = Boolean(normalizeString(currentState.contributionAuthUrl));
|
||||
if (!normalizeString(currentState.contributionSessionId) || !hasAuthUrl) {
|
||||
return '未生成登录地址';
|
||||
}
|
||||
if (status === 'waiting') {
|
||||
return '等待提交回调';
|
||||
}
|
||||
if (status === 'processing' || status === 'auto_approved' || status === 'auto_rejected') {
|
||||
return status === 'processing' ? '已提交回调' : '授权已结束';
|
||||
}
|
||||
if (status === 'expired' || status === 'error') {
|
||||
return '授权失败';
|
||||
}
|
||||
if (Number(currentState.contributionAuthOpenedAt) > 0) {
|
||||
return '已打开授权页';
|
||||
}
|
||||
return '登录地址已生成';
|
||||
}
|
||||
|
||||
function getCallbackStatusText(currentState = getLatestState()) {
|
||||
const status = normalizeCallbackStatus(currentState.contributionCallbackStatus);
|
||||
switch (status) {
|
||||
case 'captured':
|
||||
return '已捕获回调地址';
|
||||
case 'submitting':
|
||||
return '正在提交回调';
|
||||
case 'submitted':
|
||||
return '已提交回调';
|
||||
case 'failed':
|
||||
return '回调提交失败';
|
||||
case 'waiting':
|
||||
case 'idle':
|
||||
default:
|
||||
return normalizeString(currentState.contributionCallbackUrl)
|
||||
? '已捕获回调地址'
|
||||
: '等待回调';
|
||||
}
|
||||
}
|
||||
|
||||
function getSummaryText(currentState = getLatestState()) {
|
||||
const statusMessage = normalizeString(currentState.contributionStatusMessage);
|
||||
if (statusMessage) {
|
||||
return statusMessage;
|
||||
}
|
||||
if (getContributionSource(currentState) === CONTRIBUTION_SOURCE_SUB2API) {
|
||||
const groupName = normalizeString(currentState.contributionTargetGroupName) || CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME;
|
||||
return `当前账号将用于支持项目维护。贡献会通过 SUB2API 完成,并固定写入 ${groupName} 分组;如检测到回调地址,扩展会自动提交并等待服务端确认。`;
|
||||
}
|
||||
return DEFAULT_COPY;
|
||||
}
|
||||
|
||||
function getContributionPortalPageUrl() {
|
||||
return normalizeString(contributionPortalUrl);
|
||||
}
|
||||
|
||||
function getContributionUploadPageUrl() {
|
||||
return normalizeString(contributionUploadUrl);
|
||||
}
|
||||
|
||||
function openContributionPortalPage() {
|
||||
const targetUrl = getContributionPortalPageUrl();
|
||||
if (!targetUrl) {
|
||||
return;
|
||||
}
|
||||
helpers.openExternalUrl?.(targetUrl);
|
||||
}
|
||||
|
||||
function openContributionUploadPage() {
|
||||
const targetUrl = getContributionUploadPageUrl();
|
||||
if (!targetUrl) {
|
||||
return;
|
||||
}
|
||||
helpers.openExternalUrl?.(targetUrl);
|
||||
}
|
||||
|
||||
async function syncContributionProfile(partial = {}) {
|
||||
const payload = {
|
||||
nickname: normalizeString(partial.nickname),
|
||||
qq: normalizeString(partial.qq),
|
||||
};
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'SET_CONTRIBUTION_PROFILE',
|
||||
source: 'sidepanel',
|
||||
payload,
|
||||
});
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
if (response?.state) {
|
||||
helpers.applySettingsState?.(response.state);
|
||||
}
|
||||
}
|
||||
|
||||
async function requestContributionMode(enabled) {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'SET_CONTRIBUTION_MODE',
|
||||
source: 'sidepanel',
|
||||
payload: { enabled: Boolean(enabled) },
|
||||
});
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
if (!response?.state) {
|
||||
throw new Error('贡献模式切换后未返回最新状态。');
|
||||
}
|
||||
|
||||
helpers.applySettingsState?.(response.state);
|
||||
helpers.updateStatusDisplay?.(response.state);
|
||||
render();
|
||||
}
|
||||
|
||||
async function pollOnce(options = {}) {
|
||||
if (pollInFlight || !isContributionModeEnabled() || !hasActiveContributionSession()) {
|
||||
if (!hasActiveContributionSession()) {
|
||||
stopPolling();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
pollInFlight = true;
|
||||
try {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'POLL_CONTRIBUTION_STATUS',
|
||||
source: 'sidepanel',
|
||||
payload: {
|
||||
reason: options.reason || 'sidepanel_poll',
|
||||
},
|
||||
});
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
if (response?.state) {
|
||||
helpers.applySettingsState?.(response.state);
|
||||
helpers.updateStatusDisplay?.(response.state);
|
||||
}
|
||||
} finally {
|
||||
pollInFlight = false;
|
||||
render();
|
||||
if (hasActiveContributionSession()) {
|
||||
schedulePolling();
|
||||
} else {
|
||||
stopPolling();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function startContributionFlow() {
|
||||
if (typeof helpers.startContributionAutoRun !== 'function') {
|
||||
throw new Error('贡献模式尚未接入主自动流程启动能力。');
|
||||
}
|
||||
|
||||
const profile = helpers.getContributionProfile?.() || {};
|
||||
const qq = normalizeString(profile.qq);
|
||||
if (qq && !/^\d{1,20}$/.test(qq)) {
|
||||
throw new Error('QQ 只能填写数字,且长度不能超过 20 位。');
|
||||
}
|
||||
await syncContributionProfile(profile);
|
||||
|
||||
const started = await helpers.startContributionAutoRun();
|
||||
if (!started) {
|
||||
return;
|
||||
}
|
||||
|
||||
helpers.showToast?.('贡献自动流程已启动。', 'info', 1800);
|
||||
render();
|
||||
}
|
||||
|
||||
async function enterContributionMode() {
|
||||
await requestContributionMode(true);
|
||||
helpers.showToast?.('已进入贡献模式。', 'success', 1800);
|
||||
}
|
||||
|
||||
async function exitContributionMode() {
|
||||
stopPolling();
|
||||
await requestContributionMode(false);
|
||||
helpers.showToast?.('已退出贡献模式。', 'info', 1800);
|
||||
}
|
||||
|
||||
function render() {
|
||||
const currentState = getLatestState();
|
||||
const enabled = isContributionModeEnabled(currentState);
|
||||
const blocked = isModeSwitchBlocked();
|
||||
const activeElement = typeof document !== 'undefined' ? document.activeElement : null;
|
||||
const sourceLabel = getContributionSourceLabel(currentState);
|
||||
|
||||
if (enabled && dom.selectPanelMode) {
|
||||
dom.selectPanelMode.value = getContributionSource(currentState);
|
||||
}
|
||||
|
||||
helpers.updatePanelModeUI?.();
|
||||
helpers.updateAccountRunHistorySettingsUI?.();
|
||||
|
||||
if (dom.contributionModePanel) {
|
||||
dom.contributionModePanel.hidden = !enabled;
|
||||
}
|
||||
if (dom.contributionModeText) {
|
||||
dom.contributionModeText.textContent = getSummaryText({
|
||||
contributionSource: currentState.contributionSource,
|
||||
contributionTargetGroupName: currentState.contributionTargetGroupName,
|
||||
});
|
||||
}
|
||||
if (dom.contributionModeBadge) {
|
||||
dom.contributionModeBadge.textContent = enabled ? sourceLabel : '';
|
||||
}
|
||||
if (dom.inputContributionNickname && activeElement !== dom.inputContributionNickname) {
|
||||
const nextNickname = normalizeString(currentState.contributionNickname);
|
||||
if (nextNickname || !normalizeString(dom.inputContributionNickname.value)) {
|
||||
dom.inputContributionNickname.value = nextNickname;
|
||||
}
|
||||
}
|
||||
if (dom.inputContributionQq && activeElement !== dom.inputContributionQq) {
|
||||
const nextQq = normalizeString(currentState.contributionQq);
|
||||
if (nextQq || !normalizeString(dom.inputContributionQq.value)) {
|
||||
dom.inputContributionQq.value = nextQq;
|
||||
}
|
||||
}
|
||||
if (dom.contributionOauthStatus) {
|
||||
dom.contributionOauthStatus.textContent = getOauthStatusText(currentState);
|
||||
}
|
||||
if (dom.contributionCallbackStatus) {
|
||||
dom.contributionCallbackStatus.textContent = getCallbackStatusText(currentState);
|
||||
}
|
||||
if (dom.contributionModeSummary) {
|
||||
dom.contributionModeSummary.textContent = getSummaryText(currentState);
|
||||
}
|
||||
|
||||
syncContributionRows(enabled);
|
||||
syncContributionButton(enabled, blocked);
|
||||
|
||||
if (dom.selectPanelMode) {
|
||||
dom.selectPanelMode.disabled = enabled;
|
||||
}
|
||||
|
||||
if (dom.btnStartContribution) {
|
||||
dom.btnStartContribution.disabled = actionInFlight || blocked;
|
||||
}
|
||||
|
||||
if (dom.btnOpenContributionUpload) {
|
||||
dom.btnOpenContributionUpload.disabled = !getContributionUploadPageUrl();
|
||||
}
|
||||
|
||||
if (dom.btnExitContributionMode) {
|
||||
dom.btnExitContributionMode.disabled = actionInFlight || blocked;
|
||||
dom.btnExitContributionMode.title = blocked ? '当前流程运行中,暂时不能退出贡献模式' : '退出贡献模式';
|
||||
}
|
||||
|
||||
if (dom.btnOpenAccountRecords) {
|
||||
dom.btnOpenAccountRecords.disabled = enabled;
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
helpers.closeConfigMenu?.();
|
||||
helpers.closeAccountRecordsPanel?.();
|
||||
ensurePolling();
|
||||
} else {
|
||||
stopPolling();
|
||||
}
|
||||
|
||||
helpers.updateConfigMenuControls?.();
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
dom.btnContributionMode?.addEventListener('click', async () => {
|
||||
try {
|
||||
helpers.openExternalUrl?.(guideRepositoryUrl);
|
||||
} catch (error) {
|
||||
helpers.showToast?.(`打开说明页失败:${error.message}`, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
dom.btnStartContribution?.addEventListener('click', async () => {
|
||||
if (actionInFlight) {
|
||||
return;
|
||||
}
|
||||
actionInFlight = true;
|
||||
render();
|
||||
try {
|
||||
await startContributionFlow();
|
||||
} catch (error) {
|
||||
helpers.showToast?.(error.message, 'error');
|
||||
} finally {
|
||||
actionInFlight = false;
|
||||
render();
|
||||
}
|
||||
});
|
||||
|
||||
dom.inputContributionNickname?.addEventListener('change', async () => {
|
||||
try {
|
||||
await syncContributionProfile({
|
||||
nickname: dom.inputContributionNickname?.value,
|
||||
qq: dom.inputContributionQq?.value,
|
||||
});
|
||||
} catch (error) {
|
||||
helpers.showToast?.(error.message, 'error');
|
||||
} finally {
|
||||
render();
|
||||
}
|
||||
});
|
||||
|
||||
dom.inputContributionQq?.addEventListener('change', async () => {
|
||||
try {
|
||||
await syncContributionProfile({
|
||||
nickname: dom.inputContributionNickname?.value,
|
||||
qq: dom.inputContributionQq?.value,
|
||||
});
|
||||
} catch (error) {
|
||||
helpers.showToast?.(error.message, 'error');
|
||||
} finally {
|
||||
render();
|
||||
}
|
||||
});
|
||||
|
||||
dom.btnOpenContributionUpload?.addEventListener('click', () => {
|
||||
try {
|
||||
openContributionUploadPage();
|
||||
} catch (error) {
|
||||
helpers.showToast?.(`打开上传页面失败:${error.message}`, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
dom.btnExitContributionMode?.addEventListener('click', async () => {
|
||||
if (actionInFlight) {
|
||||
return;
|
||||
}
|
||||
actionInFlight = true;
|
||||
render();
|
||||
try {
|
||||
await exitContributionMode();
|
||||
} catch (error) {
|
||||
helpers.showToast?.(error.message, 'error');
|
||||
} finally {
|
||||
actionInFlight = false;
|
||||
render();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
bindEvents,
|
||||
pollOnce,
|
||||
render,
|
||||
stopPolling,
|
||||
};
|
||||
}
|
||||
|
||||
globalScope.SidepanelContributionMode = {
|
||||
createContributionModeManager,
|
||||
};
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
@@ -0,0 +1,534 @@
|
||||
(function attachSidepanelCustomEmailPoolManager(globalScope) {
|
||||
function createCustomEmailPoolManager(context = {}) {
|
||||
const {
|
||||
dom,
|
||||
helpers,
|
||||
state,
|
||||
actions,
|
||||
constants = {},
|
||||
} = context;
|
||||
|
||||
const copyIcon = constants.copyIcon || '';
|
||||
|
||||
let renderedEntries = [];
|
||||
let selectedEntryIds = new Set();
|
||||
let searchTerm = '';
|
||||
let filterMode = 'all';
|
||||
let refreshQueued = false;
|
||||
let loading = false;
|
||||
|
||||
function normalizeEmail(value = '') {
|
||||
return String(value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function isValidEmail(value = '') {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizeEmail(value));
|
||||
}
|
||||
|
||||
function createEntryId() {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
return `custom-pool-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
}
|
||||
|
||||
function normalizeEntry(rawEntry = {}) {
|
||||
const email = normalizeEmail(rawEntry?.email || '');
|
||||
if (!isValidEmail(email)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: String(rawEntry?.id || createEntryId()),
|
||||
email,
|
||||
enabled: rawEntry?.enabled !== undefined ? Boolean(rawEntry.enabled) : true,
|
||||
used: Boolean(rawEntry?.used),
|
||||
note: String(rawEntry?.note || '').trim(),
|
||||
lastUsedAt: Number.isFinite(Number(rawEntry?.lastUsedAt)) ? Number(rawEntry.lastUsedAt) : 0,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeEntries(entries = []) {
|
||||
if (!Array.isArray(entries)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const seenEmails = new Set();
|
||||
const normalized = [];
|
||||
for (const entry of entries) {
|
||||
const item = normalizeEntry(entry);
|
||||
if (!item) continue;
|
||||
if (seenEmails.has(item.email)) continue;
|
||||
seenEmails.add(item.email);
|
||||
normalized.push(item);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function withCurrentFlag(entries = renderedEntries) {
|
||||
const currentEmail = normalizeEmail(state.getCurrentEmail?.());
|
||||
return normalizeEntries(entries).map((entry) => ({
|
||||
...entry,
|
||||
current: Boolean(currentEmail) && entry.email === currentEmail,
|
||||
}));
|
||||
}
|
||||
|
||||
function normalizeSearchText(value = '') {
|
||||
return String(value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function getFilteredEntries(entries = renderedEntries) {
|
||||
const normalizedSearchTerm = normalizeSearchText(searchTerm);
|
||||
return withCurrentFlag(entries).filter((entry) => {
|
||||
const matchesFilter = (() => {
|
||||
switch (filterMode) {
|
||||
case 'enabled': return Boolean(entry.enabled);
|
||||
case 'disabled': return !entry.enabled;
|
||||
case 'used': return Boolean(entry.used);
|
||||
case 'unused': return !entry.used;
|
||||
case 'current': return Boolean(entry.current);
|
||||
default: return true;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!matchesFilter) return false;
|
||||
if (!normalizedSearchTerm) return true;
|
||||
|
||||
const haystack = [
|
||||
entry.email,
|
||||
entry.note,
|
||||
entry.enabled ? 'enabled 启用' : 'disabled 停用',
|
||||
entry.used ? 'used 已用' : 'unused 未用',
|
||||
entry.current ? 'current 当前' : '',
|
||||
].join(' ').toLowerCase();
|
||||
|
||||
return haystack.includes(normalizedSearchTerm);
|
||||
});
|
||||
}
|
||||
|
||||
function pruneSelection(entries = renderedEntries) {
|
||||
const existingIds = new Set(withCurrentFlag(entries).map((entry) => String(entry.id)));
|
||||
selectedEntryIds = new Set([...selectedEntryIds].filter((id) => existingIds.has(id)));
|
||||
}
|
||||
|
||||
function updateBulkUi(visibleEntries = getFilteredEntries()) {
|
||||
if (!dom.checkboxCustomEmailPoolSelectAll || !dom.customEmailPoolSelectionSummary) {
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleIds = visibleEntries.map((entry) => String(entry.id));
|
||||
const selectedVisibleCount = visibleIds.filter((id) => selectedEntryIds.has(id)).length;
|
||||
const hasVisible = visibleIds.length > 0;
|
||||
const hasSelection = selectedEntryIds.size > 0;
|
||||
|
||||
dom.checkboxCustomEmailPoolSelectAll.checked = hasVisible && selectedVisibleCount === visibleIds.length;
|
||||
dom.checkboxCustomEmailPoolSelectAll.indeterminate = selectedVisibleCount > 0 && selectedVisibleCount < visibleIds.length;
|
||||
dom.checkboxCustomEmailPoolSelectAll.disabled = loading || !hasVisible;
|
||||
dom.customEmailPoolSelectionSummary.textContent = `已选 ${selectedEntryIds.size} 个(当前显示 ${visibleIds.length} 个)`;
|
||||
|
||||
if (dom.btnCustomEmailPoolBulkUsed) dom.btnCustomEmailPoolBulkUsed.disabled = loading || !hasSelection;
|
||||
if (dom.btnCustomEmailPoolBulkUnused) dom.btnCustomEmailPoolBulkUnused.disabled = loading || !hasSelection;
|
||||
if (dom.btnCustomEmailPoolBulkEnable) dom.btnCustomEmailPoolBulkEnable.disabled = loading || !hasSelection;
|
||||
if (dom.btnCustomEmailPoolBulkDisable) dom.btnCustomEmailPoolBulkDisable.disabled = loading || !hasSelection;
|
||||
if (dom.btnCustomEmailPoolBulkDelete) dom.btnCustomEmailPoolBulkDelete.disabled = loading || !hasSelection;
|
||||
}
|
||||
|
||||
function setLoadingState(nextLoading, summary = '') {
|
||||
loading = Boolean(nextLoading);
|
||||
if (dom.btnCustomEmailPoolImport) dom.btnCustomEmailPoolImport.disabled = loading;
|
||||
if (dom.btnCustomEmailPoolRefresh) dom.btnCustomEmailPoolRefresh.disabled = loading;
|
||||
if (dom.btnCustomEmailPoolClearUsed) dom.btnCustomEmailPoolClearUsed.disabled = loading;
|
||||
if (dom.btnCustomEmailPoolDeleteAll) dom.btnCustomEmailPoolDeleteAll.disabled = loading;
|
||||
if (dom.inputCustomEmailPoolImport) dom.inputCustomEmailPoolImport.disabled = loading;
|
||||
|
||||
if (summary && dom.customEmailPoolSummary) {
|
||||
dom.customEmailPoolSummary.textContent = summary;
|
||||
}
|
||||
|
||||
updateBulkUi(getFilteredEntries());
|
||||
}
|
||||
|
||||
function renderCustomEmailPoolEntries(entries = state.getEntries?.()) {
|
||||
if (!dom.customEmailPoolList || !dom.customEmailPoolSummary) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderedEntries = normalizeEntries(entries);
|
||||
pruneSelection(renderedEntries);
|
||||
dom.customEmailPoolList.innerHTML = '';
|
||||
|
||||
if (!renderedEntries.length) {
|
||||
selectedEntryIds.clear();
|
||||
dom.customEmailPoolList.innerHTML = '<div class="luckmail-empty">还没有自定义邮箱,先导入一批邮箱再开始。</div>';
|
||||
dom.customEmailPoolSummary.textContent = '导入你提前准备好的注册邮箱,每行一个邮箱地址。';
|
||||
if (dom.btnCustomEmailPoolClearUsed) dom.btnCustomEmailPoolClearUsed.disabled = true;
|
||||
if (dom.btnCustomEmailPoolDeleteAll) dom.btnCustomEmailPoolDeleteAll.disabled = true;
|
||||
updateBulkUi([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const entriesWithCurrent = withCurrentFlag(renderedEntries);
|
||||
const usedCount = entriesWithCurrent.filter((entry) => entry.used).length;
|
||||
const enabledCount = entriesWithCurrent.filter((entry) => entry.enabled).length;
|
||||
dom.customEmailPoolSummary.textContent = `已加载 ${entriesWithCurrent.length} 个邮箱,其中 ${enabledCount} 个启用,${usedCount} 个已标记为已用。`;
|
||||
if (dom.btnCustomEmailPoolClearUsed) dom.btnCustomEmailPoolClearUsed.disabled = loading || usedCount === 0;
|
||||
if (dom.btnCustomEmailPoolDeleteAll) dom.btnCustomEmailPoolDeleteAll.disabled = loading || entriesWithCurrent.length === 0;
|
||||
|
||||
const visibleEntries = getFilteredEntries(entriesWithCurrent);
|
||||
if (!visibleEntries.length) {
|
||||
dom.customEmailPoolList.innerHTML = '<div class="luckmail-empty">没有匹配当前筛选条件的邮箱。</div>';
|
||||
updateBulkUi([]);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of visibleEntries) {
|
||||
const entryId = String(entry.id);
|
||||
const item = document.createElement('div');
|
||||
item.className = `luckmail-item${entry.current ? ' is-current' : ''}`;
|
||||
item.innerHTML = `
|
||||
<input class="luckmail-item-check" type="checkbox" data-action="select" ${selectedEntryIds.has(entryId) ? 'checked' : ''} />
|
||||
<div class="luckmail-item-main">
|
||||
<div class="luckmail-item-email-row">
|
||||
<div class="luckmail-item-email">${helpers.escapeHtml(entry.email || '(未知邮箱)')}</div>
|
||||
<button
|
||||
class="hotmail-copy-btn"
|
||||
type="button"
|
||||
data-action="copy-email"
|
||||
title="复制邮箱"
|
||||
aria-label="复制邮箱 ${helpers.escapeHtml(entry.email || '')}"
|
||||
>${copyIcon}</button>
|
||||
</div>
|
||||
<div class="luckmail-item-meta">
|
||||
${entry.current ? '<span class="luckmail-tag current">当前</span>' : ''}
|
||||
${entry.used ? '<span class="luckmail-tag used">已用</span>' : '<span class="luckmail-tag active">未用</span>'}
|
||||
${entry.enabled ? '<span class="luckmail-tag active">启用</span>' : '<span class="luckmail-tag disabled">停用</span>'}
|
||||
${entry.note ? `<span class="luckmail-tag">${helpers.escapeHtml(entry.note)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="luckmail-item-actions">
|
||||
<button class="btn btn-outline btn-xs" type="button" data-action="use">使用此邮箱</button>
|
||||
<button class="btn btn-outline btn-xs" type="button" data-action="toggle-used">${helpers.escapeHtml(entry.used ? '标记未用' : '标记已用')}</button>
|
||||
<button class="btn btn-outline btn-xs" type="button" data-action="toggle-enabled">${helpers.escapeHtml(entry.enabled ? '停用' : '启用')}</button>
|
||||
<button class="btn btn-outline btn-xs" type="button" data-action="delete">删除</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
item.querySelector('[data-action="select"]').addEventListener('change', (event) => {
|
||||
if (event.target.checked) {
|
||||
selectedEntryIds.add(entryId);
|
||||
} else {
|
||||
selectedEntryIds.delete(entryId);
|
||||
}
|
||||
updateBulkUi(visibleEntries);
|
||||
});
|
||||
|
||||
item.querySelector('[data-action="copy-email"]').addEventListener('click', async () => {
|
||||
await helpers.copyTextToClipboard(entry.email || '');
|
||||
helpers.showToast('邮箱已复制', 'success', 1600);
|
||||
});
|
||||
|
||||
item.querySelector('[data-action="use"]').addEventListener('click', async () => {
|
||||
try {
|
||||
setLoadingState(true, '正在切换当前邮箱...');
|
||||
await actions.setRuntimeEmail?.(entry.email);
|
||||
helpers.showToast(`已切换到 ${entry.email}`, 'success', 1800);
|
||||
queueCustomEmailPoolRefresh();
|
||||
} catch (error) {
|
||||
helpers.showToast(`切换邮箱失败:${error.message}`, 'error');
|
||||
} finally {
|
||||
setLoadingState(false);
|
||||
}
|
||||
});
|
||||
|
||||
item.querySelector('[data-action="toggle-used"]').addEventListener('click', async () => {
|
||||
await patchEntries((entriesList) => entriesList.map((candidate) => (
|
||||
String(candidate.id) === entryId
|
||||
? {
|
||||
...candidate,
|
||||
used: !entry.used,
|
||||
lastUsedAt: !entry.used ? Date.now() : candidate.lastUsedAt,
|
||||
}
|
||||
: candidate
|
||||
)));
|
||||
});
|
||||
|
||||
item.querySelector('[data-action="toggle-enabled"]').addEventListener('click', async () => {
|
||||
await patchEntries((entriesList) => entriesList.map((candidate) => (
|
||||
String(candidate.id) === entryId
|
||||
? { ...candidate, enabled: !entry.enabled }
|
||||
: candidate
|
||||
)));
|
||||
});
|
||||
|
||||
item.querySelector('[data-action="delete"]').addEventListener('click', async () => {
|
||||
await deleteEntries({
|
||||
ids: [entry.id],
|
||||
}, `确认删除 ${entry.email} 吗?此操作不可撤销。`);
|
||||
});
|
||||
|
||||
dom.customEmailPoolList.appendChild(item);
|
||||
}
|
||||
|
||||
updateBulkUi(visibleEntries);
|
||||
}
|
||||
|
||||
async function patchEntries(mutator) {
|
||||
const previousEntries = normalizeEntries(state.getEntries?.() || []);
|
||||
const nextEntries = normalizeEntries(mutator(previousEntries.map((entry) => ({ ...entry }))));
|
||||
|
||||
setLoadingState(true, '正在更新自定义邮箱池...');
|
||||
state.setEntries?.(nextEntries);
|
||||
renderCustomEmailPoolEntries(nextEntries);
|
||||
|
||||
try {
|
||||
await actions.persistEntries?.();
|
||||
} catch (error) {
|
||||
state.setEntries?.(previousEntries);
|
||||
renderCustomEmailPoolEntries(previousEntries);
|
||||
helpers.showToast(`更新自定义邮箱池失败:${error.message}`, 'error');
|
||||
} finally {
|
||||
setLoadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteEntries(payload = {}, confirmMessage = '') {
|
||||
const confirmed = await helpers.openConfirmModal({
|
||||
title: '删除邮箱',
|
||||
message: confirmMessage || '确认删除选中的邮箱吗?此操作不可撤销。',
|
||||
confirmLabel: '确认删除',
|
||||
confirmVariant: 'btn-danger',
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ids = Array.isArray(payload.ids)
|
||||
? payload.ids.map((id) => String(id || '').trim()).filter(Boolean)
|
||||
: [];
|
||||
const mode = String(payload.mode || '').trim().toLowerCase();
|
||||
|
||||
await patchEntries((entriesList) => {
|
||||
if (mode === 'all') {
|
||||
selectedEntryIds.clear();
|
||||
return [];
|
||||
}
|
||||
if (mode === 'used') {
|
||||
const usedIds = new Set(entriesList.filter((entry) => entry.used).map((entry) => String(entry.id)));
|
||||
usedIds.forEach((id) => selectedEntryIds.delete(id));
|
||||
return entriesList.filter((entry) => !entry.used);
|
||||
}
|
||||
|
||||
const targetIds = new Set(ids);
|
||||
ids.forEach((id) => selectedEntryIds.delete(id));
|
||||
return entriesList.filter((entry) => !targetIds.has(String(entry.id)));
|
||||
});
|
||||
}
|
||||
|
||||
async function importEntriesFromTextarea() {
|
||||
const text = String(dom.inputCustomEmailPoolImport?.value || '');
|
||||
if (!text.trim()) {
|
||||
helpers.showToast('请先粘贴邮箱列表,每行一个邮箱。', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
const previousEntries = normalizeEntries(state.getEntries?.() || []);
|
||||
const knownEmails = new Set(previousEntries.map((entry) => entry.email));
|
||||
const importedEntries = [];
|
||||
let skippedCount = 0;
|
||||
|
||||
for (const line of String(text || '').split(/[\r\n,,;;]+/)) {
|
||||
const email = normalizeEmail(line);
|
||||
if (!email) {
|
||||
continue;
|
||||
}
|
||||
if (!isValidEmail(email) || knownEmails.has(email)) {
|
||||
skippedCount += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
knownEmails.add(email);
|
||||
importedEntries.push({
|
||||
id: createEntryId(),
|
||||
email,
|
||||
enabled: true,
|
||||
used: false,
|
||||
note: '',
|
||||
lastUsedAt: 0,
|
||||
});
|
||||
}
|
||||
|
||||
if (!importedEntries.length && skippedCount > 0) {
|
||||
helpers.showToast('没有可导入的新邮箱(可能都重复或无效)。', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
const nextEntries = normalizeEntries([...previousEntries, ...importedEntries]);
|
||||
setLoadingState(true, '正在导入邮箱...');
|
||||
state.setEntries?.(nextEntries);
|
||||
renderCustomEmailPoolEntries(nextEntries);
|
||||
|
||||
try {
|
||||
await actions.persistEntries?.();
|
||||
if (dom.inputCustomEmailPoolImport) {
|
||||
dom.inputCustomEmailPoolImport.value = '';
|
||||
}
|
||||
helpers.showToast(
|
||||
skippedCount > 0
|
||||
? `已导入 ${importedEntries.length} 个邮箱,跳过 ${skippedCount} 条无效或重复数据。`
|
||||
: `已导入 ${importedEntries.length} 个邮箱。`,
|
||||
importedEntries.length > 0 ? 'success' : 'warn',
|
||||
2400
|
||||
);
|
||||
} catch (error) {
|
||||
state.setEntries?.(previousEntries);
|
||||
renderCustomEmailPoolEntries(previousEntries);
|
||||
helpers.showToast(`导入邮箱失败:${error.message}`, 'error');
|
||||
} finally {
|
||||
setLoadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshCustomEmailPoolEntries(options = {}) {
|
||||
const { silent = false } = options;
|
||||
if (state.isVisible && !state.isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!silent) {
|
||||
setLoadingState(true, '正在刷新自定义邮箱池...');
|
||||
}
|
||||
renderCustomEmailPoolEntries(state.getEntries?.());
|
||||
if (!silent) {
|
||||
setLoadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
function queueCustomEmailPoolRefresh() {
|
||||
if (refreshQueued) return;
|
||||
refreshQueued = true;
|
||||
setTimeout(() => {
|
||||
refreshQueued = false;
|
||||
refreshCustomEmailPoolEntries({ silent: true });
|
||||
}, 120);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
selectedEntryIds.clear();
|
||||
searchTerm = '';
|
||||
filterMode = 'all';
|
||||
if (dom.inputCustomEmailPoolSearch) dom.inputCustomEmailPoolSearch.value = '';
|
||||
if (dom.selectCustomEmailPoolFilter) dom.selectCustomEmailPoolFilter.value = 'all';
|
||||
if (dom.customEmailPoolList) {
|
||||
dom.customEmailPoolList.innerHTML = '';
|
||||
}
|
||||
if (dom.customEmailPoolSummary) {
|
||||
dom.customEmailPoolSummary.textContent = '导入你提前准备好的注册邮箱,每行一个邮箱地址。';
|
||||
}
|
||||
updateBulkUi([]);
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
dom.btnCustomEmailPoolRefresh?.addEventListener('click', async () => {
|
||||
await refreshCustomEmailPoolEntries();
|
||||
});
|
||||
|
||||
dom.btnCustomEmailPoolImport?.addEventListener('click', async () => {
|
||||
await importEntriesFromTextarea();
|
||||
});
|
||||
|
||||
dom.inputCustomEmailPoolImport?.addEventListener('keydown', async (event) => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
await importEntriesFromTextarea();
|
||||
}
|
||||
});
|
||||
|
||||
dom.inputCustomEmailPoolSearch?.addEventListener('input', (event) => {
|
||||
searchTerm = normalizeSearchText(event.target.value);
|
||||
renderCustomEmailPoolEntries(renderedEntries);
|
||||
});
|
||||
|
||||
dom.selectCustomEmailPoolFilter?.addEventListener('change', (event) => {
|
||||
filterMode = String(event.target.value || 'all');
|
||||
renderCustomEmailPoolEntries(renderedEntries);
|
||||
});
|
||||
|
||||
dom.checkboxCustomEmailPoolSelectAll?.addEventListener('change', (event) => {
|
||||
const visibleEntries = getFilteredEntries(renderedEntries);
|
||||
const visibleIds = visibleEntries.map((entry) => String(entry.id));
|
||||
if (event.target.checked) {
|
||||
visibleIds.forEach((id) => selectedEntryIds.add(id));
|
||||
} else {
|
||||
visibleIds.forEach((id) => selectedEntryIds.delete(id));
|
||||
}
|
||||
renderCustomEmailPoolEntries(renderedEntries);
|
||||
});
|
||||
|
||||
dom.btnCustomEmailPoolBulkUsed?.addEventListener('click', async () => {
|
||||
const targetIds = new Set([...selectedEntryIds]);
|
||||
await patchEntries((entriesList) => entriesList.map((entry) => (
|
||||
targetIds.has(String(entry.id))
|
||||
? { ...entry, used: true, lastUsedAt: entry.lastUsedAt || Date.now() }
|
||||
: entry
|
||||
)));
|
||||
});
|
||||
|
||||
dom.btnCustomEmailPoolBulkUnused?.addEventListener('click', async () => {
|
||||
const targetIds = new Set([...selectedEntryIds]);
|
||||
await patchEntries((entriesList) => entriesList.map((entry) => (
|
||||
targetIds.has(String(entry.id))
|
||||
? { ...entry, used: false }
|
||||
: entry
|
||||
)));
|
||||
});
|
||||
|
||||
dom.btnCustomEmailPoolBulkEnable?.addEventListener('click', async () => {
|
||||
const targetIds = new Set([...selectedEntryIds]);
|
||||
await patchEntries((entriesList) => entriesList.map((entry) => (
|
||||
targetIds.has(String(entry.id))
|
||||
? { ...entry, enabled: true }
|
||||
: entry
|
||||
)));
|
||||
});
|
||||
|
||||
dom.btnCustomEmailPoolBulkDisable?.addEventListener('click', async () => {
|
||||
const targetIds = new Set([...selectedEntryIds]);
|
||||
await patchEntries((entriesList) => entriesList.map((entry) => (
|
||||
targetIds.has(String(entry.id))
|
||||
? { ...entry, enabled: false }
|
||||
: entry
|
||||
)));
|
||||
});
|
||||
|
||||
dom.btnCustomEmailPoolBulkDelete?.addEventListener('click', async () => {
|
||||
await deleteEntries({
|
||||
ids: [...selectedEntryIds],
|
||||
}, `确认删除当前选中的 ${selectedEntryIds.size} 个邮箱吗?此操作不可撤销。`);
|
||||
});
|
||||
|
||||
dom.btnCustomEmailPoolClearUsed?.addEventListener('click', async () => {
|
||||
await deleteEntries({
|
||||
mode: 'used',
|
||||
}, '确认删除当前所有已用邮箱吗?此操作不可撤销。');
|
||||
});
|
||||
|
||||
dom.btnCustomEmailPoolDeleteAll?.addEventListener('click', async () => {
|
||||
await deleteEntries({
|
||||
mode: 'all',
|
||||
}, '确认删除当前全部邮箱吗?此操作不可撤销。');
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
bindEvents,
|
||||
queueCustomEmailPoolRefresh,
|
||||
refreshCustomEmailPoolEntries,
|
||||
renderCustomEmailPoolEntries,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
globalScope.SidepanelCustomEmailPoolManager = {
|
||||
createCustomEmailPoolManager,
|
||||
};
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
@@ -0,0 +1,228 @@
|
||||
(function attachSidepanelEditableListPicker(globalScope) {
|
||||
const editableListPickers = [];
|
||||
|
||||
function splitEditableListValues(value = '') {
|
||||
return String(value || '')
|
||||
.split(/[\r\n,,、]+/)
|
||||
.map((name) => name.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function normalizeEditableListValues(...sources) {
|
||||
const values = [];
|
||||
const seen = new Set();
|
||||
|
||||
const append = (value) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(append);
|
||||
return;
|
||||
}
|
||||
for (const item of splitEditableListValues(value)) {
|
||||
const key = item.toLowerCase();
|
||||
if (!key || seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
values.push(item);
|
||||
}
|
||||
};
|
||||
|
||||
sources.forEach(append);
|
||||
return values;
|
||||
}
|
||||
|
||||
function createEditableListPicker(config = {}) {
|
||||
const {
|
||||
root,
|
||||
input,
|
||||
trigger,
|
||||
current,
|
||||
menu,
|
||||
emptyLabel = '请先添加',
|
||||
fallbackItems = [],
|
||||
minItems = 0,
|
||||
deleteLabel = '删除',
|
||||
itemLabel = '项目',
|
||||
normalizeItems = normalizeEditableListValues,
|
||||
normalizeValue = (value) => String(value || '').trim(),
|
||||
getItemValue = (item) => String(item || '').trim(),
|
||||
getItemLabel = (item) => getItemValue(item),
|
||||
getItemDeleteLabel = (item) => getItemLabel(item),
|
||||
onDelete = null,
|
||||
onDeleteError = null,
|
||||
} = config;
|
||||
|
||||
const picker = {
|
||||
root,
|
||||
input,
|
||||
trigger,
|
||||
current,
|
||||
menu,
|
||||
items: [],
|
||||
open: false,
|
||||
};
|
||||
|
||||
const getFallbackItems = () => normalizeItems(fallbackItems);
|
||||
const getNormalizedItemValue = (item) => normalizeValue(getItemValue(item));
|
||||
const findItemByValue = (value) => {
|
||||
const normalized = normalizeValue(value);
|
||||
return picker.items.find((item) => getNormalizedItemValue(item) === normalized) || null;
|
||||
};
|
||||
const reportDeleteError = (error) => {
|
||||
const fallbackMessage = `${deleteLabel}${itemLabel}失败。`;
|
||||
if (typeof onDeleteError === 'function') {
|
||||
onDeleteError(error, fallbackMessage);
|
||||
return;
|
||||
}
|
||||
if (typeof globalScope.showToast === 'function') {
|
||||
globalScope.showToast(error?.message || fallbackMessage, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
picker.setOpen = (open) => {
|
||||
picker.open = Boolean(open) && !trigger?.disabled;
|
||||
if (menu) {
|
||||
menu.hidden = !picker.open;
|
||||
}
|
||||
if (trigger) {
|
||||
trigger.setAttribute('aria-expanded', picker.open ? 'true' : 'false');
|
||||
trigger.classList?.toggle('is-open', picker.open);
|
||||
}
|
||||
};
|
||||
|
||||
picker.close = () => {
|
||||
picker.setOpen(false);
|
||||
};
|
||||
|
||||
picker.setVisible = (visible) => {
|
||||
if (root) {
|
||||
root.style.display = visible ? '' : 'none';
|
||||
}
|
||||
if (!visible) {
|
||||
picker.close();
|
||||
}
|
||||
};
|
||||
|
||||
picker.setSelection = (value, options = {}) => {
|
||||
const fallback = getNormalizedItemValue(picker.items[0]) || getNormalizedItemValue(getFallbackItems()[0]) || '';
|
||||
const selected = normalizeValue(value) || fallback;
|
||||
const selectedItem = findItemByValue(selected);
|
||||
if (input) {
|
||||
input.value = selected;
|
||||
if (options.emit && typeof input.dispatchEvent === 'function') {
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
if (current) {
|
||||
current.textContent = selectedItem ? getItemLabel(selectedItem) : (selected || emptyLabel);
|
||||
}
|
||||
};
|
||||
|
||||
picker.render = (items = [], selectedValue = '') => {
|
||||
const normalizedItems = normalizeItems(items);
|
||||
picker.items = normalizedItems.length ? normalizedItems : getFallbackItems();
|
||||
const inputValue = normalizeValue(input?.value);
|
||||
const selected = normalizeValue(selectedValue)
|
||||
|| (findItemByValue(inputValue) ? inputValue : '')
|
||||
|| getNormalizedItemValue(picker.items[0])
|
||||
|| '';
|
||||
|
||||
if (trigger) {
|
||||
trigger.disabled = picker.items.length === 0;
|
||||
}
|
||||
if (picker.items.length === 0) {
|
||||
if (menu) {
|
||||
menu.innerHTML = '';
|
||||
}
|
||||
picker.setSelection('', { emit: false });
|
||||
picker.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!menu
|
||||
|| typeof menu.appendChild !== 'function'
|
||||
|| typeof globalScope.document === 'undefined'
|
||||
|| typeof globalScope.document.createElement !== 'function'
|
||||
) {
|
||||
picker.setSelection(selected, { emit: false });
|
||||
return;
|
||||
}
|
||||
|
||||
menu.innerHTML = '';
|
||||
picker.items.forEach((item) => {
|
||||
const itemValue = getNormalizedItemValue(item);
|
||||
const row = globalScope.document.createElement('div');
|
||||
row.className = 'editable-list-option-row';
|
||||
|
||||
const option = globalScope.document.createElement('button');
|
||||
option.type = 'button';
|
||||
option.className = 'editable-list-option';
|
||||
option.setAttribute('role', 'option');
|
||||
option.setAttribute('aria-selected', itemValue === selected ? 'true' : 'false');
|
||||
option.textContent = getItemLabel(item);
|
||||
option.addEventListener('click', () => {
|
||||
picker.setSelection(itemValue, { emit: true });
|
||||
picker.close();
|
||||
});
|
||||
|
||||
const deleteButton = globalScope.document.createElement('button');
|
||||
deleteButton.type = 'button';
|
||||
deleteButton.className = 'editable-list-delete';
|
||||
deleteButton.textContent = deleteLabel;
|
||||
deleteButton.title = `${deleteLabel}${itemLabel} ${getItemDeleteLabel(item)}`;
|
||||
deleteButton.setAttribute('aria-label', `${deleteLabel}${itemLabel} ${getItemDeleteLabel(item)}`);
|
||||
deleteButton.disabled = picker.items.length <= minItems;
|
||||
deleteButton.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (typeof onDelete === 'function') {
|
||||
Promise.resolve(onDelete(itemValue, item)).catch(reportDeleteError);
|
||||
}
|
||||
});
|
||||
|
||||
row.appendChild(option);
|
||||
row.appendChild(deleteButton);
|
||||
menu.appendChild(row);
|
||||
});
|
||||
picker.setSelection(selected, { emit: false });
|
||||
};
|
||||
|
||||
trigger?.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
editableListPickers.forEach((item) => {
|
||||
if (item !== picker) {
|
||||
item.close();
|
||||
}
|
||||
});
|
||||
picker.setOpen(!picker.open);
|
||||
});
|
||||
trigger?.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
picker.close();
|
||||
}
|
||||
});
|
||||
menu?.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
editableListPickers.push(picker);
|
||||
return picker;
|
||||
}
|
||||
|
||||
function closeEditableListPickers() {
|
||||
editableListPickers.forEach((picker) => picker.close());
|
||||
}
|
||||
|
||||
function isClickInsideEditableListPicker(target) {
|
||||
return editableListPickers.some((picker) => Boolean(picker.root?.contains(target)));
|
||||
}
|
||||
|
||||
globalScope.SidepanelEditableListPicker = {
|
||||
closeEditableListPickers,
|
||||
createEditableListPicker,
|
||||
isClickInsideEditableListPicker,
|
||||
normalizeEditableListValues,
|
||||
splitEditableListValues,
|
||||
};
|
||||
})(window);
|
||||
@@ -0,0 +1,269 @@
|
||||
(function attachSidepanelFormDialog(globalScope) {
|
||||
const FORM_EYE_OPEN_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>';
|
||||
const FORM_EYE_CLOSED_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.94 10.94 0 0 1 12 19C5 19 1 12 1 12a21.77 21.77 0 0 1 5.06-6.94"/><path d="M9.9 4.24A10.94 10.94 0 0 1 12 5c7 0 11 7 11 7a21.86 21.86 0 0 1-2.16 3.19"/><path d="M1 1l22 22"/><path d="M14.12 14.12a3 3 0 1 1-4.24-4.24"/></svg>';
|
||||
|
||||
function syncPasswordToggleButton(button, input, labels) {
|
||||
if (!button || !input) return;
|
||||
const isHidden = input.type === 'password';
|
||||
button.innerHTML = isHidden ? FORM_EYE_OPEN_ICON : FORM_EYE_CLOSED_ICON;
|
||||
button.setAttribute('aria-label', isHidden ? labels.show : labels.hide);
|
||||
button.title = isHidden ? labels.show : labels.hide;
|
||||
}
|
||||
|
||||
function createFormDialog(context = {}) {
|
||||
const {
|
||||
overlay = null,
|
||||
titleNode = null,
|
||||
closeButton = null,
|
||||
messageNode = null,
|
||||
alertNode = null,
|
||||
fieldsContainer = null,
|
||||
cancelButton = null,
|
||||
confirmButton = null,
|
||||
documentRef = globalScope.document,
|
||||
} = context;
|
||||
|
||||
let resolver = null;
|
||||
let currentConfig = null;
|
||||
let currentInputs = [];
|
||||
|
||||
function setHidden(node, hidden) {
|
||||
if (!node) return;
|
||||
node.hidden = Boolean(hidden);
|
||||
}
|
||||
|
||||
function resetAlert() {
|
||||
if (!alertNode) return;
|
||||
alertNode.textContent = '';
|
||||
alertNode.className = 'modal-alert modal-form-alert';
|
||||
alertNode.hidden = true;
|
||||
}
|
||||
|
||||
function setAlert(message = '', tone = 'danger') {
|
||||
if (!alertNode) return;
|
||||
const text = String(message || '').trim();
|
||||
if (!text) {
|
||||
resetAlert();
|
||||
return;
|
||||
}
|
||||
alertNode.textContent = text;
|
||||
alertNode.className = `modal-alert modal-form-alert${tone === 'danger' ? ' is-danger' : ''}`;
|
||||
alertNode.hidden = false;
|
||||
}
|
||||
|
||||
function close(result = null) {
|
||||
if (resolver) {
|
||||
resolver(result);
|
||||
resolver = null;
|
||||
}
|
||||
currentConfig = null;
|
||||
currentInputs = [];
|
||||
resetAlert();
|
||||
if (fieldsContainer) {
|
||||
fieldsContainer.innerHTML = '';
|
||||
}
|
||||
if (overlay) {
|
||||
overlay.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
function buildFieldNode(field, values) {
|
||||
const wrapper = documentRef.createElement('div');
|
||||
wrapper.className = 'modal-form-row';
|
||||
|
||||
const label = documentRef.createElement('label');
|
||||
label.className = 'modal-form-label';
|
||||
const labelText = String(field.label || field.key || '').trim();
|
||||
label.textContent = labelText;
|
||||
wrapper.appendChild(label);
|
||||
|
||||
let input = null;
|
||||
if (field.type === 'textarea') {
|
||||
input = documentRef.createElement('textarea');
|
||||
input.className = 'data-textarea';
|
||||
} else if (field.type === 'select') {
|
||||
input = documentRef.createElement('select');
|
||||
input.className = 'data-select';
|
||||
const options = Array.isArray(field.options) ? field.options : [];
|
||||
options.forEach((option) => {
|
||||
const optionNode = documentRef.createElement('option');
|
||||
optionNode.value = String(option?.value || '');
|
||||
optionNode.textContent = String(option?.label || option?.value || '');
|
||||
input.appendChild(optionNode);
|
||||
});
|
||||
} else {
|
||||
input = documentRef.createElement('input');
|
||||
input.type = field.type === 'password' ? 'password' : 'text';
|
||||
input.className = field.type === 'password'
|
||||
? 'data-input data-input-with-icon'
|
||||
: 'data-input';
|
||||
}
|
||||
|
||||
const normalizedValue = Object.prototype.hasOwnProperty.call(values, field.key)
|
||||
? values[field.key]
|
||||
: field.value;
|
||||
if (normalizedValue !== undefined && normalizedValue !== null) {
|
||||
input.value = String(normalizedValue);
|
||||
}
|
||||
if (field.placeholder) {
|
||||
input.placeholder = String(field.placeholder);
|
||||
}
|
||||
if (field.autocomplete) {
|
||||
input.autocomplete = String(field.autocomplete);
|
||||
}
|
||||
if (field.inputMode) {
|
||||
input.inputMode = String(field.inputMode);
|
||||
}
|
||||
if (field.rows && field.type === 'textarea') {
|
||||
input.rows = Number(field.rows) || 3;
|
||||
}
|
||||
input.dataset.fieldKey = String(field.key || '');
|
||||
label.htmlFor = field.key;
|
||||
input.id = field.key;
|
||||
if (field.type === 'password') {
|
||||
const inputShell = documentRef.createElement('div');
|
||||
inputShell.className = 'input-with-icon';
|
||||
inputShell.appendChild(input);
|
||||
|
||||
const toggleButton = documentRef.createElement('button');
|
||||
toggleButton.className = 'input-icon-btn';
|
||||
toggleButton.type = 'button';
|
||||
const labels = {
|
||||
show: String(field.showPasswordLabel || `\u663e\u793a${labelText || '\u5bc6\u7801'}`),
|
||||
hide: String(field.hidePasswordLabel || `\u9690\u85cf${labelText || '\u5bc6\u7801'}`),
|
||||
};
|
||||
syncPasswordToggleButton(toggleButton, input, labels);
|
||||
toggleButton.addEventListener('click', () => {
|
||||
input.type = input.type === 'password' ? 'text' : 'password';
|
||||
syncPasswordToggleButton(toggleButton, input, labels);
|
||||
});
|
||||
|
||||
inputShell.appendChild(toggleButton);
|
||||
wrapper.appendChild(inputShell);
|
||||
} else {
|
||||
wrapper.appendChild(input);
|
||||
}
|
||||
|
||||
if (field.type !== 'textarea') {
|
||||
input.addEventListener('keydown', (event) => {
|
||||
if (event.key !== 'Enter') {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
void handleConfirm();
|
||||
});
|
||||
}
|
||||
|
||||
currentInputs.push({ field, input });
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function collectValues() {
|
||||
return currentInputs.reduce((result, item) => {
|
||||
result[item.field.key] = item.input.value;
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
async function handleConfirm() {
|
||||
if (!currentConfig) {
|
||||
close(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const values = collectValues();
|
||||
resetAlert();
|
||||
|
||||
for (const item of currentInputs) {
|
||||
const { field, input } = item;
|
||||
const rawValue = values[field.key];
|
||||
const textValue = String(rawValue || '').trim();
|
||||
if (field.required && !textValue) {
|
||||
setAlert(field.requiredMessage || `${field.label || field.key}不能为空。`);
|
||||
input.focus?.();
|
||||
return;
|
||||
}
|
||||
if (typeof field.validate === 'function') {
|
||||
const validationMessage = await field.validate(rawValue, values);
|
||||
if (validationMessage) {
|
||||
setAlert(validationMessage);
|
||||
input.focus?.();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close(values);
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
overlay?.addEventListener('click', (event) => {
|
||||
if (event.target === overlay) {
|
||||
close(null);
|
||||
}
|
||||
});
|
||||
closeButton?.addEventListener('click', () => close(null));
|
||||
cancelButton?.addEventListener('click', () => close(null));
|
||||
confirmButton?.addEventListener('click', () => {
|
||||
void handleConfirm();
|
||||
});
|
||||
}
|
||||
|
||||
async function open(config = {}) {
|
||||
if (!overlay || !titleNode || !fieldsContainer || !confirmButton) {
|
||||
return null;
|
||||
}
|
||||
if (resolver) {
|
||||
close(null);
|
||||
}
|
||||
|
||||
currentConfig = config || {};
|
||||
currentInputs = [];
|
||||
titleNode.textContent = String(currentConfig.title || '填写表单');
|
||||
if (messageNode) {
|
||||
const message = String(currentConfig.message || '').trim();
|
||||
messageNode.textContent = message;
|
||||
setHidden(messageNode, !message);
|
||||
}
|
||||
resetAlert();
|
||||
if (currentConfig.alert?.text) {
|
||||
setAlert(currentConfig.alert.text, currentConfig.alert.tone || 'danger');
|
||||
}
|
||||
|
||||
confirmButton.textContent = String(currentConfig.confirmLabel || '确认');
|
||||
confirmButton.className = `btn ${currentConfig.confirmVariant || 'btn-primary'} btn-sm`;
|
||||
fieldsContainer.innerHTML = '';
|
||||
|
||||
const initialValues = currentConfig.initialValues && typeof currentConfig.initialValues === 'object'
|
||||
? currentConfig.initialValues
|
||||
: {};
|
||||
const fields = Array.isArray(currentConfig.fields) ? currentConfig.fields : [];
|
||||
fields.forEach((field) => {
|
||||
fieldsContainer.appendChild(buildFieldNode(field, initialValues));
|
||||
});
|
||||
|
||||
overlay.hidden = false;
|
||||
const firstInput = currentInputs[0]?.input || null;
|
||||
if (firstInput && typeof globalScope.requestAnimationFrame === 'function') {
|
||||
globalScope.requestAnimationFrame(() => firstInput.focus?.());
|
||||
} else {
|
||||
firstInput?.focus?.();
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
resolver = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
bindEvents();
|
||||
|
||||
return {
|
||||
close,
|
||||
open,
|
||||
};
|
||||
}
|
||||
|
||||
globalScope.SidepanelFormDialog = {
|
||||
createFormDialog,
|
||||
};
|
||||
})(window);
|
||||
@@ -0,0 +1,482 @@
|
||||
(function attachSidepanelHostedSmsPoolManager(globalScope) {
|
||||
const SEPARATOR = '----';
|
||||
|
||||
function createHostedSmsPoolManager(context = {}) {
|
||||
const {
|
||||
dom = {},
|
||||
helpers = {},
|
||||
state = {},
|
||||
actions = {},
|
||||
constants = {},
|
||||
} = context;
|
||||
|
||||
const copyIcon = constants.copyIcon || '';
|
||||
let renderedEntries = [];
|
||||
let searchTerm = '';
|
||||
let filterMode = 'all';
|
||||
let loading = false;
|
||||
let refreshQueued = false;
|
||||
|
||||
function normalizeText(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function normalizePoolText(value = '') {
|
||||
return String(value || '')
|
||||
.replace(/\r/g, '')
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function normalizePoolPhone(value = '') {
|
||||
return normalizeText(value);
|
||||
}
|
||||
|
||||
function normalizePoolUrl(value = '') {
|
||||
const rawValue = normalizeText(value);
|
||||
if (!rawValue) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(rawValue);
|
||||
parsed.searchParams.delete('t');
|
||||
return parsed.toString();
|
||||
} catch {
|
||||
return rawValue
|
||||
.replace(/([?&])t=\d+(?=(&|$))/i, '$1')
|
||||
.replace(/[?&]$/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
function formatPayPalLocalPhone(value = '') {
|
||||
const rawValue = normalizeText(value);
|
||||
const digits = rawValue.replace(/\D+/g, '');
|
||||
if (rawValue.startsWith('+1') && digits.length === 11 && digits.startsWith('1')) {
|
||||
return digits.slice(1);
|
||||
}
|
||||
return digits;
|
||||
}
|
||||
|
||||
function buildKey(phone = '', verificationUrl = '') {
|
||||
const normalizedPhone = normalizePoolPhone(phone);
|
||||
const normalizedUrl = normalizePoolUrl(verificationUrl);
|
||||
return normalizedPhone && normalizedUrl ? `${normalizedPhone}${SEPARATOR}${normalizedUrl}` : '';
|
||||
}
|
||||
|
||||
function parseEntries(text = '') {
|
||||
const lines = normalizePoolText(text).split('\n').filter(Boolean);
|
||||
const seen = new Set();
|
||||
const entries = [];
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const line = lines[index];
|
||||
const separatorIndex = line.indexOf(SEPARATOR);
|
||||
const hasSeparator = separatorIndex > 0;
|
||||
const phone = hasSeparator
|
||||
? normalizePoolPhone(line.slice(0, separatorIndex))
|
||||
: normalizePoolPhone(line);
|
||||
const verificationUrl = hasSeparator
|
||||
? normalizePoolUrl(line.slice(separatorIndex + SEPARATOR.length))
|
||||
: normalizePoolUrl(lines[index + 1] || '');
|
||||
if (!hasSeparator && verificationUrl) {
|
||||
index += 1;
|
||||
}
|
||||
const key = buildKey(phone, verificationUrl);
|
||||
if (!phone || !verificationUrl || !key || seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
entries.push({
|
||||
index,
|
||||
key,
|
||||
phone,
|
||||
verificationUrl,
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function entriesToText(entries = []) {
|
||||
return parseEntries(entries.map((entry) => `${entry.phone}${SEPARATOR}${entry.verificationUrl}`).join('\n'))
|
||||
.map((entry) => `${entry.phone}${SEPARATOR}${entry.verificationUrl}`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function normalizeUsage(value = {}) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return {};
|
||||
}
|
||||
return Object.fromEntries(Object.entries(value).map(([key, item]) => {
|
||||
const usage = item && typeof item === 'object' && !Array.isArray(item) ? item : {};
|
||||
const legacyUsedCount = Number(usage.usedAt) > 0 ? 1 : 0;
|
||||
const useCount = Math.max(0, Math.floor(Number(usage.useCount ?? usage.usageCount ?? legacyUsedCount) || 0));
|
||||
return [normalizeText(key), {
|
||||
useCount,
|
||||
usedAt: Math.max(0, Number(usage.usedAt) || 0),
|
||||
lastAttemptAt: Math.max(0, Number(usage.lastAttemptAt) || 0),
|
||||
lastError: normalizeText(usage.lastError),
|
||||
}];
|
||||
}).filter(([key]) => Boolean(key)));
|
||||
}
|
||||
|
||||
function getCurrentKey() {
|
||||
const current = state.getCurrentEntry?.() || null;
|
||||
return normalizeText(current?.key || buildKey(current?.phone, current?.verificationUrl));
|
||||
}
|
||||
|
||||
function getEntriesWithState(entries = renderedEntries) {
|
||||
const usage = normalizeUsage(state.getUsage?.());
|
||||
const currentKey = getCurrentKey();
|
||||
return parseEntries(entriesToText(entries)).map((entry) => {
|
||||
const itemUsage = usage[entry.key] || {};
|
||||
return {
|
||||
...entry,
|
||||
current: Boolean(currentKey && entry.key === currentKey),
|
||||
useCount: Math.max(0, Math.floor(Number(itemUsage.useCount) || 0)),
|
||||
used: Math.max(0, Math.floor(Number(itemUsage.useCount) || 0)) > 0,
|
||||
lastAttemptAt: Math.max(0, Number(itemUsage.lastAttemptAt) || 0),
|
||||
lastError: normalizeText(itemUsage.lastError),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getFilteredEntries(entries = renderedEntries) {
|
||||
const normalizedSearch = normalizeText(searchTerm).toLowerCase();
|
||||
return getEntriesWithState(entries).filter((entry) => {
|
||||
const matchesFilter = (() => {
|
||||
switch (filterMode) {
|
||||
case 'current': return Boolean(entry.current);
|
||||
case 'used': return Boolean(entry.used);
|
||||
case 'unused': return !entry.used;
|
||||
case 'error': return Boolean(entry.lastError);
|
||||
default: return true;
|
||||
}
|
||||
})();
|
||||
if (!matchesFilter) {
|
||||
return false;
|
||||
}
|
||||
if (!normalizedSearch) {
|
||||
return true;
|
||||
}
|
||||
return [
|
||||
entry.phone,
|
||||
entry.verificationUrl,
|
||||
entry.current ? 'current 当前' : '',
|
||||
entry.used ? 'used 已用' : 'unused 未用',
|
||||
entry.lastError ? `error 异常 ${entry.lastError}` : '',
|
||||
].join(' ').toLowerCase().includes(normalizedSearch);
|
||||
});
|
||||
}
|
||||
|
||||
function setLoading(nextLoading, summary = '') {
|
||||
loading = Boolean(nextLoading);
|
||||
[
|
||||
dom.btnHostedSmsPoolRefresh,
|
||||
dom.btnHostedSmsPoolClearUsed,
|
||||
dom.btnHostedSmsPoolDeleteAll,
|
||||
dom.btnHostedSmsPoolImport,
|
||||
].forEach((button) => {
|
||||
if (button) button.disabled = loading;
|
||||
});
|
||||
if (dom.inputHostedSmsPoolImport) {
|
||||
dom.inputHostedSmsPoolImport.disabled = loading;
|
||||
}
|
||||
if (summary && dom.hostedSmsPoolSummary) {
|
||||
dom.hostedSmsPoolSummary.textContent = summary;
|
||||
}
|
||||
}
|
||||
|
||||
function updateControls(entries = renderedEntries) {
|
||||
const entriesWithState = getEntriesWithState(entries);
|
||||
const usedCount = entriesWithState.filter((entry) => entry.useCount > 0).length;
|
||||
if (dom.btnHostedSmsPoolClearUsed) {
|
||||
dom.btnHostedSmsPoolClearUsed.disabled = loading || usedCount === 0;
|
||||
}
|
||||
if (dom.btnHostedSmsPoolDeleteAll) {
|
||||
dom.btnHostedSmsPoolDeleteAll.disabled = loading || entriesWithState.length === 0;
|
||||
}
|
||||
}
|
||||
|
||||
function render(entries = parseEntries(state.getText?.())) {
|
||||
if (!dom.hostedSmsPoolList || !dom.hostedSmsPoolSummary) {
|
||||
return;
|
||||
}
|
||||
renderedEntries = parseEntries(entriesToText(entries));
|
||||
dom.hostedSmsPoolList.innerHTML = '';
|
||||
|
||||
const entriesWithState = getEntriesWithState(renderedEntries);
|
||||
if (!entriesWithState.length) {
|
||||
dom.hostedSmsPoolList.innerHTML = '<div class="luckmail-empty">还没有 Hosted 号码,先导入一批号码再开始。</div>';
|
||||
dom.hostedSmsPoolSummary.textContent = '导入 PayPal Hosted 接码号码,每行一个号码和验证码接口。';
|
||||
updateControls([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const usedCount = entriesWithState.filter((entry) => entry.useCount > 0).length;
|
||||
const totalUseCount = entriesWithState.reduce((sum, entry) => sum + Math.max(0, Number(entry.useCount) || 0), 0);
|
||||
dom.hostedSmsPoolSummary.textContent = `已加载 ${entriesWithState.length} 个号码,${usedCount} 个有使用记录,累计使用 ${totalUseCount} 次。`;
|
||||
|
||||
const visibleEntries = getFilteredEntries(renderedEntries);
|
||||
if (!visibleEntries.length) {
|
||||
dom.hostedSmsPoolList.innerHTML = '<div class="luckmail-empty">没有匹配当前筛选条件的号码。</div>';
|
||||
updateControls(renderedEntries);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of visibleEntries) {
|
||||
const item = document.createElement('div');
|
||||
item.className = `luckmail-item${entry.current ? ' is-current' : ''}`;
|
||||
const localPhone = formatPayPalLocalPhone(entry.phone);
|
||||
item.innerHTML = `
|
||||
<div class="luckmail-item-main">
|
||||
<div class="luckmail-item-email-row">
|
||||
<div class="luckmail-item-email hosted-sms-pool-phone">
|
||||
<span>${helpers.escapeHtml?.(entry.phone) || entry.phone}</span>
|
||||
${localPhone && localPhone !== entry.phone ? `<span class="hosted-sms-pool-phone-local">PayPal 填 ${helpers.escapeHtml?.(localPhone) || localPhone}</span>` : ''}
|
||||
</div>
|
||||
<button
|
||||
class="hotmail-copy-btn"
|
||||
type="button"
|
||||
data-action="copy-phone"
|
||||
title="复制号码"
|
||||
aria-label="复制号码 ${helpers.escapeHtml?.(entry.phone) || entry.phone}"
|
||||
>${copyIcon}</button>
|
||||
</div>
|
||||
<div class="luckmail-item-details mono">${helpers.escapeHtml?.(entry.verificationUrl) || entry.verificationUrl}</div>
|
||||
<div class="luckmail-item-meta">
|
||||
${entry.current ? '<span class="luckmail-tag current">当前</span>' : ''}
|
||||
<span class="luckmail-tag active">使用 ${Math.max(0, Number(entry.useCount) || 0)} 次</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="luckmail-item-actions">
|
||||
<button class="btn btn-outline btn-xs" type="button" data-action="increment-usage">次数 +1</button>
|
||||
<button class="btn btn-outline btn-xs" type="button" data-action="reset-usage">清零</button>
|
||||
<button class="btn btn-outline btn-xs" type="button" data-action="delete">删除</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
item.querySelector('[data-action="copy-phone"]')?.addEventListener('click', async () => {
|
||||
await helpers.copyTextToClipboard?.(entry.phone || '');
|
||||
helpers.showToast?.('号码已复制', 'success', 1600);
|
||||
});
|
||||
|
||||
item.querySelector('[data-action="increment-usage"]')?.addEventListener('click', async () => {
|
||||
await patchPool(({ entries: entriesList, usage }) => {
|
||||
const nextUsage = { ...usage };
|
||||
nextUsage[entry.key] = {
|
||||
...(nextUsage[entry.key] || {}),
|
||||
useCount: Math.max(0, Number(nextUsage[entry.key]?.useCount) || 0) + 1,
|
||||
usedAt: Date.now(),
|
||||
lastAttemptAt: Math.max(0, Number(nextUsage[entry.key]?.lastAttemptAt) || 0),
|
||||
lastError: normalizeText(nextUsage[entry.key]?.lastError),
|
||||
};
|
||||
return { entries: entriesList, usage: nextUsage };
|
||||
});
|
||||
});
|
||||
|
||||
item.querySelector('[data-action="reset-usage"]')?.addEventListener('click', async () => {
|
||||
await patchPool(({ entries: entriesList, usage }) => {
|
||||
const nextUsage = { ...usage };
|
||||
nextUsage[entry.key] = {
|
||||
...(nextUsage[entry.key] || {}),
|
||||
useCount: 0,
|
||||
usedAt: 0,
|
||||
lastError: '',
|
||||
};
|
||||
return { entries: entriesList, usage: nextUsage };
|
||||
});
|
||||
});
|
||||
|
||||
item.querySelector('[data-action="delete"]')?.addEventListener('click', async () => {
|
||||
const confirmed = await helpers.openConfirmModal?.({
|
||||
title: '删除 Hosted 号码',
|
||||
message: `确认删除 ${entry.phone} 吗?此操作不可撤销。`,
|
||||
confirmLabel: '确认删除',
|
||||
confirmVariant: 'btn-danger',
|
||||
});
|
||||
if (!confirmed) return;
|
||||
await patchPool(({ entries: entriesList, usage }) => {
|
||||
const nextUsage = { ...usage };
|
||||
delete nextUsage[entry.key];
|
||||
return {
|
||||
entries: entriesList.filter((candidate) => candidate.key !== entry.key),
|
||||
usage: nextUsage,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
dom.hostedSmsPoolList.appendChild(item);
|
||||
}
|
||||
|
||||
updateControls(renderedEntries);
|
||||
}
|
||||
|
||||
async function patchPool(mutator) {
|
||||
const previousText = normalizePoolText(state.getText?.());
|
||||
const previousUsage = normalizeUsage(state.getUsage?.());
|
||||
const previousEntries = parseEntries(previousText);
|
||||
const result = mutator({
|
||||
entries: previousEntries.map((entry) => ({ ...entry })),
|
||||
usage: { ...previousUsage },
|
||||
}) || {};
|
||||
const nextEntries = parseEntries(entriesToText(result.entries || previousEntries));
|
||||
const nextUsage = normalizeUsage(result.usage || previousUsage);
|
||||
const nextText = entriesToText(nextEntries);
|
||||
|
||||
setLoading(true, '正在更新 Hosted 号池...');
|
||||
state.setText?.(nextText);
|
||||
state.setUsage?.(nextUsage);
|
||||
if (!nextText) {
|
||||
actions.clearFallback?.();
|
||||
}
|
||||
render(nextEntries);
|
||||
try {
|
||||
await actions.persistPool?.();
|
||||
return true;
|
||||
} catch (error) {
|
||||
state.setText?.(previousText);
|
||||
state.setUsage?.(previousUsage);
|
||||
render(previousEntries);
|
||||
helpers.showToast?.(`更新 Hosted 号池失败:${error.message}`, 'error');
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function importEntries() {
|
||||
const text = normalizePoolText(dom.inputHostedSmsPoolImport?.value || '');
|
||||
if (!text) {
|
||||
helpers.showToast?.('请先粘贴 Hosted 号码,每行一个号码和验证码接口。', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
const previousEntries = parseEntries(state.getText?.());
|
||||
const knownKeys = new Set(previousEntries.map((entry) => entry.key));
|
||||
const imported = [];
|
||||
let skippedCount = 0;
|
||||
for (const entry of parseEntries(text)) {
|
||||
if (knownKeys.has(entry.key)) {
|
||||
skippedCount += 1;
|
||||
continue;
|
||||
}
|
||||
knownKeys.add(entry.key);
|
||||
imported.push(entry);
|
||||
}
|
||||
if (!imported.length) {
|
||||
helpers.showToast?.(skippedCount > 0 ? '没有可导入的新号码(可能都重复或格式无效)。' : '没有识别到有效号码。', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
const persisted = await patchPool(({ entries, usage }) => ({
|
||||
entries: [...entries, ...imported],
|
||||
usage,
|
||||
}));
|
||||
if (!persisted) {
|
||||
return;
|
||||
}
|
||||
if (dom.inputHostedSmsPoolImport) {
|
||||
dom.inputHostedSmsPoolImport.value = '';
|
||||
}
|
||||
helpers.showToast?.(
|
||||
skippedCount > 0
|
||||
? `已导入 ${imported.length} 个号码,跳过 ${skippedCount} 条重复数据。`
|
||||
: `已导入 ${imported.length} 个号码。`,
|
||||
'success',
|
||||
2200
|
||||
);
|
||||
}
|
||||
|
||||
async function clearUsedState() {
|
||||
const confirmed = await helpers.openConfirmModal?.({
|
||||
title: '清空使用次数',
|
||||
message: '确认清空 Hosted 号池的使用次数吗?号码本身会保留。',
|
||||
confirmLabel: '清空次数',
|
||||
});
|
||||
if (!confirmed) return;
|
||||
await patchPool(({ entries }) => ({ entries, usage: {} }));
|
||||
}
|
||||
|
||||
async function deleteAll() {
|
||||
const confirmed = await helpers.openConfirmModal?.({
|
||||
title: '删除 Hosted 号池',
|
||||
message: '确认删除当前全部 Hosted 号码吗?此操作不可撤销。',
|
||||
confirmLabel: '确认删除',
|
||||
confirmVariant: 'btn-danger',
|
||||
});
|
||||
if (!confirmed) return;
|
||||
await patchPool(() => ({ entries: [], usage: {} }));
|
||||
}
|
||||
|
||||
function refresh(options = {}) {
|
||||
const { silent = false } = options;
|
||||
if (state.isVisible && !state.isVisible()) {
|
||||
return;
|
||||
}
|
||||
if (!silent) setLoading(true, '正在刷新 Hosted 号池...');
|
||||
render(parseEntries(state.getText?.()));
|
||||
if (!silent) setLoading(false);
|
||||
}
|
||||
|
||||
function queueRefresh() {
|
||||
if (refreshQueued) return;
|
||||
refreshQueued = true;
|
||||
setTimeout(() => {
|
||||
refreshQueued = false;
|
||||
refresh({ silent: true });
|
||||
}, 120);
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
dom.btnHostedSmsPoolRefresh?.addEventListener('click', () => refresh());
|
||||
dom.btnHostedSmsPoolImport?.addEventListener('click', () => {
|
||||
void importEntries();
|
||||
});
|
||||
dom.inputHostedSmsPoolImport?.addEventListener('keydown', (event) => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
void importEntries();
|
||||
}
|
||||
});
|
||||
dom.inputHostedSmsPoolSearch?.addEventListener('input', (event) => {
|
||||
searchTerm = normalizeText(event.target.value);
|
||||
render(renderedEntries);
|
||||
});
|
||||
dom.selectHostedSmsPoolFilter?.addEventListener('change', (event) => {
|
||||
filterMode = normalizeText(event.target.value) || 'all';
|
||||
render(renderedEntries);
|
||||
});
|
||||
dom.btnHostedSmsPoolClearUsed?.addEventListener('click', () => {
|
||||
void clearUsedState();
|
||||
});
|
||||
dom.btnHostedSmsPoolDeleteAll?.addEventListener('click', () => {
|
||||
void deleteAll();
|
||||
});
|
||||
}
|
||||
|
||||
function reset() {
|
||||
searchTerm = '';
|
||||
filterMode = 'all';
|
||||
if (dom.inputHostedSmsPoolSearch) dom.inputHostedSmsPoolSearch.value = '';
|
||||
if (dom.selectHostedSmsPoolFilter) dom.selectHostedSmsPoolFilter.value = 'all';
|
||||
if (dom.hostedSmsPoolList) dom.hostedSmsPoolList.innerHTML = '';
|
||||
if (dom.hostedSmsPoolSummary) {
|
||||
dom.hostedSmsPoolSummary.textContent = '导入 PayPal Hosted 接码号码,每行一个号码和验证码接口。';
|
||||
}
|
||||
updateControls([]);
|
||||
}
|
||||
|
||||
return {
|
||||
bindEvents,
|
||||
queueRefresh,
|
||||
refresh,
|
||||
render,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
globalScope.SidepanelHostedSmsPoolManager = {
|
||||
createHostedSmsPoolManager,
|
||||
};
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
@@ -0,0 +1,608 @@
|
||||
(function attachSidepanelHotmailManager(globalScope) {
|
||||
function createHotmailManager(context = {}) {
|
||||
const {
|
||||
state,
|
||||
dom,
|
||||
helpers,
|
||||
runtime,
|
||||
constants = {},
|
||||
hotmailUtils = {},
|
||||
} = context;
|
||||
|
||||
const expandedStorageKey = constants.expandedStorageKey || 'multipage-hotmail-list-expanded';
|
||||
const displayTimeZone = constants.displayTimeZone || 'Asia/Shanghai';
|
||||
const copyIcon = constants.copyIcon || '';
|
||||
const createAccountPoolFormController = globalScope.SidepanelAccountPoolUi?.createAccountPoolFormController;
|
||||
|
||||
let actionInFlight = false;
|
||||
let listExpanded = false;
|
||||
let searchTerm = '';
|
||||
let filterMode = 'all';
|
||||
|
||||
function getHotmailAccountsByUsage(mode = 'all', currentState = state.getLatestState()) {
|
||||
const accounts = helpers.getHotmailAccounts(currentState);
|
||||
if (typeof hotmailUtils.filterHotmailAccountsByUsage === 'function') {
|
||||
return hotmailUtils.filterHotmailAccountsByUsage(accounts, mode);
|
||||
}
|
||||
if (mode === 'used') {
|
||||
return accounts.filter((account) => Boolean(account?.used));
|
||||
}
|
||||
return accounts.slice();
|
||||
}
|
||||
|
||||
function getHotmailBulkActionText(mode, count) {
|
||||
if (typeof hotmailUtils.getHotmailBulkActionLabel === 'function') {
|
||||
return hotmailUtils.getHotmailBulkActionLabel(mode, count);
|
||||
}
|
||||
const normalizedCount = Number.isFinite(Number(count)) ? Math.max(0, Number(count)) : 0;
|
||||
const prefix = mode === 'used' ? '清空已用' : '全部删除';
|
||||
const suffix = normalizedCount > 0 ? `(${normalizedCount})` : '';
|
||||
return `${prefix}${suffix}`;
|
||||
}
|
||||
|
||||
function getHotmailListToggleText(expanded, count) {
|
||||
if (typeof hotmailUtils.getHotmailListToggleLabel === 'function') {
|
||||
return hotmailUtils.getHotmailListToggleLabel(expanded, count);
|
||||
}
|
||||
const normalizedCount = Number.isFinite(Number(count)) ? Math.max(0, Number(count)) : 0;
|
||||
const suffix = normalizedCount > 0 ? `(${normalizedCount})` : '';
|
||||
return `${expanded ? '收起列表' : '展开列表'}${suffix}`;
|
||||
}
|
||||
|
||||
function updateHotmailListViewport() {
|
||||
const count = helpers.getHotmailAccounts().length;
|
||||
const usedCount = getHotmailAccountsByUsage('used').length;
|
||||
if (dom.btnClearUsedHotmailAccounts) {
|
||||
dom.btnClearUsedHotmailAccounts.textContent = getHotmailBulkActionText('used', usedCount);
|
||||
dom.btnClearUsedHotmailAccounts.disabled = usedCount === 0;
|
||||
}
|
||||
if (dom.btnDeleteAllHotmailAccounts) {
|
||||
dom.btnDeleteAllHotmailAccounts.textContent = getHotmailBulkActionText('all', count);
|
||||
dom.btnDeleteAllHotmailAccounts.disabled = count === 0;
|
||||
}
|
||||
if (dom.btnToggleHotmailList) {
|
||||
dom.btnToggleHotmailList.textContent = getHotmailListToggleText(listExpanded, count);
|
||||
dom.btnToggleHotmailList.setAttribute('aria-expanded', String(listExpanded));
|
||||
dom.btnToggleHotmailList.disabled = count === 0;
|
||||
}
|
||||
if (dom.hotmailListShell) {
|
||||
dom.hotmailListShell.classList.toggle('is-expanded', listExpanded);
|
||||
dom.hotmailListShell.classList.toggle('is-collapsed', !listExpanded);
|
||||
}
|
||||
}
|
||||
|
||||
function setHotmailListExpanded(expanded, options = {}) {
|
||||
const { persist = true } = options;
|
||||
listExpanded = Boolean(expanded);
|
||||
updateHotmailListViewport();
|
||||
if (persist) {
|
||||
localStorage.setItem(expandedStorageKey, listExpanded ? '1' : '0');
|
||||
}
|
||||
}
|
||||
|
||||
function initHotmailListExpandedState() {
|
||||
const saved = localStorage.getItem(expandedStorageKey);
|
||||
setHotmailListExpanded(saved === '1', { persist: false });
|
||||
}
|
||||
|
||||
function shouldClearCurrentHotmailSelectionLocally(account) {
|
||||
if (typeof hotmailUtils.shouldClearHotmailCurrentSelection === 'function') {
|
||||
return hotmailUtils.shouldClearHotmailCurrentSelection(account);
|
||||
}
|
||||
return Boolean(account) && account.used === true;
|
||||
}
|
||||
|
||||
function upsertHotmailAccountListLocally(accounts, nextAccount) {
|
||||
if (typeof hotmailUtils.upsertHotmailAccountInList === 'function') {
|
||||
return hotmailUtils.upsertHotmailAccountInList(accounts, nextAccount);
|
||||
}
|
||||
|
||||
const list = Array.isArray(accounts) ? accounts.slice() : [];
|
||||
if (!nextAccount?.id) return list;
|
||||
|
||||
const existingIndex = list.findIndex((account) => account?.id === nextAccount.id);
|
||||
if (existingIndex === -1) {
|
||||
list.push(nextAccount);
|
||||
return list;
|
||||
}
|
||||
|
||||
list[existingIndex] = nextAccount;
|
||||
return list;
|
||||
}
|
||||
|
||||
function refreshHotmailSelectionUI() {
|
||||
renderHotmailAccounts();
|
||||
if (dom.selectMailProvider.value === 'hotmail-api') {
|
||||
dom.inputEmail.value = helpers.getCurrentHotmailEmail();
|
||||
}
|
||||
}
|
||||
|
||||
function applyHotmailAccountMutation(account, options = {}) {
|
||||
if (!account?.id) return;
|
||||
const { preserveCurrentSelection = false } = options;
|
||||
|
||||
const latestState = state.getLatestState();
|
||||
const nextState = {
|
||||
hotmailAccounts: upsertHotmailAccountListLocally(helpers.getHotmailAccounts(), account),
|
||||
};
|
||||
|
||||
if (!preserveCurrentSelection
|
||||
&& latestState?.currentHotmailAccountId === account.id
|
||||
&& shouldClearCurrentHotmailSelectionLocally(account)) {
|
||||
nextState.currentHotmailAccountId = null;
|
||||
if (dom.selectMailProvider.value === 'hotmail-api') {
|
||||
nextState.email = null;
|
||||
}
|
||||
}
|
||||
|
||||
state.syncLatestState(nextState);
|
||||
refreshHotmailSelectionUI();
|
||||
}
|
||||
|
||||
function formatDateTime(timestamp) {
|
||||
const value = Number(timestamp);
|
||||
if (!Number.isFinite(value) || value <= 0) {
|
||||
return '未使用';
|
||||
}
|
||||
return new Date(value).toLocaleString('zh-CN', {
|
||||
hour12: false,
|
||||
timeZone: displayTimeZone,
|
||||
});
|
||||
}
|
||||
|
||||
function getHotmailAvailabilityLabel(account) {
|
||||
if (account.used) return '已用';
|
||||
return '可分配';
|
||||
}
|
||||
|
||||
function getHotmailStatusLabel(account) {
|
||||
if (account.used) return '已用';
|
||||
|
||||
switch (account.status) {
|
||||
case 'authorized':
|
||||
return '可用';
|
||||
case 'error':
|
||||
return '异常';
|
||||
default:
|
||||
return '待校验';
|
||||
}
|
||||
}
|
||||
|
||||
function getHotmailStatusClass(account) {
|
||||
if (account.used) return 'status-used';
|
||||
return `status-${account.status || 'pending'}`;
|
||||
}
|
||||
|
||||
function normalizeSearchText(value = '') {
|
||||
return String(value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function getFilteredHotmailAccounts(accounts, currentId = '') {
|
||||
const normalizedSearchTerm = normalizeSearchText(searchTerm);
|
||||
return accounts.filter((account) => {
|
||||
const isCurrent = Boolean(currentId) && account.id === currentId;
|
||||
const matchesFilter = (() => {
|
||||
switch (filterMode) {
|
||||
case 'current': return isCurrent;
|
||||
case 'available': return !account.used;
|
||||
case 'used': return Boolean(account.used);
|
||||
case 'error': return account.status === 'error';
|
||||
default: return true;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!matchesFilter) return false;
|
||||
if (!normalizedSearchTerm) return true;
|
||||
|
||||
const haystack = [
|
||||
account.email,
|
||||
account.status,
|
||||
getHotmailAvailabilityLabel(account),
|
||||
getHotmailStatusLabel(account),
|
||||
isCurrent ? 'current 当前' : '',
|
||||
].join(' ').toLowerCase();
|
||||
|
||||
return haystack.includes(normalizedSearchTerm);
|
||||
});
|
||||
}
|
||||
|
||||
function clearHotmailForm() {
|
||||
dom.inputHotmailEmail.value = '';
|
||||
dom.inputHotmailClientId.value = '';
|
||||
dom.inputHotmailPassword.value = '';
|
||||
dom.inputHotmailRefreshToken.value = '';
|
||||
}
|
||||
|
||||
const formController = typeof createAccountPoolFormController === 'function'
|
||||
? createAccountPoolFormController({
|
||||
formShell: dom.hotmailFormShell,
|
||||
toggleButton: dom.btnToggleHotmailForm,
|
||||
hiddenLabel: '添加账号',
|
||||
visibleLabel: '取消添加',
|
||||
onClear: () => {
|
||||
clearHotmailForm();
|
||||
},
|
||||
onFocus: () => {
|
||||
dom.inputHotmailEmail?.focus?.();
|
||||
},
|
||||
})
|
||||
: {
|
||||
isVisible: () => false,
|
||||
setVisible() {},
|
||||
sync() {},
|
||||
};
|
||||
|
||||
function renderHotmailAccounts() {
|
||||
if (!dom.hotmailAccountsList) return;
|
||||
const latestState = state.getLatestState();
|
||||
const accounts = helpers.getHotmailAccounts();
|
||||
const currentId = latestState?.currentHotmailAccountId || '';
|
||||
|
||||
if (!accounts.length) {
|
||||
dom.hotmailAccountsList.innerHTML = '<div class="hotmail-empty">还没有 Hotmail 账号,先添加一条再校验。</div>';
|
||||
updateHotmailListViewport();
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleAccounts = getFilteredHotmailAccounts(accounts, currentId);
|
||||
if (!visibleAccounts.length) {
|
||||
dom.hotmailAccountsList.innerHTML = '<div class="hotmail-empty">没有匹配当前筛选条件的 Hotmail 账号。</div>';
|
||||
updateHotmailListViewport();
|
||||
return;
|
||||
}
|
||||
|
||||
dom.hotmailAccountsList.innerHTML = visibleAccounts.map((account) => `
|
||||
<div class="hotmail-account-item${account.id === currentId ? ' is-current' : ''}">
|
||||
<div class="hotmail-account-top">
|
||||
<div class="hotmail-account-title-row">
|
||||
<div class="hotmail-account-email">${helpers.escapeHtml(account.email || '(未命名账号)')}</div>
|
||||
<button
|
||||
class="hotmail-copy-btn"
|
||||
type="button"
|
||||
data-account-action="copy-email"
|
||||
data-account-id="${helpers.escapeHtml(account.id)}"
|
||||
title="复制邮箱"
|
||||
aria-label="复制邮箱 ${helpers.escapeHtml(account.email || '')}"
|
||||
>${copyIcon}</button>
|
||||
</div>
|
||||
<span class="hotmail-status-chip ${helpers.escapeHtml(getHotmailStatusClass(account))}">${helpers.escapeHtml(getHotmailStatusLabel(account))}</span>
|
||||
</div>
|
||||
<div class="hotmail-account-meta">
|
||||
<span>客户端 ID:${helpers.escapeHtml(account.clientId ? `${account.clientId.slice(0, 10)}...` : '未填写')}</span>
|
||||
<span>刷新令牌:${account.refreshToken ? '已保存' : '未保存'}</span>
|
||||
<span>分配状态: ${helpers.escapeHtml(getHotmailAvailabilityLabel(account))}</span>
|
||||
<span>上次校验: ${helpers.escapeHtml(formatDateTime(account.lastAuthAt))}</span>
|
||||
<span>上次使用: ${helpers.escapeHtml(formatDateTime(account.lastUsedAt))}</span>
|
||||
</div>
|
||||
${account.lastError ? `<div class="hotmail-account-error">${helpers.escapeHtml(account.lastError)}</div>` : ''}
|
||||
<div class="hotmail-account-actions">
|
||||
<button class="btn btn-outline btn-sm" type="button" data-account-action="select" data-account-id="${helpers.escapeHtml(account.id)}">使用此账号</button>
|
||||
<button class="btn btn-outline btn-sm" type="button" data-account-action="toggle-used" data-account-id="${helpers.escapeHtml(account.id)}">${account.used ? '标记未用' : '标记已用'}</button>
|
||||
<button class="btn btn-primary btn-sm" type="button" data-account-action="verify" data-account-id="${helpers.escapeHtml(account.id)}">校验</button>
|
||||
<button class="btn btn-outline btn-sm" type="button" data-account-action="test" data-account-id="${helpers.escapeHtml(account.id)}">复制最新验证码</button>
|
||||
<button class="btn btn-ghost btn-sm" type="button" data-account-action="delete" data-account-id="${helpers.escapeHtml(account.id)}">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
updateHotmailListViewport();
|
||||
}
|
||||
|
||||
async function deleteHotmailAccountsByMode(mode) {
|
||||
const isUsedMode = mode === 'used';
|
||||
const targetAccounts = getHotmailAccountsByUsage(isUsedMode ? 'used' : 'all');
|
||||
if (!targetAccounts.length) {
|
||||
helpers.showToast(isUsedMode ? '没有已用账号可清空。' : '没有可删除的 Hotmail 账号。', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await helpers.openConfirmModal({
|
||||
title: isUsedMode ? '清空已用账号' : '全部删除账号',
|
||||
message: isUsedMode
|
||||
? `确认删除当前 ${targetAccounts.length} 个已用 Hotmail 账号吗?`
|
||||
: `确认删除全部 ${targetAccounts.length} 个 Hotmail 账号吗?`,
|
||||
confirmLabel: isUsedMode ? '确认清空已用' : '确认全部删除',
|
||||
confirmVariant: isUsedMode ? 'btn-outline' : 'btn-danger',
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'DELETE_HOTMAIL_ACCOUNTS',
|
||||
source: 'sidepanel',
|
||||
payload: { mode: isUsedMode ? 'used' : 'all' },
|
||||
});
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
const latestState = state.getLatestState();
|
||||
const targetIds = new Set(targetAccounts.map((account) => account.id));
|
||||
const nextAccounts = isUsedMode
|
||||
? helpers.getHotmailAccounts().filter((account) => !targetIds.has(account.id))
|
||||
: [];
|
||||
const nextState = { hotmailAccounts: nextAccounts };
|
||||
if (latestState?.currentHotmailAccountId && targetIds.has(latestState.currentHotmailAccountId)) {
|
||||
nextState.currentHotmailAccountId = null;
|
||||
if (dom.selectMailProvider.value === 'hotmail-api') {
|
||||
nextState.email = null;
|
||||
}
|
||||
}
|
||||
state.syncLatestState(nextState);
|
||||
refreshHotmailSelectionUI();
|
||||
|
||||
helpers.showToast(
|
||||
isUsedMode
|
||||
? `已清空 ${response.deletedCount || 0} 个已用 Hotmail 账号`
|
||||
: `已删除全部 ${response.deletedCount || 0} 个 Hotmail 账号`,
|
||||
'success',
|
||||
2200
|
||||
);
|
||||
}
|
||||
|
||||
async function handleAddHotmailAccount() {
|
||||
if (actionInFlight) return;
|
||||
|
||||
const email = dom.inputHotmailEmail.value.trim();
|
||||
const clientId = dom.inputHotmailClientId.value.trim();
|
||||
const refreshToken = dom.inputHotmailRefreshToken.value.trim();
|
||||
if (!email) {
|
||||
helpers.showToast('请先填写 Hotmail 邮箱。', 'warn');
|
||||
return;
|
||||
}
|
||||
if (!clientId) {
|
||||
helpers.showToast('请先填写微软应用客户端 ID。', 'warn');
|
||||
return;
|
||||
}
|
||||
if (!refreshToken) {
|
||||
helpers.showToast('请先填写刷新令牌(refresh token)。', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
actionInFlight = true;
|
||||
dom.btnAddHotmailAccount.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'UPSERT_HOTMAIL_ACCOUNT',
|
||||
source: 'sidepanel',
|
||||
payload: {
|
||||
email,
|
||||
clientId,
|
||||
password: dom.inputHotmailPassword.value,
|
||||
refreshToken,
|
||||
},
|
||||
});
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
helpers.showToast(`已保存 Hotmail 账号 ${email}`, 'success', 1800);
|
||||
formController.setVisible(false, { clearForm: true });
|
||||
} catch (err) {
|
||||
helpers.showToast(`保存 Hotmail 账号失败:${err.message}`, 'error');
|
||||
} finally {
|
||||
actionInFlight = false;
|
||||
dom.btnAddHotmailAccount.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleImportHotmailAccounts() {
|
||||
if (actionInFlight) return;
|
||||
if (typeof hotmailUtils.parseHotmailImportText !== 'function') {
|
||||
helpers.showToast('导入解析器未加载,请刷新扩展后重试。', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const rawText = dom.inputHotmailImport.value.trim();
|
||||
if (!rawText) {
|
||||
helpers.showToast('请先粘贴账号导入内容。', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedAccounts = hotmailUtils.parseHotmailImportText(rawText);
|
||||
if (!parsedAccounts.length) {
|
||||
helpers.showToast('没有解析到有效账号,请检查格式是否为 账号----密码----ID----Token。', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
actionInFlight = true;
|
||||
dom.btnImportHotmailAccounts.disabled = true;
|
||||
|
||||
try {
|
||||
for (const account of parsedAccounts) {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'UPSERT_HOTMAIL_ACCOUNT',
|
||||
source: 'sidepanel',
|
||||
payload: account,
|
||||
});
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
}
|
||||
|
||||
dom.inputHotmailImport.value = '';
|
||||
helpers.showToast(`已导入 ${parsedAccounts.length} 条 Hotmail 账号`, 'success', 2200);
|
||||
} catch (err) {
|
||||
helpers.showToast(`批量导入失败:${err.message}`, 'error');
|
||||
} finally {
|
||||
actionInFlight = false;
|
||||
dom.btnImportHotmailAccounts.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAccountListClick(event) {
|
||||
const actionButton = event.target.closest('[data-account-action]');
|
||||
if (!actionButton || actionInFlight) {
|
||||
return;
|
||||
}
|
||||
|
||||
const accountId = actionButton.dataset.accountId;
|
||||
const action = actionButton.dataset.accountAction;
|
||||
if (!accountId || !action) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetAccount = helpers.getHotmailAccounts().find((account) => account.id === accountId) || null;
|
||||
|
||||
actionInFlight = true;
|
||||
actionButton.disabled = true;
|
||||
|
||||
try {
|
||||
if (action === 'copy-email') {
|
||||
if (!targetAccount?.email) throw new Error('未找到可复制的邮箱地址。');
|
||||
await helpers.copyTextToClipboard(targetAccount.email);
|
||||
helpers.showToast(`已复制 ${targetAccount.email}`, 'success', 1800);
|
||||
} else if (action === 'select') {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'SELECT_HOTMAIL_ACCOUNT',
|
||||
source: 'sidepanel',
|
||||
payload: { accountId },
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
state.syncLatestState({ currentHotmailAccountId: response.account.id });
|
||||
applyHotmailAccountMutation(response.account, { preserveCurrentSelection: true });
|
||||
helpers.showToast(`已切换当前 Hotmail 账号为 ${response.account.email}`, 'success', 1800);
|
||||
} else if (action === 'toggle-used') {
|
||||
if (!targetAccount) throw new Error('未找到目标 Hotmail 账号。');
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'PATCH_HOTMAIL_ACCOUNT',
|
||||
source: 'sidepanel',
|
||||
payload: {
|
||||
accountId,
|
||||
updates: { used: !targetAccount.used },
|
||||
},
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
applyHotmailAccountMutation(response.account);
|
||||
helpers.showToast(`账号 ${response.account.email} 已${response.account.used ? '标记为已用' : '恢复为未用'}`, 'success', 2200);
|
||||
} else if (action === 'verify') {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'VERIFY_HOTMAIL_ACCOUNT',
|
||||
source: 'sidepanel',
|
||||
payload: { accountId },
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
applyHotmailAccountMutation(response.account, { preserveCurrentSelection: true });
|
||||
helpers.showToast(`账号 ${response.account.email} 校验通过`, 'success', 2200);
|
||||
} else if (action === 'test') {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'TEST_HOTMAIL_ACCOUNT',
|
||||
source: 'sidepanel',
|
||||
payload: { accountId },
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
applyHotmailAccountMutation(response.account, { preserveCurrentSelection: true });
|
||||
if (response.latestCode) {
|
||||
await helpers.copyTextToClipboard(response.latestCode);
|
||||
const mailbox = response.latestMailbox ? `(${response.latestMailbox})` : '';
|
||||
helpers.showToast(`已复制最新验证码 ${response.latestCode}${mailbox}`, 'success', 2600);
|
||||
} else if (response.latestSubject) {
|
||||
const mailbox = response.latestMailbox ? `(${response.latestMailbox})` : '';
|
||||
helpers.showToast(`最新邮件${mailbox}没有验证码:${response.latestSubject}`, 'warn', 3200);
|
||||
} else {
|
||||
helpers.showToast('当前没有可读取的最新邮件。', 'warn', 2600);
|
||||
}
|
||||
} else if (action === 'delete') {
|
||||
const confirmed = await helpers.openConfirmModal({
|
||||
title: '删除账号',
|
||||
message: '确认删除这个 Hotmail 账号吗?对应 token 也会一起移除。',
|
||||
confirmLabel: '确认删除',
|
||||
confirmVariant: 'btn-danger',
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'DELETE_HOTMAIL_ACCOUNT',
|
||||
source: 'sidepanel',
|
||||
payload: { accountId },
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
helpers.showToast('Hotmail 账号已删除', 'success', 1800);
|
||||
}
|
||||
} catch (err) {
|
||||
helpers.showToast(err.message, 'error');
|
||||
} finally {
|
||||
actionInFlight = false;
|
||||
actionButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function bindHotmailEvents() {
|
||||
dom.btnToggleHotmailList?.addEventListener('click', () => {
|
||||
setHotmailListExpanded(!listExpanded);
|
||||
});
|
||||
|
||||
dom.btnToggleHotmailForm?.addEventListener('click', () => {
|
||||
if (formController.isVisible()) {
|
||||
formController.setVisible(false, { clearForm: true });
|
||||
return;
|
||||
}
|
||||
formController.setVisible(true, { focusField: true });
|
||||
});
|
||||
|
||||
dom.btnHotmailUsageGuide?.addEventListener('click', async () => {
|
||||
await helpers.openConfirmModal({
|
||||
title: '使用教程',
|
||||
message: 'API对接模式会直接调用微软邮箱接口取件;本地助手模式仍走本地服务。两种模式继续共用同一套 Hotmail 账号池与导入格式。',
|
||||
confirmLabel: '确定',
|
||||
confirmVariant: 'btn-primary',
|
||||
});
|
||||
});
|
||||
|
||||
dom.btnClearUsedHotmailAccounts?.addEventListener('click', async () => {
|
||||
if (actionInFlight) return;
|
||||
actionInFlight = true;
|
||||
dom.btnClearUsedHotmailAccounts.disabled = true;
|
||||
try {
|
||||
await deleteHotmailAccountsByMode('used');
|
||||
} catch (err) {
|
||||
helpers.showToast(err.message, 'error');
|
||||
} finally {
|
||||
actionInFlight = false;
|
||||
updateHotmailListViewport();
|
||||
}
|
||||
});
|
||||
|
||||
dom.btnDeleteAllHotmailAccounts?.addEventListener('click', async () => {
|
||||
if (actionInFlight) return;
|
||||
actionInFlight = true;
|
||||
dom.btnDeleteAllHotmailAccounts.disabled = true;
|
||||
try {
|
||||
await deleteHotmailAccountsByMode('all');
|
||||
} catch (err) {
|
||||
helpers.showToast(err.message, 'error');
|
||||
} finally {
|
||||
actionInFlight = false;
|
||||
updateHotmailListViewport();
|
||||
}
|
||||
});
|
||||
|
||||
dom.btnAddHotmailAccount?.addEventListener('click', handleAddHotmailAccount);
|
||||
dom.btnImportHotmailAccounts?.addEventListener('click', handleImportHotmailAccounts);
|
||||
dom.inputHotmailSearch?.addEventListener('input', (event) => {
|
||||
searchTerm = normalizeSearchText(event.target.value);
|
||||
renderHotmailAccounts();
|
||||
});
|
||||
dom.selectHotmailFilter?.addEventListener('change', (event) => {
|
||||
filterMode = String(event.target.value || 'all');
|
||||
renderHotmailAccounts();
|
||||
});
|
||||
dom.hotmailAccountsList?.addEventListener('click', handleAccountListClick);
|
||||
formController.sync();
|
||||
}
|
||||
|
||||
return {
|
||||
bindHotmailEvents,
|
||||
initHotmailListExpandedState,
|
||||
renderHotmailAccounts,
|
||||
};
|
||||
}
|
||||
|
||||
globalScope.SidepanelHotmailManager = {
|
||||
createHotmailManager,
|
||||
};
|
||||
})(window);
|
||||
@@ -0,0 +1,522 @@
|
||||
(function attachSidepanelIcloudManager(globalScope) {
|
||||
function createIcloudManager(context = {}) {
|
||||
const {
|
||||
dom,
|
||||
helpers,
|
||||
runtime,
|
||||
} = context;
|
||||
|
||||
let refreshQueued = false;
|
||||
let renderedAliases = [];
|
||||
let selectedEmails = new Set();
|
||||
let searchTerm = '';
|
||||
let filterMode = 'all';
|
||||
|
||||
function normalizeIcloudSearchText(value) {
|
||||
return String(value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function getFilteredIcloudAliases(aliases = renderedAliases) {
|
||||
const normalizedSearchTerm = normalizeIcloudSearchText(searchTerm);
|
||||
return (Array.isArray(aliases) ? aliases : []).filter((alias) => {
|
||||
const matchesFilter = (() => {
|
||||
switch (filterMode) {
|
||||
case 'active': return Boolean(alias.active);
|
||||
case 'used': return Boolean(alias.used);
|
||||
case 'unused': return !alias.used;
|
||||
case 'preserved': return Boolean(alias.preserved);
|
||||
default: return true;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!matchesFilter) return false;
|
||||
if (!normalizedSearchTerm) return true;
|
||||
|
||||
const haystack = [
|
||||
alias.email,
|
||||
alias.label,
|
||||
alias.note,
|
||||
alias.used ? '已用 used' : '未用 unused',
|
||||
alias.active ? '可用 active' : '不可用 inactive',
|
||||
alias.preserved ? '保留 preserved' : '',
|
||||
].join(' ').toLowerCase();
|
||||
|
||||
return haystack.includes(normalizedSearchTerm);
|
||||
});
|
||||
}
|
||||
|
||||
function pruneIcloudSelection(aliases = renderedAliases) {
|
||||
const existing = new Set((Array.isArray(aliases) ? aliases : []).map((alias) => alias.email));
|
||||
selectedEmails = new Set([...selectedEmails].filter((email) => existing.has(email)));
|
||||
}
|
||||
|
||||
function updateIcloudBulkUI(visibleAliases = getFilteredIcloudAliases()) {
|
||||
if (!dom.checkboxIcloudSelectAll || !dom.icloudSelectionSummary) {
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleEmails = visibleAliases.map((alias) => alias.email);
|
||||
const selectedVisibleCount = visibleEmails.filter((email) => selectedEmails.has(email)).length;
|
||||
const hasVisible = visibleEmails.length > 0;
|
||||
|
||||
dom.checkboxIcloudSelectAll.checked = hasVisible && selectedVisibleCount === visibleEmails.length;
|
||||
dom.checkboxIcloudSelectAll.indeterminate = selectedVisibleCount > 0 && selectedVisibleCount < visibleEmails.length;
|
||||
dom.checkboxIcloudSelectAll.disabled = !hasVisible;
|
||||
dom.icloudSelectionSummary.textContent = `已选 ${selectedEmails.size} 个(当前显示 ${visibleEmails.length} 个)`;
|
||||
|
||||
const hasSelection = selectedEmails.size > 0;
|
||||
if (dom.btnIcloudBulkUsed) dom.btnIcloudBulkUsed.disabled = !hasSelection;
|
||||
if (dom.btnIcloudBulkUnused) dom.btnIcloudBulkUnused.disabled = !hasSelection;
|
||||
if (dom.btnIcloudBulkPreserve) dom.btnIcloudBulkPreserve.disabled = !hasSelection;
|
||||
if (dom.btnIcloudBulkUnpreserve) dom.btnIcloudBulkUnpreserve.disabled = !hasSelection;
|
||||
if (dom.btnIcloudBulkDelete) dom.btnIcloudBulkDelete.disabled = !hasSelection;
|
||||
}
|
||||
|
||||
function setIcloudLoadingState(loading, summary = '') {
|
||||
if (dom.btnIcloudRefresh) dom.btnIcloudRefresh.disabled = loading;
|
||||
if (dom.btnIcloudDeleteUsed) dom.btnIcloudDeleteUsed.disabled = loading;
|
||||
if (dom.btnIcloudLoginDone) dom.btnIcloudLoginDone.disabled = loading;
|
||||
if (dom.inputIcloudSearch) dom.inputIcloudSearch.disabled = loading;
|
||||
if (dom.selectIcloudFilter) dom.selectIcloudFilter.disabled = loading;
|
||||
if (dom.checkboxIcloudSelectAll) dom.checkboxIcloudSelectAll.disabled = loading || getFilteredIcloudAliases().length === 0;
|
||||
if (dom.btnIcloudBulkUsed) dom.btnIcloudBulkUsed.disabled = loading || selectedEmails.size === 0;
|
||||
if (dom.btnIcloudBulkUnused) dom.btnIcloudBulkUnused.disabled = loading || selectedEmails.size === 0;
|
||||
if (dom.btnIcloudBulkPreserve) dom.btnIcloudBulkPreserve.disabled = loading || selectedEmails.size === 0;
|
||||
if (dom.btnIcloudBulkUnpreserve) dom.btnIcloudBulkUnpreserve.disabled = loading || selectedEmails.size === 0;
|
||||
if (dom.btnIcloudBulkDelete) dom.btnIcloudBulkDelete.disabled = loading || selectedEmails.size === 0;
|
||||
if (summary && dom.icloudSummary) dom.icloudSummary.textContent = summary;
|
||||
}
|
||||
|
||||
function showIcloudLoginHelp(payload = {}) {
|
||||
if (!dom.icloudLoginHelp) return;
|
||||
const loginUrl = String(payload.loginUrl || '').trim();
|
||||
let host = 'icloud.com.cn / icloud.com';
|
||||
if (loginUrl) {
|
||||
try {
|
||||
host = new URL(loginUrl).host;
|
||||
} catch {
|
||||
host = loginUrl;
|
||||
}
|
||||
}
|
||||
if (dom.icloudLoginHelpTitle) dom.icloudLoginHelpTitle.textContent = '需要登录 iCloud';
|
||||
if (dom.icloudLoginHelpText) dom.icloudLoginHelpText.textContent = `我已经为你打开 ${host}。请在那个页面完成登录,然后回到这里点击“我已登录”。`;
|
||||
dom.icloudLoginHelp.style.display = 'flex';
|
||||
}
|
||||
|
||||
function hideIcloudLoginHelp() {
|
||||
if (dom.icloudLoginHelp) {
|
||||
dom.icloudLoginHelp.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function renderIcloudAliases(aliases = []) {
|
||||
if (!dom.icloudList || !dom.icloudSummary) return;
|
||||
|
||||
renderedAliases = Array.isArray(aliases) ? aliases : [];
|
||||
pruneIcloudSelection(renderedAliases);
|
||||
dom.icloudList.innerHTML = '';
|
||||
|
||||
if (!aliases.length) {
|
||||
selectedEmails.clear();
|
||||
dom.icloudList.innerHTML = '<div class="icloud-empty">未找到 iCloud Hide My Email 别名。</div>';
|
||||
dom.icloudSummary.textContent = '加载你的 iCloud Hide My Email 别名以便在这里管理。';
|
||||
if (dom.btnIcloudDeleteUsed) dom.btnIcloudDeleteUsed.disabled = true;
|
||||
updateIcloudBulkUI([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const usedCount = aliases.filter((alias) => alias.used).length;
|
||||
const deletableUsedCount = aliases.filter((alias) => alias.used && !alias.preserved).length;
|
||||
dom.icloudSummary.textContent = `已加载 ${aliases.length} 个别名,其中 ${usedCount} 个已标记为已用。`;
|
||||
if (dom.btnIcloudDeleteUsed) dom.btnIcloudDeleteUsed.disabled = deletableUsedCount === 0;
|
||||
|
||||
const visibleAliases = getFilteredIcloudAliases(aliases);
|
||||
if (!visibleAliases.length) {
|
||||
dom.icloudList.innerHTML = '<div class="icloud-empty">没有匹配当前筛选条件的别名。</div>';
|
||||
updateIcloudBulkUI([]);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const alias of visibleAliases) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'icloud-item';
|
||||
item.innerHTML = `
|
||||
<input class="icloud-item-check" type="checkbox" data-action="select" ${selectedEmails.has(alias.email) ? 'checked' : ''} />
|
||||
<div class="icloud-item-main">
|
||||
<div class="icloud-item-email">${helpers.escapeHtml(alias.email)}</div>
|
||||
<div class="icloud-item-meta">
|
||||
${alias.used ? '<span class="icloud-tag used">已用</span>' : ''}
|
||||
${!alias.used && alias.active ? '<span class="icloud-tag active">可用</span>' : ''}
|
||||
${alias.preserved ? '<span class="icloud-tag">保留</span>' : ''}
|
||||
${alias.label ? `<span class="icloud-tag">${helpers.escapeHtml(alias.label)}</span>` : ''}
|
||||
${alias.note ? `<span class="icloud-tag">${helpers.escapeHtml(alias.note)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="icloud-item-actions">
|
||||
<button class="btn btn-outline btn-xs" type="button" data-action="toggle-used">${helpers.escapeHtml(alias.used ? '标记未用' : '标记已用')}</button>
|
||||
<button class="btn btn-outline btn-xs" type="button" data-action="toggle-preserved">${helpers.escapeHtml(alias.preserved ? '取消保留' : '保留')}</button>
|
||||
<button class="btn btn-outline btn-xs" type="button" data-action="delete">删除</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
item.querySelector('[data-action="select"]').addEventListener('change', (event) => {
|
||||
if (event.target.checked) {
|
||||
selectedEmails.add(alias.email);
|
||||
} else {
|
||||
selectedEmails.delete(alias.email);
|
||||
}
|
||||
updateIcloudBulkUI(visibleAliases);
|
||||
});
|
||||
item.querySelector('[data-action="toggle-used"]').addEventListener('click', async () => {
|
||||
await setSingleIcloudAliasUsedState(alias, !alias.used);
|
||||
});
|
||||
item.querySelector('[data-action="toggle-preserved"]').addEventListener('click', async () => {
|
||||
await setSingleIcloudAliasPreservedState(alias, !alias.preserved);
|
||||
});
|
||||
item.querySelector('[data-action="delete"]').addEventListener('click', async () => {
|
||||
await deleteSingleIcloudAlias(alias);
|
||||
});
|
||||
dom.icloudList.appendChild(item);
|
||||
}
|
||||
|
||||
updateIcloudBulkUI(visibleAliases);
|
||||
}
|
||||
|
||||
async function refreshIcloudAliases(options = {}) {
|
||||
const { silent = false } = options;
|
||||
if (!dom.icloudSection || dom.icloudSection.style.display === 'none') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!silent) setIcloudLoadingState(true, '正在加载 iCloud 别名...');
|
||||
try {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'LIST_ICLOUD_ALIASES',
|
||||
source: 'sidepanel',
|
||||
payload: {},
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
hideIcloudLoginHelp();
|
||||
renderIcloudAliases(response?.aliases || []);
|
||||
} catch (err) {
|
||||
selectedEmails.clear();
|
||||
if (dom.icloudList) {
|
||||
dom.icloudList.innerHTML = '<div class="icloud-empty">无法加载 iCloud 别名。</div>';
|
||||
}
|
||||
if (dom.icloudSummary) {
|
||||
dom.icloudSummary.textContent = err.message;
|
||||
}
|
||||
updateIcloudBulkUI([]);
|
||||
if (!silent) helpers.showToast(`iCloud 别名加载失败:${err.message}`, 'error');
|
||||
} finally {
|
||||
setIcloudLoadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
function queueIcloudAliasRefresh() {
|
||||
if (refreshQueued) return;
|
||||
refreshQueued = true;
|
||||
setTimeout(async () => {
|
||||
refreshQueued = false;
|
||||
await refreshIcloudAliases({ silent: true });
|
||||
}, 150);
|
||||
}
|
||||
|
||||
async function deleteSingleIcloudAlias(alias) {
|
||||
const confirmed = await helpers.openConfirmModal({
|
||||
title: '删除 iCloud 别名',
|
||||
message: `确认删除 ${alias.email} 吗?此操作不可撤销。`,
|
||||
confirmLabel: '确认删除',
|
||||
confirmVariant: 'btn-danger',
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIcloudLoadingState(true, `正在删除 ${alias.email} ...`);
|
||||
try {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'DELETE_ICLOUD_ALIAS',
|
||||
source: 'sidepanel',
|
||||
payload: { email: alias.email, anonymousId: alias.anonymousId },
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
helpers.showToast(`已删除 ${alias.email}`, 'success', 2200);
|
||||
await refreshIcloudAliases({ silent: true });
|
||||
} catch (err) {
|
||||
if (dom.icloudSummary) dom.icloudSummary.textContent = err.message;
|
||||
helpers.showToast(`删除 iCloud 别名失败:${err.message}`, 'error');
|
||||
} finally {
|
||||
setIcloudLoadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function setSingleIcloudAliasUsedState(alias, used) {
|
||||
setIcloudLoadingState(true, `正在更新 ${alias.email} 的使用状态...`);
|
||||
try {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'SET_ICLOUD_ALIAS_USED_STATE',
|
||||
source: 'sidepanel',
|
||||
payload: { email: alias.email, used },
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
helpers.showToast(`${alias.email} 已${used ? '标记为已用' : '恢复为未用'}`, 'success', 2200);
|
||||
await refreshIcloudAliases({ silent: true });
|
||||
} catch (err) {
|
||||
if (dom.icloudSummary) dom.icloudSummary.textContent = err.message;
|
||||
helpers.showToast(`更新 iCloud 使用状态失败:${err.message}`, 'error');
|
||||
} finally {
|
||||
setIcloudLoadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function setSingleIcloudAliasPreservedState(alias, preserved) {
|
||||
setIcloudLoadingState(true, `正在更新 ${alias.email} 的保留状态...`);
|
||||
try {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'SET_ICLOUD_ALIAS_PRESERVED_STATE',
|
||||
source: 'sidepanel',
|
||||
payload: { email: alias.email, preserved },
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
helpers.showToast(`${alias.email} 已${preserved ? '设为保留' : '取消保留'}`, 'success', 2200);
|
||||
await refreshIcloudAliases({ silent: true });
|
||||
} catch (err) {
|
||||
if (dom.icloudSummary) dom.icloudSummary.textContent = err.message;
|
||||
helpers.showToast(`更新 iCloud 保留状态失败:${err.message}`, 'error');
|
||||
} finally {
|
||||
setIcloudLoadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function runBulkIcloudAction(action) {
|
||||
const selectedAliases = renderedAliases.filter((alias) => selectedEmails.has(alias.email));
|
||||
if (!selectedAliases.length) {
|
||||
updateIcloudBulkUI();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'delete') {
|
||||
const confirmed = await helpers.openConfirmModal({
|
||||
title: '批量删除 iCloud 别名',
|
||||
message: `确认删除选中的 ${selectedAliases.length} 个 iCloud 别名吗?此操作不可撤销。`,
|
||||
confirmLabel: '确认删除',
|
||||
confirmVariant: 'btn-danger',
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const actionLabelMap = {
|
||||
used: '标记已用',
|
||||
unused: '标记未用',
|
||||
preserve: '保留',
|
||||
unpreserve: '取消保留',
|
||||
delete: '删除',
|
||||
};
|
||||
setIcloudLoadingState(true, `正在批量${actionLabelMap[action] || '处理'} iCloud 别名...`);
|
||||
|
||||
try {
|
||||
for (const alias of selectedAliases) {
|
||||
let response = null;
|
||||
if (action === 'used' || action === 'unused') {
|
||||
response = await runtime.sendMessage({
|
||||
type: 'SET_ICLOUD_ALIAS_USED_STATE',
|
||||
source: 'sidepanel',
|
||||
payload: { email: alias.email, used: action === 'used' },
|
||||
});
|
||||
} else if (action === 'preserve' || action === 'unpreserve') {
|
||||
response = await runtime.sendMessage({
|
||||
type: 'SET_ICLOUD_ALIAS_PRESERVED_STATE',
|
||||
source: 'sidepanel',
|
||||
payload: { email: alias.email, preserved: action === 'preserve' },
|
||||
});
|
||||
} else if (action === 'delete') {
|
||||
response = await runtime.sendMessage({
|
||||
type: 'DELETE_ICLOUD_ALIAS',
|
||||
source: 'sidepanel',
|
||||
payload: { email: alias.email, anonymousId: alias.anonymousId },
|
||||
});
|
||||
selectedEmails.delete(alias.email);
|
||||
}
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
}
|
||||
|
||||
helpers.showToast(`已批量${actionLabelMap[action] || '处理'} ${selectedAliases.length} 个 iCloud 别名`, 'success', 2400);
|
||||
await refreshIcloudAliases({ silent: true });
|
||||
} catch (err) {
|
||||
if (dom.icloudSummary) dom.icloudSummary.textContent = err.message;
|
||||
helpers.showToast(`批量处理 iCloud 别名失败:${err.message}`, 'error');
|
||||
} finally {
|
||||
setIcloudLoadingState(false);
|
||||
updateIcloudBulkUI();
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUsedIcloudAliases() {
|
||||
const confirmed = await helpers.openConfirmModal({
|
||||
title: '删除已用 iCloud 别名',
|
||||
message: '确认删除所有未保留的已用 iCloud 别名吗?此操作不可撤销。',
|
||||
confirmLabel: '确认删除',
|
||||
confirmVariant: 'btn-danger',
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIcloudLoadingState(true, '正在删除已用 iCloud 别名...');
|
||||
try {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'DELETE_USED_ICLOUD_ALIASES',
|
||||
source: 'sidepanel',
|
||||
payload: {},
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
const deleted = response?.deleted || [];
|
||||
const skipped = response?.skipped || [];
|
||||
helpers.showToast(`已删除 ${deleted.length} 个已用别名,跳过 ${skipped.length} 个`, skipped.length ? 'warn' : 'success', 2800);
|
||||
await refreshIcloudAliases({ silent: true });
|
||||
} catch (err) {
|
||||
if (dom.icloudSummary) dom.icloudSummary.textContent = err.message;
|
||||
helpers.showToast(`删除已用 iCloud 别名失败:${err.message}`, 'error');
|
||||
} finally {
|
||||
setIcloudLoadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
function isLikelyIcloudLoginRequiredMessage(message = '') {
|
||||
const lower = String(message || '').toLowerCase();
|
||||
return lower.includes('请先在新打开的 icloud 页面中完成登录')
|
||||
|| lower.includes('请先在当前浏览器登录')
|
||||
|| lower.includes('需要先登录')
|
||||
|| lower.includes('请先登录')
|
||||
|| lower.includes('please sign in')
|
||||
|| lower.includes('sign in required')
|
||||
|| lower.includes('not logged in')
|
||||
|| lower.includes('authentication required')
|
||||
|| lower.includes('unauthenticated');
|
||||
}
|
||||
|
||||
async function handleLoginDone() {
|
||||
if (dom.btnIcloudLoginDone) {
|
||||
dom.btnIcloudLoginDone.disabled = true;
|
||||
}
|
||||
try {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'CHECK_ICLOUD_SESSION',
|
||||
source: 'sidepanel',
|
||||
payload: {},
|
||||
});
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
hideIcloudLoginHelp();
|
||||
helpers.showToast('iCloud 会话已恢复,别名列表已刷新。', 'success', 2600);
|
||||
await refreshIcloudAliases({ silent: true });
|
||||
} catch (err) {
|
||||
const errorMessage = String(err?.message || '未知错误');
|
||||
if (isLikelyIcloudLoginRequiredMessage(errorMessage)) {
|
||||
helpers.showToast(`看起来还没有登录完成:${errorMessage}`, 'warn', 4200);
|
||||
return;
|
||||
}
|
||||
|
||||
await refreshIcloudAliases({ silent: true }).catch(() => { });
|
||||
helpers.showToast(`iCloud 会话校验失败(非登录态):${errorMessage}`, 'warn', 4200);
|
||||
} finally {
|
||||
if (dom.btnIcloudLoginDone) {
|
||||
dom.btnIcloudLoginDone.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
selectedEmails.clear();
|
||||
renderedAliases = [];
|
||||
searchTerm = '';
|
||||
filterMode = 'all';
|
||||
refreshQueued = false;
|
||||
if (dom.inputIcloudSearch) dom.inputIcloudSearch.value = '';
|
||||
if (dom.selectIcloudFilter) dom.selectIcloudFilter.value = 'all';
|
||||
if (dom.icloudList) dom.icloudList.innerHTML = '';
|
||||
if (dom.icloudSummary) dom.icloudSummary.textContent = '加载你的 iCloud Hide My Email 别名以便在这里管理。';
|
||||
updateIcloudBulkUI([]);
|
||||
hideIcloudLoginHelp();
|
||||
}
|
||||
|
||||
function hasDeletableUsedAliases() {
|
||||
return renderedAliases.some((alias) => alias.used && !alias.preserved);
|
||||
}
|
||||
|
||||
function bindIcloudEvents() {
|
||||
dom.btnIcloudRefresh?.addEventListener('click', async () => {
|
||||
await refreshIcloudAliases();
|
||||
});
|
||||
|
||||
dom.btnIcloudDeleteUsed?.addEventListener('click', async () => {
|
||||
await deleteUsedIcloudAliases();
|
||||
});
|
||||
|
||||
dom.inputIcloudSearch?.addEventListener('input', () => {
|
||||
searchTerm = dom.inputIcloudSearch.value || '';
|
||||
renderIcloudAliases(renderedAliases);
|
||||
});
|
||||
|
||||
dom.selectIcloudFilter?.addEventListener('change', () => {
|
||||
filterMode = dom.selectIcloudFilter.value || 'all';
|
||||
renderIcloudAliases(renderedAliases);
|
||||
});
|
||||
|
||||
dom.checkboxIcloudSelectAll?.addEventListener('change', () => {
|
||||
const visibleAliases = getFilteredIcloudAliases();
|
||||
if (dom.checkboxIcloudSelectAll.checked) {
|
||||
visibleAliases.forEach((alias) => selectedEmails.add(alias.email));
|
||||
} else {
|
||||
visibleAliases.forEach((alias) => selectedEmails.delete(alias.email));
|
||||
}
|
||||
renderIcloudAliases(renderedAliases);
|
||||
});
|
||||
|
||||
dom.btnIcloudBulkUsed?.addEventListener('click', async () => {
|
||||
await runBulkIcloudAction('used');
|
||||
});
|
||||
|
||||
dom.btnIcloudBulkUnused?.addEventListener('click', async () => {
|
||||
await runBulkIcloudAction('unused');
|
||||
});
|
||||
|
||||
dom.btnIcloudBulkPreserve?.addEventListener('click', async () => {
|
||||
await runBulkIcloudAction('preserve');
|
||||
});
|
||||
|
||||
dom.btnIcloudBulkUnpreserve?.addEventListener('click', async () => {
|
||||
await runBulkIcloudAction('unpreserve');
|
||||
});
|
||||
|
||||
dom.btnIcloudBulkDelete?.addEventListener('click', async () => {
|
||||
await runBulkIcloudAction('delete');
|
||||
});
|
||||
|
||||
dom.btnIcloudLoginDone?.addEventListener('click', handleLoginDone);
|
||||
}
|
||||
|
||||
return {
|
||||
bindIcloudEvents,
|
||||
hideIcloudLoginHelp,
|
||||
hasDeletableUsedAliases,
|
||||
queueIcloudAliasRefresh,
|
||||
refreshIcloudAliases,
|
||||
renderIcloudAliases,
|
||||
reset,
|
||||
showIcloudLoginHelp,
|
||||
updateIcloudBulkUI,
|
||||
};
|
||||
}
|
||||
|
||||
globalScope.SidepanelIcloudManager = {
|
||||
createIcloudManager,
|
||||
};
|
||||
})(window);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
// sidepanel/ip-proxy-provider-711proxy.js — 711Proxy 面板专属逻辑
|
||||
function normalizeIpProxyCountryCode(value = '') {
|
||||
const raw = String(value || '').trim().toUpperCase().replace(/[^A-Z]/g, '');
|
||||
return /^[A-Z]{2}$/.test(raw) ? raw : '';
|
||||
}
|
||||
|
||||
function infer711RegionFromHost(host = '') {
|
||||
const text = String(host || '').trim().toLowerCase().replace(/\.$/, '');
|
||||
if (!text || !text.includes('.')) {
|
||||
return '';
|
||||
}
|
||||
const firstLabel = String(text.split('.')[0] || '').trim();
|
||||
return /^[a-z]{2}$/.test(firstLabel) ? firstLabel.toUpperCase() : '';
|
||||
}
|
||||
|
||||
function infer711RegionFromUsername(username = '') {
|
||||
const text = String(username || '').trim();
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
const match = text.match(/(?:^|[-_])region[-_:]?([A-Za-z]{2})\b/i);
|
||||
return normalizeIpProxyCountryCode(match ? match[1] : '');
|
||||
}
|
||||
|
||||
function resolve711ProxyRegionFromInputs({ host = '', username = '', region = '' } = {}) {
|
||||
const fromUsername = infer711RegionFromUsername(username);
|
||||
if (fromUsername) {
|
||||
return fromUsername;
|
||||
}
|
||||
const fromHost = infer711RegionFromHost(host);
|
||||
if (fromHost) {
|
||||
return fromHost;
|
||||
}
|
||||
return normalizeIpProxyCountryCode(region);
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
(function attachSidepanelLuckmailManager(globalScope) {
|
||||
function createLuckmailManager(context = {}) {
|
||||
const {
|
||||
dom,
|
||||
helpers,
|
||||
runtime,
|
||||
constants = {},
|
||||
} = context;
|
||||
|
||||
const copyIcon = constants.copyIcon || '';
|
||||
|
||||
let renderedPurchases = [];
|
||||
let selectedPurchaseIds = new Set();
|
||||
let searchTerm = '';
|
||||
let filterMode = 'all';
|
||||
let refreshQueued = false;
|
||||
|
||||
function normalizeLuckmailSearchText(value) {
|
||||
return String(value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function getFilteredLuckmailPurchases(purchases = renderedPurchases) {
|
||||
const normalizedSearchTerm = normalizeLuckmailSearchText(searchTerm);
|
||||
return (Array.isArray(purchases) ? purchases : []).filter((purchase) => {
|
||||
const matchesFilter = (() => {
|
||||
switch (filterMode) {
|
||||
case 'reusable': return Boolean(purchase.reusable);
|
||||
case 'used': return Boolean(purchase.used);
|
||||
case 'unused': return !purchase.used;
|
||||
case 'preserved': return Boolean(purchase.preserved);
|
||||
case 'disabled': return Boolean(purchase.disabled);
|
||||
default: return true;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!matchesFilter) return false;
|
||||
if (!normalizedSearchTerm) return true;
|
||||
|
||||
const haystack = [
|
||||
purchase.email_address,
|
||||
purchase.project_name,
|
||||
purchase.tag_name,
|
||||
purchase.used ? '已用 used' : '未用 unused',
|
||||
purchase.preserved ? '保留 preserved' : '',
|
||||
purchase.disabled ? '已禁用 disabled' : '',
|
||||
purchase.reusable ? '可复用 reusable' : '',
|
||||
].join(' ').toLowerCase();
|
||||
|
||||
return haystack.includes(normalizedSearchTerm);
|
||||
});
|
||||
}
|
||||
|
||||
function pruneLuckmailSelection(purchases = renderedPurchases) {
|
||||
const existingIds = new Set((Array.isArray(purchases) ? purchases : []).map((purchase) => String(purchase.id)));
|
||||
selectedPurchaseIds = new Set([...selectedPurchaseIds].filter((id) => existingIds.has(id)));
|
||||
}
|
||||
|
||||
function updateLuckmailBulkUI(visiblePurchases = getFilteredLuckmailPurchases()) {
|
||||
if (!dom.checkboxLuckmailSelectAll || !dom.luckmailSelectionSummary) {
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleIds = visiblePurchases.map((purchase) => String(purchase.id));
|
||||
const selectedVisibleCount = visibleIds.filter((id) => selectedPurchaseIds.has(id)).length;
|
||||
const hasVisible = visibleIds.length > 0;
|
||||
const hasSelection = selectedPurchaseIds.size > 0;
|
||||
|
||||
dom.checkboxLuckmailSelectAll.checked = hasVisible && selectedVisibleCount === visibleIds.length;
|
||||
dom.checkboxLuckmailSelectAll.indeterminate = selectedVisibleCount > 0 && selectedVisibleCount < visibleIds.length;
|
||||
dom.checkboxLuckmailSelectAll.disabled = !hasVisible;
|
||||
dom.luckmailSelectionSummary.textContent = `已选 ${selectedPurchaseIds.size} 个(当前显示 ${visibleIds.length} 个)`;
|
||||
|
||||
if (dom.btnLuckmailBulkUsed) dom.btnLuckmailBulkUsed.disabled = !hasSelection;
|
||||
if (dom.btnLuckmailBulkUnused) dom.btnLuckmailBulkUnused.disabled = !hasSelection;
|
||||
if (dom.btnLuckmailBulkPreserve) dom.btnLuckmailBulkPreserve.disabled = !hasSelection;
|
||||
if (dom.btnLuckmailBulkUnpreserve) dom.btnLuckmailBulkUnpreserve.disabled = !hasSelection;
|
||||
if (dom.btnLuckmailBulkDisable) dom.btnLuckmailBulkDisable.disabled = !hasSelection;
|
||||
if (dom.btnLuckmailBulkEnable) dom.btnLuckmailBulkEnable.disabled = !hasSelection;
|
||||
}
|
||||
|
||||
function setLuckmailLoadingState(loading, summary = '') {
|
||||
if (dom.btnLuckmailRefresh) dom.btnLuckmailRefresh.disabled = loading;
|
||||
if (dom.btnLuckmailDisableUsed) dom.btnLuckmailDisableUsed.disabled = loading;
|
||||
if (dom.inputLuckmailSearch) dom.inputLuckmailSearch.disabled = loading;
|
||||
if (dom.selectLuckmailFilter) dom.selectLuckmailFilter.disabled = loading;
|
||||
if (dom.checkboxLuckmailSelectAll) dom.checkboxLuckmailSelectAll.disabled = loading || getFilteredLuckmailPurchases().length === 0;
|
||||
if (dom.btnLuckmailBulkUsed) dom.btnLuckmailBulkUsed.disabled = loading || selectedPurchaseIds.size === 0;
|
||||
if (dom.btnLuckmailBulkUnused) dom.btnLuckmailBulkUnused.disabled = loading || selectedPurchaseIds.size === 0;
|
||||
if (dom.btnLuckmailBulkPreserve) dom.btnLuckmailBulkPreserve.disabled = loading || selectedPurchaseIds.size === 0;
|
||||
if (dom.btnLuckmailBulkUnpreserve) dom.btnLuckmailBulkUnpreserve.disabled = loading || selectedPurchaseIds.size === 0;
|
||||
if (dom.btnLuckmailBulkDisable) dom.btnLuckmailBulkDisable.disabled = loading || selectedPurchaseIds.size === 0;
|
||||
if (dom.btnLuckmailBulkEnable) dom.btnLuckmailBulkEnable.disabled = loading || selectedPurchaseIds.size === 0;
|
||||
if (summary && dom.luckmailSummary) {
|
||||
dom.luckmailSummary.textContent = summary;
|
||||
}
|
||||
}
|
||||
|
||||
function renderLuckmailPurchases(purchases = renderedPurchases) {
|
||||
if (!dom.luckmailList || !dom.luckmailSummary) return;
|
||||
|
||||
renderedPurchases = Array.isArray(purchases) ? purchases : [];
|
||||
pruneLuckmailSelection(renderedPurchases);
|
||||
dom.luckmailList.innerHTML = '';
|
||||
|
||||
if (!renderedPurchases.length) {
|
||||
selectedPurchaseIds.clear();
|
||||
dom.luckmailList.innerHTML = '<div class="luckmail-empty">未找到 openai 项目的 LuckMail 邮箱。</div>';
|
||||
dom.luckmailSummary.textContent = '加载已购邮箱后可在这里管理 openai 项目的 LuckMail 邮箱。';
|
||||
if (dom.btnLuckmailDisableUsed) dom.btnLuckmailDisableUsed.disabled = true;
|
||||
updateLuckmailBulkUI([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const usedCount = renderedPurchases.filter((purchase) => purchase.used).length;
|
||||
const reusableCount = renderedPurchases.filter((purchase) => purchase.reusable).length;
|
||||
const disableUsedCount = renderedPurchases.filter((purchase) => purchase.used && !purchase.preserved && !purchase.disabled).length;
|
||||
dom.luckmailSummary.textContent = `已加载 ${renderedPurchases.length} 个 openai 邮箱,其中 ${reusableCount} 个可复用,${usedCount} 个已本地标记为已用。`;
|
||||
if (dom.btnLuckmailDisableUsed) {
|
||||
dom.btnLuckmailDisableUsed.textContent = `禁用已用${disableUsedCount > 0 ? `(${disableUsedCount})` : ''}`;
|
||||
dom.btnLuckmailDisableUsed.disabled = disableUsedCount === 0;
|
||||
}
|
||||
|
||||
const visiblePurchases = getFilteredLuckmailPurchases(renderedPurchases);
|
||||
if (!visiblePurchases.length) {
|
||||
dom.luckmailList.innerHTML = '<div class="luckmail-empty">没有匹配当前筛选条件的 LuckMail 邮箱。</div>';
|
||||
updateLuckmailBulkUI([]);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const purchase of visiblePurchases) {
|
||||
const purchaseId = String(purchase.id);
|
||||
const item = document.createElement('div');
|
||||
item.className = `luckmail-item${purchase.current ? ' is-current' : ''}`;
|
||||
item.innerHTML = `
|
||||
<input class="luckmail-item-check" type="checkbox" data-action="select" ${selectedPurchaseIds.has(purchaseId) ? 'checked' : ''} />
|
||||
<div class="luckmail-item-main">
|
||||
<div class="luckmail-item-email-row">
|
||||
<div class="luckmail-item-email">${helpers.escapeHtml(purchase.email_address || '(未知邮箱)')}</div>
|
||||
<button
|
||||
class="hotmail-copy-btn"
|
||||
type="button"
|
||||
data-action="copy-email"
|
||||
title="复制邮箱"
|
||||
aria-label="复制邮箱 ${helpers.escapeHtml(purchase.email_address || '')}"
|
||||
>${copyIcon}</button>
|
||||
</div>
|
||||
<div class="luckmail-item-meta">
|
||||
<span class="luckmail-tag">${helpers.escapeHtml(helpers.normalizeLuckmailProjectName(purchase.project_name) || 'openai')}</span>
|
||||
${purchase.reusable ? '<span class="luckmail-tag active">可复用</span>' : ''}
|
||||
${purchase.current ? '<span class="luckmail-tag current">当前</span>' : ''}
|
||||
${purchase.used ? '<span class="luckmail-tag used">已用</span>' : ''}
|
||||
${purchase.preserved ? '<span class="luckmail-tag">保留</span>' : ''}
|
||||
${purchase.disabled ? '<span class="luckmail-tag disabled">已禁用</span>' : ''}
|
||||
${purchase.tag_name && normalizeLuckmailSearchText(purchase.tag_name) !== normalizeLuckmailSearchText(helpers.getLuckmailPreserveTagName())
|
||||
? `<span class="luckmail-tag">${helpers.escapeHtml(purchase.tag_name)}</span>`
|
||||
: ''}
|
||||
</div>
|
||||
<div class="luckmail-item-details">
|
||||
<span>ID:${helpers.escapeHtml(String(purchase.id || ''))}</span>
|
||||
<span>保修至:${helpers.escapeHtml(helpers.formatLuckmailDateTime(purchase.warranty_until))}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="luckmail-item-actions">
|
||||
<button class="btn btn-outline btn-xs" type="button" data-action="use">使用此邮箱</button>
|
||||
<button class="btn btn-outline btn-xs" type="button" data-action="toggle-used">${helpers.escapeHtml(purchase.used ? '标记未用' : '标记已用')}</button>
|
||||
<button class="btn btn-outline btn-xs" type="button" data-action="toggle-preserved">${helpers.escapeHtml(purchase.preserved ? '取消保留' : '保留')}</button>
|
||||
<button class="btn btn-outline btn-xs" type="button" data-action="toggle-disabled">${helpers.escapeHtml(purchase.disabled ? '启用' : '禁用')}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
item.querySelector('[data-action="select"]').addEventListener('change', (event) => {
|
||||
if (event.target.checked) {
|
||||
selectedPurchaseIds.add(purchaseId);
|
||||
} else {
|
||||
selectedPurchaseIds.delete(purchaseId);
|
||||
}
|
||||
updateLuckmailBulkUI(visiblePurchases);
|
||||
});
|
||||
item.querySelector('[data-action="copy-email"]').addEventListener('click', async () => {
|
||||
await helpers.copyTextToClipboard(purchase.email_address || '');
|
||||
helpers.showToast('邮箱已复制', 'success', 1600);
|
||||
});
|
||||
item.querySelector('[data-action="use"]').addEventListener('click', async () => {
|
||||
await selectSingleLuckmailPurchase(purchase);
|
||||
});
|
||||
item.querySelector('[data-action="toggle-used"]').addEventListener('click', async () => {
|
||||
await setSingleLuckmailPurchaseUsedState(purchase, !purchase.used);
|
||||
});
|
||||
item.querySelector('[data-action="toggle-preserved"]').addEventListener('click', async () => {
|
||||
await setSingleLuckmailPurchasePreservedState(purchase, !purchase.preserved);
|
||||
});
|
||||
item.querySelector('[data-action="toggle-disabled"]').addEventListener('click', async () => {
|
||||
await setSingleLuckmailPurchaseDisabledState(purchase, !purchase.disabled);
|
||||
});
|
||||
dom.luckmailList.appendChild(item);
|
||||
}
|
||||
|
||||
updateLuckmailBulkUI(visiblePurchases);
|
||||
}
|
||||
|
||||
async function refreshLuckmailPurchases(options = {}) {
|
||||
const { silent = false } = options;
|
||||
if (!dom.luckmailSection || dom.luckmailSection.style.display === 'none') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!silent) setLuckmailLoadingState(true, '正在加载 LuckMail openai 邮箱...');
|
||||
try {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'LIST_LUCKMAIL_PURCHASES',
|
||||
source: 'sidepanel',
|
||||
payload: {},
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
renderLuckmailPurchases(response?.purchases || []);
|
||||
} catch (err) {
|
||||
selectedPurchaseIds.clear();
|
||||
if (dom.luckmailList) {
|
||||
dom.luckmailList.innerHTML = '<div class="luckmail-empty">无法加载 LuckMail 邮箱列表。</div>';
|
||||
}
|
||||
if (dom.luckmailSummary) {
|
||||
dom.luckmailSummary.textContent = err.message;
|
||||
}
|
||||
updateLuckmailBulkUI([]);
|
||||
if (!silent) {
|
||||
helpers.showToast(`LuckMail 邮箱列表加载失败:${err.message}`, 'error');
|
||||
}
|
||||
} finally {
|
||||
setLuckmailLoadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
function queueLuckmailPurchaseRefresh() {
|
||||
if (refreshQueued) return;
|
||||
refreshQueued = true;
|
||||
setTimeout(async () => {
|
||||
refreshQueued = false;
|
||||
await refreshLuckmailPurchases({ silent: true });
|
||||
}, 150);
|
||||
}
|
||||
|
||||
async function selectSingleLuckmailPurchase(purchase) {
|
||||
setLuckmailLoadingState(true, `正在切换到 ${purchase.email_address} ...`);
|
||||
try {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'SELECT_LUCKMAIL_PURCHASE',
|
||||
source: 'sidepanel',
|
||||
payload: { purchaseId: purchase.id },
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
dom.inputEmail.value = response?.purchase?.email_address || purchase.email_address || '';
|
||||
helpers.showToast(`已切换当前 LuckMail 邮箱为 ${purchase.email_address}`, 'success', 2200);
|
||||
await refreshLuckmailPurchases({ silent: true });
|
||||
} catch (err) {
|
||||
if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
|
||||
helpers.showToast(`切换 LuckMail 邮箱失败:${err.message}`, 'error');
|
||||
} finally {
|
||||
setLuckmailLoadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function setSingleLuckmailPurchaseUsedState(purchase, used) {
|
||||
setLuckmailLoadingState(true, `正在更新 ${purchase.email_address} 的已用状态...`);
|
||||
try {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'SET_LUCKMAIL_PURCHASE_USED_STATE',
|
||||
source: 'sidepanel',
|
||||
payload: { purchaseId: purchase.id, used },
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
helpers.showToast(`${purchase.email_address} 已${used ? '标记为已用' : '恢复为未用'}`, 'success', 2200);
|
||||
await refreshLuckmailPurchases({ silent: true });
|
||||
} catch (err) {
|
||||
if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
|
||||
helpers.showToast(`更新 LuckMail 已用状态失败:${err.message}`, 'error');
|
||||
} finally {
|
||||
setLuckmailLoadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function setSingleLuckmailPurchasePreservedState(purchase, preserved) {
|
||||
setLuckmailLoadingState(true, `正在更新 ${purchase.email_address} 的保留状态...`);
|
||||
try {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'SET_LUCKMAIL_PURCHASE_PRESERVED_STATE',
|
||||
source: 'sidepanel',
|
||||
payload: { purchaseId: purchase.id, preserved },
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
helpers.showToast(`${purchase.email_address} 已${preserved ? '设为保留' : '取消保留'}`, 'success', 2200);
|
||||
await refreshLuckmailPurchases({ silent: true });
|
||||
} catch (err) {
|
||||
if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
|
||||
helpers.showToast(`更新 LuckMail 保留状态失败:${err.message}`, 'error');
|
||||
} finally {
|
||||
setLuckmailLoadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function setSingleLuckmailPurchaseDisabledState(purchase, disabled) {
|
||||
setLuckmailLoadingState(true, `正在${disabled ? '禁用' : '启用'} ${purchase.email_address} ...`);
|
||||
try {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'SET_LUCKMAIL_PURCHASE_DISABLED_STATE',
|
||||
source: 'sidepanel',
|
||||
payload: { purchaseId: purchase.id, disabled },
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
helpers.showToast(`${purchase.email_address} 已${disabled ? '禁用' : '启用'}`, 'success', 2200);
|
||||
await refreshLuckmailPurchases({ silent: true });
|
||||
} catch (err) {
|
||||
if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
|
||||
helpers.showToast(`更新 LuckMail 禁用状态失败:${err.message}`, 'error');
|
||||
} finally {
|
||||
setLuckmailLoadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function runBulkLuckmailAction(action) {
|
||||
const selectedIds = renderedPurchases
|
||||
.filter((purchase) => selectedPurchaseIds.has(String(purchase.id)))
|
||||
.map((purchase) => purchase.id);
|
||||
if (!selectedIds.length) {
|
||||
updateLuckmailBulkUI();
|
||||
return;
|
||||
}
|
||||
|
||||
const actionLabelMap = {
|
||||
used: '标记已用',
|
||||
unused: '标记未用',
|
||||
preserve: '保留',
|
||||
unpreserve: '取消保留',
|
||||
disable: '禁用',
|
||||
enable: '启用',
|
||||
};
|
||||
|
||||
setLuckmailLoadingState(true, `正在批量${actionLabelMap[action] || '处理'} LuckMail 邮箱...`);
|
||||
try {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'BATCH_UPDATE_LUCKMAIL_PURCHASES',
|
||||
source: 'sidepanel',
|
||||
payload: { action, ids: selectedIds },
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
helpers.showToast(`已批量${actionLabelMap[action] || '处理'} ${selectedIds.length} 个 LuckMail 邮箱`, 'success', 2400);
|
||||
await refreshLuckmailPurchases({ silent: true });
|
||||
} catch (err) {
|
||||
if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
|
||||
helpers.showToast(`批量处理 LuckMail 邮箱失败:${err.message}`, 'error');
|
||||
} finally {
|
||||
setLuckmailLoadingState(false);
|
||||
updateLuckmailBulkUI();
|
||||
}
|
||||
}
|
||||
|
||||
async function disableUsedLuckmailPurchases() {
|
||||
const confirmed = await helpers.openConfirmModal({
|
||||
title: '禁用已用 LuckMail 邮箱',
|
||||
message: '确认禁用所有本地已用且未保留的 openai LuckMail 邮箱吗?',
|
||||
confirmLabel: '确认禁用',
|
||||
confirmVariant: 'btn-danger',
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLuckmailLoadingState(true, '正在禁用已用 LuckMail 邮箱...');
|
||||
try {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'DISABLE_USED_LUCKMAIL_PURCHASES',
|
||||
source: 'sidepanel',
|
||||
payload: {},
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
const disabledCount = Array.isArray(response?.disabledIds) ? response.disabledIds.length : 0;
|
||||
helpers.showToast(`已禁用 ${disabledCount} 个 LuckMail 邮箱`, disabledCount > 0 ? 'success' : 'info', 2400);
|
||||
await refreshLuckmailPurchases({ silent: true });
|
||||
} catch (err) {
|
||||
if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
|
||||
helpers.showToast(`禁用已用 LuckMail 邮箱失败:${err.message}`, 'error');
|
||||
} finally {
|
||||
setLuckmailLoadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
renderedPurchases = [];
|
||||
selectedPurchaseIds.clear();
|
||||
searchTerm = '';
|
||||
filterMode = 'all';
|
||||
refreshQueued = false;
|
||||
if (dom.inputLuckmailSearch) dom.inputLuckmailSearch.value = '';
|
||||
if (dom.selectLuckmailFilter) dom.selectLuckmailFilter.value = 'all';
|
||||
if (dom.luckmailList) dom.luckmailList.innerHTML = '';
|
||||
if (dom.luckmailSummary) dom.luckmailSummary.textContent = '加载已购邮箱后可在这里管理 openai 项目的 LuckMail 邮箱。';
|
||||
if (dom.btnLuckmailDisableUsed) dom.btnLuckmailDisableUsed.disabled = true;
|
||||
updateLuckmailBulkUI([]);
|
||||
}
|
||||
|
||||
function bindLuckmailEvents() {
|
||||
dom.inputLuckmailSearch?.addEventListener('input', (event) => {
|
||||
searchTerm = event.target.value || '';
|
||||
renderLuckmailPurchases(renderedPurchases);
|
||||
});
|
||||
|
||||
dom.selectLuckmailFilter?.addEventListener('change', (event) => {
|
||||
filterMode = String(event.target.value || 'all').trim() || 'all';
|
||||
renderLuckmailPurchases(renderedPurchases);
|
||||
});
|
||||
|
||||
dom.checkboxLuckmailSelectAll?.addEventListener('change', () => {
|
||||
const visiblePurchases = getFilteredLuckmailPurchases();
|
||||
if (dom.checkboxLuckmailSelectAll.checked) {
|
||||
visiblePurchases.forEach((purchase) => selectedPurchaseIds.add(String(purchase.id)));
|
||||
} else {
|
||||
visiblePurchases.forEach((purchase) => selectedPurchaseIds.delete(String(purchase.id)));
|
||||
}
|
||||
renderLuckmailPurchases(renderedPurchases);
|
||||
});
|
||||
|
||||
dom.btnLuckmailRefresh?.addEventListener('click', async () => {
|
||||
await refreshLuckmailPurchases();
|
||||
});
|
||||
|
||||
dom.btnLuckmailDisableUsed?.addEventListener('click', async () => {
|
||||
await disableUsedLuckmailPurchases();
|
||||
});
|
||||
|
||||
dom.btnLuckmailBulkUsed?.addEventListener('click', async () => {
|
||||
await runBulkLuckmailAction('used');
|
||||
});
|
||||
|
||||
dom.btnLuckmailBulkUnused?.addEventListener('click', async () => {
|
||||
await runBulkLuckmailAction('unused');
|
||||
});
|
||||
|
||||
dom.btnLuckmailBulkPreserve?.addEventListener('click', async () => {
|
||||
await runBulkLuckmailAction('preserve');
|
||||
});
|
||||
|
||||
dom.btnLuckmailBulkUnpreserve?.addEventListener('click', async () => {
|
||||
await runBulkLuckmailAction('unpreserve');
|
||||
});
|
||||
|
||||
dom.btnLuckmailBulkDisable?.addEventListener('click', async () => {
|
||||
await runBulkLuckmailAction('disable');
|
||||
});
|
||||
|
||||
dom.btnLuckmailBulkEnable?.addEventListener('click', async () => {
|
||||
await runBulkLuckmailAction('enable');
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
bindLuckmailEvents,
|
||||
disableUsedLuckmailPurchases,
|
||||
queueLuckmailPurchaseRefresh,
|
||||
refreshLuckmailPurchases,
|
||||
renderLuckmailPurchases,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
globalScope.SidepanelLuckmailManager = {
|
||||
createLuckmailManager,
|
||||
};
|
||||
})(window);
|
||||
@@ -0,0 +1,596 @@
|
||||
(function attachSidepanelMail2925Manager(globalScope) {
|
||||
function createMail2925Manager(context = {}) {
|
||||
const {
|
||||
state,
|
||||
dom,
|
||||
helpers,
|
||||
runtime,
|
||||
constants = {},
|
||||
mail2925Utils = {},
|
||||
} = context;
|
||||
|
||||
const expandedStorageKey = constants.expandedStorageKey || 'multipage-mail2925-list-expanded';
|
||||
const displayTimeZone = constants.displayTimeZone || 'Asia/Shanghai';
|
||||
const copyIcon = constants.copyIcon || '';
|
||||
const createAccountPoolFormController = globalScope.SidepanelAccountPoolUi?.createAccountPoolFormController;
|
||||
|
||||
let actionInFlight = false;
|
||||
let listExpanded = false;
|
||||
let editingAccountId = '';
|
||||
let searchTerm = '';
|
||||
let filterMode = 'all';
|
||||
|
||||
function getMail2925Accounts(currentState = state.getLatestState()) {
|
||||
return helpers.getMail2925Accounts(currentState);
|
||||
}
|
||||
|
||||
function getCurrentMail2925AccountId(currentState = state.getLatestState()) {
|
||||
return String(currentState?.currentMail2925AccountId || '');
|
||||
}
|
||||
|
||||
function updateMail2925ListViewport() {
|
||||
const count = getMail2925Accounts().length;
|
||||
if (dom.btnDeleteAllMail2925Accounts) {
|
||||
dom.btnDeleteAllMail2925Accounts.textContent = `全部删除${count > 0 ? `(${count})` : ''}`;
|
||||
dom.btnDeleteAllMail2925Accounts.disabled = count === 0;
|
||||
}
|
||||
if (dom.btnToggleMail2925List) {
|
||||
const label = typeof mail2925Utils.getMail2925ListToggleLabel === 'function'
|
||||
? mail2925Utils.getMail2925ListToggleLabel(listExpanded, count)
|
||||
: `${listExpanded ? '收起列表' : '展开列表'}${count > 0 ? `(${count})` : ''}`;
|
||||
dom.btnToggleMail2925List.textContent = label;
|
||||
dom.btnToggleMail2925List.setAttribute('aria-expanded', String(listExpanded));
|
||||
dom.btnToggleMail2925List.disabled = count === 0;
|
||||
}
|
||||
if (dom.mail2925ListShell) {
|
||||
dom.mail2925ListShell.classList.toggle('is-expanded', listExpanded);
|
||||
dom.mail2925ListShell.classList.toggle('is-collapsed', !listExpanded);
|
||||
}
|
||||
}
|
||||
|
||||
function setMail2925ListExpanded(expanded, options = {}) {
|
||||
const { persist = true } = options;
|
||||
listExpanded = Boolean(expanded);
|
||||
updateMail2925ListViewport();
|
||||
if (persist) {
|
||||
localStorage.setItem(expandedStorageKey, listExpanded ? '1' : '0');
|
||||
}
|
||||
}
|
||||
|
||||
function initMail2925ListExpandedState() {
|
||||
const saved = localStorage.getItem(expandedStorageKey);
|
||||
setMail2925ListExpanded(saved === '1', { persist: false });
|
||||
}
|
||||
|
||||
function formatDateTime(timestamp) {
|
||||
const value = Number(timestamp);
|
||||
if (!Number.isFinite(value) || value <= 0) {
|
||||
return '未记录';
|
||||
}
|
||||
return new Date(value).toLocaleString('zh-CN', {
|
||||
hour12: false,
|
||||
timeZone: displayTimeZone,
|
||||
});
|
||||
}
|
||||
|
||||
function getStatusSnapshot(account) {
|
||||
const status = typeof mail2925Utils.getMail2925AccountStatus === 'function'
|
||||
? mail2925Utils.getMail2925AccountStatus(account, Date.now())
|
||||
: 'ready';
|
||||
switch (status) {
|
||||
case 'cooldown':
|
||||
return { label: '冷却中', className: 'status-used' };
|
||||
case 'disabled':
|
||||
return { label: '已禁用', className: 'status-disabled' };
|
||||
case 'error':
|
||||
return { label: '异常', className: 'status-error' };
|
||||
case 'pending':
|
||||
return { label: '待完善', className: 'status-pending' };
|
||||
default:
|
||||
return { label: '可用', className: 'status-authorized' };
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSearchText(value = '') {
|
||||
return String(value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function getStatusKey(account) {
|
||||
return typeof mail2925Utils.getMail2925AccountStatus === 'function'
|
||||
? mail2925Utils.getMail2925AccountStatus(account, Date.now())
|
||||
: 'ready';
|
||||
}
|
||||
|
||||
function getFilteredMail2925Accounts(accounts, currentId = '') {
|
||||
const normalizedSearchTerm = normalizeSearchText(searchTerm);
|
||||
return accounts.filter((account) => {
|
||||
const statusKey = getStatusKey(account);
|
||||
const status = getStatusSnapshot(account);
|
||||
const isCurrent = Boolean(currentId) && account.id === currentId;
|
||||
const matchesFilter = (() => {
|
||||
switch (filterMode) {
|
||||
case 'current': return isCurrent;
|
||||
case 'ready': return statusKey === 'ready';
|
||||
case 'cooldown': return statusKey === 'cooldown';
|
||||
case 'disabled': return statusKey === 'disabled';
|
||||
case 'error': return statusKey === 'error';
|
||||
default: return true;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!matchesFilter) return false;
|
||||
if (!normalizedSearchTerm) return true;
|
||||
|
||||
const haystack = [
|
||||
account.email,
|
||||
statusKey,
|
||||
status.label,
|
||||
isCurrent ? 'current 当前' : '',
|
||||
].join(' ').toLowerCase();
|
||||
|
||||
return haystack.includes(normalizedSearchTerm);
|
||||
});
|
||||
}
|
||||
|
||||
function refreshManagedAliasBaseEmail() {
|
||||
if (typeof helpers.refreshManagedAliasBaseEmail === 'function') {
|
||||
helpers.refreshManagedAliasBaseEmail();
|
||||
}
|
||||
}
|
||||
|
||||
function applyMail2925AccountMutation(account) {
|
||||
if (!account?.id) return;
|
||||
const latestState = state.getLatestState();
|
||||
const currentId = getCurrentMail2925AccountId(latestState);
|
||||
const nextAccounts = typeof mail2925Utils.upsertMail2925AccountInList === 'function'
|
||||
? mail2925Utils.upsertMail2925AccountInList(getMail2925Accounts(latestState), account)
|
||||
: getMail2925Accounts(latestState).map((item) => (item.id === account.id ? account : item));
|
||||
|
||||
const nextState = {
|
||||
mail2925Accounts: nextAccounts,
|
||||
};
|
||||
if (currentId === account.id && account.enabled === false) {
|
||||
nextState.currentMail2925AccountId = null;
|
||||
}
|
||||
state.syncLatestState(nextState);
|
||||
refreshManagedAliasBaseEmail();
|
||||
renderMail2925Accounts();
|
||||
}
|
||||
|
||||
function clearMail2925Form() {
|
||||
if (dom.inputMail2925Email) dom.inputMail2925Email.value = '';
|
||||
if (dom.inputMail2925Password) dom.inputMail2925Password.value = '';
|
||||
}
|
||||
|
||||
const formController = typeof createAccountPoolFormController === 'function'
|
||||
? createAccountPoolFormController({
|
||||
formShell: dom.mail2925FormShell,
|
||||
toggleButton: dom.btnToggleMail2925Form,
|
||||
hiddenLabel: '添加账号',
|
||||
visibleLabel: '取消添加',
|
||||
onClear: () => {
|
||||
stopEditingAccount({ clearForm: true });
|
||||
},
|
||||
onFocus: () => {
|
||||
dom.inputMail2925Email?.focus?.();
|
||||
},
|
||||
})
|
||||
: {
|
||||
isVisible: () => false,
|
||||
setVisible() {},
|
||||
sync() {},
|
||||
};
|
||||
|
||||
function syncEditUi() {
|
||||
if (dom.btnAddMail2925Account) {
|
||||
dom.btnAddMail2925Account.textContent = editingAccountId ? '保存修改' : '添加账号';
|
||||
}
|
||||
}
|
||||
|
||||
function startEditingAccount(account) {
|
||||
if (!account?.id) return;
|
||||
editingAccountId = account.id;
|
||||
if (dom.inputMail2925Email) dom.inputMail2925Email.value = String(account.email || '').trim();
|
||||
if (dom.inputMail2925Password) dom.inputMail2925Password.value = String(account.password || '');
|
||||
formController.setVisible(true, { focusField: false });
|
||||
syncEditUi();
|
||||
}
|
||||
|
||||
function stopEditingAccount(options = {}) {
|
||||
const { clearForm = true } = options;
|
||||
editingAccountId = '';
|
||||
if (clearForm) {
|
||||
clearMail2925Form();
|
||||
}
|
||||
syncEditUi();
|
||||
}
|
||||
|
||||
function renderMail2925Accounts() {
|
||||
if (!dom.mail2925AccountsList) return;
|
||||
|
||||
const latestState = state.getLatestState();
|
||||
const accounts = getMail2925Accounts(latestState);
|
||||
const currentId = getCurrentMail2925AccountId(latestState);
|
||||
|
||||
if (!accounts.length) {
|
||||
dom.mail2925AccountsList.innerHTML = '<div class="hotmail-empty">还没有 2925 账号,先添加一条再使用。</div>';
|
||||
updateMail2925ListViewport();
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleAccounts = getFilteredMail2925Accounts(accounts, currentId);
|
||||
if (!visibleAccounts.length) {
|
||||
dom.mail2925AccountsList.innerHTML = '<div class="hotmail-empty">没有匹配当前筛选条件的 2925 账号。</div>';
|
||||
updateMail2925ListViewport();
|
||||
return;
|
||||
}
|
||||
|
||||
dom.mail2925AccountsList.innerHTML = visibleAccounts.map((account) => {
|
||||
const status = getStatusSnapshot(account);
|
||||
const coolingDown = status.label === '冷却中';
|
||||
return `
|
||||
<div class="hotmail-account-item${account.id === currentId ? ' is-current' : ''}">
|
||||
<div class="hotmail-account-top">
|
||||
<div class="hotmail-account-title-row">
|
||||
<div class="hotmail-account-email">${helpers.escapeHtml(account.email || '(未命名账号)')}</div>
|
||||
<button
|
||||
class="hotmail-copy-btn"
|
||||
type="button"
|
||||
data-account-action="copy-email"
|
||||
data-account-id="${helpers.escapeHtml(account.id)}"
|
||||
title="复制邮箱"
|
||||
aria-label="复制邮箱 ${helpers.escapeHtml(account.email || '')}"
|
||||
>${copyIcon}</button>
|
||||
</div>
|
||||
<span class="hotmail-status-chip ${helpers.escapeHtml(status.className)}">${helpers.escapeHtml(status.label)}</span>
|
||||
</div>
|
||||
<div class="hotmail-account-meta">
|
||||
<span>密码:${account.password ? '已保存' : '未保存'}</span>
|
||||
<span>上次登录:${helpers.escapeHtml(formatDateTime(account.lastLoginAt))}</span>
|
||||
<span>上次使用:${helpers.escapeHtml(formatDateTime(account.lastUsedAt))}</span>
|
||||
<span>上限记录:${helpers.escapeHtml(formatDateTime(account.lastLimitAt))}</span>
|
||||
<span>恢复时间:${helpers.escapeHtml(formatDateTime(account.disabledUntil))}</span>
|
||||
</div>
|
||||
${account.lastError ? `<div class="hotmail-account-error">${helpers.escapeHtml(account.lastError)}</div>` : ''}
|
||||
<div class="hotmail-account-actions">
|
||||
<button class="btn btn-outline btn-sm" type="button" data-account-action="select" data-account-id="${helpers.escapeHtml(account.id)}">使用此账号</button>
|
||||
<button class="btn btn-primary btn-sm" type="button" data-account-action="login" data-account-id="${helpers.escapeHtml(account.id)}">登录</button>
|
||||
<button class="btn btn-outline btn-sm" type="button" data-account-action="edit" data-account-id="${helpers.escapeHtml(account.id)}">编辑</button>
|
||||
<button class="btn btn-outline btn-sm" type="button" data-account-action="toggle-enabled" data-account-id="${helpers.escapeHtml(account.id)}">${account.enabled === false ? '启用' : '禁用'}</button>
|
||||
${coolingDown ? `<button class="btn btn-outline btn-sm" type="button" data-account-action="clear-cooldown" data-account-id="${helpers.escapeHtml(account.id)}">清冷却</button>` : ''}
|
||||
<button class="btn btn-ghost btn-sm" type="button" data-account-action="delete" data-account-id="${helpers.escapeHtml(account.id)}">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
updateMail2925ListViewport();
|
||||
}
|
||||
|
||||
async function handleAddMail2925Account() {
|
||||
if (actionInFlight) return;
|
||||
|
||||
const email = String(dom.inputMail2925Email?.value || '').trim();
|
||||
const password = String(dom.inputMail2925Password?.value || '');
|
||||
if (!email) {
|
||||
helpers.showToast('请先填写 2925 邮箱。', 'warn');
|
||||
return;
|
||||
}
|
||||
if (!password) {
|
||||
helpers.showToast('请先填写 2925 密码。', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
const updatingExisting = Boolean(editingAccountId);
|
||||
actionInFlight = true;
|
||||
if (dom.btnAddMail2925Account) {
|
||||
dom.btnAddMail2925Account.disabled = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'UPSERT_MAIL2925_ACCOUNT',
|
||||
source: 'sidepanel',
|
||||
payload: {
|
||||
...(editingAccountId ? { id: editingAccountId } : {}),
|
||||
email,
|
||||
password,
|
||||
},
|
||||
});
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
applyMail2925AccountMutation(response.account);
|
||||
formController.setVisible(false, { clearForm: true });
|
||||
helpers.showToast(
|
||||
updatingExisting
|
||||
? `已更新 2925 账号 ${email}`
|
||||
: `已保存 2925 账号 ${email}`,
|
||||
'success',
|
||||
1800
|
||||
);
|
||||
} catch (err) {
|
||||
helpers.showToast(`保存 2925 账号失败:${err.message}`, 'error');
|
||||
} finally {
|
||||
actionInFlight = false;
|
||||
if (dom.btnAddMail2925Account) {
|
||||
dom.btnAddMail2925Account.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleImportMail2925Accounts() {
|
||||
if (actionInFlight) return;
|
||||
if (typeof mail2925Utils.parseMail2925ImportText !== 'function') {
|
||||
helpers.showToast('2925 导入解析器未加载,请刷新扩展后重试。', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const rawText = String(dom.inputMail2925Import?.value || '').trim();
|
||||
if (!rawText) {
|
||||
helpers.showToast('请先粘贴 2925 账号导入内容。', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedAccounts = mail2925Utils.parseMail2925ImportText(rawText);
|
||||
if (!parsedAccounts.length) {
|
||||
helpers.showToast('没有解析到有效账号,请检查格式是否为 邮箱----密码。', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
actionInFlight = true;
|
||||
if (dom.btnImportMail2925Accounts) {
|
||||
dom.btnImportMail2925Accounts.disabled = true;
|
||||
}
|
||||
|
||||
try {
|
||||
for (const account of parsedAccounts) {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'UPSERT_MAIL2925_ACCOUNT',
|
||||
source: 'sidepanel',
|
||||
payload: account,
|
||||
});
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
}
|
||||
|
||||
if (dom.inputMail2925Import) {
|
||||
dom.inputMail2925Import.value = '';
|
||||
}
|
||||
helpers.showToast(`已导入 ${parsedAccounts.length} 条 2925 账号`, 'success', 2200);
|
||||
} catch (err) {
|
||||
helpers.showToast(`批量导入 2925 账号失败:${err.message}`, 'error');
|
||||
} finally {
|
||||
actionInFlight = false;
|
||||
if (dom.btnImportMail2925Accounts) {
|
||||
dom.btnImportMail2925Accounts.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAllMail2925Accounts() {
|
||||
const accounts = getMail2925Accounts();
|
||||
if (!accounts.length) {
|
||||
helpers.showToast('没有可删除的 2925 账号。', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await helpers.openConfirmModal({
|
||||
title: '全部删除 2925 账号',
|
||||
message: `确认删除当前全部 ${accounts.length} 个 2925 账号吗?`,
|
||||
confirmLabel: '确认全部删除',
|
||||
confirmVariant: 'btn-danger',
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'DELETE_MAIL2925_ACCOUNTS',
|
||||
source: 'sidepanel',
|
||||
payload: { mode: 'all' },
|
||||
});
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
state.syncLatestState({
|
||||
mail2925Accounts: [],
|
||||
currentMail2925AccountId: null,
|
||||
});
|
||||
formController.setVisible(false, { clearForm: true });
|
||||
refreshManagedAliasBaseEmail();
|
||||
renderMail2925Accounts();
|
||||
helpers.showToast(`已删除全部 ${response.deletedCount || 0} 个 2925 账号`, 'success', 2200);
|
||||
}
|
||||
|
||||
async function handleAccountListClick(event) {
|
||||
const actionButton = event.target.closest('[data-account-action]');
|
||||
if (!actionButton || actionInFlight) {
|
||||
return;
|
||||
}
|
||||
|
||||
const accountId = String(actionButton.dataset.accountId || '');
|
||||
const action = String(actionButton.dataset.accountAction || '');
|
||||
if (!accountId || !action) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetAccount = getMail2925Accounts().find((account) => account.id === accountId) || null;
|
||||
actionInFlight = true;
|
||||
actionButton.disabled = true;
|
||||
|
||||
try {
|
||||
if (action === 'copy-email') {
|
||||
if (!targetAccount?.email) throw new Error('未找到可复制的 2925 邮箱。');
|
||||
await helpers.copyTextToClipboard(targetAccount.email);
|
||||
helpers.showToast(`已复制 ${targetAccount.email}`, 'success', 1800);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'select') {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'SELECT_MAIL2925_ACCOUNT',
|
||||
source: 'sidepanel',
|
||||
payload: { accountId },
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
state.syncLatestState({ currentMail2925AccountId: response.account.id });
|
||||
refreshManagedAliasBaseEmail();
|
||||
renderMail2925Accounts();
|
||||
helpers.showToast(`已切换当前 2925 账号为 ${response.account.email}`, 'success', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'login') {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'LOGIN_MAIL2925_ACCOUNT',
|
||||
source: 'sidepanel',
|
||||
payload: {
|
||||
accountId,
|
||||
forceRelogin: true,
|
||||
},
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
state.syncLatestState({ currentMail2925AccountId: response.account.id });
|
||||
refreshManagedAliasBaseEmail();
|
||||
renderMail2925Accounts();
|
||||
helpers.showToast(`已使用 ${response.account.email} 登录 2925 邮箱`, 'success', 2200);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'edit') {
|
||||
if (!targetAccount) throw new Error('未找到目标 2925 账号。');
|
||||
startEditingAccount(targetAccount);
|
||||
helpers.showToast(`已载入 ${targetAccount.email},修改后点“保存修改”即可`, 'info', 1800);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'toggle-enabled') {
|
||||
if (!targetAccount) throw new Error('未找到目标 2925 账号。');
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'PATCH_MAIL2925_ACCOUNT',
|
||||
source: 'sidepanel',
|
||||
payload: {
|
||||
accountId,
|
||||
updates: {
|
||||
enabled: targetAccount.enabled === false,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
applyMail2925AccountMutation(response.account);
|
||||
helpers.showToast(`2925 账号 ${response.account.email} 已${response.account.enabled === false ? '禁用' : '启用'}`, 'success', 2200);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'clear-cooldown') {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'PATCH_MAIL2925_ACCOUNT',
|
||||
source: 'sidepanel',
|
||||
payload: {
|
||||
accountId,
|
||||
updates: {
|
||||
disabledUntil: 0,
|
||||
lastError: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
applyMail2925AccountMutation(response.account);
|
||||
helpers.showToast(`2925 账号 ${response.account.email} 已清除冷却`, 'success', 2200);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'delete') {
|
||||
const confirmed = await helpers.openConfirmModal({
|
||||
title: '删除 2925 账号',
|
||||
message: '确认删除这个 2925 账号吗?',
|
||||
confirmLabel: '确认删除',
|
||||
confirmVariant: 'btn-danger',
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'DELETE_MAIL2925_ACCOUNT',
|
||||
source: 'sidepanel',
|
||||
payload: { accountId },
|
||||
});
|
||||
if (response?.error) throw new Error(response.error);
|
||||
|
||||
const nextAccounts = getMail2925Accounts().filter((account) => account.id !== accountId);
|
||||
const nextState = { mail2925Accounts: nextAccounts };
|
||||
if (getCurrentMail2925AccountId() === accountId) {
|
||||
nextState.currentMail2925AccountId = null;
|
||||
}
|
||||
state.syncLatestState(nextState);
|
||||
if (editingAccountId === accountId) {
|
||||
formController.setVisible(false, { clearForm: true });
|
||||
}
|
||||
refreshManagedAliasBaseEmail();
|
||||
renderMail2925Accounts();
|
||||
helpers.showToast('2925 账号已删除', 'success', 1800);
|
||||
}
|
||||
} catch (err) {
|
||||
helpers.showToast(err.message, 'error');
|
||||
} finally {
|
||||
actionInFlight = false;
|
||||
actionButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function bindMail2925Events() {
|
||||
dom.btnToggleMail2925List?.addEventListener('click', () => {
|
||||
setMail2925ListExpanded(!listExpanded);
|
||||
});
|
||||
|
||||
dom.btnToggleMail2925Form?.addEventListener('click', () => {
|
||||
if (formController.isVisible()) {
|
||||
formController.setVisible(false, { clearForm: true });
|
||||
return;
|
||||
}
|
||||
formController.setVisible(true, { clearForm: !editingAccountId, focusField: true });
|
||||
});
|
||||
|
||||
dom.btnDeleteAllMail2925Accounts?.addEventListener('click', async () => {
|
||||
if (actionInFlight) return;
|
||||
actionInFlight = true;
|
||||
try {
|
||||
await deleteAllMail2925Accounts();
|
||||
} catch (err) {
|
||||
helpers.showToast(err.message, 'error');
|
||||
} finally {
|
||||
actionInFlight = false;
|
||||
updateMail2925ListViewport();
|
||||
}
|
||||
});
|
||||
|
||||
dom.btnAddMail2925Account?.addEventListener('click', handleAddMail2925Account);
|
||||
dom.btnImportMail2925Accounts?.addEventListener('click', handleImportMail2925Accounts);
|
||||
dom.inputMail2925Search?.addEventListener('input', (event) => {
|
||||
searchTerm = normalizeSearchText(event.target.value);
|
||||
renderMail2925Accounts();
|
||||
});
|
||||
dom.selectMail2925Filter?.addEventListener('change', (event) => {
|
||||
filterMode = String(event.target.value || 'all');
|
||||
renderMail2925Accounts();
|
||||
});
|
||||
dom.mail2925AccountsList?.addEventListener('click', handleAccountListClick);
|
||||
syncEditUi();
|
||||
formController.sync();
|
||||
}
|
||||
|
||||
return {
|
||||
bindMail2925Events,
|
||||
initMail2925ListExpandedState,
|
||||
renderMail2925Accounts,
|
||||
};
|
||||
}
|
||||
|
||||
globalScope.SidepanelMail2925Manager = {
|
||||
createMail2925Manager,
|
||||
};
|
||||
})(window);
|
||||
@@ -0,0 +1,308 @@
|
||||
(function attachSidepanelPayPalManager(globalScope) {
|
||||
function createPayPalManager(context = {}) {
|
||||
const {
|
||||
state,
|
||||
dom,
|
||||
helpers,
|
||||
runtime,
|
||||
paypalUtils = {},
|
||||
} = context;
|
||||
|
||||
let actionInFlight = false;
|
||||
let payPalAccountPicker = null;
|
||||
|
||||
function getPayPalAccounts(currentState = state.getLatestState()) {
|
||||
return helpers.getPayPalAccounts(currentState);
|
||||
}
|
||||
|
||||
function getCurrentPayPalAccountId(currentState = state.getLatestState()) {
|
||||
return String(currentState?.currentPayPalAccountId || '').trim();
|
||||
}
|
||||
|
||||
function buildSelectOptions(accounts = []) {
|
||||
if (!accounts.length) {
|
||||
return '<option value=""></option>';
|
||||
}
|
||||
return accounts.map((account) => (
|
||||
`<option value="${helpers.escapeHtml(account.id)}">${helpers.escapeHtml(account.email || '(未命名账号)')}</option>`
|
||||
)).join('');
|
||||
}
|
||||
|
||||
function getPayPalAccountValue(account = {}) {
|
||||
return String(account?.id || '').trim();
|
||||
}
|
||||
|
||||
function getPayPalAccountLabel(account = {}) {
|
||||
return String(account?.email || '(未命名账号)');
|
||||
}
|
||||
|
||||
function normalizePickerPayPalAccounts(accounts = []) {
|
||||
return Array.isArray(accounts)
|
||||
? accounts.filter((account) => getPayPalAccountValue(account))
|
||||
: [];
|
||||
}
|
||||
|
||||
function getPayPalAccountPicker() {
|
||||
if (payPalAccountPicker) {
|
||||
return payPalAccountPicker;
|
||||
}
|
||||
|
||||
const pickerModule = helpers.editableListPicker || globalScope.SidepanelEditableListPicker;
|
||||
const createEditableListPicker = pickerModule?.createEditableListPicker;
|
||||
if (
|
||||
typeof createEditableListPicker !== 'function'
|
||||
|| !dom.payPalAccountPickerRoot
|
||||
|| !dom.btnPayPalAccountMenu
|
||||
|| !dom.payPalAccountCurrent
|
||||
|| !dom.payPalAccountMenu
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
payPalAccountPicker = createEditableListPicker({
|
||||
root: dom.payPalAccountPickerRoot,
|
||||
input: dom.selectPayPalAccount,
|
||||
trigger: dom.btnPayPalAccountMenu,
|
||||
current: dom.payPalAccountCurrent,
|
||||
menu: dom.payPalAccountMenu,
|
||||
emptyLabel: '',
|
||||
itemLabel: '账号',
|
||||
normalizeItems: normalizePickerPayPalAccounts,
|
||||
normalizeValue: (value) => String(value || '').trim(),
|
||||
getItemValue: getPayPalAccountValue,
|
||||
getItemLabel: getPayPalAccountLabel,
|
||||
getItemDeleteLabel: getPayPalAccountLabel,
|
||||
onDelete: (accountId) => handleDeletePayPalAccount(accountId),
|
||||
onDeleteError: (error, fallbackMessage) => {
|
||||
helpers.showToast(error?.message || fallbackMessage, 'error');
|
||||
},
|
||||
});
|
||||
return payPalAccountPicker;
|
||||
}
|
||||
|
||||
function applyPayPalAccountMutation(account) {
|
||||
if (!account?.id) return;
|
||||
const latestState = state.getLatestState();
|
||||
const nextAccounts = typeof paypalUtils.upsertPayPalAccountInList === 'function'
|
||||
? paypalUtils.upsertPayPalAccountInList(getPayPalAccounts(latestState), account)
|
||||
: [...getPayPalAccounts(latestState), account];
|
||||
state.syncLatestState({ paypalAccounts: nextAccounts });
|
||||
renderPayPalAccounts();
|
||||
}
|
||||
|
||||
function renderPayPalAccounts() {
|
||||
if (!dom.selectPayPalAccount) return;
|
||||
|
||||
const latestState = state.getLatestState();
|
||||
const accounts = getPayPalAccounts(latestState);
|
||||
const currentId = getCurrentPayPalAccountId(latestState);
|
||||
const selectedId = accounts.some((account) => account.id === currentId) ? currentId : '';
|
||||
const picker = getPayPalAccountPicker();
|
||||
|
||||
if (picker) {
|
||||
picker.render(accounts, selectedId);
|
||||
return;
|
||||
}
|
||||
|
||||
dom.selectPayPalAccount.innerHTML = buildSelectOptions(accounts);
|
||||
dom.selectPayPalAccount.disabled = accounts.length === 0;
|
||||
dom.selectPayPalAccount.value = selectedId;
|
||||
}
|
||||
|
||||
async function syncSelectedPayPalAccount(options = {}) {
|
||||
const { silent = false } = options;
|
||||
const accountId = String(dom.selectPayPalAccount?.value || '').trim();
|
||||
if (!accountId) {
|
||||
state.syncLatestState({
|
||||
currentPayPalAccountId: null,
|
||||
paypalEmail: '',
|
||||
paypalPassword: '',
|
||||
});
|
||||
renderPayPalAccounts();
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'SELECT_PAYPAL_ACCOUNT',
|
||||
source: 'sidepanel',
|
||||
payload: { accountId },
|
||||
});
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
state.syncLatestState({
|
||||
currentPayPalAccountId: response.account?.id || accountId,
|
||||
paypalEmail: String(response.account?.email || '').trim(),
|
||||
paypalPassword: String(response.account?.password || ''),
|
||||
});
|
||||
renderPayPalAccounts();
|
||||
if (!silent) {
|
||||
helpers.showToast(`已切换当前 PayPal 账号为 ${response.account?.email || accountId}`, 'success', 1800);
|
||||
}
|
||||
return response.account || null;
|
||||
}
|
||||
|
||||
async function handleDeletePayPalAccount(accountId) {
|
||||
if (actionInFlight) return;
|
||||
|
||||
const targetId = String(accountId || '').trim();
|
||||
if (!targetId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const latestState = state.getLatestState();
|
||||
const accounts = getPayPalAccounts(latestState);
|
||||
const targetAccount = accounts.find((account) => account.id === targetId);
|
||||
if (!targetAccount) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionInFlight = true;
|
||||
if (dom.btnAddPayPalAccount) {
|
||||
dom.btnAddPayPalAccount.disabled = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const nextAccounts = accounts.filter((account) => account.id !== targetId);
|
||||
const currentId = getCurrentPayPalAccountId(latestState);
|
||||
const nextCurrentAccount = currentId === targetId
|
||||
? (nextAccounts[0] || null)
|
||||
: (nextAccounts.find((account) => account.id === currentId) || null);
|
||||
const payload = {
|
||||
paypalAccounts: nextAccounts,
|
||||
currentPayPalAccountId: nextCurrentAccount?.id || '',
|
||||
paypalEmail: String(nextCurrentAccount?.email || '').trim(),
|
||||
paypalPassword: String(nextCurrentAccount?.password || ''),
|
||||
};
|
||||
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'SAVE_SETTING',
|
||||
source: 'sidepanel',
|
||||
payload,
|
||||
});
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
state.syncLatestState({
|
||||
...payload,
|
||||
currentPayPalAccountId: payload.currentPayPalAccountId || null,
|
||||
});
|
||||
renderPayPalAccounts();
|
||||
helpers.showToast(`已删除 PayPal 账号:${targetAccount.email || targetId}`, 'success', 1600);
|
||||
} finally {
|
||||
actionInFlight = false;
|
||||
if (dom.btnAddPayPalAccount) {
|
||||
dom.btnAddPayPalAccount.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function openPayPalAccountDialog() {
|
||||
if (typeof helpers.openFormDialog !== 'function') {
|
||||
throw new Error('表单弹窗能力未加载,请刷新扩展后重试。');
|
||||
}
|
||||
return helpers.openFormDialog({
|
||||
title: '添加 PayPal 账号',
|
||||
confirmLabel: '保存账号',
|
||||
confirmVariant: 'btn-primary',
|
||||
fields: [
|
||||
{
|
||||
key: 'email',
|
||||
label: 'PayPal 账号',
|
||||
type: 'text',
|
||||
placeholder: '请输入 PayPal 登录邮箱',
|
||||
autocomplete: 'username',
|
||||
required: true,
|
||||
requiredMessage: '请先填写 PayPal 账号。',
|
||||
validate: (value) => {
|
||||
const normalized = String(value || '').trim();
|
||||
if (!normalized.includes('@')) {
|
||||
return 'PayPal 账号需填写邮箱格式。';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
label: 'PayPal 密码',
|
||||
type: 'password',
|
||||
placeholder: '请输入 PayPal 登录密码',
|
||||
autocomplete: 'current-password',
|
||||
required: true,
|
||||
requiredMessage: '请先填写 PayPal 密码。',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async function handleAddPayPalAccount() {
|
||||
if (actionInFlight) return;
|
||||
|
||||
const formValues = await openPayPalAccountDialog();
|
||||
if (!formValues) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionInFlight = true;
|
||||
if (dom.btnAddPayPalAccount) {
|
||||
dom.btnAddPayPalAccount.disabled = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await runtime.sendMessage({
|
||||
type: 'UPSERT_PAYPAL_ACCOUNT',
|
||||
source: 'sidepanel',
|
||||
payload: {
|
||||
email: String(formValues.email || '').trim(),
|
||||
password: String(formValues.password || ''),
|
||||
},
|
||||
});
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
applyPayPalAccountMutation(response.account);
|
||||
if (response.account?.id) {
|
||||
state.syncLatestState({ currentPayPalAccountId: response.account.id });
|
||||
renderPayPalAccounts();
|
||||
dom.selectPayPalAccount.value = response.account.id;
|
||||
await syncSelectedPayPalAccount({ silent: true });
|
||||
}
|
||||
helpers.showToast(`已保存 PayPal 账号 ${response.account?.email || ''}`, 'success', 2200);
|
||||
} catch (error) {
|
||||
helpers.showToast(`保存 PayPal 账号失败:${error.message}`, 'error');
|
||||
throw error;
|
||||
} finally {
|
||||
actionInFlight = false;
|
||||
if (dom.btnAddPayPalAccount) {
|
||||
dom.btnAddPayPalAccount.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function bindPayPalEvents() {
|
||||
dom.btnAddPayPalAccount?.addEventListener('click', () => {
|
||||
void handleAddPayPalAccount();
|
||||
});
|
||||
dom.selectPayPalAccount?.addEventListener('change', () => {
|
||||
void syncSelectedPayPalAccount().catch((error) => {
|
||||
helpers.showToast(error.message, 'error');
|
||||
renderPayPalAccounts();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
bindPayPalEvents,
|
||||
handleDeletePayPalAccount,
|
||||
renderPayPalAccounts,
|
||||
syncSelectedPayPalAccount,
|
||||
};
|
||||
}
|
||||
|
||||
globalScope.SidepanelPayPalManager = {
|
||||
createPayPalManager,
|
||||
};
|
||||
})(window);
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+16419
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,459 @@
|
||||
(() => {
|
||||
const GITHUB_OWNER = 'FoundZiGu';
|
||||
const GITHUB_REPO = 'GuJumpgate';
|
||||
const RELEASES_PAGE_URL = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases`;
|
||||
const RELEASES_API_URL = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases?per_page=10`;
|
||||
const CACHE_KEY = 'multipage-release-snapshot-v1';
|
||||
const IGNORED_UPDATE_VERSION_KEY = 'multipage-ignored-release-version-v1';
|
||||
const CACHE_TTL_MS = 60 * 60 * 1000;
|
||||
const FETCH_TIMEOUT_MS = 8000;
|
||||
const MAX_RELEASES = 10;
|
||||
const MAX_NOTES_PER_RELEASE = 5;
|
||||
const VERSION_FAMILY_GUJUMPGATE = 'gujumpgate';
|
||||
const VERSION_FAMILY_ULTRA = 'ultra';
|
||||
const VERSION_FAMILY_PRO = 'pro';
|
||||
const VERSION_FAMILY_LEGACY = 'legacy';
|
||||
|
||||
function getVersionFamily(version, fallbackFamily = VERSION_FAMILY_LEGACY) {
|
||||
const trimmed = String(version || '').trim();
|
||||
if (/^(?:gujumpgate|flowpilot)/i.test(trimmed)) {
|
||||
return VERSION_FAMILY_GUJUMPGATE;
|
||||
}
|
||||
if (/^ultra/i.test(trimmed)) {
|
||||
return VERSION_FAMILY_ULTRA;
|
||||
}
|
||||
if (/^pro/i.test(trimmed)) {
|
||||
return VERSION_FAMILY_PRO;
|
||||
}
|
||||
if (/^v/i.test(trimmed)) {
|
||||
return VERSION_FAMILY_LEGACY;
|
||||
}
|
||||
return fallbackFamily;
|
||||
}
|
||||
|
||||
function stripVersionPrefix(version) {
|
||||
return String(version || '').trim().replace(/^(?:gujumpgate|flowpilot|ultra|pro|v)\s*/i, '');
|
||||
}
|
||||
|
||||
function extractVersionCore(version) {
|
||||
const core = stripVersionPrefix(version).split('-')[0];
|
||||
return /^\d+(?:\.\d+){1,3}$/.test(core) ? core : '';
|
||||
}
|
||||
|
||||
function formatDisplayVersion(version, fallbackFamily = VERSION_FAMILY_LEGACY) {
|
||||
const core = extractVersionCore(version);
|
||||
if (!core) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const family = getVersionFamily(version, fallbackFamily);
|
||||
return `${getVersionFamilyPrefix(family)}${core}`;
|
||||
}
|
||||
|
||||
function parseVersionMeta(version, fallbackFamily = VERSION_FAMILY_LEGACY) {
|
||||
const family = getVersionFamily(version, fallbackFamily);
|
||||
const core = extractVersionCore(version);
|
||||
return {
|
||||
family,
|
||||
core,
|
||||
displayVersion: core ? `${getVersionFamilyPrefix(family)}${core}` : '',
|
||||
parts: core
|
||||
? core.split('.').map((part) => {
|
||||
const numeric = Number.parseInt(part, 10);
|
||||
return Number.isFinite(numeric) ? numeric : 0;
|
||||
})
|
||||
: [0],
|
||||
};
|
||||
}
|
||||
|
||||
function getVersionFamilyPrefix(family) {
|
||||
if (family === VERSION_FAMILY_GUJUMPGATE) {
|
||||
return 'GuJumpgate';
|
||||
}
|
||||
if (family === VERSION_FAMILY_ULTRA) {
|
||||
return 'Ultra';
|
||||
}
|
||||
if (family === VERSION_FAMILY_PRO) {
|
||||
return 'Pro';
|
||||
}
|
||||
return 'v';
|
||||
}
|
||||
|
||||
function getVersionFamilyRank(family) {
|
||||
if (family === VERSION_FAMILY_GUJUMPGATE) {
|
||||
return 4;
|
||||
}
|
||||
if (family === VERSION_FAMILY_ULTRA) {
|
||||
return 3;
|
||||
}
|
||||
if (family === VERSION_FAMILY_PRO) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
function parseVersionParts(version, fallbackFamily = VERSION_FAMILY_LEGACY) {
|
||||
return parseVersionMeta(version, fallbackFamily).parts;
|
||||
}
|
||||
|
||||
function compareVersions(left, right, fallbackFamily = VERSION_FAMILY_LEGACY) {
|
||||
const leftMeta = parseVersionMeta(left, fallbackFamily);
|
||||
const rightMeta = parseVersionMeta(right, fallbackFamily);
|
||||
const leftFamilyRank = getVersionFamilyRank(leftMeta.family);
|
||||
const rightFamilyRank = getVersionFamilyRank(rightMeta.family);
|
||||
|
||||
if (leftFamilyRank > rightFamilyRank) {
|
||||
return 1;
|
||||
}
|
||||
if (leftFamilyRank < rightFamilyRank) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const leftParts = leftMeta.parts;
|
||||
const rightParts = rightMeta.parts;
|
||||
const maxLength = Math.max(leftParts.length, rightParts.length, 3);
|
||||
|
||||
for (let index = 0; index < maxLength; index += 1) {
|
||||
const leftPart = leftParts[index] || 0;
|
||||
const rightPart = rightParts[index] || 0;
|
||||
if (leftPart > rightPart) {
|
||||
return 1;
|
||||
}
|
||||
if (leftPart < rightPart) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function sanitizeInlineMarkdown(text) {
|
||||
return String(text || '')
|
||||
.replace(/!\[[^\]]*]\(([^)]+)\)/g, '')
|
||||
.replace(/\[([^\]]+)]\(([^)]+)\)/g, '$1')
|
||||
.replace(/`([^`]+)`/g, '$1')
|
||||
.replace(/[*_~>#]/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function parseReleaseNotes(body) {
|
||||
const lines = String(body || '')
|
||||
.replace(/\r\n/g, '\n')
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const bulletLines = [];
|
||||
const plainLines = [];
|
||||
|
||||
for (const line of lines) {
|
||||
if (/^#{1,6}\s*/.test(line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^```/.test(line) || /^---+$/.test(line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^[-*+]\s+/.test(line)) {
|
||||
bulletLines.push(line.replace(/^[-*+]\s+/, ''));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^\d+\.\s+/.test(line)) {
|
||||
bulletLines.push(line.replace(/^\d+\.\s+/, ''));
|
||||
continue;
|
||||
}
|
||||
|
||||
plainLines.push(line);
|
||||
}
|
||||
|
||||
const noteLines = bulletLines.length > 0 ? bulletLines : plainLines;
|
||||
return noteLines
|
||||
.map(sanitizeInlineMarkdown)
|
||||
.filter(Boolean)
|
||||
.slice(0, MAX_NOTES_PER_RELEASE);
|
||||
}
|
||||
|
||||
function normalizeReleaseVersion(release) {
|
||||
const candidates = [
|
||||
release?.tag_name,
|
||||
release?.name,
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const meta = parseVersionMeta(candidate);
|
||||
if (meta.core) {
|
||||
return meta;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function sanitizeRelease(release) {
|
||||
const versionMeta = normalizeReleaseVersion(release);
|
||||
if (!versionMeta?.core) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawTitle = sanitizeInlineMarkdown(release?.name || '');
|
||||
const titleMeta = parseVersionMeta(rawTitle, versionMeta.family);
|
||||
const normalizedTitle = titleMeta.core && titleMeta.displayVersion === versionMeta.displayVersion
|
||||
? ''
|
||||
: rawTitle;
|
||||
|
||||
return {
|
||||
version: versionMeta.core,
|
||||
displayVersion: versionMeta.displayVersion,
|
||||
family: versionMeta.family,
|
||||
title: normalizedTitle,
|
||||
url: String(release?.html_url || RELEASES_PAGE_URL),
|
||||
publishedAt: String(release?.published_at || release?.created_at || ''),
|
||||
notes: parseReleaseNotes(release?.body || ''),
|
||||
};
|
||||
}
|
||||
|
||||
function getComparableReleaseVersion(release) {
|
||||
const fallbackFamily = [VERSION_FAMILY_GUJUMPGATE, VERSION_FAMILY_ULTRA, VERSION_FAMILY_PRO].includes(release?.family)
|
||||
? release.family
|
||||
: VERSION_FAMILY_LEGACY;
|
||||
const displayVersion = String(release?.displayVersion || '').trim();
|
||||
if (displayVersion) {
|
||||
return displayVersion;
|
||||
}
|
||||
return formatDisplayVersion(release?.version || '', fallbackFamily);
|
||||
}
|
||||
|
||||
function sortReleasesByVersion(releases = []) {
|
||||
return [...releases].sort((left, right) => (
|
||||
compareVersions(
|
||||
getComparableReleaseVersion(right),
|
||||
getComparableReleaseVersion(left)
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
function readCache() {
|
||||
try {
|
||||
const raw = localStorage.getItem(CACHE_KEY);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || !Array.isArray(parsed.releases) || !Number.isFinite(parsed.fetchedAt)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((Date.now() - parsed.fetchedAt) > CACHE_TTL_MS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sortReleasesByVersion(parsed.releases).slice(0, MAX_RELEASES);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeCache(releases) {
|
||||
try {
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify({
|
||||
fetchedAt: Date.now(),
|
||||
releases,
|
||||
}));
|
||||
} catch (error) {
|
||||
// Ignore cache write failures.
|
||||
}
|
||||
}
|
||||
|
||||
function getIgnoredUpdateVersion() {
|
||||
try {
|
||||
return String(localStorage.getItem(IGNORED_UPDATE_VERSION_KEY) || '').trim();
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function setIgnoredUpdateVersion(version) {
|
||||
const normalized = formatDisplayVersion(version, VERSION_FAMILY_GUJUMPGATE) || String(version || '').trim();
|
||||
try {
|
||||
if (normalized) {
|
||||
localStorage.setItem(IGNORED_UPDATE_VERSION_KEY, normalized);
|
||||
} else {
|
||||
localStorage.removeItem(IGNORED_UPDATE_VERSION_KEY);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore localStorage failures.
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function clearIgnoredUpdateVersion() {
|
||||
return setIgnoredUpdateVersion('');
|
||||
}
|
||||
|
||||
async function fetchReleases() {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const response = await fetch(RELEASES_API_URL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/vnd.github+json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub Releases 请求失败(${response.status})`);
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
if (!Array.isArray(payload)) {
|
||||
throw new Error('GitHub Releases 返回格式异常');
|
||||
}
|
||||
|
||||
const releases = payload
|
||||
.filter((release) => release && !release.draft && !release.prerelease)
|
||||
.map(sanitizeRelease)
|
||||
.filter(Boolean);
|
||||
|
||||
const sortedReleases = sortReleasesByVersion(releases).slice(0, MAX_RELEASES);
|
||||
|
||||
writeCache(sortedReleases);
|
||||
return sortedReleases;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error('GitHub Releases 请求超时');
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadReleases(options = {}) {
|
||||
if (!options.force) {
|
||||
const cached = readCache();
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
return fetchReleases();
|
||||
}
|
||||
|
||||
function buildReleaseSnapshot(releases, localVersion) {
|
||||
const latestRelease = releases[0] || null;
|
||||
if (!latestRelease) {
|
||||
return {
|
||||
status: 'empty',
|
||||
localVersion,
|
||||
latestVersion: null,
|
||||
latestRelease: null,
|
||||
newerReleases: [],
|
||||
logUrl: RELEASES_PAGE_URL,
|
||||
releasesPageUrl: RELEASES_PAGE_URL,
|
||||
checkedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
const newerReleases = releases.filter((release) => compareVersions(release.displayVersion, localVersion) > 0);
|
||||
const ignoredVersion = getIgnoredUpdateVersion();
|
||||
const newestUpdateVersion = newerReleases[0]?.displayVersion || '';
|
||||
const updateIgnored = Boolean(newestUpdateVersion)
|
||||
&& Boolean(ignoredVersion)
|
||||
&& compareVersions(ignoredVersion, newestUpdateVersion) === 0;
|
||||
return {
|
||||
status: newerReleases.length > 0
|
||||
? (updateIgnored ? 'ignored' : 'update-available')
|
||||
: 'latest',
|
||||
localVersion,
|
||||
latestVersion: latestRelease.displayVersion,
|
||||
latestRelease,
|
||||
newerReleases,
|
||||
ignoredVersion: updateIgnored ? ignoredVersion : '',
|
||||
logUrl: latestRelease.url || RELEASES_PAGE_URL,
|
||||
releasesPageUrl: RELEASES_PAGE_URL,
|
||||
checkedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function getSnapshotUpdateVersion(snapshot = {}) {
|
||||
const newerReleases = Array.isArray(snapshot?.newerReleases) ? snapshot.newerReleases : [];
|
||||
return newerReleases[0]?.displayVersion || snapshot?.latestVersion || '';
|
||||
}
|
||||
|
||||
function ignoreReleaseSnapshot(snapshot = {}) {
|
||||
const targetVersion = getSnapshotUpdateVersion(snapshot);
|
||||
if (!targetVersion) {
|
||||
return '';
|
||||
}
|
||||
return setIgnoredUpdateVersion(targetVersion);
|
||||
}
|
||||
|
||||
function getLocalVersionLabel(manifest = chrome.runtime.getManifest()) {
|
||||
const versionName = formatDisplayVersion(manifest?.version_name, VERSION_FAMILY_GUJUMPGATE);
|
||||
if (versionName) {
|
||||
return versionName;
|
||||
}
|
||||
|
||||
const versionCore = extractVersionCore(manifest?.version || '');
|
||||
return versionCore ? formatDisplayVersion(`GuJumpgate${versionCore}`, VERSION_FAMILY_GUJUMPGATE) : '';
|
||||
}
|
||||
|
||||
async function getReleaseSnapshot(options = {}) {
|
||||
const localVersion = getLocalVersionLabel(chrome.runtime.getManifest()) || 'GuJumpgate0.0';
|
||||
|
||||
try {
|
||||
const releases = await loadReleases(options);
|
||||
return buildReleaseSnapshot(releases, localVersion);
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'error',
|
||||
localVersion,
|
||||
latestVersion: null,
|
||||
latestRelease: null,
|
||||
newerReleases: [],
|
||||
logUrl: RELEASES_PAGE_URL,
|
||||
releasesPageUrl: RELEASES_PAGE_URL,
|
||||
checkedAt: Date.now(),
|
||||
errorMessage: error?.message || '更新检查失败',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function formatReleaseDate(value) {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
window.SidepanelUpdateService = {
|
||||
compareVersions,
|
||||
formatDisplayVersion,
|
||||
formatReleaseDate,
|
||||
clearIgnoredUpdateVersion,
|
||||
getIgnoredUpdateVersion,
|
||||
getLocalVersionLabel,
|
||||
getReleaseSnapshot,
|
||||
ignoreReleaseSnapshot,
|
||||
repositoryUrl: `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}`,
|
||||
releasesPageUrl: RELEASES_PAGE_URL,
|
||||
stripVersionPrefix,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user