hotmail-manager.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. (function attachSidepanelHotmailManager(globalScope) {
  2. function createHotmailManager(context = {}) {
  3. const {
  4. state,
  5. dom,
  6. helpers,
  7. runtime,
  8. constants = {},
  9. hotmailUtils = {},
  10. } = context;
  11. const expandedStorageKey = constants.expandedStorageKey || 'multipage-hotmail-list-expanded';
  12. const displayTimeZone = constants.displayTimeZone || 'Asia/Shanghai';
  13. const copyIcon = constants.copyIcon || '';
  14. let actionInFlight = false;
  15. let listExpanded = false;
  16. function getHotmailAccountsByUsage(mode = 'all', currentState = state.getLatestState()) {
  17. const accounts = helpers.getHotmailAccounts(currentState);
  18. if (typeof hotmailUtils.filterHotmailAccountsByUsage === 'function') {
  19. return hotmailUtils.filterHotmailAccountsByUsage(accounts, mode);
  20. }
  21. if (mode === 'used') {
  22. return accounts.filter((account) => Boolean(account?.used));
  23. }
  24. return accounts.slice();
  25. }
  26. function getHotmailBulkActionText(mode, count) {
  27. if (typeof hotmailUtils.getHotmailBulkActionLabel === 'function') {
  28. return hotmailUtils.getHotmailBulkActionLabel(mode, count);
  29. }
  30. const normalizedCount = Number.isFinite(Number(count)) ? Math.max(0, Number(count)) : 0;
  31. const prefix = mode === 'used' ? '清空已用' : '全部删除';
  32. const suffix = normalizedCount > 0 ? `(${normalizedCount})` : '';
  33. return `${prefix}${suffix}`;
  34. }
  35. function getHotmailListToggleText(expanded, count) {
  36. if (typeof hotmailUtils.getHotmailListToggleLabel === 'function') {
  37. return hotmailUtils.getHotmailListToggleLabel(expanded, count);
  38. }
  39. const normalizedCount = Number.isFinite(Number(count)) ? Math.max(0, Number(count)) : 0;
  40. const suffix = normalizedCount > 0 ? `(${normalizedCount})` : '';
  41. return `${expanded ? '收起列表' : '展开列表'}${suffix}`;
  42. }
  43. function updateHotmailListViewport() {
  44. const count = helpers.getHotmailAccounts().length;
  45. const usedCount = getHotmailAccountsByUsage('used').length;
  46. if (dom.btnClearUsedHotmailAccounts) {
  47. dom.btnClearUsedHotmailAccounts.textContent = getHotmailBulkActionText('used', usedCount);
  48. dom.btnClearUsedHotmailAccounts.disabled = usedCount === 0;
  49. }
  50. if (dom.btnDeleteAllHotmailAccounts) {
  51. dom.btnDeleteAllHotmailAccounts.textContent = getHotmailBulkActionText('all', count);
  52. dom.btnDeleteAllHotmailAccounts.disabled = count === 0;
  53. }
  54. if (dom.btnToggleHotmailList) {
  55. dom.btnToggleHotmailList.textContent = getHotmailListToggleText(listExpanded, count);
  56. dom.btnToggleHotmailList.setAttribute('aria-expanded', String(listExpanded));
  57. dom.btnToggleHotmailList.disabled = count === 0;
  58. }
  59. if (dom.hotmailListShell) {
  60. dom.hotmailListShell.classList.toggle('is-expanded', listExpanded);
  61. dom.hotmailListShell.classList.toggle('is-collapsed', !listExpanded);
  62. }
  63. }
  64. function setHotmailListExpanded(expanded, options = {}) {
  65. const { persist = true } = options;
  66. listExpanded = Boolean(expanded);
  67. updateHotmailListViewport();
  68. if (persist) {
  69. localStorage.setItem(expandedStorageKey, listExpanded ? '1' : '0');
  70. }
  71. }
  72. function initHotmailListExpandedState() {
  73. const saved = localStorage.getItem(expandedStorageKey);
  74. setHotmailListExpanded(saved === '1', { persist: false });
  75. }
  76. function shouldClearCurrentHotmailSelectionLocally(account) {
  77. if (typeof hotmailUtils.shouldClearHotmailCurrentSelection === 'function') {
  78. return hotmailUtils.shouldClearHotmailCurrentSelection(account);
  79. }
  80. return Boolean(account) && account.used === true;
  81. }
  82. function upsertHotmailAccountListLocally(accounts, nextAccount) {
  83. if (typeof hotmailUtils.upsertHotmailAccountInList === 'function') {
  84. return hotmailUtils.upsertHotmailAccountInList(accounts, nextAccount);
  85. }
  86. const list = Array.isArray(accounts) ? accounts.slice() : [];
  87. if (!nextAccount?.id) return list;
  88. const existingIndex = list.findIndex((account) => account?.id === nextAccount.id);
  89. if (existingIndex === -1) {
  90. list.push(nextAccount);
  91. return list;
  92. }
  93. list[existingIndex] = nextAccount;
  94. return list;
  95. }
  96. function refreshHotmailSelectionUI() {
  97. renderHotmailAccounts();
  98. if (dom.selectMailProvider.value === 'hotmail-api') {
  99. dom.inputEmail.value = helpers.getCurrentHotmailEmail();
  100. }
  101. }
  102. function applyHotmailAccountMutation(account, options = {}) {
  103. if (!account?.id) return;
  104. const { preserveCurrentSelection = false } = options;
  105. const latestState = state.getLatestState();
  106. const nextState = {
  107. hotmailAccounts: upsertHotmailAccountListLocally(helpers.getHotmailAccounts(), account),
  108. };
  109. if (!preserveCurrentSelection
  110. && latestState?.currentHotmailAccountId === account.id
  111. && shouldClearCurrentHotmailSelectionLocally(account)) {
  112. nextState.currentHotmailAccountId = null;
  113. if (dom.selectMailProvider.value === 'hotmail-api') {
  114. nextState.email = null;
  115. }
  116. }
  117. state.syncLatestState(nextState);
  118. refreshHotmailSelectionUI();
  119. }
  120. function formatDateTime(timestamp) {
  121. const value = Number(timestamp);
  122. if (!Number.isFinite(value) || value <= 0) {
  123. return '未使用';
  124. }
  125. return new Date(value).toLocaleString('zh-CN', {
  126. hour12: false,
  127. timeZone: displayTimeZone,
  128. });
  129. }
  130. function getHotmailAvailabilityLabel(account) {
  131. if (account.used) return '已用';
  132. return '可分配';
  133. }
  134. function getHotmailStatusLabel(account) {
  135. if (account.used) return '已用';
  136. switch (account.status) {
  137. case 'authorized':
  138. return '可用';
  139. case 'error':
  140. return '异常';
  141. default:
  142. return '待校验';
  143. }
  144. }
  145. function getHotmailStatusClass(account) {
  146. if (account.used) return 'status-used';
  147. return `status-${account.status || 'pending'}`;
  148. }
  149. function clearHotmailForm() {
  150. dom.inputHotmailEmail.value = '';
  151. dom.inputHotmailClientId.value = '';
  152. dom.inputHotmailPassword.value = '';
  153. dom.inputHotmailRefreshToken.value = '';
  154. }
  155. function renderHotmailAccounts() {
  156. if (!dom.hotmailAccountsList) return;
  157. const latestState = state.getLatestState();
  158. const accounts = helpers.getHotmailAccounts();
  159. const currentId = latestState?.currentHotmailAccountId || '';
  160. if (!accounts.length) {
  161. dom.hotmailAccountsList.innerHTML = '<div class="hotmail-empty">还没有 Hotmail 账号,先添加一条再校验。</div>';
  162. updateHotmailListViewport();
  163. return;
  164. }
  165. dom.hotmailAccountsList.innerHTML = accounts.map((account) => `
  166. <div class="hotmail-account-item${account.id === currentId ? ' is-current' : ''}">
  167. <div class="hotmail-account-top">
  168. <div class="hotmail-account-title-row">
  169. <div class="hotmail-account-email">${helpers.escapeHtml(account.email || '(未命名账号)')}</div>
  170. <button
  171. class="hotmail-copy-btn"
  172. type="button"
  173. data-account-action="copy-email"
  174. data-account-id="${helpers.escapeHtml(account.id)}"
  175. title="复制邮箱"
  176. aria-label="复制邮箱 ${helpers.escapeHtml(account.email || '')}"
  177. >${copyIcon}</button>
  178. </div>
  179. <span class="hotmail-status-chip ${helpers.escapeHtml(getHotmailStatusClass(account))}">${helpers.escapeHtml(getHotmailStatusLabel(account))}</span>
  180. </div>
  181. <div class="hotmail-account-meta">
  182. <span>客户端 ID:${helpers.escapeHtml(account.clientId ? `${account.clientId.slice(0, 10)}...` : '未填写')}</span>
  183. <span>刷新令牌:${account.refreshToken ? '已保存' : '未保存'}</span>
  184. <span>分配状态: ${helpers.escapeHtml(getHotmailAvailabilityLabel(account))}</span>
  185. <span>上次校验: ${helpers.escapeHtml(formatDateTime(account.lastAuthAt))}</span>
  186. <span>上次使用: ${helpers.escapeHtml(formatDateTime(account.lastUsedAt))}</span>
  187. </div>
  188. ${account.lastError ? `<div class="hotmail-account-error">${helpers.escapeHtml(account.lastError)}</div>` : ''}
  189. <div class="hotmail-account-actions">
  190. <button class="btn btn-outline btn-sm" type="button" data-account-action="select" data-account-id="${helpers.escapeHtml(account.id)}">使用此账号</button>
  191. <button class="btn btn-outline btn-sm" type="button" data-account-action="toggle-used" data-account-id="${helpers.escapeHtml(account.id)}">${account.used ? '标记未用' : '标记已用'}</button>
  192. <button class="btn btn-primary btn-sm" type="button" data-account-action="verify" data-account-id="${helpers.escapeHtml(account.id)}">校验</button>
  193. <button class="btn btn-outline btn-sm" type="button" data-account-action="test" data-account-id="${helpers.escapeHtml(account.id)}">复制最新验证码</button>
  194. <button class="btn btn-ghost btn-sm" type="button" data-account-action="delete" data-account-id="${helpers.escapeHtml(account.id)}">删除</button>
  195. </div>
  196. </div>
  197. `).join('');
  198. updateHotmailListViewport();
  199. }
  200. async function deleteHotmailAccountsByMode(mode) {
  201. const isUsedMode = mode === 'used';
  202. const targetAccounts = getHotmailAccountsByUsage(isUsedMode ? 'used' : 'all');
  203. if (!targetAccounts.length) {
  204. helpers.showToast(isUsedMode ? '没有已用账号可清空。' : '没有可删除的 Hotmail 账号。', 'warn');
  205. return;
  206. }
  207. const confirmed = await helpers.openConfirmModal({
  208. title: isUsedMode ? '清空已用账号' : '全部删除账号',
  209. message: isUsedMode
  210. ? `确认删除当前 ${targetAccounts.length} 个已用 Hotmail 账号吗?`
  211. : `确认删除全部 ${targetAccounts.length} 个 Hotmail 账号吗?`,
  212. confirmLabel: isUsedMode ? '确认清空已用' : '确认全部删除',
  213. confirmVariant: isUsedMode ? 'btn-outline' : 'btn-danger',
  214. });
  215. if (!confirmed) {
  216. return;
  217. }
  218. const response = await runtime.sendMessage({
  219. type: 'DELETE_HOTMAIL_ACCOUNTS',
  220. source: 'sidepanel',
  221. payload: { mode: isUsedMode ? 'used' : 'all' },
  222. });
  223. if (response?.error) {
  224. throw new Error(response.error);
  225. }
  226. const latestState = state.getLatestState();
  227. const targetIds = new Set(targetAccounts.map((account) => account.id));
  228. const nextAccounts = isUsedMode
  229. ? helpers.getHotmailAccounts().filter((account) => !targetIds.has(account.id))
  230. : [];
  231. const nextState = { hotmailAccounts: nextAccounts };
  232. if (latestState?.currentHotmailAccountId && targetIds.has(latestState.currentHotmailAccountId)) {
  233. nextState.currentHotmailAccountId = null;
  234. if (dom.selectMailProvider.value === 'hotmail-api') {
  235. nextState.email = null;
  236. }
  237. }
  238. state.syncLatestState(nextState);
  239. refreshHotmailSelectionUI();
  240. helpers.showToast(
  241. isUsedMode
  242. ? `已清空 ${response.deletedCount || 0} 个已用 Hotmail 账号`
  243. : `已删除全部 ${response.deletedCount || 0} 个 Hotmail 账号`,
  244. 'success',
  245. 2200
  246. );
  247. }
  248. async function handleAddHotmailAccount() {
  249. if (actionInFlight) return;
  250. const email = dom.inputHotmailEmail.value.trim();
  251. const clientId = dom.inputHotmailClientId.value.trim();
  252. const refreshToken = dom.inputHotmailRefreshToken.value.trim();
  253. if (!email) {
  254. helpers.showToast('请先填写 Hotmail 邮箱。', 'warn');
  255. return;
  256. }
  257. if (!clientId) {
  258. helpers.showToast('请先填写微软应用客户端 ID。', 'warn');
  259. return;
  260. }
  261. if (!refreshToken) {
  262. helpers.showToast('请先填写刷新令牌(refresh token)。', 'warn');
  263. return;
  264. }
  265. actionInFlight = true;
  266. dom.btnAddHotmailAccount.disabled = true;
  267. try {
  268. const response = await runtime.sendMessage({
  269. type: 'UPSERT_HOTMAIL_ACCOUNT',
  270. source: 'sidepanel',
  271. payload: {
  272. email,
  273. clientId,
  274. password: dom.inputHotmailPassword.value,
  275. refreshToken,
  276. },
  277. });
  278. if (response?.error) {
  279. throw new Error(response.error);
  280. }
  281. helpers.showToast(`已保存 Hotmail 账号 ${email}`, 'success', 1800);
  282. clearHotmailForm();
  283. } catch (err) {
  284. helpers.showToast(`保存 Hotmail 账号失败:${err.message}`, 'error');
  285. } finally {
  286. actionInFlight = false;
  287. dom.btnAddHotmailAccount.disabled = false;
  288. }
  289. }
  290. async function handleImportHotmailAccounts() {
  291. if (actionInFlight) return;
  292. if (typeof hotmailUtils.parseHotmailImportText !== 'function') {
  293. helpers.showToast('导入解析器未加载,请刷新扩展后重试。', 'error');
  294. return;
  295. }
  296. const rawText = dom.inputHotmailImport.value.trim();
  297. if (!rawText) {
  298. helpers.showToast('请先粘贴账号导入内容。', 'warn');
  299. return;
  300. }
  301. const parsedAccounts = hotmailUtils.parseHotmailImportText(rawText);
  302. if (!parsedAccounts.length) {
  303. helpers.showToast('没有解析到有效账号,请检查格式是否为 账号----密码----ID----Token。', 'error');
  304. return;
  305. }
  306. actionInFlight = true;
  307. dom.btnImportHotmailAccounts.disabled = true;
  308. try {
  309. for (const account of parsedAccounts) {
  310. const response = await runtime.sendMessage({
  311. type: 'UPSERT_HOTMAIL_ACCOUNT',
  312. source: 'sidepanel',
  313. payload: account,
  314. });
  315. if (response?.error) {
  316. throw new Error(response.error);
  317. }
  318. }
  319. dom.inputHotmailImport.value = '';
  320. helpers.showToast(`已导入 ${parsedAccounts.length} 条 Hotmail 账号`, 'success', 2200);
  321. } catch (err) {
  322. helpers.showToast(`批量导入失败:${err.message}`, 'error');
  323. } finally {
  324. actionInFlight = false;
  325. dom.btnImportHotmailAccounts.disabled = false;
  326. }
  327. }
  328. async function handleAccountListClick(event) {
  329. const actionButton = event.target.closest('[data-account-action]');
  330. if (!actionButton || actionInFlight) {
  331. return;
  332. }
  333. const accountId = actionButton.dataset.accountId;
  334. const action = actionButton.dataset.accountAction;
  335. if (!accountId || !action) {
  336. return;
  337. }
  338. const targetAccount = helpers.getHotmailAccounts().find((account) => account.id === accountId) || null;
  339. actionInFlight = true;
  340. actionButton.disabled = true;
  341. try {
  342. if (action === 'copy-email') {
  343. if (!targetAccount?.email) throw new Error('未找到可复制的邮箱地址。');
  344. await helpers.copyTextToClipboard(targetAccount.email);
  345. helpers.showToast(`已复制 ${targetAccount.email}`, 'success', 1800);
  346. } else if (action === 'select') {
  347. const response = await runtime.sendMessage({
  348. type: 'SELECT_HOTMAIL_ACCOUNT',
  349. source: 'sidepanel',
  350. payload: { accountId },
  351. });
  352. if (response?.error) throw new Error(response.error);
  353. state.syncLatestState({ currentHotmailAccountId: response.account.id });
  354. applyHotmailAccountMutation(response.account, { preserveCurrentSelection: true });
  355. helpers.showToast(`已切换当前 Hotmail 账号为 ${response.account.email}`, 'success', 1800);
  356. } else if (action === 'toggle-used') {
  357. if (!targetAccount) throw new Error('未找到目标 Hotmail 账号。');
  358. const response = await runtime.sendMessage({
  359. type: 'PATCH_HOTMAIL_ACCOUNT',
  360. source: 'sidepanel',
  361. payload: {
  362. accountId,
  363. updates: { used: !targetAccount.used },
  364. },
  365. });
  366. if (response?.error) throw new Error(response.error);
  367. applyHotmailAccountMutation(response.account);
  368. helpers.showToast(`账号 ${response.account.email} 已${response.account.used ? '标记为已用' : '恢复为未用'}`, 'success', 2200);
  369. } else if (action === 'verify') {
  370. const response = await runtime.sendMessage({
  371. type: 'VERIFY_HOTMAIL_ACCOUNT',
  372. source: 'sidepanel',
  373. payload: { accountId },
  374. });
  375. if (response?.error) throw new Error(response.error);
  376. applyHotmailAccountMutation(response.account, { preserveCurrentSelection: true });
  377. helpers.showToast(`账号 ${response.account.email} 校验通过`, 'success', 2200);
  378. } else if (action === 'test') {
  379. const response = await runtime.sendMessage({
  380. type: 'TEST_HOTMAIL_ACCOUNT',
  381. source: 'sidepanel',
  382. payload: { accountId },
  383. });
  384. if (response?.error) throw new Error(response.error);
  385. applyHotmailAccountMutation(response.account, { preserveCurrentSelection: true });
  386. if (response.latestCode) {
  387. await helpers.copyTextToClipboard(response.latestCode);
  388. const mailbox = response.latestMailbox ? `(${response.latestMailbox})` : '';
  389. helpers.showToast(`已复制最新验证码 ${response.latestCode}${mailbox}`, 'success', 2600);
  390. } else if (response.latestSubject) {
  391. const mailbox = response.latestMailbox ? `(${response.latestMailbox})` : '';
  392. helpers.showToast(`最新邮件${mailbox}没有验证码:${response.latestSubject}`, 'warn', 3200);
  393. } else {
  394. helpers.showToast('当前没有可读取的最新邮件。', 'warn', 2600);
  395. }
  396. } else if (action === 'delete') {
  397. const confirmed = await helpers.openConfirmModal({
  398. title: '删除账号',
  399. message: '确认删除这个 Hotmail 账号吗?对应 token 也会一起移除。',
  400. confirmLabel: '确认删除',
  401. confirmVariant: 'btn-danger',
  402. });
  403. if (!confirmed) {
  404. return;
  405. }
  406. const response = await runtime.sendMessage({
  407. type: 'DELETE_HOTMAIL_ACCOUNT',
  408. source: 'sidepanel',
  409. payload: { accountId },
  410. });
  411. if (response?.error) throw new Error(response.error);
  412. helpers.showToast('Hotmail 账号已删除', 'success', 1800);
  413. }
  414. } catch (err) {
  415. helpers.showToast(err.message, 'error');
  416. } finally {
  417. actionInFlight = false;
  418. actionButton.disabled = false;
  419. }
  420. }
  421. function bindHotmailEvents() {
  422. dom.btnToggleHotmailList?.addEventListener('click', () => {
  423. setHotmailListExpanded(!listExpanded);
  424. });
  425. dom.btnHotmailUsageGuide?.addEventListener('click', async () => {
  426. await helpers.openConfirmModal({
  427. title: '使用教程',
  428. message: 'API对接模式会直接调用微软邮箱接口取件;本地助手模式仍走本地服务。两种模式继续共用同一套 Hotmail 账号池与导入格式。',
  429. confirmLabel: '确定',
  430. confirmVariant: 'btn-primary',
  431. });
  432. });
  433. dom.btnClearUsedHotmailAccounts?.addEventListener('click', async () => {
  434. if (actionInFlight) return;
  435. actionInFlight = true;
  436. dom.btnClearUsedHotmailAccounts.disabled = true;
  437. try {
  438. await deleteHotmailAccountsByMode('used');
  439. } catch (err) {
  440. helpers.showToast(err.message, 'error');
  441. } finally {
  442. actionInFlight = false;
  443. updateHotmailListViewport();
  444. }
  445. });
  446. dom.btnDeleteAllHotmailAccounts?.addEventListener('click', async () => {
  447. if (actionInFlight) return;
  448. actionInFlight = true;
  449. dom.btnDeleteAllHotmailAccounts.disabled = true;
  450. try {
  451. await deleteHotmailAccountsByMode('all');
  452. } catch (err) {
  453. helpers.showToast(err.message, 'error');
  454. } finally {
  455. actionInFlight = false;
  456. updateHotmailListViewport();
  457. }
  458. });
  459. dom.btnAddHotmailAccount?.addEventListener('click', handleAddHotmailAccount);
  460. dom.btnImportHotmailAccounts?.addEventListener('click', handleImportHotmailAccounts);
  461. dom.hotmailAccountsList?.addEventListener('click', handleAccountListClick);
  462. }
  463. return {
  464. bindHotmailEvents,
  465. initHotmailListExpandedState,
  466. renderHotmailAccounts,
  467. };
  468. }
  469. globalScope.SidepanelHotmailManager = {
  470. createHotmailManager,
  471. };
  472. })(window);