account-records-manager.js 9.4 KB

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