luckmail-manager.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. (function attachSidepanelLuckmailManager(globalScope) {
  2. function createLuckmailManager(context = {}) {
  3. const {
  4. dom,
  5. helpers,
  6. runtime,
  7. constants = {},
  8. } = context;
  9. const copyIcon = constants.copyIcon || '';
  10. let renderedPurchases = [];
  11. let selectedPurchaseIds = new Set();
  12. let searchTerm = '';
  13. let filterMode = 'all';
  14. let refreshQueued = false;
  15. function normalizeLuckmailSearchText(value) {
  16. return String(value || '').trim().toLowerCase();
  17. }
  18. function getFilteredLuckmailPurchases(purchases = renderedPurchases) {
  19. const normalizedSearchTerm = normalizeLuckmailSearchText(searchTerm);
  20. return (Array.isArray(purchases) ? purchases : []).filter((purchase) => {
  21. const matchesFilter = (() => {
  22. switch (filterMode) {
  23. case 'reusable': return Boolean(purchase.reusable);
  24. case 'used': return Boolean(purchase.used);
  25. case 'unused': return !purchase.used;
  26. case 'preserved': return Boolean(purchase.preserved);
  27. case 'disabled': return Boolean(purchase.disabled);
  28. default: return true;
  29. }
  30. })();
  31. if (!matchesFilter) return false;
  32. if (!normalizedSearchTerm) return true;
  33. const haystack = [
  34. purchase.email_address,
  35. purchase.project_name,
  36. purchase.tag_name,
  37. purchase.used ? '已用 used' : '未用 unused',
  38. purchase.preserved ? '保留 preserved' : '',
  39. purchase.disabled ? '已禁用 disabled' : '',
  40. purchase.reusable ? '可复用 reusable' : '',
  41. ].join(' ').toLowerCase();
  42. return haystack.includes(normalizedSearchTerm);
  43. });
  44. }
  45. function pruneLuckmailSelection(purchases = renderedPurchases) {
  46. const existingIds = new Set((Array.isArray(purchases) ? purchases : []).map((purchase) => String(purchase.id)));
  47. selectedPurchaseIds = new Set([...selectedPurchaseIds].filter((id) => existingIds.has(id)));
  48. }
  49. function updateLuckmailBulkUI(visiblePurchases = getFilteredLuckmailPurchases()) {
  50. if (!dom.checkboxLuckmailSelectAll || !dom.luckmailSelectionSummary) {
  51. return;
  52. }
  53. const visibleIds = visiblePurchases.map((purchase) => String(purchase.id));
  54. const selectedVisibleCount = visibleIds.filter((id) => selectedPurchaseIds.has(id)).length;
  55. const hasVisible = visibleIds.length > 0;
  56. const hasSelection = selectedPurchaseIds.size > 0;
  57. dom.checkboxLuckmailSelectAll.checked = hasVisible && selectedVisibleCount === visibleIds.length;
  58. dom.checkboxLuckmailSelectAll.indeterminate = selectedVisibleCount > 0 && selectedVisibleCount < visibleIds.length;
  59. dom.checkboxLuckmailSelectAll.disabled = !hasVisible;
  60. dom.luckmailSelectionSummary.textContent = `已选 ${selectedPurchaseIds.size} 个(当前显示 ${visibleIds.length} 个)`;
  61. if (dom.btnLuckmailBulkUsed) dom.btnLuckmailBulkUsed.disabled = !hasSelection;
  62. if (dom.btnLuckmailBulkUnused) dom.btnLuckmailBulkUnused.disabled = !hasSelection;
  63. if (dom.btnLuckmailBulkPreserve) dom.btnLuckmailBulkPreserve.disabled = !hasSelection;
  64. if (dom.btnLuckmailBulkUnpreserve) dom.btnLuckmailBulkUnpreserve.disabled = !hasSelection;
  65. if (dom.btnLuckmailBulkDisable) dom.btnLuckmailBulkDisable.disabled = !hasSelection;
  66. if (dom.btnLuckmailBulkEnable) dom.btnLuckmailBulkEnable.disabled = !hasSelection;
  67. }
  68. function setLuckmailLoadingState(loading, summary = '') {
  69. if (dom.btnLuckmailRefresh) dom.btnLuckmailRefresh.disabled = loading;
  70. if (dom.btnLuckmailDisableUsed) dom.btnLuckmailDisableUsed.disabled = loading;
  71. if (dom.inputLuckmailSearch) dom.inputLuckmailSearch.disabled = loading;
  72. if (dom.selectLuckmailFilter) dom.selectLuckmailFilter.disabled = loading;
  73. if (dom.checkboxLuckmailSelectAll) dom.checkboxLuckmailSelectAll.disabled = loading || getFilteredLuckmailPurchases().length === 0;
  74. if (dom.btnLuckmailBulkUsed) dom.btnLuckmailBulkUsed.disabled = loading || selectedPurchaseIds.size === 0;
  75. if (dom.btnLuckmailBulkUnused) dom.btnLuckmailBulkUnused.disabled = loading || selectedPurchaseIds.size === 0;
  76. if (dom.btnLuckmailBulkPreserve) dom.btnLuckmailBulkPreserve.disabled = loading || selectedPurchaseIds.size === 0;
  77. if (dom.btnLuckmailBulkUnpreserve) dom.btnLuckmailBulkUnpreserve.disabled = loading || selectedPurchaseIds.size === 0;
  78. if (dom.btnLuckmailBulkDisable) dom.btnLuckmailBulkDisable.disabled = loading || selectedPurchaseIds.size === 0;
  79. if (dom.btnLuckmailBulkEnable) dom.btnLuckmailBulkEnable.disabled = loading || selectedPurchaseIds.size === 0;
  80. if (summary && dom.luckmailSummary) {
  81. dom.luckmailSummary.textContent = summary;
  82. }
  83. }
  84. function renderLuckmailPurchases(purchases = renderedPurchases) {
  85. if (!dom.luckmailList || !dom.luckmailSummary) return;
  86. renderedPurchases = Array.isArray(purchases) ? purchases : [];
  87. pruneLuckmailSelection(renderedPurchases);
  88. dom.luckmailList.innerHTML = '';
  89. if (!renderedPurchases.length) {
  90. selectedPurchaseIds.clear();
  91. dom.luckmailList.innerHTML = '<div class="luckmail-empty">未找到 openai 项目的 LuckMail 邮箱。</div>';
  92. dom.luckmailSummary.textContent = '加载已购邮箱后可在这里管理 openai 项目的 LuckMail 邮箱。';
  93. if (dom.btnLuckmailDisableUsed) dom.btnLuckmailDisableUsed.disabled = true;
  94. updateLuckmailBulkUI([]);
  95. return;
  96. }
  97. const usedCount = renderedPurchases.filter((purchase) => purchase.used).length;
  98. const reusableCount = renderedPurchases.filter((purchase) => purchase.reusable).length;
  99. const disableUsedCount = renderedPurchases.filter((purchase) => purchase.used && !purchase.preserved && !purchase.disabled).length;
  100. dom.luckmailSummary.textContent = `已加载 ${renderedPurchases.length} 个 openai 邮箱,其中 ${reusableCount} 个可复用,${usedCount} 个已本地标记为已用。`;
  101. if (dom.btnLuckmailDisableUsed) {
  102. dom.btnLuckmailDisableUsed.textContent = `禁用已用${disableUsedCount > 0 ? `(${disableUsedCount})` : ''}`;
  103. dom.btnLuckmailDisableUsed.disabled = disableUsedCount === 0;
  104. }
  105. const visiblePurchases = getFilteredLuckmailPurchases(renderedPurchases);
  106. if (!visiblePurchases.length) {
  107. dom.luckmailList.innerHTML = '<div class="luckmail-empty">没有匹配当前筛选条件的 LuckMail 邮箱。</div>';
  108. updateLuckmailBulkUI([]);
  109. return;
  110. }
  111. for (const purchase of visiblePurchases) {
  112. const purchaseId = String(purchase.id);
  113. const item = document.createElement('div');
  114. item.className = `luckmail-item${purchase.current ? ' is-current' : ''}`;
  115. item.innerHTML = `
  116. <input class="luckmail-item-check" type="checkbox" data-action="select" ${selectedPurchaseIds.has(purchaseId) ? 'checked' : ''} />
  117. <div class="luckmail-item-main">
  118. <div class="luckmail-item-email-row">
  119. <div class="luckmail-item-email">${helpers.escapeHtml(purchase.email_address || '(未知邮箱)')}</div>
  120. <button
  121. class="hotmail-copy-btn"
  122. type="button"
  123. data-action="copy-email"
  124. title="复制邮箱"
  125. aria-label="复制邮箱 ${helpers.escapeHtml(purchase.email_address || '')}"
  126. >${copyIcon}</button>
  127. </div>
  128. <div class="luckmail-item-meta">
  129. <span class="luckmail-tag">${helpers.escapeHtml(helpers.normalizeLuckmailProjectName(purchase.project_name) || 'openai')}</span>
  130. ${purchase.reusable ? '<span class="luckmail-tag active">可复用</span>' : ''}
  131. ${purchase.current ? '<span class="luckmail-tag current">当前</span>' : ''}
  132. ${purchase.used ? '<span class="luckmail-tag used">已用</span>' : ''}
  133. ${purchase.preserved ? '<span class="luckmail-tag">保留</span>' : ''}
  134. ${purchase.disabled ? '<span class="luckmail-tag disabled">已禁用</span>' : ''}
  135. ${purchase.tag_name && normalizeLuckmailSearchText(purchase.tag_name) !== normalizeLuckmailSearchText(helpers.getLuckmailPreserveTagName())
  136. ? `<span class="luckmail-tag">${helpers.escapeHtml(purchase.tag_name)}</span>`
  137. : ''}
  138. </div>
  139. <div class="luckmail-item-details">
  140. <span>ID:${helpers.escapeHtml(String(purchase.id || ''))}</span>
  141. <span>保修至:${helpers.escapeHtml(helpers.formatLuckmailDateTime(purchase.warranty_until))}</span>
  142. </div>
  143. </div>
  144. <div class="luckmail-item-actions">
  145. <button class="btn btn-outline btn-xs" type="button" data-action="use">使用此邮箱</button>
  146. <button class="btn btn-outline btn-xs" type="button" data-action="toggle-used">${helpers.escapeHtml(purchase.used ? '标记未用' : '标记已用')}</button>
  147. <button class="btn btn-outline btn-xs" type="button" data-action="toggle-preserved">${helpers.escapeHtml(purchase.preserved ? '取消保留' : '保留')}</button>
  148. <button class="btn btn-outline btn-xs" type="button" data-action="toggle-disabled">${helpers.escapeHtml(purchase.disabled ? '启用' : '禁用')}</button>
  149. </div>
  150. `;
  151. item.querySelector('[data-action="select"]').addEventListener('change', (event) => {
  152. if (event.target.checked) {
  153. selectedPurchaseIds.add(purchaseId);
  154. } else {
  155. selectedPurchaseIds.delete(purchaseId);
  156. }
  157. updateLuckmailBulkUI(visiblePurchases);
  158. });
  159. item.querySelector('[data-action="copy-email"]').addEventListener('click', async () => {
  160. await helpers.copyTextToClipboard(purchase.email_address || '');
  161. helpers.showToast('邮箱已复制', 'success', 1600);
  162. });
  163. item.querySelector('[data-action="use"]').addEventListener('click', async () => {
  164. await selectSingleLuckmailPurchase(purchase);
  165. });
  166. item.querySelector('[data-action="toggle-used"]').addEventListener('click', async () => {
  167. await setSingleLuckmailPurchaseUsedState(purchase, !purchase.used);
  168. });
  169. item.querySelector('[data-action="toggle-preserved"]').addEventListener('click', async () => {
  170. await setSingleLuckmailPurchasePreservedState(purchase, !purchase.preserved);
  171. });
  172. item.querySelector('[data-action="toggle-disabled"]').addEventListener('click', async () => {
  173. await setSingleLuckmailPurchaseDisabledState(purchase, !purchase.disabled);
  174. });
  175. dom.luckmailList.appendChild(item);
  176. }
  177. updateLuckmailBulkUI(visiblePurchases);
  178. }
  179. async function refreshLuckmailPurchases(options = {}) {
  180. const { silent = false } = options;
  181. if (!dom.luckmailSection || dom.luckmailSection.style.display === 'none') {
  182. return;
  183. }
  184. if (!silent) setLuckmailLoadingState(true, '正在加载 LuckMail openai 邮箱...');
  185. try {
  186. const response = await runtime.sendMessage({
  187. type: 'LIST_LUCKMAIL_PURCHASES',
  188. source: 'sidepanel',
  189. payload: {},
  190. });
  191. if (response?.error) throw new Error(response.error);
  192. renderLuckmailPurchases(response?.purchases || []);
  193. } catch (err) {
  194. selectedPurchaseIds.clear();
  195. if (dom.luckmailList) {
  196. dom.luckmailList.innerHTML = '<div class="luckmail-empty">无法加载 LuckMail 邮箱列表。</div>';
  197. }
  198. if (dom.luckmailSummary) {
  199. dom.luckmailSummary.textContent = err.message;
  200. }
  201. updateLuckmailBulkUI([]);
  202. if (!silent) {
  203. helpers.showToast(`LuckMail 邮箱列表加载失败:${err.message}`, 'error');
  204. }
  205. } finally {
  206. setLuckmailLoadingState(false);
  207. }
  208. }
  209. function queueLuckmailPurchaseRefresh() {
  210. if (refreshQueued) return;
  211. refreshQueued = true;
  212. setTimeout(async () => {
  213. refreshQueued = false;
  214. await refreshLuckmailPurchases({ silent: true });
  215. }, 150);
  216. }
  217. async function selectSingleLuckmailPurchase(purchase) {
  218. setLuckmailLoadingState(true, `正在切换到 ${purchase.email_address} ...`);
  219. try {
  220. const response = await runtime.sendMessage({
  221. type: 'SELECT_LUCKMAIL_PURCHASE',
  222. source: 'sidepanel',
  223. payload: { purchaseId: purchase.id },
  224. });
  225. if (response?.error) throw new Error(response.error);
  226. dom.inputEmail.value = response?.purchase?.email_address || purchase.email_address || '';
  227. helpers.showToast(`已切换当前 LuckMail 邮箱为 ${purchase.email_address}`, 'success', 2200);
  228. await refreshLuckmailPurchases({ silent: true });
  229. } catch (err) {
  230. if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
  231. helpers.showToast(`切换 LuckMail 邮箱失败:${err.message}`, 'error');
  232. } finally {
  233. setLuckmailLoadingState(false);
  234. }
  235. }
  236. async function setSingleLuckmailPurchaseUsedState(purchase, used) {
  237. setLuckmailLoadingState(true, `正在更新 ${purchase.email_address} 的已用状态...`);
  238. try {
  239. const response = await runtime.sendMessage({
  240. type: 'SET_LUCKMAIL_PURCHASE_USED_STATE',
  241. source: 'sidepanel',
  242. payload: { purchaseId: purchase.id, used },
  243. });
  244. if (response?.error) throw new Error(response.error);
  245. helpers.showToast(`${purchase.email_address} 已${used ? '标记为已用' : '恢复为未用'}`, 'success', 2200);
  246. await refreshLuckmailPurchases({ silent: true });
  247. } catch (err) {
  248. if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
  249. helpers.showToast(`更新 LuckMail 已用状态失败:${err.message}`, 'error');
  250. } finally {
  251. setLuckmailLoadingState(false);
  252. }
  253. }
  254. async function setSingleLuckmailPurchasePreservedState(purchase, preserved) {
  255. setLuckmailLoadingState(true, `正在更新 ${purchase.email_address} 的保留状态...`);
  256. try {
  257. const response = await runtime.sendMessage({
  258. type: 'SET_LUCKMAIL_PURCHASE_PRESERVED_STATE',
  259. source: 'sidepanel',
  260. payload: { purchaseId: purchase.id, preserved },
  261. });
  262. if (response?.error) throw new Error(response.error);
  263. helpers.showToast(`${purchase.email_address} 已${preserved ? '设为保留' : '取消保留'}`, 'success', 2200);
  264. await refreshLuckmailPurchases({ silent: true });
  265. } catch (err) {
  266. if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
  267. helpers.showToast(`更新 LuckMail 保留状态失败:${err.message}`, 'error');
  268. } finally {
  269. setLuckmailLoadingState(false);
  270. }
  271. }
  272. async function setSingleLuckmailPurchaseDisabledState(purchase, disabled) {
  273. setLuckmailLoadingState(true, `正在${disabled ? '禁用' : '启用'} ${purchase.email_address} ...`);
  274. try {
  275. const response = await runtime.sendMessage({
  276. type: 'SET_LUCKMAIL_PURCHASE_DISABLED_STATE',
  277. source: 'sidepanel',
  278. payload: { purchaseId: purchase.id, disabled },
  279. });
  280. if (response?.error) throw new Error(response.error);
  281. helpers.showToast(`${purchase.email_address} 已${disabled ? '禁用' : '启用'}`, 'success', 2200);
  282. await refreshLuckmailPurchases({ silent: true });
  283. } catch (err) {
  284. if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
  285. helpers.showToast(`更新 LuckMail 禁用状态失败:${err.message}`, 'error');
  286. } finally {
  287. setLuckmailLoadingState(false);
  288. }
  289. }
  290. async function runBulkLuckmailAction(action) {
  291. const selectedIds = renderedPurchases
  292. .filter((purchase) => selectedPurchaseIds.has(String(purchase.id)))
  293. .map((purchase) => purchase.id);
  294. if (!selectedIds.length) {
  295. updateLuckmailBulkUI();
  296. return;
  297. }
  298. const actionLabelMap = {
  299. used: '标记已用',
  300. unused: '标记未用',
  301. preserve: '保留',
  302. unpreserve: '取消保留',
  303. disable: '禁用',
  304. enable: '启用',
  305. };
  306. setLuckmailLoadingState(true, `正在批量${actionLabelMap[action] || '处理'} LuckMail 邮箱...`);
  307. try {
  308. const response = await runtime.sendMessage({
  309. type: 'BATCH_UPDATE_LUCKMAIL_PURCHASES',
  310. source: 'sidepanel',
  311. payload: { action, ids: selectedIds },
  312. });
  313. if (response?.error) throw new Error(response.error);
  314. helpers.showToast(`已批量${actionLabelMap[action] || '处理'} ${selectedIds.length} 个 LuckMail 邮箱`, 'success', 2400);
  315. await refreshLuckmailPurchases({ silent: true });
  316. } catch (err) {
  317. if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
  318. helpers.showToast(`批量处理 LuckMail 邮箱失败:${err.message}`, 'error');
  319. } finally {
  320. setLuckmailLoadingState(false);
  321. updateLuckmailBulkUI();
  322. }
  323. }
  324. async function disableUsedLuckmailPurchases() {
  325. const confirmed = await helpers.openConfirmModal({
  326. title: '禁用已用 LuckMail 邮箱',
  327. message: '确认禁用所有本地已用且未保留的 openai LuckMail 邮箱吗?',
  328. confirmLabel: '确认禁用',
  329. confirmVariant: 'btn-danger',
  330. });
  331. if (!confirmed) {
  332. return;
  333. }
  334. setLuckmailLoadingState(true, '正在禁用已用 LuckMail 邮箱...');
  335. try {
  336. const response = await runtime.sendMessage({
  337. type: 'DISABLE_USED_LUCKMAIL_PURCHASES',
  338. source: 'sidepanel',
  339. payload: {},
  340. });
  341. if (response?.error) throw new Error(response.error);
  342. const disabledCount = Array.isArray(response?.disabledIds) ? response.disabledIds.length : 0;
  343. helpers.showToast(`已禁用 ${disabledCount} 个 LuckMail 邮箱`, disabledCount > 0 ? 'success' : 'info', 2400);
  344. await refreshLuckmailPurchases({ silent: true });
  345. } catch (err) {
  346. if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
  347. helpers.showToast(`禁用已用 LuckMail 邮箱失败:${err.message}`, 'error');
  348. } finally {
  349. setLuckmailLoadingState(false);
  350. }
  351. }
  352. function reset() {
  353. renderedPurchases = [];
  354. selectedPurchaseIds.clear();
  355. searchTerm = '';
  356. filterMode = 'all';
  357. refreshQueued = false;
  358. if (dom.inputLuckmailSearch) dom.inputLuckmailSearch.value = '';
  359. if (dom.selectLuckmailFilter) dom.selectLuckmailFilter.value = 'all';
  360. if (dom.luckmailList) dom.luckmailList.innerHTML = '';
  361. if (dom.luckmailSummary) dom.luckmailSummary.textContent = '加载已购邮箱后可在这里管理 openai 项目的 LuckMail 邮箱。';
  362. if (dom.btnLuckmailDisableUsed) dom.btnLuckmailDisableUsed.disabled = true;
  363. updateLuckmailBulkUI([]);
  364. }
  365. function bindLuckmailEvents() {
  366. dom.inputLuckmailSearch?.addEventListener('input', (event) => {
  367. searchTerm = event.target.value || '';
  368. renderLuckmailPurchases(renderedPurchases);
  369. });
  370. dom.selectLuckmailFilter?.addEventListener('change', (event) => {
  371. filterMode = String(event.target.value || 'all').trim() || 'all';
  372. renderLuckmailPurchases(renderedPurchases);
  373. });
  374. dom.checkboxLuckmailSelectAll?.addEventListener('change', () => {
  375. const visiblePurchases = getFilteredLuckmailPurchases();
  376. if (dom.checkboxLuckmailSelectAll.checked) {
  377. visiblePurchases.forEach((purchase) => selectedPurchaseIds.add(String(purchase.id)));
  378. } else {
  379. visiblePurchases.forEach((purchase) => selectedPurchaseIds.delete(String(purchase.id)));
  380. }
  381. renderLuckmailPurchases(renderedPurchases);
  382. });
  383. dom.btnLuckmailRefresh?.addEventListener('click', async () => {
  384. await refreshLuckmailPurchases();
  385. });
  386. dom.btnLuckmailDisableUsed?.addEventListener('click', async () => {
  387. await disableUsedLuckmailPurchases();
  388. });
  389. dom.btnLuckmailBulkUsed?.addEventListener('click', async () => {
  390. await runBulkLuckmailAction('used');
  391. });
  392. dom.btnLuckmailBulkUnused?.addEventListener('click', async () => {
  393. await runBulkLuckmailAction('unused');
  394. });
  395. dom.btnLuckmailBulkPreserve?.addEventListener('click', async () => {
  396. await runBulkLuckmailAction('preserve');
  397. });
  398. dom.btnLuckmailBulkUnpreserve?.addEventListener('click', async () => {
  399. await runBulkLuckmailAction('unpreserve');
  400. });
  401. dom.btnLuckmailBulkDisable?.addEventListener('click', async () => {
  402. await runBulkLuckmailAction('disable');
  403. });
  404. dom.btnLuckmailBulkEnable?.addEventListener('click', async () => {
  405. await runBulkLuckmailAction('enable');
  406. });
  407. }
  408. return {
  409. bindLuckmailEvents,
  410. disableUsedLuckmailPurchases,
  411. queueLuckmailPurchaseRefresh,
  412. refreshLuckmailPurchases,
  413. renderLuckmailPurchases,
  414. reset,
  415. };
  416. }
  417. globalScope.SidepanelLuckmailManager = {
  418. createLuckmailManager,
  419. };
  420. })(window);