account-records-manager.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. (function attachSidepanelAccountRecordsManager(globalScope) {
  2. function createAccountRecordsManager(context = {}) {
  3. const {
  4. state,
  5. dom,
  6. helpers,
  7. runtime,
  8. constants = {},
  9. } = context;
  10. const displayTimeZone = constants.displayTimeZone || 'Asia/Shanghai';
  11. const pageSize = Math.max(1, Math.floor(Number(constants.pageSize) || 10));
  12. let currentPage = 1;
  13. function escapeHtml(value) {
  14. if (typeof helpers.escapeHtml === 'function') {
  15. return helpers.escapeHtml(String(value || ''));
  16. }
  17. return String(value || '');
  18. }
  19. function normalizeTimestamp(value) {
  20. const timestamp = Date.parse(String(value || ''));
  21. return Number.isFinite(timestamp) ? timestamp : 0;
  22. }
  23. function getAccountRunRecords(currentState = state.getLatestState()) {
  24. return (Array.isArray(currentState?.accountRunHistory) ? currentState.accountRunHistory : [])
  25. .filter((item) => item && typeof item === 'object')
  26. .slice()
  27. .sort((left, right) => normalizeTimestamp(right.finishedAt) - normalizeTimestamp(left.finishedAt));
  28. }
  29. function summarizeAccountRunHistory(records = []) {
  30. return records.reduce((summary, record) => {
  31. summary.total += 1;
  32. if (record.finalStatus === 'success') {
  33. summary.success += 1;
  34. } else if (record.finalStatus === 'failed') {
  35. summary.failed += 1;
  36. }
  37. summary.retryTotal += Math.max(0, Math.floor(Number(record.retryCount) || 0));
  38. return summary;
  39. }, {
  40. total: 0,
  41. success: 0,
  42. failed: 0,
  43. retryTotal: 0,
  44. });
  45. }
  46. function formatAccountRecordTime(value) {
  47. const date = new Date(value);
  48. if (Number.isNaN(date.getTime())) {
  49. return '--:--';
  50. }
  51. const now = new Date();
  52. const sameYear = date.getFullYear() == now.getFullYear();
  53. const sameDay = date.toDateString() === now.toDateString();
  54. if (sameDay) {
  55. return date.toLocaleTimeString('zh-CN', {
  56. hour12: false,
  57. hour: '2-digit',
  58. minute: '2-digit',
  59. timeZone: displayTimeZone,
  60. });
  61. }
  62. return date.toLocaleString('zh-CN', {
  63. hour12: false,
  64. month: '2-digit',
  65. day: '2-digit',
  66. hour: '2-digit',
  67. minute: '2-digit',
  68. ...(sameYear ? {} : { year: '2-digit' }),
  69. timeZone: displayTimeZone,
  70. }).replace(/\//g, '-');
  71. }
  72. function getStatusMeta(record = {}) {
  73. return record.finalStatus === 'success'
  74. ? { kind: 'success', label: '成功' }
  75. : { kind: 'failed', label: '失败' };
  76. }
  77. function getRecordSummaryText(record = {}) {
  78. if (record.finalStatus === 'success') {
  79. return '流程完成';
  80. }
  81. return String(record.failureLabel || '').trim() || '流程失败';
  82. }
  83. function createStatChip(label, value, className = '') {
  84. return `<span class="account-records-stat${className ? ` ${className}` : ''}"><strong>${escapeHtml(String(value))}</strong>${escapeHtml(label)}</span>`;
  85. }
  86. function updateHeader(records) {
  87. if (!dom.accountRecordsMeta || !dom.accountRecordsStats) {
  88. return;
  89. }
  90. if (!records.length) {
  91. dom.accountRecordsMeta.textContent = '暂无邮箱记录';
  92. } else {
  93. dom.accountRecordsMeta.textContent = `共 ${records.length} 条,最近更新于 ${formatAccountRecordTime(records[0]?.finishedAt)}`;
  94. }
  95. const summary = summarizeAccountRunHistory(records);
  96. dom.accountRecordsStats.innerHTML = [
  97. createStatChip('总', summary.total),
  98. createStatChip('成', summary.success, 'is-success'),
  99. createStatChip('失', summary.failed, 'is-failed'),
  100. createStatChip('重试', summary.retryTotal, 'is-retry'),
  101. ].join('');
  102. }
  103. function updatePagination(totalRecords) {
  104. const totalPages = totalRecords > 0 ? Math.ceil(totalRecords / pageSize) : 0;
  105. if (totalPages === 0) {
  106. currentPage = 1;
  107. } else if (currentPage > totalPages) {
  108. currentPage = totalPages;
  109. } else if (currentPage < 1) {
  110. currentPage = 1;
  111. }
  112. if (dom.accountRecordsPageLabel) {
  113. dom.accountRecordsPageLabel.textContent = totalPages > 0 ? `${currentPage} / ${totalPages}` : '0 / 0';
  114. }
  115. if (dom.btnAccountRecordsPrev) {
  116. dom.btnAccountRecordsPrev.disabled = totalPages <= 1 || currentPage <= 1;
  117. }
  118. if (dom.btnAccountRecordsNext) {
  119. dom.btnAccountRecordsNext.disabled = totalPages <= 1 || currentPage >= totalPages;
  120. }
  121. if (dom.btnClearAccountRecords) {
  122. dom.btnClearAccountRecords.disabled = totalRecords === 0;
  123. }
  124. return totalPages;
  125. }
  126. function renderRecordList(records = []) {
  127. if (!dom.accountRecordsList) {
  128. return;
  129. }
  130. const totalPages = updatePagination(records.length);
  131. if (!records.length) {
  132. dom.accountRecordsList.innerHTML = '<div class="account-records-empty">暂无邮箱记录。</div>';
  133. return;
  134. }
  135. const startIndex = (currentPage - 1) * pageSize;
  136. const visibleRecords = records.slice(startIndex, startIndex + pageSize);
  137. dom.accountRecordsList.innerHTML = visibleRecords.map((record) => {
  138. const statusMeta = getStatusMeta(record);
  139. const summaryText = getRecordSummaryText(record);
  140. const retryCount = Math.max(0, Math.floor(Number(record.retryCount) || 0));
  141. return `
  142. <div class="account-record-item is-${escapeHtml(statusMeta.kind)}" title="${escapeHtml(String(record.email || ''))}">
  143. <div class="account-record-item-top">
  144. <div class="account-record-item-email mono">${escapeHtml(String(record.email || '').trim() || '(空邮箱)')}</div>
  145. <div class="account-record-item-side">
  146. <span class="account-record-item-status">${escapeHtml(statusMeta.label)}</span>
  147. <span class="account-record-item-time mono">${escapeHtml(formatAccountRecordTime(record.finishedAt))}</span>
  148. </div>
  149. </div>
  150. <div class="account-record-item-bottom">
  151. <div class="account-record-item-summary">${escapeHtml(summaryText)}</div>
  152. <span class="account-record-item-retry mono">重试 ${escapeHtml(String(retryCount))}</span>
  153. </div>
  154. </div>
  155. `;
  156. }).join('');
  157. if (totalPages <= 1 && dom.accountRecordsPageLabel) {
  158. dom.accountRecordsPageLabel.textContent = '1 / 1';
  159. }
  160. }
  161. function render(currentState = state.getLatestState()) {
  162. const records = getAccountRunRecords(currentState);
  163. updateHeader(records);
  164. renderRecordList(records);
  165. }
  166. function openPanel() {
  167. if (dom.accountRecordsOverlay) {
  168. dom.accountRecordsOverlay.hidden = false;
  169. }
  170. render();
  171. }
  172. function closePanel() {
  173. if (dom.accountRecordsOverlay) {
  174. dom.accountRecordsOverlay.hidden = true;
  175. }
  176. }
  177. async function clearRecords() {
  178. const records = getAccountRunRecords();
  179. if (!records.length) {
  180. helpers.showToast?.('没有可清理的邮箱记录。', 'warn', 1800);
  181. return;
  182. }
  183. const confirmed = await helpers.openConfirmModal({
  184. title: '清理邮箱记录',
  185. message: '确认清理当前全部邮箱记录吗?该操作会同时清空面板记录与本地同步快照。',
  186. confirmLabel: '确认清理',
  187. confirmVariant: 'btn-danger',
  188. });
  189. if (!confirmed) {
  190. return;
  191. }
  192. const response = await runtime.sendMessage({
  193. type: 'CLEAR_ACCOUNT_RUN_HISTORY',
  194. source: 'sidepanel',
  195. });
  196. if (response?.error) {
  197. throw new Error(response.error);
  198. }
  199. currentPage = 1;
  200. state.syncLatestState({ accountRunHistory: [] });
  201. helpers.showToast?.(`已清理 ${Math.max(0, Number(response?.clearedCount) || 0)} 条邮箱记录`, 'success', 2200);
  202. }
  203. function bindEvents() {
  204. dom.btnOpenAccountRecords?.addEventListener('click', () => {
  205. openPanel();
  206. });
  207. dom.btnCloseAccountRecords?.addEventListener('click', () => {
  208. closePanel();
  209. });
  210. dom.accountRecordsOverlay?.addEventListener('click', (event) => {
  211. if (event.target === dom.accountRecordsOverlay) {
  212. closePanel();
  213. }
  214. });
  215. dom.btnAccountRecordsPrev?.addEventListener('click', () => {
  216. if (currentPage <= 1) {
  217. return;
  218. }
  219. currentPage -= 1;
  220. render();
  221. });
  222. dom.btnAccountRecordsNext?.addEventListener('click', () => {
  223. currentPage += 1;
  224. render();
  225. });
  226. dom.btnClearAccountRecords?.addEventListener('click', () => {
  227. clearRecords().catch((error) => {
  228. helpers.showToast?.(`清理邮箱记录失败:${error.message}`, 'error');
  229. });
  230. });
  231. }
  232. function reset() {
  233. currentPage = 1;
  234. closePanel();
  235. render();
  236. }
  237. return {
  238. bindEvents,
  239. closePanel,
  240. openPanel,
  241. render,
  242. reset,
  243. summarizeAccountRunHistory,
  244. };
  245. }
  246. globalScope.SidepanelAccountRecordsManager = {
  247. createAccountRecordsManager,
  248. };
  249. })(typeof window !== 'undefined' ? window : globalThis);