Spaces:
Running
Running
Upload 104 files
Browse files- 前端页面/components/性能优化工具.js +74 -9
- 前端页面/core/榜单分页控制.js +1 -0
- 前端页面/post/讨论区组件.js +1 -0
- 前端页面/prompt/提示词组件.js +1 -0
- 前端页面/task/任务榜组件.js +1 -0
前端页面/components/性能优化工具.js
CHANGED
|
@@ -240,6 +240,8 @@ export function createPaginationLoader(options) {
|
|
| 240 |
let debounceTimer = null;
|
| 241 |
let generation = 0; // 🔄 轻量级取消令牌,reset 时递增以丢弃飞行中请求
|
| 242 |
let stopped = false; // 保持创建后可手动加载;stop 后仅 start 能重新启用。
|
|
|
|
|
|
|
| 243 |
|
| 244 |
// 加载指示器
|
| 245 |
const loadingIndicator = document.createElement("div");
|
|
@@ -258,6 +260,7 @@ export function createPaginationLoader(options) {
|
|
| 258 |
// "没有更多"提示
|
| 259 |
const endIndicator = document.createElement("div");
|
| 260 |
endIndicator.className = "pagination-end";
|
|
|
|
| 261 |
endIndicator.setAttribute("role", "status");
|
| 262 |
endIndicator.setAttribute("aria-live", "polite");
|
| 263 |
endIndicator.textContent = translate("pagination.end");
|
|
@@ -276,6 +279,63 @@ export function createPaginationLoader(options) {
|
|
| 276 |
retryIndicator.append(retryMessage, retryButton);
|
| 277 |
retryIndicator.style.display = "none";
|
| 278 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
// 滚动事件处理(带防抖)
|
| 280 |
const handleScroll = () => {
|
| 281 |
if (stopped) return;
|
|
@@ -294,9 +354,14 @@ export function createPaginationLoader(options) {
|
|
| 294 |
};
|
| 295 |
|
| 296 |
// 加载下一页
|
| 297 |
-
const _loadNextPage = async () => {
|
| 298 |
-
if (stopped || isLoading || !hasMore) return;
|
| 299 |
if (typeof canLoad === "function" && !canLoad()) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
|
| 301 |
isLoading = true;
|
| 302 |
loadingIndicator.style.display = "block";
|
|
@@ -339,19 +404,17 @@ export function createPaginationLoader(options) {
|
|
| 339 |
loadingIndicator.style.display = "none";
|
| 340 |
// Search callers can scan pages with no local matches without
|
| 341 |
// changing pagination behavior for other lists.
|
| 342 |
-
|
| 343 |
-
(autoFill() || container.scrollHeight <= container.clientHeight + threshold)
|
| 344 |
-
|
| 345 |
-
|
| 346 |
}
|
| 347 |
}
|
| 348 |
};
|
| 349 |
|
| 350 |
retryButton.addEventListener("click", () => {
|
| 351 |
if (stopped || isLoading) return;
|
| 352 |
-
|
| 353 |
-
hasMore = true;
|
| 354 |
-
_loadNextPage();
|
| 355 |
});
|
| 356 |
|
| 357 |
// 启动监听
|
|
@@ -367,6 +430,7 @@ export function createPaginationLoader(options) {
|
|
| 367 |
|
| 368 |
// 停止监听
|
| 369 |
const stop = () => {
|
|
|
|
| 370 |
stopped = true;
|
| 371 |
generation++;
|
| 372 |
if (isLoading) currentPage = Math.max(1, currentPage - 1);
|
|
@@ -382,6 +446,7 @@ export function createPaginationLoader(options) {
|
|
| 382 |
|
| 383 |
// 重置状态
|
| 384 |
const reset = () => {
|
|
|
|
| 385 |
generation++; // 🔄 递增使飞行中请求结果被忽略
|
| 386 |
currentPage = 1; // 首屏已渲染第1页,分页器从第2页开始
|
| 387 |
hasMore = true;
|
|
|
|
| 240 |
let debounceTimer = null;
|
| 241 |
let generation = 0; // 🔄 轻量级取消令牌,reset 时递增以丢弃飞行中请求
|
| 242 |
let stopped = false; // 保持创建后可手动加载;stop 后仅 start 能重新启用。
|
| 243 |
+
const focusContainer = options.focusContainer || container;
|
| 244 |
+
let retryFocus = null;
|
| 245 |
|
| 246 |
// 加载指示器
|
| 247 |
const loadingIndicator = document.createElement("div");
|
|
|
|
| 260 |
// "没有更多"提示
|
| 261 |
const endIndicator = document.createElement("div");
|
| 262 |
endIndicator.className = "pagination-end";
|
| 263 |
+
endIndicator.tabIndex = -1;
|
| 264 |
endIndicator.setAttribute("role", "status");
|
| 265 |
endIndicator.setAttribute("aria-live", "polite");
|
| 266 |
endIndicator.textContent = translate("pagination.end");
|
|
|
|
| 279 |
retryIndicator.append(retryMessage, retryButton);
|
| 280 |
retryIndicator.style.display = "none";
|
| 281 |
|
| 282 |
+
const clearRetryFocus = () => {
|
| 283 |
+
if (!retryFocus) return;
|
| 284 |
+
retryFocus.doc.removeEventListener('focusin', retryFocus.onFocus, true);
|
| 285 |
+
retryFocus.doc.removeEventListener('pointerdown', retryFocus.onPointer, true);
|
| 286 |
+
retryFocus = null;
|
| 287 |
+
};
|
| 288 |
+
const captureRetryFocus = () => {
|
| 289 |
+
clearRetryFocus();
|
| 290 |
+
const doc = container.ownerDocument;
|
| 291 |
+
if (doc.activeElement !== retryButton) return;
|
| 292 |
+
const record = { generation, children: new Set(focusContainer.children), doc };
|
| 293 |
+
record.onFocus = event => { if (event.target !== retryButton && event.target !== doc.body) clearRetryFocus(); };
|
| 294 |
+
record.onPointer = () => clearRetryFocus();
|
| 295 |
+
retryFocus = record;
|
| 296 |
+
doc.addEventListener('focusin', record.onFocus, true);
|
| 297 |
+
doc.addEventListener('pointerdown', record.onPointer, true);
|
| 298 |
+
};
|
| 299 |
+
const focusableSelector = 'button, a[href], input, select, textarea, [tabindex]';
|
| 300 |
+
const firstFocusable = element => {
|
| 301 |
+
const candidates = [element, ...element.querySelectorAll(focusableSelector)];
|
| 302 |
+
return candidates.find(node => node.matches(focusableSelector) && !node.disabled && node.tabIndex >= 0
|
| 303 |
+
&& !node.closest('[inert], [aria-hidden="true"]') && node.getClientRects().length > 0
|
| 304 |
+
&& node.ownerDocument.defaultView.getComputedStyle(node).visibility !== 'hidden');
|
| 305 |
+
};
|
| 306 |
+
const restoreRetryFocus = willContinue => {
|
| 307 |
+
if (!retryFocus) return;
|
| 308 |
+
const doc = container.ownerDocument;
|
| 309 |
+
if (stopped || retryFocus.generation !== generation || !container.isConnected || !focusContainer.isConnected
|
| 310 |
+
|| !doc.hasFocus() || !focusContainer.getClientRects().length
|
| 311 |
+
|| doc.defaultView.getComputedStyle(focusContainer).visibility === 'hidden'
|
| 312 |
+
|| (doc.activeElement !== doc.body && doc.activeElement !== retryButton)) {
|
| 313 |
+
clearRetryFocus();
|
| 314 |
+
return;
|
| 315 |
+
}
|
| 316 |
+
let target;
|
| 317 |
+
if (retryIndicator.style.display !== 'none') target = retryButton;
|
| 318 |
+
else {
|
| 319 |
+
const children = Array.from(focusContainer.children);
|
| 320 |
+
for (const child of children) {
|
| 321 |
+
if (!retryFocus.children.has(child) && (target = firstFocusable(child))) break;
|
| 322 |
+
}
|
| 323 |
+
if (!target && willContinue) return;
|
| 324 |
+
if (!target && !hasMore) target = endIndicator;
|
| 325 |
+
if (!target) {
|
| 326 |
+
for (const child of children.reverse()) {
|
| 327 |
+
if ((target = firstFocusable(child))) break;
|
| 328 |
+
}
|
| 329 |
+
}
|
| 330 |
+
if (!target) {
|
| 331 |
+
if (!focusContainer.hasAttribute('tabindex')) focusContainer.tabIndex = -1;
|
| 332 |
+
target = focusContainer;
|
| 333 |
+
}
|
| 334 |
+
}
|
| 335 |
+
clearRetryFocus();
|
| 336 |
+
target.focus({ preventScroll: true });
|
| 337 |
+
};
|
| 338 |
+
|
| 339 |
// 滚动事件处理(带防抖)
|
| 340 |
const handleScroll = () => {
|
| 341 |
if (stopped) return;
|
|
|
|
| 354 |
};
|
| 355 |
|
| 356 |
// 加载下一页
|
| 357 |
+
const _loadNextPage = async (retry = false) => {
|
| 358 |
+
if (stopped || isLoading || (!hasMore && !retry)) return;
|
| 359 |
if (typeof canLoad === "function" && !canLoad()) return;
|
| 360 |
+
if (retry) {
|
| 361 |
+
captureRetryFocus();
|
| 362 |
+
retryIndicator.style.display = "none";
|
| 363 |
+
hasMore = true;
|
| 364 |
+
}
|
| 365 |
|
| 366 |
isLoading = true;
|
| 367 |
loadingIndicator.style.display = "block";
|
|
|
|
| 404 |
loadingIndicator.style.display = "none";
|
| 405 |
// Search callers can scan pages with no local matches without
|
| 406 |
// changing pagination behavior for other lists.
|
| 407 |
+
const willContinue = hasMore && typeof autoFill === "function" &&
|
| 408 |
+
(autoFill() || container.scrollHeight <= container.clientHeight + threshold);
|
| 409 |
+
restoreRetryFocus(willContinue);
|
| 410 |
+
if (willContinue) _loadNextPage();
|
| 411 |
}
|
| 412 |
}
|
| 413 |
};
|
| 414 |
|
| 415 |
retryButton.addEventListener("click", () => {
|
| 416 |
if (stopped || isLoading) return;
|
| 417 |
+
_loadNextPage(true);
|
|
|
|
|
|
|
| 418 |
});
|
| 419 |
|
| 420 |
// 启动监听
|
|
|
|
| 430 |
|
| 431 |
// 停止监听
|
| 432 |
const stop = () => {
|
| 433 |
+
clearRetryFocus();
|
| 434 |
stopped = true;
|
| 435 |
generation++;
|
| 436 |
if (isLoading) currentPage = Math.max(1, currentPage - 1);
|
|
|
|
| 446 |
|
| 447 |
// 重置状态
|
| 448 |
const reset = () => {
|
| 449 |
+
clearRetryFocus();
|
| 450 |
generation++; // 🔄 递增使飞行中请求结果被忽略
|
| 451 |
currentPage = 1; // 首屏已渲染第1页,分页器从第2页开始
|
| 452 |
hasMore = true;
|
前端页面/core/榜单分页控制.js
CHANGED
|
@@ -75,6 +75,7 @@ export function setupSidebarPagination(contentArea, state, pageSize, loadMoreDat
|
|
| 75 |
// 创建分页加载器
|
| 76 |
const loader = createPaginationLoader({
|
| 77 |
container: scrollContainer,
|
|
|
|
| 78 |
pageSize: pageSize,
|
| 79 |
threshold: 300,
|
| 80 |
t,
|
|
|
|
| 75 |
// 创建分页加载器
|
| 76 |
const loader = createPaginationLoader({
|
| 77 |
container: scrollContainer,
|
| 78 |
+
focusContainer: contentArea,
|
| 79 |
pageSize: pageSize,
|
| 80 |
threshold: 300,
|
| 81 |
t,
|
前端页面/post/讨论区组件.js
CHANGED
|
@@ -194,6 +194,7 @@ export function createPostsView(currentUser, keyword = "", { sort: initialSort =
|
|
| 194 |
|
| 195 |
currentPaginator = createPaginationLoader({
|
| 196 |
container: scrollContainer,
|
|
|
|
| 197 |
canLoad: () => !disposed && firstPageReady && !isLoadingFromNetwork,
|
| 198 |
loadMore: loadMorePosts,
|
| 199 |
pageSize: PAGE_SIZE,
|
|
|
|
| 194 |
|
| 195 |
currentPaginator = createPaginationLoader({
|
| 196 |
container: scrollContainer,
|
| 197 |
+
focusContainer: postsGrid,
|
| 198 |
canLoad: () => !disposed && firstPageReady && !isLoadingFromNetwork,
|
| 199 |
loadMore: loadMorePosts,
|
| 200 |
pageSize: PAGE_SIZE,
|
前端页面/prompt/提示词组件.js
CHANGED
|
@@ -339,6 +339,7 @@ export function createPromptsView(currentUser, keyword = "", { sort: initialSort
|
|
| 339 |
|
| 340 |
currentPaginator = createPaginationLoader({
|
| 341 |
container: scrollContainer,
|
|
|
|
| 342 |
canLoad: () => !disposed && firstPageReady && !isLoadingFromNetwork,
|
| 343 |
loadMore: loadMorePrompts,
|
| 344 |
pageSize: PAGE_SIZE,
|
|
|
|
| 339 |
|
| 340 |
currentPaginator = createPaginationLoader({
|
| 341 |
container: scrollContainer,
|
| 342 |
+
focusContainer: promptsGrid,
|
| 343 |
canLoad: () => !disposed && firstPageReady && !isLoadingFromNetwork,
|
| 344 |
loadMore: loadMorePrompts,
|
| 345 |
pageSize: PAGE_SIZE,
|
前端页面/task/任务榜组件.js
CHANGED
|
@@ -498,6 +498,7 @@ export function createTasksView(currentUser, keyword = "", { sort: initialSort =
|
|
| 498 |
const scrollContainer = container.closest('.sidebar-scroll-container') || container;
|
| 499 |
paginator = createPaginationLoader({
|
| 500 |
container: scrollContainer,
|
|
|
|
| 501 |
canLoad: () => !disposed && firstPageReady && !isLoadingFromNetwork,
|
| 502 |
autoFill: () => Boolean(searchKeyword) && !allTasksData.some(task => _filterTasksByKeyword([task], searchKeyword).length),
|
| 503 |
loadMore: async (page, pageSize, { isCurrent = () => true } = {}) => {
|
|
|
|
| 498 |
const scrollContainer = container.closest('.sidebar-scroll-container') || container;
|
| 499 |
paginator = createPaginationLoader({
|
| 500 |
container: scrollContainer,
|
| 501 |
+
focusContainer: tasksList,
|
| 502 |
canLoad: () => !disposed && firstPageReady && !isLoadingFromNetwork,
|
| 503 |
autoFill: () => Boolean(searchKeyword) && !allTasksData.some(task => _filterTasksByKeyword([task], searchKeyword).length),
|
| 504 |
loadMore: async (page, pageSize, { isCurrent = () => true } = {}) => {
|