Spaces:
Running
Running
Upload 103 files
Browse files- 前端页面/components/UI交互提示组件.js +6 -4
- 前端页面/components/全局设置组件.js +52 -23
- 前端页面/components/性能优化工具.js +22 -4
- 前端页面/core/侧边栏主程序.js +27 -4
- 前端页面/core/侧边栏入口注册.js +99 -29
- 前端页面/core/侧边栏数据引擎.js +2 -1
- 前端页面/social/私信聊天组件.js +4 -2
- 前端页面/social/通知中心组件.js +6 -11
- 前端页面/styles/主题基础.css +10 -3
前端页面/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 || !
|
| 206 |
event.preventDefault();
|
| 207 |
last.focus();
|
| 208 |
-
} else if (!event.shiftKey && (document.activeElement === last || !
|
| 209 |
event.preventDefault();
|
| 210 |
first.focus();
|
| 211 |
}
|
| 212 |
};
|
| 213 |
dialogStack.push(box);
|
| 214 |
document.addEventListener("keydown", handleKeydown);
|
| 215 |
-
const
|
|
|
|
|
|
|
| 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 |
-
<
|
| 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 |
-
<
|
| 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 |
-
<
|
| 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 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 578 |
-
|
| 579 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 581 |
}
|
| 582 |
|
| 583 |
/**
|
|
@@ -1012,10 +1025,14 @@ export function createVirtualScroller(options) {
|
|
| 1012 |
};
|
| 1013 |
|
| 1014 |
// 窗口尺寸变化
|
| 1015 |
-
const
|
|
|
|
| 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 {
|
| 497 |
-
finally {
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 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',
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
}
|
|
|
|
|
|
|
| 62 |
container.innerHTML = '';
|
| 63 |
container.appendChild(globalSidebarDOM);
|
| 64 |
-
} catch
|
| 65 |
-
|
|
|
|
| 66 |
const failure = document.createElement("div");
|
| 67 |
failure.setAttribute("role", "alert");
|
| 68 |
failure.style.cssText = "text-align:center; padding:40px; color:#F44336;";
|
| 69 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 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 (_) {
|
| 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
|
| 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 (
|
| 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 |
-
|
| 295 |
-
|
|
|
|
|
|
|
| 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
|
| 451 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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 {
|