ZHIWEI666 commited on
Commit
7b8e13b
·
verified ·
1 Parent(s): 786c475

Upload 103 files

Browse files
前端页面/components/UI交互提示组件.js CHANGED
@@ -183,7 +183,7 @@ function _setupDialogAccessibility(box, titleId, descriptionId, initialFocusSele
183
 
184
  const getFocusable = () => Array.from(box.querySelectorAll(
185
  'button:not(:disabled), [href], input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])'
186
- )).filter(element => !element.closest('[inert], [hidden], [aria-hidden="true"]')
187
  && element.getClientRects().length > 0 && getComputedStyle(element).visibility !== "hidden");
188
 
189
  const handleKeydown = event => {
@@ -202,17 +202,19 @@ function _setupDialogAccessibility(box, titleId, descriptionId, initialFocusSele
202
  }
203
  const first = focusable[0];
204
  const last = focusable[focusable.length - 1];
205
- if (event.shiftKey && (document.activeElement === first || !box.contains(document.activeElement))) {
206
  event.preventDefault();
207
  last.focus();
208
- } else if (!event.shiftKey && (document.activeElement === last || !box.contains(document.activeElement))) {
209
  event.preventDefault();
210
  first.focus();
211
  }
212
  };
213
  dialogStack.push(box);
214
  document.addEventListener("keydown", handleKeydown);
215
- const initialFocus = box.querySelector(initialFocusSelector) || getFocusable()[0] || box;
 
 
216
  initialFocus.focus();
217
 
218
  return {
 
183
 
184
  const getFocusable = () => Array.from(box.querySelectorAll(
185
  'button:not(:disabled), [href], input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])'
186
+ )).filter(element => element.tabIndex >= 0 && !element.closest('[inert], [hidden], [aria-hidden="true"]')
187
  && element.getClientRects().length > 0 && getComputedStyle(element).visibility !== "hidden");
188
 
189
  const handleKeydown = event => {
 
202
  }
203
  const first = focusable[0];
204
  const last = focusable[focusable.length - 1];
205
+ if (event.shiftKey && (document.activeElement === first || !focusable.includes(document.activeElement))) {
206
  event.preventDefault();
207
  last.focus();
208
+ } else if (!event.shiftKey && (document.activeElement === last || !focusable.includes(document.activeElement))) {
209
  event.preventDefault();
210
  first.focus();
211
  }
212
  };
213
  dialogStack.push(box);
214
  document.addEventListener("keydown", handleKeydown);
215
+ const focusable = getFocusable();
216
+ const requestedFocus = box.querySelector(initialFocusSelector);
217
+ const initialFocus = focusable.includes(requestedFocus) ? requestedFocus : focusable[0] || box;
218
  initialFocus.focus();
219
 
220
  return {
前端页面/components/全局设置组件.js CHANGED
@@ -116,6 +116,10 @@ export function createSettingsView() {
116
  const styleSheet = document.createElement("style");
117
  styleSheet.textContent = `
118
  .setting-switch {
 
 
 
 
119
  position: relative;
120
  width: 44px;
121
  height: 24px;
@@ -125,6 +129,10 @@ export function createSettingsView() {
125
  transition: 0.3s;
126
  flex-shrink: 0;
127
  }
 
 
 
 
128
  .setting-switch.active {
129
  background: #4CAF50;
130
  }
@@ -277,10 +285,10 @@ export function createSettingsView() {
277
  <!-- 设置项:界面语言 -->
278
  <div class="setting-item">
279
  <div style="flex: 1;">
280
- <div style="color: #fff; font-size: 14px; margin-bottom: 4px; font-weight: 500;">${t('settings.language')} / Language</div>
281
  <div style="color: #888; font-size: 12px; line-height: 1.4;">${t('settings.language_desc')} / Select display language</div>
282
  </div>
283
- <select id="select-language" class="setting-select">
284
  ${LANGUAGE_OPTIONS.map(opt =>
285
  `<option value="${opt.value}" ${(settings.language || 'zh-CN') === opt.value ? 'selected' : ''}>${opt.label}</option>`
286
  ).join('')}
@@ -295,10 +303,10 @@ export function createSettingsView() {
295
  <!-- 设置项:创作者卡片背景图 -->
296
  <div class="setting-item">
297
  <div style="flex: 1;">
298
- <div style="color: #fff; font-size: 14px; margin-bottom: 4px; font-weight: 500;">${t('settings.creator_banner')}</div>
299
- <div style="color: #888; font-size: 12px; line-height: 1.4;">${t('settings.creator_banner_desc')}</div>
300
  </div>
301
- <div id="switch-creator-banner" class="setting-switch ${settings.showCreatorBanner ? 'active' : ''}"></div>
302
  </div>
303
 
304
  <!-- 分类:更多功能 -->
@@ -309,19 +317,19 @@ export function createSettingsView() {
309
  <!-- 设置项:榜单切换动画 -->
310
  <div class="setting-item">
311
  <div style="flex: 1;">
312
- <div style="color: #fff; font-size: 14px; margin-bottom: 4px; font-weight: 500;">${t('settings.animations')}</div>
313
- <div style="color: #888; font-size: 12px; line-height: 1.4;">${t('settings.animations_desc')}</div>
314
  </div>
315
- <div id="switch-animations" class="setting-switch ${settings.enableAnimations ? 'active' : ''}"></div>
316
  </div>
317
 
318
  <!-- 设置项:动画音效 -->
319
  <div class="setting-item">
320
  <div style="flex: 1;">
321
- <div style="color: #fff; font-size: 14px; margin-bottom: 4px; font-weight: 500;">${t('settings.sound')}</div>
322
- <div style="color: #888; font-size: 12px; line-height: 1.4;">${t('settings.sound_desc')}</div>
323
  </div>
324
- <div id="switch-sound-effects" class="setting-switch ${settings.enableSoundEffects ? 'active' : ''}"></div>
325
  </div>
326
 
327
  <!-- 分类:数据与缓存 -->
@@ -332,16 +340,16 @@ export function createSettingsView() {
332
  <!-- 设置项:缓存刷新间隔(滑条) -->
333
  <div class="setting-item" style="flex-direction: column; align-items: flex-start; gap: 12px;">
334
  <div style="flex: 1; width: 100%;">
335
- <div style="color: #fff; font-size: 14px; margin-bottom: 4px; font-weight: 500;">${t('settings.cache_expire')}</div>
336
  <div style="color: #888; font-size: 12px; line-height: 1.4;">${t('settings.cache_expire_desc')}</div>
337
  </div>
338
  <div style="display: flex; flex-direction: column; gap: 10px; width: 100%;">
339
  <div style="display: flex; align-items: center; gap: 12px; width: 100%;">
340
- <input type="range" id="slider-cache-expire" class="setting-range-slider"
341
  value="${settings.cacheExpireSeconds || 7200}"
342
  min="60" max="86400" step="60"
343
  style="flex: 1;">
344
- <input type="number" id="input-cache-expire" class="setting-number-input"
345
  value="${settings.cacheExpireSeconds || 7200}"
346
  min="60" max="86400" step="60">
347
  <span id="cache-expire-label" style="color: #4CAF50; font-size: 12px; min-width: 55px; white-space: nowrap;"></span>
@@ -404,9 +412,11 @@ export function createSettingsView() {
404
  */
405
  function _bindSwitchHandler(selector, settingKey, labelKey, iconOn = '✅') {
406
  const switchEl = container.querySelector(selector);
 
407
  switchEl.onclick = () => {
408
  const newValue = !switchEl.classList.contains("active");
409
  switchEl.classList.toggle("active", newValue);
 
410
  const currentSettings = getSettings();
411
  currentSettings[settingKey] = newValue;
412
  saveSettings(currentSettings);
@@ -421,8 +431,15 @@ export function createSettingsView() {
421
 
422
  // 🌐 下拉控制:界面语言
423
  const selectLanguage = container.querySelector("#select-language");
 
 
424
  selectLanguage.onchange = () => {
 
425
  const newLang = selectLanguage.value;
 
 
 
 
426
 
427
  const currentSettings = getSettings();
428
  currentSettings.language = newLang;
@@ -430,19 +447,28 @@ export function createSettingsView() {
430
 
431
  // 同步到 i18n 模块
432
  setLanguage(newLang);
 
433
 
434
  const langName = LANGUAGE_OPTIONS.find(opt => opt.value === newLang)?.label || newLang;
435
  showToast(`🌐 ${t('settings.language_changed')} ${langName.split(' / ')[0]}`, "success");
436
 
437
- // 🔄 通过路由系统刷新,保持 activeInlineView 同步
438
- setTimeout(() => {
439
- // 先返回主页
440
- window.dispatchEvent(new CustomEvent("comfy-route-back"));
441
- // 再重新打开设置页
442
- setTimeout(() => {
443
- const newSettingsView = createSettingsView();
444
- window.dispatchEvent(new CustomEvent("comfy-route-view", { detail: { view: newSettingsView } }));
445
- }, 50);
 
 
 
 
 
 
 
 
446
  }, 100);
447
  };
448
 
@@ -520,6 +546,9 @@ export function createSettingsView() {
520
  container._cleanup = () => {
521
  if (cacheViewDisposed) return;
522
  cacheViewDisposed = true;
 
 
 
523
  statsGeneration++;
524
  statsController?.abort();
525
  statsController = null;
 
116
  const styleSheet = document.createElement("style");
117
  styleSheet.textContent = `
118
  .setting-switch {
119
+ color: inherit;
120
+ appearance: none;
121
+ border: 0;
122
+ padding: 0;
123
  position: relative;
124
  width: 44px;
125
  height: 24px;
 
129
  transition: 0.3s;
130
  flex-shrink: 0;
131
  }
132
+ .setting-switch:focus-visible {
133
+ outline: 2px solid currentColor;
134
+ outline-offset: 3px;
135
+ }
136
  .setting-switch.active {
137
  background: #4CAF50;
138
  }
 
285
  <!-- 设置项:界面语言 -->
286
  <div class="setting-item">
287
  <div style="flex: 1;">
288
+ <div id="label-language" style="color: #fff; font-size: 14px; margin-bottom: 4px; font-weight: 500;">${t('settings.language')} / Language</div>
289
  <div style="color: #888; font-size: 12px; line-height: 1.4;">${t('settings.language_desc')} / Select display language</div>
290
  </div>
291
+ <select aria-labelledby="label-language" id="select-language" class="setting-select">
292
  ${LANGUAGE_OPTIONS.map(opt =>
293
  `<option value="${opt.value}" ${(settings.language || 'zh-CN') === opt.value ? 'selected' : ''}>${opt.label}</option>`
294
  ).join('')}
 
303
  <!-- 设置项:创作者卡片背景图 -->
304
  <div class="setting-item">
305
  <div style="flex: 1;">
306
+ <div id="label-creator-banner" style="color: #fff; font-size: 14px; margin-bottom: 4px; font-weight: 500;">${t('settings.creator_banner')}</div>
307
+ <div id="desc-creator-banner" style="color: #888; font-size: 12px; line-height: 1.4;">${t('settings.creator_banner_desc')}</div>
308
  </div>
309
+ <button type="button" role="switch" id="switch-creator-banner" aria-labelledby="label-creator-banner" aria-describedby="desc-creator-banner" class="setting-switch ${settings.showCreatorBanner ? 'active' : ''}"></button>
310
  </div>
311
 
312
  <!-- 分类:更多功能 -->
 
317
  <!-- 设置项:榜单切换动画 -->
318
  <div class="setting-item">
319
  <div style="flex: 1;">
320
+ <div id="label-animations" style="color: #fff; font-size: 14px; margin-bottom: 4px; font-weight: 500;">${t('settings.animations')}</div>
321
+ <div id="desc-animations" style="color: #888; font-size: 12px; line-height: 1.4;">${t('settings.animations_desc')}</div>
322
  </div>
323
+ <button type="button" role="switch" id="switch-animations" aria-labelledby="label-animations" aria-describedby="desc-animations" class="setting-switch ${settings.enableAnimations ? 'active' : ''}"></button>
324
  </div>
325
 
326
  <!-- 设置项:动画音效 -->
327
  <div class="setting-item">
328
  <div style="flex: 1;">
329
+ <div id="label-sound-effects" style="color: #fff; font-size: 14px; margin-bottom: 4px; font-weight: 500;">${t('settings.sound')}</div>
330
+ <div id="desc-sound-effects" style="color: #888; font-size: 12px; line-height: 1.4;">${t('settings.sound_desc')}</div>
331
  </div>
332
+ <button type="button" role="switch" id="switch-sound-effects" aria-labelledby="label-sound-effects" aria-describedby="desc-sound-effects" class="setting-switch ${settings.enableSoundEffects ? 'active' : ''}"></button>
333
  </div>
334
 
335
  <!-- 分类:数据与缓存 -->
 
340
  <!-- 设置项:缓存刷新间隔(滑条) -->
341
  <div class="setting-item" style="flex-direction: column; align-items: flex-start; gap: 12px;">
342
  <div style="flex: 1; width: 100%;">
343
+ <div id="label-cache-expire" style="color: #fff; font-size: 14px; margin-bottom: 4px; font-weight: 500;">${t('settings.cache_expire')}</div>
344
  <div style="color: #888; font-size: 12px; line-height: 1.4;">${t('settings.cache_expire_desc')}</div>
345
  </div>
346
  <div style="display: flex; flex-direction: column; gap: 10px; width: 100%;">
347
  <div style="display: flex; align-items: center; gap: 12px; width: 100%;">
348
+ <input aria-labelledby="label-cache-expire" type="range" id="slider-cache-expire" class="setting-range-slider"
349
  value="${settings.cacheExpireSeconds || 7200}"
350
  min="60" max="86400" step="60"
351
  style="flex: 1;">
352
+ <input aria-labelledby="label-cache-expire" type="number" id="input-cache-expire" class="setting-number-input"
353
  value="${settings.cacheExpireSeconds || 7200}"
354
  min="60" max="86400" step="60">
355
  <span id="cache-expire-label" style="color: #4CAF50; font-size: 12px; min-width: 55px; white-space: nowrap;"></span>
 
412
  */
413
  function _bindSwitchHandler(selector, settingKey, labelKey, iconOn = '✅') {
414
  const switchEl = container.querySelector(selector);
415
+ switchEl.setAttribute("aria-checked", String(switchEl.classList.contains("active")));
416
  switchEl.onclick = () => {
417
  const newValue = !switchEl.classList.contains("active");
418
  switchEl.classList.toggle("active", newValue);
419
+ switchEl.setAttribute("aria-checked", String(newValue));
420
  const currentSettings = getSettings();
421
  currentSettings[settingKey] = newValue;
422
  saveSettings(currentSettings);
 
431
 
432
  // 🌐 下拉控制:界面语言
433
  const selectLanguage = container.querySelector("#select-language");
434
+ let languageTimer = null;
435
+ let languageGeneration = 0;
436
  selectLanguage.onchange = () => {
437
+ if (cacheViewDisposed || !container.isConnected) return;
438
  const newLang = selectLanguage.value;
439
+ if (!LANGUAGE_OPTIONS.some(option => option.value === newLang)) return;
440
+ const generation = ++languageGeneration;
441
+ clearTimeout(languageTimer);
442
+ languageTimer = null;
443
 
444
  const currentSettings = getSettings();
445
  currentSettings.language = newLang;
 
447
 
448
  // 同步到 i18n 模块
449
  setLanguage(newLang);
450
+ if (cacheViewDisposed || generation !== languageGeneration || !container.isConnected) return;
451
 
452
  const langName = LANGUAGE_OPTIONS.find(opt => opt.value === newLang)?.label || newLang;
453
  showToast(`🌐 ${t('settings.language_changed')} ${langName.split(' / ')[0]}`, "success");
454
 
455
+ languageTimer = setTimeout(() => {
456
+ languageTimer = null;
457
+ if (cacheViewDisposed || generation !== languageGeneration || !container.isConnected) return;
458
+ const restoreFocus = document.activeElement === selectLanguage;
459
+ const newSettingsView = createSettingsView();
460
+ if (cacheViewDisposed || generation !== languageGeneration || !container.isConnected) {
461
+ newSettingsView._cleanup?.();
462
+ return;
463
+ }
464
+ window.dispatchEvent(new CustomEvent("comfy-route-view", { detail: { view: newSettingsView } }));
465
+ if (!newSettingsView.isConnected) {
466
+ newSettingsView._cleanup?.();
467
+ return;
468
+ }
469
+ if (restoreFocus && (document.activeElement === document.body || document.activeElement === selectLanguage)) {
470
+ newSettingsView.querySelector("#select-language")?.focus({ preventScroll: true });
471
+ }
472
  }, 100);
473
  };
474
 
 
546
  container._cleanup = () => {
547
  if (cacheViewDisposed) return;
548
  cacheViewDisposed = true;
549
+ languageGeneration++;
550
+ clearTimeout(languageTimer);
551
+ languageTimer = null;
552
  statsGeneration++;
553
  statsController?.abort();
554
  statsController = null;
前端页面/components/性能优化工具.js CHANGED
@@ -574,10 +574,23 @@ export function lazyLoadImages(container, selector = "img") {
574
  */
575
  export function debounce(fn, delay = 300) {
576
  let timer = null;
577
- return function(...args) {
578
- if (timer) clearTimeout(timer);
579
- timer = setTimeout(() => fn.apply(this, args), delay);
 
 
 
 
 
 
580
  };
 
 
 
 
 
 
 
581
  }
582
 
583
  /**
@@ -1012,10 +1025,14 @@ export function createVirtualScroller(options) {
1012
  };
1013
 
1014
  // 窗口尺寸变化
1015
- const handleResize = debounce(() => {
 
1016
  _containerHeight = container.clientHeight;
1017
  render();
1018
  }, 200);
 
 
 
1019
 
1020
  // 初始化
1021
  const init = () => {
@@ -1027,6 +1044,7 @@ export function createVirtualScroller(options) {
1027
  // 销毁
1028
  const destroy = () => {
1029
  _isDestroyed = true;
 
1030
  container.removeEventListener("scroll", handleScroll);
1031
  window.removeEventListener("resize", handleResize);
1032
  if (rafId) cancelAnimationFrame(rafId);
 
574
  */
575
  export function debounce(fn, delay = 300) {
576
  let timer = null;
577
+ let generation = 0;
578
+ const debounced = function(...args) {
579
+ const current = ++generation;
580
+ if (timer !== null) clearTimeout(timer);
581
+ timer = setTimeout(() => {
582
+ if (current !== generation) return;
583
+ timer = null;
584
+ fn.apply(this, args);
585
+ }, delay);
586
  };
587
+ debounced.cancel = () => {
588
+ generation++;
589
+ const pending = timer;
590
+ timer = null;
591
+ if (pending !== null) clearTimeout(pending);
592
+ };
593
+ return debounced;
594
  }
595
 
596
  /**
 
1025
  };
1026
 
1027
  // 窗口尺寸变化
1028
+ const resizeDebounce = debounce(() => {
1029
+ if (_isDestroyed) return;
1030
  _containerHeight = container.clientHeight;
1031
  render();
1032
  }, 200);
1033
+ const handleResize = () => {
1034
+ if (!_isDestroyed) resizeDebounce();
1035
+ };
1036
 
1037
  // 初始化
1038
  const init = () => {
 
1044
  // 销毁
1045
  const destroy = () => {
1046
  _isDestroyed = true;
1047
+ resizeDebounce.cancel();
1048
  container.removeEventListener("scroll", handleScroll);
1049
  window.removeEventListener("resize", handleResize);
1050
  if (rafId) cancelAnimationFrame(rafId);
前端页面/core/侧边栏主程序.js CHANGED
@@ -305,6 +305,15 @@ export function buildSidebarDOM() {
305
  const generation = listNavigationGeneration;
306
  return () => !activeInlineView && generation === listNavigationGeneration;
307
  };
 
 
 
 
 
 
 
 
 
308
  const releaseInlineView = () => {
309
  if (!activeInlineView) return;
310
  const view = activeInlineView;
@@ -334,6 +343,7 @@ export function buildSidebarDOM() {
334
  sortContainer.style.display = "flex";
335
  contentBoxWrapper.style.display = "flex";
336
  footerContainer.style.display = "block"; // 【核心】:返回列表时恢复底部
 
337
  };
338
 
339
  _onSidebarWindow("comfy-route-view", (e) => {
@@ -490,11 +500,17 @@ export function buildSidebarDOM() {
490
  let currentTab = tabs.some(tab => tab.id === savedTab) ? savedTab : tabs[0].id;
491
  let currentSort = Store.load("activeSort", "time");
492
  let currentRenderToken = 0;
 
 
493
  const getRenderToken = () => currentRenderToken;
494
  _sidebarListCleanup = () => {
 
495
  currentRenderToken++;
496
- try { disposeSidebarContent(contentArea); }
497
- finally { cleanupImageSandbox(contentArea); }
 
 
 
498
  };
499
  const syncTabsA11y = () => {
500
  const selectedTab = tabs.find(tab => tab.id === currentTab) || tabs[0];
@@ -509,7 +525,8 @@ export function buildSidebarDOM() {
509
  };
510
 
511
  const triggerLoad = (forceRefresh = false) => {
512
- currentRenderToken++;
 
513
  loadSidebarContent({
514
  tab: currentTab, sort: currentSort,
515
  keyword: sortContainer.querySelector("#hub-search-input").value.trim().toLowerCase(),
@@ -576,6 +593,8 @@ export function buildSidebarDOM() {
576
  triggerLoad();
577
  };
578
  btn.addEventListener("keydown", (event) => {
 
 
579
  const currentIndex = tabs.findIndex(item => item.id === tab.id);
580
  let nextIndex = currentIndex;
581
  if (event.key === "ArrowRight") nextIndex = (currentIndex + 1) % tabs.length;
@@ -584,6 +603,7 @@ export function buildSidebarDOM() {
584
  else if (event.key === "End") nextIndex = tabs.length - 1;
585
  else return;
586
  event.preventDefault();
 
587
  const nextButton = tabsContainer.querySelector(`[data-tab-id="${tabs[nextIndex].id}"]`);
588
  nextButton?.focus();
589
  nextButton?.click();
@@ -658,9 +678,12 @@ export function buildSidebarDOM() {
658
  sortContainer.querySelector("#hub-sort-select").onchange = (e) => { currentSort = e.target.value; Store.save("activeSort", currentSort); triggerLoad(); };
659
 
660
  // 🔍 搜索防抖:300ms 延迟,避免每次按键都发起网络请求
661
- sortContainer.querySelector("#hub-search-input").oninput = debounce(() => {
662
  triggerLoad();
663
  }, 300);
 
 
 
664
 
665
  // 🎯 任务榜筛选控件事件绑定
666
  sortContainer.querySelector("#task-status-filter").onchange = () => {
 
305
  const generation = listNavigationGeneration;
306
  return () => !activeInlineView && generation === listNavigationGeneration;
307
  };
308
+ container._captureListNavigation = () => {
309
+ const navigationCurrent = captureListNavigation();
310
+ const renderToken = getRenderToken();
311
+ const controls = Array.from(sortContainer.querySelectorAll('input, select'), control => [control, control.value]);
312
+ const focusedElement = document.activeElement;
313
+ return () => navigationCurrent() && renderToken === getRenderToken()
314
+ && document.activeElement === focusedElement
315
+ && controls.every(([control, value]) => control.value === value);
316
+ };
317
  const releaseInlineView = () => {
318
  if (!activeInlineView) return;
319
  const view = activeInlineView;
 
343
  sortContainer.style.display = "flex";
344
  contentBoxWrapper.style.display = "flex";
345
  footerContainer.style.display = "block"; // 【核心】:返回列表时恢复底部
346
+ container._onListReturn?.();
347
  };
348
 
349
  _onSidebarWindow("comfy-route-view", (e) => {
 
500
  let currentTab = tabs.some(tab => tab.id === savedTab) ? savedTab : tabs[0].id;
501
  let currentSort = Store.load("activeSort", "time");
502
  let currentRenderToken = 0;
503
+ let sidebarDisposed = false;
504
+ let searchDebounce = null;
505
  const getRenderToken = () => currentRenderToken;
506
  _sidebarListCleanup = () => {
507
+ sidebarDisposed = true;
508
  currentRenderToken++;
509
+ try { searchDebounce?.cancel(); }
510
+ finally {
511
+ try { disposeSidebarContent(contentArea); }
512
+ finally { cleanupImageSandbox(contentArea); }
513
+ }
514
  };
515
  const syncTabsA11y = () => {
516
  const selectedTab = tabs.find(tab => tab.id === currentTab) || tabs[0];
 
525
  };
526
 
527
  const triggerLoad = (forceRefresh = false) => {
528
+ if (sidebarDisposed) return;
529
+ currentRenderToken++;
530
  loadSidebarContent({
531
  tab: currentTab, sort: currentSort,
532
  keyword: sortContainer.querySelector("#hub-search-input").value.trim().toLowerCase(),
 
593
  triggerLoad();
594
  };
595
  btn.addEventListener("keydown", (event) => {
596
+ if (event.defaultPrevented || event.isComposing || event.keyCode === 229 || event.repeat
597
+ || event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
598
  const currentIndex = tabs.findIndex(item => item.id === tab.id);
599
  let nextIndex = currentIndex;
600
  if (event.key === "ArrowRight") nextIndex = (currentIndex + 1) % tabs.length;
 
603
  else if (event.key === "End") nextIndex = tabs.length - 1;
604
  else return;
605
  event.preventDefault();
606
+ event.stopPropagation();
607
  const nextButton = tabsContainer.querySelector(`[data-tab-id="${tabs[nextIndex].id}"]`);
608
  nextButton?.focus();
609
  nextButton?.click();
 
678
  sortContainer.querySelector("#hub-sort-select").onchange = (e) => { currentSort = e.target.value; Store.save("activeSort", currentSort); triggerLoad(); };
679
 
680
  // 🔍 搜索防抖:300ms 延迟,避免每次按键都发起网络请求
681
+ searchDebounce = debounce(() => {
682
  triggerLoad();
683
  }, 300);
684
+ sortContainer.querySelector("#hub-search-input").oninput = () => {
685
+ if (!sidebarDisposed) searchDebounce();
686
+ };
687
 
688
  // 🎯 任务榜筛选控件事件绑定
689
  sortContainer.querySelector("#task-status-filter").onchange = () => {
前端页面/core/侧边栏入口注册.js CHANGED
@@ -15,67 +15,137 @@ let pendingLanguageRefresh = false;
15
  /**
16
  * 动态加载侧边栏功能模块并构建 DOM
17
  */
18
- async function ensureSidebarDOM() {
19
  const { buildSidebarDOM } = await import('./侧边栏主程序.js');
20
- return buildSidebarDOM();
21
  }
22
 
23
  app.registerExtension({
24
  name: "Comfy.CommunityLeaderboardSidebar",
25
  async setup(app) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  // 语言切换事件 - 标记需要刷新
27
  document.addEventListener('comfy-language-change', () => {
28
  pendingLanguageRefresh = true;
 
 
 
 
 
 
 
 
 
29
  });
30
 
31
  // 返回主界面时,如果有语言切换则刷新
32
- window.addEventListener('comfy-route-back', async () => {
33
- if (pendingLanguageRefresh && globalSidebarContainer) {
34
- pendingLanguageRefresh = false;
35
- setTimeout(async () => {
36
- try {
37
- globalSidebarDOM = await ensureSidebarDOM();
38
- globalSidebarContainer.innerHTML = '';
39
- globalSidebarContainer.appendChild(globalSidebarDOM);
40
- } catch (e) {
41
- console.error('侧边栏刷新失败:', e);
42
- }
43
- }, 50);
44
- }
45
- });
46
 
47
  // 注册侧边栏 Tab
48
  if (app.extensionManager && app.extensionManager.registerSidebarTab) {
49
- app.extensionManager.registerSidebarTab({
50
  id: "comfyui-ranking-sidebar",
51
  order: 20, // 侧边栏排序规则:数字小的排前面(由 NodeCraft AI 的侧边栏排序器统一排序)
52
- title: "社区精选",
53
  icon: "pi pi-trophy",
54
  type: "custom",
55
  render: async (container) => {
56
-
 
57
  globalSidebarContainer = container;
 
 
 
 
 
 
 
 
 
58
  try {
59
  if (!globalSidebarDOM) {
60
- globalSidebarDOM = await ensureSidebarDOM();
 
 
 
 
 
 
 
61
  }
 
 
62
  container.innerHTML = '';
63
  container.appendChild(globalSidebarDOM);
64
- } catch (e) {
65
- console.error('侧边栏功能加载失败:', e);
 
66
  const failure = document.createElement("div");
67
  failure.setAttribute("role", "alert");
68
  failure.style.cssText = "text-align:center; padding:40px; color:#F44336;";
69
- try {
70
- const { t } = await import("../components/用户体验增强.js");
71
- failure.textContent = `⚠️ ${t('common.sidebar_load_failed')}`;
72
- } catch {
73
- failure.textContent = "⚠️";
74
- }
75
  container.replaceChildren(failure);
76
  }
77
  }
78
- });
 
79
  }
80
  }
81
  });
 
15
  /**
16
  * 动态加载侧边栏功能模块并构建 DOM
17
  */
18
+ async function ensureSidebarDOM(isCurrent = () => true) {
19
  const { buildSidebarDOM } = await import('./侧边栏主程序.js');
20
+ return isCurrent() ? buildSidebarDOM() : null;
21
  }
22
 
23
  app.registerExtension({
24
  name: "Comfy.CommunityLeaderboardSidebar",
25
  async setup(app) {
26
+ const { t, initI18n } = await import('../components/用户体验_国际化.js');
27
+ initI18n();
28
+ let registeredTab = null;
29
+ let initialSidebarLoad = null;
30
+ let renderRevision = 0;
31
+ let renderOwnerCurrent = () => false;
32
+ let languageRevision = 0;
33
+ let refreshTimer = null;
34
+ let refreshInProgress = false;
35
+ let refreshQueued = null;
36
+ const queueLanguageRefresh = request => {
37
+ if (refreshInProgress) {
38
+ refreshQueued = request;
39
+ return;
40
+ }
41
+ if (refreshTimer !== null) clearTimeout(refreshTimer);
42
+ const { target, previous, revision, navigationCurrent } = request;
43
+ refreshTimer = setTimeout(async () => {
44
+ refreshTimer = null;
45
+ refreshInProgress = true;
46
+ refreshQueued = null;
47
+ const isOwnerCurrent = () => pendingLanguageRefresh && revision === languageRevision
48
+ && target === globalSidebarContainer && previous === globalSidebarDOM
49
+ && target.isConnected && target.contains(previous);
50
+ const isCurrent = () => isOwnerCurrent() && navigationCurrent();
51
+ try {
52
+ if (!isCurrent()) return;
53
+ const next = await ensureSidebarDOM(isCurrent);
54
+ if (!next || !isOwnerCurrent()) return;
55
+ globalSidebarDOM = next;
56
+ bindLanguageRefresh(next);
57
+ target.replaceChildren(next);
58
+ pendingLanguageRefresh = false;
59
+ } catch {
60
+ if (isOwnerCurrent()) console.error('Sidebar language refresh failed');
61
+ } finally {
62
+ refreshInProgress = false;
63
+ const queued = refreshQueued;
64
+ refreshQueued = null;
65
+ if (queued && pendingLanguageRefresh) queueLanguageRefresh(queued);
66
+ }
67
+ }, 50);
68
+ };
69
+ const scheduleLanguageRefresh = () => {
70
+ if (!pendingLanguageRefresh || !globalSidebarContainer) return;
71
+ const previous = globalSidebarDOM;
72
+ queueLanguageRefresh({
73
+ target: globalSidebarContainer,
74
+ previous,
75
+ revision: languageRevision,
76
+ navigationCurrent: previous?._captureListNavigation?.() ?? (() => true)
77
+ });
78
+ };
79
+ const bindLanguageRefresh = root => {
80
+ root._onListReturn = () => {
81
+ if (root === globalSidebarDOM) scheduleLanguageRefresh();
82
+ };
83
+ };
84
  // 语言切换事件 - 标记需要刷新
85
  document.addEventListener('comfy-language-change', () => {
86
  pendingLanguageRefresh = true;
87
+ languageRevision++;
88
+ if (registeredTab) {
89
+ const title = t('nav.community');
90
+ const tabs = app.extensionManager?.getSidebarTabs?.();
91
+ const currentTab = Array.isArray(tabs)
92
+ ? tabs.find(tab => tab.id === registeredTab.id) : null;
93
+ if (currentTab) currentTab.title = title;
94
+ registeredTab.title = title;
95
+ }
96
  });
97
 
98
  // 返回主界面时,如果有语言切换则刷新
99
+ window.addEventListener('comfy-route-back', scheduleLanguageRefresh);
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
  // 注册侧边栏 Tab
102
  if (app.extensionManager && app.extensionManager.registerSidebarTab) {
103
+ registeredTab = {
104
  id: "comfyui-ranking-sidebar",
105
  order: 20, // 侧边栏排序规则:数字小的排前面(由 NodeCraft AI 的侧边栏排序器统一排序)
106
+ title: t('nav.community'),
107
  icon: "pi pi-trophy",
108
  type: "custom",
109
  render: async (container) => {
110
+ const revision = ++renderRevision;
111
+ const connectedAtStart = container.isConnected;
112
  globalSidebarContainer = container;
113
+ const isCurrent = () => {
114
+ const sidebar = app.extensionManager?.sidebarTab;
115
+ const activeId = (sidebar?.value ?? sidebar)?.activeSidebarTabId;
116
+ return revision === renderRevision && container === globalSidebarContainer
117
+ && (!connectedAtStart || container.isConnected)
118
+ && (!activeId || activeId === registeredTab.id);
119
+ };
120
+ renderOwnerCurrent = isCurrent;
121
+ if (!isCurrent()) return;
122
  try {
123
  if (!globalSidebarDOM) {
124
+ if (!initialSidebarLoad) {
125
+ initialSidebarLoad = ensureSidebarDOM(() => renderOwnerCurrent())
126
+ .then(root => {
127
+ if (root) globalSidebarDOM = root;
128
+ })
129
+ .finally(() => { initialSidebarLoad = null; });
130
+ }
131
+ await initialSidebarLoad;
132
  }
133
+ if (!isCurrent() || !globalSidebarDOM) return;
134
+ bindLanguageRefresh(globalSidebarDOM);
135
  container.innerHTML = '';
136
  container.appendChild(globalSidebarDOM);
137
+ } catch {
138
+ if (!isCurrent()) return;
139
+ console.error('侧边栏功能加载失败');
140
  const failure = document.createElement("div");
141
  failure.setAttribute("role", "alert");
142
  failure.style.cssText = "text-align:center; padding:40px; color:#F44336;";
143
+ failure.textContent = `⚠️ ${t('common.sidebar_load_failed')}`;
 
 
 
 
 
144
  container.replaceChildren(failure);
145
  }
146
  }
147
+ };
148
+ app.extensionManager.registerSidebarTab(registeredTab);
149
  }
150
  }
151
  });
前端页面/core/侧边栏数据引擎.js CHANGED
@@ -197,6 +197,7 @@ export async function loadSidebarContent({
197
 
198
  // 首次渲染清空容器
199
  if (!append) {
 
200
  contentArea.innerHTML = "";
201
  state.displayedCount = 0;
202
  }
@@ -382,7 +383,6 @@ export async function loadSidebarContent({
382
  }
383
 
384
  // 完整重新渲染(替代之前只更新数字的方案)
385
- contentArea.innerHTML = "";
386
  // 🔧 先过滤再切片:搜索场景下静默刷新同样会触发,
387
  // 原实现在 newData 前 20 条里过滤,会漏掉靠后的匹配项
388
  const scopedNewData = _applySearchScope(newData);
@@ -478,6 +478,7 @@ export async function loadSidebarContent({
478
 
479
  // ========== 显示加载骨架屏(已有本地数据上屏时跳过,避免闪屏)==========
480
  if (!renderedInstant) {
 
481
  contentArea.innerHTML = "";
482
  const skeleton = createSkeleton(tab === "creators" ? "list" : "card", 3);
483
  contentArea.appendChild(skeleton);
 
197
 
198
  // 首次渲染清空容器
199
  if (!append) {
200
+ disposeSidebarContent(contentArea);
201
  contentArea.innerHTML = "";
202
  state.displayedCount = 0;
203
  }
 
383
  }
384
 
385
  // 完整重新渲染(替代之前只更新数字的方案)
 
386
  // 🔧 先过滤再切片:搜索场景下静默刷新同样会触发,
387
  // 原实现在 newData 前 20 条里过滤,会漏掉靠后的匹配项
388
  const scopedNewData = _applySearchScope(newData);
 
478
 
479
  // ========== 显示加载骨架屏(已有本地数据上屏时跳过,避免闪屏)==========
480
  if (!renderedInstant) {
481
+ disposeSidebarContent(contentArea);
482
  contentArea.innerHTML = "";
483
  const skeleton = createSkeleton(tab === "creators" ? "list" : "card", 3);
484
  contentArea.appendChild(skeleton);
前端页面/social/私信聊天组件.js CHANGED
@@ -470,10 +470,12 @@ export async function loadChatUnreadCount(currentUser, chatBtn) {
470
  if (!currentUser?.account || !chatBtn?.querySelector("#chat-unread-badge")) return 0;
471
  try {
472
  const res = await api.getChatList(currentUser.account);
473
- const list = Array.isArray(res?.data) ? res.data : [];
 
 
474
  const total = list.reduce((sum, chat) => Math.min(Number.MAX_SAFE_INTEGER,
475
  sum + validUnreadCount(chat?.unread_count)), 0);
476
  if (!update.isCurrent()) return 0;
477
  update.render(total); return total;
478
- } catch (_) { update.render(0); return 0; }
479
  }
 
470
  if (!currentUser?.account || !chatBtn?.querySelector("#chat-unread-badge")) return 0;
471
  try {
472
  const res = await api.getChatList(currentUser.account);
473
+ if (res?.status !== "success" || !Array.isArray(res.data)
474
+ || res.data.some(chat => !chat || !Number.isSafeInteger(chat.unread_count) || chat.unread_count < 0)) return 0;
475
+ const list = res.data;
476
  const total = list.reduce((sum, chat) => Math.min(Number.MAX_SAFE_INTEGER,
477
  sum + validUnreadCount(chat?.unread_count)), 0);
478
  if (!update.isCurrent()) return 0;
479
  update.render(total); return total;
480
+ } catch (_) { return 0; }
481
  }
前端页面/social/通知中心组件.js CHANGED
@@ -1,5 +1,5 @@
1
  // 前端页面/social/通知中心组件.js
2
- import { createUnreadBadgeUpdate, validUnreadCount } from "./未读角标工具.js";
3
  import { api, captureRequestSession, requestCancelManager } from "../core/网络请求API.js";
4
  import { openOtherUserProfileModal } from "../profile/个人中心视图.js";
5
  import { showToast, showConfirm } from "../components/UI交互提示组件.js";
@@ -274,14 +274,7 @@ export async function openNotificationCenter(currentUser, bellBtn) {
274
  && (msg.created_at >= 1e12 ? msg.created_at : msg.created_at * 1000) > clearTime)
275
  .map(msg => ({ ...msg })).sort((a, b) => b.created_at - a.created_at);
276
  renderMsgList(messages);
277
- if (validUnreadCount(res.unread_count) > 0 && scope.isCurrent()) {
278
- const result = await api.markMessagesRead(currentUser.account);
279
- if (scope.isCurrent() && result?.status === 'success') {
280
- messages.forEach(msg => { msg.is_read = true; }); renderMsgList(messages);
281
- // 独立刷新以避免标记已读的旧响应覆盖另一轮角标读取。
282
- void loadUnreadCount(currentUser, bellBtn);
283
- }
284
- }
285
  } catch (_) { if (scope.isCurrent() && messages.length === 0) listArea.innerHTML = `<div class="notification-status" role="alert">${t('common.error')}</div>`; }
286
 
287
  }
@@ -291,6 +284,8 @@ export async function loadUnreadCount(currentUser, bellBtn) {
291
  if (!currentUser?.account || !bellBtn?.querySelector("#unread-badge")) return;
292
  try {
293
  const res = await api.getUnreadCount(currentUser.account);
294
- update.render(validUnreadCount(res?.unread_count));
295
- } catch (_) { update.render(0); }
 
 
296
  }
 
1
  // 前端页面/social/通知中心组件.js
2
+ import { createUnreadBadgeUpdate } from "./未读角标工具.js";
3
  import { api, captureRequestSession, requestCancelManager } from "../core/网络请求API.js";
4
  import { openOtherUserProfileModal } from "../profile/个人中心视图.js";
5
  import { showToast, showConfirm } from "../components/UI交互提示组件.js";
 
274
  && (msg.created_at >= 1e12 ? msg.created_at : msg.created_at * 1000) > clearTime)
275
  .map(msg => ({ ...msg })).sort((a, b) => b.created_at - a.created_at);
276
  renderMsgList(messages);
277
+ if (scope.isCurrent()) void loadUnreadCount(currentUser, bellBtn);
 
 
 
 
 
 
 
278
  } catch (_) { if (scope.isCurrent() && messages.length === 0) listArea.innerHTML = `<div class="notification-status" role="alert">${t('common.error')}</div>`; }
279
 
280
  }
 
284
  if (!currentUser?.account || !bellBtn?.querySelector("#unread-badge")) return;
285
  try {
286
  const res = await api.getUnreadCount(currentUser.account);
287
+ if (res?.status === "success" && Number.isSafeInteger(res.unread_count) && res.unread_count >= 0) {
288
+ update.render(res.unread_count);
289
+ }
290
+ } catch (_) {}
291
  }
前端页面/styles/主题基础.css CHANGED
@@ -99,6 +99,7 @@
99
 
100
  .comfy-ranking-root .hub-tablist {
101
  display: flex;
 
102
  flex: 0 0 auto;
103
  min-width: 0;
104
  padding: 10px 10px 0;
@@ -447,9 +448,15 @@
447
  }
448
 
449
  .comfy-ranking-root .hub-tab-btn {
450
- flex: 1 1 0;
451
- padding: 8px;
 
 
 
 
 
452
  border: 0;
 
453
  border-radius: var(--rk-radius-sm) var(--rk-radius-sm) 0 0;
454
  background: rgb(0 0 0 / 15%);
455
  color: var(--tab-inactive, var(--rk-muted));
@@ -457,7 +464,7 @@
457
  font-weight: 700;
458
  text-shadow: 0 1px 2px rgb(0 0 0 / 80%), 0 0 4px rgb(0 0 0 / 60%), 1px 1px 3px rgb(0 0 0 / 50%);
459
  cursor: pointer;
460
- transition: all .2s ease;
461
  }
462
 
463
  .comfy-ranking-root .hub-tab-btn::before {
 
99
 
100
  .comfy-ranking-root .hub-tablist {
101
  display: flex;
102
+ flex-wrap: wrap;
103
  flex: 0 0 auto;
104
  min-width: 0;
105
  padding: 10px 10px 0;
 
448
  }
449
 
450
  .comfy-ranking-root .hub-tab-btn {
451
+ flex: 1 1 auto;
452
+ min-width: min(64px, 100%);
453
+ max-width: 100%;
454
+ box-sizing: border-box;
455
+ padding: 8px 4px;
456
+ white-space: normal;
457
+ overflow-wrap: anywhere;
458
  border: 0;
459
+ border-bottom: 2px solid transparent;
460
  border-radius: var(--rk-radius-sm) var(--rk-radius-sm) 0 0;
461
  background: rgb(0 0 0 / 15%);
462
  color: var(--tab-inactive, var(--rk-muted));
 
464
  font-weight: 700;
465
  text-shadow: 0 1px 2px rgb(0 0 0 / 80%), 0 0 4px rgb(0 0 0 / 60%), 1px 1px 3px rgb(0 0 0 / 50%);
466
  cursor: pointer;
467
+ transition: background-color .2s ease, color .2s ease, text-shadow .2s ease;
468
  }
469
 
470
  .comfy-ranking-root .hub-tab-btn::before {